1use anyhow::{Context, Result};
18use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
19use cpal::{Device, SampleFormat, Stream, StreamConfig, SupportedBufferSize};
20use tracing;
21
22use crate::AudioRequest;
23
24const MAX_PREALLOC_FRAMES: u32 = 8192;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Requested {
38 Unasked,
41 Granted,
43 Refused(u32),
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct StreamFormat {
52 pub sample_rate: u32,
55 pub buffer_size: Option<u32>,
61 pub max_buffer_frames: u32,
64 pub channels: u16,
66 pub sample_rate_request: Requested,
68 pub buffer_size_request: Requested,
70}
71
72impl StreamFormat {
73 #[must_use]
81 pub fn divergence_notice(&self) -> Option<String> {
82 let rate = match self.sample_rate_request {
83 Requested::Refused(asked) => {
84 Some(format!("{}Hz (asked for {asked}Hz)", self.sample_rate))
85 }
86 Requested::Unasked | Requested::Granted => None,
87 };
88 let block = match (self.buffer_size_request, self.buffer_size) {
89 (Requested::Refused(asked), Some(got)) => {
90 Some(format!("{got}-frame blocks (asked for {asked})"))
91 }
92 (Requested::Refused(asked), None) => {
93 Some(format!("blocks of its own choosing (asked for {asked})"))
94 }
95 _ => None,
96 };
97 match (rate, block) {
98 (None, None) => None,
99 (Some(one), None) | (None, Some(one)) => Some(format!("audio device gave {one}")),
100 (Some(rate), Some(block)) => Some(format!("audio device gave {rate} and {block}")),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub(crate) struct BufferChoice {
109 pub request: Option<u32>,
113 pub max_frames: u32,
115 pub status: Requested,
117}
118
119pub(crate) fn resolve_sample_rate(
127 requested: Option<u32>,
128 device_default: u32,
129 offered: &[(u32, u32)],
130) -> (u32, Requested) {
131 let Some(requested) = requested else {
132 return (device_default, Requested::Unasked);
133 };
134 if offered.iter().any(|&(min, max)| (min..=max).contains(&requested)) {
135 (requested, Requested::Granted)
136 } else {
137 (device_default, Requested::Refused(requested))
138 }
139}
140
141pub(crate) fn resolve_buffer_size(
163 requested: Option<u32>,
164 offered: Option<(u32, u32)>,
165 rate_scale: f64,
166) -> BufferChoice {
167 let scale = rate_scale.max(1.0);
169 match offered {
170 Some((min, max)) if min <= max => {
171 let scaled = (f64::from(max) * scale).ceil();
172 let scaled = if scaled >= f64::from(u32::MAX) { u32::MAX } else { scaled as u32 };
173 let Some(requested) = requested else {
174 return BufferChoice {
175 request: None,
176 max_frames: scaled.min(MAX_PREALLOC_FRAMES),
177 status: Requested::Unasked,
178 };
179 };
180 let clamped = requested.clamp(min, max);
181 BufferChoice {
182 request: Some(clamped),
183 max_frames: scaled.clamp(clamped, MAX_PREALLOC_FRAMES.max(clamped)),
184 status: if clamped == requested {
185 Requested::Granted
186 } else {
187 Requested::Refused(requested)
188 },
189 }
190 }
191 _ => BufferChoice {
196 request: None,
197 max_frames: MAX_PREALLOC_FRAMES.max(requested.unwrap_or(0)),
198 status: requested.map_or(Requested::Unasked, Requested::Refused),
199 },
200 }
201}
202
203pub struct CpalBackend {
205 stream: Option<Stream>,
206 device: Device,
209 config: StreamConfig,
210 sample_format: SampleFormat,
211 format: StreamFormat,
212}
213
214impl CpalBackend {
215 pub fn new(request: AudioRequest) -> Result<Self> {
222 let host = cpal::default_host();
223 let device = host
224 .default_output_device()
225 .context("no audio output device found")?;
226
227 let name = device.name().unwrap_or_else(|_| "unknown".into());
228 tracing::info!("Audio device: {name}");
229
230 let default = device.default_output_config()?;
231 let channels = default.channels();
232 let sample_format = default.sample_format();
233
234 let candidates: Vec<_> = device
237 .supported_output_configs()
238 .map(|configs| {
239 configs
240 .filter(|c| c.channels() == channels && c.sample_format() == sample_format)
241 .collect()
242 })
243 .unwrap_or_default();
244
245 let offered_rates: Vec<(u32, u32)> = candidates
246 .iter()
247 .map(|c| (c.min_sample_rate().0, c.max_sample_rate().0))
248 .collect();
249
250 let (sample_rate, sample_rate_request) =
251 resolve_sample_rate(request.sample_rate, default.sample_rate().0, &offered_rates);
252
253 let offered_buffer = candidates
255 .iter()
256 .find(|c| (c.min_sample_rate().0..=c.max_sample_rate().0).contains(&sample_rate))
257 .map_or_else(|| *default.buffer_size(), |c| *c.buffer_size());
258 let offered_buffer = match offered_buffer {
259 SupportedBufferSize::Range { min, max } => Some((min, max)),
260 SupportedBufferSize::Unknown => None,
261 };
262
263 let rate_scale = f64::from(sample_rate) / f64::from(default.sample_rate().0.max(1));
264 let buffer = resolve_buffer_size(request.buffer_size, offered_buffer, rate_scale);
265
266 let config = StreamConfig {
267 channels,
268 sample_rate: cpal::SampleRate(sample_rate),
269 buffer_size: buffer
270 .request
271 .map_or(cpal::BufferSize::Default, cpal::BufferSize::Fixed),
272 };
273
274 let format = StreamFormat {
275 sample_rate,
276 buffer_size: buffer.request,
277 max_buffer_frames: buffer.max_frames,
278 channels,
279 sample_rate_request,
280 buffer_size_request: buffer.status,
281 };
282
283 if let Some(notice) = format.divergence_notice() {
284 tracing::warn!("{notice}");
285 }
286
287 if channels != 2 {
288 tracing::warn!(
293 "Audio device reports {channels} channels; the mixer is stereo and \
294 the output will be wrong"
295 );
296 }
297
298 tracing::info!(
299 "Audio config: {}Hz, {} channels, {:?}, buffer {:?} (max {} frames)",
300 sample_rate,
301 channels,
302 sample_format,
303 config.buffer_size,
304 buffer.max_frames,
305 );
306
307 Ok(Self {
308 stream: None,
309 device,
310 config,
311 sample_format,
312 format,
313 })
314 }
315
316 pub fn start<F>(&mut self, mut callback: F) -> Result<()>
319 where
320 F: FnMut(&mut [f32]) + Send + 'static,
321 {
322 let config = self.config.clone();
323 let scratch_len = (self.format.max_buffer_frames as usize) * (self.format.channels as usize);
324
325 let stream = match self.sample_format {
326 SampleFormat::F32 => self.device.build_output_stream(
327 &config,
328 move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
329 callback(data);
330 },
331 |err| tracing::error!("Audio stream error: {err}"),
332 None,
333 )?,
334 SampleFormat::I16 => {
335 let mut float_buf = vec![0.0f32; scratch_len];
339 self.device.build_output_stream(
340 &config,
341 move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {
342 let n = data.len();
343 if float_buf.len() < n {
344 float_buf.resize(n, 0.0);
345 }
346 let float_buf = &mut float_buf[..n];
347 float_buf.fill(0.0);
348 callback(float_buf);
349 for (out, &inp) in data.iter_mut().zip(float_buf.iter()) {
350 *out = (inp * f32::from(i16::MAX)) as i16;
351 }
352 },
353 |err| tracing::error!("Audio stream error: {err}"),
354 None,
355 )?
356 }
357 format => anyhow::bail!("Unsupported sample format: {format:?}"),
358 };
359
360 stream.play()?;
361 tracing::info!("Audio stream started");
362 self.stream = Some(stream);
363 Ok(())
364 }
365
366 pub fn stop(&mut self) {
367 if let Some(stream) = self.stream.take() {
368 drop(stream);
369 tracing::info!("Audio stream stopped");
370 }
371 }
372
373 pub fn format(&self) -> StreamFormat {
376 self.format
377 }
378
379 pub fn sample_rate(&self) -> u32 {
380 self.format.sample_rate
381 }
382
383 pub fn buffer_size(&self) -> Option<u32> {
385 self.format.buffer_size
386 }
387
388 pub fn max_buffer_frames(&self) -> u32 {
391 self.format.max_buffer_frames
392 }
393
394 pub fn channels(&self) -> u16 {
395 self.format.channels
396 }
397}
398
399impl Drop for CpalBackend {
400 fn drop(&mut self) {
401 self.stop();
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 const OFFERED: [(u32, u32); 4] =
414 [(44100, 44100), (48000, 48000), (88200, 88200), (96000, 96000)];
415 const CORE_AUDIO: Option<(u32, u32)> = Some((15, 4096));
416
417 #[test]
423 fn asking_for_nothing_follows_the_device() {
424 assert_eq!(
425 resolve_sample_rate(None, 48000, &OFFERED),
426 (48000, Requested::Unasked)
427 );
428 }
429
430 #[test]
431 fn a_rate_the_device_offers_is_the_rate_we_get() {
432 assert_eq!(
433 resolve_sample_rate(Some(44100), 48000, &OFFERED),
434 (44100, Requested::Granted)
435 );
436 assert_eq!(
437 resolve_sample_rate(Some(96000), 48000, &OFFERED),
438 (96000, Requested::Granted)
439 );
440 }
441
442 #[test]
445 fn a_rate_the_device_refuses_falls_back_to_the_devices_own() {
446 assert_eq!(
447 resolve_sample_rate(Some(22050), 48000, &OFFERED),
448 (48000, Requested::Refused(22050))
449 );
450 }
451
452 #[test]
453 fn a_device_that_lists_nothing_falls_back_to_its_default() {
454 assert_eq!(
455 resolve_sample_rate(Some(44100), 48000, &[]),
456 (48000, Requested::Refused(44100))
457 );
458 assert_eq!(resolve_sample_rate(None, 48000, &[]), (48000, Requested::Unasked));
459 }
460
461 #[test]
462 fn a_continuous_range_covers_the_rates_inside_it() {
463 let offered = [(8000, 192_000)];
464 assert_eq!(
465 resolve_sample_rate(Some(44100), 48000, &offered),
466 (44100, Requested::Granted)
467 );
468 assert_eq!(
469 resolve_sample_rate(Some(300_000), 48000, &offered),
470 (48000, Requested::Refused(300_000))
471 );
472 }
473
474 #[test]
477 fn asking_for_no_block_size_leaves_the_choice_to_the_device() {
478 let choice = resolve_buffer_size(None, CORE_AUDIO, 1.0);
479 assert_eq!(choice.request, None);
480 assert_eq!(choice.status, Requested::Unasked);
481 assert_eq!(choice.max_frames, 4096);
483 }
484
485 #[test]
486 fn a_block_size_in_range_is_asked_for_by_name() {
487 let choice = resolve_buffer_size(Some(64), CORE_AUDIO, 1.0);
488 assert_eq!(choice.request, Some(64));
489 assert_eq!(choice.status, Requested::Granted);
490 }
491
492 #[test]
493 fn a_block_size_out_of_range_is_clamped_and_reported() {
494 let low = resolve_buffer_size(Some(4), CORE_AUDIO, 1.0);
495 assert_eq!(low.request, Some(15));
496 assert_eq!(low.status, Requested::Refused(4));
497
498 let high = resolve_buffer_size(Some(99_999), CORE_AUDIO, 1.0);
499 assert_eq!(high.request, Some(4096));
500 assert_eq!(high.status, Requested::Refused(99_999));
501 }
502
503 #[test]
504 fn we_pre_allocate_for_the_largest_block_the_device_admits_to() {
505 let choice = resolve_buffer_size(Some(64), CORE_AUDIO, 1.0);
509 assert_eq!(choice.max_frames, 4096);
510 }
511
512 #[test]
516 fn a_rate_above_the_devices_own_widens_the_pre_allocation() {
517 let choice = resolve_buffer_size(Some(64), CORE_AUDIO, 96_000.0 / 48_000.0);
518 assert_eq!(choice.request, Some(64));
519 assert_eq!(choice.status, Requested::Granted);
520 assert_eq!(choice.max_frames, 8192);
521 }
522
523 #[test]
526 fn a_rate_below_the_devices_own_does_not_shrink_the_pre_allocation() {
527 let choice = resolve_buffer_size(Some(64), CORE_AUDIO, 44_100.0 / 48_000.0);
528 assert_eq!(choice.max_frames, 4096);
529 }
530
531 #[test]
532 fn a_device_that_names_no_range_still_gets_a_bounded_pre_allocation() {
533 let unasked = resolve_buffer_size(None, None, 1.0);
534 assert_eq!(unasked.request, None);
535 assert_eq!(unasked.status, Requested::Unasked);
536 assert_eq!(unasked.max_frames, MAX_PREALLOC_FRAMES);
537
538 let asked = resolve_buffer_size(Some(64), None, 1.0);
541 assert_eq!(asked.request, None);
542 assert_eq!(asked.status, Requested::Refused(64));
543 }
544
545 #[test]
546 fn a_preposterous_maximum_does_not_become_a_preposterous_allocation() {
547 let choice = resolve_buffer_size(Some(64), Some((15, u32::MAX)), 4.0);
548 assert_eq!(choice.request, Some(64));
549 assert_eq!(choice.max_frames, MAX_PREALLOC_FRAMES);
550
551 let unasked = resolve_buffer_size(None, Some((15, u32::MAX)), 4.0);
552 assert_eq!(unasked.max_frames, MAX_PREALLOC_FRAMES);
553 }
554
555 #[test]
556 fn a_nonsense_rate_scale_falls_back_to_no_scaling() {
557 for scale in [f64::NAN, 0.0, -1.0, f64::NEG_INFINITY] {
558 let choice = resolve_buffer_size(Some(64), CORE_AUDIO, scale);
559 assert_eq!(choice.max_frames, 4096, "scale {scale}");
560 }
561 }
562
563 #[test]
564 fn the_pre_allocation_is_never_smaller_than_the_block_we_asked_for() {
565 let choice = resolve_buffer_size(Some(16384), Some((16384, 16384)), 1.0);
568 assert_eq!(choice.request, Some(16384));
569 assert!(choice.max_frames >= 16384);
570 }
571
572 #[test]
573 fn a_nonsense_range_is_treated_as_no_range() {
574 let choice = resolve_buffer_size(Some(64), Some((4096, 15)), 1.0);
575 assert_eq!(choice.request, None);
576 assert_eq!(choice.status, Requested::Refused(64));
577 }
578
579 fn format(rate: Requested, block: Requested, pinned: Option<u32>) -> StreamFormat {
582 StreamFormat {
583 sample_rate: 48000,
584 buffer_size: pinned,
585 max_buffer_frames: 4096,
586 channels: 2,
587 sample_rate_request: rate,
588 buffer_size_request: block,
589 }
590 }
591
592 #[test]
596 fn following_the_device_says_nothing() {
597 assert!(format(Requested::Unasked, Requested::Unasked, None)
598 .divergence_notice()
599 .is_none());
600 }
601
602 #[test]
603 fn getting_what_was_asked_for_says_nothing() {
604 assert!(format(Requested::Granted, Requested::Granted, Some(64))
605 .divergence_notice()
606 .is_none());
607 }
608
609 #[test]
610 fn a_refused_rate_is_reported_with_both_numbers() {
611 let notice = format(Requested::Refused(22050), Requested::Granted, Some(64))
612 .divergence_notice()
613 .expect("a silent divergence is the bug");
614 assert!(notice.contains("48000"), "{notice}");
615 assert!(notice.contains("22050"), "{notice}");
616 assert!(!notice.contains("frame"), "no block size was refused: {notice}");
617 }
618
619 #[test]
620 fn a_refused_block_size_is_reported_on_its_own() {
621 let notice = format(Requested::Unasked, Requested::Refused(4), Some(15))
622 .divergence_notice()
623 .expect("a clamp the player did not ask for is news");
624 assert!(notice.contains("15-frame"), "{notice}");
625 assert!(notice.contains("asked for 4"), "{notice}");
626 assert!(!notice.contains("Hz"), "the rate was never asked for: {notice}");
627 }
628
629 #[test]
630 fn two_refusals_are_reported_together() {
631 let notice = format(Requested::Refused(22050), Requested::Refused(4), Some(15))
632 .divergence_notice()
633 .unwrap();
634 assert!(notice.contains("22050") && notice.contains("asked for 4"), "{notice}");
635 }
636
637 #[test]
638 fn a_block_size_refused_outright_still_reads_sensibly() {
639 let notice = format(Requested::Unasked, Requested::Refused(64), None)
640 .divergence_notice()
641 .unwrap();
642 assert!(notice.contains("asked for 64"), "{notice}");
643 }
644}