devalang_wasm/engine/audio/effects/processors/
lfo.rs1use crate::engine::audio::effects::processors::super_trait::EffectProcessor;
2use crate::engine::audio::lfo::{LfoParams, LfoTarget, generate_lfo_value};
3
4#[derive(Debug, Clone)]
5pub struct LfoProcessor {
6 pub params: LfoParams,
7 pub bpm: f32,
8 sample_offset: usize,
9 read_pos: f32,
11 semitone_range: f32,
12
13 base_cutoff: f32,
15 cutoff_range: f32,
16 prev_l: f32,
17 prev_r: f32,
18}
19
20impl LfoProcessor {
21 pub fn new(
22 params: LfoParams,
23 bpm: f32,
24 semitone_range: f32,
25 base_cutoff: f32,
26 cutoff_range: f32,
27 ) -> Self {
28 Self {
29 params,
30 bpm,
31 sample_offset: 0,
32 read_pos: 0.0,
33 semitone_range,
34 base_cutoff,
35 cutoff_range,
36 prev_l: 0.0,
37 prev_r: 0.0,
38 }
39 }
40}
41
42impl Default for LfoProcessor {
43 fn default() -> Self {
44 Self::new(LfoParams::default(), 120.0, 2.0, 1000.0, 1000.0)
45 }
46}
47
48fn cubic_catmull_rom(y0: f32, y1: f32, y2: f32, y3: f32, t: f32) -> f32 {
50 let t2 = t * t;
56 let t3 = t2 * t;
57
58 0.5 * ((2.0 * y1)
59 + (-y0 + y2) * t
60 + (2.0 * y0 - 5.0 * y1 + 4.0 * y2 - y3) * t2
61 + (-y0 + 3.0 * y1 - 3.0 * y2 + y3) * t3)
62}
63
64impl EffectProcessor for LfoProcessor {
65 fn process(&mut self, samples: &mut [f32], sample_rate: u32) {
66 let sr = sample_rate as f32;
67
68 let frames = samples.len() / 2;
70
71 let src = samples.to_vec();
73
74 for frame in 0..frames {
75 let idx = frame * 2;
76 let time_seconds = (self.sample_offset + frame) as f32 / sr;
77
78 let lfo_val = generate_lfo_value(&self.params, time_seconds, self.bpm); match self.params.target {
81 LfoTarget::Volume => {
82 let mod_amp = 1.0 + lfo_val; samples[idx] *= mod_amp;
84 if idx + 1 < samples.len() {
85 samples[idx + 1] *= mod_amp;
86 }
87 }
88 LfoTarget::Pan => {
89 let pan = lfo_val.clamp(-1.0, 1.0);
90 let left = (1.0 - pan) * 0.5;
91 let right = (1.0 + pan) * 0.5;
92 samples[idx] *= left;
93 if idx + 1 < samples.len() {
94 samples[idx + 1] *= right;
95 }
96 }
97 LfoTarget::Pitch => {
98 let semitone = lfo_val * self.semitone_range; let speed = 2.0_f32.powf(semitone / 12.0);
101
102 let pos = self.read_pos;
104 let base = pos.floor() as usize;
105 let frac = pos - base as f32;
106
107 let get_sample = |frame_idx: usize, ch: usize| -> f32 {
108 if frame_idx >= frames {
109 return 0.0;
110 }
111 src.get(frame_idx * 2 + ch).copied().unwrap_or(0.0)
112 };
113
114 let y_m1 = get_sample(base.wrapping_sub(1), 0);
117 let y0 = get_sample(base, 0);
118 let y1 = get_sample(base + 1, 0);
119 let y2 = get_sample(base + 2, 0);
120 let out_l = cubic_catmull_rom(y_m1, y0, y1, y2, frac);
121
122 let y_m1r = get_sample(base.wrapping_sub(1), 1);
124 let y0r = get_sample(base, 1);
125 let y1r = get_sample(base + 1, 1);
126 let y2r = get_sample(base + 2, 1);
127 let out_r = cubic_catmull_rom(y_m1r, y0r, y1r, y2r, frac);
128
129 samples[idx] = out_l;
130 if idx + 1 < samples.len() {
131 samples[idx + 1] = out_r;
132 }
133
134 self.read_pos += speed;
135 }
136 LfoTarget::FilterCutoff => {
137 let cutoff = (self.base_cutoff + lfo_val * self.cutoff_range).max(20.0);
139 let a = 1.0 - (-2.0 * std::f32::consts::PI * cutoff / sr).exp();
141
142 let in_l = samples[idx];
143 let in_r = if idx + 1 < samples.len() {
144 samples[idx + 1]
145 } else {
146 0.0
147 };
148
149 let out_l = a * in_l + (1.0 - a) * self.prev_l;
150 let out_r = a * in_r + (1.0 - a) * self.prev_r;
151
152 samples[idx] = out_l;
153 if idx + 1 < samples.len() {
154 samples[idx + 1] = out_r;
155 }
156
157 self.prev_l = out_l;
158 self.prev_r = out_r;
159 }
160 }
161 }
162
163 self.sample_offset = self.sample_offset.wrapping_add(frames);
164
165 let mut max_diff = 0.0f32;
168 let len = samples.len().min(src.len());
169 for i in 0..len {
170 let diff = (samples[i] - src[i]).abs();
171 if diff > max_diff {
172 max_diff = diff;
173 }
174 }
175 }
176
177 fn reset(&mut self) {
178 self.sample_offset = 0;
179 }
180
181 fn name(&self) -> &str {
182 "LFO"
183 }
184}