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

No input device found

Error message

No input device found

What it means

Returned as a std::io::Error with kind NotFound when cpal's host cannot produce a default input device. The recorder asks the platform audio host (CoreAudio, WASAPI, ALSA/PulseAudio/PipeWire) for default_input_device() and converts a None into this error before any stream is built. It is a propagated Err, not a panic, so callers can match on it.

Source

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

            if !self.needs_reopen() {
                return Ok(()); // already open
            }
            log::warn!("Capture stream failed; rebuilding microphone stream");
            let _ = self.close();
        }

        self.stream_error.store(false, Ordering::Relaxed);

        let (sample_tx, sample_rx) = mpsc::channel::<AudioChunk>();
        let (cmd_tx, cmd_rx) = mpsc::channel::<Cmd>();
        let (init_tx, init_rx) = mpsc::sync_channel::<Result<(), String>>(1);

        let host = crate::audio_toolkit::get_cpal_host();
        let device = match device {
            Some(dev) => dev,
            None => host
                .default_input_device()
                .ok_or_else(|| Error::new(std::io::ErrorKind::NotFound, "No input device found"))?,
        };

        let thread_device = device.clone();
        let vad = self.vad.clone();
        // Move the optional level callback into the worker thread
        let level_cb = self.level_cb.clone();
        // Move the optional real-time audio frame callback into the worker thread
        let audio_cb = self.audio_cb.clone();
        let selected_channel = self.selected_channel;
        let config_cache = Arc::clone(&self.config_cache);
        let stream_error = Arc::clone(&self.stream_error);

        let worker = std::thread::spawn(move || {
            let stop_flag = Arc::new(AtomicBool::new(false));
            let stop_flag_for_stream = stop_flag.clone();
            let init_result = (|| -> Result<(cpal::Stream, u32), String> {
                let config_started = Instant::now();
                let device_name = thread_device.name().unwrap_or_default();

View on GitHub (pinned to c6fa60da2f)

Solutions

  1. Connect a microphone and set it as the default input device in OS sound settings
  2. Select an explicit input device in Handy settings instead of relying on the OS default (the Some(dev) branch bypasses default_input_device entirely)
  3. On bare Linux, install and start PipeWire or PulseAudio so cpal's host can enumerate inputs
  4. In containers/WSL/remote sessions, expose or forward an audio device, or run in a session that has one

Example fix

// before
let device = host
    .default_input_device()
    .ok_or_else(|| Error::new(std::io::ErrorKind::NotFound, "No input device found"))?;

// after — surface device-count context so support can tell no-mic from broken-audio-server
let device = match host.default_input_device() {
    Some(d) => d,
    None => {
        let visible = host.input_devices().map(|mut it| it.count()).unwrap_or(0);
        return Err(Box::new(Error::new(
            std::io::ErrorKind::NotFound,
            format!("No default input device found ({visible} input devices visible)"),
        )));
    }
};
Defensive patterns

Strategy: validation

Validate before calling

// Before entering a recording state, confirm an input exists
let has_input = crate::audio_toolkit::get_cpal_host()
    .input_devices()
    .map(|mut it| it.next().is_some())
    .unwrap_or(false);
if !has_input {
    // show a "connect a microphone" state instead of starting the recorder
}

Type guard

fn input_device_available(host: &cpal::Host) -> bool {
    host.default_input_device().is_some()
}

Prevention

When it happens

Trigger: Calling the record/start path without an explicit device while host.default_input_device() returns None: no microphone attached, no default capture device selected in OS sound settings, or a session where the audio server exposes no inputs (bare ALSA, headless/SSH, some containers, non-audio remote desktop sessions).

Common situations: Desktop or server with no mic; USB mic unplugged between device-list refresh and record start; fresh Linux install or WSL/container without PulseAudio/PipeWire; remote-desktop session that does not forward capture devices; default device pointing at a disconnected Bluetooth headset.

Related errors


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