Skip to main content

xil_audio/
audioop.rs

1//! The `audioop` primitives pydub is built on, transcribed from CPython's
2//! `Modules/audioop.c` (shipped for Python 3.13+ as `audioop-lts`).
3//!
4//! Every conversion keeps the C code's arithmetic: products are taken in
5//! `double`, clamped with `fbound` and then *floored* (not truncated), so
6//! `-3 * 0.5` is `-2`. `ratecv` interpolates on 32-bit-shifted samples and
7//! casts with C's truncating `(int)`. Those two rounding rules are the
8//! difference between a byte-identical DAW layer and one that is merely
9//! close, so nothing here is "tidied".
10//!
11//! Buffers are raw little-endian bytes of `width` 1, 2 or 4, as in C.
12
13/// Smallest and largest sample for a width, as `double`s like the C tables.
14fn bounds(width: usize) -> (f64, f64) {
15    match width {
16        1 => (-128.0, 127.0),
17        2 => (-32768.0, 32767.0),
18        4 => (-2147483648.0, 2147483647.0),
19        _ => panic!("audioop: unsupported sample width {width}"),
20    }
21}
22
23fn int_bounds(width: usize) -> (i64, i64) {
24    match width {
25        1 => (-128, 127),
26        2 => (-32768, 32767),
27        4 => (i32::MIN as i64, i32::MAX as i64),
28        _ => panic!("audioop: unsupported sample width {width}"),
29    }
30}
31
32/// `fbound`: clamp into range, with C's `val < minval + 1` lower test.
33fn fbound(val: f64, minval: f64, maxval: f64) -> f64 {
34    if val > maxval {
35        maxval
36    } else if val < minval + 1.0 {
37        minval
38    } else {
39        val
40    }
41}
42
43/// `GETRAWSAMPLE` — the signed sample at byte offset `pos`.
44pub fn get_raw(data: &[u8], width: usize, pos: usize) -> i32 {
45    match width {
46        1 => data[pos] as i8 as i32,
47        2 => i16::from_le_bytes([data[pos], data[pos + 1]]) as i32,
48        4 => i32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]),
49        _ => panic!("audioop: unsupported sample width {width}"),
50    }
51}
52
53/// `SETRAWSAMPLE` — append one sample, truncating to the width.
54pub fn put_raw(out: &mut Vec<u8>, width: usize, val: i32) {
55    match width {
56        1 => out.push(val as i8 as u8),
57        2 => out.extend_from_slice(&(val as i16).to_le_bytes()),
58        4 => out.extend_from_slice(&val.to_le_bytes()),
59        _ => panic!("audioop: unsupported sample width {width}"),
60    }
61}
62
63/// `GETSAMPLE32` — a sample scaled to occupy the top of an `int`.
64fn get_sample32(data: &[u8], width: usize, pos: usize) -> i32 {
65    match width {
66        1 => (data[pos] as i8 as i32) << 24,
67        2 => (i16::from_le_bytes([data[pos], data[pos + 1]]) as i32) << 16,
68        4 => get_raw(data, 4, pos),
69        _ => panic!("audioop: unsupported sample width {width}"),
70    }
71}
72
73/// `SETSAMPLE32` — the inverse, with an arithmetic shift.
74fn put_sample32(out: &mut Vec<u8>, width: usize, val: i32) {
75    match width {
76        1 => out.push((val >> 24) as i8 as u8),
77        2 => out.extend_from_slice(&((val >> 16) as i16).to_le_bytes()),
78        4 => out.extend_from_slice(&val.to_le_bytes()),
79        _ => panic!("audioop: unsupported sample width {width}"),
80    }
81}
82
83fn check_len(len: usize, width: usize) {
84    assert!(
85        len % width == 0,
86        "audioop.error: not a whole number of frames"
87    );
88}
89
90/// `audioop.mul(fragment, width, factor)`.
91pub fn mul(data: &[u8], width: usize, factor: f64) -> Vec<u8> {
92    check_len(data.len(), width);
93    let (lo, hi) = bounds(width);
94    let mut out = Vec::with_capacity(data.len());
95    for pos in (0..data.len()).step_by(width) {
96        let val = get_raw(data, width, pos) as f64;
97        put_raw(&mut out, width, fbound(val * factor, lo, hi).floor() as i32);
98    }
99    out
100}
101
102/// `audioop.add(fragment1, fragment2, width)` — saturating sum.
103pub fn add(a: &[u8], b: &[u8], width: usize) -> Vec<u8> {
104    check_len(a.len(), width);
105    assert_eq!(
106        a.len(),
107        b.len(),
108        "audioop.error: Lengths should be the same"
109    );
110    let (lo, hi) = int_bounds(width);
111    let mut out = Vec::with_capacity(a.len());
112    for pos in (0..a.len()).step_by(width) {
113        let v = get_raw(a, width, pos) as i64 + get_raw(b, width, pos) as i64;
114        put_raw(&mut out, width, v.clamp(lo, hi) as i32);
115    }
116    out
117}
118
119/// In-place [`add`] of `b` onto `a[offset..]`. Same arithmetic; used where
120/// the pydub code builds a new buffer that would be byte-identical anyway.
121pub fn add_into(a: &mut [u8], b: &[u8], width: usize) {
122    assert!(a.len() >= b.len());
123    let (lo, hi) = int_bounds(width);
124    match width {
125        2 => {
126            for (x, y) in a.chunks_exact_mut(2).zip(b.chunks_exact(2)) {
127                let v = i16::from_le_bytes([x[0], x[1]]) as i32
128                    + i16::from_le_bytes([y[0], y[1]]) as i32;
129                x.copy_from_slice(&(v.clamp(lo as i32, hi as i32) as i16).to_le_bytes());
130            }
131        }
132        _ => {
133            for pos in (0..b.len()).step_by(width) {
134                let v = get_raw(a, width, pos) as i64 + get_raw(b, width, pos) as i64;
135                let mut buf = Vec::with_capacity(width);
136                put_raw(&mut buf, width, v.clamp(lo, hi) as i32);
137                a[pos..pos + width].copy_from_slice(&buf);
138            }
139        }
140    }
141}
142
143/// `audioop.tostereo(fragment, width, lfactor, rfactor)`.
144pub fn tostereo(data: &[u8], width: usize, lfactor: f64, rfactor: f64) -> Vec<u8> {
145    check_len(data.len(), width);
146    let (lo, hi) = bounds(width);
147    let mut out = Vec::with_capacity(data.len() * 2);
148    for pos in (0..data.len()).step_by(width) {
149        let val = get_raw(data, width, pos) as f64;
150        put_raw(
151            &mut out,
152            width,
153            fbound(val * lfactor, lo, hi).floor() as i32,
154        );
155        put_raw(
156            &mut out,
157            width,
158            fbound(val * rfactor, lo, hi).floor() as i32,
159        );
160    }
161    out
162}
163
164/// `audioop.tomono(fragment, width, lfactor, rfactor)`.
165pub fn tomono(data: &[u8], width: usize, lfactor: f64, rfactor: f64) -> Vec<u8> {
166    check_len(data.len(), width * 2);
167    let (lo, hi) = bounds(width);
168    let mut out = Vec::with_capacity(data.len() / 2);
169    for pos in (0..data.len()).step_by(width * 2) {
170        let v1 = get_raw(data, width, pos) as f64;
171        let v2 = get_raw(data, width, pos + width) as f64;
172        let val = v1 * lfactor + v2 * rfactor;
173        put_raw(&mut out, width, fbound(val, lo, hi).floor() as i32);
174    }
175    out
176}
177
178/// `audioop.lin2lin(fragment, width, newwidth)`.
179pub fn lin2lin(data: &[u8], width: usize, newwidth: usize) -> Vec<u8> {
180    check_len(data.len(), width);
181    if width == newwidth {
182        return data.to_vec();
183    }
184    let mut out = Vec::with_capacity(data.len() / width * newwidth);
185    for pos in (0..data.len()).step_by(width) {
186        put_sample32(&mut out, newwidth, get_sample32(data, width, pos));
187    }
188    out
189}
190
191fn gcd(mut a: i64, mut b: i64) -> i64 {
192    while b > 0 {
193        let t = a % b;
194        a = b;
195        b = t;
196    }
197    a
198}
199
200/// `audioop.ratecv(fragment, width, nchannels, inrate, outrate, None)` with
201/// the default weights (1, 0). Only the converted bytes are returned; pydub
202/// discards the state.
203pub fn ratecv(data: &[u8], width: usize, nchannels: usize, inrate: u32, outrate: u32) -> Vec<u8> {
204    let bytes_per_frame = width * nchannels;
205    assert!(
206        data.len() % bytes_per_frame == 0,
207        "audioop.error: not a whole number of frames"
208    );
209    let (mut inrate, mut outrate) = (inrate as i64, outrate as i64);
210    let d0 = gcd(inrate, outrate);
211    inrate /= d0;
212    outrate /= d0;
213    // weightA = 1, weightB = 0 reduce to themselves.
214    let (weight_a, weight_b) = (1.0f64, 0.0f64);
215
216    let mut prev_i = vec![0i32; nchannels];
217    let mut cur_i = vec![0i32; nchannels];
218    let mut len = (data.len() / bytes_per_frame) as i64;
219    let mut d = -outrate;
220    let capacity = if len == 0 {
221        0
222    } else {
223        ((1 + (len - 1) / inrate) * outrate) as usize * bytes_per_frame
224    };
225    let mut out = Vec::with_capacity(capacity);
226    let mut cp = 0usize;
227
228    loop {
229        while d < 0 {
230            if len == 0 {
231                return out;
232            }
233            for chan in 0..nchannels {
234                prev_i[chan] = cur_i[chan];
235                cur_i[chan] = get_sample32(data, width, cp);
236                cp += width;
237                // The "simple digital filter"; exact for the default weights,
238                // but kept in double arithmetic as the C code has it.
239                cur_i[chan] = ((weight_a * cur_i[chan] as f64 + weight_b * prev_i[chan] as f64)
240                    / (weight_a + weight_b)) as i32;
241            }
242            len -= 1;
243            d += outrate;
244        }
245        while d >= 0 {
246            for chan in 0..nchannels {
247                let cur_o = ((prev_i[chan] as f64 * d as f64
248                    + cur_i[chan] as f64 * (outrate - d) as f64)
249                    / outrate as f64) as i32;
250                put_sample32(&mut out, width, cur_o);
251            }
252            d -= inrate;
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    fn s(v: &[i16]) -> Vec<u8> {
262        v.iter().flat_map(|x| x.to_le_bytes()).collect()
263    }
264    fn u(b: &[u8]) -> Vec<i16> {
265        b.chunks_exact(2)
266            .map(|c| i16::from_le_bytes([c[0], c[1]]))
267            .collect()
268    }
269
270    // Expected values below were printed by audioop-lts 0.2.2 on CPython 3.13.
271
272    #[test]
273    fn mul_floors_and_saturates() {
274        assert_eq!(
275            u(&mul(&s(&[3, -3, 1, -1, 32767, -32768, 100, -100]), 2, 0.5)),
276            [1, -2, 0, -1, 16383, -16384, 50, -50]
277        );
278        assert_eq!(
279            u(&mul(&s(&[3, -3, 32767, -32768, -21846, 21845]), 2, 1.5)),
280            [4, -5, 32767, -32768, -32768, 32767]
281        );
282        assert_eq!(
283            u(&mul(&s(&[-32767, -32768, 32767]), 2, 1.00001)),
284            [-32768, -32768, 32767]
285        );
286        assert_eq!(u(&mul(&s(&[5, -5]), 2, -0.3)), [-2, 1]);
287    }
288
289    #[test]
290    fn add_saturates() {
291        assert_eq!(
292            u(&add(
293                &s(&[30000, -30000, 1, -32768]),
294                &s(&[30000, -30000, -1, -1]),
295                2
296            )),
297            [32767, -32768, 0, -32768]
298        );
299        let mut a = s(&[30000, -30000, 1, -32768, 7]);
300        add_into(&mut a, &s(&[30000, -30000, -1, -1]), 2);
301        assert_eq!(u(&a), [32767, -32768, 0, -32768, 7]);
302    }
303
304    #[test]
305    fn channel_conversions_floor() {
306        assert_eq!(
307            u(&tostereo(&s(&[3, -3, 32767, -32768]), 2, 0.5, 1.0)),
308            [1, 3, -2, -3, 16383, 32767, -16384, -32768]
309        );
310        assert_eq!(
311            u(&tomono(
312                &s(&[3, 4, -3, -4, 32767, 32767, -32768, -32768, 1, -2]),
313                2,
314                0.5,
315                0.5
316            )),
317            [3, -4, 32767, -32768, -1]
318        );
319    }
320
321    #[test]
322    fn lin2lin_scales_by_shift() {
323        assert!(lin2lin(&[], 1, 2).is_empty());
324        assert_eq!(lin2lin(&[1, 255], 1, 2), vec![0x00, 0x01, 0x00, 0xff]);
325    }
326
327    fn ramp(n: usize) -> Vec<u8> {
328        s(&(0..n)
329            .map(|i| ((i * 1237) % 65536) as i64 - 32768)
330            .map(|v| v as i16)
331            .collect::<Vec<_>>())
332    }
333
334    #[test]
335    fn ratecv_matches_cpython_lengths_and_values() {
336        let cases: &[(usize, u32, u32, usize, &[i16])] = &[
337            (
338                1,
339                11025,
340                44100,
341                145,
342                &[
343                    -32768, -32459, -32150, -31841, -31531, -31222, -30913, -30604, -30294, -29985,
344                    -29676, -29367,
345                ],
346            ),
347            (
348                1,
349                44100,
350                48000,
351                40,
352                &[
353                    -32768, -31632, -30496, -29359, -28223, -27086, -25950, -24813, -23677, -22540,
354                    -21404, -20267,
355                ],
356            ),
357            (
358                2,
359                44100,
360                48000,
361                80,
362                &[
363                    -32768, -31531, -30496, -29259, -28223, -26986, -25950, -24713, -23677, -22440,
364                    -21404, -20167,
365                ],
366            ),
367            (
368                1,
369                48000,
370                44100,
371                34,
372                &[
373                    -32768, -31422, -30076, -28729, -27383, -26037, -24690, -23344, -21997, -20651,
374                    -19305, -17958,
375                ],
376            ),
377            (
378                2,
379                22050,
380                44100,
381                146,
382                &[
383                    -32768, -31531, -31531, -30294, -30294, -29057, -29057, -27820, -27820, -26583,
384                    -26583, -25346,
385                ],
386            ),
387            (
388                1,
389                8000,
390                44100,
391                199,
392                &[
393                    -32768, -32544, -32320, -32095, -31871, -31647, -31422, -31198, -30973, -30749,
394                    -30525, -30300,
395                ],
396            ),
397        ];
398        for &(ch, a, b, out_len, head) in cases {
399            let out = u(&ratecv(&ramp(ch * 37), 2, ch, a, b));
400            assert_eq!(out.len(), out_len, "{ch}ch {a}->{b}");
401            assert_eq!(&out[..12], head, "{ch}ch {a}->{b}");
402        }
403        assert!(ratecv(&[], 2, 1, 11025, 44100).is_empty());
404    }
405}