mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-31 14:06:56 -06:00
462 lines
15 KiB
Rust
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
|
|
}
|
|
}
|