Skip to main content

max / audiofiles

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