Skip to main content

sim_lib_stream_file/
pcm_convert.rs

1use std::{error::Error, fmt};
2
3const MAX_CHANNELS: usize = 32;
4
5/// Finite row-major channel mapping applied before sample quantization.
6#[derive(Clone, Debug, PartialEq)]
7pub struct ChannelMatrix {
8    input_channels: usize,
9    output_channels: usize,
10    gains: Vec<f32>,
11}
12
13impl ChannelMatrix {
14    /// Builds a bounded row-major matrix with one row per output channel.
15    pub fn new(
16        input_channels: usize,
17        output_channels: usize,
18        gains: Vec<f32>,
19    ) -> Result<Self, PcmConversionError> {
20        if input_channels == 0
21            || output_channels == 0
22            || input_channels > MAX_CHANNELS
23            || output_channels > MAX_CHANNELS
24        {
25            return Err(PcmConversionError::InvalidChannels);
26        }
27        let expected = input_channels
28            .checked_mul(output_channels)
29            .ok_or(PcmConversionError::InvalidChannels)?;
30        if gains.len() != expected {
31            return Err(PcmConversionError::MatrixShape {
32                expected,
33                actual: gains.len(),
34            });
35        }
36        if gains.iter().any(|gain| !gain.is_finite()) {
37            return Err(PcmConversionError::NonFiniteMatrix);
38        }
39        Ok(Self {
40            input_channels,
41            output_channels,
42            gains,
43        })
44    }
45
46    /// Builds an identity mapping for `channels` interleaved lanes.
47    pub fn identity(channels: usize) -> Result<Self, PcmConversionError> {
48        let cells = channels
49            .checked_mul(channels)
50            .ok_or(PcmConversionError::InvalidChannels)?;
51        let mut gains = vec![0.0; cells];
52        for channel in 0..channels {
53            gains[channel * channels + channel] = 1.0;
54        }
55        Self::new(channels, channels, gains)
56    }
57
58    /// Builds the canonical mono-to-stereo duplication mapping.
59    pub fn mono_to_stereo() -> Self {
60        Self {
61            input_channels: 1,
62            output_channels: 2,
63            gains: vec![1.0, 1.0],
64        }
65    }
66
67    /// Builds an equal-amplitude stereo-to-mono downmix.
68    ///
69    /// Each input is scaled by one half, so equal correlated channels retain
70    /// their level without clipping solely because of the mapping.
71    pub fn stereo_to_mono() -> Self {
72        Self {
73            input_channels: 2,
74            output_channels: 1,
75            gains: vec![0.5, 0.5],
76        }
77    }
78
79    /// Returns the number of interleaved source channels.
80    pub fn input_channels(&self) -> usize {
81        self.input_channels
82    }
83
84    /// Returns the number of interleaved destination channels.
85    pub fn output_channels(&self) -> usize {
86        self.output_channels
87    }
88
89    /// Borrows row-major output-by-input gains.
90    pub fn gains(&self) -> &[f32] {
91        &self.gains
92    }
93
94    fn map(&self, frame: &[f32], output_channel: usize) -> f64 {
95        let row = output_channel * self.input_channels;
96        frame
97            .iter()
98            .enumerate()
99            .map(|(input_channel, sample)| {
100                f64::from(*sample) * f64::from(self.gains[row + input_channel])
101            })
102            .sum()
103    }
104}
105
106/// Dither and error-feedback policy applied in the PCM16 quantizer.
107#[derive(Clone, Copy, Debug, PartialEq)]
108pub enum DitherPolicy {
109    /// Round directly to the closest PCM16 code.
110    None,
111    /// Add deterministic triangular probability-density dither with `seed`.
112    Tpdf {
113        /// Reproducible nonzero or zero seed for the local generator.
114        seed: u64,
115    },
116    /// Apply seeded TPDF plus first-order quantization-error feedback.
117    NoiseShapedTpdf {
118        /// Reproducible nonzero or zero seed for the local generator.
119        seed: u64,
120        /// Previous-error coefficient in the supported range `0..=0.95`.
121        feedback: f32,
122    },
123}
124
125/// Hard input bound and explicit quantization policy.
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub struct QuantizationPolicy {
128    /// Maximum whole input frames admitted by one conversion.
129    pub max_frames: usize,
130    /// Dither and optional first-order error-feedback behavior.
131    pub dither: DitherPolicy,
132}
133
134impl Default for QuantizationPolicy {
135    fn default() -> Self {
136        Self {
137            max_frames: 1_048_576,
138            dither: DitherPolicy::None,
139        }
140    }
141}
142
143/// Invalid bounded channel mapping or PCM quantization request.
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub enum PcmConversionError {
146    /// Channel counts were zero or exceeded the fixed safety ceiling.
147    InvalidChannels,
148    /// Matrix storage did not match its declared channel shape.
149    MatrixShape {
150        /// Required row-major coefficient count.
151        expected: usize,
152        /// Supplied coefficient count.
153        actual: usize,
154    },
155    /// A channel gain was NaN or infinite.
156    NonFiniteMatrix,
157    /// Interleaved source samples ended before a complete frame.
158    MisalignedInput,
159    /// A source sample was NaN or infinite.
160    NonFiniteSample {
161        /// Zero-based sample offset in the interleaved source.
162        index: usize,
163    },
164    /// Input exceeded the caller-declared work bound.
165    FrameLimit {
166        /// Whole frames supplied.
167        supplied: usize,
168        /// Maximum admitted frames.
169        maximum: usize,
170    },
171    /// A noise-shaping coefficient was outside its stable supported range.
172    InvalidDither,
173    /// Output-size or report arithmetic overflowed.
174    SizeOverflow,
175}
176
177impl fmt::Display for PcmConversionError {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Self::InvalidChannels => write!(f, "PCM channel count must be in 1..={MAX_CHANNELS}"),
181            Self::MatrixShape { expected, actual } => {
182                write!(f, "channel matrix needs {expected} gains, got {actual}")
183            }
184            Self::NonFiniteMatrix => write!(f, "channel matrix gains must be finite"),
185            Self::MisalignedInput => write!(f, "interleaved PCM input ends mid-frame"),
186            Self::NonFiniteSample { index } => write!(f, "PCM sample {index} is not finite"),
187            Self::FrameLimit { supplied, maximum } => {
188                write!(f, "PCM input has {supplied} frames, exceeding {maximum}")
189            }
190            Self::InvalidDither => write!(f, "noise-shaping feedback must be in 0..=0.95"),
191            Self::SizeOverflow => write!(f, "PCM conversion size arithmetic overflowed"),
192        }
193    }
194}
195
196impl Error for PcmConversionError {}
197
198/// Audit report for one channel-map and PCM16 quantization pass.
199#[derive(Clone, Debug, PartialEq)]
200pub struct PcmConversionReport {
201    /// Whole source frames converted.
202    pub frames: usize,
203    /// Interleaved source channels.
204    pub input_channels: usize,
205    /// Interleaved destination channels.
206    pub output_channels: usize,
207    /// Peak absolute mapped float sample before clipping or dither.
208    pub peak_before_quantization: f64,
209    /// Count of mapped samples outside the representable `[-1, 1]` interval.
210    pub clipped_samples: usize,
211    /// Root-mean-square difference between mapped floats and PCM16 reconstruction.
212    pub quantization_error_rms: f64,
213    /// Exact dither policy used by the quantizer.
214    pub dither: DitherPolicy,
215}
216
217/// PCM16 samples plus visible mapping, clipping, and quantization evidence.
218#[derive(Clone, Debug, PartialEq)]
219pub struct Pcm16Conversion {
220    /// Interleaved signed 16-bit PCM output.
221    pub samples: Vec<i16>,
222    /// Conversion evidence; clipping is reported rather than hidden.
223    pub report: PcmConversionReport,
224}
225
226/// Maps bounded interleaved `f32` PCM and quantizes it to signed PCM16.
227pub fn convert_f32_to_pcm16(
228    input: &[f32],
229    matrix: &ChannelMatrix,
230    policy: QuantizationPolicy,
231) -> Result<Pcm16Conversion, PcmConversionError> {
232    validate_policy(policy)?;
233    if !input.len().is_multiple_of(matrix.input_channels) {
234        return Err(PcmConversionError::MisalignedInput);
235    }
236    let frames = input.len() / matrix.input_channels;
237    if frames > policy.max_frames {
238        return Err(PcmConversionError::FrameLimit {
239            supplied: frames,
240            maximum: policy.max_frames,
241        });
242    }
243    let output_len = frames
244        .checked_mul(matrix.output_channels)
245        .ok_or(PcmConversionError::SizeOverflow)?;
246    let mut samples = Vec::with_capacity(output_len);
247    let mut errors = vec![0.0f64; matrix.output_channels];
248    let mut random = Random64::new(dither_seed(policy.dither));
249    let mut peak = 0.0f64;
250    let mut clipped = 0usize;
251    let mut error_energy = 0.0f64;
252
253    for (frame_index, frame) in input.chunks(matrix.input_channels).enumerate() {
254        for (channel, sample) in frame.iter().copied().enumerate() {
255            if !sample.is_finite() {
256                return Err(PcmConversionError::NonFiniteSample {
257                    index: frame_index * matrix.input_channels + channel,
258                });
259            }
260        }
261        for (output_channel, shaped_error) in errors.iter_mut().enumerate() {
262            let mapped = matrix.map(frame, output_channel);
263            peak = peak.max(mapped.abs());
264            clipped += usize::from(!(-1.0..=1.0).contains(&mapped));
265            let feedback = dither_feedback(policy.dither);
266            let shaped = mapped + *shaped_error * feedback;
267            let dither = dither_lsb(policy.dither, &mut random);
268            let code = (shaped * 32_768.0 + dither)
269                .round()
270                .clamp(f64::from(i16::MIN), f64::from(i16::MAX)) as i16;
271            let reconstructed = f64::from(code) / 32_768.0;
272            *shaped_error = shaped - reconstructed;
273            let error = reconstructed - mapped;
274            error_energy += error * error;
275            samples.push(code);
276        }
277    }
278    let quantization_error_rms = if output_len == 0 {
279        0.0
280    } else {
281        (error_energy / output_len as f64).sqrt()
282    };
283    Ok(Pcm16Conversion {
284        samples,
285        report: PcmConversionReport {
286            frames,
287            input_channels: matrix.input_channels,
288            output_channels: matrix.output_channels,
289            peak_before_quantization: peak,
290            clipped_samples: clipped,
291            quantization_error_rms,
292            dither: policy.dither,
293        },
294    })
295}
296
297fn validate_policy(policy: QuantizationPolicy) -> Result<(), PcmConversionError> {
298    if policy.max_frames == 0 {
299        return Err(PcmConversionError::FrameLimit {
300            supplied: 0,
301            maximum: 0,
302        });
303    }
304    if let DitherPolicy::NoiseShapedTpdf { feedback, .. } = policy.dither
305        && (!feedback.is_finite() || !(0.0..=0.95).contains(&feedback))
306    {
307        return Err(PcmConversionError::InvalidDither);
308    }
309    Ok(())
310}
311
312fn dither_seed(policy: DitherPolicy) -> u64 {
313    match policy {
314        DitherPolicy::None => 0,
315        DitherPolicy::Tpdf { seed } | DitherPolicy::NoiseShapedTpdf { seed, .. } => seed,
316    }
317}
318
319fn dither_feedback(policy: DitherPolicy) -> f64 {
320    match policy {
321        DitherPolicy::NoiseShapedTpdf { feedback, .. } => f64::from(feedback),
322        DitherPolicy::None | DitherPolicy::Tpdf { .. } => 0.0,
323    }
324}
325
326fn dither_lsb(policy: DitherPolicy, random: &mut Random64) -> f64 {
327    match policy {
328        DitherPolicy::None => 0.0,
329        DitherPolicy::Tpdf { .. } | DitherPolicy::NoiseShapedTpdf { .. } => {
330            random.unit() - random.unit()
331        }
332    }
333}
334
335struct Random64 {
336    state: u64,
337}
338
339impl Random64 {
340    fn new(seed: u64) -> Self {
341        Self {
342            state: if seed == 0 {
343                0x9e37_79b9_7f4a_7c15
344            } else {
345                seed
346            },
347        }
348    }
349
350    fn unit(&mut self) -> f64 {
351        let mut value = self.state;
352        value ^= value << 13;
353        value ^= value >> 7;
354        value ^= value << 17;
355        self.state = value;
356        (value >> 11) as f64 / (1u64 << 53) as f64
357    }
358}