マイクから音声を録音する

Recipe ID: hw-012

Webカメラ同様、マイク入力も HTML5 API (MediaRecorder) で可能ですが、より低レベルな音声処理やファイル保存を Rust 側で行いたい場合は cpal クレートを使用します。

1. 依存関係の追加

src-tauri ディレクトリに移動して、以下のコマンドを実行します。

cargo add cpal hound
# hound は WAV ファイル保存用

2. 録音コマンドの実装

この例では、指定秒数だけ録音して WAV ファイルに保存します。
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use std::sync::{Arc, Mutex};

#[tauri::command]
fn record_audio(duration_sec: u64, file_path: String) -> Result<String, String> {
    let host = cpal::default_host();
    let device = host.default_input_device().ok_or("No input device available")?;
    let config = device.default_input_config().map_err(|e| e.to_string())?;

    // 音声データを一時保存するバッファ
    let writer = Arc::new(Mutex::new(hound::WavWriter::create(
        &file_path,
        hound::WavSpec {
            channels: config.channels(),
            sample_rate: config.sample_rate(),
            bits_per_sample: 16,
            sample_format: hound::SampleFormat::Int,
        },
    ).map_err(|e| e.to_string())?));

    let writer_clone = writer.clone();
    let err_fn = |err| eprintln!("an error occurred on stream: {}", err);

    let stream = match config.sample_format() {
        cpal::SampleFormat::F32 => device.build_input_stream(
            &config.into(),
            move |data: &[f32], _: &_| {
                let mut guard = writer_clone.lock().unwrap();
                for &sample in data {
                    // f32 -> i16 変換
                    let s = (sample * i16::MAX as f32) as i16;
                    guard.write_sample(s).ok();
                }
            },
            err_fn,
            None // Timeout
        ),
        _ => return Err("Unsupported sample format".to_string()),
    }.map_err(|e| e.to_string())?;

    stream.play().map_err(|e| e.to_string())?;
    
    // 指定時間待機
    std::thread::sleep(std::time::Duration::from_secs(duration_sec));
    
    // ストリーム破棄(ドロップ)で停止
    // これにより writer_clone もドロップされ、writer の参照カウントが 1 に戻るはずです
    drop(stream);
    
    // Arc と Mutex のラップを解除して中の WavWriter を取り出す
    let mutex = Arc::try_unwrap(writer).map_err(|_| "Failed to unwrap Arc writer".to_string())?;
    let mut inner_writer = mutex.into_inner().map_err(|_| "Mutex is poisoned".to_string())?;

    // ファイルをフラッシュして閉じる
    inner_writer.finalize().map_err(|e| e.to_string())?;

    Ok(format!("Recorded to {}", file_path))
}