From 1b85628a401aebd0147cf1f3711cb84324eb27a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com> Date: Mon, 27 Feb 2023 15:39:00 +0000 Subject: [PATCH] feat: initial kind of working audio for CH 2 This a very work in progress situation that requires stability. The most important part is the fact that it's possible for an external entity (SDL) to call an audio tick and get a volume. --- frontends/sdl/src/audio.rs | 45 ++++++----- frontends/sdl/src/main.rs | 154 ++++++++++++++++++++++++------------- src/apu.rs | 55 +++++++++++-- src/gb.rs | 16 +++- 4 files changed, 186 insertions(+), 84 deletions(-) diff --git a/frontends/sdl/src/audio.rs b/frontends/sdl/src/audio.rs index 7a41d23a..2b5480bc 100644 --- a/frontends/sdl/src/audio.rs +++ b/frontends/sdl/src/audio.rs @@ -1,32 +1,38 @@ +use boytacean::gb::{AudioProvider, GameBoy}; use sdl2::{ - audio::{AudioCallback, AudioDevice, AudioSpecDesired}, + audio::{AudioCallback, AudioDevice, AudioSpec, AudioSpecDesired}, AudioSubsystem, Sdl, }; +use std::sync::{Arc, Mutex}; pub struct AudioWave { - phase_inc: f32, + /// Specification of the audion settings that have been put in place + /// for the playing of this audio wave. + spec: AudioSpec, - phase: f32, + /// The object that is going to be used as the provider of the audio + /// operation. + audio_provider: Arc<Mutex<Box<GameBoy>>>, - volume: f32, - - /// The relative amount of time (as a percentage decimal) the low level - /// is going to be present during a period (cycle). - /// From [Wikipedia](https://en.wikipedia.org/wiki/Duty_cycle). - duty_cycle: f32, + /// The number of audio ticks that have passed since the beginning + /// of the audio playback, the value wraps around (avoids overflow). + ticks: usize, } impl AudioCallback for AudioWave { type Channel = f32; fn callback(&mut self, out: &mut [f32]) { + self.ticks = self.ticks.wrapping_add(out.len() as usize); + for x in out.iter_mut() { - *x = if self.phase < (1.0 - self.duty_cycle) { - self.volume - } else { - -self.volume - }; - self.phase = (self.phase + self.phase_inc) % 1.0; + *x = match self.audio_provider.lock() { + Ok(mut provider) => { + let value = provider.tick_apu(self.spec.freq as u32) as f32 / 7.0; + value + } + Err(_) => 0.0, + } } } } @@ -37,7 +43,7 @@ pub struct Audio { } impl Audio { - pub fn new(sdl: &Sdl) -> Self { + pub fn new(sdl: &Sdl, audio_provider: Arc<Mutex<Box<GameBoy>>>) -> Self { let audio_subsystem = sdl.audio().unwrap(); let desired_spec = AudioSpecDesired { @@ -48,10 +54,9 @@ impl Audio { let device = audio_subsystem .open_playback(None, &desired_spec, |spec| AudioWave { - phase_inc: 440.0 / spec.freq as f32, - phase: 0.0, - volume: 0.25, - duty_cycle: 0.5, + spec: spec, + audio_provider: audio_provider, + ticks: 0, }) .unwrap(); diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index 5b978925..5844e69a 100644 --- a/frontends/sdl/src/main.rs +++ b/frontends/sdl/src/main.rs @@ -6,13 +6,17 @@ pub mod graphics; use audio::Audio; use boytacean::{ - gb::GameBoy, + gb::{AudioProvider, GameBoy}, pad::PadKey, ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH}, }; use graphics::{surface_from_bytes, Graphics}; -use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum}; -use std::{cmp::max, time::SystemTime}; +use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum, Sdl}; +use std::{ + cmp::max, + sync::{Arc, Mutex}, + time::SystemTime, +}; /// The scale at which the screen is going to be drawn /// meaning the ratio between Game Boy resolution and @@ -39,9 +43,9 @@ impl Default for Benchmark { } pub struct Emulator { - system: GameBoy, - graphics: Graphics, - audio: Audio, + system: Arc<Mutex<Box<GameBoy>>>, + graphics: Option<Graphics>, + audio: Option<Audio>, logic_frequency: u32, visual_frequency: f32, next_tick_time: f32, @@ -51,20 +55,11 @@ pub struct Emulator { } impl Emulator { - pub fn new(system: GameBoy, screen_scale: f32) -> Self { - let sdl = sdl2::init().unwrap(); - let graphics = Graphics::new( - &sdl, - TITLE, - DISPLAY_WIDTH as u32, - DISPLAY_HEIGHT as u32, - screen_scale, - ); - let audio = Audio::new(&sdl); + pub fn new(system: Arc<Mutex<Box<GameBoy>>>) -> Self { Self { system, - graphics: graphics, - audio: audio, + graphics: None, + audio: None, logic_frequency: GameBoy::CPU_FREQ, visual_frequency: GameBoy::VISUAL_FREQ, next_tick_time: 0.0, @@ -102,13 +97,40 @@ impl Emulator { } } + pub fn start(&mut self, screen_scale: f32, audio_provider: Arc<Mutex<Box<GameBoy>>>) { + let sdl = sdl2::init().unwrap(); + self.start_graphics(&sdl, screen_scale); + self.start_audio(&sdl, audio_provider); + } + + pub fn start_graphics(&mut self, sdl: &Sdl, screen_scale: f32) { + self.graphics = Some(Graphics::new( + &sdl, + TITLE, + DISPLAY_WIDTH as u32, + DISPLAY_HEIGHT as u32, + screen_scale, + )); + } + + pub fn start_audio(&mut self, sdl: &Sdl, audio_provider: Arc<Mutex<Box<GameBoy>>>) { + self.audio = Some(Audio::new(sdl, audio_provider)); + } + + pub fn tick_audio(&mut self, freq: u32) -> u8 { + self.system.lock().unwrap().tick_apu(freq) + } + pub fn load_rom(&mut self, path: &str) { - let rom = self.system.load_rom_file(path); + let mut system = self.system.lock().unwrap(); + let rom = system.load_rom_file(path); println!( "========= Cartridge =========\n{}\n=============================", rom ); self.graphics + .as_mut() + .unwrap() .window_mut() .set_title(format!("{} [{}]", TITLE, rom.title()).as_str()) .unwrap(); @@ -123,7 +145,7 @@ impl Emulator { let initial = SystemTime::now(); for _ in 0..count { - cycles += self.system.clock() as u32; + cycles += self.system.lock().unwrap().clock() as u32; } let delta = initial.elapsed().unwrap().as_millis() as f32 / 1000.0; @@ -137,6 +159,8 @@ impl Emulator { pub fn toggle_palette(&mut self) { self.system + .lock() + .unwrap() .ppu() .set_palette_colors(self.palettes[self.palette_index].colors()); self.palette_index = (self.palette_index + 1) % self.palettes.len(); @@ -146,15 +170,19 @@ impl Emulator { // updates the icon of the window to reflect the image // and style of the emulator let surface = surface_from_bytes(&data::ICON); - self.graphics.window_mut().set_icon(&surface); + self.graphics + .as_mut() + .unwrap() + .window_mut() + .set_icon(&surface); // creates an accelerated canvas to be used in the drawing // then clears it and presents it - self.graphics.canvas.present(); + self.graphics.as_mut().unwrap().canvas.present(); // creates a texture creator for the current canvas, required // for the creation of dynamic and static textures - let texture_creator = self.graphics.canvas.texture_creator(); + let texture_creator = self.graphics.as_mut().unwrap().canvas.texture_creator(); // creates the texture streaming that is going to be used // as the target for the pixel buffer @@ -183,7 +211,7 @@ impl Emulator { // obtains an event from the SDL sub-system to be // processed under the current emulation context - while let Some(event) = self.graphics.event_pump.poll_event() { + while let Some(event) = self.graphics.as_mut().unwrap().event_pump.poll_event() { match event { Event::Quit { .. } => break 'main, Event::KeyDown { @@ -211,7 +239,7 @@ impl Emulator { .. } => { if let Some(key) = key_to_pad(keycode) { - self.system.key_press(key) + self.system.lock().unwrap().key_press(key) } } Event::KeyUp { @@ -219,19 +247,19 @@ impl Emulator { .. } => { if let Some(key) = key_to_pad(keycode) { - self.system.key_lift(key) + self.system.lock().unwrap().key_lift(key) } } Event::DropFile { filename, .. } => { - self.system.reset(); - self.system.load_boot_default(); + self.system.lock().unwrap().reset(); + self.system.lock().unwrap().load_boot_default(); self.load_rom(&filename); } _ => (), } } - let current_time = self.graphics.timer_subsystem.ticks(); + let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks(); if current_time >= self.next_tick_time_i { // re-starts the counter cycles with the number of pending cycles @@ -255,24 +283,29 @@ impl Emulator { break; } - // runs the Game Boy clock, this operations should - // include the advance of both the CPU and the PPU - counter_cycles += self.system.clock() as u32; - - if self.system.ppu_mode() == PpuMode::VBlank - && self.system.ppu_frame() != last_frame { - // obtains the frame buffer of the Game Boy PPU and uses it - // to update the stream texture, that will latter be copied - // to the canvas - let frame_buffer = self.system.frame_buffer().as_ref(); - texture - .update(None, frame_buffer, DISPLAY_WIDTH * 3) - .unwrap(); - - // obtains the index of the current PPU frame, this value - // is going to be used to detect for new frame presence - last_frame = self.system.ppu_frame(); + // obtains a locked reference to the system that is going to be + // valid under the current block + let mut system = self.system.lock().unwrap(); + + // runs the Game Boy clock, this operations should + // include the advance of both the CPU and the PPU + counter_cycles += system.clock() as u32; + + if system.ppu_mode() == PpuMode::VBlank && system.ppu_frame() != last_frame + { + // obtains the frame buffer of the Game Boy PPU and uses it + // to update the stream texture, that will latter be copied + // to the canvas + let frame_buffer = system.frame_buffer().as_ref(); + texture + .update(None, frame_buffer, DISPLAY_WIDTH * 3) + .unwrap(); + + // obtains the index of the current PPU frame, this value + // is going to be used to detect for new frame presence + last_frame = system.ppu_frame(); + } } } @@ -285,15 +318,20 @@ impl Emulator { // clears the graphics canvas, making sure that no garbage // pixel data remaining in the pixel buffer, not doing this would // create visual glitches in OSs like Mac OS X - self.graphics.canvas.clear(); + self.graphics.as_mut().unwrap().canvas.clear(); // copies the texture that was created for the frame (during // the loop part of the tick) to the canvas - self.graphics.canvas.copy(&texture, None, None).unwrap(); + self.graphics + .as_mut() + .unwrap() + .canvas + .copy(&texture, None, None) + .unwrap(); // presents the canvas effectively updating the screen // information presented to the user - self.graphics.canvas.present(); + self.graphics.as_mut().unwrap().canvas.present(); } // calculates the number of ticks that have elapsed since the @@ -315,9 +353,13 @@ impl Emulator { self.next_tick_time_i = self.next_tick_time.ceil() as u32; } - let current_time = self.graphics.timer_subsystem.ticks(); + let current_time = self.graphics.as_mut().unwrap().timer_subsystem.ticks(); let pending_time = self.next_tick_time_i.saturating_sub(current_time); - self.graphics.timer_subsystem.delay(pending_time); + self.graphics + .as_mut() + .unwrap() + .timer_subsystem + .delay(pending_time); } } } @@ -325,12 +367,16 @@ impl Emulator { fn main() { // creates a new Game Boy instance and loads both the boot ROM // and the initial game ROM to "start the engine" - let mut game_boy = GameBoy::new(); - game_boy.load_boot_default(); + let game_boy = Arc::new(Mutex::new(Box::new(GameBoy::new()))); + game_boy.try_lock().unwrap().as_mut().load_boot_default(); // creates a new generic emulator structure loads the default // ROM file and starts running it - let mut emulator = Emulator::new(game_boy, SCREEN_SCALE); + let mut emulator = Emulator::new(game_boy); + + let game_boy_ref = emulator.system.clone(); + + emulator.start(SCREEN_SCALE, game_boy_ref); emulator.load_rom("../../res/roms/pocket.gb"); emulator.toggle_palette(); emulator.run(); diff --git a/src/apu.rs b/src/apu.rs index 09b6e681..349232a7 100644 --- a/src/apu.rs +++ b/src/apu.rs @@ -1,6 +1,16 @@ use crate::warnln; +const DUTY_TABLE: [[u8; 8]; 4] = [ + [0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 0], +]; + pub struct Apu { + ch1_timer: u16, + ch1_sequence: u8, + ch1_output: u8, ch1_sweep_slope: u8, ch1_sweep_increase: bool, ch1_sweep_pace: u8, @@ -13,6 +23,9 @@ pub struct Apu { ch1_sound_length: bool, ch1_enabled: bool, + ch2_timer: u16, + ch2_sequence: u8, + ch2_output: u8, ch2_length_timer: u8, ch2_wave_duty: u8, ch2_pace: u8, @@ -26,6 +39,9 @@ pub struct Apu { impl Apu { pub fn new() -> Self { Self { + ch1_timer: 0, + ch1_sequence: 0, + ch1_output: 0, ch1_sweep_slope: 0x0, ch1_sweep_increase: false, ch1_sweep_pace: 0x0, @@ -38,6 +54,9 @@ impl Apu { ch1_sound_length: false, ch1_enabled: false, + ch2_timer: 0, + ch2_sequence: 0, + ch2_output: 0, ch2_length_timer: 0x0, ch2_wave_duty: 0x0, ch2_pace: 0x0, @@ -49,13 +68,10 @@ impl Apu { } } - pub fn clock(&mut self, cycles: u8) { - // @todo implement the clock and allow for the proper - // writing of the output buffer at a fixed frequency - - - - + pub fn clock(&mut self, cycles: u8, freq: u32) { + for _ in 0..cycles { + self.cycle(freq); + } } pub fn read(&mut self, addr: u16) -> u8 { @@ -127,4 +143,29 @@ impl Apu { _ => warnln!("Writing in unknown APU location 0x{:04x}", addr), } } + + #[inline(always)] + pub fn cycle(&mut self, freq: u32) { + self.ch2_timer = self.ch2_timer.saturating_sub(1); + if self.ch2_timer == 0 { + let target_freq = 1048576.0 / (2048.0 - self.ch1_wave_length as f32); + self.ch2_timer = (freq as f32 / target_freq) as u16; + self.ch2_sequence = (self.ch2_sequence + 1) & 7; + + if self.ch2_enabled { + self.ch2_output = + if DUTY_TABLE[self.ch2_wave_duty as usize][self.ch2_sequence as usize] == 1 { + self.ch2_volume + } else { + 0 + }; + } else { + self.ch2_output = 0; + } + } + } + + pub fn output(&self) -> u8 { + self.ch2_output + } } diff --git a/src/gb.rs b/src/gb.rs index 1a747854..125ec06d 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -53,6 +53,10 @@ pub struct Registers { pub lyc: u8, } +pub trait AudioProvider { + fn tick_apu(&mut self, freq: u32) -> u8; +} + #[cfg_attr(feature = "wasm", wasm_bindgen)] impl GameBoy { #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))] @@ -75,7 +79,6 @@ impl GameBoy { pub fn clock(&mut self) -> u8 { let cycles = self.cpu_clock(); self.ppu_clock(cycles); - self.apu_clock(cycles); self.timer_clock(cycles); cycles } @@ -96,8 +99,8 @@ impl GameBoy { self.ppu().clock(cycles) } - pub fn apu_clock(&mut self, cycles: u8) { - self.apu().clock(cycles) + pub fn apu_clock(&mut self, cycles: u8, freq: u32) { + self.apu().clock(cycles, freq) } pub fn timer_clock(&mut self, cycles: u8) { @@ -372,6 +375,13 @@ pub fn hook_impl(info: &PanicInfo) { panic(message.as_str()); } +impl AudioProvider for GameBoy { + fn tick_apu(&mut self, freq: u32) -> u8 { + self.apu_clock(1, freq); + self.apu().output() + } +} + impl Default for GameBoy { fn default() -> Self { Self::new() -- GitLab