use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LpcmQuantisation {
Bits16,
Bits20,
Bits24,
Reserved,
}
impl LpcmQuantisation {
fn from_code(code: u8) -> Self {
match code & 0b11 {
0 => Self::Bits16,
1 => Self::Bits20,
2 => Self::Bits24,
_ => Self::Reserved,
}
}
pub fn bits_per_sample(self) -> Option<u8> {
match self {
Self::Bits16 => Some(16),
Self::Bits20 => Some(20),
Self::Bits24 => Some(24),
Self::Reserved => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LpcmSampleFrequency {
Hz48000,
Hz96000,
Reserved,
}
impl LpcmSampleFrequency {
fn from_code(code: u8) -> Self {
match code & 0b11 {
0 => Self::Hz48000,
1 => Self::Hz96000,
_ => Self::Reserved,
}
}
pub fn hz(self) -> Option<u32> {
match self {
Self::Hz48000 => Some(48_000),
Self::Hz96000 => Some(96_000),
Self::Reserved => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LpcmHeader {
pub sub_stream_id: u8,
pub number_of_frame_headers: u8,
pub first_access_unit_pointer: u16,
pub audio_emphasis_flag: bool,
pub audio_mute_flag: bool,
pub audio_frame_number: u8,
pub quantisation: LpcmQuantisation,
pub sample_frequency: LpcmSampleFrequency,
pub channel_count: u8,
pub dynamic_range_x: u8,
pub dynamic_range_y: u8,
}
pub const LPCM_HEADER_LEN: usize = 7;
pub const DVD_LPCM_MAX_BITRATE_KBPS: u32 = 6144;
impl LpcmHeader {
pub fn parse(payload: &[u8]) -> Result<Self> {
if payload.len() < LPCM_HEADER_LEN {
return Err(Error::InvalidUdf(
"LPCM audio-pack header truncated (< 7 bytes)",
));
}
let sub_stream_id = payload[0];
if !(0xA0..=0xA7).contains(&sub_stream_id) {
return Err(Error::InvalidUdf(
"LPCM audio-pack header: sub_stream_id not in 0xA0..=0xA7",
));
}
let number_of_frame_headers = payload[1];
let first_access_unit_pointer = u16::from_be_bytes([payload[2], payload[3]]);
let byte4 = payload[4];
let audio_emphasis_flag = (byte4 & 0b1000_0000) != 0;
let audio_mute_flag = (byte4 & 0b0100_0000) != 0;
let audio_frame_number = byte4 & 0b0001_1111;
let byte5 = payload[5];
let quantisation = LpcmQuantisation::from_code(byte5 >> 6);
let sample_frequency = LpcmSampleFrequency::from_code((byte5 >> 4) & 0b11);
let channel_count = (byte5 & 0b0000_0111) + 1;
let byte6 = payload[6];
let dynamic_range_x = byte6 >> 5;
let dynamic_range_y = byte6 & 0b0001_1111;
Ok(Self {
sub_stream_id,
number_of_frame_headers,
first_access_unit_pointer,
audio_emphasis_flag,
audio_mute_flag,
audio_frame_number,
quantisation,
sample_frequency,
channel_count,
dynamic_range_x,
dynamic_range_y,
})
}
pub fn track(self) -> u8 {
self.sub_stream_id - 0xA0
}
pub fn bits_per_sample(self) -> Option<u8> {
self.quantisation.bits_per_sample()
}
pub fn sample_rate_hz(self) -> Option<u32> {
self.sample_frequency.hz()
}
pub fn bitrate_kbps(self) -> Option<u32> {
let bits = self.bits_per_sample()? as u32;
let rate = self.sample_rate_hz()?;
Some((bits * rate * self.channel_count as u32) / 1_000)
}
pub fn is_within_dvd_video_limit(self) -> bool {
self.bitrate_kbps()
.is_some_and(|kbps| kbps <= DVD_LPCM_MAX_BITRATE_KBPS)
}
pub fn linear_gain(self) -> f32 {
let exponent = 4.0 - (self.dynamic_range_x as f32 + self.dynamic_range_y as f32 / 30.0);
2.0_f32.powf(exponent)
}
pub fn gain_db(self) -> f32 {
24.082 - 6.0206 * self.dynamic_range_x as f32 - 0.2007 * self.dynamic_range_y as f32
}
}
pub fn peel_lpcm_payload(payload: &[u8]) -> Result<(LpcmHeader, &[u8])> {
let header = LpcmHeader::parse(payload)?;
Ok((header, &payload[LPCM_HEADER_LEN..]))
}
#[cfg(test)]
mod tests {
use super::*;
fn baseline_header() -> [u8; 7] {
[
0xA0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, ]
}
#[test]
fn parse_baseline_header() {
let mut bytes = baseline_header();
bytes[5] = 0b0000_0001;
let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(h.sub_stream_id, 0xA0);
assert_eq!(h.track(), 0);
assert_eq!(h.number_of_frame_headers, 0);
assert_eq!(h.first_access_unit_pointer, 0);
assert!(!h.audio_emphasis_flag);
assert!(!h.audio_mute_flag);
assert_eq!(h.audio_frame_number, 0);
assert_eq!(h.quantisation, LpcmQuantisation::Bits16);
assert_eq!(h.sample_frequency, LpcmSampleFrequency::Hz48000);
assert_eq!(h.channel_count, 2);
assert_eq!(h.bits_per_sample(), Some(16));
assert_eq!(h.sample_rate_hz(), Some(48_000));
assert_eq!(h.bitrate_kbps(), Some(1_536));
assert!(h.is_within_dvd_video_limit());
}
#[test]
fn parse_rejects_short_buffer() {
let short = [0xA0, 0, 0, 0, 0, 0];
let err = LpcmHeader::parse(&short).unwrap_err();
matches!(err, Error::InvalidUdf(_));
}
#[test]
fn parse_rejects_non_lpcm_substream() {
let bytes = [0x80, 0, 0, 0, 0, 0, 0];
let err = LpcmHeader::parse(&bytes).unwrap_err();
matches!(err, Error::InvalidUdf(_));
}
#[test]
fn parse_decodes_each_track_id() {
for track in 0..=7u8 {
let mut bytes = baseline_header();
bytes[0] = 0xA0 + track;
let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(h.track(), track);
assert_eq!(h.sub_stream_id, 0xA0 + track);
}
}
#[test]
fn parse_decodes_quantisation_codes() {
for (code, expected, bps) in [
(0u8, LpcmQuantisation::Bits16, Some(16)),
(1, LpcmQuantisation::Bits20, Some(20)),
(2, LpcmQuantisation::Bits24, Some(24)),
(3, LpcmQuantisation::Reserved, None),
] {
let mut bytes = baseline_header();
bytes[5] = (code << 6) | 0b0000_0001;
let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(h.quantisation, expected);
assert_eq!(h.bits_per_sample(), bps);
}
}
#[test]
fn parse_decodes_sample_frequency_codes() {
for (code, expected, hz) in [
(0u8, LpcmSampleFrequency::Hz48000, Some(48_000)),
(1, LpcmSampleFrequency::Hz96000, Some(96_000)),
(2, LpcmSampleFrequency::Reserved, None),
(3, LpcmSampleFrequency::Reserved, None),
] {
let mut bytes = baseline_header();
bytes[5] = (code << 4) | 0b0000_0001;
let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(h.sample_frequency, expected);
assert_eq!(h.sample_rate_hz(), hz);
}
}
#[test]
fn parse_decodes_channel_count_offset_by_one() {
for code in 0u8..=7 {
let mut bytes = baseline_header();
bytes[5] = code & 0b0000_0111;
let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(h.channel_count, code + 1);
}
}
#[test]
fn parse_decodes_emphasis_mute_frame_number() {
let mut bytes = baseline_header();
bytes[4] = 0b1000_0000 | 0b0100_0000 | 0b0001_0101;
let h = LpcmHeader::parse(&bytes).unwrap();
assert!(h.audio_emphasis_flag);
assert!(h.audio_mute_flag);
assert_eq!(h.audio_frame_number, 0b1_0101);
}
#[test]
fn parse_decodes_first_access_unit_pointer() {
let mut bytes = baseline_header();
bytes[2] = 0x12;
bytes[3] = 0x34;
let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(h.first_access_unit_pointer, 0x1234);
}
#[test]
fn parse_decodes_dynamic_range_xy_split() {
let mut bytes = baseline_header();
bytes[6] = 0b1010_0000 | 0b0000_1011; let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(h.dynamic_range_x, 0b101);
assert_eq!(h.dynamic_range_y, 0b01011);
}
#[test]
fn dynamic_range_unity_gain_at_zero_zero() {
let bytes = baseline_header();
let h = LpcmHeader::parse(&bytes).unwrap();
assert!((h.linear_gain() - 16.0).abs() < 1e-4);
assert!((h.gain_db() - 24.082).abs() < 1e-3);
}
#[test]
fn dynamic_range_negative_attenuation_when_x_y_grow() {
let mut bytes = baseline_header();
bytes[6] = 0b1110_0000 | 0b0001_1110; let h = LpcmHeader::parse(&bytes).unwrap();
assert!((h.linear_gain() - (1.0 / 16.0)).abs() < 1e-5);
assert!(h.gain_db() < -24.0 && h.gain_db() > -24.5);
}
#[test]
fn bitrate_table_matches_limpcmaud_doc() {
let table: &[(u8, u8, u8, u32, bool)] = &[
(0, 0, 1, 768, false),
(0, 0, 2, 1536, false),
(0, 0, 3, 2304, false),
(0, 0, 4, 3072, false),
(0, 0, 5, 3840, false),
(0, 0, 6, 4608, false),
(0, 0, 7, 5376, false),
(0, 0, 8, 6144, false),
(0, 1, 1, 960, false),
(0, 1, 2, 1920, false),
(0, 1, 3, 2880, false),
(0, 1, 4, 3840, false),
(0, 1, 5, 4800, false),
(0, 1, 6, 5760, false),
(0, 1, 7, 6720, true),
(0, 1, 8, 7680, true),
(0, 2, 1, 1152, false),
(0, 2, 2, 2304, false),
(0, 2, 3, 3456, false),
(0, 2, 4, 4608, false),
(0, 2, 5, 5760, false),
(0, 2, 6, 6912, true),
(0, 2, 7, 8064, true),
(0, 2, 8, 9216, true),
(1, 0, 1, 1536, false),
(1, 0, 2, 3072, false),
(1, 0, 3, 4608, false),
(1, 0, 4, 6144, false),
(1, 0, 5, 7680, true),
(1, 0, 6, 9216, true),
(1, 0, 7, 10752, true),
(1, 0, 8, 12288, true),
(1, 1, 1, 1920, false),
(1, 1, 2, 3840, false),
(1, 1, 3, 5760, false),
(1, 1, 4, 7680, true),
(1, 1, 5, 9600, true),
(1, 1, 6, 11520, true),
(1, 1, 7, 13440, true),
(1, 1, 8, 15360, true),
(1, 2, 1, 2304, false),
(1, 2, 2, 4608, false),
(1, 2, 3, 6912, true),
(1, 2, 4, 9216, true),
(1, 2, 5, 11520, true),
(1, 2, 6, 13824, true),
(1, 2, 7, 16128, true),
(1, 2, 8, 18432, true),
];
for &(sr, q, ch, expected_kbps, is_red) in table {
let mut bytes = baseline_header();
bytes[5] = (q << 6) | (sr << 4) | ((ch - 1) & 0b111);
let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(
h.bitrate_kbps(),
Some(expected_kbps),
"sr={sr} q={q} ch={ch} mismatched bitrate",
);
assert_eq!(
h.is_within_dvd_video_limit(),
!is_red,
"sr={sr} q={q} ch={ch} mismatched DVD-limit verdict",
);
}
}
#[test]
fn bitrate_returns_none_for_reserved_codes() {
let mut bytes = baseline_header();
bytes[5] = (3 << 6) | 0b0000_0001;
let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(h.bitrate_kbps(), None);
assert!(!h.is_within_dvd_video_limit());
let mut bytes = baseline_header();
bytes[5] = (2 << 4) | 0b0000_0001;
let h = LpcmHeader::parse(&bytes).unwrap();
assert_eq!(h.bitrate_kbps(), None);
assert!(!h.is_within_dvd_video_limit());
}
#[test]
fn peel_lpcm_payload_returns_header_and_tail() {
let mut bytes = vec![0xA0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00];
bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let (h, tail) = peel_lpcm_payload(&bytes).unwrap();
assert_eq!(h.track(), 0);
assert_eq!(tail, &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn peel_lpcm_payload_rejects_short_buffer() {
let short = [0xA0, 0, 0, 0];
let err = peel_lpcm_payload(&short).unwrap_err();
matches!(err, Error::InvalidUdf(_));
}
}