use crate::{
error::{AudioIOError, AudioIOResult},
wav::fmt::FmtChunk,
};
const MS_ADAPT: [i32; 16] = [
230, 230, 230, 230, 307, 409, 512, 614, 768, 614, 512, 409, 307, 230, 230, 230,
];
const IMA_INDEX: [i32; 16] = [-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8];
const IMA_STEP: [i32; 89] = [
7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107,
118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963,
1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894,
6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
32767,
];
#[inline]
fn clamp_i16(v: i32) -> i16 {
v.clamp(i16::MIN as i32, i16::MAX as i32) as i16
}
fn samples_per_block(fmt: &FmtChunk<'_>) -> Option<usize> {
let b = fmt.as_bytes();
if b.len() >= 20 {
Some(u16::from_le_bytes([b[18], b[19]]) as usize)
} else {
None
}
}
pub fn decoded_sample_count(fmt: &FmtChunk<'_>, data_len: usize) -> usize {
let channels = fmt.channels() as usize;
let block_align = fmt.block_align() as usize;
let spb = samples_per_block(fmt).unwrap_or(0);
if channels == 0 || block_align == 0 {
return 0;
}
(data_len / block_align) * spb * channels
}
pub fn decode(fmt: &FmtChunk<'_>, data: &[u8]) -> AudioIOResult<Vec<i16>> {
use crate::wav::FormatCode;
match fmt.format_code() {
FormatCode::MsAdpcm => decode_ms(fmt, data),
FormatCode::ImaAdpcm => decode_ima(fmt, data),
other => Err(AudioIOError::unsupported_format(format!(
"not an ADPCM format: {other}"
))),
}
}
fn decode_ms(fmt: &FmtChunk<'_>, data: &[u8]) -> AudioIOResult<Vec<i16>> {
let channels = fmt.channels() as usize;
let block_align = fmt.block_align() as usize;
let spb = samples_per_block(fmt)
.ok_or_else(|| AudioIOError::unsupported_format("MS ADPCM fmt chunk missing wSamplesPerBlock"))?;
let ext = fmt.as_bytes();
if ext.len() < 22 {
return Err(AudioIOError::unsupported_format(
"MS ADPCM fmt chunk missing coefficient table",
));
}
let num_coef = u16::from_le_bytes([ext[20], ext[21]]) as usize;
if ext.len() < 22 + num_coef * 4 {
return Err(AudioIOError::unsupported_format("MS ADPCM coefficient table truncated"));
}
let coefs: Vec<(i32, i32)> = (0..num_coef)
.map(|i| {
let o = 22 + i * 4;
(
i16::from_le_bytes([ext[o], ext[o + 1]]) as i32,
i16::from_le_bytes([ext[o + 2], ext[o + 3]]) as i32,
)
})
.collect();
if coefs.is_empty() || block_align == 0 || channels == 0 {
return Err(AudioIOError::unsupported_format("invalid MS ADPCM parameters"));
}
let header_len = 7 * channels; let mut out = Vec::with_capacity(decoded_sample_count(fmt, data.len()));
for block in data.chunks_exact(block_align) {
if block.len() < header_len {
break;
}
let mut predictor = vec![0usize; channels];
let mut delta = vec![0i32; channels];
let mut samp1 = vec![0i32; channels];
let mut samp2 = vec![0i32; channels];
let mut pos = 0;
for p in predictor.iter_mut() {
*p = (block[pos] as usize).min(coefs.len() - 1);
pos += 1;
}
for d in delta.iter_mut() {
*d = i16::from_le_bytes([block[pos], block[pos + 1]]) as i32;
pos += 2;
}
for s in samp1.iter_mut() {
*s = i16::from_le_bytes([block[pos], block[pos + 1]]) as i32;
pos += 2;
}
for s in samp2.iter_mut() {
*s = i16::from_le_bytes([block[pos], block[pos + 1]]) as i32;
pos += 2;
}
out.extend(samp2.iter().map(|&s| clamp_i16(s)));
out.extend(samp1.iter().map(|&s| clamp_i16(s)));
let nibbles_total = spb.saturating_sub(2) * channels;
let mut count = 0usize;
'block: for &byte in &block[pos..] {
for nib in [byte >> 4, byte & 0x0F] {
if count >= nibbles_total {
break 'block;
}
let ch = count % channels;
let (c1, c2) = coefs[predictor[ch]];
let signed = if nib >= 8 { nib as i32 - 16 } else { nib as i32 };
let predict = (samp1[ch] * c1 + samp2[ch] * c2) / 256;
let new = clamp_i16(predict + signed * delta[ch]) as i32;
out.push(new as i16);
let mut new_delta = (MS_ADAPT[nib as usize] * delta[ch]) / 256;
if new_delta < 16 {
new_delta = 16;
}
delta[ch] = new_delta;
samp2[ch] = samp1[ch];
samp1[ch] = new;
count += 1;
}
}
}
Ok(out)
}
fn decode_ima(fmt: &FmtChunk<'_>, data: &[u8]) -> AudioIOResult<Vec<i16>> {
let channels = fmt.channels() as usize;
let block_align = fmt.block_align() as usize;
let spb = samples_per_block(fmt)
.ok_or_else(|| AudioIOError::unsupported_format("IMA ADPCM fmt chunk missing wSamplesPerBlock"))?;
if channels == 0 || block_align == 0 {
return Err(AudioIOError::unsupported_format("invalid IMA ADPCM parameters"));
}
let header_len = 4 * channels; let mut out = Vec::with_capacity(decoded_sample_count(fmt, data.len()));
for block in data.chunks_exact(block_align) {
if block.len() < header_len {
break;
}
let mut predictor = vec![0i32; channels];
let mut index = vec![0i32; channels];
let mut pos = 0;
for ch in 0..channels {
predictor[ch] = i16::from_le_bytes([block[pos], block[pos + 1]]) as i32;
index[ch] = (block[pos + 2] as i32).clamp(0, 88);
pos += 4;
}
let mut chan: Vec<Vec<i16>> = (0..channels)
.map(|ch| {
let mut v = Vec::with_capacity(spb);
v.push(clamp_i16(predictor[ch])); v
})
.collect();
let words = &block[header_len..];
let mut word_off = 0;
let mut ch = 0;
while word_off + 4 <= words.len() {
for &byte in &words[word_off..word_off + 4] {
for nib in [byte & 0x0F, byte >> 4] {
let step = IMA_STEP[index[ch] as usize];
let mut diff = step >> 3;
if nib & 4 != 0 {
diff += step;
}
if nib & 2 != 0 {
diff += step >> 1;
}
if nib & 1 != 0 {
diff += step >> 2;
}
if nib & 8 != 0 {
predictor[ch] -= diff;
} else {
predictor[ch] += diff;
}
predictor[ch] = predictor[ch].clamp(i16::MIN as i32, i16::MAX as i32);
index[ch] = (index[ch] + IMA_INDEX[nib as usize]).clamp(0, 88);
chan[ch].push(predictor[ch] as i16);
}
}
word_off += 4;
ch = (ch + 1) % channels;
}
let frames = chan.iter().map(|c| c.len()).min().unwrap_or(0).min(spb);
for i in 0..frames {
for c in chan.iter() {
out.push(c[i]);
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn ima_fmt(channels: u16, block_align: u16, samples_per_block: u16) -> Vec<u8> {
let mut v = vec![0u8; 20];
v[0..2].copy_from_slice(&0x0011u16.to_le_bytes());
v[2..4].copy_from_slice(&channels.to_le_bytes());
v[4..8].copy_from_slice(&8000u32.to_le_bytes());
v[12..14].copy_from_slice(&block_align.to_le_bytes());
v[14..16].copy_from_slice(&4u16.to_le_bytes()); v[16..18].copy_from_slice(&2u16.to_le_bytes()); v[18..20].copy_from_slice(&samples_per_block.to_le_bytes());
v
}
fn ms_fmt(channels: u16, block_align: u16, samples_per_block: u16, coef: (i16, i16)) -> Vec<u8> {
let mut v = vec![0u8; 26];
v[0..2].copy_from_slice(&0x0002u16.to_le_bytes());
v[2..4].copy_from_slice(&channels.to_le_bytes());
v[4..8].copy_from_slice(&8000u32.to_le_bytes());
v[12..14].copy_from_slice(&block_align.to_le_bytes());
v[14..16].copy_from_slice(&4u16.to_le_bytes());
v[16..18].copy_from_slice(&32u16.to_le_bytes()); v[18..20].copy_from_slice(&samples_per_block.to_le_bytes());
v[20..22].copy_from_slice(&1u16.to_le_bytes()); v[22..24].copy_from_slice(&coef.0.to_le_bytes());
v[24..26].copy_from_slice(&coef.1.to_le_bytes());
v
}
#[test]
fn ima_mono_block_decodes_hand_traced_values() {
let fmt_bytes = ima_fmt(1, 8, 9);
let fmt = FmtChunk::from_bytes(&fmt_bytes).expect("valid test fmt chunk");
let block = [0x64u8, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00];
let decoded = decode(&fmt, &block).expect("decode test block");
assert_eq!(decoded, vec![100, 107, 108, 109, 109, 109, 109, 109, 109]);
assert_eq!(decoded.len(), decoded_sample_count(&fmt, block.len()));
}
#[test]
fn ms_mono_block_decodes_hand_traced_values() {
let fmt_bytes = ms_fmt(1, 8, 4, (256, 0));
let fmt = FmtChunk::from_bytes(&fmt_bytes).expect("valid test fmt chunk");
let block = [0x00u8, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10];
let decoded = decode(&fmt, &block).expect("decode test block");
assert_eq!(decoded, vec![0, 0, 16, 16]);
assert_eq!(decoded.len(), decoded_sample_count(&fmt, block.len()));
}
#[test]
fn non_adpcm_format_is_rejected() {
let mut fmt_bytes = vec![0u8; 16];
fmt_bytes[0..2].copy_from_slice(&0x0001u16.to_le_bytes()); let fmt = FmtChunk::from_bytes(&fmt_bytes).expect("valid test fmt chunk");
assert!(decode(&fmt, &[0u8; 8]).is_err());
}
}