Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Broadcast Wav Extension Metadata #49

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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() => {
Expand Down
193 changes: 193 additions & 0 deletions src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>;

/// Reads eight bytes and interprets them as a little-endian 64-bit unsigned integer.
fn read_le_u64(&mut self) -> io::Result<u64>;

/// Reads four bytes and interprets them as a little-endian 32-bit IEEE float.
fn read_le_f32(&mut self) -> io::Result<f32>;
}
Expand Down Expand Up @@ -191,6 +194,16 @@ impl<R> ReadExt for R
(buf[1] as u32) << 8 | (buf[0] as u32) << 0)
}

#[inline(always)]
fn read_le_u64(&mut self) -> io::Result<u64> {
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<f32> {
self.read_le_u32().map(|u| unsafe { mem::transmute(u) })
Expand Down Expand Up @@ -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
Expand All @@ -290,6 +305,8 @@ pub struct ChunksReader<R: io::Read> {
reader: R,
/// the Wave format specification, if it has been read already
pub spec_ex: Option<WavSpecEx>,
/// the Broadcast Wave Extension Metadata
pub bext: Option<BwavExtMeta>,
/// when inside the main data state, keeps track of decoding and chunk
/// boundaries
pub data_state: Option<DataReadingState>,
Expand All @@ -315,6 +332,7 @@ impl<R: io::Read> ChunksReader<R> {
Ok(ChunksReader {
reader: reader,
spec_ex: None,
bext: None,
data_state: None,
})
}
Expand Down Expand Up @@ -399,6 +417,11 @@ impl<R: io::Read> ChunksReader<R> {
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 {
Expand Down Expand Up @@ -669,6 +692,75 @@ impl<R: io::Read> ChunksReader<R> {
Ok(())
}

fn read_bext_chunk(&mut self, chunk_len: u32) -> Result<BwavExtMeta> {
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
Expand Down Expand Up @@ -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 : <<Description of the sound sequenc>> 256 byes
pub description: String,
// ASCII : <<Name of the originator>> 32 bytes
pub originator: String,
// ASCII : <<Reference of the originator>> 32 bytes
pub originator_reference: String,
// ASCII : <<yyyy-mm-dd>> 10 bytes
// The separator may be a '-', '_', ':', ' ', or '.'
pub originator_date: String,
// ASCII : <<hh:mm:ss>> 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<f32>,
// Loudness range in LU (multiplied by 100)
pub loudness_range: Option<f32>,
// Maximum true peak level in dBTP (multiplied by 100)
pub max_true_peak: Option<f32>,
// Highest value of mementary loudness
// level in LUFS (multiplied by 100)
pub max_momentary_loudness: Option<f32>,
// Highest value of the short-term loudness level
// in LUFS (multiplied by 100)
pub max_shortterm_loudness: Option<f32>,

// << 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
Expand Down Expand Up @@ -804,6 +939,11 @@ impl<R> WavReader<R>
.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,
Expand Down Expand Up @@ -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")
Expand Down
Binary file added testsamples/pro_tools_bext.wav
Binary file not shown.
Binary file added testsamples/reaper_bext.wav
Binary file not shown.
Binary file added testsamples/wav_agent_bext.wav
Binary file not shown.