Skip to main content

max / audiofiles

12.4 KB · 407 lines History Blame Raw
1 //! cpal audio output stream: reads from shared preview and instrument playback state.
2
3 use std::sync::Arc;
4
5 use audiofiles_browser::instrument::render_voices;
6 use audiofiles_browser::preview::PreviewPlayback;
7 use audiofiles_browser::state::SharedState;
8 use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
9 use cpal::Stream;
10 use parking_lot::Mutex;
11 use tracing::instrument;
12 use thiserror::Error;
13
14 /// Errors from audio output stream setup.
15 #[derive(Error, Debug)]
16 pub enum AudioError {
17 #[error("no output audio device found")]
18 NoDevice,
19 #[error("default output config: {0}")]
20 DefaultConfig(#[from] cpal::DefaultStreamConfigError),
21 #[error("unsupported sample format: {0:?}")]
22 UnsupportedFormat(cpal::SampleFormat),
23 #[error("build stream: {0}")]
24 BuildStream(#[from] cpal::BuildStreamError),
25 #[error("stream play: {0}")]
26 Play(#[from] cpal::PlayStreamError),
27 }
28
29 /// Build and start a cpal output stream that reads from the shared preview state.
30 /// Returns `(stream, device_sample_rate, device_name)` — the stream handle must
31 /// be kept alive. `device_name` is whatever cpal reports for the default
32 /// output device; it's surfaced in the footer for diagnostic visibility.
33 #[instrument(skip_all)]
34 pub fn start_output_stream(shared: Arc<SharedState>) -> Result<(Stream, u32, String), AudioError> {
35 let host = cpal::default_host();
36 let device = host
37 .default_output_device()
38 .ok_or(AudioError::NoDevice)?;
39
40 let device_name = device.name().unwrap_or_else(|_| "default".to_string());
41 let config = device.default_output_config()?;
42
43 let channels = config.channels() as usize;
44 let device_sample_rate = config.sample_rate().0;
45
46 let stream = match config.sample_format() {
47 cpal::SampleFormat::F32 => build_stream::<f32>(
48 &device,
49 &config.into(),
50 shared,
51 channels,
52 device_sample_rate,
53 ),
54 cpal::SampleFormat::I16 => build_stream::<i16>(
55 &device,
56 &config.into(),
57 shared,
58 channels,
59 device_sample_rate,
60 ),
61 cpal::SampleFormat::U16 => build_stream::<u16>(
62 &device,
63 &config.into(),
64 shared,
65 channels,
66 device_sample_rate,
67 ),
68 fmt => Err(AudioError::UnsupportedFormat(fmt)),
69 }?;
70
71 stream.play()?;
72 Ok((stream, device_sample_rate, device_name))
73 }
74
75 fn build_stream<T: cpal::SizedSample + cpal::FromSample<f32>>(
76 device: &cpal::Device,
77 config: &cpal::StreamConfig,
78 shared: Arc<SharedState>,
79 channels: usize,
80 device_sample_rate: u32,
81 ) -> Result<Stream, AudioError> {
82 let mut mix_buf: Vec<f32> = Vec::new();
83
84 let stream = device
85 .build_output_stream(
86 config,
87 move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
88 let num_samples = data.len();
89
90 // Resize mix buffer if needed (no per-callback allocation after first call)
91 if mix_buf.len() < num_samples {
92 mix_buf.resize(num_samples, 0.0);
93 }
94 let buf = &mut mix_buf[..num_samples];
95
96 // Zero the mix buffer
97 for s in buf.iter_mut() {
98 *s = 0.0;
99 }
100
101 // Fill preview audio
102 fill_preview(&shared.preview, buf, channels, device_sample_rate);
103
104 // Fill instrument audio (additive)
105 if let Some(mut inst) = shared.instrument.try_lock() {
106 render_voices(&mut inst, buf, channels, device_sample_rate);
107 }
108
109 // Convert f32 mix → output format with clamp
110 for (out, &mix) in data.iter_mut().zip(buf.iter()) {
111 *out = T::from_sample(mix.clamp(-1.0, 1.0));
112 }
113 },
114 |err| {
115 tracing::error!("audio stream error: {err}");
116 },
117 None,
118 )?;
119 Ok(stream)
120 }
121
122 /// Fill an f32 buffer from the preview playback state.
123 ///
124 /// Uses fractional position advancement (`file_rate / device_rate` per output frame)
125 /// with linear interpolation for correct-speed playback at any sample rate.
126 pub(crate) fn fill_preview(
127 playback: &Mutex<PreviewPlayback>,
128 buf: &mut [f32],
129 channels: usize,
130 device_sample_rate: u32,
131 ) {
132 let Some(mut guard) = playback.try_lock() else {
133 return; // GUI thread holds lock — leave buffer unchanged (already zeroed)
134 };
135
136 if !guard.playing || device_sample_rate == 0 {
137 return;
138 }
139
140 let Some(ref preview_buf) = guard.buffer else {
141 return;
142 };
143
144 // For streaming, use the smaller of decoded_frames and actual data length
145 // to prevent OOB if decoded_frames is updated before data is fully appended.
146 let total_frames = if guard.streaming {
147 guard.decoded_frames.min(preview_buf.data.len() / 2)
148 } else {
149 preview_buf.data.len() / 2
150 };
151 let rate_ratio = preview_buf.sample_rate as f64 / device_sample_rate as f64;
152 let loop_enabled = guard.loop_enabled;
153 let mut pos_frac = guard.position_frac;
154 let num_frames = buf.len() / channels;
155
156 for frame in 0..num_frames {
157 let pos_int = pos_frac as usize;
158
159 if pos_int >= total_frames {
160 if guard.streaming {
161 // Still decoding — stop filling but keep playing
162 guard.position_frac = pos_frac;
163 return;
164 }
165 if loop_enabled && total_frames > 0 {
166 pos_frac %= total_frames as f64;
167 } else {
168 guard.playing = false;
169 guard.position_frac = 0.0;
170 return;
171 }
172 }
173
174 let pos_int = pos_frac as usize;
175 let frac = (pos_frac - pos_int as f64) as f32;
176
177 let l0 = preview_buf.data[pos_int * 2];
178 let r0 = preview_buf.data[pos_int * 2 + 1];
179
180 let next = (pos_int + 1).min(total_frames.saturating_sub(1));
181 let l1 = preview_buf.data[next * 2];
182 let r1 = preview_buf.data[next * 2 + 1];
183
184 let left = l0 + (l1 - l0) * frac;
185 let right = r0 + (r1 - r0) * frac;
186
187 let base = frame * channels;
188 if channels >= 2 {
189 buf[base] += left;
190 buf[base + 1] += right;
191 // Extra channels stay zero (already zeroed)
192 } else if channels == 1 {
193 buf[base] += (left + right) * 0.5;
194 }
195
196 pos_frac += rate_ratio;
197 }
198
199 guard.position_frac = pos_frac;
200 }
201
202 /// Fill a typed cpal output buffer from the preview playback state (test helper).
203 #[cfg(test)]
204 fn fill_cpal_output<T: cpal::SizedSample + cpal::FromSample<f32>>(
205 playback: &Mutex<PreviewPlayback>,
206 data: &mut [T],
207 channels: usize,
208 device_sample_rate: u32,
209 ) {
210 let mut buf = vec![0.0f32; data.len()];
211 fill_preview(playback, &mut buf, channels, device_sample_rate);
212 for (out, &mix) in data.iter_mut().zip(buf.iter()) {
213 *out = T::from_sample(mix);
214 }
215
216 // For backward compat: if preview stopped, silence the rest.
217 // fill_preview will have left the tail at 0.0 already, which converts to silence.
218 }
219
220 #[cfg(test)]
221 mod tests {
222 use super::*;
223 use audiofiles_browser::preview::PreviewBuffer;
224
225 fn make_playback(data: Vec<f32>, sample_rate: u32, playing: bool) -> Mutex<PreviewPlayback> {
226 Mutex::new(PreviewPlayback {
227 buffer: Some(PreviewBuffer {
228 data,
229 channels: 2,
230 sample_rate,
231 }),
232 position_frac: 0.0,
233 playing,
234 loop_enabled: false,
235 streaming: false,
236 decoded_frames: 0,
237 total_frames_estimate: None,
238 })
239 }
240
241 #[test]
242 fn fill_cpal_stereo_f32() {
243 let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6], 44100, true);
244 let mut data = vec![0.0f32; 6];
245
246 fill_cpal_output(&playback, &mut data, 2, 44100);
247
248 assert_eq!(data, vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]);
249 }
250
251 #[test]
252 fn fill_cpal_mono_f32() {
253 let playback = make_playback(vec![0.4, 0.6, 0.2, 0.8], 44100, true);
254 let mut data = vec![0.0f32; 2];
255
256 fill_cpal_output(&playback, &mut data, 1, 44100);
257
258 assert_eq!(data[0], (0.4 + 0.6) * 0.5);
259 assert_eq!(data[1], (0.2 + 0.8) * 0.5);
260 }
261
262 #[test]
263 fn fill_cpal_stops_at_end() {
264 let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 44100, true);
265 let mut data = vec![9.0f32; 8];
266
267 fill_cpal_output(&playback, &mut data, 2, 44100);
268
269 assert_eq!(data[0], 0.1);
270 assert_eq!(data[1], 0.2);
271 assert_eq!(data[2], 0.3);
272 assert_eq!(data[3], 0.4);
273 assert_eq!(data[4], 0.0);
274 assert_eq!(data[5], 0.0);
275 assert_eq!(data[6], 0.0);
276 assert_eq!(data[7], 0.0);
277
278 let guard = playback.lock();
279 assert!(!guard.playing);
280 assert_eq!(guard.position_frac, 0.0);
281 }
282
283 #[test]
284 fn fill_cpal_not_playing_outputs_silence() {
285 let playback = make_playback(vec![0.5, 0.5], 44100, false);
286 let mut data = vec![1.0f32; 4];
287
288 fill_cpal_output(&playback, &mut data, 2, 44100);
289
290 assert!(data.iter().all(|&s| s == 0.0));
291 }
292
293 #[test]
294 fn fill_cpal_same_rate_unchanged() {
295 let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 48000, true);
296 let mut data = vec![0.0f32; 4];
297
298 fill_cpal_output(&playback, &mut data, 2, 48000);
299
300 assert_eq!(data, vec![0.1, 0.2, 0.3, 0.4]);
301 let guard = playback.lock();
302 assert!((guard.position_frac - 2.0).abs() < 1e-10);
303 }
304
305 #[test]
306 fn fill_cpal_resamples_96k_to_48k() {
307 let playback = make_playback(
308 vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5],
309 96000, true,
310 );
311 let mut data = vec![0.0f32; 4];
312
313 fill_cpal_output(&playback, &mut data, 2, 48000);
314
315 assert!((data[0] - 0.0).abs() < 1e-6);
316 assert!((data[1] - 0.0).abs() < 1e-6);
317 assert!((data[2] - 1.0).abs() < 1e-6);
318 assert!((data[3] - 1.0).abs() < 1e-6);
319
320 let guard = playback.lock();
321 assert!((guard.position_frac - 4.0).abs() < 1e-10);
322 }
323
324 #[test]
325 fn fill_cpal_loop_wraps() {
326 let playback = Mutex::new(PreviewPlayback {
327 buffer: Some(PreviewBuffer {
328 data: vec![0.1, 0.2, 0.3, 0.4],
329 channels: 2,
330 sample_rate: 44100,
331 }),
332 position_frac: 0.0,
333 playing: true,
334 loop_enabled: true,
335 streaming: false,
336 decoded_frames: 0,
337 total_frames_estimate: None,
338 });
339 let mut data = vec![0.0f32; 8];
340
341 fill_cpal_output(&playback, &mut data, 2, 44100);
342
343 assert_eq!(data[0], 0.1);
344 assert_eq!(data[1], 0.2);
345 assert_eq!(data[2], 0.3);
346 assert_eq!(data[3], 0.4);
347 assert_eq!(data[4], 0.1);
348 assert_eq!(data[5], 0.2);
349 assert_eq!(data[6], 0.3);
350 assert_eq!(data[7], 0.4);
351
352 let guard = playback.lock();
353 assert!(guard.playing);
354 }
355
356 #[test]
357 fn fill_cpal_loop_disabled_stops() {
358 let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 44100, true);
359 let mut data = vec![9.0f32; 8];
360
361 fill_cpal_output(&playback, &mut data, 2, 44100);
362
363 assert_eq!(data[0], 0.1);
364 assert_eq!(data[4], 0.0);
365
366 let guard = playback.lock();
367 assert!(!guard.playing);
368 }
369
370 #[test]
371 fn audio_error_display() {
372 let variants: Vec<Box<dyn std::fmt::Display>> = vec![
373 Box::new(AudioError::NoDevice),
374 Box::new(AudioError::UnsupportedFormat(cpal::SampleFormat::U32)),
375 ];
376 for err in &variants {
377 assert!(!err.to_string().is_empty());
378 }
379 }
380
381 #[test]
382 fn fill_preview_additive() {
383 let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 44100, true);
384 let mut buf = vec![0.5f32; 4];
385
386 fill_preview(&playback, &mut buf, 2, 44100);
387
388 // 0.5 + 0.1 = 0.6, etc.
389 assert!((buf[0] - 0.6).abs() < 1e-6);
390 assert!((buf[1] - 0.7).abs() < 1e-6);
391 }
392
393 #[test]
394 fn clamp_prevents_overflow() {
395 // Simulate very loud mixed audio
396 let buf = vec![1.5f32, -1.5, 0.5, -0.5];
397 let mut data = vec![0.0f32; 4];
398 for (out, &mix) in data.iter_mut().zip(buf.iter()) {
399 *out = mix.clamp(-1.0, 1.0);
400 }
401 assert_eq!(data[0], 1.0);
402 assert_eq!(data[1], -1.0);
403 assert_eq!(data[2], 0.5);
404 assert_eq!(data[3], -0.5);
405 }
406 }
407