1use std::f64::consts::PI;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum LfoWave {
14 #[default]
15 Sine,
16 Triangle,
17 Square,
18 Ramp,
19 Pulse,
20}
21
22#[derive(Debug, Clone, PartialEq)]
26pub struct LfoChop {
27 pub wave: LfoWave,
28 pub freq_hz: f64,
29 pub amplitude: f64,
30 pub offset: f64,
31 pub phase: f64,
32 pub pulse_width: f64,
33}
34
35impl Default for LfoChop {
36 fn default() -> Self {
37 Self {
38 wave: LfoWave::Sine,
39 freq_hz: 1.0,
40 amplitude: 1.0,
41 offset: 0.0,
42 phase: 0.0,
43 pulse_width: 0.5,
44 }
45 }
46}
47
48impl LfoChop {
49 pub fn new(wave: LfoWave, freq_hz: f64) -> Self {
50 Self {
51 wave,
52 freq_hz,
53 ..Default::default()
54 }
55 }
56
57 pub fn with_amplitude(mut self, amp: f64) -> Self {
58 self.amplitude = amp;
59 self
60 }
61
62 pub fn with_offset(mut self, offset: f64) -> Self {
63 self.offset = offset;
64 self
65 }
66
67 pub fn with_phase(mut self, phase: f64) -> Self {
68 self.phase = phase;
69 self
70 }
71
72 pub fn sample(&self, frame: f64, fps: f64) -> f64 {
74 let t = frame / fps.max(1.0);
75 let cycle = (t * self.freq_hz + self.phase).rem_euclid(1.0);
76
77 let raw = match self.wave {
78 LfoWave::Sine => (cycle * 2.0 * PI).sin(),
79 LfoWave::Triangle => {
80 if cycle < 0.5 {
81 -1.0 + 4.0 * cycle
82 } else {
83 3.0 - 4.0 * cycle
84 }
85 }
86 LfoWave::Square => {
87 if cycle < self.pulse_width {
88 1.0
89 } else {
90 -1.0
91 }
92 }
93 LfoWave::Ramp => -1.0 + 2.0 * cycle,
94 LfoWave::Pulse => {
95 if cycle < self.pulse_width {
96 1.0
97 } else {
98 0.0
99 }
100 }
101 };
102
103 self.offset + self.amplitude * raw
104 }
105}
106
107#[derive(Debug, Clone, PartialEq)]
111pub struct LagChop {
112 pub lag_up: f64,
113 pub lag_down: f64,
114}
115
116impl Default for LagChop {
117 fn default() -> Self {
118 Self {
119 lag_up: 0.2,
120 lag_down: 0.2,
121 }
122 }
123}
124
125impl LagChop {
126 pub fn new(lag_up: f64, lag_down: f64) -> Self {
127 Self { lag_up, lag_down }
128 }
129
130 pub fn filter(&self, current: f64, target: f64, dt: f64) -> f64 {
132 let lag = if target >= current {
133 self.lag_up.max(0.0001)
134 } else {
135 self.lag_down.max(0.0001)
136 };
137
138 let alpha = 1.0 - (-dt.max(0.0) / lag).exp();
139 current + (target - current) * alpha.clamp(0.0, 1.0)
140 }
141}
142
143#[derive(Debug, Clone, PartialEq)]
145pub struct AudioEnvelopeChop {
146 pub attack: f64,
147 pub release: f64,
148}
149
150impl Default for AudioEnvelopeChop {
151 fn default() -> Self {
152 Self {
153 attack: 0.01, release: 0.15, }
156 }
157}
158
159impl AudioEnvelopeChop {
160 pub fn new(attack: f64, release: f64) -> Self {
161 Self { attack, release }
162 }
163
164 pub fn track(&self, current_env: f64, sample_amp: f64, dt: f64) -> f64 {
166 let target = sample_amp.abs();
167 let rate = if target > current_env {
168 self.attack.max(0.0001)
169 } else {
170 self.release.max(0.0001)
171 };
172
173 let alpha = 1.0 - (-dt.max(0.0) / rate).exp();
174 current_env + (target - current_env) * alpha.clamp(0.0, 1.0)
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum MathOp {
181 Add,
182 Multiply,
183 Average,
184 Minimum,
185 Maximum,
186}
187
188pub struct MathChop;
190
191impl MathChop {
192 pub fn map_range(val: f64, in_range: (f64, f64), out_range: (f64, f64), clamp: bool) -> f64 {
194 let (in_min, in_max) = in_range;
195 let (out_min, out_max) = out_range;
196 let span = in_max - in_min;
197 if span.abs() < 1e-9 {
198 return out_min;
199 }
200
201 let normalized = (val - in_min) / span;
202 let t = if clamp {
203 normalized.clamp(0.0, 1.0)
204 } else {
205 normalized
206 };
207
208 out_min + (out_max - out_min) * t
209 }
210
211 pub fn combine(channels: &[f64], op: MathOp) -> f64 {
213 if channels.is_empty() {
214 return 0.0;
215 }
216
217 match op {
218 MathOp::Add => channels.iter().sum(),
219 MathOp::Multiply => channels.iter().product(),
220 MathOp::Average => channels.iter().sum::<f64>() / channels.len() as f64,
221 MathOp::Minimum => channels.iter().copied().fold(f64::INFINITY, f64::min),
222 MathOp::Maximum => channels.iter().copied().fold(f64::NEG_INFINITY, f64::max),
223 }
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_lfo_sine_sampling() {
233 let lfo = LfoChop::new(LfoWave::Sine, 1.0); let s0 = lfo.sample(0.0, 30.0);
235 let s15 = lfo.sample(7.5, 30.0); let s30 = lfo.sample(15.0, 30.0); assert!(s0.abs() < 1e-4);
239 assert!((s15 - 1.0).abs() < 1e-4);
240 assert!(s30.abs() < 1e-4);
241 }
242
243 #[test]
244 fn test_lag_filter_convergence() {
245 let lag = LagChop::new(0.1, 0.1);
246 let mut val = 0.0;
247 for _ in 0..10 {
249 val = lag.filter(val, 1.0, 1.0 / 30.0);
250 }
251 assert!(val > 0.9, "Lag filter should converge towards 1.0: {val}");
252 }
253
254 #[test]
255 fn test_math_chop_range_remap() {
256 let mapped = MathChop::map_range(5.0, (0.0, 10.0), (100.0, 200.0), true);
257 assert_eq!(mapped, 150.0);
258 }
259}