use std::{
num::{NonZeroU32, NonZeroUsize},
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
};
use bon::Builder;
use kithara_bufpool::{BytePool, PcmPool};
use kithara_decode::{DecoderBackend, GaplessMode, PcmSpec};
use kithara_events::EventBus;
use kithara_stream::StreamType;
use portable_atomic::AtomicF32;
use crate::{
resampler::{ResamplerParams, ResamplerProcessor, ResamplerQuality},
traits::AudioEffect,
worker::handle,
};
#[derive(Builder)]
#[builder(state_mod(vis = "pub"))]
#[non_exhaustive]
pub struct AudioConfig<T: StreamType> {
pub stream: T::Config,
#[builder(default)]
pub decoder_backend: DecoderBackend,
#[builder(default)]
pub gapless_mode: GaplessMode,
#[builder(default = NonZeroUsize::new(3).expect("3 is non-zero"))]
pub preload_chunks: NonZeroUsize,
#[builder(name = events)]
pub bus: Option<EventBus>,
pub byte_pool: Option<BytePool>,
pub cancel: Option<tokio_util::sync::CancellationToken>,
pub hint: Option<String>,
pub host_sample_rate: Option<NonZeroU32>,
pub media_info: Option<kithara_stream::MediaInfo>,
pub pcm_pool: Option<PcmPool>,
pub playback_rate: Option<Arc<AtomicF32>>,
pub worker: Option<handle::AudioWorkerHandle>,
#[builder(default)]
pub resampler_quality: ResamplerQuality,
#[builder(default)]
pub effects: Vec<Box<dyn AudioEffect>>,
#[builder(default = default_pcm_buffer_chunks())]
pub pcm_buffer_chunks: usize,
}
#[cfg(not(target_arch = "wasm32"))]
const fn default_pcm_buffer_chunks() -> usize {
10
}
#[cfg(target_arch = "wasm32")]
const fn default_pcm_buffer_chunks() -> usize {
32
}
impl<T: StreamType> AudioConfig<T> {
pub fn new(stream: T::Config) -> Self {
Self::for_stream(stream).build()
}
pub fn for_stream(stream: T::Config) -> AudioConfigBuilder<T, audio_config_builder::SetStream> {
Self::builder().stream(stream)
}
}
pub(crate) fn expected_output_spec(
initial_spec: PcmSpec,
host_sample_rate: &Arc<AtomicU32>,
) -> PcmSpec {
let host_sr = host_sample_rate.load(Ordering::Relaxed);
if host_sr == 0 || host_sr == initial_spec.sample_rate {
initial_spec
} else {
PcmSpec {
channels: initial_spec.channels,
sample_rate: host_sr,
}
}
}
pub(crate) fn create_effects(
initial_spec: PcmSpec,
host_sample_rate: &Arc<AtomicU32>,
playback_rate: &Arc<AtomicF32>,
quality: ResamplerQuality,
pool: Option<PcmPool>,
custom_effects: Vec<Box<dyn AudioEffect>>,
) -> Vec<Box<dyn AudioEffect>> {
let params = ResamplerParams::builder()
.host_sample_rate(Arc::clone(host_sample_rate))
.source_sample_rate(initial_spec.sample_rate)
.channels(initial_spec.channels as usize)
.playback_rate(Arc::clone(playback_rate))
.quality(quality)
.maybe_pool(pool)
.build();
let mut chain: Vec<Box<dyn AudioEffect>> = vec![Box::new(ResamplerProcessor::new(params))];
chain.extend(custom_effects);
chain
}
#[cfg(test)]
mod tests {
use kithara_decode::PcmChunk;
#[cfg(not(target_arch = "wasm32"))]
use kithara_file::FileConfig;
use kithara_test_utils::kithara;
use super::*;
use crate::traits::AudioEffect;
struct PassthroughEffect;
impl AudioEffect for PassthroughEffect {
fn flush(&mut self) -> Option<PcmChunk> {
None
}
fn process(&mut self, chunk: PcmChunk) -> Option<PcmChunk> {
Some(chunk)
}
fn reset(&mut self) {}
}
#[cfg(not(target_arch = "wasm32"))]
#[kithara::test]
fn audio_config_with_effect_adds_to_chain() {
let effects: Vec<Box<dyn AudioEffect>> =
vec![Box::new(PassthroughEffect), Box::new(PassthroughEffect)];
let config = AudioConfig::<kithara_file::File>::for_stream(FileConfig::default())
.effects(effects)
.build();
assert_eq!(config.effects.len(), 2);
}
#[kithara::test]
fn create_effects_includes_custom_effects() {
let host_sr = Arc::new(AtomicU32::new(44100));
let playback_rate = Arc::new(AtomicF32::new(1.0));
let effects = create_effects(
PcmSpec {
sample_rate: 44100,
channels: 2,
},
&host_sr,
&playback_rate,
ResamplerQuality::default(),
None,
vec![Box::new(PassthroughEffect)],
);
assert_eq!(effects.len(), 2);
}
}