14 Commits

Author SHA1 Message Date
Jamie Hardt
5bd292e964 Bumped version 2020-11-23 22:39:08 -08:00
Jamie Hardt
34e473dc49 Implementation of complex formats 2020-11-23 22:38:10 -08:00
Jamie Hardt
358aa06f7c Removed validation methods
and added to main implementation
2020-11-23 13:16:28 -08:00
Jamie Hardt
8191eedf14 ADM anc channel structures 2020-11-23 11:11:12 -08:00
Jamie Hardt
521e9d0670 Reorganized source, comments 2020-11-23 00:23:17 -08:00
Jamie Hardt
dea68662f8 Comments and documentation 2020-11-22 23:32:25 -08:00
Jamie Hardt
0685e7baca Removed dead line 2020-11-22 22:38:53 -08:00
Jamie Hardt
123d971624 Comments 2020-11-22 22:38:47 -08:00
Jamie Hardt
f410cda9ed removed .DS_Store 2020-11-22 22:15:58 -08:00
Jamie Hardt
cf06b50fea Comment 2020-11-22 22:04:04 -08:00
Jamie Hardt
e8d679b725 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2020-11-22 22:02:50 -08:00
Jamie Hardt
e63ab6bffd Renamed some files 2020-11-22 22:02:45 -08:00
Jamie Hardt
271da75f90 Update ffprobe_media_tests.rs 2020-11-22 22:01:11 -08:00
Jamie Hardt
6c393d9958 Restructred test files 2020-11-22 21:36:40 -08:00
21 changed files with 747 additions and 359 deletions

9
Cargo.lock generated
View File

@@ -2,11 +2,12 @@
# It is not intended for manual editing.
[[package]]
name = "bwavfile"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"byteorder",
"encoding",
"serde_json",
"uuid",
]
[[package]]
@@ -107,3 +108,9 @@ dependencies = [
"ryu",
"serde",
]
[[package]]
name = "uuid"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"

View File

@@ -1,6 +1,6 @@
[package]
name = "bwavfile"
version = "0.1.3"
version = "0.1.4"
authors = ["Jamie Hardt <jamiehardt@me.com>"]
edition = "2018"
license = "MIT"
@@ -16,4 +16,5 @@ keywords = ["audio", "broadcast", "multimedia","smpte"]
[dependencies]
byteorder = "1.3.4"
encoding = "0.2.33"
uuid = "0.8.1"
serde_json = "1.0.59"

View File

@@ -4,9 +4,14 @@ use std::io::SeekFrom::{Start,};
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use super::chunks::WaveFmt;
use super::fmt::{WaveFmt};
use super::errors::Error;
/// Read audio frames
///
/// The inner reader is interpreted as a raw audio data
/// bitstream having a format specified by `format`.
///
#[derive(Debug)]
pub struct AudioFrameReader<R: Read + Seek> {
inner : R,
@@ -14,26 +19,53 @@ pub struct AudioFrameReader<R: Read + Seek> {
}
impl<R: Read + Seek> AudioFrameReader<R> {
/// Create a new AudioFrameReader, taking possession of a reader.
/// Create a new `AudioFrameReader`
///
/// ### Panics
///
/// This method does a few sanity checks on the provided format
/// parameter to confirm the `block_alignment` law is fulfilled
/// and the format tag is readable by this implementation (only
/// format 0x01 is supported at this time.)
pub fn new(inner: R, format: WaveFmt) -> Self {
assert!(format.block_alignment * 8 == format.bits_per_sample * format.channel_count,
"Unable to read audio frames from packed formats: block alignment is {}, should be {}",
format.block_alignment, (format.bits_per_sample / 8 ) * format.channel_count);
assert!(format.tag == 1, "Unsupported format tag {}", format.tag);
assert!(format.tag == 0x01 ,
"Unsupported format tag {:?}", format.tag);
AudioFrameReader { inner , format }
}
/// Locate the read position to a different frame
///
/// Seeks within the audio stream.
pub fn locate(&mut self, to :u64) -> Result<u64,Error> {
let position = to * self.format.block_alignment as u64;
let seek_result = self.inner.seek(Start(position))?;
Ok( seek_result / self.format.block_alignment as u64 )
}
/// Create a frame buffer sized to hold frames of the reader
///
/// This is a conveneince method that creates a `Vec<i32>` with
/// as many elements as there are channels in the underlying stream.
pub fn create_frame_buffer(&self) -> Vec<i32> {
vec![0i32; self.format.channel_count as usize]
}
/// Read a frame
///
/// A single frame is read from the audio stream and the read location
/// is advanced one frame.
///
/// ### Panics
///
/// The `buffer` must have a number of elements equal to the number of
/// channels and this method will panic if this is not the case.
pub fn read_integer_frame(&mut self, buffer:&mut [i32]) -> Result<u64,Error> {
assert!(buffer.len() as u16 == self.format.channel_count,
"read_integer_frame was called with a mis-sized buffer, expected {}, was {}",
@@ -54,5 +86,4 @@ impl<R: Read + Seek> AudioFrameReader<R> {
Ok( 1 )
}
}
}

82
src/bext.rs Normal file
View File

@@ -0,0 +1,82 @@
pub type LU = f32;
pub type LUFS = f32;
pub type Decibels = f32;
/**
* Broadcast-WAV metadata record.
*
* The `bext` record contains information about the original recording of the
* Wave file, including a longish (256 ASCII chars) description field,
* originator identification fields, creation calendar date and time, a
* sample-accurate recording time field, and a SMPTE UMID.
*
* For a Wave file to be a complaint "Broadcast-WAV" file, it must contain
* a `bext` metadata record.
*
* For reference on the structure and use of the BEXT record
* check out [EBU Tech 3285](https://tech.ebu.ch/docs/tech/tech3285.pdf).
*/
#[derive(Debug)]
pub struct Bext {
/// 256 ASCII character field with free text.
pub description: String,
/// Originating application.
pub originator: String,
/// Application-specific UID.
pub originator_reference: String,
/// Creation date in format `YYYY-MM-DD`.
pub origination_date: String,
/// Creation time in format `HH:MM:SS`.
pub origination_time: String,
/// Time of the start of this wave file, expressed as the number of samples
/// since local midnight.
pub time_reference: u64,
/// Bext chunk version.
///
/// Version 1 contains a UMID, version 2 contains a UMID and
/// loudness metadata.
pub version: u16,
/// SMPTE 330M UMID
///
///
/// This field is `None` if the version is less than 1.
pub umid: Option<[u8; 64]>,
/// Integrated loudness in LUFS.
///
/// This field is `None` if the version is less than 2.
pub loudness_value: Option<LUFS>,
/// Loudness range in LU.
///
/// This field is `None` if the version is less than 2.
pub loudness_range: Option<LU>,
/// Maximum True Peak Level in decibels True Peak.
///
/// This field is `None` if the version is less than 2.
pub max_true_peak_level: Option<Decibels>,
/// Maximum momentary loudness in LUFS.
///
/// This field is `None` if the version is less than 2.
pub max_momentary_loudness: Option<LUFS>,
/// Maximum short-term loudness in LUFS.
///
/// This field is `None` if the version is less than 2.
pub max_short_term_loudness: Option<LUFS>,
// 180 bytes of nothing
/// Coding History.
pub coding_history: String
}

View File

@@ -1,7 +1,5 @@
use std::io::{Read, Write};
use super::errors::Error as ParserError;
use encoding::{DecoderTrap, EncoderTrap};
use encoding::{Encoding};
use encoding::all::ASCII;
@@ -9,180 +7,11 @@ use encoding::all::ASCII;
use byteorder::LittleEndian;
use byteorder::{ReadBytesExt, WriteBytesExt};
/**
* References:
* - http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/multichaudP.pdf
*/
#[derive(PartialEq)]
enum FormatTags {
Integer = 0x0001,
Float = 0x0003,
Extensible = 0xFFFE
}
use uuid::Uuid;
const PCM_SUBTYPE_UUID: [u8; 16] = [0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x10,
0x80, 0x00, 0x00, 0xaa,
0x00, 0x38, 0x9b, 0x71];
const FLOAT_SUBTYPE_UUID: [u8; 16] = [0x00, 0x00, 0x00, 0x03,
0x00, 0x00, 0x00, 0x10,
0x80, 0x00, 0x00, 0xaa,
0x00, 0x38, 0x9b, 0x71];
/*
https://docs.microsoft.com/en-us/windows-hardware/drivers/audio/subformat-guids-for-compressed-audio-formats
http://dream.cs.bath.ac.uk/researchdev/wave-ex/bformat.html
These are from http://dream.cs.bath.ac.uk/researchdev/wave-ex/mulchaud.rtf
*/
#[derive(Debug)]
pub enum WaveFmtExtendedChannelMask {
FrontLeft = 0x1,
FrontRight = 0x2,
FrontCenter = 0x4,
LowFrequency = 0x8,
BackLeft = 0x10,
BackRight = 0x20,
FrontCenterLeft = 0x40,
FrontCenterRight = 0x80,
BackCenter = 0x100,
SideLeft = 0x200,
SideRight = 0x400,
TopCenter = 0x800,
TopFrontLeft = 0x1000,
TopFrontCenter = 0x2000,
TopFrontRight = 0x4000,
TopBackLeft = 0x8000,
TopBackCenter = 0x10000,
TopBackRight = 0x20000
}
/**
* Extended Wave Format
*
* https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible
*/
#[derive(Debug)]
pub struct WaveFmtExtended {
/// Valid bits per sample
pub valid_bits_per_sample : u16,
/// Channel mask
///
/// Identifies the speaker assignment for each channel in the file
pub channel_mask : WaveFmtExtendedChannelMask,
/// Codec GUID
///
/// Identifies the codec of the audio stream
pub type_guid : [u8; 16],
}
/**
* WAV file data format record.
*
* The `fmt` record contains essential information describing the binary
* structure of the data segment of the WAVE file, such as sample
* rate, sample binary format, channel count, etc.
*
*/
#[derive(Debug)]
pub struct WaveFmt {
/// A tag identifying the codec in use.
///
/// If this is 0xFFFE, the codec will be identified by a GUID
/// in `extended_format`
pub tag: u16,
/// Count of audio channels in each frame
pub channel_count: u16,
/// Sample rate of the audio data
pub sample_rate: u32,
/// Count of bytes per second
///
/// By rule, this is `block_alignment * sample_rate`
pub bytes_per_second: u32,
/// Count of bytes per audio frame
///
/// By rule, this is `channel_count * bits_per_sample / 8`
pub block_alignment: u16,
/// Count of bits stored in the file per sample
pub bits_per_sample: u16,
/// Extended format description
///
/// Additional format metadata if `channel_count` is greater than 2,
/// or if certain codecs are used.
pub extended_format: Option<WaveFmtExtended>
}
impl WaveFmt {
/// Create a new integer PCM format `WaveFmt`
pub fn new_pcm(sample_rate: u32, bits_per_sample: u16, channel_count: u16) -> Self {
let container_bits_per_sample = bits_per_sample + (bits_per_sample % 8);
let container_bytes_per_sample= container_bits_per_sample / 8;
let tag :u16 = match channel_count {
0 => panic!("Error"),
1..=2 => FormatTags::Integer as u16,
_ => FormatTags::Extensible as u16,
};
WaveFmt {
tag,
channel_count,
sample_rate,
bytes_per_second: container_bytes_per_sample as u32 * sample_rate * channel_count as u32,
block_alignment: container_bytes_per_sample * channel_count,
bits_per_sample: container_bits_per_sample,
extended_format: None
}
}
}
/**
* Broadcast-WAV metadata record.
*
* The `bext` record contains information about the original recording of the
* Wave file, including a longish (256 ASCII chars) description field,
* originator identification fields, creation calendar date and time, a
* sample-accurate recording time field, and a SMPTE UMID.
*
* For a Wave file to be a complaint "Broadcast-WAV" file, it must contain
* a `bext` metadata record.
*
* For reference on the structure and use of the BEXT record
* check out [EBU Tech 3285](https://tech.ebu.ch/docs/tech/tech3285.pdf).
*/
#[derive(Debug)]
pub struct Bext {
pub description: String,
pub originator: String,
pub originator_reference: String,
pub origination_date: String,
pub origination_time: String,
pub time_reference: u64,
pub version: u16,
pub umid: Option<[u8; 64]>,
pub loudness_value: Option<f32>,
pub loudness_range: Option<f32>,
pub max_true_peak_level: Option<f32>,
pub max_momentary_loudness: Option<f32>,
pub max_short_term_loudness: Option<f32>,
// 180 bytes of nothing
pub coding_history: String
}
use super::errors::Error as ParserError;
use super::fmt::{WaveFmt, WaveFmtExtended};
use super::bext::Bext;
pub trait ReadBWaveChunks: Read {
fn read_bext(&mut self) -> Result<Bext, ParserError>;
@@ -198,7 +27,7 @@ pub trait WriteBWaveChunks: Write {
impl<T> WriteBWaveChunks for T where T: Write {
fn write_wave_fmt(&mut self, format : &WaveFmt) -> Result<(), ParserError> {
self.write_u16::<LittleEndian>(format.tag)?;
self.write_u16::<LittleEndian>(format.tag as u16 )?;
self.write_u16::<LittleEndian>(format.channel_count)?;
self.write_u32::<LittleEndian>(format.sample_rate)?;
self.write_u32::<LittleEndian>(format.bytes_per_second)?;
@@ -258,14 +87,34 @@ impl<T> WriteBWaveChunks for T where T: Write {
impl<T> ReadBWaveChunks for T where T: Read {
fn read_wave_fmt(&mut self) -> Result<WaveFmt, ParserError> {
let tag_value : u16;
Ok(WaveFmt {
tag: self.read_u16::<LittleEndian>()?,
tag: {
tag_value = self.read_u16::<LittleEndian>()?;
tag_value
},
channel_count: self.read_u16::<LittleEndian>()?,
sample_rate: self.read_u32::<LittleEndian>()?,
bytes_per_second: self.read_u32::<LittleEndian>()?,
block_alignment: self.read_u16::<LittleEndian>()?,
bits_per_sample: self.read_u16::<LittleEndian>()?,
extended_format: None
extended_format: {
if tag_value == 0xFFFE {
let cb_size = self.read_u16::<LittleEndian>()?;
assert!(cb_size >= 22, "Format extension is not correct size");
Some(WaveFmtExtended {
valid_bits_per_sample: self.read_u16::<LittleEndian>()?,
channel_mask: self.read_u32::<LittleEndian>()?,
type_guid: {
let mut buf : [u8; 16] = [0; 16];
self.read_exact(&mut buf)?;
Uuid::from_slice(&buf)?
}
})
} else {
None
}
}
})
}
@@ -315,11 +164,36 @@ impl<T> ReadBWaveChunks for T where T: Read {
if version > 1 { Some(val) } else { None }
},
coding_history: {
for _ in 0..=180 { self.read_u8()?; }
for _ in 0..180 { self.read_u8()?; }
let mut buf = vec![];
self.read_to_end(&mut buf)?;
ASCII.decode(&buf, DecoderTrap::Ignore).expect("Error decoding text")
}
})
}
}
#[test]
fn test_read_51_wav() {
use super::fmt::ChannelMask;
use super::common_format::CommonFormat;
let path = "tests/media/pt_24bit_51.wav";
let mut w = super::wavereader::WaveReader::open(path).unwrap();
let format = w.format().unwrap();
assert_eq!(format.tag, 0xFFFE);
assert_eq!(format.channel_count, 6);
assert_eq!(format.sample_rate, 48000);
let extended = format.extended_format.unwrap();
assert_eq!(extended.valid_bits_per_sample, 24);
let channels = ChannelMask::channels(extended.channel_mask, format.channel_count);
assert_eq!(channels, [ChannelMask::FrontLeft, ChannelMask::FrontRight,
ChannelMask::FrontCenter, ChannelMask::LowFrequency,
ChannelMask::BackLeft, ChannelMask::BackRight]);
assert_eq!(format.common_format(), CommonFormat::IntegerPCM);
}

86
src/common_format.rs Normal file
View File

@@ -0,0 +1,86 @@
use uuid::Uuid;
/**
* References:
* - http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/multichaudP.pdf
*/
// http://dream.cs.bath.ac.uk/researchdev/wave-ex/bformat.html
const BASIC_PCM: u16 = 0x0001;
const BASIC_FLOAT: u16 = 0x0003;
const BASIC_MPEG: u16 = 0x0050;
const BASIC_EXTENDED: u16 = 0xFFFE;
/* RC 2361 §4:
WAVE Format IDs are converted to GUIDs by inserting the hexadecimal
value of the WAVE Format ID into the XXXXXXXX part of the following
template: {XXXXXXXX-0000-0010-8000-00AA00389B71}. For example, a WAVE
Format ID of 123 has the GUID value of {00000123-0000-0010-8000-
00AA00389B71}.
*/
const UUID_PCM: Uuid = Uuid::from_bytes([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71]);
const UUID_FLOAT: Uuid = Uuid::from_bytes([0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71]);
const UUID_MPEG: Uuid = Uuid::from_bytes([0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71]);
const UUID_BFORMAT_PCM: Uuid = Uuid::from_bytes([0x01, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11,
0x86, 0x44, 0xc8, 0xc1, 0xca, 0x00, 0x00, 0x00]);
const UUID_BFORMAT_FLOAT: Uuid = Uuid::from_bytes([0x03, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11,
0x86, 0x44, 0xc8, 0xc1, 0xca, 0x00, 0x00, 0x00]);
fn uuid_from_basic_tag(tag: u16) -> Uuid {
let tail : [u8; 6] = [0x00,0xaa,0x00,0x38,0x9b,0x71];
Uuid::from_fields_le(tag as u32, 0x0000, 0x0010, &tail).unwrap()
}
/// Sample format of the Wave file.
///
///
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum CommonFormat {
IntegerPCM,
IeeeFloatPCM,
Mpeg,
AmbisonicBFormatIntegerPCM,
AmbisonicBFormatIeeeFloatPCM,
UnknownBasic(u16),
UnknownExtended(Uuid),
}
impl CommonFormat {
pub fn make(basic: u16, uuid: Option<Uuid>) -> Self {
match (basic, uuid) {
(BASIC_PCM, _) => Self::IntegerPCM,
(BASIC_FLOAT, _) => Self::IeeeFloatPCM,
(BASIC_MPEG, _) => Self::Mpeg,
(BASIC_EXTENDED, Some(UUID_PCM)) => Self::IntegerPCM,
(BASIC_EXTENDED, Some(UUID_FLOAT))=> Self::IeeeFloatPCM,
(BASIC_EXTENDED, Some(UUID_BFORMAT_PCM)) => Self::AmbisonicBFormatIntegerPCM,
(BASIC_EXTENDED, Some(UUID_BFORMAT_FLOAT)) => Self::AmbisonicBFormatIeeeFloatPCM,
(BASIC_EXTENDED, Some(x)) => CommonFormat::UnknownExtended(x),
(x, _) => CommonFormat::UnknownBasic(x)
}
}
pub fn take(self) -> (u16, Uuid) {
match self {
Self::IntegerPCM => (BASIC_PCM, UUID_PCM),
Self::IeeeFloatPCM => (BASIC_FLOAT, UUID_FLOAT),
Self::Mpeg => (BASIC_MPEG, UUID_MPEG),
Self::AmbisonicBFormatIntegerPCM => (BASIC_EXTENDED, UUID_BFORMAT_PCM),
Self::AmbisonicBFormatIeeeFloatPCM => (BASIC_EXTENDED, UUID_BFORMAT_FLOAT),
Self::UnknownBasic(x) => ( x, uuid_from_basic_tag(x) ),
Self::UnknownExtended(x) => ( BASIC_EXTENDED, x)
}
}
}

View File

@@ -1,6 +1,8 @@
use std::io;
use super::fourcc::FourCC;
use uuid;
/// Errors returned by methods in this crate.
#[derive(Debug)]
pub enum Error {
@@ -8,6 +10,9 @@ pub enum Error {
/// An `io::Error` occurred
IOError(io::Error),
/// An error occured reading a tag UUID
UuidError(uuid::Error),
/// The file does not begin with a recognized WAVE header
HeaderNotRecognized,
@@ -34,7 +39,8 @@ pub enum Error {
InsufficientDS64Reservation {expected: u64, actual: u64},
/// The file is not optimized for writing new data
DataChunkNotPreparedForAppend
DataChunkNotPreparedForAppend,
}
@@ -42,4 +48,10 @@ impl From<io::Error> for Error {
fn from(error: io::Error) -> Error {
Error::IOError(error)
}
}
impl From <uuid::Error> for Error {
fn from(error: uuid::Error) -> Error {
Error::UuidError(error)
}
}

242
src/fmt.rs Normal file
View File

@@ -0,0 +1,242 @@
use std::convert::TryFrom;
use uuid::Uuid;
use super::errors::Error;
use super::common_format::CommonFormat;
/// ADM Audio ID record
///
/// This structure relates a channel in the wave file to either a common ADM
/// channel definition or further definition in the WAV file's ADM metadata
/// chunk.
///
/// An individual channel in a WAV file can have multiple Audio IDs in an ADM
/// AudioProgramme.
///
/// See BS.2088-1 § 8, also BS.2094, also blahblahblah...
pub struct ADMAudioID {
track_uid: [char; 12],
channel_format_ref: [char; 14],
pack_ref: [char; 11]
}
/// Describes a single channel in a WAV file.
pub struct ChannelDescriptor {
/// Index, the offset of this channel's samples in one frame.
index: u16,
/// Channel assignment
///
/// This is either implied (in the case of mono or stereo wave files) or
/// explicitly given in `WaveFormatExtentended` for files with more tracks.
speaker: ChannelMask,
/// ADM audioTrackUIDs
adm_track_audio_ids: Vec<ADMAudioID>,
}
/*
https://docs.microsoft.com/en-us/windows-hardware/drivers/audio/subformat-guids-for-compressed-audio-formats
These are from http://dream.cs.bath.ac.uk/researchdev/wave-ex/mulchaud.rtf
*/
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ChannelMask {
DirectOut = 0x0,
FrontLeft = 0x1,
FrontRight = 0x2,
FrontCenter = 0x4,
LowFrequency = 0x8,
BackLeft = 0x10,
BackRight = 0x20,
FrontCenterLeft = 0x40,
FrontCenterRight = 0x80,
BackCenter = 0x100,
SideLeft = 0x200,
SideRight = 0x400,
TopCenter = 0x800,
TopFrontLeft = 0x1000,
TopFrontCenter = 0x2000,
TopFrontRight = 0x4000,
TopBackLeft = 0x8000,
TopBackCenter = 0x10000,
TopBackRight = 0x20000,
}
impl From<u32> for ChannelMask {
fn from(value: u32) -> Self {
match value {
0x1 => Self::FrontLeft,
0x2 => Self::FrontRight,
0x4 => Self::FrontCenter,
0x8 => Self::LowFrequency,
0x10 => Self::BackLeft,
0x20 => Self::BackRight,
0x40 => Self::FrontCenterLeft,
0x80 => Self::FrontCenterRight,
0x100 => Self::BackCenter,
0x200 => Self::SideLeft,
0x400 => Self::SideRight,
0x800 => Self::TopCenter,
0x1000 => Self::TopFrontLeft,
0x2000 => Self::TopFrontCenter,
0x4000 => Self::TopFrontRight,
0x8000 => Self::TopBackLeft,
0x10000 => Self::TopBackCenter,
0x20000 => Self::TopBackRight,
_ => Self::DirectOut
}
}
}
impl ChannelMask {
pub fn channels(input_mask : u32, channel_count: u16) -> Vec<ChannelMask> {
let reserved_mask = 0xfff2_0000_u32;
if (input_mask & reserved_mask) > 0 {
vec![ ChannelMask::DirectOut ; channel_count as usize ]
} else {
(0..18).map(|i| 1 << i )
.filter(|mask| mask & input_mask > 0)
.map(|mask| Into::<ChannelMask>::into(mask))
.collect()
}
}
}
/**
* Extended Wave Format
*
* https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible
*/
#[derive(Debug, Copy, Clone)]
pub struct WaveFmtExtended {
/// Valid bits per sample
pub valid_bits_per_sample : u16,
/// Channel mask
///
/// Identifies the speaker assignment for each channel in the file
pub channel_mask : u32,
/// Codec GUID
///
/// Identifies the codec of the audio stream
pub type_guid : Uuid,
}
/**
* WAV file data format record.
*
* The `fmt` record contains essential information describing the binary
* structure of the data segment of the WAVE file, such as sample
* rate, sample binary format, channel count, etc.
*
*/
#[derive(Debug, Copy, Clone)]
pub struct WaveFmt {
/// A tag identifying the codec in use.
///
/// If this is 0xFFFE, the codec will be identified by a GUID
/// in `extended_format`
pub tag: u16,
/// Count of audio channels in each frame
pub channel_count: u16,
/// Sample rate of the audio data
pub sample_rate: u32,
/// Count of bytes per second
///
/// By rule, this is `block_alignment * sample_rate`
pub bytes_per_second: u32,
/// Count of bytes per audio frame
///
/// By rule, this is `channel_count * bits_per_sample / 8`
pub block_alignment: u16,
/// Count of bits stored in the file per sample
pub bits_per_sample: u16,
/// Extended format description
///
/// Additional format metadata if `channel_count` is greater than 2,
/// or if certain codecs are used.
pub extended_format: Option<WaveFmtExtended>
}
impl WaveFmt {
/// Create a new integer PCM format `WaveFmt`
pub fn new_pcm(sample_rate: u32, bits_per_sample: u16, channel_count: u16) -> Self {
let container_bits_per_sample = bits_per_sample + (bits_per_sample % 8);
let container_bytes_per_sample= container_bits_per_sample / 8;
let tag : u16 = match channel_count {
1..=2 => 0x01,
x if x > 2 => 0xFFFE,
x => panic!("Invalid channel count {}", x)
};
WaveFmt {
tag,
channel_count,
sample_rate,
bytes_per_second: container_bytes_per_sample as u32 * sample_rate * channel_count as u32,
block_alignment: container_bytes_per_sample * channel_count,
bits_per_sample: container_bits_per_sample,
extended_format: None
}
}
pub fn common_format(&self) -> CommonFormat {
CommonFormat::make( self.tag, self.extended_format.map(|ext| ext.type_guid))
}
pub fn channels(&self) -> Vec<ChannelDescriptor> {
match self.channel_count {
1 => vec![
ChannelDescriptor {
index: 0,
speaker: ChannelMask::FrontCenter,
adm_track_audio_ids: vec![]
}
],
2 => vec![
ChannelDescriptor {
index: 0,
speaker: ChannelMask::FrontLeft,
adm_track_audio_ids: vec![]
},
ChannelDescriptor {
index: 1,
speaker: ChannelMask::FrontRight,
adm_track_audio_ids: vec![]
}
],
x if x > 2 => {
let channel_mask = self.extended_format.map(|x| x.channel_mask).unwrap_or(0);
let channels = ChannelMask::channels(channel_mask, self.channel_count);
let channels_expanded = channels.iter().chain(std::iter::repeat(&ChannelMask::DirectOut));
(0..self.channel_count)
.zip(channels_expanded)
.map(|(n,chan)| ChannelDescriptor {
index: n,
speaker: *chan,
adm_track_audio_ids: vec![]
}).collect()
},
x => panic!("Channel count ({}) was illegal!", x),
}
}
}

View File

@@ -1,23 +1,63 @@
//
/*!
# bwavfile
Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support
(Note: This crate is still in an alpha or pre-alpha stage of development. Reading of
__(Note: This crate is still in an alpha or pre-alpha stage of development. Reading of
files works however the interfaces may change significantly. Stay up-to-date on the
status of this project at [Github][github].)
status of this project at [Github][github].)__
## Objectives and Roadmap
This package aims to support read and writing any kind of WAV file you are likely
to encounter in a professional audio, motion picture production, broadcast, or music
production.
Apps we test against:
- Avid Pro Tools
- FFMpeg
- Audacity
Wave features we want to support with maximum reliability and ease of use:
- Large file size, RF64 support
- Multichannel audio formats
- Embedded metadata
In addition to reading the audio, we want to support all of the different
metadata planes you are liable to need to use.
- Broadcast-WAV metadata (including the SMPTE UMID and EBU v2 extensions)
- iXML Production recorder metadata
- ADM XML (with associated `chna` mappings)
- Dolby metadata block
Things that are _not_ necessarily in the scope of this package:
- Broad codec support. There are a little more than one-hundred
[registered wave codecs][rfc3261], but because this library is targeting
professional formats being created today, we only plan on supporting
two of them: tag 0x0001 (Integer Linear PCM) and tag 0x0003 (IEEE Float
Linear PCM).
- Music library metadata. There are several packages that can read ID3
metadata and it's not particuarly common in wave files in any case. INFO
metadata is more common though in professional applications it tends not
to be used by many applications.
## Resources
### Implementation of Broadcast Wave Files
- [EBU Tech 3285][ebu3285] (May 2011), "Specification of the Broadcast Wave Format (BWF)"
- [Supplement 1](https://tech.ebu.ch/docs/tech/tech3285s1.pdf) (July 1997): MPEG Audio
- [EBU Rec 68](https://tech.ebu.ch/docs/r/r068.pdf): Signal modulation and format constraints
### Implementation of 64-bit Wave Files
- [ITU-R 2088][itu2088] (October 2019), "Long-form file format for the international exchange of audio programme materials with metadata"
- Presently in force, adopted by the EBU in [EBU Tech 3306v2][ebu3306v2] (June 2018).
- Presently in force, adopted by the EBU in [EBU Tech 3306v2][ebu3306v2] (June 2018).
- [EBU Tech 3306v1][ebu3306v1] (July 2009), "MBWF / RF64: An extended File Format for Audio"
- No longer in force, however long-established.
- No longer in force, however long-established.
### Implementation of Wave format `fmt` chunk
@@ -29,9 +69,21 @@ status of this project at [Github][github].)
- [RFC 3261][rfc3261] (June 1998) "WAVE and AVI Codec Registries"
- [Peter Kabal, McGill University](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html)
- [Multimedia Programming Interface and Data Specifications 1.0](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf)
IBM Corporation and Microsoft Corporation, (August 1991)
(August 1991), IBM Corporation and Microsoft Corporation
### Formatting of Specific Metadatums
- [iXML Metadata Specification](http://www.gallery.co.uk/ixml/) (April 2019)
- EBU 3285 Supplements:
- [Supplement 2](https://tech.ebu.ch/docs/tech/tech3285s2.pdf) (July 2001): Quality chunk and cuesheet
- [Supplement 3](https://tech.ebu.ch/docs/tech/tech3285s3.pdf) (July 2001): Peak Metadata
- [Supplement 4](https://tech.ebu.ch/docs/tech/tech3285s4.pdf) (April 2003): Link Metadata
- [Supplement 5](https://tech.ebu.ch/docs/tech/tech3285s5.pdf) (May 2018): ADM Metadata
- [Supplement 6](https://tech.ebu.ch/docs/tech/tech3285s6.pdf) (October 2009): Dolby Metadata
- [EBU Tech R099](https://tech.ebu.ch/docs/r/r099.pdf) (October 2011) "Unique Source Identifier (USID) for use in the
<OriginatorReference> field of the Broadcast Wave Format"
- [EBU Tech R098](https://tech.ebu.ch/docs/r/r098.pdf) (1999) "Format for the <CodingHistory> field in Broadcast Wave Format files, BWF"
[ebu3285]: https://tech.ebu.ch/docs/tech/tech3285.pdf
[ebu3306v1]: https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf
[ebu3306v2]: https://tech.ebu.ch/docs/tech/tech3306.pdf
@@ -48,20 +100,27 @@ status of this project at [Github][github].)
extern crate encoding;
extern crate byteorder;
extern crate uuid;
mod parser;
mod fourcc;
mod errors;
mod common_format;
mod parser;
mod validation;
mod raw_chunk_reader;
mod audio_frame_reader;
mod chunks;
mod bext;
mod fmt;
mod wavereader;
mod wavewriter;
pub use errors::Error;
pub use wavereader::{WaveReader};
pub use chunks::{WaveFmt,Bext};
pub use errors::Error;
pub use bext::Bext;
pub use fmt::{WaveFmt, WaveFmtExtended, ChannelDescriptor};
pub use common_format::CommonFormat;
pub use audio_frame_reader::AudioFrameReader;

View File

@@ -103,7 +103,6 @@ impl<R: Read + Seek> Iterator for Parser<R> {
fn next(&mut self) -> Option<Event> {
let (event, next_state) = self.advance();
//println!("{:?}", event);
self.state = next_state;
return event;
}

View File

@@ -1,144 +0,0 @@
use super::parser::{Parser};
use super::fourcc::{FourCC, FMT__SIG,DATA_SIG, BEXT_SIG, JUNK_SIG, FLLR_SIG};
use super::errors::Error as ParserError;
use super::wavereader::{WaveReader};
use std::io::{Read,Seek};
impl<R:Read + Seek> WaveReader<R> {
/**
* Returns without `Err` if the source meets the minimum standard of
* readability by a permissive client:
* 1. `fmt` chunk and `data` chunk are present
* 1. `fmt` chunk appears before `data` chunk
*/
pub fn validate_readable(&mut self) -> Result<(), ParserError> {
let (fmt_pos, _) = self.get_chunk_extent_at_index(FMT__SIG, 0)?;
let (data_pos, _) = self.get_chunk_extent_at_index(DATA_SIG, 0)?;
if fmt_pos < data_pos {
Ok(())
} else {
Err( ParserError::FmtChunkAfterData)
}
}
/**
* Validate minimal WAVE file
*
* Returns without `Err` the source is `validate_readable` AND
*
* - Contains _only_ a `fmt` chunk and `data` chunk, with no other chunks present
* - is not an RF64/BW64
*
* Some clients require a WAVE file to only contain format and data without any other
* metadata and this function is provided to validate this condition.
*
* ```
* # use bwavfile::WaveReader;
*
* let mut w = WaveReader::open("tests/media/ff_minimal.wav").unwrap();
* w.validate_minimal().expect("Minimal wav did not validate not minimal!");
* ```
*
* ```
* # use bwavfile::WaveReader;
*
* let mut x = WaveReader::open("tests/media/pt_24bit_51.wav").unwrap();
* x.validate_minimal().expect_err("Complex WAV validated minimal!");
* ```
*/
pub fn validate_minimal(&mut self) -> Result<(), ParserError> {
self.validate_readable()?;
let chunk_fourccs : Vec<FourCC> = Parser::make(&mut self.inner)?
.into_chunk_list()?.iter().map(|c| c.signature ).collect();
if chunk_fourccs == vec![FMT__SIG, DATA_SIG] {
Ok(())
} else {
Err( ParserError::NotMinimalWaveFile )
}
}
/**
* Validate Broadcast-WAVE file format
*
* Returns without `Err` if `validate_readable()` and file contains a
* Broadcast-WAV metadata record (a `bext` chunk).
*
* ```
* # use bwavfile::WaveReader;
*
* let mut w = WaveReader::open("tests/media/ff_bwav_stereo.wav").unwrap();
* w.validate_broadcast_wave().expect("BWAVE file did not validate BWAVE");
*
* let mut x = WaveReader::open("tests/media/pt_24bit.wav").unwrap();
* x.validate_broadcast_wave().expect("BWAVE file did not validate BWAVE");
*
* let mut y = WaveReader::open("tests/media/audacity_16bit.wav").unwrap();
* y.validate_broadcast_wave().expect_err("Plain WAV file DID validate BWAVE");
* ```
*/
pub fn validate_broadcast_wave(&mut self) -> Result<(), ParserError> {
self.validate_readable()?;
let (_, _) = self.get_chunk_extent_at_index(BEXT_SIG, 0)?;
Ok(())
}
/**
* Verify data is aligned to a block boundary
*
* Returns without `Err` if `validate_readable()` and the start of the
* `data` chunk's content begins at 0x4000.
*/
pub fn validate_data_chunk_alignment(&mut self) -> Result<() , ParserError> {
self.validate_readable()?;
let (start, _) = self.get_chunk_extent_at_index(DATA_SIG, 0)?;
if start == 0x4000 {
Ok(())
} else {
Err(ParserError::DataChunkNotAligned)
}
}
/**
* Returns without `Err` if:
* - `validate_readable()`
* - there is a `JUNK` or `FLLR` immediately at the beginning of the chunk
* list adequately large enough to be overwritten by a `ds64` (96 bytes)
* - `data` is the final chunk
*/
pub fn validate_prepared_for_append(&mut self) -> Result<(), ParserError> {
self.validate_readable()?;
let chunks = Parser::make(&mut self.inner)?.into_chunk_list()?;
let ds64_space_required = 92;
let eligible_filler_chunks = chunks.iter()
.take_while(|c| c.signature == JUNK_SIG || c.signature == FLLR_SIG);
let filler = eligible_filler_chunks
.enumerate()
.fold(0, |accum, (n, item)| if n == 0 { accum + item.length } else {accum + item.length + 8});
if filler < ds64_space_required {
Err(ParserError::InsufficientDS64Reservation {expected: ds64_space_required, actual: filler})
} else {
let data_pos = chunks.iter().position(|c| c.signature == DATA_SIG);
match data_pos {
Some(p) if p == chunks.len() - 1 => Ok(()),
_ => Err(ParserError::DataChunkNotPreparedForAppend)
}
}
}
}

View File

@@ -2,14 +2,15 @@
use std::fs::File;
use super::parser::Parser;
use super::fourcc::{FourCC, FMT__SIG, BEXT_SIG, DATA_SIG};
use super::fourcc::{FourCC, FMT__SIG,DATA_SIG, BEXT_SIG, JUNK_SIG, FLLR_SIG};
use super::errors::Error as ParserError;
use super::raw_chunk_reader::RawChunkReader;
use super::chunks::{WaveFmt, Bext};
use super::fmt::WaveFmt;
use super::bext::Bext;
use super::audio_frame_reader::AudioFrameReader;
use super::chunks::ReadBWaveChunks;
//use super::validation;
//use std::io::SeekFrom::{Start};
use std::io::{Read, Seek};
@@ -130,6 +131,142 @@ impl<R: Read + Seek> WaveReader<R> {
pub fn broadcast_extension(&mut self) -> Result<Bext, ParserError> {
self.chunk_reader(BEXT_SIG, 0)?.read_bext()
}
/**
* Validate file is readable.
*
* `Ok(())` if the source meets the minimum standard of
* readability by a permissive client:
* - `fmt` chunk and `data` chunk are present
* - `fmt` chunk appears before `data` chunk
*/
pub fn validate_readable(&mut self) -> Result<(), ParserError> {
let (fmt_pos, _) = self.get_chunk_extent_at_index(FMT__SIG, 0)?;
let (data_pos, _) = self.get_chunk_extent_at_index(DATA_SIG, 0)?;
if fmt_pos < data_pos {
Ok(())
} else {
Err( ParserError::FmtChunkAfterData)
}
}
/**
* Validate minimal WAVE file.
*
* `Ok(())` if the source is `validate_readable()` AND
*
* - Contains _only_ a `fmt` chunk and `data` chunk, with no other chunks present
* - is not an RF64/BW64
*
* Some clients require a WAVE file to only contain format and data without any other
* metadata and this function is provided to validate this condition.
*
* ### Examples
*
* ```
* # use bwavfile::WaveReader;
*
* let mut w = WaveReader::open("tests/media/ff_minimal.wav").unwrap();
* w.validate_minimal().expect("Minimal wav did not validate not minimal!");
* ```
*
* ```
* # use bwavfile::WaveReader;
*
* let mut x = WaveReader::open("tests/media/pt_24bit_51.wav").unwrap();
* x.validate_minimal().expect_err("Complex WAV validated minimal!");
* ```
*/
pub fn validate_minimal(&mut self) -> Result<(), ParserError> {
self.validate_readable()?;
let chunk_fourccs : Vec<FourCC> = Parser::make(&mut self.inner)?
.into_chunk_list()?.iter().map(|c| c.signature ).collect();
if chunk_fourccs == vec![FMT__SIG, DATA_SIG] {
Ok(())
} else {
Err( ParserError::NotMinimalWaveFile )
}
}
/**
* Validate Broadcast-WAVE file format
*
* Returns `Ok(())` if `validate_readable()` and file contains a
* Broadcast-WAV metadata record (a `bext` chunk).
*
* ### Examples
*
* ```
* # use bwavfile::WaveReader;
*
* let mut w = WaveReader::open("tests/media/ff_bwav_stereo.wav").unwrap();
* w.validate_broadcast_wave().expect("BWAVE file did not validate BWAVE");
*
* let mut x = WaveReader::open("tests/media/pt_24bit.wav").unwrap();
* x.validate_broadcast_wave().expect("BWAVE file did not validate BWAVE");
*
* let mut y = WaveReader::open("tests/media/audacity_16bit.wav").unwrap();
* y.validate_broadcast_wave().expect_err("Plain WAV file DID validate BWAVE");
* ```
*/
pub fn validate_broadcast_wave(&mut self) -> Result<(), ParserError> {
self.validate_readable()?;
let (_, _) = self.get_chunk_extent_at_index(BEXT_SIG, 0)?;
Ok(())
}
/**
* Verify data is aligned to a block boundary.
*
* Returns `Ok(())` if `validate_readable()` and the start of the
* `data` chunk's content begins at 0x4000.
*/
pub fn validate_data_chunk_alignment(&mut self) -> Result<() , ParserError> {
self.validate_readable()?;
let (start, _) = self.get_chunk_extent_at_index(DATA_SIG, 0)?;
if start == 0x4000 {
Ok(())
} else {
Err(ParserError::DataChunkNotAligned)
}
}
/**
* Verify audio data can be appended immediately to this file.
*
* Returns `Ok(())` if:
* - `validate_readable()`
* - there is a `JUNK` or `FLLR` immediately at the beginning of the chunk
* list adequately large enough to be overwritten by a `ds64` (92 bytes)
* - `data` is the final chunk
*/
pub fn validate_prepared_for_append(&mut self) -> Result<(), ParserError> {
self.validate_readable()?;
let chunks = Parser::make(&mut self.inner)?.into_chunk_list()?;
let ds64_space_required = 92;
let eligible_filler_chunks = chunks.iter()
.take_while(|c| c.signature == JUNK_SIG || c.signature == FLLR_SIG);
let filler = eligible_filler_chunks
.enumerate()
.fold(0, |accum, (n, item)| if n == 0 { accum + item.length } else {accum + item.length + 8});
if filler < ds64_space_required {
Err(ParserError::InsufficientDS64Reservation {expected: ds64_space_required, actual: filler})
} else {
let data_pos = chunks.iter().position(|c| c.signature == DATA_SIG);
match data_pos {
Some(p) if p == chunks.len() - 1 => Ok(()),
_ => Err(ParserError::DataChunkNotPreparedForAppend)
}
}
}
}
impl<R:Read+Seek> WaveReader<R> { /* Private Implementation */
@@ -139,7 +276,7 @@ impl<R:Read+Seek> WaveReader<R> { /* Private Implementation */
Ok( RawChunkReader::new(&mut self.inner, start, length) )
}
pub fn get_chunk_extent_at_index(&mut self, fourcc: FourCC, index: u32) -> Result<(u64,u64), ParserError> {
fn get_chunk_extent_at_index(&mut self, fourcc: FourCC, index: u32) -> Result<(u64,u64), ParserError> {
let p = Parser::make(&mut self.inner)?.into_chunk_list()?;
if let Some(chunk) = p.iter().filter(|item| item.signature == fourcc).nth(index as usize) {

View File

@@ -3,7 +3,9 @@ use std::fs::File;
use std::io::Cursor;
use super::errors::Error;
use super::chunks::{WaveFmt, Bext, WriteBWaveChunks};
use super::chunks::{WriteBWaveChunks};
use super::bext::Bext;
use super::fmt::{WaveFmt};
use super::fourcc::{FourCC, RIFF_SIG, WAVE_SIG, FMT__SIG, JUNK_SIG, BEXT_SIG, DATA_SIG, WriteFourCC};
use byteorder::LittleEndian;

BIN
tests/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,15 +1,10 @@
#!/bin/zsh
mkdir -p media
cd media
touch media/error.wav
touch error.wav
unzip ../arch_pt_media.zip
unzip ../arch_audacity_media.zip
unzip ../arch_ff_media.zip
rm -rf __MACOSX
tar xzf test_media.tgz
# create a silent bext wave file with fixture metadata and a time refernce starting at
# one minute

View File

@@ -7,14 +7,19 @@ use std::io::Read;
use bwavfile::WaveReader;
// This seems rickety but we're going with it
// Media Tests
//
// These tests compare metadata and format data read by ffprobe with the same values
// as read by `WaveReader`.
// This is rickety but we're going with it
fn assert_match_stream<T>(stream_key: &str,
other: impl Fn(&mut WaveReader<File>) -> T)
where T: PartialEq + Debug,
T: Into<Value>
{
let mut json_file = File::open("tests/media_ffprobe_result.json").unwrap();
let mut json_file = File::open("tests/ffprobe_media_tests.json").unwrap();
let mut s = String::new();
json_file.read_to_string(&mut s).unwrap();
if let Value::Array(v) = from_str(&mut s).unwrap() { /* */
@@ -47,4 +52,4 @@ fn test_sample_rate() {
#[test]
fn test_channel_count() {
assert_match_stream("channels", |w| w.format().unwrap().channel_count );
}
}

View File

@@ -27,7 +27,7 @@ fn test_format_silence() -> Result<(),Error> {
assert_eq!(format.sample_rate, 44100);
assert_eq!(format.channel_count, 1);
assert_eq!(format.tag, 1);
assert_eq!(format.tag as u16, 1);
Ok( () )
}