#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
pub mod resource_daemon;
#[cfg(feature = "cpal")]
pub mod cpal_daemon;
use std::sync::{Arc, LazyLock};
use moosicbox_audio_decoder::{AudioDecode, AudioDecodeError};
use moosicbox_resampler::{Resampler, to_audio_buffer};
use switchy_async::sync::Mutex;
use switchy_async::task::JoinError;
use symphonia::core::audio::{AudioBuffer, Signal as _};
use symphonia::core::conv::FromSample;
use symphonia::core::formats::{Packet, Track};
use thiserror::Error;
pub use symphonia::core::audio::{Channels, SignalSpec};
pub use symphonia::core::conv::{ConvertibleSample, IntoSample};
pub use symphonia::core::units::Duration;
pub use progress_tracker::ProgressTracker;
pub use command::{AudioCommand, AudioError, AudioHandle, AudioResponse, CommandMessage};
pub mod command;
pub mod encoder;
#[cfg(feature = "api")]
pub mod api;
#[cfg(feature = "cpal")]
pub mod cpal;
pub mod progress_tracker;
pub struct AudioOutput {
pub id: String,
pub name: String,
pub spec: SignalSpec,
resampler: Option<Resampler<f32>>,
writer: Box<dyn AudioWrite>,
}
impl std::fmt::Debug for AudioOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AudioOutput")
.field("id", &self.id)
.field("name", &self.name)
.field("spec", &self.spec)
.finish_non_exhaustive()
}
}
impl AudioOutput {
#[must_use]
pub fn new(id: String, name: String, spec: SignalSpec, writer: Box<dyn AudioWrite>) -> Self {
Self {
id,
name,
spec,
resampler: None,
writer,
}
}
fn resample_if_needed(
&mut self,
decoded: AudioBuffer<f32>,
) -> Result<AudioBuffer<f32>, AudioOutputError> {
Ok(if let Some(resampler) = &mut self.resampler {
let Some(samples) = resampler.resample(&decoded) else {
return Err(AudioOutputError::StreamEnd);
};
to_audio_buffer(samples, self.spec)
} else if decoded.spec().rate != self.spec.rate {
let duration = decoded.capacity();
log::debug!(
"audio_output: resample_if_needed: resampling from {} to {} original_duration={} target_duration={}",
decoded.spec().rate,
self.spec.rate,
decoded.capacity(),
duration,
);
self.resampler.replace(Resampler::new(
*decoded.spec(),
self.spec.rate as usize,
duration as u64,
));
self.resample_if_needed(decoded)?
} else {
decoded
})
}
}
impl AudioWrite for AudioOutput {
fn write(&mut self, decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
let buf = {
match self.resample_if_needed(decoded) {
Ok(buf) => buf,
Err(e) => match e {
AudioOutputError::StreamEnd => return Ok(0),
_ => return Err(e),
},
}
};
self.writer.write(buf)
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
AudioWrite::flush(&mut *self.writer)
}
fn get_playback_position(&self) -> Option<f64> {
self.writer.get_playback_position()
}
fn set_consumed_samples(&mut self, consumed_samples: Arc<std::sync::atomic::AtomicUsize>) {
self.writer.set_consumed_samples(consumed_samples);
}
fn set_volume(&mut self, volume: f64) {
self.writer.set_volume(volume);
}
fn set_shared_volume(&mut self, shared_volume: std::sync::Arc<atomic_float::AtomicF64>) {
self.writer.set_shared_volume(shared_volume);
}
fn get_output_spec(&self) -> Option<symphonia::core::audio::SignalSpec> {
self.writer.get_output_spec()
}
fn set_progress_callback(
&mut self,
callback: Option<Box<dyn Fn(f64) + Send + Sync + 'static>>,
) {
self.writer.set_progress_callback(callback);
}
fn handle(&self) -> AudioHandle {
self.writer.handle()
}
}
impl AudioDecode for AudioOutput {
fn decoded(
&mut self,
decoded: AudioBuffer<f32>,
_packet: &Packet,
_track: &Track,
) -> Result<(), AudioDecodeError> {
let buf = {
match self.resample_if_needed(decoded) {
Ok(buf) => buf,
Err(e) => match e {
AudioOutputError::StreamEnd => return Ok(()),
_ => return Err(AudioDecodeError::Other(Box::new(e))),
},
}
};
self.writer
.write(buf)
.map_err(|e| AudioDecodeError::Other(Box::new(e)))?;
Ok(())
}
fn flush(&mut self) -> Result<(), AudioDecodeError> {
AudioWrite::flush(self).map_err(|e| AudioDecodeError::Other(Box::new(e)))
}
}
type InnerType = Box<dyn AudioWrite>;
pub type GetWriter = Box<dyn Fn() -> Result<InnerType, AudioOutputError> + Send>;
#[derive(Clone)]
pub struct AudioOutputFactory {
pub id: String,
pub name: String,
pub spec: SignalSpec,
get_writer: Arc<std::sync::Mutex<GetWriter>>,
}
impl std::fmt::Debug for AudioOutputFactory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AudioOutputFactory")
.field("id", &self.id)
.field("name", &self.name)
.field("spec", &self.spec)
.field("get_writer", &"{{get_writer}}")
.finish()
}
}
impl AudioOutputFactory {
#[must_use]
pub fn new(
id: String,
name: String,
spec: SignalSpec,
writer: impl (Fn() -> Result<InnerType, AudioOutputError>) + Send + 'static,
) -> Self {
Self {
id,
name,
spec,
get_writer: Arc::new(std::sync::Mutex::new(Box::new(writer))),
}
}
#[must_use]
pub fn new_box(id: String, name: String, spec: SignalSpec, writer: GetWriter) -> Self {
Self {
id,
name,
spec,
get_writer: Arc::new(std::sync::Mutex::new(writer)),
}
}
pub fn try_into_output(&self) -> Result<AudioOutput, AudioOutputError> {
self.try_into()
}
}
impl TryFrom<AudioOutputFactory> for AudioOutput {
type Error = AudioOutputError;
fn try_from(value: AudioOutputFactory) -> Result<Self, Self::Error> {
Ok(Self {
id: value.id,
name: value.name,
spec: value.spec,
resampler: None,
writer: (value.get_writer.lock().unwrap())()?,
})
}
}
impl TryFrom<&AudioOutputFactory> for AudioOutput {
type Error = AudioOutputError;
fn try_from(value: &AudioOutputFactory) -> Result<Self, Self::Error> {
Ok(Self {
id: value.id.clone(),
name: value.name.clone(),
spec: value.spec,
resampler: None,
writer: (value.get_writer.lock().unwrap())()?,
})
}
}
pub trait AudioWrite {
fn write(&mut self, decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError>;
fn flush(&mut self) -> Result<(), AudioOutputError>;
fn get_playback_position(&self) -> Option<f64> {
None
}
fn set_consumed_samples(&mut self, _consumed_samples: Arc<std::sync::atomic::AtomicUsize>) {}
fn set_volume(&mut self, _volume: f64) {}
fn set_shared_volume(&mut self, _shared_volume: std::sync::Arc<atomic_float::AtomicF64>) {}
fn get_output_spec(&self) -> Option<SignalSpec> {
None
}
fn set_progress_callback(
&mut self,
_callback: Option<Box<dyn Fn(f64) + Send + Sync + 'static>>,
) {
}
fn handle(&self) -> AudioHandle;
}
impl AudioDecode for Box<dyn AudioWrite> {
fn decoded(
&mut self,
decoded: symphonia::core::audio::AudioBuffer<f32>,
_packet: &Packet,
_track: &Track,
) -> Result<(), AudioDecodeError> {
self.write(decoded)
.map_err(|e| AudioDecodeError::Other(Box::new(e)))?;
Ok(())
}
fn flush(&mut self) -> Result<(), AudioDecodeError> {
(**self)
.flush()
.map_err(|e| AudioDecodeError::Other(Box::new(e)))
}
}
impl AudioDecode for &mut dyn AudioWrite {
fn decoded(
&mut self,
decoded: symphonia::core::audio::AudioBuffer<f32>,
_packet: &Packet,
_track: &Track,
) -> Result<(), AudioDecodeError> {
self.write(decoded)
.map_err(|e| AudioDecodeError::Other(Box::new(e)))?;
Ok(())
}
fn flush(&mut self) -> Result<(), AudioDecodeError> {
(*self)
.flush()
.map_err(|e| AudioDecodeError::Other(Box::new(e)))
}
}
impl From<Box<dyn AudioWrite>> for Box<dyn AudioDecode> {
fn from(value: Box<dyn AudioWrite>) -> Self {
Box::new(value)
}
}
#[allow(dead_code)]
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Error)]
pub enum AudioOutputError {
#[error("No audio outputs")]
NoOutputs,
#[error("Unsupported output configuration")]
UnsupportedOutputConfiguration,
#[error("Unsupported channels: {0}")]
UnsupportedChannels(usize),
#[error("OpenStreamError")]
OpenStream,
#[error("PlayStreamError")]
PlayStream,
#[error("StreamClosedError")]
StreamClosed,
#[error("StreamEndError")]
StreamEnd,
#[error("InterruptError")]
Interrupt,
#[error(transparent)]
IO(#[from] std::io::Error),
#[cfg(feature = "cpal")]
#[error(transparent)]
SupportedStreamConfigs(#[from] ::cpal::SupportedStreamConfigsError),
}
#[allow(unused)]
fn to_samples<S: FromSample<f32> + Default + Clone>(decoded: &AudioBuffer<f32>) -> Vec<S> {
let n_channels = decoded.spec().channels.count();
let n_samples = decoded.frames() * n_channels;
let mut buf: Vec<S> = vec![S::default(); n_samples];
for ch in 0..n_channels {
let ch_slice = decoded.chan(ch);
for (dst, decoded) in buf[ch..].iter_mut().step_by(n_channels).zip(ch_slice) {
*dst = (*decoded).into_sample();
}
}
buf
}
static AUDIO_OUTPUT_SCANNER: LazyLock<Arc<Mutex<AudioOutputScanner>>> =
LazyLock::new(|| Arc::new(Mutex::new(AudioOutputScanner::new())));
pub async fn scan_outputs() -> Result<(), AudioOutputScannerError> {
AUDIO_OUTPUT_SCANNER.lock().await.scan().await
}
#[must_use]
pub async fn output_factories() -> Vec<AudioOutputFactory> {
AUDIO_OUTPUT_SCANNER.lock().await.outputs.clone()
}
#[must_use]
pub async fn default_output_factory() -> Option<AudioOutputFactory> {
AUDIO_OUTPUT_SCANNER
.lock()
.await
.default_output_factory()
.cloned()
}
pub async fn default_output() -> Result<AudioOutput, AudioOutputScannerError> {
AUDIO_OUTPUT_SCANNER.lock().await.default_output()
}
pub struct AudioOutputScanner {
pub outputs: Vec<AudioOutputFactory>,
pub default_output: Option<AudioOutputFactory>,
}
#[allow(dead_code)]
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Error)]
pub enum AudioOutputScannerError {
#[error("No outputs available")]
NoOutputs,
#[error(transparent)]
AudioOutput(#[from] AudioOutputError),
#[error(transparent)]
Join(#[from] JoinError),
}
impl AudioOutputScanner {
#[must_use]
pub const fn new() -> Self {
Self {
outputs: vec![],
default_output: None,
}
}
#[allow(clippy::too_many_lines, clippy::unused_async)]
pub async fn scan(&mut self) -> Result<(), AudioOutputScannerError> {
self.default_output = None;
self.outputs = vec![];
#[cfg(feature = "cpal")]
{
self.outputs.extend(
switchy_async::runtime::Handle::current()
.spawn_with_name(
"server: scan cpal outputs",
switchy_async::runtime::Handle::current().spawn_blocking_with_name(
"server: scan cpal outputs (blocking)",
|| {
let start = switchy_time::now();
let outputs =
crate::cpal::scan_available_outputs().collect::<Vec<_>>();
for output in &outputs {
log::debug!("cpal output: {}", output.name);
}
let end = switchy_time::now();
log::debug!(
"took {}ms to scan outputs",
end.duration_since(start).unwrap().as_millis()
);
outputs
},
),
)
.await??,
);
if self.default_output.is_none() {
self.default_output = switchy_async::runtime::Handle::current()
.spawn_with_name(
"server: scan cpal default output",
switchy_async::runtime::Handle::current().spawn_blocking_with_name(
"server: scan cpal default output (blocking)",
|| {
let start = switchy_time::now();
let output = crate::cpal::scan_default_output();
if let Some(output) = &output {
log::debug!("cpal output: {}", output.name);
}
let end = switchy_time::now();
log::debug!(
"took {}ms to scan default output",
end.duration_since(start).unwrap().as_millis()
);
output
},
),
)
.await??;
if let Some(output) = &self.default_output
&& !self.outputs.iter().any(|x| x.id == output.id)
{
if self.outputs.is_empty() {
self.outputs.push(output.clone());
} else {
self.outputs.insert(0, output.clone());
}
}
}
}
Ok(())
}
#[must_use]
pub const fn default_output_factory(&self) -> Option<&AudioOutputFactory> {
self.default_output.as_ref()
}
pub fn default_output(&self) -> Result<AudioOutput, AudioOutputScannerError> {
self.default_output_factory()
.map(TryInto::try_into)
.transpose()?
.ok_or(AudioOutputScannerError::NoOutputs)
}
}
impl Default for AudioOutputScanner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
#[test_log::test]
fn test_audio_output_error_debug() {
let err = AudioOutputError::NoOutputs;
assert_eq!(format!("{err:?}"), "NoOutputs");
let err = AudioOutputError::UnsupportedOutputConfiguration;
assert_eq!(format!("{err:?}"), "UnsupportedOutputConfiguration");
let err = AudioOutputError::UnsupportedChannels(5);
assert_eq!(format!("{err:?}"), "UnsupportedChannels(5)");
let err = AudioOutputError::OpenStream;
assert_eq!(format!("{err:?}"), "OpenStream");
let err = AudioOutputError::PlayStream;
assert_eq!(format!("{err:?}"), "PlayStream");
let err = AudioOutputError::StreamClosed;
assert_eq!(format!("{err:?}"), "StreamClosed");
let err = AudioOutputError::StreamEnd;
assert_eq!(format!("{err:?}"), "StreamEnd");
let err = AudioOutputError::Interrupt;
assert_eq!(format!("{err:?}"), "Interrupt");
}
#[test_log::test]
fn test_audio_output_error_display() {
let err = AudioOutputError::NoOutputs;
assert_eq!(format!("{err}"), "No audio outputs");
let err = AudioOutputError::UnsupportedOutputConfiguration;
assert_eq!(format!("{err}"), "Unsupported output configuration");
let err = AudioOutputError::UnsupportedChannels(5);
assert_eq!(format!("{err}"), "Unsupported channels: 5");
let err = AudioOutputError::OpenStream;
assert_eq!(format!("{err}"), "OpenStreamError");
let err = AudioOutputError::PlayStream;
assert_eq!(format!("{err}"), "PlayStreamError");
let err = AudioOutputError::StreamClosed;
assert_eq!(format!("{err}"), "StreamClosedError");
let err = AudioOutputError::StreamEnd;
assert_eq!(format!("{err}"), "StreamEndError");
let err = AudioOutputError::Interrupt;
assert_eq!(format!("{err}"), "InterruptError");
}
#[test_log::test]
fn test_audio_output_error_from_io() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let err: AudioOutputError = io_err.into();
assert!(matches!(err, AudioOutputError::IO(_)));
}
#[test_log::test]
fn test_audio_output_scanner_error_debug() {
let err = AudioOutputScannerError::NoOutputs;
assert_eq!(format!("{err:?}"), "NoOutputs");
}
#[test_log::test]
fn test_audio_output_scanner_error_display() {
let err = AudioOutputScannerError::NoOutputs;
assert_eq!(format!("{err}"), "No outputs available");
}
#[test_log::test]
fn test_audio_output_scanner_error_from_audio_output() {
let err = AudioOutputError::NoOutputs;
let scanner_err: AudioOutputScannerError = err.into();
assert!(matches!(
scanner_err,
AudioOutputScannerError::AudioOutput(_)
));
}
#[test_log::test]
fn test_audio_output_scanner_new() {
let scanner = AudioOutputScanner::new();
assert_eq!(scanner.outputs.len(), 0);
assert!(scanner.default_output.is_none());
}
#[test_log::test]
fn test_audio_output_scanner_default() {
let scanner = AudioOutputScanner::default();
assert_eq!(scanner.outputs.len(), 0);
assert!(scanner.default_output.is_none());
}
#[test_log::test]
fn test_audio_output_scanner_default_output_factory() {
let scanner = AudioOutputScanner::new();
assert!(scanner.default_output_factory().is_none());
}
#[test_log::test]
fn test_audio_output_scanner_default_output_error() {
let scanner = AudioOutputScanner::new();
let result = scanner.default_output();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
AudioOutputScannerError::NoOutputs
));
}
#[test_log::test]
fn test_audio_output_factory_debug() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let factory = AudioOutputFactory::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
|| Err(AudioOutputError::NoOutputs),
);
let debug_str = format!("{factory:?}");
assert!(debug_str.contains("test-id"));
assert!(debug_str.contains("Test Output"));
assert!(debug_str.contains("get_writer"));
}
#[test_log::test]
fn test_audio_output_factory_clone() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let factory = AudioOutputFactory::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
|| Err(AudioOutputError::NoOutputs),
);
let cloned = factory.clone();
assert_eq!(cloned.id, factory.id);
assert_eq!(cloned.name, factory.name);
}
#[test_log::test]
fn test_audio_output_factory_new_box() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let writer: GetWriter = Box::new(|| Err(AudioOutputError::NoOutputs));
let factory = AudioOutputFactory::new_box(
"test-id".to_string(),
"Test Output".to_string(),
spec,
writer,
);
assert_eq!(factory.id, "test-id");
assert_eq!(factory.name, "Test Output");
}
#[test_log::test]
fn test_audio_output_factory_try_into_output_error() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let factory = AudioOutputFactory::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
|| Err(AudioOutputError::NoOutputs),
);
let result = factory.try_into_output();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AudioOutputError::NoOutputs));
}
#[test_log::test]
fn test_audio_output_factory_try_from_error() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let factory = AudioOutputFactory::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
|| Err(AudioOutputError::NoOutputs),
);
let result: Result<AudioOutput, _> = factory.try_into();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AudioOutputError::NoOutputs));
}
#[test_log::test]
fn test_audio_output_factory_try_from_ref_error() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let factory = AudioOutputFactory::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
|| Err(AudioOutputError::NoOutputs),
);
let result: Result<AudioOutput, _> = (&factory).try_into();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AudioOutputError::NoOutputs));
}
struct MockAudioWrite {
handle: AudioHandle,
}
impl MockAudioWrite {
fn new() -> Self {
let (tx, _rx) = flume::bounded(1);
Self {
handle: AudioHandle::new(tx),
}
}
}
impl AudioWrite for MockAudioWrite {
fn write(&mut self, decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
Ok(decoded.frames())
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
Ok(())
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
#[test_log::test]
fn test_audio_output_new() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(MockAudioWrite::new()),
);
assert_eq!(output.id, "test-id");
assert_eq!(output.name, "Test Output");
assert_eq!(output.spec.rate, 44100);
}
#[test_log::test]
fn test_audio_output_debug() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(MockAudioWrite::new()),
);
let debug_str = format!("{output:?}");
assert!(debug_str.contains("test-id"));
assert!(debug_str.contains("Test Output"));
assert!(debug_str.contains("AudioOutput"));
}
#[test_log::test]
fn test_audio_output_handle() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(MockAudioWrite::new()),
);
let _handle = output.handle();
}
#[test_log::test]
fn test_to_samples_stereo_interleaving() {
use symphonia::core::audio::Signal;
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(4, spec);
buffer.render_reserved(Some(4));
{
let left = buffer.chan_mut(0);
left[0] = 1.0;
left[1] = 2.0;
left[2] = 3.0;
left[3] = 4.0;
let right = buffer.chan_mut(1);
right[0] = 5.0;
right[1] = 6.0;
right[2] = 7.0;
right[3] = 8.0;
}
let samples: Vec<f32> = to_samples(&buffer);
assert_eq!(samples.len(), 8);
assert!((samples[0] - 1.0).abs() < f32::EPSILON);
assert!((samples[1] - 5.0).abs() < f32::EPSILON);
assert!((samples[2] - 2.0).abs() < f32::EPSILON);
assert!((samples[3] - 6.0).abs() < f32::EPSILON);
assert!((samples[4] - 3.0).abs() < f32::EPSILON);
assert!((samples[5] - 7.0).abs() < f32::EPSILON);
assert!((samples[6] - 4.0).abs() < f32::EPSILON);
assert!((samples[7] - 8.0).abs() < f32::EPSILON);
}
#[test_log::test]
fn test_to_samples_mono() {
use symphonia::core::audio::Signal;
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT);
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(4, spec);
buffer.render_reserved(Some(4));
{
let mono = buffer.chan_mut(0);
mono[0] = 1.0;
mono[1] = 2.0;
mono[2] = 3.0;
mono[3] = 4.0;
}
let samples: Vec<f32> = to_samples(&buffer);
assert_eq!(samples.len(), 4);
assert!((samples[0] - 1.0).abs() < f32::EPSILON);
assert!((samples[1] - 2.0).abs() < f32::EPSILON);
assert!((samples[2] - 3.0).abs() < f32::EPSILON);
assert!((samples[3] - 4.0).abs() < f32::EPSILON);
}
#[test_log::test]
fn test_to_samples_type_conversion_to_i16() {
use symphonia::core::audio::Signal;
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(2, spec);
buffer.render_reserved(Some(2));
{
let left = buffer.chan_mut(0);
left[0] = 0.5;
left[1] = -0.5;
let right = buffer.chan_mut(1);
right[0] = 0.25;
right[1] = -0.25;
}
let samples: Vec<i16> = to_samples(&buffer);
assert_eq!(samples.len(), 4);
assert!((samples[0] - 16383).abs() <= 1);
assert!((samples[1] - 8191).abs() <= 1);
assert!((samples[2] - (-16384)).abs() <= 1);
assert!((samples[3] - (-8192)).abs() <= 1);
}
struct TrackingMockAudioWrite {
handle: AudioHandle,
written_frames: std::sync::Arc<std::sync::atomic::AtomicUsize>,
flush_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl TrackingMockAudioWrite {
fn new() -> Self {
let (tx, _rx) = flume::bounded(1);
Self {
handle: AudioHandle::new(tx),
written_frames: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
flush_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
}
impl AudioWrite for TrackingMockAudioWrite {
fn write(&mut self, decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
let frames = decoded.frames();
self.written_frames
.fetch_add(frames, std::sync::atomic::Ordering::SeqCst);
Ok(frames)
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
self.flush_count
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(())
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
#[test_log::test]
fn test_audio_output_write_same_sample_rate() {
use symphonia::core::audio::Signal;
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mock_writer = TrackingMockAudioWrite::new();
let written_frames = mock_writer.written_frames.clone();
let mut output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(100, spec);
buffer.render_reserved(Some(100));
for ch in 0..2 {
let chan = buffer.chan_mut(ch);
#[allow(clippy::cast_precision_loss)]
for (i, sample) in chan.iter_mut().enumerate().take(100) {
*sample = (i as f32) / 100.0;
}
}
let result = AudioWrite::write(&mut output, buffer);
assert!(result.is_ok());
assert_eq!(
written_frames.load(std::sync::atomic::Ordering::SeqCst),
100
);
}
#[test_log::test]
fn test_audio_output_write_empty_buffer() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mock_writer = TrackingMockAudioWrite::new();
let written_frames = mock_writer.written_frames.clone();
let mut output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
let buffer: AudioBuffer<f32> = AudioBuffer::new(0, spec);
let result = AudioWrite::write(&mut output, buffer);
assert!(result.is_ok());
assert_eq!(written_frames.load(std::sync::atomic::Ordering::SeqCst), 0);
}
#[test_log::test]
fn test_audio_output_flush() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mock_writer = TrackingMockAudioWrite::new();
let flush_count = mock_writer.flush_count.clone();
let mut output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
let result = AudioWrite::flush(&mut output);
assert!(result.is_ok());
assert_eq!(flush_count.load(std::sync::atomic::Ordering::SeqCst), 1);
let result = AudioWrite::flush(&mut output);
assert!(result.is_ok());
assert_eq!(flush_count.load(std::sync::atomic::Ordering::SeqCst), 2);
}
#[test_log::test]
fn test_audio_output_get_playback_position() {
struct MockAudioWriteWithPosition {
handle: AudioHandle,
position: f64,
}
impl AudioWrite for MockAudioWriteWithPosition {
fn write(&mut self, decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
Ok(decoded.frames())
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
Ok(())
}
fn get_playback_position(&self) -> Option<f64> {
Some(self.position)
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let (tx, _rx) = flume::bounded(1);
let mock_writer = MockAudioWriteWithPosition {
handle: AudioHandle::new(tx),
position: 42.5,
};
let output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
assert_eq!(output.get_playback_position(), Some(42.5));
}
#[test_log::test]
fn test_audio_output_get_output_spec() {
struct MockAudioWriteWithSpec {
handle: AudioHandle,
output_spec: SignalSpec,
}
impl AudioWrite for MockAudioWriteWithSpec {
fn write(&mut self, decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
Ok(decoded.frames())
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
Ok(())
}
fn get_output_spec(&self) -> Option<SignalSpec> {
Some(self.output_spec)
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
let input_spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let output_spec = SignalSpec::new(48000, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let (tx, _rx) = flume::bounded(1);
let mock_writer = MockAudioWriteWithSpec {
handle: AudioHandle::new(tx),
output_spec,
};
let output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
input_spec,
Box::new(mock_writer),
);
let spec = output.get_output_spec();
assert!(spec.is_some());
assert_eq!(spec.unwrap().rate, 48000);
}
#[test_log::test]
fn test_audio_output_audio_decode_decoded_success() {
use moosicbox_audio_decoder::AudioDecode;
use symphonia::core::audio::Signal;
use symphonia::core::codecs::{CODEC_TYPE_NULL, CodecParameters};
use symphonia::core::formats::{Packet, Track};
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mock_writer = TrackingMockAudioWrite::new();
let written_frames = mock_writer.written_frames.clone();
let mut output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(50, spec);
buffer.render_reserved(Some(50));
for ch in 0..2 {
let chan = buffer.chan_mut(ch);
#[allow(clippy::cast_precision_loss)]
for (i, sample) in chan.iter_mut().enumerate().take(50) {
*sample = (i as f32) / 100.0;
}
}
let packet = Packet::new_from_slice(0, 0, 0, &[]);
let track = Track::new(0, CodecParameters::new().for_codec(CODEC_TYPE_NULL).clone());
let result = AudioDecode::decoded(&mut output, buffer, &packet, &track);
assert!(result.is_ok());
assert_eq!(written_frames.load(std::sync::atomic::Ordering::SeqCst), 50);
}
#[test_log::test]
fn test_audio_output_audio_decode_flush_success() {
use moosicbox_audio_decoder::AudioDecode;
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mock_writer = TrackingMockAudioWrite::new();
let flush_count = mock_writer.flush_count.clone();
let mut output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
let result = AudioDecode::flush(&mut output);
assert!(result.is_ok());
assert_eq!(flush_count.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[test_log::test]
fn test_audio_output_audio_decode_flush_error() {
use moosicbox_audio_decoder::AudioDecode;
struct FailingFlushWriter {
handle: AudioHandle,
}
impl AudioWrite for FailingFlushWriter {
fn write(&mut self, decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
Ok(decoded.frames())
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
Err(AudioOutputError::StreamClosed)
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let (tx, _rx) = flume::bounded(1);
let mock_writer = FailingFlushWriter {
handle: AudioHandle::new(tx),
};
let mut output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
let result = AudioDecode::flush(&mut output);
assert!(result.is_err());
}
#[test_log::test]
fn test_box_dyn_audio_write_audio_decode_decoded() {
use moosicbox_audio_decoder::AudioDecode;
use symphonia::core::audio::Signal;
use symphonia::core::codecs::{CODEC_TYPE_NULL, CodecParameters};
use symphonia::core::formats::{Packet, Track};
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mock_writer = TrackingMockAudioWrite::new();
let written_frames = mock_writer.written_frames.clone();
let mut boxed_writer: Box<dyn AudioWrite> = Box::new(mock_writer);
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(25, spec);
buffer.render_reserved(Some(25));
for ch in 0..2 {
let chan = buffer.chan_mut(ch);
for sample in chan.iter_mut().take(25) {
*sample = 0.5;
}
}
let packet = Packet::new_from_slice(0, 0, 0, &[]);
let track = Track::new(0, CodecParameters::new().for_codec(CODEC_TYPE_NULL).clone());
let result = AudioDecode::decoded(&mut boxed_writer, buffer, &packet, &track);
assert!(result.is_ok());
assert_eq!(written_frames.load(std::sync::atomic::Ordering::SeqCst), 25);
}
#[test_log::test]
fn test_box_dyn_audio_write_audio_decode_flush() {
use moosicbox_audio_decoder::AudioDecode;
let _spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mock_writer = TrackingMockAudioWrite::new();
let flush_count = mock_writer.flush_count.clone();
let mut boxed_writer: Box<dyn AudioWrite> = Box::new(mock_writer);
let result = AudioDecode::flush(&mut boxed_writer);
assert!(result.is_ok());
assert_eq!(flush_count.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[test_log::test]
fn test_ref_mut_dyn_audio_write_audio_decode_decoded() {
use moosicbox_audio_decoder::AudioDecode;
use symphonia::core::audio::Signal;
use symphonia::core::codecs::{CODEC_TYPE_NULL, CodecParameters};
use symphonia::core::formats::{Packet, Track};
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mock_writer = TrackingMockAudioWrite::new();
let written_frames = mock_writer.written_frames.clone();
let mut boxed_writer: Box<dyn AudioWrite> = Box::new(mock_writer);
let writer_ref: &mut dyn AudioWrite = &mut *boxed_writer;
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(30, spec);
buffer.render_reserved(Some(30));
for ch in 0..2 {
let chan = buffer.chan_mut(ch);
for sample in chan.iter_mut().take(30) {
*sample = 0.3;
}
}
let packet = Packet::new_from_slice(0, 0, 0, &[]);
let track = Track::new(0, CodecParameters::new().for_codec(CODEC_TYPE_NULL).clone());
let result = AudioDecode::decoded(&mut { writer_ref }, buffer, &packet, &track);
assert!(result.is_ok());
assert_eq!(written_frames.load(std::sync::atomic::Ordering::SeqCst), 30);
}
#[test_log::test]
fn test_ref_mut_dyn_audio_write_audio_decode_flush() {
use moosicbox_audio_decoder::AudioDecode;
let _spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let mock_writer = TrackingMockAudioWrite::new();
let flush_count = mock_writer.flush_count.clone();
let mut boxed_writer: Box<dyn AudioWrite> = Box::new(mock_writer);
let writer_ref: &mut dyn AudioWrite = &mut *boxed_writer;
let result = AudioDecode::flush(&mut { writer_ref });
assert!(result.is_ok());
assert_eq!(flush_count.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[test_log::test]
fn test_box_dyn_audio_write_audio_decode_write_error() {
use moosicbox_audio_decoder::AudioDecode;
use symphonia::core::audio::Signal;
use symphonia::core::codecs::{CODEC_TYPE_NULL, CodecParameters};
use symphonia::core::formats::{Packet, Track};
struct FailingWriter {
handle: AudioHandle,
}
impl AudioWrite for FailingWriter {
fn write(&mut self, _decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
Err(AudioOutputError::StreamClosed)
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
Ok(())
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let (tx, _rx) = flume::bounded(1);
let mock_writer = FailingWriter {
handle: AudioHandle::new(tx),
};
let mut boxed_writer: Box<dyn AudioWrite> = Box::new(mock_writer);
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(10, spec);
buffer.render_reserved(Some(10));
let packet = Packet::new_from_slice(0, 0, 0, &[]);
let track = Track::new(0, CodecParameters::new().for_codec(CODEC_TYPE_NULL).clone());
let result = AudioDecode::decoded(&mut boxed_writer, buffer, &packet, &track);
assert!(result.is_err());
}
#[test_log::test]
fn test_box_dyn_audio_write_audio_decode_flush_error() {
use moosicbox_audio_decoder::AudioDecode;
struct FailingFlushWriter {
handle: AudioHandle,
}
impl AudioWrite for FailingFlushWriter {
fn write(&mut self, decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
Ok(decoded.frames())
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
Err(AudioOutputError::Interrupt)
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
let (tx, _rx) = flume::bounded(1);
let mock_writer = FailingFlushWriter {
handle: AudioHandle::new(tx),
};
let mut boxed_writer: Box<dyn AudioWrite> = Box::new(mock_writer);
let result = AudioDecode::flush(&mut boxed_writer);
assert!(result.is_err());
}
#[test_log::test]
fn test_audio_output_factory_try_into_output_success() {
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let factory = AudioOutputFactory::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
|| Ok(Box::new(MockAudioWrite::new())),
);
let result = factory.try_into_output();
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output.id, "test-id");
assert_eq!(output.name, "Test Output");
assert_eq!(output.spec.rate, 44100);
}
#[test_log::test]
fn test_audio_output_factory_try_from_success() {
let spec = SignalSpec::new(48000, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let factory = AudioOutputFactory::new(
"factory-id".to_string(),
"Factory Output".to_string(),
spec,
|| Ok(Box::new(MockAudioWrite::new())),
);
let result: Result<AudioOutput, _> = factory.try_into();
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output.id, "factory-id");
assert_eq!(output.name, "Factory Output");
assert_eq!(output.spec.rate, 48000);
}
#[test_log::test]
fn test_audio_output_factory_try_from_ref_success() {
let spec = SignalSpec::new(96000, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let factory = AudioOutputFactory::new(
"ref-factory-id".to_string(),
"Ref Factory Output".to_string(),
spec,
|| Ok(Box::new(MockAudioWrite::new())),
);
let result: Result<AudioOutput, _> = (&factory).try_into();
assert!(result.is_ok());
let output = result.unwrap();
assert_eq!(output.id, "ref-factory-id");
assert_eq!(output.name, "Ref Factory Output");
assert_eq!(output.spec.rate, 96000);
}
#[test_log::test]
fn test_audio_output_write_underlying_writer_error() {
use symphonia::core::audio::Signal;
struct FailingWriter {
handle: AudioHandle,
}
impl AudioWrite for FailingWriter {
fn write(&mut self, _decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
Err(AudioOutputError::StreamClosed)
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
Ok(())
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let (tx, _rx) = flume::bounded(1);
let mock_writer = FailingWriter {
handle: AudioHandle::new(tx),
};
let mut output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(10, spec);
buffer.render_reserved(Some(10));
let result = AudioWrite::write(&mut output, buffer);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
AudioOutputError::StreamClosed
));
}
#[test_log::test]
fn test_audio_output_flush_underlying_writer_error() {
struct FailingFlushWriter {
handle: AudioHandle,
}
impl AudioWrite for FailingFlushWriter {
fn write(&mut self, decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
Ok(decoded.frames())
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
Err(AudioOutputError::Interrupt)
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let (tx, _rx) = flume::bounded(1);
let mock_writer = FailingFlushWriter {
handle: AudioHandle::new(tx),
};
let mut output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
let result = AudioWrite::flush(&mut output);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AudioOutputError::Interrupt));
}
#[test_log::test]
fn test_audio_output_audio_decode_decoded_writer_error() {
use moosicbox_audio_decoder::AudioDecode;
use symphonia::core::audio::Signal;
use symphonia::core::codecs::{CODEC_TYPE_NULL, CodecParameters};
use symphonia::core::formats::{Packet, Track};
struct FailingWriter {
handle: AudioHandle,
}
impl AudioWrite for FailingWriter {
fn write(&mut self, _decoded: AudioBuffer<f32>) -> Result<usize, AudioOutputError> {
Err(AudioOutputError::OpenStream)
}
fn flush(&mut self) -> Result<(), AudioOutputError> {
Ok(())
}
fn handle(&self) -> AudioHandle {
self.handle.clone()
}
}
let spec = SignalSpec::new(44100, Channels::FRONT_LEFT | Channels::FRONT_RIGHT);
let (tx, _rx) = flume::bounded(1);
let mock_writer = FailingWriter {
handle: AudioHandle::new(tx),
};
let mut output = AudioOutput::new(
"test-id".to_string(),
"Test Output".to_string(),
spec,
Box::new(mock_writer),
);
let mut buffer: AudioBuffer<f32> = AudioBuffer::new(10, spec);
buffer.render_reserved(Some(10));
let packet = Packet::new_from_slice(0, 0, 0, &[]);
let track = Track::new(0, CodecParameters::new().for_codec(CODEC_TYPE_NULL).clone());
let result = AudioDecode::decoded(&mut output, buffer, &packet, &track);
assert!(result.is_err());
}
}