we bringing back threaded audio components babey
This commit is contained in:
parent
df21f69128
commit
5ad81c3c5e
|
@ -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"
|
||||
|
|
|
@ -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))?;
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -6,4 +6,7 @@ pub mod ffmpeg;
|
|||
#[cfg(feature = "sdl2_comp")]
|
||||
pub mod sdl2;
|
||||
|
||||
#[cfg(feature = "cpal_comp")]
|
||||
pub mod cpal;
|
||||
|
||||
pub mod stdlib;
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue