bbx_dsp/
polyblep.rs

1//! PolyBLEP and PolyBLAMP anti-aliasing for band-limited waveform generation.
2//!
3//! This module provides polynomial corrections to eliminate aliasing artifacts from
4//! discontinuous waveforms (sawtooth, square, pulse) and slope discontinuities (triangle).
5//!
6//! PolyBLEP (Polynomial Band-Limited Step) smooths step discontinuities by applying
7//! a polynomial correction near the transition point. PolyBLAMP (Band-Limited rAMP)
8//! is the integrated form, used for slope discontinuities.
9//!
10//! All functions are generic over the `Sample` trait for efficient f32/f64 processing.
11
12#[cfg(feature = "simd")]
13use std::simd::StdFloat;
14
15#[cfg(feature = "simd")]
16use crate::sample::SIMD_LANES;
17use crate::sample::Sample;
18
19/// Fractional part of a Sample value.
20#[inline]
21fn fract<S: Sample>(x: S) -> S {
22    x - S::from_f64(x.to_f64().floor())
23}
24
25/// PolyBLEP correction for step discontinuities.
26///
27/// Takes raw phase and phase increment (both normalized 0-1), handles normalization
28/// internally. Returns a correction value to SUBTRACT from the naive waveform.
29///
30/// # Arguments
31/// * `t` - Current phase (0.0 to 1.0, normalized)
32/// * `dt` - Phase increment per sample (normalized)
33#[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/// PolyBLAMP correction for slope discontinuities.
48///
49/// Used for triangle waves where the slope changes sign but there's no step discontinuity.
50/// This is the integrated form of PolyBLEP.
51///
52/// # Arguments
53/// * `t` - Current phase (0.0 to 1.0, normalized)
54/// * `dt` - Phase increment per sample (normalized)
55#[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/// Branchless SIMD PolyBLEP correction for 4 phase values.
74///
75/// Computes all branches and selects using masks for efficient vectorization.
76/// The branches are mutually exclusive when `dt < 0.5` (valid for all practical
77/// oscillator frequencies below Nyquist/2).
78#[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    // Branch 1: t < dt (just after discontinuity)
87    let t_norm_after = t / dt;
88    let result_after = two * t_norm_after - t_norm_after * t_norm_after - one;
89
90    // Branch 2: t > 1-dt (just before discontinuity)
91    let t_norm_before = (t - one) / dt;
92    let result_before = t_norm_before * t_norm_before + two * t_norm_before + one;
93
94    // Select with masks: after_or_zero where t < dt, then override with before where t > 1-dt
95    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/// Branchless SIMD PolyBLAMP correction for 4 phase values.
100///
101/// Used for slope discontinuities (triangle waves). Computes all branches
102/// and selects using masks for efficient vectorization.
103#[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    // Branch 1: t < dt
112    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    // Branch 2: t > 1-dt
118    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/// Generate a band-limited sawtooth sample using PolyBLEP.
128///
129/// # Arguments
130/// * `phase` - Current normalized phase (0.0 to 1.0)
131/// * `phase_inc` - Phase increment per sample (normalized)
132#[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/// Generate a band-limited square wave sample using PolyBLEP.
140///
141/// # Arguments
142/// * `phase` - Current normalized phase (0.0 to 1.0)
143/// * `phase_inc` - Phase increment per sample (normalized)
144#[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/// Generate a band-limited pulse wave sample using PolyBLEP.
156///
157/// # Arguments
158/// * `phase` - Current normalized phase (0.0 to 1.0)
159/// * `phase_inc` - Phase increment per sample (normalized)
160/// * `duty_cycle` - Duty cycle (0.0 to 1.0)
161#[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/// Generate a band-limited triangle wave sample using PolyBLAMP.
172///
173/// # Arguments
174/// * `phase` - Current normalized phase (0.0 to 1.0)
175/// * `phase_inc` - Phase increment per sample (normalized)
176#[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/// Apply PolyBLEP corrections to a SIMD chunk of sawtooth samples.
196///
197/// Uses branchless SIMD for efficient vectorized correction.
198#[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/// Apply PolyBLEP corrections to a SIMD chunk of square samples.
209///
210/// Uses branchless SIMD for both rising (phase=0) and falling (phase=0.5) edges.
211#[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    // Rising edge correction at phase = 0
219    let rising = poly_blep_simd::<S>(phases_simd, phase_inc_simd);
220
221    // Falling edge correction at phase = 0.5 (SIMD fract)
222    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/// Apply PolyBLEP corrections to a SIMD chunk of pulse samples.
230///
231/// Uses branchless SIMD for both rising (phase=0) and falling (phase=duty_cycle) edges.
232#[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    // Rising edge correction at phase = 0
246    let rising = poly_blep_simd::<S>(phases_simd, phase_inc_simd);
247
248    // Falling edge correction at phase = duty_cycle (SIMD fract)
249    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/// Apply PolyBLAMP corrections to a SIMD chunk of triangle samples.
257///
258/// Uses branchless SIMD for slope changes at phase=0 and phase=0.5.
259#[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    // Slope change at phase = 0
268    let at_zero = poly_blamp_simd::<S>(phases_simd, phase_inc_simd);
269
270    // Slope change at phase = 0.5 (SIMD fract)
271    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        // Just after discontinuity (phase near 0)
295        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        // Just before discontinuity (phase near 1)
303        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        // At t=0 (just after discontinuity), correction should be -1
316        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        // At t approaching dt, correction should approach 0
324        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        // Reference implementation test case:
331        // At phase=0.005, phase_inc=0.01:
332        // naive = 2*0.005 - 1 = -0.99
333        // poly_blep: t < dt, t_norm = 0.5, correction = 2*0.5 - 0.25 - 1 = -0.25
334        // result = -0.99 - (-0.25) = -0.74
335        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        // Far from discontinuity, should be close to naive
352        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; // 0.0
356        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        // Just after rising edge at phase = 0
367        let at_rising = polyblep_square(0.005, phase_inc);
368        // Should be close to 1.0 but with some correction
369        assert!(at_rising > 0.5 && at_rising < 1.5, "Rising edge should be positive");
370
371        // Just after falling edge at phase = 0.5
372        let at_falling = polyblep_square(0.505, phase_inc);
373        // Should be close to -1.0 but with some correction
374        assert!(
375            at_falling < -0.5 && at_falling > -1.5,
376            "Falling edge should be negative"
377        );
378
379        // Middle of high section (no edges nearby)
380        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        // Middle of low section (no edges nearby)
387        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        // Middle of high section (phase < duty_cycle, away from edges)
400        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        // Middle of low section (phase > duty_cycle, away from edges)
404        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        // Middle of rising section
413        let rising = polyblamp_triangle(0.25, phase_inc);
414        // Naive: 4*0.25 - 1 = 0.0, correction should be minimal
415        assert!(rising.abs() < 0.1, "Should be near 0 at phase 0.25");
416
417        // Middle of falling section
418        let falling = polyblamp_triangle(0.75, phase_inc);
419        // Naive: 3 - 4*0.75 = 0.0, correction should be minimal
420        assert!(falling.abs() < 0.1, "Should be near 0 at phase 0.75");
421    }
422
423    #[test]
424    fn test_generic_f32() {
425        // Verify functions work with f32
426        // Use phase=0.75 to be in middle of section, away from discontinuities
427        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        // saw at 0.75: naive = 2*0.75 - 1 = 0.5
435        assert!((saw - 0.5f32).abs() < 1e-6);
436        // square at 0.75: in low section, away from edges
437        assert!((square - (-1.0f32)).abs() < 1e-6);
438        // triangle at 0.75: naive = 3 - 4*0.75 = 0
439        assert!((triangle - 0.0f32).abs() < 0.1);
440    }
441
442    #[test]
443    fn test_generic_f64() {
444        // Verify functions work with f64
445        // Use phase=0.75 to be in middle of section, away from discontinuities
446        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        // saw at 0.75: naive = 2*0.75 - 1 = 0.5
454        assert!((saw - 0.5f64).abs() < 1e-10);
455        // square at 0.75: in low section, away from edges
456        assert!((square - (-1.0f64)).abs() < 1e-10);
457        // triangle at 0.75: naive = 3 - 4*0.75 = 0
458        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            // Test various phase combinations including near discontinuities
520            let test_cases: [[f64; SIMD_LANES]; 5] = [
521                [0.25, 0.5, 0.75, 0.9],       // Middle phases (no correction)
522                [0.005, 0.5, 0.75, 0.995],    // Near discontinuities
523                [0.0, 0.01, 0.99, 1.0],       // At boundaries
524                [0.001, 0.002, 0.003, 0.004], // All near start
525                [0.996, 0.997, 0.998, 0.999], // All near end
526            ];
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            // Compute expected via scalar
593            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            // Generate naive samples and apply SIMD correction
600            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            // Compute expected via scalar
626            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            // Generate naive samples and apply SIMD correction
632            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            // Compute expected via scalar
658            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            // Generate naive samples and apply SIMD correction
664            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}