From 5ad81c3c5eb42492e1be1c4e0ffd29a9fc419126 Mon Sep 17 00:00:00 2001 From: lifning <> Date: Fri, 17 Dec 2021 02:47:08 -0800 Subject: [PATCH] we bringing back threaded audio components babey --- ferretro_components/Cargo.toml | 5 +- .../examples/multifunction_emulator.rs | 11 ++- ferretro_components/src/provided/cpal/mod.rs | 74 +++++++++++++++ ferretro_components/src/provided/mod.rs | 3 + .../src/provided/sdl2/audio.rs | 49 +++++----- .../src/provided/sdl2/audio_thread.rs | 92 +++++++++++++++++++ ferretro_components/src/provided/sdl2/mod.rs | 4 +- .../src/provided/stdlib/saves.rs | 15 +-- 8 files changed, 219 insertions(+), 34 deletions(-) create mode 100644 ferretro_components/src/provided/cpal/mod.rs create mode 100644 ferretro_components/src/provided/sdl2/audio_thread.rs diff --git a/ferretro_components/Cargo.toml b/ferretro_components/Cargo.toml index f99f2e0..1f92116 100644 --- a/ferretro_components/Cargo.toml +++ b/ferretro_components/Cargo.toml @@ -14,6 +14,8 @@ num_enum = "0.4" ffmpeg-next = { version = "4.3.8", optional = true } sdl2 = { version = "0.35.1", optional = true, features = ["gfx"] } gl = { version = "0.14", optional = true } +cpal = { version = "0.13.3", optional = true } +ringbuf = { version = "0.2", optional = true } tempfile = "3" [dev-dependencies] @@ -22,7 +24,8 @@ structopt = "0.3" [features] static = ["ferretro_base/static"] ffmpeg_comp = ["ffmpeg-next"] -sdl2_comp = ["sdl2", "gl"] +sdl2_comp = ["sdl2", "gl", "ringbuf"] +cpal_comp = ["cpal", "ringbuf"] [[example]] name = "multifunction_emulator" diff --git a/ferretro_components/examples/multifunction_emulator.rs b/ferretro_components/examples/multifunction_emulator.rs index a848f37..d49908e 100644 --- a/ferretro_components/examples/multifunction_emulator.rs +++ b/ferretro_components/examples/multifunction_emulator.rs @@ -13,6 +13,8 @@ use ferretro_components::provided::{ sdl2::*, stdlib::*, }; +#[cfg(feature = "cpal_comp")] +use ferretro_components::provided::cpal::CpalAudioComponent; #[cfg(feature = "ffmpeg_comp")] use ferretro_components::provided::ffmpeg::FfmpegComponent; @@ -98,7 +100,14 @@ pub fn main() -> Result<(), Box> { } } - emu.register_component(SimpleSdl2AudioComponent::new(&mut sdl_context)?)?; + #[cfg(not(feature = "cpal_comp"))] + { + emu.register_component(SimpleSdl2AudioThreadComponent::new(&mut sdl_context)?)?; + } + #[cfg(feature = "cpal_comp")] + { + emu.register_component(CpalAudioComponent::default())?; + } emu.register_component(SimpleSdl2KeyboardComponent::new(&mut sdl_context)?)?; emu.register_component(SimpleSdl2GamepadComponent::new(&mut sdl_context))?; diff --git a/ferretro_components/src/provided/cpal/mod.rs b/ferretro_components/src/provided/cpal/mod.rs new file mode 100644 index 0000000..4277759 --- /dev/null +++ b/ferretro_components/src/provided/cpal/mod.rs @@ -0,0 +1,74 @@ +use crate::prelude::*; + +use std::path::Path; + +use cpal::{SampleFormat, SampleRate, Stream}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use ringbuf::{Producer, RingBuffer}; + +use ferretro_base::retro::ffi::SystemTiming; + +#[derive(Default)] +pub struct CpalAudioComponent { + stream: Option, + writer: Option>, +} + +impl RetroCallbacks for CpalAudioComponent { + fn audio_samples(&mut self, stereo_pcm: &[i16]) -> usize { + if let Some(writer) = &mut self.writer { + writer.push_slice(stereo_pcm); + } + stereo_pcm.len() / 2 + } + + fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option { + let (stream, writer) = Self::new_stream_from_timing(&av_info.timing).ok()?; + self.stream = Some(stream); + self.writer = Some(writer); + Some(true) + } +} + +impl RetroComponent for CpalAudioComponent { + fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> crate::base::Result<()> { + let timing = retro.get_system_av_info().timing; + + let (stream, writer) = Self::new_stream_from_timing(&timing)?; + self.stream = Some(stream); + self.writer = Some(writer); + + Ok(()) + } +} + +impl CpalAudioComponent { + fn new_stream_from_timing(timing: &SystemTiming) -> crate::base::Result<(Stream, Producer)> { + let samples_per_frame = timing.sample_rate / timing.fps; + let buf_len = samples_per_frame.ceil() as usize * 4; + + let host = cpal::default_host(); + let device = host.default_output_device().ok_or("cpal: no output device available")?; + let supported_config = device + .supported_output_configs()? + .filter(|cfg| cfg.channels() == 2 && cfg.sample_format() == SampleFormat::I16) + .next() + .ok_or("cpal: no supported stream config for signed 16-bit stereo")? + .with_sample_rate(SampleRate(timing.sample_rate.round() as u32)); + + let (writer, mut reader) = RingBuffer::new(buf_len).split(); + + let stream = device.build_output_stream( + &supported_config.config(), + move |data: &mut [i16], _output_callback_info| { + reader.pop_slice(data); + }, + move |err| { + eprintln!("cpal: {:?}", err); + } + )?; + + stream.play()?; + Ok((stream, writer)) + } +} diff --git a/ferretro_components/src/provided/mod.rs b/ferretro_components/src/provided/mod.rs index f59078a..92750ce 100644 --- a/ferretro_components/src/provided/mod.rs +++ b/ferretro_components/src/provided/mod.rs @@ -6,4 +6,7 @@ pub mod ffmpeg; #[cfg(feature = "sdl2_comp")] pub mod sdl2; +#[cfg(feature = "cpal_comp")] +pub mod cpal; + pub mod stdlib; diff --git a/ferretro_components/src/provided/sdl2/audio.rs b/ferretro_components/src/provided/sdl2/audio.rs index 3023024..9c121fd 100644 --- a/ferretro_components/src/provided/sdl2/audio.rs +++ b/ferretro_components/src/provided/sdl2/audio.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -use std::error::Error; use std::mem::size_of; use std::path::Path; @@ -9,7 +8,7 @@ use sdl2::audio::{AudioCVT, AudioFormat, AudioFormatNum, AudioQueue, AudioSpec, use crate::base::ControlFlow; -pub struct SimpleSdl2AudioComponent { +pub struct SimpleSdl2AudioQueueComponent { sdl_audio: sdl2::AudioSubsystem, src_freq: f64, audio_buffer: Vec, @@ -40,7 +39,21 @@ pub(crate) fn resample(converter: &AudioCVT, mut samples: Vec unsafe { Vec::from_raw_parts(ptr, length, capacity) } } -impl RetroCallbacks for SimpleSdl2AudioComponent { +pub(crate) fn make_converter(src_freq: f64, dest_spec: &AudioSpec) -> Result { + // note on the `* 16`: 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 * 16.0).round() as i32, + dest_spec.format, + dest_spec.channels, + dest_spec.freq * 16, + ) +} + +impl RetroCallbacks for SimpleSdl2AudioQueueComponent { fn video_refresh(&mut self, _frame: &VideoFrame) { self.started = true; } @@ -54,7 +67,7 @@ impl RetroCallbacks for SimpleSdl2AudioComponent { fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option { if let Some(queue) = &self.queue { - if Self::make_converter(av_info.timing.sample_rate, queue.spec()).is_ok() { + if make_converter(av_info.timing.sample_rate, queue.spec()).is_ok() { self.src_freq = av_info.timing.sample_rate; Some(true) } else { @@ -66,8 +79,8 @@ impl RetroCallbacks for SimpleSdl2AudioComponent { } } -impl RetroComponent for SimpleSdl2AudioComponent { - fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> Result<(), Box> { +impl RetroComponent for SimpleSdl2AudioQueueComponent { + fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> crate::base::Result<()> { let timing = retro.get_system_av_info().timing; self.src_freq = timing.sample_rate; @@ -79,7 +92,7 @@ impl RetroComponent for SimpleSdl2AudioComponent { samples: Some((2.0 * samples_per_frame).ceil() as u16), }; - let queue = AudioQueue::open_queue(&self.sdl_audio, None, &desired_spec)?; + let queue = self.sdl_audio.open_queue(None, &desired_spec)?; if queue.spec().freq != desired_spec.freq.unwrap() { self.must_resample = true; eprintln!("warning: using naive resampling"); @@ -95,7 +108,7 @@ impl RetroComponent for SimpleSdl2AudioComponent { if self.src_freq != 0.0 { let queue = self.queue.as_mut().unwrap(); if self.must_resample { - match Self::make_converter(self.src_freq, queue.spec()) { + match make_converter(self.src_freq, queue.spec()) { Ok(converter) => { if !self.audio_buffer.is_empty() { let mut samples = std::mem::take(&mut self.audio_buffer); @@ -122,11 +135,11 @@ impl RetroComponent for SimpleSdl2AudioComponent { } } -impl SimpleSdl2AudioComponent { - pub fn new(sdl_context: &mut Sdl) -> Result> { +impl SimpleSdl2AudioQueueComponent { + pub fn new(sdl_context: &mut Sdl) -> crate::base::Result { let sdl_audio = sdl_context.audio()?; - Ok(SimpleSdl2AudioComponent { + Ok(SimpleSdl2AudioQueueComponent { sdl_audio, src_freq: 32040.5, // nod to the old libsnes default til load_game or set_system_av_info audio_buffer: Default::default(), @@ -135,18 +148,4 @@ impl SimpleSdl2AudioComponent { started: false, }) } - - fn make_converter(src_freq: f64, dest_spec: &AudioSpec) -> Result { - // note on the `* 16`: 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 * 16.0).round() as i32, - dest_spec.format, - dest_spec.channels, - dest_spec.freq * 16, - ) - } } diff --git a/ferretro_components/src/provided/sdl2/audio_thread.rs b/ferretro_components/src/provided/sdl2/audio_thread.rs new file mode 100644 index 0000000..a1c9f5c --- /dev/null +++ b/ferretro_components/src/provided/sdl2/audio_thread.rs @@ -0,0 +1,92 @@ +use crate::prelude::*; + +use std::path::Path; + +use sdl2::Sdl; +use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired}; +use ringbuf::{Consumer, Producer, RingBuffer}; +use ferretro_base::retro::ffi::SystemTiming; + +pub struct SimpleSdl2AudioThreadComponent { + sdl_audio: sdl2::AudioSubsystem, + device: Option>, + writer: Option>, +} + +struct MySdlAudio { + must_resample: bool, + reader: Consumer, +} + +impl AudioCallback for MySdlAudio { + type Channel = i16; + + fn callback(&mut self, data: &mut [Self::Channel]) { + if self.must_resample { + todo!("correct software resampling") + } + self.reader.pop_slice(data); + } +} + +impl RetroCallbacks for SimpleSdl2AudioThreadComponent { + fn audio_samples(&mut self, stereo_pcm: &[i16]) -> usize { + if let Some(writer) = &mut self.writer { + writer.push_slice(stereo_pcm); + } + stereo_pcm.len() / 2 + } + + fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option { + let timing = &av_info.timing; + let (device, writer) = Self::new_device_from_timing(&self.sdl_audio, timing).ok()?; + self.device = Some(device); + self.writer = Some(writer); + Some(true) + } +} + +impl RetroComponent for SimpleSdl2AudioThreadComponent { + fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> crate::base::Result<()> { + let timing = &retro.get_system_av_info().timing; + let (device, writer) = Self::new_device_from_timing(&self.sdl_audio, timing)?; + self.device = Some(device); + self.writer = Some(writer); + Ok(()) + } +} + +impl SimpleSdl2AudioThreadComponent { + pub fn new(sdl_context: &mut Sdl) -> crate::base::Result { + Ok(SimpleSdl2AudioThreadComponent { + sdl_audio: sdl_context.audio()?, + device: None, + writer: None, + }) + } + + fn new_device_from_timing(audio: &sdl2::AudioSubsystem, timing: &SystemTiming) -> crate::base::Result<(AudioDevice, Producer)> { + let samples_per_frame = timing.sample_rate / timing.fps; + let buf_len = samples_per_frame.ceil() as usize * 4; + let (writer, reader) = RingBuffer::new(buf_len).split(); + + let desired_spec = AudioSpecDesired { + freq: Some(timing.sample_rate.round() as i32), + channels: Some(2), + samples: Some((samples_per_frame.ceil() * 1.0) as u16), + }; + + let device = audio.open_playback( + None, + &desired_spec, + move |spec| { + let must_resample = { spec.freq != desired_spec.freq.unwrap() }; + MySdlAudio { + must_resample, // todo: include freqs from & to + reader, + } + })?; + device.resume(); + Ok((device, writer)) + } +} diff --git a/ferretro_components/src/provided/sdl2/mod.rs b/ferretro_components/src/provided/sdl2/mod.rs index d722719..54b1991 100644 --- a/ferretro_components/src/provided/sdl2/mod.rs +++ b/ferretro_components/src/provided/sdl2/mod.rs @@ -3,6 +3,7 @@ mod audio; mod audio_ratecontrol; +mod audio_thread; mod canvas; mod fps; mod gamepad; @@ -10,7 +11,8 @@ mod keyboard; mod opengl; mod surface; -pub use audio::SimpleSdl2AudioComponent; +pub use audio::SimpleSdl2AudioQueueComponent; +pub use audio_thread::SimpleSdl2AudioThreadComponent; pub use audio_ratecontrol::Sdl2RateControlledAudioComponent; pub use canvas::SimpleSdl2CanvasComponent; pub use fps::SimpleSdl2FramerateLimitComponent; diff --git a/ferretro_components/src/provided/stdlib/saves.rs b/ferretro_components/src/provided/stdlib/saves.rs index 589c827..f5fc768 100644 --- a/ferretro_components/src/provided/stdlib/saves.rs +++ b/ferretro_components/src/provided/stdlib/saves.rs @@ -1,4 +1,5 @@ use std::error::Error; +use std::ffi::OsStr; use std::fs::File; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; @@ -32,12 +33,14 @@ impl RetroComponent for LocalFileSaveComponent { let final_sram = retro.get_memory(MEMORY_SAVE_RAM); if &self.initial_sram != final_sram { let save = self.save_path(); - match File::create(&save) { - Ok(mut f) => if let Err(e) = f.write(final_sram) { - eprintln!("Couldn't write save to {:?}: {:?}", save, e); - } - Err(e) => { - eprintln!("Couldn't (over)write {:?}: {:?}", save, e); + if save.file_name().map(OsStr::len).unwrap_or_default() != 0 { + match File::create(&save) { + Ok(mut f) => if let Err(e) = f.write(final_sram) { + eprintln!("Couldn't write save to {:?}: {:?}", save, e); + } + Err(e) => { + eprintln!("Couldn't (over)write {:?}: {:?}", save, e); + } } } }