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 }
|
ffmpeg-next = { version = "4.3.8", optional = true }
|
||||||
sdl2 = { version = "0.35.1", optional = true, features = ["gfx"] }
|
sdl2 = { version = "0.35.1", optional = true, features = ["gfx"] }
|
||||||
gl = { version = "0.14", optional = true }
|
gl = { version = "0.14", optional = true }
|
||||||
|
cpal = { version = "0.13.3", optional = true }
|
||||||
|
ringbuf = { version = "0.2", optional = true }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -22,7 +24,8 @@ structopt = "0.3"
|
||||||
[features]
|
[features]
|
||||||
static = ["ferretro_base/static"]
|
static = ["ferretro_base/static"]
|
||||||
ffmpeg_comp = ["ffmpeg-next"]
|
ffmpeg_comp = ["ffmpeg-next"]
|
||||||
sdl2_comp = ["sdl2", "gl"]
|
sdl2_comp = ["sdl2", "gl", "ringbuf"]
|
||||||
|
cpal_comp = ["cpal", "ringbuf"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "multifunction_emulator"
|
name = "multifunction_emulator"
|
||||||
|
|
|
@ -13,6 +13,8 @@ use ferretro_components::provided::{
|
||||||
sdl2::*,
|
sdl2::*,
|
||||||
stdlib::*,
|
stdlib::*,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "cpal_comp")]
|
||||||
|
use ferretro_components::provided::cpal::CpalAudioComponent;
|
||||||
#[cfg(feature = "ffmpeg_comp")]
|
#[cfg(feature = "ffmpeg_comp")]
|
||||||
use ferretro_components::provided::ffmpeg::FfmpegComponent;
|
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(SimpleSdl2KeyboardComponent::new(&mut sdl_context)?)?;
|
||||||
emu.register_component(SimpleSdl2GamepadComponent::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")]
|
#[cfg(feature = "sdl2_comp")]
|
||||||
pub mod sdl2;
|
pub mod sdl2;
|
||||||
|
|
||||||
|
#[cfg(feature = "cpal_comp")]
|
||||||
|
pub mod cpal;
|
||||||
|
|
||||||
pub mod stdlib;
|
pub mod stdlib;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@ use sdl2::audio::{AudioCVT, AudioFormat, AudioFormatNum, AudioQueue, AudioSpec,
|
||||||
|
|
||||||
use crate::base::ControlFlow;
|
use crate::base::ControlFlow;
|
||||||
|
|
||||||
pub struct SimpleSdl2AudioComponent {
|
pub struct SimpleSdl2AudioQueueComponent {
|
||||||
sdl_audio: sdl2::AudioSubsystem,
|
sdl_audio: sdl2::AudioSubsystem,
|
||||||
src_freq: f64,
|
src_freq: f64,
|
||||||
audio_buffer: Vec<i16>,
|
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) }
|
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) {
|
fn video_refresh(&mut self, _frame: &VideoFrame) {
|
||||||
self.started = true;
|
self.started = true;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +67,7 @@ impl RetroCallbacks for SimpleSdl2AudioComponent {
|
||||||
|
|
||||||
fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option<bool> {
|
fn set_system_av_info(&mut self, av_info: &SystemAvInfo) -> Option<bool> {
|
||||||
if let Some(queue) = &self.queue {
|
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;
|
self.src_freq = av_info.timing.sample_rate;
|
||||||
Some(true)
|
Some(true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -66,8 +79,8 @@ impl RetroCallbacks for SimpleSdl2AudioComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RetroComponent for SimpleSdl2AudioComponent {
|
impl RetroComponent for SimpleSdl2AudioQueueComponent {
|
||||||
fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> Result<(), Box<dyn Error + Send + Sync>> {
|
fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> crate::base::Result<()> {
|
||||||
let timing = retro.get_system_av_info().timing;
|
let timing = retro.get_system_av_info().timing;
|
||||||
self.src_freq = timing.sample_rate;
|
self.src_freq = timing.sample_rate;
|
||||||
|
|
||||||
|
@ -79,7 +92,7 @@ impl RetroComponent for SimpleSdl2AudioComponent {
|
||||||
samples: Some((2.0 * samples_per_frame).ceil() as u16),
|
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() {
|
if queue.spec().freq != desired_spec.freq.unwrap() {
|
||||||
self.must_resample = true;
|
self.must_resample = true;
|
||||||
eprintln!("warning: using naive resampling");
|
eprintln!("warning: using naive resampling");
|
||||||
|
@ -95,7 +108,7 @@ impl RetroComponent for SimpleSdl2AudioComponent {
|
||||||
if self.src_freq != 0.0 {
|
if self.src_freq != 0.0 {
|
||||||
let queue = self.queue.as_mut().unwrap();
|
let queue = self.queue.as_mut().unwrap();
|
||||||
if self.must_resample {
|
if self.must_resample {
|
||||||
match Self::make_converter(self.src_freq, queue.spec()) {
|
match make_converter(self.src_freq, queue.spec()) {
|
||||||
Ok(converter) => {
|
Ok(converter) => {
|
||||||
if !self.audio_buffer.is_empty() {
|
if !self.audio_buffer.is_empty() {
|
||||||
let mut samples = std::mem::take(&mut self.audio_buffer);
|
let mut samples = std::mem::take(&mut self.audio_buffer);
|
||||||
|
@ -122,11 +135,11 @@ impl RetroComponent for SimpleSdl2AudioComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SimpleSdl2AudioComponent {
|
impl SimpleSdl2AudioQueueComponent {
|
||||||
pub fn new(sdl_context: &mut Sdl) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
pub fn new(sdl_context: &mut Sdl) -> crate::base::Result<Self> {
|
||||||
let sdl_audio = sdl_context.audio()?;
|
let sdl_audio = sdl_context.audio()?;
|
||||||
|
|
||||||
Ok(SimpleSdl2AudioComponent {
|
Ok(SimpleSdl2AudioQueueComponent {
|
||||||
sdl_audio,
|
sdl_audio,
|
||||||
src_freq: 32040.5, // nod to the old libsnes default til load_game or set_system_av_info
|
src_freq: 32040.5, // nod to the old libsnes default til load_game or set_system_av_info
|
||||||
audio_buffer: Default::default(),
|
audio_buffer: Default::default(),
|
||||||
|
@ -135,18 +148,4 @@ impl SimpleSdl2AudioComponent {
|
||||||
started: false,
|
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;
|
||||||
mod audio_ratecontrol;
|
mod audio_ratecontrol;
|
||||||
|
mod audio_thread;
|
||||||
mod canvas;
|
mod canvas;
|
||||||
mod fps;
|
mod fps;
|
||||||
mod gamepad;
|
mod gamepad;
|
||||||
|
@ -10,7 +11,8 @@ mod keyboard;
|
||||||
mod opengl;
|
mod opengl;
|
||||||
mod surface;
|
mod surface;
|
||||||
|
|
||||||
pub use audio::SimpleSdl2AudioComponent;
|
pub use audio::SimpleSdl2AudioQueueComponent;
|
||||||
|
pub use audio_thread::SimpleSdl2AudioThreadComponent;
|
||||||
pub use audio_ratecontrol::Sdl2RateControlledAudioComponent;
|
pub use audio_ratecontrol::Sdl2RateControlledAudioComponent;
|
||||||
pub use canvas::SimpleSdl2CanvasComponent;
|
pub use canvas::SimpleSdl2CanvasComponent;
|
||||||
pub use fps::SimpleSdl2FramerateLimitComponent;
|
pub use fps::SimpleSdl2FramerateLimitComponent;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -32,12 +33,14 @@ impl RetroComponent for LocalFileSaveComponent {
|
||||||
let final_sram = retro.get_memory(MEMORY_SAVE_RAM);
|
let final_sram = retro.get_memory(MEMORY_SAVE_RAM);
|
||||||
if &self.initial_sram != final_sram {
|
if &self.initial_sram != final_sram {
|
||||||
let save = self.save_path();
|
let save = self.save_path();
|
||||||
match File::create(&save) {
|
if save.file_name().map(OsStr::len).unwrap_or_default() != 0 {
|
||||||
Ok(mut f) => if let Err(e) = f.write(final_sram) {
|
match File::create(&save) {
|
||||||
eprintln!("Couldn't write save to {:?}: {:?}", save, e);
|
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);
|
Err(e) => {
|
||||||
|
eprintln!("Couldn't (over)write {:?}: {:?}", save, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue