Skip to main content

vst3_host/backends/
cpal_backend.rs

1//! CPAL audio backend implementation
2
3use crate::{
4    audio::{AudioBackend, AudioConfig, AudioStream},
5    error::{Error, Result},
6};
7use cpal::{
8    traits::{DeviceTrait, HostTrait, StreamTrait},
9    BufferSize, Device, Stream, StreamConfig, SupportedBufferSize,
10};
11
12/// Clamp a requested `block_size` into a device-advertised supported buffer range.
13///
14/// `SupportedBufferSize::Range` → `Fixed(block_size clamped to [min, max])`;
15/// `Unknown` → `Default` (the device gives no range, so let it choose). Pure and unit-tested.
16fn clamp_block_to_buffer_size(supported: &SupportedBufferSize, block_size: u32) -> BufferSize {
17    match supported {
18        SupportedBufferSize::Range { min, max } => BufferSize::Fixed(block_size.clamp(*min, *max)),
19        SupportedBufferSize::Unknown => BufferSize::Default,
20    }
21}
22
23/// Pick a buffer size the device will actually accept, given its advertised config ranges,
24/// the requested sample rate / channel count, and the desired `block_size`.
25///
26/// Many devices (notably CoreAudio on macOS) reject `BufferSize::Fixed` outright, so we only
27/// request a fixed size when a matching range is advertised — and clamp into it. Otherwise we
28/// fall back to `BufferSize::Default`. The channel count and sample rate are NOT changed here:
29/// the bridge interleaves based on the requested channel count, so silently changing it would
30/// garble audio.
31fn resolve_buffer_size(
32    ranges: impl Iterator<Item = cpal::SupportedStreamConfigRange>,
33    want_sr: u32,
34    want_ch: u16,
35    block_size: u32,
36) -> BufferSize {
37    // Prefer a range matching both the channel count and sample rate. Many devices (notably
38    // pro multichannel interfaces) advertise their config ranges only at their native channel
39    // count, so a stereo request would never find an exact match and silently lose the
40    // configured block size. The buffer-size range is a device-level property independent of
41    // the stream's channel count, so fall back to any range that covers the sample rate.
42    let mut sr_only_fallback: Option<BufferSize> = None;
43    for range in ranges {
44        if want_sr < range.min_sample_rate() || want_sr > range.max_sample_rate() {
45            continue;
46        }
47        let resolved = clamp_block_to_buffer_size(range.buffer_size(), block_size);
48        if range.channels() == want_ch {
49            return resolved; // exact channel + sample-rate match wins
50        }
51        sr_only_fallback.get_or_insert(resolved);
52    }
53    sr_only_fallback.unwrap_or(BufferSize::Default)
54}
55
56/// Resolve the output-stream buffer size for `device` and `config`.
57fn resolve_output_buffer_size(device: &Device, config: &AudioConfig) -> BufferSize {
58    // cpal 0.18: `SampleRate` is a `u32` type alias.
59    match device.supported_output_configs() {
60        Ok(ranges) => resolve_buffer_size(
61            ranges,
62            config.sample_rate as u32,
63            config.output_channels as u16,
64            config.block_size as u32,
65        ),
66        Err(_) => BufferSize::Default,
67    }
68}
69
70/// Resolve the input-stream buffer size for `device` and `config`.
71///
72/// Mirrors [`resolve_output_buffer_size`] for the capture side: an unconditional
73/// `BufferSize::Fixed` is rejected by CoreAudio on many input devices.
74fn resolve_input_buffer_size(device: &Device, config: &AudioConfig) -> BufferSize {
75    match device.supported_input_configs() {
76        Ok(ranges) => resolve_buffer_size(
77            ranges,
78            config.sample_rate as u32,
79            config.input_channels as u16,
80            config.block_size as u32,
81        ),
82        Err(_) => BufferSize::Default,
83    }
84}
85
86/// CPAL stream wrapper.
87///
88/// Inherits cpal's thread affinity: `cpal::Stream` is deliberately `!Send`, because on some
89/// backends the stream must be constructed, controlled and torn down on the same thread. This
90/// wrapper adds no synchronization, so it is `!Send` too and must stay on the thread that
91/// created it (which is why [`AudioHandle`](crate::AudioHandle), holding one, is also `!Send`).
92pub struct CpalStream {
93    // We use Option to allow moving the stream in drop
94    stream: Option<Stream>,
95}
96
97impl AudioStream for CpalStream {
98    fn play(&self) -> std::result::Result<(), Box<dyn std::error::Error>> {
99        if let Some(ref stream) = self.stream {
100            stream
101                .play()
102                .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
103        } else {
104            Err(Box::new(std::io::Error::other("Stream has been dropped")))
105        }
106    }
107
108    fn pause(&self) -> std::result::Result<(), Box<dyn std::error::Error>> {
109        if let Some(ref stream) = self.stream {
110            stream
111                .pause()
112                .map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
113        } else {
114            Err(Box::new(std::io::Error::other("Stream has been dropped")))
115        }
116    }
117}
118
119impl Drop for CpalStream {
120    fn drop(&mut self) {
121        // Drop the stream
122        self.stream.take();
123    }
124}
125
126/// CPAL-based audio backend
127pub struct CpalBackend {
128    host: cpal::Host,
129}
130
131impl CpalBackend {
132    /// Create a new CPAL backend
133    pub fn new() -> Result<Self> {
134        Ok(Self {
135            host: cpal::default_host(),
136        })
137    }
138
139    /// List all available output devices
140    pub fn list_output_devices(&self) -> Result<Vec<String>> {
141        let devices: Vec<String> = self
142            .host
143            .output_devices()
144            .map_err(|e| Error::AudioBackendError(format!("Failed to enumerate devices: {}", e)))?
145            .map(|d| d.to_string())
146            .collect();
147        Ok(devices)
148    }
149
150    /// List all available input devices
151    pub fn list_input_devices(&self) -> Result<Vec<String>> {
152        let devices: Vec<String> = self
153            .host
154            .input_devices()
155            .map_err(|e| Error::AudioBackendError(format!("Failed to enumerate devices: {}", e)))?
156            .map(|d| d.to_string())
157            .collect();
158        Ok(devices)
159    }
160}
161
162impl AudioBackend for CpalBackend {
163    type Stream = CpalStream;
164    type Device = Device;
165    type Error = Error;
166
167    fn enumerate_output_devices(&self) -> Result<Vec<Self::Device>> {
168        let devices: Vec<Device> = self
169            .host
170            .output_devices()
171            .map_err(|e| {
172                Error::AudioBackendError(format!("Failed to enumerate output devices: {}", e))
173            })?
174            .collect();
175        Ok(devices)
176    }
177
178    fn enumerate_input_devices(&self) -> Result<Vec<Self::Device>> {
179        let devices: Vec<Device> = self
180            .host
181            .input_devices()
182            .map_err(|e| {
183                Error::AudioBackendError(format!("Failed to enumerate input devices: {}", e))
184            })?
185            .collect();
186        Ok(devices)
187    }
188
189    fn default_output_device(&self) -> Option<Self::Device> {
190        self.host.default_output_device()
191    }
192
193    fn default_input_device(&self) -> Option<Self::Device> {
194        self.host.default_input_device()
195    }
196
197    fn create_output_stream(
198        &self,
199        device: &Self::Device,
200        config: AudioConfig,
201        mut data_callback: Box<dyn FnMut(&mut [f32]) + Send>,
202        mut error_callback: Box<dyn FnMut(Self::Error) + Send>,
203    ) -> Result<Self::Stream> {
204        let stream_config = StreamConfig {
205            channels: config.output_channels as u16,
206            sample_rate: config.sample_rate as u32,
207            buffer_size: resolve_output_buffer_size(device, &config),
208        };
209
210        let stream = device
211            .build_output_stream(
212                stream_config,
213                move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
214                    data_callback(data);
215                },
216                move |err| {
217                    error_callback(Error::AudioBackendError(format!("Stream error: {}", err)));
218                },
219                None,
220            )
221            .map_err(|e| {
222                Error::AudioBackendError(format!("Failed to build output stream: {}", e))
223            })?;
224
225        Ok(CpalStream {
226            stream: Some(stream),
227        })
228    }
229
230    fn create_input_stream(
231        &self,
232        device: &Self::Device,
233        config: AudioConfig,
234        mut data_callback: Box<dyn FnMut(&[f32]) + Send>,
235        mut error_callback: Box<dyn FnMut(Self::Error) + Send>,
236    ) -> Result<Self::Stream> {
237        let stream_config = StreamConfig {
238            channels: config.input_channels as u16,
239            sample_rate: config.sample_rate as u32,
240            buffer_size: resolve_input_buffer_size(device, &config),
241        };
242
243        let stream = device
244            .build_input_stream(
245                stream_config,
246                move |data: &[f32], _: &cpal::InputCallbackInfo| {
247                    data_callback(data);
248                },
249                move |err| {
250                    error_callback(Error::AudioBackendError(format!("Stream error: {}", err)));
251                },
252                None,
253            )
254            .map_err(|e| {
255                Error::AudioBackendError(format!("Failed to build input stream: {}", e))
256            })?;
257
258        Ok(CpalStream {
259            stream: Some(stream),
260        })
261    }
262}
263
264impl Default for CpalBackend {
265    fn default() -> Self {
266        Self::new().expect("Failed to create CPAL backend")
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use cpal::{SampleFormat, SupportedStreamConfigRange};
274
275    /// Build a config range with the given channels, sample-rate bounds, and fixed buffer range.
276    fn range(
277        channels: u16,
278        min_sr: u32,
279        max_sr: u32,
280        buf_min: u32,
281        buf_max: u32,
282    ) -> SupportedStreamConfigRange {
283        SupportedStreamConfigRange::new(
284            channels,
285            min_sr,
286            max_sr,
287            SupportedBufferSize::Range {
288                min: buf_min,
289                max: buf_max,
290            },
291            SampleFormat::F32,
292        )
293    }
294
295    #[test]
296    fn resolve_exact_channel_and_sr_match_clamps() {
297        let ranges = vec![range(2, 44_100, 48_000, 64, 2048)];
298        assert_eq!(
299            resolve_buffer_size(ranges.into_iter(), 48_000, 2, 512),
300            BufferSize::Fixed(512)
301        );
302    }
303
304    #[test]
305    fn resolve_channel_mismatch_falls_back_to_sample_rate_match() {
306        // Device advertises only an 8-channel range (e.g. a pro interface); a stereo request
307        // must still honor the device's buffer-size range rather than dropping to Default.
308        let ranges = vec![range(8, 44_100, 96_000, 128, 1024)];
309        assert_eq!(
310            resolve_buffer_size(ranges.into_iter(), 48_000, 2, 4096),
311            BufferSize::Fixed(1024) // clamped into the device range
312        );
313    }
314
315    #[test]
316    fn resolve_prefers_exact_channel_over_sr_only() {
317        // sr-only candidate first, exact match second — exact must win.
318        let ranges = vec![
319            range(8, 44_100, 96_000, 128, 1024),
320            range(2, 44_100, 96_000, 64, 2048),
321        ];
322        assert_eq!(
323            resolve_buffer_size(ranges.into_iter(), 48_000, 2, 512),
324            BufferSize::Fixed(512)
325        );
326    }
327
328    #[test]
329    fn resolve_sample_rate_out_of_range_is_default() {
330        let ranges = vec![range(2, 44_100, 48_000, 64, 2048)];
331        assert_eq!(
332            resolve_buffer_size(ranges.into_iter(), 96_000, 2, 512),
333            BufferSize::Default
334        );
335    }
336
337    #[test]
338    fn resolve_no_ranges_is_default() {
339        let ranges: Vec<SupportedStreamConfigRange> = vec![];
340        assert_eq!(
341            resolve_buffer_size(ranges.into_iter(), 48_000, 2, 512),
342            BufferSize::Default
343        );
344    }
345
346    #[test]
347    fn clamp_within_range_keeps_requested_size() {
348        let supported = SupportedBufferSize::Range { min: 64, max: 2048 };
349        assert_eq!(
350            clamp_block_to_buffer_size(&supported, 512),
351            BufferSize::Fixed(512)
352        );
353    }
354
355    #[test]
356    fn clamp_below_min_raises_to_min() {
357        let supported = SupportedBufferSize::Range {
358            min: 256,
359            max: 2048,
360        };
361        assert_eq!(
362            clamp_block_to_buffer_size(&supported, 64),
363            BufferSize::Fixed(256)
364        );
365    }
366
367    #[test]
368    fn clamp_above_max_lowers_to_max() {
369        let supported = SupportedBufferSize::Range { min: 64, max: 1024 };
370        assert_eq!(
371            clamp_block_to_buffer_size(&supported, 4096),
372            BufferSize::Fixed(1024)
373        );
374    }
375
376    #[test]
377    fn clamp_unknown_range_falls_back_to_default() {
378        assert_eq!(
379            clamp_block_to_buffer_size(&SupportedBufferSize::Unknown, 512),
380            BufferSize::Default
381        );
382    }
383}