//! Audio playback implementation using cpal.
use anyhow::Result;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{Device, SampleFormat, Stream, StreamConfig};
use crossbeam_channel::{bounded, Receiver, Sender};
use std::sync::Arc;
use std::collections::VecDeque;
use parking_lot::Mutex;
/// Audio playback stream.
pub struct PlaybackStream {
stream: Stream,
sample_tx: Sender<Vec<f32>>,
config: StreamConfig,
}
impl PlaybackStream {
/// Create a new playback stream.
pub fn new() -> Result<Self> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| anyhow::anyhow!("No output device available"))?;
let config = device.default_output_config()?;
let sample_format = config.sample_format();
let config: StreamConfig = config.into();
tracing::info!(
"Using output device: {} ({:?} @ {}Hz)",
device.name().unwrap_or_default(),
sample_format,
config.sample_rate.0
);
let (sample_tx, sample_rx) = bounded::<Vec<f32>>(8);
let stream = match sample_format {
SampleFormat::F32 => build_stream::<f32>(&device, &config, sample_rx)?,
SampleFormat::I16 => build_stream::<i16>(&device, &config, sample_rx)?,
SampleFormat::U16 => build_stream::<u16>(&device, &config, sample_rx)?,
_ => return Err(anyhow::anyhow!("Unsupported sample format: {:?}", sample_format)),
};
Ok(Self {
stream,
sample_tx,
config,
})
}
/// Start playback.
pub fn start(&self) -> Result<()> {
self.stream.play()?;
Ok(())
}
/// Queue samples for playback.
pub fn queue(&self, samples: Vec<f32>) -> Result<()> {
self.sample_tx.send(samples)?;
Ok(())
}
/// Get the sample rate.
pub fn sample_rate(&self) -> u32 {
self.config.sample_rate.0
}
}
fn build_stream<T>(
device: &Device,
config: &StreamConfig,
sample_rx: Receiver<Vec<f32>>,
) -> Result<Stream>
where
T: cpal::Sample + cpal::SizedSample + cpal::FromSample<f32>,
{
let buffer: Arc<Mutex<VecDeque<f32>>> = Arc::new(Mutex::new(VecDeque::new()));
let buffer_clone = buffer.clone();
let channels = config.channels as usize;
tracing::info!("Playback stream: {} channels @ {}Hz", channels, config.sample_rate.0);
// Spawn thread to receive samples
std::thread::spawn(move || {
while let Ok(samples) = sample_rx.recv() {
let mut buf = buffer_clone.lock();
buf.extend(samples.iter());
tracing::debug!("Queued {} samples, buffer size: {}", samples.len(), buf.len());
}
});
let err_fn = |err| tracing::error!("Audio stream error: {}", err);
let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
let mut buf = buffer.lock();
// Process in frames (channels samples per frame)
for frame in data.chunks_mut(channels) {
let sample = buf.pop_front().unwrap_or(0.0);
// Duplicate mono sample to all channels
for channel_sample in frame.iter_mut() {
*channel_sample = T::from_sample(sample);
}
}
},
err_fn,
None,
)?;
Ok(stream)
}