From 9cfa17ee0e34317bf283841167ac5234f3de4140 Mon Sep 17 00:00:00 2001 From: Alex Janka Date: Wed, 1 Nov 2023 17:13:52 +1100 Subject: [PATCH] some of most settings --- Cargo.lock | 1 + gb-emu/Cargo.toml | 1 + gb-emu/src/bin/macos/mod.rs | 18 +- gb-emu/src/bin/macos/preferences.rs | 39 +- gb-emu/src/bin/macos/preferences/views.rs | 202 ++++++++-- .../bin/macos/preferences/views/widgets.rs | 83 +++- gb-emu/src/lib.rs | 8 +- gb-vst/Cargo.toml | 19 +- gb-vst/src/lib.rs | 368 +----------------- gb-vst/src/plugin.rs | 361 +++++++++++++++++ gb-vst/src/{ => plugin}/ui.rs | 4 +- lib/src/config/mod.rs | 1 + 12 files changed, 676 insertions(+), 429 deletions(-) create mode 100644 gb-vst/src/plugin.rs rename gb-vst/src/{ => plugin}/ui.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 1e45d3f..8703055 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,6 +1338,7 @@ dependencies = [ "raw-window-handle", "send_wrapper", "serde", + "twinc_emu_vst", "winit", "winit_input_helper", ] diff --git a/gb-emu/Cargo.toml b/gb-emu/Cargo.toml index 6877a17..1eb33b1 100644 --- a/gb-emu/Cargo.toml +++ b/gb-emu/Cargo.toml @@ -40,6 +40,7 @@ serde = { version = "1.0", features = ["derive"] } image = { version = "0.24", default-features = false, features = ["png"] } bytemuck = "1.14" chrono = "0.4" +twinc_emu_vst = { path = "../gb-vst", default-features = false } [target.'cfg(any(target_os = "macos"))'.dependencies] cacao = "0.4.0-beta2" diff --git a/gb-emu/src/bin/macos/mod.rs b/gb-emu/src/bin/macos/mod.rs index 71c3570..0e71967 100644 --- a/gb-emu/src/bin/macos/mod.rs +++ b/gb-emu/src/bin/macos/mod.rs @@ -1,3 +1,5 @@ +use std::sync::RwLock; + use cacao::appkit::menu::{Menu, MenuItem}; use cacao::appkit::window::{Window, WindowConfig, WindowStyle, WindowToolbarStyle}; use cacao::appkit::{App, AppDelegate}; @@ -18,16 +20,16 @@ pub(crate) enum CoreMessage { } pub(crate) struct TwincUiApp { - preferences: Window, + preferences: RwLock>, } impl Default for TwincUiApp { fn default() -> Self { Self { - preferences: Window::with( + preferences: RwLock::new(Window::with( { let mut config = WindowConfig::default(); - config.set_initial_dimensions(100., 100., 400., 400.); + config.set_initial_dimensions(0., 0., 800., 800.); config.set_styles(&[ WindowStyle::Resizable, @@ -40,7 +42,7 @@ impl Default for TwincUiApp { config }, PreferencesUi::new(), - ), + )), } } } @@ -59,11 +61,13 @@ impl Dispatcher for TwincUiApp { match message { AppMessage::Core(CoreMessage::Open) => println!("open"), AppMessage::Core(CoreMessage::OpenPreferences) => { - self.preferences.show(); + self.preferences.read().unwrap().show(); } AppMessage::Preferences(prefs_message) => { - if let Some(delegate) = &self.preferences.delegate { - delegate.message(prefs_message) + if let Ok(mut prefs) = self.preferences.write() { + if let Some(ref mut delegate) = prefs.delegate { + delegate.message(prefs_message) + } } } } diff --git a/gb-emu/src/bin/macos/preferences.rs b/gb-emu/src/bin/macos/preferences.rs index 811f2c5..9af2d7a 100644 --- a/gb-emu/src/bin/macos/preferences.rs +++ b/gb-emu/src/bin/macos/preferences.rs @@ -5,11 +5,13 @@ use cacao::{ }, view::ViewController, }; +use gb_emu_lib::config::ConfigManager; use self::{ toolbar::PreferencesToolbar, views::{ - CorePreferencesContentView, StandalonePreferencesContentView, VstPreferencesContentView, + CorePreferencesContentView, CorePreferencesUpdates, StandalonePreferencesContentView, + StandalonePreferencesUpdates, VstPreferencesContentView, VstPreferencesUpdates, }, }; @@ -18,6 +20,9 @@ mod views; pub(crate) enum PreferencesMessage { SwitchPane(PreferencesPane), + UpdateCore(CorePreferencesUpdates), + UpdateStandalone(StandalonePreferencesUpdates), + UpdateVst(VstPreferencesUpdates), } pub(crate) enum PreferencesPane { @@ -36,31 +41,48 @@ pub(crate) struct PreferencesUi { impl PreferencesUi { pub(crate) fn new() -> Self { + let config_manager = ConfigManager::get().expect("Couldn't load config dir"); Self { toolbar: Toolbar::new("PreferencesToolbar", PreferencesToolbar::default()), - core_prefs: ViewController::new(CorePreferencesContentView::default()), - standalone_prefs: ViewController::new(StandalonePreferencesContentView::default()), - vst_prefs: ViewController::new(VstPreferencesContentView::default()), + core_prefs: ViewController::new(CorePreferencesContentView::new( + config_manager.clone(), + )), + standalone_prefs: ViewController::new(StandalonePreferencesContentView::new( + config_manager.clone(), + )), + vst_prefs: ViewController::new(VstPreferencesContentView::new(config_manager)), window: None, } } - pub(crate) fn message(&self, message: PreferencesMessage) { + pub(crate) fn message(&mut self, message: PreferencesMessage) { let window = self.window.as_ref().unwrap(); match message { PreferencesMessage::SwitchPane(PreferencesPane::Core) => { - window.set_title("Core"); window.set_content_view_controller(&self.core_prefs); } PreferencesMessage::SwitchPane(PreferencesPane::Standalone) => { - window.set_title("Standalone"); window.set_content_view_controller(&self.standalone_prefs); } PreferencesMessage::SwitchPane(PreferencesPane::Vst) => { - window.set_title("VST"); window.set_content_view_controller(&self.vst_prefs); } + PreferencesMessage::UpdateCore(update) => { + if let Some(ref mut delegate) = self.core_prefs.view.delegate { + delegate.update(update) + } + } + PreferencesMessage::UpdateStandalone(update) => { + if let Some(ref mut delegate) = self.standalone_prefs.view.delegate { + delegate.update(update) + } + } + PreferencesMessage::UpdateVst(update) => { + if let Some(ref mut delegate) = self.vst_prefs.view.delegate { + delegate.update(update) + } + } } } } @@ -72,6 +94,7 @@ impl WindowDelegate for PreferencesUi { window.set_autosave_name("PreferencesWindow"); window.set_movable_by_background(true); window.set_toolbar(&self.toolbar); + window.set_title("Preferences"); self.window = Some(window); diff --git a/gb-emu/src/bin/macos/preferences/views.rs b/gb-emu/src/bin/macos/preferences/views.rs index 5dd13e2..9fdc98a 100644 --- a/gb-emu/src/bin/macos/preferences/views.rs +++ b/gb-emu/src/bin/macos/preferences/views.rs @@ -1,65 +1,195 @@ use cacao::{ - layout::{Layout, LayoutConstraint}, + layout::Layout, view::{View, ViewDelegate}, }; +use gb_emu::StandaloneConfig; +use gb_emu_lib::config::{Config, ConfigManager}; -use self::widgets::ToggleOptionView; +use crate::macos::dispatch; + +use self::widgets::{PathView, ToggleView}; mod widgets; -#[derive(Default)] pub(crate) struct CorePreferencesContentView { - pub example_option: ToggleOptionView, + config: Config, + config_manager: ConfigManager, + show_bootrom: ToggleView, + prefer_cgb: ToggleView, + dmg_bootrom: PathView, +} + +impl CorePreferencesContentView { + pub(crate) fn new(config_manager: ConfigManager) -> Self { + Self { + config: config_manager.load_or_create_base_config(), + config_manager, + show_bootrom: Default::default(), + prefer_cgb: Default::default(), + dmg_bootrom: Default::default(), + } + } + + pub(crate) fn update(&mut self, update: CorePreferencesUpdates) { + match update { + CorePreferencesUpdates::ShowBootrom => { + self.config.show_bootrom = !self.config.show_bootrom + } + CorePreferencesUpdates::PreferCGB => self.config.prefer_cgb = !self.config.prefer_cgb, + } + + self.config_manager + .save_custom_config(self.config.clone()) + .expect("failed to save config"); + } +} + +pub(crate) enum CorePreferencesUpdates { + ShowBootrom, + PreferCGB, } impl ViewDelegate for CorePreferencesContentView { const NAME: &'static str = "CorePreferencesContentView"; fn did_load(&mut self, view: View) { - self.example_option.configure( - "An example preference", - "This can be true, or it can be false.", - false, // initial value - |_v| {}, + self.show_bootrom + .configure("Show BootROM", "", self.config.show_bootrom, |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::ShowBootrom), + )) + }); + view.add_subview(&self.show_bootrom.view); + + self.prefer_cgb.configure( + "Prefer Game Boy Colour mode", + "", + self.config.prefer_cgb, + |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::PreferCGB), + )) + }, ); + view.add_subview(&self.prefer_cgb.view); - view.add_subview(&self.example_option.view); + self.dmg_bootrom + .configure("DMG bootrom", self.config.dmg_bootrom.clone()); + view.add_subview(&self.dmg_bootrom.view); - LayoutConstraint::activate(&[ - self.example_option - .view - .top - .constraint_equal_to(&view.top) - .offset(22.), - self.example_option - .view - .leading - .constraint_equal_to(&view.leading) - .offset(22.), - self.example_option - .view - .trailing - .constraint_equal_to(&view.trailing) - .offset(-22.), - self.example_option - .view - .bottom - .constraint_equal_to(&view.bottom) - .offset(-22.), - ]); + widgets::auto_layout( + &view, + vec![ + &self.show_bootrom.view, + &self.prefer_cgb.view, + &self.dmg_bootrom.view, + ], + ); } } -#[derive(Default)] -pub(crate) struct StandalonePreferencesContentView {} +pub(crate) struct StandalonePreferencesContentView { + config: StandaloneConfig, + config_manager: ConfigManager, + group_screenshots_by_rom: ToggleView, +} + +impl StandalonePreferencesContentView { + pub(crate) fn new(config_manager: ConfigManager) -> Self { + Self { + config: config_manager.load_or_create_config(), + config_manager, + group_screenshots_by_rom: Default::default(), + } + } + + pub(crate) fn update(&mut self, update: StandalonePreferencesUpdates) { + match update { + StandalonePreferencesUpdates::GroupScreenshotsByRom => { + self.config.group_screenshots_by_rom = !self.config.group_screenshots_by_rom + } + } + self.config_manager + .save_custom_config(self.config.clone()) + .expect("failed to save config"); + } +} + +pub(crate) enum StandalonePreferencesUpdates { + GroupScreenshotsByRom, +} impl ViewDelegate for StandalonePreferencesContentView { const NAME: &'static str = "StandalonePreferencesContentView"; + + fn did_load(&mut self, view: View) { + self.group_screenshots_by_rom.configure( + "Group screenshots by ROM", + "", + self.config.group_screenshots_by_rom, + |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateStandalone( + StandalonePreferencesUpdates::GroupScreenshotsByRom, + ), + )) + }, + ); + + view.add_subview(&self.group_screenshots_by_rom.view); + + widgets::auto_layout(&view, vec![&self.group_screenshots_by_rom.view]); + } } -#[derive(Default)] -pub(crate) struct VstPreferencesContentView {} +pub(crate) struct VstPreferencesContentView { + config: twinc_emu_vst::VstConfig, + config_manager: ConfigManager, + force_skip_bootrom: ToggleView, +} + +impl VstPreferencesContentView { + pub(crate) fn new(config_manager: ConfigManager) -> Self { + Self { + config: config_manager.load_or_create_config(), + config_manager, + force_skip_bootrom: Default::default(), + } + } + + pub(crate) fn update(&mut self, update: VstPreferencesUpdates) { + match update { + VstPreferencesUpdates::ForceSkipBootrom => { + self.config.force_skip_bootrom = !self.config.force_skip_bootrom + } + } + self.config_manager + .save_custom_config(self.config.clone()) + .expect("failed to save config"); + } +} + +pub(crate) enum VstPreferencesUpdates { + ForceSkipBootrom, +} impl ViewDelegate for VstPreferencesContentView { const NAME: &'static str = "VstPreferencesContentView"; + + fn did_load(&mut self, view: View) { + self.force_skip_bootrom.configure( + "Force skip bootrom", + "", + self.config.force_skip_bootrom, + |_v| { + dispatch(crate::macos::AppMessage::Preferences( + super::PreferencesMessage::UpdateVst(VstPreferencesUpdates::ForceSkipBootrom), + )) + }, + ); + + view.add_subview(&self.force_skip_bootrom.view); + + widgets::auto_layout(&view, vec![&self.force_skip_bootrom.view]); + } } diff --git a/gb-emu/src/bin/macos/preferences/views/widgets.rs b/gb-emu/src/bin/macos/preferences/views/widgets.rs index 57b88bf..e9268d5 100644 --- a/gb-emu/src/bin/macos/preferences/views/widgets.rs +++ b/gb-emu/src/bin/macos/preferences/views/widgets.rs @@ -1,18 +1,44 @@ +use cacao::button::Button; +use cacao::input::TextField; use cacao::layout::{Layout, LayoutConstraint}; use cacao::switch::Switch; use cacao::text::Label; use cacao::view::View; use objc::runtime::Object; +pub(crate) fn auto_layout(container: &View, items: Vec<&View>) { + for (i, item) in items.iter().enumerate() { + LayoutConstraint::activate(&[ + if i > 0 { + item.top.constraint_equal_to(&items[i - 1].bottom) + } else { + item.top.constraint_equal_to(&container.top).offset(22.) + }, + item.leading + .constraint_equal_to(&container.leading) + .offset(22.), + item.trailing + .constraint_equal_to(&container.trailing) + .offset(-22.), + ]); + } + LayoutConstraint::activate(&[items + .last() + .unwrap() + .bottom + .constraint_equal_to(&container.bottom) + .offset(-22.)]); +} + #[derive(Debug)] -pub struct ToggleOptionView { +pub struct ToggleView { pub view: View, pub switch: Switch, pub title: Label, pub subtitle: Label, } -impl Default for ToggleOptionView { +impl Default for ToggleView { fn default() -> Self { let view = View::new(); @@ -41,7 +67,7 @@ impl Default for ToggleOptionView { .constraint_greater_than_or_equal_to_constant(200.), ]); - ToggleOptionView { + ToggleView { view, switch, title, @@ -50,7 +76,7 @@ impl Default for ToggleOptionView { } } -impl ToggleOptionView { +impl ToggleView { pub fn configure(&mut self, text: &str, subtitle: &str, state: bool, handler: F) where F: Fn(*const Object) + Send + Sync + 'static, @@ -61,3 +87,52 @@ impl ToggleOptionView { self.switch.set_checked(state); } } + +pub struct PathView { + pub view: View, + pub field: TextField, + pub button: Button, +} + +impl Default for PathView { + fn default() -> Self { + let view = View::new(); + let field = TextField::new(); + field.set_uses_single_line(true); + view.add_subview(&field); + let button = Button::new("Browse"); + view.add_subview(&button); + + LayoutConstraint::activate(&[ + field.top.constraint_equal_to(&view.top), + field.leading.constraint_equal_to(&view.leading), + field.bottom.constraint_equal_to(&view.bottom), + field + .trailing + .constraint_equal_to(&button.leading) + .offset(-10.), + field + .width + .constraint_greater_than_or_equal_to_constant(400.), + button.trailing.constraint_equal_to(&view.trailing), + button.width.constraint_equal_to_constant(100.), + button.top.constraint_equal_to(&view.top), + button.bottom.constraint_equal_to(&view.bottom), + ]); + + Self { + view, + field, + button, + } + } +} + +impl PathView { + pub fn configure(&mut self, placeholder: &str, state: Option) { + self.field.set_placeholder_text(placeholder); + if let Some(state) = state { + self.field.set_text(&state); + } + } +} diff --git a/gb-emu/src/lib.rs b/gb-emu/src/lib.rs index ef57432..00e62f3 100644 --- a/gb-emu/src/lib.rs +++ b/gb-emu/src/lib.rs @@ -33,10 +33,10 @@ compile_error!("select one rendering backend!"); #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(default)] pub struct StandaloneConfig { - scale_factor: usize, - group_screenshots_by_rom: bool, - buffers_per_frame: usize, - output_buffer_size: u32, + pub scale_factor: usize, + pub group_screenshots_by_rom: bool, + pub buffers_per_frame: usize, + pub output_buffer_size: u32, } impl NamedConfig for StandaloneConfig { diff --git a/gb-vst/Cargo.toml b/gb-vst/Cargo.toml index baa8186..b840b20 100644 --- a/gb-vst/Cargo.toml +++ b/gb-vst/Cargo.toml @@ -7,17 +7,24 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [features] -default = ["vulkan-static"] +default = ["plugin", "vulkan-static"] pixels = ["gb-emu-lib/pixels-renderer"] vulkan = ["dep:raw-window-handle", "gb-emu-lib/vulkan-renderer"] vulkan-static = ["vulkan", "gb-emu-lib/vulkan-static"] +plugin = [ + "dep:nih_plug", + "dep:baseview", + "dep:async-ringbuf", + "dep:futures", + "dep:keyboard-types", +] [dependencies] gb-emu-lib = { workspace = true } -nih_plug = { workspace = true, features = ["standalone"] } -baseview = { workspace = true } -async-ringbuf = "0.1" -futures = "0.3" -keyboard-types = "0.6.2" +nih_plug = { workspace = true, features = ["standalone"], optional = true } +baseview = { workspace = true, optional = true } +async-ringbuf = { version = "0.1", optional = true } +futures = { version = "0.3", optional = true } +keyboard-types = { version = "0.6.2", optional = true } raw-window-handle = { version = "0.5", optional = true } serde = { version = "1.0", features = ["derive"] } diff --git a/gb-vst/src/lib.rs b/gb-vst/src/lib.rs index 5217ad0..574d3a9 100644 --- a/gb-vst/src/lib.rs +++ b/gb-vst/src/lib.rs @@ -1,72 +1,15 @@ -use async_ringbuf::AsyncHeapConsumer; -use baseview::Size; -use futures::executor; -use gb_emu_lib::{ - config::{ConfigManager, NamedConfig}, - connect::{ - AudioOutput, CgbRomType, DownsampleType, EmulatorCoreTrait, EmulatorMessage, - EmulatorOptions, NoCamera, RendererMessage, RomFile, SerialTarget, - }, - EmulatorCore, HEIGHT, WIDTH, -}; -use nih_plug::prelude::*; -use nih_plug::{midi::MidiResult::Basic, params::persist::PersistentField}; - +use gb_emu_lib::config::NamedConfig; use serde::{Deserialize, Serialize}; -use std::{ - path::PathBuf, - sync::{ - mpsc::{self, channel, Receiver, Sender}, - Arc, Mutex, OnceLock, RwLock, - }, -}; -use ui::TwincEditor; -mod ui; - -#[derive(Default)] -struct SramParam { - state: Arc>>, -} - -impl PersistentField<'_, Vec> for SramParam { - fn set(&self, new_value: Vec) { - let mut w = self.state.write().unwrap(); - w.resize(new_value.len(), 0); - w.copy_from_slice(&new_value); - } - - fn map(&self, f: F) -> R - where - F: Fn(&Vec) -> R, - { - f(&self.state.read().unwrap()) - } -} - -#[derive(Params, Default)] -struct EmuParams { - #[persist = "sram"] - sram_save: SramParam, -} - -struct EmuVars { - rx: AsyncHeapConsumer<[f32; 2]>, - emulator_core: EmulatorCore<[u8; 4], NoCamera>, - serial_tx: Sender, -} - -struct EmuComms { - sender: Sender, - receiver: Receiver>, -} +#[cfg(feature = "plugin")] +mod plugin; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(default)] pub struct VstConfig { - scale_factor: usize, - rom: String, - force_skip_bootrom: bool, + pub scale_factor: usize, + pub rom: String, + pub force_skip_bootrom: bool, } impl NamedConfig for VstConfig { @@ -84,302 +27,3 @@ impl Default for VstConfig { } } } - -struct Configs { - vst_config: VstConfig, - emu_config: gb_emu_lib::config::Config, - config_dir: PathBuf, -} - -static CONFIGS: OnceLock = OnceLock::new(); -static IS_CGB: OnceLock = OnceLock::new(); - -fn access_config<'a>() -> &'a Configs { - CONFIGS.get_or_init(|| { - let config_manager = ConfigManager::get().expect("Could not open config folder"); - let emu_config = config_manager.load_or_create_base_config(); - let vst_config: VstConfig = config_manager.load_or_create_config(); - - Configs { - vst_config, - emu_config, - config_dir: config_manager.dir(), - } - }) -} - -#[derive(Default)] -pub struct GameboyEmu { - vars: Option, - emu_comms: Arc>>, - params: Arc, -} - -const BUFFERS_PER_FRAME: usize = 1; -const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::Linear; - -impl Plugin for GameboyEmu { - const NAME: &'static str = "Gameboy"; - - const VENDOR: &'static str = "Alex Janka"; - - const URL: &'static str = "alexjanka.com"; - - const EMAIL: &'static str = "alex@alexjanka.com"; - - const VERSION: &'static str = "0.1"; - - const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ - AudioIOLayout { - main_input_channels: None, - main_output_channels: NonZeroU32::new(2), - - aux_input_ports: &[], - aux_output_ports: &[], - - // Individual ports and the layout as a whole can be named here. By default these names - // are generated as needed. This layout will be called 'Stereo', while the other one is - // given the name 'Mono' based no the number of input and output channels. - names: PortNames::const_default(), - }, - AudioIOLayout { - main_input_channels: None, - main_output_channels: NonZeroU32::new(1), - - aux_input_ports: &[], - aux_output_ports: &[], - - // Individual ports and the layout as a whole can be named here. By default these names - // are generated as needed. This layout will be called 'Stereo', while the other one is - // given the name 'Mono' based no the number of input and output channels. - names: PortNames::const_default(), - }, - ]; - - const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs; - const SAMPLE_ACCURATE_AUTOMATION: bool = true; - - type SysExMessage = (); - - type BackgroundTask = (); - - fn params(&self) -> Arc { - self.params.clone() - } - - fn process( - &mut self, - buffer: &mut Buffer, - _: &mut AuxiliaryBuffers, - context: &mut impl ProcessContext, - ) -> ProcessStatus { - if let Some(ref mut vars) = self.vars { - while let Some(event) = context.next_event() { - if let Some(Basic(as_bytes)) = event.as_midi() { - match event { - NoteEvent::NoteOn { - timing: _, - voice_id: _, - channel, - note: _, - velocity: _, - } => { - if channel < 5 { - vars.serial_tx.send(0x90 + channel).unwrap(); - vars.serial_tx.send(as_bytes[1]).unwrap(); - vars.serial_tx.send(as_bytes[2]).unwrap(); - } - } - NoteEvent::NoteOff { - timing: _, - voice_id: _, - channel, - note: _, - velocity: _, - } => { - if channel < 5 { - vars.serial_tx.send(0x80 + channel).unwrap(); - vars.serial_tx.send(as_bytes[1]).unwrap(); - vars.serial_tx.send(as_bytes[2]).unwrap(); - } - } - NoteEvent::MidiPitchBend { - timing: _, - channel, - value: _, - } => { - if channel < 5 { - vars.serial_tx.send(0xE0 + channel).unwrap(); - vars.serial_tx.send(as_bytes[1]).unwrap(); - vars.serial_tx.send(as_bytes[2]).unwrap(); - } - } - NoteEvent::MidiCC { - timing: _, - channel, - cc: _, - value: _, - } => { - if channel < 5 { - vars.serial_tx.send(0xB0 + channel).unwrap(); - vars.serial_tx.send(as_bytes[1]).unwrap(); - vars.serial_tx.send(as_bytes[2]).unwrap(); - } - } - NoteEvent::MidiProgramChange { - timing: _, - channel, - program: _, - } => { - if channel < 5 { - vars.serial_tx.send(0xC0 + channel).unwrap(); - vars.serial_tx.send(as_bytes[1]).unwrap(); - vars.serial_tx.send(as_bytes[2]).unwrap(); - } - } - _ => {} - } - } - } - if buffer.channels() != 2 { - for mut sample in buffer.iter_samples() { - if vars.rx.is_empty() { - vars.emulator_core.run_until_buffer_full(); - } - if let Some(a) = executor::block_on(vars.rx.pop()) { - if let Some(g) = sample.get_mut(0) { - *g = (a[0] + a[1]) / 2.; - } - } - } - } else { - for sample in buffer.iter_samples() { - if vars.rx.is_empty() { - vars.emulator_core.run_until_buffer_full(); - } - if let Some(a) = executor::block_on(vars.rx.pop()) { - for (source, dest) in a.iter().zip(sample) { - *dest = *source; - } - } - } - } - vars.emulator_core.run_until_buffer_full(); - } else { - while context.next_event().is_some() {} - } - ProcessStatus::KeepAlive - } - - fn editor(&mut self, _e: AsyncExecutor) -> Option> { - let configs = access_config(); - - let size = Size::new( - (WIDTH * configs.vst_config.scale_factor) as f64, - (HEIGHT * configs.vst_config.scale_factor) as f64, - ); - - Some(Box::new(TwincEditor::new(self.emu_comms.clone(), size))) - } - - fn initialize( - &mut self, - _audio_io_layout: &AudioIOLayout, - buffer_config: &BufferConfig, - _context: &mut impl InitContext, - ) -> bool { - if let Some(ref mut vars) = self.vars { - let (output, rx) = AudioOutput::new( - buffer_config.sample_rate, - BUFFERS_PER_FRAME, - DOWNSAMPLE_TYPE, - ); - - vars.emulator_core.replace_output(output); - vars.rx = rx; - } else { - let configs = access_config(); - let rom_path = configs.config_dir.join(configs.vst_config.rom.clone()); - - let (rom, camera) = RomFile::Path(rom_path) - .load(gb_emu_lib::connect::SramType::None, NoCamera::default()) - .unwrap_or_else(|_v| { - RomFile::Raw(include_bytes!("../error.gb").to_vec()) - .load(gb_emu_lib::connect::SramType::None, NoCamera::default()) - .expect("Couldn't load built-in fallback rom") - }); - - let _ = - IS_CGB.set(rom.rom_type == CgbRomType::CgbOnly || configs.emu_config.prefer_cgb); - - let (sender, receiver) = channel::(); - - let (output, rx) = AudioOutput::new( - buffer_config.sample_rate, - BUFFERS_PER_FRAME, - DOWNSAMPLE_TYPE, - ); - - let (emu_sender, renderer_receiver) = mpsc::channel(); - - *self.emu_comms.lock().unwrap() = Some(EmuComms { - sender, - receiver: renderer_receiver, - }); - - let (serial_tx, gb_serial_rx) = mpsc::channel::(); - let serial_target = SerialTarget::Custom { - rx: Some(gb_serial_rx), - tx: None, - }; - - let will_skip_bootrom = - configs.vst_config.force_skip_bootrom || !configs.emu_config.show_bootrom; - - let mut emulator_core = { - let options = EmulatorOptions::new_with_config( - configs.emu_config.clone(), - configs.config_dir.clone(), - emu_sender, - rom, - output, - ) - .with_serial_target(serial_target) - .with_sram_buffer(self.params.sram_save.state.clone()) - .with_show_bootrom(!will_skip_bootrom); - - EmulatorCore::init(false, receiver, options, camera) - }; - - emulator_core.run_until_buffer_full(); - - self.vars = Some(EmuVars { - rx, - emulator_core, - serial_tx, - }); - } - - true - } - - fn deactivate(&mut self) { - if let Ok(comms) = self.emu_comms.lock() { - if let Some(ref comms) = *comms { - match comms.sender.send(EmulatorMessage::Exit) { - Ok(_) => self.vars = None, - Err(e) => nih_log!("error {e} sending message to emulator"), - } - } - } - } -} - -impl Vst3Plugin for GameboyEmu { - const VST3_CLASS_ID: [u8; 16] = *b"alexjankagbemula"; - - const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = - &[Vst3SubCategory::Instrument, Vst3SubCategory::Synth]; -} - -nih_export_vst3!(GameboyEmu); diff --git a/gb-vst/src/plugin.rs b/gb-vst/src/plugin.rs new file mode 100644 index 0000000..6e7d5dc --- /dev/null +++ b/gb-vst/src/plugin.rs @@ -0,0 +1,361 @@ +use async_ringbuf::AsyncHeapConsumer; +use baseview::Size; +use futures::executor; +use gb_emu_lib::{ + config::ConfigManager, + connect::{ + AudioOutput, CgbRomType, DownsampleType, EmulatorCoreTrait, EmulatorMessage, + EmulatorOptions, NoCamera, RendererMessage, RomFile, SerialTarget, + }, + EmulatorCore, HEIGHT, WIDTH, +}; +use nih_plug::prelude::*; +use nih_plug::{midi::MidiResult::Basic, params::persist::PersistentField}; +use std::{ + path::PathBuf, + sync::{ + mpsc::{self, channel, Receiver, Sender}, + Arc, Mutex, OnceLock, RwLock, + }, +}; +use ui::TwincEditor; + +use crate::VstConfig; + +mod ui; + +#[derive(Default)] +struct SramParam { + state: Arc>>, +} + +impl PersistentField<'_, Vec> for SramParam { + fn set(&self, new_value: Vec) { + let mut w = self.state.write().unwrap(); + w.resize(new_value.len(), 0); + w.copy_from_slice(&new_value); + } + + fn map(&self, f: F) -> R + where + F: Fn(&Vec) -> R, + { + f(&self.state.read().unwrap()) + } +} + +#[derive(Params, Default)] +struct EmuParams { + #[persist = "sram"] + sram_save: SramParam, +} + +struct EmuVars { + rx: AsyncHeapConsumer<[f32; 2]>, + emulator_core: EmulatorCore<[u8; 4], NoCamera>, + serial_tx: Sender, +} + +struct EmuComms { + sender: Sender, + receiver: Receiver>, +} + +struct Configs { + vst_config: VstConfig, + emu_config: gb_emu_lib::config::Config, + config_dir: PathBuf, +} + +static CONFIGS: OnceLock = OnceLock::new(); +static IS_CGB: OnceLock = OnceLock::new(); + +fn access_config<'a>() -> &'a Configs { + CONFIGS.get_or_init(|| { + let config_manager = ConfigManager::get().expect("Could not open config folder"); + let emu_config = config_manager.load_or_create_base_config(); + let vst_config: VstConfig = config_manager.load_or_create_config(); + + Configs { + vst_config, + emu_config, + config_dir: config_manager.dir(), + } + }) +} + +#[derive(Default)] +pub struct GameboyEmu { + vars: Option, + emu_comms: Arc>>, + params: Arc, +} + +const BUFFERS_PER_FRAME: usize = 1; +const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::Linear; + +impl Plugin for GameboyEmu { + const NAME: &'static str = "Gameboy"; + + const VENDOR: &'static str = "Alex Janka"; + + const URL: &'static str = "alexjanka.com"; + + const EMAIL: &'static str = "alex@alexjanka.com"; + + const VERSION: &'static str = "0.1"; + + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: None, + main_output_channels: NonZeroU32::new(2), + + aux_input_ports: &[], + aux_output_ports: &[], + + // Individual ports and the layout as a whole can be named here. By default these names + // are generated as needed. This layout will be called 'Stereo', while the other one is + // given the name 'Mono' based no the number of input and output channels. + names: PortNames::const_default(), + }, + AudioIOLayout { + main_input_channels: None, + main_output_channels: NonZeroU32::new(1), + + aux_input_ports: &[], + aux_output_ports: &[], + + // Individual ports and the layout as a whole can be named here. By default these names + // are generated as needed. This layout will be called 'Stereo', while the other one is + // given the name 'Mono' based no the number of input and output channels. + names: PortNames::const_default(), + }, + ]; + + const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs; + const SAMPLE_ACCURATE_AUTOMATION: bool = true; + + type SysExMessage = (); + + type BackgroundTask = (); + + fn params(&self) -> Arc { + self.params.clone() + } + + fn process( + &mut self, + buffer: &mut Buffer, + _: &mut AuxiliaryBuffers, + context: &mut impl ProcessContext, + ) -> ProcessStatus { + if let Some(ref mut vars) = self.vars { + while let Some(event) = context.next_event() { + if let Some(Basic(as_bytes)) = event.as_midi() { + match event { + NoteEvent::NoteOn { + timing: _, + voice_id: _, + channel, + note: _, + velocity: _, + } => { + if channel < 5 { + vars.serial_tx.send(0x90 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + NoteEvent::NoteOff { + timing: _, + voice_id: _, + channel, + note: _, + velocity: _, + } => { + if channel < 5 { + vars.serial_tx.send(0x80 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + NoteEvent::MidiPitchBend { + timing: _, + channel, + value: _, + } => { + if channel < 5 { + vars.serial_tx.send(0xE0 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + NoteEvent::MidiCC { + timing: _, + channel, + cc: _, + value: _, + } => { + if channel < 5 { + vars.serial_tx.send(0xB0 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + NoteEvent::MidiProgramChange { + timing: _, + channel, + program: _, + } => { + if channel < 5 { + vars.serial_tx.send(0xC0 + channel).unwrap(); + vars.serial_tx.send(as_bytes[1]).unwrap(); + vars.serial_tx.send(as_bytes[2]).unwrap(); + } + } + _ => {} + } + } + } + if buffer.channels() != 2 { + for mut sample in buffer.iter_samples() { + if vars.rx.is_empty() { + vars.emulator_core.run_until_buffer_full(); + } + if let Some(a) = executor::block_on(vars.rx.pop()) { + if let Some(g) = sample.get_mut(0) { + *g = (a[0] + a[1]) / 2.; + } + } + } + } else { + for sample in buffer.iter_samples() { + if vars.rx.is_empty() { + vars.emulator_core.run_until_buffer_full(); + } + if let Some(a) = executor::block_on(vars.rx.pop()) { + for (source, dest) in a.iter().zip(sample) { + *dest = *source; + } + } + } + } + vars.emulator_core.run_until_buffer_full(); + } else { + while context.next_event().is_some() {} + } + ProcessStatus::KeepAlive + } + + fn editor(&mut self, _e: AsyncExecutor) -> Option> { + let configs = access_config(); + + let size = Size::new( + (WIDTH * configs.vst_config.scale_factor) as f64, + (HEIGHT * configs.vst_config.scale_factor) as f64, + ); + + Some(Box::new(TwincEditor::new(self.emu_comms.clone(), size))) + } + + fn initialize( + &mut self, + _audio_io_layout: &AudioIOLayout, + buffer_config: &BufferConfig, + _context: &mut impl InitContext, + ) -> bool { + if let Some(ref mut vars) = self.vars { + let (output, rx) = AudioOutput::new( + buffer_config.sample_rate, + BUFFERS_PER_FRAME, + DOWNSAMPLE_TYPE, + ); + + vars.emulator_core.replace_output(output); + vars.rx = rx; + } else { + let configs = access_config(); + let rom_path = configs.config_dir.join(configs.vst_config.rom.clone()); + + let (rom, camera) = RomFile::Path(rom_path) + .load(gb_emu_lib::connect::SramType::None, NoCamera::default()) + .unwrap_or_else(|_v| { + RomFile::Raw(include_bytes!("../error.gb").to_vec()) + .load(gb_emu_lib::connect::SramType::None, NoCamera::default()) + .expect("Couldn't load built-in fallback rom") + }); + + let _ = + IS_CGB.set(rom.rom_type == CgbRomType::CgbOnly || configs.emu_config.prefer_cgb); + + let (sender, receiver) = channel::(); + + let (output, rx) = AudioOutput::new( + buffer_config.sample_rate, + BUFFERS_PER_FRAME, + DOWNSAMPLE_TYPE, + ); + + let (emu_sender, renderer_receiver) = mpsc::channel(); + + *self.emu_comms.lock().unwrap() = Some(EmuComms { + sender, + receiver: renderer_receiver, + }); + + let (serial_tx, gb_serial_rx) = mpsc::channel::(); + let serial_target = SerialTarget::Custom { + rx: Some(gb_serial_rx), + tx: None, + }; + + let will_skip_bootrom = + configs.vst_config.force_skip_bootrom || !configs.emu_config.show_bootrom; + + let mut emulator_core = { + let options = EmulatorOptions::new_with_config( + configs.emu_config.clone(), + configs.config_dir.clone(), + emu_sender, + rom, + output, + ) + .with_serial_target(serial_target) + .with_sram_buffer(self.params.sram_save.state.clone()) + .with_show_bootrom(!will_skip_bootrom); + + EmulatorCore::init(false, receiver, options, camera) + }; + + emulator_core.run_until_buffer_full(); + + self.vars = Some(EmuVars { + rx, + emulator_core, + serial_tx, + }); + } + + true + } + + fn deactivate(&mut self) { + if let Ok(comms) = self.emu_comms.lock() { + if let Some(ref comms) = *comms { + match comms.sender.send(EmulatorMessage::Exit) { + Ok(_) => self.vars = None, + Err(e) => nih_log!("error {e} sending message to emulator"), + } + } + } + } +} + +impl Vst3Plugin for GameboyEmu { + const VST3_CLASS_ID: [u8; 16] = *b"alexjankagbemula"; + + const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = + &[Vst3SubCategory::Instrument, Vst3SubCategory::Synth]; +} + +nih_export_vst3!(GameboyEmu); diff --git a/gb-vst/src/ui.rs b/gb-vst/src/plugin/ui.rs similarity index 98% rename from gb-vst/src/ui.rs rename to gb-vst/src/plugin/ui.rs index dfecefc..859d641 100644 --- a/gb-vst/src/ui.rs +++ b/gb-vst/src/plugin/ui.rs @@ -10,7 +10,7 @@ use gb_emu_lib::{ use keyboard_types::{Code, KeyState}; use nih_plug::prelude::*; -use crate::{access_config, EmuComms}; +use super::{access_config, EmuComms}; pub struct TwincEditor { emu_comms: Arc>>, @@ -47,7 +47,7 @@ impl Editor for TwincEditor { #[cfg(feature = "vulkan")] let shader_path = { - if crate::IS_CGB.get().is_some_and(|v| *v) { + if super::IS_CGB.get().is_some_and(|v| *v) { config.emu_config.vulkan_config.cgb_shader_path.as_ref() } else { config.emu_config.vulkan_config.dmg_shader_path.as_ref() diff --git a/lib/src/config/mod.rs b/lib/src/config/mod.rs index 1c5579b..ba0534b 100644 --- a/lib/src/config/mod.rs +++ b/lib/src/config/mod.rs @@ -10,6 +10,7 @@ pub trait NamedConfig { fn name() -> String; } +#[derive(Clone)] pub struct ConfigManager { path: PathBuf, }