From 9c3b1fb5540cd3b107720fda6b3158a774f1a1e8 Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Wed, 16 Nov 2022 21:22:25 +0100 Subject: [PATCH] Add support for reading WAVE files --- rhubarb/Cargo.lock | 110 ++++++ rhubarb/rhubarb-audio/Cargo.toml | 5 + rhubarb/rhubarb-audio/src/audio_clip.rs | 3 +- rhubarb/rhubarb-audio/src/lib.rs | 7 + rhubarb/rhubarb-audio/src/open_audio_file.rs | 35 ++ rhubarb/rhubarb-audio/src/read_and_seek.rs | 6 + .../src/sample_reader_assertions.rs | 27 ++ .../src/wave_audio_clip/codecs.rs | 285 ++++++++++++++ .../src/wave_audio_clip/four_cc.rs | 66 ++++ .../rhubarb-audio/src/wave_audio_clip/mod.rs | 4 + .../src/wave_audio_clip/wave_audio_clip.rs | 196 ++++++++++ .../src/wave_audio_clip/wave_file_info.rs | 164 ++++++++ .../tests/open_audio_file_test.rs | 369 ++++++++++++++++++ .../tests/res/corrupt_file_type_pal.wav | 3 + .../tests/res/corrupt_file_type_txt.wav | 3 + .../tests/res/corrupt_truncated_data.wav | 3 + .../tests/res/corrupt_truncated_header.wav | 3 + ...¬β€¦β€‘β€°β€˜β€™β€œβ€β€’β„’Β©Β±Β²Β½Γ¦.wav | 3 + ...filename-ascii !#$%&'()+,-.;=@[]^_`{}~.wav | 3 + .../res/filename-unicode-bmp-β‘ βˆ€β‡¨.wav | 3 + ...filename-unicode-wide-πŸ˜€πŸ€£πŸ™ˆπŸ¨.wav | 3 + .../tests/res/sine-triangle-f32-audacity.wav | 3 + .../tests/res/sine-triangle-f32-audition.wav | 3 + .../tests/res/sine-triangle-f32-ffmpeg.wav | 3 + .../res/sine-triangle-f32-soundforge.wav | 3 + .../tests/res/sine-triangle-f64-ffmpeg.wav | 3 + .../tests/res/sine-triangle-flac-ffmpeg.wav | 3 + .../tests/res/sine-triangle-i16-audacity.wav | 3 + .../tests/res/sine-triangle-i16-audition.wav | 3 + .../tests/res/sine-triangle-i16-ffmpeg.wav | 3 + .../res/sine-triangle-i16-soundforge.wav | 3 + .../tests/res/sine-triangle-i24-audacity.wav | 3 + .../tests/res/sine-triangle-i24-audition.wav | 3 + .../tests/res/sine-triangle-i24-ffmpeg.wav | 3 + .../res/sine-triangle-i24-soundforge.wav | 3 + .../tests/res/sine-triangle-i32-ffmpeg.wav | 3 + .../res/sine-triangle-i32-soundforge.wav | 3 + .../tests/res/sine-triangle-u8-audition.wav | 3 + .../tests/res/sine-triangle-u8-ffmpeg.wav | 3 + .../tests/res/sine-triangle-u8-soundforge.wav | 3 + .../tests/res/sine-triangle-vorbis-ffmpeg.wav | 3 + .../tests/res/sine-triangle.flac | 3 + .../rhubarb-audio/tests/res/sine-triangle.mp3 | 3 + .../rhubarb-audio/tests/res/sine-triangle.ogg | 3 + .../rhubarb-audio/tests/res/zero-samples.wav | 3 + 45 files changed, 1371 insertions(+), 2 deletions(-) create mode 100644 rhubarb/rhubarb-audio/src/open_audio_file.rs create mode 100644 rhubarb/rhubarb-audio/src/read_and_seek.rs create mode 100644 rhubarb/rhubarb-audio/src/sample_reader_assertions.rs create mode 100644 rhubarb/rhubarb-audio/src/wave_audio_clip/codecs.rs create mode 100644 rhubarb/rhubarb-audio/src/wave_audio_clip/four_cc.rs create mode 100644 rhubarb/rhubarb-audio/src/wave_audio_clip/mod.rs create mode 100644 rhubarb/rhubarb-audio/src/wave_audio_clip/wave_audio_clip.rs create mode 100644 rhubarb/rhubarb-audio/src/wave_audio_clip/wave_file_info.rs create mode 100644 rhubarb/rhubarb-audio/tests/open_audio_file_test.rs create mode 100644 rhubarb/rhubarb-audio/tests/res/corrupt_file_type_pal.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/corrupt_file_type_txt.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/corrupt_truncated_data.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/corrupt_truncated_header.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/filename-ansi-β‚¬β€¦β€‘β€°β€˜β€™β€œβ€β€’β„’Β©Β±Β²Β½Γ¦.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/filename-ascii !#$%&'()+,-.;=@[]^_`{}~.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/filename-unicode-bmp-β‘ βˆ€β‡¨.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/filename-unicode-wide-πŸ˜€πŸ€£πŸ™ˆπŸ¨.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-audacity.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-audition.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-ffmpeg.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-soundforge.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-f64-ffmpeg.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-flac-ffmpeg.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-audacity.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-audition.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-ffmpeg.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-soundforge.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-audacity.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-audition.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-ffmpeg.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-soundforge.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i32-ffmpeg.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-i32-soundforge.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-audition.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-ffmpeg.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-soundforge.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle-vorbis-ffmpeg.wav create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle.flac create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle.mp3 create mode 100644 rhubarb/rhubarb-audio/tests/res/sine-triangle.ogg create mode 100644 rhubarb/rhubarb-audio/tests/res/zero-samples.wav diff --git a/rhubarb/Cargo.lock b/rhubarb/Cargo.lock index 71c0367..14bf39d 100644 --- a/rhubarb/Cargo.lock +++ b/rhubarb/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. 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]] name = "assert_matches" version = "1.5.0" @@ -14,12 +23,41 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "dyn-clone" version = "1.0.10" @@ -121,6 +159,12 @@ dependencies = [ "slab", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "log" version = "0.4.17" @@ -212,6 +256,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -242,14 +292,36 @@ dependencies = [ "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]] name = "rhubarb-audio" version = "0.1.0" dependencies = [ "assert_matches", + "byteorder", + "demonstrate", + "derivative", "dyn-clone", "log", + "once_cell", "rstest", + "rstest_reuse", "speculoos", ] @@ -278,6 +350,17 @@ dependencies = [ "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]] name = "rustc_version" version = "0.4.0" @@ -311,6 +394,16 @@ dependencies = [ "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]] name = "syn" version = "1.0.107" @@ -327,3 +420,20 @@ name = "unicode-ident" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" 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", +] diff --git a/rhubarb/rhubarb-audio/Cargo.toml b/rhubarb/rhubarb-audio/Cargo.toml index ec5cd42..33e3871 100644 --- a/rhubarb/rhubarb-audio/Cargo.toml +++ b/rhubarb/rhubarb-audio/Cargo.toml @@ -4,10 +4,15 @@ version = "0.1.0" edition = "2021" [dependencies] +byteorder = "1.4.3" +derivative = "2.2.0" dyn-clone = "1.0.10" log = "0.4.17" +once_cell = "1.17.0" [dev-dependencies] assert_matches = "1.5.0" +demonstrate = "0.4.5" rstest = "0.15.0" +rstest_reuse = "0.4.0" speculoos = "0.10.0" diff --git a/rhubarb/rhubarb-audio/src/audio_clip.rs b/rhubarb/rhubarb-audio/src/audio_clip.rs index e73170d..2527549 100644 --- a/rhubarb/rhubarb-audio/src/audio_clip.rs +++ b/rhubarb/rhubarb-audio/src/audio_clip.rs @@ -1,7 +1,6 @@ use crate::audio_error::AudioError; use dyn_clone::DynClone; -use std::fmt::Debug; -use std::time::Duration; +use std::{fmt::Debug, time::Duration}; const NANOS_PER_SEC: u32 = 1_000_000_000; diff --git a/rhubarb/rhubarb-audio/src/lib.rs b/rhubarb/rhubarb-audio/src/lib.rs index 84009f3..f8b5a0f 100644 --- a/rhubarb/rhubarb-audio/src/lib.rs +++ b/rhubarb/rhubarb-audio/src/lib.rs @@ -14,6 +14,13 @@ mod audio_clip; 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_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; diff --git a/rhubarb/rhubarb-audio/src/open_audio_file.rs b/rhubarb/rhubarb-audio/src/open_audio_file.rs new file mode 100644 index 0000000..e92559d --- /dev/null +++ b/rhubarb/rhubarb-audio/src/open_audio_file.rs @@ -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) -> Result, 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( + path: impl Into, + create_reader: Box Result>, +) -> Result, 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), + } +} diff --git a/rhubarb/rhubarb-audio/src/read_and_seek.rs b/rhubarb/rhubarb-audio/src/read_and_seek.rs new file mode 100644 index 0000000..69c0be1 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/read_and_seek.rs @@ -0,0 +1,6 @@ +use std::io::{Read, Seek}; + +/// A seekable reader. +pub trait ReadAndSeek: Read + Seek {} + +impl ReadAndSeek for T {} diff --git a/rhubarb/rhubarb-audio/src/sample_reader_assertions.rs b/rhubarb/rhubarb-audio/src/sample_reader_assertions.rs new file mode 100644 index 0000000..b4c2b25 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/sample_reader_assertions.rs @@ -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 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() + ); + } +} diff --git a/rhubarb/rhubarb-audio/src/wave_audio_clip/codecs.rs b/rhubarb/rhubarb-audio/src/wave_audio_clip/codecs.rs new file mode 100644 index 0000000..e8dbd35 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/wave_audio_clip/codecs.rs @@ -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> = 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()); + } + } +} diff --git a/rhubarb/rhubarb-audio/src/wave_audio_clip/four_cc.rs b/rhubarb/rhubarb-audio/src/wave_audio_clip/four_cc.rs new file mode 100644 index 0000000..3e32a5c --- /dev/null +++ b/rhubarb/rhubarb-audio/src/wave_audio_clip/four_cc.rs @@ -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::() +} + +pub trait ReadFourCcExt { + fn read_four_cc(&mut self) -> Result; +} + +impl ReadFourCcExt for TReader { + fn read_four_cc(&mut self) -> Result { + 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); + } + } +} diff --git a/rhubarb/rhubarb-audio/src/wave_audio_clip/mod.rs b/rhubarb/rhubarb-audio/src/wave_audio_clip/mod.rs new file mode 100644 index 0000000..8c64b86 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/wave_audio_clip/mod.rs @@ -0,0 +1,4 @@ +mod codecs; +mod four_cc; +pub mod wave_audio_clip; +mod wave_file_info; diff --git a/rhubarb/rhubarb-audio/src/wave_audio_clip/wave_audio_clip.rs b/rhubarb/rhubarb-audio/src/wave_audio_clip/wave_audio_clip.rs new file mode 100644 index 0000000..7e9352a --- /dev/null +++ b/rhubarb/rhubarb-audio/src/wave_audio_clip/wave_audio_clip.rs @@ -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 { + wave_file_info: WaveFileInfo, + #[derivative(Debug = "ignore")] + create_reader: Arc Result>, +} + +impl WaveAudioClip +where + TReader: ReadAndSeek, +{ + /// Creates a new `WaveAudioClip` for the reader returned by the given callback function. + pub fn new( + create_reader: Box Result>, + ) -> Result { + Ok(Self { + wave_file_info: get_wave_file_info(&mut create_reader()?)?, + create_reader: Arc::from(create_reader), + }) + } +} + +impl Clone for WaveAudioClip { + fn clone(&self) -> Self { + Self { + wave_file_info: self.wave_file_info, + create_reader: self.create_reader.clone(), + } + } +} + +impl AudioClip for WaveAudioClip +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, 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::()?) * 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::()? as f32 * FACTOR) + })?; + Ok(Box::new(sample_reader)) + } + SampleFormat::F32 => { + let sample_reader = + WaveFileSampleReader::new(self, |reader| Ok(reader.read_f32::()?))?; + Ok(Box::new(sample_reader)) + } + SampleFormat::F64 => { + let sample_reader = + WaveFileSampleReader::new(self, |reader| Ok(reader.read_f64::()? as f32))?; + Ok(Box::new(sample_reader)) + } + } + } +} + +#[derive(Derivative)] +#[derivative(Debug)] +struct WaveFileSampleReader +where + TReadSample: Fn(&mut TReader) -> Result, +{ + 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, + #[derivative(Debug = "ignore")] + read_sample: TReadSample, +} + +impl WaveFileSampleReader +where + TReader: ReadAndSeek, + TReadSample: Fn(&mut TReader) -> Result, +{ + fn new( + audio_clip: &WaveAudioClip, + read_sample: TReadSample, + ) -> Result { + 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 SampleReader for WaveFileSampleReader +where + TReader: ReadAndSeek, + TReadSample: Fn(&mut TReader) -> Result, +{ + 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(()) + } +} diff --git a/rhubarb/rhubarb-audio/src/wave_audio_clip/wave_file_info.rs b/rhubarb/rhubarb-audio/src/wave_audio_clip/wave_file_info.rs new file mode 100644 index 0000000..b7eb80b --- /dev/null +++ b/rhubarb/rhubarb-audio/src/wave_audio_clip/wave_file_info.rs @@ -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 { + 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::()?; // 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 = None; + let mut data_info: Option = None; + + while format_info.is_none() || data_info.is_none() { + let chunk_id = reader.read_four_cc()?; + let chunk_size = reader.read_u32::()?; + 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::()?; + let channel_count = reader.read_u16::()?; + let frame_rate = reader.read_u32::()?; + reader.read_u32::()?; // Skip bytes per second + let bytes_per_frame = reader.read_u16::()?; + let mut bits_per_sample = reader.read_u16::()?; + if chunk_size > 16 { + let extension_size = reader.read_u16::()?; + if extension_size >= 22 { + // Read extension fields + bits_per_sample = reader.read_u16::()?; + reader.read_u32::()?; // Skip channel mask + let codec_override = reader.read_u16::()?; + 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 } +} diff --git a/rhubarb/rhubarb-audio/tests/open_audio_file_test.rs b/rhubarb/rhubarb-audio/tests/open_audio_file_test.rs new file mode 100644 index 0000000..123cbb0 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/open_audio_file_test.rs @@ -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>>, +} + +impl MockFile { + fn from_resource( + file_name: &str, + next_error_kind: Rc>>, + ) -> 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 { + 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 { + 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))); + } +} diff --git a/rhubarb/rhubarb-audio/tests/res/corrupt_file_type_pal.wav b/rhubarb/rhubarb-audio/tests/res/corrupt_file_type_pal.wav new file mode 100644 index 0000000..1ef2743 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/corrupt_file_type_pal.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3abfa1df98db3264f8e85d306a37a6b4cf6058d447bf12be291ca191c5845685 +size 1048 diff --git a/rhubarb/rhubarb-audio/tests/res/corrupt_file_type_txt.wav b/rhubarb/rhubarb-audio/tests/res/corrupt_file_type_txt.wav new file mode 100644 index 0000000..2f4c623 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/corrupt_file_type_txt.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9a66978f378456c818fb8a3e7c6ad3d2c83e62724ccbdea7b36253fb8df5edd +size 11 diff --git a/rhubarb/rhubarb-audio/tests/res/corrupt_truncated_data.wav b/rhubarb/rhubarb-audio/tests/res/corrupt_truncated_data.wav new file mode 100644 index 0000000..17d8e37 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/corrupt_truncated_data.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d742c211f2f780d7359d38b210fdb80c435b30db57996f2ab833e54eb1a85964 +size 512 diff --git a/rhubarb/rhubarb-audio/tests/res/corrupt_truncated_header.wav b/rhubarb/rhubarb-audio/tests/res/corrupt_truncated_header.wav new file mode 100644 index 0000000..e7db576 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/corrupt_truncated_header.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a85d64e1c8d49bedad7310ad18e7fdab200e14f4eb1053c38dbfb65fe6718d8b +size 57 diff --git a/rhubarb/rhubarb-audio/tests/res/filename-ansi-β‚¬β€¦β€‘β€°β€˜β€™β€œβ€β€’β„’Β©Β±Β²Β½Γ¦.wav b/rhubarb/rhubarb-audio/tests/res/filename-ansi-β‚¬β€¦β€‘β€°β€˜β€™β€œβ€β€’β„’Β©Β±Β²Β½Γ¦.wav new file mode 100644 index 0000000..e3e4b68 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/filename-ansi-β‚¬β€¦β€‘β€°β€˜β€™β€œβ€β€’β„’Β©Β±Β²Β½Γ¦.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63e38d3e7b9e03c08a8f46a25b8d7325d0565130a5b74296cbe64e49eea65696 +size 1920078 diff --git a/rhubarb/rhubarb-audio/tests/res/filename-ascii !#$%&'()+,-.;=@[]^_`{}~.wav b/rhubarb/rhubarb-audio/tests/res/filename-ascii !#$%&'()+,-.;=@[]^_`{}~.wav new file mode 100644 index 0000000..e3e4b68 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/filename-ascii !#$%&'()+,-.;=@[]^_`{}~.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63e38d3e7b9e03c08a8f46a25b8d7325d0565130a5b74296cbe64e49eea65696 +size 1920078 diff --git a/rhubarb/rhubarb-audio/tests/res/filename-unicode-bmp-β‘ βˆ€β‡¨.wav b/rhubarb/rhubarb-audio/tests/res/filename-unicode-bmp-β‘ βˆ€β‡¨.wav new file mode 100644 index 0000000..e3e4b68 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/filename-unicode-bmp-β‘ βˆ€β‡¨.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63e38d3e7b9e03c08a8f46a25b8d7325d0565130a5b74296cbe64e49eea65696 +size 1920078 diff --git a/rhubarb/rhubarb-audio/tests/res/filename-unicode-wide-πŸ˜€πŸ€£πŸ™ˆπŸ¨.wav b/rhubarb/rhubarb-audio/tests/res/filename-unicode-wide-πŸ˜€πŸ€£πŸ™ˆπŸ¨.wav new file mode 100644 index 0000000..e3e4b68 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/filename-unicode-wide-πŸ˜€πŸ€£πŸ™ˆπŸ¨.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63e38d3e7b9e03c08a8f46a25b8d7325d0565130a5b74296cbe64e49eea65696 +size 1920078 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-audacity.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-audacity.wav new file mode 100644 index 0000000..9c3ac39 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-audacity.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61a5d297769ac42ffc8e54d0f3923ee0064ce8ecba14ccc874b31c25e5c8c9e1 +size 3840194 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-audition.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-audition.wav new file mode 100644 index 0000000..d11a86c --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-audition.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5645586937fa012dfc051e2c38126c6221a1937dd2fbceebfddb971be53a44d +size 3845534 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-ffmpeg.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-ffmpeg.wav new file mode 100644 index 0000000..7e0c1a6 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-ffmpeg.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4092089069670b6b1259f5cd34f91afa36066678f80464186118ddcff695923 +size 3840114 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-soundforge.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-soundforge.wav new file mode 100644 index 0000000..3410566 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f32-soundforge.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e91311e5d7a266cb15f057e60f59295b535f0bf3313c4d9cde54037cc6fa3f +size 3840078 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-f64-ffmpeg.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f64-ffmpeg.wav new file mode 100644 index 0000000..4b84ce5 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-f64-ffmpeg.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdf972bf29dc137ff022761260169dbd6e15c668f7cde69f1925de393fa88544 +size 7680114 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-flac-ffmpeg.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-flac-ffmpeg.wav new file mode 100644 index 0000000..02d91ff --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-flac-ffmpeg.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:491fee574896f2da599659b08498ea9b605bb4679dc38ce588490222831a0c31 +size 1937792 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-audacity.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-audacity.wav new file mode 100644 index 0000000..0d44a08 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-audacity.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd8d91cf1d1593a103b5b43ea08ab46ca1d66e55e10bbe97c097f7e605155904 +size 1920150 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-audition.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-audition.wav new file mode 100644 index 0000000..7f9fc8a --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-audition.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:885f25245b4227c1761828a6365cb60fa36d81d7e636773ab3702621640bf651 +size 1925534 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-ffmpeg.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-ffmpeg.wav new file mode 100644 index 0000000..e3e4b68 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-ffmpeg.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63e38d3e7b9e03c08a8f46a25b8d7325d0565130a5b74296cbe64e49eea65696 +size 1920078 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-soundforge.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-soundforge.wav new file mode 100644 index 0000000..4825a4a --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i16-soundforge.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13741b9dfbf358485cbbc0076713ee9473be970f8cc4686ca75cf286845b2d20 +size 1920078 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-audacity.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-audacity.wav new file mode 100644 index 0000000..9b8bbb5 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-audacity.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b06e78f2ba11722d516b8367b7c847452d55b2936ab2f28b82fa6fd5c97f383 +size 2880150 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-audition.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-audition.wav new file mode 100644 index 0000000..8359a18 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-audition.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f668649a95293cd031e2d169e444fedfb07aabc6ded2548ce310262aa66c1082 +size 2885534 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-ffmpeg.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-ffmpeg.wav new file mode 100644 index 0000000..2ab556e --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-ffmpeg.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93248779904023c1e22bd4970f507980a6496e8327dd8a49ce81616375c96f09 +size 2880102 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-soundforge.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-soundforge.wav new file mode 100644 index 0000000..af729a0 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i24-soundforge.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:706c94020f6fe8368383ef28720e10c701e239a9665213eee831e28d59a7a753 +size 2880078 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i32-ffmpeg.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i32-ffmpeg.wav new file mode 100644 index 0000000..cc051f7 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i32-ffmpeg.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:374e6a01afc020355abfffb4b8bfbfefee9ace878081dc265134ae79ccfaf820 +size 3840102 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-i32-soundforge.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i32-soundforge.wav new file mode 100644 index 0000000..d885aec --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-i32-soundforge.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f88ac09446b567afa0783a275ee2d3f15e3a05749c08b7bdc75b6f4bbfa1acb +size 3840078 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-audition.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-audition.wav new file mode 100644 index 0000000..168a106 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-audition.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e8fbc6809850523bc274863f07d13b258647b496977ffbf62f2d9ef20f1f40d +size 965534 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-ffmpeg.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-ffmpeg.wav new file mode 100644 index 0000000..57fbdab --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-ffmpeg.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bff9c1914f12e3bb1f8c1de515bb4d0b38a44dd5d3e5ba7e8e5f9406fe3dfd04 +size 960078 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-soundforge.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-soundforge.wav new file mode 100644 index 0000000..e46e807 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-u8-soundforge.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c50aa57ea11921d4be417d5339a91ecfa095e755be06adbfba0d4bb52bac56c6 +size 960078 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle-vorbis-ffmpeg.wav b/rhubarb/rhubarb-audio/tests/res/sine-triangle-vorbis-ffmpeg.wav new file mode 100644 index 0000000..6b84312 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle-vorbis-ffmpeg.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3ffb9e1aaf3b06cb02f07b8aeeb9c9f400e40e265618991e027b59295f7e562 +size 72652 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle.flac b/rhubarb/rhubarb-audio/tests/res/sine-triangle.flac new file mode 100644 index 0000000..09aa6a9 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle.flac @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96925f2cbfea3a207d1713b5f12478f2264637e0920c4183f7b6fe24178e7c2a +size 1945953 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle.mp3 b/rhubarb/rhubarb-audio/tests/res/sine-triangle.mp3 new file mode 100644 index 0000000..d63d120 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3592993d519f6976da59a0b1095be156cb78fd19e14bc7d76fa117bca02b8c1c +size 160749 diff --git a/rhubarb/rhubarb-audio/tests/res/sine-triangle.ogg b/rhubarb/rhubarb-audio/tests/res/sine-triangle.ogg new file mode 100644 index 0000000..efcff5c --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/sine-triangle.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0684aecb92870be3fe92dd1fb4e1b1801e0e36d35e2701333bc8508fa75bb4a2 +size 73343 diff --git a/rhubarb/rhubarb-audio/tests/res/zero-samples.wav b/rhubarb/rhubarb-audio/tests/res/zero-samples.wav new file mode 100644 index 0000000..6d0ee74 --- /dev/null +++ b/rhubarb/rhubarb-audio/tests/res/zero-samples.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10ebb3069837f59e9ce2a37a1ceea64a4b1c8344444f065881ebf868a7fa0595 +size 78