Skip to main content

rill_core_dsp/
math.rs

1//! # Математические абстракции и утилиты для DSP
2//!
3//! Этот модуль объединяет:
4//! - Числовые типы (`Transcendental`) для абстракции f32/f64
5//! - Конвертацию между линейными шкалами и dB
6//! - Быстрые математические функции (tanh, sin, exp)
7//! - Генерацию сигналов (синус, пила, квадрат)
8//! - Оконные функции для гранулярного синтеза
9//! - Интерполяцию и сглаживание
10
11use rill_core::Transcendental;
12
13// -----------------------------------------------------------------------------
14// Конвертация между шкалами
15// -----------------------------------------------------------------------------
16
17/// Преобразовать децибелы в линейный коэффициент
18///
19/// # Формула
20/// `linear = 10^(dB/20)`
21///
22/// # Примеры
23/// - 0 dB → 1.0
24/// - -6 dB → 0.5
25/// - +6 dB → 2.0
26#[inline(always)]
27pub fn db_to_linear<T: Transcendental>(db: T) -> T {
28    T::from_f32(10.0_f32.powf(db.to_f32() / 20.0))
29}
30
31/// Преобразовать линейный коэффициент в децибелы
32///
33/// # Формула
34/// `dB = 20 * log10(linear)`
35#[inline(always)]
36pub fn linear_to_db<T: Transcendental>(linear: T) -> T {
37    T::from_f32(20.0 * linear.to_f32().log10())
38}
39
40/// Преобразовать MIDI ноту в частоту
41///
42/// # Формула
43/// `freq = 440 * 2^((note - 69)/12)`
44#[inline(always)]
45pub fn midi_to_freq<T: Transcendental>(note: u8) -> T {
46    let exp = (note as f32 - 69.0) / 12.0;
47    T::from_f32(440.0 * 2.0_f32.powf(exp))
48}
49
50/// Преобразовать частоту в MIDI ноту
51#[inline(always)]
52pub fn freq_to_midi<T: Transcendental>(freq: T) -> f32 {
53    69.0 + 12.0 * (freq.to_f32() / 440.0).log2()
54}
55
56/// Преобразовать семплы в секунды
57#[inline(always)]
58pub fn samples_to_seconds(samples: usize, sample_rate: f32) -> f32 {
59    samples as f32 / sample_rate
60}
61
62/// Преобразовать секунды в семплы
63#[inline(always)]
64pub fn seconds_to_samples(seconds: f32, sample_rate: f32) -> usize {
65    (seconds * sample_rate) as usize
66}
67
68// -----------------------------------------------------------------------------
69// Быстрые математические аппроксимации
70// -----------------------------------------------------------------------------
71
72/// Быстрая аппроксимация экспоненты (Pade аппроксимант)
73///
74/// Точность ~ 1e-5, в 2-3 раза быстрее стандартной exp()
75#[inline(always)]
76pub fn fast_exp<T: Transcendental>(x: T) -> T {
77    let xf = x.to_f32();
78
79    // exp(x) ≈ (1 + x/n)^n для большого n
80    // Используем n = 2^4 = 16 для хорошего баланса
81    let mut result = 1.0 + xf / 16.0;
82    result *= result; // ^2
83    result *= result; // ^4
84    result *= result; // ^8
85    result *= result; // ^16
86
87    T::from_f32(result)
88}
89
90/// Быстрая аппроксимация tanh (Pade аппроксимант)
91///
92/// Точность ~ 1e-3, очень быстрая (без ветвлений)
93#[inline(always)]
94pub fn fast_tanh<T: Transcendental>(x: T) -> T {
95    let xf = x.to_f32();
96
97    // tanh(x) ≈ x * (27 + x^2) / (27 + 9*x^2)
98    // Точность хороша для |x| < 3
99    let x2 = xf * xf;
100    let numerator = xf * (27.0 + x2);
101    let denominator = 27.0 + 9.0 * x2;
102
103    T::from_f32(numerator / denominator)
104}
105
106/// Быстрая аппроксимация синуса (Taylor ряд)
107///
108/// Точность ~ 1e-3 для |x| < π, очень быстрая
109#[inline(always)]
110pub fn fast_sin<T: Transcendental>(x: T) -> T {
111    let xf = x.to_f32();
112
113    // sin(x) ≈ x - x^3/6 + x^5/120
114    let x2 = xf * xf;
115    let x3 = x2 * xf;
116    let x5 = x3 * x2;
117
118    T::from_f32(xf - x3 / 6.0 + x5 / 120.0)
119}
120
121/// Мягкое клиппирование (wave shaping)
122#[inline(always)]
123pub fn soft_clip<T: Transcendental>(x: T, threshold: T) -> T {
124    let xf = x.to_f32();
125    let t = threshold.to_f32();
126
127    if xf > t {
128        T::from_f32(t + (xf - t) / (1.0 + ((xf - t) / (1.0 - t)).powi(2)))
129    } else if xf < -t {
130        T::from_f32(-t - (-xf - t) / (1.0 + ((-xf - t) / (1.0 - t)).powi(2)))
131    } else {
132        x
133    }
134}
135
136// -----------------------------------------------------------------------------
137// Генерация сигналов
138// -----------------------------------------------------------------------------
139
140/// Генерация синусоиды (фаза 0..1)
141#[inline(always)]
142pub fn sine_phase<T: Transcendental>(phase: T) -> T {
143    (phase * T::from_f32(2.0) * T::PI).sin()
144}
145
146/// Генерация пилообразной волны (фаза 0..1)
147#[inline(always)]
148pub fn saw_phase<T: Transcendental>(phase: T) -> T {
149    // 2 * phase - 1
150    phase.mul(T::from_f32(2.0)).sub(T::from_f32(1.0))
151}
152
153/// Генерация треугольной волны (фаза 0..1)
154#[inline(always)]
155pub fn triangle_phase<T: Transcendental>(phase: T) -> T {
156    // 4 * |phase - 0.5| - 1
157    let p = phase.sub(T::from_f32(0.5));
158    let abs_p = p.abs();
159    abs_p.mul(T::from_f32(4.0)).sub(T::from_f32(1.0))
160}
161
162/// Генерация квадратной волны (фаза 0..1, pulse_width 0..1)
163#[inline(always)]
164pub fn square_phase<T: Transcendental>(phase: T, pulse_width: T) -> T {
165    if phase.to_f32() < pulse_width.to_f32() {
166        T::from_f32(1.0)
167    } else {
168        T::from_f32(-1.0)
169    }
170}
171
172// -----------------------------------------------------------------------------
173// Оконные функции для гранулярного синтеза
174// -----------------------------------------------------------------------------
175
176/// Окно Ханна (Hann)
177#[inline(always)]
178pub fn hann_window<T: Transcendental>(x: T) -> T {
179    // 0.5 * (1 - cos(2πx))
180    let cos_term = (x * T::from_f32(2.0) * T::PI).cos();
181    T::from_f32(0.5) * (T::from_f32(1.0) - cos_term)
182}
183
184/// Окно Хэмминга (Hamming)
185#[inline(always)]
186pub fn hamming_window<T: Transcendental>(x: T) -> T {
187    // 0.54 - 0.46 * cos(2πx)
188    let cos_term = (x * T::from_f32(2.0) * T::PI).cos();
189    T::from_f32(0.54) - T::from_f32(0.46) * cos_term
190}
191
192/// Окно Блэкмана (Blackman)
193#[inline(always)]
194pub fn blackman_window<T: Transcendental>(x: T) -> T {
195    // 0.42 - 0.5 * cos(2πx) + 0.08 * cos(4πx)
196    let cos1 = (x * T::from_f32(2.0) * T::PI).cos();
197    let cos2 = (x * T::from_f32(4.0) * T::PI).cos();
198
199    T::from_f32(0.42) - T::from_f32(0.5) * cos1 + T::from_f32(0.08) * cos2
200}
201
202/// Окно с переменной формой (0 = прямоугольное, 1 = Ханна)
203#[inline(always)]
204pub fn variable_window<T: Transcendental>(x: T, shape: T) -> T {
205    let one = T::from_f32(1.0);
206    let rect = one;
207    let hann = hann_window(x);
208
209    // Линейная интерполяция между прямоугольным и ханна
210    rect.mul(one.sub(shape)).add(hann.mul(shape))
211}
212
213// -----------------------------------------------------------------------------
214// Интерполяция
215// -----------------------------------------------------------------------------
216
217/// Линейная интерполяция
218#[inline(always)]
219pub fn lerp<T: Transcendental>(a: T, b: T, t: T) -> T {
220    a.add(b.sub(a).mul(t))
221}
222
223/// Кубическая интерполяция (Hermite)
224#[inline(always)]
225pub fn cubic_interpolate<T: Transcendental>(y0: T, y1: T, y2: T, y3: T, t: T) -> T {
226    let t2 = t.mul(t);
227    let t3 = t2.mul(t);
228
229    let a0 = y3.sub(y2).sub(y0.sub(y1));
230    let a1 = y0.sub(y1).sub(a0);
231    let a2 = y2.sub(y0);
232    let a3 = y1;
233
234    a0.mul(t3).add(a1.mul(t2)).add(a2.mul(t)).add(a3)
235}
236
237/// Интерполяция методом наименьших квадратов (для дробных задержек)
238#[inline(always)]
239pub fn lagrange_interpolate<T: Transcendental>(y: &[T; 4], x: T) -> T {
240    let x0 = T::from_f32(0.0);
241    let x1 = T::from_f32(1.0);
242    let x2 = T::from_f32(2.0);
243    let x3 = T::from_f32(3.0);
244
245    let term0 = y[0].mul(
246        (x.sub(x1))
247            .mul(x.sub(x2))
248            .mul(x.sub(x3))
249            .div((x0.sub(x1)).mul(x0.sub(x2)).mul(x0.sub(x3))),
250    );
251
252    let term1 = y[1].mul(
253        (x.sub(x0))
254            .mul(x.sub(x2))
255            .mul(x.sub(x3))
256            .div((x1.sub(x0)).mul(x1.sub(x2)).mul(x1.sub(x3))),
257    );
258
259    let term2 = y[2].mul(
260        (x.sub(x0))
261            .mul(x.sub(x1))
262            .mul(x.sub(x3))
263            .div((x2.sub(x0)).mul(x2.sub(x1)).mul(x2.sub(x3))),
264    );
265
266    let term3 = y[3].mul(
267        (x.sub(x0))
268            .mul(x.sub(x1))
269            .mul(x.sub(x2))
270            .div((x3.sub(x0)).mul(x3.sub(x1)).mul(x3.sub(x2))),
271    );
272
273    term0.add(term1).add(term2).add(term3)
274}
275
276// -----------------------------------------------------------------------------
277// Сглаживание параметров (для избежания щелчков)
278// -----------------------------------------------------------------------------
279
280/// Экспоненциальное сглаживание (однополюсный фильтр)
281#[derive(Debug, Clone)]
282pub struct Smoother<T: Transcendental> {
283    current: T,
284    target: T,
285    coeff: T,
286}
287
288impl<T: Transcendental> Smoother<T> {
289    /// Создать новый сглаживатель
290    pub fn new(coeff: T) -> Self {
291        Self {
292            current: T::ZERO,
293            target: T::ZERO,
294            coeff,
295        }
296    }
297
298    /// Установить целевое значение
299    #[inline(always)]
300    pub fn set_target(&mut self, target: T) {
301        self.target = target;
302    }
303
304    /// Получить текущее сглаженное значение (и обновить)
305    #[inline(always)]
306    pub fn next(&mut self) -> T {
307        self.current = self
308            .current
309            .add(self.target.sub(self.current).mul(self.coeff));
310        self.current
311    }
312
313    /// Обработать один семпл (однополюсный фильтр низких частот)
314    #[inline(always)]
315    pub fn process_sample(&mut self, input: T) -> T {
316        self.current = self.current.add(input.sub(self.current).mul(self.coeff));
317        self.current
318    }
319
320    /// Мгновенно установить значение (без сглаживания)
321    #[inline(always)]
322    pub fn set_current(&mut self, value: T) {
323        self.current = value;
324        self.target = value;
325    }
326
327    /// Получить текущее значение без обновления
328    #[inline(always)]
329    pub fn current(&self) -> T {
330        self.current
331    }
332}
333
334// -----------------------------------------------------------------------------
335// Тесты
336// -----------------------------------------------------------------------------
337
338// В файле rill-core-dsp/src/math.rs
339
340// В файле rill-core-dsp/src/math.rs
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use float_cmp::approx_eq;
346
347    // Константы для допусков
348    const EPSILON: f32 = 1e-4; // Базовый допуск
349    const EPSILON_DB: f32 = 0.1; // Допуск для dB (0.1 dB)
350    const EPSILON_WINDOW: f32 = 1e-3; // Допуск для оконных функций
351
352    #[test]
353    fn test_midi_conversion() {
354        println!("\n=== Testing MIDI conversion ===");
355
356        let freq: f32 = midi_to_freq(69);
357        println!("MIDI 69 -> frequency: {:.6} Hz", freq);
358        assert!(
359            (freq - 440.0).abs() < 1.0,
360            "MIDI 69 should be ≈440 Hz, got {:.6}",
361            freq
362        );
363
364        let midi: f32 = freq_to_midi(440.0f32);
365        println!("440 Hz -> MIDI: {:.6}", midi);
366        assert!(
367            (midi - 69.0).abs() < 0.1,
368            "440 Hz should be ≈69, got {:.6}",
369            midi
370        );
371
372        let freq_low: f32 = midi_to_freq(0);
373        println!("MIDI 0 -> frequency: {:.6} Hz", freq_low);
374        assert!(
375            freq_low > 0.0 && freq_low < 100.0,
376            "MIDI 0 should be low frequency, got {}",
377            freq_low
378        );
379
380        let freq_high: f32 = midi_to_freq(127);
381        println!("MIDI 127 -> frequency: {:.6} Hz", freq_high);
382        assert!(
383            freq_high > 10000.0,
384            "MIDI 127 should be high frequency, got {}",
385            freq_high
386        );
387    }
388
389    #[test]
390    fn test_fast_tanh() {
391        println!("\n=== Testing fast tanh approximation ===");
392
393        // Явно указываем тип для массива
394        let test_values: [f32; 7] = [-3.0, -1.0, -0.5, 0.0, 0.5, 1.0, 3.0];
395
396        for &x in &test_values {
397            let exact: f32 = x.tanh();
398            let fast: f32 = fast_tanh(x);
399            let diff: f32 = (exact - fast).abs();
400
401            println!(
402                "x = {:4.1}: exact = {:8.6}, fast = {:8.6}, diff = {:8.6}",
403                x, exact, fast, diff
404            );
405
406            assert!(
407                diff < 0.1,
408                "Fast tanh at x={} differs too much: exact={}, fast={}",
409                x,
410                exact,
411                fast
412            );
413        }
414    }
415
416    #[test]
417    fn test_windows() {
418        println!("\n=== Testing window functions ===");
419
420        // Явно указываем тип для массива
421        let test_positions: [f32; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
422
423        println!("Hann window:");
424        for &x in &test_positions {
425            let val: f32 = hann_window(x);
426            println!("  x = {:4.2}: {:.6}", x, val);
427
428            if (x - 0.0).abs() < EPSILON_WINDOW {
429                assert!(
430                    (val - 0.0).abs() < EPSILON_WINDOW,
431                    "Hann at 0 should be ≈0, got {}",
432                    val
433                );
434            }
435            if (x - 0.5).abs() < EPSILON_WINDOW {
436                assert!(
437                    (val - 1.0).abs() < EPSILON_WINDOW,
438                    "Hann at 0.5 should be ≈1.0, got {}",
439                    val
440                );
441            }
442            if (x - 1.0).abs() < EPSILON_WINDOW {
443                assert!(
444                    (val - 0.0).abs() < EPSILON_WINDOW,
445                    "Hann at 1.0 should be ≈0, got {}",
446                    val
447                );
448            }
449        }
450
451        println!("Hamming window:");
452        for &x in &test_positions {
453            let val: f32 = hamming_window(x);
454            println!("  x = {:4.2}: {:.6}", x, val);
455
456            if (x - 0.0).abs() < EPSILON_WINDOW {
457                assert!(
458                    (val - 0.08).abs() < EPSILON_WINDOW * 10.0, // Увеличиваем допуск для краев
459                    "Hamming at 0 should be ≈0.08, got {}",
460                    val
461                );
462            }
463            if (x - 0.5).abs() < EPSILON_WINDOW {
464                assert!(
465                    (val - 1.0).abs() < EPSILON_WINDOW,
466                    "Hamming at 0.5 should be ≈1.0, got {}",
467                    val
468                );
469            }
470        }
471
472        println!("Blackman window:");
473        for &x in &test_positions {
474            let val: f32 = blackman_window(x);
475            println!("  x = {:4.2}: {:.6}", x, val);
476        }
477    }
478
479    #[test]
480    fn test_smoother() {
481        println!("\n=== Testing smoother ===");
482
483        let mut smooth = Smoother::new(0.1f32);
484        smooth.set_target(1.0f32);
485
486        println!("Smoothing from 0 to 1 with coeff=0.1:");
487
488        let mut values: Vec<f32> = Vec::new();
489        for i in 0..10 {
490            let val: f32 = smooth.next();
491            values.push(val);
492            println!("  step {}: {:.6}", i, val);
493        }
494
495        for i in 1..values.len() {
496            assert!(
497                values[i] >= values[i - 1] - 1e-6,
498                "Smoother should increase monotonically: {} < {}",
499                values[i],
500                values[i - 1]
501            );
502        }
503
504        for _ in 0..100 {
505            smooth.next();
506        }
507        let final_val: f32 = smooth.next();
508        println!("Final value after many steps: {:.6}", final_val);
509        assert!(
510            (final_val - 1.0).abs() < 0.1,
511            "Smoother should approach 1.0, got {}",
512            final_val
513        );
514    }
515
516    #[test]
517    fn test_lerp() {
518        println!("\n=== Testing linear interpolation ===");
519
520        // Явно указываем типы в кортежах
521        let test_cases: [(f32, f32, f32, f32); 4] = [
522            (0.0, 10.0, 0.0, 0.0),
523            (0.0, 10.0, 0.5, 5.0),
524            (0.0, 10.0, 1.0, 10.0),
525            (-5.0, 5.0, 0.25, -2.5),
526        ];
527
528        for (a, b, t, expected) in test_cases {
529            let result: f32 = lerp(a, b, t);
530            println!(
531                "lerp({}, {}, {}) = {}, expected {}",
532                a, b, t, result, expected
533            );
534            assert!(
535                (result - expected).abs() < 1e-6,
536                "lerp({}, {}, {}) = {}, expected {}",
537                a,
538                b,
539                t,
540                result,
541                expected
542            );
543        }
544    }
545
546    #[test]
547    fn test_seconds_to_samples() {
548        println!("\n=== Testing time conversions ===");
549
550        let sample_rate: f32 = 44100.0;
551
552        // Явно указываем типы в кортежах
553        let test_cases: [(f32, usize); 4] = [(0.0, 0), (0.5, 22050), (1.0, 44100), (2.0, 88200)];
554
555        for (seconds, expected) in test_cases {
556            let samples: usize = seconds_to_samples(seconds, sample_rate);
557            println!("{} seconds = {} samples", seconds, samples);
558            assert_eq!(
559                samples, expected,
560                "{} seconds should be {} samples",
561                seconds, expected
562            );
563
564            let back_to_seconds: f32 = samples_to_seconds(samples, sample_rate);
565            println!("  back to seconds: {:.6}", back_to_seconds);
566            assert!(
567                (back_to_seconds - seconds).abs() < 1e-6,
568                "Round trip failed: {} -> {} -> {}",
569                seconds,
570                samples,
571                back_to_seconds
572            );
573        }
574    }
575
576    #[test]
577    fn test_sine_phase() {
578        println!("\n=== Testing sine phase generation ===");
579
580        // Явно указываем тип
581        let test_phases: [f32; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
582
583        for &phase in &test_phases {
584            let val: f32 = sine_phase(phase);
585            println!("sine_phase({}) = {:.6}", phase, val);
586
587            // Проверяем основные свойства синуса
588            if (phase - 0.0).abs() < EPSILON {
589                assert!(
590                    (val - 0.0).abs() < EPSILON,
591                    "sin(0) should be 0, got {}",
592                    val
593                );
594            }
595            if (phase - 0.25).abs() < EPSILON {
596                assert!(
597                    (val - 1.0).abs() < EPSILON,
598                    "sin(π/2) should be 1, got {}",
599                    val
600                );
601            }
602            if (phase - 0.5).abs() < EPSILON {
603                assert!(
604                    (val - 0.0).abs() < EPSILON,
605                    "sin(π) should be 0, got {}",
606                    val
607                );
608            }
609        }
610    }
611
612    #[test]
613    fn test_saw_phase() {
614        println!("\n=== Testing saw phase generation ===");
615
616        let test_phases: [f32; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
617
618        for &phase in &test_phases {
619            let val: f32 = saw_phase(phase);
620            println!("saw_phase({}) = {:.6}", phase, val);
621
622            // Пила должна быть линейной от -1 до 1
623            let expected: f32 = 2.0 * phase - 1.0;
624            assert!(
625                (val - expected).abs() < EPSILON,
626                "saw_phase({}) should be {}, got {}",
627                phase,
628                expected,
629                val
630            );
631        }
632    }
633
634    /// Генерация треугольной волны (фаза 0..1)
635    #[inline(always)]
636    pub fn triangle_phase<T: Transcendental>(phase: T) -> T {
637        // Исправленная формула:
638        // Для фазы 0..0.5: 4 * phase - 1
639        // Для фазы 0.5..1: 3 - 4 * phase
640        let p = phase.to_f32();
641        if p < 0.5 {
642            T::from_f32(4.0 * p - 1.0)
643        } else {
644            T::from_f32(3.0 - 4.0 * p)
645        }
646    }
647
648    // ... в модуле тестов ...
649
650    #[test]
651    fn test_db_conversion() {
652        println!("\n=== Testing dB conversion ===");
653
654        // 0 dB -> 1.0
655        let linear: f32 = db_to_linear(0.0f32);
656        println!("0 dB -> linear: {:.6}", linear);
657        assert!(
658            (linear - 1.0).abs() < 1e-4,
659            "0 dB should be ≈1.0, got {:.6}",
660            linear
661        );
662
663        // -6 dB -> 10^(-0.3) ≈ 0.501187
664        let linear: f32 = db_to_linear(-6.0f32);
665        println!("-6 dB -> linear: {:.6}", linear);
666        let expected: f32 = 10.0_f32.powf(-6.0 / 20.0);
667        println!("Expected: {:.6}", expected);
668        assert!(
669            (linear - expected).abs() < 1e-4,
670            "-6 dB should be ≈{:.6}, got {:.6}",
671            expected,
672            linear
673        );
674
675        // +6 dB -> 10^(0.3) ≈ 1.99526
676        let linear: f32 = db_to_linear(6.0f32);
677        println!("+6 dB -> linear: {:.6}", linear);
678        let expected: f32 = 10.0_f32.powf(6.0 / 20.0);
679        assert!(
680            (linear - expected).abs() < 1e-4,
681            "+6 dB should be ≈{:.6}, got {:.6}",
682            expected,
683            linear
684        );
685
686        // Обратное преобразование
687        let db: f32 = linear_to_db(0.5f32);
688        println!("0.5 linear -> dB: {:.6}", db);
689        let expected_db: f32 = 20.0 * 0.5f32.log10();
690        assert!(
691            (db - expected_db).abs() < 1e-4,
692            "0.5 should be ≈{:.6} dB, got {:.6}",
693            expected_db,
694            db
695        );
696    }
697
698    #[test]
699    fn test_triangle_phase() {
700        println!("\n=== Testing triangle phase generation ===");
701
702        let test_phases: [f32; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
703
704        for &phase in &test_phases {
705            let val: f32 = triangle_phase(phase);
706            println!("triangle_phase({}) = {:.6}", phase, val);
707
708            if (phase - 0.0).abs() < 1e-6 {
709                assert!(
710                    (val - -1.0).abs() < 1e-4,
711                    "triangle(0) should be -1, got {}",
712                    val
713                );
714            } else if (phase - 0.25).abs() < 1e-6 {
715                assert!(
716                    (val - 0.0).abs() < 1e-4,
717                    "triangle(0.25) should be 0, got {}",
718                    val
719                );
720            } else if (phase - 0.5).abs() < 1e-6 {
721                assert!(
722                    (val - 1.0).abs() < 1e-4,
723                    "triangle(0.5) should be 1, got {}",
724                    val
725                );
726            } else if (phase - 0.75).abs() < 1e-6 {
727                assert!(
728                    (val - 0.0).abs() < 1e-4,
729                    "triangle(0.75) should be 0, got {}",
730                    val
731                );
732            } else if (phase - 1.0).abs() < 1e-6 {
733                assert!(
734                    (val - -1.0).abs() < 1e-4,
735                    "triangle(1.0) should be -1, got {}",
736                    val
737                );
738            }
739        }
740    }
741}