use anyhow::{anyhow, Result}; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, StreamConfig, }; use rb::{RbConsumer, RbProducer, SpscRb, RB}; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use symphonia::core::audio::AudioBufferRef; const RING_BUFFER_SIZE: usize = 32 * 1024; pub struct AudioOutput { ring_buf_producer: rb::Producer, viz_producer: Option>, _stream: cpal::Stream, pub sample_rate: u32, pub channels: usize, } impl AudioOutput { pub fn try_open(sample_rate: u32, channels: usize) -> Result { let host = cpal::default_host(); let device = host .default_output_device() .ok_or_else(|| anyhow!("no output device"))?; let config = StreamConfig { channels: channels as u16, sample_rate: cpal::SampleRate(sample_rate), buffer_size: cpal::BufferSize::Default, }; let ring = SpscRb::new(RING_BUFFER_SIZE); let (producer, consumer) = (ring.producer(), ring.consumer()); let stream = device.build_output_stream( &config, move |data: &mut [f32], _| { let n = consumer.read(data).unwrap_or(0); for s in &mut data[n..] { *s = 0.0; } }, |e| eprintln!("audio stream error: {e}"), None, )?; stream.play()?; Ok(Self { ring_buf_producer: producer, viz_producer: None, _stream: stream, sample_rate, channels, }) } pub fn set_viz_producer(&mut self, producer: rb::Producer) { self.viz_producer = Some(producer); } pub fn write( &mut self, decoded: AudioBufferRef<'_>, volume: f32, stop: &Arc, ) -> Result<()> { let mut sample_buf = symphonia::core::audio::SampleBuffer::::new( decoded.capacity() as u64, *decoded.spec(), ); sample_buf.copy_interleaved_ref(decoded); let samples: Vec = sample_buf.samples().iter().map(|s| s * volume).collect(); // Best-effort copy for visualizer (non-blocking, ok to drop samples) if let Some(ref mut viz) = self.viz_producer { let _ = viz.write(&samples); } let mut remaining = &samples[..]; while !remaining.is_empty() { if stop.load(Ordering::SeqCst) { return Ok(()); } match self.ring_buf_producer.write_blocking(remaining) { Some(n) => remaining = &remaining[n..], None => break, } } Ok(()) } }