Skip to main content

aura_params/
sample.rs

1//! Precision routing for `f32` / `f64`.
2//!
3//! Sealed to those two types. Authors rarely name the traits — use
4//! `Float` for math helpers, `Sample` for buffer/DSP surfaces.
5//!
6//! ```
7//! use aura_params::sample::Float;
8//! let v: f64 = 0.5f32.to_f64();
9//! let back: f32 = f32::from_f64(v);
10//! ```
11
12use std::ops::{Add, Div, Mul, Sub};
13
14/// Math on `f32` or `f64` (gains, freqs, coefficients — not necessarily samples).
15///
16/// Prefer [`Sample`] for `AudioBuffer<S>` and similar buffer bounds.
17pub trait Float:
18    sealed::Sealed
19    + Copy
20    + Add<Output = Self>
21    + Sub<Output = Self>
22    + Mul<Output = Self>
23    + Div<Output = Self>
24{
25    /// `true` for `f64`. Sealed, so equal `IS_F64` means same type.
26    const IS_F64: bool;
27
28    /// Widen `f32` → this precision (identity on `f32`).
29    #[must_use]
30    fn from_f32(v: f32) -> Self;
31
32    /// Narrow `f64` → this precision. Debug-asserts non-NaN on `f32`
33    /// (NaN in the audio path is always a bug). Release keeps NaN via `as`.
34    #[must_use]
35    fn from_f64(v: f64) -> Self;
36
37    /// Narrow to `f32` (same NaN debug-assert as [`Self::from_f64`]).
38    #[must_use]
39    fn to_f32(self) -> f32;
40
41    /// Widen to `f64`.
42    #[must_use]
43    fn to_f64(self) -> f64;
44
45    #[must_use]
46    fn exp(self) -> Self;
47
48    #[must_use]
49    fn log10(self) -> Self;
50
51    #[must_use]
52    fn powf(self, exp: Self) -> Self;
53}
54
55/// [`Float`] plus `Default + Send + Sync + 'static` for buffers and scratch.
56pub trait Sample: Float + Default + Send + Sync + 'static {}
57
58impl Sample for f32 {}
59impl Sample for f64 {}
60
61mod sealed {
62    pub trait Sealed {}
63    impl Sealed for f32 {}
64    impl Sealed for f64 {}
65}
66
67impl Float for f32 {
68    const IS_F64: bool = false;
69
70    #[inline]
71    fn from_f32(v: f32) -> Self {
72        v
73    }
74
75    #[inline]
76    #[allow(clippy::cast_possible_truncation)]
77    fn from_f64(v: f64) -> Self {
78        debug_assert!(
79            !v.is_nan(),
80            "Float::from_f64: NaN narrowed to f32 - DSP loop or coefficient \
81             computation produced an undefined value?",
82        );
83        v as f32
84    }
85
86    #[inline]
87    fn to_f32(self) -> f32 {
88        self
89    }
90
91    #[inline]
92    fn to_f64(self) -> f64 {
93        f64::from(self)
94    }
95
96    #[inline]
97    fn exp(self) -> Self {
98        f32::exp(self)
99    }
100    #[inline]
101    fn log10(self) -> Self {
102        f32::log10(self)
103    }
104    #[inline]
105    fn powf(self, exp: Self) -> Self {
106        f32::powf(self, exp)
107    }
108}
109
110impl Float for f64 {
111    const IS_F64: bool = true;
112
113    #[inline]
114    fn from_f32(v: f32) -> Self {
115        f64::from(v)
116    }
117
118    #[inline]
119    fn from_f64(v: f64) -> Self {
120        v
121    }
122
123    #[inline]
124    #[allow(clippy::cast_possible_truncation)]
125    fn to_f32(self) -> f32 {
126        debug_assert!(
127            !self.is_nan(),
128            "Float::to_f32: NaN narrowed to f32 - DSP loop or coefficient \
129             computation produced an undefined value?",
130        );
131        self as f32
132    }
133
134    #[inline]
135    fn to_f64(self) -> f64 {
136        self
137    }
138
139    #[inline]
140    fn exp(self) -> Self {
141        f64::exp(self)
142    }
143    #[inline]
144    fn log10(self) -> Self {
145        f64::log10(self)
146    }
147    #[inline]
148    fn powf(self, exp: Self) -> Self {
149        f64::powf(self, exp)
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    #[allow(clippy::float_cmp)]
159    fn widen_narrow_round_trip_f32() {
160        let v: f32 = 0.123_456_7;
161        assert_eq!(f32::from_f64(v.to_f64()), v);
162    }
163
164    #[test]
165    fn widen_narrow_round_trip_f64_lossy() {
166        let v: f64 = 0.123_456_789_012_345;
167        let round_tripped = f32::from_f64(v).to_f64();
168        assert!((round_tripped - v).abs() < 1e-7);
169    }
170
171    #[test]
172    #[should_panic(expected = "NaN narrowed to f32")]
173    #[cfg(debug_assertions)]
174    fn nan_narrow_debug_panics() {
175        let _ = f32::from_f64(f64::NAN);
176    }
177}