pipecrab_audio/
resampler.rs1use std::fmt;
4use std::sync::Mutex;
5
6use pipecrab_core::{
7 AudioChunk, AudioFormat, DataFrame, Decision, Direction, Processor, SystemFrame,
8};
9use pipecrab_runtime::{MaybeSend, Outbound, Stage, StageError, maybe_async_trait};
10
11use crate::rubato_sinc::RubatoSincResampler;
12
13pub trait Resampler: MaybeSend {
23 fn output_format(&self) -> AudioFormat;
25
26 fn resample(&mut self, input: &AudioChunk) -> Result<Option<AudioChunk>, ResamplerError>;
28
29 fn reset(&mut self);
31}
32
33pub struct ResamplerStage<R: Resampler = RubatoSincResampler> {
55 output_format: AudioFormat,
56 resampler: Mutex<R>,
60}
61
62impl ResamplerStage<RubatoSincResampler> {
63 pub fn new(output_format: AudioFormat) -> Result<Self, ResamplerError> {
65 Self::with_resampler(RubatoSincResampler::new(output_format)?)
66 }
67}
68
69impl<R: Resampler> ResamplerStage<R> {
70 pub fn with_resampler(resampler: R) -> Result<Self, ResamplerError> {
72 let output_format = resampler.output_format();
73 validate_format(output_format)?;
74 Ok(Self {
75 output_format,
76 resampler: Mutex::new(resampler),
77 })
78 }
79
80 pub fn output_format(&self) -> AudioFormat {
82 self.output_format
83 }
84}
85
86pub struct ResamplerEffect(Result<AudioChunk, ResamplerError>);
90
91impl<R: Resampler> Processor for ResamplerStage<R> {
92 type Effect = ResamplerEffect;
93
94 fn decide_data(&mut self, frame: &DataFrame) -> Decision<Self::Effect> {
95 let DataFrame::Audio(chunk) = frame else {
96 return Decision::forward();
97 };
98 let result = self
99 .resampler
100 .get_mut()
101 .expect("resampler mutex poisoned")
102 .resample(chunk);
103 match result {
104 Ok(Some(chunk)) => Decision::drop().emit(ResamplerEffect(Ok(chunk))),
105 Ok(None) => Decision::drop(),
106 Err(error) => Decision::drop().emit(ResamplerEffect(Err(error))),
107 }
108 }
109
110 fn decide_system(
111 &mut self,
112 _direction: Direction,
113 frame: &SystemFrame,
114 ) -> Decision<Self::Effect> {
115 if matches!(frame, SystemFrame::Interrupt) {
116 self.resampler
117 .get_mut()
118 .expect("resampler mutex poisoned")
119 .reset();
120 }
121 Decision::forward()
122 }
123}
124
125maybe_async_trait! {
126 impl<R: Resampler> Stage for ResamplerStage<R> {
127 async fn perform(
128 &self,
129 ResamplerEffect(result): ResamplerEffect,
130 out: &Outbound,
131 ) -> Result<(), StageError> {
132 let chunk = result.map_err(|error| StageError::fatal(error.to_string()))?;
133 let _ = out.send_data(DataFrame::Audio(chunk)).await;
134 Ok(())
135 }
136 }
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
141pub enum ResamplerError {
142 InvalidFormat {
144 format: AudioFormat,
146 },
147 MisalignedSamples {
149 samples: usize,
151 channels: u16,
153 },
154 Resampling(String),
156}
157
158impl fmt::Display for ResamplerError {
159 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160 match self {
161 Self::InvalidFormat { format } => write!(
162 formatter,
163 "invalid audio format: sample rate and channels must be non-zero, got {} Hz/{} ch",
164 format.sample_rate, format.channels
165 ),
166 Self::MisalignedSamples { samples, channels } => write!(
167 formatter,
168 "audio buffer has {samples} samples, which is not divisible by {channels} channels"
169 ),
170 Self::Resampling(message) => write!(formatter, "audio resampling failed: {message}"),
171 }
172 }
173}
174
175impl std::error::Error for ResamplerError {}
176
177pub(crate) fn validate_format(format: AudioFormat) -> Result<(), ResamplerError> {
178 if format.sample_rate == 0 || format.channels == 0 {
179 Err(ResamplerError::InvalidFormat { format })
180 } else {
181 Ok(())
182 }
183}
184
185pub(crate) fn validate_chunk(chunk: &AudioChunk) -> Result<(), ResamplerError> {
186 validate_format(chunk.format)?;
187 if chunk.samples.len() % usize::from(chunk.format.channels) != 0 {
188 return Err(ResamplerError::MisalignedSamples {
189 samples: chunk.samples.len(),
190 channels: chunk.format.channels,
191 });
192 }
193 Ok(())
194}
195
196#[cfg(test)]
197mod tests {
198 use std::sync::Arc;
199 use std::sync::atomic::{AtomicUsize, Ordering};
200
201 use pipecrab_core::Disposition;
202
203 use super::*;
204
205 struct Gain {
206 output_format: AudioFormat,
207 resets: Arc<AtomicUsize>,
208 }
209
210 impl Resampler for Gain {
211 fn output_format(&self) -> AudioFormat {
212 self.output_format
213 }
214
215 fn resample(&mut self, input: &AudioChunk) -> Result<Option<AudioChunk>, ResamplerError> {
216 let samples: Arc<[f32]> = input.samples.iter().map(|sample| sample * 2.0).collect();
217 Ok(Some(AudioChunk::new(samples, self.output_format)))
218 }
219
220 fn reset(&mut self) {
221 self.resets.fetch_add(1, Ordering::Relaxed);
222 }
223 }
224
225 #[test]
226 fn custom_resampler_drives_the_generic_stage() {
227 let output_format = AudioFormat::new(24_000, 1);
228 let resets = Arc::new(AtomicUsize::new(0));
229 let mut stage = ResamplerStage::with_resampler(Gain {
230 output_format,
231 resets: resets.clone(),
232 })
233 .unwrap();
234 let frame = DataFrame::Audio(AudioChunk::new(
235 Arc::from([0.25, -0.5]),
236 AudioFormat::new(48_000, 1),
237 ));
238
239 let decision = stage.decide_data(&frame);
240 assert_eq!(decision.disposition, Disposition::Drop);
241 let chunk = decision.effects.into_iter().next().unwrap().0.unwrap();
242 assert_eq!(chunk.format, output_format);
243 assert_eq!(&*chunk.samples, &[0.5, -1.0]);
244
245 stage.decide_system(Direction::Down, &SystemFrame::Interrupt);
246 assert_eq!(resets.load(Ordering::Relaxed), 1);
247 }
248
249 #[test]
250 fn rejects_a_custom_resampler_with_an_invalid_output_format() {
251 let result = ResamplerStage::with_resampler(Gain {
252 output_format: AudioFormat::new(0, 1),
253 resets: Arc::new(AtomicUsize::new(0)),
254 });
255 assert!(matches!(result, Err(ResamplerError::InvalidFormat { .. })));
256 }
257}