From 0922e6e69315f81570a050a4ba0d57737b4d6841 Mon Sep 17 00:00:00 2001 From: Julian Aichholz Date: Sat, 26 Dec 2020 15:21:19 -0800 Subject: [PATCH 1/4] Bext chunk parsing. Update read_non_standard_chunks test --- src/lib.rs | 8 +--- src/read.rs | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index eb16765..ab77289 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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..d4a9f6b 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); + } + + 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, + coding_history: String::new(), + }) + } + /// 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 + 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 From f593581dbfc8fa604eaef8c1f10bc9f88d3beefa Mon Sep 17 00:00:00 2001 From: Julian Aichholz Date: Sat, 26 Dec 2020 16:03:08 -0800 Subject: [PATCH 2/4] Remove coding history --- src/read.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/read.rs b/src/read.rs index d4a9f6b..0f43590 100644 --- a/src/read.rs +++ b/src/read.rs @@ -736,6 +736,7 @@ impl ChunksReader { 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)); } @@ -757,7 +758,6 @@ impl ChunksReader { max_true_peak, max_momentary_loudness, max_shortterm_loudness, - coding_history: String::new(), }) } @@ -845,9 +845,9 @@ pub struct BwavExtMeta { // << 180 bytes reserved for extension >> - // ASCII : History Coding - // Terminated by CR/LF - pub coding_history: String, + // 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. From c852d9e3710226b35e726e3765bfcb784d87e016 Mon Sep 17 00:00:00 2001 From: Julian Aichholz Date: Sat, 26 Dec 2020 17:05:08 -0800 Subject: [PATCH 3/4] Pub use BwavExtMeta --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index ab77289..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. From 581a78e2f0bd4bdf9622fe3a4d7f6c174145a8bd Mon Sep 17 00:00:00 2001 From: Julian Aichholz Date: Sat, 26 Dec 2020 19:29:06 -0800 Subject: [PATCH 4/4] WavReader method for getting a reference to bext. Tests for various daw bexts --- src/read.rs | 58 +++++++++++++++++++++++++++++++++ testsamples/pro_tools_bext.wav | Bin 0 -> 1612 bytes testsamples/reaper_bext.wav | Bin 0 -> 2108 bytes testsamples/wav_agent_bext.wav | Bin 0 -> 11208 bytes 4 files changed, 58 insertions(+) create mode 100644 testsamples/pro_tools_bext.wav create mode 100755 testsamples/reaper_bext.wav create mode 100644 testsamples/wav_agent_bext.wav diff --git a/src/read.rs b/src/read.rs index 0f43590..a37be13 100644 --- a/src/read.rs +++ b/src/read.rs @@ -939,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, @@ -1408,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 0000000000000000000000000000000000000000..fff41588e4f1b9a3dd5c34c0c98c436e970b2997 GIT binary patch literal 1612 zcmWIYbaQiIV_*n(40H7g_4AHlpe;yBttg3NqOBc6!SsNle1(ww{G4JOj!jIgEb@yA z^)oi}%*Lj~$iT=z*U(7U$k52Z%E-vd(8S@dHdrAe0~?pN2_qvTD*|Hz-Hd?5lhQ0QP2Q-hY<)Hb~7+E7%?$0Gf3d{U~Xn!ngCGNWBFf6 zsT$ONu>GidYz8fl?r2kR+HGY`}s6iOJciCBY>{sfoEj^+4r}3`$VtptwYq_smPF z0Gnq5vPOh2itphr9-d| kBsfna1{WT0zkq-$hoWTDJQKg5<#Cws*y}$-Kbelr?CSo~4I$csrX;u7Qd4oT{*m z$&F+Fh8_=`kI)!a=S-0?_GuSi;&2@6wj}E^vjP}cS_jl&Q`0ac$n|6-uW9!HqPp@Y;lj)nz z(NKiXPmf-@2m9@_QHT)gakTP+r5R6R*FOHPU0XxI7^KPf%olFER`vj9K8qQkyagb< zbFx*V%t3CVff@3M$srHiVU`6KPNQWvZO3k$o;Y@^(X?#KIc+$N6U%PbmQ;id&$!5t zHE#jLiJ-Kqf$8(qk5~Yt>`_q0Qi`Dl=7>ju6shN*1FRTuWdnv_bT-oVy-cuF=I(mK zvaFhIQEh7u#QDP%D@zniRIz4*Ab6koNx);PnSkUd6X-yKw|4&8yr-0~0QOiz;4}n+fFINKVCW zEUon z(pLl~DU^@BNRps*;<&TvSkBccC17{6N_|9th(u5Htiqg+EDmST&I3D-SGUaUtLj0KXQSpi_S}ri_x&+Oi{gc2IT)_pl9U3+xC|t|F>D)^RZ?YH1hlAvRP|wCWac z%5ZXCs8f~lFU`V@4k-2-=Y84Y9q1Wfp zt4FShrd@BTqiIE6g{m!wV=MP5i=0ITlE(qNq=7dODz}7av8&rc(mx$5%TI=}aB&CZ z&fF}yMDu%I9J8nx1jKa!3?AcFi5ULlS&_Cd@KoImKr5gX&