use super::RingBuffer;
use rodio::{OutputStream, Sink, Source};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct AudioDeviceError(pub String);
impl std::fmt::Display for AudioDeviceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for AudioDeviceError {}
struct RingBufferSource {
ring_buffer: Arc<RingBuffer>,
current_pos: usize,
sample_rate: u32,
channels: u16,
finished: Arc<AtomicBool>,
buffer: Vec<f32>,
buffer_pos: usize,
}
impl RingBufferSource {
fn new(
ring_buffer: Arc<RingBuffer>,
sample_rate: u32,
channels: u16,
finished: Arc<AtomicBool>,
) -> Self {
RingBufferSource {
ring_buffer,
current_pos: 0,
sample_rate,
channels,
finished,
buffer: vec![0.0f32; 4096],
buffer_pos: 4096, }
}
}
impl Source for RingBufferSource {
fn current_frame_len(&self) -> Option<usize> {
let available = self.ring_buffer.available_read();
if available > 0 {
Some(available)
} else {
Some(4096) }
}
fn channels(&self) -> u16 {
self.channels
}
fn sample_rate(&self) -> u32 {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
impl Iterator for RingBufferSource {
type Item = f32;
fn next(&mut self) -> Option<f32> {
if self.finished.load(Ordering::Relaxed) {
return None;
}
if self.buffer_pos >= self.buffer.len() {
let read = self.ring_buffer.read(&mut self.buffer);
if read > 0 {
self.buffer_pos = 0;
} else {
self.buffer_pos = 0;
self.buffer.fill(0.0);
}
}
if self.buffer_pos < self.buffer.len() {
let sample = self.buffer[self.buffer_pos];
self.buffer_pos += 1;
self.current_pos += 1;
Some(sample)
} else {
Some(0.0)
}
}
}
pub struct AudioDevice {
_stream: OutputStream,
_sink: Sink,
running: Arc<AtomicBool>,
finished: Arc<AtomicBool>,
}
impl AudioDevice {
pub fn new(
sample_rate: u32,
channels: u16,
ring_buffer: Arc<RingBuffer>,
) -> Result<Self, AudioDeviceError> {
let (stream, stream_handle) = OutputStream::try_default()
.map_err(|e| AudioDeviceError(format!("Failed to create audio stream: {e}")))?;
let sink = Sink::try_new(&stream_handle)
.map_err(|e| AudioDeviceError(format!("Failed to create audio sink: {e}")))?;
let finished = Arc::new(AtomicBool::new(false));
let source =
RingBufferSource::new(ring_buffer, sample_rate, channels, Arc::clone(&finished));
sink.append(source);
let running = Arc::new(AtomicBool::new(true));
Ok(AudioDevice {
_stream: stream,
_sink: sink,
running,
finished,
})
}
pub fn pause(&self) {
self._sink.pause();
}
#[cfg(test)]
pub fn play(&self) {
self._sink.play();
}
#[cfg(test)]
pub fn is_running(&self) -> bool {
self.running.load(Ordering::Relaxed)
}
pub fn finish(&self) {
self.finished.store(true, Ordering::Relaxed);
}
}
impl Drop for AudioDevice {
fn drop(&mut self) {
self.pause();
self.running.store(false, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn try_audio_device(
buffer_len: usize,
sample_rate: u32,
channels: u16,
) -> Option<(AudioDevice, Arc<RingBuffer>)> {
let ring_buffer =
Arc::new(RingBuffer::new(buffer_len).expect("Failed to create ring buffer"));
match AudioDevice::new(sample_rate, channels, Arc::clone(&ring_buffer)) {
Ok(device) => Some((device, ring_buffer)),
Err(err) => {
eprintln!("Skipping audio::audio_device test (audio backend unavailable): {err}");
None
}
}
}
#[test]
fn test_audio_device_creation() {
let Some((device, _ring)) = try_audio_device(4096, 44100, 1) else {
return;
};
assert!(
device.is_running(),
"Audio device should be running after creation"
);
}
#[test]
fn test_finish_signal() {
let Some((device, _ring)) = try_audio_device(4096, 44100, 1) else {
return;
};
device.finish();
}
#[test]
fn test_pause_and_play() {
let Some((device, _ring)) = try_audio_device(4096, 44100, 1) else {
return;
};
device.pause();
assert!(
device.is_running(),
"Device should still be marked running after pause"
);
device.play();
assert!(
device.is_running(),
"Device should still be marked running after play"
);
}
#[test]
fn test_ring_buffer_source_creation() {
let ring_buffer = Arc::new(RingBuffer::new(4096).expect("Failed to create ring buffer"));
let finished = Arc::new(AtomicBool::new(false));
let source = RingBufferSource::new(ring_buffer, 44100, 1, finished);
assert_eq!(source.sample_rate(), 44100);
assert_eq!(source.channels(), 1);
assert!(source.current_frame_len().is_some());
}
#[test]
fn test_ring_buffer_source_silence_on_underrun() {
let ring_buffer = Arc::new(RingBuffer::new(4096).expect("Failed to create ring buffer"));
let finished = Arc::new(AtomicBool::new(false));
let mut source = RingBufferSource::new(ring_buffer, 44100, 1, finished);
let sample = source.next();
assert!(
sample.is_some(),
"Source should return Some value on buffer underrun"
);
assert_eq!(
sample.unwrap(),
0.0,
"Source should return silence (0.0) on underrun"
);
}
#[test]
fn test_ring_buffer_source_finished_signal() {
let ring_buffer = Arc::new(RingBuffer::new(4096).expect("Failed to create ring buffer"));
let finished = Arc::new(AtomicBool::new(false));
let mut source =
RingBufferSource::new(Arc::clone(&ring_buffer), 44100, 1, Arc::clone(&finished));
assert!(source.next().is_some());
finished.store(true, Ordering::Relaxed);
assert_eq!(
source.next(),
None,
"Source should return None after finished signal"
);
}
#[test]
fn test_audio_device_drop_pauses() {
let Some((device, _ring)) = try_audio_device(4096, 44100, 1) else {
return;
};
let running = device.is_running();
assert!(running, "Device should be running before drop");
drop(device);
}
#[test]
fn test_stereo_audio_device() {
let Some((_device, _ring)) = try_audio_device(8192, 44100, 2) else {
return;
};
let source = RingBufferSource::new(
Arc::new(RingBuffer::new(8192).expect("Failed to create ring buffer")),
44100,
2,
Arc::new(AtomicBool::new(false)),
);
assert_eq!(
source.channels(),
2,
"Source should report 2 channels for stereo"
);
}
#[test]
fn test_various_sample_rates() {
let sample_rates = vec![22050, 44100, 48000, 96000];
let mut succeeded = false;
for rate in sample_rates {
let Some((_device, _ring)) = try_audio_device(4096, rate, 1) else {
continue;
};
succeeded = true;
let source = RingBufferSource::new(
Arc::new(RingBuffer::new(4096).expect("Failed to create ring buffer")),
rate,
1,
Arc::new(AtomicBool::new(false)),
);
assert_eq!(
source.sample_rate(),
rate,
"Source should report correct sample rate"
);
}
if !succeeded {
eprintln!(
"Skipping sample rate checks (audio backend unavailable for all tested rates)"
);
}
}
}