use std::f32::consts::{PI, TAU};
use super::internal::InternalPlugin;
use super::params::{ParamError, ParamSpec, Params, Value};
use super::Plugin;
pub trait Effect: Send + Sync + 'static {
fn schema(&self) -> Vec<ParamSpec>;
fn set(&mut self, key: &str, value: &Value) -> Result<(), ParamError>;
fn get(&self, key: &str) -> Option<Value>;
fn process(&mut self, buffer: &mut [f32], channels: u16);
fn latency(&self) -> usize {
0
}
fn reset(&mut self);
fn duplicate(&self) -> Box<dyn Effect>;
}
pub(crate) fn params_of(effect: &dyn Effect) -> Params {
effect
.schema()
.into_iter()
.filter_map(|spec| effect.get(&spec.name).map(|value| (spec.name, value)))
.collect()
}
pub(crate) fn apply(effect: &mut dyn Effect, params: &Params) -> Result<(), ParamError> {
let schema = effect.schema();
for (key, value) in params.iter() {
let Some(spec) = schema.iter().find(|spec| spec.name == key) else {
return Err(ParamError::Unknown {
key: key.to_string(),
known: schema.into_iter().map(|spec| spec.name).collect(),
});
};
spec.check(value)?;
}
for (key, value) in params.iter() {
effect.set(key, value)?;
}
Ok(())
}
macro_rules! parameters {
(
$(#[$meta:meta])*
$name:ident {
$(
$field:ident : $default:expr, $min:expr, $max:expr, $unit:literal, $about:literal;
)*
$(
@toggle $flag:ident : $flag_default:expr, $flag_about:literal;
)*
}
) => {
$(#[$meta])*
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct $name {
$(pub $field: f32,)*
$(pub $flag: bool,)*
}
impl Default for $name {
fn default() -> Self {
Self {
$($field: $default,)*
$($flag: $flag_default,)*
}
}
}
impl $name {
pub fn schema() -> Vec<ParamSpec> {
vec![
$(ParamSpec::number(
stringify!($field), $unit, $min as f64, $max as f64,
$default as f64, $about,
),)*
$(ParamSpec::toggle(stringify!($flag), $flag_default, $flag_about),)*
]
}
fn spec_for(key: &str) -> Option<ParamSpec> {
Self::schema().into_iter().find(|spec| spec.name == key)
}
fn names() -> Vec<String> {
Self::schema().into_iter().map(|spec| spec.name).collect()
}
pub fn set(&mut self, key: &str, value: &Value) -> Result<(), ParamError> {
let Some(spec) = Self::spec_for(key) else {
return Err(ParamError::Unknown {
key: key.to_string(),
known: Self::names(),
});
};
let number = spec.check(value)?;
match key {
$(stringify!($field) => self.$field = number as f32,)*
$(stringify!($flag) => self.$flag = number != 0.0,)*
_ => unreachable!("'{key}' has a spec but no field"),
}
Ok(())
}
pub fn get(&self, key: &str) -> Option<Value> {
match key {
$(stringify!($field) => Some(Value::from_f32(self.$field)),)*
$(stringify!($flag) => Some(Value::Bool(self.$flag)),)*
_ => None,
}
}
}
};
}
macro_rules! effect_common {
($params:ty) => {
fn schema(&self) -> Vec<ParamSpec> {
<$params>::schema()
}
fn set(&mut self, key: &str, value: &Value) -> Result<(), ParamError> {
self.params.set(key, value)
}
fn get(&self, key: &str) -> Option<Value> {
self.params.get(key)
}
fn duplicate(&self) -> Box<dyn Effect> {
Box::new(Self::new(self.sample_rate).configured(self.params))
}
};
}
#[derive(Clone, Debug, Default)]
struct PerChannel<T> {
slots: Vec<T>,
}
impl<T: Clone + Default> PerChannel<T> {
fn new() -> Self {
Self { slots: Vec::new() }
}
fn get(&mut self, channels: usize) -> &mut [T] {
if self.slots.len() < channels {
self.slots.resize(channels, T::default());
}
&mut self.slots[..channels]
}
fn clear(&mut self) {
self.slots.clear();
}
}
#[derive(Clone, Copy, Debug, Default)]
struct Biquad {
b0: f32,
b1: f32,
b2: f32,
a1: f32,
a2: f32,
z1: f32,
z2: f32,
}
impl Biquad {
fn set(&mut self, b0: f32, b1: f32, b2: f32, a0: f32, a1: f32, a2: f32) {
self.b0 = b0 / a0;
self.b1 = b1 / a0;
self.b2 = b2 / a0;
self.a1 = a1 / a0;
self.a2 = a2 / a0;
}
fn process(&mut self, input: f32) -> f32 {
let output = self.b0 * input + self.z1;
self.z1 = self.b1 * input - self.a1 * output + self.z2;
self.z2 = self.b2 * input - self.a2 * output;
output
}
fn low_pass(&mut self, cutoff_hz: f32, q: f32, sample_rate: f32) {
let (omega, sin, cos) = angles(cutoff_hz, sample_rate);
let alpha = sin / (2.0 * q.max(0.01));
let _ = omega;
self.set(
(1.0 - cos) / 2.0,
1.0 - cos,
(1.0 - cos) / 2.0,
1.0 + alpha,
-2.0 * cos,
1.0 - alpha,
);
}
fn high_pass(&mut self, cutoff_hz: f32, q: f32, sample_rate: f32) {
let (_, sin, cos) = angles(cutoff_hz, sample_rate);
let alpha = sin / (2.0 * q.max(0.01));
self.set(
(1.0 + cos) / 2.0,
-(1.0 + cos),
(1.0 + cos) / 2.0,
1.0 + alpha,
-2.0 * cos,
1.0 - alpha,
);
}
fn peaking(&mut self, centre_hz: f32, q: f32, gain_db: f32, sample_rate: f32) {
let a = 10.0_f32.powf(gain_db / 40.0);
let (_, sin, cos) = angles(centre_hz, sample_rate);
let alpha = sin / (2.0 * q.max(0.01));
self.set(
1.0 + alpha * a,
-2.0 * cos,
1.0 - alpha * a,
1.0 + alpha / a,
-2.0 * cos,
1.0 - alpha / a,
);
}
fn low_shelf(&mut self, corner_hz: f32, gain_db: f32, sample_rate: f32) {
let a = 10.0_f32.powf(gain_db / 40.0);
let (_, sin, cos) = angles(corner_hz, sample_rate);
let beta = 2.0 * a.sqrt() * (sin / 2.0) * (2.0_f32).sqrt();
self.set(
a * ((a + 1.0) - (a - 1.0) * cos + beta),
2.0 * a * ((a - 1.0) - (a + 1.0) * cos),
a * ((a + 1.0) - (a - 1.0) * cos - beta),
(a + 1.0) + (a - 1.0) * cos + beta,
-2.0 * ((a - 1.0) + (a + 1.0) * cos),
(a + 1.0) + (a - 1.0) * cos - beta,
);
}
fn high_shelf(&mut self, corner_hz: f32, gain_db: f32, sample_rate: f32) {
let a = 10.0_f32.powf(gain_db / 40.0);
let (_, sin, cos) = angles(corner_hz, sample_rate);
let beta = 2.0 * a.sqrt() * (sin / 2.0) * (2.0_f32).sqrt();
self.set(
a * ((a + 1.0) + (a - 1.0) * cos + beta),
-2.0 * a * ((a - 1.0) + (a + 1.0) * cos),
a * ((a + 1.0) + (a - 1.0) * cos - beta),
(a + 1.0) - (a - 1.0) * cos + beta,
2.0 * ((a - 1.0) - (a + 1.0) * cos),
(a + 1.0) - (a - 1.0) * cos - beta,
);
}
}
fn angles(frequency_hz: f32, sample_rate: f32) -> (f32, f32, f32) {
let nyquist = sample_rate / 2.0;
let frequency = frequency_hz.clamp(1.0, nyquist * 0.99);
let omega = TAU * frequency / sample_rate;
(omega, omega.sin(), omega.cos())
}
#[derive(Clone, Debug, Default)]
struct DelayLine {
buffer: Vec<f32>,
write: usize,
}
impl DelayLine {
fn with_capacity(frames: usize) -> Self {
Self {
buffer: vec![0.0; frames.max(1) + 1],
write: 0,
}
}
fn push(&mut self, sample: f32) {
self.buffer[self.write] = sample;
self.write = (self.write + 1) % self.buffer.len();
}
fn read(&self, delay: f32) -> f32 {
let length = self.buffer.len();
let delay = delay.clamp(0.0, (length - 1) as f32);
let whole = delay.floor() as usize;
let fraction = delay - whole as f32;
let first = (self.write + length - whole - 1) % length;
let second = (first + length - 1) % length;
self.buffer[first] * (1.0 - fraction) + self.buffer[second] * fraction
}
}
fn from_db(db: f32) -> f32 {
10.0_f32.powf(db / 20.0)
}
fn to_db(linear: f32) -> f32 {
20.0 * linear.abs().max(1.0e-9).log10()
}
fn time_coefficient(ms: f32, sample_rate: f32) -> f32 {
if ms <= 0.0 {
return 1.0;
}
1.0 - (-1.0 / (ms * 0.001 * sample_rate)).exp()
}
parameters! {
GainParams {
gain_db: 0.0, -60.0, 24.0, "dB", "level change applied to every channel";
@toggle mute: false, "silence the signal entirely";
}
}
pub struct GainEffect {
params: GainParams,
sample_rate: u32,
}
impl GainEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: GainParams::default(),
sample_rate,
}
}
fn configured(mut self, params: GainParams) -> Self {
self.params = params;
self
}
}
impl Effect for GainEffect {
effect_common!(GainParams);
fn process(&mut self, buffer: &mut [f32], _channels: u16) {
let scale = if self.params.mute {
0.0
} else {
from_db(self.params.gain_db)
};
for sample in buffer {
*sample *= scale;
}
}
fn reset(&mut self) {}
}
parameters! {
PanParams {
pan: 0.0, -1.0, 1.0, "", "-1 is hard left, 0 centre, 1 hard right";
}
}
pub struct PanEffect {
params: PanParams,
sample_rate: u32,
}
impl PanEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: PanParams::default(),
sample_rate,
}
}
fn configured(mut self, params: PanParams) -> Self {
self.params = params;
self
}
}
impl Effect for PanEffect {
effect_common!(PanParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
if channels != 2 {
return;
}
let angle = (self.params.pan.clamp(-1.0, 1.0) + 1.0) * 0.25 * PI;
let (left, right) = (angle.cos(), angle.sin());
for frame in buffer.chunks_mut(2) {
frame[0] *= left;
frame[1] *= right;
}
}
fn reset(&mut self) {}
}
parameters! {
WidthParams {
width: 1.0, 0.0, 2.0, "", "0 is mono, 1 unchanged, 2 twice as wide";
}
}
pub struct WidthEffect {
params: WidthParams,
sample_rate: u32,
}
impl WidthEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: WidthParams::default(),
sample_rate,
}
}
fn configured(mut self, params: WidthParams) -> Self {
self.params = params;
self
}
}
impl Effect for WidthEffect {
effect_common!(WidthParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
if channels != 2 {
return;
}
let width = self.params.width;
for frame in buffer.chunks_mut(2) {
let mid = (frame[0] + frame[1]) * 0.5;
let side = (frame[0] - frame[1]) * 0.5 * width;
frame[0] = mid + side;
frame[1] = mid - side;
}
}
fn reset(&mut self) {}
}
parameters! {
FilterParams {
cutoff_hz: 1000.0, 20.0, 20000.0, "Hz", "corner frequency";
resonance: 0.707, 0.1, 20.0, "Q", "peak at the corner; 0.707 is flat";
@toggle high_pass: false, "high-pass instead of low-pass";
}
}
pub struct FilterEffect {
params: FilterParams,
applied: Option<FilterParams>,
filters: PerChannel<Biquad>,
sample_rate: u32,
}
impl FilterEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: FilterParams::default(),
applied: None,
filters: PerChannel::new(),
sample_rate,
}
}
fn configured(mut self, params: FilterParams) -> Self {
self.params = params;
self
}
}
impl Effect for FilterEffect {
effect_common!(FilterParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channels = channels as usize;
if channels == 0 {
return;
}
let rate = self.sample_rate as f32;
let params = self.params;
let filters = self.filters.get(channels);
if self.applied != Some(params) {
for filter in filters.iter_mut() {
if params.high_pass {
filter.high_pass(params.cutoff_hz, params.resonance, rate);
} else {
filter.low_pass(params.cutoff_hz, params.resonance, rate);
}
}
self.applied = Some(params);
}
for frame in buffer.chunks_mut(channels) {
for (sample, filter) in frame.iter_mut().zip(filters.iter_mut()) {
*sample = filter.process(*sample);
}
}
}
fn reset(&mut self) {
self.filters.clear();
self.applied = None;
}
}
parameters! {
EqParams {
low_gain_db: 0.0, -24.0, 24.0, "dB", "low shelf gain";
low_freq_hz: 200.0, 20.0, 2000.0, "Hz", "low shelf corner";
mid_gain_db: 0.0, -24.0, 24.0, "dB", "mid peak gain";
mid_freq_hz: 1000.0, 100.0, 10000.0, "Hz", "mid peak centre";
mid_q: 0.7, 0.1, 10.0, "Q", "mid peak width; higher is narrower";
high_gain_db: 0.0, -24.0, 24.0, "dB", "high shelf gain";
high_freq_hz: 4000.0, 1000.0, 20000.0, "Hz", "high shelf corner";
}
}
pub struct EqEffect {
params: EqParams,
applied: Option<EqParams>,
bands: PerChannel<[Biquad; 3]>,
sample_rate: u32,
}
impl EqEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: EqParams::default(),
applied: None,
bands: PerChannel::new(),
sample_rate,
}
}
fn configured(mut self, params: EqParams) -> Self {
self.params = params;
self
}
}
impl Effect for EqEffect {
effect_common!(EqParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channels = channels as usize;
if channels == 0 {
return;
}
let rate = self.sample_rate as f32;
let params = self.params;
let bands = self.bands.get(channels);
if self.applied != Some(params) {
for band in bands.iter_mut() {
band[0].low_shelf(params.low_freq_hz, params.low_gain_db, rate);
band[1].peaking(params.mid_freq_hz, params.mid_q, params.mid_gain_db, rate);
band[2].high_shelf(params.high_freq_hz, params.high_gain_db, rate);
}
self.applied = Some(params);
}
for frame in buffer.chunks_mut(channels) {
for (sample, band) in frame.iter_mut().zip(bands.iter_mut()) {
let mut value = *sample;
for filter in band.iter_mut() {
value = filter.process(value);
}
*sample = value;
}
}
}
fn reset(&mut self) {
self.bands.clear();
self.applied = None;
}
}
parameters! {
CompressorParams {
threshold_db: -18.0, -60.0, 0.0, "dB", "level above which gain reduction starts";
ratio: 4.0, 1.0, 20.0, "", "input-to-output ratio above the threshold";
attack_ms: 10.0, 0.1, 200.0, "ms", "how fast it reacts to a rise";
release_ms: 120.0, 5.0, 2000.0, "ms", "how fast it lets go";
knee_db: 6.0, 0.0, 24.0, "dB", "width of the soft knee around the threshold";
makeup_db: 0.0, -12.0, 24.0, "dB", "level added after the reduction";
}
}
pub struct CompressorEffect {
params: CompressorParams,
envelope_db: f32,
reduction_db: f32,
sample_rate: u32,
}
impl CompressorEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: CompressorParams::default(),
envelope_db: -120.0,
reduction_db: 0.0,
sample_rate,
}
}
fn configured(mut self, params: CompressorParams) -> Self {
self.params = params;
self
}
fn curve(&self, level_db: f32) -> f32 {
let CompressorParams {
threshold_db,
ratio,
knee_db,
..
} = self.params;
let over = level_db - threshold_db;
if knee_db > 0.0 && over > -knee_db / 2.0 && over < knee_db / 2.0 {
let x = over + knee_db / 2.0;
return -(1.0 - 1.0 / ratio) * x * x / (2.0 * knee_db);
}
if over <= 0.0 {
return 0.0;
}
-over * (1.0 - 1.0 / ratio)
}
}
impl Effect for CompressorEffect {
effect_common!(CompressorParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channels = channels as usize;
if channels == 0 {
return;
}
let rate = self.sample_rate as f32;
let attack = time_coefficient(self.params.attack_ms, rate);
let release = time_coefficient(self.params.release_ms, rate);
let makeup = from_db(self.params.makeup_db);
for frame in buffer.chunks_mut(channels) {
let peak = frame.iter().fold(0.0_f32, |loudest, sample| {
loudest.max(sample.abs())
});
self.envelope_db = to_db(peak);
let wanted = self.curve(self.envelope_db);
let coefficient = if wanted < self.reduction_db {
attack
} else {
release
};
self.reduction_db += coefficient * (wanted - self.reduction_db);
let scale = from_db(self.reduction_db) * makeup;
for sample in frame.iter_mut() {
*sample *= scale;
}
}
}
fn reset(&mut self) {
self.envelope_db = -120.0;
self.reduction_db = 0.0;
}
}
parameters! {
LimiterParams {
ceiling_db: -1.0, -24.0, 0.0, "dB", "level the output is held under";
release_ms: 50.0, 1.0, 1000.0, "ms", "how fast it lets go";
}
}
pub struct LimiterEffect {
params: LimiterParams,
reduction_db: f32,
sample_rate: u32,
}
impl LimiterEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: LimiterParams::default(),
reduction_db: 0.0,
sample_rate,
}
}
fn configured(mut self, params: LimiterParams) -> Self {
self.params = params;
self
}
}
impl Effect for LimiterEffect {
effect_common!(LimiterParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channels = channels as usize;
if channels == 0 {
return;
}
let release = time_coefficient(self.params.release_ms, self.sample_rate as f32);
let ceiling = self.params.ceiling_db;
for frame in buffer.chunks_mut(channels) {
let peak = frame
.iter()
.fold(0.0_f32, |loudest, sample| loudest.max(sample.abs()));
let over = to_db(peak) - ceiling;
let wanted = if over > 0.0 { -over } else { 0.0 };
self.reduction_db = if wanted < self.reduction_db {
wanted
} else {
self.reduction_db + release * (wanted - self.reduction_db)
};
let scale = from_db(self.reduction_db);
for sample in frame.iter_mut() {
*sample *= scale;
}
}
}
fn reset(&mut self) {
self.reduction_db = 0.0;
}
}
parameters! {
GateParams {
threshold_db: -40.0, -90.0, 0.0, "dB", "level below which the signal is cut";
attack_ms: 1.0, 0.1, 100.0, "ms", "how fast it opens";
hold_ms: 50.0, 0.0, 1000.0, "ms", "how long it stays open after falling below";
release_ms: 100.0, 1.0, 2000.0, "ms", "how fast it closes";
floor_db: -80.0, -120.0, 0.0, "dB", "level the closed gate attenuates to";
}
}
pub struct GateEffect {
params: GateParams,
gain: f32,
holding: usize,
sample_rate: u32,
}
impl GateEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: GateParams::default(),
gain: 0.0,
holding: 0,
sample_rate,
}
}
fn configured(mut self, params: GateParams) -> Self {
self.params = params;
self
}
}
impl Effect for GateEffect {
effect_common!(GateParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channels = channels as usize;
if channels == 0 {
return;
}
let rate = self.sample_rate as f32;
let attack = time_coefficient(self.params.attack_ms, rate);
let release = time_coefficient(self.params.release_ms, rate);
let hold_frames = (self.params.hold_ms * 0.001 * rate) as usize;
let floor = from_db(self.params.floor_db);
for frame in buffer.chunks_mut(channels) {
let peak = frame
.iter()
.fold(0.0_f32, |loudest, sample| loudest.max(sample.abs()));
if to_db(peak) >= self.params.threshold_db {
self.holding = hold_frames;
} else {
self.holding = self.holding.saturating_sub(1);
}
let open = self.holding > 0;
let coefficient = if open { attack } else { release };
self.gain += coefficient * (if open { 1.0 } else { 0.0 } - self.gain);
let scale = floor + (1.0 - floor) * self.gain;
for sample in frame.iter_mut() {
*sample *= scale;
}
}
}
fn reset(&mut self) {
self.gain = 0.0;
self.holding = 0;
}
}
parameters! {
SaturationParams {
drive: 2.0, 1.0, 50.0, "", "how hard the signal is pushed into the curve";
mix: 1.0, 0.0, 1.0, "", "0 is dry, 1 is fully saturated";
output_db: 0.0, -24.0, 12.0, "dB", "level after the curve";
}
}
pub struct SaturationEffect {
params: SaturationParams,
sample_rate: u32,
}
impl SaturationEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: SaturationParams::default(),
sample_rate,
}
}
fn configured(mut self, params: SaturationParams) -> Self {
self.params = params;
self
}
}
impl Effect for SaturationEffect {
effect_common!(SaturationParams);
fn process(&mut self, buffer: &mut [f32], _channels: u16) {
let drive = self.params.drive;
let normalise = 1.0 / drive.tanh();
let mix = self.params.mix;
let output = from_db(self.params.output_db);
for sample in buffer {
let wet = (*sample * drive).tanh() * normalise;
*sample = (*sample * (1.0 - mix) + wet * mix) * output;
}
}
fn reset(&mut self) {}
}
parameters! {
TremoloParams {
rate_hz: 4.0, 0.05, 20.0, "Hz", "cycles per second";
depth: 0.5, 0.0, 1.0, "", "0 is no modulation, 1 is down to silence";
}
}
pub struct TremoloEffect {
params: TremoloParams,
phase: f32,
sample_rate: u32,
}
impl TremoloEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: TremoloParams::default(),
phase: 0.0,
sample_rate,
}
}
fn configured(mut self, params: TremoloParams) -> Self {
self.params = params;
self
}
}
impl Effect for TremoloEffect {
effect_common!(TremoloParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channels = channels as usize;
if channels == 0 {
return;
}
let step = self.params.rate_hz / self.sample_rate as f32;
let depth = self.params.depth;
for frame in buffer.chunks_mut(channels) {
let lfo = 1.0 - depth * 0.5 * (1.0 - (self.phase * TAU).cos());
for sample in frame.iter_mut() {
*sample *= lfo;
}
self.phase = (self.phase + step).fract();
}
}
fn reset(&mut self) {
self.phase = 0.0;
}
}
parameters! {
DelayParams {
time_ms: 250.0, 1.0, 2000.0, "ms", "how far back the echo comes from";
feedback: 0.35, 0.0, 0.95, "", "how much of the echo is fed back in";
mix: 0.3, 0.0, 1.0, "", "0 is dry, 1 is echo only";
damping_hz: 8000.0, 200.0, 20000.0, "Hz", "each repeat is filtered above this";
}
}
pub struct DelayEffect {
params: DelayParams,
lines: PerChannel<DelayLine>,
dampers: PerChannel<Biquad>,
applied_damping: Option<f32>,
capacity: usize,
sample_rate: u32,
}
impl DelayEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: DelayParams::default(),
lines: PerChannel::new(),
dampers: PerChannel::new(),
applied_damping: None,
capacity: 0,
sample_rate,
}
}
fn configured(mut self, params: DelayParams) -> Self {
self.params = params;
self
}
fn line_frames(&self) -> usize {
let max_ms = DelayParams::schema()
.into_iter()
.find(|spec| spec.name == "time_ms")
.map(|spec| spec.max as f32)
.unwrap_or(2000.0);
(max_ms * 0.001 * self.sample_rate as f32).ceil() as usize
}
}
impl Effect for DelayEffect {
effect_common!(DelayParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channels = channels as usize;
if channels == 0 {
return;
}
let rate = self.sample_rate as f32;
let wanted = self.line_frames();
if self.capacity != wanted {
self.lines.clear();
self.capacity = wanted;
}
let params = self.params;
let delay_frames = params.time_ms * 0.001 * rate;
let lines = self.lines.get(channels);
for line in lines.iter_mut() {
if line.buffer.len() <= wanted {
*line = DelayLine::with_capacity(wanted);
}
}
let dampers = self.dampers.get(channels);
if self.applied_damping != Some(params.damping_hz) {
for damper in dampers.iter_mut() {
damper.low_pass(params.damping_hz, 0.707, rate);
}
self.applied_damping = Some(params.damping_hz);
}
let lines = self.lines.get(channels);
for frame in buffer.chunks_mut(channels) {
for (channel, sample) in frame.iter_mut().enumerate() {
let echo = lines[channel].read(delay_frames);
let damped = self.dampers.slots[channel].process(echo);
lines[channel].push(*sample + damped * params.feedback);
*sample = *sample * (1.0 - params.mix) + echo * params.mix;
}
}
}
fn reset(&mut self) {
self.lines.clear();
self.dampers.clear();
self.applied_damping = None;
}
}
parameters! {
ChorusParams {
rate_hz: 0.8, 0.01, 10.0, "Hz", "how fast the delay is swept";
depth_ms: 3.0, 0.1, 20.0, "ms", "how far the delay is swept";
delay_ms: 12.0, 1.0, 50.0, "ms", "the delay the sweep is centred on";
mix: 0.5, 0.0, 1.0, "", "0 is dry, 1 is the modulated copy only";
spread: 1.0, 0.0, 1.0, "", "how far apart the channels' LFOs are";
}
}
pub struct ChorusEffect {
params: ChorusParams,
lines: PerChannel<DelayLine>,
phase: f32,
capacity: usize,
sample_rate: u32,
}
impl ChorusEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: ChorusParams::default(),
lines: PerChannel::new(),
phase: 0.0,
capacity: 0,
sample_rate,
}
}
fn configured(mut self, params: ChorusParams) -> Self {
self.params = params;
self
}
fn line_frames(&self) -> usize {
let longest_ms = 50.0 + 20.0;
(longest_ms * 0.001 * self.sample_rate as f32).ceil() as usize + 2
}
}
impl Effect for ChorusEffect {
effect_common!(ChorusParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channels = channels as usize;
if channels == 0 {
return;
}
let rate = self.sample_rate as f32;
let wanted = self.line_frames();
if self.capacity != wanted {
self.lines.clear();
self.capacity = wanted;
}
let params = self.params;
let centre = params.delay_ms * 0.001 * rate;
let sweep = params.depth_ms * 0.001 * rate;
let step = params.rate_hz / rate;
let lines = self.lines.get(channels);
for line in lines.iter_mut() {
if line.buffer.len() <= wanted {
*line = DelayLine::with_capacity(wanted);
}
}
for frame in buffer.chunks_mut(channels) {
for (channel, sample) in frame.iter_mut().enumerate() {
let offset = if channels > 1 {
params.spread * channel as f32 / channels as f32
} else {
0.0
};
let lfo = ((self.phase + offset).fract() * TAU).sin();
let delay = (centre + lfo * sweep).max(1.0);
let wet = lines[channel].read(delay);
lines[channel].push(*sample);
*sample = *sample * (1.0 - params.mix) + wet * params.mix;
}
self.phase = (self.phase + step).fract();
}
}
fn reset(&mut self) {
self.lines.clear();
self.phase = 0.0;
}
}
parameters! {
FlangerParams {
rate_hz: 0.3, 0.01, 10.0, "Hz", "how fast the delay is swept";
depth_ms: 2.0, 0.1, 10.0, "ms", "how far the delay is swept";
delay_ms: 3.0, 0.1, 20.0, "ms", "the delay the sweep is centred on";
feedback: 0.5, -0.95, 0.95, "", "resonance; negative inverts the comb";
mix: 0.5, 0.0, 1.0, "", "0 is dry, 1 is the swept copy only";
}
}
pub struct FlangerEffect {
params: FlangerParams,
lines: PerChannel<DelayLine>,
phase: f32,
capacity: usize,
sample_rate: u32,
}
impl FlangerEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: FlangerParams::default(),
lines: PerChannel::new(),
phase: 0.0,
capacity: 0,
sample_rate,
}
}
fn configured(mut self, params: FlangerParams) -> Self {
self.params = params;
self
}
fn line_frames(&self) -> usize {
((20.0 + 10.0) * 0.001 * self.sample_rate as f32).ceil() as usize + 2
}
}
impl Effect for FlangerEffect {
effect_common!(FlangerParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channels = channels as usize;
if channels == 0 {
return;
}
let rate = self.sample_rate as f32;
let wanted = self.line_frames();
if self.capacity != wanted {
self.lines.clear();
self.capacity = wanted;
}
let params = self.params;
let centre = params.delay_ms * 0.001 * rate;
let sweep = params.depth_ms * 0.001 * rate;
let step = params.rate_hz / rate;
let lines = self.lines.get(channels);
for line in lines.iter_mut() {
if line.buffer.len() <= wanted {
*line = DelayLine::with_capacity(wanted);
}
}
for frame in buffer.chunks_mut(channels) {
let lfo = (self.phase * TAU).sin();
let delay = (centre + lfo * sweep).max(1.0);
for (channel, sample) in frame.iter_mut().enumerate() {
let wet = lines[channel].read(delay);
lines[channel].push(*sample + wet * params.feedback);
*sample = *sample * (1.0 - params.mix) + wet * params.mix;
}
self.phase = (self.phase + step).fract();
}
}
fn reset(&mut self) {
self.lines.clear();
self.phase = 0.0;
}
}
parameters! {
ReverbParams {
room_size: 0.5, 0.0, 1.0, "", "how long the tail rings for";
damping: 0.5, 0.0, 1.0, "", "how fast the high end dies away";
mix: 0.3, 0.0, 1.0, "", "0 is dry, 1 is tail only";
width: 1.0, 0.0, 1.0, "", "how far apart the channels' tails are";
pre_delay_ms: 0.0, 0.0, 200.0, "ms", "gap before the tail starts";
}
}
pub struct ReverbEffect {
params: ReverbParams,
channels: PerChannel<ReverbChannel>,
pre_delay: PerChannel<DelayLine>,
sample_rate: u32,
}
#[derive(Clone, Debug, Default)]
struct ReverbChannel {
combs: Vec<Comb>,
allpasses: Vec<AllPass>,
}
#[derive(Clone, Debug, Default)]
struct Comb {
line: DelayLine,
length: usize,
filtered: f32,
}
#[derive(Clone, Debug, Default)]
struct AllPass {
line: DelayLine,
length: usize,
}
const COMB_LENGTHS: [usize; 8] = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
const ALLPASS_LENGTHS: [usize; 4] = [556, 441, 341, 225];
const STEREO_SPREAD: usize = 23;
impl ReverbEffect {
pub fn new(sample_rate: u32) -> Self {
Self {
params: ReverbParams::default(),
channels: PerChannel::new(),
pre_delay: PerChannel::new(),
sample_rate,
}
}
fn configured(mut self, params: ReverbParams) -> Self {
self.params = params;
self
}
fn build_channel(sample_rate: u32, channel: usize) -> ReverbChannel {
let scale = sample_rate as f32 / 44_100.0;
let offset = channel * STEREO_SPREAD;
let sized = |base: usize| -> usize {
(((base + offset) as f32) * scale).round().max(1.0) as usize
};
ReverbChannel {
combs: COMB_LENGTHS
.iter()
.map(|base| {
let length = sized(*base);
Comb {
line: DelayLine::with_capacity(length),
length,
filtered: 0.0,
}
})
.collect(),
allpasses: ALLPASS_LENGTHS
.iter()
.map(|base| {
let length = sized(*base);
AllPass {
line: DelayLine::with_capacity(length),
length,
}
})
.collect(),
}
}
}
impl Effect for ReverbEffect {
effect_common!(ReverbParams);
fn process(&mut self, buffer: &mut [f32], channels: u16) {
let channel_count = channels as usize;
if channel_count == 0 {
return;
}
let rate = self.sample_rate as f32;
let params = self.params;
let feedback = 0.7 + params.room_size * 0.28;
let damping = params.damping * 0.4;
let pre_delay_frames = params.pre_delay_ms * 0.001 * rate;
{
let sample_rate = self.sample_rate;
let slots = self.channels.get(channel_count);
for (channel, slot) in slots.iter_mut().enumerate() {
if slot.combs.is_empty() {
*slot = Self::build_channel(sample_rate, channel);
}
}
}
let pre_frames = ((200.0 * 0.001 * rate) as usize).max(1);
let pre = self.pre_delay.get(channel_count);
for line in pre.iter_mut() {
if line.buffer.len() <= pre_frames {
*line = DelayLine::with_capacity(pre_frames);
}
}
for frame in buffer.chunks_mut(channel_count) {
for (index, sample) in frame.iter_mut().enumerate() {
let dry = *sample;
let input = if pre_delay_frames > 0.0 {
self.pre_delay.slots[index].push(dry);
self.pre_delay.slots[index].read(pre_delay_frames)
} else {
dry
};
let state = &mut self.channels.slots[index];
let mut tail = 0.0;
for comb in state.combs.iter_mut() {
let delayed = comb.line.read(comb.length as f32);
comb.filtered = delayed * (1.0 - damping) + comb.filtered * damping;
comb.line.push(input * 0.015 + comb.filtered * feedback);
tail += delayed;
}
for allpass in state.allpasses.iter_mut() {
let delayed = allpass.line.read(allpass.length as f32);
allpass.line.push(tail + delayed * 0.5);
tail = delayed - tail;
}
*sample = dry * (1.0 - params.mix) + tail * params.mix;
}
if channel_count == 2 && params.width < 1.0 {
let mid = (frame[0] + frame[1]) * 0.5;
let side = (frame[0] - frame[1]) * 0.5 * params.width;
frame[0] = mid + side;
frame[1] = mid - side;
}
}
}
fn reset(&mut self) {
self.channels.clear();
self.pre_delay.clear();
}
}
fn plugin(name: &str, effect: impl Effect) -> Plugin {
Plugin::from_internal(InternalPlugin::from_effect(name, effect))
}
macro_rules! constructors {
($( $(#[$meta:meta])* $function:ident => $kind:literal, $effect:ty; )*) => {
$(
$(#[$meta])*
///
/// Built with its default parameters; set them with
/// [`Plugin::set_params`] or [`Plugin::set_params_str`].
pub fn $function(sample_rate: u32) -> Plugin {
plugin($kind, <$effect>::new(sample_rate))
}
)*
pub fn kinds() -> &'static [&'static str] {
&[$($kind,)*]
}
pub fn create(kind: &str, sample_rate: u32, params: &Params)
-> Result<Plugin, ParamError>
{
let mut plugin = match kind {
$($kind => $function(sample_rate),)*
_ => return Err(ParamError::Unknown {
key: kind.to_string(),
known: kinds().iter().map(|kind| kind.to_string()).collect(),
}),
};
plugin.set_params(params)?;
Ok(plugin)
}
pub fn schema(kind: &str) -> Option<Vec<ParamSpec>> {
match kind {
$($kind => Some(<$effect>::new(48_000).schema()),)*
_ => None,
}
}
};
}
constructors! {
gain => "gain", GainEffect;
pan => "pan", PanEffect;
width => "width", WidthEffect;
filter => "filter", FilterEffect;
eq => "eq", EqEffect;
compressor => "compressor", CompressorEffect;
limiter => "limiter", LimiterEffect;
gate => "gate", GateEffect;
saturation => "saturation", SaturationEffect;
tremolo => "tremolo", TremoloEffect;
delay => "delay", DelayEffect;
chorus => "chorus", ChorusEffect;
flanger => "flanger", FlangerEffect;
reverb => "reverb", ReverbEffect;
}
#[cfg(test)]
mod tests {
use super::*;
fn tone(hz: f32, sample_rate: u32, frames: usize, channels: usize) -> Vec<f32> {
(0..frames)
.flat_map(|frame| {
let value = (TAU * hz * frame as f32 / sample_rate as f32).sin();
(0..channels).map(move |_| value)
})
.collect()
}
fn rms(samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
(samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32).sqrt()
}
fn peak(samples: &[f32]) -> f32 {
samples.iter().fold(0.0_f32, |max, s| max.max(s.abs()))
}
#[test]
fn every_kind_builds_and_has_a_schema() {
for kind in kinds() {
let specs = schema(kind).unwrap_or_else(|| panic!("{kind} has no schema"));
assert!(!specs.is_empty(), "{kind} declares no parameters");
let plugin = create(kind, 48_000, &Params::new())
.unwrap_or_else(|e| panic!("{kind} would not build: {e}"));
assert!(plugin.is_loaded(), "{kind} came back unloaded");
}
}
#[test]
fn every_default_is_inside_its_own_range() {
for kind in kinds() {
for spec in schema(kind).expect("a schema") {
assert!(
spec.default >= spec.min && spec.default <= spec.max,
"{kind}.{} defaults to {}, outside {}..={}",
spec.name,
spec.default,
spec.min,
spec.max
);
}
}
}
#[test]
fn every_parameter_reads_back_what_was_written() {
for kind in kinds() {
for spec in schema(kind).expect("a schema") {
let wanted = match spec.kind {
crate::plugins::ParamKind::Toggle => {
if spec.default == 0.0 { 1.0 } else { 0.0 }
}
crate::plugins::ParamKind::Number => (spec.min + spec.max) / 2.0,
};
let params = Params::new().with(spec.name.clone(), wanted);
let plugin = create(kind, 48_000, ¶ms)
.unwrap_or_else(|e| panic!("{kind}.{}: {e}", spec.name));
let got = plugin
.params()
.number(&spec.name)
.unwrap_or_else(|| panic!("{kind}.{} did not read back", spec.name));
assert!(
(got - wanted).abs() < 1e-4,
"{kind}.{}: wrote {wanted}, read {got}",
spec.name
);
}
}
}
#[test]
fn every_effect_refuses_a_name_it_does_not_have() {
for kind in kinds() {
let params = Params::new().with("definitely_not_a_parameter", 1.0);
let error = create(kind, 48_000, ¶ms).expect_err("{kind} accepted rubbish");
assert!(matches!(error, ParamError::Unknown { .. }), "{kind}: {error}");
}
}
#[test]
fn every_effect_refuses_a_value_out_of_range() {
for kind in kinds() {
for spec in schema(kind).expect("a schema") {
if spec.kind != crate::plugins::ParamKind::Number {
continue;
}
let params = Params::new().with(spec.name.clone(), spec.max + 1.0);
let error = create(kind, 48_000, ¶ms)
.expect_err("should have refused an out-of-range value");
assert!(
matches!(error, ParamError::Range { .. }),
"{kind}.{}: {error}",
spec.name
);
}
}
}
#[test]
fn every_effect_leaves_silence_silent() {
for kind in kinds() {
let mut plugin = create(kind, 48_000, &Params::new()).expect("builds");
let mut buffer = vec![0.0_f32; 2048];
for block in buffer.chunks_mut(256) {
plugin.apply(block, 2).expect("processes");
}
assert!(
peak(&buffer) < 1e-6,
"{kind} produced {} from silence",
peak(&buffer)
);
}
}
#[test]
fn every_effect_stays_finite_and_bounded() {
for kind in kinds() {
let mut params = Params::new();
for spec in schema(kind).expect("a schema") {
params.set(spec.name.clone(), spec.max);
}
let mut plugin = create(kind, 48_000, ¶ms)
.unwrap_or_else(|e| panic!("{kind} at maximum: {e}"));
let mut buffer = tone(220.0, 48_000, 480_000, 2);
for block in buffer.chunks_mut(512) {
plugin.apply(block, 2).expect("processes");
}
assert!(
buffer.iter().all(|s| s.is_finite()),
"{kind} produced a non-finite sample"
);
assert!(
peak(&buffer) < 100.0,
"{kind} reached {}, which is running away",
peak(&buffer)
);
}
}
#[test]
fn every_effect_handles_odd_channel_counts() {
for kind in kinds() {
for channels in [1_u16, 2, 3, 6] {
let mut plugin = create(kind, 48_000, &Params::new()).expect("builds");
let mut buffer = tone(440.0, 48_000, 1024, channels as usize);
plugin
.apply(&mut buffer, channels)
.unwrap_or_else(|e| panic!("{kind} at {channels} channels: {e}"));
assert!(
buffer.iter().all(|s| s.is_finite()),
"{kind} at {channels} channels produced a non-finite sample"
);
}
}
}
#[test]
fn a_clone_keeps_the_parameters_and_drops_the_state() {
let mut original = delay(48_000);
original
.set_params_str("time_ms: 100, feedback: 0.9, mix: 1")
.expect("valid");
let mut buffer = tone(440.0, 48_000, 48_000, 2);
original.apply(&mut buffer, 2).expect("processes");
let mut copy = original.clone();
assert_eq!(copy.params().number("time_ms"), Some(100.0));
let feedback = copy.params().number("feedback").expect("set above");
assert!((feedback - 0.9).abs() < 1e-6, "got {feedback}");
let mut silence = vec![0.0_f32; 4096];
copy.apply(&mut silence, 2).expect("processes");
assert!(peak(&silence) < 1e-6, "the clone carried state over");
}
#[test]
fn gain_scales_by_the_decibels_asked_for() {
let mut plugin = gain(48_000);
plugin.set_params_str("gain_db: -6").expect("valid");
let mut buffer = vec![1.0_f32; 64];
plugin.apply(&mut buffer, 2).expect("processes");
assert!((buffer[0] - 0.5012).abs() < 1e-3, "got {}", buffer[0]);
}
#[test]
fn gain_mutes() {
let mut plugin = gain(48_000);
plugin.set_params_str("mute: true").expect("valid");
let mut buffer = vec![1.0_f32; 64];
plugin.apply(&mut buffer, 2).expect("processes");
assert_eq!(peak(&buffer), 0.0);
}
#[test]
fn pan_holds_power_constant_across_the_sweep() {
for (position, expected_left) in [(-1.0, 1.0), (0.0, 0.707), (1.0, 0.0)] {
let mut plugin = pan(48_000);
plugin
.set_params(&Params::new().with("pan", position))
.expect("valid");
let mut buffer = vec![1.0_f32; 2];
plugin.apply(&mut buffer, 2).expect("processes");
assert!(
(buffer[0] - expected_left).abs() < 1e-2,
"at {position} the left channel was {}",
buffer[0]
);
let power = buffer[0] * buffer[0] + buffer[1] * buffer[1];
assert!((power - 1.0).abs() < 1e-3, "power was {power} at {position}");
}
}
#[test]
fn width_collapses_to_mono_at_zero() {
let mut plugin = width(48_000);
plugin.set_params_str("width: 0").expect("valid");
let mut buffer = vec![1.0_f32, -1.0, 0.5, 0.1];
plugin.apply(&mut buffer, 2).expect("processes");
assert!((buffer[0] - buffer[1]).abs() < 1e-6, "channels differ");
assert!((buffer[2] - buffer[3]).abs() < 1e-6, "channels differ");
}
#[test]
fn a_low_pass_keeps_the_low_and_loses_the_high() {
let quiet = |hz: f32| {
let mut plugin = filter(48_000);
plugin
.set_params_str("cutoff_hz: 1000, resonance: 0.707")
.expect("valid");
let mut buffer = tone(hz, 48_000, 48_000, 1);
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
rms(&buffer[4800..])
};
let low = quiet(100.0);
let high = quiet(10_000.0);
assert!(low > 0.6, "100 Hz should pass, rms was {low}");
assert!(high < 0.05, "10 kHz should not, rms was {high}");
}
#[test]
fn a_high_pass_does_the_opposite() {
let quiet = |hz: f32| {
let mut plugin = filter(48_000);
plugin
.set_params_str("cutoff_hz: 1000, high_pass: true")
.expect("valid");
let mut buffer = tone(hz, 48_000, 48_000, 1);
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
rms(&buffer[4800..])
};
assert!(quiet(100.0) < 0.05, "100 Hz should not pass");
assert!(quiet(10_000.0) > 0.6, "10 kHz should");
}
#[test]
fn eq_lifts_and_cuts_the_band_it_is_pointed_at() {
let at = |gain_db: f64| {
let mut plugin = eq(48_000);
plugin
.set_params(
&Params::new()
.with("mid_freq_hz", 1000.0)
.with("mid_gain_db", gain_db)
.with("mid_q", 2.0),
)
.expect("valid");
let mut buffer = tone(1000.0, 48_000, 48_000, 1);
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
rms(&buffer[4800..])
};
let flat = at(0.0);
let boosted = at(12.0);
let cut = at(-12.0);
assert!((flat - 0.707).abs() < 0.02, "flat should pass, got {flat}");
assert!(boosted > flat * 3.0, "+12 dB gave {boosted} against {flat}");
assert!(cut < flat / 3.0, "-12 dB gave {cut} against {flat}");
}
#[test]
fn the_compressor_reduces_what_is_over_the_threshold() {
let mut plugin = compressor(48_000);
plugin
.set_params_str(
"threshold_db: -20, ratio: 8, attack_ms: 1, release_ms: 50, knee_db: 0",
)
.expect("valid");
let loud = from_db(-6.0);
let mut buffer: Vec<f32> = tone(440.0, 48_000, 48_000, 1)
.iter()
.map(|s| s * loud)
.collect();
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
let out = to_db(peak(&buffer[24_000..]));
assert!(
(out - -18.25).abs() < 2.0,
"expected about -18 dB out, got {out}"
);
}
#[test]
fn the_compressor_leaves_quiet_signal_alone() {
let mut plugin = compressor(48_000);
plugin
.set_params_str("threshold_db: -20, ratio: 8, knee_db: 0, makeup_db: 0")
.expect("valid");
let quiet = from_db(-40.0);
let mut buffer: Vec<f32> = tone(440.0, 48_000, 48_000, 1)
.iter()
.map(|s| s * quiet)
.collect();
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
let out = to_db(peak(&buffer[24_000..]));
assert!((out - -40.0).abs() < 0.5, "expected -40 dB out, got {out}");
}
#[test]
fn the_limiter_holds_the_ceiling() {
let mut plugin = limiter(48_000);
plugin
.set_params_str("ceiling_db: -3, release_ms: 20")
.expect("valid");
let mut buffer: Vec<f32> = tone(440.0, 48_000, 48_000, 1).iter().map(|s| s * 4.0).collect();
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
let out = to_db(peak(&buffer[2400..]));
assert!(out <= -3.0 + 0.5, "expected under -3 dB, got {out}");
}
#[test]
fn the_gate_cuts_what_is_under_the_threshold() {
let mut plugin = gate(48_000);
plugin
.set_params_str("threshold_db: -30, attack_ms: 1, hold_ms: 5, release_ms: 10")
.expect("valid");
let quiet = from_db(-60.0);
let mut buffer: Vec<f32> = tone(440.0, 48_000, 48_000, 1)
.iter()
.map(|s| s * quiet)
.collect();
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
let out = to_db(peak(&buffer[24_000..]));
assert!(out < -100.0, "a closed gate should be quiet, got {out}");
}
#[test]
fn the_gate_passes_what_is_over_it() {
let mut plugin = gate(48_000);
plugin
.set_params_str("threshold_db: -30, attack_ms: 1, hold_ms: 5")
.expect("valid");
let mut buffer = tone(440.0, 48_000, 48_000, 1);
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
let out = to_db(peak(&buffer[24_000..]));
assert!(out > -1.0, "an open gate should pass, got {out}");
}
#[test]
fn saturation_cannot_exceed_unity() {
let mut plugin = saturation(48_000);
plugin.set_params_str("drive: 50, mix: 1").expect("valid");
let mut buffer: Vec<f32> = tone(440.0, 48_000, 4800, 1).iter().map(|s| s * 20.0).collect();
plugin.apply(&mut buffer, 1).expect("processes");
assert!(peak(&buffer) <= 1.0001, "reached {}", peak(&buffer));
}
#[test]
fn tremolo_modulates_over_its_cycle() {
let mut plugin = tremolo(48_000);
plugin.set_params_str("rate_hz: 1, depth: 1").expect("valid");
let mut buffer = vec![1.0_f32; 48_000];
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
assert!(peak(&buffer) > 0.99, "never reached the top");
assert!(
buffer.iter().any(|s| s.abs() < 0.01),
"never reached the bottom"
);
}
#[test]
fn a_delay_repeats_after_the_time_asked_for() {
let mut plugin = delay(48_000);
plugin
.set_params_str("time_ms: 100, feedback: 0, mix: 1, damping_hz: 20000")
.expect("valid");
let mut buffer = vec![0.0_f32; 48_000];
buffer[0] = 1.0;
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
let echo = peak(&buffer[4790..4815]);
assert!(echo > 0.5, "no echo at 100 ms, peak was {echo}");
let before = peak(&buffer[100..4700]);
assert!(before < 0.01, "something arrived early: {before}");
}
#[test]
fn delay_feedback_decays() {
let mut plugin = delay(48_000);
plugin
.set_params_str("time_ms: 50, feedback: 0.5, mix: 1, damping_hz: 20000")
.expect("valid");
let mut buffer = vec![0.0_f32; 48_000];
buffer[0] = 1.0;
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
let first = peak(&buffer[2390..2415]);
let second = peak(&buffer[4790..4815]);
let third = peak(&buffer[7190..7215]);
assert!(first > second && second > third, "{first} {second} {third}");
assert!(second > 0.05, "the second repeat vanished: {second}");
}
#[test]
fn chorus_and_flanger_change_the_signal_without_destroying_it() {
for (name, mut plugin) in [
("chorus", chorus(48_000)),
("flanger", flanger(48_000)),
] {
let dry = tone(440.0, 48_000, 48_000, 2);
let mut wet = dry.clone();
for block in wet.chunks_mut(512) {
plugin.apply(block, 2).expect("processes");
}
let changed = wet[24_000..]
.iter()
.zip(&dry[24_000..])
.any(|(a, b)| (a - b).abs() > 0.01);
assert!(changed, "{name} left the signal alone");
let level = rms(&wet[24_000..]);
assert!(
level > 0.1 && level < 2.0,
"{name} came out at {level}, which is not a mix of the input"
);
}
}
#[test]
fn reverb_rings_on_after_the_input_stops() {
let mut plugin = reverb(48_000);
plugin
.set_params_str("room_size: 0.9, damping: 0.2, mix: 1")
.expect("valid");
let mut buffer = vec![0.0_f32; 96_000];
for (index, sample) in buffer[..4800].iter_mut().enumerate() {
*sample = (index as f32 * 0.37).sin() * 0.5;
}
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
let tail = rms(&buffer[28_800..38_400]);
assert!(tail > 1e-4, "no tail, rms was {tail}");
let later = rms(&buffer[76_800..86_400]);
assert!(later < tail, "tail grew: {tail} then {later}");
}
#[test]
fn reverb_pre_delay_holds_the_tail_back() {
let mut plugin = reverb(48_000);
plugin
.set_params_str("room_size: 0.8, mix: 1, pre_delay_ms: 100")
.expect("valid");
let mut buffer = vec![0.0_f32; 48_000];
buffer[0] = 1.0;
for block in buffer.chunks_mut(512) {
plugin.apply(block, 1).expect("processes");
}
let early = peak(&buffer[100..4000]);
let after = peak(&buffer[4800..20_000]);
assert!(early < after, "early {early} was not quieter than {after}");
}
#[test]
fn create_builds_from_a_name_and_a_parameter_string() {
let params = Params::parse("time_ms: 375, feedback: 0.4, mix: 0.3").expect("valid");
let plugin = create("delay", 48_000, ¶ms).expect("builds");
assert_eq!(plugin.params().number("time_ms"), Some(375.0));
assert_eq!(plugin.name, "delay");
}
#[test]
fn create_refuses_a_kind_it_does_not_have() {
let error = create("phaser", 48_000, &Params::new()).expect_err("no phaser yet");
let message = error.to_string();
assert!(message.contains("phaser"), "{message}");
assert!(message.contains("chorus"), "{message}");
}
#[test]
fn parameters_can_be_changed_after_the_plugin_is_built() {
let mut plugin = compressor(48_000);
assert_eq!(plugin.params().number("ratio"), Some(4.0));
plugin.set_param("ratio", 10.0).expect("valid");
assert_eq!(plugin.params().number("ratio"), Some(10.0));
assert_eq!(plugin.params().number("threshold_db"), Some(-18.0));
}
#[test]
fn a_failed_set_changes_nothing() {
let mut plugin = compressor(48_000);
let before = plugin.params();
for params in [
Params::new().with("ratio", 10.0).with("nonsense", 1.0),
Params::new().with("aaa_nonsense", 1.0).with("ratio", 10.0),
] {
assert!(plugin.set_params(¶ms).is_err());
assert_eq!(plugin.params(), before, "a rejected set was partly applied");
}
let params = Params::new().with("ratio", 10.0).with("threshold_db", 500.0);
assert!(plugin.set_params(¶ms).is_err());
assert_eq!(plugin.params(), before);
}
}