use super::PcmFrame;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum G711Mapping {
Mulaw,
Alaw,
}
pub const G711_SAMPLE_RATE: u32 = 8_000;
impl PcmFrame {
pub fn to_f32(&self) -> Vec<f32> {
self.samples
.iter()
.map(|&s| f32::from(s) / 32_768.0)
.collect()
}
pub fn from_f32(samples: &[f32], sample_rate: u32, channels: u16) -> Self {
let s16 = samples
.iter()
.map(|&v| (v.clamp(-1.0, 1.0) * 32_767.0) as i16)
.collect();
Self::new(s16, sample_rate, channels)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.samples.len() * 2);
for &s in &self.samples {
out.extend_from_slice(&s.to_le_bytes());
}
out
}
pub fn from_bytes(bytes: &[u8], sample_rate: u32, channels: u16) -> Self {
let samples = bytes
.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]))
.collect();
Self::new(samples, sample_rate, channels)
}
pub fn to_wav_bytes(&self) -> Vec<u8> {
const HEADER_LEN: u32 = 36;
const PCM_FORMAT: u16 = 1;
const BITS_PER_SAMPLE: u16 = 16;
let channels = self.channels.max(1);
let block_align = channels * BITS_PER_SAMPLE / 8;
let byte_rate = self.sample_rate * u32::from(block_align);
let data = self.to_bytes();
let data_len = u32::try_from(data.len()).unwrap_or(u32::MAX);
let mut out = Vec::with_capacity(44 + data.len());
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&HEADER_LEN.saturating_add(data_len).to_le_bytes());
out.extend_from_slice(b"WAVE");
out.extend_from_slice(b"fmt ");
out.extend_from_slice(&16u32.to_le_bytes()); out.extend_from_slice(&PCM_FORMAT.to_le_bytes());
out.extend_from_slice(&channels.to_le_bytes());
out.extend_from_slice(&self.sample_rate.to_le_bytes());
out.extend_from_slice(&byte_rate.to_le_bytes());
out.extend_from_slice(&block_align.to_le_bytes());
out.extend_from_slice(&BITS_PER_SAMPLE.to_le_bytes());
out.extend_from_slice(b"data");
out.extend_from_slice(&data_len.to_le_bytes());
out.extend_from_slice(&data);
out
}
pub fn from_g711(bytes: &[u8], sample_rate: u32, channels: u16, mapping: G711Mapping) -> Self {
let decode = match mapping {
G711Mapping::Mulaw => mulaw_to_linear,
G711Mapping::Alaw => alaw_to_linear,
};
Self::new(
bytes.iter().copied().map(decode).collect(),
sample_rate,
channels,
)
}
pub fn to_g711(&self, mapping: G711Mapping) -> Vec<u8> {
let encode = match mapping {
G711Mapping::Mulaw => linear_to_mulaw,
G711Mapping::Alaw => linear_to_alaw,
};
self.samples.iter().copied().map(encode).collect()
}
}
const MULAW_BIAS: i32 = 0x84;
const MULAW_MASK: u8 = 0xFF;
const ALAW_MASK: u8 = 0xD5;
const fn mulaw_to_linear(byte: u8) -> i16 {
let val = !byte;
let t = (((val & 0x0F) as i32) << 3) + MULAW_BIAS;
let t = t << (((val & 0x70) >> 4) as u32);
if val & 0x80 != 0 {
(MULAW_BIAS - t) as i16
} else {
(t - MULAW_BIAS) as i16
}
}
const fn alaw_to_linear(byte: u8) -> i16 {
let val = byte ^ 0x55;
let t = (val & 0x0F) as i32;
let seg = ((val & 0x70) >> 4) as u32;
let t = if seg > 0 {
(t + t + 1 + 32) << (seg + 2)
} else {
(t + t + 1) << 3
};
if val & 0x80 != 0 { t as i16 } else { -t as i16 }
}
const MULAW_COMPRESS: [u8; 16384] = build_compress_table(MULAW_MASK, Law::Mulaw);
const ALAW_COMPRESS: [u8; 16384] = build_compress_table(ALAW_MASK, Law::Alaw);
#[derive(Clone, Copy, PartialEq, Eq)]
enum Law {
Mulaw,
Alaw,
}
const fn expand(byte: u8, law: Law) -> i32 {
match law {
Law::Mulaw => mulaw_to_linear(byte) as i32,
Law::Alaw => alaw_to_linear(byte) as i32,
}
}
const fn build_compress_table(mask: u8, law: Law) -> [u8; 16384] {
let mut table = [0u8; 16384];
table[8192] = mask;
let mut j: i32 = 1;
let mut i: i32 = 0;
while i < 127 {
let v1 = expand((i as u8) ^ mask, law);
let v2 = expand(((i + 1) as u8) ^ mask, law);
let boundary = (v1 + v2 + 4) >> 3;
while j < boundary {
table[(8192 - j) as usize] = (i as u8) ^ (mask ^ 0x80);
table[(8192 + j) as usize] = (i as u8) ^ mask;
j += 1;
}
i += 1;
}
while j < 8192 {
table[(8192 - j) as usize] = 127u8 ^ (mask ^ 0x80);
table[(8192 + j) as usize] = 127u8 ^ mask;
j += 1;
}
table[0] = table[1];
table
}
fn linear_to_mulaw(pcm: i16) -> u8 {
MULAW_COMPRESS[((i32::from(pcm) + 32_768) >> 2) as usize]
}
fn linear_to_alaw(pcm: i16) -> u8 {
ALAW_COMPRESS[((i32::from(pcm) + 32_768) >> 2) as usize]
}
#[cfg(test)]
mod tests {
use super::*;
const MULAW_NEGATIVE_ZERO: u8 = 0x7F;
#[test]
fn f32_round_trip_preserves_samples_within_one_step() {
let frame = PcmFrame::mono(vec![0, 1, -1, 1000, -1000, i16::MAX, i16::MIN], 48_000);
let back = PcmFrame::from_f32(&frame.to_f32(), 48_000, 1);
for (a, b) in frame.samples.iter().zip(&back.samples) {
assert!((a - b).abs() <= 1, "{a} != {b}");
}
}
#[test]
fn f32_conversion_uses_the_documented_asymmetric_scaling() {
assert_eq!(
PcmFrame::mono(vec![32_767], 8_000).to_f32()[0],
32_767.0 / 32_768.0
);
assert_eq!(PcmFrame::from_f32(&[1.0], 8_000, 1).samples[0], i16::MAX);
assert_eq!(PcmFrame::from_f32(&[-1.0], 8_000, 1).samples[0], -32_767);
}
#[test]
fn from_f32_clamps_out_of_range_input() {
let f = PcmFrame::from_f32(&[9.0, -9.0], 8_000, 1);
assert_eq!(f.samples, vec![i16::MAX, -32_767]);
}
#[test]
fn bytes_round_trip_little_endian() {
let frame = PcmFrame::new(vec![1, -2, 300, -400], 48_000, 2);
let bytes = frame.to_bytes();
assert_eq!(&bytes[..2], &1i16.to_le_bytes());
let back = PcmFrame::from_bytes(&bytes, 48_000, 2);
assert_eq!(back, frame);
}
#[test]
fn from_bytes_drops_a_trailing_partial_sample() {
let f = PcmFrame::from_bytes(&[0x01, 0x00, 0x7F], 8_000, 1);
assert_eq!(f.samples, vec![1]);
}
#[test]
fn wav_header_describes_the_payload() {
let frame = PcmFrame::new(vec![0; 200], 16_000, 2);
let wav = frame.to_wav_bytes();
assert_eq!(&wav[0..4], b"RIFF");
assert_eq!(&wav[8..12], b"WAVE");
assert_eq!(&wav[12..16], b"fmt ");
assert_eq!(&wav[36..40], b"data");
assert_eq!(wav.len(), 44 + 400);
let field = |at: usize| u32::from_le_bytes(wav[at..at + 4].try_into().unwrap());
let field16 = |at: usize| u16::from_le_bytes(wav[at..at + 2].try_into().unwrap());
assert_eq!(field(4) as usize, wav.len() - 8); assert_eq!(field16(20), 1); assert_eq!(field16(22), 2); assert_eq!(field(24), 16_000); assert_eq!(field(28), 16_000 * 4); assert_eq!(field16(32), 4); assert_eq!(field16(34), 16); assert_eq!(field(40) as usize, 400); }
#[test]
fn g711_decode_encode_is_stable_for_every_byte() {
for byte in 0..=u8::MAX {
if byte != MULAW_NEGATIVE_ZERO {
assert_eq!(
linear_to_mulaw(mulaw_to_linear(byte)),
byte,
"mulaw byte {byte:#04x}"
);
}
assert_eq!(
linear_to_alaw(alaw_to_linear(byte)),
byte,
"alaw byte {byte:#04x}"
);
}
}
#[test]
fn mulaw_has_a_negative_zero_that_encodes_to_positive_zero() {
assert_eq!(mulaw_to_linear(MULAW_NEGATIVE_ZERO), 0);
assert_eq!(mulaw_to_linear(0xFF), 0);
assert_eq!(linear_to_mulaw(0), 0xFF);
assert_ne!(alaw_to_linear(0xD5), alaw_to_linear(0x55));
}
#[test]
fn g711_round_trip_stays_within_companding_error() {
for law in [G711Mapping::Mulaw, G711Mapping::Alaw] {
let input: Vec<i16> = (-32_000..32_000).step_by(97).collect();
let frame = PcmFrame::mono(input.clone(), G711_SAMPLE_RATE);
let back = PcmFrame::from_g711(&frame.to_g711(law), G711_SAMPLE_RATE, 1, law);
assert_eq!(back.samples.len(), input.len());
for (a, b) in input.iter().zip(&back.samples) {
let err = (i32::from(*a) - i32::from(*b)).abs();
let tolerance = (i32::from(a.abs()) / 16).max(256);
assert!(err <= tolerance, "{law:?}: {a} -> {b} (err {err})");
}
}
}
#[test]
fn g711_anchors_match_the_itu_tables() {
assert_eq!(mulaw_to_linear(0xFF), 0);
assert_eq!(mulaw_to_linear(0x7F), 0);
assert_eq!(mulaw_to_linear(0x00), -32_124);
assert_eq!(mulaw_to_linear(0x80), 32_124);
assert_eq!(alaw_to_linear(0xD5), 8);
assert_eq!(alaw_to_linear(0x55), -8);
assert_eq!(alaw_to_linear(0x2A), -32_256);
assert_eq!(alaw_to_linear(0xAA), 32_256);
}
#[test]
fn g711_encodes_one_byte_per_sample() {
let frame = PcmFrame::new(vec![0; 320], G711_SAMPLE_RATE, 2);
assert_eq!(frame.to_g711(G711Mapping::Mulaw).len(), 320);
assert_eq!(frame.to_g711(G711Mapping::Alaw).len(), 320);
}
#[test]
fn silence_companded_and_expanded_stays_quiet() {
let quiet = PcmFrame::mono(vec![0; 160], G711_SAMPLE_RATE);
for law in [G711Mapping::Mulaw, G711Mapping::Alaw] {
let back = PcmFrame::from_g711(&quiet.to_g711(law), G711_SAMPLE_RATE, 1, law);
assert!(back.rms() < 0.001, "{law:?} introduced noise into silence");
}
}
}