Files
soundcraft/crates/dsp/src/plugins/modulation.rs
T

462 lines
15 KiB
Rust

//! Delay and modulation effects: Mod Delay, Chorus, Flanger, Phaser.
use crate::params::{Params, choice, lin, log, param_plumbing};
use crate::util::{DelayLine, Lfo, Smoother, flush, frames_in, ms_to_samples, sane_channels, sane_sr};
use crate::{Category, ParamInfo, Plugin, PluginInfo, Unit};
/// Per-channel LFO phase offset for stereo movement (channel 1 is offset by `spread` cycles).
#[inline]
fn channel_phase(phase: f32, c: usize, spread: f32) -> f32 {
let p = phase + if c % 2 == 1 { spread } else { 0.0 };
if p >= 1.0 { p - 1.0 } else { p }
}
// ---------------------------------------------------------------------------------------------
const MAX_DELAY_MS: f32 = 2000.0;
const MOD_DELAY_DEPTH_MS: f32 = 5.0;
static DELAY_PARAMS: [ParamInfo; 6] = [
log("time", "Time", 1.0, MAX_DELAY_MS, 250.0, Unit::Ms),
lin("feedback", "Feedback", -99.0, 99.0, 30.0, Unit::Percent),
lin("mix", "Mix", 0.0, 100.0, 30.0, Unit::Percent),
log("mod_rate", "Mod Rate", 0.01, 10.0, 0.5, Unit::Hz),
lin("mod_depth", "Mod Depth", 0.0, 100.0, 0.0, Unit::Percent),
log("lpf", "LPF", 500.0, 20000.0, 12000.0, Unit::Hz),
];
pub static DELAY_INFO: PluginInfo = PluginInfo {
id: "mod_delay",
name: "Mod Delay",
short_name: "ModDly",
category: Category::Delay,
params: &DELAY_PARAMS,
is_instrument: false,
audiosuite: true,
};
/// Feedback delay with modulation and a low-pass in the feedback path.
pub struct ModDelay {
p: Params,
sr: f32,
lines: Vec<DelayLine>,
lp: Vec<f32>,
lfo: Lfo,
time: Smoother,
fb: Smoother,
mix: Smoother,
depth: Smoother,
lp_coef: f32,
}
impl Default for ModDelay {
fn default() -> Self {
Self::new()
}
}
impl ModDelay {
pub fn new() -> Self {
let mut d = Self {
p: Params::new(&DELAY_PARAMS),
sr: 48_000.0,
lines: Vec::new(),
lp: Vec::new(),
lfo: Lfo::new(),
time: Smoother::new(250.0),
fb: Smoother::new(0.0),
mix: Smoother::new(0.0),
depth: Smoother::new(0.0),
lp_coef: 0.0,
};
d.update();
d
}
fn update(&mut self) {
self.time.set(self.p.v(0));
self.fb.set(self.p.v(1) / 100.0);
self.mix.set(self.p.v(2) / 100.0);
self.depth.set(self.p.v(4) / 100.0 * MOD_DELAY_DEPTH_MS);
self.lp_coef = (-std::f32::consts::TAU * self.p.v(5) / self.sr).exp();
}
}
impl Plugin for ModDelay {
param_plumbing!(DELAY_INFO);
fn prepare(&mut self, sample_rate: f32, _max_block: usize, channels: usize) {
self.sr = sane_sr(sample_rate);
let max = ms_to_samples(MAX_DELAY_MS + 2.0 * MOD_DELAY_DEPTH_MS, self.sr) as usize + 8;
self.lines = (0..sane_channels(channels))
.map(|_| {
let mut d = DelayLine::default();
d.allocate(max);
d
})
.collect();
self.lp = vec![0.0; self.lines.len()];
self.time.set_time(120.0, self.sr);
for s in [&mut self.fb, &mut self.mix, &mut self.depth] {
s.set_time(20.0, self.sr);
}
self.update();
self.reset();
}
fn reset(&mut self) {
self.lines.iter_mut().for_each(DelayLine::clear);
self.lp.iter_mut().for_each(|v| *v = 0.0);
self.lfo.reset();
for s in [&mut self.time, &mut self.fb, &mut self.mix, &mut self.depth] {
s.snap();
}
}
fn process(&mut self, io: &mut [Vec<f32>], frames: usize) {
let ch = self.lines.len().min(io.len());
let frames = frames_in(io, frames, ch);
let inc = self.p.v(3) / self.sr;
let a = self.lp_coef;
for n in 0..frames {
let phase = self.lfo.tick(inc);
let t = self.time.next();
let fb = self.fb.next();
let m = self.mix.next();
let depth = self.depth.next();
for c in 0..ch {
let (Some(buf), Some(line), Some(lp)) = (io.get_mut(c), self.lines.get_mut(c), self.lp.get_mut(c)) else { continue };
let Some(x) = buf.get_mut(n) else { continue };
let dry = if x.is_finite() { *x } else { 0.0 };
let modv = depth * 0.5 * (1.0 + Lfo::sin_at(channel_phase(phase, c, 0.25)));
let d = ms_to_samples(t + modv, self.sr);
let y = line.read_cubic(d);
*lp = flush(y + (*lp - y) * a);
line.push(flush(dry + fb * *lp));
*x = dry * (1.0 - m) + *lp * m;
}
}
}
fn tail_samples(&self) -> usize {
// Time for feedback to fall by 60 dB.
let fb = (self.p.v(1).abs() / 100.0).clamp(0.0, 0.99);
let repeats = if fb < 1e-3 { 1.0 } else { (-60.0 / (20.0 * fb.log10())).min(500.0) + 1.0 };
ms_to_samples(self.p.v(0) * repeats, self.sr) as usize
}
}
// ---------------------------------------------------------------------------------------------
static CHORUS_PARAMS: [ParamInfo; 6] = [
log("rate", "Rate", 0.05, 5.0, 0.8, Unit::Hz),
lin("depth", "Depth", 0.0, 100.0, 50.0, Unit::Percent),
lin("delay", "Delay", 5.0, 30.0, 12.0, Unit::Ms),
lin("feedback", "Feedback", 0.0, 90.0, 0.0, Unit::Percent),
lin("mix", "Mix", 0.0, 100.0, 50.0, Unit::Percent),
lin("spread", "Spread", 0.0, 100.0, 100.0, Unit::Percent),
];
pub static CHORUS_INFO: PluginInfo = PluginInfo {
id: "chorus",
name: "Chorus",
short_name: "Chorus",
category: Category::Modulation,
params: &CHORUS_PARAMS,
is_instrument: false,
audiosuite: true,
};
static FLANGER_PARAMS: [ParamInfo; 5] = [
log("rate", "Rate", 0.02, 5.0, 0.25, Unit::Hz),
lin("depth", "Depth", 0.0, 100.0, 70.0, Unit::Percent),
lin("delay", "Delay", 0.1, 10.0, 2.0, Unit::Ms),
lin("feedback", "Feedback", -95.0, 95.0, 50.0, Unit::Percent),
lin("mix", "Mix", 0.0, 100.0, 50.0, Unit::Percent),
];
pub static FLANGER_INFO: PluginInfo = PluginInfo {
id: "flanger",
name: "Flanger",
short_name: "Flanger",
category: Category::Modulation,
params: &FLANGER_PARAMS,
is_instrument: false,
audiosuite: true,
};
#[derive(Clone, Copy, PartialEq)]
enum ModKind {
Chorus,
Flanger,
}
/// Modulated short delay used by both Chorus and Flanger.
pub struct ModFx {
kind: ModKind,
p: Params,
sr: f32,
lines: Vec<DelayLine>,
fbs: Vec<f32>,
lfo: Lfo,
delay: Smoother,
depth: Smoother,
fb: Smoother,
mix: Smoother,
}
impl ModFx {
pub fn chorus() -> Self {
Self::build(ModKind::Chorus)
}
pub fn flanger() -> Self {
Self::build(ModKind::Flanger)
}
fn build(kind: ModKind) -> Self {
let info = if kind == ModKind::Chorus { &CHORUS_INFO } else { &FLANGER_INFO };
let mut m = Self {
kind,
p: Params::new(info.params),
sr: 48_000.0,
lines: Vec::new(),
fbs: Vec::new(),
lfo: Lfo::new(),
delay: Smoother::new(0.0),
depth: Smoother::new(0.0),
fb: Smoother::new(0.0),
mix: Smoother::new(0.0),
};
m.update();
m
}
fn update(&mut self) {
self.delay.set(self.p.v(2));
self.depth.set(self.p.v(1) / 100.0);
self.fb.set(self.p.v(3) / 100.0);
self.mix.set(self.p.v(4) / 100.0);
}
}
impl Plugin for ModFx {
fn info(&self) -> &'static PluginInfo {
if self.kind == ModKind::Chorus { &CHORUS_INFO } else { &FLANGER_INFO }
}
fn set_param(&mut self, id: &str, value: f32) -> bool {
if self.p.set(id, value).is_some() {
self.update();
true
} else {
false
}
}
fn param(&self, id: &str) -> Option<f32> {
self.p.get(id)
}
fn prepare(&mut self, sample_rate: f32, _max_block: usize, channels: usize) {
self.sr = sane_sr(sample_rate);
let max = ms_to_samples(45.0, self.sr) as usize + 8;
self.lines = (0..sane_channels(channels))
.map(|_| {
let mut d = DelayLine::default();
d.allocate(max);
d
})
.collect();
self.fbs = vec![0.0; self.lines.len()];
for s in [&mut self.delay, &mut self.depth, &mut self.fb, &mut self.mix] {
s.set_time(30.0, self.sr);
}
self.update();
self.reset();
}
fn reset(&mut self) {
self.lines.iter_mut().for_each(DelayLine::clear);
self.fbs.iter_mut().for_each(|v| *v = 0.0);
self.lfo.reset();
for s in [&mut self.delay, &mut self.depth, &mut self.fb, &mut self.mix] {
s.snap();
}
}
fn process(&mut self, io: &mut [Vec<f32>], frames: usize) {
let ch = self.lines.len().min(io.len());
let frames = frames_in(io, frames, ch);
let inc = self.p.v(0) / self.sr;
let spread = if self.kind == ModKind::Chorus { 0.25 * self.p.v(5) / 100.0 } else { 0.25 };
for n in 0..frames {
let phase = self.lfo.tick(inc);
let base = self.delay.next();
let depth = self.depth.next();
let fb = self.fb.next();
let m = self.mix.next();
for c in 0..ch {
let (Some(buf), Some(line), Some(fbs)) = (io.get_mut(c), self.lines.get_mut(c), self.fbs.get_mut(c)) else { continue };
let Some(x) = buf.get_mut(n) else { continue };
let dry = if x.is_finite() { *x } else { 0.0 };
let s = Lfo::sin_at(channel_phase(phase, c, spread));
let ms = match self.kind {
// Up to ±6 ms of sweep around the base delay.
ModKind::Chorus => base + depth * 6.0 * 0.5 * (1.0 + s),
// Sweep from (1 - depth)·delay up to delay.
ModKind::Flanger => (base * (1.0 - depth * 0.5 * (1.0 - s))).max(0.05),
};
line.push(flush(dry + fb * *fbs));
let y = line.read_cubic(ms_to_samples(ms, self.sr).max(2.0));
*fbs = flush(y);
*x = dry * (1.0 - m) + y * m;
}
}
}
fn tail_samples(&self) -> usize {
// Time for feedback to fall by 60 dB at the longest delay of the sweep, as for the delay
// above; never shorter than the 200 ms this used to report.
let longest = match self.kind {
ModKind::Chorus => self.p.v(2) + self.p.v(1) / 100.0 * 6.0,
ModKind::Flanger => self.p.v(2),
};
let fb = (self.p.v(3).abs() / 100.0).clamp(0.0, 0.99);
let repeats = if fb < 1e-3 { 1.0 } else { (-60.0 / (20.0 * fb.log10())).min(500.0) + 1.0 };
ms_to_samples((longest * repeats).max(200.0), self.sr) as usize
}
}
// ---------------------------------------------------------------------------------------------
const STAGE_CHOICES: &[&str] = &["2", "4", "6", "8", "12"];
const STAGE_COUNTS: [usize; 5] = [2, 4, 6, 8, 12];
const MAX_STAGES: usize = 12;
static PHASER_PARAMS: [ParamInfo; 6] = [
log("rate", "Rate", 0.02, 5.0, 0.5, Unit::Hz),
lin("depth", "Depth", 0.0, 100.0, 70.0, Unit::Percent),
choice("stages", "Stages", STAGE_CHOICES, 2),
lin("feedback", "Feedback", -95.0, 95.0, 40.0, Unit::Percent),
log("center", "Center", 100.0, 4000.0, 800.0, Unit::Hz),
lin("mix", "Mix", 0.0, 100.0, 50.0, Unit::Percent),
];
pub static PHASER_INFO: PluginInfo = PluginInfo {
id: "phaser",
name: "Phaser",
short_name: "Phaser",
category: Category::Modulation,
params: &PHASER_PARAMS,
is_instrument: false,
audiosuite: true,
};
/// Cascade of modulated first-order allpasses with feedback.
pub struct Phaser {
p: Params,
sr: f32,
ch: usize,
z: Vec<[f32; MAX_STAGES]>,
fbs: Vec<f32>,
lfo: Lfo,
depth: Smoother,
fb: Smoother,
center: Smoother,
mix: Smoother,
}
impl Default for Phaser {
fn default() -> Self {
Self::new()
}
}
impl Phaser {
pub fn new() -> Self {
let mut p = Self {
p: Params::new(&PHASER_PARAMS),
sr: 48_000.0,
ch: 0,
z: Vec::new(),
fbs: Vec::new(),
lfo: Lfo::new(),
depth: Smoother::new(0.0),
fb: Smoother::new(0.0),
center: Smoother::new(800.0),
mix: Smoother::new(0.0),
};
p.update();
p
}
fn update(&mut self) {
self.depth.set(self.p.v(1) / 100.0);
self.fb.set(self.p.v(3) / 100.0);
self.center.set(self.p.v(4));
self.mix.set(self.p.v(5) / 100.0);
}
}
impl Plugin for Phaser {
param_plumbing!(PHASER_INFO);
fn prepare(&mut self, sample_rate: f32, _max_block: usize, channels: usize) {
self.sr = sane_sr(sample_rate);
self.ch = sane_channels(channels);
self.z = vec![[0.0; MAX_STAGES]; self.ch];
self.fbs = vec![0.0; self.ch];
for s in [&mut self.depth, &mut self.fb, &mut self.center, &mut self.mix] {
s.set_time(30.0, self.sr);
}
self.update();
self.reset();
}
fn reset(&mut self) {
self.z.iter_mut().for_each(|z| *z = [0.0; MAX_STAGES]);
self.fbs.iter_mut().for_each(|v| *v = 0.0);
self.lfo.reset();
for s in [&mut self.depth, &mut self.fb, &mut self.center, &mut self.mix] {
s.snap();
}
}
fn process(&mut self, io: &mut [Vec<f32>], frames: usize) {
let ch = self.ch.min(io.len());
let frames = frames_in(io, frames, ch);
let inc = self.p.v(0) / self.sr;
let stages = STAGE_COUNTS.get(self.p.choice(2)).copied().unwrap_or(6);
let nyq = self.sr * 0.45;
for n in 0..frames {
let phase = self.lfo.tick(inc);
let depth = self.depth.next();
let fb = self.fb.next();
let center = self.center.next();
let m = self.mix.next();
for c in 0..ch {
let (Some(buf), Some(z), Some(fbs)) = (io.get_mut(c), self.z.get_mut(c), self.fbs.get_mut(c)) else { continue };
let Some(x) = buf.get_mut(n) else { continue };
let dry = if x.is_finite() { *x } else { 0.0 };
let s = Lfo::sin_at(channel_phase(phase, c, 0.25));
let f = (center * 2f32.powf(2.0 * depth * s)).clamp(20.0, nyq);
let t = (std::f32::consts::PI * f / self.sr).tan();
let a = (t - 1.0) / (t + 1.0);
let mut y = dry + fb * *fbs;
for zi in z.iter_mut().take(stages) {
let out = a * y + *zi;
*zi = flush(y - a * out);
y = out;
}
*fbs = flush(y);
*x = dry * (1.0 - m) + y * m;
}
}
}
fn tail_samples(&self) -> usize {
ms_to_samples(100.0, self.sr) as usize
}
}