Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi, space didn't work. had to hit ENTER to get it to stop


### stopping modes

**manual stop (default)**
```sh
listen # press SPACE to stop
listen # press ENTER to stop
```

**auto-stop with silence detection (VAD)**
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
63 changes: 59 additions & 4 deletions src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,21 @@ fn record_audio(config: &Config) -> Result<Vec<f32>> {
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,
};

Expand Down Expand Up @@ -84,10 +96,53 @@ fn record_audio(config: &Config) -> Result<Vec<f32>> {
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<f32> {
samples
.chunks(channels)
.map(|frame| frame.iter().sum::<f32>() / channels as f32)
.collect()
}

fn resample_linear(samples: &[f32], from_rate: u32, to_rate: u32) -> Vec<f32> {
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<()> {
Expand Down