cjpais/Handy · critical · anyhow::Error

Failed to create AudioRecorder

Error message

Failed to create AudioRecorder: {}

What it means

AudioRecorder::new() failed at recorder construction — acquiring the default audio input device and host via cpal. This throws when the machine exposes no default input device, the device is exclusively held by another process, or the OS denies microphone access (macOS TCC). It is the first audio-touching call in create_audio_recorder, so it fails before any VAD/streaming setup.

Solutions

  1. Connect or re-enable a microphone and retry
  2. On macOS: System Settings → Privacy & Security → Microphone → allow the app
  3. Close apps that may hold the device exclusively, or switch them to shared mode
  4. Select a specific input device in Handy settings rather than relying on the default
  5. On macOS restart coreaudiod; on Linux check PipeWire/Pulse is running and the source is not suspended

Example fix

// before
let recorder = AudioRecorder::new()
    .map_err(|e| anyhow::anyhow!("Failed to create AudioRecorder: {}", e))?

// after — preflight the device so users get a precise message instead of a generic init error
let host = cpal::default_host();
let device = host.default_input_device().ok_or_else(|| {
    anyhow::anyhow!("No default microphone found — connect an input device or grant microphone permission")
})?;
let recorder = AudioRecorder::new()
    .map_err(|e| anyhow::anyhow!("Failed to create AudioRecorder: {}", e))?
Defensive patterns

Strategy: fallback

Validate before calling

// preflight the default input device before constructing the recorder
let host = cpal::default_host();
if host.default_input_device().is_none() {
    anyhow::bail!(
        "No microphone available — connect an input device or grant microphone permission"
    );
}

Try / catch

match create_audio_recorder(&vad_path, ...) {
    Err(e) if e.to_string().contains("Failed to create AudioRecorder") => {
        // device problem, not code: enumerate devices, tell the user to connect/grant,
        // and fall back to a device-picker instead of auto-retrying the default
    }
    other => other?,
}

Prevention

When it happens

Trigger: No microphone connected or enabled; mic exclusively captured by another app; macOS microphone permission not granted to the app; headless/CI environment with no audio devices; default device switched/disconnected between enumeration and recorder creation.

Common situations: Fresh macOS install prompting for mic permission and user denying; Bluetooth headset disconnecting mid-session; remote desktop / VM sessions without audio passthrough; another app (e.g. DAW or meeting software with exclusive mode) holding the device.

Related errors


AI-assisted analysis of cjpais/Handy@c6fa60da2f (2026-08-28). Data as JSON: /api/errors/332dc37fcbbede3d. Report an issue: GitHub.

Appendix: source

Thrown at src-tauri/src/managers/audio.rs:331

        frames_for_duration_ms(VAD_STREAMING_HANGOVER_MS, frame_samples);
    let onset_frames = frames_for_duration_ms(VAD_ONSET_MS, frame_samples);
    let smoothed_vad = SmoothedVad::new(
        detector,
        prefill_frames,
        offline_hangover_frames,
        onset_frames,
    );

    info!(
        "Initialized {:?} VAD backend ({} samples/frame)",
        backend, frame_samples
    );

    // Recorder with VAD, a spectrum-level callback that forwards level updates to
    // the frontend, and an audio-frame callback that feeds live streaming via a
    // shared `StreamRouter` (captured directly, not via Tauri state — see its docs).
    let recorder = AudioRecorder::new()
        .map_err(|e| anyhow::anyhow!("Failed to create AudioRecorder: {}", e))?
        .with_vad(
            Box::new(smoothed_vad),
            offline_hangover_frames,
            streaming_hangover_frames,
        )
        .with_selected_channel(selected_channel)
        .with_level_callback({
            let app_handle = app_handle.clone();
            move |levels| {
                utils::emit_levels(&app_handle, &levels);
            }
        })
        .with_audio_callback({
            let router = stream_router;
            move |frame| {
                router.feed(frame);
            }
        });

View on GitHub (pinned to c6fa60da2f)