Add support for reading Ogg Vorbis files
This commit is contained in:
parent
9c3b1fb554
commit
ca575d841e
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod ogg_audio_clip;
|
||||||
|
mod vorbis_file;
|
||||||
|
mod vorbis_file_raw;
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Binary file not shown.
Binary file not shown.
BIN
rhubarb/rhubarb-audio/tests/res/filename-ansi-€…‡‰‘’“”•™©±²½æ.ogg (Stored with Git LFS)
Normal file
BIN
rhubarb/rhubarb-audio/tests/res/filename-ansi-€…‡‰‘’“”•™©±²½æ.ogg (Stored with Git LFS)
Normal file
Binary file not shown.
BIN
rhubarb/rhubarb-audio/tests/res/filename-ascii !#$%&'()+,-.;=@[]^_`{}~.ogg (Stored with Git LFS)
Normal file
BIN
rhubarb/rhubarb-audio/tests/res/filename-ascii !#$%&'()+,-.;=@[]^_`{}~.ogg (Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
BIN
rhubarb/rhubarb-audio/tests/res/filename-unicode-wide-😀🤣🙈🍨.ogg (Stored with Git LFS)
Normal file
BIN
rhubarb/rhubarb-audio/tests/res/filename-unicode-wide-😀🤣🙈🍨.ogg (Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue