From 62aea78156c9fe2939d4ae82f54be9c949fdb3d0 Mon Sep 17 00:00:00 2001 From: Dan Vernon <41971475+dan-vernon@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:31:55 +0000 Subject: [PATCH 1/2] fix: always use default_input_config to avoid shared-mode format rejection CoreAudio (macOS) and WASAPI (Windows) only accept streams in the OS mixer's native shared-mode format. The previous get_preferred_config() tried to find a higher-quality format (preferring F32 over I16) by iterating supported_input_configs(), but those enumerate exclusive-mode formats that both backends reject with "stream configuration not supported" when opening in shared mode. Fixes the consistent recording failure on macOS and Windows where build_input_stream fails immediately. Resolves the same class of issue as #990. Co-Authored-By: Claude Sonnet 4.6 --- src-tauri/src/audio_toolkit/audio/recorder.rs | 60 ++++--------------- 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/src-tauri/src/audio_toolkit/audio/recorder.rs b/src-tauri/src/audio_toolkit/audio/recorder.rs index ef94a9836..975b05d62 100644 --- a/src-tauri/src/audio_toolkit/audio/recorder.rs +++ b/src-tauri/src/audio_toolkit/audio/recorder.rs @@ -282,56 +282,16 @@ impl AudioRecorder { fn get_preferred_config( device: &cpal::Device, ) -> Result> { - // Use the device's native/default sample rate and let the FrameResampler - // in run_consumer() downsample to 16kHz. This avoids forcing hardware into - // a non-native rate which can cause issues on some devices (Bluetooth - // codecs, certain ALSA drivers, etc.). - let default_config = device.default_input_config()?; - let target_rate = default_config.sample_rate(); - - // Try to find the best sample format at the device's default rate - let supported_configs = match device.supported_input_configs() { - Ok(configs) => configs, - Err(e) => { - log::warn!("Could not enumerate input configs ({e}), using device default"); - return Ok(default_config); - } - }; - let mut best_config: Option = None; - - for config_range in supported_configs { - if config_range.min_sample_rate() <= target_rate - && config_range.max_sample_rate() >= target_rate - { - match best_config { - None => best_config = Some(config_range), - Some(ref current) => { - // Prioritize F32 > I16 > I32 > others - let score = |fmt: cpal::SampleFormat| match fmt { - cpal::SampleFormat::F32 => 4, - cpal::SampleFormat::I16 => 3, - cpal::SampleFormat::I32 => 2, - _ => 1, - }; - - if score(config_range.sample_format()) > score(current.sample_format()) { - best_config = Some(config_range); - } - } - } - } - } - - if let Some(config) = best_config { - return Ok(config.with_sample_rate(target_rate)); - } - - // Fall back to device default if no config matched (exotic/virtual devices) - log::warn!( - "No supported config matched device default rate {:?}, using default config", - target_rate - ); - Ok(default_config) + // Always use the device's default config. This returns the OS mixer's + // shared-mode native format (sample rate, channels, and sample format), + // which is the only format that CoreAudio and WASAPI will accept when + // opening a stream in shared mode. Trying to override the sample format + // (e.g. preferring F32 over I16) causes "stream configuration not + // supported" errors on macOS and Windows even when enumeration claims + // the format is available — those formats are exclusive-mode only. + // The FrameResampler in run_consumer() handles any necessary downsampling + // to the 16 kHz rate required by Whisper/Parakeet. + Ok(device.default_input_config()?) } } From c980060d252889c931db2621b002ba257505d43e Mon Sep 17 00:00:00 2001 From: Dan Vernon <41971475+dan-vernon@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:24:05 +0000 Subject: [PATCH 2/2] fix: fall back to I16 when default format fails to build stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some devices (notably Bluetooth HFP on macOS, e.g. Sony MDR-1000X) report F32 in default_input_config() but CoreAudio rejects that format when the stream is actually opened — only I16 works for the HFP shared-mode stream. Pre-clone the sender and stop-flag (both cheap ref-count bumps) so we can retry with I16 before giving up. Co-Authored-By: Claude Sonnet 4.6 --- src-tauri/src/audio_toolkit/audio/recorder.rs | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/audio_toolkit/audio/recorder.rs b/src-tauri/src/audio_toolkit/audio/recorder.rs index 975b05d62..30ca1a30c 100644 --- a/src-tauri/src/audio_toolkit/audio/recorder.rs +++ b/src-tauri/src/audio_toolkit/audio/recorder.rs @@ -102,52 +102,73 @@ impl AudioRecorder { config.sample_format() ); - let stream = match config.sample_format() { + // Pre-clone for I16 fallback — both are cheap (ref-count bumps). + // Some devices (e.g. Bluetooth HFP on macOS) report one format + // in default_input_config() but only accept I16 when the stream + // is actually opened. We retry with I16 before giving up. + let sample_tx_i16 = sample_tx.clone(); + let stop_i16 = stop_flag_for_stream.clone(); + + let stream_result = match config.sample_format() { cpal::SampleFormat::U8 => AudioRecorder::build_stream::( &thread_device, &config, sample_tx, channels, stop_flag_for_stream, - ) - .map_err(|e| format!("Failed to build input stream: {e}"))?, + ), cpal::SampleFormat::I8 => AudioRecorder::build_stream::( &thread_device, &config, sample_tx, channels, stop_flag_for_stream, - ) - .map_err(|e| format!("Failed to build input stream: {e}"))?, + ), cpal::SampleFormat::I16 => AudioRecorder::build_stream::( &thread_device, &config, sample_tx, channels, stop_flag_for_stream, - ) - .map_err(|e| format!("Failed to build input stream: {e}"))?, + ), cpal::SampleFormat::I32 => AudioRecorder::build_stream::( &thread_device, &config, sample_tx, channels, stop_flag_for_stream, - ) - .map_err(|e| format!("Failed to build input stream: {e}"))?, + ), cpal::SampleFormat::F32 => AudioRecorder::build_stream::( &thread_device, &config, sample_tx, channels, stop_flag_for_stream, - ) - .map_err(|e| format!("Failed to build input stream: {e}"))?, + ), sample_format => { return Err(format!("Unsupported sample format: {sample_format:?}")); } }; + let stream = match stream_result { + Ok(s) => s, + Err(e) if config.sample_format() != cpal::SampleFormat::I16 => { + log::warn!( + "Failed with {:?} format, retrying with I16: {e}", + config.sample_format() + ); + AudioRecorder::build_stream::( + &thread_device, + &config, + sample_tx_i16, + channels, + stop_i16, + ) + .map_err(|e| format!("Failed to build input stream: {e}"))? + } + Err(e) => return Err(format!("Failed to build input stream: {e}")), + }; + stream .play() .map_err(|e| format!("Failed to start microphone stream: {e}"))?;