683 lines
28 KiB
Rust
683 lines
28 KiB
Rust
//! 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"));
|
||
}
|
||
}
|