khora_infra/audio/backends/cpal/
device.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Contains the `CpalAudioDevice` struct.
16
17use anyhow::{anyhow, Result};
18use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
19use khora_core::audio::device::{AudioDevice, StreamInfo};
20
21/// An `AudioDevice` implementation that uses the host's default audio output device via CPAL.
22#[derive(Default)]
23pub struct CpalAudioDevice;
24
25impl CpalAudioDevice {
26    /// Creates a new instance of the CPAL audio device backend.
27    pub fn new() -> Self {
28        Self
29    }
30}
31
32impl AudioDevice for CpalAudioDevice {
33    fn start(
34        self: Box<Self>,
35        mut on_mix_needed: Box<dyn FnMut(&mut [f32], &StreamInfo) + Send>,
36    ) -> Result<()> {
37        // Set up the CPAL audio stream.
38        let host = cpal::default_host();
39        let device = host
40            .default_output_device()
41            .ok_or_else(|| anyhow!("No default output device available"))?;
42        let config = device.default_output_config()?;
43
44        let stream_info = StreamInfo {
45            channels: config.channels(),
46            sample_rate: config.sample_rate(),
47        };
48
49        let audio_callback = move |output_buffer: &mut [f32], _: &cpal::OutputCallbackInfo| {
50            on_mix_needed(output_buffer, &stream_info);
51        };
52
53        let error_callback = |err| {
54            eprintln!("An error occurred on the audio stream: {}", err);
55        };
56
57        let stream = match config.sample_format() {
58            cpal::SampleFormat::F32 => {
59                device.build_output_stream(&config.into(), audio_callback, error_callback, None)?
60            }
61            format => return Err(anyhow!("Unsupported sample format: {}", format)),
62        };
63
64        stream.play()?;
65
66        // Detach the stream to keep it running for the lifetime of the application.
67        std::mem::forget(stream);
68
69        Ok(())
70    }
71}