42 Commits

Author SHA1 Message Date
Jamie Hardt
5bd292e964 Bumped version 2020-11-23 22:39:08 -08:00
Jamie Hardt
34e473dc49 Implementation of complex formats 2020-11-23 22:38:10 -08:00
Jamie Hardt
358aa06f7c Removed validation methods
and added to main implementation
2020-11-23 13:16:28 -08:00
Jamie Hardt
8191eedf14 ADM anc channel structures 2020-11-23 11:11:12 -08:00
Jamie Hardt
521e9d0670 Reorganized source, comments 2020-11-23 00:23:17 -08:00
Jamie Hardt
dea68662f8 Comments and documentation 2020-11-22 23:32:25 -08:00
Jamie Hardt
0685e7baca Removed dead line 2020-11-22 22:38:53 -08:00
Jamie Hardt
123d971624 Comments 2020-11-22 22:38:47 -08:00
Jamie Hardt
f410cda9ed removed .DS_Store 2020-11-22 22:15:58 -08:00
Jamie Hardt
cf06b50fea Comment 2020-11-22 22:04:04 -08:00
Jamie Hardt
e8d679b725 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2020-11-22 22:02:50 -08:00
Jamie Hardt
e63ab6bffd Renamed some files 2020-11-22 22:02:45 -08:00
Jamie Hardt
271da75f90 Update ffprobe_media_tests.rs 2020-11-22 22:01:11 -08:00
Jamie Hardt
6c393d9958 Restructred test files 2020-11-22 21:36:40 -08:00
Jamie Hardt
99e5d6fe29 Nudging version 2020-11-22 21:20:23 -08:00
Jamie Hardt
bebfa235e6 Added very basic file verification 2020-11-22 21:07:16 -08:00
Jamie Hardt
a5c55dbcf1 added json parsing 2020-11-22 18:48:08 -08:00
Jamie Hardt
02a91f2b1d More documentation, killed some code 2020-11-22 15:18:13 -08:00
Jamie Hardt
781e8769b0 Readability 2020-11-22 14:57:17 -08:00
Jamie Hardt
e637dc86d3 Re-ran this to fix filename fields 2020-11-22 14:54:57 -08:00
Jamie Hardt
d9f8855f84 Documentation 2020-11-22 14:36:39 -08:00
Jamie Hardt
0da6594081 Replaced the TOML file with an FFPROBE output 2020-11-22 14:36:35 -08:00
Jamie Hardt
a3920a922e Doc cleanup 2020-11-22 13:24:09 -08:00
Jamie Hardt
afbeb7d737 Documentation tweaks 2020-11-22 13:08:54 -08:00
Jamie Hardt
bfbe0f0b9d Made more items public 2020-11-22 13:07:26 -08:00
Jamie Hardt
d242e33de3 Update README.md
Removed resources
2020-11-22 12:57:39 -08:00
Jamie Hardt
da229c7396 Update README.md 2020-11-22 12:56:47 -08:00
Jamie Hardt
5b480c919b Update rust.yml 2020-11-20 23:11:36 -08:00
Jamie Hardt
5cd58c236b Update rust.yml
Added tarpaulin as an experiment
2020-11-20 23:05:15 -08:00
Jamie Hardt
048f58d856 Update Cargo.toml
Removed license-file key
2020-11-20 22:29:56 -08:00
Jamie Hardt
79f146a222 Update rust.yml 2020-11-20 22:27:34 -08:00
Jamie Hardt
5384ebfdc5 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2020-11-20 22:25:34 -08:00
Jamie Hardt
c6ffbc4559 ds 2020-11-20 22:25:20 -08:00
Jamie Hardt
46fe38e3d6 Added empirical test data 2020-11-20 22:25:09 -08:00
Jamie Hardt
6d2223d8c2 Update README.md 2020-11-20 20:22:47 -08:00
Jamie Hardt
84f5a955ab Update README.md
Added build badge
2020-11-20 20:21:50 -08:00
Jamie Hardt
013145d267 Update rust.yml 2020-11-20 20:17:55 -08:00
Jamie Hardt
63ee997eb3 Update rust.yml 2020-11-20 20:15:15 -08:00
Jamie Hardt
c5072c76b0 Update rust.yml
Added Create Test Media step
2020-11-20 20:11:19 -08:00
Jamie Hardt
acd0fe6793 Create rust.yml 2020-11-20 20:06:14 -08:00
Jamie Hardt
9178e48a5b Update README.md 2020-11-20 19:29:55 -08:00
Jamie Hardt
044d1327c0 Update README.md 2020-11-20 19:28:18 -08:00
24 changed files with 1751 additions and 416 deletions

28
.github/workflows/rust.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Rust
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# - name: Install ffmpeg
# run: sudo apt-get install ffmpeg
- name: Create Test Media
run: cd tests; sh create_test_media.sh
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
# - name: rust-tarpaulin
# uses: actions-rs/tarpaulin@v0.1.0

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
.DS_Store .DS_Store
target/* target/*
tests/media/* tests/media/*
tests/.DS_Store

39
Cargo.lock generated
View File

@@ -2,10 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]] [[package]]
name = "bwavfile" name = "bwavfile"
version = "0.1.1" version = "0.1.4"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"encoding", "encoding",
"serde_json",
"uuid",
] ]
[[package]] [[package]]
@@ -77,3 +79,38 @@ name = "encoding_index_tests"
version = "0.1.4" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
[[package]]
name = "itoa"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "serde"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
[[package]]
name = "serde_json"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "uuid"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"

View File

@@ -1,10 +1,9 @@
[package] [package]
name = "bwavfile" name = "bwavfile"
version = "0.1.1" version = "0.1.4"
authors = ["Jamie Hardt <jamiehardt@me.com>"] authors = ["Jamie Hardt <jamiehardt@me.com>"]
edition = "2018" edition = "2018"
license = "MIT" license = "MIT"
license-file = "LICENSE"
description = "Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support" description = "Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support"
homepage = "https://github.com/iluvcapra/bwavfile" homepage = "https://github.com/iluvcapra/bwavfile"
readme = "README.md" readme = "README.md"
@@ -17,3 +16,5 @@ keywords = ["audio", "broadcast", "multimedia","smpte"]
[dependencies] [dependencies]
byteorder = "1.3.4" byteorder = "1.3.4"
encoding = "0.2.33" encoding = "0.2.33"
uuid = "0.8.1"
serde_json = "1.0.59"

View File

@@ -1,69 +1,38 @@
![Crates.io](https://img.shields.io/crates/l/bwavfile) [![Crates.io](https://img.shields.io/crates/l/bwavfile)](LICENSE)
![Crates.io](https://img.shields.io/crates/v/bwavfile) [![Crates.io](https://img.shields.io/crates/v/bwavfile)](https://crates.io/crates/bwavfile/)
![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/bwavfile) ![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/bwavfile)
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/iluvcapra/bwavfile/Rust)](https://github.com/iluvcapra/bwavfile/actions?query=workflow%3ARust)
# bwavfile # bwavfile
Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support
This is currently a work-in-progress! This is currently a work-in-progress!
## Use ## Use Examples
### Reading a File
```rust ```rust
let path = "tests/media/ff_silence.wav"; use bwavfile::WaveReader;
let mut r = WaveReader::open("tests/media/ff_silence.wav").unwrap();
let mut w = WaveReader::open(path)?; let format = r.format().unwrap();
let length = w.frame_length()?; assert_eq!(format.sample_rate, 44100);
let format = w.format()?; assert_eq!(format.channel_count, 1);
let bext = w.broadcast_extension()?; let mut frame_reader = r.audio_frame_reader().unwrap();
println!("Description field: {}", &bext.description); let mut buffer = frame_reader.create_frame_buffer();
println!("Originator field: {}", &bext.originator);
let frame_reader = w.audio_frame_reader()?; let read = frame_reader.read_integer_frame(&mut buffer).unwrap();
let mut buffer: Vec<i32> = w.create_frame_buffer();
while( frame_reader.read_integer_frame(&mut buffer) > 0) {
println!("Read frames {:?}", &buffer);
}
assert_eq!(buffer, [0i32]);
assert_eq!(read, 1);
``` ```
## Note on Testing ## Note on Testing
All of the media for the integration tests is committed to the respository All of the media for the integration tests is committed to the respository
in either zipped form or is created by ffmpeg. Before you can run tests, you in zipped form. Before you can run tests, you need to `cd` into the `tests`
will need to have ffmpeg installed, and you need to `cd` into the `tests` directory and run the `create_test_media.sh` script. Note that one of the
directory and run the `create_test_media.sh` script. test files (the RF64 test case) is over four gigs in size.
## 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

View File

@@ -4,9 +4,14 @@ use std::io::SeekFrom::{Start,};
use byteorder::LittleEndian; use byteorder::LittleEndian;
use byteorder::ReadBytesExt; use byteorder::ReadBytesExt;
use super::chunks::WaveFmt; use super::fmt::{WaveFmt};
use super::errors::Error; use super::errors::Error;
/// Read audio frames
///
/// The inner reader is interpreted as a raw audio data
/// bitstream having a format specified by `format`.
///
#[derive(Debug)] #[derive(Debug)]
pub struct AudioFrameReader<R: Read + Seek> { pub struct AudioFrameReader<R: Read + Seek> {
inner : R, inner : R,
@@ -14,26 +19,53 @@ pub struct AudioFrameReader<R: Read + Seek> {
} }
impl<R: Read + Seek> AudioFrameReader<R> { 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 { pub fn new(inner: R, format: WaveFmt) -> Self {
assert!(format.block_alignment * 8 == format.bits_per_sample * format.channel_count, 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 {}", "Unable to read audio frames from packed formats: block alignment is {}, should be {}",
format.block_alignment, (format.bits_per_sample / 8 ) * format.channel_count); format.block_alignment, (format.bits_per_sample / 8 ) * format.channel_count);
assert!(format.tag == 1, "Unsupported format tag {}", format.tag);
assert!(format.tag == 0x01 ,
"Unsupported format tag {:?}", format.tag);
AudioFrameReader { inner , format } AudioFrameReader { inner , format }
} }
/// Locate the read position to a different frame
///
/// Seeks within the audio stream.
pub fn locate(&mut self, to :u64) -> Result<u64,Error> { pub fn locate(&mut self, to :u64) -> Result<u64,Error> {
let position = to * self.format.block_alignment as u64; let position = to * self.format.block_alignment as u64;
let seek_result = self.inner.seek(Start(position))?; let seek_result = self.inner.seek(Start(position))?;
Ok( seek_result / self.format.block_alignment as u64 ) 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> { pub fn create_frame_buffer(&self) -> Vec<i32> {
vec![0i32; self.format.channel_count as usize] vec![0i32; self.format.channel_count as usize]
} }
/// Read a frame
///
/// A single frame is read from the audio stream and the read location
/// is advanced one frame.
///
/// ### Panics
///
/// The `buffer` must have a number of elements equal to the number of
/// channels and this method will panic if this is not the case.
pub fn read_integer_frame(&mut self, buffer:&mut [i32]) -> Result<u64,Error> { pub fn read_integer_frame(&mut self, buffer:&mut [i32]) -> Result<u64,Error> {
assert!(buffer.len() as u16 == self.format.channel_count, assert!(buffer.len() as u16 == self.format.channel_count,
"read_integer_frame was called with a mis-sized buffer, expected {}, was {}", "read_integer_frame was called with a mis-sized buffer, expected {}, was {}",
@@ -55,4 +87,3 @@ impl<R: Read + Seek> AudioFrameReader<R> {
Ok( 1 ) Ok( 1 )
} }
} }

82
src/bext.rs Normal file
View File

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

View File

@@ -1,7 +1,5 @@
use std::io::{Read, Write}; use std::io::{Read, Write};
use super::errors::Error as ParserError;
use encoding::{DecoderTrap, EncoderTrap}; use encoding::{DecoderTrap, EncoderTrap};
use encoding::{Encoding}; use encoding::{Encoding};
use encoding::all::ASCII; use encoding::all::ASCII;
@@ -9,156 +7,11 @@ use encoding::all::ASCII;
use byteorder::LittleEndian; use byteorder::LittleEndian;
use byteorder::{ReadBytesExt, WriteBytesExt}; use byteorder::{ReadBytesExt, WriteBytesExt};
/** use uuid::Uuid;
* 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, use super::errors::Error as ParserError;
0x00, 0x00, 0x00, 0x10, use super::fmt::{WaveFmt, WaveFmtExtended};
0x80, 0x00, 0x00, 0xaa, use super::bext::Bext;
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 { pub trait ReadBWaveChunks: Read {
fn read_bext(&mut self) -> Result<Bext, ParserError>; fn read_bext(&mut self) -> Result<Bext, ParserError>;
@@ -174,7 +27,7 @@ pub trait WriteBWaveChunks: Write {
impl<T> WriteBWaveChunks for T where T: Write { impl<T> WriteBWaveChunks for T where T: Write {
fn write_wave_fmt(&mut self, format : &WaveFmt) -> Result<(), ParserError> { 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_u16::<LittleEndian>(format.channel_count)?;
self.write_u32::<LittleEndian>(format.sample_rate)?; self.write_u32::<LittleEndian>(format.sample_rate)?;
self.write_u32::<LittleEndian>(format.bytes_per_second)?; self.write_u32::<LittleEndian>(format.bytes_per_second)?;
@@ -234,14 +87,34 @@ impl<T> WriteBWaveChunks for T where T: Write {
impl<T> ReadBWaveChunks for T where T: Read { impl<T> ReadBWaveChunks for T where T: Read {
fn read_wave_fmt(&mut self) -> Result<WaveFmt, ParserError> { fn read_wave_fmt(&mut self) -> Result<WaveFmt, ParserError> {
let tag_value : u16;
Ok(WaveFmt { Ok(WaveFmt {
tag: self.read_u16::<LittleEndian>()?, tag: {
tag_value = self.read_u16::<LittleEndian>()?;
tag_value
},
channel_count: self.read_u16::<LittleEndian>()?, channel_count: self.read_u16::<LittleEndian>()?,
sample_rate: self.read_u32::<LittleEndian>()?, sample_rate: self.read_u32::<LittleEndian>()?,
bytes_per_second: self.read_u32::<LittleEndian>()?, bytes_per_second: self.read_u32::<LittleEndian>()?,
block_alignment: self.read_u16::<LittleEndian>()?, block_alignment: self.read_u16::<LittleEndian>()?,
bits_per_sample: 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
}
}
}) })
} }
@@ -291,7 +164,7 @@ impl<T> ReadBWaveChunks for T where T: Read {
if version > 1 { Some(val) } else { None } if version > 1 { Some(val) } else { None }
}, },
coding_history: { coding_history: {
for _ in 0..=180 { self.read_u8()?; } for _ in 0..180 { self.read_u8()?; }
let mut buf = vec![]; let mut buf = vec![];
self.read_to_end(&mut buf)?; self.read_to_end(&mut buf)?;
ASCII.decode(&buf, DecoderTrap::Ignore).expect("Error decoding text") ASCII.decode(&buf, DecoderTrap::Ignore).expect("Error decoding text")
@@ -299,3 +172,28 @@ impl<T> ReadBWaveChunks for T where T: Read {
}) })
} }
} }
#[test]
fn test_read_51_wav() {
use super::fmt::ChannelMask;
use super::common_format::CommonFormat;
let path = "tests/media/pt_24bit_51.wav";
let mut w = super::wavereader::WaveReader::open(path).unwrap();
let format = w.format().unwrap();
assert_eq!(format.tag, 0xFFFE);
assert_eq!(format.channel_count, 6);
assert_eq!(format.sample_rate, 48000);
let extended = format.extended_format.unwrap();
assert_eq!(extended.valid_bits_per_sample, 24);
let channels = ChannelMask::channels(extended.channel_mask, format.channel_count);
assert_eq!(channels, [ChannelMask::FrontLeft, ChannelMask::FrontRight,
ChannelMask::FrontCenter, ChannelMask::LowFrequency,
ChannelMask::BackLeft, ChannelMask::BackRight]);
assert_eq!(format.common_format(), CommonFormat::IntegerPCM);
}

86
src/common_format.rs Normal file
View File

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

View File

@@ -1,17 +1,46 @@
use std::io; use std::io;
use super::fourcc::FourCC; use super::fourcc::FourCC;
use uuid;
/// Errors returned by methods in this crate.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
/// An `io::Error` occurred
IOError(io::Error), IOError(io::Error),
/// An error occured reading a tag UUID
UuidError(uuid::Error),
/// The file does not begin with a recognized WAVE header
HeaderNotRecognized, HeaderNotRecognized,
/// A wave file with a 64-bit header does not contain
/// the required `ds64` metadata element
MissingRequiredDS64, MissingRequiredDS64,
/// A data chunk required to complete the operation
/// is not present in the file
ChunkMissing { signature : FourCC }, ChunkMissing { signature : FourCC },
/// The file is formatted improperly
FmtChunkAfterData, FmtChunkAfterData,
/// The file did not validate as a minimal WAV file
NotMinimalWaveFile, NotMinimalWaveFile,
/// The `data` chunk is not aligned to the desired page
/// boundary
DataChunkNotAligned, DataChunkNotAligned,
/// The file cannot be converted into an RF64 file due
/// to its internal structure
InsufficientDS64Reservation {expected: u64, actual: u64}, InsufficientDS64Reservation {expected: u64, actual: u64},
DataChunkNotPreparedForAppend
/// The file is not optimized for writing new data
DataChunkNotPreparedForAppend,
} }
@@ -20,3 +49,9 @@ impl From<io::Error> for Error {
Error::IOError(error) Error::IOError(error)
} }
} }
impl From <uuid::Error> for Error {
fn from(error: uuid::Error) -> Error {
Error::UuidError(error)
}
}

242
src/fmt.rs Normal file
View File

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

View File

@@ -1,30 +1,126 @@
/*!
# 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
files works however the interfaces may change significantly. Stay up-to-date on the
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).
- [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)
(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
[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
[github]: https://github.com/iluvcapra/bwavfile
*/
// #![feature(external_doc)]
// #[doc(include="../README.md")]
// #[cfg(doctest)]
// pub struct ReadmeDoctests;
extern crate encoding; extern crate encoding;
extern crate byteorder; extern crate byteorder;
extern crate uuid;
/**!
* bwavfile
*
* Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support
*
* This crate is currently a work-in-progress (I'm using it to teach myself
* rust) so the interface may change dramatically and not all features work.
*
!*/
mod parser;
mod fourcc; mod fourcc;
mod errors; mod errors;
mod common_format;
mod parser;
mod validation;
mod raw_chunk_reader; mod raw_chunk_reader;
mod audio_frame_reader; mod audio_frame_reader;
mod chunks; mod chunks;
mod bext;
mod fmt;
mod wavereader; mod wavereader;
mod wavewriter; mod wavewriter;
pub use wavereader::{WaveReader};
pub use chunks::{WaveFmt,Bext};
pub use errors::Error; pub use errors::Error;
pub use wavereader::{WaveReader};
pub use bext::Bext;
pub use fmt::{WaveFmt, WaveFmtExtended, ChannelDescriptor};
pub use common_format::CommonFormat;
pub use audio_frame_reader::AudioFrameReader;

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,9 @@ use std::fs::File;
use std::io::Cursor; use std::io::Cursor;
use super::errors::Error; 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 super::fourcc::{FourCC, RIFF_SIG, WAVE_SIG, FMT__SIG, JUNK_SIG, BEXT_SIG, DATA_SIG, WriteFourCC};
use byteorder::LittleEndian; use byteorder::LittleEndian;

BIN
tests/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,10 @@
#!/bin/zsh #!/bin/zsh
mkdir -p media mkdir -p media
cd media
touch media/error.wav
tar xzf test_media.tgz
# create a silent bext wave file with fixture metadata and a time refernce starting at # create a silent bext wave file with fixture metadata and a time refernce starting at
# one minute # one minute
@@ -9,40 +12,34 @@ cd media
# Keywords for bext metadata are here... # Keywords for bext metadata are here...
# https://github.com/FFmpeg/FFmpeg/blob/17a0dfebf55f67653c29a607545a799f12bc0c01/libavformat/wavenc.c#L110 # 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 \ # 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 "description=FFMPEG-generated stereo WAV file with bext metadata" \
-metadata "originator=ffmpeg" \ # -metadata "originator=ffmpeg" \
-metadata "originator_reference=STEREO_WAVE_TEST" \ # -metadata "originator_reference=STEREO_WAVE_TEST" \
-metadata "time_reference=2880000" \ # -metadata "time_reference=2880000" \
-metadata "origination_date=2020-11-18" \ # -metadata "origination_date=2020-11-18" \
-metadata "origination_time=12:00:00" \ # -metadata "origination_time=12:00:00" \
-metadata "umid=0xFF00FF00FF00FF00FF00FF00FF00FF00" \ # -metadata "umid=0xFF00FF00FF00FF00FF00FF00FF00FF00" \
-metadata "coding_history=A:PCM,48K" ff_bwav_stereo.wav # -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 \ # 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 "description=FFMPEG-generated 5.1 WAV file with bext metadata" \
-metadata "originator=ffmpeg" \ # -metadata "originator=ffmpeg" \
-metadata "originator_reference=5_1_WAVE_TEST" \ # -metadata "originator_reference=5_1_WAVE_TEST" \
-metadata "time_reference=0" \ # -metadata "time_reference=0" \
-metadata "origination_date=2020-11-18" \ # -metadata "origination_date=2020-11-18" \
-metadata "origination_time=13:00:00" \ # -metadata "origination_time=13:00:00" \
-metadata "umid=0xFF00FF00FF00FF00FF00FF00FF00FF01" \ # -metadata "umid=0xFF00FF00FF00FF00FF00FF00FF00FF01" \
-metadata "coding_history=A:PCM,48K" ff_bwav_51.wav # -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 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" -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 \ # 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 \ # -write_bext 1 \
# -metadata "description=rf64 test file" ff_longfile.wav # -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 \ # 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 \ # -write_bext 1 \
-metadata "description=float test file" ff_float.wav # -metadata "description=float test file" ff_float.wav
touch error.wav
unzip ../arch_pt_media.zip
unzip ../arch_audacity_media.zip
rm -rf __MACOSX

View File

@@ -0,0 +1,783 @@
[
{
"streams": [
{
"index": 0,
"codec_name": "pcm_f32le",
"codec_long_name": "PCM 32-bit floating point little-endian",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "[3][0][0][0]",
"codec_tag": "0x0003",
"sample_fmt": "flt",
"sample_rate": "48000",
"channels": 1,
"bits_per_sample": 32,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"duration_ts": 24000,
"duration": "0.500000",
"bit_rate": "1536000",
"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/pt_float.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.500000",
"size": "113344",
"bit_rate": "1813504",
"probe_score": 99,
"tags": {
"encoded_by": "Pro Tools",
"originator_reference": "aaijNFpjYOIk",
"date": "2020-11-18",
"creation_time": "19:57:21",
"time_reference": "1036416000",
"umid": "0x060A2B340101010501010F10130000002A3224F7E7248000F0B04F71BFE13E00"
}
}
},
{
"streams": [
{
"index": 0,
"codec_name": "pcm_s16le",
"codec_long_name": "PCM signed 16-bit little-endian",
"codec_type": "audio",
"codec_time_base": "1/44100",
"codec_tag_string": "[1][0][0][0]",
"codec_tag": "0x0001",
"sample_fmt": "s16",
"sample_rate": "44100",
"channels": 1,
"bits_per_sample": 16,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/44100",
"duration_ts": 4418,
"duration": "0.100181",
"bit_rate": "705600",
"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/audacity_16bit.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.100181",
"size": "9046",
"bit_rate": "722372",
"probe_score": 99,
"tags": {
"title": "Pink Noise 16bit",
"album": "Test Media",
"artist": "Jamie Hardt"
}
}
},
{
"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": 6,
"channel_layout": "5.1",
"bits_per_sample": 24,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"duration_ts": 24000,
"duration": "0.500000",
"bit_rate": "6912000",
"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/pt_24bit_51.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.500000",
"size": "452910",
"bit_rate": "7246560",
"probe_score": 99,
"tags": {
"encoded_by": "Pro Tools",
"originator_reference": "aaijNdlHDPIk",
"date": "2020-11-18",
"creation_time": "19:58:18",
"time_reference": "1036416000",
"umid": "0x060A2B340101010501010F10130000002A5D84B0E724800060654F71BFE13E00"
}
}
},
{
"streams": [
{
"index": 0,
"codec_name": "pcm_f32le",
"codec_long_name": "PCM 32-bit floating point little-endian",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "[3][0][0][0]",
"codec_tag": "0x0003",
"sample_fmt": "flt",
"sample_rate": "48000",
"channels": 2,
"bits_per_sample": 32,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"duration_ts": 24000,
"duration": "0.500000",
"bit_rate": "3072000",
"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/pt_float_stereo.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.500000",
"size": "210058",
"bit_rate": "3360928",
"probe_score": 99,
"tags": {
"encoded_by": "Pro Tools",
"originator_reference": "aaijN!1TjQIk",
"date": "2020-11-18",
"creation_time": "19:59:16",
"time_reference": "1036416000",
"umid": "0x060A2B340101010501010F10130000002A89B75FE724800012164F71BFE13E00"
}
}
},
{
"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,
"bits_per_sample": 24,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"duration_ts": 24000,
"duration": "0.500000",
"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/pt_24bit_stereo.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.500000",
"size": "162058",
"bit_rate": "2592928",
"probe_score": 99,
"tags": {
"encoded_by": "Pro Tools",
"originator_reference": "aaijNF9aoPIk",
"date": "2020-11-18",
"creation_time": "19:57:57",
"time_reference": "1036416000",
"umid": "0x060A2B340101010501010F10130000002A4E03D7E724800059E44F71BFE13E00"
}
}
},
{
"streams": [
{
"index": 0,
"codec_name": "pcm_f32le",
"codec_long_name": "PCM 32-bit floating point little-endian",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "[3][0][0][0]",
"codec_tag": "0x0003",
"sample_fmt": "flt",
"sample_rate": "48000",
"channels": 1,
"channel_layout": "mono",
"bits_per_sample": 32,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"duration_ts": 4800,
"duration": "0.100000",
"bit_rate": "1536000",
"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_float.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.100000",
"size": "19924",
"bit_rate": "1593920",
"probe_score": 99,
"tags": {
"comment": "float test file",
"time_reference": "0",
"encoder": "Lavf58.29.100"
}
}
},
{
"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_bwav_stereo.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.100000",
"size": "29522",
"bit_rate": "2361760",
"probe_score": 99,
"tags": {
"comment": "FFMPEG-generated stereo WAV file with bext metadata",
"encoded_by": "ffmpeg",
"originator_reference": "STEREO_WAVE_TEST",
"date": "2020-11-18",
"creation_time": "12:00:00",
"time_reference": "2880000",
"umid": "0x7FFFFFFFFFFFFFFF7FFFFFFFFFFFFFFF00000000000000000000000000000000",
"coding_history": "A:PCM,48K",
"encoder": "Lavf58.29.100"
}
}
},
{
"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": 1,
"bits_per_sample": 24,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"duration_ts": 24000,
"duration": "0.500000",
"bit_rate": "1152000",
"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/pt_24bit.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.500000",
"size": "89344",
"bit_rate": "1429504",
"probe_score": 99,
"tags": {
"encoded_by": "Pro Tools",
"originator_reference": "aaijNtnVOOIk",
"date": "2020-11-18",
"creation_time": "19:57:08",
"time_reference": "1036416000",
"umid": "0x060A2B340101010501010F10130000002A28BCD4E72480004F4A4F71BFE13E00"
}
}
},
{
"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": 6,
"channel_layout": "5.1",
"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": "6912000",
"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_bwav_51.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.100000",
"size": "87122",
"bit_rate": "6969760",
"probe_score": 99,
"tags": {
"comment": "FFMPEG-generated 5.1 WAV file with bext metadata",
"encoded_by": "ffmpeg",
"originator_reference": "5_1_WAVE_TEST",
"date": "2020-11-18",
"creation_time": "13:00:00",
"time_reference": "0",
"umid": "0x7FFFFFFFFFFFFFFF7FFFFFFFFFFFFFFF00000000000000000000000000000000",
"coding_history": "A:PCM,48K",
"encoder": "Lavf58.29.100"
}
}
},
{
"streams": [
{
"index": 0,
"codec_name": "pcm_s24le",
"codec_long_name": "PCM signed 24-bit little-endian",
"codec_type": "audio",
"codec_time_base": "1/96000",
"codec_tag_string": "[1][0][0][0]",
"codec_tag": "0x0001",
"sample_fmt": "s32",
"sample_rate": "96000",
"channels": 6,
"channel_layout": "5.1",
"bits_per_sample": 24,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/96000",
"duration_ts": 259200000,
"duration": "2700.000000",
"bit_rate": "13824000",
"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_longfile.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "2700.000000",
"size": "4665600748",
"bit_rate": "13824002",
"probe_score": 100,
"tags": {
"comment": "rf64 test file",
"time_reference": "0",
"encoder": "Lavf58.29.100"
}
}
},
{
"streams": [
{
"index": 0,
"codec_name": "pcm_f32le",
"codec_long_name": "PCM 32-bit floating point little-endian",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "[3][0][0][0]",
"codec_tag": "0x0003",
"sample_fmt": "flt",
"sample_rate": "48000",
"channels": 6,
"channel_layout": "5.1",
"bits_per_sample": 32,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"duration_ts": 24000,
"duration": "0.500000",
"bit_rate": "9216000",
"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/pt_float_51.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.500000",
"size": "596910",
"bit_rate": "9550560",
"probe_score": 99,
"tags": {
"encoded_by": "Pro Tools",
"originator_reference": "aaijNViK8PIk",
"date": "2020-11-18",
"creation_time": "19:58:59",
"time_reference": "1036416000",
"umid": "0x060A2B340101010501010F10130000002A7C908BE724800047E94F71BFE13E00"
}
}
},
{
"streams": [
{
"index": 0,
"codec_name": "pcm_s16le",
"codec_long_name": "PCM signed 16-bit little-endian",
"codec_type": "audio",
"codec_time_base": "1/44100",
"codec_tag_string": "[1][0][0][0]",
"codec_tag": "0x0001",
"sample_fmt": "s16",
"sample_rate": "44100",
"channels": 1,
"bits_per_sample": 16,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/44100",
"duration_ts": 44100,
"duration": "1.000000",
"bit_rate": "705600",
"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_minimal.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "1.000000",
"size": "88244",
"bit_rate": "705952",
"probe_score": 99
}
},
{},
{
"streams": [
{
"index": 0,
"codec_name": "pcm_s16le",
"codec_long_name": "PCM signed 16-bit little-endian",
"codec_type": "audio",
"codec_time_base": "1/44100",
"codec_tag_string": "[1][0][0][0]",
"codec_tag": "0x0001",
"sample_fmt": "s16",
"sample_rate": "44100",
"channels": 1,
"bits_per_sample": 16,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/44100",
"duration_ts": 44100,
"duration": "1.000000",
"bit_rate": "705600",
"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_silence.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "1.000000",
"size": "88278",
"bit_rate": "706224",
"probe_score": 99,
"tags": {
"encoder": "Lavf58.29.100"
}
}
},
{
"streams": [
{
"index": 0,
"codec_name": "pcm_s16le",
"codec_long_name": "PCM signed 16-bit little-endian",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "[1][0][0][0]",
"codec_tag": "0x0001",
"sample_fmt": "s16",
"sample_rate": "48000",
"channels": 1,
"bits_per_sample": 16,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"duration_ts": 24000,
"duration": "0.500000",
"bit_rate": "768000",
"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/pt_16bit.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.500000",
"size": "65344",
"bit_rate": "1045504",
"probe_score": 99,
"tags": {
"encoded_by": "Pro Tools",
"originator_reference": "aaijNZdiDOIk",
"date": "2020-11-18",
"creation_time": "19:56:53",
"time_reference": "1036416000",
"umid": "0x060A2B340101010501010F10130000002A1D203CE7248000711E4F71BFE13E00"
}
}
}
]

View File

@@ -0,0 +1,55 @@
extern crate serde_json;
use core::fmt::Debug;
use serde_json::{Value, from_str};
use std::fs::File;
use std::io::Read;
use bwavfile::WaveReader;
// 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/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() { /* */
v.iter()
.filter(|value| {
!value["format"]["filename"].is_null()
})
.for_each(|value| {
let filen : &str = value["format"]["filename"].as_str().unwrap();
let json_value : &Value = &value["streams"][0][stream_key];
let mut wavfile = WaveReader::open(filen).unwrap();
let wavfile_value: T = other(&mut wavfile);
println!("asserting {} for {}",stream_key, filen);
assert_eq!(Into::<Value>::into(wavfile_value), *json_value);
})
}
}
#[test]
fn test_frame_count() {
assert_match_stream("duration_ts", |w| w.frame_length().unwrap() );
}
#[test]
fn test_sample_rate() {
assert_match_stream("sample_rate", |w| format!("{}", w.format().unwrap().sample_rate) );
}
#[test]
fn test_channel_count() {
assert_match_stream("channels", |w| w.format().unwrap().channel_count );
}

View File

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

BIN
tests/test_media.tgz Normal file

Binary file not shown.