From a42aa936fe94daf4c2de99126a5da17fc832ba9e Mon Sep 17 00:00:00 2001 From: lifning <> Date: Wed, 3 Nov 2021 17:16:38 -0700 Subject: [PATCH] replace SimpleSdl2AudioComponent with the AudioCVT-based resampler --- ferretro_components/Cargo.toml | 2 +- .../examples/multifunction_emulator.rs | 2 +- .../src/provided/sdl2/audio.rs | 143 +++++++++--------- .../src/provided/sdl2/audio_ratecontrol.rs | 26 +--- .../src/provided/sdl2/opengl.rs | 1 - 5 files changed, 78 insertions(+), 96 deletions(-) diff --git a/ferretro_components/Cargo.toml b/ferretro_components/Cargo.toml index e9d0334..8e4e1f8 100644 --- a/ferretro_components/Cargo.toml +++ b/ferretro_components/Cargo.toml @@ -12,7 +12,7 @@ ferretro_base = { path = "../ferretro_base"} libloading = "0.5" num_enum = "0.4" ffmpeg-next = { version = "4.3.8", optional = true } -sdl2 = { version = "0.32", optional = true } +sdl2 = { version = "0.32", optional = true, features = ["gfx"] } gl = { version = "0.14", optional = true } crossbeam-channel = { version = "0.4", optional = true } diff --git a/ferretro_components/examples/multifunction_emulator.rs b/ferretro_components/examples/multifunction_emulator.rs index d514594..1f657fb 100644 --- a/ferretro_components/examples/multifunction_emulator.rs +++ b/ferretro_components/examples/multifunction_emulator.rs @@ -55,7 +55,7 @@ pub fn main() -> Result<(), Box> { let sdl2_ogl = SimpleSdl2OpenglComponent::new(&mut sdl_context, emu.libretro_core())?; emu.register_component(sdl2_ogl)?; - let sdl2_audio = SimpleSdl2AudioComponent::new(&mut sdl_context, emu.libretro_core()); + let sdl2_audio = SimpleSdl2AudioComponent::new(&mut sdl_context, emu.libretro_core())?; emu.register_component(sdl2_audio)?; emu.register_component(SimpleSdl2GamepadComponent::new(&mut sdl_context))?; diff --git a/ferretro_components/src/provided/sdl2/audio.rs b/ferretro_components/src/provided/sdl2/audio.rs index 0074acb..f7cfab9 100644 --- a/ferretro_components/src/provided/sdl2/audio.rs +++ b/ferretro_components/src/provided/sdl2/audio.rs @@ -1,111 +1,112 @@ use crate::prelude::*; use std::error::Error; +use std::mem::size_of; use std::path::Path; -use core::time::Duration; use sdl2::Sdl; -use sdl2::audio::{AudioCallback, AudioDevice, AudioFormat, AudioSpec, AudioSpecDesired}; +use sdl2::audio::{AudioCVT, AudioFormat, AudioFormatNum, AudioQueue, AudioSpec, AudioSpecDesired}; -struct MySdlAudio { - audio_spec: AudioSpec, - audio_receiver: crossbeam_channel::Receiver>, -} +use crate::base::ControlFlow; -impl AudioCallback for MySdlAudio { - type Channel = i16; - - fn callback(&mut self, out: &mut [Self::Channel]) { - if self.audio_spec.format == AudioFormat::S16LSB { - if let Ok(samples) = self.audio_receiver.recv_timeout(Duration::from_millis(500)) { - out.copy_from_slice(&samples[..out.len()]); - } - } - } -} - -/// Trivially sends the core's audio data to the SDL audio subsystem for playback. pub struct SimpleSdl2AudioComponent { - sample_rate: f64, + src_freq: f64, audio_buffer: Vec, - audio_spec: AudioSpec, - audio_device: AudioDevice, - audio_sender: crossbeam_channel::Sender>, + queue: AudioQueue, +} + +/// Work around an idiosyncrasy of the sdl2 crate's AudioCVT API. +/// +/// It only deals in Vec, but from where I'm sitting it should really be abstracting away the +/// "u8" part if the rest of the API deals in the appropriate sample type. +pub(crate) fn resample(converter: &AudioCVT, mut samples: Vec) -> Vec { + let sample_size = size_of::(); + + let ptr = samples.as_mut_ptr() as *mut u8; + let length = samples.len() * sample_size; + let capacity = samples.capacity() * sample_size; + std::mem::forget(samples); + let samples_u8 = unsafe { Vec::from_raw_parts(ptr, length, capacity) }; + + let mut resampled_u8 = converter.convert(samples_u8); + + let ptr = resampled_u8.as_mut_ptr() as *mut C; + let length = resampled_u8.len() / sample_size; + let capacity = resampled_u8.capacity() / sample_size; + std::mem::forget(resampled_u8); + unsafe { Vec::from_raw_parts(ptr, length, capacity) } } impl RetroCallbacks for SimpleSdl2AudioComponent { fn audio_samples(&mut self, stereo_pcm: &[i16]) -> usize { self.audio_buffer.extend(stereo_pcm); - self.send_audio_samples(); stereo_pcm.len() / 2 } fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option { - self.sample_rate = av_info.timing.sample_rate; - // TODO: change/reinitialize - Some(true) + if Self::make_converter(av_info.timing.sample_rate, self.queue.spec()).is_ok() { + self.src_freq = av_info.timing.sample_rate; + Some(true) + } else { + Some(false) + } } } impl RetroComponent for SimpleSdl2AudioComponent { fn post_load_game(&mut self, _retro: &mut LibretroWrapper, _rom: &Path) -> Result<(), Box> { - self.audio_device.resume(); + self.queue.resume(); Ok(()) } + + fn post_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow { + if let Ok(converter) = Self::make_converter(self.src_freq, self.queue.spec()) { + let mut samples = std::mem::take(&mut self.audio_buffer); + samples = resample(&converter, samples); + self.audio_buffer = samples; + self.queue.queue(&self.audio_buffer); + self.audio_buffer.clear(); + } + ControlFlow::Continue + } } impl SimpleSdl2AudioComponent { - pub fn new(sdl_context: &mut Sdl, retro: &LibretroWrapper) -> Self { - let mut sample_rate = retro.get_system_av_info().timing.sample_rate; - - // HACK: some cores don't report this 'til we get an env call to set_system_av_info... - // which is too late for this constructor to pass along to SDL, so... - // less than ideal, but default to the old libsnes default value i guess. - if sample_rate == 0.0 { - sample_rate = 32040.0; - } - - let (audio_sender, audio_receiver) = crossbeam_channel::bounded(2); - + pub fn new(sdl_context: &mut Sdl, retro: &LibretroWrapper) -> Result> { let audio = sdl_context.audio().unwrap(); let desired_spec = AudioSpecDesired { - freq: Some(sample_rate.round() as i32), + freq: None, channels: Some(2), samples: None, }; - let mut audio_spec = None; - let audio_device = audio - .open_playback(None, &desired_spec, |spec| { - if spec.format != AudioFormat::s16_sys() { - panic!("unsupported audio format {:?}", spec.format); - } - audio_spec = Some(spec.clone()); - MySdlAudio { - audio_spec: spec, - audio_receiver, - } - }) - .unwrap(); - SimpleSdl2AudioComponent { - sample_rate, + let queue = AudioQueue::open_queue(&audio, None, &desired_spec)?; + + let mut src_freq = retro.get_system_av_info().timing.sample_rate; + // HACK: some cores don't report this 'til we get an env call to set_system_av_info, + // so we can just default to the old libsnes default value. + if src_freq == 0.0 { + src_freq = 32040.5; + } + + Ok(SimpleSdl2AudioComponent { + src_freq, audio_buffer: Default::default(), - audio_spec: audio_spec.unwrap(), - audio_device, - audio_sender, - } + queue, + }) } - pub fn silence_buffer(&self) { - let _ = self.audio_sender.try_send(vec![0; self.audio_spec.samples as usize * self.audio_spec.channels as usize]); - } - - fn send_audio_samples(&mut self) { - let stereo_samples = self.audio_spec.samples as usize * 2; - while self.audio_buffer.len() >= stereo_samples { - let remainder = self.audio_buffer.split_off(stereo_samples); - let msg = std::mem::replace(&mut self.audio_buffer, remainder); - let _ = self.audio_sender.try_send(msg); - } + fn make_converter(src_freq: f64, dest_spec: &AudioSpec) -> Result { + // note on the `* 64`: as long as the ratio between src_rate and dst_rate is right, + // we should be in the clear -- this is to make up for SDL not giving us floats for + // this, we can at least get some quasi-fixed-point precision going on... + AudioCVT::new( + AudioFormat::s16_sys(), + 2, + (src_freq * 64.0).round() as i32, + dest_spec.format, + dest_spec.channels, + dest_spec.freq * 64, + ) } } diff --git a/ferretro_components/src/provided/sdl2/audio_ratecontrol.rs b/ferretro_components/src/provided/sdl2/audio_ratecontrol.rs index 20bdfbd..f2947dd 100644 --- a/ferretro_components/src/provided/sdl2/audio_ratecontrol.rs +++ b/ferretro_components/src/provided/sdl2/audio_ratecontrol.rs @@ -1,39 +1,21 @@ use crate::prelude::*; use std::error::Error; -use std::mem::size_of; use std::path::Path; -use core::time::Duration; use sdl2::Sdl; -use sdl2::audio::{AudioCVT, AudioFormat, AudioFormatNum, AudioQueue, AudioSpec, AudioSpecDesired}; +use sdl2::audio::{AudioCVT, AudioFormat, AudioQueue, AudioSpec, AudioSpecDesired}; use crate::base::ControlFlow; +use super::audio::resample; + pub struct Sdl2RateControlledAudioComponent { src_freq: f64, audio_buffer: Vec, queue: AudioQueue, } -fn resample(converter: &AudioCVT, mut samples: Vec) -> Vec { - let sample_size = size_of::(); - - let ptr = samples.as_mut_ptr() as *mut u8; - let length = samples.len() * sample_size; - let capacity = samples.capacity() * sample_size; - std::mem::forget(samples); - let samples_u8 = unsafe { Vec::from_raw_parts(ptr, length, capacity) }; - - let mut resampled_u8 = converter.convert(samples_u8); - - let ptr = resampled_u8.as_mut_ptr() as *mut C; - let length = (resampled_u8.len() + (sample_size - 1)) / sample_size; - let capacity = (resampled_u8.capacity() + (sample_size - 1)) / sample_size; - std::mem::forget(resampled_u8); - unsafe { Vec::from_raw_parts(ptr, length, capacity) } -} - impl RetroCallbacks for Sdl2RateControlledAudioComponent { fn audio_samples(&mut self, stereo_pcm: &[i16]) -> usize { self.audio_buffer.extend(stereo_pcm); @@ -98,7 +80,7 @@ impl Sdl2RateControlledAudioComponent { // we should be in the clear -- this is to make up for SDL not giving us floats for // this, we can at least get some quasi-fixed-point precision going on... AudioCVT::new( - AudioFormat::S16LSB, + AudioFormat::s16_sys(), 2, (src_freq * 64.0).round() as i32, dest_spec.format, diff --git a/ferretro_components/src/provided/sdl2/opengl.rs b/ferretro_components/src/provided/sdl2/opengl.rs index 99c3dbe..212907b 100644 --- a/ferretro_components/src/provided/sdl2/opengl.rs +++ b/ferretro_components/src/provided/sdl2/opengl.rs @@ -152,7 +152,6 @@ impl RetroCallbacks for SimpleSdl2OpenglComponent { } fn set_geometry(&mut self, geom: &GameGeometry) -> Option { - eprintln!("set_geometry({:?})", geom); let _ = self.canvas.window_mut().set_size(geom.base_width, geom.base_height).ok()?; let _ = self.canvas.set_logical_size(geom.base_width, geom.base_height).ok()?; self.window_fbo = self.current_framebuffer_binding();