mirror of
https://github.com/iluvcapra/bwavfile.git
synced 2025-12-31 17:00:44 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f69ec8c61 | ||
|
|
ac6bd9c1e8 | ||
|
|
010f261598 | ||
|
|
b40e8edf23 | ||
|
|
3c221bce40 | ||
|
|
883b6c73e8 | ||
|
|
5bd292e964 | ||
|
|
34e473dc49 | ||
|
|
358aa06f7c | ||
|
|
8191eedf14 | ||
|
|
521e9d0670 | ||
|
|
dea68662f8 | ||
|
|
0685e7baca | ||
|
|
123d971624 | ||
|
|
f410cda9ed | ||
|
|
cf06b50fea | ||
|
|
e8d679b725 | ||
|
|
e63ab6bffd | ||
|
|
271da75f90 | ||
|
|
6c393d9958 | ||
|
|
99e5d6fe29 |
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -2,11 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "bwavfile"
|
||||
version = "0.1.2"
|
||||
version = "0.1.5"
|
||||
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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bwavfile"
|
||||
version = "0.1.2"
|
||||
version = "0.1.5"
|
||||
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"
|
||||
@@ -4,9 +4,15 @@ use std::io::SeekFrom::{Start,};
|
||||
use byteorder::LittleEndian;
|
||||
use byteorder::ReadBytesExt;
|
||||
|
||||
use super::chunks::WaveFmt;
|
||||
use super::fmt::{WaveFmt};
|
||||
use super::errors::Error;
|
||||
use super::CommonFormat;
|
||||
|
||||
/// 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 +20,64 @@ 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.common_format() == CommonFormat::IntegerPCM ,
|
||||
"Unsupported format tag {:?}", format.tag);
|
||||
|
||||
AudioFrameReader { inner , format }
|
||||
}
|
||||
|
||||
/// Locate the read position to a different frame
|
||||
///
|
||||
/// Seeks within the audio stream.
|
||||
///
|
||||
/// Returns the new location of the read position.
|
||||
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.
|
||||
///
|
||||
/// Regardless of the number of bits in the audio sample, this method
|
||||
/// always writes `i32` samples back to the buffer. These samples are
|
||||
/// written back "left-aligned" so samples that are shorter than i32
|
||||
/// will leave the MSB bits empty.
|
||||
///
|
||||
/// For example: A full-code sample in 16 bit (0xFFFF) will be written
|
||||
/// back to the buffer as 0x0000FFFF.
|
||||
///
|
||||
///
|
||||
/// ### 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 +98,4 @@ impl<R: Read + Seek> AudioFrameReader<R> {
|
||||
|
||||
Ok( 1 )
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
82
src/bext.rs
Normal file
82
src/bext.rs
Normal 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
|
||||
}
|
||||
232
src/chunks.rs
232
src/chunks.rs
@@ -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);
|
||||
}
|
||||
99
src/common_format.rs
Normal file
99
src/common_format.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
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 {
|
||||
/// Integer linear PCM
|
||||
IntegerPCM,
|
||||
|
||||
/// IEEE Floating-point Linear PCM
|
||||
IeeeFloatPCM,
|
||||
|
||||
/// MPEG
|
||||
Mpeg,
|
||||
|
||||
/// Ambisonic B-Format Linear PCM
|
||||
AmbisonicBFormatIntegerPCM,
|
||||
|
||||
/// Ambisonic B-Format Float PCM
|
||||
AmbisonicBFormatIeeeFloatPCM,
|
||||
|
||||
/// An unknown format identified by a basic format tag.
|
||||
UnknownBasic(u16),
|
||||
|
||||
/// An unknown format identified by an extension UUID.
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
247
src/fmt.rs
Normal file
247
src/fmt.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
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.
|
||||
pub 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.
|
||||
pub speaker: ChannelMask,
|
||||
|
||||
/// ADM audioTrackUIDs
|
||||
pub 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
|
||||
}
|
||||
}
|
||||
|
||||
/// Format or codec of the file's audio data.
|
||||
///
|
||||
/// The `CommonFormat` unifies the format tag and the format extension GUID. Use this
|
||||
/// method to determine the codec.
|
||||
pub fn common_format(&self) -> CommonFormat {
|
||||
CommonFormat::make( self.tag, self.extended_format.map(|ext| ext.type_guid))
|
||||
}
|
||||
|
||||
/// Channel descriptors for each channel.
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
src/lib.rs
79
src/lib.rs
@@ -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, ChannelMask};
|
||||
pub use common_format::CommonFormat;
|
||||
pub use audio_frame_reader::AudioFrameReader;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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, ChannelDescriptor, ChannelMask};
|
||||
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,173 @@ impl<R: Read + Seek> WaveReader<R> {
|
||||
pub fn broadcast_extension(&mut self) -> Result<Bext, ParserError> {
|
||||
self.chunk_reader(BEXT_SIG, 0)?.read_bext()
|
||||
}
|
||||
|
||||
/// Describe the channels in this file
|
||||
///
|
||||
/// Returns a vector of channel descriptors, one for each channel
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bwavfile::WaveReader;
|
||||
/// # use bwavfile::ChannelMask;
|
||||
/// let mut f = WaveReader::open("tests/media/pt_24bit_51.wav").unwrap();
|
||||
///
|
||||
/// let chans = f.channels().unwrap();
|
||||
/// assert_eq!(chans[0].index, 0);
|
||||
/// assert_eq!(chans[0].speaker, ChannelMask::FrontLeft);
|
||||
/// assert_eq!(chans[3].index, 3);
|
||||
/// assert_eq!(chans[3].speaker, ChannelMask::LowFrequency);
|
||||
/// assert_eq!(chans[4].speaker, ChannelMask::BackLeft);
|
||||
/// ```
|
||||
pub fn channels(&mut self) -> Result<Vec<ChannelDescriptor>, ParserError> {
|
||||
|
||||
let format = self.format()?;
|
||||
let channel_masks : Vec<ChannelMask> = match (format.channel_count, format.extended_format) {
|
||||
(1,_) => vec![ChannelMask::FrontCenter],
|
||||
(2,_) => vec![ChannelMask::FrontLeft, ChannelMask::FrontRight],
|
||||
(n,Some(x)) => ChannelMask::channels(x.channel_mask, n),
|
||||
(n,_) => vec![ChannelMask::DirectOut; n as usize]
|
||||
};
|
||||
|
||||
Ok( (0..format.channel_count).zip(channel_masks)
|
||||
.map(|(i,m)| ChannelDescriptor { index: i, speaker:m, adm_track_audio_ids: vec![] } )
|
||||
.collect() )
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +307,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) {
|
||||
|
||||
@@ -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
BIN
tests/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
[
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
"index": 0,
|
||||
"codec_name": "pcm_s24le",
|
||||
"codec_long_name": "PCM signed 24-bit little-endian",
|
||||
"codec_type": "audio",
|
||||
"codec_time_base": "1/48000",
|
||||
"codec_tag_string": "[1][0][0][0]",
|
||||
"codec_tag": "0x0001",
|
||||
"sample_fmt": "s32",
|
||||
"sample_rate": "48000",
|
||||
"channels": 2,
|
||||
"channel_layout": "stereo",
|
||||
"bits_per_sample": 24,
|
||||
"r_frame_rate": "0/0",
|
||||
"avg_frame_rate": "0/0",
|
||||
"time_base": "1/48000",
|
||||
"duration_ts": 4800,
|
||||
"duration": "0.100000",
|
||||
"bit_rate": "2304000",
|
||||
"bits_per_raw_sample": "24",
|
||||
"disposition": {
|
||||
"default": 0,
|
||||
"dub": 0,
|
||||
"original": 0,
|
||||
"comment": 0,
|
||||
"lyrics": 0,
|
||||
"karaoke": 0,
|
||||
"forced": 0,
|
||||
"hearing_impaired": 0,
|
||||
"visual_impaired": 0,
|
||||
"clean_effects": 0,
|
||||
"attached_pic": 0,
|
||||
"timed_thumbnails": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"format": {
|
||||
"filename": "tests/media/ff_pink.wav",
|
||||
"nb_streams": 1,
|
||||
"nb_programs": 0,
|
||||
"format_name": "wav",
|
||||
"format_long_name": "WAV / WAVE (Waveform Audio)",
|
||||
"duration": "0.100000",
|
||||
"size": "28902",
|
||||
"bit_rate": "2312160",
|
||||
"probe_score": 99,
|
||||
"tags": {
|
||||
"encoder": "Lavf58.45.100"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ extern crate bwavfile;
|
||||
|
||||
use bwavfile::WaveReader;
|
||||
use bwavfile::Error;
|
||||
use bwavfile::{ ChannelMask};
|
||||
|
||||
#[test]
|
||||
fn test_open() {
|
||||
@@ -27,7 +28,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( () )
|
||||
}
|
||||
|
||||
@@ -74,4 +75,85 @@ fn test_minimal_wave() {
|
||||
} else {
|
||||
assert!(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read() {
|
||||
let path = "tests/media/audacity_16bit.wav";
|
||||
|
||||
let mut w = WaveReader::open(path).expect("Failure opening test file");
|
||||
|
||||
let mut reader = w.audio_frame_reader().unwrap();
|
||||
|
||||
let mut buffer = reader.create_frame_buffer();
|
||||
|
||||
assert_eq!(reader.read_integer_frame(&mut buffer).unwrap(), 1);
|
||||
assert_eq!(buffer[0], -2823_i32);
|
||||
assert_eq!(reader.read_integer_frame(&mut buffer).unwrap(), 1);
|
||||
assert_eq!(buffer[0], 2012_i32);
|
||||
assert_eq!(reader.read_integer_frame(&mut buffer).unwrap(), 1);
|
||||
assert_eq!(buffer[0], 4524_i32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_locate_multichannel_read() {
|
||||
let path = "tests/media/ff_pink.wav";
|
||||
|
||||
let mut w = WaveReader::open(path).expect("Failure opening test file");
|
||||
|
||||
let mut reader = w.audio_frame_reader().unwrap();
|
||||
|
||||
let mut buffer = reader.create_frame_buffer();
|
||||
|
||||
assert_eq!(reader.read_integer_frame(&mut buffer).unwrap(), 1);
|
||||
assert_eq!(buffer[0], 332702_i32);
|
||||
assert_eq!(buffer[1], 3258791_i32);
|
||||
assert_eq!(reader.read_integer_frame(&mut buffer).unwrap(), 1);
|
||||
assert_eq!(buffer[0], -258742_i32); // 0x800000 = 8388608 // 8129866 - 8388608
|
||||
assert_eq!(buffer[1], 0x0D7EF9_i32);
|
||||
|
||||
assert_eq!(reader.locate(100).unwrap(), 100);
|
||||
assert_eq!(reader.read_integer_frame(&mut buffer).unwrap(), 1);
|
||||
assert_eq!(buffer[0], 0x109422_i32);
|
||||
assert_eq!(buffer[1], -698901_i32); // 7689707 - 8388608
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channels_stereo() {
|
||||
let path = "tests/media/ff_pink.wav";
|
||||
|
||||
let mut w = WaveReader::open(path).expect("Failure opening test file");
|
||||
let channels = w.channels().unwrap();
|
||||
|
||||
assert_eq!(channels.len(), 2);
|
||||
assert_eq!(channels[0].index,0);
|
||||
assert_eq!(channels[1].index,1);
|
||||
assert_eq!(channels[0].speaker,ChannelMask::FrontLeft);
|
||||
assert_eq!(channels[1].speaker,ChannelMask::FrontRight);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channels_mono_no_extended() {
|
||||
let path = "tests/media/audacity_16bit.wav";
|
||||
|
||||
let mut w = WaveReader::open(path).expect("Failure opening test file");
|
||||
let channels = w.channels().unwrap();
|
||||
|
||||
assert_eq!(channels.len(), 1);
|
||||
assert_eq!(channels[0].index,0);
|
||||
assert_eq!(channels[0].speaker,ChannelMask::FrontCenter);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channels_stereo_no_fmt_extended() {
|
||||
let path = "tests/media/pt_24bit_stereo.wav";
|
||||
|
||||
let mut w = WaveReader::open(path).expect("Failure opening test file");
|
||||
let channels = w.channels().unwrap();
|
||||
|
||||
assert_eq!(channels.len(), 2);
|
||||
assert_eq!(channels[0].index,0);
|
||||
assert_eq!(channels[1].index,1);
|
||||
assert_eq!(channels[0].speaker,ChannelMask::FrontLeft);
|
||||
assert_eq!(channels[1].speaker,ChannelMask::FrontRight);
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user