From 043806c713873c3b8c43ba94a94e14cea50b93a2 Mon Sep 17 00:00:00 2001 From: lif Date: Fri, 15 Nov 2019 23:03:30 -0800 Subject: [PATCH] audio support --- Cargo.toml | 5 +- src/bin/example.rs | 227 ++++++++++++++++++------- src/retro/{convert.rs => constants.rs} | 50 ------ src/retro/loading.rs | 4 +- src/retro/mod.rs | 2 +- src/retro/wrapper.rs | 65 ++++++- 6 files changed, 228 insertions(+), 125 deletions(-) rename src/retro/{convert.rs => constants.rs} (66%) diff --git a/Cargo.toml b/Cargo.toml index e2d77d0..c008a00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,11 @@ failure = "^0.1" libloading = "^0.5" num_enum = "^0.4" structopt = "^0.3" -sdl2 = { version = "*", optional = true, features = ["unsafe_textures"] } +sdl2 = { version = "^0.32", optional = true } +crossbeam-channel = { version = "^0.4", optional = true } [features] -with-sdl2 = ["sdl2"] +with-sdl2 = ["sdl2", "crossbeam-channel"] [[bin]] name = "example" diff --git a/src/bin/example.rs b/src/bin/example.rs index 8bb49d7..0149aa6 100644 --- a/src/bin/example.rs +++ b/src/bin/example.rs @@ -1,31 +1,41 @@ +extern crate crossbeam_channel; extern crate rustro; extern crate sdl2; use rustro::retro; -use rustro::retro::ffi::GameGeometry; -use rustro::retro::ffi::SystemAvInfo; +use rustro::retro::ffi::{GameGeometry, SystemAvInfo}; use std::ffi::CStr; -use std::io::Write; -use std::ops::Deref; -use std::path::{PathBuf, Path}; +use std::io::Read; +use std::path::{Path, PathBuf}; use std::pin::Pin; -use std::thread::sleep; use std::time::{Duration, Instant}; use structopt::StructOpt; -use sdl2::render::{WindowCanvas, Texture, TextureCreator}; -use sdl2::rect::Rect; +use sdl2::audio::{AudioCallback, AudioFormat, AudioSpec, AudioSpecDesired, AudioDevice}; use sdl2::event::Event; use sdl2::keyboard::Keycode; +use sdl2::rect::Rect; +use sdl2::render::{TextureCreator, WindowCanvas}; use sdl2::video::WindowContext; struct MyEmulator { retro: retro::wrapper::LibretroWrapper, av_info: SystemAvInfo, + + sdl_context: sdl2::Sdl, + + // video bits + canvas: WindowCanvas, pixel_format: sdl2::pixels::PixelFormatEnum, - texture: Option, + texture_creator: TextureCreator, + + // audio bits + audio_buffer: Vec, + audio_spec: AudioSpec, + audio_device: AudioDevice, + audio_sender: crossbeam_channel::Sender>, } impl MyEmulator { @@ -34,40 +44,143 @@ impl MyEmulator { let raw_retro = retro::loading::LibretroApi::from_library(lib).unwrap(); let retro = retro::wrapper::LibretroWrapper::from(raw_retro); println!("api version: {}", retro.as_ref().api_version()); - println!("name: {}", unsafe { CStr::from_ptr(retro.as_ref().get_system_info().library_name) }.to_string_lossy()); + println!( + "name: {}", + unsafe { CStr::from_ptr(retro.as_ref().get_system_info().library_name) } + .to_string_lossy() + ); let av_info = retro.as_ref().get_system_av_info(); - let pixel_format = sdl2::pixels::PixelFormatEnum::ABGR1555; - let mut emu = MyEmulator { retro, av_info, pixel_format, texture: None }; - let mut pin_emu = Box::pin(emu); + let sdl_context = sdl2::init().unwrap(); + + let (width, height) = (av_info.geometry.base_width, av_info.geometry.base_height); + let window = sdl_context + .video() + .unwrap() + .window("rust libretro", width, height) + .opengl() + .build() + .unwrap(); + + let canvas = window.into_canvas().build().unwrap(); + let texture_creator = canvas.texture_creator(); + + let (audio_sender, audio_receiver) = crossbeam_channel::bounded(2); + + let audio = sdl_context.audio().unwrap(); + let desired_spec = AudioSpecDesired { + freq: Some(av_info.timing.sample_rate.round() as i32), + channels: Some(2), + samples: None, + }; + let mut audio_spec = None; + let audio_device = audio + .open_playback(None, &desired_spec, |spec| { + println!("audio format {:?}", spec.format); + audio_spec = Some(spec.clone()); + MySdlAudio { + audio_spec: spec, + audio_receiver, + } + }) + .unwrap(); + + let emu = MyEmulator { + retro, + av_info, + sdl_context, + canvas, + pixel_format, + texture_creator, + audio_buffer: Default::default(), + audio_spec: audio_spec.unwrap(), + audio_device, + audio_sender, + }; + let mut pin_emu = Box::pin(emu); retro::wrapper::register_handler(pin_emu.as_mut()); pin_emu.retro.as_ref().init(); pin_emu } - pub fn run(&self) { - self.retro.as_ref().run(); - } + pub fn run(&mut self) { + self.audio_device.resume(); + let mut event_pump = self.sdl_context.event_pump().unwrap(); + 'running: loop { + let frame_begin = Instant::now(); - pub fn base_dimensions(&self) -> (u32, u32) { - (self.av_info.geometry.base_width, self.av_info.geometry.base_height) + for event in event_pump.poll_iter() { + match event { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), + .. + } => break 'running, + _ => {} + } + } + + // The rest of the game loop goes here... + self.retro.as_ref().run(); + self.canvas.present(); + + Duration::from_secs_f64(1.0 / self.av_info.timing.fps) + .checked_sub(frame_begin.elapsed()) + .map(std::thread::sleep); + } } pub fn load_game(&self, rom: impl AsRef) { - self.retro.as_ref().load_game(Some(rom.as_ref()), None, None); + let mut data = None; + let mut v = Vec::new(); + if let Ok(mut f) = std::fs::File::open(rom.as_ref()) { + if f.read_to_end(&mut v).is_ok() { + data = Some(v.as_ref()); + } + } + self.retro + .as_ref() + .load_game(Some(rom.as_ref()), data, None) + .unwrap(); + } + + 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); + } } } - -impl retro::convert::Handler for MyEmulator { +impl retro::wrapper::Handler for MyEmulator { fn video_refresh(&mut self, data: &[u8], width: u32, height: u32, pitch: u32) { let rect = Rect::new(0, 0, width, height); - if let Some(tex) = self.texture.as_mut() { - tex.update(rect, data, pitch as usize).unwrap(); + + if let Ok(mut tex) = + self.texture_creator + .create_texture_static(self.pixel_format, width, height) + { + if tex.update(rect, data, pitch as usize).is_ok() { + self.canvas.copy(&tex, None, None).unwrap(); + } } } + fn audio_sample(&mut self, left: i16, right: i16) { + self.audio_buffer.push(left); + self.audio_buffer.push(right); + self.send_audio_samples() + } + + fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { + self.audio_buffer.extend(stereo_pcm); + self.send_audio_samples(); + stereo_pcm.len() + } + fn set_pixel_format(&mut self, pix_fmt: retro::ffi::PixelFormat) -> bool { self.pixel_format = match pix_fmt { retro::ffi::PixelFormat::ARGB1555 => sdl2::pixels::PixelFormatEnum::RGB555, @@ -76,12 +189,34 @@ impl retro::convert::Handler for MyEmulator { }; true } + + fn set_system_av_info(&mut self, av_info: SystemAvInfo) -> bool { + self.av_info = av_info; + true + } + + fn set_geometry(&mut self, geom: GameGeometry) -> bool { + self.av_info.geometry = geom; + true + } } -impl Drop for MyEmulator { - fn drop(&mut self) { - if let Some(x) = self.texture.take() { - unsafe { x.destroy(); } +struct MySdlAudio { + audio_spec: AudioSpec, + audio_receiver: crossbeam_channel::Receiver>, +} + +impl AudioCallback for MySdlAudio { + type Channel = i16; + + fn callback(&mut self, out: &mut [Self::Channel]) { + match self.audio_spec.format { + AudioFormat::S16LSB | AudioFormat::U16LSB => { + if let Ok(samples) = self.audio_receiver.recv() { + out.copy_from_slice(&samples[..out.len()]); + } + } + _ => {} } } } @@ -90,43 +225,8 @@ pub fn main() -> failure::Fallible<()> { let opt: Opt = Opt::from_args(); let mut emu = MyEmulator::with_core(&opt.core); emu.load_game(&opt.rom); + emu.run(); - let sdl_context = sdl2::init().map_err(failure::err_msg)?; - let video_subsystem = sdl_context.video().map_err(failure::err_msg)?; - - let (width, height) = emu.base_dimensions(); - let window = video_subsystem.window("rust libretro", width, height) - .opengl() - .build()?; - - let mut canvas = window.into_canvas().build()?; - let texture_creator = canvas.texture_creator(); - emu.texture = texture_creator.create_texture_streaming(emu.pixel_format, width, height).ok(); - - let target_frame_time = Duration::from_secs_f64(1.0 / emu.av_info.timing.fps); - - let mut event_pump = sdl_context.event_pump().map_err(failure::err_msg)?; - 'running: loop { - let frame_begin = Instant::now(); - - for event in event_pump.poll_iter() { - match event { - Event::Quit {..} - | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => { - break 'running - }, - _ => {} - } - } - - // The rest of the game loop goes here... - emu.run(); - canvas.clear(); - canvas.copy(emu.texture.as_ref().unwrap(), None, Some(Rect::new(0, 0, width, height))).map_err(failure::err_msg)?; - canvas.present(); - - target_frame_time.checked_sub(frame_begin.elapsed()).map(sleep); - } Ok(()) } @@ -139,3 +239,4 @@ struct Opt { #[structopt(short, long, parse(from_os_str))] rom: PathBuf, } + diff --git a/src/retro/convert.rs b/src/retro/constants.rs similarity index 66% rename from src/retro/convert.rs rename to src/retro/constants.rs index 106a133..25d7bb1 100644 --- a/src/retro/convert.rs +++ b/src/retro/constants.rs @@ -1,6 +1,5 @@ use std::convert::{TryFrom, TryInto}; use std::os::raw::c_uint; -use std::path::PathBuf; use num_enum::{TryFromPrimitive, IntoPrimitive}; @@ -188,52 +187,3 @@ pub enum EnvCmd { GetLanguage = ENVIRONMENT_GET_LANGUAGE, // SetSerializationQuirks = ENVIRONMENT_SET_SERIALIZATION_QUIRKS, } - -pub trait Handler: Unpin + 'static { - fn video_refresh(&mut self, data: &[u8], width: c_uint, height: c_uint, pitch: c_uint) {} - fn audio_sample(&mut self, left: i16, right: i16) {} - fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { stereo_pcm.len() } - fn input_poll(&mut self) {} - fn input_state(&mut self, port: u32, device: Input, index: DeviceIndex) -> i16 { 0 } - - // -- environment callbacks -- - fn set_rotation(&mut self, rotation: EnvRotation) -> bool { false } - fn get_overscan(&mut self) -> Option { None } - fn get_can_dupe(&mut self) -> Option { None } - fn set_message(&mut self, message: Message) -> bool { false } - fn shutdown(&mut self) -> bool { false } - fn set_performance_level(&mut self, level: c_uint) -> bool { false } - fn get_system_directory(&mut self) -> Option { None } - fn set_pixel_format(&mut self, format: PixelFormat) -> bool { false } - fn set_input_descriptors(&mut self, input_descriptors: &[InputDescriptor]) -> bool { false } - fn set_keyboard_callback(&mut self, cb: KeyboardCallback) -> bool { false } - fn set_disk_control_interface(&mut self, cb: DiskControlCallback) -> bool { false } - fn set_hw_render(&mut self, hw_render_callback: HwRenderCallback) -> bool { false } - fn get_variable(&mut self) -> Option { None } - fn set_variables(&mut self, variables: &[EnvVariable]) -> bool { false } - fn get_variable_update(&mut self) -> Option { None } - fn set_support_no_game(&mut self, supports_no_game: bool) -> bool { false } - fn get_libretro_path(&mut self) -> Option { None } - fn set_frame_time_callback(&mut self, cb: FrameTimeCallback) -> bool { false } - fn set_audio_callback(&mut self, audio_callback: AudioCallback) -> bool { false } - fn get_rumble_interface(&mut self) -> Option { None } - fn get_input_device_capabilities(&mut self) -> Option { None } - fn get_sensor_interface(&mut self) -> Option { None } - fn get_camera_interface(&mut self) -> Option { None } - fn get_log_interface(&mut self) -> Option { None } - fn get_perf_interface(&mut self) -> Option { None } - fn get_location_interface(&mut self) -> Option { None } - fn get_core_assets_directory(&mut self) -> Option { None } - fn get_save_directory(&mut self) -> Option { None } - fn set_system_av_info(&mut self, system_av_info: SystemAvInfo) -> bool { false } - fn set_proc_address_callback(&mut self, cb: GetProcAddressInterface) -> bool { false } - fn set_subsystem_info(&mut self, subsystem_info: SubsystemInfo) -> bool { false } - fn set_controller_info(&mut self, controller_info: ControllerInfo) -> bool { false } - fn set_memory_maps(&mut self, memory_map: MemoryMap) -> bool { false } - fn set_geometry(&mut self, game_geometry: GameGeometry) -> bool { false } - fn get_username(&mut self) -> Option { None } - fn get_language(&mut self) -> Option { None } - // fn set_serialization_quirks(&mut self, quirks: &mut u64) -> bool { false } -} - -// impl Unpin for T {} diff --git a/src/retro/loading.rs b/src/retro/loading.rs index eb05a79..84bb191 100644 --- a/src/retro/loading.rs +++ b/src/retro/loading.rs @@ -7,7 +7,7 @@ use libloading; use super::ffi::*; pub struct LibretroApi { - lib: libloading::Library, // for tying our lifetime to its own + _lib: libloading::Library, // for tying our lifetime to its own pub core_api: CoreAPI, } @@ -41,7 +41,7 @@ impl LibretroApi { retro_get_memory_data: *lib.get(b"retro_get_memory_data")?, retro_get_memory_size: *lib.get(b"retro_get_memory_size")?, }; - Ok(LibretroApi { lib, core_api }) + Ok(LibretroApi { _lib: lib, core_api }) } } /// set_environment() must be called before init(). diff --git a/src/retro/mod.rs b/src/retro/mod.rs index 5234288..36c8e8b 100644 --- a/src/retro/mod.rs +++ b/src/retro/mod.rs @@ -1,4 +1,4 @@ -pub mod convert; +pub mod constants; #[allow(non_camel_case_types, non_upper_case_globals, non_snake_case, dead_code)] pub mod ffi; pub mod loading; diff --git a/src/retro/wrapper.rs b/src/retro/wrapper.rs index 107c47b..aa6de5e 100644 --- a/src/retro/wrapper.rs +++ b/src/retro/wrapper.rs @@ -4,20 +4,67 @@ use core::slice::from_raw_parts; use std::ffi::CString; use std::os::raw::{c_uint, c_char}; -use std::ops::DerefMut; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::pin::Pin; -use libretro_sys::{Message, PixelFormat, SystemAvInfo, GameGeometry}; use num_enum::TryFromPrimitive; -use super::convert::*; +use super::constants::*; +use super::ffi::*; use super::loading::*; static mut CB_SINGLETON: StaticCallbacks = StaticCallbacks { handler: None, }; +#[allow(unused)] +pub trait Handler: Unpin + 'static { + fn video_refresh(&mut self, data: &[u8], width: c_uint, height: c_uint, pitch: c_uint) {} + fn audio_sample(&mut self, left: i16, right: i16) {} + fn audio_sample_batch(&mut self, stereo_pcm: &[i16]) -> usize { stereo_pcm.len() } + fn input_poll(&mut self) {} + fn input_state(&mut self, port: u32, device: Input, index: DeviceIndex) -> i16 { 0 } + + // -- environment callbacks -- + fn set_rotation(&mut self, rotation: EnvRotation) -> bool { false } + fn get_overscan(&mut self) -> Option { None } + fn get_can_dupe(&mut self) -> Option { None } + fn set_message(&mut self, message: Message) -> bool { false } + fn shutdown(&mut self) -> bool { false } + fn set_performance_level(&mut self, level: c_uint) -> bool { false } + fn get_system_directory(&mut self) -> Option { None } + fn set_pixel_format(&mut self, format: PixelFormat) -> bool { false } + fn set_input_descriptors(&mut self, input_descriptors: &[InputDescriptor]) -> bool { false } + fn set_keyboard_callback(&mut self, cb: KeyboardCallback) -> bool { false } + fn set_disk_control_interface(&mut self, cb: DiskControlCallback) -> bool { false } + fn set_hw_render(&mut self, hw_render_callback: HwRenderCallback) -> bool { false } + fn get_variable(&mut self) -> Option { None } + fn set_variables(&mut self, variables: &[EnvVariable]) -> bool { false } + fn get_variable_update(&mut self) -> Option { None } + fn set_support_no_game(&mut self, supports_no_game: bool) -> bool { false } + fn get_libretro_path(&mut self) -> Option { None } + fn set_frame_time_callback(&mut self, cb: FrameTimeCallback) -> bool { false } + fn set_audio_callback(&mut self, audio_callback: AudioCallback) -> bool { false } + fn get_rumble_interface(&mut self) -> Option { None } + fn get_input_device_capabilities(&mut self) -> Option { None } + fn get_sensor_interface(&mut self) -> Option { None } + fn get_camera_interface(&mut self) -> Option { None } + fn get_log_interface(&mut self) -> Option { None } + fn get_perf_interface(&mut self) -> Option { None } + fn get_location_interface(&mut self) -> Option { None } + fn get_core_assets_directory(&mut self) -> Option { None } + fn get_save_directory(&mut self) -> Option { None } + fn set_system_av_info(&mut self, system_av_info: SystemAvInfo) -> bool { false } + fn set_proc_address_callback(&mut self, cb: GetProcAddressInterface) -> bool { false } + fn set_subsystem_info(&mut self, subsystem_info: SubsystemInfo) -> bool { false } + fn set_controller_info(&mut self, controller_info: ControllerInfo) -> bool { false } + fn set_memory_maps(&mut self, memory_map: MemoryMap) -> bool { false } + fn set_geometry(&mut self, game_geometry: GameGeometry) -> bool { false } + fn get_username(&mut self) -> Option { None } + fn get_language(&mut self) -> Option { None } + // fn set_serialization_quirks(&mut self, quirks: &mut u64) -> bool { false } +} + #[derive(Default)] struct StaticCallbacks { handler: Option>, @@ -48,7 +95,7 @@ impl StaticCallbacks { T::try_from_primitive(number).ok() } fn environment_cb_inner(cmd: u32, data: *mut c_void) -> Option { - let mut handler = unsafe { CB_SINGLETON.handler.as_mut() }?; + let handler = unsafe { CB_SINGLETON.handler.as_mut() }?; match cmd.try_into().ok()? { EnvCmd::SetRotation => handler.set_rotation(Self::enum_from_void(data)?), EnvCmd::GetOverscan => Self::clone_into_void(data, &handler.get_overscan()?)?, @@ -119,7 +166,11 @@ impl StaticCallbacks { match CB_SINGLETON.handler.as_mut() { Some(cb) => match data.is_null() { true => 0, - false => cb.audio_sample_batch(from_raw_parts(data, frames)), + false => { + let len = frames * 2; // stereo + let result = cb.audio_sample_batch(from_raw_parts(data, len)); + result / 2 + }, }, None => 0, } @@ -176,7 +227,7 @@ impl Drop for LibretroWrapper { // the library wrapper itself we're good (we wipe our 'static references on drop() too) pub fn register_handler(handler: Pin<&'_ mut (dyn Handler + '_)>) { unsafe { - let mut ptr = handler.get_unchecked_mut() as *mut dyn Handler; + let ptr = handler.get_unchecked_mut() as *mut dyn Handler; CB_SINGLETON.handler.replace(Pin::new_unchecked(ptr.as_mut().unwrap())); } }