kithara_audio/pipeline/
config.rs1use 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#[derive(Builder)]
27#[builder(state_mod(vis = "pub"))]
28#[non_exhaustive]
29pub struct AudioConfig<T: StreamType> {
30 pub stream: T::Config,
32 #[builder(default)]
34 pub decoder_backend: DecoderBackend,
35 #[builder(default)]
37 pub gapless_mode: GaplessMode,
38 #[builder(default = NonZeroUsize::new(3).expect("3 is non-zero"))]
40 pub preload_chunks: NonZeroUsize,
41 #[builder(name = events)]
43 pub bus: Option<EventBus>,
44 pub byte_pool: Option<BytePool>,
46 pub cancel: Option<tokio_util::sync::CancellationToken>,
48 pub hint: Option<String>,
50 pub host_sample_rate: Option<NonZeroU32>,
52 pub media_info: Option<kithara_stream::MediaInfo>,
54 pub pcm_pool: Option<PcmPool>,
56 pub playback_rate: Option<Arc<AtomicF32>>,
58 pub worker: Option<handle::AudioWorkerHandle>,
60 #[builder(default)]
62 pub resampler_quality: ResamplerQuality,
63 #[builder(default)]
65 pub effects: Vec<Box<dyn AudioEffect>>,
66 #[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 pub fn new(stream: T::Config) -> Self {
85 Self::for_stream(stream).build()
86 }
87
88 pub fn for_stream(stream: T::Config) -> AudioConfigBuilder<T, audio_config_builder::SetStream> {
91 Self::builder().stream(stream)
92 }
93}
94
95pub(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
111pub(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}