use std::{error::Error, fmt};
const MAX_CHANNELS: usize = 32;
#[derive(Clone, Debug, PartialEq)]
pub struct ChannelMatrix {
input_channels: usize,
output_channels: usize,
gains: Vec<f32>,
}
impl ChannelMatrix {
pub fn new(
input_channels: usize,
output_channels: usize,
gains: Vec<f32>,
) -> Result<Self, PcmConversionError> {
if input_channels == 0
|| output_channels == 0
|| input_channels > MAX_CHANNELS
|| output_channels > MAX_CHANNELS
{
return Err(PcmConversionError::InvalidChannels);
}
let expected = input_channels
.checked_mul(output_channels)
.ok_or(PcmConversionError::InvalidChannels)?;
if gains.len() != expected {
return Err(PcmConversionError::MatrixShape {
expected,
actual: gains.len(),
});
}
if gains.iter().any(|gain| !gain.is_finite()) {
return Err(PcmConversionError::NonFiniteMatrix);
}
Ok(Self {
input_channels,
output_channels,
gains,
})
}
pub fn identity(channels: usize) -> Result<Self, PcmConversionError> {
let cells = channels
.checked_mul(channels)
.ok_or(PcmConversionError::InvalidChannels)?;
let mut gains = vec![0.0; cells];
for channel in 0..channels {
gains[channel * channels + channel] = 1.0;
}
Self::new(channels, channels, gains)
}
pub fn mono_to_stereo() -> Self {
Self {
input_channels: 1,
output_channels: 2,
gains: vec![1.0, 1.0],
}
}
pub fn stereo_to_mono() -> Self {
Self {
input_channels: 2,
output_channels: 1,
gains: vec![0.5, 0.5],
}
}
pub fn input_channels(&self) -> usize {
self.input_channels
}
pub fn output_channels(&self) -> usize {
self.output_channels
}
pub fn gains(&self) -> &[f32] {
&self.gains
}
fn map(&self, frame: &[f32], output_channel: usize) -> f64 {
let row = output_channel * self.input_channels;
frame
.iter()
.enumerate()
.map(|(input_channel, sample)| {
f64::from(*sample) * f64::from(self.gains[row + input_channel])
})
.sum()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DitherPolicy {
None,
Tpdf {
seed: u64,
},
NoiseShapedTpdf {
seed: u64,
feedback: f32,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct QuantizationPolicy {
pub max_frames: usize,
pub dither: DitherPolicy,
}
impl Default for QuantizationPolicy {
fn default() -> Self {
Self {
max_frames: 1_048_576,
dither: DitherPolicy::None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PcmConversionError {
InvalidChannels,
MatrixShape {
expected: usize,
actual: usize,
},
NonFiniteMatrix,
MisalignedInput,
NonFiniteSample {
index: usize,
},
FrameLimit {
supplied: usize,
maximum: usize,
},
InvalidDither,
SizeOverflow,
}
impl fmt::Display for PcmConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidChannels => write!(f, "PCM channel count must be in 1..={MAX_CHANNELS}"),
Self::MatrixShape { expected, actual } => {
write!(f, "channel matrix needs {expected} gains, got {actual}")
}
Self::NonFiniteMatrix => write!(f, "channel matrix gains must be finite"),
Self::MisalignedInput => write!(f, "interleaved PCM input ends mid-frame"),
Self::NonFiniteSample { index } => write!(f, "PCM sample {index} is not finite"),
Self::FrameLimit { supplied, maximum } => {
write!(f, "PCM input has {supplied} frames, exceeding {maximum}")
}
Self::InvalidDither => write!(f, "noise-shaping feedback must be in 0..=0.95"),
Self::SizeOverflow => write!(f, "PCM conversion size arithmetic overflowed"),
}
}
}
impl Error for PcmConversionError {}
#[derive(Clone, Debug, PartialEq)]
pub struct PcmConversionReport {
pub frames: usize,
pub input_channels: usize,
pub output_channels: usize,
pub peak_before_quantization: f64,
pub clipped_samples: usize,
pub quantization_error_rms: f64,
pub dither: DitherPolicy,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Pcm16Conversion {
pub samples: Vec<i16>,
pub report: PcmConversionReport,
}
pub fn convert_f32_to_pcm16(
input: &[f32],
matrix: &ChannelMatrix,
policy: QuantizationPolicy,
) -> Result<Pcm16Conversion, PcmConversionError> {
validate_policy(policy)?;
if !input.len().is_multiple_of(matrix.input_channels) {
return Err(PcmConversionError::MisalignedInput);
}
let frames = input.len() / matrix.input_channels;
if frames > policy.max_frames {
return Err(PcmConversionError::FrameLimit {
supplied: frames,
maximum: policy.max_frames,
});
}
let output_len = frames
.checked_mul(matrix.output_channels)
.ok_or(PcmConversionError::SizeOverflow)?;
let mut samples = Vec::with_capacity(output_len);
let mut errors = vec![0.0f64; matrix.output_channels];
let mut random = Random64::new(dither_seed(policy.dither));
let mut peak = 0.0f64;
let mut clipped = 0usize;
let mut error_energy = 0.0f64;
for (frame_index, frame) in input.chunks(matrix.input_channels).enumerate() {
for (channel, sample) in frame.iter().copied().enumerate() {
if !sample.is_finite() {
return Err(PcmConversionError::NonFiniteSample {
index: frame_index * matrix.input_channels + channel,
});
}
}
for (output_channel, shaped_error) in errors.iter_mut().enumerate() {
let mapped = matrix.map(frame, output_channel);
peak = peak.max(mapped.abs());
clipped += usize::from(!(-1.0..=1.0).contains(&mapped));
let feedback = dither_feedback(policy.dither);
let shaped = mapped + *shaped_error * feedback;
let dither = dither_lsb(policy.dither, &mut random);
let code = (shaped * 32_768.0 + dither)
.round()
.clamp(f64::from(i16::MIN), f64::from(i16::MAX)) as i16;
let reconstructed = f64::from(code) / 32_768.0;
*shaped_error = shaped - reconstructed;
let error = reconstructed - mapped;
error_energy += error * error;
samples.push(code);
}
}
let quantization_error_rms = if output_len == 0 {
0.0
} else {
(error_energy / output_len as f64).sqrt()
};
Ok(Pcm16Conversion {
samples,
report: PcmConversionReport {
frames,
input_channels: matrix.input_channels,
output_channels: matrix.output_channels,
peak_before_quantization: peak,
clipped_samples: clipped,
quantization_error_rms,
dither: policy.dither,
},
})
}
fn validate_policy(policy: QuantizationPolicy) -> Result<(), PcmConversionError> {
if policy.max_frames == 0 {
return Err(PcmConversionError::FrameLimit {
supplied: 0,
maximum: 0,
});
}
if let DitherPolicy::NoiseShapedTpdf { feedback, .. } = policy.dither
&& (!feedback.is_finite() || !(0.0..=0.95).contains(&feedback))
{
return Err(PcmConversionError::InvalidDither);
}
Ok(())
}
fn dither_seed(policy: DitherPolicy) -> u64 {
match policy {
DitherPolicy::None => 0,
DitherPolicy::Tpdf { seed } | DitherPolicy::NoiseShapedTpdf { seed, .. } => seed,
}
}
fn dither_feedback(policy: DitherPolicy) -> f64 {
match policy {
DitherPolicy::NoiseShapedTpdf { feedback, .. } => f64::from(feedback),
DitherPolicy::None | DitherPolicy::Tpdf { .. } => 0.0,
}
}
fn dither_lsb(policy: DitherPolicy, random: &mut Random64) -> f64 {
match policy {
DitherPolicy::None => 0.0,
DitherPolicy::Tpdf { .. } | DitherPolicy::NoiseShapedTpdf { .. } => {
random.unit() - random.unit()
}
}
}
struct Random64 {
state: u64,
}
impl Random64 {
fn new(seed: u64) -> Self {
Self {
state: if seed == 0 {
0x9e37_79b9_7f4a_7c15
} else {
seed
},
}
}
fn unit(&mut self) -> f64 {
let mut value = self.state;
value ^= value << 13;
value ^= value >> 7;
value ^= value << 17;
self.state = value;
(value >> 11) as f64 / (1u64 << 53) as f64
}
}