1use bbx_core::random::XorShiftRng;
4
5#[cfg(feature = "simd")]
6use crate::sample::SIMD_LANES;
7#[cfg(feature = "simd")]
8use crate::waveform::generate_waveform_samples_simd;
9use crate::{
10 block::{Block, DEFAULT_MODULATOR_INPUT_COUNT, DEFAULT_MODULATOR_OUTPUT_COUNT},
11 context::DspContext,
12 parameter::{ModulationOutput, Parameter},
13 sample::Sample,
14 waveform::{Waveform, process_waveform_scalar},
15};
16
17pub struct LfoBlock<S: Sample> {
22 pub frequency: Parameter<S>,
24
25 pub depth: Parameter<S>,
27
28 phase: f64,
29 waveform: Waveform,
30 rng: XorShiftRng,
31}
32
33impl<S: Sample> LfoBlock<S> {
34 const MODULATION_OUTPUTS: &'static [ModulationOutput] = &[ModulationOutput {
35 name: "LFO",
36 min_value: -1.0,
37 max_value: 1.0,
38 }];
39
40 pub fn new(frequency: f64, depth: f64, waveform: Waveform, seed: Option<u64>) -> Self {
42 Self {
43 frequency: Parameter::Constant(S::from_f64(frequency)),
44 depth: Parameter::Constant(S::from_f64(depth)),
45 phase: 0.0,
46 waveform,
47 rng: XorShiftRng::new(seed.unwrap_or_default()),
48 }
49 }
50}
51
52impl<S: Sample> Block<S> for LfoBlock<S> {
53 fn process(&mut self, _inputs: &[&[S]], outputs: &mut [&mut [S]], modulation_values: &[S], context: &DspContext) {
54 let frequency = self.frequency.get_value(modulation_values);
55 let depth = self.depth.get_value(modulation_values).to_f64();
56 let phase_increment = frequency.to_f64() / context.sample_rate * S::TAU.to_f64();
57
58 #[cfg(feature = "simd")]
59 {
60 use crate::waveform::DEFAULT_DUTY_CYCLE;
61
62 if !matches!(self.waveform, Waveform::Noise) {
63 let buffer_size = context.buffer_size;
64 let chunks = buffer_size / SIMD_LANES;
65 let remainder_start = chunks * SIMD_LANES;
66 let chunk_phase_step = phase_increment * SIMD_LANES as f64;
67 let depth_s = S::from_f64(depth);
68 let depth_vec = S::simd_splat(depth_s);
69
70 let base_phase = S::simd_splat(S::from_f64(self.phase));
71 let sample_inc_simd = S::simd_splat(S::from_f64(phase_increment));
72 let mut phases = base_phase + S::simd_lane_offsets() * sample_inc_simd;
73 let chunk_inc_simd = S::simd_splat(S::from_f64(chunk_phase_step));
74 let duty = S::from_f64(DEFAULT_DUTY_CYCLE);
75 let two_pi = S::simd_splat(S::TAU);
76 let inv_two_pi = S::simd_splat(S::INV_TAU);
77 let phase_inc_normalized = S::from_f64(phase_increment * S::INV_TAU.to_f64());
78 let tau = S::TAU.to_f64();
79 let inv_tau = 1.0 / tau;
80
81 for chunk_idx in 0..chunks {
82 let phases_array = S::simd_to_array(phases);
83 let phases_normalized: [S; SIMD_LANES] = [
84 S::from_f64(phases_array[0].to_f64().rem_euclid(tau) * inv_tau),
85 S::from_f64(phases_array[1].to_f64().rem_euclid(tau) * inv_tau),
86 S::from_f64(phases_array[2].to_f64().rem_euclid(tau) * inv_tau),
87 S::from_f64(phases_array[3].to_f64().rem_euclid(tau) * inv_tau),
88 ];
89
90 if let Some(samples) = generate_waveform_samples_simd::<S>(
91 self.waveform,
92 phases,
93 phases_normalized,
94 phase_inc_normalized,
95 duty,
96 two_pi,
97 inv_two_pi,
98 ) {
99 let samples_vec = S::simd_from_slice(&samples);
100 let scaled = samples_vec * depth_vec;
101 let base = chunk_idx * SIMD_LANES;
102 outputs[0][base..base + SIMD_LANES].copy_from_slice(&S::simd_to_array(scaled));
103 }
104
105 phases = phases + chunk_inc_simd;
106 }
107
108 self.phase += chunk_phase_step * chunks as f64;
109 self.phase = self.phase.rem_euclid(S::TAU.to_f64());
110
111 process_waveform_scalar(
112 &mut outputs[0][remainder_start..],
113 self.waveform,
114 &mut self.phase,
115 phase_increment,
116 &mut self.rng,
117 depth,
118 );
119 } else {
120 process_waveform_scalar(
121 outputs[0],
122 self.waveform,
123 &mut self.phase,
124 phase_increment,
125 &mut self.rng,
126 depth,
127 );
128 }
129 }
130
131 #[cfg(not(feature = "simd"))]
132 {
133 process_waveform_scalar(
134 outputs[0],
135 self.waveform,
136 &mut self.phase,
137 phase_increment,
138 &mut self.rng,
139 depth,
140 );
141 }
142 }
143
144 #[inline]
145 fn input_count(&self) -> usize {
146 DEFAULT_MODULATOR_INPUT_COUNT
147 }
148
149 #[inline]
150 fn output_count(&self) -> usize {
151 DEFAULT_MODULATOR_OUTPUT_COUNT
152 }
153
154 #[inline]
155 fn modulation_outputs(&self) -> &[ModulationOutput] {
156 Self::MODULATION_OUTPUTS
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::channel::ChannelLayout;
164
165 fn test_context(buffer_size: usize, sample_rate: f64) -> DspContext {
166 DspContext {
167 sample_rate,
168 num_channels: 1,
169 buffer_size,
170 current_sample: 0,
171 channel_layout: ChannelLayout::Mono,
172 }
173 }
174
175 fn process_lfo<S: Sample>(lfo: &mut LfoBlock<S>, context: &DspContext) -> Vec<S> {
176 let inputs: [&[S]; 0] = [];
177 let mut output = vec![S::ZERO; context.buffer_size];
178 let mut outputs: [&mut [S]; 1] = [&mut output];
179 lfo.process(&inputs, &mut outputs, &[], context);
180 output
181 }
182
183 #[test]
184 fn test_lfo_input_output_counts_f32() {
185 let lfo = LfoBlock::<f32>::new(1.0, 1.0, Waveform::Sine, None);
186 assert_eq!(lfo.input_count(), DEFAULT_MODULATOR_INPUT_COUNT);
187 assert_eq!(lfo.output_count(), DEFAULT_MODULATOR_OUTPUT_COUNT);
188 }
189
190 #[test]
191 fn test_lfo_input_output_counts_f64() {
192 let lfo = LfoBlock::<f64>::new(1.0, 1.0, Waveform::Sine, None);
193 assert_eq!(lfo.input_count(), DEFAULT_MODULATOR_INPUT_COUNT);
194 assert_eq!(lfo.output_count(), DEFAULT_MODULATOR_OUTPUT_COUNT);
195 }
196
197 #[test]
198 fn test_lfo_modulation_output_f32() {
199 let lfo = LfoBlock::<f32>::new(1.0, 1.0, Waveform::Sine, None);
200 let outputs = lfo.modulation_outputs();
201 assert_eq!(outputs.len(), 1);
202 assert_eq!(outputs[0].name, "LFO");
203 assert!((outputs[0].min_value - (-1.0)).abs() < 1e-10);
204 assert!((outputs[0].max_value - 1.0).abs() < 1e-10);
205 }
206
207 #[test]
208 fn test_lfo_output_range_unity_depth_f32() {
209 let mut lfo = LfoBlock::<f32>::new(1.0, 1.0, Waveform::Sine, Some(42));
210 let context = test_context(512, 44100.0);
211
212 for _ in 0..10 {
213 let output = process_lfo(&mut lfo, &context);
214 for &sample in &output {
215 assert!(
216 sample >= -1.1 && sample <= 1.1,
217 "LFO with depth=1 should be in [-1, 1]: {}",
218 sample
219 );
220 }
221 }
222 }
223
224 #[test]
225 fn test_lfo_output_range_unity_depth_f64() {
226 let mut lfo = LfoBlock::<f64>::new(1.0, 1.0, Waveform::Sine, Some(42));
227 let context = test_context(512, 44100.0);
228
229 for _ in 0..10 {
230 let output = process_lfo(&mut lfo, &context);
231 for &sample in &output {
232 assert!(
233 sample >= -1.1 && sample <= 1.1,
234 "LFO with depth=1 should be in [-1, 1]: {}",
235 sample
236 );
237 }
238 }
239 }
240
241 #[test]
242 fn test_lfo_depth_scaling_f32() {
243 let depth = 0.5;
244 let mut lfo = LfoBlock::<f32>::new(1.0, depth, Waveform::Sine, Some(42));
245 let context = test_context(512, 44100.0);
246
247 for _ in 0..10 {
248 let output = process_lfo(&mut lfo, &context);
249 for &sample in &output {
250 assert!(
251 sample >= -depth as f32 * 1.1 && sample <= depth as f32 * 1.1,
252 "LFO with depth={} should be in [{}, {}]: {}",
253 depth,
254 -depth,
255 depth,
256 sample
257 );
258 }
259 }
260 }
261
262 #[test]
263 fn test_lfo_depth_scaling_f64() {
264 let depth = 0.5;
265 let mut lfo = LfoBlock::<f64>::new(1.0, depth, Waveform::Sine, Some(42));
266 let context = test_context(512, 44100.0);
267
268 for _ in 0..10 {
269 let output = process_lfo(&mut lfo, &context);
270 for &sample in &output {
271 assert!(
272 sample >= -depth * 1.1 && sample <= depth * 1.1,
273 "LFO with depth={} should be in [{}, {}]: {}",
274 depth,
275 -depth,
276 depth,
277 sample
278 );
279 }
280 }
281 }
282
283 #[test]
284 fn test_lfo_sine_produces_variation_f32() {
285 let mut lfo = LfoBlock::<f32>::new(5.0, 1.0, Waveform::Sine, Some(42));
286 let context = test_context(512, 44100.0);
287
288 let output = process_lfo(&mut lfo, &context);
289
290 let min = output.iter().fold(f32::MAX, |acc, &x| acc.min(x));
291 let max = output.iter().fold(f32::MIN, |acc, &x| acc.max(x));
292
293 assert!(
294 max - min > 0.1,
295 "LFO should produce variation: min={}, max={}",
296 min,
297 max
298 );
299 }
300
301 #[test]
302 fn test_lfo_sine_produces_variation_f64() {
303 let mut lfo = LfoBlock::<f64>::new(5.0, 1.0, Waveform::Sine, Some(42));
304 let context = test_context(512, 44100.0);
305
306 let output = process_lfo(&mut lfo, &context);
307
308 let min = output.iter().fold(f64::MAX, |acc, &x| acc.min(x));
309 let max = output.iter().fold(f64::MIN, |acc, &x| acc.max(x));
310
311 assert!(
312 max - min > 0.1,
313 "LFO should produce variation: min={}, max={}",
314 min,
315 max
316 );
317 }
318
319 #[test]
320 fn test_lfo_low_frequency_f32() {
321 let mut lfo = LfoBlock::<f32>::new(0.1, 1.0, Waveform::Sine, Some(42));
322 let context = test_context(4096, 44100.0);
323
324 let mut all_samples = Vec::new();
325 for _ in 0..50 {
326 let output = process_lfo(&mut lfo, &context);
327 all_samples.extend(output);
328 }
329
330 let has_positive = all_samples.iter().any(|&x| x > 0.1);
331 let has_negative = all_samples.iter().any(|&x| x < -0.1);
332
333 assert!(
334 has_positive || has_negative,
335 "Very low frequency LFO should still produce signal"
336 );
337 }
338
339 #[test]
340 fn test_lfo_high_frequency_f32() {
341 let mut lfo = LfoBlock::<f32>::new(20.0, 1.0, Waveform::Sine, Some(42));
342 let context = test_context(512, 44100.0);
343
344 let output = process_lfo(&mut lfo, &context);
345
346 let min = output.iter().fold(f32::MAX, |acc, &x| acc.min(x));
347 let max = output.iter().fold(f32::MIN, |acc, &x| acc.max(x));
348
349 assert!(max - min > 0.5, "High frequency LFO should oscillate within buffer");
350 }
351
352 #[test]
353 fn test_lfo_square_output_range_f32() {
354 let mut lfo = LfoBlock::<f32>::new(2.0, 1.0, Waveform::Square, Some(42));
355 let context = test_context(512, 44100.0);
356
357 for _ in 0..10 {
358 let output = process_lfo(&mut lfo, &context);
359 for &sample in &output {
360 assert!(
361 sample >= -1.1 && sample <= 1.1,
362 "Square LFO should be in [-1, 1]: {}",
363 sample
364 );
365 }
366 }
367 }
368
369 #[test]
370 fn test_lfo_square_output_range_f64() {
371 let mut lfo = LfoBlock::<f64>::new(2.0, 1.0, Waveform::Square, Some(42));
372 let context = test_context(512, 44100.0);
373
374 for _ in 0..10 {
375 let output = process_lfo(&mut lfo, &context);
376 for &sample in &output {
377 assert!(
378 sample >= -1.1 && sample <= 1.1,
379 "Square LFO should be in [-1, 1]: {}",
380 sample
381 );
382 }
383 }
384 }
385
386 #[test]
387 fn test_lfo_sawtooth_output_range_f32() {
388 let mut lfo = LfoBlock::<f32>::new(2.0, 1.0, Waveform::Sawtooth, Some(42));
389 let context = test_context(512, 44100.0);
390
391 for _ in 0..10 {
392 let output = process_lfo(&mut lfo, &context);
393 for &sample in &output {
394 assert!(
395 sample >= -1.1 && sample <= 1.1,
396 "Sawtooth LFO should be in [-1, 1]: {}",
397 sample
398 );
399 }
400 }
401 }
402
403 #[test]
404 fn test_lfo_sawtooth_output_range_f64() {
405 let mut lfo = LfoBlock::<f64>::new(2.0, 1.0, Waveform::Sawtooth, Some(42));
406 let context = test_context(512, 44100.0);
407
408 for _ in 0..10 {
409 let output = process_lfo(&mut lfo, &context);
410 for &sample in &output {
411 assert!(
412 sample >= -1.1 && sample <= 1.1,
413 "Sawtooth LFO should be in [-1, 1]: {}",
414 sample
415 );
416 }
417 }
418 }
419
420 #[test]
421 fn test_lfo_triangle_output_range_f32() {
422 let mut lfo = LfoBlock::<f32>::new(2.0, 1.0, Waveform::Triangle, Some(42));
423 let context = test_context(512, 44100.0);
424
425 for _ in 0..10 {
426 let output = process_lfo(&mut lfo, &context);
427 for &sample in &output {
428 assert!(
429 sample >= -1.1 && sample <= 1.1,
430 "Triangle LFO should be in [-1, 1]: {}",
431 sample
432 );
433 }
434 }
435 }
436
437 #[test]
438 fn test_lfo_triangle_output_range_f64() {
439 let mut lfo = LfoBlock::<f64>::new(2.0, 1.0, Waveform::Triangle, Some(42));
440 let context = test_context(512, 44100.0);
441
442 for _ in 0..10 {
443 let output = process_lfo(&mut lfo, &context);
444 for &sample in &output {
445 assert!(
446 sample >= -1.1 && sample <= 1.1,
447 "Triangle LFO should be in [-1, 1]: {}",
448 sample
449 );
450 }
451 }
452 }
453
454 #[test]
455 fn test_lfo_noise_output_range_f32() {
456 let mut lfo = LfoBlock::<f32>::new(1.0, 1.0, Waveform::Noise, Some(42));
457 let context = test_context(512, 44100.0);
458
459 for _ in 0..10 {
460 let output = process_lfo(&mut lfo, &context);
461 for &sample in &output {
462 assert!(
463 sample >= -1.0 && sample <= 1.0,
464 "Noise LFO should be in [-1, 1]: {}",
465 sample
466 );
467 }
468 }
469 }
470
471 #[test]
472 fn test_lfo_noise_output_range_f64() {
473 let mut lfo = LfoBlock::<f64>::new(1.0, 1.0, Waveform::Noise, Some(42));
474 let context = test_context(512, 44100.0);
475
476 for _ in 0..10 {
477 let output = process_lfo(&mut lfo, &context);
478 for &sample in &output {
479 assert!(
480 sample >= -1.0 && sample <= 1.0,
481 "Noise LFO should be in [-1, 1]: {}",
482 sample
483 );
484 }
485 }
486 }
487
488 #[test]
489 fn test_lfo_deterministic_with_seed_f32() {
490 let output1 = {
491 let mut lfo = LfoBlock::<f32>::new(5.0, 1.0, Waveform::Sine, Some(42));
492 let context = test_context(256, 44100.0);
493 process_lfo(&mut lfo, &context)
494 };
495
496 let output2 = {
497 let mut lfo = LfoBlock::<f32>::new(5.0, 1.0, Waveform::Sine, Some(42));
498 let context = test_context(256, 44100.0);
499 process_lfo(&mut lfo, &context)
500 };
501
502 for (a, b) in output1.iter().zip(output2.iter()) {
503 assert!((a - b).abs() < 1e-6, "Same seed should produce identical output");
504 }
505 }
506
507 #[test]
508 fn test_lfo_deterministic_with_seed_f64() {
509 let output1 = {
510 let mut lfo = LfoBlock::<f64>::new(5.0, 1.0, Waveform::Sine, Some(42));
511 let context = test_context(256, 44100.0);
512 process_lfo(&mut lfo, &context)
513 };
514
515 let output2 = {
516 let mut lfo = LfoBlock::<f64>::new(5.0, 1.0, Waveform::Sine, Some(42));
517 let context = test_context(256, 44100.0);
518 process_lfo(&mut lfo, &context)
519 };
520
521 for (a, b) in output1.iter().zip(output2.iter()) {
522 assert!((a - b).abs() < 1e-12, "Same seed should produce identical output");
523 }
524 }
525
526 #[test]
527 fn test_lfo_zero_depth_f32() {
528 let mut lfo = LfoBlock::<f32>::new(5.0, 0.0, Waveform::Sine, Some(42));
529 let context = test_context(512, 44100.0);
530
531 let output = process_lfo(&mut lfo, &context);
532
533 for &sample in &output {
534 assert!(sample.abs() < 1e-6, "Zero depth LFO should produce zero: {}", sample);
535 }
536 }
537
538 #[test]
539 fn test_lfo_zero_depth_f64() {
540 let mut lfo = LfoBlock::<f64>::new(5.0, 0.0, Waveform::Sine, Some(42));
541 let context = test_context(512, 44100.0);
542
543 let output = process_lfo(&mut lfo, &context);
544
545 for &sample in &output {
546 assert!(sample.abs() < 1e-12, "Zero depth LFO should produce zero: {}", sample);
547 }
548 }
549
550 #[test]
551 fn test_lfo_phase_continuity_f32() {
552 let mut lfo = LfoBlock::<f32>::new(5.0, 1.0, Waveform::Sine, Some(42));
553 let context = test_context(256, 44100.0);
554
555 let output1 = process_lfo(&mut lfo, &context);
556 let output2 = process_lfo(&mut lfo, &context);
557
558 let last = output1[255];
559 let first = output2[0];
560 let diff = (last - first).abs();
561
562 let samples_per_cycle = 44100.0 / 5.0;
563 let expected_diff_per_sample = 2.0 / samples_per_cycle;
564
565 assert!(
566 diff < expected_diff_per_sample * 10.0,
567 "Phase discontinuity detected: last={}, first={}, diff={}",
568 last,
569 first,
570 diff
571 );
572 }
573}