{"record":{"id":"7e33c332915c0221","repo":"cjpais/Handy","slug":"failed-to-open-recorder","errorCode":null,"errorMessage":"Failed to open recorder: {}","messagePattern":"Failed to open recorder: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/managers/audio.rs","lineNumber":720,"sourceCode":"        let resolve_elapsed = resolve_started.elapsed();\n\n        // Ensure VAD is loaded if it wasn't for whatever reason\n        let vad_started = Instant::now();\n        self.preload_vad()?;\n        let vad_elapsed = vad_started.elapsed();\n\n        let open_started = Instant::now();\n        let mut recorder_opt = self.recorder.lock().unwrap();\n        if let Some(rec) = recorder_opt.as_mut() {\n            if let Err(first_err) = rec.open(resolution.device.clone()) {\n                // A cached device or config may have gone stale (unplugged,\n                // rate/format changed). Re-resolve from a fresh enumeration and\n                // retry once before surfacing the error.\n                warn!(\"Recorder open failed ({first_err}); re-resolving device and retrying once\");\n                self.invalidate_device_cache();\n                resolution = self.resolve_microphone_device(&settings);\n                rec.open(resolution.device.clone())\n                    .map_err(|e| anyhow::anyhow!(\"Failed to open recorder: {}\", e))?;\n            }\n        }\n        debug!(\n            \"mic stream breakdown: device_resolve={:?} vad_ensure={:?} open={:?}\",\n            resolve_elapsed,\n            vad_elapsed,\n            open_started.elapsed()\n        );\n        drop(recorder_opt);\n\n        *open_flag = true;\n        if let Some(unavailable_name) = resolution.unavailable_selected_microphone {\n            // Do this only after the default stream opened successfully. A\n            // failed fallback must not erase the user's microphone preference.\n            self.persist_default_microphone_after_fallback(&unavailable_name);\n        }\n        // This timing covers through cpal's stream.play() returning — i.e. the\n        // point cpal surfaces as \"stream running.\" It does NOT guarantee the","sourceCodeStart":702,"sourceCodeEnd":738,"githubUrl":"https://github.com/cjpais/Handy/blob/c6fa60da2f13a5af660fba17f37af548855119c5/src-tauri/src/managers/audio.rs#L702-L738","documentation":"Thrown when the cpal-backed Recorder::open() fails on the resolved input device, even after one re-resolution retry. The underlying cpal build_input_stream error occurs when the device cannot be opened with the requested config: it is unplugged, exclusively held by another app, lacks OS microphone permission, or rejects the stream config. The code already invalidates the device cache, re-enumerates, and retries once before surfacing this error, so seeing it means both attempts failed.","triggerScenarios":"Calling managers/audio.rs start-recording flow when: the selected microphone was unplugged/changed (stale cached Device handle); another application holds the device exclusively; macOS TCC microphone permission was denied; the device does not support the sample rate/format requested by Recorder::open (audio_toolkit/audio/recorder.rs:154).","commonSituations":"Hot-unplugging a USB headset between settings selection and recording; Bluetooth headset re-pairing changing the device index; a VoIP app holding exclusive mode on Windows; fresh macOS install where Terminal/the app has not been granted mic access; USB audio interfaces that power-sleep and refuse the first open.","solutions":["Check OS microphone permission for the app (macOS: System Settings > Privacy & Security > Microphone; Windows: Settings > Privacy > Microphone) and restart the app after granting.","Close other applications that may hold the microphone exclusively (Zoom, Discord, OBS) and retry recording.","Open settings and re-select the microphone (or set it to System Default) so a fresh device handle is resolved, then retry.","Replug or re-pair the audio device and verify it appears in the device list before recording.","If a specific device consistently fails, try another input device to isolate a hardware/driver problem."],"exampleFix":"// before\nlet mut rec = Recorder::new();\nrec.open(device)?; // hard fail on stale handle\n\n// after — re-resolve from a fresh enumeration and retry once (mirrors audio.rs:699)\nif let Err(first_err) = rec.open(device.clone()) {\n    warn!(\"Recorder open failed ({first_err}); re-resolving device\");\n    devices.invalidate_cache();\n    let fresh = devices.resolve_microphone_device(&settings);\n    rec.open(fresh.device)?;\n}","handlingStrategy":"retry","validationCode":"// Before recording: confirm the configured device still exists\nlet devices = manager.enumerate_input_devices()?;\nlet name = settings.selected_microphone.clone();\nif let Some(wanted) = &name {\n    if !devices.iter().any(|d| &d.name == wanted) {\n        warn!(\"configured mic '{wanted}' not present; falling back to default\");\n    }\n}","typeGuard":null,"tryCatchPattern":"match start_recording(&manager, &settings) {\n    Ok(_) => {},\n    Err(e) if e.to_string().contains(\"Failed to open recorder\") => {\n        // code already retried once with a fresh enumeration;\n        // surface to the user to re-pick the device / check OS permission\n        emit_event(\"recording-error\", e.to_string());\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Re-resolve the device from a fresh enumeration right before opening instead of caching Device handles across recordings.","Request microphone permission up front (macOS TCC) during onboarding rather than at first record.","Handle device-change notifications (plug/unplug) by invalidating the cached device.","Avoid exclusive-mode audio apps while recording, and prefer the system default device when reliability matters more than a specific mic."],"tags":["audio","cpal","microphone","device-open","recording"],"backgroundTag":"audio-device-open-failed","analyzedSha":"c6fa60da2f13a5af660fba17f37af548855119c5","analyzedAt":"2026-08-17T10:29:55.597Z","contentChangedAt":"2026-08-17T10:29:55.597Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}