From ed94c7f92d950c67e7b74dd9da15e11bcff83b93 Mon Sep 17 00:00:00 2001 From: Chris Scheper Date: Sun, 19 Apr 2026 12:00:44 -0400 Subject: [PATCH 1/2] fix default rate bug on mac --- src/audio.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/src/audio.rs b/src/audio.rs index 4c1805a..cf921ec 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -49,9 +49,21 @@ fn record_audio(config: &Config) -> Result> { println!("[DEBUG] Using device: {}", device.name().unwrap_or_default()); } + // Use device's native config to avoid unsupported stream errors + let default_cfg = device + .default_input_config() + .map_err(|e| ListenError::Audio(format!("Failed to get device config: {}", e)))?; + + let native_rate = default_cfg.sample_rate().0; + let native_channels = default_cfg.channels(); + + if config.verbose { + println!("[DEBUG] Device native: {}Hz, {} ch", native_rate, native_channels); + } + let cpal_config = cpal::StreamConfig { - channels: config.channels, - sample_rate: cpal::SampleRate(config.sample_rate), + channels: native_channels, + sample_rate: cpal::SampleRate(native_rate), buffer_size: cpal::BufferSize::Default, }; @@ -84,10 +96,53 @@ fn record_audio(config: &Config) -> Result> { let samples = recorded_samples.lock().unwrap().clone(); if config.verbose { - println!("[DEBUG] Recorded {} samples", samples.len()); + println!("[DEBUG] Recorded {} samples at {}Hz", samples.len(), native_rate); + } + + // Mix to mono + let mono = if native_channels > 1 { + mix_to_mono(&samples, native_channels as usize) + } else { + samples + }; + + // Resample to target rate (16kHz for Whisper) + let target_rate = config.sample_rate; + let resampled = if native_rate != target_rate { + resample_linear(&mono, native_rate, target_rate) + } else { + mono + }; + + if config.verbose { + println!("[DEBUG] Final: {} samples at {}Hz", resampled.len(), target_rate); + } + + Ok(resampled) +} + +fn mix_to_mono(samples: &[f32], channels: usize) -> Vec { + samples + .chunks(channels) + .map(|frame| frame.iter().sum::() / channels as f32) + .collect() +} + +fn resample_linear(samples: &[f32], from_rate: u32, to_rate: u32) -> Vec { + let ratio = from_rate as f64 / to_rate as f64; + let output_len = (samples.len() as f64 / ratio) as usize; + let mut output = Vec::with_capacity(output_len); + + for i in 0..output_len { + let src_pos = i as f64 * ratio; + let src_idx = src_pos as usize; + let frac = (src_pos - src_idx as f64) as f32; + let s0 = samples.get(src_idx).copied().unwrap_or(0.0); + let s1 = samples.get(src_idx + 1).copied().unwrap_or(s0); + output.push(s0 + (s1 - s0) * frac); } - Ok(samples) + output } fn wait_for_stop_signal(config: &Config) -> Result<()> { From 6c9c33d059d7ea0edf78be0ea56dc4f772a72208 Mon Sep 17 00:00:00 2001 From: Chris Scheper Date: Sun, 19 Apr 2026 12:01:04 -0400 Subject: [PATCH 2/2] update README * spacebar doesn't stop sample, enter does * fix instructions for model downloads --- README.md | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9c734de..52faaf9 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,13 @@ listen -c # send to claude listen -v # verbose mode ``` -press SPACE to stop recording (default mode) +press ENTER to stop recording (default mode) ### stopping modes **manual stop (default)** ```sh -listen # press SPACE to stop +listen # press ENTER to stop ``` **auto-stop with silence detection (VAD)** @@ -180,6 +180,36 @@ models ordered by speed/accuracy tradeoff: - production: `small` or `medium` - mobile/termux: `tiny` (avoid medium/large due to memory) +### downloading models + +models are **not downloaded automatically**. you must download them manually before first use. models are stored in `~/.cache/whisper/`. + +```sh +# create model directory +mkdir -p ~/.cache/whisper + +# download a model (replace base with tiny/small/medium/large) +wget https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin \ + -P ~/.cache/whisper/ +``` + +**download all models at once:** +```sh +mkdir -p ~/.cache/whisper +BASE=https://huggingface.co/ggerganov/whisper.cpp/resolve/main + +wget $BASE/ggml-tiny.bin -P ~/.cache/whisper/ +wget $BASE/ggml-base.bin -P ~/.cache/whisper/ +wget $BASE/ggml-small.bin -P ~/.cache/whisper/ +wget $BASE/ggml-medium.bin -P ~/.cache/whisper/ +wget $BASE/ggml-large.bin -P ~/.cache/whisper/ +``` + +**verify downloads:** +```sh +ls -lh ~/.cache/whisper/ +``` + ## platform notes ### termux (android) @@ -351,5 +381,4 @@ rm -rf ~/.local/share/listen ~/.local/bin/listen - all processing happens locally on your device - no API keys required - no cloud services or external dependencies -- first run downloads the specified Whisper model (~75MB-3GB) -- models are cached for future use +- models must be downloaded manually to `~/.cache/whisper/` before first use (see [downloading models](#downloading-models))