#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod analog_delay;
pub mod auto_pan;
pub mod barrel_lens;
pub mod bass_enhancer;
pub mod bitcrusher;
pub mod blend;
pub mod chorus;
pub mod chorus_flanger;
pub mod color_grade;
pub mod composite;
pub mod compressor;
pub mod compressor_look;
pub mod deesser;
pub mod delay;
pub mod delay_line;
pub mod distort;
pub mod distortion;
pub mod ducking;
pub mod dynamics;
pub mod eq;
pub mod filter;
pub mod filter_bank;
pub mod flanger;
pub mod glitch;
pub mod harmonic_exciter;
pub mod keying;
pub mod lookahead_limiter;
pub mod lufs_meter;
pub mod luma_key;
pub mod mix;
pub mod modulation;
pub mod multiband_compressor;
pub mod parametric_eq;
pub mod pitch;
pub mod reverb;
pub mod reverb_hall;
pub mod ring_mod;
pub mod room_reverb;
pub mod saturation;
pub mod spatial_audio;
pub mod stereo_upmix;
pub mod stereo_widener;
pub mod stereo_wider;
pub mod tape_echo;
pub mod tape_sat;
pub mod time_stretch;
pub mod transient_shaper;
pub mod tremolo;
pub mod utils;
pub mod vibrato;
pub mod video;
pub mod vocoder;
pub mod warp;
pub mod waveshaper;
pub mod wet_dry;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum EffectError {
#[error("Invalid parameter: {0}")]
InvalidParameter(String),
#[error("Invalid sample rate: {0}")]
InvalidSampleRate(f32),
#[error("Buffer size mismatch: expected {expected}, got {actual}")]
BufferSizeMismatch {
expected: usize,
actual: usize,
},
#[error("Insufficient buffer size: need at least {required}, got {actual}")]
InsufficientBuffer {
required: usize,
actual: usize,
},
#[error("Effect not initialized")]
NotInitialized,
#[error("Processing error: {0}")]
ProcessingError(String),
}
pub type Result<T> = std::result::Result<T, EffectError>;
pub trait AudioEffect {
fn process_sample(&mut self, input: f32) -> f32;
fn process(&mut self, buffer: &mut [f32]) {
for sample in buffer {
*sample = self.process_sample(*sample);
}
}
fn process_stereo(&mut self, left: &mut [f32], right: &mut [f32]) {
let len = left.len().min(right.len());
for i in 0..len {
let (l, r) = self.process_sample_stereo(left[i], right[i]);
left[i] = l;
right[i] = r;
}
}
fn process_sample_stereo(&mut self, left: f32, right: f32) -> (f32, f32) {
(self.process_sample(left), self.process_sample(right))
}
fn reset(&mut self);
fn latency_samples(&self) -> usize {
0
}
fn set_sample_rate(&mut self, _sample_rate: f32) {}
fn set_wet_dry(&mut self, _wet: f32) {}
fn wet_dry(&self) -> f32 {
1.0
}
fn set_wet_mix(&mut self, wet: f32) {
self.set_wet_dry(wet);
}
fn wet_mix(&self) -> f32 {
self.wet_dry()
}
fn process_with_wet_dry(&mut self, input: &[f32], output: &mut [f32], wet: f32) {
let wet = wet.clamp(0.0, 1.0);
let dry = 1.0 - wet;
let len = input.len().min(output.len());
for i in 0..len {
let processed = self.process_sample(input[i]);
output[i] = processed * wet + input[i] * dry;
}
}
}
pub struct WetDryWrapper<E: AudioEffect> {
inner: E,
wet: f32,
dry: f32,
}
impl<E: AudioEffect> WetDryWrapper<E> {
#[must_use]
pub fn new(inner: E, wet: f32) -> Self {
let wet = wet.clamp(0.0, 1.0);
Self {
inner,
wet,
dry: 1.0 - wet,
}
}
#[must_use]
pub fn inner(&self) -> &E {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut E {
&mut self.inner
}
#[must_use]
pub fn into_inner(self) -> E {
self.inner
}
}
impl<E: AudioEffect> AudioEffect for WetDryWrapper<E> {
fn process_sample(&mut self, input: f32) -> f32 {
let wet_out = self.inner.process_sample(input);
wet_out * self.wet + input * self.dry
}
fn process_sample_stereo(&mut self, left: f32, right: f32) -> (f32, f32) {
let (wl, wr) = self.inner.process_sample_stereo(left, right);
(
wl * self.wet + left * self.dry,
wr * self.wet + right * self.dry,
)
}
fn reset(&mut self) {
self.inner.reset();
}
fn latency_samples(&self) -> usize {
self.inner.latency_samples()
}
fn set_sample_rate(&mut self, sample_rate: f32) {
self.inner.set_sample_rate(sample_rate);
}
fn set_wet_dry(&mut self, wet: f32) {
self.wet = wet.clamp(0.0, 1.0);
self.dry = 1.0 - self.wet;
}
fn wet_dry(&self) -> f32 {
self.wet
}
}
#[derive(Debug, Clone)]
pub struct ReverbConfig {
pub room_size: f32,
pub damping: f32,
pub wet: f32,
pub dry: f32,
pub width: f32,
pub predelay_ms: f32,
}
impl Default for ReverbConfig {
fn default() -> Self {
Self {
room_size: 0.5,
damping: 0.5,
wet: 0.33,
dry: 0.67,
width: 1.0,
predelay_ms: 0.0,
}
}
}
impl ReverbConfig {
#[must_use]
pub fn new(room_size: f32, damping: f32, wet: f32) -> Self {
Self {
room_size: room_size.clamp(0.0, 1.0),
damping: damping.clamp(0.0, 1.0),
wet: wet.clamp(0.0, 1.0),
dry: (1.0 - wet).clamp(0.0, 1.0),
width: 1.0,
predelay_ms: 0.0,
}
}
#[must_use]
pub fn with_room_size(mut self, room_size: f32) -> Self {
self.room_size = room_size.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_damping(mut self, damping: f32) -> Self {
self.damping = damping.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_wet(mut self, wet: f32) -> Self {
self.wet = wet.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_dry(mut self, dry: f32) -> Self {
self.dry = dry.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_width(mut self, width: f32) -> Self {
self.width = width.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_predelay(mut self, predelay_ms: f32) -> Self {
self.predelay_ms = predelay_ms.max(0.0);
self
}
#[must_use]
pub fn small_room() -> Self {
Self::new(0.3, 0.4, 0.2)
}
#[must_use]
pub fn medium_room() -> Self {
Self::new(0.5, 0.5, 0.3)
}
#[must_use]
pub fn hall() -> Self {
Self::new(0.8, 0.6, 0.4).with_predelay(20.0)
}
#[must_use]
pub fn cathedral() -> Self {
Self::new(0.95, 0.7, 0.5).with_predelay(40.0)
}
#[must_use]
pub fn chamber() -> Self {
Self::new(0.6, 0.4, 0.35).with_predelay(10.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reverb_config_defaults() {
let config = ReverbConfig::default();
assert_eq!(config.room_size, 0.5);
assert_eq!(config.damping, 0.5);
assert_eq!(config.wet, 0.33);
}
#[test]
fn test_reverb_config_builder() {
let config = ReverbConfig::default()
.with_room_size(0.8)
.with_damping(0.6)
.with_wet(0.4);
assert_eq!(config.room_size, 0.8);
assert_eq!(config.damping, 0.6);
assert_eq!(config.wet, 0.4);
}
#[test]
fn test_reverb_config_clamping() {
let config = ReverbConfig::new(1.5, -0.5, 2.0);
assert_eq!(config.room_size, 1.0);
assert_eq!(config.damping, 0.0);
assert_eq!(config.wet, 1.0);
}
#[test]
fn test_reverb_presets() {
let small = ReverbConfig::small_room();
assert!(small.room_size < 0.5);
let hall = ReverbConfig::hall();
assert!(hall.room_size > 0.7);
assert!(hall.predelay_ms > 0.0);
}
}
#[cfg(test)]
mod wet_dry_tests {
use super::*;
use crate::chorus::{ChorusParams, ChorusProcessor};
use crate::distortion::fuzz::{Fuzz, FuzzConfig};
use crate::distortion::overdrive::{Overdrive, OverdriveConfig};
use crate::flanger::{Flanger, FlangerConfig};
use crate::reverb::Freeverb;
#[test]
fn test_wrapper_wet_zero_returns_dry() {
let mut wrapped = WetDryWrapper::new(Fuzz::new(FuzzConfig::default()), 0.0);
let out = wrapped.process_sample(0.5);
assert!(
(out - 0.5).abs() < 1e-5,
"wet=0 should return dry signal, got {out}"
);
}
#[test]
fn test_wrapper_wet_one_returns_processed() {
let inner = Fuzz::new(FuzzConfig {
fuzz: 1.0,
level: 1.0,
});
let mut wrapped = WetDryWrapper::new(inner, 1.0);
let out = wrapped.process_sample(0.5);
assert!(out.is_finite());
}
#[test]
fn test_wrapper_wet_half_blends() {
let inner = Fuzz::new(FuzzConfig {
fuzz: 100.0,
level: 1.0,
});
let mut wrapped = WetDryWrapper::new(inner, 0.5);
let out = wrapped.process_sample(0.5);
assert!((out - 0.75).abs() < 1e-5, "blend mismatch: got {out}");
}
#[test]
fn test_wrapper_set_wet_dry_updates() {
let inner = Fuzz::new(FuzzConfig::default());
let mut wrapped = WetDryWrapper::new(inner, 0.3);
assert!((wrapped.wet_dry() - 0.3).abs() < 1e-5);
wrapped.set_wet_dry(0.8);
assert!((wrapped.wet_dry() - 0.8).abs() < 1e-5);
}
#[test]
fn test_wrapper_set_wet_dry_clamps() {
let inner = Fuzz::new(FuzzConfig::default());
let mut wrapped = WetDryWrapper::new(inner, 0.5);
wrapped.set_wet_dry(2.0);
assert!((wrapped.wet_dry() - 1.0).abs() < 1e-5);
wrapped.set_wet_dry(-1.0);
assert!((wrapped.wet_dry() - 0.0).abs() < 1e-5);
}
#[test]
fn test_process_with_wet_dry_zero_equals_input() {
let mut fuzz = Fuzz::new(FuzzConfig {
fuzz: 100.0,
level: 1.0,
});
let input = vec![0.3_f32, -0.5, 0.7];
let mut output = vec![0.0_f32; 3];
fuzz.process_with_wet_dry(&input, &mut output, 0.0);
for (i, (&inp, &out)) in input.iter().zip(output.iter()).enumerate() {
assert!(
(out - inp).abs() < 1e-5,
"output[{i}]={out} should equal input {inp}"
);
}
}
#[test]
fn test_process_with_wet_dry_one_equals_processed() {
let mut fuzz = Fuzz::new(FuzzConfig {
fuzz: 1.0,
level: 1.0,
});
let input = vec![0.3_f32, -0.4, 0.2];
let mut output = vec![0.0_f32; 3];
fuzz.process_with_wet_dry(&input, &mut output, 1.0);
for &s in &output {
assert!(s.is_finite());
}
}
#[test]
fn test_process_with_wet_dry_half_blends() {
let mut fuzz = Fuzz::new(FuzzConfig {
fuzz: 100.0,
level: 1.0,
});
let input = vec![0.5_f32];
let mut output = vec![0.0_f32; 1];
fuzz.process_with_wet_dry(&input, &mut output, 0.5);
assert!((output[0] - 0.75).abs() < 0.01, "blend={}", output[0]);
}
#[test]
fn test_overdrive_wet_dry_default_is_one() {
let od = Overdrive::new(OverdriveConfig::default());
assert!((od.wet_dry() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_overdrive_wet_zero_passes_dry() {
let mut od = Overdrive::new(OverdriveConfig::default());
od.set_wet_dry(0.0);
let out = od.process_sample(0.4);
assert!((out - 0.4).abs() < 1e-5, "dry pass-through failed: {out}");
}
#[test]
fn test_overdrive_set_wet_dry_clamps() {
let mut od = Overdrive::new(OverdriveConfig::default());
od.set_wet_dry(5.0);
assert!((od.wet_dry() - 1.0).abs() < f32::EPSILON);
od.set_wet_dry(-2.0);
assert!((od.wet_dry() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_fuzz_wet_zero_passes_dry() {
let mut f = Fuzz::new(FuzzConfig::default());
f.set_wet_dry(0.0);
let out = f.process_sample(0.6);
assert!((out - 0.6).abs() < 1e-5, "fuzz dry failed: {out}");
}
#[test]
fn test_fuzz_wet_one_full_effect() {
let mut f = Fuzz::new(FuzzConfig {
fuzz: 50.0,
level: 1.0,
});
f.set_wet_dry(1.0);
let out = f.process_sample(0.5);
assert!((out - 1.0).abs() < 1e-5, "full wet fuzz: {out}");
}
#[test]
fn test_flanger_wet_dry_stores() {
let mut fl = Flanger::new(FlangerConfig::default(), 48_000.0);
fl.set_wet_dry(0.3);
assert!((fl.wet_dry() - 0.3).abs() < 1e-5);
}
#[test]
fn test_flanger_wet_zero_bypasses() {
let mut fl = Flanger::new(
FlangerConfig {
feedback: 0.0,
..FlangerConfig::default()
},
48_000.0,
);
fl.set_wet_dry(0.0);
let out = fl.process_sample(0.5);
assert!((out - 0.5).abs() < 1e-5, "flanger dry bypass: {out}");
}
#[test]
fn test_chorus_wet_zero_passes_dry() {
let mut cp = ChorusProcessor::new(48_000.0, ChorusParams::default());
cp.set_wet_dry(0.0);
let out: f32 = crate::AudioEffect::process_sample(&mut cp, 0.7);
assert!((out - 0.7).abs() < 1e-4, "chorus dry: {out}");
}
#[test]
fn test_chorus_wet_dry_stores() {
let mut cp = ChorusProcessor::new(48_000.0, ChorusParams::default());
cp.set_wet_dry(0.6);
assert!((cp.wet_dry() - 0.6).abs() < 1e-5);
}
#[test]
fn test_freeverb_wet_dry_stores() {
let mut rv = Freeverb::new(ReverbConfig::default(), 48_000.0);
rv.set_wet_dry(0.5);
assert!((rv.wet_dry() - 0.5).abs() < 1e-5);
}
#[test]
fn test_freeverb_wet_dry_clamps() {
let mut rv = Freeverb::new(ReverbConfig::default(), 48_000.0);
rv.set_wet_dry(2.0);
assert!((rv.wet_dry() - 1.0).abs() < f32::EPSILON);
rv.set_wet_dry(-1.0);
assert!((rv.wet_dry() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_freeverb_wet_zero_output_is_dry() {
let mut rv = Freeverb::new(ReverbConfig::default(), 48_000.0);
rv.set_wet_dry(0.0);
let out = rv.process_sample(1.0);
assert!(
out.is_finite(),
"freeverb wet=0 should produce finite output"
);
assert!(
(out - 1.0).abs() < 0.05,
"freeverb wet=0: expected ~1.0, got {out}"
);
}
}