use async_ringbuf::AsyncHeapConsumer; use futures::executor; use gb_emu_lib::{ connect::{ AudioOutput, DownsampleType, EmulatorMessage, EmulatorOptions, JoypadButtons, NoCamera, RomFile, SerialTarget, }, EmulatorCore, }; use nih_plug::midi::MidiResult::Basic; use nih_plug::prelude::*; use std::sync::{ mpsc::{self, channel, Receiver, Sender}, Arc, Mutex, }; use ui::{Emulator, EmulatorRenderer}; #[cfg(feature = "savestate")] use gb_emu_lib::connect::CpuSaveState; #[cfg(feature = "savestate")] use nih_plug::params::persist::PersistentField; mod ui; #[cfg(feature = "savestate")] #[derive(Default)] struct SaveStateParam { state: Arc>>>, } #[cfg(feature = "savestate")] impl PersistentField<'_, Option>> for SaveStateParam { fn set(&self, new_value: Option>) { *self.state.lock().unwrap() = new_value; } fn map(&self, f: F) -> R where F: Fn(&Option>) -> R, { f(&self.state.lock().unwrap()) } } #[derive(Params, Default)] struct EmuParams { #[cfg(feature = "savestate")] #[persist = "save_state"] last_save_state: SaveStateParam, } struct EmuVars { rx: AsyncHeapConsumer<[f32; 2]>, sender: Sender, emulator_core: EmulatorCore<[u8; 4], EmulatorRenderer, NoCamera>, serial_tx: Sender, } #[derive(Default)] pub struct GameboyEmu { vars: Option, frame_receiver: Arc, key_handler: Arc, params: Arc, } type FrameReceiver = Mutex>>>; type JoypadSender = Mutex>>; const FRAMES_TO_BUFFER: usize = 1; const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::ZeroOrderHold; const ROM: &[u8; 65536] = include_bytes!("../../test-roms/mGB1_3_1.gb"); const BOOTROM: Option<&[u8; 256]> = None; 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(), }]; 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 { panic!() } 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() {} } self.update_save_state(); ProcessStatus::KeepAlive } fn editor(&self, _: AsyncExecutor) -> Option> { Some(Box::new(Emulator::new( self.frame_receiver.clone(), self.key_handler.clone(), ))) } 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, false, FRAMES_TO_BUFFER, DOWNSAMPLE_TYPE, ); vars.emulator_core.replace_output(output); vars.rx = rx; } else { let bootrom = BOOTROM.map(|v| RomFile::Raw(v.to_vec())); let rom = RomFile::Raw(ROM.to_vec()); let (sender, receiver) = channel::(); let (output, rx) = AudioOutput::new( buffer_config.sample_rate, false, FRAMES_TO_BUFFER, DOWNSAMPLE_TYPE, ); let (window, frame_receiver, key_handler) = EmulatorRenderer::new(); *self.frame_receiver.lock().unwrap() = Some(frame_receiver); *self.key_handler.lock().unwrap() = Some(key_handler); let (serial_tx, gb_serial_rx) = mpsc::channel::(); let serial_target = SerialTarget::Custom { rx: Some(gb_serial_rx), tx: None, }; #[cfg(feature = "savestate")] let mut emulator_core = if let Some(state) = self.params.last_save_state.state.lock().unwrap().take() { EmulatorCore::from_save_state(state, rom, receiver, window, output, serial_target) } else { let options = gb_emu_lib::Options::new(window, rom, output) .with_bootrom(bootrom) .with_serial_target(serial_target) .force_no_save(); EmulatorCore::init(receiver, options) }; #[cfg(not(feature = "savestate"))] let mut emulator_core = { let options = EmulatorOptions::new(window, rom, output) .with_bootrom(bootrom) .with_serial_target(serial_target) .force_no_save(); EmulatorCore::init(receiver, options) }; emulator_core.run_until_buffer_full(); self.vars = Some(EmuVars { rx, sender, emulator_core, serial_tx, }); self.update_save_state(); } true } fn deactivate(&mut self) { self.update_save_state(); if let Some(ref mut vars) = self.vars { match vars.sender.send(EmulatorMessage::Stop) { Ok(_) => self.vars = None, Err(e) => nih_log!("error {e} sending message to emulator"), } } } } impl GameboyEmu { fn update_save_state(&mut self) { #[cfg(feature = "savestate")] if let Some(ref mut vars) = self.vars { *self.params.last_save_state.state.lock().unwrap() = Some(vars.emulator_core.get_save_state()); } } } 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);