we bringing back threaded audio components babey

This commit is contained in:
lifning 2021-12-17 02:47:08 -08:00
parent df21f69128
commit 5ad81c3c5e
8 changed files with 219 additions and 34 deletions

View File

@ -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"

View File

@ -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<dyn std::error::Error + Send + Sync>> {
}
}
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))?;

View File

@ -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<Stream>,
writer: Option<Producer<i16>>,
}
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<bool> {
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<i16>)> {
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))
}
}

View File

@ -6,4 +6,7 @@ pub mod ffmpeg;
#[cfg(feature = "sdl2_comp")]
pub mod sdl2;
#[cfg(feature = "cpal_comp")]
pub mod cpal;
pub mod stdlib;

View File

@ -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<i16>,
@ -40,7 +39,21 @@ pub(crate) fn resample<C: AudioFormatNum>(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<AudioCVT, String> {
// 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<bool> {
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<dyn Error + Send + Sync>> {
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<Self, Box<dyn std::error::Error + Send + Sync>> {
impl SimpleSdl2AudioQueueComponent {
pub fn new(sdl_context: &mut Sdl) -> crate::base::Result<Self> {
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<AudioCVT, String> {
// 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,
)
}
}

View File

@ -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<AudioDevice<MySdlAudio>>,
writer: Option<Producer<i16>>,
}
struct MySdlAudio {
must_resample: bool,
reader: Consumer<i16>,
}
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<bool> {
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<Self> {
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<MySdlAudio>, Producer<i16>)> {
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))
}
}

View File

@ -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;

View File

@ -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);
}
}
}
}