GitHub - OxideAV/oxideav-audio-filter: Volume / NoiseGate / Echo / Resample / Spectrogram audio filters for oxideav · GitHub
Skip to content

About

Volume / NoiseGate / Echo / Resample / Spectrogram audio filters for oxideav

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

94 Commits

Folders and files

Repository files navigation

oxideav-audio-filter

CI crates.io docs.rs License: MIT

Pure-Rust audio filters (volume, gate, echo, resample, spectrogram) for oxideav

Part of the oxideav framework — a pure-Rust media transcoding and streaming stack. Codec, container, and filter crates are implemented from the spec (no C codec libraries linked or wrapped, no *-sys crates). Optional hardware-engine crates (oxideav-videotoolbox / -audiotoolbox / -vaapi / -vdpau / -nvidia / -vulkan-video) bridge to OS APIs via runtime dlsym; pass --no-hwaccel (or omit the hwaccel feature) to opt out.

Filters

  • volume — linear / dB gain with hard clipping

  • noise_gate — threshold + attack/release/hold envelope gate; optional two-threshold hysteresis (open_db / close_db, sticky latch eliminates chatter when the drive dances around the threshold) and Hermite-smoothstep soft-knee (knee_db, C¹-continuous transition with 0.5 gain at the knee centre). NoiseGate::new keeps the legacy hard-knee single-threshold behaviour byte-for-byte; NoiseGate::with exposes the upgraded parameters.

  • echo — single-tap delay line with feedback and wet/dry mix

  • resample — polyphase windowed-sinc rational sample-rate conversion, implementing the ideal-bandlimited-interpolation theory of docs/audio/filter/jos-theory-of-sample-rate-conversion.html / jos-bandlimited-interpolation.html / jos-resample.html (Shannon reconstruction x(t) = Σ x(nTs)·hₛ(t − nTs), hₛ the normalised sinc, evaluated at the new sampling instants). Rational factoring L = lcm(src, dst), up = L/src phases, down = L/dst phase step; Kaiser-β8 windowed-sinc prototype low-pass with cutoff at half the lower rate (the theory's anti-alias / anti-image placement). The prototype length now scales with max(up, down) so the anti-aliasing filter stays sharp under decimation: a fixed 32-tap window leaked aliasing at ≈ −13 dB on a 48 k→16 k downsample (an above-Nyquist tone folding back into band); the ratio-scaled length rejects it ≥ 40 dB end-to-end. Closed-form prototype_response_db(f) evaluator (DTFT of the windowed-sinc prototype, 0 dB DC reference) plus passband_edge_hz / up_factor / down_factor / src_rate / dst_rate accessors expose the design for verification. Frequency-response identities asserted across integer-decimate, integer-interpolate, and the coprime 48 k↔44.1 k ratios: passband flat to ±0.05 dB out to 0.8·edge, exactly −6.02 dB at the band edge (half-band point), ≥ 70 dB stop-band rejection by 1.2·edge, monotone transition with no passband bump, plus the up·src = down·dst = L / coprime-up,down LCM invariant and DC-preservation through integer interpolation.

  • spectrogram — STFT renderer with PNG / video output

  • downmix — channel-layout fold-down (LoRo / LtRt / Average / Binaural)

  • biquad — eleven-config IIR EQ family with DF-II-T f64 state, covering the complete staged EQ-cookbook catalogue (docs/audio/filter/audio-eq-cookbook.html): LPF / HPF / BPF in both constant-skirt (peak gain = Q) and constant-0-dB-peak variants / notch / peaking / low + high shelves in both Q- and shelf-slope-(S)-parameterised forms (α = (sinω/2)·√((A+1/A)(1/S−1)+2); S = 1 → steepest monotonic transition, equal to Q = 1/√2 for any gain) / all-pass. Closed-form magnitude_response_db(f, fs) evaluator for response plotting and design verification. Frequency-response identities asserted analytically to ≤ 1e-9 dB: constant-peak BPF = 0 dB at fc for Q ∈ {0.3 … 32}; skirt − peak = 20·log10(Q) dB at every frequency; shelf midpoint = gain/2 dB exactly; shelf DC/Nyquist plateaus ± 0.01 dB; S = 1 monotonic over a 1/12-octave 20 Hz–20 kHz sweep while S = 2 overshoots ±1.38 dB

  • compressor — soft-knee compressor with attack/release follower, make-up gain, and a selectable peak / RMS detector. Compressor::new keeps the legacy peak-sensing behaviour byte-for-byte; Compressor::with_detector selects EnvelopeMode::Rms (power-averaged, runs the one-pole follower on x² and reports √env) for the perceptually-relaxed "power measurement" sensing described in the docs/audio/filter/ dynamic-range reference's "Peak vs RMS sensing" section — a sine drives the RMS detector off its true RMS (−3.01 dB below the peak) so the gain reduction tracks loudness, not peaks. Orthogonally, a detector topology (DetectorTopology, Compressor::with_topology) selects the detector placement per the docs/audio/filter/ dynamic-range reference's "Design" section: FeedForward (default) senses the input (the modern layout "used today in most compressors"); Feedback senses the previous output y[n-1] (the earlier vintage layout where "the signal level was measured after the amplifier"), a self-stabilising loop whose gain reduction settles softer and more program-dependent than feed-forward (steady-state GR strictly smaller in magnitude for the same threshold/ratio — the gentle opto / vari-mu "knee"). prev_out carry keeps y[n-1] continuous across processing blocks. Registry detector key ("peak" default / "rms") and topology key ("feedforward" default / "feedback")

  • parallel_compressor — parallel ("New York" / "Motown") compression (docs/audio/filter/'s dynamic-range reference §7): the dry input is split, one copy passes through untouched and the other through a soft-knee compressor, and the two are summed (y = x·10^(dry_db/20) + comp(x)·10^(wet_db/20)). Distinct from the in-series compressor: the reference calls it "a form of upward compression that facilitates dynamic control without significant audible side effects" — "combining a linear signal with a compressor … results in low-level detail enhancement without any peak reduction; the compressor significantly adds to the combined gain at low levels only." At a quiet passage the wet copy is un-reduced and (with the trims at unity) the sum reads +6 dB; at a loud peak the wet copy is squashed flat and the dry path dominates so the peak survives — quiet detail lifted, transients preserved. dry_db / wet_db trim the two paths before the sum (the classic recipe pulls wet_db under the dry signal); the inner compressor's make-up gain is folded into wet_db. Same soft-knee static curve + attack/release one-pole follower + selectable peak / RMS detector as compressor, peak-linked across channels so both paths' stereo image is preserved. ParallelCompressor::new (unity trims, peak detector) / with_mix (explicit dry_db / wet_db / detector). Registry entry "parallel_compressor" accepts JSON threshold_db / ratio / attack_ms / release_ms / knee_db / dry_db / wet_db / detector keys.

  • limiter — brickwall peak limiter with optional look-ahead (0..=2048 samples)

  • dc_blocker — single-pole IIR HPF (y[n] = x[n] - x[n-1] + R·y[n-1]) for DC-offset removal

  • stereo_widener — Mid/Side width control with width ∈ [0, 2] (0 = mono, 1 = bypass, 2 = wide)

  • crossfeed — headphone crossfeed: each channel bleeds into the other delayed by the interaural time difference (default 300 µs, fractional-sample exact via the in-crate FracDelayLine), attenuated (level_db, default −6 dB), and head-shadow low-passed (one-pole, default 700 Hz). Direct/cross gains satisfy d + g/(1+g) · … = 1 so mono content passes at unity at low frequencies. Narrows hard-panned material the way loudspeakers in a room do — the direction-preserving counterpart to the M/S-based stereo_widener/stereo_imager. Stereo only; other channel counts pass through. Registry keys level_db / cutoff_hz / delay_us

  • reverb — Schroeder algorithmic reverb (4 parallel combs ║ 2 serial all-passes) with room_size / damping / wet / dry knobs

  • tremolo — sine-LFO amplitude modulation with rate / depth knobs

  • loudness_itu — ITU-R BS.1770-4 / EBU R128 integrated loudness meter (LUFS) with K-weighting + channel weights

  • pitch_shift — time-domain SOLA-style granular pitch shifter (-12..=+12 semitones, no FFT)

  • chorus — 1..=4 LFO-modulated short delay taps with phase-offset voices

  • flanger — short-delay feedback comb (1..=15 ms, swept resonance)

  • phaser — N (2..=12) cascaded first-order all-pass sections with LFO-swept cutoffs

  • equalizer — builder over N Biquad sections in series (low/high-pass, both BPF variants, notch, peaking, Q- and slope-parameterised shelves, all-pass)

  • white_noise / pink_noise / brown_noise — splitmix64-seeded generators with flat / 1/f / 1/f² spectra

  • silence_detector — pass-through observer with attack/release RMS envelope + hold-threshold flag

  • vibrato — LFO-modulated fractional-delay pitch shift (pitch counterpart to tremolo)

  • auto_pan — LFO-modulated L/R stereo placement (mono-sum conservative pan law)

  • bitcrusher — bit-depth quantisation + sample-and-hold rate reduction

  • tape_saturation — tanh soft-clip waveshaper with asymmetric drive (odd + even harmonics)

  • hum_filter — cascaded narrow notches at 50/60 Hz mains fundamental + harmonics

  • crossover — two-way LPF/HPF band split (output channels = 2× input); Butterworth-2 (12 dB/oct, default, byte-for-byte legacy via Crossover::new/butterworth) or CrossoverSlope::LinkwitzRiley4 (24 dB/oct, two cascaded Butterworth-2 per band → −6 dB at fc, in-phase, magnitude-flat |low + high| = 1 reconstruction). Registry slope key ("lr4")

  • mid_side — explicit L/R ↔ M/S transcoder (stateless, exact roundtrip)

  • envelope_follower — peak / RMS amplitude envelope detector (pass-through, observe via API)

  • de_esser — split-band downward compressor targeting sibilance

  • wah — LFO-swept resonant band-pass (classic wah log sweep)

  • octave_doubler — Octavia-style full-wave-rectifier octave-up layer with DC block

  • adaptive_noise_gate — gate with self-learned asymmetric noise floor

  • exciter — high-band tanh saturation enhancer (HPF + dry mix; adds "air" without raising broadband level)

  • multiband_compressor — three-band (low / mid / high) independent compression with Butterworth-2 crossovers (default 250 Hz / 2500 Hz)

  • stereo_imager — frequency-dependent stereo width via M/S side split (mono bass + wide treble preset)

  • talkbox — LFO-morphed parallel formant band-pass bank (vowel filter, 6 vowels, no FFT)

  • transient_designer — two-envelope (fast vs slow) attack + sustain shaper (attack/sustain ∈ [-1, +1])

  • ducker — internally-keyed sidechain compressor with safety-floored gain reduction (broadcast voice-over duck)

  • gain_normalizer — slow programme-level AGC with silence-freeze integrator (target_db, default ~1 s response)

  • freq_shifter — Hilbert-FIR SSB frequency shifter (additive Δf in Hz, harmonic-destroying)

  • ring_modulator — sine-carrier double-sideband suppressed-carrier AM (y = x · cos(2π·fc·n/fs), audible carrier; produces mirror sidebands at f ± fc, Dalek / bell timbre)

  • hard_clipper — memoryless symmetric clipping distortion (y = clamp(drive·x, ±ceiling); odd-harmonic fuzz / overdrive, distinct from the tanh soft-clip of tape_saturation)

  • slew_limiter — slope-limited smoother (Δ = x − y_prev; y = y_prev + clamp(Δ, ±s) with s = max_slew_per_sec / fs); linear-ramp response (vs the LPF's exponential), with optional asymmetric rise / fall caps. Anti-zipper / portamento / anti-pop primitive.

  • expander — proportional downward expander: gr_db = -(R − 1) · max(0, threshold_db − env_db). Distinct from noise_gate (binary open/close) and from compressor (slope on the above-threshold side). ratio = 1.0 → identity, ratio = ∞ → hard downward gate, finite ratios in between trade off between gentle noise-floor management (1.5:1 / 2:1) and aggressive expansion (4:1+). Soft-knee width smooths the threshold transition; attack/release one-pole follower; peak-linked detector across channels.

  • true_peak_detector — pass-through inter-sample peak observer reporting dBTP via 4× polyphase Kaiser-windowed FIR oversampling. Sample-domain max |x[n]| understates the band-limited reconstructed peak (canonical example: an fs/4 + π/4-phase full-scale sine reads -3.01 dBFS on the grid but reconstructs to ≈ 0 dBTP); this filter reveals the true peak by upsampling by L = 4 with a unit-DC-gain FIR (default 48 taps → 12 taps per polyphase sub-filter, Kaiser β chosen for ~100 dB stop-band) and tracking max |y| in f64. Pass-through audio (no modification); consumers read current_dbtp (last frame), max_dbtp (running), and overs (count of oversampled samples above the configurable linear threshold, default 1.0 = 0 dBTP). Distinct from limiter (sample-peak gain reduction), envelope_follower (smoothed envelope), and loudness_itu (K-weighted integrated loudness).

  • svf — Chamberlin two-integrator-loop State Variable Filter. Simultaneous LP / BP / HP / Notch taps from a single recurrence (hp = x - q·bp_prev - lp_prev; bp += f·hp; lp += f·bp), so the output mode can be reconfigured without touching state. f = 2·sin(π·f_c/f_s), q = 1/Q; per-sample modulation of cutoff is cheap (one sin on set_cutoff) and avoids the bilinear-transform coefficient resolve that a regular biquad needs — this is the canonical synth filter for envelope-/LFO-swept cutoff. State-space topology distinct from the biquad family (transfer-function DF-II-T realisation). Stable while f_c < f_s / 6 and Q ∈ [0.5, 50]; values outside that range are clamped at construction. Notch tap is hp + lp, deep at low Q (~−24 dB at Q = 0.5) and degrading as Q rises — use biquad notch for high-Q narrow-band reject.

  • median_filter — non-linear sliding-window median filter for impulse-noise (click / pop) removal. Per channel keeps an N-sample ring; output is the median of the latest N samples. Step edges spanning more than N / 2 samples pass through unaltered, but isolated impulse outliers are entirely discarded — a behaviour no linear LPF achieves. Window ∈ [1, MedianFilter::MAX_WINDOW] (= [1, 257]); default 5 (canonical click-removal value). Even windows return the mean of the two centre sorted samples. Insertion-sort over the ring (best-case O(window) on near-sorted data, steady-state common case). Joins the restoration family next to hum_filter (cyclic-mains) and dc_blocker (DC drift) — its niche is transient impulse noise instead of cyclic / DC. Registry entry "median_filter" accepts JSON window key.

  • crest_factor_meter — pass-through observer reporting peak-to-RMS ratio (crest factor) in dB over a sliding rectangular window. The crest factor is the textbook scalar that quantifies how "spiky" or transient-rich a signal is: a sine wave reads 3.01 dB, a square wave reads 0 dB, broadband noise lands near 11 dB, modern broadcast pop sits in 5..8 dB after heavy dynamic compression, sparse drum transients exceed 20 dB. Algorithm: running sum-of-squares (incremental add new sample's square, subtract leaving sample's square, with periodic per-window rebuild that bounds f64 drift on long streams) gives the windowed RMS in O(1) per sample; a monotonic-decreasing deque of (|x|, sample_index) (front always holds the active-window maximum, rear pops while incoming sample dominates, front pops when its index expires) gives the windowed max |x| in O(1) amortised per sample. Channel-linked by max so a loud transient on one channel of a split stereo bed isn't masked. Window default 400 ms (EBU R128 short-term loudness window), clamped to [0.1, 10_000] ms and additionally to [1, 192_000] samples (4 s at 48 kHz). Until the window first fills, the readout returns f32::NEG_INFINITY (and 0.0 for the linear form) so callers can branch on "not yet ready". Distinct from true_peak_detector (absolute oversampled peak only), loudness_itu (K-weighted integrated loudness only), envelope_follower (single one-pole envelope, not a ratio), and silence_detector (binary above/below threshold flag). Registry entry "crest_factor_meter" accepts JSON window_ms key.

  • stereo_correlation_meter — pass-through observer reporting the windowed Pearson correlation coefficient ρ ∈ [-1, +1] between the L and R channels. ρ = +1 for identical channels (mono content panned centre); ρ ≈ 0 for orthogonal channels (uncorrelated stereo bed); ρ = -1 for phase-inverted channels (canonical mono-fold-down cancellation hazard most downstream broadcast chains still emit). Five incremental running sums (Σx, Σy, Σx², Σy², Σxy) give the closed-form Pearson ρ = (N·Σxy − Σx·Σy) / √((N·Σx² − Σx²)·(N·Σy² − Σy²)) at O(1) per sample; per-window rebuild bounds f64 drift on long streams. Polar current_degrees() = acos(ρ)·180/π exposes the classical goniometer's 0° / 90° / 180° angular reading. Running min() latches on the worst-case phase-cancellation reading so a transiently-inverted frame survives a quieter tail. Window default 400 ms (matching crest_factor_meter, so both meters share a time axis on a display); clamped to [0.1, 10_000] ms and to [1, 192_000] samples. Stereo input only — mono and multichannel (channel count not equal to two) layouts pass through unchanged with the meter state untouched. Pearson is both mean-centred (DC offsets don't bias the metric) and scale-invariant (per-channel volume changes don't bias it). Distinct from stereo_widener / stereo_imager (both processors of the stereo image), from the single-channel meters crest_factor_meter / true_peak_detector / loudness_itu (none of which carries inter-channel phase information), and from silence_detector (single-channel binary flag). Registry entry "stereo_correlation_meter" accepts JSON window_ms key.

  • stereo_balance_meter — pass-through observer reporting the L/R energy balance B = (R_rms − L_rms)/(R_rms + L_rms) ∈ [-1, +1] over a sliding rectangular window. B = 0 centred (equal-energy channels — mono panned dead-centre or a symmetric bed), B = -1 all energy on the left (right silent), B = +1 all energy on the right, +1/3 when the right channel is twice as loud as the left, -1/3 for the mirror case. The level complement to stereo_correlation_meter (phase): Pearson correlation is mean-centred and scale-invariant, so two perfectly correlated channels at +12 dB / -12 dB still read ρ = +1 — correlation is blind to a level imbalance and balance reports exactly that discarded dimension; the two meters together pin down both axes (phase + level) of a stereo image on a shared time axis. A persistent non-zero balance flags an accidental pan offset, a channel-trim mismatch in the capture chain, one dead / intermittent channel, or a mono source mis-routed to a single leg of a stereo bus. Algorithm: per-channel f32 ring of N samples plus an f64 running sum-of-squares Q = Σ x² updated incrementally (Q ← Q + x_new² − x_old²) at O(1) per sample; windowed RMS is √(Q/N) and the balance follows in closed form. Periodic per-window rebuild of both sums bounds f64 drift on long streams (same cadence as crest_factor_meter / stereo_correlation_meter). Stereo input only — mono and multichannel (channel count not equal to two) layouts pass through unchanged with the meter state untouched. Window default 400 ms (matching the other R128-aligned meters); clamped to [0.1, 10_000] ms and to [1, 192_000] samples. Until the window first fills the readout returns the neutral 0.0 (centred); both-channels-silent over the window is likewise undefined and reads 0.0. Consumers poll current(), rms_left() / rms_right(), max_abs() / reset_max(), samples_seen(). Registry entry "stereo_balance_meter" accepts JSON window_ms key. Distinct from stereo_correlation_meter (phase, level-blind), stereo_widener / stereo_imager (image processors), and the single-channel crest_factor_meter / true_peak_detector / loudness_itu / dc_offset_meter (none of which compares the two channels' energy).

  • comb_filter — tunable single-tap comb (feedforward FIR y[n] = x[n] + g·x[n−D] or feedback IIR y[n] = x[n] + g·damped(y[n−D])) with an optional one-pole LP damped(·) in the feedback path. The FIR form yields D + 1 evenly-spaced peaks (or notches, with g < 0) in [0, fs/2] with magnitude √(1 + g² + 2g·cos(ωD)) — useful for stereo widener side-paths, decorrelation networks, and frequency-domain dereverberation. The IIR form yields resonant peaks of 1/(1−g) at fs/D and its harmonics with −3 dB bandwidth ≈ (1 − g)·fs/(πD) — the canonical building block for Karplus-Strong plucked-string synthesis ([CombFilter::karplus_strong] picks D = round(fs / freq_hz) and sets damping to 0.5 for the classic decaying-overtone string tone). Feedback gain clamped to [−0.999, +0.999] and damping clamped to [0, 0.999] at construction so the loop is strictly stable. Delay specifiable either as exact samples (sample-rate-dependent) or in milliseconds (rate-portable, resolved against the first stream's sample rate); both clamped to [1, MAX_DELAY_SAMPLES = 192_000]. Per-channel ring buffers + per-channel LP state; channels do not cross-talk. Registry entry "comb_filter" accepts JSON mode ("feedforward" / "feedback" / "karplus_strong"), delay_ms / delay_samples, gain, damping, and (for the karplus_strong shortcut) freq_hz + decay. Distinct from echo (audible tens-of-ms repetition, wet/dry mix), flanger (LFO-modulated delay length on top of a feedback comb), and reverb (four parallel combs + two serial all-passes presented as a room simulator).

  • zero_crossing_rate — pass-through observer reporting how often the signal crosses zero per unit time over a sliding rectangular window.

  • dc_offset_meter — pass-through observer reporting the per-channel running mean (DC component) over a sliding rectangular window. Where dc_blocker removes a DC bias with a single-pole HPF, this meter reports it without altering the signal. A non-zero mean reveals a low-frequency bias — preamp trim drift, ADC quantiser-midpoint offset, sagging field-recorder rail, accidental unipolar synthesis bus — that leaves the speaker cone parked off-centre and wastes a chunk of available headroom on a constant push. Algorithm: per-channel f32 ring of N samples plus an f64 running sum S = Σ x updated incrementally (S ← S + x_new − x_old) at O(1) per sample, with a periodic per-window rebuild bounding f64 drift on long streams (same cadence as crest_factor_meter / stereo_correlation_meter). Channel-link picks the per-channel mean with largest |·|, sign preserved, so a +0.05 channel and a −0.20 channel report −0.20, and equal-and-opposite biases (+0.1 / −0.1) do not cancel in the readout. Window default 400 ms (matching the other R128-aligned meters); clamped to [0.1, 10_000] ms and additionally to [1, 192_000] samples. Until the window first fills the readout returns 0.0 (linear) / NEG_INFINITY (dB) so callers can branch on "not yet ready". Registry entry "dc_offset_meter" accepts JSON window_ms key. Distinct from crest_factor_meter (insensitive to DC — peak and RMS grow together), loudness_itu (BS.1770 RLB pre-filter already wipes DC at 38 Hz), envelope_follower / silence_detector (both rectify to |x| early), stereo_correlation_meter (Pearson is mean-centred by construction), and zero_crossing_rate (DC bias is only indirectly visible because it shifts the sign-change axis). The zero-crossing rate is a textbook scalar that counts sign(x[n]) != sign(x[n-1]) events over a window of N adjacent-sample pairs, exposed either as a per-sample fraction ∈ [0, 1] or — more usefully — in crossings per second (Hz) as count · fs / N. It is a cheap proxy for the spectral centroid: a pure sine at f₀ produces ≈ 2·f₀ crossings/sec, broadband noise produces a stable elevated rate, an alternating-sign signal saturates at fs, and a constant DC signal sits at 0. Used widely in voiced/unvoiced speech classification (vowels low, fricatives high), tone-pitch proxy, percussion vs harmonic separation, and silence/noise-floor gating. Algorithm: per-channel ring of N boolean crossing flags + one-sample latch holding the previously-seen sample; each new sample forms a (prev, x) pair, the resulting flag is pushed into the ring and the flag rotating out is subtracted from the running count — all O(1) per sample, no f64 drift to manage. Channel-link by max. Sign convention reduced to {-1, +1} with sign(0.0) = +1 so a run of bit-exact zeros doesn't manufacture phantom crossings. Window default 25 ms (canonical short-time speech frame), clamped to [0.1, 10_000] ms and additionally to [1, 192_000] samples. Until the flag-ring first fills the readout returns NAN (rate) / 0.0 (fraction) / 0 (count) so callers can branch on "not yet ready". Registry entry "zero_crossing_rate" accepts JSON window_ms key. Distinct from the energy/amplitude observers — crest_factor_meter (peak-to-RMS), true_peak_detector (oversampled inter-sample peak), loudness_itu (K-weighted integrated loudness), envelope_follower (smoothed envelope), silence_detector (binary RMS threshold flag) — none of which exposes the per-sample sign sequence.

  • pre_emphasis / de_emphasis — paired analog-broadcast / tape / FM record EQ shelving filters. The cascade pre · de is the identity by construction (within f64 round-off; ≤ −60 dB RMS error per sample on white-noise round-trip). Shared EmphasisCurve enum covers Fm50us (European FM), Fm75us (North-American FM), J17 (ITU-R J.17 voice-band), Custom { tau_s } (user-specified single-time-constant), and Riaa3180_318_75 (phonograph / vinyl second-order three-time-constant curve). Coefficients are derived in-source by bilinear transform of H_pre(s) = (1 + s·τ) / (1 + s·τ/G) and its inverse, with the asymptotic shelf-top gain G (default 10, capped at 1000) bounding HF boost so the discrete transfer stays well-behaved at Nyquist. First-order (b₀, b₁, a₁) direct-form-I IIR per channel for FM / J.17 / Custom; second-order (b₀, b₁, b₂, a₁, a₂) for RIAA. Per-channel state, reset(), set_sample_rate re-derives coefficients. Registry entries "pre_emphasis" / "de_emphasis" accept JSON curve key ("fm_50us" / "fm_75us" / "j17" / "riaa" / "custom" + "tau_us" for the custom case) and g for the asymptotic gain. 15 hand-derived unit tests per filter including DC-gain-unity (both first- and second-order), Nyquist-asymptote-equals-G, derived corner frequencies (3183 Hz @ 50 µs, 2122 Hz @ 75 µs), +20 dB/decade slope-between-corners, channel-independence, streaming continuity, sample-rate-change re-derivation, alias-curve identity (J.17 ≡ FM-50, Custom ≡ named); plus three cascade-identity tests (FM-50 / FM-75 / RIAA) proving the inverse property.

  • dither — word-length-reduction requantizer with TPDF / RPDF dither and first- / second-order error-feedback noise shaping; the transparency-grade end-of-chain primitive for a float pipeline feeding a fixed-point encode. Rounds every sample onto the exact bits-wide signed mid-tread code grid (Δ = 2^(1−bits), bits ∈ [2, 24], default 16 → outputs are exact 16-bit PCM codes) while decorrelating the rounding error from the programme: bare rounding produces correlated error (harmonic distortion, and a deadband that erases any sine under Δ/2 outright), RPDF dither (uniform, pk-pk Δ) zeroes the error mean but leaves its variance signal-dependent, TPDF (triangular, pk-pk 2Δ, sum of two RPDF draws — the default) renders mean and variance signal-independent at constant Δ²/4 total (+4.77 dB over bare rounding, flat and benign). Optional error feedback shapes the noise through NTF = 1 − z⁻¹ (+6 dB/oct tilt, DC zero, total power ×2) or NTF = (1 − z⁻¹)² (+12 dB/oct, double DC zero, ×6) — noise leaves the ear's sensitive low/mid band for the top octave; the closed-form Parseval gains (×1 / ×2 / ×6) and the spectral tilt are both asserted by FFT in the unit tests, alongside the deadband-rescue pair (a 0.4Δ sine quantises to exact silence undithered but its fundamental survives ≈ 25× above the noise floor with TPDF). splitmix64 dither PRNG, seedable for bit-reproducible output; per-channel feedback state; channels draw independent dither. Distinct from bitcrusher (creative degradation: no dither, no shaping, aliasing sample-and-hold). Registry entry "dither" accepts JSON bits / mode ("none"/"rpdf"/"tpdf") / shaping ("off"/"first"/"second") / seed keys.

  • upward_compressor — the complementary dynamics quadrant to compressor / expander: boosts quiet signal below the threshold by (1 − 1/R) of each dB of under-shoot (capped at range_db), leaving peaks above the threshold untouched. The four-quadrant taxonomy from docs/audio/filter/'s dynamic-range reference is: above-threshold reduce = downward compression (compressor); below-threshold reduce = upward compression (this); below-threshold widen = downward expansion (expander); above-threshold widen = upward expansion. Where downward compression turns the loud parts down and the engineer adds make-up gain, upward compression turns the quiet parts up — narrowing the dynamic range from the bottom while preserving transient peaks and perceived punch. Static curve: under = max(0, threshold_db − env_db); boost_db = min(range_db, (1 − 1/R)·under). ratio = 1.0 → identity; ratio = ∞ → recover = 1 (lift quiet signal up to the threshold, subject to the range cap); finite ratios recover a fraction (2:1 → half each dB of under-shoot, 4:1 → three-quarters). The range_db cap (default 24 dB via UpwardCompressor::upward) bounds noise-floor amplification as the input approaches silence; range_db = 0 is identity. Soft-knee width centred on the threshold uses the same C¹ quadratic blend as compressor / expander. One-pole peak-linked detector (attack on rising, release on falling) shared across channels so loud peaks on one channel hold the boost off both, keeping the stereo image. Registry entry "upward_compressor" accepts JSON threshold_db / ratio / attack_ms / release_ms / knee_db / range_db keys. Distinct from compressor (reduces above threshold), expander (attenuates below threshold; this boosts), and gain_normalizer (slow whole-programme AGC, not per-sample peak-preserving).

  • upward_expander — the fourth and final quadrant of the dynamics taxonomy, completing compressor / upward_compressor / expander: boosts loud signal above the threshold by (R − 1) of each dB of over-shoot (boost_db = min(range_db, (R − 1)·over), capped at range_db), leaving quiet signal below the threshold untouched. The four-quadrant taxonomy from docs/audio/filter/'s dynamic-range reference ("upward expansion makes the louder sounds above the threshold even louder"): above-threshold reduce = downward compression (compressor); above-threshold widen = upward expansion (this); below-threshold reduce = upward compression (upward_compressor); below-threshold widen = downward expansion (expander). Widens the dynamic range from the top — re-opening transients an over-compressed master has flattened, or accentuating the swell of a crescendo. ratio = 1.0 → identity; ratio = 2:1 doubles each dB of over-shoot; the range_db cap (default 12 dB via UpwardExpander::upward) bounds peak amplification so an unbounded slope cannot drive the loudest peaks past full-scale; range_db = 0 is identity. C¹ soft-knee quadratic blend centred on the threshold (the above-threshold mirror of upward_compressor's knee). One-pole peak-linked detector (attack on rising, release on falling) shared across channels so a loud peak on one channel holds the boost on across both, preserving the stereo image. Registry entry "upward_expander" accepts JSON threshold_db / ratio / attack_ms / release_ms / knee_db / range_db keys. Distinct from compressor (reduces above threshold), upward_compressor (boosts below threshold), expander (attenuates below threshold), and transient_designer (two-envelope attack/sustain shaper, not a static-curve level-dependent boost).

DSP primitives

  • window (WindowFunction) — reusable FIR analysis-window catalogue gathering the full closed-form family from docs/audio/filter/wikipedia-window-function.html in one place (previously the spectrogram hard-coded a three-case Hann / Hamming / Blackman switch and the resampler / true-peak detector each grew their own one-off Kaiser taper). Variants: Rectangular, Triangular (Bartlett, 1st-order B-spline), Welch (single parabolic section w[n] = 1 − ((n − N/2)/(N/2))², endpoint-nulling — the canonical window of Welch's periodogram-averaging PSD estimate), Parzen (4th-order B-spline / de la Vallée Poussin, the smoothest piecewise-cubic polynomial taper, 2·(1/L)³ endpoints), Hann, Hamming (optimal a = [0.53836, 0.46164]), Blackman (classic α = 0.16), BlackmanExact (rational 7938/18608 …, non-nulling endpoints), Nuttall, BlackmanNuttall, BlackmanHarris, FlatTop (five-term cosine sum, minimal scalloping loss, partially negative), Sine / cosine / half-sine, Lanczos (sinc(2n/N − 1)), and the parameterised adjustable windows Gaussian(σ) (σ ≤ 0.5), Tukey(α) (α = 0 ≡ rectangular, α = 1 ≡ Hann), Kaiser(β) (I₀(β·√(1 − r²)) / I₀(β), local f64 Bessel-I₀ series). General alternating cosine-sum form w[n] = a0 − a1·cos(2πn/N) + a2·cos(4πn/N) − … on the symmetric N = L − 1 convention (matching the spectrogram's existing denom = n − 1). API: value(n, len) / generate(len) / generate_f32(len) plus the coherent_gain (DC amplitude correction) and equivalent_noise_bandwidth (ENBW in DFT bins; rectangular = 1, Hann → 1.5, Blackman / flat-top progressively wider) metrics. The polynomial B-spline family widens its ENBW with spline order: rectangular 1.0 < Welch ≈ 1.20 < Triangular ≈ 1.34 < Parzen ≈ 1.92. Pure utility — not an AudioFilter, no registry entry.
  • frac_delay (FracDelayLine) — per-channel ring buffer read at arbitrary fractional (between-sample) delays, with the reconstruction kernel chosen at construction. This is the bandlimited-interpolation problem stated in docs/audio/filter/jos-bandlimited-interpolation.html / jos-theory-of-sample-rate-conversion.html / jos-resample.html: a signal bandlimited to Fs/2 is, by Shannon's sampling theorem, uniquely recoverable at any continuous time p from its samples (x(t) = Σ x(nTs)·hₛ(t − nTs) with hₛ the normalised sinc). Time-varying delay effects (chorus / flanger / vibrato / pitch_shift, Doppler, comb resonators) all need such reads and currently each hand-roll a two-tap linear blend; this primitive gathers the kernel family in one place. Interp variants: Linear (two-tap (1 − f)·x[i] + f·x[i+1]; cheapest, treble roll-off deepest at f = 0.5); Hermite (four-tap C¹ Catmull–Rom cubic — centred-difference tangents, markedly flatter passband for one extra tap pair); Lagrange(n) (the order-n polynomial through the n + 1 nearest taps via the product Lagrange basis Lⱼ(x) = Πₘ≠ⱼ (x − xₘ)/(xⱼ − xₘ); n = 1 reduces to Linear, n = 3 is the cubic-Lagrange the reference relates to a truncated sinc, clamped to [1, 8] since high-order equally-spaced interpolation is ill-conditioned); and Sinc { half_taps, beta } (the most faithful — windowed-sinc reconstruction Σ x[k]·sinc(p − k) truncated to 2·half_taps neighbours and Kaiser-tapered via window::Window::Kaiser to suppress truncation ripple, DC-normalised so a constant input reconstructs exactly, half_taps clamped to [1, 32]). Newest-boundary reads use edge extension (clamp to the most recent sample) so a kernel reaching one or two taps past the head degrades gracefully instead of injecting a spurious zero; max_delay() reports the largest delay readable without the kernel running off the buffered history (capacity − 1 − reach, where reach grows with kernel width). All four kernels reproduce a constant DC and a pure integer delay exactly; Lagrange(3) is verified exact on a quadratic; Sinc(16, β=9) tracks an 8 kHz sine ≥ 4× more faithfully than linear at the worst-case f = 0.5. Pure utility — not an AudioFilter, no registry entry; mirrors the window catalogue.

Usage

[dependencies]
oxideav-audio-filter = "0.0"

Benchmarks

Criterion harness benches/filters.rs measures the per-sample DSP cost of seven representative filter families on deterministic xorshift32-synthesised PCM:

  • biquad_lpf — single second-order IIR (DF-II-T, f64 state), stereo F32 @ 48 kHz
  • equalizer_3band — low-shelf + peaking + high-shelf cascade, stereo F32 @ 48 kHz
  • loudness_itu — BS.1770-4 K-weighting + RLB HPF + per-channel mean-square, stereo F32 @ 48 kHz
  • compressor — peak-detector soft-knee compressor, stereo F32 @ 48 kHz
  • reverb — Schroeder 4 combs ║ 2 all-passes, stereo F32 @ 48 kHz
  • resample_44k1_48k — polyphase windowed-sinc rate conversion, mono F32
  • true_peak_4x — 4× polyphase Kaiser-FIR inter-sample peak detection, stereo F32 @ 48 kHz

Every PCM input is built once outside the timed region; the bench body exercises only the filter's public AudioFilter::process / process_in_place path. Run with:

cargo bench -p oxideav-audio-filter --bench filters

License

MIT — see LICENSE.

About

Volume / NoiseGate / Echo / Resample / Spectrogram audio filters for oxideav

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages