Skip to content

fix: decouple microphone recording rate from TTS sample rate#503

Open
GrowDev1 wants to merge 1 commit into
mbailey:masterfrom
GrowDev1:fix/recording-sample-rate-491
Open

fix: decouple microphone recording rate from TTS sample rate#503
GrowDev1 wants to merge 1 commit into
mbailey:masterfrom
GrowDev1:fix/recording-sample-rate-491

Conversation

@GrowDev1

Copy link
Copy Markdown

Fixes #491.

Summary

SAMPLE_RATE (voice_mode/config.py, 24000) was a single hardcoded constant used for both TTS playback and microphone recording. 24kHz is correct for TTS (it's what the providers emit), but wrong as a hardcoded mic capture rate — a microphone's native rate is whatever the hardware is, commonly 44.1kHz or 48kHz for USB mics, never 24kHz. Requesting a rate the device doesn't natively support isn't universally safe: some audio backends resample transparently, some don't, producing corrupted/aliased recordings on affected hardware/OS/driver combinations — exactly what #491 reports with a 48kHz-native mic.

The fix

Added RECORDING_SAMPLE_RATE = int(os.getenv("VOICEMODE_RECORDING_SAMPLE_RATE", str(SAMPLE_RATE))) — a new, independent config constant. Defaults to SAMPLE_RATE, so behavior is unchanged unless a user explicitly opts in.

Switched every recording-side usage in voice_mode/tools/converse.py to the new constant:

  • prepare_audio_for_stt()'s AudioSegment construction + the Whisper-downsample comparison
  • The full-quality WAV archival write, and the no-speech WAV dump
  • record_audio()'s sd.rec() call, sample-count math, and debug/error logging
  • record_audio_with_silence_detection()'s VAD chunk-size math, sd.InputStream() call, and the VAD-downsampling resample-length math
  • The STT stats/telemetry dict (sample_rate_hz, bitrate_kbps)

All TTS/playback-side usage (all of streaming.py, core.py's chime-playback functions) is untouched — verified by checking every remaining SAMPLE_RATE reference in the repo is genuinely playback-side, not a missed recording site.

Docs fix, found while verifying this: docs/reference/environment.md and docs/guides/configuration.md already documented a VOICEMODE_SAMPLE_RATE variable — including recommending it as a fix for exactly this kind of mic-rate mismatch — but the code never actually reads that variable anywhere (confirmed via a repo-wide grep). It was a documented no-op that would have silently done nothing for anyone following the docs. Updated those entries (and one in docs/.archive/troubleshooting/wsl2-microphone-access.md) to point at the new, working VOICEMODE_RECORDING_SAMPLE_RATE instead.

Test plan

  • tests/test_recording_sample_rate_config.py — new, 3 tests: default equals SAMPLE_RATE (24000, unchanged behavior), override is decoupled from TTS rate, no cross-test pollution
  • tests/test_recording_sample_rate_prepare_audio.py — new, 5 tests:
    • prepare_audio_for_stt() produces WAV bytes whose actual duration is correct at an overridden rate (a real check via wave module on real exported bytes — this would fail if frame_rate weren't threaded correctly, unlike a mocked-call-args check)
    • Same at the default rate (regression guard)
    • record_audio() requests the overridden rate from sounddevice
    • VAD resample math lands on an exact 480-sample (30ms/16kHz) frame across every standard rate (8k/16k/24k/32k/44.1k/48kHz) — WebRTC VAD requires an exact frame size or raises, so this guards the most delicate part of the change
  • Full existing suite (tests/test_stt_audio_saving.py, test_stt_local_compression_skip.py, test_silence_detection.py, test_vad_aggressiveness.py, test_history_buffer.py, test_audio_format_config.py): 61 passed, 2 failed (confirmed pre-existing on unmodified master too — missing ffmpeg in my environment, unrelated to this change), 5 skipped (pre-existing, unrelated)

…#491)

SAMPLE_RATE (24000) was a single hardcoded constant used for BOTH TTS
playback (where 24kHz is correct -- it's what the TTS providers emit) AND
microphone recording (where it's wrong -- a mic's native rate is whatever
the hardware is, commonly 44.1kHz or 48kHz for USB mics, never 24kHz).
Requesting a rate the device doesn't natively support isn't universally
safe: some backends resample transparently, some don't, producing
corrupted/aliased recordings on affected hardware/OS/driver combinations.

Adds RECORDING_SAMPLE_RATE, a new config constant defaulting to SAMPLE_RATE
(so behavior is unchanged unless a user opts in via
VOICEMODE_RECORDING_SAMPLE_RATE), and switches every recording-side usage
in converse.py to it: prepare_audio_for_stt's AudioSegment construction,
the WAV archival writes, record_audio's sd.rec() call, and
record_audio_with_silence_detection's VAD chunk-size math + sd.InputStream
+ resampling math. All TTS/playback-side usage (streaming.py, core.py's
chime functions) is untouched.

Also fixes docs that referenced a VOICEMODE_SAMPLE_RATE variable the code
never actually read (a documented no-op) -- including one that recommended
it as a WSL2 microphone troubleshooting step, which would have silently
done nothing. Pointed these at the new, working variable instead.

Tests: 2 new files covering the config default/override behavior, that
prepare_audio_for_stt/record_audio honor the actual capture rate (verified
via real WAV byte output, not just mocked call args), and that the VAD
resampling math lands on an exact 480-sample (30ms/16kHz) frame across
every standard rate (8k/16k/24k/32k/44.1k/48kHz) -- WebRTC VAD requires an
exact frame size or it raises.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recording sample rate mismatch causes corrupted/blank audio on some mics (e.g. Blue Yeti)

1 participant