effectcraft/crates/effects/src/audio_fx.rs

683 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Audio effects (Effect > Audio): Backwards, Bass & Treble, Delay, Flange & Chorus,
//! High-Low Pass, Modulator, Parametric EQ, Reverb, Stereo Mixer and Tone.
//!
//! They are ordinary effect instances on a layer (so their parameters animate and show in
//! Effect Controls), pass video through unchanged, and are applied to the layer's audio by the
//! composition mixdown (`effectcraft_render::audio`), which preview playback and export share.
//!
//! The mixdown renders in short blocks. To keep the result independent of block boundaries
//! every effect is stateless across calls: the mixer feeds [`preroll`] seconds of the layer's
//! audio before the block, runs the chain from a cleared state and keeps the tail.
//!
//! DSP is textbook: second-order sections use the public RBJ "Audio EQ Cookbook" formulas;
//! Reverb is a Schroeder/Moorer network (parallel low-pass feedback combs into series
//! all-passes, as in the published Freeverb description); chorus/flange/vibrato are modulated
//! fractional delay lines.
use std::f64::consts::PI;
use effectcraft_keyframe::Value;
use effectcraft_project::ParamUi;
use crate::{Buf, EffectCtx, EffectSpec, Params, num, p, popup, slider};
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>) -> EffectSpec {
EffectSpec { id, name, category: "Audio", params, render: passthrough, gpu: false, float: true }
}
fn passthrough(_: &EffectCtx, b: Buf) -> Buf {
b
}
fn pct(default: f64) -> (Value, ParamUi) {
(num(default), slider(0.0, 100.0, 0.0, 100.0, 2))
}
fn pp(id: &'static str, name: &'static str, v: (Value, ParamUi)) -> crate::ParamSpec {
p(id, name, v.0, v.1)
}
pub fn specs() -> Vec<EffectSpec> {
const BANDS: [[&str; 8]; 3] = [
["band1Enable", "Band 1 Enable", "band1Frequency", "Band 1 Frequency", "band1Bandwidth", "Band 1 Bandwidth", "band1BoostCut", "Band 1 Boost/Cut"],
["band2Enable", "Band 2 Enable", "band2Frequency", "Band 2 Frequency", "band2Bandwidth", "Band 2 Bandwidth", "band2BoostCut", "Band 2 Boost/Cut"],
["band3Enable", "Band 3 Enable", "band3Frequency", "Band 3 Frequency", "band3Bandwidth", "Band 3 Bandwidth", "band3BoostCut", "Band 3 Boost/Cut"],
];
let mut eq = vec![];
for (b, (f, on)) in BANDS.iter().zip([(200.0, true), (1000.0, false), (5000.0, false)]) {
eq.push(p(b[0], b[1], Value::Bool(on), ParamUi::Checkbox));
eq.push(p(b[2], b[3], num(f), slider(20.0, 20000.0, 20.0, 20000.0, 1)));
eq.push(p(b[4], b[5], num(30.0), slider(1.0, 100.0, 1.0, 100.0, 1)));
eq.push(p(b[6], b[7], num(0.0), slider(-24.0, 24.0, -24.0, 24.0, 2)));
}
vec![
spec("ec.audio.backwards", "Backwards", vec![p("swapChannels", "Swap Channels", Value::Bool(false), ParamUi::Checkbox)]),
spec(
"ec.audio.basstreble",
"Bass & Treble",
vec![
p("bass", "Bass", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 2)),
p("treble", "Treble", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 2)),
],
),
spec(
"ec.audio.delay",
"Delay",
vec![
p("delayTime", "Delay Time (milliseconds)", num(500.0), slider(1.0, 10000.0, 1.0, 2000.0, 2)),
pp("delayAmount", "Delay Amount", pct(50.0)),
pp("feedback", "Feedback", pct(50.0)),
pp("dryOut", "Dry Out", pct(75.0)),
pp("wetOut", "Wet Out", pct(75.0)),
],
),
spec(
"ec.audio.flangechorus",
"Flange & Chorus",
vec![
p("voiceSeparationTime", "Voice Separation Time (ms)", num(3.0), slider(0.0, 100.0, 0.0, 50.0, 2)),
p("voices", "Voices", num(1.0), slider(1.0, 8.0, 1.0, 8.0, 0)),
p("modulationRate", "Modulation Rate", num(0.4), slider(0.0, 10.0, 0.0, 5.0, 2)),
pp("modulationDepth", "Modulation Depth", pct(50.0)),
p("voicePhaseChange", "Voice Phase Change", num(0.0), ParamUi::Angle),
p("invertPhase", "Invert Phase", Value::Bool(false), ParamUi::Checkbox),
p("stereoVoices", "Stereo Voices", Value::Bool(false), ParamUi::Checkbox),
pp("dryOut", "Dry Out", pct(50.0)),
pp("wetOut", "Wet Out", pct(50.0)),
],
),
spec(
"ec.audio.highlowpass",
"High-Low Pass",
vec![
p("filterOptions", "Filter Options", Value::Enum(0), popup(&["High Pass", "Low Pass"])),
p("cutoffFrequency", "Cutoff Frequency", num(2000.0), slider(20.0, 20000.0, 20.0, 20000.0, 1)),
pp("dryOut", "Dry Out", pct(0.0)),
pp("wetOut", "Wet Out", pct(100.0)),
],
),
spec(
"ec.audio.modulator",
"Modulator",
vec![
p("modulationType", "Modulation Type", Value::Enum(0), popup(&["Sine", "Triangle"])),
p("modulationRate", "Modulation Rate", num(10.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
pp("modulationDepth", "Modulation Depth", pct(5.0)),
pp("amplitudeModulation", "Amplitude Modulation", pct(0.0)),
],
),
spec("ec.audio.parametriceq", "Parametric EQ", eq),
spec(
"ec.audio.reverb",
"Reverb",
vec![
p("reverbTime", "Reverb Time (milliseconds)", num(100.0), slider(1.0, 5000.0, 1.0, 1000.0, 2)),
pp("diffusion", "Diffusion", pct(75.0)),
pp("decay", "Decay", pct(25.0)),
pp("brightness", "Brightness", pct(10.0)),
pp("dryOut", "Dry Out", pct(90.0)),
pp("wetOut", "Wet Out", pct(10.0)),
],
),
spec(
"ec.audio.stereomixer",
"Stereo Mixer",
vec![
p("leftLevel", "Left Level", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 2)),
p("rightLevel", "Right Level", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 2)),
p("leftPan", "Left Pan", num(-100.0), slider(-100.0, 100.0, -100.0, 100.0, 2)),
p("rightPan", "Right Pan", num(100.0), slider(-100.0, 100.0, -100.0, 100.0, 2)),
p("invertPhase", "Invert Phase", Value::Bool(false), ParamUi::Checkbox),
],
),
spec(
"ec.audio.tone",
"Tone",
vec![
p("waveformOptions", "Waveform Options", Value::Enum(0), popup(&["Sine", "Triangle", "Saw", "Square"])),
p("frequency1", "Frequency 1", num(440.0), slider(0.0, 22000.0, 0.0, 2000.0, 2)),
p("frequency2", "Frequency 2", num(493.88), slider(0.0, 22000.0, 0.0, 2000.0, 2)),
p("frequency3", "Frequency 3", num(587.33), slider(0.0, 22000.0, 0.0, 2000.0, 2)),
p("frequency4", "Frequency 4", num(659.26), slider(0.0, 22000.0, 0.0, 2000.0, 2)),
p("frequency5", "Frequency 5", num(783.99), slider(0.0, 22000.0, 0.0, 2000.0, 2)),
pp("level", "Level", pct(20.0)),
],
),
]
}
/// Is `id` an audio effect (applied by the mixdown rather than the compositor)?
pub fn is_audio_effect(id: &str) -> bool {
id.starts_with("ec.audio.")
}
/// Does the effect produce sound on its own (so a layer without audio becomes audible)?
pub fn generates_audio(id: &str) -> bool {
id == "ec.audio.tone"
}
/// Does the effect play the layer's audio backwards (a time mapping the mixer applies)?
pub fn reverses(id: &str) -> bool {
id == "ec.audio.backwards"
}
/// Seconds of input history the effect needs before a block to reach the state it would have
/// had when processing continuously.
pub fn preroll(id: &str, params: &Params) -> f64 {
match id {
"ec.audio.basstreble" | "ec.audio.highlowpass" | "ec.audio.parametriceq" => 0.05,
"ec.audio.flangechorus" | "ec.audio.modulator" => 0.06,
"ec.audio.delay" => {
let d = params.f("delayTime").max(1.0) / 1000.0;
let fb = (params.f("feedback") / 100.0).clamp(0.0, 0.99);
let taps = if fb > 1e-3 { (1e-3f64.ln() / fb.ln()).ceil() + 1.0 } else { 1.0 };
(d * taps).min(10.0)
}
"ec.audio.reverb" => {
let (fb, _, _, pre) = reverb_params(params);
// T60 of the longest comb (~37 ms) with loop gain fb, plus the pre-delay.
let t60 = if fb > 1e-3 { -3.0 * 0.0367 / fb.log10() } else { 0.05 };
(t60 + pre).min(4.0)
}
_ => 0.0,
}
}
/// Run audio effect `id` over interleaved stereo `buf` (L R L R …) at `rate` Hz, from a cleared
/// state. `start` is the layer time (seconds) of the first sample (oscillator phase).
pub fn process(id: &str, params: &Params, buf: &mut [f32], rate: u32, start: f64) {
if rate == 0 || buf.len() < 2 {
return;
}
let sr = rate as f64;
match id {
"ec.audio.backwards" => {
if params.b("swapChannels") {
buf.chunks_exact_mut(2).for_each(|f| f.swap(0, 1));
}
}
"ec.audio.basstreble" => {
let bass = params.f("bass") / 100.0 * 12.0;
let treble = params.f("treble") / 100.0 * 12.0;
if bass != 0.0 {
run_biquad(buf, Biquad::low_shelf(sr, 200.0, bass));
}
if treble != 0.0 {
run_biquad(buf, Biquad::high_shelf(sr, 4000.0, treble));
}
}
"ec.audio.highlowpass" => {
let fc = params.f("cutoffFrequency").clamp(1.0, sr * 0.49);
let q = std::f64::consts::FRAC_1_SQRT_2;
let bq = if params.e("filterOptions") == 1 { Biquad::low_pass(sr, fc, q) } else { Biquad::high_pass(sr, fc, q) };
let dry = (params.f("dryOut") / 100.0) as f32;
let wet = (params.f("wetOut") / 100.0) as f32;
let orig = buf.to_vec();
run_biquad(buf, bq);
mix_dry_wet(buf, &orig, dry, wet);
}
"ec.audio.parametriceq" => {
for n in 1..=3 {
if !params.b(&format!("band{n}Enable")) {
continue;
}
let g = params.f(&format!("band{n}BoostCut"));
if g == 0.0 {
continue;
}
let f = params.f(&format!("band{n}Frequency")).clamp(1.0, sr * 0.49);
let bw = (params.f(&format!("band{n}Bandwidth")) / 100.0 * 3.0).max(0.05);
run_biquad(buf, Biquad::peaking(sr, f, bw, g));
}
}
"ec.audio.delay" => delay(params, buf, sr),
"ec.audio.flangechorus" => flange_chorus(params, buf, sr, start),
"ec.audio.modulator" => modulator(params, buf, sr, start),
"ec.audio.reverb" => reverb(params, buf, sr),
"ec.audio.stereomixer" => stereo_mixer(params, buf),
"ec.audio.tone" => tone(params, buf, sr, start),
_ => {}
}
}
fn mix_dry_wet(buf: &mut [f32], dry_src: &[f32], dry: f32, wet: f32) {
for (o, d) in buf.iter_mut().zip(dry_src) {
*o = *o * wet + *d * dry;
}
}
// ---------------------------------------------------------------- biquads (RBJ cookbook)
/// Normalised second-order section coefficients (a0 = 1).
#[derive(Clone, Copy, Debug)]
pub struct Biquad {
pub b: [f64; 3],
pub a: [f64; 2],
}
impl Biquad {
fn norm(b0: f64, b1: f64, b2: f64, a0: f64, a1: f64, a2: f64) -> Biquad {
Biquad { b: [b0 / a0, b1 / a0, b2 / a0], a: [a1 / a0, a2 / a0] }
}
pub fn low_pass(sr: f64, f0: f64, q: f64) -> Biquad {
let w = 2.0 * PI * f0 / sr;
let (s, c) = w.sin_cos();
let al = s / (2.0 * q);
Self::norm((1.0 - c) / 2.0, 1.0 - c, (1.0 - c) / 2.0, 1.0 + al, -2.0 * c, 1.0 - al)
}
pub fn high_pass(sr: f64, f0: f64, q: f64) -> Biquad {
let w = 2.0 * PI * f0 / sr;
let (s, c) = w.sin_cos();
let al = s / (2.0 * q);
Self::norm((1.0 + c) / 2.0, -(1.0 + c), (1.0 + c) / 2.0, 1.0 + al, -2.0 * c, 1.0 - al)
}
/// Peaking EQ with bandwidth `bw` in octaves and gain in dB.
pub fn peaking(sr: f64, f0: f64, bw: f64, db: f64) -> Biquad {
let a = 10f64.powf(db / 40.0);
let w = 2.0 * PI * f0 / sr;
let (s, c) = w.sin_cos();
let al = s * (2f64.ln() / 2.0 * bw * w / s).sinh();
Self::norm(1.0 + al * a, -2.0 * c, 1.0 - al * a, 1.0 + al / a, -2.0 * c, 1.0 - al / a)
}
/// Shelf with slope S = 1.
pub fn low_shelf(sr: f64, f0: f64, db: f64) -> Biquad {
let a = 10f64.powf(db / 40.0);
let w = 2.0 * PI * f0 / sr;
let (s, c) = w.sin_cos();
let al = s / 2.0 * std::f64::consts::SQRT_2;
let sq = 2.0 * a.sqrt() * al;
Self::norm(
a * ((a + 1.0) - (a - 1.0) * c + sq),
2.0 * a * ((a - 1.0) - (a + 1.0) * c),
a * ((a + 1.0) - (a - 1.0) * c - sq),
(a + 1.0) + (a - 1.0) * c + sq,
-2.0 * ((a - 1.0) + (a + 1.0) * c),
(a + 1.0) + (a - 1.0) * c - sq,
)
}
pub fn high_shelf(sr: f64, f0: f64, db: f64) -> Biquad {
let a = 10f64.powf(db / 40.0);
let w = 2.0 * PI * f0 / sr;
let (s, c) = w.sin_cos();
let al = s / 2.0 * std::f64::consts::SQRT_2;
let sq = 2.0 * a.sqrt() * al;
Self::norm(
a * ((a + 1.0) + (a - 1.0) * c + sq),
-2.0 * a * ((a - 1.0) + (a + 1.0) * c),
a * ((a + 1.0) + (a - 1.0) * c - sq),
(a + 1.0) - (a - 1.0) * c + sq,
2.0 * ((a - 1.0) - (a + 1.0) * c),
(a + 1.0) - (a - 1.0) * c - sq,
)
}
}
/// Direct form I over both channels of an interleaved buffer.
fn run_biquad(buf: &mut [f32], q: Biquad) {
for ch in 0..2 {
let (mut x1, mut x2, mut y1, mut y2) = (0.0f64, 0.0, 0.0, 0.0);
for i in (ch..buf.len()).step_by(2) {
let x = buf[i] as f64;
let y = q.b[0] * x + q.b[1] * x1 + q.b[2] * x2 - q.a[0] * y1 - q.a[1] * y2;
x2 = x1;
x1 = x;
y2 = y1;
y1 = y;
buf[i] = y as f32;
}
}
}
// ---------------------------------------------------------------- delay lines
/// Linear-interpolated read `d` samples (fractional) behind position `i` of channel `ch`.
fn read_frac(src: &[f32], i: usize, ch: usize, d: f64) -> f32 {
let pos = i as f64 - d;
if pos < 0.0 {
return 0.0;
}
let p0 = pos.floor() as usize;
let f = (pos - p0 as f64) as f32;
let a = src[p0 * 2 + ch];
let b = if p0 < i { src[(p0 + 1) * 2 + ch] } else { a };
a + (b - a) * f
}
fn delay(params: &Params, buf: &mut [f32], sr: f64) {
let d = ((params.f("delayTime").max(0.0) / 1000.0 * sr).round() as usize).max(1);
let amount = (params.f("delayAmount") / 100.0) as f32;
let fb = (params.f("feedback") / 100.0).clamp(0.0, 0.99) as f32;
let dry = (params.f("dryOut") / 100.0) as f32;
let wet = (params.f("wetOut") / 100.0) as f32;
let n = buf.len() / 2;
// Echo line: e[i] = x[i-d] + fb * e[i-d]; out = dry*x + wet*amount*e.
let mut e = vec![0.0f32; buf.len()];
for i in d..n {
for ch in 0..2 {
e[i * 2 + ch] = buf[(i - d) * 2 + ch] + fb * e[(i - d) * 2 + ch];
}
}
for (o, v) in buf.iter_mut().zip(&e) {
*o = *o * dry + v * wet * amount;
}
}
fn lfo(kind: u32, phase: f64) -> f64 {
let ph = phase.rem_euclid(1.0);
match kind {
1 => 1.0 - 4.0 * (ph - 0.5).abs(), // triangle, −1..1
_ => (2.0 * PI * ph).sin(),
}
}
fn flange_chorus(params: &Params, buf: &mut [f32], sr: f64, start: f64) {
let sep = params.f("voiceSeparationTime").max(0.0) / 1000.0 * sr;
let voices = params.f("voices").round().clamp(1.0, 8.0) as usize;
let rate = params.f("modulationRate").max(0.0);
let depth = (params.f("modulationDepth") / 100.0).clamp(0.0, 1.0);
let vphase = params.f("voicePhaseChange") / 360.0;
let inv = if params.b("invertPhase") { -1.0 } else { 1.0 };
let stereo = params.b("stereoVoices");
let dry = (params.f("dryOut") / 100.0) as f32;
let wet = (params.f("wetOut") / 100.0) as f32;
let src = buf.to_vec();
let n = buf.len() / 2;
for i in 0..n {
let t = start + i as f64 / sr;
let mut acc = [0.0f32; 2];
let mut cnt = [0usize; 2];
for v in 0..voices {
let base = sep * (v + 1) as f64;
let d = (base * (1.0 + depth * lfo(0, rate * t + vphase * v as f64))).max(0.0);
for ch in 0..2 {
if stereo && voices > 1 && v % 2 != ch {
continue;
}
acc[ch] += read_frac(&src, i, ch, d);
cnt[ch] += 1;
}
}
for ch in 0..2 {
let w = if cnt[ch] > 0 { acc[ch] / cnt[ch] as f32 } else { 0.0 };
buf[i * 2 + ch] = src[i * 2 + ch] * dry + inv * w * wet;
}
}
}
fn modulator(params: &Params, buf: &mut [f32], sr: f64, start: f64) {
let kind = params.e("modulationType");
let rate = params.f("modulationRate").max(0.0);
// Depth: vibrato as a modulated delay of up to 5 ms; amplitude: tremolo.
let depth = (params.f("modulationDepth") / 100.0).clamp(0.0, 1.0) * 0.005 * sr;
let am = (params.f("amplitudeModulation") / 100.0).clamp(0.0, 1.0);
let src = buf.to_vec();
let n = buf.len() / 2;
for i in 0..n {
let t = start + i as f64 / sr;
let l = lfo(kind, rate * t);
let d = depth * (1.0 + l) * 0.5;
let g = (1.0 - am * (1.0 - l) * 0.5) as f32;
for ch in 0..2 {
let x = if depth > 0.0 { read_frac(&src, i, ch, d) } else { src[i * 2 + ch] };
buf[i * 2 + ch] = x * g;
}
}
}
/// (comb feedback, damping, all-pass gain, pre-delay seconds)
fn reverb_params(params: &Params) -> (f64, f64, f64, f64) {
let decay = (params.f("decay") / 100.0).clamp(0.0, 1.0);
let fb = 0.7 + decay * 0.28;
let damp = (1.0 - params.f("brightness") / 100.0).clamp(0.0, 1.0) * 0.6;
let ap = 0.3 + 0.4 * (params.f("diffusion") / 100.0).clamp(0.0, 1.0);
let pre = params.f("reverbTime").max(0.0) / 1000.0;
(fb, damp, ap, pre)
}
fn reverb(params: &Params, buf: &mut [f32], sr: f64) {
const COMBS: [usize; 8] = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
const ALLPASS: [usize; 4] = [556, 441, 341, 225];
const SPREAD: usize = 23;
let (fb, damp, apg, pre) = reverb_params(params);
let dry = (params.f("dryOut") / 100.0) as f32;
let wet = (params.f("wetOut") / 100.0) as f32;
let k = sr / 44100.0;
let pre_n = (pre * sr).round() as usize;
let n = buf.len() / 2;
let src = buf.to_vec();
for ch in 0..2 {
let spread = if ch == 1 { SPREAD } else { 0 };
let mut combs: Vec<(Vec<f64>, usize, f64)> = COMBS.iter().map(|&l| (vec![0.0; (((l + spread) as f64 * k) as usize).max(1)], 0, 0.0)).collect();
let mut aps: Vec<(Vec<f64>, usize)> = ALLPASS.iter().map(|&l| (vec![0.0; (((l + spread) as f64 * k) as usize).max(1)], 0)).collect();
for i in 0..n {
let x = if i >= pre_n { (src[(i - pre_n) * 2] as f64 + src[(i - pre_n) * 2 + 1] as f64) * 0.5 * 0.03 } else { 0.0 };
let mut out = 0.0;
for (line, pos, filt) in combs.iter_mut() {
let y = line[*pos];
*filt = y * (1.0 - damp) + *filt * damp;
line[*pos] = x + *filt * fb;
*pos = (*pos + 1) % line.len();
out += y;
}
for (line, pos) in aps.iter_mut() {
let b = line[*pos];
let y = -out + b;
line[*pos] = out + b * apg;
*pos = (*pos + 1) % line.len();
out = y;
}
buf[i * 2 + ch] = src[i * 2 + ch] * dry + out as f32 * wet * 3.0;
}
}
}
fn stereo_mixer(params: &Params, buf: &mut [f32]) {
let ll = (params.f("leftLevel") / 100.0) as f32;
let rl = (params.f("rightLevel") / 100.0) as f32;
// Linear pan law: −100 = all left, 0 = both at full, +100 = all right.
let pan = |p: f64| -> (f32, f32) {
let p = (p / 100.0).clamp(-1.0, 1.0);
((1.0 - p).min(1.0) as f32, (1.0 + p).min(1.0) as f32)
};
let (l_to_l, l_to_r) = pan(params.f("leftPan"));
let (r_to_l, r_to_r) = pan(params.f("rightPan"));
let inv = if params.b("invertPhase") { -1.0 } else { 1.0 };
for f in buf.chunks_exact_mut(2) {
let (l, r) = (f[0] * ll, f[1] * rl);
f[0] = (l * l_to_l + r * r_to_l) * inv;
f[1] = (l * l_to_r + r * r_to_r) * inv;
}
}
fn tone(params: &Params, buf: &mut [f32], sr: f64, start: f64) {
let kind = params.e("waveformOptions");
let level = (params.f("level") / 100.0) as f32;
let freqs: Vec<f64> = (1..=5).map(|i| params.f(&format!("frequency{i}"))).filter(|f| *f > 0.0).collect();
let n = buf.len() / 2;
for i in 0..n {
let t = start + i as f64 / sr;
let mut v = 0.0f64;
for f in &freqs {
let ph = (f * t).rem_euclid(1.0);
v += match kind {
1 => lfo(1, ph - 0.25),
2 => 2.0 * ph - 1.0,
3 => {
if ph < 0.5 {
1.0
} else {
-1.0
}
}
_ => (2.0 * PI * ph).sin(),
};
}
let v = v as f32 * level;
buf[i * 2] += v;
buf[i * 2 + 1] += v;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn defaults(id: &str) -> Params {
let s = crate::find(id).unwrap();
Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() }
}
fn sine(freq: f64, sr: f64, n: usize) -> Vec<f32> {
(0..n)
.flat_map(|i| {
let v = (2.0 * PI * freq * i as f64 / sr).sin() as f32;
[v, v]
})
.collect()
}
fn rms(b: &[f32], from: usize) -> f32 {
let s: f32 = b[from * 2..].iter().map(|v| v * v).sum();
(s / (b.len() - from * 2) as f32).sqrt()
}
#[test]
fn registered_as_audio() {
for id in [
"ec.audio.backwards",
"ec.audio.basstreble",
"ec.audio.delay",
"ec.audio.flangechorus",
"ec.audio.highlowpass",
"ec.audio.modulator",
"ec.audio.parametriceq",
"ec.audio.reverb",
"ec.audio.stereomixer",
"ec.audio.tone",
] {
let s = crate::find(id).unwrap_or_else(|| panic!("{id}"));
assert_eq!(s.category, "Audio");
assert!(is_audio_effect(id));
}
}
#[test]
fn high_low_pass_attenuates() {
let sr = 48000.0;
let mut p = defaults("ec.audio.highlowpass");
p.values.insert("filterOptions".into(), Value::Enum(1));
p.values.insert("cutoffFrequency".into(), num(500.0));
let mut hi = sine(8000.0, sr, 4800);
process("ec.audio.highlowpass", &p, &mut hi, 48000, 0.0);
let mut lo = sine(100.0, sr, 4800);
process("ec.audio.highlowpass", &p, &mut lo, 48000, 0.0);
assert!(rms(&hi, 1000) < 0.01, "{}", rms(&hi, 1000));
assert!(rms(&lo, 1000) > 0.6, "{}", rms(&lo, 1000));
// High pass does the opposite.
p.values.insert("filterOptions".into(), Value::Enum(0));
let mut lo = sine(50.0, sr, 9600);
process("ec.audio.highlowpass", &p, &mut lo, 48000, 0.0);
assert!(rms(&lo, 2000) < 0.02);
}
#[test]
fn delay_offsets_impulse() {
let mut p = defaults("ec.audio.delay");
p.values.insert("delayTime".into(), num(10.0));
p.values.insert("feedback".into(), num(0.0));
let mut b = vec![0.0f32; 2000];
b[0] = 1.0;
b[1] = 1.0;
process("ec.audio.delay", &p, &mut b, 10000, 0.0);
// dry 75 %, then the echo at 100 samples: wet 75 % × amount 50 %.
assert!((b[0] - 0.75).abs() < 1e-6);
assert!((b[200] - 0.375).abs() < 1e-6 && (b[201] - 0.375).abs() < 1e-6);
assert!(b[2..200].iter().all(|v| *v == 0.0));
assert!(preroll("ec.audio.delay", &p) >= 0.01);
}
#[test]
fn stereo_mixer_pans() {
let mut p = defaults("ec.audio.stereomixer");
let mut b = vec![1.0, 0.0, 1.0, 0.0];
process("ec.audio.stereomixer", &p, &mut b, 48000, 0.0);
assert_eq!(b, vec![1.0, 0.0, 1.0, 0.0], "defaults are identity");
p.values.insert("leftPan".into(), num(100.0));
let mut b = vec![1.0, 0.0];
process("ec.audio.stereomixer", &p, &mut b, 48000, 0.0);
assert_eq!(b, vec![0.0, 1.0]);
p.values.insert("leftPan".into(), num(0.0));
p.values.insert("leftLevel".into(), num(50.0));
let mut b = vec![1.0, 0.0];
process("ec.audio.stereomixer", &p, &mut b, 48000, 0.0);
assert_eq!(b, vec![0.5, 0.5]);
}
#[test]
fn tone_generates_frequency() {
let mut p = defaults("ec.audio.tone");
p.values.insert("frequency1".into(), num(1000.0));
for i in 2..=5 {
p.values.insert(format!("frequency{i}"), num(0.0));
}
let sr = 48000;
let mut b = vec![0.0f32; 48000 * 2];
process("ec.audio.tone", &p, &mut b, sr, 0.0);
let left: Vec<f32> = b.iter().step_by(2).copied().collect();
let crossings = left.windows(2).filter(|w| w[0] <= 0.0 && w[1] > 0.0).count();
assert!((crossings as i64 - 1000).abs() <= 1, "{crossings}");
let peak = left.iter().fold(0.0f32, |m, v| m.max(v.abs()));
assert!((peak - 0.2).abs() < 1e-3, "{peak}");
}
#[test]
fn bass_treble_eq_reverb_modulator_flange() {
let sr = 48000.0;
// Bass boost raises 60 Hz, treble cut lowers 10 kHz.
let mut p = defaults("ec.audio.basstreble");
p.values.insert("bass".into(), num(100.0));
p.values.insert("treble".into(), num(-100.0));
let mut lo = sine(60.0, sr, 9600);
process("ec.audio.basstreble", &p, &mut lo, 48000, 0.0);
assert!(rms(&lo, 4000) > 0.707 * 3.0, "{}", rms(&lo, 4000));
let mut hi = sine(10000.0, sr, 9600);
process("ec.audio.basstreble", &p, &mut hi, 48000, 0.0);
assert!(rms(&hi, 4000) < 0.707 * 0.4, "{}", rms(&hi, 4000));
// Parametric EQ boost at its centre.
let mut p = defaults("ec.audio.parametriceq");
p.values.insert("band1BoostCut".into(), num(12.0));
p.values.insert("band1Frequency".into(), num(1000.0));
let mut s = sine(1000.0, sr, 9600);
process("ec.audio.parametriceq", &p, &mut s, 48000, 0.0);
assert!((rms(&s, 4000) * std::f32::consts::SQRT_2 - 3.98).abs() < 0.2, "{}", rms(&s, 4000) * std::f32::consts::SQRT_2);
// Reverb leaves a tail after an impulse.
let p = defaults("ec.audio.reverb");
let mut b = vec![0.0f32; 48000];
b[0] = 1.0;
b[1] = 1.0;
process("ec.audio.reverb", &p, &mut b, 48000, 0.0);
assert!(b[2 * 12000..].iter().any(|v| v.abs() > 1e-5));
assert!(b.iter().all(|v| v.is_finite() && v.abs() < 2.0));
// Modulator with amplitude modulation dips the level; flange keeps it bounded.
let mut p = defaults("ec.audio.modulator");
p.values.insert("amplitudeModulation".into(), num(100.0));
let mut b = vec![1.0f32; 9600];
process("ec.audio.modulator", &p, &mut b, 48000, 0.0);
assert!(b.iter().any(|v| *v < 0.05) && b.iter().any(|v| *v > 0.95));
let p = defaults("ec.audio.flangechorus");
let mut b = sine(440.0, sr, 4800);
process("ec.audio.flangechorus", &p, &mut b, 48000, 0.0);
assert!(b.iter().all(|v| v.abs() <= 1.0 + 1e-4));
}
#[test]
fn backwards_swaps_channels() {
let mut p = defaults("ec.audio.backwards");
p.values.insert("swapChannels".into(), Value::Bool(true));
let mut b = vec![1.0, 2.0];
process("ec.audio.backwards", &p, &mut b, 48000, 0.0);
assert_eq!(b, vec![2.0, 1.0]);
assert!(reverses("ec.audio.backwards"));
}
}