|
|
||
|---|---|---|
| .. | ||
| src | ||
| tests | ||
| Cargo.toml | ||
| README.md | ||
README.md
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).