* Tests: one integration-test binary per crate Each crate with several tests/*.rs files keeps them as modules of tests/it/, so cargo links one test executable per crate instead of one per file. Files that change process-wide state or time a workload stay binaries of their own. * Profiles: line tables for workspace crates, no debug info for dependencies * Windows ARM64: nightly and on release pushes instead of every push to main * xtask ci: clippy and tests in the dev profile The release profile's thin LTO and single codegen unit made linking the test binaries take most of the gate. The codec and audio crates stay at opt-level 3 in the dev profile, so their tests run at release speed. * xtask ci: nextest, doctests, no examples, one wasm check Tests run with cargo nextest when it is installed (cargo test otherwise) and build with --lib --bins --tests, so examples aren't linked; doctests run separately. The wasm check keeps going past the first failing crate. VP8 fixture generation locks across processes, since nextest runs each test in its own. * VP9 synthetic fixtures: write aside and rename A test in another process could read a fixture ffmpeg was still writing. * Fuzz tests: a third of each sweep by default, all of it with FILMCRAFT_FUZZ_ITERS Every seed, mutation kind and thread count still runs. Header bytes each get one overwrite value and two bit flips in rotation, so every value and bit index still occurs across the region. FILMCRAFT_FUZZ_ITERS=N runs the whole sweep plus N random rounds per seed, as before. * Platform fallback tests: one failure point per hybrid path The failure-anywhere test injected a failure at four points around every restart point; the hybrid's paths differ only in the first GOP, around the second and last restart points, at the end and never. The replay test needs the first two GOPs. * Export tests: shorter ranges with the same checks The bitrate test measures 0.5–1.5 s, where each preset reaches its own target (on 0–3 s the simple demo left two presets at the same rate). The every-preset test exports three frames per preset. * Audio tests: effects in parallel, a table-driven FFT for the Opus quality metric The extreme-settings test keeps both settings and the 40 s tail for every effect, one thread each. The Opus quality metric's FFT builds its twiddles once and allocates nothing per transform (same algorithm and summation order). * CI: dev-profile gate with nextest xtask ci no longer builds with --release, so the release-profile overrides go; nextest is installed for the test step. * CI: apt packages through the cached install-apt-packages action * Review fixes: fuzz tests with a panic hook in their own binaries, loud fixture lock The fuzz tests of av1, h264, hevc, mpeg2v, vp8 and vp9 count panics through a process-wide hook, so a panic in another test of the same process would fail them; they are binaries of their own again. The VP8 fixture lock fails the test instead of skipping it silently, and the full-sweep doc comments say how many truncation points the full run samples. * Fuzz tests: rotate the default subset per run (FILMCRAFT_FUZZ_SEED replays one) * platform tests: decode-switch tests in their own binaries, one NVENC lock
filmcraft-audio-dsp
Real-time audio DSP for FilmCraft. Layer L1, no dependencies, compiles for
wasm32-unknown-unknown, no unsafe. Clean-room: written from the public standards
(ITU-R BS.1770-4, EBU R128 / Tech 3341 / Tech 3342) and textbook DSP (RBJ cookbook biquads,
Householder FDN, phase vocoder, spectral subtraction with the statistical late-reverberation
model).
Conventions
- Planar
f32audio:&mut [&mut [f32]], one slice per channel, equal lengths. - Effects are built (allocating) with
(sample_rate, channels);process,set_paramandresetnever allocate. - Parameters change between blocks and are smoothed per sample; output does not depend on how the stream is cut into blocks (tested for every effect with 0/1/random-size blocks).
- Recursive state is flushed below ~1e-30, so no denormals without CPU FTZ flags.
Loudness (loudness)
LoudnessMeter::new(sample_rate, channels) → process(&[&[f32]]) / process_interleaved, then
momentary(), short_term(), integrated(), loudness_range(), sample_peak(),
true_peak() / true_peak_dbtp(), summary().
- K-weighting derived for any sample rate by bilinear transform (matches the 48 kHz BS.1770 table).
- Gated integrated loudness (−70 LUFS / −10 LU) and LRA (−70 / −20 LU, 10th–95th percentile) use a 0.01 LU histogram with exact energy sums: fixed memory for programmes of any length.
- True peak: 4× (2× at 88.2/96 kHz) polyphase Kaiser-sinc interpolation.
- Oracle-tested against ffmpeg's
ebur128filter (tests/loudness_oracle.rs, skipped without ffmpeg): M/S within 0.0005 LU, I within 0.007 LU, LRA within 0.04 LU and true peak within 0.045 dB (0.053 dB of the analytic value on an fs/4 tone) on sine, pink-noise, speech-like and gated stereo signals at 44.1/48/96 kHz. normalize_gain_db(measured, target)andnormalize_gain_db_peak_limited(...)for Normalize / Essential Sound loudness auto-match.
Effects (effects)
All implement AudioEffect (id, params, set_param, param, reset, process, latency).
effects() lists EffectInfo { id, name, category, params, create }; each ParamSpec has
id, name, range, default, Unit, log-scale hint and choice labels so UIs can be generated.
| id | effect |
|---|---|
parametric_eq |
8 bands, each peaking / shelf / LP / HP / notch / band-pass, RBJ biquads in TDF-II |
simple_eq |
low shelf, mid peak, high shelf |
compressor |
peak/RMS detector, soft knee, attack/release, make-up; stereo-linked |
gate |
downward expander / gate with range and hold |
limiter |
5 ms look-ahead, true-peak-aware, ceiling guaranteed (latency reported) |
delay |
feedback delay, ms or tempo-synced divisions, mix |
reverb |
8×8 Householder FDN, pre-delay, RT60 decay, HF damping, size, mix |
amplify, channel_volume, balance |
gain, per-channel gain, balance / constant-power pan |
invert, swap_channels, fill_left, fill_right |
polarity and routing |
dehum |
50/60 Hz notch comb with up to 10 harmonics |
denoise |
STFT spectral gating, minimum-statistics noise floor (latency = FFT size) |
pitch_shifter |
±12 semitones, phase vocoder with peak shifting + phase locking (latency = FFT size) |
deesser |
high-pass sidechain at frequency (2–12 kHz) detects sibilance relative to the broadband level (threshold); a dynamic high shelf cuts the band by up to reduction dB (1 ms / 60 ms, stereo-linked); exact identity when idle |
dereverb |
STFT late-reverb suppression: λ_r(t) = e^{−2Δ·T_d}·λ_x(t − T_d) (Δ = 3 ln10 / rt60, T_d ≈ 50 ms), over-subtraction 1 + amount, gain floor down to −18 dB, smoothed gains (latency = FFT size) |
speech_enhance |
Enhance Speech DSP chain: 80 Hz HPF, de-mud cut (250 / 350 Hz −3 dB), presence (2.5 / 4 kHz +4 dB) and air (+2 dB @ 10 kHz) boosts by tone, downward expander below −50 dBFS (3:1, ≤ 18 dB), 3:1 soft-knee compressor above −24 dBFS, +3 dB make-up, mix |
stereo_width |
mid/side width 0–200 % (100 % = identity, 0 % = mono) |
graphic_eq_10/20/30 |
constant-Q peaking bands at octave / half-octave / third-octave ISO centres (Q from the bandwidth), ±24 dB, master gain |
parametric_eq_full |
Premiere's Parametric Equalizer: master gain, high-pass and low-pass (12–48 dB/oct Butterworth cascades), low shelf, 5 peaking bands, high shelf |
notch_filter |
6 constant-bandwidth cuts (FilterType::Cut = 1 + (g − 1)·BP: the width stays f/Q however deep), Narrow / Very Narrow / Super Narrow |
scientific_filter |
Bessel / Butterworth / Chebyshev I / elliptic, LP / HP / BP / BS, order 1–12 (design: analog prototype → frequency transform at pre-warped edges → bilinear → biquads; elliptic via Landen-transformation Jacobi functions after Orfanidis) |
fft_filter |
8-point gain curve (log-frequency, linear or smoothstep), applied zero-phase by √Hann STFT at 75 % overlap (latency = FFT size) |
dynamics_rack |
Dynamics: auto gate (hold) → expander → compressor (auto make-up) → limiter (soft clip), stereo-linked; transfer_db gives the static curve |
multiband_compressor |
4 bands with LR4 crossovers and all-pass phase compensation (bands sum to an all-pass: flat magnitude), per-band threshold/ratio/attack/release/gain/solo/bypass, output limiter, channel link |
tube_compressor |
RMS detector, 10 dB soft knee, program-dependent release, gentle tanh(x/2)·2 saturation |
chorus_flanger |
chorus (3 voices, 15–35 ms) or flanger (0.3–5 ms, feedback), transience emphasis, mix |
flanger |
initial/final delay sweep, stereo phasing, ±feedback, inverted / special / sinusoidal modes |
phaser |
2–12 swept first-order all-passes, depth (octaves below the upper frequency), feedback, stereo phase difference |
analog_delay |
Tape / Tape+Tube / Analog: low-passed, saturating feedback (bounded up to 200 %), trash, stereo spread |
multitap_delay |
4 independent feedback taps, levels, mix |
convolution_reverb |
uniformly partitioned overlap-save convolution (256-sample partitions, latency 256) with 7 impulses generated here (noise with frequency-dependent exponential decay + early reflections, unit energy); room size (resampling), LF/HF damping, pre-delay, width |
surround_reverb |
early reflections + FDN late field, centre input, low/high cut, width, dry/wet (stereo rendering) |
click_remover |
LPC residual outlier detection (k·σ, σ from the median), AR forward/backward interpolation of ≤ 64-sample regions (latency 640) |
channel_mixer, stereo_expander, mute |
2×2 matrix with inversion; mid/side expand + centre pan; ramped mute |
distortion, guitar_suite |
waveshaper (soft/hard/tube/foldback, asymmetry, tone); compressor → distortion → amp/cabinet voicing → filter |
mastering, vocal_enhancer |
EQ → reverb → exciter → widener → loudness maximiser (transparent at defaults); fixed Male/Female/Music voicings |
binauralizer |
±angle virtual speakers through a spherical-head model (Woodworth ITD + Brown–Duda head shadow) |
ambisonics_panner |
rotates a ±30° source pair (pan/tilt/roll) and re-pans to stereo; identity at 0/0/0 |
loudness_meter |
pass-through BS.1770 meter (LoudnessMeterFx::meter) |
AudioEffect::response_db(freq) (EQs and filters) and transfer_db(band, input_db) (dynamics)
expose the analytic curves for the graphical effect editors. Modulation effects start their LFO
at the beginning of a chain, so a random-access render may differ in LFO phase from continuous
playback.
effects::premiere_tests checks every effect: neutral settings are (delayed) identity, output is
deterministic and latency constant (impulse lands at latency()), plus a level / frequency-response
test on generated tones, noise or impulses and a realtime-factor floor.
Essential Sound builds on these (Repair: denoise, high-pass via parametric_eq, dehum,
deesser, dereverb; Clarity: compressor, simple_eq, speech_enhance; Creative: reverb,
stereo_width).
Enhance Speech decision. No machine-learning model is bundled: we found no openly licensed
speech-enhancement model that is also practical to ship here (pure Rust, wasm, asset rules).
DeepFilterNet (MIT/Apache-2.0, with a Rust implementation on tract) is the candidate for a future
optional integration behind a trait outside this dependency-free crate, once its weights' licence and
size are vetted as an asset. Until then speech_enhance is the DSP chain above.
Measured on synthetic signals (effects::essential::tests, 48 kHz):
| Effect | Signal | Result |
|---|---|---|
deesser |
150 Hz harmonic voice + 5–10 kHz noise bursts | sibilance −5.1 dB (8 dB max) / −9.8 dB (16 dB max) during bursts; voice band ±0.01 dB; non-sibilant passages ±0.001 dB |
dereverb (100 %) |
harmonic syllables (100 ms / 400 ms gaps) + Polack-model late reverb, RT60 0.8 s | tail-to-burst energy −5.0 → −10.1 dB (5.0 dB better); burst energy −1.7 dB |
speech_enhance |
voice + fricative noise syllables over −60 dBFS noise | presence/mud tilt +5.5 dB; noise between syllables −8.7 dB; overall level +1.0 dB |
| Dialogue chain | denoise + parametric_eq (2 HP bands) + dehum + deesser + dereverb + compressor + simple_eq + speech_enhance + reverb, 60 s stereo |
1.36 s = 44× realtime on one core (release, Apple Silicon; cargo test --release -p filmcraft-audio-dsp dialogue_chain_realtime -- --ignored --nocapture) |
Ducking analysis (ducking)
Pure functions for Essential Sound auto-ducking:
envelope_db(channels, sample_rate, hop_s, window_s): centred-window RMS level per hop (dBFS, channel-averaged power, floor −120).activity(env_db, hop_s, threshold_db, min_on_s, bridge_s): regions above the threshold; gaps shorter thanbridge_sare merged first, then regions shorter thanmin_on_sare dropped.sensitivity_threshold_db(s): Sensitivity 0–10 → −20 − 4·s dBFS.duck_keyframes(regions, clip_start, clip_end, base_db, reduce_db, fade_s): (time, dB) volume keyframes — ramp down overfade_sbefore each region, hold, ramp up after; overlapping ramps merge; clamped to the clip (a ramp cut by an edge gets the ramp's value at the edge); strictly increasing times.
Audio synchronisation (sync)
find_offset(a, b, sample_rate, &SyncOptions) returns the lag L with b[n] ≈ g·a[n + L], a
sub-sample estimate, and two trust figures (confidence = peak / RMS of the correlation,
distinct = peak / next peak > 50 ms away; reliable() = ≥ 8 and ≥ 1.5).
- Coarse: DC removal + normalisation, Blackman-windowed-sinc decimation to ≤ 8 kHz (further for
long recordings, so the transform stays ≤ 4 M points), GCC-PHAT-β (
R = A·B*/|A·B*|^0.75, 40 Hz – 0.4·rate band) with both signals packed into one complex FFT. - Refine: the same correlation at the full rate on the loudest common window (≤ 2¹⁷ samples), ±2 coarse steps around the coarse peak, parabolic interpolation.
- Tested within one sample on speech-like multi-microphone signals (gains −20…+10 dB, SNR down to
3 dB, coloured microphones, fractional delays: worst 0.4 samples integer / 0.11 samples
sub-sample), a 0 dB SNR room with a strong echo, inverted polarity, unrelated signals
(unreliable) and
max_lag. Two 10-minute recordings: 2.4 s (release, one core).
Known limits
- Effects assume ≤ the channel count they were built with; extra channels pass through (channel-generic gain effects process all of them).
- DeNoise adapts continuously; a perfectly stationary tone is (by design) treated as noise after ~1.5 s. There is no explicit "learn noise print" mode yet.
- Filter type / choice changes switch immediately (continuous parameters are smoothed).