1use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
13use std::f32::consts::TAU;
14
15const MAX_DELAY_SAMPLES: usize = 2048;
17
18pub struct Chorus {
19 buf_l: Box<[f32; MAX_DELAY_SAMPLES]>,
21 buf_r: Box<[f32; MAX_DELAY_SAMPLES]>,
23 write_pos: usize,
25 phase_l: f32,
27 phase_r: f32,
29}
30
31impl Chorus {
32 pub fn new() -> Self {
33 Self {
34 buf_l: Box::new([0.0f32; MAX_DELAY_SAMPLES]),
35 buf_r: Box::new([0.0f32; MAX_DELAY_SAMPLES]),
36 write_pos: 0,
37 phase_l: 0.0,
38 phase_r: 0.25, }
40 }
41
42 #[inline(always)]
44 fn read_interp(buf: &[f32; MAX_DELAY_SAMPLES], write_pos: usize, delay_samples: f32) -> f32 {
45 let delay_int = delay_samples as usize;
46 let frac = delay_samples - delay_int as f32;
47
48 let pos0 = (write_pos + MAX_DELAY_SAMPLES - delay_int) % MAX_DELAY_SAMPLES;
49 let pos1 = (write_pos + MAX_DELAY_SAMPLES - delay_int - 1) % MAX_DELAY_SAMPLES;
50
51 buf[pos0] * (1.0 - frac) + buf[pos1] * frac
52 }
53}
54
55impl Default for Chorus {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl DspNode for Chorus {
62 fn process(
63 &mut self,
64 inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
65 output: &mut [f32; BUFFER_SIZE],
66 params: &mut ParamBlock,
67 sample_rate: f32,
68 ) {
69 let silence = [0.0f32; BUFFER_SIZE];
70 let input = inputs[0].unwrap_or(&silence);
71
72 let rate = params.get(0).current.clamp(0.1, 10.0);
73 let depth = params.get(1).current.clamp(0.0, 1.0);
74 let feedback = params.get(2).current.clamp(0.0, 0.95);
75 let wet = params.get(3).current.clamp(0.0, 1.0);
76
77 let phase_inc = rate / sample_rate;
78
79 let base_delay = 0.005 * sample_rate;
81 let mod_depth = depth * 0.010 * sample_rate;
82
83 for (i, out) in output.iter_mut().enumerate() {
84 let dry = input[i];
85
86 let lfo_l = (self.phase_l * TAU).sin();
88 let lfo_r = (self.phase_r * TAU).sin();
89
90 let delay_l =
91 (base_delay + lfo_l * mod_depth).clamp(1.0, (MAX_DELAY_SAMPLES - 2) as f32);
92 let delay_r =
93 (base_delay + lfo_r * mod_depth).clamp(1.0, (MAX_DELAY_SAMPLES - 2) as f32);
94
95 let delayed_l = Self::read_interp(&self.buf_l, self.write_pos, delay_l);
97 let delayed_r = Self::read_interp(&self.buf_r, self.write_pos, delay_r);
98
99 self.buf_l[self.write_pos] = dry + delayed_l * feedback;
101 self.buf_r[self.write_pos] = dry + delayed_r * feedback;
102
103 self.write_pos = (self.write_pos + 1) % MAX_DELAY_SAMPLES;
105
106 let wet_signal = (delayed_l + delayed_r) * 0.5;
108 *out = dry * (1.0 - wet) + wet_signal * wet;
109
110 self.phase_l = (self.phase_l + phase_inc).fract();
112 self.phase_r = (self.phase_r + phase_inc).fract();
113
114 params.tick_all();
115 }
116 }
117
118 fn type_name(&self) -> &'static str {
119 "Chorus"
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_chorus_dry_passthrough() {
129 let mut chorus = Chorus::new();
130 let mut params = ParamBlock::new();
131 for &v in &[1.0f32, 0.5, 0.0, 0.0] {
132 params.add(v);
133 } let input = [0.5f32; BUFFER_SIZE];
135 let inputs = [Some(&input); MAX_INPUTS];
136 let mut output = [0.0f32; BUFFER_SIZE];
137 chorus.process(&inputs, &mut output, &mut params, 48000.0);
138 for s in &output {
139 assert!(
140 (s - 0.5).abs() < 1e-5,
141 "wet=0 should pass dry signal unchanged"
142 );
143 }
144 }
145
146 #[test]
147 fn test_chorus_bounded_output() {
148 let mut chorus = Chorus::new();
149 let mut params = ParamBlock::new();
150 for &v in &[2.0f32, 1.0, 0.9, 1.0] {
151 params.add(v);
152 }
153 let input = [1.0f32; BUFFER_SIZE];
154 let inputs = [Some(&input); MAX_INPUTS];
155 let mut output = [0.0f32; BUFFER_SIZE];
156 chorus.process(&inputs, &mut output, &mut params, 48000.0);
157 for s in &output {
158 assert!(s.abs() < 10.0, "chorus output should be bounded, got {s}");
159 }
160 }
161}