diff --git a/src/lib.rs b/src/lib.rs index eb16765..8d92bc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,7 +68,7 @@ mod write; pub use read::{WavReader, WavIntoSamples, WavSamples, read_wave_header}; pub use write::{SampleWriter16, WavWriter}; -pub use read::{ Chunk, ChunksReader }; +pub use read::{ Chunk, ChunksReader, BwavExtMeta }; pub use write::ChunksWriter; /// A type that can be used to represent audio samples. @@ -911,17 +911,13 @@ macro_rules! guard { #[test] fn read_non_standard_chunks() { use std::fs; - use std::io::Read; let mut file = fs::File::open("testsamples/nonstandard-01.wav").unwrap(); let mut reader = read::ChunksReader::new(&mut file).unwrap(); guard!(Some(read::Chunk::Unknown(kind, _reader)) = reader.next().unwrap() => { assert_eq!(kind, *b"JUNK"); }); - guard!(Some(read::Chunk::Unknown(kind, mut reader)) = reader.next().unwrap() => { - assert_eq!(kind, *b"bext"); - let mut v = vec!(); - reader.read_to_end(&mut v).unwrap(); - assert!((0..v.len()).any(|offset| &v[offset..offset+9] == b"Pro Tools")); + guard!(Some(read::Chunk::Bext(bext)) = reader.next().unwrap() => { + assert_eq!(bext.originator, "Pro Tools"); }); guard!(Some(read::Chunk::Fmt(_)) = reader.next().unwrap() => { () }); guard!(Some(read::Chunk::Unknown(kind, _len)) = reader.next().unwrap() => { diff --git a/src/read.rs b/src/read.rs index 69d4383..a37be13 100644 --- a/src/read.rs +++ b/src/read.rs @@ -68,6 +68,9 @@ pub trait ReadExt: io::Read { /// Reads four bytes and interprets them as a little-endian 32-bit unsigned integer. fn read_le_u32(&mut self) -> io::Result; + /// Reads eight bytes and interprets them as a little-endian 64-bit unsigned integer. + fn read_le_u64(&mut self) -> io::Result; + /// Reads four bytes and interprets them as a little-endian 32-bit IEEE float. fn read_le_f32(&mut self) -> io::Result; } @@ -191,6 +194,16 @@ impl ReadExt for R (buf[1] as u32) << 8 | (buf[0] as u32) << 0) } + #[inline(always)] + fn read_le_u64(&mut self) -> io::Result { + let mut buf = [0u8; 8]; + try!(self.read_into(&mut buf)); + Ok((buf[7] as u64) << 56 | (buf[6] as u64) << 48 | + (buf[5] as u64) << 40 | (buf[4] as u64) << 32 | + (buf[3] as u64) << 24 | (buf[2] as u64) << 16 | + (buf[1] as u64) << 8 | (buf[0] as u64) << 0) + } + #[inline(always)] fn read_le_f32(&mut self) -> io::Result { self.read_le_u32().map(|u| unsafe { mem::transmute(u) }) @@ -274,6 +287,8 @@ pub enum Chunk<'r, R: 'r + io::Read> { Fmt(WavSpecEx), /// fact chunk, used by non-pcm encoding but redundant Fact, + /// broadcast extension chunk, parsed into a BwavExtMeta + Bext(BwavExtMeta), /// data chunk, where the samples are actually stored Data, /// any other riff chunk @@ -290,6 +305,8 @@ pub struct ChunksReader { reader: R, /// the Wave format specification, if it has been read already pub spec_ex: Option, + /// the Broadcast Wave Extension Metadata + pub bext: Option, /// when inside the main data state, keeps track of decoding and chunk /// boundaries pub data_state: Option, @@ -315,6 +332,7 @@ impl ChunksReader { Ok(ChunksReader { reader: reader, spec_ex: None, + bext: None, data_state: None, }) } @@ -399,6 +417,11 @@ impl ChunksReader { let _samples_per_channel = self.reader.read_le_u32(); Ok(Some(Chunk::Fact)) } + b"bext" => { + let bext = try!(self.read_bext_chunk(len)); + self.bext = Some(bext.clone()); + Ok(Some(Chunk::Bext(bext))) + } b"data" => { if let Some(spec_ex) = self.spec_ex { self.data_state = Some(DataReadingState { @@ -669,6 +692,75 @@ impl ChunksReader { Ok(()) } + fn read_bext_chunk(&mut self, chunk_len: u32) -> Result { + const NULL: char = '\u{0}'; + + macro_rules! into_string { + ($vec:expr) => { + { + let mut string = try!( + String::from_utf8(try!($vec)) + .map_err(|_| Error::FormatError("invalid ascii in bext")) + ); + string.truncate(string.trim_end_matches(NULL).len()); + string + } + } + } + + let description = into_string!(self.reader.read_bytes(256)); + let originator = into_string!(self.reader.read_bytes(32)); + let originator_reference = into_string!(self.reader.read_bytes(32)); + let originator_date = into_string!(self.reader.read_bytes(10)); + let originator_time = into_string!(self.reader.read_bytes(8)); + + let time_reference = try!(self.reader.read_le_u64()); + let version = try!(self.reader.read_u8()); + + let mut umid = [0u8; 64]; + let mut loudness_value = None; + let mut loudness_range = None; + let mut max_true_peak = None; + let mut max_momentary_loudness = None; + let mut max_shortterm_loudness = None; + + if version > 0 { + try!(self.reader.read_into(&mut umid)); + } + + if version > 1 { + loudness_value = Some(try!(self.reader.read_le_i16()) as f32 / 100.0); + loudness_range = Some(try!(self.reader.read_le_i16()) as f32 / 100.0); + max_true_peak = Some(try!(self.reader.read_le_i16()) as f32 / 100.0); + max_momentary_loudness = Some(try!(self.reader.read_le_i16()) as f32 / 100.0); + max_shortterm_loudness = Some(try!(self.reader.read_le_i16()) as f32 / 100.0); + } + + // Skip 180 bytes of reserve and coding_history + match version { + 0 => if chunk_len > 347 { try!(self.reader.skip_bytes((chunk_len - 347) as usize)); } + 1 => if chunk_len > 411 { try!(self.reader.skip_bytes((chunk_len - 411) as usize)); } + 2 => if chunk_len > 421 { try!(self.reader.skip_bytes((chunk_len - 421) as usize)); } + _ => {} + } + + Ok(BwavExtMeta { + description, + originator, + originator_reference, + originator_date, + originator_time, + time_reference, + version, + umid, + loudness_value, + loudness_range, + max_true_peak, + max_momentary_loudness, + max_shortterm_loudness, + }) + } + /// Unwrap the raw Reader from this Chunkreader pub fn into_inner(self) -> R { self.reader @@ -715,6 +807,49 @@ pub struct WavSpecEx { pub bytes_per_sample: u16, } +/// Definition of a Broadcast Audio Extension Chunk. +/// +/// https://tech.ebu.ch/docs/tech/tech3285.pdf +#[derive(Clone, Debug)] +pub struct BwavExtMeta { + // ASCII : <> 256 byes + pub description: String, + // ASCII : <> 32 bytes + pub originator: String, + // ASCII : <> 32 bytes + pub originator_reference: String, + // ASCII : <> 10 bytes + // The separator may be a '-', '_', ':', ' ', or '.' + pub originator_date: String, + // ASCII : <> 8 bytes + pub originator_time: String, + // The first sample count since midnight + // SampleRate is defined in the format chunk + pub time_reference: u64, + // Version of the BWF + pub version: u8, + // SMPTE UMID 64 bytes + pub umid: [u8; 64], + // Integrated loudness in LUFS (multiplied by 100) + pub loudness_value: Option, + // Loudness range in LU (multiplied by 100) + pub loudness_range: Option, + // Maximum true peak level in dBTP (multiplied by 100) + pub max_true_peak: Option, + // Highest value of mementary loudness + // level in LUFS (multiplied by 100) + pub max_momentary_loudness: Option, + // Highest value of the short-term loudness level + // in LUFS (multiplied by 100) + pub max_shortterm_loudness: Option, + + // << 180 bytes reserved for extension >> + + // ASCII : History Coding, terminated by CR/LF + // More information https://tech.ebu.ch/docs/r/r098.pdf + // pub coding_history: String, +} + /// A reader that reads the WAVE format from the underlying reader. /// /// A `WavReader` is a streaming reader. It reads data from the underlying @@ -804,6 +939,11 @@ impl WavReader .spec } + /// Returns a reference to the Broadcast Extension Metadata, if present. + pub fn bext(&self) -> Option<&BwavExtMeta> { + self.reader.bext.as_ref() + } + /// Returns an iterator over all samples. /// /// The channel data is is interleaved. The iterator is streaming. That is, @@ -1273,6 +1413,59 @@ fn read_wav_nonstandard_01() { assert_eq!(&samples[..], &[0, 0]); } +#[test] +fn read_pro_tools_bext() { + let bext = WavReader::open("testsamples/pro_tools_bext.wav") + .unwrap() + .bext() + .cloned() + .expect("test file has bext"); + + assert_eq!(bext.originator, "Pro Tools"); + assert_eq!(bext.originator_date, "2020-12-21"); + assert_eq!(bext.originator_time, "20:22:14"); + assert_eq!(bext.time_reference, 2882880); + assert_eq!(bext.version, 1); +} + +#[test] +fn read_reaper_bext() { + let bext = WavReader::open("testsamples/reaper_bext.wav") + .unwrap() + .bext() + .cloned() + .expect("test file has bext"); + + assert_eq!(bext.originator, "REAPER"); + assert_eq!(bext.originator_date, "2020-12-21"); + assert_eq!(bext.originator_time, "21-07-45"); + assert_eq!(bext.time_reference, 2645927); + assert_eq!(bext.version, 1); +} + +#[test] +fn read_wav_agent_bext() { + // WavAgent may not place the bext chunk before the data chunk, + // so WavReader will not have it set yet. + let wav_reader = WavReader::open("testsamples/wav_agent_bext.wav") + .unwrap(); + assert!(wav_reader.bext().is_none()); + + // But we can still retrieve it with ChunksReader + let mut chunks_reader = wav_reader.reader; + let mut bext = None; + while let Some(chunk) = chunks_reader.next().unwrap() { + if let Chunk::Bext(b) = chunk { + bext = Some(b); + } + } + assert!(bext.is_some()); + let bext = bext.unwrap(); + assert_eq!(bext.originator, "Sound Dev: WA20 S#349161314873"); + assert_eq!(bext.time_reference, 3137939205); + assert_eq!(bext.version, 0); +} + #[test] fn wide_read_should_signal_error() { let mut reader24 = WavReader::open("testsamples/waveformatextensible-24bit-192kHz-mono.wav") diff --git a/testsamples/pro_tools_bext.wav b/testsamples/pro_tools_bext.wav new file mode 100644 index 0000000..fff4158 Binary files /dev/null and b/testsamples/pro_tools_bext.wav differ diff --git a/testsamples/reaper_bext.wav b/testsamples/reaper_bext.wav new file mode 100755 index 0000000..ea102bd Binary files /dev/null and b/testsamples/reaper_bext.wav differ diff --git a/testsamples/wav_agent_bext.wav b/testsamples/wav_agent_bext.wav new file mode 100644 index 0000000..0978b73 Binary files /dev/null and b/testsamples/wav_agent_bext.wav differ