From 86e8a26e0af7e383168d50c301c21e9b37758d11 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 20 Nov 2020 10:55:52 -0800 Subject: [PATCH] First commit --- .DS_Store | Bin 0 -> 8196 bytes .gitignore | 2 + Cargo.lock | 79 +++++++++ Cargo.toml | 11 ++ src/audio_frame_reader.rs | 58 +++++++ src/chunks.rs | 301 ++++++++++++++++++++++++++++++++++ src/errors.rs | 22 +++ src/fourcc.rs | 120 ++++++++++++++ src/lib.rs | 19 +++ src/parser.rs | 279 +++++++++++++++++++++++++++++++ src/raw_chunk_reader.rs | 73 +++++++++ src/validation.rs | 118 +++++++++++++ src/wavereader.rs | 143 ++++++++++++++++ src/wavewriter.rs | 102 ++++++++++++ tests/.DS_Store | Bin 0 -> 6148 bytes tests/arch_audacity_media.zip | Bin 0 -> 18476 bytes tests/arch_pt_media.zip | Bin 0 -> 8291 bytes tests/create_test_media.sh | 48 ++++++ tests/integration_test.rs | 77 +++++++++ 19 files changed, 1452 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/audio_frame_reader.rs create mode 100644 src/chunks.rs create mode 100644 src/errors.rs create mode 100644 src/fourcc.rs create mode 100644 src/lib.rs create mode 100644 src/parser.rs create mode 100644 src/raw_chunk_reader.rs create mode 100644 src/validation.rs create mode 100644 src/wavereader.rs create mode 100644 src/wavewriter.rs create mode 100644 tests/.DS_Store create mode 100644 tests/arch_audacity_media.zip create mode 100644 tests/arch_pt_media.zip create mode 100644 tests/create_test_media.sh create mode 100644 tests/integration_test.rs diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0dd20f2f2a807fd1567fb172167027e2866f564c GIT binary patch literal 8196 zcmeHMU2GLa6rS(4&|O*Rv=k_p0+&__!78_ue?@M85U~)5x6l^QviI)Nu55Sf-MzO! zsVN5a#TfshMjkN!M2(3?!AJZ-9|-zlY((%0-V853p=hF>+1b_>D7+a0JI%~Dd(NCQ zbIyLdb7#vKLsQ9PlV%AAr48D<_zKC1L4dFPbdgxC;y3HI73Qe)J7SI zGH`7M#M!-wc`V1WcK6ux`|E%Ir|ETDS~9kqA&Gd*zEzK(Y8kB=f$@JNl=O>eA5i2U5G`U1k>2Ip?Sh~mUPy4!`_nP$FE^)O@_j@v7 zzs+^+(Y~J9VVV71yrS84eBE-)TwrV&CJou$ZaIeA-{j^U!wdYRr9#Z3cJbK*1IrrL zG}I-QFJCrTml#;JDoOo@m4kz-GHYS|x=nj}4;&mgH2Bn;A_#iLgs&o2Dz8V|qtqJ{ zqv2)k46Rb=$B1pO&@4lb9Ulq({AlQRP0^;tH$J>^lh)c{WnCw29W=XmsYlUoqRVYL zvp|uyXI(qr>v&zfEUVkuysi6Yqiu`9BrEl@IoGzgxt`@)uG87!oBRC{N8?l-J3Tk& zHwVtTIL~|LqbB*X3T6v=Bv<8Pwyk}_jkm+hd4x5oq>NboXxgl*1q+ufUB2eQrr}av zR<2cOGusQ6XJu@2d%tD)J)OFjH66opx_45}9pBn(nK_l8R@)rC*BmbAWu=v~5N>K0#kDq%62Nsu z4J7x}XZKKXWvC0q z>nq~%$HmN>b8QXA%)@*vKsA*c;a;b5%g*+FPE?=cWk;Pp_CUb5>&ElKUD~uWK5m zz*RSW+^rl-#ejd3U;*()h$YERM4wYNSDQc46SAGCYVKDA-+0MzFuJJE4z#sN-!1Wh!aZ}BZ-wr5koiP5wv1Uz|v0a#BP}A z4tTl`KJqAFKMvz*9KkbqE@0~`conbV7~aC$cn9y{V|;>7iMJ=rbU8kF*BOD)ZaQj~s$q*~W`G3R2-~Vs_L_`rr8MuQpfZ~>POA{qv zq8DL|bP1|^sEQ)S4M_}4s1ZYiJUEUMj{d`t`iW4N3UNpxX{h|~9|CUjV>a6Vqy0Z1 I=55~m3qX-!^#A|> literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0592392 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e98b873 --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ec0b68a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wavfile" +version = "0.1.0" +authors = ["Jamie Hardt "] +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" diff --git a/src/audio_frame_reader.rs b/src/audio_frame_reader.rs new file mode 100644 index 0000000..d5f9bca --- /dev/null +++ b/src/audio_frame_reader.rs @@ -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 { + inner : R, + format: WaveFmt +} + +impl AudioFrameReader { + /// 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 { + 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 { + 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::()? as i32, + (10..=24,24) => self.inner.read_i24::()?, + (25..=32,32) => self.inner.read_i32::()?, + (b,_)=> panic!("Unrecognized integer format, bits per sample {}, channels {}, block_alignment {}", + b, self.format.channel_count, self.format.block_alignment) + } + } + + Ok( () ) + } +} + diff --git a/src/chunks.rs b/src/chunks.rs new file mode 100644 index 0000000..1df03ad --- /dev/null +++ b/src/chunks.rs @@ -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 +} + + +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, + pub loudness_range: Option, + pub max_true_peak_level: Option, + pub max_momentary_loudness: Option, + pub max_short_term_loudness: Option, + // 180 bytes of nothing + pub coding_history: String +} + +pub trait ReadBWaveChunks: Read { + fn read_bext(&mut self) -> Result; + fn read_bext_string_field(&mut self, length: usize) -> Result; + fn read_wave_fmt(&mut self) -> Result; +} + +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 WriteBWaveChunks for T where T: Write { + fn write_wave_fmt(&mut self, format : &WaveFmt) -> Result<(), ParserError> { + self.write_u16::(format.tag)?; + self.write_u16::(format.channel_count)?; + self.write_u32::(format.sample_rate)?; + self.write_u32::(format.bytes_per_second)?; + self.write_u16::(format.block_alignment)?; + self.write_u16::(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::(bext.time_reference)?; + self.write_u16::(bext.version)?; + + let buf = bext.umid.unwrap_or([0u8; 64]); + self.write_all(&buf)?; + + self.write_i16::( + (bext.loudness_value.unwrap_or(0.0) * 100.0) as i16 )?; + self.write_i16::( + (bext.loudness_range.unwrap_or(0.0) * 100.0) as i16 )?; + self.write_i16::( + (bext.max_true_peak_level.unwrap_or(0.0) * 100.0) as i16 )?; + self.write_i16::( + (bext.max_momentary_loudness.unwrap_or(0.0) * 100.0) as i16 )?; + self.write_i16::( + (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 ReadBWaveChunks for T where T: Read { + + fn read_wave_fmt(&mut self) -> Result { + Ok(WaveFmt { + tag: self.read_u16::()?, + channel_count: self.read_u16::()?, + sample_rate: self.read_u32::()?, + bytes_per_second: self.read_u32::()?, + block_alignment: self.read_u16::()?, + bits_per_sample: self.read_u16::()?, + extended_format: None + }) + } + + fn read_bext_string_field(&mut self, length: usize) -> Result { + let mut buffer : Vec = vec![0; length]; + self.read(&mut buffer)?; + let trimmed : Vec = 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 { + 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::()?, + version: { + version = self.read_u16::()?; + 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::()? as f32) / 100f32; + if version > 1 { Some(val) } else { None } + }, + loudness_range: { + let val = self.read_i16::()? as f32 / 100f32; + if version > 1 { Some(val) } else { None } + }, + max_true_peak_level: { + let val = self.read_i16::()? as f32 / 100f32; + if version > 1 { Some(val) } else { None } + }, + max_momentary_loudness: { + let val = self.read_i16::()? as f32 / 100f32; + if version > 1 { Some(val) } else { None } + }, + max_short_term_loudness: { + let val = self.read_i16::()? 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") + } + }) + } +} \ No newline at end of file diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..deec277 --- /dev/null +++ b/src/errors.rs @@ -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 for Error { + fn from(error: io::Error) -> Error { + Error::IOError(error) + } +} \ No newline at end of file diff --git a/src/fourcc.rs b/src/fourcc.rs new file mode 100644 index 0000000..3583824 --- /dev/null +++ b/src/fourcc.rs @@ -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 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 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::() + } +} + +impl From for String { + fn from(f: FourCC) -> Self { + let chars: [char;4] = f.into(); + chars.iter().collect::() + } +} + +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; +} + +pub trait WriteFourCC: io::Write { + fn write_fourcc(&mut self, fourcc :FourCC) -> Result<(), io::Error>; +} + +impl ReadFourCC for T where T: io::Read { + fn read_fourcc(&mut self) -> Result { + let mut buf : [u8; 4] = [0 ; 4]; + self.read_exact(&mut buf)?; + Ok( FourCC::from(buf) ) + } +} + +impl 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"); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..427fddc --- /dev/null +++ b/src/lib.rs @@ -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; \ No newline at end of file diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..7738908 --- /dev/null +++ b/src/parser.rs @@ -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 }, + 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 { + stream: R, + state: State, + ds64state: HashMap +} + +pub struct ChunkIteratorItem { + pub signature: FourCC, + pub start: u64, + pub length: u64 +} + +impl Parser { + + // wraps a stream + pub fn make(stream: R) -> Result { + let newmap: HashMap = 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>{ + 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,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 Iterator for Parser { + type Item = Event; + + fn next(&mut self) -> Option { + let (event, next_state) = self.advance(); + println!("{:?}", event); + self.state = next_state; + return event; + } +} + +impl Parser { + + fn parse_header(&mut self) -> Result<(Event,State),io::Error> { + let file_sig = self.stream.read_fourcc()?; + let length = self.stream.read_u32::()?; + 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::()? 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::()?; + let long_data_size = self.stream.read_u64::()?; + let _long_frame_count = self.stream.read_u64::(); // dead frame count field + read += 24; + + let field_count = self.stream.read_u32::()?; + read += 4; + + for _ in 0..field_count { + let this_fourcc = self.stream.read_fourcc()?; + let this_field_size = self.stream.read_u64::()?; + 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::()? as u64; + } else { + this_size = self.stream.read_u32::()? 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, 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, State) { + match self.handle_state() { + Ok(( event , state) ) => { + return (event, state); + }, + Err(error) => { + return (Some(Event::Failed { error: error.into() } ), State::Error ); + } + } + } +} + diff --git a/src/raw_chunk_reader.rs b/src/raw_chunk_reader.rs new file mode 100644 index 0000000..7922a7d --- /dev/null +++ b/src/raw_chunk_reader.rs @@ -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 { + 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 { + 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) + } + } + } + } +} diff --git a/src/validation.rs b/src/validation.rs new file mode 100644 index 0000000..fd9b301 --- /dev/null +++ b/src/validation.rs @@ -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 WaveReader { + /** + * 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 = 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) + } + } + } +} + + + + + + + + diff --git a/src/wavereader.rs b/src/wavereader.rs new file mode 100644 index 0000000..fe762ab --- /dev/null +++ b/src/wavereader.rs @@ -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 { + pub inner: R, +} + +impl WaveReader { + /** + * Open a file for reading. + * + * A convenience that opens `path` and calls `Self::new()` + */ + pub fn open(path: &str) -> Result { + let inner = File::open(path)?; + return Ok( Self::new(inner)? ) + } +} + +impl WaveReader { + /** + * 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 { + 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>, 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 { + 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 { + self.chunk_reader(FMT__SIG, 0)?.read_wave_fmt() + } + + /** + * The Broadcast-WAV metadata record for this file. + */ + pub fn broadcast_extension(&mut self) -> Result { + self.chunk_reader(BEXT_SIG, 0)?.read_bext() + } +} + +impl WaveReader { /* Private Implementation */ + + fn chunk_reader(&mut self, signature: FourCC, at_index: u32) -> Result, 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 }) + } + } +} diff --git a/src/wavewriter.rs b/src/wavewriter.rs new file mode 100644 index 0000000..526fd28 --- /dev/null +++ b/src/wavewriter.rs @@ -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 where W: Write + Seek { + inner : W +} + +impl WaveWriter { + pub fn create(path : &str, format:WaveFmt, broadcast_extension: Option) -> Result { + let inner = File::create(path)?; + Self::make(inner, format, broadcast_extension) + } +} + +impl WaveWriter { + + pub fn make(inner : W, format: WaveFmt, broadcast_extension: Option) -> Result { + let mut retval = Self { inner }; + retval.prepare_created(format, broadcast_extension)?; + Ok(retval) + } + + fn prepare_created(&mut self, format : WaveFmt, broadcast_extension: Option) -> Result<(),Error> { + self.inner.write_fourcc(RIFF_SIG)?; + self.inner.write_u32::(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 = { + 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::(0)?; + + written += 8; + self.inner.seek(SeekFrom::Start(4))?; + self.inner.write_u32::(written as u32)?; + + Ok(()) + } + + fn primitive_append_chunk(&mut self, signature: FourCC, data: &[u8]) -> Result { + 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::(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 = vec![]; + let mut cursor = Cursor::new(test); + let f = WaveFmt::new_pcm(48000, 16, 1); + let mut w = WaveWriter::make(cursor, f, None)?; + + + + Ok(()) +} diff --git a/tests/.DS_Store b/tests/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0aee1392f486ebea13804f492541d892a177c3fb GIT binary patch literal 6148 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Ð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 n;yS!ezH3G3{=|Nr~Gv-dvg%14ee0FDzr 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 { + () + }, + 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); + } +} \ No newline at end of file