packages/audio/src/mel.nu — STFT and the mel filter bank.
Every constant here was read out of transformers' audio_utils.py and feature_extraction_whisper.py, not remembered. The details that look like details are the ones that decide whether a speech model hears anything:
i.e. 0.5·(1 − cos(2πn/N)). The symmetric window (…/(N−1)) is a different window, and every frame would be subtly wrong.
("center"), so frame k is centred on sample k·hop, not started there.
HTK's 2595·log10(1+f/700) is the other convention, and it gives different filters.
maps it to (x + 4)/4.
( mel_filters n_fft n_mels rate f_min f_max ) → ( Vec f ) (n_bins × n_mels) ( stft_power x plan window hop ) → ( Vec f ) frames × n_bins ( log_mel_whisper x n_fft hop n_mels rate ) → ( Vec f ) frames × n_mels
Data is row-major and flat: a spectrogram is one ( Vec f ), not a vec of vecs. It goes to a GPU next, and that is the layout a GPU wants.
: f MEL_PI 3.14159265358979323846@ hz_to_mel_slaney f hz → fSlaney mel: linear at 3f/200 below 1 kHz, logarithmic above.
@ mel_to_hz_slaney f mel → f@ mel_filters i n_fft i n_mels i rate f f_min f f_max → ( Vec f )The triangular filter bank, row-major (n_bins rows × n_mels columns) — the same orientation transformers uses, so a mel value is a dot product down a column.
@ hann_periodic i n → ( Vec f )Periodic Hann: 0.5·(1 − cos(2πn/N)). NOT the symmetric one.
@ reflect_pad ( Vec f ) x i pad → ( Vec f )Reflect-pad by pad samples on both sides: x[pad], x[pad-1], …, x[1] in front (the sample itself is not repeated) and the mirror image at the end. This is what "center=True" means, and it is why frame k is CENTRED on sample k·hop rather than starting there.
@ stft_power * FftPlan p ( Vec f ) x ( Vec f ) window i n_fft i hop → ( Vec f )|STFT|², row-major frames × (n_fft/2+1). x is already padded; the caller owns the plan (an STFT runs the same length thousands of times).
@ log_mel_whisper ( Vec f ) x i n_fft i hop i n_mels i rate → ( Vec f )The whisper log-mel spectrogram: exactly what WhisperFeatureExtractor produces, in the same layout (frames × n_mels, row-major).
@ pad_or_trim ( Vec f ) x i n → ( Vec f )Pad with zeros (or trim) to exactly n samples — whisper always feeds its encoder 30 s, whatever the clip is.
packages/audio/src/main.nu — the CLI.
audio info <file.wav> rate, channels, bits, duration audio mel <file.wav> -o mel.f32 whisper log-mel (80 × 3000), raw f32 LE audio resample <in.wav> <out.wav> --rate 16000 audio tone <out.wav> --rate 16000 a known signal, for tests audio vad <file.wav> where the speech is (and how much is not)
@ main → ipackages/audio/src/wav.nu — RIFF/WAVE, read and write.
The chunk layout is the whole format, and it is also the whole attack surface: every chunk announces its own size, and a file is free to lie about it. So the reader walks the chunks with a bounded cursor — a size that runs past the end of the file, a data chunk longer than the bytes that follow, a zero or absurd sample rate, a channel count of 0, a bit depth nobody has ever used: all of them are a clean error, none of them are an allocation sized by what the header claimed.
( wav_read path ) → !Wav String f32 samples, interleaved ( wav_read_mono_16k path ) → !( Vec f ) String what speech models want ( wav_write path samples rate ch ) → !v String 16-bit PCM ( wav_free w ) → v
Samples come back as f32 in [-1, 1], whatever the file stored them as: 8-bit unsigned, 16/24/32-bit signed PCM, or 32/64-bit IEEE float.
: i WAV_PCM 1: i WAV_FLOAT 3: i WAV_EXTENSIBLE 65534: Wav: Wav {
( Vec f ) samples // interleaved
i channels
i rate
i bits // as stored in the file (informational)
}
@ wav_free Wav w → v: WCur: WCur {
( Vec u ) d
i n
i off
b fail
}
A bounded cursor over the file bytes. Every read checks the budget against what is LEFT, never by forming off + k (which a hostile size would wrap).
@ wav_read s path → !Wav String@ wav_mono Wav w → ( Vec f )Average the channels. (Speech models want one channel; picking the left one throws away half of a stereo recording's information.)
@ wav_write s path ( Vec f ) samples i rate i channels → !v String16-bit PCM, which every tool on earth reads.
packages/audio/src/resample.nu — sample-rate conversion.
Linear interpolation is the tempting one-liner, and it is wrong in a way that matters here: it is a lousy low-pass filter, so downsampling with it folds everything above the new Nyquist frequency back into the speech band as aliasing — exactly the frequencies a speech model is listening to.
So: windowed-sinc (Kaiser-ish, a Hann-windowed sinc), the standard honest answer. The kernel is band-limited to the LOWER of the two Nyquist frequencies, which is what makes downsampling safe.
( resample x from_rate to_rate ) → ( Vec f )
: f RS_PI 3.14159265358979323846: i RS_ZEROS 16Zeros are half the taps of a windowed sinc — 16 gives a stopband deep enough that the aliasing is far below the noise floor of any real recording.
@ resample ( Vec f ) x i from_rate i to_rate → ( Vec f )packages/audio/src/vad.nu — voice activity detection.
Finding where the speech IS, so the parts that are not speech never reach a model. An hour of recording is mostly not speech, and a speech model does not get cheaper by running over silence — it costs exactly the same.
( vad_segments samples rate opts ) → ( Vec VadSeg ) [start, end) in samples ( vad_default_opts ) → VadOpts
This is an ENERGY detector with an adaptive noise floor, not a neural one. Being clear about that matters, because it decides where it fails: it hears "something loud enough, often enough" rather than "a human voice". It handles clean speech with a steady noise floor — a recording, a call, a meeting — and it will call a slammed door speech. faster-whisper uses Silero, a small neural VAD, for exactly that reason; this is the honest version of what can be done without a second model, and the seam is here for one.
What it is NOT is a fixed threshold. A fixed dB threshold works on the file it was tuned on and nowhere else. The floor here is the 10th percentile of the frame energies — whatever the quiet part of THIS recording sounds like — and speech is what stands a margin above it.
: VadSeg: VadSeg {
i start // first sample
i end // one past the last
}
: VadOpts: VadOpts {
f margin_db // how far above the noise floor speech has to be
i min_speech_ms // a burst shorter than this is not speech
i min_silence_ms // a gap shorter than this does not split a segment
i pad_ms // keep this much either side — speech starts before it
} // gets loud, and a word's tail is quiet
@ vad_default_opts → VadOptsDefaults that work on speech: 6 dB over the floor, 250 ms of it, gaps under 400 ms bridged (that is a pause between words, not between sentences), and 200 ms of padding kept either side so no consonant is clipped off.
@ vad_segments ( Vec f ) x i rate VadOpts o → ( Vec VadSeg )@ vad_voiced_samples ( Vec VadSeg ) segs → iHow much of the audio the segments actually cover — what a caller saves.
: VadRun: VadRun {
i cond
i orig
i len
}
A run of the condensed audio and where it came from: condensed sample cond … cond+len is original sample orig … orig+len. What the condensation THREW AWAY is exactly what is not covered by any run — and it is why the runs exist at all: a model transcribing the condensed audio reports times in the CONDENSED timeline, and a caller who wants to say "this was said at 3:12 of the recording" has to walk the map back.
@ vad_extract_runs ( Vec f ) x ( Vec VadSeg ) segs i max_gap ( Vec VadRun ) runs → ( Vec f )The audio with the silence taken out: the segments, with at most max_gap samples of the real audio kept between consecutive ones, and one VadRun appended to runs for every contiguous stretch that survives.
The cap is not a detail. Glue two segments together with NOTHING between them and a listener — a speech model included — hears one utterance where there were two: whisper, handed two sentences back to back with no pause, will run them together and get the second one's words wrong. Keeping a fraction of a second of the actual room gives it the boundary it needs, and capping that fraction is what keeps the silence from being the cost again. max_gap = 0 concatenates.
@ vad_extract ( Vec f ) x ( Vec VadSeg ) segs i max_gap → ( Vec f )@ vad_map_sample ( Vec VadRun ) runs i s → iA condensed sample position, mapped back to the original recording. Positions past the end of a run but before the next (which cannot arise from a model reading the condensed audio, but can arise from a rounded-up timestamp) clamp to the end of the nearer run.