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)]
|
#![feature(let_chains)]
|
||||||
|
|
||||||
use std::sync::mpsc::channel;
|
|
||||||
|
|
||||||
use clap::{ArgGroup, Parser};
|
use clap::{ArgGroup, Parser};
|
||||||
use cpal::{
|
|
||||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
|
||||||
Stream,
|
|
||||||
};
|
|
||||||
use futures::executor;
|
|
||||||
use gb_emu_lib::{
|
use gb_emu_lib::{
|
||||||
connect::{
|
connect::{EmulatorMessage, EmulatorOptions, RomFile, SerialTarget},
|
||||||
AudioOutput, DownsampleType, EmulatorMessage, EmulatorOptions, JoypadState, Renderer,
|
|
||||||
RomFile, SerialTarget,
|
|
||||||
},
|
|
||||||
util::scale_buffer,
|
|
||||||
EmulatorCore,
|
EmulatorCore,
|
||||||
};
|
};
|
||||||
use gilrs::{
|
use gilrs::Gilrs;
|
||||||
ff::{BaseEffect, BaseEffectType, EffectBuilder, Replay, Ticks},
|
|
||||||
Button, Gilrs,
|
use std::sync::mpsc::channel;
|
||||||
};
|
use window::WindowRenderer;
|
||||||
use minifb::{Key, Window, WindowOptions};
|
|
||||||
|
mod audio;
|
||||||
|
mod window;
|
||||||
|
|
||||||
/// Gameboy (DMG-A/B/C) emulator
|
/// Gameboy (DMG-A/B/C) emulator
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
@ -88,7 +80,7 @@ fn main() {
|
||||||
|
|
||||||
ctrlc::set_handler(move || sender.send(EmulatorMessage::Stop).unwrap()).unwrap();
|
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(
|
let options = EmulatorOptions::new(
|
||||||
WindowRenderer::new(factor, Some(Gilrs::new().unwrap())),
|
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