diff --git a/gb-emu/src/audio.rs b/gb-emu/src/audio.rs new file mode 100644 index 0000000..332b5b3 --- /dev/null +++ b/gb-emu/src/audio.rs @@ -0,0 +1,52 @@ +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Stream, +}; +use futures::executor; +use gb_emu_lib::connect::{AudioOutput, DownsampleType}; + +const FRAMES_TO_BUFFER: usize = 1; +const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::ZeroOrderHold; + +pub fn create_output() -> (AudioOutput, Stream) { + let host = cpal::default_host(); + + let device = host + .default_output_device() + .expect("no output device available"); + + let mut supported_configs_range = device + .supported_output_configs() + .expect("error while querying configs"); + let config = supported_configs_range + .next() + .expect("no supported config?!") + .with_max_sample_rate(); + + let sample_rate = config.sample_rate().0; + + let (output, mut rx) = + AudioOutput::new(sample_rate as f32, true, FRAMES_TO_BUFFER, DOWNSAMPLE_TYPE); + + let stream = device + .build_output_stream( + &config.config(), + move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| { + for v in data.chunks_exact_mut(2) { + match executor::block_on(rx.pop()) { + Some(a) => v.copy_from_slice(&a), + None => panic!("Audio queue disconnected!"), + } + } + }, + move |err| { + // react to errors here. + println!("audio error: {err}"); + }, + None, + ) + .unwrap(); + stream.play().unwrap(); + + (output, stream) +} diff --git a/gb-emu/src/main.rs b/gb-emu/src/main.rs index ad67865..37942c1 100644 --- a/gb-emu/src/main.rs +++ b/gb-emu/src/main.rs @@ -1,26 +1,18 @@ #![feature(let_chains)] -use std::sync::mpsc::channel; - use clap::{ArgGroup, Parser}; -use cpal::{ - traits::{DeviceTrait, HostTrait, StreamTrait}, - Stream, -}; -use futures::executor; + use gb_emu_lib::{ - connect::{ - AudioOutput, DownsampleType, EmulatorMessage, EmulatorOptions, JoypadState, Renderer, - RomFile, SerialTarget, - }, - util::scale_buffer, + connect::{EmulatorMessage, EmulatorOptions, RomFile, SerialTarget}, EmulatorCore, }; -use gilrs::{ - ff::{BaseEffect, BaseEffectType, EffectBuilder, Replay, Ticks}, - Button, Gilrs, -}; -use minifb::{Key, Window, WindowOptions}; +use gilrs::Gilrs; + +use std::sync::mpsc::channel; +use window::WindowRenderer; + +mod audio; +mod window; /// Gameboy (DMG-A/B/C) emulator #[derive(Parser, Debug)] @@ -88,7 +80,7 @@ fn main() { ctrlc::set_handler(move || sender.send(EmulatorMessage::Stop).unwrap()).unwrap(); - let (output, _stream) = create_audio_output(); + let (output, _stream) = audio::create_output(); let options = EmulatorOptions::new( WindowRenderer::new(factor, Some(Gilrs::new().unwrap())), @@ -117,188 +109,3 @@ fn main() { }, } } - -const FRAMES_TO_BUFFER: usize = 1; -const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::ZeroOrderHold; - -fn create_audio_output() -> (AudioOutput, Stream) { - let host = cpal::default_host(); - - let device = host - .default_output_device() - .expect("no output device available"); - - let mut supported_configs_range = device - .supported_output_configs() - .expect("error while querying configs"); - let config = supported_configs_range - .next() - .expect("no supported config?!") - .with_max_sample_rate(); - - let sample_rate = config.sample_rate().0; - - let (output, mut rx) = - AudioOutput::new(sample_rate as f32, true, FRAMES_TO_BUFFER, DOWNSAMPLE_TYPE); - - let stream = device - .build_output_stream( - &config.config(), - move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| { - for v in data.chunks_exact_mut(2) { - match executor::block_on(rx.pop()) { - Some(a) => v.copy_from_slice(&a), - None => panic!("Audio queue disconnected!"), - } - } - }, - move |err| { - // react to errors here. - println!("audio error: {err}"); - }, - None, - ) - .unwrap(); - stream.play().unwrap(); - - (output, stream) -} - -struct WindowRenderer { - window: Option, - scaled_buf: Vec, - width: usize, - height: usize, - factor: usize, - gamepad_handler: Option, - joypad_state: JoypadState, - current_rumble: bool, -} - -impl WindowRenderer { - fn new(factor: usize, gamepad_handler: Option) -> Self { - Self { - window: None, - scaled_buf: vec![], - width: 0, - height: 0, - factor, - gamepad_handler, - joypad_state: JoypadState::default(), - current_rumble: false, - } - } -} - -impl Renderer for WindowRenderer { - fn prepare(&mut self, width: usize, height: usize) { - self.width = width; - self.height = height; - self.window = Some( - Window::new( - "Gameboy", - width * self.factor, - height * self.factor, - WindowOptions::default(), - ) - .unwrap(), - ); - } - - fn display(&mut self, buffer: &[u32]) { - if let Some(ref mut window) = self.window { - self.scaled_buf = scale_buffer(buffer, self.width, self.height, self.factor); - window - .update_with_buffer( - &self.scaled_buf, - self.width * self.factor, - self.height * self.factor, - ) - .unwrap(); - } - } - - fn set_title(&mut self, title: String) { - if let Some(ref mut window) = self.window { - window.set_title(&title); - } - } - - fn latest_joypad_state(&mut self) -> JoypadState { - self.joypad_state.reset(); - - if let Some(ref mut gamepad_handler) = self.gamepad_handler { - while let Some(event) = gamepad_handler.next_event() { - if let gilrs::EventType::ButtonPressed(button, _) = event.event { - match button { - Button::DPadDown => self.joypad_state.down = true, - Button::DPadUp => self.joypad_state.up = true, - Button::DPadLeft => self.joypad_state.left = true, - Button::DPadRight => self.joypad_state.right = true, - Button::Start => self.joypad_state.start = true, - Button::Select => self.joypad_state.select = true, - Button::East => self.joypad_state.a = true, - Button::South => self.joypad_state.b = true, - _ => {} - } - } - } - - for (_, pad) in gamepad_handler.gamepads() { - self.joypad_state.down |= pad.is_pressed(Button::DPadDown); - self.joypad_state.up |= pad.is_pressed(Button::DPadUp); - self.joypad_state.left |= pad.is_pressed(Button::DPadLeft); - self.joypad_state.right |= pad.is_pressed(Button::DPadRight); - self.joypad_state.start |= pad.is_pressed(Button::Start); - self.joypad_state.select |= pad.is_pressed(Button::Select); - self.joypad_state.a |= pad.is_pressed(Button::East); - self.joypad_state.b |= pad.is_pressed(Button::South); - } - } - - if let Some(window) = &self.window { - let keys = window.get_keys(); - - self.joypad_state.down |= keys.contains(&Key::Down) || keys.contains(&Key::S); - self.joypad_state.up |= keys.contains(&Key::Up) || keys.contains(&Key::W); - self.joypad_state.left |= keys.contains(&Key::Left) || keys.contains(&Key::A); - self.joypad_state.right |= keys.contains(&Key::Right) || keys.contains(&Key::D); - self.joypad_state.start |= keys.contains(&Key::Equal); - self.joypad_state.select |= keys.contains(&Key::Minus); - self.joypad_state.a |= keys.contains(&Key::Apostrophe); - self.joypad_state.b |= keys.contains(&Key::Semicolon); - } - - self.joypad_state - } - - fn set_rumble(&mut self, rumbling: bool) { - if rumbling != self.current_rumble && let Some(ref mut gamepad_handler) = self.gamepad_handler { - self.current_rumble = rumbling; - - let ids = gamepad_handler - .gamepads() - .filter_map(|(id, gp)| if gp.is_ff_supported() { Some(id) } else { None }) - .collect::>(); - if ids.is_empty() { - return; - } - - let magnitude = if rumbling { 0xFF } else { 0x0 }; - - EffectBuilder::new() - .add_effect(BaseEffect { - kind: BaseEffectType::Strong { magnitude }, - scheduling: Replay { - after: Ticks::from_ms(0), - play_for: Ticks::from_ms(16), - with_delay: Ticks::from_ms(0), - }, - envelope: Default::default(), - }) - .gamepads(&ids) - .finish(gamepad_handler) - .unwrap(); - } - } -} diff --git a/gb-emu/src/window.rs b/gb-emu/src/window.rs new file mode 100644 index 0000000..01297f3 --- /dev/null +++ b/gb-emu/src/window.rs @@ -0,0 +1,148 @@ +use gb_emu_lib::{ + connect::{JoypadState, Renderer}, + util::scale_buffer, +}; +use gilrs::{ + ff::{BaseEffect, BaseEffectType, EffectBuilder, Replay, Ticks}, + Button, Gilrs, +}; +use minifb::{Key, Window, WindowOptions}; + +pub struct WindowRenderer { + window: Option, + scaled_buf: Vec, + width: usize, + height: usize, + factor: usize, + gamepad_handler: Option, + joypad_state: JoypadState, + current_rumble: bool, +} + +impl WindowRenderer { + pub fn new(factor: usize, gamepad_handler: Option) -> Self { + Self { + window: None, + scaled_buf: vec![], + width: 0, + height: 0, + factor, + gamepad_handler, + joypad_state: JoypadState::default(), + current_rumble: false, + } + } +} + +impl Renderer for WindowRenderer { + fn prepare(&mut self, width: usize, height: usize) { + self.width = width; + self.height = height; + self.window = Some( + Window::new( + "Gameboy", + width * self.factor, + height * self.factor, + WindowOptions::default(), + ) + .unwrap(), + ); + } + + fn display(&mut self, buffer: &[u32]) { + if let Some(ref mut window) = self.window { + self.scaled_buf = scale_buffer(buffer, self.width, self.height, self.factor); + window + .update_with_buffer( + &self.scaled_buf, + self.width * self.factor, + self.height * self.factor, + ) + .unwrap(); + } + } + + fn set_title(&mut self, title: String) { + if let Some(ref mut window) = self.window { + window.set_title(&title); + } + } + + fn latest_joypad_state(&mut self) -> JoypadState { + self.joypad_state.reset(); + + if let Some(ref mut gamepad_handler) = self.gamepad_handler { + while let Some(event) = gamepad_handler.next_event() { + if let gilrs::EventType::ButtonPressed(button, _) = event.event { + match button { + Button::DPadDown => self.joypad_state.down = true, + Button::DPadUp => self.joypad_state.up = true, + Button::DPadLeft => self.joypad_state.left = true, + Button::DPadRight => self.joypad_state.right = true, + Button::Start => self.joypad_state.start = true, + Button::Select => self.joypad_state.select = true, + Button::East => self.joypad_state.a = true, + Button::South => self.joypad_state.b = true, + _ => {} + } + } + } + + for (_, pad) in gamepad_handler.gamepads() { + self.joypad_state.down |= pad.is_pressed(Button::DPadDown); + self.joypad_state.up |= pad.is_pressed(Button::DPadUp); + self.joypad_state.left |= pad.is_pressed(Button::DPadLeft); + self.joypad_state.right |= pad.is_pressed(Button::DPadRight); + self.joypad_state.start |= pad.is_pressed(Button::Start); + self.joypad_state.select |= pad.is_pressed(Button::Select); + self.joypad_state.a |= pad.is_pressed(Button::East); + self.joypad_state.b |= pad.is_pressed(Button::South); + } + } + + if let Some(window) = &self.window { + let keys = window.get_keys(); + + self.joypad_state.down |= keys.contains(&Key::Down) || keys.contains(&Key::S); + self.joypad_state.up |= keys.contains(&Key::Up) || keys.contains(&Key::W); + self.joypad_state.left |= keys.contains(&Key::Left) || keys.contains(&Key::A); + self.joypad_state.right |= keys.contains(&Key::Right) || keys.contains(&Key::D); + self.joypad_state.start |= keys.contains(&Key::Equal); + self.joypad_state.select |= keys.contains(&Key::Minus); + self.joypad_state.a |= keys.contains(&Key::Apostrophe); + self.joypad_state.b |= keys.contains(&Key::Semicolon); + } + + self.joypad_state + } + + fn set_rumble(&mut self, rumbling: bool) { + if rumbling != self.current_rumble && let Some(ref mut gamepad_handler) = self.gamepad_handler { + self.current_rumble = rumbling; + + let ids = gamepad_handler + .gamepads() + .filter_map(|(id, gp)| if gp.is_ff_supported() { Some(id) } else { None }) + .collect::>(); + if ids.is_empty() { + return; + } + + let magnitude = if rumbling { 0xFF } else { 0x0 }; + + EffectBuilder::new() + .add_effect(BaseEffect { + kind: BaseEffectType::Strong { magnitude }, + scheduling: Replay { + after: Ticks::from_ms(0), + play_for: Ticks::from_ms(16), + with_delay: Ticks::from_ms(0), + }, + envelope: Default::default(), + }) + .gamepads(&ids) + .finish(gamepad_handler) + .unwrap(); + } + } +}