use crate::debugging::log_info;
use crate::sdl_frontend::sdl_audio_callback::SdlAudioCallbackImpl;
use crate::sdl_frontend::sdl_audio_resampler::SdlAudioResampler;
use ringbuf::HeapRb;
use ringbuf::traits::{Producer, Split};
use sdl2::audio::{AudioDevice, AudioSpecDesired};
use std::sync::{
Arc,
atomic::AtomicU64,
atomic::{AtomicU32, AtomicUsize, Ordering},
};
pub(crate) type AudioProducer = <HeapRb<f32> as Split>::Prod;
pub(crate) type AudioConsumer = <HeapRb<f32> as Split>::Cons;
pub struct SdlNesAudio {
device: AudioDevice<SdlAudioCallbackImpl>,
sample_producer: AudioProducer,
volume: Arc<AtomicU32>,
stats: Arc<AudioStats>,
fill_level: Arc<AtomicUsize>,
actual_sample_rate: i32,
}
#[derive(Default)]
pub(crate) struct AudioStats {
pub(crate) received_samples: AtomicU64,
pub(crate) dropped_samples: AtomicU64,
pub(crate) underrun_samples: AtomicU64,
}
impl SdlNesAudio {
const BUFFER_SIZE: usize = 22050;
pub fn new(sdl_context: &sdl2::Sdl, sample_rate: i32) -> Result<Self, String> {
let audio_subsystem = sdl_context.audio()?;
let desired_spec = AudioSpecDesired {
freq: Some(sample_rate),
channels: Some(1), samples: Some(1024), };
let ring_buffer = HeapRb::<f32>::new(Self::BUFFER_SIZE);
let (producer, consumer) = ring_buffer.split();
let fill_level = Arc::new(AtomicUsize::new(0));
let volume = Arc::new(AtomicU32::new(f32::to_bits(0.75)));
let volume_clone = Arc::clone(&volume);
let stats = Arc::new(AudioStats::default());
let stats_clone = Arc::clone(&stats);
let fill_level_clone = Arc::clone(&fill_level);
let device =
audio_subsystem.open_playback(None, &desired_spec, |_spec| SdlAudioCallbackImpl {
sample_consumer: consumer,
volume: volume_clone,
stats: stats_clone,
fill_level: fill_level_clone,
resampler: SdlAudioResampler::new(Self::BUFFER_SIZE / 2),
})?;
let actual_rate = device.spec().freq;
if actual_rate != sample_rate {
log_info(format!(
"Audio: requested {} Hz, got {} Hz from SDL device",
sample_rate, actual_rate
));
}
Ok(Self {
device,
sample_producer: producer,
volume,
stats,
fill_level,
actual_sample_rate: actual_rate,
})
}
pub fn actual_sample_rate(&self) -> i32 {
self.actual_sample_rate
}
pub fn queue_sample(&mut self, sample: f32) {
queue_sample_to_producer(
&mut self.sample_producer,
sample,
&self.stats,
&self.fill_level,
);
}
pub fn take_and_reset_stats(&self) -> (u64, u64, u64) {
let received = self.stats.received_samples.swap(0, Ordering::Relaxed);
let dropped = self.stats.dropped_samples.swap(0, Ordering::Relaxed);
let underrun = self.stats.underrun_samples.swap(0, Ordering::Relaxed);
(received, dropped, underrun)
}
#[cfg(test)]
pub fn buffered_samples(&self) -> usize {
self.fill_level.load(Ordering::Relaxed)
}
pub fn resume(&self) {
self.device.resume();
}
pub fn prime_startup(&mut self, samples: usize) {
for _ in 0..samples {
queue_sample_to_producer(
&mut self.sample_producer,
0.0,
&self.stats,
&self.fill_level,
);
}
}
#[cfg(test)]
pub fn pause(&self) {
self.device.pause();
}
pub fn set_volume(&self, volume: f32) {
let clamped = volume.clamp(0.0, 1.0);
self.volume.store(f32::to_bits(clamped), Ordering::Relaxed);
}
pub fn get_volume(&self) -> f32 {
f32::from_bits(self.volume.load(Ordering::Relaxed))
}
}
fn queue_sample_to_producer(
producer: &mut AudioProducer,
sample: f32,
_stats: &AudioStats,
fill_level: &AtomicUsize,
) {
let mut pending = sample;
loop {
match producer.try_push(pending) {
Ok(()) => {
fill_level.fetch_add(1, Ordering::Relaxed);
return;
}
Err(sample) => {
pending = sample;
std::thread::yield_now();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdl_frontend::sdl_audio_resampler::SdlAudioResampler;
use ringbuf::traits::{Consumer, Split};
use serial_test::serial;
use std::collections::VecDeque;
use std::env;
use std::sync::{Arc, Barrier};
use std::time::{Duration, Instant};
#[test]
#[serial]
fn test_audio_functionality() {
struct EnvRestore {
key: &'static str,
prev: Option<String>,
}
impl Drop for EnvRestore {
fn drop(&mut self) {
match &self.prev {
Some(value) => unsafe {
env::set_var(self.key, value)
},
None => unsafe {
env::remove_var(self.key)
},
}
}
}
let restore = EnvRestore {
key: "SDL_AUDIODRIVER",
prev: env::var("SDL_AUDIODRIVER").ok(),
};
unsafe {
env::set_var("SDL_AUDIODRIVER", "dummy");
}
let sdl_context = sdl2::init().expect("Failed to initialize SDL2");
let audio = SdlNesAudio::new(&sdl_context, 44100);
assert!(audio.is_ok(), "Audio initialization should succeed");
let mut audio = audio.unwrap();
audio.set_volume(0.5);
assert_eq!(audio.get_volume(), 0.5, "Volume should be 0.5");
audio.set_volume(2.0); assert_eq!(audio.get_volume(), 1.0, "Volume should clamp to 1.0");
audio.set_volume(-0.5); assert_eq!(audio.get_volume(), 0.0, "Volume should clamp to 0.0");
audio.resume();
audio.pause();
audio.queue_sample(0.5);
audio.queue_sample(0.3);
audio.queue_sample(0.8);
drop(restore);
}
#[test]
#[serial]
fn test_prime_startup_buffers_silence() {
struct EnvRestore {
key: &'static str,
prev: Option<String>,
}
impl Drop for EnvRestore {
fn drop(&mut self) {
match &self.prev {
Some(value) => unsafe { env::set_var(self.key, value) },
None => unsafe { env::remove_var(self.key) },
}
}
}
let restore = EnvRestore {
key: "SDL_AUDIODRIVER",
prev: env::var("SDL_AUDIODRIVER").ok(),
};
unsafe {
env::set_var("SDL_AUDIODRIVER", "dummy");
}
let sdl_context = sdl2::init().expect("Failed to initialize SDL2");
let mut audio = SdlNesAudio::new(&sdl_context, 44100).expect("Audio init should succeed");
assert_eq!(audio.buffered_samples(), 0);
audio.prime_startup(2048);
assert!(audio.buffered_samples() >= 2048);
drop(restore);
}
#[test]
fn test_queue_sample_does_not_drop_when_buffer_full() {
let stats = Arc::new(AudioStats::default());
let ring_buffer = HeapRb::<f32>::new(1);
let (mut producer, mut consumer) = ring_buffer.split();
let fill_level = Arc::new(AtomicUsize::new(0));
queue_sample_to_producer(&mut producer, 0.1, &stats, &fill_level);
let barrier = Arc::new(Barrier::new(2));
let barrier_consumer = Arc::clone(&barrier);
let (result_tx, result_rx) = std::sync::mpsc::channel::<(f32, f32)>();
let (producer_ready_tx, producer_ready_rx) = std::sync::mpsc::channel::<()>();
let fill_level_consumer = Arc::clone(&fill_level);
let consumer = std::thread::spawn(move || {
barrier_consumer.wait();
let first = {
let start = Instant::now();
loop {
if let Some(value) = consumer.try_pop() {
fill_level_consumer.fetch_sub(1, Ordering::Relaxed);
break value;
}
if start.elapsed() > Duration::from_millis(200) {
panic!("expected first sample");
}
std::thread::yield_now();
}
};
let second = {
let start = Instant::now();
loop {
if let Some(value) = consumer.try_pop() {
fill_level_consumer.fetch_sub(1, Ordering::Relaxed);
break value;
}
if start.elapsed() > Duration::from_millis(200) {
panic!("expected second sample (must not be dropped)");
}
std::thread::yield_now();
}
};
result_tx
.send((first, second))
.expect("failed to send samples to main thread");
});
let stats_producer = Arc::clone(&stats);
let fill_level_producer = Arc::clone(&fill_level);
let producer = std::thread::spawn(move || {
producer_ready_tx
.send(())
.expect("failed to signal producer readiness");
queue_sample_to_producer(&mut producer, 0.2, &stats_producer, &fill_level_producer);
});
producer_ready_rx
.recv_timeout(Duration::from_millis(200))
.expect("producer did not become ready");
barrier.wait();
producer.join().expect("producer thread panicked");
consumer.join().expect("consumer thread panicked");
let (first, second) = result_rx
.recv_timeout(Duration::from_millis(200))
.expect("expected samples from consumer");
assert_eq!(first, 0.1);
assert_eq!(second, 0.2);
let dropped = stats.dropped_samples.load(Ordering::Relaxed);
assert_eq!(dropped, 0, "no samples should be dropped");
}
#[test]
fn test_resampler_rate_clamps_to_limits() {
let mut resampler = SdlAudioResampler::new(100);
resampler.update_rate(100);
assert!((resampler.rate() - 1.0).abs() < 0.00001);
resampler.update_rate(0);
assert!((resampler.rate() - (1.0 - SdlAudioResampler::MAX_RATE_ADJUST)).abs() < 0.00001);
resampler.update_rate(200);
assert!((resampler.rate() - (1.0 + SdlAudioResampler::MAX_RATE_ADJUST)).abs() < 0.00001);
}
#[test]
fn test_resampler_outputs_source_sequence_at_unity_rate() {
let mut resampler = SdlAudioResampler::new(4);
resampler.set_rate_for_test(1.0);
let mut samples = VecDeque::from([0.0, 1.0, 0.0, 1.0]);
let mut pop_sample = || samples.pop_front();
let first = resampler
.render_next(&mut pop_sample)
.expect("first sample");
let second = resampler
.render_next(&mut pop_sample)
.expect("second sample");
let third = resampler
.render_next(&mut pop_sample)
.expect("third sample");
assert!((first - 0.0).abs() < 0.00001);
assert!((second - 1.0).abs() < 0.00001);
assert!((third - 0.0).abs() < 0.00001);
}
}