Skip to main content

phosphor_core/
cpal_backend.rs

1//! Real audio output via cpal.
2//!
3//! Creates a high-priority audio thread that calls our callback
4//! each buffer cycle. This is the production audio path.
5//!
6//! The device has the last word on sample rate and block size, and whatever
7//! it grants is what the engine must be built from — an engine synthesising
8//! at 44100 into a stream running at 48000 plays every note 1.47 semitones
9//! sharp and runs the transport 8.8% fast. So the format is resolved here,
10//! once, before anything downstream is constructed, and
11//! [`CpalBackend::format`] reports it.
12//!
13//! Nothing is requested unless it was asked for. On CoreAudio, pinning a
14//! sample rate changes the machine's nominal rate for every other
15//! application as well, and launching a DAW is not consent to that.
16
17use anyhow::{Context, Result};
18use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
19use cpal::{Device, SampleFormat, Stream, StreamConfig, SupportedBufferSize};
20use tracing;
21
22use crate::AudioRequest;
23
24/// The largest block we will pre-allocate for, however large a device claims
25/// its blocks may get. A device that then hands the callback more than this
26/// gets its buffers grown once and never again; a device that reports a
27/// preposterous maximum does not get to reserve a preposterous amount of
28/// memory up front.
29const MAX_PREALLOC_FRAMES: u32 = 8192;
30
31/// What became of one of the two things the command line can ask for.
32///
33/// Three states rather than a bool, because "nothing was asked for" and "what
34/// was asked for was granted" are both silent but are not the same event, and
35/// only the third has anything to tell the player.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Requested {
38    /// Nothing was asked for, so the device's own setting stands. The default,
39    /// and the only outcome that touches no shared state.
40    Unasked,
41    /// What was asked for is what the stream runs at.
42    Granted,
43    /// The device would not take it. Carries the value that was asked for, so
44    /// the divergence can be reported without the caller having to hold on to
45    /// the original request.
46    Refused(u32),
47}
48
49/// The stream format the device agreed to, and what became of the request.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct StreamFormat {
52    /// The rate the stream actually runs at. Every oscillator increment,
53    /// envelope time and transport advance must be derived from this number.
54    pub sample_rate: u32,
55    /// The block size pinned on the stream, or `None` when the device was
56    /// left to choose. Nominal even when it is `Some`: what the callback is
57    /// actually handed varies from block to block and is scaled by any rate
58    /// conversion in between. Use it for reporting latency, never for sizing
59    /// a buffer.
60    pub buffer_size: Option<u32>,
61    /// The largest block the callback can be handed. Audio-thread buffers are
62    /// sized from this so `process()` never has to grow one.
63    pub max_buffer_frames: u32,
64    /// Output channel count.
65    pub channels: u16,
66    /// What became of the sample rate on the command line.
67    pub sample_rate_request: Requested,
68    /// What became of the block size on the command line.
69    pub buffer_size_request: Requested,
70}
71
72impl StreamFormat {
73    /// What the player needs to be told, or `None` when there is nothing to
74    /// tell them.
75    ///
76    /// Following the device is the ordinary case and says nothing. Only a
77    /// refusal is news: they asked for something and are not getting it, and
78    /// a session that is silently a semitone and a half sharp is exactly the
79    /// failure this whole path exists to prevent.
80    #[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/// What to ask the device for, given a requested block size and the range it
106/// offers.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub(crate) struct BufferChoice {
109    /// Frames to pin the stream to, or `None` to leave the choice to the
110    /// device — either because nothing was asked for, or because the device
111    /// would not name a range to choose from.
112    pub request: Option<u32>,
113    /// The largest block to pre-allocate for.
114    pub max_frames: u32,
115    /// What became of the request.
116    pub status: Requested,
117}
118
119/// Pick a sample rate: the device's own unless one was asked for, then the
120/// one asked for if any offered range covers it.
121///
122/// `offered` is a list of inclusive `(min, max)` rate ranges, already filtered
123/// to the channel count and sample format the stream will use — chasing a rate
124/// into a different format would break the rest of the pipeline, which is
125/// written for interleaved stereo f32.
126pub(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
141/// Pick a block size from the range the device offers.
142///
143/// With nothing asked for, the device is left to choose and we only work out
144/// how much room its choice could need. With something asked for,
145/// `BufferSize::Default` is not a neutral answer — it means the requested size
146/// is never asked for and the size we get is never known — so a concrete
147/// number goes on the stream whenever the device names a range.
148///
149/// The number asked for is not the number that arrives. Two separate reasons,
150/// both measured against CoreAudio rather than assumed:
151///
152/// * The count is only approximate. Asking a 48000 device for 44100 at 64
153///   frames delivers blocks that alternate between 58 and 59.
154/// * The device counts in its own frames. Asking that same device for 96000
155///   at 64 frames delivers blocks of 128, because each device frame becomes
156///   two after conversion. `rate_scale` — the rate we settled on over the
157///   device's own — is what corrects for that.
158///
159/// Which is why `max_frames` is a headroom figure and not the requested size:
160/// it is what buffers get allocated to so that no block, whatever its actual
161/// length, makes the audio thread call the allocator.
162pub(crate) fn resolve_buffer_size(
163    requested: Option<u32>,
164    offered: Option<(u32, u32)>,
165    rate_scale: f64,
166) -> BufferChoice {
167    // NaN loses against 1.0 here, which is the fallback we want anyway.
168    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        // Either the device would not name a range, or it named a nonsense
192        // one. Nothing can be pinned to it, so pre-allocate for the worst
193        // case we are prepared to absorb — and if a size was asked for, it
194        // has been refused, whatever the device goes on to deliver.
195        _ => 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
203/// Real audio backend using cpal.
204pub struct CpalBackend {
205    stream: Option<Stream>,
206    /// Kept so `start()` opens the stream on the same device the format was
207    /// resolved against, rather than re-querying and possibly diverging.
208    device: Device,
209    config: StreamConfig,
210    sample_format: SampleFormat,
211    format: StreamFormat,
212}
213
214impl CpalBackend {
215    /// Resolve the output format against the default device. Does NOT start
216    /// the stream yet.
217    ///
218    /// Every field of `request` is a request and none of them is required.
219    /// Read [`CpalBackend::format`] afterwards to find out what the device
220    /// settled on, and build the engine from that.
221    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        // Only ranges matching the default channel count and sample format are
235        // candidates; see `resolve_sample_rate`.
236        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        // The block-size range that goes with the rate we settled on.
254        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            // Everything past this point — `Mixer::process`, `EngineAudio` —
289            // is written for interleaved stereo and divides the block length
290            // by two to get the frame count. A device with any other channel
291            // count is not handled, only reported.
292            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    /// Start the audio stream, calling `callback` for each buffer.
317    /// The callback receives an interleaved f32 buffer: [L, R, L, R, ...]
318    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                // Allocated here, not per callback: the conversion buffer used
336                // to be a `vec!` inside the closure, which is a heap
337                // allocation on the audio thread every single block.
338                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    /// The format the device granted. Build the engine from this, not from
374    /// what was requested.
375    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    /// The block size pinned on the stream, `None` when the device chose.
384    pub fn buffer_size(&self) -> Option<u32> {
385        self.format.buffer_size
386    }
387
388    /// The largest block the callback can be handed — the size audio-thread
389    /// buffers must be pre-allocated to.
390    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    // These cover the decision, not the driver: no sound card is involved.
410
411    /// The device this was developed against: MacBook Pro Speakers, sitting at
412    /// 48000 Hz, offering four discrete rates and blocks of 15..=4096.
413    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    // ── Sample rate ──
418
419    /// The default, and the reason it is the default: pinning a rate on
420    /// CoreAudio changes it for every other application on the machine, and
421    /// launching a DAW is not consent to that.
422    #[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    /// The defect this whole path exists for: an engine built at a rate the
443    /// stream is not running at.
444    #[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    // ── Block size ──
475
476    #[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        // Still has to be able to absorb whatever the device picks.
482        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        // Asking for 64 does not mean 64 is all that can arrive: the device
506        // said it may deliver up to 4096, so that is what the audio thread
507        // has to be able to take without touching the allocator.
508        let choice = resolve_buffer_size(Some(64), CORE_AUDIO, 1.0);
509        assert_eq!(choice.max_frames, 4096);
510    }
511
512    /// Measured: asking a 48000 device for 96000 at 64 frames delivers blocks
513    /// of 128. The device counts its own frames; we are handed the converted
514    /// ones, and have to have room for them.
515    #[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    /// A rate below the device's own gives shorter blocks, not longer ones —
524    /// no reason to reserve less than the device's stated maximum for it.
525    #[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        // Nothing can be pinned to a range that was never named, so a size
539        // that was asked for has been refused.
540        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        // A device demanding blocks larger than our own cap still gets buffers
566        // big enough for them.
567        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    // ── What the player is told ──
580
581    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    /// Following the device is the ordinary case and is not news. The old
593    /// wording would have printed "gave 48000Hz (asked for 48000Hz)" on every
594    /// launch, which trains people to ignore the line that matters.
595    #[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}