use crate::{utils::DelayLine, AudioEffect};
#[derive(Debug, Clone)]
pub struct PingPongConfig {
pub delay_ms: f32,
pub feedback: f32,
pub wet: f32,
pub dry: f32,
pub width: f32,
}
impl Default for PingPongConfig {
fn default() -> Self {
Self {
delay_ms: 375.0, feedback: 0.5,
wet: 0.5,
dry: 0.5,
width: 1.0,
}
}
}
impl PingPongConfig {
#[must_use]
pub fn new(delay_ms: f32, feedback: f32, wet: f32) -> Self {
Self {
delay_ms: delay_ms.max(0.0),
feedback: feedback.clamp(0.0, 0.99),
wet: wet.clamp(0.0, 1.0),
dry: (1.0 - wet).clamp(0.0, 1.0),
width: 1.0,
}
}
#[must_use]
pub fn with_width(mut self, width: f32) -> Self {
self.width = width.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn fast() -> Self {
Self::new(200.0, 0.4, 0.4)
}
#[must_use]
pub fn medium() -> Self {
Self::new(375.0, 0.5, 0.5)
}
#[must_use]
pub fn slow() -> Self {
Self::new(600.0, 0.6, 0.6)
}
}
pub struct PingPongDelay {
delay_l: DelayLine,
delay_r: DelayLine,
delay_samples: usize,
config: PingPongConfig,
sample_rate: f32,
}
impl PingPongDelay {
#[must_use]
pub fn new(config: PingPongConfig, sample_rate: f32) -> Self {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let max_delay_samples = ((2000.0 * sample_rate) / 1000.0) as usize;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let delay_samples = ((config.delay_ms * sample_rate) / 1000.0) as usize;
Self {
delay_l: DelayLine::new(max_delay_samples),
delay_r: DelayLine::new(max_delay_samples),
delay_samples,
config,
sample_rate,
}
}
pub fn set_delay_ms(&mut self, delay_ms: f32) {
self.config.delay_ms = delay_ms.max(0.0);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let delay_samp = ((delay_ms * self.sample_rate) / 1000.0) as usize;
self.delay_samples = delay_samp.min(self.delay_l.max_delay());
}
pub fn set_feedback(&mut self, feedback: f32) {
self.config.feedback = feedback.clamp(0.0, 0.99);
}
pub fn set_wet(&mut self, wet: f32) {
self.config.wet = wet.clamp(0.0, 1.0);
}
pub fn set_dry(&mut self, dry: f32) {
self.config.dry = dry.clamp(0.0, 1.0);
}
pub fn set_width(&mut self, width: f32) {
self.config.width = width.clamp(0.0, 1.0);
}
fn process_sample_internal(&mut self, input_l: f32, input_r: f32) -> (f32, f32) {
let delayed_l = self.delay_l.read(self.delay_samples);
let delayed_r = self.delay_r.read(self.delay_samples);
let feedback = self.config.feedback;
self.delay_l.write(input_l + delayed_r * feedback);
self.delay_r.write(input_r + delayed_l * feedback);
let width = self.config.width;
let mid = (delayed_l + delayed_r) * 0.5;
let side = (delayed_l - delayed_r) * 0.5 * width;
let wet_l = mid + side;
let wet_r = mid - side;
let out_l = wet_l * self.config.wet + input_l * self.config.dry;
let out_r = wet_r * self.config.wet + input_r * self.config.dry;
(out_l, out_r)
}
}
impl AudioEffect for PingPongDelay {
const EFFECT_ID: &'static str = "ping_pong_delay";
fn process_sample(&mut self, input: f32) -> f32 {
let (left, _right) = self.process_sample_internal(input, input);
left
}
fn process_sample_stereo(&mut self, left: f32, right: f32) -> (f32, f32) {
self.process_sample_internal(left, right)
}
fn reset(&mut self) {
self.delay_l.clear();
self.delay_r.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pingpong_config() {
let config = PingPongConfig::default();
assert_eq!(config.delay_ms, 375.0);
assert_eq!(config.feedback, 0.5);
}
#[test]
fn test_pingpong_presets() {
let fast = PingPongConfig::fast();
assert!(fast.delay_ms < 300.0);
let slow = PingPongConfig::slow();
assert!(slow.delay_ms > 500.0);
}
#[test]
fn test_pingpong_delay() {
let config = PingPongConfig::default();
let mut delay = PingPongDelay::new(config, 48000.0);
let (out_l, out_r) = delay.process_sample_stereo(1.0, 0.0);
assert!(out_l > out_r);
for _ in 0..1000 {
delay.process_sample_stereo(0.0, 0.0);
}
let (echo_l, echo_r) = delay.process_sample_stereo(0.0, 0.0);
assert!(echo_l.is_finite());
assert!(echo_r.is_finite());
}
#[test]
fn test_pingpong_width() {
let config = PingPongConfig::default().with_width(0.0);
let mut delay = PingPongDelay::new(config, 48000.0);
delay.process_sample_stereo(1.0, 0.0);
for _ in 0..20000 {
delay.process_sample_stereo(0.0, 0.0);
}
let (out_l, out_r) = delay.process_sample_stereo(0.0, 0.0);
assert!((out_l - out_r).abs() < 0.1);
}
#[test]
fn test_pingpong_reset() {
let config = PingPongConfig::default();
let mut delay = PingPongDelay::new(config, 48000.0);
for _ in 0..1000 {
delay.process_sample_stereo(1.0, 1.0);
}
delay.reset();
let (out_l, out_r) = delay.process_sample_stereo(0.0, 0.0);
assert!(out_l.abs() < 0.01);
assert!(out_r.abs() < 0.01);
}
}