use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use crate::error::{AudioError, AudioResult};
use crate::frame::AudioFrame;
use super::device::AudioDevice;
struct SyncStream(#[allow(dead_code)] cpal::Stream);
unsafe impl Sync for SyncStream {}
unsafe impl Send for SyncStream {}
pub struct AudioPlayback {
stream: Option<SyncStream>,
sample_buffer: Option<Arc<Mutex<VecDeque<i16>>>>,
current_device_id: Option<String>,
}
impl AudioPlayback {
pub fn new() -> Self {
Self { stream: None, sample_buffer: None, current_device_id: None }
}
pub fn list_output_devices() -> AudioResult<Vec<AudioDevice>> {
let host = cpal::default_host();
let devices = host.output_devices().map_err(|e| {
AudioError::Device(format!(
"failed to enumerate output devices: {e}. Check that audio drivers are installed."
))
})?;
let result: Vec<AudioDevice> = devices
.filter_map(|device| {
let name = device.name().unwrap_or_default();
if name.is_empty() { None } else { Some(AudioDevice::new(name.clone(), name)) }
})
.collect();
Ok(result)
}
pub async fn play(&mut self, device_id: &str, frame: &AudioFrame) -> AudioResult<()> {
let needs_open = match &self.current_device_id {
Some(current) => current != device_id,
None => true,
};
if needs_open {
self.stream = None;
self.sample_buffer = None;
self.current_device_id = None;
self.open_stream(device_id, frame.sample_rate, frame.channels)?;
}
let samples = frame.samples();
if let Some(buffer) = &self.sample_buffer {
let mut buf = buffer.lock().expect("playback sample buffer lock poisoned");
buf.extend(samples.iter().copied());
}
Ok(())
}
pub fn stop(&mut self) {
self.stream = None;
self.sample_buffer = None;
self.current_device_id = None;
}
fn open_stream(&mut self, device_id: &str, sample_rate: u32, channels: u8) -> AudioResult<()> {
let host = cpal::default_host();
let devices = host.output_devices().map_err(|e| {
AudioError::Device(format!(
"failed to enumerate output devices: {e}. Check that audio drivers are installed."
))
})?;
let device = devices
.into_iter()
.find(|d| d.name().unwrap_or_default() == device_id)
.ok_or_else(|| {
AudioError::Device(format!(
"output device not found: '{device_id}'. Use list_output_devices() to see available devices."
))
})?;
let stream_config = cpal::StreamConfig {
channels: channels as u16,
sample_rate: cpal::SampleRate(sample_rate),
buffer_size: cpal::BufferSize::Default,
};
let buffer: Arc<Mutex<VecDeque<i16>>> = Arc::new(Mutex::new(VecDeque::new()));
let callback_buffer = Arc::clone(&buffer);
let cpal_stream = device
.build_output_stream(
&stream_config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
let mut buf =
callback_buffer.lock().expect("playback sample buffer lock poisoned");
for sample in data.iter_mut() {
if let Some(s) = buf.pop_front() {
*sample = s as f32 / i16::MAX as f32;
} else {
*sample = 0.0;
}
}
},
move |err| {
tracing::error!("cpal output stream error: {err}");
},
None, )
.map_err(|e| {
AudioError::Device(format!("failed to open output stream on '{device_id}': {e}"))
})?;
cpal_stream.play().map_err(|e| {
AudioError::Device(format!("failed to start output stream on '{device_id}': {e}"))
})?;
self.stream = Some(SyncStream(cpal_stream));
self.sample_buffer = Some(buffer);
self.current_device_id = Some(device_id.to_string());
Ok(())
}
}
impl Default for AudioPlayback {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<AudioPlayback>();
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stop_idempotent() {
let mut playback = AudioPlayback::new();
playback.stop();
assert!(playback.stream.is_none());
assert!(playback.sample_buffer.is_none());
assert!(playback.current_device_id.is_none());
playback.stop();
assert!(playback.stream.is_none());
}
#[test]
fn test_audio_playback_default() {
let playback = AudioPlayback::default();
assert!(playback.stream.is_none());
assert!(playback.sample_buffer.is_none());
assert!(playback.current_device_id.is_none());
}
#[test]
fn test_audio_playback_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<AudioPlayback>();
assert_sync::<AudioPlayback>();
}
}