1#[cfg(feature = "simd")]
13use std::simd::StdFloat;
14
15#[cfg(feature = "simd")]
16use crate::sample::SIMD_LANES;
17use crate::sample::Sample;
18
19#[inline]
21fn fract<S: Sample>(x: S) -> S {
22 x - S::from_f64(x.to_f64().floor())
23}
24
25#[inline]
34pub fn poly_blep<S: Sample>(t: S, dt: S) -> S {
35 let two = S::from_f64(2.0);
36 if t < dt {
37 let t_norm = t / dt;
38 two * t_norm - t_norm * t_norm - S::ONE
39 } else if t > S::ONE - dt {
40 let t_norm = (t - S::ONE) / dt;
41 t_norm * t_norm + two * t_norm + S::ONE
42 } else {
43 S::ZERO
44 }
45}
46
47#[inline]
56pub fn poly_blamp<S: Sample>(t: S, dt: S) -> S {
57 let third = S::from_f64(1.0 / 3.0);
58 if t < dt {
59 let t_norm = t / dt;
60 let t2 = t_norm * t_norm;
61 let t3 = t2 * t_norm;
62 (t2 - t3 * third - t_norm) * dt
63 } else if t > S::ONE - dt {
64 let t_norm = (t - S::ONE) / dt;
65 let t2 = t_norm * t_norm;
66 let t3 = t2 * t_norm;
67 (t3 * third + t2 + t_norm) * dt
68 } else {
69 S::ZERO
70 }
71}
72
73#[cfg(feature = "simd")]
79#[inline]
80pub fn poly_blep_simd<S: Sample>(t: S::Simd, dt: S::Simd) -> S::Simd {
81 let zero = S::simd_splat(S::ZERO);
82 let one = S::simd_splat(S::ONE);
83 let two = S::simd_splat(S::from_f64(2.0));
84 let one_minus_dt = one - dt;
85
86 let t_norm_after = t / dt;
88 let result_after = two * t_norm_after - t_norm_after * t_norm_after - one;
89
90 let t_norm_before = (t - one) / dt;
92 let result_before = t_norm_before * t_norm_before + two * t_norm_before + one;
93
94 let after_or_zero = S::simd_select_lt(t, dt, result_after, zero);
96 S::simd_select_gt(t, one_minus_dt, result_before, after_or_zero)
97}
98
99#[cfg(feature = "simd")]
104#[inline]
105pub fn poly_blamp_simd<S: Sample>(t: S::Simd, dt: S::Simd) -> S::Simd {
106 let zero = S::simd_splat(S::ZERO);
107 let one = S::simd_splat(S::ONE);
108 let third = S::simd_splat(S::from_f64(1.0 / 3.0));
109 let one_minus_dt = one - dt;
110
111 let t_norm_after = t / dt;
113 let t2_after = t_norm_after * t_norm_after;
114 let t3_after = t2_after * t_norm_after;
115 let result_after = (t2_after - t3_after * third - t_norm_after) * dt;
116
117 let t_norm_before = (t - one) / dt;
119 let t2_before = t_norm_before * t_norm_before;
120 let t3_before = t2_before * t_norm_before;
121 let result_before = (t3_before * third + t2_before + t_norm_before) * dt;
122
123 let after_or_zero = S::simd_select_lt(t, dt, result_after, zero);
124 S::simd_select_gt(t, one_minus_dt, result_before, after_or_zero)
125}
126
127#[inline]
133pub fn polyblep_saw<S: Sample>(phase: S, phase_inc: S) -> S {
134 let two = S::from_f64(2.0);
135 let naive = two * phase - S::ONE;
136 naive - poly_blep(phase, phase_inc)
137}
138
139#[inline]
145pub fn polyblep_square<S: Sample>(phase: S, phase_inc: S) -> S {
146 let half = S::from_f64(0.5);
147 let naive = if phase < half { S::ONE } else { -S::ONE };
148 let mut out = naive;
149 out += poly_blep(phase, phase_inc);
150 let falling_phase = fract(phase + half);
151 out -= poly_blep(falling_phase, phase_inc);
152 out
153}
154
155#[inline]
162pub fn polyblep_pulse<S: Sample>(phase: S, phase_inc: S, duty_cycle: S) -> S {
163 let naive = if phase < duty_cycle { S::ONE } else { -S::ONE };
164 let mut out = naive;
165 out += poly_blep(phase, phase_inc);
166 let falling_phase = fract(phase - duty_cycle + S::ONE);
167 out -= poly_blep(falling_phase, phase_inc);
168 out
169}
170
171#[inline]
177pub fn polyblamp_triangle<S: Sample>(phase: S, phase_inc: S) -> S {
178 let half = S::from_f64(0.5);
179 let four = S::from_f64(4.0);
180 let three = S::from_f64(3.0);
181 let eight = S::from_f64(8.0);
182
183 let naive = if phase < half {
184 four * phase - S::ONE
185 } else {
186 three - four * phase
187 };
188
189 let mut out = naive;
190 out += eight * poly_blamp(phase, phase_inc);
191 out -= eight * poly_blamp(fract(phase + half), phase_inc);
192 out
193}
194
195#[cfg(feature = "simd")]
199pub fn apply_polyblep_saw<S: Sample>(samples: &mut [S; SIMD_LANES], phases: [S; SIMD_LANES], phase_inc: S) {
200 let samples_simd = S::simd_from_slice(samples);
201 let phases_simd = S::simd_from_slice(&phases);
202 let phase_inc_simd = S::simd_splat(phase_inc);
203
204 let correction = poly_blep_simd::<S>(phases_simd, phase_inc_simd);
205 *samples = S::simd_to_array(samples_simd - correction);
206}
207
208#[cfg(feature = "simd")]
212pub fn apply_polyblep_square<S: Sample>(samples: &mut [S; SIMD_LANES], phases: [S; SIMD_LANES], phase_inc: S) {
213 let samples_simd = S::simd_from_slice(samples);
214 let phases_simd = S::simd_from_slice(&phases);
215 let phase_inc_simd = S::simd_splat(phase_inc);
216 let half = S::simd_splat(S::from_f64(0.5));
217
218 let rising = poly_blep_simd::<S>(phases_simd, phase_inc_simd);
220
221 let falling_phase_raw = phases_simd + half;
223 let falling_phase = falling_phase_raw - falling_phase_raw.floor();
224 let falling = poly_blep_simd::<S>(falling_phase, phase_inc_simd);
225
226 *samples = S::simd_to_array(samples_simd + rising - falling);
227}
228
229#[cfg(feature = "simd")]
233pub fn apply_polyblep_pulse<S: Sample>(
234 samples: &mut [S; SIMD_LANES],
235 phases: [S; SIMD_LANES],
236 phase_inc: S,
237 duty_cycle: S,
238) {
239 let samples_simd = S::simd_from_slice(samples);
240 let phases_simd = S::simd_from_slice(&phases);
241 let phase_inc_simd = S::simd_splat(phase_inc);
242 let duty = S::simd_splat(duty_cycle);
243 let one = S::simd_splat(S::ONE);
244
245 let rising = poly_blep_simd::<S>(phases_simd, phase_inc_simd);
247
248 let falling_phase_raw = phases_simd - duty + one;
250 let falling_phase = falling_phase_raw - falling_phase_raw.floor();
251 let falling = poly_blep_simd::<S>(falling_phase, phase_inc_simd);
252
253 *samples = S::simd_to_array(samples_simd + rising - falling);
254}
255
256#[cfg(feature = "simd")]
260pub fn apply_polyblamp_triangle<S: Sample>(samples: &mut [S; SIMD_LANES], phases: [S; SIMD_LANES], phase_inc: S) {
261 let samples_simd = S::simd_from_slice(samples);
262 let phases_simd = S::simd_from_slice(&phases);
263 let phase_inc_simd = S::simd_splat(phase_inc);
264 let half = S::simd_splat(S::from_f64(0.5));
265 let eight = S::simd_splat(S::from_f64(8.0));
266
267 let at_zero = poly_blamp_simd::<S>(phases_simd, phase_inc_simd);
269
270 let at_half_phase_raw = phases_simd + half;
272 let at_half_phase = at_half_phase_raw - at_half_phase_raw.floor();
273 let at_half = poly_blamp_simd::<S>(at_half_phase, phase_inc_simd);
274
275 *samples = S::simd_to_array(samples_simd + eight * at_zero - eight * at_half);
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_poly_blep_zero_outside_range() {
284 let dt = 0.1f64;
285 assert_eq!(poly_blep(0.5, dt), 0.0);
286 assert_eq!(poly_blep(0.3, dt), 0.0);
287 assert_eq!(poly_blep(0.7, dt), 0.0);
288 }
289
290 #[test]
291 fn test_poly_blep_near_discontinuity() {
292 let dt = 0.1f64;
293
294 let correction_after = poly_blep(0.05, dt);
296 assert!(
297 correction_after != 0.0,
298 "Should have correction just after discontinuity"
299 );
300 assert!(correction_after < 0.0, "Correction should be negative just after");
301
302 let correction_before = poly_blep(0.95, dt);
304 assert!(
305 correction_before != 0.0,
306 "Should have correction just before discontinuity"
307 );
308 assert!(correction_before > 0.0, "Correction should be positive just before");
309 }
310
311 #[test]
312 fn test_poly_blep_boundary_values() {
313 let dt = 0.1f64;
314
315 let at_zero = poly_blep(0.0, dt);
317 assert!(
318 (at_zero - (-1.0)).abs() < 1e-10,
319 "poly_blep(0, dt) should be -1, got {}",
320 at_zero
321 );
322
323 let near_dt = poly_blep(dt - 0.001, dt);
325 assert!(near_dt.abs() < 0.1, "Should approach 0 near dt boundary");
326 }
327
328 #[test]
329 fn test_polyblep_saw_matches_reference() {
330 let phase = 0.005f64;
336 let phase_inc = 0.01f64;
337 let result = polyblep_saw(phase, phase_inc);
338 let expected = -0.74f64;
339 assert!(
340 (result - expected).abs() < 0.01,
341 "polyblep_saw({}, {}) = {}, expected ~{}",
342 phase,
343 phase_inc,
344 result,
345 expected
346 );
347 }
348
349 #[test]
350 fn test_polyblep_saw_no_correction_mid_phase() {
351 let phase = 0.5f64;
353 let phase_inc = 0.01f64;
354 let result = polyblep_saw(phase, phase_inc);
355 let naive = 2.0 * phase - 1.0; assert!(
357 (result - naive).abs() < 1e-10,
358 "Should equal naive far from discontinuity"
359 );
360 }
361
362 #[test]
363 fn test_polyblep_square_at_edges() {
364 let phase_inc = 0.01f64;
365
366 let at_rising = polyblep_square(0.005, phase_inc);
368 assert!(at_rising > 0.5 && at_rising < 1.5, "Rising edge should be positive");
370
371 let at_falling = polyblep_square(0.505, phase_inc);
373 assert!(
375 at_falling < -0.5 && at_falling > -1.5,
376 "Falling edge should be negative"
377 );
378
379 let middle_high = polyblep_square(0.25, phase_inc);
381 assert!(
382 (middle_high - 1.0).abs() < 1e-10,
383 "Should be 1.0 in middle of high section"
384 );
385
386 let middle_low = polyblep_square(0.75, phase_inc);
388 assert!(
389 (middle_low - (-1.0)).abs() < 1e-10,
390 "Should be -1.0 in middle of low section"
391 );
392 }
393
394 #[test]
395 fn test_polyblep_pulse_variable_duty() {
396 let phase_inc = 0.01f64;
397 let duty_cycle = 0.25f64;
398
399 let high = polyblep_pulse(0.125, phase_inc, duty_cycle);
401 assert!((high - 1.0).abs() < 1e-10, "Should be 1.0 in high section");
402
403 let low = polyblep_pulse(0.6, phase_inc, duty_cycle);
405 assert!((low - (-1.0)).abs() < 1e-10, "Should be -1.0 in low section");
406 }
407
408 #[test]
409 fn test_polyblamp_triangle() {
410 let phase_inc = 0.01f64;
411
412 let rising = polyblamp_triangle(0.25, phase_inc);
414 assert!(rising.abs() < 0.1, "Should be near 0 at phase 0.25");
416
417 let falling = polyblamp_triangle(0.75, phase_inc);
419 assert!(falling.abs() < 0.1, "Should be near 0 at phase 0.75");
421 }
422
423 #[test]
424 fn test_generic_f32() {
425 let phase = 0.75f32;
428 let phase_inc = 0.01f32;
429
430 let saw = polyblep_saw(phase, phase_inc);
431 let square = polyblep_square(phase, phase_inc);
432 let triangle = polyblamp_triangle(phase, phase_inc);
433
434 assert!((saw - 0.5f32).abs() < 1e-6);
436 assert!((square - (-1.0f32)).abs() < 1e-6);
438 assert!((triangle - 0.0f32).abs() < 0.1);
440 }
441
442 #[test]
443 fn test_generic_f64() {
444 let phase = 0.75f64;
447 let phase_inc = 0.01f64;
448
449 let saw = polyblep_saw(phase, phase_inc);
450 let square = polyblep_square(phase, phase_inc);
451 let triangle = polyblamp_triangle(phase, phase_inc);
452
453 assert!((saw - 0.5f64).abs() < 1e-10);
455 assert!((square - (-1.0f64)).abs() < 1e-10);
457 assert!((triangle - 0.0f64).abs() < 0.1);
459 }
460
461 #[cfg(feature = "simd")]
462 mod simd_tests {
463 use super::*;
464 use crate::sample::Sample;
465
466 fn compare_scalar_simd_blep<S: Sample>(phases: [S; SIMD_LANES], phase_inc: S, tolerance: S) {
467 let phases_simd = S::simd_from_slice(&phases);
468 let phase_inc_simd = S::simd_splat(phase_inc);
469
470 let simd_result = S::simd_to_array(poly_blep_simd::<S>(phases_simd, phase_inc_simd));
471
472 for i in 0..SIMD_LANES {
473 let scalar_result = poly_blep(phases[i], phase_inc);
474 let diff = if simd_result[i] > scalar_result {
475 simd_result[i] - scalar_result
476 } else {
477 scalar_result - simd_result[i]
478 };
479 assert!(
480 diff < tolerance,
481 "SIMD vs scalar mismatch at lane {}: simd={:?}, scalar={:?}, diff={:?}",
482 i,
483 simd_result[i],
484 scalar_result,
485 diff
486 );
487 }
488 }
489
490 fn compare_scalar_simd_blamp<S: Sample>(phases: [S; SIMD_LANES], phase_inc: S, tolerance: S) {
491 let phases_simd = S::simd_from_slice(&phases);
492 let phase_inc_simd = S::simd_splat(phase_inc);
493
494 let simd_result = S::simd_to_array(poly_blamp_simd::<S>(phases_simd, phase_inc_simd));
495
496 for i in 0..SIMD_LANES {
497 let scalar_result = poly_blamp(phases[i], phase_inc);
498 let diff = if simd_result[i] > scalar_result {
499 simd_result[i] - scalar_result
500 } else {
501 scalar_result - simd_result[i]
502 };
503 assert!(
504 diff < tolerance,
505 "SIMD vs scalar mismatch at lane {}: simd={:?}, scalar={:?}, diff={:?}",
506 i,
507 simd_result[i],
508 scalar_result,
509 diff
510 );
511 }
512 }
513
514 #[test]
515 fn test_poly_blep_simd_matches_scalar_f64() {
516 let phase_inc = 0.01f64;
517 let tolerance = 1e-10f64;
518
519 let test_cases: [[f64; SIMD_LANES]; 5] = [
521 [0.25, 0.5, 0.75, 0.9], [0.005, 0.5, 0.75, 0.995], [0.0, 0.01, 0.99, 1.0], [0.001, 0.002, 0.003, 0.004], [0.996, 0.997, 0.998, 0.999], ];
527
528 for phases in test_cases {
529 compare_scalar_simd_blep(phases, phase_inc, tolerance);
530 }
531 }
532
533 #[test]
534 fn test_poly_blep_simd_matches_scalar_f32() {
535 let phase_inc = 0.01f32;
536 let tolerance = 1e-5f32;
537
538 let test_cases: [[f32; SIMD_LANES]; 5] = [
539 [0.25, 0.5, 0.75, 0.9],
540 [0.005, 0.5, 0.75, 0.995],
541 [0.0, 0.01, 0.99, 1.0],
542 [0.001, 0.002, 0.003, 0.004],
543 [0.996, 0.997, 0.998, 0.999],
544 ];
545
546 for phases in test_cases {
547 compare_scalar_simd_blep(phases, phase_inc, tolerance);
548 }
549 }
550
551 #[test]
552 fn test_poly_blamp_simd_matches_scalar_f64() {
553 let phase_inc = 0.01f64;
554 let tolerance = 1e-10f64;
555
556 let test_cases: [[f64; SIMD_LANES]; 5] = [
557 [0.25, 0.5, 0.75, 0.9],
558 [0.005, 0.5, 0.75, 0.995],
559 [0.0, 0.01, 0.99, 1.0],
560 [0.001, 0.002, 0.003, 0.004],
561 [0.996, 0.997, 0.998, 0.999],
562 ];
563
564 for phases in test_cases {
565 compare_scalar_simd_blamp(phases, phase_inc, tolerance);
566 }
567 }
568
569 #[test]
570 fn test_poly_blamp_simd_matches_scalar_f32() {
571 let phase_inc = 0.01f32;
572 let tolerance = 1e-5f32;
573
574 let test_cases: [[f32; SIMD_LANES]; 5] = [
575 [0.25, 0.5, 0.75, 0.9],
576 [0.005, 0.5, 0.75, 0.995],
577 [0.0, 0.01, 0.99, 1.0],
578 [0.001, 0.002, 0.003, 0.004],
579 [0.996, 0.997, 0.998, 0.999],
580 ];
581
582 for phases in test_cases {
583 compare_scalar_simd_blamp(phases, phase_inc, tolerance);
584 }
585 }
586
587 #[test]
588 fn test_apply_polyblep_saw_simd_correctness() {
589 let phase_inc = 0.01f64;
590 let phases: [f64; SIMD_LANES] = [0.005, 0.25, 0.75, 0.995];
591
592 let mut expected = [0.0f64; SIMD_LANES];
594 for i in 0..SIMD_LANES {
595 let naive = 2.0 * phases[i] - 1.0;
596 expected[i] = naive - poly_blep(phases[i], phase_inc);
597 }
598
599 let mut samples: [f64; SIMD_LANES] = [
601 2.0 * phases[0] - 1.0,
602 2.0 * phases[1] - 1.0,
603 2.0 * phases[2] - 1.0,
604 2.0 * phases[3] - 1.0,
605 ];
606 apply_polyblep_saw(&mut samples, phases, phase_inc);
607
608 for i in 0..SIMD_LANES {
609 let diff = (samples[i] - expected[i]).abs();
610 assert!(
611 diff < 1e-10,
612 "Saw mismatch at lane {}: got {:?}, expected {:?}",
613 i,
614 samples[i],
615 expected[i]
616 );
617 }
618 }
619
620 #[test]
621 fn test_apply_polyblep_square_simd_correctness() {
622 let phase_inc = 0.01f64;
623 let phases: [f64; SIMD_LANES] = [0.005, 0.25, 0.505, 0.75];
624
625 let mut expected = [0.0f64; SIMD_LANES];
627 for i in 0..SIMD_LANES {
628 expected[i] = polyblep_square(phases[i], phase_inc);
629 }
630
631 let mut samples: [f64; SIMD_LANES] = [
633 if phases[0] < 0.5 { 1.0 } else { -1.0 },
634 if phases[1] < 0.5 { 1.0 } else { -1.0 },
635 if phases[2] < 0.5 { 1.0 } else { -1.0 },
636 if phases[3] < 0.5 { 1.0 } else { -1.0 },
637 ];
638 apply_polyblep_square(&mut samples, phases, phase_inc);
639
640 for i in 0..SIMD_LANES {
641 let diff = (samples[i] - expected[i]).abs();
642 assert!(
643 diff < 1e-10,
644 "Square mismatch at lane {}: got {:?}, expected {:?}",
645 i,
646 samples[i],
647 expected[i]
648 );
649 }
650 }
651
652 #[test]
653 fn test_apply_polyblamp_triangle_simd_correctness() {
654 let phase_inc = 0.01f64;
655 let phases: [f64; SIMD_LANES] = [0.005, 0.25, 0.505, 0.75];
656
657 let mut expected = [0.0f64; SIMD_LANES];
659 for i in 0..SIMD_LANES {
660 expected[i] = polyblamp_triangle(phases[i], phase_inc);
661 }
662
663 let mut samples: [f64; SIMD_LANES] = [
665 if phases[0] < 0.5 {
666 4.0 * phases[0] - 1.0
667 } else {
668 3.0 - 4.0 * phases[0]
669 },
670 if phases[1] < 0.5 {
671 4.0 * phases[1] - 1.0
672 } else {
673 3.0 - 4.0 * phases[1]
674 },
675 if phases[2] < 0.5 {
676 4.0 * phases[2] - 1.0
677 } else {
678 3.0 - 4.0 * phases[2]
679 },
680 if phases[3] < 0.5 {
681 4.0 * phases[3] - 1.0
682 } else {
683 3.0 - 4.0 * phases[3]
684 },
685 ];
686 apply_polyblamp_triangle(&mut samples, phases, phase_inc);
687
688 for i in 0..SIMD_LANES {
689 let diff = (samples[i] - expected[i]).abs();
690 assert!(
691 diff < 1e-10,
692 "Triangle mismatch at lane {}: got {:?}, expected {:?}",
693 i,
694 samples[i],
695 expected[i]
696 );
697 }
698 }
699 }
700}