First commit

This commit is contained in:
Jamie Hardt
2020-11-20 10:55:52 -08:00
commit 86e8a26e0a
19 changed files with 1452 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.DS_Store

79
Cargo.lock generated Normal file
View File

@@ -0,0 +1,79 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "encoding"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
dependencies = [
"encoding-index-japanese",
"encoding-index-korean",
"encoding-index-simpchinese",
"encoding-index-singlebyte",
"encoding-index-tradchinese",
]
[[package]]
name = "encoding-index-japanese"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-korean"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-simpchinese"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-singlebyte"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-tradchinese"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding_index_tests"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
[[package]]
name = "wavfile"
version = "0.1.0"
dependencies = [
"byteorder",
"encoding",
]

11
Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "wavfile"
version = "0.1.0"
authors = ["Jamie Hardt <jamiehardt@me.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
byteorder = "1.3.4"
encoding = "0.2.33"

58
src/audio_frame_reader.rs Normal file
View File

@@ -0,0 +1,58 @@
use std::io::{Read, Seek};
use std::io::SeekFrom::{Start,};
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use super::chunks::WaveFmt;
use super::errors::Error;
#[derive(Debug)]
pub struct AudioFrameReader<R: Read + Seek> {
inner : R,
format: WaveFmt
}
impl<R: Read + Seek> AudioFrameReader<R> {
/// Create a new AudioFrameReader, taking possession of a reader.
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);
AudioFrameReader { inner , format }
}
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 )
}
pub fn create_frame_buffer(&self) -> Vec<i32> {
vec![0i32; self.format.channel_count as usize]
}
pub fn read_integer_frame(&mut self, buffer:&mut [i32]) -> Result<(),Error> {
assert!(buffer.len() as u16 == self.format.channel_count,
"read_integer_frame was called with a mis-sized buffer, expected {}, was {}",
self.format.channel_count, buffer.len());
let framed_bits_per_sample = self.format.block_alignment * 8 / self.format.channel_count;
for n in 0..(self.format.channel_count as usize) {
buffer[n] = match (self.format.bits_per_sample, framed_bits_per_sample) {
(0..=8,8) => self.inner.read_u8()? as i32 - 0x80_i32, // EBU 3285 §A2.2
(9..=16,16) => self.inner.read_i16::<LittleEndian>()? as i32,
(10..=24,24) => self.inner.read_i24::<LittleEndian>()?,
(25..=32,32) => self.inner.read_i32::<LittleEndian>()?,
(b,_)=> panic!("Unrecognized integer format, bits per sample {}, channels {}, block_alignment {}",
b, self.format.channel_count, self.format.block_alignment)
}
}
Ok( () )
}
}

301
src/chunks.rs Normal file
View File

@@ -0,0 +1,301 @@
use std::io::{Read, Write};
use super::errors::Error as ParserError;
use encoding::{DecoderTrap, EncoderTrap};
use encoding::{Encoding};
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
}
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)]
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 : u16,
channel_mask : WaveFmtExtendedChannelMask,
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 {
pub tag: u16,
pub channel_count: u16,
pub sample_rate: u32,
pub bytes_per_second: u32,
pub block_alignment: u16,
pub bits_per_sample: u16,
pub extended_format: Option<WaveFmtExtended>
}
impl 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
}
}
pub fn bytes_per_frame(&self) -> u16 {
let bits_per_byte = 8;
let bits_per_sample_with_pad = self.bits_per_sample + (self.bits_per_sample % 8);
bits_per_sample_with_pad * self.channel_count / bits_per_byte
}
pub fn valid_broadcast_wave_format(&self) -> bool {
let real_alignment = self.block_alignment;
self.bytes_per_frame() == real_alignment
}
}
/**
* 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
}
pub trait ReadBWaveChunks: Read {
fn read_bext(&mut self) -> Result<Bext, ParserError>;
fn read_bext_string_field(&mut self, length: usize) -> Result<String,ParserError>;
fn read_wave_fmt(&mut self) -> Result<WaveFmt, ParserError>;
}
pub trait WriteBWaveChunks: Write {
fn write_wave_fmt(&mut self, format : &WaveFmt) -> Result<(), ParserError>;
fn write_bext_string_field(&mut self, string: &String, length: usize) -> Result<(),ParserError>;
fn write_bext(&mut self, bext: &Bext) -> Result<(),ParserError>;
}
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.channel_count)?;
self.write_u32::<LittleEndian>(format.sample_rate)?;
self.write_u32::<LittleEndian>(format.bytes_per_second)?;
self.write_u16::<LittleEndian>(format.block_alignment)?;
self.write_u16::<LittleEndian>(format.bits_per_sample)?;
// self.write_u8(0)?;
Ok(())
}
fn write_bext_string_field(&mut self, string: &String, length: usize) -> Result<(),ParserError> {
let mut buf = ASCII.encode(&string, EncoderTrap::Ignore).expect("Error encoding text");
buf.truncate(length);
let filler_length = length - buf.len();
if filler_length > 0{
let mut filler = vec![0u8; filler_length ];
buf.append(&mut filler);
}
self.write_all(&buf)?;
Ok(())
}
fn write_bext(&mut self, bext: &Bext) -> Result<(),ParserError> {
self.write_bext_string_field(&bext.description, 256)?;
self.write_bext_string_field(&bext.originator, 32)?;
self.write_bext_string_field(&bext.originator_reference, 32)?;
self.write_bext_string_field(&bext.origination_date, 10)?;
self.write_bext_string_field(&bext.origination_time, 8)?;
self.write_u64::<LittleEndian>(bext.time_reference)?;
self.write_u16::<LittleEndian>(bext.version)?;
let buf = bext.umid.unwrap_or([0u8; 64]);
self.write_all(&buf)?;
self.write_i16::<LittleEndian>(
(bext.loudness_value.unwrap_or(0.0) * 100.0) as i16 )?;
self.write_i16::<LittleEndian>(
(bext.loudness_range.unwrap_or(0.0) * 100.0) as i16 )?;
self.write_i16::<LittleEndian>(
(bext.max_true_peak_level.unwrap_or(0.0) * 100.0) as i16 )?;
self.write_i16::<LittleEndian>(
(bext.max_momentary_loudness.unwrap_or(0.0) * 100.0) as i16 )?;
self.write_i16::<LittleEndian>(
(bext.max_short_term_loudness.unwrap_or(0.0) * 100.0) as i16 )?;
let padding = [0u8; 180];
self.write_all(&padding)?;
let coding = ASCII.encode(&bext.coding_history, EncoderTrap::Ignore)
.expect("Error");
self.write_all(&coding)?;
Ok(())
}
}
impl<T> ReadBWaveChunks for T where T: Read {
fn read_wave_fmt(&mut self) -> Result<WaveFmt, ParserError> {
Ok(WaveFmt {
tag: self.read_u16::<LittleEndian>()?,
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
})
}
fn read_bext_string_field(&mut self, length: usize) -> Result<String,ParserError> {
let mut buffer : Vec<u8> = vec![0; length];
self.read(&mut buffer)?;
let trimmed : Vec<u8> = buffer.iter().take_while(|c| **c != 0 as u8).cloned().collect();
Ok(ASCII.decode(&trimmed, DecoderTrap::Ignore).expect("Error decoding text"))
}
fn read_bext(&mut self) -> Result<Bext, ParserError> {
let version : u16;
Ok( Bext {
description: self.read_bext_string_field(256)?,
originator: self.read_bext_string_field(32)?,
originator_reference : self.read_bext_string_field(32)?,
origination_date : self.read_bext_string_field(10)?,
origination_time : self.read_bext_string_field(8)?,
time_reference: self.read_u64::<LittleEndian>()?,
version: {
version = self.read_u16::<LittleEndian>()?;
version
},
umid: {
let mut buf = [0u8 ; 64];
self.read(&mut buf)?;
if version > 0 { Some(buf) } else { None }
},
loudness_value: {
let val = (self.read_i16::<LittleEndian>()? as f32) / 100f32;
if version > 1 { Some(val) } else { None }
},
loudness_range: {
let val = self.read_i16::<LittleEndian>()? as f32 / 100f32;
if version > 1 { Some(val) } else { None }
},
max_true_peak_level: {
let val = self.read_i16::<LittleEndian>()? as f32 / 100f32;
if version > 1 { Some(val) } else { None }
},
max_momentary_loudness: {
let val = self.read_i16::<LittleEndian>()? as f32 / 100f32;
if version > 1 { Some(val) } else { None }
},
max_short_term_loudness: {
let val = self.read_i16::<LittleEndian>()? as f32 / 100f32;
if version > 1 { Some(val) } else { None }
},
coding_history: {
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")
}
})
}
}

22
src/errors.rs Normal file
View File

@@ -0,0 +1,22 @@
use std::io;
use super::fourcc::FourCC;
#[derive(Debug)]
pub enum Error {
IOError(io::Error),
HeaderNotRecognized,
MissingRequiredDS64,
ChunkMissing { signature : FourCC },
FmtChunkAfterData,
NotMinimalWaveFile,
DataChunkNotAligned,
InsufficientDS64Reservation {expected: u64, actual: u64},
DataChunkNotPreparedForAppend
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Error {
Error::IOError(error)
}
}

120
src/fourcc.rs Normal file
View File

@@ -0,0 +1,120 @@
use std::fmt::Debug;
use std::io;
/// A Four-character Code
///
/// For idetifying chunks, structured contiguous slices or segments
/// within a WAV file.
#[derive(Eq, PartialEq, Hash, Copy, Clone)]
pub struct FourCC([u8; 4]);
impl FourCC {
pub const fn make(s: &[u8; 4]) -> Self {
Self(*s)
}
}
impl From<[char; 4]> for FourCC {
fn from(chars : [char; 4]) -> Self {
Self([chars[0] as u8 , chars[1] as u8, chars[2] as u8, chars[3] as u8])
}
}
impl From<[u8; 4]> for FourCC {
fn from(bytes: [u8; 4]) -> Self {
FourCC(bytes)
}
}
impl From<FourCC> for [u8; 4] {
fn from(fourcc: FourCC) -> Self {
fourcc.0
}
}
impl From<&FourCC> for [char;4] {
fn from( f: &FourCC) -> Self {
[f.0[0] as char, f.0[1] as char, f.0[2] as char, f.0[3] as char,]
}
}
impl From<FourCC> for [char;4] {
fn from( f: FourCC) -> Self {
[f.0[0] as char, f.0[1] as char, f.0[2] as char, f.0[3] as char,]
}
}
impl From<&FourCC> for String {
fn from(f: &FourCC) -> Self {
let chars: [char;4] = f.into();
chars.iter().collect::<String>()
}
}
impl From<FourCC> for String {
fn from(f: FourCC) -> Self {
let chars: [char;4] = f.into();
chars.iter().collect::<String>()
}
}
impl Debug for FourCC {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
let s : String = self.into();
write!(f, "FourCC({})", s)
}
}
pub trait ReadFourCC: io::Read {
fn read_fourcc(&mut self) -> Result<FourCC, io::Error>;
}
pub trait WriteFourCC: io::Write {
fn write_fourcc(&mut self, fourcc :FourCC) -> Result<(), io::Error>;
}
impl<T> ReadFourCC for T where T: io::Read {
fn read_fourcc(&mut self) -> Result<FourCC, io::Error> {
let mut buf : [u8; 4] = [0 ; 4];
self.read_exact(&mut buf)?;
Ok( FourCC::from(buf) )
}
}
impl<T> WriteFourCC for T where T: io::Write {
fn write_fourcc(&mut self, fourcc :FourCC) -> Result<(), io::Error> {
let buf : [u8; 4] = fourcc.into();
self.write_all(&buf)?;
Ok(())
}
}
pub const RIFF_SIG: FourCC = FourCC::make(b"RIFF");
pub const WAVE_SIG: FourCC = FourCC::make(b"WAVE");
pub const RF64_SIG: FourCC = FourCC::make(b"RF64");
pub const DS64_SIG: FourCC = FourCC::make(b"ds64");
pub const BW64_SIG: FourCC = FourCC::make(b"BW64");
pub const DATA_SIG: FourCC = FourCC::make(b"data");
pub const FMT__SIG: FourCC = FourCC::make(b"fmt ");
pub const BEXT_SIG: FourCC = FourCC::make(b"bext");
pub const JUNK_SIG: FourCC = FourCC::make(b"JUNK");
pub const FLLR_SIG: FourCC = FourCC::make(b"FLLR");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_string() {
let a = FourCC::make(b"a1b2");
let s : String = a.into();
assert_eq!(s, "a1b2");
}
}

19
src/lib.rs Normal file
View File

@@ -0,0 +1,19 @@
extern crate encoding;
extern crate byteorder;
mod parser;
mod fourcc;
mod errors;
mod validation;
mod raw_chunk_reader;
mod audio_frame_reader;
mod chunks;
mod wavereader;
mod wavewriter;
pub use wavereader::{WaveReader};
pub use chunks::{WaveFmt,Bext};
pub use errors::Error;

279
src/parser.rs Normal file
View File

@@ -0,0 +1,279 @@
use std::io;
use std::io::SeekFrom::{Current, Start};
use std::io::{Seek, Read};
use std::collections::HashMap;
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use super::errors::Error;
use super::fourcc::{FourCC, ReadFourCC};
use super::fourcc::{RIFF_SIG, RF64_SIG, BW64_SIG, WAVE_SIG, DS64_SIG, DATA_SIG};
// just for your reference...
// RF64 documentation https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
// EBU long files being with RF64, and the ITU recommends using BW64, so we recorgnize both.
const RF64_SIZE_MARKER: u32 = 0xFF_FF_FF_FF;
#[derive(Debug)]
pub enum Event {
StartParse,
ReadHeader { signature: FourCC, length_field: u32 },
ReadRF64Header { signature: FourCC },
ReadDS64 {file_size: u64, long_sizes: HashMap<FourCC,u64> },
BeginChunk { signature: FourCC, content_start: u64, content_length: u64 },
Failed { error: Error },
FinishParse
}
#[derive(Debug)]
enum State {
New,
ReadyForHeader,
ReadyForDS64,
ReadyForChunk { at: u64, remaining: u64 },
Error,
Complete
}
pub struct Parser<R: Read + Seek> {
stream: R,
state: State,
ds64state: HashMap<FourCC,u64>
}
pub struct ChunkIteratorItem {
pub signature: FourCC,
pub start: u64,
pub length: u64
}
impl<R: Read + Seek> Parser<R> {
// wraps a stream
pub fn make(stream: R) -> Result<Self, Error> {
let newmap: HashMap<FourCC, u64> = HashMap::new();
let mut the_stream = stream;
the_stream.seek(Start(0))?;
return Ok(Parser {
stream: the_stream,
state: State::New,
ds64state: newmap,
})
}
// pub fn into_inner(self) -> R {
// self.stream
// }
pub fn into_chunk_iterator(self) -> impl Iterator<Item = Result<ChunkIteratorItem, Error>>{
self.filter_map({|event|
if let Event::BeginChunk {signature , content_start, content_length } = event {
Some(Ok(ChunkIteratorItem {signature, start: content_start, length: content_length }))
} else if let Event::Failed { error } = event {
Some(Err(error))
} else {
None
}
})
}
pub fn into_chunk_list(self) -> Result<Vec<ChunkIteratorItem>,Error> {
let mut error = Ok(());
let chunks = self.into_chunk_iterator()
.scan(&mut error, |err, res| match res {
Ok(ok) => Some(ok),
Err(e) => { **err = Err(e); None }
})
.collect();
error?;
Ok( chunks )
}
}
impl<R: Read + Seek> Iterator for Parser<R> {
type Item = Event;
fn next(&mut self) -> Option<Event> {
let (event, next_state) = self.advance();
println!("{:?}", event);
self.state = next_state;
return event;
}
}
impl<R: Read + Seek> Parser<R> {
fn parse_header(&mut self) -> Result<(Event,State),io::Error> {
let file_sig = self.stream.read_fourcc()?;
let length = self.stream.read_u32::<LittleEndian>()?;
let list_sig = self.stream.read_fourcc()?;
let event : Event;
let next_state: State;
match (file_sig, length, list_sig) {
(RIFF_SIG, size, WAVE_SIG) => {
event = Event::ReadHeader {
signature: file_sig,
length_field: size
};
next_state = State::ReadyForChunk {
at: 12,
remaining: (length - 4) as u64,
};
},
(RF64_SIG, RF64_SIZE_MARKER, WAVE_SIG) | (BW64_SIG, RF64_SIZE_MARKER, WAVE_SIG) => {
event = Event::ReadRF64Header {
signature: file_sig
};
next_state = State::ReadyForDS64;
},
_ => {
event = Event::Failed {
error: Error::HeaderNotRecognized
};
next_state = State::Error;
}
}
return Ok( (event, next_state) );
}
fn parse_ds64(&mut self) -> Result<(Event, State), Error> {
let at :u64 = 12;
let ds64_sig = self.stream.read_fourcc()?;
let ds64_size = self.stream.read_u32::<LittleEndian>()? as u64;
let mut read :u64 = 0;
if ds64_sig != DS64_SIG {
return Err(Error::MissingRequiredDS64);
} else {
let long_file_size = self.stream.read_u64::<LittleEndian>()?;
let long_data_size = self.stream.read_u64::<LittleEndian>()?;
let _long_frame_count = self.stream.read_u64::<LittleEndian>(); // dead frame count field
read += 24;
let field_count = self.stream.read_u32::<LittleEndian>()?;
read += 4;
for _ in 0..field_count {
let this_fourcc = self.stream.read_fourcc()?;
let this_field_size = self.stream.read_u64::<LittleEndian>()?;
self.ds64state.insert(this_fourcc, this_field_size);
read += 12;
}
self.ds64state.insert(DATA_SIG, long_data_size);
if read < ds64_size {
/* for some reason the ds64 chunk returned by Pro Tools is longer than
it should be but it's all zeroes so... skip.
For the record libsndfile seems to do the same thing...
https://github.com/libsndfile/libsndfile/blob/08d802a3d18fa19c74f38ed910d9e33f80248187/src/rf64.c#L230
*/
let _ = self.stream.seek(Current((ds64_size - read) as i64));
}
let event = Event::ReadDS64 {
file_size: long_file_size,
long_sizes : self.ds64state.clone(),
};
let state = State::ReadyForChunk {
at: at + 8 + ds64_size,
remaining: long_file_size - (4 + 8 + ds64_size),
};
return Ok( (event, state) );
}
}
fn enter_chunk(&mut self, at :u64, remaining: u64) -> Result<(Event, State), io::Error> {
let event;
let state;
if remaining == 0 {
event = Event::FinishParse;
state = State::Complete;
} else {
let this_fourcc = self.stream.read_fourcc()?;
let this_size: u64;
if self.ds64state.contains_key(&this_fourcc) {
this_size = self.ds64state[&this_fourcc];
let _skip = self.stream.read_u32::<LittleEndian>()? as u64;
} else {
this_size = self.stream.read_u32::<LittleEndian>()? as u64;
}
let this_displacement :u64 = if this_size % 2 == 1 { this_size + 1 } else { this_size };
self.stream.seek(Current(this_displacement as i64))?;
event = Event::BeginChunk {
signature: this_fourcc,
content_start: at + 8,
content_length: this_size
};
state = State::ReadyForChunk {
at: at + 8 + this_displacement,
remaining: remaining - 8 - this_displacement
}
}
return Ok( (event, state) );
}
fn handle_state(&mut self) -> Result<(Option<Event>, State), Error> {
match self.state {
State::New => {
return Ok( ( Some(Event::StartParse) , State::ReadyForHeader) );
},
State::ReadyForHeader => {
let (event, state) = self.parse_header()?;
return Ok( ( Some(event), state ) );
},
State::ReadyForDS64 => {
let (event, state) = self.parse_ds64()?;
return Ok( ( Some(event), state ) );
},
State::ReadyForChunk { at, remaining } => {
let (event, state) = self.enter_chunk(at, remaining)?;
return Ok( ( Some(event), state ) );
},
State::Error => {
return Ok( ( Some(Event::FinishParse) , State::Complete ) );
},
State::Complete => {
return Ok( ( None, State::Complete ) );
}
}
}
fn advance(&mut self) -> (Option<Event>, State) {
match self.handle_state() {
Ok(( event , state) ) => {
return (event, state);
},
Err(error) => {
return (Some(Event::Failed { error: error.into() } ), State::Error );
}
}
}
}

73
src/raw_chunk_reader.rs Normal file
View File

@@ -0,0 +1,73 @@
use std::cmp::min;
use std::io::SeekFrom;
use std::io::SeekFrom::{Start, Current, End};
use std::io::{Seek,Read,Error,ErrorKind};
// I'm not sure this hasn't already been written somewhere in
// std but I'm just doing this here as an exercise.
#[derive(Debug)]
pub struct RawChunkReader<'a, R: Read + Seek> {
reader: &'a mut R,
start: u64,
length: u64,
position: u64
}
impl<'a,R: Read + Seek> RawChunkReader<'a, R> {
pub fn new(reader: &'a mut R, start: u64, length: u64) -> Self {
return Self {
reader: reader,
start: start,
length: length,
position: 0
}
}
pub fn length(&self) -> u64 {
self.length
}
}
impl<'a, R:Read + Seek> Read for RawChunkReader<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
if self.position >= self.length {
Err(Error::new(ErrorKind::UnexpectedEof, "RawChunkReader encountered end-of-file"))
} else {
self.reader.seek(Start(self.start + self.position))?;
let to_read = min(self.length - self.position, buf.len() as u64);
self.reader.take(to_read).read(buf)?;
self.position += to_read;
Ok(to_read as usize)
}
}
}
impl<'a, R:Read + Seek> Seek for RawChunkReader<'_, R> {
fn seek(&mut self, seek: SeekFrom) -> Result<u64, std::io::Error> {
match seek {
Start(s) => {
self.position = s;
Ok(self.position)
},
Current(s) => {
let new_position = s + self.position as i64;
if new_position < 0 {
Err( Error::new(ErrorKind::Other, "Attempted seek before beginning of chunk") )
} else {
self.position = new_position as u64;
Ok(self.position)
}
},
End(s) => {
let new_position = s + self.length as i64;
if new_position < 0 {
Err( Error::new(ErrorKind::Other, "Attempted seek before beginning of chunk") )
} else {
self.position = new_position as u64;
Ok(self.position)
}
}
}
}
}

118
src/validation.rs Normal file
View File

@@ -0,0 +1,118 @@
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.
*/
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).
*/
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)
}
}
}
}

143
src/wavereader.rs Normal file
View File

@@ -0,0 +1,143 @@
use std::fs::File;
use super::parser::Parser;
use super::fourcc::{FourCC, FMT__SIG, BEXT_SIG, DATA_SIG};
use super::errors::Error as ParserError;
use super::raw_chunk_reader::RawChunkReader;
use super::chunks::{WaveFmt, Bext};
use super::audio_frame_reader::AudioFrameReader;
use super::chunks::ReadBWaveChunks;
//use super::validation;
use std::io::SeekFrom::{Start};
use std::io::{Read, Seek};
/**
* Wave, Broadcast-WAV and RF64/BW64 parser/reader.
*
* ## Resources
*
* ### Implementation of Broadcast Wave Files
* - [EBU Tech 3285][ebu3285] (May 2011), "Specification of the Broadcast Wave Format (BWF)"
*
*
* ### 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).
*
* - [EBU Tech 3306v1][ebu3306v1] (July 2009), "MBWF / RF64: An extended File Format for Audio"
* - No longer in force, however long-established.
*
*
* ### Implementation of Wave format `fmt` chunk
* - [MSDN WAVEFORMATEX](https://docs.microsoft.com/en-us/windows/win32/api/mmeapi/ns-mmeapi-waveformatex)
* - [MSDN WAVEFORMATEXTENSIBLE](https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible)
*
*
* ### Other resources
* - [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)
*
* [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
* [itu2088]: https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
* [rfc3261]: https://tools.ietf.org/html/rfc2361
*/
#[derive(Debug)]
pub struct WaveReader<R: Read + Seek> {
pub inner: R,
}
impl WaveReader<File> {
/**
* Open a file for reading.
*
* A convenience that opens `path` and calls `Self::new()`
*/
pub fn open(path: &str) -> Result<Self, ParserError> {
let inner = File::open(path)?;
return Ok( Self::new(inner)? )
}
}
impl<R: Read + Seek> WaveReader<R> {
/**
* Wrap a `Read` struct in a new `WaveReader`.
*
* This is the primary entry point into the `WaveReader` interface. The
* stream passed as `inner` must be at the beginning of the header of the
* WAVE data. For a .wav file, this means it must be at the start of the
* file.
*
* This function does a minimal validation on the provided stream and
* will return an `Err(errors::Error)` immediately if there is a structural
* inconsistency that makes the stream unreadable or if it's missing
* essential components that make interpreting the audio data impoossible.
*/
pub fn new(inner: R) -> Result<Self,ParserError> {
let mut retval = Self { inner };
retval.validate_readable()?;
Ok(retval)
}
/**
* Unwrap and reliqnish ownership of the inner reader.
*/
pub fn into_inner(self) -> R {
return self.inner;
}
/**
* Create an `AudioFrameReader` for reading each audio frame.
*/
pub fn audio_frame_reader(&mut self) -> Result<AudioFrameReader<RawChunkReader<R>>, ParserError> {
let format = self.format()?;
let audio_chunk_reader = self.chunk_reader(DATA_SIG, 0)?;
Ok(AudioFrameReader::new(audio_chunk_reader, format))
}
/**
* The count of audio frames in the file.
*/
pub fn frame_length(&mut self) -> Result<u64, ParserError> {
let (_, data_length ) = self.get_chunk_extent_at_index(DATA_SIG, 0)?;
let format = self.format()?;
Ok( data_length / (format.block_alignment as u64) )
}
/**
* Sample and frame format of this wave file.
*/
pub fn format(&mut self) -> Result<WaveFmt, ParserError> {
self.chunk_reader(FMT__SIG, 0)?.read_wave_fmt()
}
/**
* The Broadcast-WAV metadata record for this file.
*/
pub fn broadcast_extension(&mut self) -> Result<Bext, ParserError> {
self.chunk_reader(BEXT_SIG, 0)?.read_bext()
}
}
impl<R:Read+Seek> WaveReader<R> { /* Private Implementation */
fn chunk_reader(&mut self, signature: FourCC, at_index: u32) -> Result<RawChunkReader<R>, ParserError> {
let (start, length) = self.get_chunk_extent_at_index(signature, at_index)?;
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> {
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) {
Ok ((chunk.start, chunk.length))
} else {
Err( ParserError::ChunkMissing { signature : fourcc })
}
}
}

102
src/wavewriter.rs Normal file
View File

@@ -0,0 +1,102 @@
use std::io::{Write, Seek, SeekFrom};
use std::fs::File;
use std::io::Cursor;
use super::errors::Error;
use super::chunks::{WaveFmt, Bext, WriteBWaveChunks};
use super::fourcc::{FourCC, RIFF_SIG, WAVE_SIG, FMT__SIG, JUNK_SIG, BEXT_SIG, DATA_SIG, WriteFourCC};
use byteorder::LittleEndian;
use byteorder::WriteBytesExt;
struct WaveWriter<W> where W: Write + Seek {
inner : W
}
impl WaveWriter<File> {
pub fn create(path : &str, format:WaveFmt, broadcast_extension: Option<Bext>) -> Result<Self,Error> {
let inner = File::create(path)?;
Self::make(inner, format, broadcast_extension)
}
}
impl<W:Write + Seek> WaveWriter<W> {
pub fn make(inner : W, format: WaveFmt, broadcast_extension: Option<Bext>) -> Result<Self, Error> {
let mut retval = Self { inner };
retval.prepare_created(format, broadcast_extension)?;
Ok(retval)
}
fn prepare_created(&mut self, format : WaveFmt, broadcast_extension: Option<Bext>) -> Result<(),Error> {
self.inner.write_fourcc(RIFF_SIG)?;
self.inner.write_u32::<LittleEndian>(4)?;
self.inner.write_fourcc(WAVE_SIG)?;
let mut written : u64 = 4;
let ds64_reservation = [0u8; 92];
written += self.primitive_append_chunk(JUNK_SIG, &ds64_reservation)?;
let fmt_data : Vec<u8> = {
let mut c = Cursor::new(vec![]);
c.write_wave_fmt(&format)?;
c.into_inner()
};
written += self.primitive_append_chunk(FMT__SIG, &fmt_data)?;
if let Some(bext) = broadcast_extension {
let mut b = Cursor::new(vec![]);
b.write_bext(&bext)?;
let data = b.into_inner();
written += self.primitive_append_chunk(BEXT_SIG, &data)?;
}
// show our work
let desired_data_alignment = 0x4000;
let data_fourcc_start = desired_data_alignment - 8;
let current_position_from_start = written + 8;
let data_pad_length = data_fourcc_start - current_position_from_start;
let data_padding = vec![0u8; data_pad_length as usize];
written += self.primitive_append_chunk(JUNK_SIG, &data_padding)?;
self.inner.write_fourcc(DATA_SIG)?;
self.inner.write_u32::<LittleEndian>(0)?;
written += 8;
self.inner.seek(SeekFrom::Start(4))?;
self.inner.write_u32::<LittleEndian>(written as u32)?;
Ok(())
}
fn primitive_append_chunk(&mut self, signature: FourCC, data: &[u8]) -> Result<u64,Error> {
assert!((data.len() as u32) < u32::MAX,
"primitive_append_chunk called with a long data buffer");
self.inner.write_fourcc(signature)?;
self.inner.write_u32::<LittleEndian>(data.len() as u32)?;
self.inner.write_all(&data)?;
let padding : u64 = data.len() as u64 % 2;
if padding == 1 {
self.inner.write_u8(0)?;
}
Ok(8 + data.len() as u64 + padding)
}
}
#[test]
fn test_chunk_append() -> Result<(), Error> {
let mut test :Vec<u8> = vec![];
let mut cursor = Cursor::new(test);
let f = WaveFmt::new_pcm(48000, 16, 1);
let mut w = WaveWriter::make(cursor, f, None)?;
Ok(())
}

BIN
tests/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
tests/arch_pt_media.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,48 @@
#!/bin/zsh
mkdir -p media
cd media
# create a silent bext wave file with fixture metadata and a time refernce starting at
# one minute
#
# Keywords for bext metadata are here...
# https://github.com/FFmpeg/FFmpeg/blob/17a0dfebf55f67653c29a607545a799f12bc0c01/libavformat/wavenc.c#L110
#
ffmpeg -y -f lavfi -i "aevalsrc=0|0:c=stereo" -to 0.1 -ar 48000 -c:a pcm_s24le -write_bext 1 \
-metadata "description=FFMPEG-generated stereo WAV file with bext metadata" \
-metadata "originator=ffmpeg" \
-metadata "originator_reference=STEREO_WAVE_TEST" \
-metadata "time_reference=2880000" \
-metadata "origination_date=2020-11-18" \
-metadata "origination_time=12:00:00" \
-metadata "umid=0xFF00FF00FF00FF00FF00FF00FF00FF00" \
-metadata "coding_history=A:PCM,48K" ff_bwav_stereo.wav
ffmpeg -y -f lavfi -i "aevalsrc=0|0|0|0|0|0:c=5.1" -to 0.1 -ar 48000 -c:a pcm_s24le -write_bext 1 \
-metadata "description=FFMPEG-generated 5.1 WAV file with bext metadata" \
-metadata "originator=ffmpeg" \
-metadata "originator_reference=5_1_WAVE_TEST" \
-metadata "time_reference=0" \
-metadata "origination_date=2020-11-18" \
-metadata "origination_time=13:00:00" \
-metadata "umid=0xFF00FF00FF00FF00FF00FF00FF00FF01" \
-metadata "coding_history=A:PCM,48K" ff_bwav_51.wav
ffmpeg -y -f lavfi -i "aevalsrc=0" -to 1 -ar 44100 ff_silence.wav
ffmpeg -y -f lavfi -i "aevalsrc=0" -to 1 -ar 44100 -fflags bitexact ff_minimal.wav
# ffmpeg -y -f lavfi -i "aevalsrc=0|0|0|0|0|0:c=5.1" -to 0:45:00 -ar 96000 -c:a pcm_s24le -rf64 1 \
# -write_bext 1 \
# -metadata "description=rf64 test file" ff_longfile.wav
ffmpeg -y -f lavfi -i "anoisesrc=r=48000:a=0.5:c=pink:s=41879" -to 0.1 -ar 48000 -c:a pcm_f32le \
-write_bext 1 \
-metadata "description=float test file" ff_float.wav
touch error.wav
unzip ../arch_pt_media.zip
unzip ../arch_audacity_media.zip
rm -rf __MACOSX

77
tests/integration_test.rs Normal file
View File

@@ -0,0 +1,77 @@
extern crate wavfile;
use wavfile::WaveReader;
use wavfile::Error;
#[test]
fn test_open() {
let path = "tests/media/ff_silence.wav";
match WaveReader::open(path) {
Ok(_) => {
()
},
Err(x) => {
assert!(false, "Opened error.wav with unexpected error {:?}", x)
}
}
}
#[test]
fn test_format_silence() -> Result<(),Error> {
let path = "tests/media/ff_silence.wav";
let mut w = WaveReader::open(path)?;
let format = w.format()?;
assert_eq!(format.sample_rate, 44100);
assert_eq!(format.channel_count, 1);
assert_eq!(format.tag, 1);
Ok( () )
}
#[test]
fn test_format_error() {
let path = "tests/media/error.wav";
if let Ok(_) = WaveReader::open(path) {
assert!(false);
} else {
assert!(true);
}
}
#[test]
fn test_frame_count() -> Result<(),Error> {
let path = "tests/media/ff_silence.wav";
let mut w = WaveReader::open(path)?;
let l = w.frame_length()?;
assert_eq!(l, 44100);
Ok( () )
}
#[test]
fn test_minimal_wave() {
let path = "tests/media/ff_silence.wav";
let mut w = WaveReader::open(path).expect("Failure opening file");
if let Err(Error::NotMinimalWaveFile) = w.validate_minimal() {
assert!(true);
} else {
assert!(false);
}
let min_path = "tests/media/ff_minimal.wav";
let mut w = WaveReader::open(min_path).expect("Failure opening file");
if let Err(Error::NotMinimalWaveFile) = w.validate_minimal() {
assert!(false);
} else {
assert!(true);
}
}