Skip to content

Commit

Permalink
refactor: use symphonia for audio handling to support additional file…
Browse files Browse the repository at this point in the history
… formats (#733)

* refactor(symphonia): replace error types

* refactor(symphonia): rewrite method bodies and signatures

Doesn't compile because source is used twice in new()

* refactor(symphonia): inline helper functions for OggDecoder::new

Fixes issues with passing the DataSource around, but now it can't be
returned as the error

* refactor(symphonia): replace format-specific decoder implementations

* style: clippy lints

* refactor(symphonia): impl Debug for Decoder

* style: fix naming

no longer a VorbisError, so "ve" doesn't make sense anymore

* refactor(symphonia): replace old decoder error variants

* refactor(symphonia): replace DataSource in error variants

* refactor(symphonia): clean up unwraps and error logging

* refactor(symphonia): extract samples method

* style: fix clippy lint about unnecessary to_owned call

* refactor(symphonia): clean up rewind impl to use the returned symphonia error

* fix: fill audio buffer

AudioBufferRef::make_equivalent() does not copy the data, so we have to
call convert() afterwards. This will also likely need to be changed when
upgrading symphonia. See
pdeljanov/Symphonia#198.

* refactor(sound): change time_seek to return a result, and use it to implement rewind

* refactor: replace BufferError with SoundBufferResourceLoadError variants

* doc: fix broken method link

* feat: add missing symphonia codecs for mp3, etc.

* fix(sound): interleave audio with multiple channels

* feat: use file extension as hint for symphonia probe

* style: rename variables for clarity

* fix: fix MediaSource impl for DataSource

* style: condense imports

* fix: fix MediaSource impl for DataSource::Memory

* refactor: add byte_len function to FileReader trait
  • Loading branch information
septante authored Feb 20, 2025
1 parent ebf90eb commit 81133c3
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 416 deletions.
31 changes: 29 additions & 2 deletions fyrox-resource/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
//! things such as loading assets within archive files
use fyrox_core::io::FileLoadError;
use std::fs::File;
use std::future::{ready, Future};
use std::io::BufReader;
use std::iter::empty;
use std::pin::Pin;
use std::{
Expand All @@ -32,9 +34,34 @@ use std::{
};

/// Trait for files readers ensuring they implement the required traits
pub trait FileReader: Debug + Send + Read + Seek + 'static {}
pub trait FileReader: Debug + Send + Sync + Read + Seek + 'static {
/// Returns the length in bytes, if available
fn byte_len(&self) -> Option<u64>;
}

impl FileReader for File {
fn byte_len(&self) -> Option<u64> {
match self.metadata() {
Ok(metadata) => Some(metadata.len()),
_ => None,
}
}
}

impl<F> FileReader for F where F: Debug + Send + Read + Seek + 'static {}
impl<T> FileReader for Cursor<T>
where
T: Debug + Send + Sync + std::convert::AsRef<[u8]> + 'static,
{
fn byte_len(&self) -> Option<u64> {
let inner = self.get_ref();
Some(inner.as_ref().len().try_into().unwrap())
}
}
impl FileReader for BufReader<File> {
fn byte_len(&self) -> Option<u64> {
self.get_ref().byte_len()
}
}

/// Interface wrapping IO operations for doing this like loading files
/// for resources
Expand Down
5 changes: 2 additions & 3 deletions fyrox-sound/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ rust-version = "1.80"
[dependencies]
fyrox-core = { path = "../fyrox-core", version = "0.36.0" }
fyrox-resource = { path = "../fyrox-resource", version = "0.36.0" }
lewton = "0.10.2"
ogg = "0.8.0"
hrtf = "0.8.0"
hound = "3.4.0"
strum = "0.26.1"
strum_macros = "0.26.1"
tinyaudio = "1"
serde = { version = "1", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
symphonia = { version = "0.5.4", features = ["all-codecs"] }
32 changes: 6 additions & 26 deletions fyrox-sound/src/buffer/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ use fyrox_core::{reflect::prelude::*, visitor::prelude::*};
use std::ops::{Deref, DerefMut};
use std::time::Duration;

use super::SoundBufferResourceLoadError;

/// Samples container.
#[derive(Debug, Default, Visit, Reflect)]
pub struct Samples(pub Vec<f32>);
Expand Down Expand Up @@ -93,19 +95,15 @@ impl GenericBuffer {
///
/// Data source with raw samples must have sample count multiple of channel count, otherwise this
/// function will return `Err`.
pub fn new(source: DataSource) -> Result<Self, DataSource> {
pub fn new(source: DataSource) -> Result<Self, SoundBufferResourceLoadError> {
match source {
DataSource::Raw {
sample_rate,
channel_count,
samples,
} => {
if channel_count < 1 || channel_count > 2 || samples.len() % channel_count != 0 {
Err(DataSource::Raw {
sample_rate,
channel_count,
samples,
})
Err(SoundBufferResourceLoadError::DataSourceError)
} else {
Ok(Self {
channel_duration_in_samples: samples.len() / channel_count,
Expand All @@ -115,29 +113,11 @@ impl GenericBuffer {
})
}
}
DataSource::RawStreaming(_) => Err(source),
DataSource::RawStreaming(_) => Err(SoundBufferResourceLoadError::DataSourceError),
_ => {
// Store cursor to handle errors.
let (is_memory, external_cursor) = if let DataSource::Memory(cursor) = &source {
(true, cursor.clone())
} else {
(false, Default::default())
};

let decoder = Decoder::new(source)?;
if decoder.get_channel_count() < 1 || decoder.get_channel_count() > 2 {
if is_memory {
return Err(DataSource::Memory(external_cursor));
} else {
// There is not much we can do here: if the user supplied DataSource::File,
// they probably do not want us to re-read the file again in
// DataSource::from_file.
return Err(DataSource::Raw {
sample_rate: decoder.get_sample_rate(),
channel_count: decoder.get_channel_count(),
samples: vec![],
});
}
return Err(SoundBufferResourceLoadError::DataSourceError);
}

Ok(Self {
Expand Down
78 changes: 69 additions & 9 deletions fyrox-sound/src/buffer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ use fyrox_resource::{
io::{FileReader, ResourceIo},
Resource, ResourceData, SOUND_BUFFER_RESOURCE_UUID,
};
use std::error::Error;
use std::{
error::Error,
fmt::Debug,
io::{Cursor, Read, Seek, SeekFrom},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
time::Duration,
};
use symphonia::core::io::MediaSource;

pub mod generic;
pub mod loader;
Expand Down Expand Up @@ -107,12 +108,16 @@ pub trait RawStreamingDataSource: Iterator<Item = f32> + Send + Sync + Debug {
fn channel_count(&self) -> usize;

/// Tells whether the provider should restart.
///
/// Default implementation calls [`Self::time_seek`] with a zero duration
fn rewind(&mut self) -> Result<(), SoundError> {
Ok(())
self.time_seek(Duration::from_secs(0))
}

/// Allows you to start playback from given duration.
fn time_seek(&mut self, _duration: Duration) {}
fn time_seek(&mut self, _duration: Duration) -> Result<(), SoundError> {
Ok(())
}

/// Returns total duration of the data.
fn channel_duration_in_samples(&self) -> usize {
Expand Down Expand Up @@ -181,15 +186,56 @@ impl Seek for DataSource {
}
}

impl MediaSource for DataSource {
fn is_seekable(&self) -> bool {
match self {
DataSource::File { .. } | DataSource::Memory(_) => true,
DataSource::Raw { .. } | DataSource::RawStreaming(_) => false,
}
}

fn byte_len(&self) -> Option<u64> {
match self {
DataSource::File { path: _, data } => data.byte_len(),
DataSource::Memory(cursor) => MediaSource::byte_len(cursor),
DataSource::Raw { .. } | DataSource::RawStreaming(_) => None,
}
}
}

/// An error that can occur during loading of sound buffer.
#[derive(Debug)]
pub enum SoundBufferResourceLoadError {
/// A format is not supported.
UnsupportedFormat,
/// File load error.
Io(FileLoadError),
/// Errors involving the data source
///
/// This could be, e.g., wrong number of channels, or attempting to use a
/// [`DataSource::RawStreaming`] where it doesn't make sense
DataSourceError,
/// Underlying sound error
SoundError(SoundError),
}

impl std::fmt::Display for SoundBufferResourceLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SoundBufferResourceLoadError::UnsupportedFormat => {
write!(f, "unsupported file format")
}
SoundBufferResourceLoadError::Io(e) => write!(f, "{e:?}"),
SoundBufferResourceLoadError::DataSourceError => {
write!(f, "error in underlying data source")
}
SoundBufferResourceLoadError::SoundError(e) => write!(f, "{e:?}"),
}
}
}

impl std::error::Error for SoundBufferResourceLoadError {}

/// Sound buffer is a data source for sound sources. See module documentation for more info.
#[derive(Debug, Visit, Reflect)]
pub enum SoundBuffer {
Expand All @@ -204,28 +250,42 @@ pub enum SoundBuffer {
Streaming(StreamingBuffer),
}

impl From<SoundError> for SoundBufferResourceLoadError {
fn from(err: SoundError) -> Self {
Self::SoundError(err)
}
}

/// Type alias for sound buffer resource.
pub type SoundBufferResource = Resource<SoundBuffer>;

/// Extension trait for sound buffer resource.
pub trait SoundBufferResourceExtension {
/// Tries to create new streaming sound buffer from a given data source.
fn new_streaming(data_source: DataSource) -> Result<Resource<SoundBuffer>, DataSource>;
fn new_streaming(
data_source: DataSource,
) -> Result<Resource<SoundBuffer>, SoundBufferResourceLoadError>;

/// Tries to create new generic sound buffer from a given data source.
fn new_generic(data_source: DataSource) -> Result<Resource<SoundBuffer>, DataSource>;
fn new_generic(
data_source: DataSource,
) -> Result<Resource<SoundBuffer>, SoundBufferResourceLoadError>;
}

impl SoundBufferResourceExtension for SoundBufferResource {
fn new_streaming(data_source: DataSource) -> Result<Resource<SoundBuffer>, DataSource> {
fn new_streaming(
data_source: DataSource,
) -> Result<Resource<SoundBuffer>, SoundBufferResourceLoadError> {
let path = data_source.path_owned();
Ok(Resource::new_ok(
path.into(),
SoundBuffer::Streaming(StreamingBuffer::new(data_source)?),
))
}

fn new_generic(data_source: DataSource) -> Result<Resource<SoundBuffer>, DataSource> {
fn new_generic(
data_source: DataSource,
) -> Result<Resource<SoundBuffer>, SoundBufferResourceLoadError> {
let path = data_source.path_owned();
Ok(Resource::new_ok(
path.into(),
Expand All @@ -243,13 +303,13 @@ impl TypeUuidProvider for SoundBuffer {
impl SoundBuffer {
/// Tries to create new streaming sound buffer from a given data source. It returns raw sound
/// buffer that has to be wrapped into Arc<Mutex<>> for use with sound sources.
pub fn raw_streaming(data_source: DataSource) -> Result<Self, DataSource> {
pub fn raw_streaming(data_source: DataSource) -> Result<Self, SoundBufferResourceLoadError> {
Ok(Self::Streaming(StreamingBuffer::new(data_source)?))
}

/// Tries to create new generic sound buffer from a given data source. It returns raw sound
/// buffer that has to be wrapped into Arc<Mutex<>> for use with sound sources.
pub fn raw_generic(data_source: DataSource) -> Result<Self, DataSource> {
pub fn raw_generic(data_source: DataSource) -> Result<Self, SoundBufferResourceLoadError> {
Ok(Self::Generic(GenericBuffer::new(data_source)?))
}
}
Expand Down
14 changes: 7 additions & 7 deletions fyrox-sound/src/buffer/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ impl Default for StreamingSource {

impl StreamingSource {
#[inline]
fn new(data_source: DataSource) -> Result<Self, DataSource> {
fn new(data_source: DataSource) -> Result<Self, SoundError> {
match data_source {
DataSource::File { .. } | DataSource::Memory(_) => {
Ok(Self::Decoder(Decoder::new(data_source)?))
}
DataSource::RawStreaming(raw) => Ok(Self::Raw(raw)),
// It makes no sense to stream raw data which is already loaded into memory.
_ => Err(data_source),
_ => Err(SoundError::UnsupportedFormat),
}
}

Expand Down Expand Up @@ -133,9 +133,9 @@ impl StreamingSource {
}
}

fn time_seek(&mut self, location: Duration) {
fn time_seek(&mut self, location: Duration) -> Result<(), SoundError> {
match self {
StreamingSource::Null => {}
StreamingSource::Null => Ok(()),
StreamingSource::Decoder(decoder) => decoder.time_seek(location),
StreamingSource::Raw(raw) => raw.time_seek(location),
}
Expand Down Expand Up @@ -183,7 +183,7 @@ impl StreamingBuffer {
///
/// This function will return Err if data source is `Raw`. It makes no sense to stream raw data which
/// is already loaded into memory. Use Generic source instead!
pub fn new(source: DataSource) -> Result<Self, DataSource> {
pub fn new(source: DataSource) -> Result<Self, SoundError> {
let mut streaming_source = StreamingSource::new(source)?;

let mut samples = Vec::new();
Expand Down Expand Up @@ -215,8 +215,8 @@ impl StreamingBuffer {
}

#[inline]
pub(crate) fn time_seek(&mut self, location: Duration) {
self.streaming_source.time_seek(location);
pub(crate) fn time_seek(&mut self, location: Duration) -> Result<(), SoundError> {
self.streaming_source.time_seek(location)
}
}

Expand Down
Loading

0 comments on commit 81133c3

Please sign in to comment.