some of most settings

This commit is contained in:
Alex Janka 2023-11-01 17:13:52 +11:00
parent 59ee750d26
commit 9cfa17ee0e
12 changed files with 676 additions and 429 deletions

1
Cargo.lock generated
View file

@ -1338,6 +1338,7 @@ dependencies = [
"raw-window-handle", "raw-window-handle",
"send_wrapper", "send_wrapper",
"serde", "serde",
"twinc_emu_vst",
"winit", "winit",
"winit_input_helper", "winit_input_helper",
] ]

View file

@ -40,6 +40,7 @@ serde = { version = "1.0", features = ["derive"] }
image = { version = "0.24", default-features = false, features = ["png"] } image = { version = "0.24", default-features = false, features = ["png"] }
bytemuck = "1.14" bytemuck = "1.14"
chrono = "0.4" chrono = "0.4"
twinc_emu_vst = { path = "../gb-vst", default-features = false }
[target.'cfg(any(target_os = "macos"))'.dependencies] [target.'cfg(any(target_os = "macos"))'.dependencies]
cacao = "0.4.0-beta2" cacao = "0.4.0-beta2"

View file

@ -1,3 +1,5 @@
use std::sync::RwLock;
use cacao::appkit::menu::{Menu, MenuItem}; use cacao::appkit::menu::{Menu, MenuItem};
use cacao::appkit::window::{Window, WindowConfig, WindowStyle, WindowToolbarStyle}; use cacao::appkit::window::{Window, WindowConfig, WindowStyle, WindowToolbarStyle};
use cacao::appkit::{App, AppDelegate}; use cacao::appkit::{App, AppDelegate};
@ -18,16 +20,16 @@ pub(crate) enum CoreMessage {
} }
pub(crate) struct TwincUiApp { pub(crate) struct TwincUiApp {
preferences: Window<PreferencesUi>, preferences: RwLock<Window<PreferencesUi>>,
} }
impl Default for TwincUiApp { impl Default for TwincUiApp {
fn default() -> Self { fn default() -> Self {
Self { Self {
preferences: Window::with( preferences: RwLock::new(Window::with(
{ {
let mut config = WindowConfig::default(); let mut config = WindowConfig::default();
config.set_initial_dimensions(100., 100., 400., 400.); config.set_initial_dimensions(0., 0., 800., 800.);
config.set_styles(&[ config.set_styles(&[
WindowStyle::Resizable, WindowStyle::Resizable,
@ -40,7 +42,7 @@ impl Default for TwincUiApp {
config config
}, },
PreferencesUi::new(), PreferencesUi::new(),
), )),
} }
} }
} }
@ -59,16 +61,18 @@ impl Dispatcher for TwincUiApp {
match message { match message {
AppMessage::Core(CoreMessage::Open) => println!("open"), AppMessage::Core(CoreMessage::Open) => println!("open"),
AppMessage::Core(CoreMessage::OpenPreferences) => { AppMessage::Core(CoreMessage::OpenPreferences) => {
self.preferences.show(); self.preferences.read().unwrap().show();
} }
AppMessage::Preferences(prefs_message) => { AppMessage::Preferences(prefs_message) => {
if let Some(delegate) = &self.preferences.delegate { if let Ok(mut prefs) = self.preferences.write() {
if let Some(ref mut delegate) = prefs.delegate {
delegate.message(prefs_message) delegate.message(prefs_message)
} }
} }
} }
} }
} }
}
fn menu() -> Vec<Menu> { fn menu() -> Vec<Menu> {
vec![ vec![

View file

@ -5,11 +5,13 @@ use cacao::{
}, },
view::ViewController, view::ViewController,
}; };
use gb_emu_lib::config::ConfigManager;
use self::{ use self::{
toolbar::PreferencesToolbar, toolbar::PreferencesToolbar,
views::{ views::{
CorePreferencesContentView, StandalonePreferencesContentView, VstPreferencesContentView, CorePreferencesContentView, CorePreferencesUpdates, StandalonePreferencesContentView,
StandalonePreferencesUpdates, VstPreferencesContentView, VstPreferencesUpdates,
}, },
}; };
@ -18,6 +20,9 @@ mod views;
pub(crate) enum PreferencesMessage { pub(crate) enum PreferencesMessage {
SwitchPane(PreferencesPane), SwitchPane(PreferencesPane),
UpdateCore(CorePreferencesUpdates),
UpdateStandalone(StandalonePreferencesUpdates),
UpdateVst(VstPreferencesUpdates),
} }
pub(crate) enum PreferencesPane { pub(crate) enum PreferencesPane {
@ -36,31 +41,48 @@ pub(crate) struct PreferencesUi {
impl PreferencesUi { impl PreferencesUi {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
let config_manager = ConfigManager::get().expect("Couldn't load config dir");
Self { Self {
toolbar: Toolbar::new("PreferencesToolbar", PreferencesToolbar::default()), toolbar: Toolbar::new("PreferencesToolbar", PreferencesToolbar::default()),
core_prefs: ViewController::new(CorePreferencesContentView::default()), core_prefs: ViewController::new(CorePreferencesContentView::new(
standalone_prefs: ViewController::new(StandalonePreferencesContentView::default()), config_manager.clone(),
vst_prefs: ViewController::new(VstPreferencesContentView::default()), )),
standalone_prefs: ViewController::new(StandalonePreferencesContentView::new(
config_manager.clone(),
)),
vst_prefs: ViewController::new(VstPreferencesContentView::new(config_manager)),
window: None, window: None,
} }
} }
pub(crate) fn message(&self, message: PreferencesMessage) { pub(crate) fn message(&mut self, message: PreferencesMessage) {
let window = self.window.as_ref().unwrap(); let window = self.window.as_ref().unwrap();
match message { match message {
PreferencesMessage::SwitchPane(PreferencesPane::Core) => { PreferencesMessage::SwitchPane(PreferencesPane::Core) => {
window.set_title("Core");
window.set_content_view_controller(&self.core_prefs); window.set_content_view_controller(&self.core_prefs);
} }
PreferencesMessage::SwitchPane(PreferencesPane::Standalone) => { PreferencesMessage::SwitchPane(PreferencesPane::Standalone) => {
window.set_title("Standalone");
window.set_content_view_controller(&self.standalone_prefs); window.set_content_view_controller(&self.standalone_prefs);
} }
PreferencesMessage::SwitchPane(PreferencesPane::Vst) => { PreferencesMessage::SwitchPane(PreferencesPane::Vst) => {
window.set_title("VST");
window.set_content_view_controller(&self.vst_prefs); 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_autosave_name("PreferencesWindow");
window.set_movable_by_background(true); window.set_movable_by_background(true);
window.set_toolbar(&self.toolbar); window.set_toolbar(&self.toolbar);
window.set_title("Preferences");
self.window = Some(window); self.window = Some(window);

View file

@ -1,65 +1,195 @@
use cacao::{ use cacao::{
layout::{Layout, LayoutConstraint}, layout::Layout,
view::{View, ViewDelegate}, 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; mod widgets;
#[derive(Default)]
pub(crate) struct CorePreferencesContentView { 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 { impl ViewDelegate for CorePreferencesContentView {
const NAME: &'static str = "CorePreferencesContentView"; const NAME: &'static str = "CorePreferencesContentView";
fn did_load(&mut self, view: View) { fn did_load(&mut self, view: View) {
self.example_option.configure( self.show_bootrom
"An example preference", .configure("Show BootROM", "", self.config.show_bootrom, |_v| {
"This can be true, or it can be false.", dispatch(crate::macos::AppMessage::Preferences(
false, // initial value super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::ShowBootrom),
|_v| {}, ))
});
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(&[ widgets::auto_layout(
self.example_option &view,
.view vec![
.top &self.show_bootrom.view,
.constraint_equal_to(&view.top) &self.prefer_cgb.view,
.offset(22.), &self.dmg_bootrom.view,
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.),
]);
} }
} }
#[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 { impl ViewDelegate for StandalonePreferencesContentView {
const NAME: &'static str = "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 { impl ViewDelegate for VstPreferencesContentView {
const NAME: &'static str = "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]);
}
} }

View file

@ -1,18 +1,44 @@
use cacao::button::Button;
use cacao::input::TextField;
use cacao::layout::{Layout, LayoutConstraint}; use cacao::layout::{Layout, LayoutConstraint};
use cacao::switch::Switch; use cacao::switch::Switch;
use cacao::text::Label; use cacao::text::Label;
use cacao::view::View; use cacao::view::View;
use objc::runtime::Object; 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)] #[derive(Debug)]
pub struct ToggleOptionView { pub struct ToggleView {
pub view: View, pub view: View,
pub switch: Switch, pub switch: Switch,
pub title: Label, pub title: Label,
pub subtitle: Label, pub subtitle: Label,
} }
impl Default for ToggleOptionView { impl Default for ToggleView {
fn default() -> Self { fn default() -> Self {
let view = View::new(); let view = View::new();
@ -41,7 +67,7 @@ impl Default for ToggleOptionView {
.constraint_greater_than_or_equal_to_constant(200.), .constraint_greater_than_or_equal_to_constant(200.),
]); ]);
ToggleOptionView { ToggleView {
view, view,
switch, switch,
title, title,
@ -50,7 +76,7 @@ impl Default for ToggleOptionView {
} }
} }
impl ToggleOptionView { impl ToggleView {
pub fn configure<F>(&mut self, text: &str, subtitle: &str, state: bool, handler: F) pub fn configure<F>(&mut self, text: &str, subtitle: &str, state: bool, handler: F)
where where
F: Fn(*const Object) + Send + Sync + 'static, F: Fn(*const Object) + Send + Sync + 'static,
@ -61,3 +87,52 @@ impl ToggleOptionView {
self.switch.set_checked(state); 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<String>) {
self.field.set_placeholder_text(placeholder);
if let Some(state) = state {
self.field.set_text(&state);
}
}
}

View file

@ -33,10 +33,10 @@ compile_error!("select one rendering backend!");
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)] #[serde(default)]
pub struct StandaloneConfig { pub struct StandaloneConfig {
scale_factor: usize, pub scale_factor: usize,
group_screenshots_by_rom: bool, pub group_screenshots_by_rom: bool,
buffers_per_frame: usize, pub buffers_per_frame: usize,
output_buffer_size: u32, pub output_buffer_size: u32,
} }
impl NamedConfig for StandaloneConfig { impl NamedConfig for StandaloneConfig {

View file

@ -7,17 +7,24 @@ edition = "2021"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features] [features]
default = ["vulkan-static"] default = ["plugin", "vulkan-static"]
pixels = ["gb-emu-lib/pixels-renderer"] pixels = ["gb-emu-lib/pixels-renderer"]
vulkan = ["dep:raw-window-handle", "gb-emu-lib/vulkan-renderer"] vulkan = ["dep:raw-window-handle", "gb-emu-lib/vulkan-renderer"]
vulkan-static = ["vulkan", "gb-emu-lib/vulkan-static"] vulkan-static = ["vulkan", "gb-emu-lib/vulkan-static"]
plugin = [
"dep:nih_plug",
"dep:baseview",
"dep:async-ringbuf",
"dep:futures",
"dep:keyboard-types",
]
[dependencies] [dependencies]
gb-emu-lib = { workspace = true } gb-emu-lib = { workspace = true }
nih_plug = { workspace = true, features = ["standalone"] } nih_plug = { workspace = true, features = ["standalone"], optional = true }
baseview = { workspace = true } baseview = { workspace = true, optional = true }
async-ringbuf = "0.1" async-ringbuf = { version = "0.1", optional = true }
futures = "0.3" futures = { version = "0.3", optional = true }
keyboard-types = "0.6.2" keyboard-types = { version = "0.6.2", optional = true }
raw-window-handle = { version = "0.5", optional = true } raw-window-handle = { version = "0.5", optional = true }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -1,72 +1,15 @@
use async_ringbuf::AsyncHeapConsumer; use gb_emu_lib::config::NamedConfig;
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 serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{
path::PathBuf,
sync::{
mpsc::{self, channel, Receiver, Sender},
Arc, Mutex, OnceLock, RwLock,
},
};
use ui::TwincEditor;
mod ui; #[cfg(feature = "plugin")]
mod plugin;
#[derive(Default)]
struct SramParam {
state: Arc<RwLock<Vec<u8>>>,
}
impl PersistentField<'_, Vec<u8>> for SramParam {
fn set(&self, new_value: Vec<u8>) {
let mut w = self.state.write().unwrap();
w.resize(new_value.len(), 0);
w.copy_from_slice(&new_value);
}
fn map<F, R>(&self, f: F) -> R
where
F: Fn(&Vec<u8>) -> 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<u8>,
}
struct EmuComms {
sender: Sender<EmulatorMessage>,
receiver: Receiver<RendererMessage<[u8; 4]>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)] #[serde(default)]
pub struct VstConfig { pub struct VstConfig {
scale_factor: usize, pub scale_factor: usize,
rom: String, pub rom: String,
force_skip_bootrom: bool, pub force_skip_bootrom: bool,
} }
impl NamedConfig for VstConfig { 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<Configs> = OnceLock::new();
static IS_CGB: OnceLock<bool> = 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<EmuVars>,
emu_comms: Arc<Mutex<Option<EmuComms>>>,
params: Arc<EmuParams>,
}
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<dyn Params> {
self.params.clone()
}
fn process(
&mut self,
buffer: &mut Buffer,
_: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext<Self>,
) -> 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<Self>) -> Option<Box<dyn Editor>> {
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<Self>,
) -> 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::<EmulatorMessage>();
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::<u8>();
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);

361
gb-vst/src/plugin.rs Normal file
View file

@ -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<RwLock<Vec<u8>>>,
}
impl PersistentField<'_, Vec<u8>> for SramParam {
fn set(&self, new_value: Vec<u8>) {
let mut w = self.state.write().unwrap();
w.resize(new_value.len(), 0);
w.copy_from_slice(&new_value);
}
fn map<F, R>(&self, f: F) -> R
where
F: Fn(&Vec<u8>) -> 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<u8>,
}
struct EmuComms {
sender: Sender<EmulatorMessage>,
receiver: Receiver<RendererMessage<[u8; 4]>>,
}
struct Configs {
vst_config: VstConfig,
emu_config: gb_emu_lib::config::Config,
config_dir: PathBuf,
}
static CONFIGS: OnceLock<Configs> = OnceLock::new();
static IS_CGB: OnceLock<bool> = 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<EmuVars>,
emu_comms: Arc<Mutex<Option<EmuComms>>>,
params: Arc<EmuParams>,
}
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<dyn Params> {
self.params.clone()
}
fn process(
&mut self,
buffer: &mut Buffer,
_: &mut AuxiliaryBuffers,
context: &mut impl ProcessContext<Self>,
) -> 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<Self>) -> Option<Box<dyn Editor>> {
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<Self>,
) -> 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::<EmulatorMessage>();
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::<u8>();
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);

View file

@ -10,7 +10,7 @@ use gb_emu_lib::{
use keyboard_types::{Code, KeyState}; use keyboard_types::{Code, KeyState};
use nih_plug::prelude::*; use nih_plug::prelude::*;
use crate::{access_config, EmuComms}; use super::{access_config, EmuComms};
pub struct TwincEditor { pub struct TwincEditor {
emu_comms: Arc<Mutex<Option<EmuComms>>>, emu_comms: Arc<Mutex<Option<EmuComms>>>,
@ -47,7 +47,7 @@ impl Editor for TwincEditor {
#[cfg(feature = "vulkan")] #[cfg(feature = "vulkan")]
let shader_path = { 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() config.emu_config.vulkan_config.cgb_shader_path.as_ref()
} else { } else {
config.emu_config.vulkan_config.dmg_shader_path.as_ref() config.emu_config.vulkan_config.dmg_shader_path.as_ref()

View file

@ -10,6 +10,7 @@ pub trait NamedConfig {
fn name() -> String; fn name() -> String;
} }
#[derive(Clone)]
pub struct ConfigManager { pub struct ConfigManager {
path: PathBuf, path: PathBuf,
} }