restructure
This commit is contained in:
parent
8edf466271
commit
608817e3b7
52
gb-emu/src/audio.rs
Normal file
52
gb-emu/src/audio.rs
Normal file
|
@ -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)
|
||||
}
|
|
@ -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<Window>,
|
||||
scaled_buf: Vec<u32>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
factor: usize,
|
||||
gamepad_handler: Option<Gilrs>,
|
||||
joypad_state: JoypadState,
|
||||
current_rumble: bool,
|
||||
}
|
||||
|
||||
impl WindowRenderer {
|
||||
fn new(factor: usize, gamepad_handler: Option<Gilrs>) -> Self {
|
||||
Self {
|
||||
window: None,
|
||||
scaled_buf: vec![],
|
||||
width: 0,
|
||||
height: 0,
|
||||
factor,
|
||||
gamepad_handler,
|
||||
joypad_state: JoypadState::default(),
|
||||
current_rumble: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer<u32> 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::<Vec<_>>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
148
gb-emu/src/window.rs
Normal file
148
gb-emu/src/window.rs
Normal file
|
@ -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<Window>,
|
||||
scaled_buf: Vec<u32>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
factor: usize,
|
||||
gamepad_handler: Option<Gilrs>,
|
||||
joypad_state: JoypadState,
|
||||
current_rumble: bool,
|
||||
}
|
||||
|
||||
impl WindowRenderer {
|
||||
pub fn new(factor: usize, gamepad_handler: Option<Gilrs>) -> Self {
|
||||
Self {
|
||||
window: None,
|
||||
scaled_buf: vec![],
|
||||
width: 0,
|
||||
height: 0,
|
||||
factor,
|
||||
gamepad_handler,
|
||||
joypad_state: JoypadState::default(),
|
||||
current_rumble: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer<u32> 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::<Vec<_>>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue