Skip to content

audiofeatures: a voice activity flag per slice - #336

Merged
bdbarnett merged 1 commit into
mainfrom
audiofeatures-vad
Oct 29, 2026
Merged

bdbarnett merged 1 commit into
mainfrom
audiofeatures-vad

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

Every slice the audiofeatures frontend makes now says whether it holds speech. Pass a second buffer to readinto() (or readinto_raw()), one byte a slice, and it fills with the flags; speaking() gives the newest one. A wake word detector can skip its model while nothing is said, and an intercom can hold the floor while someone is talking. The flags travel with their slices through the queue, so attach() to a tap and feed() from Python both get them, at any rate the frontend takes.

How it decides

It reads the frontend's 40 band amplitudes before noise reduction, as integer log levels (a 64th of an octave), and tracks a noise floor per band that falls fast and rises slowly. The score is the mean of the four bands furthest over their floors, averaged over 50 ms; about 10 dB over is speech, held 100 ms. All integer arithmetic, no libm, so the parity gate covers it: features_probe.py prints the flags, and CPython and unix MicroPython agree byte for byte.

Measured against Silero VAD

97 recordings (spoken phrases, longer recordings, alert sounds, steady noise), with the phrases also mixed with loud noise and turned down 30 dB, each after two seconds of its own background. Agreement per 10 ms slice:

Audio This WebRTC VAD, mode 3, same hold
Clean phrases 92% 96%
Noise 10 dB under the speech 97% 55%
Noise as loud as the speech 90% 42%
Phrases 30 dB quieter 97% 62%
Longer recordings 92% 94%
Steady noise, called speech 0.3% 0%

Fed at 48 kHz the figures match 16 kHz to 0.1%. Known misses: an alarm or doorbell reads as speech, and speech under louder noise is mostly missed. The guide section has the details.

Cost

About 0.7 KB of RISC-V text at -O2 on top of the frontend (13.07 KB to 13.75 KB linked), against about 9.6 KB for WebRTC's detector. 176 bytes of state and a byte per queued slice.

Tests

Goldens of the flags for a synthetic signal and for tone bursts, at block sizes 1, 160 and 4096, which fail when the threshold moves or the hold is removed (checked by planting both); steady noise at three levels never flagged; a noise that steps up 20 dB is learnt; digital silence then noise; 48 kHz against 16 kHz; reset. examples/audiofeatures_voice_activity.py prints the speech in a WAV file.

Each slice now carries a flag saying whether it holds speech, from the
frontend's own band levels against a per-band noise floor, held 100 ms.
readinto() and readinto_raw() take an optional second buffer for the
flags, and speaking() is the newest one. Integer only, so CPython,
MicroPython and a board agree; about 0.7 KB of flash on an ESP32-P4.
@bdbarnett
bdbarnett merged commit 707e0a6 into main Oct 29, 2026
31 checks passed
@bdbarnett
bdbarnett deleted the audiofeatures-vad branch October 29, 2026 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant