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
- Connect or re-enable a microphone and retry
- On macOS: System Settings → Privacy & Security → Microphone → allow the app
- Close apps that may hold the device exclusively, or switch them to shared mode
- Select a specific input device in Handy settings rather than relying on the default
- 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
- Enumerate input devices and let users pick an explicit device instead of relying on the default
- Handle device-disconnect events: rebuild the recorder on device-change rather than crashing the session
- On first run, guide users through macOS microphone permission before recording starts
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
- {error_message}
- No input device found
- Failed to open recorder
- Failed to check Windows microphone permissions:
- Failed to create resampler
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)