cjpais/Handy · error · std::io::Error

{error_message}

Error message

{error_message}

What it means

The microphone worker thread failed to initialize and its string message is re-raised on the caller's thread. The message originates inside the worker: 'Failed to build input stream: {e}', 'Failed to start microphone stream: {e}', 'Unsupported sample format: {format:?}', or a device-config fetch failure. is_microphone_access_denied() promotes OS permission messages ('Access is denied', 'permission denied', WASAPI 0x80070005) to ErrorKind::PermissionDenied; everything else becomes ErrorKind::Other.

Source

Thrown at src-tauri/src/audio_toolkit/audio/recorder.rs:359

                }
            }
        });

        match init_rx.recv() {
            Ok(Ok(())) => {
                self.device = Some(device);
                self.cmd_tx = Some(cmd_tx);
                self.worker_handle = Some(worker);
                Ok(())
            }
            Ok(Err(error_message)) => {
                let _ = worker.join();
                let kind = if is_microphone_access_denied(&error_message) {
                    std::io::ErrorKind::PermissionDenied
                } else {
                    std::io::ErrorKind::Other
                };
                Err(Box::new(Error::new(kind, error_message)))
            }
            Err(recv_error) => {
                let _ = worker.join();
                Err(Box::new(Error::other(format!(
                    "Failed to initialize microphone worker: {recv_error}"
                ))))
            }
        }
    }

    /// Queue a recording start and return a one-shot receiver that resolves only
    /// after the first real microphone sample chunk has entered the capture path.
    /// `Stream::play()` returning is not sufficient: some Bluetooth and USB
    /// devices take much longer to begin delivering callbacks.
    pub fn start(
        &self,
        vad_policy: VadPolicy,
    ) -> Result<mpsc::Receiver<()>, Box<dyn std::error::Error>> {

View on GitHub (pinned to c6fa60da2f)

Solutions

  1. Grant microphone access: macOS System Settings > Privacy & Security > Microphone; Windows Settings > Privacy & Security > Microphone
  2. Re-select the input device in settings (a stale cached config is dropped on failure, so a retry re-queries the device) and retry
  3. Unplug/replug the mic or restart it; on Windows, disable exclusive-mode capture by other apps
  4. If the message mentions an unsupported sample format, change the device's default rate/format in OS sound settings or update the audio driver

Example fix

// before
if let Err(e) = recorder.start() {
    eprintln!("{e}");
}

// after — branch on the promoted error kind the start() handshake already produced
if let Err(e) = recorder.start() {
    if e.kind() == std::io::ErrorKind::PermissionDenied {
        // guide user to OS microphone privacy settings
    } else if is_no_input_device_error(&e.to_string()) {
        // refresh the device list and prompt re-selection
    } else {
        log::error!("microphone init failed: {e}");
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: confirm the selected device still exists before opening it
let device_still_present = host
    .input_devices()?
    .filter_map(|d| d.name().ok())
    .any(|name| name == selected_device_name);
if !device_still_present {
    // re-prompt device selection before attempting start()
}

Type guard

fn is_permission_error(e: &std::io::Error) -> bool {
    e.kind() == std::io::ErrorKind::PermissionDenied
}

Try / catch

match recorder.start() {
    Ok(()) => {}
    Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
        // surface a "grant microphone access" action pointing at OS privacy settings
    }
    Err(e) => {
        // log; the worker already dropped its stale config cache, so re-select the device and retry once
    }
}

Prevention

When it happens

Trigger: start() spawns the worker, which fetches the device config, builds a cpal input stream and calls stream.play(); any of these failing sends Err(msg) over init_tx and this branch re-raises it. Triggers: mic permission denied (macOS TCC, Windows privacy), device unplugged mid-open, device occupied, exotic sample format, or a stale cached config after the device changed rate/format in the OS.

Common situations: First run on macOS without granting Microphone permission; Windows microphone access toggled off in Privacy settings; recording right after unplugging a USB mic; Bluetooth headset switching profile (A2DP to HFP) between enumeration and open; devices exposing sample formats cpal cannot map.

Related errors


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