Add support for reading WAVE files

This commit is contained in:
Daniel Wolf 2022-11-16 21:22:25 +01:00
parent 2cd2ba77b9
commit 9c3b1fb554
45 changed files with 1371 additions and 2 deletions

110
rhubarb/Cargo.lock generated
View File

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "assert_matches" name = "assert_matches"
version = "1.5.0" version = "1.5.0"
@ -14,12 +23,41 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "demonstrate"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "484663f95fe33ff3576d9109a474d37bc163fa3c4a134c7288e856db93230987"
dependencies = [
"proc-macro2",
"quote",
"syn",
"voca_rs",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.10" version = "1.0.10"
@ -121,6 +159,12 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.17" version = "0.4.17"
@ -212,6 +256,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "once_cell"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.9" version = "0.2.9"
@ -242,14 +292,36 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "regex"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]] [[package]]
name = "rhubarb-audio" name = "rhubarb-audio"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"assert_matches", "assert_matches",
"byteorder",
"demonstrate",
"derivative",
"dyn-clone", "dyn-clone",
"log", "log",
"once_cell",
"rstest", "rstest",
"rstest_reuse",
"speculoos", "speculoos",
] ]
@ -278,6 +350,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "rstest_reuse"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9b5aed35457441e7e0db509695ba3932d4c47e046777141c167efe584d0ec17"
dependencies = [
"quote",
"rustc_version",
"syn",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.0" version = "0.4.0"
@ -311,6 +394,16 @@ dependencies = [
"num", "num",
] ]
[[package]]
name = "stfu8"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1310970b29733b601839578f8ba24991a97057dbedc4ac0decea835474054ee7"
dependencies = [
"lazy_static",
"regex",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.107" version = "1.0.107"
@ -327,3 +420,20 @@ name = "unicode-ident"
version = "1.0.6" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
[[package]]
name = "unicode-segmentation"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
[[package]]
name = "voca_rs"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e44efbf25e32768d5ecd22244feacc3d3b3eca72d318f5ef0a4764c2c158e18"
dependencies = [
"regex",
"stfu8",
"unicode-segmentation",
]

View File

@ -4,10 +4,15 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
byteorder = "1.4.3"
derivative = "2.2.0"
dyn-clone = "1.0.10" dyn-clone = "1.0.10"
log = "0.4.17" log = "0.4.17"
once_cell = "1.17.0"
[dev-dependencies] [dev-dependencies]
assert_matches = "1.5.0" assert_matches = "1.5.0"
demonstrate = "0.4.5"
rstest = "0.15.0" rstest = "0.15.0"
rstest_reuse = "0.4.0"
speculoos = "0.10.0" speculoos = "0.10.0"

View File

@ -1,7 +1,6 @@
use crate::audio_error::AudioError; use crate::audio_error::AudioError;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use std::fmt::Debug; use std::{fmt::Debug, time::Duration};
use std::time::Duration;
const NANOS_PER_SEC: u32 = 1_000_000_000; const NANOS_PER_SEC: u32 = 1_000_000_000;

View File

@ -14,6 +14,13 @@
mod audio_clip; mod audio_clip;
mod audio_error; mod audio_error;
mod open_audio_file;
mod read_and_seek;
mod sample_reader_assertions;
mod wave_audio_clip;
pub use audio_clip::{AudioClip, Sample, SampleReader}; pub use audio_clip::{AudioClip, Sample, SampleReader};
pub use audio_error::AudioError; pub use audio_error::AudioError;
pub use open_audio_file::{open_audio_file, open_audio_file_with_reader};
pub use read_and_seek::ReadAndSeek;
pub use wave_audio_clip::wave_audio_clip::WaveAudioClip;

View File

@ -0,0 +1,35 @@
use std::{
fs::File,
io::{self, BufReader},
path::PathBuf,
};
use crate::{AudioClip, AudioError, ReadAndSeek, WaveAudioClip};
/// Creates an audio clip from the specified file.
pub fn open_audio_file(path: impl Into<PathBuf>) -> Result<Box<dyn AudioClip>, AudioError> {
let path: PathBuf = path.into();
open_audio_file_with_reader(
path.clone(),
Box::new(move || Ok(BufReader::new(File::open(path.clone())?))),
)
}
/// Creates an audio clip from the specified file, using the reader returned by the specified
/// factory function.
pub fn open_audio_file_with_reader<TReader>(
path: impl Into<PathBuf>,
create_reader: Box<dyn Fn() -> Result<TReader, io::Error>>,
) -> Result<Box<dyn AudioClip>, AudioError>
where
TReader: 'static + ReadAndSeek,
{
let path: PathBuf = path.into();
let lower_case_extension = path
.extension()
.map(|e| e.to_os_string().into_string().unwrap_or_default());
match lower_case_extension.as_deref() {
Some("wav") => Ok(Box::new(WaveAudioClip::new(create_reader)?)),
_ => Err(AudioError::UnsupportedFileType),
}
}

View File

@ -0,0 +1,6 @@
use std::io::{Read, Seek};
/// A seekable reader.
pub trait ReadAndSeek: Read + Seek {}
impl<T: Seek + Read> ReadAndSeek for T {}

View File

@ -0,0 +1,27 @@
use crate::{Sample, SampleReader};
pub trait SampleReaderAssertions {
fn assert_valid_seek_position(&self, position: u64);
fn assert_valid_read_size(&self, buffer: &[Sample]);
}
impl<TSampleReader: SampleReader> SampleReaderAssertions for TSampleReader {
fn assert_valid_seek_position(&self, position: u64) {
assert!(
position <= self.len(),
"Attempting to seek to position {} of {}-frame audio clip.",
position,
self.len()
);
}
fn assert_valid_read_size(&self, buffer: &[Sample]) {
let end = self.position() + buffer.len() as u64;
assert!(
end <= self.len(),
"Attempting to read up to position {} of {}-frame audio clip.",
end,
self.len()
);
}
}

View File

@ -0,0 +1,285 @@
use once_cell::sync::Lazy;
use std::collections::HashMap;
pub fn codec_to_string(codec: u16) -> String {
CODEC_NAMES
.get(&codec)
.map_or_else(|| format!("{codec:#06X}"), |str| (*str).to_owned())
}
static CODEC_NAMES: Lazy<HashMap<u16, &str>> = Lazy::new(|| {
HashMap::from([
(0x0001, "PCM"),
(0x0002, "Microsoft ADPCM"),
(0x0003, "IEEE Float"),
(0x0004, "Compaq VSELP"),
(0x0005, "IBM CVSD"),
(0x0006, "Microsoft a-Law"),
(0x0007, "Microsoft u-Law"),
(0x0008, "Microsoft DTS"),
(0x0009, "DRM"),
(0x000a, "WMA 9 Speech"),
(0x000b, "Microsoft Windows Media RT Voice"),
(0x0010, "OKI-ADPCM"),
(0x0011, "Intel IMA/DVI-ADPCM"),
(0x0012, "Videologic Mediaspace ADPCM"),
(0x0013, "Sierra ADPCM"),
(0x0014, "Antex G.723 ADPCM"),
(0x0015, "DSP Solutions DIGISTD"),
(0x0016, "DSP Solutions DIGIFIX"),
(0x0017, "Dialoic OKI ADPCM"),
(0x0018, "Media Vision ADPCM"),
(0x0019, "HP CU"),
(0x001a, "HP Dynamic Voice"),
(0x0020, "Yamaha ADPCM"),
(0x0021, "SONARC Speech Compression"),
(0x0022, "DSP Group True Speech"),
(0x0023, "Echo Speech Corp."),
(0x0024, "Virtual Music Audiofile AF36"),
(0x0025, "Audio Processing Tech."),
(0x0026, "Virtual Music Audiofile AF10"),
(0x0027, "Aculab Prosody 1612"),
(0x0028, "Merging Tech. LRC"),
(0x0030, "Dolby AC2"),
(0x0031, "Microsoft GSM610"),
(0x0032, "MSN Audio"),
(0x0033, "Antex ADPCME"),
(0x0034, "Control Resources VQLPC"),
(0x0035, "DSP Solutions DIGIREAL"),
(0x0036, "DSP Solutions DIGIADPCM"),
(0x0037, "Control Resources CR10"),
(0x0038, "Natural MicroSystems VBX ADPCM"),
(0x0039, "Crystal Semiconductor IMA ADPCM"),
(0x003a, "Echo Speech ECHOSC3"),
(0x003b, "Rockwell ADPCM"),
(0x003c, "Rockwell DIGITALK"),
(0x003d, "Xebec Multimedia"),
(0x0040, "Antex G.721 ADPCM"),
(0x0041, "Antex G.728 CELP"),
(0x0042, "Microsoft MSG723"),
(0x0043, "IBM AVC ADPCM"),
(0x0045, "ITU-T G.726"),
(0x0050, "Microsoft MPEG"),
(0x0051, "RT23 or PAC"),
(0x0052, "InSoft RT24"),
(0x0053, "InSoft PAC"),
(0x0055, "MP3"),
(0x0059, "Cirrus"),
(0x0060, "Cirrus Logic"),
(0x0061, "ESS Tech. PCM"),
(0x0062, "Voxware Inc."),
(0x0063, "Canopus ATRAC"),
(0x0064, "APICOM G.726 ADPCM"),
(0x0065, "APICOM G.722 ADPCM"),
(0x0066, "Microsoft DSAT"),
(0x0067, "Micorsoft DSAT DISPLAY"),
(0x0069, "Voxware Byte Aligned"),
(0x0070, "Voxware AC8"),
(0x0071, "Voxware AC10"),
(0x0072, "Voxware AC16"),
(0x0073, "Voxware AC20"),
(0x0074, "Voxware MetaVoice"),
(0x0075, "Voxware MetaSound"),
(0x0076, "Voxware RT29HW"),
(0x0077, "Voxware VR12"),
(0x0078, "Voxware VR18"),
(0x0079, "Voxware TQ40"),
(0x007a, "Voxware SC3"),
(0x007b, "Voxware SC3"),
(0x0080, "Soundsoft"),
(0x0081, "Voxware TQ60"),
(0x0082, "Microsoft MSRT24"),
(0x0083, "AT&T G.729A"),
(0x0084, "Motion Pixels MVI MV12"),
(0x0085, "DataFusion G.726"),
(0x0086, "DataFusion GSM610"),
(0x0088, "Iterated Systems Audio"),
(0x0089, "Onlive"),
(0x008a, "Multitude, Inc. FT SX20"),
(0x008b, "Infocom ITS A/S G.721 ADPCM"),
(0x008c, "Convedia G729"),
(0x008d, "Not specified congruency, Inc."),
(0x0091, "Siemens SBC24"),
(0x0092, "Sonic Foundry Dolby AC3 APDIF"),
(0x0093, "MediaSonic G.723"),
(0x0094, "Aculab Prosody 8kbps"),
(0x0097, "ZyXEL ADPCM"),
(0x0098, "Philips LPCBB"),
(0x0099, "Studer Professional Audio Packed"),
(0x00a0, "Malden PhonyTalk"),
(0x00a1, "Racal Recorder GSM"),
(0x00a2, "Racal Recorder G720.a"),
(0x00a3, "Racal G723.1"),
(0x00a4, "Racal Tetra ACELP"),
(0x00b0, "NEC AAC NEC Corporation"),
(0x00ff, "AAC"),
(0x0100, "Rhetorex ADPCM"),
(0x0101, "IBM u-Law"),
(0x0102, "IBM a-Law"),
(0x0103, "IBM ADPCM"),
(0x0111, "Vivo G.723"),
(0x0112, "Vivo Siren"),
(0x0120, "Philips Speech Processing CELP"),
(0x0121, "Philips Speech Processing GRUNDIG"),
(0x0123, "Digital G.723"),
(0x0125, "Sanyo LD ADPCM"),
(0x0130, "Sipro Lab ACEPLNET"),
(0x0131, "Sipro Lab ACELP4800"),
(0x0132, "Sipro Lab ACELP8V3"),
(0x0133, "Sipro Lab G.729"),
(0x0134, "Sipro Lab G.729A"),
(0x0135, "Sipro Lab Kelvin"),
(0x0136, "VoiceAge AMR"),
(0x0140, "Dictaphone G.726 ADPCM"),
(0x0150, "Qualcomm PureVoice"),
(0x0151, "Qualcomm HalfRate"),
(0x0155, "Ring Zero Systems TUBGSM"),
(0x0160, "Microsoft Audio1"),
(
0x0161,
"Windows Media Audio V2 V7 V8 V9 / DivX audio (WMA) / Alex AC3 Audio",
),
(0x0162, "Windows Media Audio Professional V9"),
(0x0163, "Windows Media Audio Lossless V9"),
(0x0164, "WMA Pro over S/PDIF"),
(0x0170, "UNISYS NAP ADPCM"),
(0x0171, "UNISYS NAP ULAW"),
(0x0172, "UNISYS NAP ALAW"),
(0x0173, "UNISYS NAP 16K"),
(0x0174, "MM SYCOM ACM SYC008 SyCom Technologies"),
(0x0175, "MM SYCOM ACM SYC701 G726L SyCom Technologies"),
(0x0176, "MM SYCOM ACM SYC701 CELP54 SyCom Technologies"),
(0x0177, "MM SYCOM ACM SYC701 CELP68 SyCom Technologies"),
(0x0178, "Knowledge Adventure ADPCM"),
(0x0180, "Fraunhofer IIS MPEG2AAC"),
(0x0190, "Digital Theater Systems DTS DS"),
(0x0200, "Creative Labs ADPCM"),
(0x0202, "Creative Labs FASTSPEECH8"),
(0x0203, "Creative Labs FASTSPEECH10"),
(0x0210, "UHER ADPCM"),
(0x0215, "Ulead DV ACM"),
(0x0216, "Ulead DV ACM"),
(0x0220, "Quarterdeck Corp."),
(0x0230, "I-Link VC"),
(0x0240, "Aureal Semiconductor Raw Sport"),
(0x0241, "ESST AC3"),
(0x0250, "Interactive Products HSX"),
(0x0251, "Interactive Products RPELP"),
(0x0260, "Consistent CS2"),
(0x0270, "Sony SCX"),
(0x0271, "Sony SCY"),
(0x0272, "Sony ATRAC3"),
(0x0273, "Sony SPC"),
(0x0280, "TELUM Telum Inc."),
(0x0281, "TELUMIA Telum Inc."),
(0x0285, "Norcom Voice Systems ADPCM"),
(0x0300, "Fujitsu FM TOWNS SND"),
(0x0301, "Fujitsu (not specified)"),
(0x0302, "Fujitsu (not specified)"),
(0x0303, "Fujitsu (not specified)"),
(0x0304, "Fujitsu (not specified)"),
(0x0305, "Fujitsu (not specified)"),
(0x0306, "Fujitsu (not specified)"),
(0x0307, "Fujitsu (not specified)"),
(0x0308, "Fujitsu (not specified)"),
(0x0350, "Micronas Semiconductors, Inc. Development"),
(0x0351, "Micronas Semiconductors, Inc. CELP833"),
(0x0400, "Brooktree Digital"),
(0x0401, "Intel Music Coder (IMC)"),
(0x0402, "Ligos Indeo Audio"),
(0x0450, "QDesign Music"),
(0x0500, "On2 VP7 On2 Technologies"),
(0x0501, "On2 VP6 On2 Technologies"),
(0x0680, "AT&T VME VMPCM"),
(0x0681, "AT&T TCP"),
(0x0700, "YMPEG Alpha (dummy for MPEG-2 compressor)"),
(0x08ae, "ClearJump LiteWave (lossless)"),
(0x1000, "Olivetti GSM"),
(0x1001, "Olivetti ADPCM"),
(0x1002, "Olivetti CELP"),
(0x1003, "Olivetti SBC"),
(0x1004, "Olivetti OPR"),
(0x1100, "Lernout & Hauspie"),
(0x1101, "Lernout & Hauspie CELP codec"),
(0x1102, "Lernout & Hauspie SBC codec"),
(0x1103, "Lernout & Hauspie SBC codec"),
(0x1104, "Lernout & Hauspie SBC codec"),
(0x1400, "Norris Comm. Inc."),
(0x1401, "ISIAudio"),
(0x1500, "AT&T Soundspace Music Compression"),
(0x181c, "VoxWare RT24 speech codec"),
(0x181e, "Lucent elemedia AX24000P Music codec"),
(0x1971, "Sonic Foundry LOSSLESS"),
(0x1979, "Innings Telecom Inc. ADPCM"),
(0x1c07, "Lucent SX8300P speech codec"),
(0x1c0c, "Lucent SX5363S G.723 compliant codec"),
(0x1f03, "CUseeMe DigiTalk (ex-Rocwell)"),
(0x1fc4, "NCT Soft ALF2CD ACM"),
(0x2000, "FAST Multimedia DVM"),
(0x2001, "Dolby DTS (Digital Theater System)"),
(0x2002, "RealAudio 1 / 2 14.4"),
(0x2003, "RealAudio 1 / 2 28.8"),
(0x2004, "RealAudio G2 / 8 Cook (low bitrate)"),
(0x2005, "RealAudio 3 / 4 / 5 Music (DNET)"),
(0x2006, "RealAudio 10 AAC (RAAC)"),
(0x2007, "RealAudio 10 AAC+ (RACP)"),
(0x2500, "Reserved range to 0x2600 Microsoft"),
(
0x3313,
"makeAVIS (ffvfw fake AVI sound from AviSynth scripts)",
),
(0x4143, "Divio MPEG-4 AAC audio"),
(0x4201, "Nokia adaptive multirate"),
(0x4243, "Divio G726 Divio, Inc."),
(0x434c, "LEAD Speech"),
(0x564c, "LEAD Vorbis"),
(0x5756, "WavPack Audio"),
(0x674f, "Ogg Vorbis (mode 1)"),
(0x6750, "Ogg Vorbis (mode 2)"),
(0x6751, "Ogg Vorbis (mode 3)"),
(0x676f, "Ogg Vorbis (mode 1+)"),
(0x6770, "Ogg Vorbis (mode 2+)"),
(0x6771, "Ogg Vorbis (mode 3+)"),
(0x7000, "3COM NBX 3Com Corporation"),
(0x706d, "FAAD AAC"),
(0x7a21, "GSM-AMR (CBR, no SID)"),
(0x7a22, "GSM-AMR (VBR, including SID)"),
(0xa100, "Comverse Infosys Ltd. G723 1"),
(0xa101, "Comverse Infosys Ltd. AVQSBC"),
(0xa102, "Comverse Infosys Ltd. OLDSBC"),
(0xa103, "Symbol Technologies G729A"),
(0xa104, "VoiceAge AMR WB VoiceAge Corporation"),
(0xa105, "Ingenient Technologies Inc. G726"),
(0xa106, "ISO/MPEG-4 advanced audio Coding"),
(0xa107, "Encore Software Ltd G726"),
(0xa109, "Speex ACM Codec xiph.org"),
(0xdfac, "DebugMode SonicFoundry Vegas FrameServer ACM Codec"),
(0xf1ac, "Free Lossless Audio Codec FLAC"),
(0xfffe, "Extensible"),
(0xffff, "Development"),
])
});
#[cfg(test)]
mod tests {
use super::*;
use speculoos::prelude::*;
mod codec_to_string {
use super::*;
#[test]
fn returns_name_for_known_codecs() {
assert_that!(codec_to_string(0x0001)).is_equal_to("PCM".to_owned());
assert_that!(codec_to_string(0x674f)).is_equal_to("Ogg Vorbis (mode 1)".to_owned());
assert_that!(codec_to_string(0xf1ac))
.is_equal_to("Free Lossless Audio Codec FLAC".to_owned());
}
#[test]
fn returns_hex_id_for_unknown_codecs() {
assert_that!(codec_to_string(0x0000)).is_equal_to("0x0000".to_owned());
assert_that!(codec_to_string(0xeeee)).is_equal_to("0xEEEE".to_owned());
}
}
}

View File

@ -0,0 +1,66 @@
use std::io::{self, Read};
/// A four-character code (see https://en.wikipedia.org/wiki/FourCC).
pub type FourCc = [u8; 4];
pub fn four_cc_to_string(four_cc: &FourCc) -> String {
four_cc
.iter()
.map(|char_code| char::from_u32(u32::from(*char_code)).unwrap())
.collect::<String>()
}
pub trait ReadFourCcExt {
fn read_four_cc(&mut self) -> Result<FourCc, io::Error>;
}
impl<TReader: Read> ReadFourCcExt for TReader {
fn read_four_cc(&mut self) -> Result<FourCc, io::Error> {
let mut result = FourCc::default();
self.read_exact(&mut result)?;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use speculoos::prelude::*;
mod four_cc_to_string {
use super::*;
#[test]
fn returns_a_string_representation() {
assert_that!(four_cc_to_string(b"WAVE")).is_equal_to("WAVE".to_owned());
assert_that!(four_cc_to_string(b"fmt ")).is_equal_to("fmt ".to_owned());
}
#[test]
fn supports_arbitrary_bytes() {
assert_that!(four_cc_to_string(&[0xa9, 0xff, 0x00, 0x0a]))
.is_equal_to("©ÿ\0\n".to_owned());
}
}
mod read_four_cc {
use std::io::{Cursor, ErrorKind};
use super::*;
#[test]
fn reads_from_a_reader() {
let mut reader = Cursor::new(b"ABCDEFGH");
assert_that!(reader.read_four_cc()).is_ok_containing(b"ABCD");
assert_that!(reader.read_four_cc()).is_ok_containing(b"EFGH");
}
#[test]
fn fails_when_reading_past_the_end() {
let mut reader = Cursor::new(b"AB");
assert_that!(reader.read_four_cc())
.is_err()
.matches(|error| error.kind() == ErrorKind::UnexpectedEof);
}
}
}

View File

@ -0,0 +1,4 @@
mod codecs;
mod four_cc;
pub mod wave_audio_clip;
mod wave_file_info;

View File

@ -0,0 +1,196 @@
use byteorder::{ReadBytesExt, LE};
use derivative::Derivative;
use crate::{
audio_clip::{AudioClip, SampleReader},
audio_error::AudioError,
sample_reader_assertions::SampleReaderAssertions,
ReadAndSeek,
};
use std::{
io::{self, SeekFrom},
sync::Arc,
};
use super::wave_file_info::{get_wave_file_info, SampleFormat, WaveFileInfo};
/// An audio clip read on the fly from a WAVE file.
#[derive(Derivative)]
#[derivative(Debug)]
pub struct WaveAudioClip<TReader> {
wave_file_info: WaveFileInfo,
#[derivative(Debug = "ignore")]
create_reader: Arc<dyn Fn() -> Result<TReader, io::Error>>,
}
impl<TReader> WaveAudioClip<TReader>
where
TReader: ReadAndSeek,
{
/// Creates a new `WaveAudioClip` for the reader returned by the given callback function.
pub fn new(
create_reader: Box<dyn Fn() -> Result<TReader, io::Error>>,
) -> Result<Self, AudioError> {
Ok(Self {
wave_file_info: get_wave_file_info(&mut create_reader()?)?,
create_reader: Arc::from(create_reader),
})
}
}
impl<TReader> Clone for WaveAudioClip<TReader> {
fn clone(&self) -> Self {
Self {
wave_file_info: self.wave_file_info,
create_reader: self.create_reader.clone(),
}
}
}
impl<TReader> AudioClip for WaveAudioClip<TReader>
where
TReader: ReadAndSeek + 'static,
{
fn len(&self) -> u64 {
self.wave_file_info.frame_count
}
fn sampling_rate(&self) -> u32 {
self.wave_file_info.sampling_rate
}
fn create_sample_reader(&self) -> Result<Box<dyn SampleReader>, AudioError> {
match self.wave_file_info.sample_format {
SampleFormat::U8 => {
const FACTOR: f32 = 1.0f32 / 0x80 as f32;
let sample_reader = WaveFileSampleReader::new(self, |reader| {
Ok((i32::from(reader.read_u8()?) - 0x80) as f32 * FACTOR)
})?;
Ok(Box::new(sample_reader))
}
SampleFormat::I16 => {
const FACTOR: f32 = 1.0f32 / 0x8000 as f32;
let sample_reader = WaveFileSampleReader::new(self, |reader| {
Ok(f32::from(reader.read_i16::<LE>()?) * FACTOR)
})?;
Ok(Box::new(sample_reader))
}
SampleFormat::I24 => {
const FACTOR: f32 = 1.0f32 / 0x800000 as f32;
let sample_reader = WaveFileSampleReader::new(self, |reader| {
let mut buffer = [0; 3];
reader.read_exact(&mut buffer)?;
// Make sure the most significant byte is set correctly for two's complement
let i24 = ((u32::from(buffer[0]) << 8
| u32::from(buffer[1]) << 16
| u32::from(buffer[2]) << 24) as i32)
>> 8;
Ok(i24 as f32 * FACTOR)
})?;
Ok(Box::new(sample_reader))
}
SampleFormat::I32 => {
const FACTOR: f32 = 1.0f32 / 0x80000000u32 as f32;
let sample_reader = WaveFileSampleReader::new(self, |reader| {
Ok(reader.read_i32::<LE>()? as f32 * FACTOR)
})?;
Ok(Box::new(sample_reader))
}
SampleFormat::F32 => {
let sample_reader =
WaveFileSampleReader::new(self, |reader| Ok(reader.read_f32::<LE>()?))?;
Ok(Box::new(sample_reader))
}
SampleFormat::F64 => {
let sample_reader =
WaveFileSampleReader::new(self, |reader| Ok(reader.read_f64::<LE>()? as f32))?;
Ok(Box::new(sample_reader))
}
}
}
}
#[derive(Derivative)]
#[derivative(Debug)]
struct WaveFileSampleReader<TReader, TReadSample>
where
TReadSample: Fn(&mut TReader) -> Result<f32, AudioError>,
{
wave_file_info: WaveFileInfo,
#[derivative(Debug = "ignore")]
reader: TReader,
// the position we're claiming to be at
logical_position: u64,
// the position we're actually at
physical_position: Option<u64>,
#[derivative(Debug = "ignore")]
read_sample: TReadSample,
}
impl<TReader, TReadSample> WaveFileSampleReader<TReader, TReadSample>
where
TReader: ReadAndSeek,
TReadSample: Fn(&mut TReader) -> Result<f32, AudioError>,
{
fn new(
audio_clip: &WaveAudioClip<TReader>,
read_sample: TReadSample,
) -> Result<Self, AudioError> {
let sample_reader = Self {
wave_file_info: audio_clip.wave_file_info,
reader: (audio_clip.create_reader)()?,
logical_position: 0,
physical_position: None,
read_sample,
};
Ok(sample_reader)
}
fn seek_physically(&mut self) -> Result<(), AudioError> {
if self.physical_position == Some(self.logical_position) {
return Ok(());
}
self.reader.seek(SeekFrom::Start(
self.wave_file_info.data_offset
+ self.logical_position * u64::from(self.wave_file_info.bytes_per_frame),
))?;
self.physical_position = Some(self.logical_position);
Ok(())
}
}
impl<TReader, TReadSample> SampleReader for WaveFileSampleReader<TReader, TReadSample>
where
TReader: ReadAndSeek,
TReadSample: Fn(&mut TReader) -> Result<f32, AudioError>,
{
fn len(&self) -> u64 {
self.wave_file_info.frame_count
}
fn position(&self) -> u64 {
self.logical_position
}
fn set_position(&mut self, position: u64) {
self.assert_valid_seek_position(position);
self.logical_position = position;
}
fn read(&mut self, buffer: &mut [crate::audio_clip::Sample]) -> Result<(), AudioError> {
self.assert_valid_read_size(buffer);
self.seek_physically()?;
let channel_count = self.wave_file_info.channel_count;
let factor = 1.0 / channel_count as f32;
for sample in buffer {
let mut sum: f32 = 0.0;
for _ in 0..channel_count {
sum += (self.read_sample)(&mut self.reader)?;
}
*sample = sum * factor;
}
Ok(())
}
}

View File

@ -0,0 +1,164 @@
use super::{codecs::codec_to_string, four_cc::ReadFourCcExt};
use crate::{audio_error::AudioError, wave_audio_clip::four_cc::four_cc_to_string, ReadAndSeek};
use byteorder::{ReadBytesExt, LE};
use std::io::SeekFrom;
#[derive(Debug, Copy, Clone)]
pub enum SampleFormat {
U8,
I16,
I24,
I32,
F32,
F64,
}
#[derive(Debug, Copy, Clone)]
pub struct WaveFileInfo {
pub sample_format: SampleFormat,
pub channel_count: u32,
pub sampling_rate: u32,
pub frame_count: u64,
pub bytes_per_frame: u32,
// The offset, in bytes, of the raw audio data within the file
pub data_offset: u64,
}
struct FormatInfo {
sample_format: SampleFormat,
channel_count: u32,
frame_rate: u32,
bytes_per_frame: u32,
}
struct DataInfo {
data_offset: u64,
data_byte_count: u64,
}
mod codecs {
pub const PCM: u16 = 0x0001;
pub const FLOAT: u16 = 0x0003;
pub const EXTENSIBLE: u16 = 0xFFFE;
}
pub fn get_wave_file_info(reader: &mut impl ReadAndSeek) -> Result<WaveFileInfo, AudioError> {
let master_chunk_id = reader.read_four_cc()?;
if &master_chunk_id != b"RIFF" {
return Err(AudioError::CorruptFile(format!(
"Expected master chunk ID \"RIFF\", got {:?}.",
four_cc_to_string(&master_chunk_id)
)));
}
reader.read_u32::<LE>()?; // Skip chunk size
let wave_chunk_id = reader.read_four_cc()?;
if &wave_chunk_id != b"WAVE" {
return Err(AudioError::CorruptFile(format!(
"Expected WAVE chunk ID \"WAVE\", got {:?}.",
four_cc_to_string(&wave_chunk_id),
)));
}
let mut format_info: Option<FormatInfo> = None;
let mut data_info: Option<DataInfo> = None;
while format_info.is_none() || data_info.is_none() {
let chunk_id = reader.read_four_cc()?;
let chunk_size = reader.read_u32::<LE>()?;
let chunk_end = round_up_to_even(reader.stream_position()? + u64::from(chunk_size));
match &chunk_id {
b"fmt " => {
// Format chunk
let mut codec = reader.read_u16::<LE>()?;
let channel_count = reader.read_u16::<LE>()?;
let frame_rate = reader.read_u32::<LE>()?;
reader.read_u32::<LE>()?; // Skip bytes per second
let bytes_per_frame = reader.read_u16::<LE>()?;
let mut bits_per_sample = reader.read_u16::<LE>()?;
if chunk_size > 16 {
let extension_size = reader.read_u16::<LE>()?;
if extension_size >= 22 {
// Read extension fields
bits_per_sample = reader.read_u16::<LE>()?;
reader.read_u32::<LE>()?; // Skip channel mask
let codec_override = reader.read_u16::<LE>()?;
if codec == codecs::EXTENSIBLE {
codec = codec_override
}
}
}
let (sample_format, bytes_per_sample) = match codec {
codecs::PCM => match bits_per_sample {
8 => (SampleFormat::U8, 1),
16 => (SampleFormat::I16, 2),
24 => (SampleFormat::I24, 3),
32 => (SampleFormat::I32, 4),
_ => {
return Err(AudioError::UnsupportedFileFeature(format!(
"Unsupported PCM sample size: {bits_per_sample} bits."
)));
}
},
codecs::FLOAT => match bits_per_sample {
32 => (SampleFormat::F32, 4),
64 => (SampleFormat::F64, 8),
_ => {
return Err(AudioError::UnsupportedFileFeature(format!(
"Unsupported floating-point sample size: {bits_per_sample} bits."
)));
}
},
_ => {
return Err(AudioError::UnsupportedFileFeature(format!(
"Unsupported audio codec: {}.",
codec_to_string(codec)
)));
}
};
let calculated_bytes_per_frame = bytes_per_sample * channel_count;
if bytes_per_frame != calculated_bytes_per_frame {
return Err(AudioError::CorruptFile(format!(
"Expected {calculated_bytes_per_frame} bytes per frame, got {bytes_per_frame}."
)));
}
format_info = Some(FormatInfo {
sample_format,
channel_count: u32::from(channel_count),
frame_rate,
bytes_per_frame: u32::from(bytes_per_frame),
});
}
b"data" => {
// Data chunk
let data_offset = reader.stream_position()?;
let data_byte_count = chunk_size;
data_info = Some(DataInfo {
data_offset,
data_byte_count: u64::from(data_byte_count),
});
}
_ => {}
}
reader.seek(SeekFrom::Start(chunk_end))?;
}
let data_info = data_info.unwrap();
let format_info = format_info.unwrap();
let frame_count = data_info.data_byte_count / u64::from(format_info.bytes_per_frame);
Ok(WaveFileInfo {
sample_format: format_info.sample_format,
channel_count: format_info.channel_count,
sampling_rate: format_info.frame_rate,
frame_count,
bytes_per_frame: format_info.bytes_per_frame,
data_offset: data_info.data_offset,
})
}
fn round_up_to_even(i: u64) -> u64 {
let is_even = i % 2 == 0;
if is_even { i } else { i + 1 }
}

View File

@ -0,0 +1,369 @@
use rstest::*;
use rstest_reuse::{self, *};
use speculoos::prelude::*;
use std::{
cell::RefCell,
fs::File,
io::{self, ErrorKind, Read, Seek},
path::{Path, PathBuf},
rc::Rc,
time::Duration,
};
use rhubarb_audio::{open_audio_file, open_audio_file_with_reader, AudioError, Sample};
/// A sine wave
fn sine(t: f64, f: f64) -> Sample {
f64::sin(t * f * 2.0 * std::f64::consts::PI) as f32
}
/// A triangle wave
fn triangle(t: f64, f: f64) -> Sample {
// See https://en.wikipedia.org/wiki/Triangle_wave#Definition
let t2 = t + 0.25 / f;
(2.0 * f64::abs(2.0 * (t2 * f - f64::floor(t2 * f + 0.5))) - 1.0) as f32
}
/// 50:50 mix of 1-kHz sine and triangle wave
fn sine_triangle_1_khz(t: f64) -> Sample {
let f = 1000.0;
(sine(t, f) + triangle(t, f)) / 2.0
}
fn get_resource_file_path(file_name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/res")
.join(file_name)
}
mod open_audio_file {
use super::*;
#[rustfmt::skip]
#[template]
#[rstest]
#[case::wav_u8_audition ("sine-triangle-u8-audition.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-7))]
#[case::wav_u8_ffmpeg ("sine-triangle-u8-ffmpeg.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-7))]
#[case::wav_u8_soundforge ("sine-triangle-u8-soundforge.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-7))]
#[case::wav_i16_audacity ("sine-triangle-i16-audacity.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-15))]
#[case::wav_i16_audition ("sine-triangle-i16-audition.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-15))]
#[case::wav_i16_ffmpeg ("sine-triangle-i16-ffmpeg.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-15))]
#[case::wav_i16_soundforge ("sine-triangle-i16-soundforge.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-15))]
#[case::wav_i24_audacity ("sine-triangle-i24-audacity.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_i24_audition ("sine-triangle-i24-audition.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_i24_ffmpeg ("sine-triangle-i24-ffmpeg.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_i24_soundforge ("sine-triangle-i24-soundforge.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_i32_ffmpeg ("sine-triangle-i32-ffmpeg.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_i32_soundforge ("sine-triangle-i32-soundforge.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_f32_audacity ("sine-triangle-f32-audacity.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_f32_audition ("sine-triangle-f32-audition.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_f32_ffmpeg ("sine-triangle-f32-ffmpeg.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_f32_soundforge ("sine-triangle-f32-soundforge.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::wav_f64_ffmpeg ("sine-triangle-f64-ffmpeg.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
fn supported_audio_files(
#[case] file_name: &str,
#[case] sampling_rate: u32,
#[case] signal_fn: fn(f64) -> Sample,
#[case] tolerance: f32,
) {}
#[rstest]
#[case::wav(
"sine-triangle-i16-audacity.wav",
"WaveAudioClip { wave_file_info: WaveFileInfo { sample_format: I16, channel_count: 2, sampling_rate: 48000, frame_count: 480000, bytes_per_frame: 4, data_offset: 44 } }"
)]
fn supports_debug(#[case] file_name: &str, #[case] expected: &str) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
assert_that!(format!("{audio_clip:?}")).is_equal_to(expected.to_owned());
}
#[rstest]
#[case::wav(
"sine-triangle-i16-audacity.wav",
"WaveFileSampleReader { wave_file_info: WaveFileInfo { sample_format: I16, channel_count: 2, sampling_rate: 48000, frame_count: 480000, bytes_per_frame: 4, data_offset: 44 }, logical_position: 0, physical_position: None }"
)]
fn sample_reader_supports_debug(#[case] file_name: &str, #[case] expected: &str) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
let sample_reader = audio_clip.create_sample_reader().unwrap();
assert_that!(format!("{sample_reader:?}")).is_equal_to(expected.to_owned());
}
#[apply(supported_audio_files)]
fn provides_metadata(
#[case] file_name: &str,
#[case] sampling_rate: u32,
#[case] _signal_fn: fn(f64) -> Sample,
#[case] _tolerance: f32,
) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
assert_that!(audio_clip.len()).is_equal_to(10 * sampling_rate as u64);
assert_that!(audio_clip.sampling_rate()).is_equal_to(sampling_rate);
assert_that!(audio_clip.duration()).is_equal_to(Duration::from_secs(10));
}
#[apply(supported_audio_files)]
fn reads_samples(
#[case] file_name: &str,
#[case] sampling_rate: u32,
#[case] signal_fn: fn(f64) -> Sample,
#[case] tolerance: f32,
) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
let mut sample_reader = audio_clip.create_sample_reader().unwrap();
let mut buffer = [0.0f32; 48 * 2];
sample_reader.read(&mut buffer).unwrap();
for (i, sample) in buffer.iter().enumerate() {
let expected = signal_fn(i as f64 / sampling_rate as f64);
assert_that!(*sample)
.named(&i.to_string())
.is_close_to(expected, tolerance);
}
}
#[apply(supported_audio_files)]
fn reads_samples_in_one_large_chunk(
#[case] file_name: &str,
#[case] sampling_rate: u32,
#[case] signal_fn: fn(f64) -> Sample,
#[case] tolerance: f32,
) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
let mut sample_reader = audio_clip.create_sample_reader().unwrap();
let mut buffer = vec![0.0f32; sample_reader.len() as usize];
sample_reader.read(&mut buffer).unwrap();
for (i, sample) in buffer.iter().enumerate() {
let expected = signal_fn(i as f64 / sampling_rate as f64);
assert_that!(*sample)
.named(&i.to_string())
.is_close_to(expected, tolerance);
}
}
#[apply(supported_audio_files)]
fn seeks_up_to_the_end(
#[case] file_name: &str,
#[case] sampling_rate: u32,
#[case] signal_fn: fn(f64) -> Sample,
#[case] tolerance: f32,
) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
let mut sample_reader = audio_clip.create_sample_reader().unwrap();
let mut buffer = [0.0f32; 48 * 2];
for offset in [
9 * sampling_rate as u64 - 5,
2 * sampling_rate as u64 + 3,
audio_clip.len() - buffer.len() as u64,
] {
sample_reader.set_position(offset);
sample_reader.read(&mut buffer).unwrap();
for (i, sample) in buffer.iter().enumerate() {
let expected = signal_fn((i as u64 + offset) as f64 / sampling_rate as f64);
assert_that!(*sample)
.named(&i.to_string())
.is_close_to(expected, tolerance);
}
}
sample_reader.set_position(audio_clip.len());
}
#[should_panic(expected = "Attempting to seek to position 480001 of 480000-frame audio clip.")]
#[apply(supported_audio_files)]
fn seeking_beyond_the_end(
#[case] file_name: &str,
#[case] _sampling_rate: u32,
#[case] _signal_fn: fn(f64) -> Sample,
#[case] _tolerance: f32,
) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
let mut sample_reader = audio_clip.create_sample_reader().unwrap();
sample_reader.set_position(audio_clip.len() + 1);
}
#[should_panic(
expected = "Attempting to read up to position 480001 of 480000-frame audio clip."
)]
#[apply(supported_audio_files)]
fn reading_beyond_the_end(
#[case] file_name: &str,
#[case] _sampling_rate: u32,
#[case] _signal_fn: fn(f64) -> Sample,
#[case] _tolerance: f32,
) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
let mut sample_reader = audio_clip.create_sample_reader().unwrap();
let mut buffer = [0.0f32; 48 * 2];
sample_reader.set_position(audio_clip.len() - buffer.len() as u64 + 1);
sample_reader.read(&mut buffer).unwrap();
}
#[rstest]
#[case::ogg("sine-triangle.ogg")]
#[case::mp3("sine-triangle.mp3")]
#[case::flac("sine-triangle.flac")]
fn fails_when_opening_file_of_unsupported_type(#[case] file_name: &str) {
let path = get_resource_file_path(file_name);
let result = open_audio_file(path);
assert_that!(result).is_err_containing(AudioError::UnsupportedFileType);
}
#[rstest]
#[case::wav_codec_flac(
"sine-triangle-flac-ffmpeg.wav",
"Unsupported audio codec: Free Lossless Audio Codec FLAC."
)]
#[case::wav_codec_vorbis("sine-triangle-vorbis-ffmpeg.wav", "Unsupported audio codec: 0x566F.")]
fn fails_when_opening_file_using_unsupported_feature(
#[case] file_name: &str,
#[case] expected_message: &str,
) {
let path = get_resource_file_path(file_name);
let result = open_audio_file(path);
assert_that!(result).is_err_containing(AudioError::UnsupportedFileFeature(
expected_message.to_owned(),
));
}
#[rstest]
#[case::wav("no-such-file.wav")]
fn fails_if_file_does_not_exist(#[case] file_name: &str) {
let path = get_resource_file_path(file_name);
let result = open_audio_file(path);
assert_that!(result)
.is_err_containing(AudioError::IoError(io::Error::from(ErrorKind::NotFound)));
}
#[rstest]
#[case::wav_file_type_txt(
"corrupt_file_type_txt.wav",
"Expected master chunk ID \"RIFF\", got \"Lore\"."
)]
#[case::wav_file_type_pal(
"corrupt_file_type_pal.wav",
"Expected WAVE chunk ID \"WAVE\", got \"PAL \"."
)]
#[case::wav_truncated_header("corrupt_truncated_header.wav", "Unexpected end of file.")]
#[case::wav_truncated_data("corrupt_truncated_data.wav", "Unexpected end of file.")]
fn fails_if_file_is_corrupt(#[case] file_name: &str, #[case] expected_message: &str) {
let path = get_resource_file_path(file_name);
let result = open_audio_file(path)
.and_then(|audio_clip| audio_clip.create_sample_reader())
.and_then(|mut sample_reader| {
let mut buffer = vec![0.0f32; sample_reader.len() as usize];
sample_reader.read(&mut buffer)
});
assert_that!(result)
.is_err_containing(AudioError::CorruptFile(expected_message.to_owned()));
}
#[rstest]
#[case::wav_ascii("filename-ascii !#$%&'()+,-.;=@[]^_`{}~.wav")]
#[case::wav_ansi("filename-ansi-€…‡‰‘’“”•™©±²½æ.wav")]
#[case::wav_unicode_bmp("filename-unicode-bmp-①∀⇨.wav")]
#[case::wav_unicode_wide("filename-unicode-wide-😀🤣🙈🍨.wav")]
fn supports_special_characters_in_file_names(#[case] file_name: &str) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
let mut sample_reader = audio_clip.create_sample_reader().unwrap();
let mut buffer = [0.0f32; 48 * 2];
sample_reader.read(&mut buffer).unwrap();
}
#[rstest]
#[case::wav("zero-samples.wav")]
fn supports_zero_sample_files(#[case] file_name: &str) {
let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap();
let mut sample_reader = audio_clip.create_sample_reader().unwrap();
let mut buffer = [0.0f32; 0];
sample_reader.read(&mut buffer).unwrap();
sample_reader.set_position(0);
}
}
struct MockFile {
pub file: File,
pub next_error_kind: Rc<RefCell<Option<io::ErrorKind>>>,
}
impl MockFile {
fn from_resource(
file_name: &str,
next_error_kind: Rc<RefCell<Option<io::ErrorKind>>>,
) -> MockFile {
let path = get_resource_file_path(file_name);
MockFile {
file: File::open(&path).unwrap(),
next_error_kind,
}
}
}
impl Read for MockFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match &*(*self.next_error_kind).borrow() {
None => self.file.read(buf),
Some(error_kind) => Err(io::Error::from(*error_kind)),
}
}
}
impl Seek for MockFile {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
match &*(*self.next_error_kind).borrow() {
None => self.file.seek(pos),
Some(error_kind) => Err(io::Error::from(*error_kind)),
}
}
}
mod open_audio_file_with_reader {
use super::*;
#[rstest]
#[case::wav_not_found("sine-triangle-i16-audacity.wav", io::ErrorKind::NotFound)]
#[case::wav_permission_denied(
"sine-triangle-i16-audacity.wav",
io::ErrorKind::PermissionDenied
)]
fn fails_on_io_errors(#[case] file_name: &'static str, #[case] error_kind: io::ErrorKind) {
let next_error_kind = Rc::new(RefCell::new(None));
let audio_clip = {
let next_error_kind = next_error_kind.clone();
open_audio_file_with_reader(
file_name,
Box::new(move || Ok(MockFile::from_resource(file_name, next_error_kind.clone()))),
)
.unwrap()
};
next_error_kind.replace(Some(error_kind));
let mut buffer = [0.0f32; 48 * 2];
let result = audio_clip
.create_sample_reader()
.and_then(|mut sample_reader| sample_reader.read(&mut buffer));
assert_that!(result).is_err_containing(AudioError::IoError(io::Error::from(error_kind)));
}
}

BIN
rhubarb/rhubarb-audio/tests/res/corrupt_file_type_pal.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/corrupt_file_type_txt.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/corrupt_truncated_data.wav (Stored with Git LFS) Normal file

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/corrupt_truncated_header.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-ffmpeg.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/sine-triangle-f64-ffmpeg.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-ffmpeg.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-ffmpeg.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/sine-triangle-i32-ffmpeg.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-ffmpeg.wav (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/sine-triangle.flac (Stored with Git LFS) Normal file

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/sine-triangle.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/sine-triangle.ogg (Stored with Git LFS) Normal file

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/zero-samples.wav (Stored with Git LFS) Normal file

Binary file not shown.