all: refactoring audio stuff and using structopt in desktop app

Former-commit-id: 8fb2e158eba5f81bc9fb953bfa6d0f4d9e505a61
Former-commit-id: c436751be80c9517401777ec5060061383d75929
This commit is contained in:
Michel Heily 2022-09-13 01:50:50 +03:00
parent b431b2605a
commit c9811cc272
19 changed files with 321 additions and 420 deletions

View file

@ -7,9 +7,6 @@ use std::rc::Rc;
use rustboyadvance_core::prelude::*; use rustboyadvance_core::prelude::*;
struct BenchmarkHardware {}
impl AudioInterface for BenchmarkHardware {}
fn create_gba() -> GameBoyAdvance { fn create_gba() -> GameBoyAdvance {
// TODO: do I really want this file in my repository ? // TODO: do I really want this file in my repository ?
let bios = include_bytes!("roms/normatt_gba_bios.bin"); let bios = include_bytes!("roms/normatt_gba_bios.bin");
@ -22,14 +19,7 @@ fn create_gba() -> GameBoyAdvance {
.build() .build()
.unwrap(); .unwrap();
let dummy = Rc::new(RefCell::new(BenchmarkHardware {})); let mut gba = GameBoyAdvance::new(bios.to_vec().into_boxed_slice(), gpak, NullAudio::new());
let mut gba = GameBoyAdvance::new(
bios.to_vec().into_boxed_slice(),
gpak,
dummy.clone(),
dummy.clone(),
);
gba.skip_bios(); gba.skip_bios();
// skip initialization of the ROM to get to a stabilized scene // skip initialization of the ROM to get to a stabilized scene
for _ in 0..60 { for _ in 0..60 {

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt; use std::fmt;
use std::str::FromStr;
mod backup_file; mod backup_file;
pub use backup_file::BackupFile; pub use backup_file::BackupFile;
@ -16,10 +16,9 @@ pub enum BackupType {
AutoDetect = 5, AutoDetect = 5,
} }
impl TryFrom<&str> for BackupType { impl FromStr for BackupType {
type Error = String; type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn try_from(s: &str) -> Result<Self, Self::Error> {
use BackupType::*; use BackupType::*;
match s { match s {
"autodetect" => Ok(AutoDetect), "autodetect" => Ok(AutoDetect),

View file

@ -1,5 +1,5 @@
/// Struct containing everything /// Struct containing everything
use std::cell::{Cell, RefCell}; use std::cell::Cell;
use std::rc::Rc; use std::rc::Rc;
use bincode; use bincode;
@ -15,7 +15,7 @@ use super::sound::SoundController;
use super::sysbus::SysBus; use super::sysbus::SysBus;
use super::timer::Timers; use super::timer::Timers;
use super::AudioInterface; use super::sound::interface::DynAudioInterface;
use arm7tdmi::{self, Arm7tdmiCore}; use arm7tdmi::{self, Arm7tdmiCore};
use rustboyadvance_utils::Shared; use rustboyadvance_utils::Shared;
@ -26,7 +26,7 @@ pub struct GameBoyAdvance {
pub io_devs: Shared<IoDevices>, pub io_devs: Shared<IoDevices>,
pub scheduler: SharedScheduler, pub scheduler: SharedScheduler,
interrupt_flags: SharedInterruptFlags, interrupt_flags: SharedInterruptFlags,
pub audio_device: Rc<RefCell<dyn AudioInterface>>, pub audio_interface: DynAudioInterface,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -63,7 +63,7 @@ impl GameBoyAdvance {
pub fn new( pub fn new(
bios_rom: Box<[u8]>, bios_rom: Box<[u8]>,
gamepak: Cartridge, gamepak: Cartridge,
audio_device: Rc<RefCell<dyn AudioInterface>>, audio_interface: DynAudioInterface,
) -> GameBoyAdvance { ) -> GameBoyAdvance {
// Warn the user if the bios is not the real one // Warn the user if the bios is not the real one
match check_real_bios(&bios_rom) { match check_real_bios(&bios_rom) {
@ -80,7 +80,7 @@ impl GameBoyAdvance {
let timers = Timers::new(interrupt_flags.clone()); let timers = Timers::new(interrupt_flags.clone());
let sound_controller = Box::new(SoundController::new( let sound_controller = Box::new(SoundController::new(
&mut scheduler, &mut scheduler,
audio_device.borrow().get_sample_rate() as f32, audio_interface.get_sample_rate() as f32,
)); ));
let io_devs = Shared::new(IoDevices::new( let io_devs = Shared::new(IoDevices::new(
intc, intc,
@ -103,7 +103,7 @@ impl GameBoyAdvance {
cpu, cpu,
sysbus, sysbus,
io_devs, io_devs,
audio_device, audio_interface,
scheduler, scheduler,
interrupt_flags, interrupt_flags,
}; };
@ -117,7 +117,7 @@ impl GameBoyAdvance {
savestate: &[u8], savestate: &[u8],
bios: Box<[u8]>, bios: Box<[u8]>,
rom: Box<[u8]>, rom: Box<[u8]>,
audio_device: Rc<RefCell<dyn AudioInterface>>, audio_interface: DynAudioInterface,
) -> bincode::Result<GameBoyAdvance> { ) -> bincode::Result<GameBoyAdvance> {
let decoded: Box<SaveState> = bincode::deserialize_from(savestate)?; let decoded: Box<SaveState> = bincode::deserialize_from(savestate)?;
@ -148,7 +148,7 @@ impl GameBoyAdvance {
sysbus, sysbus,
io_devs, io_devs,
interrupt_flags: interrupts, interrupt_flags: interrupts,
audio_device, audio_interface,
scheduler, scheduler,
}) })
} }
@ -299,7 +299,7 @@ impl GameBoyAdvance {
Some(timers.handle_overflow_event(channel_id, event_time, apu, dmac)) Some(timers.handle_overflow_event(channel_id, event_time, apu, dmac))
} }
EventType::Gpu(gpu_event) => Some(io.gpu.on_event(gpu_event, &mut *self.sysbus)), EventType::Gpu(gpu_event) => Some(io.gpu.on_event(gpu_event, &mut *self.sysbus)),
EventType::Apu(event) => Some(io.sound.on_event(event, &self.audio_device)), EventType::Apu(event) => Some(io.sound.on_event(event, &mut self.audio_interface)),
}; };
if let Some((new_event, when)) = new_event { if let Some((new_event, when)) = new_event {
// We schedule events added by event handlers relative to the handled event time // We schedule events added by event handlers relative to the handled event time
@ -378,21 +378,8 @@ impl GameBoyAdvance {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::cell::RefCell;
use std::rc::Rc;
use crate::cartridge::GamepakBuilder; use crate::prelude::*;
use arm7tdmi::memory::BusIO;
struct DummyAudio {}
impl DummyAudio {
fn new() -> DummyAudio {
DummyAudio {}
}
}
impl AudioInterface for DummyAudio {}
fn make_mock_gba(rom: &[u8]) -> GameBoyAdvance { fn make_mock_gba(rom: &[u8]) -> GameBoyAdvance {
let bios = vec![0; 0x4000].into_boxed_slice(); let bios = vec![0; 0x4000].into_boxed_slice();
@ -402,8 +389,7 @@ mod tests {
.without_backup_to_file() .without_backup_to_file()
.build() .build()
.unwrap(); .unwrap();
let dummy = Rc::new(RefCell::new(DummyAudio::new())); let mut gba = GameBoyAdvance::new(bios, cartridge, NullAudio::new());
let mut gba = GameBoyAdvance::new(bios, cartridge, dummy.clone());
gba.skip_bios(); gba.skip_bios();
gba gba

View file

@ -55,26 +55,6 @@ pub(crate) mod overrides;
#[cfg(feature = "debugger")] #[cfg(feature = "debugger")]
pub mod debugger; pub mod debugger;
pub type StereoSample<T> = [T; 2];
pub trait AudioInterface {
fn get_sample_rate(&self) -> i32 {
44100
}
/// Pushes a stereo sample into the audio device
/// Sample should be normilized to siged 16bit values
/// Note: It is not guarentied that the sample will be played
#[allow(unused_variables)]
fn push_sample(&mut self, samples: &[i16]) {}
}
pub trait InputInterface {
fn poll(&mut self) -> u16 {
keypad::KEYINPUT_ALL_RELEASED
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum GBAError { pub enum GBAError {
IO(::std::io::Error), IO(::std::io::Error),
@ -122,8 +102,10 @@ pub mod prelude {
#[cfg(feature = "debugger")] #[cfg(feature = "debugger")]
pub use super::debugger::Debugger; pub use super::debugger::Debugger;
pub use super::gpu::{DISPLAY_HEIGHT, DISPLAY_WIDTH}; pub use super::gpu::{DISPLAY_HEIGHT, DISPLAY_WIDTH};
pub use super::sound::interface::{
AudioInterface, DynAudioInterface, NullAudio, SimpleAudioInterface,
};
pub use super::Bus; pub use super::Bus;
pub use super::{AudioInterface, StereoSample};
pub use super::{GBAError, GBAResult, GameBoyAdvance}; pub use super::{GBAError, GBAResult, GameBoyAdvance};
pub use rustboyadvance_utils::{read_bin_file, write_bin_file}; pub use rustboyadvance_utils::{read_bin_file, write_bin_file};
} }

View file

@ -1,5 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom; use std::str::FromStr;
use yaml_rust::YamlLoader; use yaml_rust::YamlLoader;
@ -34,7 +34,7 @@ lazy_static! {
let game_code = String::from(game["code"].as_str().unwrap()); let game_code = String::from(game["code"].as_str().unwrap());
let force_rtc = game["rtc"].as_bool().unwrap_or(false); let force_rtc = game["rtc"].as_bool().unwrap_or(false);
let save_type = if let Some(save_type) = game["save_type"].as_str() { let save_type = if let Some(save_type) = game["save_type"].as_str() {
match BackupType::try_from(save_type) { match BackupType::from_str(save_type) {
Ok(x) => Some(x), Ok(x) => Some(x),
_ => panic!("{}: invalid save type {:#}", game_code, save_type), _ => panic!("{}: invalid save type {:#}", game_code, save_type),
} }

View file

@ -1,4 +1,4 @@
use crate::StereoSample; use super::StereoSample;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -0,0 +1,63 @@
use rustboyadvance_utils::audio::{AudioRingBuffer, SampleConsumer, SampleProducer};
pub type StereoSample<T> = [T; 2];
pub trait AudioInterface {
fn get_sample_rate(&self) -> i32 {
44100
}
/// Pushes stereo sample buffer into the audio device
/// Sample should be normilized to siged 16bit values
/// Note: It is not guarentied that the sample will be played
#[allow(unused_variables)]
fn push_sample(&mut self, sample: &StereoSample<i16>) {}
}
pub struct SimpleAudioInterface {
producer: SampleProducer,
sample_rate: i32,
}
impl SimpleAudioInterface {
pub fn create_channel(
sample_rate: i32,
buffer_size: Option<usize>,
) -> (Box<Self>, SampleConsumer) {
let (producer, consumer) =
AudioRingBuffer::new_with_capacity(buffer_size.unwrap_or(8192)).split();
(
Box::new(SimpleAudioInterface {
producer,
sample_rate,
}),
consumer,
)
}
}
impl AudioInterface for SimpleAudioInterface {
#[inline]
fn get_sample_rate(&self) -> i32 {
self.sample_rate
}
#[inline]
fn push_sample(&mut self, sample: &StereoSample<i16>) {
let _ = self.producer.push(sample[0]);
let _ = self.producer.push(sample[1]);
}
}
pub type DynAudioInterface = Box<dyn AudioInterface>;
#[derive(Debug, Default)]
pub struct NullAudio {}
impl AudioInterface for NullAudio {}
impl NullAudio {
pub fn new() -> Box<NullAudio> {
Box::new(NullAudio::default())
}
}

View file

@ -1,6 +1,3 @@
use std::cell::RefCell;
use std::rc::Rc;
use bit::BitIndex; use bit::BitIndex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -8,10 +5,10 @@ use super::dma::DmaController;
use super::iodev::consts::*; use super::iodev::consts::*;
use super::sched::*; use super::sched::*;
use crate::{AudioInterface, StereoSample};
mod fifo; mod fifo;
use fifo::SoundFifo; use fifo::SoundFifo;
pub mod interface;
pub use interface::{AudioInterface, DynAudioInterface, StereoSample};
mod dsp; mod dsp;
use dsp::{CosineResampler, Resampler}; use dsp::{CosineResampler, Resampler};
@ -59,8 +56,6 @@ const REG_FIFO_A_H: u32 = REG_FIFO_A + 2;
const REG_FIFO_B_L: u32 = REG_FIFO_B; const REG_FIFO_B_L: u32 = REG_FIFO_B;
const REG_FIFO_B_H: u32 = REG_FIFO_B + 2; const REG_FIFO_B_H: u32 = REG_FIFO_B + 2;
type AudioDeviceRcRefCell = Rc<RefCell<dyn AudioInterface>>;
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SoundController { pub struct SoundController {
cycles: usize, // cycles count when we last provided a new sample. cycles: usize, // cycles count when we last provided a new sample.
@ -321,7 +316,7 @@ impl SoundController {
} }
#[inline] #[inline]
fn on_sample(&mut self, audio_device: &AudioDeviceRcRefCell) -> FutureEvent { fn on_sample(&mut self, audio_device: &mut DynAudioInterface) -> FutureEvent {
let mut sample = [0f32, 0f32]; let mut sample = [0f32, 0f32];
for (channel, out_sample) in sample.iter_mut().enumerate() { for (channel, out_sample) in sample.iter_mut().enumerate() {
@ -339,9 +334,8 @@ impl SoundController {
self.resampler.feed(&sample, &mut self.output_buffer); self.resampler.feed(&sample, &mut self.output_buffer);
let mut audio = audio_device.borrow_mut();
self.output_buffer.drain(..).for_each(|[left, right]| { self.output_buffer.drain(..).for_each(|[left, right]| {
audio.push_sample(&[ audio_device.push_sample(&[
(left.round() as i16) * (std::i16::MAX / 512), (left.round() as i16) * (std::i16::MAX / 512),
(right.round() as i16) * (std::i16::MAX / 512), (right.round() as i16) * (std::i16::MAX / 512),
]); ]);
@ -352,7 +346,7 @@ impl SoundController {
pub fn on_event( pub fn on_event(
&mut self, &mut self,
event: ApuEvent, event: ApuEvent,
audio_device: &AudioDeviceRcRefCell, audio_device: &mut DynAudioInterface,
) -> FutureEvent { ) -> FutureEvent {
match event { match event {
ApuEvent::Sample => self.on_sample(audio_device), ApuEvent::Sample => self.on_sample(audio_device),

View file

@ -1,21 +1,9 @@
use std::cell::RefCell;
use std::env; use std::env;
use std::path::Path; use std::path::Path;
use std::rc::Rc;
use rustboyadvance_core::prelude::*; use rustboyadvance_core::prelude::*;
use rustboyadvance_utils::FpsCounter; use rustboyadvance_utils::FpsCounter;
struct DummyAudio {}
impl DummyAudio {
fn new() -> DummyAudio {
DummyAudio {}
}
}
impl AudioInterface for DummyAudio {}
fn main() { fn main() {
if env::args().count() < 3 { if env::args().count() < 3 {
eprintln!("usage: {} <bios> <rom>", env::args().nth(0).unwrap()); eprintln!("usage: {} <bios> <rom>", env::args().nth(0).unwrap());
@ -35,9 +23,7 @@ fn main() {
.build() .build()
.unwrap(); .unwrap();
let dummy = Rc::new(RefCell::new(DummyAudio::new())); let mut gba = GameBoyAdvance::new(bios.into_boxed_slice(), gamepak, NullAudio::new());
let mut gba = GameBoyAdvance::new(bios.into_boxed_slice(), gamepak, dummy.clone());
gba.skip_bios(); gba.skip_bios();
let mut fps_counter = FpsCounter::default(); let mut fps_counter = FpsCounter::default();

View file

@ -7,23 +7,34 @@ pub mod util {
use jni::signature::{JavaType, Primitive}; use jni::signature::{JavaType, Primitive};
use jni::JNIEnv; use jni::JNIEnv;
pub fn get_sample_rate(env: &JNIEnv, audio_player_obj: JObject) -> i32 { macro_rules! call_audio_player_method {
let audio_player_klass = env.get_object_class(audio_player_obj).unwrap(); ($env:ident, $audio_player_obj:ident, $method_name:literal, "()I") => {{
let mid_get_sample_rate = env let audio_player_klass = $env
.get_method_id(audio_player_klass, "getSampleRate", "()I") .get_object_class($audio_player_obj)
.expect("failed to get methodID for getSampleRate"); .map_err(|e| format!("failed to get class: {:?}", e))?;
let result = env let mid_get_sample_rate = $env
.get_method_id(audio_player_klass, $method_name, "()I")
.map_err(|e| format!("failed to get methodID for {}: {:?}", $method_name, e))?;
let result = $env
.call_method_unchecked( .call_method_unchecked(
audio_player_obj, $audio_player_obj,
mid_get_sample_rate, mid_get_sample_rate,
JavaType::Primitive(Primitive::Int), JavaType::Primitive(Primitive::Int),
&[], &[],
) )
.unwrap(); .map_err(|e| format!("getSampleRate() failed: {:?}", e))?;
let sample_rate = match result { match result {
JValue::Int(sample_rate) => sample_rate as i32, JValue::Int(sample_rate) => Ok(sample_rate),
_ => panic!("bad return value"), value => panic!("bad return value {:?}", value),
}; }
return sample_rate; }};
}
pub fn get_sample_rate(env: &JNIEnv, audio_player_obj: JObject) -> Result<i32, String> {
call_audio_player_method!(env, audio_player_obj, "getSampleRate", "()I")
}
pub fn get_sample_count(env: &JNIEnv, audio_player_obj: JObject) -> Result<i32, String> {
call_audio_player_method!(env, audio_player_obj, "getSampleCount", "()I")
} }
} }

View file

@ -4,7 +4,7 @@ use std::sync::mpsc::{channel, Sender};
use std::thread; use std::thread;
use std::thread::JoinHandle; use std::thread::JoinHandle;
use rustboyadvance_utils::audio::Consumer; use rustboyadvance_utils::audio::SampleConsumer;
use jni::JavaVM; use jni::JavaVM;
@ -20,8 +20,11 @@ pub enum AudioThreadCommand {
pub(crate) fn spawn_audio_worker_thread( pub(crate) fn spawn_audio_worker_thread(
audio_connector: AudioJNIConnector, audio_connector: AudioJNIConnector,
jvm: JavaVM, jvm: JavaVM,
mut consumer: Consumer<i16>, mut consumer: SampleConsumer,
) -> (JoinHandle<AudioJNIConnector>, Sender<AudioThreadCommand>) { ) -> (
JoinHandle<(AudioJNIConnector, SampleConsumer)>,
Sender<AudioThreadCommand>,
) {
let (tx, rx) = channel(); let (tx, rx) = channel();
let handle = thread::spawn(move || { let handle = thread::spawn(move || {
@ -58,8 +61,8 @@ pub(crate) fn spawn_audio_worker_thread(
info!("[AudioWorker] terminating"); info!("[AudioWorker] terminating");
// return the audio connector back // return the audio stuff back to main thread
audio_connector (audio_connector, consumer)
}); });
(handle, tx) (handle, tx)

View file

@ -1,10 +1,8 @@
use rustboyadvance_core::prelude::*; use rustboyadvance_core::prelude::*;
use rustboyadvance_utils::audio::{AudioRingBuffer, Producer}; use rustboyadvance_utils::audio::SampleConsumer;
// use rustboyadvance_core::util::FpsCounter; // use rustboyadvance_core::util::FpsCounter;
use std::cell::RefCell;
use std::path::Path; use std::path::Path;
use std::rc::Rc;
use std::sync::{Mutex, MutexGuard}; use std::sync::{Mutex, MutexGuard};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -15,28 +13,6 @@ use jni::JNIEnv;
use crate::audio::{self, connector::AudioJNIConnector, thread::AudioThreadCommand}; use crate::audio::{self, connector::AudioJNIConnector, thread::AudioThreadCommand};
struct AudioDevice {
sample_rate: i32,
audio_producer: Option<Producer<i16>>,
}
impl AudioInterface for AudioDevice {
fn push_sample(&mut self, samples: &[i16]) {
if let Some(prod) = &mut self.audio_producer {
for s in samples.iter() {
let _ = prod.push(*s);
}
} else {
// The gba is never ran before audio_producer is initialized
unreachable!()
}
}
fn get_sample_rate(&self) -> i32 {
self.sample_rate
}
}
struct Renderer { struct Renderer {
renderer_ref: GlobalRef, renderer_ref: GlobalRef,
frame_buffer_ref: GlobalRef, frame_buffer_ref: GlobalRef,
@ -145,8 +121,20 @@ impl Default for EmulationState {
} }
} }
fn create_audio(
env: &JNIEnv,
audio_player_obj: JObject,
) -> Result<(Box<SimpleAudioInterface>, SampleConsumer), String> {
let sample_rate = audio::util::get_sample_rate(env, audio_player_obj)?;
let sample_count = audio::util::get_sample_count(env, audio_player_obj)? as usize;
Ok(SimpleAudioInterface::create_channel(
sample_rate,
Some(sample_count * 2),
))
}
pub struct EmulatorContext { pub struct EmulatorContext {
audio_device: Rc<RefCell<AudioDevice>>, audio_consumer: Option<SampleConsumer>,
renderer: Renderer, renderer: Renderer,
audio_player_ref: GlobalRef, audio_player_ref: GlobalRef,
keypad: Keypad, keypad: Keypad,
@ -188,11 +176,9 @@ impl EmulatorContext {
let renderer = Renderer::new(env, renderer_obj)?; let renderer = Renderer::new(env, renderer_obj)?;
info!("Creating GBA Instance"); info!("Creating GBA Instance");
let audio = Rc::new(RefCell::new(AudioDevice { let audio_player_ref = env.new_global_ref(audio_player).unwrap();
sample_rate: audio::util::get_sample_rate(env, audio_player), let (audio_device, audio_consumer) = create_audio(env, audio_player_ref.as_obj())?;
audio_producer: None, let mut gba = GameBoyAdvance::new(bios, gamepak, audio_device);
}));
let mut gba = GameBoyAdvance::new(bios, gamepak, audio.clone());
if skip_bios != 0 { if skip_bios != 0 {
info!("skipping bios"); info!("skipping bios");
gba.skip_bios(); gba.skip_bios();
@ -202,14 +188,13 @@ impl EmulatorContext {
let keypad = Keypad::new(env, keypad_obj); let keypad = Keypad::new(env, keypad_obj);
info!("creating context"); info!("creating context");
let audio_player_ref = env.new_global_ref(audio_player).unwrap();
let context = EmulatorContext { let context = EmulatorContext {
gba, gba,
keypad, keypad,
renderer, renderer,
audio_player_ref, audio_player_ref,
emustate: Mutex::new(EmulationState::default()), emustate: Mutex::new(EmulationState::default()),
audio_device: audio.clone(), audio_consumer: Some(audio_consumer),
}; };
Ok(context) Ok(context)
} }
@ -236,30 +221,25 @@ impl EmulatorContext {
.map_err(|e| format!("could not get savestate buffer, error {}", e))?; .map_err(|e| format!("could not get savestate buffer, error {}", e))?;
let renderer = Renderer::new(env, renderer_obj)?; let renderer = Renderer::new(env, renderer_obj)?;
let audio_player_ref = env.new_global_ref(audio_player).unwrap();
let audio = Rc::new(RefCell::new(AudioDevice { let (audio_device, audio_consumer) = create_audio(env, audio_player_ref.as_obj())?;
sample_rate: audio::util::get_sample_rate(env, audio_player), let gba =
audio_producer: None, GameBoyAdvance::from_saved_state(&savestate, bios, rom, audio_device).map_err(|e| {
}));
let gba = GameBoyAdvance::from_saved_state(&savestate, bios, rom, audio.clone()).map_err(
|e| {
format!( format!(
"failed to create GameBoyAdvance from saved savestate, error {:?}", "failed to create GameBoyAdvance from saved savestate, error {:?}",
e e
) )
}, })?;
)?;
let keypad = Keypad::new(env, keypad_obj); let keypad = Keypad::new(env, keypad_obj);
let audio_player_ref = env.new_global_ref(audio_player).unwrap();
Ok(EmulatorContext { Ok(EmulatorContext {
gba, gba,
keypad, keypad,
renderer, renderer,
audio_player_ref, audio_player_ref,
emustate: Mutex::new(EmulationState::default()), emustate: Mutex::new(EmulationState::default()),
audio_device: audio.clone(), audio_consumer: Some(audio_consumer),
}) })
} }
@ -285,15 +265,13 @@ impl EmulatorContext {
// Instanciate an audio player connector // Instanciate an audio player connector
let audio_connector = AudioJNIConnector::new(env, self.audio_player_ref.as_obj()); let audio_connector = AudioJNIConnector::new(env, self.audio_player_ref.as_obj());
// Create a ringbuffer between the emulator and the audio thread
let (prod, cons) = AudioRingBuffer::new_with_capacity(audio_connector.sample_count).split();
// Store the ringbuffer producer in the emulator
self.audio_device.borrow_mut().audio_producer = Some(prod);
// Spawn the audio worker thread, give it the audio connector, jvm and ringbuffer consumer // Spawn the audio worker thread, give it the audio connector, jvm and ringbuffer consumer
let (audio_thread_handle, audio_thread_tx) = // Note - after this operation `self` no longer has `audio_consumer`
audio::thread::spawn_audio_worker_thread(audio_connector, jvm, cons); let (audio_thread_handle, audio_thread_tx) = audio::thread::spawn_audio_worker_thread(
audio_connector,
jvm,
self.audio_consumer.take().unwrap(),
);
info!("starting main emulation loop"); info!("starting main emulation loop");
@ -302,7 +280,7 @@ impl EmulatorContext {
'running: loop { 'running: loop {
let emustate = *self.emustate.lock().unwrap(); let emustate = *self.emustate.lock().unwrap();
let limiter = match emustate { let vsync = match emustate {
EmulationState::Initial => unsafe { std::hint::unreachable_unchecked() }, EmulationState::Initial => unsafe { std::hint::unreachable_unchecked() },
EmulationState::Stopped => unsafe { std::hint::unreachable_unchecked() }, EmulationState::Stopped => unsafe { std::hint::unreachable_unchecked() },
EmulationState::Pausing => { EmulationState::Pausing => {
@ -334,7 +312,7 @@ impl EmulatorContext {
// info!("FPS {}", fps); // info!("FPS {}", fps);
// } // }
if limiter { if vsync {
let time_passed = start_time.elapsed(); let time_passed = start_time.elapsed();
let delay = FRAME_TIME.checked_sub(time_passed); let delay = FRAME_TIME.checked_sub(time_passed);
match delay { match delay {
@ -350,13 +328,12 @@ impl EmulatorContext {
audio_thread_tx.send(AudioThreadCommand::Terminate).unwrap(); // we surely have an endpoint, so it will work audio_thread_tx.send(AudioThreadCommand::Terminate).unwrap(); // we surely have an endpoint, so it will work
info!("waiting for audio worker to complete"); info!("waiting for audio worker to complete");
let audio_connector = audio_thread_handle.join().unwrap(); let (audio_connector, audio_consumer) = audio_thread_handle.join().unwrap();
self.audio_consumer.replace(audio_consumer);
info!("audio worker terminated"); info!("audio worker terminated");
audio_connector.pause(env); audio_connector.pause(env);
self.audio_device.borrow_mut().audio_producer = None;
*self.emustate.lock().unwrap() = EmulationState::Stopped; *self.emustate.lock().unwrap() = EmulationState::Stopped;
Ok(()) Ok(())

View file

@ -13,34 +13,18 @@ use unsafe_unwrap::UnsafeUnwrap;
use rustboyadvance_core::keypad::Keys as _GbaButton; use rustboyadvance_core::keypad::Keys as _GbaButton;
use rustboyadvance_core::prelude::*; use rustboyadvance_core::prelude::*;
use rustboyadvance_utils::audio::AudioRingBuffer; use rustboyadvance_utils::audio::SampleConsumer;
use std::ops::Deref; use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::cell::RefCell;
use std::default::Default; use std::default::Default;
use std::rc::Rc;
#[derive(Default)]
struct AudioDevice {
audio_ring_buffer: AudioRingBuffer,
}
impl AudioInterface for AudioDevice {
fn push_sample(&mut self, samples: &[i16]) {
let prod = self.audio_ring_buffer.producer();
for s in samples.iter() {
let _ = prod.push(*s);
}
}
}
#[derive(Default)] #[derive(Default)]
struct RustBoyAdvanceCore { struct RustBoyAdvanceCore {
gba: Option<GameBoyAdvance>, gba: Option<GameBoyAdvance>,
game_data: Option<GameData>, game_data: Option<GameData>,
audio: Option<Rc<RefCell<AudioDevice>>>, audio_consumer: Option<SampleConsumer>,
} }
#[repr(transparent)] #[repr(transparent)]
@ -119,10 +103,12 @@ impl libretro_backend::Core for RustBoyAdvanceCore {
.video(240, 160, 60.0, PixelFormat::ARGB8888) .video(240, 160, 60.0, PixelFormat::ARGB8888)
.audio(44100.0); .audio(44100.0);
let audio = Rc::new(RefCell::new(AudioDevice::default())); let (audio_device, audio_consumer) =
let gba = GameBoyAdvance::new(bios.into_boxed_slice(), gamepak, audio.clone()); SimpleAudioInterface::create_channel(44100, None);
self.audio = Some(audio); let gba = GameBoyAdvance::new(bios.into_boxed_slice(), gamepak, audio_device);
self.audio_consumer = Some(audio_consumer);
self.gba = Some(gba); self.gba = Some(gba);
self.game_data = Some(game_data); self.game_data = Some(game_data);
LoadGameResult::Success(av_info) LoadGameResult::Success(av_info)
@ -136,7 +122,6 @@ impl libretro_backend::Core for RustBoyAdvanceCore {
// gba and audio are `Some` after the game is loaded, so avoiding overhead of unwrap // gba and audio are `Some` after the game is loaded, so avoiding overhead of unwrap
let gba = unsafe { self.gba.as_mut().unsafe_unwrap() }; let gba = unsafe { self.gba.as_mut().unsafe_unwrap() };
let audio = unsafe { self.audio.as_mut().unsafe_unwrap() };
let key_state = gba.get_key_state_mut(); let key_state = gba.get_key_state_mut();
macro_rules! update_controllers { macro_rules! update_controllers {
@ -165,12 +150,11 @@ impl libretro_backend::Core for RustBoyAdvanceCore {
// upload sound samples // upload sound samples
{ {
let mut audio_consumer = self.audio_consumer.take().unwrap();
let mut audio_samples = [0; 4096 * 2]; let mut audio_samples = [0; 4096 * 2];
let mut audio = audio.borrow_mut(); let count = audio_consumer.pop_slice(&mut audio_samples);
let consumer = audio.audio_ring_buffer.consumer();
let count = consumer.pop_slice(&mut audio_samples);
handle.upload_audio_frame(&audio_samples[..count]); handle.upload_audio_frame(&audio_samples[..count]);
self.audio_consumer.replace(audio_consumer);
} }
} }

View file

@ -10,7 +10,7 @@ rustboyadvance-utils = { path = "../../utils/" }
sdl2 = { version = "0.33.0", features = ["image"] } sdl2 = { version = "0.33.0", features = ["image"] }
ringbuf = "0.2.2" ringbuf = "0.2.2"
bytesize = "1.0.0" bytesize = "1.0.0"
clap = { version = "2.33", features = ["color", "yaml"] } structopt = "0.3"
log = "0.4.8" log = "0.4.8"
flexi_logger = { version = "0.14", features = ["colors"] } flexi_logger = { version = "0.14", features = ["colors"] }
bit = "^0.1" bit = "^0.1"

View file

@ -1,98 +1,63 @@
use sdl2; use sdl2;
use sdl2::audio::{AudioCallback, AudioDevice, AudioSpec, AudioSpecDesired}; use sdl2::audio::{AudioCallback, AudioDevice, AudioFormat, AudioSpec, AudioSpecDesired};
use rustboyadvance_core::{AudioInterface, StereoSample}; use rustboyadvance_core::prelude::SimpleAudioInterface;
use rustboyadvance_utils::audio::SampleConsumer;
use ringbuf; pub struct GbaAudioCallback {
use ringbuf::{Consumer, Producer, RingBuffer}; consumer: SampleConsumer,
#[allow(unused)]
struct GbaAudioCallback {
consumer: Consumer<StereoSample<i16>>,
spec: AudioSpec, spec: AudioSpec,
} }
pub struct DummyAudioPlayer {}
impl AudioInterface for DummyAudioPlayer {}
pub struct Sdl2AudioPlayer {
_device: AudioDevice<GbaAudioCallback>,
producer: Producer<StereoSample<i16>>,
freq: i32,
}
impl AudioCallback for GbaAudioCallback { impl AudioCallback for GbaAudioCallback {
type Channel = i16; type Channel = i16;
fn callback(&mut self, out_samples: &mut [i16]) { fn callback(&mut self, out_samples: &mut [i16]) {
let sample_count = out_samples.len() / 2; let written = self.consumer.pop_slice(out_samples);
for s in out_samples.iter_mut().skip(written) {
for i in 0..sample_count { *s = self.spec.silence as i16;
if let Some([left, right]) = self.consumer.pop() {
out_samples[2 * i] = left;
out_samples[2 * i + 1] = right;
} else {
out_samples[2 * i] = self.spec.silence as i16;
out_samples[2 * i + 1] = self.spec.silence as i16;
}
} }
} }
} }
impl AudioInterface for Sdl2AudioPlayer { pub fn create_audio_player(
fn get_sample_rate(&self) -> i32 { sdl: &sdl2::Sdl,
self.freq ) -> Result<(Box<SimpleAudioInterface>, AudioDevice<GbaAudioCallback>), String> {
}
fn push_sample(&mut self, sample: &[i16]) {
#![allow(unused_must_use)]
self.producer.push([sample[0], sample[1]]);
}
}
pub fn create_audio_player(sdl: &sdl2::Sdl) -> Sdl2AudioPlayer {
let desired_spec = AudioSpecDesired { let desired_spec = AudioSpecDesired {
freq: Some(44_100), freq: Some(44_100),
channels: Some(2), // stereo channels: Some(2), // stereo
samples: None, samples: None,
}; };
let audio_subsystem = sdl.audio().unwrap(); let audio_subsystem = sdl.audio()?;
let mut freq = 0; let mut freq = 0;
let mut producer: Option<Producer<StereoSample<i16>>> = None; let mut gba_audio = None;
let device = audio_subsystem let device = audio_subsystem.open_playback(None, &desired_spec, |spec| {
.open_playback(None, &desired_spec, |spec| {
info!("Found audio device: {:?}", spec); info!("Found audio device: {:?}", spec);
freq = spec.freq; freq = spec.freq;
// Create a thread-safe SPSC fifo if spec.format != AudioFormat::S16LSB {
let ringbuf_size = (spec.samples as usize) * 2; panic!("Unsupported audio format {:?}", spec.format);
let rb = RingBuffer::<StereoSample<i16>>::new(ringbuf_size);
let (prod, cons) = rb.split();
// move producer to the outer scope
producer = Some(prod);
GbaAudioCallback {
consumer: cons,
spec,
} }
})
.unwrap(); // Create a thread-safe SPSC fifo
let ringbuf_samples_per_channel = (spec.samples as usize) * 2; // we want the ringbuf to hold 2 frames worth of samples
let ringbuf_size = (spec.channels as usize) * ringbuf_samples_per_channel;
info!("ringbuffer size = {}", ringbuf_size);
let (audio_device, consumer) =
SimpleAudioInterface::create_channel(freq, Some(ringbuf_size));
// Move the audio to outer scope
gba_audio = Some(audio_device);
GbaAudioCallback { consumer, spec }
})?;
device.resume(); device.resume();
Sdl2AudioPlayer { Ok((gba_audio.take().unwrap(), device))
_device: device,
freq,
producer: producer.unwrap(),
}
}
pub fn create_dummy_player() -> DummyAudioPlayer {
info!("Dummy audio device");
DummyAudioPlayer {}
} }

View file

@ -12,21 +12,14 @@ use sdl2::EventPump;
use bytesize; use bytesize;
use spin_sleep; use spin_sleep;
use structopt::StructOpt;
use std::cell::RefCell;
use std::rc::Rc;
use std::fs; use std::fs;
use std::io::Cursor; use std::io::Cursor;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::process; use std::process;
use std::time; use std::time;
use std::convert::TryFrom;
#[macro_use]
extern crate clap;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
use flexi_logger; use flexi_logger;
@ -34,12 +27,11 @@ use flexi_logger::*;
mod audio; mod audio;
mod input; mod input;
mod options;
mod video; mod video;
use audio::{create_audio_player, create_dummy_player};
use video::{SCREEN_HEIGHT, SCREEN_WIDTH}; use video::{SCREEN_HEIGHT, SCREEN_WIDTH};
use rustboyadvance_core::cartridge::BackupType;
use rustboyadvance_core::prelude::*; use rustboyadvance_core::prelude::*;
use rustboyadvance_utils::FpsCounter; use rustboyadvance_utils::FpsCounter;
@ -50,9 +42,6 @@ const DEFAULT_GDB_SERVER_ADDR: &'static str = "localhost:1337";
const CANVAS_WIDTH: u32 = SCREEN_WIDTH; const CANVAS_WIDTH: u32 = SCREEN_WIDTH;
const CANVAS_HEIGHT: u32 = SCREEN_HEIGHT; const CANVAS_HEIGHT: u32 = SCREEN_HEIGHT;
fn get_savestate_path(rom_filename: &Path) -> PathBuf {
rom_filename.with_extension("savestate")
}
/// Waits for the user to drag a rom file to window /// Waits for the user to drag a rom file to window
fn wait_for_rom(canvas: &mut WindowCanvas, event_pump: &mut EventPump) -> Result<String, String> { fn wait_for_rom(canvas: &mut WindowCanvas, event_pump: &mut EventPump) -> Result<String, String> {
@ -101,6 +90,17 @@ fn ask_download_bios() {
const OPEN_SOURCE_BIOS_URL: &'static str = const OPEN_SOURCE_BIOS_URL: &'static str =
"https://github.com/Nebuleon/ReGBA/raw/master/bios/gba_bios.bin"; "https://github.com/Nebuleon/ReGBA/raw/master/bios/gba_bios.bin";
println!("Missing BIOS file. If you don't have the original GBA BIOS, you can download an open-source bios from {}", OPEN_SOURCE_BIOS_URL); println!("Missing BIOS file. If you don't have the original GBA BIOS, you can download an open-source bios from {}", OPEN_SOURCE_BIOS_URL);
std::process::exit(0);
}
fn load_bios(bios_path: &Path) -> Box<[u8]> {
match read_bin_file(bios_path) {
Ok(bios) => bios.into_boxed_slice(),
_ => {
ask_download_bios();
unreachable!()
}
}
} }
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -114,24 +114,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.start() .start()
.unwrap(); .unwrap();
let mut frame_limiter = true; let opts = options::Options::from_args();
let yaml = load_yaml!("cli.yml");
let matches = clap::App::from_yaml(yaml).get_matches();
let bios_path = Path::new(matches.value_of("bios").unwrap_or_default());
let bios_bin = match read_bin_file(bios_path) {
Ok(bios) => bios.into_boxed_slice(),
_ => {
ask_download_bios();
std::process::exit(0);
}
};
let skip_bios = matches.occurrences_of("skip_bios") != 0;
let debug = matches.occurrences_of("debug") != 0;
let silent = matches.occurrences_of("silent") != 0;
let with_gdbserver = matches.occurrences_of("with_gdbserver") != 0;
info!("Initializing SDL2 context"); info!("Initializing SDL2 context");
let sdl_context = sdl2::init().expect("failed to initialize sdl2"); let sdl_context = sdl2::init().expect("failed to initialize sdl2");
@ -174,65 +157,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
}; };
let mut rom_path = match matches.value_of("game_rom") {
Some(path) => path.to_string(),
_ => {
info!("[!] Rom file missing, please drag a rom file into the emulator window...");
wait_for_rom(&mut canvas, &mut event_pump)?
}
};
let mut renderer = video::Renderer::from_canvas(canvas); let mut renderer = video::Renderer::from_canvas(canvas);
let audio: Rc<RefCell<dyn AudioInterface>> = if silent { let (audio_interface, mut _sdl_audio_device) = audio::create_audio_player(&sdl_context)?;
Rc::new(RefCell::new(create_dummy_player())) let mut rom_name = opts.rom_name();
} else {
Rc::new(RefCell::new(create_audio_player(&sdl_context)))
};
let mut savestate_path = get_savestate_path(&Path::new(&rom_path)); let bios_bin = load_bios(&opts.bios);
let mut rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap(); let mut gba = GameBoyAdvance::new(
bios_bin.clone(),
opts.cartridge_from_opts()?,
audio_interface,
);
let mut builder = GamepakBuilder::new() if opts.skip_bios {
.save_type(BackupType::try_from(
matches.value_of("save_type").unwrap(),
)?)
.file(Path::new(&rom_path));
if matches.occurrences_of("rtc") != 0 {
builder = builder.with_rtc();
}
let gamepak = builder.build()?;
let mut gba = GameBoyAdvance::new(bios_bin.clone(), gamepak, audio.clone());
if skip_bios {
gba.skip_bios(); gba.skip_bios();
} }
if debug { if opts.gdbserver {
#[cfg(feature = "debugger")]
{
gba.cpu.set_verbose(true);
let mut debugger = Debugger::new();
info!("starting debugger...");
debugger
.repl(&mut gba, matches.value_of("script_file"))
.unwrap();
info!("ending debugger...");
return Ok(());
}
#[cfg(not(feature = "debugger"))]
{
panic!("Please compile me with 'debugger' feature");
}
}
if with_gdbserver {
todo!("gdb") todo!("gdb")
} }
let mut vsync = true;
let mut fps_counter = FpsCounter::default(); let mut fps_counter = FpsCounter::default();
let frame_time = time::Duration::new(0, 1_000_000_000u32 / 60); let frame_time = time::Duration::new(0, 1_000_000_000u32 / 60);
'running: loop { 'running: loop {
@ -244,7 +189,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
scancode: Some(scancode), scancode: Some(scancode),
.. ..
} => match scancode { } => match scancode {
Scancode::Space => frame_limiter = false, Scancode::Space => vsync = false,
k => input::on_keyboard_key_down(&mut gba, k), k => input::on_keyboard_key_down(&mut gba, k),
}, },
Event::KeyUp { Event::KeyUp {
@ -265,28 +210,28 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Scancode::F5 => { Scancode::F5 => {
info!("Saving state ..."); info!("Saving state ...");
let save = gba.save_state()?; let save = gba.save_state()?;
write_bin_file(&savestate_path, &save)?; write_bin_file(&opts.savestate_path(), &save)?;
info!( info!(
"Saved to {:?} ({})", "Saved to {:?} ({})",
savestate_path, opts.savestate_path(),
bytesize::ByteSize::b(save.len() as u64) bytesize::ByteSize::b(save.len() as u64)
); );
} }
Scancode::F9 => { Scancode::F9 => {
if savestate_path.is_file() { if opts.savestate_path().is_file() {
let save = read_bin_file(&savestate_path)?; let save = read_bin_file(&opts.savestate_path())?;
info!("Restoring state from {:?}...", savestate_path); info!("Restoring state from {:?}...", opts.savestate_path());
gba.restore_state(&save)?; gba.restore_state(&save)?;
info!("Restored!"); info!("Restored!");
} else { } else {
info!("Savestate not created, please create one by pressing F5"); info!("Savestate not created, please create one by pressing F5");
} }
} }
Scancode::Space => frame_limiter = true, Scancode::Space => vsync = true,
k => input::on_keyboard_key_up(&mut gba, k), k => input::on_keyboard_key_up(&mut gba, k),
}, },
Event::ControllerButtonDown { button, .. } => match button { Event::ControllerButtonDown { button, .. } => match button {
Button::RightStick => frame_limiter = !frame_limiter, Button::RightStick => vsync = !vsync,
b => input::on_controller_button_down(&mut gba, b), b => input::on_controller_button_down(&mut gba, b),
}, },
Event::ControllerButtonUp { button, .. } => { Event::ControllerButtonUp { button, .. } => {
@ -318,16 +263,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
Event::Quit { .. } => break 'running, Event::Quit { .. } => break 'running,
Event::DropFile { filename, .. } => { Event::DropFile { filename, .. } => {
// load the new rom todo!("impl DropFile again")
rom_path = filename;
savestate_path = get_savestate_path(&Path::new(&rom_path));
rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap();
let gamepak = GamepakBuilder::new().file(Path::new(&rom_path)).build()?;
let bios_bin = read_bin_file(bios_path).unwrap();
// create a new emulator - TODO, export to a function
gba = GameBoyAdvance::new(bios_bin.into_boxed_slice(), gamepak, audio.clone());
gba.skip_bios();
} }
_ => {} _ => {}
} }
@ -341,7 +277,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
renderer.set_window_title(&title); renderer.set_window_title(&title);
} }
if frame_limiter { if vsync {
let time_passed = start_time.elapsed(); let time_passed = start_time.elapsed();
let delay = frame_time.checked_sub(time_passed); let delay = frame_time.checked_sub(time_passed);
match delay { match delay {

View file

@ -0,0 +1,64 @@
use std::path::PathBuf;
use rustboyadvance_core::{
cartridge::{BackupType, GamepakBuilder},
prelude::Cartridge,
};
use structopt::StructOpt;
const SAVE_TYPE_POSSIBLE_VALUES: &[&str] =
&["sram", "flash128k", "flash64k", "eeprom", "autodetect"];
#[derive(StructOpt, Debug)]
#[structopt(name = "rustboyadvance-sdl2")]
pub struct Options {
/// Rom file to emulate, may be a raw dump from a cartridge or a compiled ELF file
#[structopt(name = "ROM", parse(from_os_str))]
pub rom: PathBuf,
/// Bios file to use
#[structopt(long, parse(from_os_str), default_value = "gba_bios.bin")]
pub bios: PathBuf,
/// Skip running the bios boot animation and jump straight to the ROM
#[structopt(long)]
pub skip_bios: bool,
/// Do not output sound
#[structopt(long)]
pub silent: bool,
/// Initalize gdbserver and wait for a connection from gdb
#[structopt(short = "d", long)]
pub gdbserver: bool,
/// Force emulation of RTC, use for games that have RTC but the emulator fails to detect
#[structopt(long)]
pub rtc: bool,
/// Override save type, useful for troublemaking games that fool the auto detection
#[structopt(long, default_value = "autodetect", possible_values = SAVE_TYPE_POSSIBLE_VALUES)]
pub save_type: BackupType,
}
type DynError = Box<dyn std::error::Error>;
impl Options {
pub fn cartridge_from_opts(&self) -> Result<Cartridge, DynError> {
let mut builder = GamepakBuilder::new()
.save_type(self.save_type)
.file(&self.rom);
if self.rtc {
builder = builder.with_rtc();
}
Ok(builder.build()?)
}
pub fn savestate_path(&self) -> PathBuf {
self.rom.with_extension("savestate")
}
pub fn rom_name(&self) -> &str {
self.rom.file_name().unwrap().to_str().unwrap()
}
}

View file

@ -1,24 +1,20 @@
use std::cell::RefCell; use rustboyadvance_utils::audio::SampleConsumer;
use std::rc::Rc;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped; use wasm_bindgen::Clamped;
use js_sys::Float32Array; use js_sys::Float32Array;
use web_sys::AudioContext;
use web_sys::CanvasRenderingContext2d; use web_sys::CanvasRenderingContext2d;
use rustboyadvance_core::keypad as gba_keypad; use rustboyadvance_core::keypad as gba_keypad;
use rustboyadvance_core::prelude::*; use rustboyadvance_core::prelude::*;
use rustboyadvance_utils::audio::AudioRingBuffer;
use bit::BitIndex; use bit::BitIndex;
#[wasm_bindgen] #[wasm_bindgen]
pub struct Emulator { pub struct Emulator {
gba: GameBoyAdvance, gba: GameBoyAdvance,
interface: Rc<RefCell<Interface>>, audio_consumer: SampleConsumer,
frame: Option<Box<[u8]>>, frame: Option<Box<[u8]>>,
} }
@ -33,51 +29,17 @@ fn translate_frame_to_u8(input_fb: &[u32], out_fb: &mut [u8]) {
} }
} }
struct Interface {
sample_rate: i32,
audio_ctx: AudioContext,
audio_ring_buffer: AudioRingBuffer,
}
impl Drop for Interface {
fn drop(&mut self) {
let _ = self.audio_ctx.clone();
}
}
impl Interface {
fn new(audio_ctx: AudioContext) -> Result<Interface, JsValue> {
Ok(Interface {
sample_rate: audio_ctx.sample_rate() as i32,
audio_ctx: audio_ctx,
audio_ring_buffer: Default::default(),
})
}
}
fn convert_sample(s: i16) -> f32 { fn convert_sample(s: i16) -> f32 {
(s as f32) / 32767_f32 (s as f32) / 32767_f32
} }
impl AudioInterface for Interface {
fn get_sample_rate(&self) -> i32 {
self.sample_rate
}
fn push_sample(&mut self, samples: &[i16]) {
let prod = self.audio_ring_buffer.producer();
for s in samples.iter() {
let _ = prod.push(*s);
}
}
}
#[wasm_bindgen] #[wasm_bindgen]
impl Emulator { impl Emulator {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(bios: &[u8], rom: &[u8]) -> Result<Emulator, JsValue> { pub fn new(bios: &[u8], rom: &[u8]) -> Result<Emulator, JsValue> {
let audio_ctx = web_sys::AudioContext::new()?; let audio_ctx = web_sys::AudioContext::new()?;
let interface = Rc::new(RefCell::new(Interface::new(audio_ctx)?)); let (audio_device, audio_consumer) =
SimpleAudioInterface::create_channel(audio_ctx.sample_rate() as i32, None);
let gamepak = GamepakBuilder::new() let gamepak = GamepakBuilder::new()
.take_buffer(rom.to_vec().into_boxed_slice()) .take_buffer(rom.to_vec().into_boxed_slice())
@ -85,11 +47,11 @@ impl Emulator {
.build() .build()
.unwrap(); .unwrap();
let gba = GameBoyAdvance::new(bios.to_vec().into_boxed_slice(), gamepak, interface.clone()); let gba = GameBoyAdvance::new(bios.to_vec().into_boxed_slice(), gamepak, audio_device);
Ok(Emulator { Ok(Emulator {
gba, gba,
interface, audio_consumer,
frame: Some(vec![0; 240 * 160 * 4].into_boxed_slice()), frame: Some(vec![0; 240 * 160 * 4].into_boxed_slice()),
}) })
} }
@ -152,12 +114,9 @@ impl Emulator {
} }
} }
pub fn collect_audio_samples(&self) -> Result<Float32Array, JsValue> { pub fn collect_audio_samples(&mut self) -> Result<Float32Array, JsValue> {
let mut interface = self.interface.borrow_mut(); let mut samples = Vec::with_capacity(self.audio_consumer.len());
while let Some(sample) = self.audio_consumer.pop() {
let consumer = interface.audio_ring_buffer.consumer();
let mut samples = Vec::with_capacity(consumer.len());
while let Some(sample) = consumer.pop() {
samples.push(convert_sample(sample)); samples.push(convert_sample(sample));
} }

View file

@ -90,10 +90,12 @@ macro_rules! host_breakpoint {
pub mod audio { pub mod audio {
pub use ringbuf::{Consumer, Producer, RingBuffer}; pub use ringbuf::{Consumer, Producer, RingBuffer};
pub type SampleProducer = Producer<i16>;
pub type SampleConsumer = Consumer<i16>;
pub struct AudioRingBuffer { pub struct AudioRingBuffer {
prod: Producer<i16>, prod: SampleProducer,
cons: Consumer<i16>, cons: SampleConsumer,
} }
impl Default for AudioRingBuffer { impl Default for AudioRingBuffer {
@ -110,15 +112,15 @@ pub mod audio {
AudioRingBuffer { prod, cons } AudioRingBuffer { prod, cons }
} }
pub fn producer(&mut self) -> &mut Producer<i16> { pub fn producer(&mut self) -> &mut SampleProducer {
&mut self.prod &mut self.prod
} }
pub fn consumer(&mut self) -> &mut Consumer<i16> { pub fn consumer(&mut self) -> &mut SampleConsumer {
&mut self.cons &mut self.cons
} }
pub fn split(self) -> (Producer<i16>, Consumer<i16>) { pub fn split(self) -> (SampleProducer, SampleConsumer) {
(self.prod, self.cons) (self.prod, self.cons)
} }
} }