Skip to main content

kithara_audio/pipeline/
config.rs

1use std::{
2    num::{NonZeroU32, NonZeroUsize},
3    sync::{
4        Arc,
5        atomic::{AtomicU32, Ordering},
6    },
7};
8
9use bon::Builder;
10use kithara_bufpool::{BytePool, PcmPool};
11use kithara_decode::{DecoderBackend, GaplessMode, PcmSpec};
12use kithara_events::EventBus;
13use kithara_stream::StreamType;
14use portable_atomic::AtomicF32;
15
16use crate::{
17    resampler::{ResamplerParams, ResamplerProcessor, ResamplerQuality},
18    traits::AudioEffect,
19    worker::handle,
20};
21
22/// Configuration for audio pipeline with stream config.
23///
24/// Generic over `StreamType` to include stream-specific configuration.
25/// Combines stream config and audio pipeline settings into a single builder.
26#[derive(Builder)]
27#[builder(state_mod(vis = "pub"))]
28#[non_exhaustive]
29pub struct AudioConfig<T: StreamType> {
30    /// Stream configuration (`HlsConfig`, `FileConfig`, etc.)
31    pub stream: T::Config,
32    /// Decoder backend selection. See [`DecoderBackend`].
33    #[builder(default)]
34    pub decoder_backend: DecoderBackend,
35    /// How leading/trailing PCM is trimmed after the decode.
36    #[builder(default)]
37    pub gapless_mode: GaplessMode,
38    /// Number of chunks to buffer before signaling preload readiness.
39    #[builder(default = NonZeroUsize::new(3).expect("3 is non-zero"))]
40    pub preload_chunks: NonZeroUsize,
41    /// Unified event bus (optional — if not provided, one is created internally).
42    #[builder(name = events)]
43    pub bus: Option<EventBus>,
44    /// Shared byte pool for temporary buffers (probe, etc.).
45    pub byte_pool: Option<BytePool>,
46    /// Master cancel token for the audio pipeline.
47    pub cancel: Option<tokio_util::sync::CancellationToken>,
48    /// Optional format hint (file extension like "mp3", "wav")
49    pub hint: Option<String>,
50    /// Target sample rate of the audio host (for resampling).
51    pub host_sample_rate: Option<NonZeroU32>,
52    /// Media info hint for format detection
53    pub media_info: Option<kithara_stream::MediaInfo>,
54    /// Shared PCM pool for temporary buffers.
55    pub pcm_pool: Option<PcmPool>,
56    /// Shared atomic for dynamic playback rate (1.0 = normal speed).
57    pub playback_rate: Option<Arc<AtomicF32>>,
58    /// Optional shared audio worker handle.
59    pub worker: Option<handle::AudioWorkerHandle>,
60    /// Resampling quality preset.
61    #[builder(default)]
62    pub resampler_quality: ResamplerQuality,
63    /// Additional effects to append after resampler in the processing chain.
64    #[builder(default)]
65    pub effects: Vec<Box<dyn AudioEffect>>,
66    /// PCM buffer size in chunks (~100ms per chunk = 10 chunks ≈ 1s).
67    /// Default: 10 on native, 32 on wasm32.
68    #[builder(default = default_pcm_buffer_chunks())]
69    pub pcm_buffer_chunks: usize,
70}
71
72#[cfg(not(target_arch = "wasm32"))]
73const fn default_pcm_buffer_chunks() -> usize {
74    10
75}
76
77#[cfg(target_arch = "wasm32")]
78const fn default_pcm_buffer_chunks() -> usize {
79    32
80}
81
82impl<T: StreamType> AudioConfig<T> {
83    /// Create config with stream config and default audio settings.
84    pub fn new(stream: T::Config) -> Self {
85        Self::for_stream(stream).build()
86    }
87
88    /// Chainable counterpart to [`AudioConfig::new`]: returns a builder
89    /// with `stream` set so callers can attach further setters.
90    pub fn for_stream(stream: T::Config) -> AudioConfigBuilder<T, audio_config_builder::SetStream> {
91        Self::builder().stream(stream)
92    }
93}
94
95/// Compute expected output spec after effects (primarily resampling).
96pub(crate) fn expected_output_spec(
97    initial_spec: PcmSpec,
98    host_sample_rate: &Arc<AtomicU32>,
99) -> PcmSpec {
100    let host_sr = host_sample_rate.load(Ordering::Relaxed);
101    if host_sr == 0 || host_sr == initial_spec.sample_rate {
102        initial_spec
103    } else {
104        PcmSpec {
105            channels: initial_spec.channels,
106            sample_rate: host_sr,
107        }
108    }
109}
110
111/// Create effects chain for audio pipeline.
112pub(crate) fn create_effects(
113    initial_spec: PcmSpec,
114    host_sample_rate: &Arc<AtomicU32>,
115    playback_rate: &Arc<AtomicF32>,
116    quality: ResamplerQuality,
117    pool: Option<PcmPool>,
118    custom_effects: Vec<Box<dyn AudioEffect>>,
119) -> Vec<Box<dyn AudioEffect>> {
120    let params = ResamplerParams::builder()
121        .host_sample_rate(Arc::clone(host_sample_rate))
122        .source_sample_rate(initial_spec.sample_rate)
123        .channels(initial_spec.channels as usize)
124        .playback_rate(Arc::clone(playback_rate))
125        .quality(quality)
126        .maybe_pool(pool)
127        .build();
128
129    let mut chain: Vec<Box<dyn AudioEffect>> = vec![Box::new(ResamplerProcessor::new(params))];
130    chain.extend(custom_effects);
131    chain
132}
133
134#[cfg(test)]
135mod tests {
136    use kithara_decode::PcmChunk;
137    #[cfg(not(target_arch = "wasm32"))]
138    use kithara_file::FileConfig;
139    use kithara_test_utils::kithara;
140
141    use super::*;
142    use crate::traits::AudioEffect;
143
144    struct PassthroughEffect;
145
146    impl AudioEffect for PassthroughEffect {
147        fn flush(&mut self) -> Option<PcmChunk> {
148            None
149        }
150        fn process(&mut self, chunk: PcmChunk) -> Option<PcmChunk> {
151            Some(chunk)
152        }
153        fn reset(&mut self) {}
154    }
155
156    #[cfg(not(target_arch = "wasm32"))]
157    #[kithara::test]
158    fn audio_config_with_effect_adds_to_chain() {
159        let effects: Vec<Box<dyn AudioEffect>> =
160            vec![Box::new(PassthroughEffect), Box::new(PassthroughEffect)];
161        let config = AudioConfig::<kithara_file::File>::for_stream(FileConfig::default())
162            .effects(effects)
163            .build();
164        assert_eq!(config.effects.len(), 2);
165    }
166
167    #[kithara::test]
168    fn create_effects_includes_custom_effects() {
169        let host_sr = Arc::new(AtomicU32::new(44100));
170        let playback_rate = Arc::new(AtomicF32::new(1.0));
171        let effects = create_effects(
172            PcmSpec {
173                sample_rate: 44100,
174                channels: 2,
175            },
176            &host_sr,
177            &playback_rate,
178            ResamplerQuality::default(),
179            None,
180            vec![Box::new(PassthroughEffect)],
181        );
182        assert_eq!(effects.len(), 2);
183    }
184}