use async_ringbuf::AsyncHeapConsumer; use futures::executor; use gb_emu_lib::{ connect::{AudioOutput, 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, } type FrameReceiver = Mutex>>>; type JoypadSender = Mutex>>; const FRAMES_TO_BUFFER: usize = 1; 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: NonZeroU32::new(2), 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(), }]; 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 { let options = gb_emu_lib::Options { rom: RomFile::Raw( include_bytes!("../../test-roms/TwinPeaksMeanwhile.gb") .to_vec() .to_vec(), ), save_path: None, no_save: true, bootrom: Some(RomFile::Raw( include_bytes!("../../bootrom/dmg_boot.bin").to_vec(), )), connect_serial: false, verbose: false, cycle_count: false, }; if let Some(ref mut vars) = self.vars { let (output, rx) = AudioOutput::new_unfilled(buffer_config.sample_rate, false, FRAMES_TO_BUFFER); vars.emulator_core.replace_output(output); vars.rx = rx; } else { let (sender, receiver) = channel::(); let (output, rx) = AudioOutput::new_unfilled(buffer_config.sample_rate, false, FRAMES_TO_BUFFER); 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 mut emulator_core = EmulatorCore::init(receiver, options, Box::new(renderer), 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 { 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::Distortion, Vst3SubCategory::Dynamics]; } nih_export_vst3!(GameboyEmu);