Skip to main content

rill_core_dsp/
math.rs

1//! # Mathematical abstractions and utilities for DSP
2//!
3//! This module combines:
4//! - Numeric types (`Transcendental`) for f32/f64 abstraction
5//! - Conversion between linear scales and dB
6//! - Fast math functions (tanh, sin, exp)
7//! - Signal generation (sine, saw, square)
8//! - Window functions for granular synthesis
9//! - Interpolation and smoothing
10
11use rill_core::Transcendental;
12
13// -----------------------------------------------------------------------------
14// Conversion between scales
15// -----------------------------------------------------------------------------
16
17/// Convert decibels to linear coefficient
18///
19/// # Formula
20/// `linear = 10^(dB/20)`
21///
22/// # Examples
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/// Convert linear coefficient to decibels
32///
33/// # Formula
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/// Convert MIDI note to frequency
41///
42/// # Formula
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/// Convert frequency to MIDI note
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/// Convert samples to seconds
57#[inline(always)]
58pub fn samples_to_seconds(samples: usize, sample_rate: f32) -> f32 {
59    samples as f32 / sample_rate
60}
61
62/// Convert seconds to samples
63#[inline(always)]
64pub fn seconds_to_samples(seconds: f32, sample_rate: f32) -> usize {
65    (seconds * sample_rate) as usize
66}
67
68// -----------------------------------------------------------------------------
69// Fast math approximations
70// -----------------------------------------------------------------------------
71
72/// Fast exponential approximation (Padé approximant)
73///
74/// Accuracy ~ 1e-5, 2-3x faster than standard 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 for large n
80    // Use n = 2^4 = 16 for good balance
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/// Fast tanh approximation (Padé approximant)
91///
92/// Accuracy ~ 1e-3, very fast (branchless)
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    // Good accuracy for |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/// Fast sine approximation (Taylor series)
107///
108/// Accuracy ~ 1e-3 for |x| < π, very fast
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/// Soft clipping (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// Signal generation
138// -----------------------------------------------------------------------------
139
140/// Generate sine wave (phase 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/// Generate sawtooth wave (phase 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/// Generate triangle wave (phase 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/// Generate square wave (phase 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// Window functions for granular synthesis
174// -----------------------------------------------------------------------------
175
176/// Hann window
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 window
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 window
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/// Variable-shape window (0 = rectangular, 1 = Hann)
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    // Linear interpolation between rectangular and Hann
210    rect.mul(one.sub(shape)).add(hann.mul(shape))
211}
212
213// -----------------------------------------------------------------------------
214// Interpolation
215// -----------------------------------------------------------------------------
216
217/// Linear interpolation
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/// Cubic interpolation (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/// Least-squares interpolation (for fractional delays)
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// Parameter smoothing (to avoid clicks)
278// -----------------------------------------------------------------------------
279
280/// Exponential smoothing (one-pole filter)
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    /// Create a new smoother
290    pub fn new(coeff: T) -> Self {
291        Self {
292            current: T::ZERO,
293            target: T::ZERO,
294            coeff,
295        }
296    }
297
298    /// Set target value
299    #[inline(always)]
300    pub fn set_target(&mut self, target: T) {
301        self.target = target;
302    }
303
304    /// Get current smoothed value (and update)
305    #[allow(clippy::should_implement_trait)]
306    #[inline(always)]
307    pub fn next(&mut self) -> T {
308        self.current = self
309            .current
310            .add(self.target.sub(self.current).mul(self.coeff));
311        self.current
312    }
313
314    /// Process one sample (one-pole low-pass filter)
315    #[inline(always)]
316    pub fn process_sample(&mut self, input: T) -> T {
317        self.current = self.current.add(input.sub(self.current).mul(self.coeff));
318        self.current
319    }
320
321    /// Set value instantly (no smoothing)
322    #[inline(always)]
323    pub fn set_current(&mut self, value: T) {
324        self.current = value;
325        self.target = value;
326    }
327
328    /// Get current value without update
329    #[inline(always)]
330    pub fn current(&self) -> T {
331        self.current
332    }
333}
334
335// -----------------------------------------------------------------------------
336// Tests
337// -----------------------------------------------------------------------------
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    // Tolerance constants
344    const EPSILON: f32 = 1e-4; // Base tolerance
345    const EPSILON_WINDOW: f32 = 1e-3; // Tolerance for window functions
346
347    #[test]
348    fn test_midi_conversion() {
349        println!("\n=== Testing MIDI conversion ===");
350
351        let freq: f32 = midi_to_freq(69);
352        println!("MIDI 69 -> frequency: {:.6} Hz", freq);
353        assert!(
354            (freq - 440.0).abs() < 1.0,
355            "MIDI 69 should be ≈440 Hz, got {:.6}",
356            freq
357        );
358
359        let midi: f32 = freq_to_midi(440.0f32);
360        println!("440 Hz -> MIDI: {:.6}", midi);
361        assert!(
362            (midi - 69.0).abs() < 0.1,
363            "440 Hz should be ≈69, got {:.6}",
364            midi
365        );
366
367        let freq_low: f32 = midi_to_freq(0);
368        println!("MIDI 0 -> frequency: {:.6} Hz", freq_low);
369        assert!(
370            freq_low > 0.0 && freq_low < 100.0,
371            "MIDI 0 should be low frequency, got {}",
372            freq_low
373        );
374
375        let freq_high: f32 = midi_to_freq(127);
376        println!("MIDI 127 -> frequency: {:.6} Hz", freq_high);
377        assert!(
378            freq_high > 10000.0,
379            "MIDI 127 should be high frequency, got {}",
380            freq_high
381        );
382    }
383
384    #[test]
385    fn test_fast_tanh() {
386        println!("\n=== Testing fast tanh approximation ===");
387
388        // Explicitly specify array type
389        let test_values: [f32; 7] = [-3.0, -1.0, -0.5, 0.0, 0.5, 1.0, 3.0];
390
391        for &x in &test_values {
392            let exact: f32 = x.tanh();
393            let fast: f32 = fast_tanh(x);
394            let diff: f32 = (exact - fast).abs();
395
396            println!(
397                "x = {:4.1}: exact = {:8.6}, fast = {:8.6}, diff = {:8.6}",
398                x, exact, fast, diff
399            );
400
401            assert!(
402                diff < 0.1,
403                "Fast tanh at x={} differs too much: exact={}, fast={}",
404                x,
405                exact,
406                fast
407            );
408        }
409    }
410
411    #[test]
412    fn test_windows() {
413        println!("\n=== Testing window functions ===");
414
415        // Explicitly specify array type
416        let test_positions: [f32; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
417
418        println!("Hann window:");
419        for &x in &test_positions {
420            let val: f32 = hann_window(x);
421            println!("  x = {:4.2}: {:.6}", x, val);
422
423            if (x - 0.0).abs() < EPSILON_WINDOW {
424                assert!(
425                    (val - 0.0).abs() < EPSILON_WINDOW,
426                    "Hann at 0 should be ≈0, got {}",
427                    val
428                );
429            }
430            if (x - 0.5).abs() < EPSILON_WINDOW {
431                assert!(
432                    (val - 1.0).abs() < EPSILON_WINDOW,
433                    "Hann at 0.5 should be ≈1.0, got {}",
434                    val
435                );
436            }
437            if (x - 1.0).abs() < EPSILON_WINDOW {
438                assert!(
439                    (val - 0.0).abs() < EPSILON_WINDOW,
440                    "Hann at 1.0 should be ≈0, got {}",
441                    val
442                );
443            }
444        }
445
446        println!("Hamming window:");
447        for &x in &test_positions {
448            let val: f32 = hamming_window(x);
449            println!("  x = {:4.2}: {:.6}", x, val);
450
451            if (x - 0.0).abs() < EPSILON_WINDOW {
452                assert!(
453                    (val - 0.08).abs() < EPSILON_WINDOW * 10.0, // Increase tolerance near edges
454                    "Hamming at 0 should be ≈0.08, got {}",
455                    val
456                );
457            }
458            if (x - 0.5).abs() < EPSILON_WINDOW {
459                assert!(
460                    (val - 1.0).abs() < EPSILON_WINDOW,
461                    "Hamming at 0.5 should be ≈1.0, got {}",
462                    val
463                );
464            }
465        }
466
467        println!("Blackman window:");
468        for &x in &test_positions {
469            let val: f32 = blackman_window(x);
470            println!("  x = {:4.2}: {:.6}", x, val);
471        }
472    }
473
474    #[test]
475    fn test_smoother() {
476        println!("\n=== Testing smoother ===");
477
478        let mut smooth = Smoother::new(0.1f32);
479        smooth.set_target(1.0f32);
480
481        println!("Smoothing from 0 to 1 with coeff=0.1:");
482
483        let mut values: Vec<f32> = Vec::new();
484        for i in 0..10 {
485            let val: f32 = smooth.next();
486            values.push(val);
487            println!("  step {}: {:.6}", i, val);
488        }
489
490        for i in 1..values.len() {
491            assert!(
492                values[i] >= values[i - 1] - 1e-6,
493                "Smoother should increase monotonically: {} < {}",
494                values[i],
495                values[i - 1]
496            );
497        }
498
499        for _ in 0..100 {
500            smooth.next();
501        }
502        let final_val: f32 = smooth.next();
503        println!("Final value after many steps: {:.6}", final_val);
504        assert!(
505            (final_val - 1.0).abs() < 0.1,
506            "Smoother should approach 1.0, got {}",
507            final_val
508        );
509    }
510
511    #[test]
512    fn test_lerp() {
513        println!("\n=== Testing linear interpolation ===");
514
515        // Explicitly specify types in tuples
516        let test_cases: [(f32, f32, f32, f32); 4] = [
517            (0.0, 10.0, 0.0, 0.0),
518            (0.0, 10.0, 0.5, 5.0),
519            (0.0, 10.0, 1.0, 10.0),
520            (-5.0, 5.0, 0.25, -2.5),
521        ];
522
523        for (a, b, t, expected) in test_cases {
524            let result: f32 = lerp(a, b, t);
525            println!(
526                "lerp({}, {}, {}) = {}, expected {}",
527                a, b, t, result, expected
528            );
529            assert!(
530                (result - expected).abs() < 1e-6,
531                "lerp({}, {}, {}) = {}, expected {}",
532                a,
533                b,
534                t,
535                result,
536                expected
537            );
538        }
539    }
540
541    #[test]
542    fn test_seconds_to_samples() {
543        println!("\n=== Testing time conversions ===");
544
545        let sample_rate: f32 = 44100.0;
546
547        // Explicitly specify types in tuples
548        let test_cases: [(f32, usize); 4] = [(0.0, 0), (0.5, 22050), (1.0, 44100), (2.0, 88200)];
549
550        for (seconds, expected) in test_cases {
551            let samples: usize = seconds_to_samples(seconds, sample_rate);
552            println!("{} seconds = {} samples", seconds, samples);
553            assert_eq!(
554                samples, expected,
555                "{} seconds should be {} samples",
556                seconds, expected
557            );
558
559            let back_to_seconds: f32 = samples_to_seconds(samples, sample_rate);
560            println!("  back to seconds: {:.6}", back_to_seconds);
561            assert!(
562                (back_to_seconds - seconds).abs() < 1e-6,
563                "Round trip failed: {} -> {} -> {}",
564                seconds,
565                samples,
566                back_to_seconds
567            );
568        }
569    }
570
571    #[test]
572    fn test_sine_phase() {
573        println!("\n=== Testing sine phase generation ===");
574
575        // Explicitly specify type
576        let test_phases: [f32; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
577
578        for &phase in &test_phases {
579            let val: f32 = sine_phase(phase);
580            println!("sine_phase({}) = {:.6}", phase, val);
581
582            // Verify basic sine properties
583            if (phase - 0.0).abs() < EPSILON {
584                assert!(
585                    (val - 0.0).abs() < EPSILON,
586                    "sin(0) should be 0, got {}",
587                    val
588                );
589            }
590            if (phase - 0.25).abs() < EPSILON {
591                assert!(
592                    (val - 1.0).abs() < EPSILON,
593                    "sin(π/2) should be 1, got {}",
594                    val
595                );
596            }
597            if (phase - 0.5).abs() < EPSILON {
598                assert!(
599                    (val - 0.0).abs() < EPSILON,
600                    "sin(π) should be 0, got {}",
601                    val
602                );
603            }
604        }
605    }
606
607    #[test]
608    fn test_saw_phase() {
609        println!("\n=== Testing saw phase generation ===");
610
611        let test_phases: [f32; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
612
613        for &phase in &test_phases {
614            let val: f32 = saw_phase(phase);
615            println!("saw_phase({}) = {:.6}", phase, val);
616
617            // Saw should be linear from -1 to 1
618            let expected: f32 = 2.0 * phase - 1.0;
619            assert!(
620                (val - expected).abs() < EPSILON,
621                "saw_phase({}) should be {}, got {}",
622                phase,
623                expected,
624                val
625            );
626        }
627    }
628
629    /// Generate triangle wave (phase 0..1)
630    #[inline(always)]
631    pub fn triangle_phase<T: Transcendental>(phase: T) -> T {
632        // Corrected formula:
633        // For phase 0..0.5: 4 * phase - 1
634        // For phase 0.5..1: 3 - 4 * phase
635        let p = phase.to_f32();
636        if p < 0.5 {
637            T::from_f32(4.0 * p - 1.0)
638        } else {
639            T::from_f32(3.0 - 4.0 * p)
640        }
641    }
642
643    // ... in test module ...
644
645    #[test]
646    fn test_db_conversion() {
647        println!("\n=== Testing dB conversion ===");
648
649        // 0 dB -> 1.0
650        let linear: f32 = db_to_linear(0.0f32);
651        println!("0 dB -> linear: {:.6}", linear);
652        assert!(
653            (linear - 1.0).abs() < 1e-4,
654            "0 dB should be ≈1.0, got {:.6}",
655            linear
656        );
657
658        // -6 dB -> 10^(-0.3) ≈ 0.501187
659        let linear: f32 = db_to_linear(-6.0f32);
660        println!("-6 dB -> linear: {:.6}", linear);
661        let expected: f32 = 10.0_f32.powf(-6.0 / 20.0);
662        println!("Expected: {:.6}", expected);
663        assert!(
664            (linear - expected).abs() < 1e-4,
665            "-6 dB should be ≈{:.6}, got {:.6}",
666            expected,
667            linear
668        );
669
670        // +6 dB -> 10^(0.3) ≈ 1.99526
671        let linear: f32 = db_to_linear(6.0f32);
672        println!("+6 dB -> linear: {:.6}", linear);
673        let expected: f32 = 10.0_f32.powf(6.0 / 20.0);
674        assert!(
675            (linear - expected).abs() < 1e-4,
676            "+6 dB should be ≈{:.6}, got {:.6}",
677            expected,
678            linear
679        );
680
681        // Reverse conversion
682        let db: f32 = linear_to_db(0.5f32);
683        println!("0.5 linear -> dB: {:.6}", db);
684        let expected_db: f32 = 20.0 * 0.5f32.log10();
685        assert!(
686            (db - expected_db).abs() < 1e-4,
687            "0.5 should be ≈{:.6} dB, got {:.6}",
688            expected_db,
689            db
690        );
691    }
692
693    #[test]
694    fn test_triangle_phase() {
695        println!("\n=== Testing triangle phase generation ===");
696
697        let test_phases: [f32; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
698
699        for &phase in &test_phases {
700            let val: f32 = triangle_phase(phase);
701            println!("triangle_phase({}) = {:.6}", phase, val);
702
703            if (phase - 0.0).abs() < 1e-6 {
704                assert!(
705                    (val - -1.0).abs() < 1e-4,
706                    "triangle(0) should be -1, got {}",
707                    val
708                );
709            } else if (phase - 0.25).abs() < 1e-6 {
710                assert!(
711                    (val - 0.0).abs() < 1e-4,
712                    "triangle(0.25) should be 0, got {}",
713                    val
714                );
715            } else if (phase - 0.5).abs() < 1e-6 {
716                assert!(
717                    (val - 1.0).abs() < 1e-4,
718                    "triangle(0.5) should be 1, got {}",
719                    val
720                );
721            } else if (phase - 0.75).abs() < 1e-6 {
722                assert!(
723                    (val - 0.0).abs() < 1e-4,
724                    "triangle(0.75) should be 0, got {}",
725                    val
726                );
727            } else if (phase - 1.0).abs() < 1e-6 {
728                assert!(
729                    (val - -1.0).abs() < 1e-4,
730                    "triangle(1.0) should be -1, got {}",
731                    val
732                );
733            }
734        }
735    }
736}