Add support for reading Ogg Vorbis files

This commit is contained in:
Daniel Wolf 2022-11-29 11:42:07 +01:00
parent 9c3b1fb554
commit ca575d841e
19 changed files with 684 additions and 3 deletions

7
rhubarb/Cargo.lock generated
View File

@ -29,6 +29,12 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cc"
version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -315,6 +321,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"assert_matches", "assert_matches",
"byteorder", "byteorder",
"cc",
"demonstrate", "demonstrate",
"derivative", "derivative",
"dyn-clone", "dyn-clone",

View File

@ -16,3 +16,6 @@ demonstrate = "0.4.5"
rstest = "0.15.0" rstest = "0.15.0"
rstest_reuse = "0.4.0" rstest_reuse = "0.4.0"
speculoos = "0.10.0" speculoos = "0.10.0"
[build-dependencies]
cc = "1.0.77"

View File

@ -0,0 +1,132 @@
use std::{
env::var_os,
fs::remove_dir_all,
path::{Path, PathBuf},
process::Command,
};
fn exec(current_dir: impl AsRef<Path>, command: &str, args: &[&str]) {
let exit_status = Command::new(command)
.args(args)
.current_dir(current_dir)
.status()
.unwrap();
let exit_code = exit_status.code().unwrap();
assert_eq!(exit_code, 0);
}
fn checkout(parent_dir: impl AsRef<Path>, git_url: &str, refname: &str, dir_name: &str) -> PathBuf {
let repo_dir = parent_dir.as_ref().join(dir_name);
if repo_dir.exists() && !repo_dir.join(".git").join("config").exists() {
// The repository directory exists, but is not a valid Git repository.
// Delete and check out from scratch.
remove_dir_all(&repo_dir).unwrap();
}
if repo_dir.exists() {
// If the repo directory's contents got corrupted, Git may not consider it a valid
// repository, applying the following commands to the Rhubarb repository instead.
// Prevent any resulting data loss this by failing if the repo was modified.
exec(&repo_dir, "git", &["diff", "--exit-code"]);
exec(&repo_dir, "git", &["reset", "--hard"]);
exec(&repo_dir, "git", &["fetch"]);
exec(&repo_dir, "git", &["checkout", refname]);
} else {
exec(
&parent_dir,
"git",
&["clone", "--branch", refname, git_url, dir_name],
);
};
repo_dir
}
struct OggBuildResult {
ogg_include_dir: PathBuf,
}
fn build_ogg(parent_dir: impl AsRef<Path>) -> OggBuildResult {
let repo_dir = checkout(
&parent_dir,
"https://github.com/xiph/ogg.git",
"v1.3.5",
"ogg",
);
let include_dir = repo_dir.join("include");
let src_dir = repo_dir.join("src");
cc::Build::new()
.include(&include_dir)
.files(["bitwise.c", "framing.c"].map(|name| src_dir.join(name)))
.compile("ogg");
println!("cargo:rustc-link-lib=static=ogg");
OggBuildResult {
ogg_include_dir: include_dir,
}
}
struct VorbisBuildResult {
vorbis_utils_path: PathBuf,
}
fn build_vorbis(
parent_dir: impl AsRef<Path>,
ogg_include_dir: impl AsRef<Path>,
) -> VorbisBuildResult {
let repo_dir = checkout(
&parent_dir,
"https://github.com/xiph/vorbis.git",
"v1.3.7",
"vorbis",
);
let include_dir = repo_dir.join("include");
let src_dir = repo_dir.join("lib");
let vorbis_utils_path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("src/ogg_audio_clip/vorbis-utils.c");
cc::Build::new()
.include(&include_dir)
.include(&ogg_include_dir)
.files(
[
"bitrate.c",
"block.c",
"codebook.c",
"envelope.c",
"floor0.c",
"floor1.c",
"info.c",
"lpc.c",
"lsp.c",
"mapping0.c",
"mdct.c",
"psy.c",
"registry.c",
"res0.c",
"sharedbook.c",
"smallft.c",
"synthesis.c",
"vorbisfile.c",
"window.c",
]
.iter()
.map(|name| src_dir.join(name)),
)
.file(&vorbis_utils_path)
.compile("vorbis");
println!("cargo:rustc-link-lib=static=vorbis");
VorbisBuildResult { vorbis_utils_path }
}
fn main() {
let out_dir = Path::new(&var_os("OUT_DIR").unwrap()).to_path_buf();
println!("cargo:rustc-link-search=native={}", out_dir.display());
let OggBuildResult { ogg_include_dir } = build_ogg(&out_dir);
let VorbisBuildResult { vorbis_utils_path } = build_vorbis(&out_dir, ogg_include_dir);
println!("cargo:rerun-if-changed={}", vorbis_utils_path.display());
}

View File

@ -1,6 +1,7 @@
use crate::audio_error::AudioError; use crate::audio_error::AudioError;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use std::{fmt::Debug, time::Duration}; use std::fmt::Debug;
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,7 @@
mod audio_clip; mod audio_clip;
mod audio_error; mod audio_error;
mod ogg_audio_clip;
mod open_audio_file; mod open_audio_file;
mod read_and_seek; mod read_and_seek;
mod sample_reader_assertions; mod sample_reader_assertions;
@ -21,6 +22,7 @@ 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 ogg_audio_clip::ogg_audio_clip::OggAudioClip;
pub use open_audio_file::{open_audio_file, open_audio_file_with_reader}; pub use open_audio_file::{open_audio_file, open_audio_file_with_reader};
pub use read_and_seek::ReadAndSeek; pub use read_and_seek::ReadAndSeek;
pub use wave_audio_clip::wave_audio_clip::WaveAudioClip; pub use wave_audio_clip::wave_audio_clip::WaveAudioClip;

View File

@ -0,0 +1,3 @@
pub mod ogg_audio_clip;
mod vorbis_file;
mod vorbis_file_raw;

View File

@ -0,0 +1,133 @@
use std::{io, pin::Pin, sync::Arc};
use derivative::Derivative;
use crate::{
sample_reader_assertions::SampleReaderAssertions, AudioClip, AudioError, ReadAndSeek,
SampleReader,
};
use super::vorbis_file::{Metadata, VorbisFile};
/// An audio clip read on the fly from an Ogg Vorbis file.
#[derive(Derivative)]
#[derivative(Debug)]
pub struct OggAudioClip<TReader> {
metadata: Metadata,
#[derivative(Debug = "ignore")]
create_reader: Arc<dyn Fn() -> Result<TReader, io::Error>>,
}
impl<TReader> OggAudioClip<TReader>
where
TReader: ReadAndSeek + 'static,
{
/// Creates a new `OggAudioClip` for the reader returned by the given callback function.
pub fn new(
create_reader: Box<dyn Fn() -> Result<TReader, io::Error>>,
) -> Result<Self, AudioError> {
let mut vorbis_file = VorbisFile::new(create_reader()?)?;
let metadata = vorbis_file.metadata()?;
Ok(Self {
metadata,
create_reader: Arc::from(create_reader),
})
}
}
impl<TReader> Clone for OggAudioClip<TReader> {
fn clone(&self) -> Self {
Self {
metadata: self.metadata,
create_reader: self.create_reader.clone(),
}
}
}
impl<TReader> AudioClip for OggAudioClip<TReader>
where
TReader: ReadAndSeek + 'static,
{
fn len(&self) -> u64 {
self.metadata.frame_count
}
fn sampling_rate(&self) -> u32 {
self.metadata.sampling_rate
}
fn create_sample_reader(&self) -> Result<Box<dyn SampleReader>, AudioError> {
Ok(Box::new(OggFileSampleReader::new(self)?))
}
}
#[derive(Derivative)]
#[derivative(Debug)]
struct OggFileSampleReader {
#[derivative(Debug = "ignore")]
vorbis_file: Pin<Box<VorbisFile>>,
metadata: Metadata,
// the position we're claiming to be at
logical_position: u64,
// the position we're actually at
physical_position: u64,
}
impl OggFileSampleReader {
fn new<TReader>(audio_clip: &OggAudioClip<TReader>) -> Result<Self, AudioError>
where
TReader: ReadAndSeek + 'static,
{
let vorbis_file = VorbisFile::new((audio_clip.create_reader)()?)?;
Ok(Self {
vorbis_file,
metadata: audio_clip.metadata,
logical_position: 0,
physical_position: 0,
})
}
}
impl SampleReader for OggFileSampleReader {
fn len(&self) -> u64 {
self.metadata.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::Sample]) -> Result<(), AudioError> {
self.assert_valid_read_size(buffer);
if self.physical_position != self.logical_position {
self.vorbis_file.seek(self.logical_position)?;
self.physical_position = self.logical_position;
}
let mut remaining_buffer = buffer;
let factor = 1.0f32 / self.metadata.channel_count as f32;
while !remaining_buffer.is_empty() {
let read_frame_count = self.vorbis_file.read(
remaining_buffer,
|channels, frame_count, target_buffer| {
// Downmix channels to output buffer
for frame_index in 0..frame_count as usize {
let mut sum = 0f32;
for channel in channels {
sum += channel[frame_index];
}
target_buffer[frame_index] = sum * factor;
}
},
)?;
remaining_buffer = remaining_buffer.split_at_mut(read_frame_count as usize).1;
}
Ok(())
}
}

View File

@ -0,0 +1,17 @@
#include <vorbis/vorbisfile.h>
#include <stdlib.h>
#include <stdio.h>
// Creates an OggVorbis_File structure on the heap so that the caller doesn't need to know its size
extern OggVorbis_File* vu_create_oggvorbisfile() {
return malloc(sizeof (OggVorbis_File));
}
extern void vu_free_oggvorbisfile(OggVorbis_File* vf) {
ov_clear(vf); // never fails
free(vf);
}
extern const int vu_seek_origin_start = SEEK_SET;
extern const int vu_seek_origin_current = SEEK_CUR;
extern const int vu_seek_origin_end = SEEK_END;

View File

@ -0,0 +1,272 @@
use std::{
fmt::Debug,
io::{self, Read, Seek, SeekFrom},
mem::MaybeUninit,
pin::Pin,
ptr::{null, null_mut},
slice,
};
use std::os::raw::{c_int, c_long, c_void};
use crate::{AudioError, ReadAndSeek, Sample};
use super::vorbis_file_raw::{
self as raw, ov_info, ov_open_callbacks, ov_pcm_seek, ov_pcm_total, ov_read_float,
vu_create_oggvorbisfile, vu_free_oggvorbisfile, vu_seek_origin_current, vu_seek_origin_end,
vu_seek_origin_start,
};
/// A safe wrapper around the vorbisfile API.
pub struct VorbisFile {
vf: *mut raw::OggVorbisFile,
reader: Box<dyn ReadAndSeek>,
last_error: Option<io::Error>,
cached_metadata: Option<Metadata>,
}
impl VorbisFile {
pub fn new(reader: impl ReadAndSeek + 'static) -> Result<Pin<Box<Self>>, AudioError> {
let vf = unsafe { vu_create_oggvorbisfile() };
assert!(!vf.is_null(), "Error creating raw OggVorbisFile.");
// Pin the struct so that the pointer we pass for the callbacks stays valid
let mut vorbis_file = Pin::new(Box::new(Self {
vf,
reader: Box::new(reader),
last_error: None,
cached_metadata: None,
}));
let callbacks = raw::Callbacks {
read_func,
seek_func: Some(seek_func),
close_func: None, // The reader will be closed by Rust
tell_func: Some(tell_func),
};
unsafe {
let result_code = ov_open_callbacks(
&*vorbis_file as *const Self as *mut c_void,
vf,
null(),
0,
callbacks,
);
vorbis_file.handle_error(result_code)?;
}
Ok(vorbis_file)
}
pub fn metadata(&mut self) -> Result<Metadata, AudioError> {
if self.cached_metadata.is_some() {
return Ok(self.cached_metadata.unwrap());
}
unsafe {
let vorbis_info = ov_info(self.vf, -1);
assert!(!vorbis_info.is_null(), "Error retrieving Vorbis info.");
let metadata = Metadata {
frame_count: self.handle_error(ov_pcm_total(self.vf, -1))? as u64,
sampling_rate: (*vorbis_info).sampling_rate as u32,
channel_count: (*vorbis_info).channel_count as u32,
};
self.cached_metadata = Some(metadata);
Ok(metadata)
}
}
pub fn seek(&mut self, position: u64) -> Result<(), AudioError> {
unsafe {
self.handle_error(ov_pcm_seek(self.vf, position as i64))
.map(|_| ())
}
}
pub fn read(
&mut self,
target_buffer: &mut [Sample],
callback: impl Fn(&Vec<&[Sample]>, u64, &mut [Sample]),
) -> Result<u64, AudioError> {
let channel_count = self.metadata()?.channel_count;
unsafe {
// Read to multi-channel buffer
let mut buffer = MaybeUninit::uninit();
let read_frame_count = self.handle_error(ov_read_float(
self.vf,
buffer.as_mut_ptr(),
target_buffer.len().clamp(0, i32::MAX as usize) as i32,
null_mut(),
))? as u64;
let multi_channel_buffer = buffer.assume_init();
// Transform to vector of slices
let mut channels = Vec::<&[Sample]>::new();
for channel_index in 0..channel_count as usize {
let channel_buffer = *multi_channel_buffer.add(channel_index);
channels.push(slice::from_raw_parts(
channel_buffer,
read_frame_count as usize,
));
}
callback(&channels, read_frame_count, target_buffer);
Ok(read_frame_count)
}
}
fn handle_error<T>(&mut self, result_code: T) -> Result<T, AudioError>
where
T: Copy + TryInto<i32> + PartialOrd<T> + Default,
<T as TryInto<i32>>::Error: Debug,
{
// Constants from vorbis's codec.h file
const OV_HOLE: c_int = -3;
const OV_EREAD: c_int = -128;
const OV_EFAULT: c_int = -129;
const OV_EIMPL: c_int = -130;
const OV_EINVAL: c_int = -131;
const OV_ENOTVORBIS: c_int = -132;
const OV_EBADHEADER: c_int = -133;
const OV_EVERSION: c_int = -134;
const OV_ENOTAUDIO: c_int = -135;
const OV_EBADPACKET: c_int = -136;
const OV_EBADLINK: c_int = -137;
const OV_ENOSEEK: c_int = -138;
if result_code >= Default::default() {
// A non-negative value is always valid
return Ok(result_code);
}
let error_code: i32 = result_code.try_into().expect("Error code out of range.");
if error_code == OV_HOLE {
// OV_HOLE, though technically an error code, is only informational
return Ok(result_code);
}
// If we captured a Rust error object, it is probably more precise than an error code
if let Some(last_error) = self.last_error.take() {
return Err(AudioError::IoError(last_error));
}
// The call failed. Handle the error.
match error_code {
OV_EREAD => Err(AudioError::IoError(io::Error::new(
io::ErrorKind::Other,
"Read error while fetching compressed data for decoding.".to_owned(),
))),
OV_EFAULT => panic!("Internal logic fault; indicates a bug or heap/stack corruption."),
OV_EIMPL => panic!("Feature not implemented."),
OV_EINVAL => panic!(
"Either an invalid argument, or incompletely initialized argument passed to a call."
),
OV_ENOTVORBIS => Err(AudioError::CorruptFile(
"The given file was not recognized as Ogg Vorbis data.".to_owned(),
)),
OV_EBADHEADER => Err(AudioError::CorruptFile(
"Ogg Vorbis stream contains a corrupted or undecipherable header.".to_owned(),
)),
OV_EVERSION => Err(AudioError::UnsupportedFileFeature(
"Unsupported bit stream format revision.".to_owned(),
)),
OV_ENOTAUDIO => Err(AudioError::UnsupportedFileFeature(
"Packet is not an audio packet.".to_owned(),
)),
OV_EBADPACKET => Err(AudioError::CorruptFile("Error in packet.".to_owned())),
OV_EBADLINK => Err(AudioError::CorruptFile(
"Link in Vorbis data stream is not decipherable due to garbage or corruption."
.to_owned(),
)),
OV_ENOSEEK => {
// This would indicate a bug, since we're implementing the stream ourselves
panic!("The given stream is not seekable.");
}
_ => panic!("An unexpected Vorbis error with code {error_code} occurred."),
}
}
}
impl Drop for VorbisFile {
fn drop(&mut self) {
unsafe {
vu_free_oggvorbisfile(self.vf);
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct Metadata {
pub frame_count: u64,
pub sampling_rate: u32,
pub channel_count: u32,
}
unsafe extern "C" fn read_func(
buffer: *mut c_void,
element_size: usize,
element_count: usize,
data_source: *mut c_void,
) -> usize {
let vorbis_file = data_source.cast::<VorbisFile>();
(*vorbis_file).last_error = None;
let requested_byte_count = element_count * element_size;
let mut remaining_buffer = slice::from_raw_parts_mut(buffer.cast::<u8>(), requested_byte_count);
while !remaining_buffer.is_empty() {
let read_result = (*vorbis_file).reader.read(remaining_buffer);
match read_result {
Ok(read_bytes) => {
if read_bytes == 0 {
break;
}
remaining_buffer = remaining_buffer.split_at_mut(read_bytes).1;
}
Err(error) => {
(*vorbis_file).last_error = Some(error);
break;
}
}
}
requested_byte_count - remaining_buffer.len()
}
unsafe extern "C" fn seek_func(data_source: *mut c_void, offset: i64, seek_origin: c_int) -> c_int {
let vorbis_file = data_source.cast::<VorbisFile>();
(*vorbis_file).last_error = None;
let seek_from = if seek_origin == vu_seek_origin_start {
SeekFrom::Start(offset as u64)
} else if seek_origin == vu_seek_origin_current {
SeekFrom::Current(offset)
} else if seek_origin == vu_seek_origin_end {
SeekFrom::End(offset)
} else {
panic!("Invalid seek origin {seek_origin}.");
};
let seek_result = (*vorbis_file).reader.seek(seek_from);
match seek_result {
Ok(_) => 0,
Err(error) => {
(*vorbis_file).last_error = Some(error);
-1
}
}
}
unsafe extern "C" fn tell_func(data_source: *mut c_void) -> c_long {
let vorbis_file = data_source.cast::<VorbisFile>();
(*vorbis_file).last_error = None;
let position_result = (*vorbis_file).reader.stream_position();
match position_result {
Ok(position) => position as c_long,
Err(error) => {
(*vorbis_file).last_error = Some(error);
-1
}
}
}

View File

@ -0,0 +1,65 @@
use std::os::raw::{c_int, c_long, c_void};
// Raw FFI to the vorbisfile API
#[link(name = "vorbis")]
extern "C" {
// vorbisfile functions
pub fn ov_open_callbacks(
data_source: *mut c_void,
vf: *mut OggVorbisFile,
initial: *const u8,
ibytes: c_long,
callbacks: Callbacks,
) -> c_int;
pub fn ov_pcm_total(vf: *mut OggVorbisFile, i: c_int) -> i64;
pub fn ov_pcm_seek(vf: *mut OggVorbisFile, pos: i64) -> c_int;
pub fn ov_info(vf: *mut OggVorbisFile, link: c_int) -> *const VorbisInfo;
pub fn ov_read_float(
vf: *mut OggVorbisFile,
pcm_channels: *mut *const *const f32,
samples: c_int,
bitstream: *mut c_int,
) -> c_long;
// Functions and constants defined by us in vorbis-utils.c
pub fn vu_create_oggvorbisfile() -> *mut OggVorbisFile;
pub fn vu_free_oggvorbisfile(vf: *mut OggVorbisFile);
pub static vu_seek_origin_start: c_int;
pub static vu_seek_origin_current: c_int;
pub static vu_seek_origin_end: c_int;
}
#[repr(C)]
pub struct OggVorbisFile {
private: [u8; 0],
}
#[repr(C)]
pub struct Callbacks {
pub read_func: unsafe extern "C" fn(
buffer: *mut c_void,
element_size: usize,
element_count: usize,
data_source: *mut c_void,
) -> usize,
pub seek_func: Option<
unsafe extern "C" fn(data_source: *mut c_void, offset: i64, seek_origin: c_int) -> c_int,
>,
pub close_func: Option<unsafe extern "C" fn(data_source: *mut c_void) -> c_int>,
pub tell_func: Option<unsafe extern "C" fn(data_source: *mut c_void) -> c_long>,
}
#[repr(C)]
pub struct VorbisInfo {
pub encoder_version: c_int,
pub channel_count: c_int,
pub sampling_rate: c_int,
pub bitrate_upper: c_long,
pub bitrate_nominal: c_long,
pub bitrate_lower: c_long,
pub bitrate_window: c_long,
codec_setup: *mut c_void,
}

View File

@ -4,7 +4,7 @@ use std::{
path::PathBuf, path::PathBuf,
}; };
use crate::{AudioClip, AudioError, ReadAndSeek, WaveAudioClip}; use crate::{AudioClip, AudioError, OggAudioClip, ReadAndSeek, WaveAudioClip};
/// Creates an audio clip from the specified file. /// Creates an audio clip from the specified file.
pub fn open_audio_file(path: impl Into<PathBuf>) -> Result<Box<dyn AudioClip>, AudioError> { pub fn open_audio_file(path: impl Into<PathBuf>) -> Result<Box<dyn AudioClip>, AudioError> {
@ -30,6 +30,7 @@ where
.map(|e| e.to_os_string().into_string().unwrap_or_default()); .map(|e| e.to_os_string().into_string().unwrap_or_default());
match lower_case_extension.as_deref() { match lower_case_extension.as_deref() {
Some("wav") => Ok(Box::new(WaveAudioClip::new(create_reader)?)), Some("wav") => Ok(Box::new(WaveAudioClip::new(create_reader)?)),
Some("ogg") => Ok(Box::new(OggAudioClip::new(create_reader)?)),
_ => Err(AudioError::UnsupportedFileType), _ => Err(AudioError::UnsupportedFileType),
} }
} }

View File

@ -60,6 +60,7 @@ mod open_audio_file {
#[case::wav_f32_ffmpeg ("sine-triangle-f32-ffmpeg.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_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))] #[case::wav_f64_ffmpeg ("sine-triangle-f64-ffmpeg.wav", 48000, sine_triangle_1_khz, 2.0f32.powi(-21))]
#[case::ogg ("sine-triangle.ogg", 48000, sine_triangle_1_khz, 2.0f32.powi(-3))] // lossy
fn supported_audio_files( fn supported_audio_files(
#[case] file_name: &str, #[case] file_name: &str,
#[case] sampling_rate: u32, #[case] sampling_rate: u32,
@ -72,6 +73,10 @@ mod open_audio_file {
"sine-triangle-i16-audacity.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 } }" "WaveAudioClip { wave_file_info: WaveFileInfo { sample_format: I16, channel_count: 2, sampling_rate: 48000, frame_count: 480000, bytes_per_frame: 4, data_offset: 44 } }"
)] )]
#[case::ogg(
"sine-triangle.ogg",
"OggAudioClip { metadata: Metadata { frame_count: 480000, sampling_rate: 48000, channel_count: 2 } }"
)]
fn supports_debug(#[case] file_name: &str, #[case] expected: &str) { fn supports_debug(#[case] file_name: &str, #[case] expected: &str) {
let path = get_resource_file_path(file_name); let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap(); let audio_clip = open_audio_file(path).unwrap();
@ -83,6 +88,10 @@ mod open_audio_file {
"sine-triangle-i16-audacity.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 }" "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 }"
)] )]
#[case::ogg(
"sine-triangle.ogg",
"OggFileSampleReader { metadata: Metadata { frame_count: 480000, sampling_rate: 48000, channel_count: 2 }, logical_position: 0, physical_position: 0 }"
)]
fn sample_reader_supports_debug(#[case] file_name: &str, #[case] expected: &str) { fn sample_reader_supports_debug(#[case] file_name: &str, #[case] expected: &str) {
let path = get_resource_file_path(file_name); let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap(); let audio_clip = open_audio_file(path).unwrap();
@ -216,7 +225,6 @@ mod open_audio_file {
} }
#[rstest] #[rstest]
#[case::ogg("sine-triangle.ogg")]
#[case::mp3("sine-triangle.mp3")] #[case::mp3("sine-triangle.mp3")]
#[case::flac("sine-triangle.flac")] #[case::flac("sine-triangle.flac")]
fn fails_when_opening_file_of_unsupported_type(#[case] file_name: &str) { fn fails_when_opening_file_of_unsupported_type(#[case] file_name: &str) {
@ -244,6 +252,7 @@ mod open_audio_file {
#[rstest] #[rstest]
#[case::wav("no-such-file.wav")] #[case::wav("no-such-file.wav")]
#[case::ogg("no-such-file.ogg")]
fn fails_if_file_does_not_exist(#[case] file_name: &str) { fn fails_if_file_does_not_exist(#[case] file_name: &str) {
let path = get_resource_file_path(file_name); let path = get_resource_file_path(file_name);
let result = open_audio_file(path); let result = open_audio_file(path);
@ -262,6 +271,14 @@ mod open_audio_file {
)] )]
#[case::wav_truncated_header("corrupt_truncated_header.wav", "Unexpected end of file.")] #[case::wav_truncated_header("corrupt_truncated_header.wav", "Unexpected end of file.")]
#[case::wav_truncated_data("corrupt_truncated_data.wav", "Unexpected end of file.")] #[case::wav_truncated_data("corrupt_truncated_data.wav", "Unexpected end of file.")]
#[case::ogg_file_type_txt(
"corrupt_file_type_txt.ogg",
"The given file was not recognized as Ogg Vorbis data."
)]
#[case::ogg_truncated_header(
"corrupt_truncated_header.ogg",
"The given file was not recognized as Ogg Vorbis data."
)]
fn fails_if_file_is_corrupt(#[case] file_name: &str, #[case] expected_message: &str) { fn fails_if_file_is_corrupt(#[case] file_name: &str, #[case] expected_message: &str) {
let path = get_resource_file_path(file_name); let path = get_resource_file_path(file_name);
let result = open_audio_file(path) let result = open_audio_file(path)
@ -280,6 +297,10 @@ mod open_audio_file {
#[case::wav_ansi("filename-ansi-€…‡‰‘’“”•™©±²½æ.wav")] #[case::wav_ansi("filename-ansi-€…‡‰‘’“”•™©±²½æ.wav")]
#[case::wav_unicode_bmp("filename-unicode-bmp-①∀⇨.wav")] #[case::wav_unicode_bmp("filename-unicode-bmp-①∀⇨.wav")]
#[case::wav_unicode_wide("filename-unicode-wide-😀🤣🙈🍨.wav")] #[case::wav_unicode_wide("filename-unicode-wide-😀🤣🙈🍨.wav")]
#[case::ogg_ascii("filename-ascii !#$%&'()+,-.;=@[]^_`{}~.ogg")]
#[case::ogg_ansi("filename-ansi-€…‡‰‘’“”•™©±²½æ.ogg")]
#[case::ogg_unicode_bmp("filename-unicode-bmp-①∀⇨.ogg")]
#[case::ogg_unicode_wide("filename-unicode-wide-😀🤣🙈🍨.ogg")]
fn supports_special_characters_in_file_names(#[case] file_name: &str) { fn supports_special_characters_in_file_names(#[case] file_name: &str) {
let path = get_resource_file_path(file_name); let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap(); let audio_clip = open_audio_file(path).unwrap();
@ -291,6 +312,7 @@ mod open_audio_file {
#[rstest] #[rstest]
#[case::wav("zero-samples.wav")] #[case::wav("zero-samples.wav")]
#[case::wav("zero-samples.ogg")]
fn supports_zero_sample_files(#[case] file_name: &str) { fn supports_zero_sample_files(#[case] file_name: &str) {
let path = get_resource_file_path(file_name); let path = get_resource_file_path(file_name);
let audio_clip = open_audio_file(path).unwrap(); let audio_clip = open_audio_file(path).unwrap();
@ -344,10 +366,12 @@ mod open_audio_file_with_reader {
#[rstest] #[rstest]
#[case::wav_not_found("sine-triangle-i16-audacity.wav", io::ErrorKind::NotFound)] #[case::wav_not_found("sine-triangle-i16-audacity.wav", io::ErrorKind::NotFound)]
#[case::ogg_not_found("sine-triangle.ogg", io::ErrorKind::NotFound)]
#[case::wav_permission_denied( #[case::wav_permission_denied(
"sine-triangle-i16-audacity.wav", "sine-triangle-i16-audacity.wav",
io::ErrorKind::PermissionDenied io::ErrorKind::PermissionDenied
)] )]
#[case::ogg_permission_denied("sine-triangle.ogg", io::ErrorKind::PermissionDenied)]
fn fails_on_io_errors(#[case] file_name: &'static str, #[case] error_kind: io::ErrorKind) { 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 next_error_kind = Rc::new(RefCell::new(None));
let audio_clip = { let audio_clip = {

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

Binary file not shown.

BIN
rhubarb/rhubarb-audio/tests/res/corrupt_truncated_header.ogg (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/zero-samples.ogg (Stored with Git LFS) Normal file

Binary file not shown.