Skip to main content

xil_audio/
segment.rs

1//! pydub's `AudioSegment`, transcribed operation by operation.
2//!
3//! The pipeline's DAW layers are only byte-identical to Python's if every
4//! quirk of pydub 0.25.1 survives, and several are load-bearing:
5//!
6//! * `AudioSegment.silent()` is **11025 Hz mono**. Overlaying a 44.1 kHz
7//!   stereo stem onto it converts the whole layer through `ratecv`, which
8//!   changes its frame count.
9//! * `AudioSegment.empty()` is width 1, rate 1, mono; the first `+=` syncs
10//!   it up to the appended stem.
11//! * Slicing is by milliseconds through `int(ms * rate / 1000.0)`, so
12//!   `seg[a:]` can drop or *pad* a few trailing frames. `overlay` is built
13//!   from two such slices, so it can change a buffer's length.
14//! * Negative positions count from the end, and byte slices use Python's
15//!   clamping rules — a fade longer than its clip reaches both.
16//!
17//! Buffers are raw little-endian bytes, as pydub keeps them, so the same
18//! arithmetic applies whatever the sample width.
19
20use std::path::Path;
21use std::process::{Command, Stdio};
22
23use crate::audioop;
24use crate::ffmpeg::{self, AudioError};
25
26/// The C library's `pow`, `log` and `log10`, called at run time.
27///
28/// Python's `**`, `math.log` and `math.log10` go straight to libm, so the
29/// port calls the same functions rather than trusting Rust's intrinsics to
30/// round identically. The last bit matters: it decides `floor()` on
31/// negative samples in `pan(0.0)`.
32pub mod libm {
33    extern "C" {
34        #[link_name = "pow"]
35        fn c_pow(x: f64, y: f64) -> f64;
36        #[link_name = "log"]
37        fn c_log(x: f64) -> f64;
38        #[link_name = "log10"]
39        fn c_log10(x: f64) -> f64;
40    }
41
42    /// `math.log(x)` (natural logarithm).
43    pub fn ln(x: f64) -> f64 {
44        // SAFETY: pure libm function over plain doubles.
45        unsafe { c_log(std::hint::black_box(x)) }
46    }
47
48    // LLVM recognises `pow`/`log10` as library functions and folds calls
49    // with constant arguments at compile time, which would undo the point
50    // of calling libm; `black_box` keeps the arguments opaque.
51    pub fn pow(x: f64, y: f64) -> f64 {
52        // SAFETY: pure libm function over plain doubles.
53        unsafe { c_pow(std::hint::black_box(x), std::hint::black_box(y)) }
54    }
55
56    pub fn log10(x: f64) -> f64 {
57        // SAFETY: pure libm function over plain doubles.
58        unsafe { c_log10(std::hint::black_box(x)) }
59    }
60}
61
62/// pydub's `db_to_float` in amplitude mode: `10 ** (db / 20)`.
63pub fn db_to_float(db: f64) -> f64 {
64    libm::pow(10.0, db / 20.0)
65}
66
67/// pydub's `ratio_to_db` in amplitude mode: `20 * log(ratio, 10)`, which
68/// CPython evaluates as `log(ratio) / log(10)` — *not* `log10(ratio)`.
69/// `20 * log(2, 10)` is `6.020599913279623`; `20 * log10(2)` is `...624`.
70pub fn ratio_to_db(ratio: f64) -> f64 {
71    if ratio == 0.0 {
72        return f64::NEG_INFINITY;
73    }
74    20.0 * (libm::ln(ratio) / libm::ln(10.0))
75}
76
77/// Python's `round()` of a float to an int: ties go to even.
78pub fn py_round(x: f64) -> i64 {
79    let r = x.round();
80    if (x - x.trunc()).abs() == 0.5 {
81        let t = x.trunc();
82        if (t as i64) % 2 == 0 {
83            t as i64
84        } else {
85            r as i64
86        }
87    } else {
88        r as i64
89    }
90}
91
92/// Python byte slicing `data[a:b]`: negative indexes count from the end,
93/// both ends clamp, and an inverted range is empty.
94fn py_slice(data: &[u8], a: i64, b: i64) -> &[u8] {
95    let n = data.len() as i64;
96    let fix = |i: i64| {
97        let i = if i < 0 { i + n } else { i };
98        i.clamp(0, n) as usize
99    };
100    let (a, b) = (fix(a), fix(b));
101    if b <= a {
102        &[]
103    } else {
104        &data[a..b]
105    }
106}
107
108/// Python's `//` on ints: rounds toward negative infinity.
109fn floor_div(a: i64, b: i64) -> i64 {
110    a.div_euclid(b) - if a.rem_euclid(b) != 0 && b < 0 { 1 } else { 0 }
111}
112
113/// An immutable-in-spirit block of interleaved PCM. Methods consume and
114/// return segments the way pydub's `_spawn` hands back new objects.
115#[derive(Clone, Debug, PartialEq)]
116pub struct Segment {
117    pub data: Vec<u8>,
118    pub sample_width: usize,
119    pub frame_rate: u32,
120    pub channels: usize,
121}
122
123impl Segment {
124    pub fn new(data: Vec<u8>, sample_width: usize, frame_rate: u32, channels: usize) -> Segment {
125        Segment {
126            data,
127            sample_width,
128            frame_rate,
129            channels,
130        }
131    }
132
133    fn spawn(&self, data: Vec<u8>) -> Segment {
134        Segment::new(data, self.sample_width, self.frame_rate, self.channels)
135    }
136
137    pub fn frame_width(&self) -> usize {
138        self.channels * self.sample_width
139    }
140
141    /// `AudioSegment.empty()`.
142    pub fn empty() -> Segment {
143        Segment::new(Vec::new(), 1, 1, 1)
144    }
145
146    /// `AudioSegment.silent(duration, frame_rate)`.
147    pub fn silent_at(duration_ms: f64, frame_rate: u32) -> Segment {
148        let frames = (frame_rate as f64 * (duration_ms / 1000.0)) as i64;
149        Segment::new(vec![0u8; 2 * frames.max(0) as usize], 2, frame_rate, 1)
150    }
151
152    /// `AudioSegment.silent(duration)` — at pydub's default 11025 Hz.
153    pub fn silent(duration_ms: f64) -> Segment {
154        Segment::silent_at(duration_ms, 11025)
155    }
156
157    /// `frame_count()` — whole frames, as a float.
158    pub fn frame_count(&self) -> f64 {
159        (self.data.len() / self.frame_width()) as f64
160    }
161
162    /// `frame_count(ms=...)` — fractional frames for a duration.
163    pub fn frame_count_ms(&self, ms: f64) -> f64 {
164        ms * (self.frame_rate as f64 / 1000.0)
165    }
166
167    /// `len(segment)` in milliseconds.
168    pub fn len_ms(&self) -> i64 {
169        py_round(1000.0 * (self.frame_count() / self.frame_rate as f64))
170    }
171
172    fn parse_position(&self, val: f64) -> i64 {
173        let len = self.len_ms() as f64;
174        let val = if val < 0.0 { len - val.abs() } else { val };
175        let frames = if val == f64::INFINITY {
176            self.frame_count_ms(len)
177        } else {
178            self.frame_count_ms(val)
179        };
180        frames as i64
181    }
182
183    fn slice_frames(&self, start: f64, end: f64) -> Segment {
184        let fw = self.frame_width() as i64;
185        let s = self.parse_position(start) * fw;
186        let e = self.parse_position(end) * fw;
187        let mut data = py_slice(&self.data, s, e).to_vec();
188        let expected = e - s;
189        let missing = floor_div(expected - data.len() as i64, fw);
190        if missing != 0 {
191            if missing as f64 > self.frame_count_ms(2.0) {
192                panic!(
193                    "TooManyMissingFrames: You should never be filling in    more than 2 ms with silence here, missing frames: {missing}"
194                );
195            }
196            let head = &data[..data.len().min(fw as usize)];
197            let silence = audioop::mul(head, self.sample_width, 0.0);
198            if missing > 0 {
199                data.extend(silence.repeat(missing as usize));
200            }
201        }
202        self.spawn(data)
203    }
204
205    /// `segment[start:end]` in milliseconds; `None` is an open end.
206    pub fn slice(&self, start: Option<f64>, end: Option<f64>) -> Segment {
207        let len = self.len_ms() as f64;
208        let start = start.unwrap_or(0.0).min(len);
209        let end = end.unwrap_or(len).min(len);
210        self.slice_frames(start, end)
211    }
212
213    /// `segment[ms]` — one millisecond, deliberately *not* clamped.
214    pub fn at(&self, ms: f64) -> Segment {
215        self.slice_frames(ms, ms + 1.0)
216    }
217
218    /// `get_frame(index)`.
219    fn get_frame(&self, index: i64) -> &[u8] {
220        let fw = self.frame_width() as i64;
221        py_slice(&self.data, index * fw, index * fw + fw)
222    }
223
224    /// `segment * n` for an integer `n`.
225    pub fn repeat(&self, n: i64) -> Segment {
226        self.spawn(self.data.repeat(n.max(0) as usize))
227    }
228
229    /// `segment + db` / `segment - db` — `apply_gain`.
230    pub fn gain(&self, db: f64) -> Segment {
231        self.spawn(audioop::mul(&self.data, self.sample_width, db_to_float(db)))
232    }
233
234    pub fn set_sample_width(self, sample_width: usize) -> Segment {
235        if sample_width == self.sample_width {
236            return self;
237        }
238        let data = audioop::lin2lin(&self.data, self.sample_width, sample_width);
239        Segment::new(data, sample_width, self.frame_rate, self.channels)
240    }
241
242    pub fn set_frame_rate(self, frame_rate: u32) -> Segment {
243        if frame_rate == self.frame_rate {
244            return self;
245        }
246        let data = if self.data.is_empty() {
247            self.data
248        } else {
249            audioop::ratecv(
250                &self.data,
251                self.sample_width,
252                self.channels,
253                self.frame_rate,
254                frame_rate,
255            )
256        };
257        Segment::new(data, self.sample_width, frame_rate, self.channels)
258    }
259
260    pub fn set_channels(self, channels: usize) -> Segment {
261        if channels == self.channels {
262            return self;
263        }
264        let data = match (self.channels, channels) {
265            (1, 2) => audioop::tostereo(&self.data, self.sample_width, 1.0, 1.0),
266            (2, 1) => audioop::tomono(&self.data, self.sample_width, 0.5, 0.5),
267            (from, to) => panic!("set_channels: {from} -> {to} is not used by the pipeline"),
268        };
269        Segment::new(data, self.sample_width, self.frame_rate, channels)
270    }
271
272    /// `AudioSegment._sync(a, b)`.
273    pub fn sync(a: Segment, b: Segment) -> (Segment, Segment) {
274        let channels = a.channels.max(b.channels);
275        let frame_rate = a.frame_rate.max(b.frame_rate);
276        let width = a.sample_width.max(b.sample_width);
277        let conv = |s: Segment| {
278            s.set_channels(channels)
279                .set_frame_rate(frame_rate)
280                .set_sample_width(width)
281        };
282        (conv(a), conv(b))
283    }
284
285    /// `self + other` — `append(other, crossfade=0)`.
286    pub fn append(self, other: Segment) -> Segment {
287        let (mut a, b) = Segment::sync(self, other);
288        a.data.extend_from_slice(&b.data);
289        a
290    }
291
292    /// `overlay(seg, position)` with pydub's defaults (no loop, one pass).
293    pub fn overlay(self, seg: Segment, position: i64) -> Segment {
294        let (seg1, seg2) = Segment::sync(self, seg);
295        let width = seg1.sample_width;
296        let pos_ms = position as f64;
297
298        // Fast path: when both of pydub's slices are exact (no dropped or
299        // padded frames), its output is the base buffer with the overlay
300        // summed in place, so do exactly that instead of copying.
301        let fw = seg1.frame_width() as i64;
302        let len = seg1.len_ms() as f64;
303        let cut = seg1.parse_position(pos_ms.min(len)) * fw;
304        let tail_end = seg1.parse_position(len) * fw;
305        if cut >= 0 && tail_end == seg1.data.len() as i64 && cut <= tail_end {
306            let mut out = seg1;
307            let cut = cut as usize;
308            let n = seg2.data.len().min(out.data.len() - cut);
309            audioop::add_into(&mut out.data[cut..cut + n], &seg2.data[..n], width);
310            return out;
311        }
312
313        let mut output = seg1.slice(None, Some(pos_ms)).data;
314        let base = seg1.slice(Some(pos_ms), None).data;
315        let mut overlay = seg2.data;
316        let remaining = base.len();
317        if overlay.len() >= remaining {
318            overlay.truncate(remaining);
319        }
320        let n = overlay.len();
321        output.extend(audioop::add(&base[..n], &overlay, width));
322        output.extend_from_slice(&base[n..]);
323        seg1.spawn(output)
324    }
325
326    /// `fade(to_gain, from_gain, start, end, duration)`.
327    pub fn fade(
328        &self,
329        to_gain: f64,
330        from_gain: f64,
331        start: Option<f64>,
332        end: Option<f64>,
333        duration: Option<f64>,
334    ) -> Segment {
335        if to_gain == 0.0 && from_gain == 0.0 {
336            return self.clone();
337        }
338        let len = self.len_ms() as f64;
339        let mut start = start.map(|s| s.min(len));
340        let mut end = end.map(|e| e.min(len));
341        if let Some(s) = start {
342            if s < 0.0 {
343                start = Some(s + len);
344            }
345        }
346        if let Some(e) = end {
347            if e < 0.0 {
348                end = Some(e + len);
349            }
350        }
351        let duration = match duration {
352            Some(d) if d != 0.0 => {
353                if let Some(s) = start {
354                    end = Some(s + d);
355                } else if let Some(e) = end {
356                    start = Some(e - d);
357                }
358                d
359            }
360            _ => end.unwrap() - start.unwrap(),
361        };
362        let (start, end) = (start.unwrap(), end.unwrap());
363
364        let from_power = db_to_float(from_gain);
365        let mut output = Vec::new();
366
367        let mut before = self.slice(None, Some(start)).data;
368        if from_gain != 0.0 {
369            before = audioop::mul(&before, self.sample_width, from_power);
370        }
371        output.extend(before);
372
373        let gain_delta = db_to_float(to_gain) - from_power;
374        if duration > 100.0 {
375            let scale_step = gain_delta / duration;
376            for i in 0..duration as i64 {
377                let volume_change = from_power + scale_step * i as f64;
378                let chunk = self.at(start + i as f64);
379                output.extend(audioop::mul(&chunk.data, self.sample_width, volume_change));
380            }
381        } else {
382            let start_frame = self.frame_count_ms(start);
383            let end_frame = self.frame_count_ms(end);
384            let fade_frames = end_frame - start_frame;
385            let scale_step = gain_delta / fade_frames;
386            for i in 0..fade_frames as i64 {
387                let volume_change = from_power + scale_step * i as f64;
388                let sample = self.get_frame((start_frame + i as f64) as i64);
389                output.extend(audioop::mul(sample, self.sample_width, volume_change));
390            }
391        }
392
393        let mut after = self.slice(Some(end), None).data;
394        if to_gain != 0.0 {
395            after = audioop::mul(&after, self.sample_width, db_to_float(to_gain));
396        }
397        output.extend(after);
398        self.spawn(output)
399    }
400
401    pub fn fade_in(&self, duration_ms: f64) -> Segment {
402        self.fade(0.0, -120.0, Some(0.0), None, Some(duration_ms))
403    }
404
405    pub fn fade_out(&self, duration_ms: f64) -> Segment {
406        self.fade(-120.0, 0.0, None, Some(f64::INFINITY), Some(duration_ms))
407    }
408
409    /// `split_to_mono()` for mono and stereo.
410    pub fn split_to_mono(&self) -> Vec<Segment> {
411        if self.channels == 1 {
412            return vec![self.clone()];
413        }
414        let w = self.sample_width;
415        (0..self.channels)
416            .map(|c| {
417                let data: Vec<u8> = self
418                    .data
419                    .chunks_exact(self.frame_width())
420                    .flat_map(|f| f[c * w..(c + 1) * w].to_vec())
421                    .collect();
422                Segment::new(data, w, self.frame_rate, 1)
423            })
424            .collect()
425    }
426
427    /// `effects.apply_gain_stereo(left_gain, right_gain)`.
428    pub fn apply_gain_stereo(&self, left_gain: f64, right_gain: f64) -> Segment {
429        let (left, right) = if self.channels == 1 {
430            (self.clone(), self.clone())
431        } else {
432            let mut parts = self.split_to_mono();
433            let r = parts.pop().unwrap();
434            (parts.pop().unwrap(), r)
435        };
436        let w = self.sample_width;
437        let l = audioop::mul(&left.data, w, db_to_float(left_gain));
438        let l = audioop::tostereo(&l, w, 1.0, 0.0);
439        let r = audioop::mul(&right.data, w, db_to_float(right_gain));
440        let r = audioop::tostereo(&r, w, 0.0, 1.0);
441        Segment::new(audioop::add(&l, &r, w), w, self.frame_rate, 2)
442    }
443
444    /// `effects.pan(pan_amount)` — not equal-power; pydub's own formula.
445    pub fn pan(&self, pan_amount: f64) -> Segment {
446        assert!(
447            (-1.0..=1.0).contains(&pan_amount),
448            "pan_amount should be between -1.0 (100% left) and +1.0 (100% right)"
449        );
450        let max_boost_db = ratio_to_db(2.0);
451        let boost_db = pan_amount.abs() * max_boost_db;
452        let boost_factor = db_to_float(boost_db);
453        let reduce_factor = db_to_float(max_boost_db) - boost_factor;
454        let reduce_db = ratio_to_db(reduce_factor);
455        let boost_db = boost_db / 2.0;
456        if pan_amount < 0.0 {
457            self.apply_gain_stereo(boost_db, reduce_db)
458        } else {
459            self.apply_gain_stereo(reduce_db, boost_db)
460        }
461    }
462
463    fn samples_i16(&self) -> Vec<i16> {
464        assert_eq!(
465            self.sample_width, 2,
466            "filters are only used on 16-bit audio"
467        );
468        self.data
469            .chunks_exact(2)
470            .map(|c| i16::from_le_bytes([c[0], c[1]]))
471            .collect()
472    }
473
474    fn spawn_i16(&self, s: &[i16]) -> Segment {
475        self.spawn(s.iter().flat_map(|v| v.to_le_bytes()).collect())
476    }
477
478    /// `effects.low_pass_filter(cutoff)` — single pole, as pydub has it.
479    // Index loops kept on purpose: this is a line-by-line transcription.
480    #[allow(clippy::needless_range_loop)]
481    pub fn low_pass_filter(&self, cutoff: f64) -> Segment {
482        let rc = 1.0 / (cutoff * 2.0 * std::f64::consts::PI);
483        let dt = 1.0 / self.frame_rate as f64;
484        let alpha = dt / (rc + dt);
485        let original = self.samples_i16();
486        let mut filtered = original.clone();
487        let frames = self.frame_count() as usize;
488        let ch = self.channels;
489        let mut last: Vec<f64> = vec![0.0; ch];
490        for i in 0..ch.min(original.len()) {
491            last[i] = original[i] as f64;
492            filtered[i] = original[i];
493        }
494        for i in 1..frames {
495            for j in 0..ch {
496                let off = i * ch + j;
497                last[j] += alpha * (original[off] as f64 - last[j]);
498                filtered[off] = py_array_int(last[j]);
499            }
500        }
501        self.spawn_i16(&filtered)
502    }
503
504    /// `effects.high_pass_filter(cutoff)`.
505    #[allow(clippy::needless_range_loop)]
506    pub fn high_pass_filter(&self, cutoff: f64) -> Segment {
507        let rc = 1.0 / (cutoff * 2.0 * std::f64::consts::PI);
508        let dt = 1.0 / self.frame_rate as f64;
509        let alpha = rc / (rc + dt);
510        let (minval, maxval) = (-32768.0f64, 32767.0f64);
511        let original = self.samples_i16();
512        let mut filtered = original.clone();
513        let frames = self.frame_count() as usize;
514        let ch = self.channels;
515        let mut last: Vec<f64> = vec![0.0; ch];
516        for i in 0..ch.min(original.len()) {
517            last[i] = original[i] as f64;
518            filtered[i] = original[i];
519        }
520        for i in 1..frames {
521            for j in 0..ch {
522                let off = i * ch + j;
523                let prev = (i - 1) * ch + j;
524                last[j] = alpha * (last[j] + original[off] as f64 - original[prev] as f64);
525                filtered[off] = py_array_int(last[j].max(minval).min(maxval));
526            }
527        }
528        self.spawn_i16(&filtered)
529    }
530
531    /// `AudioSegment.from_file(path)`: WAVs by name are read directly,
532    /// everything else goes through ffmpeg exactly as pydub drives it.
533    pub fn from_file(path: &Path) -> Result<Segment, AudioError> {
534        let is_wav = path.to_string_lossy().to_lowercase().ends_with(".wav");
535        if is_wav {
536            if let Some(seg) = std::fs::read(path).ok().and_then(|b| read_wav(&b)) {
537                return Ok(seg);
538            }
539        }
540        let pcm = ffmpeg::decode(path)?;
541        let data = pcm.samples.iter().flat_map(|v| v.to_le_bytes()).collect();
542        Ok(Segment::new(data, 2, pcm.frame_rate, pcm.channels as usize))
543    }
544
545    /// `export(path, format="wav")` with no codec or parameters — Python's
546    /// `wave` module writes the file, no ffmpeg involved.
547    pub fn export_wav(&self, path: &Path) -> std::io::Result<()> {
548        std::fs::write(path, self.wav_bytes())
549    }
550
551    /// The bytes `wave.open(f, "wb")` produces for this segment.
552    pub fn wav_bytes(&self) -> Vec<u8> {
553        let nframes = self.frame_count() as u32;
554        let datalength = nframes * self.channels as u32 * self.sample_width as u32;
555        let mut out = Vec::with_capacity(44 + self.data.len());
556        out.extend_from_slice(b"RIFF");
557        out.extend_from_slice(&(36 + datalength).to_le_bytes());
558        out.extend_from_slice(b"WAVEfmt ");
559        out.extend_from_slice(&16u32.to_le_bytes());
560        out.extend_from_slice(&1u16.to_le_bytes());
561        out.extend_from_slice(&(self.channels as u16).to_le_bytes());
562        out.extend_from_slice(&self.frame_rate.to_le_bytes());
563        let block_align = (self.channels * self.sample_width) as u32;
564        out.extend_from_slice(&(self.frame_rate * block_align).to_le_bytes());
565        out.extend_from_slice(&(block_align as u16).to_le_bytes());
566        out.extend_from_slice(&((self.sample_width * 8) as u16).to_le_bytes());
567        out.extend_from_slice(b"data");
568        out.extend_from_slice(&datalength.to_le_bytes());
569        if self.sample_width == 1 {
570            out.extend(self.data.iter().map(|b| b.wrapping_add(128)));
571        } else {
572            out.extend_from_slice(&self.data);
573        }
574        out
575    }
576
577    /// `export(path, format=fmt, parameters=...)` through ffmpeg: a temporary
578    /// WAV in, `ffmpeg -y -f wav -i tmp [params] -f fmt out` — the exact
579    /// command pydub builds, so the encoded bytes match.
580    pub fn export_ffmpeg(
581        &self,
582        path: &Path,
583        format: &str,
584        parameters: &[&str],
585    ) -> Result<(), AudioError> {
586        // Closed paths, not open handles: Windows will not let ffmpeg write
587        // to a file another handle still holds.
588        let tmp_in = tempfile::Builder::new()
589            .suffix(".wav")
590            .tempfile()
591            .map_err(|e| io_err(path, e))?
592            .into_temp_path();
593        std::fs::write(&tmp_in, self.wav_bytes()).map_err(|e| io_err(path, e))?;
594        let tmp_out = tempfile::NamedTempFile::new()
595            .map_err(|e| io_err(path, e))?
596            .into_temp_path();
597        let mut cmd = Command::new("ffmpeg");
598        cmd.arg("-y").args(["-f", "wav", "-i"]).arg(&tmp_in);
599        cmd.args(parameters);
600        cmd.args(["-f", format]).arg(&tmp_out);
601        let out = cmd
602            .stdin(Stdio::null())
603            .output()
604            .map_err(|_| AudioError::FfmpegMissing)?;
605        if !out.status.success() {
606            return Err(AudioError::CouldntEncode {
607                code: out.status.code().unwrap_or(-1),
608                stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
609            });
610        }
611        let bytes = std::fs::read(&tmp_out).map_err(|e| io_err(path, e))?;
612        std::fs::write(path, bytes).map_err(|e| io_err(path, e))
613    }
614}
615
616fn io_err(path: &Path, e: std::io::Error) -> AudioError {
617    AudioError::Malformed {
618        path: path.display().to_string(),
619        what: e.to_string(),
620    }
621}
622
623/// `int(x)` stored into an `array('h')`: truncates toward zero; a value
624/// outside the type would raise `OverflowError` in Python.
625fn py_array_int(x: f64) -> i16 {
626    let v = x as i64;
627    assert!(
628        (-32768..=32767).contains(&v),
629        "OverflowError: signed short integer is greater than maximum"
630    );
631    v as i16
632}
633
634/// pydub's `read_wav_audio` (with `extract_wav_headers`): scan at most ten
635/// sub-chunks up to `data`, honour its declared size, reject non-PCM.
636pub fn read_wav(data: &[u8]) -> Option<Segment> {
637    if data.len() < 12 {
638        return None;
639    }
640    let u16at = |i: usize| u16::from_le_bytes([data[i], data[i + 1]]);
641    let u32at = |i: usize| u32::from_le_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]);
642    let mut pos = 12usize;
643    let mut chunks: Vec<(&[u8], usize, usize)> = Vec::new();
644    while pos + 8 <= data.len() && chunks.len() < 10 {
645        let id = &data[pos..pos + 4];
646        let size = u32at(pos + 4) as usize;
647        chunks.push((id, pos, size));
648        if id == b"data" {
649            break;
650        }
651        pos += size + 8;
652    }
653    let (_, fpos, fsize) = *chunks.iter().find(|c| c.0 == b"fmt ")?;
654    if fsize < 16 || fpos + 24 > data.len() {
655        return None;
656    }
657    let p = fpos + 8;
658    let audio_format = u16at(p);
659    if audio_format != 1 && audio_format != 0xFFFE {
660        return None;
661    }
662    let channels = u16at(p + 2) as usize;
663    let rate = u32at(p + 4);
664    let bits = u16at(p + 14) as usize;
665    let &(id, dpos, dsize) = chunks.last()?;
666    if id != b"data" {
667        return None;
668    }
669    let start = dpos + 8;
670    let end = (start + dsize).min(data.len());
671    let width = bits / 8;
672    if !(width == 1 || width == 2 || width == 4) || channels == 0 {
673        return None;
674    }
675    let mut raw = data[start.min(end)..end].to_vec();
676    if width == 1 {
677        raw.iter_mut().for_each(|b| *b = b.wrapping_sub(128));
678    }
679    Some(Segment::new(raw, width, rate, channels))
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    fn seg(v: &[i16], rate: u32, ch: usize) -> Segment {
687        Segment::new(
688            v.iter().flat_map(|x| x.to_le_bytes()).collect(),
689            2,
690            rate,
691            ch,
692        )
693    }
694    fn vals(s: &Segment) -> Vec<i16> {
695        s.samples_i16()
696    }
697
698    #[test]
699    fn silent_and_empty_have_pydubs_defaults() {
700        let s = Segment::silent(600.0);
701        assert_eq!((s.frame_rate, s.channels, s.sample_width), (11025, 1, 2));
702        assert_eq!(s.frame_count(), 6615.0);
703        assert_eq!(s.len_ms(), 600);
704        let e = Segment::empty();
705        assert_eq!((e.frame_rate, e.channels, e.sample_width), (1, 1, 1));
706    }
707
708    #[test]
709    fn appending_to_empty_syncs_up() {
710        let a = seg(&[1, 2, 3, 4], 1000, 2);
711        let out = Segment::empty().append(a.clone());
712        assert_eq!(out, a);
713    }
714
715    #[test]
716    fn python_slicing_rules() {
717        assert_eq!(py_slice(b"abcdef", -2, 100), b"ef");
718        assert_eq!(py_slice(b"abcdef", 4, 2), b"");
719        assert_eq!(floor_div(-3, 2), -2);
720        assert_eq!(floor_div(3, 2), 1);
721        assert_eq!(py_round(2.5), 2);
722        assert_eq!(py_round(3.5), 4);
723        assert_eq!(py_round(1234.4), 1234);
724    }
725
726    #[test]
727    fn overlay_truncates_at_base_end() {
728        let base = seg(&[10, 10, 10, 10], 1000, 1);
729        let top = seg(&[1, 2, 3], 1000, 1);
730        assert_eq!(
731            vals(&base.clone().overlay(top.clone(), 2)),
732            [10, 10, 11, 12]
733        );
734        assert_eq!(vals(&base.overlay(top, 0)), [11, 12, 13, 10]);
735    }
736
737    #[test]
738    fn pan_centre_keeps_levels_close() {
739        let s = seg(&[1000, -1000], 44100, 1);
740        let p = s.pan(0.0);
741        assert_eq!(p.channels, 2);
742        let v = vals(&p);
743        assert!(v.iter().all(|x| (x.abs() - 1000).abs() <= 1), "{v:?}");
744    }
745
746    #[test]
747    fn wav_round_trip() {
748        let s = seg(&[1, -2, 3, -4], 8000, 2);
749        let bytes = s.wav_bytes();
750        assert_eq!(bytes.len(), 44 + 8);
751        assert_eq!(read_wav(&bytes).unwrap(), s);
752    }
753}