cjpais/Handy · error

Failed to open recorder: {}

Error message

Failed to open recorder: {}

What it means

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).

Source

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

        let resolve_elapsed = resolve_started.elapsed();

        // Ensure VAD is loaded if it wasn't for whatever reason
        let vad_started = Instant::now();
        self.preload_vad()?;
        let vad_elapsed = vad_started.elapsed();

        let open_started = Instant::now();
        let mut recorder_opt = self.recorder.lock().unwrap();
        if let Some(rec) = recorder_opt.as_mut() {
            if let Err(first_err) = rec.open(selected_device.clone()) {
                // A cached device or config may have gone stale (unplugged,
                // rate/format changed). Re-resolve from a fresh enumeration and
                // retry once before surfacing the error.
                warn!("Recorder open failed ({first_err}); re-resolving device and retrying once");
                self.invalidate_device_cache();
                let fresh_device = self.get_effective_microphone_device(&settings);
                rec.open(fresh_device)
                    .map_err(|e| anyhow::anyhow!("Failed to open recorder: {}", e))?;
            }
        }
        debug!(
            "mic stream breakdown: device_resolve={:?} vad_ensure={:?} open={:?}",
            resolve_elapsed,
            vad_elapsed,
            open_started.elapsed()
        );

        *open_flag = true;
        // This timing covers through cpal's stream.play() returning — i.e. the
        // point cpal surfaces as "stream running." It does NOT guarantee the
        // host audio device is producing samples yet; the first input callback
        // fires asynchronously one buffer period later (hardware dependent,
        // typically ~10–200ms on macOS, longer on Bluetooth/USB).
        info!(
            "Microphone stream initialized in {:?}",
            start_time.elapsed()

View on GitHub (pinned to 98a4d80cce)

Solutions

  1. On macOS, grant Microphone permission in System Settings > Privacy & Security
  2. Pick a different input device in Handy settings and retry
  3. Close other apps that may hold the microphone exclusively
  4. Re-plug/re-select the device, or switch the system default away and back to force a fresh profile
  5. 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
Defensive patterns

Strategy: retry

Validate before calling

// confirm the device still exists before opening
let devices = list_input_devices()?;
if !devices.iter().any(|d| d.name == selected.name) {
    // fall back to the system default instead of opening a dead device
    selected = default_input_device()?;
}

Try / catch

// the code already retries once after re-enumeration; wrap the final error with device context
rec.open(device).map_err(|e| anyhow::anyhow!(
    "cannot open input device {:?} (retry after re-enumeration also failed): {e}",
    device.name()
))?

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of cjpais/Handy@98a4d80cce (2026-08-16). Data as JSON: /api/errors/7e33c332915c0221. Report an issue: GitHub.