audio support

This commit is contained in:
lif 2019-11-15 23:03:30 -08:00
parent e183627018
commit 043806c713
6 changed files with 228 additions and 125 deletions

View File

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

View File

@ -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>,
texture_creator: TextureCreator<WindowContext>,
// audio bits
audio_buffer: Vec<i16>,
audio_spec: AudioSpec,
audio_device: AudioDevice<MySdlAudio>,
audio_sender: crossbeam_channel::Sender<Vec<i16>>,
}
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<Path>) {
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<Vec<i16>>,
}
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,
}

View File

@ -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<bool> { None }
fn get_can_dupe(&mut self) -> Option<bool> { 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<PathBuf> { 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<EnvVariable> { None }
fn set_variables(&mut self, variables: &[EnvVariable]) -> bool { false }
fn get_variable_update(&mut self) -> Option<bool> { None }
fn set_support_no_game(&mut self, supports_no_game: bool) -> bool { false }
fn get_libretro_path(&mut self) -> Option<PathBuf> { 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<RumbleInterface> { None }
fn get_input_device_capabilities(&mut self) -> Option<u64> { None }
fn get_sensor_interface(&mut self) -> Option<SensorInterface> { None }
fn get_camera_interface(&mut self) -> Option<CameraCallback> { None }
fn get_log_interface(&mut self) -> Option<LogCallback> { None }
fn get_perf_interface(&mut self) -> Option<PerfCallback> { None }
fn get_location_interface(&mut self) -> Option<LocationCallback> { None }
fn get_core_assets_directory(&mut self) -> Option<PathBuf> { None }
fn get_save_directory(&mut self) -> Option<PathBuf> { 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<String> { None }
fn get_language(&mut self) -> Option<Language> { None }
// fn set_serialization_quirks(&mut self, quirks: &mut u64) -> bool { false }
}
// impl<T: Handler> Unpin for T {}

View File

@ -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().

View File

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

View File

@ -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<bool> { None }
fn get_can_dupe(&mut self) -> Option<bool> { 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<PathBuf> { 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<EnvVariable> { None }
fn set_variables(&mut self, variables: &[EnvVariable]) -> bool { false }
fn get_variable_update(&mut self) -> Option<bool> { None }
fn set_support_no_game(&mut self, supports_no_game: bool) -> bool { false }
fn get_libretro_path(&mut self) -> Option<PathBuf> { 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<RumbleInterface> { None }
fn get_input_device_capabilities(&mut self) -> Option<u64> { None }
fn get_sensor_interface(&mut self) -> Option<SensorInterface> { None }
fn get_camera_interface(&mut self) -> Option<CameraCallback> { None }
fn get_log_interface(&mut self) -> Option<LogCallback> { None }
fn get_perf_interface(&mut self) -> Option<PerfCallback> { None }
fn get_location_interface(&mut self) -> Option<LocationCallback> { None }
fn get_core_assets_directory(&mut self) -> Option<PathBuf> { None }
fn get_save_directory(&mut self) -> Option<PathBuf> { 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<String> { None }
fn get_language(&mut self) -> Option<Language> { None }
// fn set_serialization_quirks(&mut self, quirks: &mut u64) -> bool { false }
}
#[derive(Default)]
struct StaticCallbacks {
handler: Option<Pin<&'static mut dyn Handler>>,
@ -48,7 +95,7 @@ impl StaticCallbacks {
T::try_from_primitive(number).ok()
}
fn environment_cb_inner(cmd: u32, data: *mut c_void) -> Option<bool> {
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()));
}
}