Skip to main content

oximedia_codec/pcm/
mod.rs

1//! PCM (Pulse Code Modulation) codec — trivial encode/decode for raw audio.
2//!
3//! PCM is the canonical raw audio format used as a reference baseline and for
4//! uncompressed audio interchange. This module implements:
5//!
6//! - **`PcmEncoder`** — converts `AudioFrame` samples to raw bytes in a
7//!   configurable byte order and sample format.
8//! - **`PcmDecoder`** — parses raw PCM bytes back into an `AudioFrame`.
9//!
10//! Supported sample formats: `U8`, `I16` (LE/BE), `I24` (LE/BE, stored as
11//! 3-byte samples), `I32` (LE/BE), `F32` (IEEE 754, LE/BE), `F64` (LE/BE).
12//!
13//! # Example
14//!
15//! ```rust
16//! use oximedia_codec::pcm::{PcmConfig, PcmEncoder, PcmDecoder, PcmFormat, ByteOrder};
17//! use oximedia_codec::audio::{AudioFrame, SampleFormat};
18//!
19//! let config = PcmConfig {
20//!     format: PcmFormat::I16,
21//!     byte_order: ByteOrder::Little,
22//!     sample_rate: 44100,
23//!     channels: 2,
24//! };
25//!
26//! let encoder = PcmEncoder::new(config.clone());
27//! // Build a stereo frame with 128 zero samples (raw f32 bytes, little-endian)
28//! let raw_f32: Vec<f32> = vec![0.0f32; 256];
29//! let raw_bytes: Vec<u8> = raw_f32.iter().flat_map(|s| s.to_le_bytes()).collect();
30//! let frame = AudioFrame::new(raw_bytes, 128, 44100, 2, SampleFormat::F32);
31//! let bytes = encoder.encode_frame(&frame).expect("encode");
32//!
33//! let decoder = PcmDecoder::new(config);
34//! let decoded = decoder.decode_bytes(&bytes).expect("decode");
35//! assert_eq!(decoded.sample_count, 128);
36//! ```
37
38#![forbid(unsafe_code)]
39#![allow(clippy::cast_possible_truncation)]
40#![allow(clippy::cast_sign_loss)]
41#![allow(clippy::cast_precision_loss)]
42#![allow(clippy::cast_lossless)]
43#![allow(dead_code)]
44
45use crate::audio::{AudioFrame, SampleFormat};
46use crate::error::{CodecError, CodecResult};
47
48// =============================================================================
49// PCM Format Enum
50// =============================================================================
51
52/// PCM sample encoding format.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum PcmFormat {
55    /// 8-bit unsigned (0..255, centre = 128).
56    U8,
57    /// 16-bit signed integer.
58    I16,
59    /// 24-bit signed integer (3 bytes per sample, sign-extended on decode).
60    I24,
61    /// 32-bit signed integer.
62    I32,
63    /// 32-bit IEEE-754 float.
64    F32,
65    /// 64-bit IEEE-754 float.
66    F64,
67}
68
69impl PcmFormat {
70    /// Bytes per sample.
71    #[must_use]
72    pub const fn bytes_per_sample(self) -> usize {
73        match self {
74            Self::U8 => 1,
75            Self::I16 => 2,
76            Self::I24 => 3,
77            Self::I32 => 4,
78            Self::F32 => 4,
79            Self::F64 => 8,
80        }
81    }
82}
83
84// =============================================================================
85// Byte order
86// =============================================================================
87
88/// Byte ordering for multi-byte samples.
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum ByteOrder {
91    /// Little-endian (LSB first, e.g. WAV default).
92    Little,
93    /// Big-endian (MSB first, e.g. AIFF default).
94    Big,
95}
96
97// =============================================================================
98// PcmConfig
99// =============================================================================
100
101/// Configuration for PCM encoder and decoder.
102#[derive(Clone, Debug)]
103pub struct PcmConfig {
104    /// PCM sample format.
105    pub format: PcmFormat,
106    /// Byte order for multi-byte formats.
107    pub byte_order: ByteOrder,
108    /// Sample rate in Hz.
109    pub sample_rate: u32,
110    /// Number of interleaved channels.
111    pub channels: u8,
112}
113
114impl Default for PcmConfig {
115    fn default() -> Self {
116        Self {
117            format: PcmFormat::I16,
118            byte_order: ByteOrder::Little,
119            sample_rate: 48000,
120            channels: 2,
121        }
122    }
123}
124
125// =============================================================================
126// Encoding helpers (f32 normalised → raw bytes)
127// =============================================================================
128
129/// Clamp a float to `[-1.0, 1.0]` and convert to `i16`.
130#[inline]
131fn f32_to_i16(s: f32) -> i16 {
132    let v = (s.clamp(-1.0, 1.0) * 32767.0) as i32;
133    v.clamp(-32768, 32767) as i16
134}
135
136/// Clamp a float to `[-1.0, 1.0]` and convert to `i24` (return i32 in ±8_388_607).
137#[inline]
138fn f32_to_i24(s: f32) -> i32 {
139    let v = (s.clamp(-1.0, 1.0) * 8_388_607.0) as i64;
140    v.clamp(-8_388_608, 8_388_607) as i32
141}
142
143/// Clamp a float to `[-1.0, 1.0]` and convert to `i32`.
144#[inline]
145fn f32_to_i32(s: f32) -> i32 {
146    let v = (s.clamp(-1.0, 1.0) * 2_147_483_647.0_f64 as f32) as i64;
147    v.clamp(-2_147_483_648, 2_147_483_647) as i32
148}
149
150/// Convert `f32` to `u8` (maps `-1..1` → `0..255`, centre = 128).
151#[inline]
152fn f32_to_u8(s: f32) -> u8 {
153    let v = ((s.clamp(-1.0, 1.0) + 1.0) * 127.5) as i32;
154    v.clamp(0, 255) as u8
155}
156
157// =============================================================================
158// Decoding helpers (raw bytes → f32 normalised)
159// =============================================================================
160
161#[inline]
162fn i16_to_f32(v: i16) -> f32 {
163    v as f32 / 32768.0
164}
165
166#[inline]
167fn i24_to_f32(v: i32) -> f32 {
168    v as f32 / 8_388_608.0
169}
170
171#[inline]
172fn i32_to_f32(v: i32) -> f32 {
173    v as f32 / 2_147_483_648.0_f64 as f32
174}
175
176#[inline]
177fn u8_to_f32(v: u8) -> f32 {
178    (v as f32 - 128.0) / 128.0
179}
180
181// =============================================================================
182// Internal helpers for frame sample extraction
183// =============================================================================
184
185/// Extract normalised f32 samples from an `AudioFrame`'s raw byte buffer.
186///
187/// The frame's `format` field determines how the bytes are interpreted.
188/// Returns an error if the byte buffer length is not aligned for the format.
189fn frame_to_f32_samples(frame: &AudioFrame) -> CodecResult<Vec<f32>> {
190    match frame.format {
191        SampleFormat::F32 => {
192            if frame.samples.len() % 4 != 0 {
193                return Err(CodecError::InvalidData(
194                    "F32 frame: sample byte count is not a multiple of 4".to_string(),
195                ));
196            }
197            let mut out = Vec::with_capacity(frame.samples.len() / 4);
198            for chunk in frame.samples.chunks_exact(4) {
199                let arr = [chunk[0], chunk[1], chunk[2], chunk[3]];
200                out.push(f32::from_le_bytes(arr));
201            }
202            Ok(out)
203        }
204        SampleFormat::I16 => {
205            if frame.samples.len() % 2 != 0 {
206                return Err(CodecError::InvalidData(
207                    "I16 frame: sample byte count is not a multiple of 2".to_string(),
208                ));
209            }
210            let mut out = Vec::with_capacity(frame.samples.len() / 2);
211            for chunk in frame.samples.chunks_exact(2) {
212                let arr = [chunk[0], chunk[1]];
213                let v = i16::from_le_bytes(arr);
214                out.push(i16_to_f32(v));
215            }
216            Ok(out)
217        }
218        SampleFormat::I32 => {
219            if frame.samples.len() % 4 != 0 {
220                return Err(CodecError::InvalidData(
221                    "I32 frame: sample byte count is not a multiple of 4".to_string(),
222                ));
223            }
224            let mut out = Vec::with_capacity(frame.samples.len() / 4);
225            for chunk in frame.samples.chunks_exact(4) {
226                let arr = [chunk[0], chunk[1], chunk[2], chunk[3]];
227                let v = i32::from_le_bytes(arr);
228                out.push(i32_to_f32(v));
229            }
230            Ok(out)
231        }
232        SampleFormat::U8 => {
233            let mut out = Vec::with_capacity(frame.samples.len());
234            for &b in &frame.samples {
235                out.push(u8_to_f32(b));
236            }
237            Ok(out)
238        }
239    }
240}
241
242/// Convert a slice of normalised f32 samples into raw F32-LE bytes.
243fn f32_samples_to_bytes(samples: &[f32]) -> Vec<u8> {
244    let mut out = Vec::with_capacity(samples.len() * 4);
245    for &s in samples {
246        out.extend_from_slice(&s.to_le_bytes());
247    }
248    out
249}
250
251// =============================================================================
252// PcmEncoder
253// =============================================================================
254
255/// Encodes `AudioFrame` samples to raw PCM bytes.
256///
257/// The output is interleaved PCM data with no header — suitable for embedding
258/// inside WAV/AIFF containers or piping to raw audio sinks.
259#[derive(Debug, Clone)]
260pub struct PcmEncoder {
261    config: PcmConfig,
262}
263
264impl PcmEncoder {
265    /// Create a new encoder with the given configuration.
266    #[must_use]
267    pub fn new(config: PcmConfig) -> Self {
268        Self { config }
269    }
270
271    /// Encode one `AudioFrame` to raw bytes.
272    ///
273    /// The frame's `format` field controls how `samples` bytes are interpreted
274    /// before re-encoding into the configured `PcmFormat`.
275    ///
276    /// # Errors
277    ///
278    /// Returns `CodecError::InvalidParameter` if the frame channel count or
279    /// sample rate does not match the encoder configuration.
280    pub fn encode_frame(&self, frame: &AudioFrame) -> CodecResult<Vec<u8>> {
281        let ch = frame.channels as u8;
282        if ch != self.config.channels {
283            return Err(CodecError::InvalidParameter(format!(
284                "PCM encoder: expected {} channels, got {}",
285                self.config.channels, ch
286            )));
287        }
288        if frame.sample_rate != self.config.sample_rate {
289            return Err(CodecError::InvalidParameter(format!(
290                "PCM encoder: expected sample_rate={}, got {}",
291                self.config.sample_rate, frame.sample_rate
292            )));
293        }
294
295        // Decode the frame's internal byte representation to normalised f32.
296        let f32_samples = frame_to_f32_samples(frame)?;
297
298        let bps = self.config.format.bytes_per_sample();
299        let mut out = Vec::with_capacity(f32_samples.len() * bps);
300        let le = self.config.byte_order == ByteOrder::Little;
301
302        for s in f32_samples {
303            match self.config.format {
304                PcmFormat::U8 => out.push(f32_to_u8(s)),
305                PcmFormat::I16 => {
306                    let v = f32_to_i16(s);
307                    if le {
308                        out.extend_from_slice(&v.to_le_bytes());
309                    } else {
310                        out.extend_from_slice(&v.to_be_bytes());
311                    }
312                }
313                PcmFormat::I24 => {
314                    let v = f32_to_i24(s);
315                    if le {
316                        out.push((v & 0xFF) as u8);
317                        out.push(((v >> 8) & 0xFF) as u8);
318                        out.push(((v >> 16) & 0xFF) as u8);
319                    } else {
320                        out.push(((v >> 16) & 0xFF) as u8);
321                        out.push(((v >> 8) & 0xFF) as u8);
322                        out.push((v & 0xFF) as u8);
323                    }
324                }
325                PcmFormat::I32 => {
326                    let v = f32_to_i32(s);
327                    if le {
328                        out.extend_from_slice(&v.to_le_bytes());
329                    } else {
330                        out.extend_from_slice(&v.to_be_bytes());
331                    }
332                }
333                PcmFormat::F32 => {
334                    if le {
335                        out.extend_from_slice(&s.to_le_bytes());
336                    } else {
337                        out.extend_from_slice(&s.to_be_bytes());
338                    }
339                }
340                PcmFormat::F64 => {
341                    let d = f64::from(s);
342                    if le {
343                        out.extend_from_slice(&d.to_le_bytes());
344                    } else {
345                        out.extend_from_slice(&d.to_be_bytes());
346                    }
347                }
348            }
349        }
350
351        Ok(out)
352    }
353
354    /// Encode a slice of interleaved `f32` samples directly.
355    ///
356    /// # Errors
357    ///
358    /// Returns error if `samples.len()` is not a multiple of `channels`.
359    pub fn encode_raw(&self, samples: &[f32]) -> CodecResult<Vec<u8>> {
360        if self.config.channels > 0 && samples.len() % self.config.channels as usize != 0 {
361            return Err(CodecError::InvalidParameter(
362                "PCM encode_raw: sample count not multiple of channels".to_string(),
363            ));
364        }
365        let sample_count = if self.config.channels > 0 {
366            samples.len() / self.config.channels as usize
367        } else {
368            samples.len()
369        };
370        // Store f32 samples as raw LE bytes in the frame.
371        let raw_bytes = f32_samples_to_bytes(samples);
372        let frame = AudioFrame::new(
373            raw_bytes,
374            sample_count,
375            self.config.sample_rate,
376            self.config.channels as usize,
377            SampleFormat::F32,
378        );
379        self.encode_frame(&frame)
380    }
381
382    /// Return a reference to the encoder configuration.
383    #[must_use]
384    pub fn config(&self) -> &PcmConfig {
385        &self.config
386    }
387}
388
389// =============================================================================
390// PcmDecoder
391// =============================================================================
392
393/// Decodes raw PCM bytes to an `AudioFrame`.
394///
395/// No header parsing is performed — the caller must know the format ahead of time.
396#[derive(Debug, Clone)]
397pub struct PcmDecoder {
398    config: PcmConfig,
399}
400
401impl PcmDecoder {
402    /// Create a new decoder with the given configuration.
403    #[must_use]
404    pub fn new(config: PcmConfig) -> Self {
405        Self { config }
406    }
407
408    /// Decode raw PCM bytes into an `AudioFrame`.
409    ///
410    /// Samples are decoded to normalised f32 and stored as F32-LE bytes in
411    /// the returned `AudioFrame`.
412    ///
413    /// # Errors
414    ///
415    /// Returns `CodecError::InvalidBitstream` if `bytes.len()` is not an
416    /// exact multiple of bytes-per-sample.
417    pub fn decode_bytes(&self, bytes: &[u8]) -> CodecResult<AudioFrame> {
418        let bps = self.config.format.bytes_per_sample();
419        if bytes.len() % bps != 0 {
420            return Err(CodecError::InvalidBitstream(format!(
421                "PCM decode: byte count {} is not a multiple of bytes-per-sample {}",
422                bytes.len(),
423                bps
424            )));
425        }
426
427        let n_samples = bytes.len() / bps;
428        let mut f32_samples: Vec<f32> = Vec::with_capacity(n_samples);
429        let le = self.config.byte_order == ByteOrder::Little;
430
431        let mut i = 0;
432        while i < bytes.len() {
433            let s = match self.config.format {
434                PcmFormat::U8 => {
435                    let v = bytes[i];
436                    i += 1;
437                    u8_to_f32(v)
438                }
439                PcmFormat::I16 => {
440                    let arr = [bytes[i], bytes[i + 1]];
441                    let v = if le {
442                        i16::from_le_bytes(arr)
443                    } else {
444                        i16::from_be_bytes(arr)
445                    };
446                    i += 2;
447                    i16_to_f32(v)
448                }
449                PcmFormat::I24 => {
450                    let raw = if le {
451                        (bytes[i] as i32)
452                            | ((bytes[i + 1] as i32) << 8)
453                            | ((bytes[i + 2] as i32) << 16)
454                    } else {
455                        ((bytes[i] as i32) << 16)
456                            | ((bytes[i + 1] as i32) << 8)
457                            | (bytes[i + 2] as i32)
458                    };
459                    // Sign-extend from 24 bits
460                    let v = if raw & 0x80_0000 != 0 {
461                        raw | !0xFF_FFFF_i32
462                    } else {
463                        raw
464                    };
465                    i += 3;
466                    i24_to_f32(v)
467                }
468                PcmFormat::I32 => {
469                    let arr = [bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]];
470                    let v = if le {
471                        i32::from_le_bytes(arr)
472                    } else {
473                        i32::from_be_bytes(arr)
474                    };
475                    i += 4;
476                    i32_to_f32(v)
477                }
478                PcmFormat::F32 => {
479                    let arr = [bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]];
480                    let v = if le {
481                        f32::from_le_bytes(arr)
482                    } else {
483                        f32::from_be_bytes(arr)
484                    };
485                    i += 4;
486                    v
487                }
488                PcmFormat::F64 => {
489                    let arr = [
490                        bytes[i],
491                        bytes[i + 1],
492                        bytes[i + 2],
493                        bytes[i + 3],
494                        bytes[i + 4],
495                        bytes[i + 5],
496                        bytes[i + 6],
497                        bytes[i + 7],
498                    ];
499                    let v = if le {
500                        f64::from_le_bytes(arr)
501                    } else {
502                        f64::from_be_bytes(arr)
503                    };
504                    i += 8;
505                    v as f32
506                }
507            };
508            f32_samples.push(s);
509        }
510
511        // Store decoded f32 samples as raw LE bytes.
512        let raw_bytes = f32_samples_to_bytes(&f32_samples);
513        let channels = self.config.channels as usize;
514        let sample_count = f32_samples
515            .len()
516            .checked_div(channels)
517            .unwrap_or(f32_samples.len());
518
519        Ok(AudioFrame::new(
520            raw_bytes,
521            sample_count,
522            self.config.sample_rate,
523            channels,
524            SampleFormat::F32,
525        ))
526    }
527
528    /// Return a reference to the decoder configuration.
529    #[must_use]
530    pub fn config(&self) -> &PcmConfig {
531        &self.config
532    }
533
534    /// Compute the number of frames (per-channel sample groups) in a byte slice.
535    #[must_use]
536    pub fn frame_count(&self, bytes: &[u8]) -> usize {
537        let bps = self.config.format.bytes_per_sample();
538        let total_samples = bytes.len() / bps;
539        let ch = self.config.channels as usize;
540        total_samples.checked_div(ch).unwrap_or(0)
541    }
542}
543
544// =============================================================================
545// Tests
546// =============================================================================
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551
552    /// Build an `AudioFrame` containing `f32` samples encoded as raw LE bytes.
553    fn make_frame(samples: Vec<f32>, sample_rate: u32, channels: u8) -> AudioFrame {
554        let sample_count = if channels > 0 {
555            samples.len() / channels as usize
556        } else {
557            samples.len()
558        };
559        let raw_bytes = f32_samples_to_bytes(&samples);
560        AudioFrame::new(
561            raw_bytes,
562            sample_count,
563            sample_rate,
564            channels as usize,
565            SampleFormat::F32,
566        )
567    }
568
569    /// Extract decoded f32 samples from a frame (stored as F32-LE bytes).
570    fn frame_samples(frame: &AudioFrame) -> Vec<f32> {
571        frame_to_f32_samples(frame).expect("frame_to_f32_samples")
572    }
573
574    // ---------- U8 -----------------------------------------------------------
575
576    #[test]
577    fn test_u8_roundtrip() {
578        let cfg = PcmConfig {
579            format: PcmFormat::U8,
580            byte_order: ByteOrder::Little,
581            sample_rate: 44100,
582            channels: 1,
583        };
584        let input = vec![0.0f32, 0.5, -0.5, 1.0, -1.0];
585        let enc = PcmEncoder::new(cfg.clone());
586        let dec = PcmDecoder::new(cfg);
587        let frame = make_frame(input.clone(), 44100, 1);
588        let bytes = enc.encode_frame(&frame).expect("encode");
589        assert_eq!(bytes.len(), input.len());
590        let decoded = dec.decode_bytes(&bytes).expect("decode");
591        let got = frame_samples(&decoded);
592        assert_eq!(got.len(), input.len());
593        // U8 has 1/256 resolution — check rough roundtrip.
594        for (&orig, &g) in input.iter().zip(got.iter()) {
595            assert!((orig - g).abs() < 0.02, "U8 roundtrip: orig={orig} got={g}");
596        }
597    }
598
599    // ---------- I16 LE -------------------------------------------------------
600
601    #[test]
602    fn test_i16_le_roundtrip() {
603        let cfg = PcmConfig {
604            format: PcmFormat::I16,
605            byte_order: ByteOrder::Little,
606            sample_rate: 48000,
607            channels: 2,
608        };
609        let input: Vec<f32> = (0..256).map(|i| i as f32 / 128.0 - 1.0).collect();
610        let enc = PcmEncoder::new(cfg.clone());
611        let dec = PcmDecoder::new(cfg);
612        let frame = make_frame(input.clone(), 48000, 2);
613        let bytes = enc.encode_frame(&frame).expect("encode");
614        assert_eq!(bytes.len(), input.len() * 2);
615        let decoded = dec.decode_bytes(&bytes).expect("decode");
616        let got = frame_samples(&decoded);
617        assert_eq!(got.len(), input.len());
618        for (&orig, &g) in input.iter().zip(got.iter()) {
619            assert!(
620                (orig - g).abs() < 0.0001,
621                "I16 roundtrip: orig={orig} got={g}"
622            );
623        }
624    }
625
626    // ---------- I16 BE -------------------------------------------------------
627
628    #[test]
629    fn test_i16_be_roundtrip() {
630        let cfg = PcmConfig {
631            format: PcmFormat::I16,
632            byte_order: ByteOrder::Big,
633            sample_rate: 44100,
634            channels: 1,
635        };
636        let input = vec![0.0f32, 0.25, -0.25, 0.99, -0.99];
637        let enc = PcmEncoder::new(cfg.clone());
638        let dec = PcmDecoder::new(cfg);
639        let frame = make_frame(input.clone(), 44100, 1);
640        let bytes = enc.encode_frame(&frame).expect("encode");
641        assert_eq!(bytes.len(), input.len() * 2);
642        let decoded = dec.decode_bytes(&bytes).expect("decode");
643        let got = frame_samples(&decoded);
644        for (&orig, &g) in input.iter().zip(got.iter()) {
645            assert!((orig - g).abs() < 0.0001, "I16 BE roundtrip");
646        }
647    }
648
649    // ---------- I24 ----------------------------------------------------------
650
651    #[test]
652    fn test_i24_le_roundtrip() {
653        let cfg = PcmConfig {
654            format: PcmFormat::I24,
655            byte_order: ByteOrder::Little,
656            sample_rate: 96000,
657            channels: 2,
658        };
659        let input: Vec<f32> = vec![0.0, 0.5, -0.5, 0.999, -0.999];
660        let enc = PcmEncoder::new(cfg.clone());
661        let dec = PcmDecoder::new(cfg);
662        let frame = make_frame(input.clone(), 96000, 2);
663        let bytes = enc.encode_frame(&frame).expect("encode");
664        assert_eq!(bytes.len(), input.len() * 3);
665        let decoded = dec.decode_bytes(&bytes).expect("decode");
666        let got = frame_samples(&decoded);
667        for (&orig, &g) in input.iter().zip(got.iter()) {
668            assert!(
669                (orig - g).abs() < 0.000001,
670                "I24 LE roundtrip orig={orig} got={g}"
671            );
672        }
673    }
674
675    #[test]
676    fn test_i24_be_roundtrip() {
677        let cfg = PcmConfig {
678            format: PcmFormat::I24,
679            byte_order: ByteOrder::Big,
680            sample_rate: 96000,
681            channels: 1,
682        };
683        let input: Vec<f32> = vec![0.0, -0.1, 0.3, -0.7, 0.9];
684        let enc = PcmEncoder::new(cfg.clone());
685        let dec = PcmDecoder::new(cfg);
686        let frame = make_frame(input.clone(), 96000, 1);
687        let bytes = enc.encode_frame(&frame).expect("encode");
688        let decoded = dec.decode_bytes(&bytes).expect("decode");
689        let got = frame_samples(&decoded);
690        for (&orig, &g) in input.iter().zip(got.iter()) {
691            assert!(
692                (orig - g).abs() < 0.000001,
693                "I24 BE roundtrip orig={orig} got={g}"
694            );
695        }
696    }
697
698    // ---------- I32 ----------------------------------------------------------
699
700    #[test]
701    fn test_i32_le_roundtrip() {
702        let cfg = PcmConfig {
703            format: PcmFormat::I32,
704            byte_order: ByteOrder::Little,
705            sample_rate: 192000,
706            channels: 1,
707        };
708        let input = vec![0.0f32, 0.5, -0.5, 0.9999, -0.9999];
709        let enc = PcmEncoder::new(cfg.clone());
710        let dec = PcmDecoder::new(cfg);
711        let frame = make_frame(input.clone(), 192000, 1);
712        let bytes = enc.encode_frame(&frame).expect("encode");
713        assert_eq!(bytes.len(), input.len() * 4);
714        let decoded = dec.decode_bytes(&bytes).expect("decode");
715        let got = frame_samples(&decoded);
716        for (&orig, &g) in input.iter().zip(got.iter()) {
717            assert!(
718                (orig - g).abs() < 0.0001,
719                "I32 LE roundtrip orig={orig} got={g}"
720            );
721        }
722    }
723
724    // ---------- F32 ----------------------------------------------------------
725
726    #[test]
727    fn test_f32_le_roundtrip() {
728        let cfg = PcmConfig {
729            format: PcmFormat::F32,
730            byte_order: ByteOrder::Little,
731            sample_rate: 48000,
732            channels: 2,
733        };
734        let input: Vec<f32> = (0..64).map(|i| (i as f32 / 32.0) - 1.0).collect();
735        let enc = PcmEncoder::new(cfg.clone());
736        let dec = PcmDecoder::new(cfg);
737        let frame = make_frame(input.clone(), 48000, 2);
738        let bytes = enc.encode_frame(&frame).expect("encode");
739        assert_eq!(bytes.len(), input.len() * 4);
740        let decoded = dec.decode_bytes(&bytes).expect("decode");
741        let got = frame_samples(&decoded);
742        for (&orig, &g) in input.iter().zip(got.iter()) {
743            assert_eq!(orig, g, "F32 LE should be lossless");
744        }
745    }
746
747    #[test]
748    fn test_f32_be_roundtrip() {
749        let cfg = PcmConfig {
750            format: PcmFormat::F32,
751            byte_order: ByteOrder::Big,
752            sample_rate: 44100,
753            channels: 1,
754        };
755        let input = vec![0.0f32, 0.5, -0.5, 1.0, -1.0];
756        let enc = PcmEncoder::new(cfg.clone());
757        let dec = PcmDecoder::new(cfg);
758        let frame = make_frame(input.clone(), 44100, 1);
759        let bytes = enc.encode_frame(&frame).expect("encode");
760        let decoded = dec.decode_bytes(&bytes).expect("decode");
761        let got = frame_samples(&decoded);
762        for (&orig, &g) in input.iter().zip(got.iter()) {
763            assert_eq!(orig, g, "F32 BE should be lossless");
764        }
765    }
766
767    // ---------- F64 ----------------------------------------------------------
768
769    #[test]
770    fn test_f64_le_roundtrip() {
771        let cfg = PcmConfig {
772            format: PcmFormat::F64,
773            byte_order: ByteOrder::Little,
774            sample_rate: 48000,
775            channels: 1,
776        };
777        let input = vec![0.0f32, 0.123, -0.456, 0.789, -0.999];
778        let enc = PcmEncoder::new(cfg.clone());
779        let dec = PcmDecoder::new(cfg);
780        let frame = make_frame(input.clone(), 48000, 1);
781        let bytes = enc.encode_frame(&frame).expect("encode");
782        assert_eq!(bytes.len(), input.len() * 8);
783        let decoded = dec.decode_bytes(&bytes).expect("decode");
784        let got = frame_samples(&decoded);
785        for (&orig, &g) in input.iter().zip(got.iter()) {
786            assert!(
787                (orig - g).abs() < 1e-6,
788                "F64 LE roundtrip orig={orig} got={g}"
789            );
790        }
791    }
792
793    // ---------- Error cases --------------------------------------------------
794
795    #[test]
796    fn test_mismatched_channels_error() {
797        let cfg = PcmConfig {
798            format: PcmFormat::I16,
799            byte_order: ByteOrder::Little,
800            sample_rate: 44100,
801            channels: 2,
802        };
803        let enc = PcmEncoder::new(cfg);
804        // Mono frame with stereo encoder
805        let frame = make_frame(vec![0.0f32; 128], 44100, 1);
806        assert!(enc.encode_frame(&frame).is_err());
807    }
808
809    #[test]
810    fn test_mismatched_sample_rate_error() {
811        let cfg = PcmConfig {
812            format: PcmFormat::I16,
813            byte_order: ByteOrder::Little,
814            sample_rate: 44100,
815            channels: 1,
816        };
817        let enc = PcmEncoder::new(cfg);
818        let frame = make_frame(vec![0.0f32; 64], 48000, 1); // wrong rate
819        assert!(enc.encode_frame(&frame).is_err());
820    }
821
822    #[test]
823    fn test_decode_bad_alignment_error() {
824        let cfg = PcmConfig {
825            format: PcmFormat::I16,
826            byte_order: ByteOrder::Little,
827            sample_rate: 48000,
828            channels: 1,
829        };
830        let dec = PcmDecoder::new(cfg);
831        let bytes = vec![0u8; 3]; // not multiple of 2
832        assert!(dec.decode_bytes(&bytes).is_err());
833    }
834
835    #[test]
836    fn test_encode_raw_roundtrip() {
837        let cfg = PcmConfig {
838            format: PcmFormat::I16,
839            byte_order: ByteOrder::Little,
840            sample_rate: 44100,
841            channels: 2,
842        };
843        let enc = PcmEncoder::new(cfg.clone());
844        let dec = PcmDecoder::new(cfg);
845        let raw: Vec<f32> = (0..64).map(|i| (i as f32 / 32.0) - 1.0).collect();
846        let bytes = enc.encode_raw(&raw).expect("encode_raw");
847        let decoded = dec.decode_bytes(&bytes).expect("decode");
848        let got = frame_samples(&decoded);
849        assert_eq!(got.len(), raw.len());
850    }
851
852    #[test]
853    fn test_encode_raw_bad_alignment_error() {
854        let cfg = PcmConfig {
855            format: PcmFormat::I16,
856            byte_order: ByteOrder::Little,
857            sample_rate: 44100,
858            channels: 2,
859        };
860        let enc = PcmEncoder::new(cfg);
861        // 3 samples is not divisible by 2 channels
862        let raw = vec![0.0f32; 3];
863        assert!(enc.encode_raw(&raw).is_err());
864    }
865
866    #[test]
867    fn test_frame_count() {
868        let cfg = PcmConfig {
869            format: PcmFormat::I16,
870            byte_order: ByteOrder::Little,
871            sample_rate: 44100,
872            channels: 2,
873        };
874        let dec = PcmDecoder::new(cfg);
875        // 256 bytes / 2 bps = 128 samples / 2 ch = 64 frames
876        assert_eq!(dec.frame_count(&vec![0u8; 256]), 64);
877    }
878
879    #[test]
880    fn test_silence_encode_decode() {
881        let cfg = PcmConfig {
882            format: PcmFormat::I16,
883            byte_order: ByteOrder::Little,
884            sample_rate: 44100,
885            channels: 2,
886        };
887        let enc = PcmEncoder::new(cfg.clone());
888        let dec = PcmDecoder::new(cfg);
889        let silence = make_frame(vec![0.0f32; 512], 44100, 2);
890        let bytes = enc.encode_frame(&silence).expect("encode");
891        // All bytes should be zero for silence
892        assert!(bytes.iter().all(|&b| b == 0));
893        let decoded = dec.decode_bytes(&bytes).expect("decode");
894        let got = frame_samples(&decoded);
895        assert!(got.iter().all(|&s| s == 0.0));
896    }
897
898    #[test]
899    fn test_config_accessor() {
900        let cfg = PcmConfig::default();
901        let enc = PcmEncoder::new(cfg.clone());
902        let dec = PcmDecoder::new(cfg);
903        assert_eq!(enc.config().channels, 2);
904        assert_eq!(dec.config().sample_rate, 48000);
905    }
906}