1use std::{error::Error, fmt};
2
3const MAX_CHANNELS: usize = 32;
4
5#[derive(Clone, Debug, PartialEq)]
7pub struct ChannelMatrix {
8 input_channels: usize,
9 output_channels: usize,
10 gains: Vec<f32>,
11}
12
13impl ChannelMatrix {
14 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 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 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 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 pub fn input_channels(&self) -> usize {
81 self.input_channels
82 }
83
84 pub fn output_channels(&self) -> usize {
86 self.output_channels
87 }
88
89 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#[derive(Clone, Copy, Debug, PartialEq)]
108pub enum DitherPolicy {
109 None,
111 Tpdf {
113 seed: u64,
115 },
116 NoiseShapedTpdf {
118 seed: u64,
120 feedback: f32,
122 },
123}
124
125#[derive(Clone, Copy, Debug, PartialEq)]
127pub struct QuantizationPolicy {
128 pub max_frames: usize,
130 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#[derive(Clone, Debug, PartialEq, Eq)]
145pub enum PcmConversionError {
146 InvalidChannels,
148 MatrixShape {
150 expected: usize,
152 actual: usize,
154 },
155 NonFiniteMatrix,
157 MisalignedInput,
159 NonFiniteSample {
161 index: usize,
163 },
164 FrameLimit {
166 supplied: usize,
168 maximum: usize,
170 },
171 InvalidDither,
173 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#[derive(Clone, Debug, PartialEq)]
200pub struct PcmConversionReport {
201 pub frames: usize,
203 pub input_channels: usize,
205 pub output_channels: usize,
207 pub peak_before_quantization: f64,
209 pub clipped_samples: usize,
211 pub quantization_error_rms: f64,
213 pub dither: DitherPolicy,
215}
216
217#[derive(Clone, Debug, PartialEq)]
219pub struct Pcm16Conversion {
220 pub samples: Vec<i16>,
222 pub report: PcmConversionReport,
224}
225
226pub 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}