From caf3a9426e8432db0ddd87fd1e7099f525ad3298 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 24 Mar 2025 23:22:47 -0400 Subject: [PATCH] Add a fast-forward command --- src/app.rs | 10 +-- src/audio.rs | 22 +++++- src/emulator.rs | 10 +++ src/input.rs | 163 ++++++++++++++++++++++++++++++++-------- src/window.rs | 4 +- src/window/game.rs | 8 +- src/window/hotkeys.rs | 150 ++++++++++++++++++++++++++++++++++++ src/window/shortcuts.rs | 103 ------------------------- 8 files changed, 325 insertions(+), 145 deletions(-) create mode 100644 src/window/hotkeys.rs delete mode 100644 src/window/shortcuts.rs diff --git a/src/app.rs b/src/app.rs index f1deb45..f715617 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,7 +24,7 @@ use crate::{ persistence::Persistence, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow, - GdbServerWindow, InputWindow, ObjectWindow, RegisterWindow, ShortcutsWindow, WorldWindow, + GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, RegisterWindow, WorldWindow, }, }; @@ -252,9 +252,9 @@ impl ApplicationHandler for Application { let input = InputWindow::new(self.mappings.clone()); self.open(event_loop, Box::new(input)); } - UserEvent::OpenShortcuts => { - let shortcuts = ShortcutsWindow::new(self.shortcuts.clone()); - self.open(event_loop, Box::new(shortcuts)); + UserEvent::OpenHotkeys => { + let hotkeys = HotkeysWindow::new(self.shortcuts.clone()); + self.open(event_loop, Box::new(hotkeys)); } UserEvent::OpenPlayer2 => { let p2 = GameWindow::new( @@ -515,7 +515,7 @@ pub enum UserEvent { OpenRegisters(SimId), OpenDebugger(SimId), OpenInput, - OpenShortcuts, + OpenHotkeys, OpenPlayer2, Quit(SimId), } diff --git a/src/audio.rs b/src/audio.rs index c5653a0..351c9f7 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -3,18 +3,20 @@ use std::time::Duration; use anyhow::{Result, bail}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use itertools::Itertools; -use rubato::{FftFixedInOut, Resampler}; +use rubato::{FastFixedOut, Resampler}; use tracing::error; pub struct Audio { #[allow(unused)] stream: cpal::Stream, - sampler: FftFixedInOut, + sampler: FastFixedOut, input_buffer: Vec>, output_buffer: Vec>, sample_sink: rtrb::Producer, } +const VB_FREQUENCY: usize = 41700; + impl Audio { pub fn init() -> Result { let host = cpal::default_host(); @@ -28,7 +30,15 @@ impl Audio { bail!("No suitable output config available"); }; let mut config = config.with_max_sample_rate().config(); - let sampler = FftFixedInOut::new(41700, config.sample_rate.0 as usize, 834, 2)?; + let resample_ratio = config.sample_rate.0 as f64 / VB_FREQUENCY as f64; + let chunk_size = (834.0 * resample_ratio) as usize; + let sampler = FastFixedOut::new( + resample_ratio, + 64.0, + rubato::PolynomialDegree::Cubic, + chunk_size, + 2, + )?; config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32); let input_buffer = sampler.input_buffer_allocate(true); @@ -101,4 +111,10 @@ impl Audio { std::thread::sleep(Duration::from_micros(500)); } } + + pub fn set_speed(&mut self, speed: f64) -> Result<()> { + self.sampler + .set_resample_ratio_relative(1.0 / speed, false)?; + Ok(()) + } } diff --git a/src/emulator.rs b/src/emulator.rs index 6ed9a2b..f2e83dd 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -311,6 +311,10 @@ impl Emulator { } } + fn set_speed(&mut self, speed: f64) -> Result<()> { + self.audio.set_speed(speed) + } + fn save_sram(&mut self, sim_id: SimId) -> Result<()> { let sim = self.sims.get_mut(sim_id.to_index()); let cart = self.carts[sim_id.to_index()].as_mut(); @@ -567,6 +571,11 @@ impl Emulator { EmulatorCommand::FrameAdvance => { self.frame_advance(); } + EmulatorCommand::SetSpeed(speed) => { + if let Err(error) = self.set_speed(speed) { + self.report_error(SimId::Player1, format!("Error setting speed: {error}")); + } + } EmulatorCommand::StartDebugging(sim_id, debugger) => { self.start_debugging(sim_id, debugger); } @@ -694,6 +703,7 @@ pub enum EmulatorCommand { Pause, Resume, FrameAdvance, + SetSpeed(f64), StartDebugging(SimId, DebugSender), StopDebugging(SimId), DebugInterrupt(SimId), diff --git a/src/input.rs b/src/input.rs index bec1d17..ce7a06f 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,13 +1,13 @@ use std::{ cmp::Ordering, - collections::{HashMap, HashSet, hash_map::Entry}, + collections::{HashMap, hash_map::Entry}, fmt::Display, str::FromStr, sync::{Arc, Mutex, RwLock}, }; use anyhow::anyhow; -use egui::{Key, KeyboardShortcut, Modifiers}; +use egui::{Event, Key, KeyboardShortcut, Modifiers}; use gilrs::{Axis, Button, Gamepad, GamepadId, ev::Code}; use serde::{Deserialize, Serialize}; use winit::keyboard::{KeyCode, PhysicalKey}; @@ -468,19 +468,21 @@ pub enum Command { OpenRom, Quit, FrameAdvance, + FastForward(u32), Reset, PauseResume, // if you update this, update Command::all and add a default } impl Command { - pub fn all() -> [Self; 5] { + pub fn all() -> [Self; 6] { [ Self::OpenRom, Self::Quit, Self::PauseResume, Self::Reset, Self::FrameAdvance, + Self::FastForward(0), ] } @@ -491,6 +493,7 @@ impl Command { Self::PauseResume => "Pause/Resume", Self::Reset => "Reset", Self::FrameAdvance => "Frame Advance", + Self::FastForward(_) => "Fast Forward", } } } @@ -532,6 +535,10 @@ impl Default for Shortcuts { Command::FrameAdvance, KeyboardShortcut::new(Modifiers::NONE, Key::F6), ); + shortcuts.set( + Command::FastForward(0), + KeyboardShortcut::new(Modifiers::NONE, Key::Space), + ); shortcuts } } @@ -557,13 +564,11 @@ impl Shortcuts { } } - fn save(&self) -> PersistedShortcuts { - let mut shortcuts = PersistedShortcuts { shortcuts: vec![] }; + fn save(&self, saved: &mut PersistedSettings) { for command in Command::all() { let shortcut = self.by_command.get(&command).copied(); - shortcuts.shortcuts.push((command, shortcut)); + saved.shortcuts.push((command, shortcut)); } - shortcuts } } @@ -589,52 +594,123 @@ fn specificity(modifiers: egui::Modifiers) -> usize { mods } -#[derive(Serialize, Deserialize)] -struct PersistedShortcuts { +#[derive(Serialize, Deserialize, Default)] +struct PersistedSettings { shortcuts: Vec<(Command, Option)>, + #[serde(default)] + ff_settings: FastForwardSettings, +} + +#[derive(Default, Clone)] +struct ShortcutState { + ff_toggled: bool, +} + +#[derive(Default)] +struct Settings { + shortcuts: Shortcuts, + ff_settings: FastForwardSettings, + state: ShortcutState, +} + +impl Settings { + fn save(&self) -> PersistedSettings { + let mut saved = PersistedSettings { + shortcuts: vec![], + ff_settings: self.ff_settings.clone(), + }; + self.shortcuts.save(&mut saved); + saved + } } #[derive(Clone)] pub struct ShortcutProvider { persistence: Persistence, - shortcuts: Arc>, + settings: Arc>, } impl ShortcutProvider { pub fn new(persistence: Persistence) -> Self { - let mut shortcuts = Shortcuts::default(); - if let Ok(saved) = persistence.load_config::("shortcuts") { + let mut settings = Settings::default(); + if let Ok(saved) = persistence.load_config::("shortcuts") { for (command, shortcut) in saved.shortcuts { if let Some(shortcut) = shortcut { - shortcuts.set(command, shortcut); + settings.shortcuts.set(command, shortcut); } else { - shortcuts.unset(command); + settings.shortcuts.unset(command); } } - } + settings.ff_settings = saved.ff_settings; + }; Self { persistence, - shortcuts: Arc::new(Mutex::new(shortcuts)), + settings: Arc::new(Mutex::new(settings)), } } pub fn shortcut_for(&self, command: Command) -> Option { - let lock = self.shortcuts.lock().unwrap(); - lock.by_command.get(&command).copied() + let lock = self.settings.lock().unwrap(); + lock.shortcuts.by_command.get(&command).copied() } - pub fn consume_all(&self, input: &mut egui::InputState) -> HashSet { - let lock = self.shortcuts.lock().unwrap(); - lock.all - .iter() - .filter_map(|(command, shortcut)| input.consume_shortcut(shortcut).then_some(*command)) - .collect() + pub fn ff_settings(&self) -> FastForwardSettings { + let lock = self.settings.lock().unwrap(); + lock.ff_settings.clone() + } + + pub fn consume_all(&self, input: &mut egui::InputState) -> Vec { + let mut lock = self.settings.lock().unwrap(); + let mut state = lock.state.clone(); + let mut consumed = vec![]; + for (command, shortcut) in &lock.shortcuts.all { + input.events.retain(|event| { + let Event::Key { + key, + pressed, + repeat, + modifiers, + .. + } = event + else { + return true; + }; + if shortcut.logical_key != *key || !shortcut.modifiers.contains(*modifiers) { + return true; + } + if matches!(command, Command::FastForward(_)) { + if *repeat { + return true; + } + let sped_up = if lock.ff_settings.toggle { + if !*pressed { + return true; + } + state.ff_toggled = !state.ff_toggled; + state.ff_toggled + } else { + *pressed + }; + let speed = if sped_up { lock.ff_settings.speed } else { 1 }; + consumed.push(Command::FastForward(speed)); + false + } else { + if !*pressed { + return true; + } + consumed.push(*command); + false + } + }); + } + lock.state = state; + consumed } pub fn set(&self, command: Command, shortcut: KeyboardShortcut) { let updated = { - let mut lock = self.shortcuts.lock().unwrap(); - lock.set(command, shortcut); + let mut lock = self.settings.lock().unwrap(); + lock.shortcuts.set(command, shortcut); lock.save() }; let _ = self.persistence.save_config("shortcuts", &updated); @@ -642,8 +718,20 @@ impl ShortcutProvider { pub fn unset(&self, command: Command) { let updated = { - let mut lock = self.shortcuts.lock().unwrap(); - lock.unset(command); + let mut lock = self.settings.lock().unwrap(); + lock.shortcuts.unset(command); + lock.save() + }; + let _ = self.persistence.save_config("shortcuts", &updated); + } + + pub fn update_ff_settings(&self, ff_settings: FastForwardSettings) { + let updated = { + let mut lock = self.settings.lock().unwrap(); + lock.ff_settings = ff_settings; + if !lock.ff_settings.toggle { + lock.state.ff_toggled = false; + } lock.save() }; let _ = self.persistence.save_config("shortcuts", &updated); @@ -651,10 +739,25 @@ impl ShortcutProvider { pub fn reset(&self) { let updated = { - let mut lock = self.shortcuts.lock().unwrap(); - *lock = Shortcuts::default(); + let mut lock = self.settings.lock().unwrap(); + *lock = Settings::default(); lock.save() }; let _ = self.persistence.save_config("shortcuts", &updated); } } + +#[derive(Serialize, Deserialize, Clone)] +pub struct FastForwardSettings { + pub toggle: bool, + pub speed: u32, +} + +impl Default for FastForwardSettings { + fn default() -> Self { + Self { + toggle: false, + speed: 10, + } + } +} diff --git a/src/window.rs b/src/window.rs index ff8bb6d..2573d82 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,8 +2,8 @@ pub use about::AboutWindow; use egui::{Context, ViewportBuilder, ViewportId}; pub use game::GameWindow; pub use gdb::GdbServerWindow; +pub use hotkeys::HotkeysWindow; pub use input::InputWindow; -pub use shortcuts::ShortcutsWindow; pub use vip::{ BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, }; @@ -15,8 +15,8 @@ mod about; mod game; mod game_screen; mod gdb; +mod hotkeys; mod input; -mod shortcuts; mod utils; mod vip; diff --git a/src/window/game.rs b/src/window/game.rs index 622b2ff..4ec3c25 100644 --- a/src/window/game.rs +++ b/src/window/game.rs @@ -109,6 +109,10 @@ impl GameWindow { self.client.send_command(EmulatorCommand::FrameAdvance); } } + Command::FastForward(speed) => { + self.client + .send_command(EmulatorCommand::SetSpeed(speed as f64)); + } } } @@ -358,8 +362,8 @@ impl GameWindow { ui.close_menu(); } }); - if ui.button("Key Shortcuts").clicked() { - self.proxy.send_event(UserEvent::OpenShortcuts).unwrap(); + if ui.button("Hotkeys").clicked() { + self.proxy.send_event(UserEvent::OpenHotkeys).unwrap(); ui.close_menu(); } } diff --git a/src/window/hotkeys.rs b/src/window/hotkeys.rs new file mode 100644 index 0000000..5b8a835 --- /dev/null +++ b/src/window/hotkeys.rs @@ -0,0 +1,150 @@ +use egui::{ + Button, CentralPanel, Context, Event, KeyboardShortcut, Label, Layout, Slider, Ui, + ViewportBuilder, ViewportId, +}; +use egui_extras::{Column, TableBuilder}; + +use crate::input::{Command, ShortcutProvider}; + +use super::{AppWindow, utils::UiExt}; + +pub struct HotkeysWindow { + shortcuts: ShortcutProvider, + now_binding: Option, +} + +impl HotkeysWindow { + pub fn new(shortcuts: ShortcutProvider) -> Self { + Self { + shortcuts, + now_binding: None, + } + } + + fn show_shortcuts(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + ui.section("Shortcuts", |ui| { + let width = ui.available_width() - 16.0; + TableBuilder::new(ui) + .column(Column::exact(width * 0.3)) + .column(Column::exact(width * 0.5)) + .column(Column::exact(width * 0.2)) + .cell_layout(Layout::left_to_right(egui::Align::Center)) + .body(|mut body| { + for command in Command::all() { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.add_sized(ui.available_size(), Label::new(command.name())); + }); + row.col(|ui| { + let button = if self.now_binding == Some(command) { + Button::new("Binding...") + } else if let Some(shortcut) = self.shortcuts.shortcut_for(command) + { + Button::new(ui.ctx().format_shortcut(&shortcut)) + } else { + Button::new("") + }; + if ui.add_sized(ui.available_size(), button).clicked() { + self.now_binding = Some(command); + } + }); + row.col(|ui| { + if ui + .add_sized(ui.available_size(), Button::new("Clear")) + .clicked() + { + self.shortcuts.unset(command); + self.now_binding = None; + } + }); + }); + } + }); + }); + if let Some(command) = self.now_binding { + if let Some(shortcut) = ui.input_mut(|i| { + i.events.iter().find_map(|event| match event { + Event::Key { + key, + pressed: true, + modifiers, + .. + } => Some(KeyboardShortcut::new(*modifiers, *key)), + _ => None, + }) + }) { + self.shortcuts.set(command, shortcut); + self.now_binding = None; + } + } + } + + fn show_ff_settings(&mut self, ui: &mut Ui) { + let row_height = ui.spacing().interact_size.y; + ui.section("Fast Forward", |ui| { + let width = ui.available_width() - 8.0; + let mut ff_settings = self.shortcuts.ff_settings(); + let mut updated = false; + TableBuilder::new(ui) + .column(Column::exact(width * 0.5)) + .column(Column::exact(width * 0.5)) + .body(|mut body| { + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Treat button as toggle"); + }); + row.col(|ui| { + if ui.checkbox(&mut ff_settings.toggle, "").changed() { + updated = true; + } + }); + }); + body.row(row_height, |mut row| { + row.col(|ui| { + ui.label("Speed multiplier"); + }); + row.col(|ui| { + if ui + .add_sized( + ui.available_size(), + Slider::new(&mut ff_settings.speed, 1..=15), + ) + .changed() + { + updated = true; + } + }); + }); + }); + if updated { + self.shortcuts.update_ff_settings(ff_settings); + } + }); + } +} + +impl AppWindow for HotkeysWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of("shortcuts") + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title("Keyboard Shortcuts") + .with_inner_size((400.0, 400.0)) + } + + fn show(&mut self, ctx: &Context) { + CentralPanel::default().show(ctx, |ui| { + ui.horizontal(|ui| { + if ui.button("Use defaults").clicked() { + self.shortcuts.reset(); + } + }); + ui.separator(); + self.show_shortcuts(ui); + self.show_ff_settings(ui); + }); + } +} diff --git a/src/window/shortcuts.rs b/src/window/shortcuts.rs deleted file mode 100644 index ad7742f..0000000 --- a/src/window/shortcuts.rs +++ /dev/null @@ -1,103 +0,0 @@ -use egui::{ - Button, CentralPanel, Context, Event, KeyboardShortcut, Label, Layout, Ui, ViewportBuilder, - ViewportId, -}; -use egui_extras::{Column, TableBuilder}; - -use crate::input::{Command, ShortcutProvider}; - -use super::AppWindow; - -pub struct ShortcutsWindow { - shortcuts: ShortcutProvider, - now_binding: Option, -} - -impl ShortcutsWindow { - pub fn new(shortcuts: ShortcutProvider) -> Self { - Self { - shortcuts, - now_binding: None, - } - } - - fn show_shortcuts(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - if ui.button("Use defaults").clicked() { - self.shortcuts.reset(); - } - }); - ui.separator(); - let row_height = ui.spacing().interact_size.y; - let width = ui.available_width() - 20.0; - TableBuilder::new(ui) - .column(Column::exact(width * 0.3)) - .column(Column::exact(width * 0.5)) - .column(Column::exact(width * 0.2)) - .cell_layout(Layout::left_to_right(egui::Align::Center)) - .body(|mut body| { - for command in Command::all() { - body.row(row_height, |mut row| { - row.col(|ui| { - ui.add_sized(ui.available_size(), Label::new(command.name())); - }); - row.col(|ui| { - let button = if self.now_binding == Some(command) { - Button::new("Binding...") - } else if let Some(shortcut) = self.shortcuts.shortcut_for(command) { - Button::new(ui.ctx().format_shortcut(&shortcut)) - } else { - Button::new("") - }; - if ui.add_sized(ui.available_size(), button).clicked() { - self.now_binding = Some(command); - } - }); - row.col(|ui| { - if ui - .add_sized(ui.available_size(), Button::new("Clear")) - .clicked() - { - self.shortcuts.unset(command); - self.now_binding = None; - } - }); - }); - } - }); - if let Some(command) = self.now_binding { - if let Some(shortcut) = ui.input_mut(|i| { - i.events.iter().find_map(|event| match event { - Event::Key { - key, - pressed: true, - modifiers, - .. - } => Some(KeyboardShortcut::new(*modifiers, *key)), - _ => None, - }) - }) { - self.shortcuts.set(command, shortcut); - self.now_binding = None; - } - } - } -} - -impl AppWindow for ShortcutsWindow { - fn viewport_id(&self) -> ViewportId { - ViewportId::from_hash_of("shortcuts") - } - - fn initial_viewport(&self) -> ViewportBuilder { - ViewportBuilder::default() - .with_title("Keyboard Shortcuts") - .with_inner_size((400.0, 400.0)) - } - - fn show(&mut self, ctx: &Context) { - CentralPanel::default().show(ctx, |ui| { - self.show_shortcuts(ui); - }); - } -}