use super::bits::{BitReader, BitWriter};
use super::mode::{AmrFrameType, AmrVariant};
use crate::error::{CodecError, Result};
const CMR_NO_REQUEST: u8 = 15;
const MAX_FRAMES_PER_PACKET: usize = 32;
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AmrPayloadConfig {
pub variant: AmrVariant,
pub octet_aligned: bool,
pub crc: bool,
pub robust_sorting: bool,
pub interleaving: bool,
}
impl AmrPayloadConfig {
#[must_use]
pub const fn bandwidth_efficient(variant: AmrVariant) -> Self {
Self {
variant,
octet_aligned: false,
crc: false,
robust_sorting: false,
interleaving: false,
}
}
#[must_use]
pub const fn octet_aligned(variant: AmrVariant) -> Self {
Self {
octet_aligned: true,
..Self::bandwidth_efficient(variant)
}
}
#[must_use]
pub const fn with_crc(mut self) -> Self {
self.crc = true;
self.octet_aligned = true;
self
}
#[must_use]
pub const fn with_robust_sorting(mut self) -> Self {
self.robust_sorting = true;
self.octet_aligned = true;
self
}
#[must_use]
pub const fn with_interleaving(mut self) -> Self {
self.interleaving = true;
self.octet_aligned = true;
self
}
const fn needs_octet_align(self) -> bool {
self.crc || self.robust_sorting || self.interleaving
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AmrPayloadFrame {
pub frame_type: AmrFrameType,
pub quality_ok: bool,
pub data: Vec<u8>,
}
impl AmrPayloadFrame {
pub fn new(frame_type: AmrFrameType, quality_ok: bool, data: Vec<u8>) -> Result<Self> {
let expected = frame_type.octet_aligned_bytes();
if data.len() != expected {
return Err(CodecError::InvalidFrameSize {
expected,
actual: data.len(),
});
}
Ok(Self {
frame_type,
quality_ok,
data,
})
}
#[must_use]
pub const fn no_data() -> Self {
Self {
frame_type: AmrFrameType::NoData,
quality_ok: true,
data: Vec::new(),
}
}
#[must_use]
pub const fn speech_lost() -> Self {
Self {
frame_type: AmrFrameType::SpeechLost,
quality_ok: false,
data: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AmrInterleaving {
pub ill: u8,
pub ilp: u8,
}
impl AmrInterleaving {
pub fn new(ill: u8, ilp: u8) -> Result<Self> {
if ill > 0x0F || ilp > 0x0F {
return Err(CodecError::invalid_format(
"AMR ILL and ILP are 4-bit fields",
));
}
if ilp > ill {
return Err(CodecError::invalid_format(format!(
"AMR interleaving index {ilp} is outside its group (ILL={ill})"
)));
}
Ok(Self { ill, ilp })
}
#[must_use]
pub const fn group_len(self) -> u8 {
self.ill.saturating_add(1)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AmrPacket {
pub cmr: Option<u8>,
pub interleaving: Option<AmrInterleaving>,
pub frames: Vec<AmrPayloadFrame>,
}
impl AmrPacket {
#[must_use]
pub fn single(frame: AmrPayloadFrame) -> Self {
Self {
cmr: None,
interleaving: None,
frames: vec![frame],
}
}
#[must_use]
pub const fn with_interleaving(mut self, interleaving: Option<AmrInterleaving>) -> Self {
self.interleaving = interleaving;
self
}
#[must_use]
pub const fn with_cmr(mut self, cmr: Option<u8>) -> Self {
self.cmr = cmr;
self
}
#[must_use]
pub const fn frame_count(&self) -> usize {
self.frames.len()
}
}
const CRC_FEEDBACK: u8 = 0b1011_1000;
fn frame_crc(data: &[u8], class_a_bits: usize) -> u8 {
let mut crc = 0u8;
for index in 0..class_a_bits {
let bit = data[index / 8] >> (7 - index % 8) & 1;
let feedback = (crc & 1) ^ bit;
crc >>= 1;
if feedback == 1 {
crc ^= CRC_FEEDBACK;
}
}
crc
}
const fn crc_class_a_bits(frame_type: AmrFrameType) -> Option<usize> {
match frame_type {
AmrFrameType::Speech(mode) => Some(mode.class_a_bits()),
AmrFrameType::Sid(variant) => Some(variant.sid_bits()),
AmrFrameType::NoData | AmrFrameType::SpeechLost => None,
}
}
#[derive(Debug, Clone, Copy)]
pub struct AmrPayloadCodec {
config: AmrPayloadConfig,
}
impl AmrPayloadCodec {
pub fn new(config: AmrPayloadConfig) -> Result<Self> {
if config.needs_octet_align() && !config.octet_aligned {
return Err(CodecError::invalid_config(
"AMR crc, robust-sorting and interleaving all require octet-align=1",
));
}
Ok(Self { config })
}
#[must_use]
pub const fn config(&self) -> AmrPayloadConfig {
self.config
}
pub fn pack(&self, packet: &AmrPacket) -> Result<Vec<u8>> {
if packet.frames.is_empty() {
return Err(CodecError::invalid_format(
"an AMR payload must carry at least one frame",
));
}
if packet.frames.len() > MAX_FRAMES_PER_PACKET {
return Err(CodecError::invalid_format(format!(
"{} frames exceeds the {MAX_FRAMES_PER_PACKET}-frame limit",
packet.frames.len()
)));
}
let mut writer = BitWriter::new();
writer.write_bits(u32::from(packet.cmr.unwrap_or(CMR_NO_REQUEST)), 4);
if self.config.octet_aligned {
writer.write_bits(0, 4);
}
if self.config.interleaving {
let interleaving = packet.interleaving.ok_or_else(|| {
CodecError::invalid_format(
"interleaving is negotiated but the packet carries no ILL/ILP",
)
})?;
writer.write_bits(u32::from(interleaving.ill), 4);
writer.write_bits(u32::from(interleaving.ilp), 4);
}
for (index, frame) in packet.frames.iter().enumerate() {
self.validate_frame(frame)?;
let last = index + 1 == packet.frames.len();
writer.write_bit(!last);
writer.write_bits(u32::from(frame.frame_type.frame_type_index()), 4);
writer.write_bit(frame.quality_ok);
if self.config.octet_aligned {
writer.write_bits(0, 2);
}
}
if self.config.crc {
for frame in &packet.frames {
if let Some(class_a) = crc_class_a_bits(frame.frame_type) {
writer.write_bits(u32::from(frame_crc(&frame.data, class_a)), 8);
}
}
}
if self.config.robust_sorting {
Self::write_robust_sorted(&mut writer, &packet.frames);
} else {
for frame in &packet.frames {
let bits = frame.frame_type.bits();
if bits == 0 {
continue;
}
writer.write_slice_bits(&frame.data, bits)?;
if self.config.octet_aligned {
writer.align_to_octet();
}
}
}
Ok(writer.finish())
}
pub fn unpack(&self, payload: &[u8]) -> Result<AmrPacket> {
if payload.is_empty() {
return Err(CodecError::InvalidPayload {
details: "empty AMR payload".to_string(),
});
}
let mut reader = BitReader::new(payload);
let cmr_raw = u8::try_from(reader.read_bits(4)?).unwrap_or(CMR_NO_REQUEST);
if self.config.octet_aligned {
reader.read_bits(4)?; }
let interleaving = if self.config.interleaving {
let ill = u8::try_from(reader.read_bits(4)?).unwrap_or(0);
let ilp = u8::try_from(reader.read_bits(4)?).unwrap_or(0);
Some(AmrInterleaving {
ill,
ilp: ilp.min(ill),
})
} else {
None
};
let cmr = (cmr_raw < self.config.variant.speech_mode_count()).then_some(cmr_raw);
let descriptors = self.read_table_of_contents(&mut reader)?;
let mut crcs = Vec::new();
if self.config.crc {
for (frame_type, _) in &descriptors {
if crc_class_a_bits(*frame_type).is_some() {
crcs.push(u8::try_from(reader.read_bits(8)?).unwrap_or(0));
}
}
}
let mut frames = if self.config.robust_sorting {
Self::read_robust_sorted(&mut reader, &descriptors)?
} else {
let mut frames = Vec::with_capacity(descriptors.len());
for (frame_type, quality_ok) in &descriptors {
let bits = frame_type.bits();
let data = if bits == 0 {
Vec::new()
} else {
let data = reader.read_slice_bits(bits)?;
if self.config.octet_aligned {
reader.align_to_octet();
}
data
};
frames.push(AmrPayloadFrame {
frame_type: *frame_type,
quality_ok: *quality_ok,
data,
});
}
frames
};
if self.config.crc {
let mut crc_index = 0;
for frame in &mut frames {
let Some(class_a) = crc_class_a_bits(frame.frame_type) else {
continue;
};
let expected = crcs.get(crc_index).copied().unwrap_or(0);
crc_index += 1;
if frame_crc(&frame.data, class_a) != expected {
frame.quality_ok = false;
}
}
}
if reader.remaining_bits() >= 8 {
return Err(CodecError::InvalidPayload {
details: format!(
"{} trailing bits after the AMR table of contents was satisfied; \
frame sizes disagree",
reader.remaining_bits()
),
});
}
Ok(AmrPacket {
cmr,
interleaving,
frames,
})
}
fn read_table_of_contents(
self,
reader: &mut BitReader<'_>,
) -> Result<Vec<(AmrFrameType, bool)>> {
let mut descriptors = Vec::new();
loop {
let more = reader.read_bits(1)? == 1;
let ft_index = u8::try_from(reader.read_bits(4)?).unwrap_or(CMR_NO_REQUEST);
let quality_ok = reader.read_bits(1)? == 1;
if self.config.octet_aligned {
reader.read_bits(2)?; }
descriptors.push((
AmrFrameType::from_index(self.config.variant, ft_index)?,
quality_ok,
));
if !more {
return Ok(descriptors);
}
if descriptors.len() >= MAX_FRAMES_PER_PACKET {
return Err(CodecError::InvalidPayload {
details: format!(
"AMR table of contents exceeds {MAX_FRAMES_PER_PACKET} frames; \
payload is corrupt"
),
});
}
}
}
fn write_robust_sorted(writer: &mut BitWriter, frames: &[AmrPayloadFrame]) {
let longest = frames.iter().map(|f| f.data.len()).max().unwrap_or(0);
for round in 0..longest {
for frame in frames {
if let Some(&octet) = frame.data.get(round) {
writer.write_bits(u32::from(octet), 8);
}
}
}
}
fn read_robust_sorted(
reader: &mut BitReader<'_>,
descriptors: &[(AmrFrameType, bool)],
) -> Result<Vec<AmrPayloadFrame>> {
let lengths: Vec<usize> = descriptors
.iter()
.map(|(frame_type, _)| frame_type.octet_aligned_bytes())
.collect();
let mut buffers: Vec<Vec<u8>> = lengths.iter().map(|&len| vec![0u8; len]).collect();
let longest = lengths.iter().copied().max().unwrap_or(0);
for round in 0..longest {
for buffer in &mut buffers {
if let Some(slot) = buffer.get_mut(round) {
*slot = u8::try_from(reader.read_bits(8)?).unwrap_or(0);
}
}
}
Ok(descriptors
.iter()
.zip(buffers)
.map(|(&(frame_type, quality_ok), data)| AmrPayloadFrame {
frame_type,
quality_ok,
data,
})
.collect())
}
fn validate_frame(self, frame: &AmrPayloadFrame) -> Result<()> {
let frame_variant = match frame.frame_type {
AmrFrameType::Speech(mode) => Some(mode.variant()),
AmrFrameType::Sid(variant) => Some(variant),
AmrFrameType::NoData => None,
AmrFrameType::SpeechLost => Some(AmrVariant::WideBand),
};
if let Some(frame_variant) = frame_variant {
if frame_variant != self.config.variant {
return Err(CodecError::invalid_format(format!(
"cannot pack a {frame_variant} frame into a {} payload",
self.config.variant
)));
}
}
let expected = frame.frame_type.octet_aligned_bytes();
if frame.data.len() != expected {
return Err(CodecError::InvalidFrameSize {
expected,
actual: frame.data.len(),
});
}
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unusual_byte_groupings)]
mod tests {
use super::*;
use crate::codecs::amr::mode::AmrMode;
fn frame_data(bits: usize, seed: u8) -> Vec<u8> {
let len = bits.div_ceil(8);
let mut data: Vec<u8> = (0..len)
.map(|i| {
u8::try_from(i % 256)
.unwrap_or(0)
.wrapping_mul(31)
.wrapping_add(seed)
| 0x41
})
.collect();
let tail = bits % 8;
if tail != 0 {
let last = len - 1;
data[last] &= 0xFFu8 << (8 - tail);
}
data
}
fn speech(variant: AmrVariant, index: u8, seed: u8) -> AmrPayloadFrame {
let mode = AmrMode::new(variant, index).unwrap();
AmrPayloadFrame::new(
AmrFrameType::Speech(mode),
true,
frame_data(mode.bits(), seed),
)
.unwrap()
}
fn both_configs(variant: AmrVariant) -> [AmrPayloadConfig; 2] {
[
AmrPayloadConfig::bandwidth_efficient(variant),
AmrPayloadConfig::octet_aligned(variant),
]
}
#[test]
fn round_trips_every_mode_in_both_framings() {
for variant in [AmrVariant::NarrowBand, AmrVariant::WideBand] {
for config in both_configs(variant) {
let codec = AmrPayloadCodec::new(config).unwrap();
for mode in AmrMode::all(variant) {
let packet = AmrPacket::single(speech(variant, mode.index(), 7));
let bytes = codec.pack(&packet).unwrap();
let back = codec.unpack(&bytes).unwrap();
assert_eq!(back, packet, "{variant} mode {} {config:?}", mode.index());
}
}
}
}
#[test]
fn round_trips_sid_no_data_and_speech_lost() {
for variant in [AmrVariant::NarrowBand, AmrVariant::WideBand] {
for config in both_configs(variant) {
let codec = AmrPayloadCodec::new(config).unwrap();
let sid = AmrPayloadFrame::new(
AmrFrameType::Sid(variant),
true,
frame_data(variant.sid_bits(), 3),
)
.unwrap();
let packet = AmrPacket::single(sid);
assert_eq!(codec.unpack(&codec.pack(&packet).unwrap()).unwrap(), packet);
let packet = AmrPacket::single(AmrPayloadFrame::no_data());
assert_eq!(codec.unpack(&codec.pack(&packet).unwrap()).unwrap(), packet);
if variant == AmrVariant::WideBand {
let packet = AmrPacket::single(AmrPayloadFrame::speech_lost());
assert_eq!(codec.unpack(&codec.pack(&packet).unwrap()).unwrap(), packet);
}
}
}
}
#[test]
fn round_trips_multi_frame_packets_with_mixed_types() {
for variant in [AmrVariant::NarrowBand, AmrVariant::WideBand] {
for config in both_configs(variant) {
let codec = AmrPayloadCodec::new(config).unwrap();
let top = variant.speech_mode_count() - 1;
let packet = AmrPacket {
cmr: Some(1),
interleaving: None,
frames: vec![
speech(variant, 0, 11),
AmrPayloadFrame::new(
AmrFrameType::Sid(variant),
true,
frame_data(variant.sid_bits(), 12),
)
.unwrap(),
AmrPayloadFrame::no_data(),
speech(variant, top, 13),
],
};
let bytes = codec.pack(&packet).unwrap();
assert_eq!(codec.unpack(&bytes).unwrap(), packet, "{config:?}");
}
}
}
#[test]
fn round_trips_every_frame_count_up_to_the_limit() {
let variant = AmrVariant::WideBand;
for config in both_configs(variant) {
let codec = AmrPayloadCodec::new(config).unwrap();
for count in 1..=MAX_FRAMES_PER_PACKET {
let frames = (0..count)
.map(|i| speech(variant, u8::try_from(i % 9).unwrap(), 5))
.collect();
let packet = AmrPacket {
cmr: None,
interleaving: None,
frames,
};
let bytes = codec.pack(&packet).unwrap();
assert_eq!(codec.unpack(&bytes).unwrap(), packet, "{count} frames");
}
}
}
#[test]
fn cmr_round_trips_and_out_of_range_values_are_ignored() {
let variant = AmrVariant::NarrowBand;
for config in both_configs(variant) {
let codec = AmrPayloadCodec::new(config).unwrap();
for cmr in 0..variant.speech_mode_count() {
let packet = AmrPacket::single(speech(variant, 0, 1)).with_cmr(Some(cmr));
assert_eq!(
codec.unpack(&codec.pack(&packet).unwrap()).unwrap().cmr,
Some(cmr)
);
}
let packet = AmrPacket::single(speech(variant, 0, 1)).with_cmr(None);
assert_eq!(
codec.unpack(&codec.pack(&packet).unwrap()).unwrap().cmr,
None
);
let packet = AmrPacket::single(speech(variant, 0, 1)).with_cmr(Some(9));
let decoded = codec.unpack(&codec.pack(&packet).unwrap()).unwrap();
assert_eq!(decoded.cmr, None);
assert_eq!(decoded.frames, packet.frames, "frames must still decode");
}
}
#[test]
fn octet_aligned_layout_matches_rfc4867() {
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(AmrVariant::WideBand)).unwrap();
let bytes = codec
.pack(&AmrPacket::single(AmrPayloadFrame::no_data()))
.unwrap();
assert_eq!(bytes, vec![0b1111_0000, 0b0_1111_1_00]);
let frame = speech(AmrVariant::WideBand, 0, 2);
let bytes = codec.pack(&AmrPacket::single(frame)).unwrap();
assert_eq!(bytes.len(), 19);
assert_eq!(bytes[0], 0b1111_0000);
assert_eq!(bytes[1], 0b0_0000_1_00);
}
#[test]
fn bandwidth_efficient_layout_matches_rfc4867() {
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::bandwidth_efficient(AmrVariant::WideBand))
.unwrap();
let frame = speech(AmrVariant::WideBand, 0, 2);
let bytes = codec.pack(&AmrPacket::single(frame)).unwrap();
assert_eq!(bytes.len(), 18);
assert_eq!(bytes[0], 0b1111_0000);
assert_eq!(bytes[1] >> 6, 0b01);
let bytes = codec
.pack(&AmrPacket::single(AmrPayloadFrame::no_data()))
.unwrap();
assert_eq!(bytes.len(), 2);
}
#[test]
fn the_two_framings_are_not_interoperable() {
let variant = AmrVariant::WideBand;
let be = AmrPayloadCodec::new(AmrPayloadConfig::bandwidth_efficient(variant)).unwrap();
let oa = AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant)).unwrap();
let packet = AmrPacket::single(speech(variant, 8, 4));
let be_bytes = be.pack(&packet).unwrap();
let oa_bytes = oa.pack(&packet).unwrap();
assert_ne!(be_bytes, oa_bytes);
assert_ne!(oa.unpack(&be_bytes).ok(), Some(packet.clone()));
assert_ne!(be.unpack(&oa_bytes).ok(), Some(packet));
}
#[test]
fn rejects_truncated_payloads_at_every_prefix() {
for variant in [AmrVariant::NarrowBand, AmrVariant::WideBand] {
for config in both_configs(variant) {
let codec = AmrPayloadCodec::new(config).unwrap();
let packet = AmrPacket {
cmr: Some(0),
interleaving: None,
frames: vec![speech(variant, 0, 9), speech(variant, 1, 10)],
};
let bytes = codec.pack(&packet).unwrap();
for cut in 0..bytes.len() {
let result = codec.unpack(&bytes[..cut]);
assert!(
result.is_err(),
"{config:?}: {cut}-byte prefix decoded but should not have"
);
}
assert!(codec.unpack(&bytes).is_ok());
}
}
}
#[test]
fn rejects_reserved_frame_types() {
let nb =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(AmrVariant::NarrowBand)).unwrap();
for ft in 9..=14u8 {
let bytes = vec![0b1111_0000, ft << 3 | 0b100];
assert!(nb.unpack(&bytes).is_err(), "NB FT {ft} should be rejected");
}
let wb =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(AmrVariant::WideBand)).unwrap();
for ft in 10..=13u8 {
let bytes = vec![0b1111_0000, ft << 3 | 0b100];
assert!(wb.unpack(&bytes).is_err(), "WB FT {ft} should be rejected");
}
let bytes = vec![0b1111_0000, 14 << 3 | 0b100];
assert!(wb.unpack(&bytes).is_ok());
}
#[test]
fn rejects_an_unterminated_toc_chain() {
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(AmrVariant::WideBand)).unwrap();
let mut bytes = vec![0b1111_0000];
bytes.extend(std::iter::repeat_n(0b1_1111_1_00u8, 64));
let err = codec.unpack(&bytes).unwrap_err();
assert!(matches!(err, CodecError::InvalidPayload { .. }));
}
#[test]
fn rejects_trailing_data_beyond_the_toc() {
let variant = AmrVariant::WideBand;
for config in both_configs(variant) {
let codec = AmrPayloadCodec::new(config).unwrap();
let mut bytes = codec
.pack(&AmrPacket::single(speech(variant, 0, 6)))
.unwrap();
bytes.push(0x00);
assert!(codec.unpack(&bytes).is_err(), "{config:?}");
}
}
#[test]
fn rejects_empty_payloads_and_empty_packets() {
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(AmrVariant::WideBand)).unwrap();
assert!(codec.unpack(&[]).is_err());
assert!(codec
.pack(&AmrPacket {
cmr: None,
interleaving: None,
frames: vec![]
})
.is_err());
}
#[test]
fn rejects_too_many_frames_on_pack() {
let variant = AmrVariant::NarrowBand;
let codec = AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant)).unwrap();
let frames = (0..=MAX_FRAMES_PER_PACKET)
.map(|_| AmrPayloadFrame::no_data())
.collect();
assert!(codec
.pack(&AmrPacket {
cmr: None,
interleaving: None,
frames
})
.is_err());
}
#[test]
fn rejects_cross_variant_frames_on_pack() {
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(AmrVariant::NarrowBand)).unwrap();
let wb_frame = speech(AmrVariant::WideBand, 2, 1);
assert!(codec.pack(&AmrPacket::single(wb_frame)).is_err());
let wb_sid = AmrPayloadFrame::new(
AmrFrameType::Sid(AmrVariant::WideBand),
true,
frame_data(AmrVariant::WideBand.sid_bits(), 1),
)
.unwrap();
assert!(codec.pack(&AmrPacket::single(wb_sid)).is_err());
}
#[test]
fn frame_constructor_enforces_the_declared_size() {
let mode = AmrMode::new(AmrVariant::WideBand, 8).unwrap();
assert_eq!(mode.octet_aligned_bytes(), 60);
assert!(AmrPayloadFrame::new(AmrFrameType::Speech(mode), true, vec![0; 60]).is_ok());
assert!(AmrPayloadFrame::new(AmrFrameType::Speech(mode), true, vec![0; 59]).is_err());
assert!(AmrPayloadFrame::new(AmrFrameType::Speech(mode), true, vec![0; 61]).is_err());
assert!(AmrPayloadFrame::new(AmrFrameType::NoData, true, vec![0; 1]).is_err());
}
#[test]
fn quality_bit_round_trips() {
let variant = AmrVariant::WideBand;
for config in both_configs(variant) {
let codec = AmrPayloadCodec::new(config).unwrap();
let mut frame = speech(variant, 3, 8);
frame.quality_ok = false;
let packet = AmrPacket::single(frame);
let back = codec.unpack(&codec.pack(&packet).unwrap()).unwrap();
assert!(!back.frames[0].quality_ok, "{config:?}");
}
}
#[test]
fn crc_matches_the_rfc4867_reference_algorithm() {
fn reference_crc(data: &[u8], class_a_bits: usize) -> u8 {
let mut reg = [0u8; 8];
for index in 0..class_a_bits {
let bit = data[index / 8] >> (7 - index % 8) & 1;
let feedback = reg[0] ^ bit;
reg.rotate_left(1);
reg[7] = 0;
if feedback == 1 {
for (i, mask) in [1, 0, 1, 1, 1, 0, 0, 0].into_iter().enumerate() {
reg[7 - i] ^= mask;
}
}
}
reg.iter()
.enumerate()
.fold(0u8, |acc, (i, &b)| acc | b << i)
}
for variant in [AmrVariant::NarrowBand, AmrVariant::WideBand] {
for mode in AmrMode::all(variant) {
for seed in [0u8, 1, 37, 200, 255] {
let data = frame_data(mode.bits(), seed);
assert_eq!(
frame_crc(&data, mode.class_a_bits()),
reference_crc(&data, mode.class_a_bits()),
"{mode} seed {seed}"
);
}
}
}
}
#[test]
fn crc_matches_hand_worked_vectors() {
assert_eq!(frame_crc(&[0x80], 1), 0xB8);
assert_eq!(frame_crc(&[0xC0], 2), 0xE4);
assert_eq!(frame_crc(&[0x80], 8), 0x64);
assert_eq!(frame_crc(&[0x00], 8), 0x00);
}
#[test]
fn crc_detects_damage_to_class_a_bits() {
let variant = AmrVariant::WideBand;
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant).with_crc()).unwrap();
let mode = AmrMode::new(variant, 2).unwrap();
let packet = AmrPacket::single(speech(variant, 2, 5));
let mut bytes = codec.pack(&packet).unwrap();
assert!(codec.unpack(&bytes).unwrap().frames[0].quality_ok);
let speech_start = 3;
bytes[speech_start] ^= 0x80;
let damaged = codec.unpack(&bytes).unwrap();
assert!(
!damaged.frames[0].quality_ok,
"a corrupt class A bit must clear the Q bit"
);
assert_eq!(damaged.frames.len(), 1);
let class_b_byte = speech_start + mode.class_a_bits() / 8 + 1;
let mut bytes = codec.pack(&packet).unwrap();
bytes[class_b_byte] ^= 0x01;
assert!(codec.unpack(&bytes).unwrap().frames[0].quality_ok);
}
#[test]
fn crc_round_trips_for_every_mode_and_frame_type() {
for variant in [AmrVariant::NarrowBand, AmrVariant::WideBand] {
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant).with_crc()).unwrap();
for mode in AmrMode::all(variant) {
let packet = AmrPacket::single(speech(variant, mode.index(), 17));
let back = codec.unpack(&codec.pack(&packet).unwrap()).unwrap();
assert_eq!(back, packet, "{mode}");
}
let sid = AmrPayloadFrame::new(
AmrFrameType::Sid(variant),
true,
frame_data(variant.sid_bits(), 4),
)
.unwrap();
let packet = AmrPacket {
cmr: None,
interleaving: None,
frames: vec![sid, AmrPayloadFrame::no_data()],
};
assert_eq!(codec.unpack(&codec.pack(&packet).unwrap()).unwrap(), packet);
}
}
#[test]
fn crc_adds_one_octet_per_frame_that_carries_one() {
let variant = AmrVariant::WideBand;
let plain = AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant)).unwrap();
let with_crc =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant).with_crc()).unwrap();
let packet = AmrPacket {
cmr: None,
interleaving: None,
frames: vec![
speech(variant, 0, 1),
AmrPayloadFrame::no_data(),
speech(variant, 1, 2),
],
};
let a = plain.pack(&packet).unwrap().len();
let b = with_crc.pack(&packet).unwrap().len();
assert_eq!(b - a, 2);
}
#[test]
fn robust_sorting_interleaves_octets_across_frames() {
let variant = AmrVariant::WideBand;
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant).with_robust_sorting())
.unwrap();
let mut f0 = speech(variant, 0, 1);
let mut f1 = speech(variant, 0, 2);
f0.data[0] = 0xA0;
f0.data[1] = 0xA1;
f1.data[0] = 0xB0;
f1.data[1] = 0xB1;
let packet = AmrPacket {
cmr: None,
interleaving: None,
frames: vec![f0, f1],
};
let bytes = codec.pack(&packet).unwrap();
assert_eq!(&bytes[3..7], &[0xA0, 0xB0, 0xA1, 0xB1]);
assert_eq!(codec.unpack(&bytes).unwrap(), packet);
}
#[test]
fn robust_sorting_round_trips_with_mixed_frame_lengths() {
let variant = AmrVariant::WideBand;
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant).with_robust_sorting())
.unwrap();
let packet = AmrPacket {
cmr: Some(3),
interleaving: None,
frames: vec![
speech(variant, 8, 1), speech(variant, 0, 2), AmrPayloadFrame::new(
AmrFrameType::Sid(variant),
true,
frame_data(variant.sid_bits(), 3),
)
.unwrap(), AmrPayloadFrame::no_data(), speech(variant, 4, 4), ],
};
let bytes = codec.pack(&packet).unwrap();
assert_eq!(codec.unpack(&bytes).unwrap(), packet);
let plain = AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant)).unwrap();
assert_eq!(bytes.len(), plain.pack(&packet).unwrap().len());
}
#[test]
fn robust_sorting_round_trips_every_mode() {
for variant in [AmrVariant::NarrowBand, AmrVariant::WideBand] {
let codec = AmrPayloadCodec::new(
AmrPayloadConfig::octet_aligned(variant).with_robust_sorting(),
)
.unwrap();
for mode in AmrMode::all(variant) {
let packet = AmrPacket {
cmr: None,
interleaving: None,
frames: vec![speech(variant, mode.index(), 6), speech(variant, 0, 7)],
};
assert_eq!(codec.unpack(&codec.pack(&packet).unwrap()).unwrap(), packet);
}
}
}
#[test]
fn interleaving_fields_round_trip() {
let variant = AmrVariant::WideBand;
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant).with_interleaving())
.unwrap();
for ill in 0..=15u8 {
for ilp in 0..=ill {
let interleaving = AmrInterleaving::new(ill, ilp).unwrap();
let packet =
AmrPacket::single(speech(variant, 0, 3)).with_interleaving(Some(interleaving));
let back = codec.unpack(&codec.pack(&packet).unwrap()).unwrap();
assert_eq!(back.interleaving, Some(interleaving));
assert_eq!(back.frames, packet.frames);
}
}
}
#[test]
fn interleaving_adds_exactly_one_octet() {
let variant = AmrVariant::WideBand;
let plain = AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant)).unwrap();
let interleaved =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant).with_interleaving())
.unwrap();
let frame = speech(variant, 0, 3);
let a = plain.pack(&AmrPacket::single(frame.clone())).unwrap().len();
let b = interleaved
.pack(
&AmrPacket::single(frame)
.with_interleaving(Some(AmrInterleaving::new(3, 1).unwrap())),
)
.unwrap()
.len();
assert_eq!(b - a, 1);
}
#[test]
fn interleaving_index_outside_its_group_is_rejected_locally_but_clamped_on_the_wire() {
assert!(AmrInterleaving::new(2, 3).is_err());
assert!(AmrInterleaving::new(16, 0).is_err());
assert_eq!(AmrInterleaving::new(3, 3).unwrap().group_len(), 4);
let variant = AmrVariant::WideBand;
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant).with_interleaving())
.unwrap();
let mut bytes = codec
.pack(
&AmrPacket::single(speech(variant, 0, 3))
.with_interleaving(Some(AmrInterleaving::new(2, 1).unwrap())),
)
.unwrap();
bytes[1] = 2 << 4 | 5;
let back = codec.unpack(&bytes).unwrap();
assert_eq!(back.interleaving.unwrap().ilp, 2, "ILP clamped to ILL");
}
#[test]
fn packing_without_required_interleaving_fields_is_an_error() {
let variant = AmrVariant::WideBand;
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(variant).with_interleaving())
.unwrap();
assert!(codec
.pack(&AmrPacket::single(speech(variant, 0, 3)))
.is_err());
}
#[test]
fn extensions_force_octet_alignment() {
let variant = AmrVariant::WideBand;
for config in [
AmrPayloadConfig::bandwidth_efficient(variant).with_crc(),
AmrPayloadConfig::bandwidth_efficient(variant).with_robust_sorting(),
AmrPayloadConfig::bandwidth_efficient(variant).with_interleaving(),
] {
assert!(
config.octet_aligned,
"{config:?} must force octet alignment"
);
}
let inconsistent = AmrPayloadConfig {
variant,
octet_aligned: false,
crc: true,
robust_sorting: false,
interleaving: false,
};
assert!(AmrPayloadCodec::new(inconsistent).is_err());
}
#[test]
fn all_extensions_together_round_trip() {
let variant = AmrVariant::WideBand;
let config = AmrPayloadConfig::octet_aligned(variant)
.with_crc()
.with_robust_sorting()
.with_interleaving();
let codec = AmrPayloadCodec::new(config).unwrap();
let packet = AmrPacket {
cmr: Some(5),
interleaving: Some(AmrInterleaving::new(4, 2).unwrap()),
frames: vec![
speech(variant, 8, 1),
AmrPayloadFrame::no_data(),
speech(variant, 2, 2),
],
};
let bytes = codec.pack(&packet).unwrap();
assert_eq!(codec.unpack(&bytes).unwrap(), packet);
}
#[test]
fn extension_payloads_reject_truncation_at_every_prefix() {
let variant = AmrVariant::WideBand;
let config = AmrPayloadConfig::octet_aligned(variant)
.with_crc()
.with_robust_sorting()
.with_interleaving();
let codec = AmrPayloadCodec::new(config).unwrap();
let packet = AmrPacket {
cmr: None,
interleaving: Some(AmrInterleaving::new(1, 0).unwrap()),
frames: vec![speech(variant, 3, 1), speech(variant, 0, 2)],
};
let bytes = codec.pack(&packet).unwrap();
for cut in 0..bytes.len() {
assert!(codec.unpack(&bytes[..cut]).is_err(), "{cut}-byte prefix");
}
assert!(codec.unpack(&bytes).is_ok());
}
const FREESWITCH_AMRWB_RTP: &[u8] = include_bytes!("testdata/freeswitch_amrwb_be.rtp");
fn freeswitch_payloads() -> Vec<Vec<u8>> {
let mut out = Vec::new();
let mut rest = FREESWITCH_AMRWB_RTP;
while rest.len() >= 2 {
let len = usize::from(u16::from_be_bytes([rest[0], rest[1]]));
assert!(rest.len() >= 2 + len, "truncated capture fixture");
out.push(rest[2..2 + len].to_vec());
rest = &rest[2 + len..];
}
out
}
#[test]
fn unpacks_real_freeswitch_amr_wb_rtp() {
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::bandwidth_efficient(AmrVariant::WideBand))
.unwrap();
let payloads = freeswitch_payloads();
assert_eq!(payloads.len(), 50, "fixture should carry 50 payloads");
for (index, payload) in payloads.iter().enumerate() {
assert_eq!(payload.len(), 61, "payload {index}");
let packet = codec
.unpack(payload)
.unwrap_or_else(|e| panic!("payload {index} failed to parse: {e}"));
assert_eq!(packet.frames.len(), 1, "payload {index}");
let frame = &packet.frames[0];
let AmrFrameType::Speech(mode) = frame.frame_type else {
panic!(
"payload {index} was not a speech frame: {:?}",
frame.frame_type
);
};
assert_eq!(mode.index(), 8, "payload {index}: expected mode 8 (23.85)");
assert_eq!(mode.bits(), 477);
assert!(frame.quality_ok, "payload {index}: Q bit clear");
assert_eq!(frame.data.len(), 60, "payload {index}: speech octets");
assert_eq!(packet.cmr, None, "payload {index}");
}
}
#[test]
fn real_payloads_repack_to_the_same_bytes() {
let codec =
AmrPayloadCodec::new(AmrPayloadConfig::bandwidth_efficient(AmrVariant::WideBand))
.unwrap();
for (index, payload) in freeswitch_payloads().iter().enumerate() {
let packet = codec.unpack(payload).unwrap();
let repacked = codec.pack(&packet).unwrap();
assert_eq!(&repacked, payload, "payload {index} did not round-trip");
}
}
#[test]
fn real_payloads_carry_genuine_encoder_output() {
let payloads = freeswitch_payloads();
let distinct: std::collections::HashSet<&Vec<u8>> = payloads.iter().collect();
assert!(
distinct.len() > payloads.len() / 2,
"fixture looks like pass-through, not encoder output: only {} distinct of {}",
distinct.len(),
payloads.len()
);
assert!(payloads.iter().all(|p| p[2..].iter().any(|&b| b != 0)));
}
#[test]
fn real_payloads_are_rejected_as_octet_aligned() {
let oa =
AmrPayloadCodec::new(AmrPayloadConfig::octet_aligned(AmrVariant::WideBand)).unwrap();
let mismatched = freeswitch_payloads()
.iter()
.filter(|p| oa.unpack(p).is_ok())
.count();
assert_eq!(mismatched, 0, "octet-aligned parse accepted BE payloads");
}
#[test]
fn never_panics_on_arbitrary_input() {
for variant in [AmrVariant::NarrowBand, AmrVariant::WideBand] {
let configs = [
AmrPayloadConfig::bandwidth_efficient(variant),
AmrPayloadConfig::octet_aligned(variant),
AmrPayloadConfig::octet_aligned(variant)
.with_crc()
.with_robust_sorting()
.with_interleaving(),
];
for config in configs {
let codec = AmrPayloadCodec::new(config).unwrap();
for seed in 0u16..=u16::from(u8::MAX) {
let byte = u8::try_from(seed).unwrap_or(0);
for len in 0..8usize {
let bytes: Vec<u8> = (0..len)
.map(|i| byte.rotate_left(u32::try_from(i).unwrap_or(0)))
.collect();
let _ = codec.unpack(&bytes);
}
}
}
}
}
}