use async_ringbuf::AsyncHeapConsumer; use futures::executor; use gb_emu_lib::{ connect::{AudioOutput, CpuSaveState, DownsampleType, EmulatorMessage, JoypadButtons, RomFile}, EmulatorCore, }; use nih_plug::prelude::*; use std::sync::{ mpsc::{channel, Receiver, Sender}, Arc, Mutex, }; use ui::{Emulator, EmulatorRenderer}; mod ui; #[derive(Params, Default)] struct EmuParams {} struct EmuVars { rx: AsyncHeapConsumer<[f32; 2]>, sender: Sender, emulator_core: EmulatorCore<[u8; 4]>, } #[derive(Default)] pub struct GameboyEmu { vars: Option, frame_receiver: Arc, key_handler: Arc, last_save_state: Option>, } type FrameReceiver = Mutex>>>; type JoypadSender = Mutex>>; const FRAMES_TO_BUFFER: usize = 1; const INCLUDE_BOOTROM: bool = false; const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::ZeroOrderHold; const ROM: &[u8; 65536] = include_bytes!("../../test-roms/mGB1_3_0.gb"); const BOOTROM: &[u8; 256] = include_bytes!("../../bootrom/dmg_boot.bin"); 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 { Arc::new(EmuParams::default()) } fn process( &mut self, buffer: &mut Buffer, _: &mut AuxiliaryBuffers, _: &mut impl ProcessContext, ) -> ProcessStatus { if let Some(ref mut vars) = self.vars { 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(); } 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 = if INCLUDE_BOOTROM { Some(RomFile::Raw(BOOTROM.to_vec())) } else { None }; 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 (renderer, frame_receiver, key_handler) = EmulatorRenderer::new(); *self.frame_receiver.lock().unwrap() = Some(frame_receiver); *self.key_handler.lock().unwrap() = Some(key_handler); let window = Box::new(renderer); let mut emulator_core = if let Some(state) = self.last_save_state.take() { EmulatorCore::from_save_state(state, rom, receiver, window, output) } else { let options = gb_emu_lib::Options { rom, save_path: None, no_save: true, bootrom, connect_serial: false, verbose: false, }; EmulatorCore::init(receiver, options, window, output, None) }; emulator_core.run_until_buffer_full(); self.vars = Some(EmuVars { rx, sender, emulator_core, }); } true } fn deactivate(&mut self) { nih_log!("deactivating"); if let Some(ref mut vars) = self.vars { self.last_save_state = Some(vars.emulator_core.get_save_state()); match vars.sender.send(EmulatorMessage::Stop) { 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);