{"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":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/src/managers/audio.rs","lineNumber":638,"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(selected_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                let fresh_device = self.get_effective_microphone_device(&settings);\n                rec.open(fresh_device)\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\n        *open_flag = true;\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\n        // host audio device is producing samples yet; the first input callback\n        // fires asynchronously one buffer period later (hardware dependent,\n        // typically ~10–200ms on macOS, longer on Bluetooth/USB).\n        info!(\n            \"Microphone stream initialized in {:?}\",\n            start_time.elapsed()","sourceCodeStart":620,"sourceCodeEnd":656,"githubUrl":"https://github.com/cjpais/Handy/blob/98a4d80cce8ad41efec2a419b59d9e81229a35d7/src-tauri/src/managers/audio.rs#L620-L656","documentation":"The final failure of rec.open(device) in start_microphone_stream: the first open already failed, the code invalidated the device/config cache, re-resolved the device from a fresh enumeration, retried once, and the retry also failed. Inner errors come from cpal stream creation — device gone, unsupported config, or a host-level error (ALSA 'device or resource busy', macOS permission denied, Windows device disabled).","triggerScenarios":"Selected USB mic unplugged between enumeration and open; another app holds the device exclusively; macOS microphone TCC permission not granted; Linux device grabbed by a different audio server (PipeWire/JACK/pulse exclusivity); cached sample rate/format no longer supported after a device mode change (the retry should heal this one — failing twice means it is not a stale-cache problem).","commonSituations":"First recording after boot with a Bluetooth headset still negotiating its profile; macOS users denying the mic permission prompt; default device switching while Handy is idle; devices that expose only rates the stream config does not accept.","solutions":["On macOS, grant Microphone permission in System Settings > Privacy & Security","Pick a different input device in Handy settings and retry","Close other apps that may hold the microphone exclusively","Re-plug/re-select the device, or switch the system default away and back to force a fresh profile","If it persists, check the inner {e}: an unsupported-config error points at the device's rate/format, a permission/busy error at OS access"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// confirm the device still exists before opening\nlet devices = list_input_devices()?;\nif !devices.iter().any(|d| d.name == selected.name) {\n    // fall back to the system default instead of opening a dead device\n    selected = default_input_device()?;\n}","typeGuard":null,"tryCatchPattern":"// the code already retries once after re-enumeration; wrap the final error with device context\nrec.open(device).map_err(|e| anyhow::anyhow!(\n    \"cannot open input device {:?} (retry after re-enumeration also failed): {e}\",\n    device.name()\n))?","preventionTips":["Handle macOS microphone TCC permission proactively — check and prompt at startup, not at first open failure","Re-select the input device after unplugging hardware instead of trusting the cached selection","Watch for the unsupported-config inner error and clear the cached rate/format before retrying"],"tags":["rust","cpal","audio-device","permissions","open-failed"],"backgroundTag":"audio-device-open-failed","analyzedSha":"98a4d80cce8ad41efec2a419b59d9e81229a35d7","analyzedAt":"2026-08-16T20:58:09.966Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}