use super::error::WavError;
pub struct BextChunk<'a> {
bytes: &'a [u8],
}
impl<'a> BextChunk<'a> {
pub const MIN_SIZE: usize = 602;
pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, WavError> {
if bytes.len() < Self::MIN_SIZE {
return Err(WavError::chunk_parsing(
"bext",
"0",
format!(
"bext chunk must be at least {} bytes, got {}",
Self::MIN_SIZE,
bytes.len()
),
));
}
Ok(BextChunk { bytes })
}
pub fn description(&self) -> String {
fixed_str(self.bytes, 0, 256)
}
pub fn originator(&self) -> String {
fixed_str(self.bytes, 256, 32)
}
pub fn originator_reference(&self) -> String {
fixed_str(self.bytes, 288, 32)
}
pub fn origination_date(&self) -> String {
fixed_str(self.bytes, 320, 10)
}
pub fn origination_time(&self) -> String {
fixed_str(self.bytes, 330, 8)
}
pub fn time_reference(&self) -> u64 {
let lo = u32::from_le_bytes([self.bytes[338], self.bytes[339], self.bytes[340], self.bytes[341]]) as u64;
let hi = u32::from_le_bytes([self.bytes[342], self.bytes[343], self.bytes[344], self.bytes[345]]) as u64;
lo | (hi << 32)
}
pub fn version(&self) -> u16 {
u16::from_le_bytes([self.bytes[346], self.bytes[347]])
}
pub fn umid(&self) -> &[u8] {
&self.bytes[348..412]
}
pub fn loudness_value(&self) -> Option<i16> {
self.loudness_field(412)
}
pub fn loudness_range(&self) -> Option<u16> {
if self.version() < 2 {
return None;
}
let v = u16::from_le_bytes([self.bytes[414], self.bytes[415]]);
if v == 0x7FFF { None } else { Some(v) }
}
pub fn max_true_peak_level(&self) -> Option<i16> {
self.loudness_field(416)
}
pub fn max_momentary_loudness(&self) -> Option<i16> {
self.loudness_field(418)
}
pub fn max_short_term_loudness(&self) -> Option<i16> {
self.loudness_field(420)
}
pub fn coding_history(&self) -> String {
if self.bytes.len() <= Self::MIN_SIZE {
return String::new();
}
fixed_str(self.bytes, Self::MIN_SIZE, self.bytes.len() - Self::MIN_SIZE)
}
fn loudness_field(&self, offset: usize) -> Option<i16> {
if self.version() < 2 {
return None;
}
let v = i16::from_le_bytes([self.bytes[offset], self.bytes[offset + 1]]);
if v == 0x7FFF_u16 as i16 { None } else { Some(v) }
}
}
fn fixed_str(bytes: &[u8], start: usize, len: usize) -> String {
let field = &bytes[start..start + len];
let end = field.iter().position(|&b| b == 0).unwrap_or(len);
String::from_utf8_lossy(&field[..end]).trim().to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_bext(
description: &str,
originator: &str,
originator_ref: &str,
date: &str,
time: &str,
time_ref: u64,
version: u16,
coding_history: &str,
) -> Vec<u8> {
let mut buf = vec![0u8; BextChunk::MIN_SIZE];
let copy_str = |buf: &mut Vec<u8>, s: &str, offset: usize, max: usize| {
let b = s.as_bytes();
let n = b.len().min(max);
buf[offset..offset + n].copy_from_slice(&b[..n]);
};
copy_str(&mut buf, description, 0, 256);
copy_str(&mut buf, originator, 256, 32);
copy_str(&mut buf, originator_ref, 288, 32);
copy_str(&mut buf, date, 320, 10);
copy_str(&mut buf, time, 330, 8);
let lo = (time_ref & 0xFFFF_FFFF) as u32;
let hi = (time_ref >> 32) as u32;
buf[338..342].copy_from_slice(&lo.to_le_bytes());
buf[342..346].copy_from_slice(&hi.to_le_bytes());
buf[346..348].copy_from_slice(&version.to_le_bytes());
for offset in [412, 414, 416, 418, 420] {
buf[offset] = 0xFF;
buf[offset + 1] = 0x7F;
}
if !coding_history.is_empty() {
buf.extend_from_slice(coding_history.as_bytes());
buf.push(0);
}
buf
}
#[test]
fn test_bext_basic_fields() {
let bytes = make_bext(
"Test recording",
"Studio A",
"REF-001",
"2024-01-15",
"10:30:00",
44100 * 3600,
1,
"",
);
let chunk = BextChunk::from_bytes(&bytes).expect("valid bext chunk");
assert_eq!(chunk.description(), "Test recording");
assert_eq!(chunk.originator(), "Studio A");
assert_eq!(chunk.originator_reference(), "REF-001");
assert_eq!(chunk.origination_date(), "2024-01-15");
assert_eq!(chunk.origination_time(), "10:30:00");
assert_eq!(chunk.time_reference(), 44100 * 3600);
assert_eq!(chunk.version(), 1);
}
#[test]
fn test_bext_loudness_unset_for_version_1() {
let bytes = make_bext("", "", "", "", "", 0, 1, "");
let chunk = BextChunk::from_bytes(&bytes).expect("valid bext");
assert!(chunk.loudness_value().is_none());
assert!(chunk.loudness_range().is_none());
assert!(chunk.max_true_peak_level().is_none());
assert!(chunk.max_momentary_loudness().is_none());
assert!(chunk.max_short_term_loudness().is_none());
}
#[test]
fn test_bext_loudness_version_2() {
let mut bytes = make_bext("", "", "", "", "", 0, 2, "");
let lufs: i16 = -2300;
bytes[412..414].copy_from_slice(&lufs.to_le_bytes());
bytes[414..416].copy_from_slice(&800u16.to_le_bytes());
let chunk = BextChunk::from_bytes(&bytes).expect("valid bext");
assert_eq!(chunk.loudness_value(), Some(-2300));
assert_eq!(chunk.loudness_range(), Some(800));
assert!(chunk.max_true_peak_level().is_none());
}
#[test]
fn test_bext_coding_history() {
let bytes = make_bext("", "", "", "", "", 0, 0, "A=PCM,F=44100,W=24,M=stereo");
let chunk = BextChunk::from_bytes(&bytes).expect("valid bext");
assert_eq!(chunk.coding_history(), "A=PCM,F=44100,W=24,M=stereo");
}
#[test]
fn test_bext_no_coding_history() {
let bytes = make_bext("", "", "", "", "", 0, 0, "");
let chunk = BextChunk::from_bytes(&bytes).expect("valid bext");
assert_eq!(chunk.coding_history(), "");
}
#[test]
fn test_bext_too_small() {
assert!(BextChunk::from_bytes(&[0u8; 601]).is_err());
}
#[test]
fn test_bext_time_reference_64bit() {
let large_ref: u64 = u64::from(u32::MAX) + 100;
let bytes = make_bext("", "", "", "", "", large_ref, 0, "");
let chunk = BextChunk::from_bytes(&bytes).expect("valid bext");
assert_eq!(chunk.time_reference(), large_ref);
}
#[test]
fn test_bext_umid_all_zeros() {
let bytes = make_bext("", "", "", "", "", 0, 0, "");
let chunk = BextChunk::from_bytes(&bytes).expect("valid bext");
assert_eq!(chunk.umid(), &[0u8; 64]);
}
}