Skip to main content

truce_simd/
ops.rs

1//! Block-rate audio ops at `f32`. See [`crate::ops64`] for the
2//! `f64` mirror.
3//!
4//! Two implementations per op when `wide-backend` is enabled:
5//! a scalar fallback (the `*_scalar` variant, also the body when
6//! the feature is off) and a vector path that processes `f32x8`
7//! chunks. The scalar variants are kept `pub` so the Criterion
8//! benches can compare pre-/post-vectorization on exactly the
9//! same operation.
10
11/// `buf[i] *= gain` for every element. Scalar.
12#[inline]
13pub fn gain_block_scalar(buf: &mut [f32], gain: f32) {
14    for s in buf {
15        *s *= gain;
16    }
17}
18
19/// `buf[i] *= gain` for every element. Vectorized when
20/// `wide-backend` is on, otherwise identical to the scalar path.
21#[inline]
22pub fn gain_block(buf: &mut [f32], gain: f32) {
23    #[cfg(feature = "wide-backend")]
24    {
25        use wide::f32x8;
26        let g = f32x8::splat(gain);
27        let n = buf.len();
28        let n8 = n / 8 * 8;
29        let (head, tail) = buf.split_at_mut(n8);
30        for chunk in head.chunks_exact_mut(8) {
31            let v = f32x8::from(<[f32; 8]>::try_from(&chunk[..]).unwrap_or_default());
32            chunk.copy_from_slice((v * g).as_array_ref());
33        }
34        gain_block_scalar(tail, gain);
35    }
36    #[cfg(not(feature = "wide-backend"))]
37    gain_block_scalar(buf, gain);
38}
39
40/// `out[i] = src[i] * scale` (length: `min(out, src)`). Scalar.
41/// The non-aliased counterpart to [`gain_block`] - fills the
42/// "copy then gain" hole in the API so a single line covers what
43/// would otherwise need two calls and a discarded intermediate.
44#[inline]
45pub fn scale_block_scalar(out: &mut [f32], src: &[f32], scale: f32) {
46    let n = out.len().min(src.len());
47    for i in 0..n {
48        out[i] = src[i] * scale;
49    }
50}
51
52/// `out[i] = src[i] * scale`. Vectorized when `wide-backend` is on.
53#[inline]
54pub fn scale_block(out: &mut [f32], src: &[f32], scale: f32) {
55    #[cfg(feature = "wide-backend")]
56    {
57        use wide::f32x8;
58        let n = out.len().min(src.len());
59        let n8 = n / 8 * 8;
60        let g = f32x8::splat(scale);
61        let (out_v, out_tail) = out[..n].split_at_mut(n8);
62        let src_v = &src[..n8];
63        let src_tail = &src[n8..n];
64        for (out_chunk, src_chunk) in out_v.chunks_exact_mut(8).zip(src_v.chunks_exact(8)) {
65            let v = f32x8::from(<[f32; 8]>::try_from(src_chunk).unwrap_or_default());
66            out_chunk.copy_from_slice((v * g).as_array_ref());
67        }
68        scale_block_scalar(out_tail, src_tail, scale);
69    }
70    #[cfg(not(feature = "wide-backend"))]
71    scale_block_scalar(out, src, scale);
72}
73
74/// `out[i] = a[i] * b[i]` (length: `min(out, a, b)`). Scalar.
75#[inline]
76pub fn mul_block_scalar(out: &mut [f32], a: &[f32], b: &[f32]) {
77    let n = out.len().min(a.len()).min(b.len());
78    for i in 0..n {
79        out[i] = a[i] * b[i];
80    }
81}
82
83/// `out[i] = a[i] * b[i]`. Vectorized when `wide-backend` is on.
84#[inline]
85pub fn mul_block(out: &mut [f32], a: &[f32], b: &[f32]) {
86    #[cfg(feature = "wide-backend")]
87    {
88        use wide::f32x8;
89        let n = out.len().min(a.len()).min(b.len());
90        let n8 = n / 8 * 8;
91        let (out_v, out_tail) = out[..n].split_at_mut(n8);
92        let a_v = &a[..n8];
93        let b_v = &b[..n8];
94        let a_tail = &a[n8..n];
95        let b_tail = &b[n8..n];
96        for ((out_chunk, a_chunk), b_chunk) in out_v
97            .chunks_exact_mut(8)
98            .zip(a_v.chunks_exact(8))
99            .zip(b_v.chunks_exact(8))
100        {
101            // chunks_exact guarantees length == 8, so the array
102            // conversions are infallible by construction.
103            let av = f32x8::from(<[f32; 8]>::try_from(a_chunk).unwrap_or_default());
104            let bv = f32x8::from(<[f32; 8]>::try_from(b_chunk).unwrap_or_default());
105            let mv = av * bv;
106            out_chunk.copy_from_slice(mv.as_array_ref());
107        }
108        mul_block_scalar(out_tail, a_tail, b_tail);
109    }
110    #[cfg(not(feature = "wide-backend"))]
111    mul_block_scalar(out, a, b);
112}
113
114/// Multiply-accumulate: `out[i] += src[i] * scale`. Scalar.
115#[inline]
116pub fn mac_block_scalar(out: &mut [f32], src: &[f32], scale: f32) {
117    let n = out.len().min(src.len());
118    for i in 0..n {
119        out[i] += src[i] * scale;
120    }
121}
122
123/// `out[i] += src[i] * scale`. Vectorized when `wide-backend` is on.
124#[inline]
125pub fn mac_block(out: &mut [f32], src: &[f32], scale: f32) {
126    #[cfg(feature = "wide-backend")]
127    {
128        use wide::f32x8;
129        let n = out.len().min(src.len());
130        let n8 = n / 8 * 8;
131        let (out_v, out_tail) = out[..n].split_at_mut(n8);
132        let src_v = &src[..n8];
133        let src_tail = &src[n8..n];
134        let s = f32x8::splat(scale);
135        for (out_chunk, src_chunk) in out_v.chunks_exact_mut(8).zip(src_v.chunks_exact(8)) {
136            let ov = f32x8::from(<[f32; 8]>::try_from(&out_chunk[..]).unwrap_or_default());
137            let sv = f32x8::from(<[f32; 8]>::try_from(src_chunk).unwrap_or_default());
138            let r = ov + sv * s;
139            out_chunk.copy_from_slice(r.as_array_ref());
140        }
141        mac_block_scalar(out_tail, src_tail, scale);
142    }
143    #[cfg(not(feature = "wide-backend"))]
144    mac_block_scalar(out, src, scale);
145}
146
147/// `out[i] = a[i] * gain_a + b[i] * gain_b`. Scalar.
148#[inline]
149pub fn mix_block_scalar(out: &mut [f32], a: &[f32], gain_a: f32, b: &[f32], gain_b: f32) {
150    let n = out.len().min(a.len()).min(b.len());
151    for i in 0..n {
152        out[i] = a[i] * gain_a + b[i] * gain_b;
153    }
154}
155
156/// `out[i] = a[i] * gain_a + b[i] * gain_b`. Vectorized when
157/// `wide-backend` is on.
158#[inline]
159pub fn mix_block(out: &mut [f32], a: &[f32], gain_a: f32, b: &[f32], gain_b: f32) {
160    #[cfg(feature = "wide-backend")]
161    {
162        use wide::f32x8;
163        let n = out.len().min(a.len()).min(b.len());
164        let n8 = n / 8 * 8;
165        let (out_v, out_tail) = out[..n].split_at_mut(n8);
166        let a_v = &a[..n8];
167        let b_v = &b[..n8];
168        let a_tail = &a[n8..n];
169        let b_tail = &b[n8..n];
170        let ga = f32x8::splat(gain_a);
171        let gb = f32x8::splat(gain_b);
172        for ((out_chunk, a_chunk), b_chunk) in out_v
173            .chunks_exact_mut(8)
174            .zip(a_v.chunks_exact(8))
175            .zip(b_v.chunks_exact(8))
176        {
177            let av = f32x8::from(<[f32; 8]>::try_from(a_chunk).unwrap_or_default());
178            let bv = f32x8::from(<[f32; 8]>::try_from(b_chunk).unwrap_or_default());
179            let r = av * ga + bv * gb;
180            out_chunk.copy_from_slice(r.as_array_ref());
181        }
182        mix_block_scalar(out_tail, a_tail, gain_a, b_tail, gain_b);
183    }
184    #[cfg(not(feature = "wide-backend"))]
185    mix_block_scalar(out, a, gain_a, b, gain_b);
186}
187
188/// `out[i] = src[i]`. Equivalent to `copy_from_slice` but exposed
189/// in the same surface as the other ops for code that wires up its
190/// inner loops from this module.
191#[inline]
192pub fn copy_block(out: &mut [f32], src: &[f32]) {
193    let n = out.len().min(src.len());
194    out[..n].copy_from_slice(&src[..n]);
195}
196
197/// `out[i] = 0.0` for all `i`.
198#[inline]
199pub fn zero_block(buf: &mut [f32]) {
200    buf.fill(0.0);
201}
202
203/// `max(buf[i].abs())`. Returns `0.0` for an empty slice; returns
204/// `f32::NAN` on first NaN (so meters can flag a runaway plugin
205/// instead of silently reporting in-range peaks).
206#[inline]
207#[must_use]
208pub fn abs_max_block(buf: &[f32]) -> f32 {
209    let mut peak = 0.0_f32;
210    for &v in buf {
211        if v.is_nan() {
212            return f32::NAN;
213        }
214        let a = v.abs();
215        if a > peak {
216            peak = a;
217        }
218    }
219    peak
220}
221
222#[cfg(test)]
223mod tests {
224    // SIMD outputs are bit-identical to the scalar ones for the
225    // ops we ship here (no transcendentals, no fused multiply-add
226    // discrepancies on the targets we care about).
227    #![allow(clippy::float_cmp, clippy::cast_precision_loss)]
228
229    use super::*;
230
231    #[test]
232    fn gain_block_matches_scalar() {
233        for n in [0, 1, 7, 8, 9, 16, 31, 32, 33, 128] {
234            let init: Vec<f32> = (0..n).map(|i| i as f32 * 0.5 - 1.0).collect();
235            let mut a = init.clone();
236            let mut b = init.clone();
237            gain_block_scalar(&mut a, 0.75);
238            gain_block(&mut b, 0.75);
239            assert_eq!(a, b, "mismatch at n={n}");
240        }
241    }
242
243    #[test]
244    fn scale_block_matches_scalar() {
245        for n in [0, 1, 7, 8, 9, 16, 33] {
246            let src: Vec<f32> = (0..n).map(|i| i as f32 * 0.3 - 1.0).collect();
247            let mut out_s = vec![0.0; n];
248            let mut out_v = vec![0.0; n];
249            scale_block_scalar(&mut out_s, &src, 0.5);
250            scale_block(&mut out_v, &src, 0.5);
251            assert_eq!(out_s, out_v, "mismatch at n={n}");
252        }
253    }
254
255    #[test]
256    fn mul_block_matches_scalar() {
257        for n in [0, 1, 7, 8, 33] {
258            let a: Vec<f32> = (0..n).map(|i| i as f32 * 0.1).collect();
259            let b: Vec<f32> = (0..n).map(|i| i as f32 * -0.2).collect();
260            let mut out_s = vec![0.0; n];
261            let mut out_v = vec![0.0; n];
262            mul_block_scalar(&mut out_s, &a, &b);
263            mul_block(&mut out_v, &a, &b);
264            assert_eq!(out_s, out_v, "mismatch at n={n}");
265        }
266    }
267
268    #[test]
269    fn mac_block_matches_scalar() {
270        for n in [0, 7, 16, 65] {
271            let src: Vec<f32> = (0..n).map(|i| i as f32).collect();
272            let mut a = vec![1.0; n];
273            let mut b = vec![1.0; n];
274            mac_block_scalar(&mut a, &src, 0.25);
275            mac_block(&mut b, &src, 0.25);
276            assert_eq!(a, b);
277        }
278    }
279
280    #[test]
281    fn mix_block_matches_scalar() {
282        let a: Vec<f32> = (0..32).map(|i| i as f32).collect();
283        let b: Vec<f32> = (0..32).map(|i| (i as f32) * 2.0).collect();
284        let mut out_s = vec![0.0; 32];
285        let mut out_v = vec![0.0; 32];
286        mix_block_scalar(&mut out_s, &a, 0.5, &b, 0.25);
287        mix_block(&mut out_v, &a, 0.5, &b, 0.25);
288        assert_eq!(out_s, out_v);
289    }
290
291    #[test]
292    fn abs_max_block_finds_peak() {
293        let buf = [-0.1, 0.7, -0.9, 0.3];
294        assert!((abs_max_block(&buf) - 0.9).abs() < 1e-6);
295        assert_eq!(abs_max_block(&[]), 0.0);
296        assert!(abs_max_block(&[1.0, f32::NAN, 2.0]).is_nan());
297    }
298}