Skip to content
Snippets Groups Projects
Verified Commit 1b85628a authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

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.
parent 3928e4f1
No related branches found
No related tags found
1 merge request!19Initial tentative audio support 🔉
Pipeline #2264 failed
use boytacean::gb::{AudioProvider, GameBoy};
use sdl2::{ use sdl2::{
audio::{AudioCallback, AudioDevice, AudioSpecDesired}, audio::{AudioCallback, AudioDevice, AudioSpec, AudioSpecDesired},
AudioSubsystem, Sdl, AudioSubsystem, Sdl,
}; };
use std::sync::{Arc, Mutex};
pub struct AudioWave { 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 number of audio ticks that have passed since the beginning
/// of the audio playback, the value wraps around (avoids overflow).
/// The relative amount of time (as a percentage decimal) the low level ticks: usize,
/// is going to be present during a period (cycle).
/// From [Wikipedia](https://en.wikipedia.org/wiki/Duty_cycle).
duty_cycle: f32,
} }
impl AudioCallback for AudioWave { impl AudioCallback for AudioWave {
type Channel = f32; type Channel = f32;
fn callback(&mut self, out: &mut [f32]) { fn callback(&mut self, out: &mut [f32]) {
self.ticks = self.ticks.wrapping_add(out.len() as usize);
for x in out.iter_mut() { for x in out.iter_mut() {
*x = if self.phase < (1.0 - self.duty_cycle) { *x = match self.audio_provider.lock() {
self.volume Ok(mut provider) => {
} else { let value = provider.tick_apu(self.spec.freq as u32) as f32 / 7.0;
-self.volume value
}; }
self.phase = (self.phase + self.phase_inc) % 1.0; Err(_) => 0.0,
}
} }
} }
} }
...@@ -37,7 +43,7 @@ pub struct Audio { ...@@ -37,7 +43,7 @@ pub struct Audio {
} }
impl 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 audio_subsystem = sdl.audio().unwrap();
let desired_spec = AudioSpecDesired { let desired_spec = AudioSpecDesired {
...@@ -48,10 +54,9 @@ impl Audio { ...@@ -48,10 +54,9 @@ impl Audio {
let device = audio_subsystem let device = audio_subsystem
.open_playback(None, &desired_spec, |spec| AudioWave { .open_playback(None, &desired_spec, |spec| AudioWave {
phase_inc: 440.0 / spec.freq as f32, spec: spec,
phase: 0.0, audio_provider: audio_provider,
volume: 0.25, ticks: 0,
duty_cycle: 0.5,
}) })
.unwrap(); .unwrap();
......
...@@ -6,13 +6,17 @@ pub mod graphics; ...@@ -6,13 +6,17 @@ pub mod graphics;
use audio::Audio; use audio::Audio;
use boytacean::{ use boytacean::{
gb::GameBoy, gb::{AudioProvider, GameBoy},
pad::PadKey, pad::PadKey,
ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH}, ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH},
}; };
use graphics::{surface_from_bytes, Graphics}; use graphics::{surface_from_bytes, Graphics};
use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum}; use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum, Sdl};
use std::{cmp::max, time::SystemTime}; use std::{
cmp::max,
sync::{Arc, Mutex},
time::SystemTime,
};
/// The scale at which the screen is going to be drawn /// The scale at which the screen is going to be drawn
/// meaning the ratio between Game Boy resolution and /// meaning the ratio between Game Boy resolution and
...@@ -39,9 +43,9 @@ impl Default for Benchmark { ...@@ -39,9 +43,9 @@ impl Default for Benchmark {
} }
pub struct Emulator { pub struct Emulator {
system: GameBoy, system: Arc<Mutex<Box<GameBoy>>>,
graphics: Graphics, graphics: Option<Graphics>,
audio: Audio, audio: Option<Audio>,
logic_frequency: u32, logic_frequency: u32,
visual_frequency: f32, visual_frequency: f32,
next_tick_time: f32, next_tick_time: f32,
...@@ -51,20 +55,11 @@ pub struct Emulator { ...@@ -51,20 +55,11 @@ pub struct Emulator {
} }
impl Emulator { impl Emulator {
pub fn new(system: GameBoy, screen_scale: f32) -> Self { pub fn new(system: Arc<Mutex<Box<GameBoy>>>) -> 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);
Self { Self {
system, system,
graphics: graphics, graphics: None,
audio: audio, audio: None,
logic_frequency: GameBoy::CPU_FREQ, logic_frequency: GameBoy::CPU_FREQ,
visual_frequency: GameBoy::VISUAL_FREQ, visual_frequency: GameBoy::VISUAL_FREQ,
next_tick_time: 0.0, next_tick_time: 0.0,
...@@ -102,13 +97,40 @@ impl Emulator { ...@@ -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) { 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!( println!(
"========= Cartridge =========\n{}\n=============================", "========= Cartridge =========\n{}\n=============================",
rom rom
); );
self.graphics self.graphics
.as_mut()
.unwrap()
.window_mut() .window_mut()
.set_title(format!("{} [{}]", TITLE, rom.title()).as_str()) .set_title(format!("{} [{}]", TITLE, rom.title()).as_str())
.unwrap(); .unwrap();
...@@ -123,7 +145,7 @@ impl Emulator { ...@@ -123,7 +145,7 @@ impl Emulator {
let initial = SystemTime::now(); let initial = SystemTime::now();
for _ in 0..count { 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; let delta = initial.elapsed().unwrap().as_millis() as f32 / 1000.0;
...@@ -137,6 +159,8 @@ impl Emulator { ...@@ -137,6 +159,8 @@ impl Emulator {
pub fn toggle_palette(&mut self) { pub fn toggle_palette(&mut self) {
self.system self.system
.lock()
.unwrap()
.ppu() .ppu()
.set_palette_colors(self.palettes[self.palette_index].colors()); .set_palette_colors(self.palettes[self.palette_index].colors());
self.palette_index = (self.palette_index + 1) % self.palettes.len(); self.palette_index = (self.palette_index + 1) % self.palettes.len();
...@@ -146,15 +170,19 @@ impl Emulator { ...@@ -146,15 +170,19 @@ impl Emulator {
// updates the icon of the window to reflect the image // updates the icon of the window to reflect the image
// and style of the emulator // and style of the emulator
let surface = surface_from_bytes(&data::ICON); 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 // creates an accelerated canvas to be used in the drawing
// then clears it and presents it // 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 // creates a texture creator for the current canvas, required
// for the creation of dynamic and static textures // 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 // creates the texture streaming that is going to be used
// as the target for the pixel buffer // as the target for the pixel buffer
...@@ -183,7 +211,7 @@ impl Emulator { ...@@ -183,7 +211,7 @@ impl Emulator {
// obtains an event from the SDL sub-system to be // obtains an event from the SDL sub-system to be
// processed under the current emulation context // 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 { match event {
Event::Quit { .. } => break 'main, Event::Quit { .. } => break 'main,
Event::KeyDown { Event::KeyDown {
...@@ -211,7 +239,7 @@ impl Emulator { ...@@ -211,7 +239,7 @@ impl Emulator {
.. ..
} => { } => {
if let Some(key) = key_to_pad(keycode) { if let Some(key) = key_to_pad(keycode) {
self.system.key_press(key) self.system.lock().unwrap().key_press(key)
} }
} }
Event::KeyUp { Event::KeyUp {
...@@ -219,19 +247,19 @@ impl Emulator { ...@@ -219,19 +247,19 @@ impl Emulator {
.. ..
} => { } => {
if let Some(key) = key_to_pad(keycode) { if let Some(key) = key_to_pad(keycode) {
self.system.key_lift(key) self.system.lock().unwrap().key_lift(key)
} }
} }
Event::DropFile { filename, .. } => { Event::DropFile { filename, .. } => {
self.system.reset(); self.system.lock().unwrap().reset();
self.system.load_boot_default(); self.system.lock().unwrap().load_boot_default();
self.load_rom(&filename); 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 { if current_time >= self.next_tick_time_i {
// re-starts the counter cycles with the number of pending cycles // re-starts the counter cycles with the number of pending cycles
...@@ -255,24 +283,29 @@ impl Emulator { ...@@ -255,24 +283,29 @@ impl Emulator {
break; 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 // obtains a locked reference to the system that is going to be
// to update the stream texture, that will latter be copied // valid under the current block
// to the canvas let mut system = self.system.lock().unwrap();
let frame_buffer = self.system.frame_buffer().as_ref();
texture // runs the Game Boy clock, this operations should
.update(None, frame_buffer, DISPLAY_WIDTH * 3) // include the advance of both the CPU and the PPU
.unwrap(); counter_cycles += system.clock() as u32;
// obtains the index of the current PPU frame, this value if system.ppu_mode() == PpuMode::VBlank && system.ppu_frame() != last_frame
// is going to be used to detect for new frame presence {
last_frame = self.system.ppu_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 { ...@@ -285,15 +318,20 @@ impl Emulator {
// clears the graphics canvas, making sure that no garbage // clears the graphics canvas, making sure that no garbage
// pixel data remaining in the pixel buffer, not doing this would // pixel data remaining in the pixel buffer, not doing this would
// create visual glitches in OSs like Mac OS X // 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 // copies the texture that was created for the frame (during
// the loop part of the tick) to the canvas // 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 // presents the canvas effectively updating the screen
// information presented to the user // 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 // calculates the number of ticks that have elapsed since the
...@@ -315,9 +353,13 @@ impl Emulator { ...@@ -315,9 +353,13 @@ impl Emulator {
self.next_tick_time_i = self.next_tick_time.ceil() as u32; 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); 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 { ...@@ -325,12 +367,16 @@ impl Emulator {
fn main() { fn main() {
// creates a new Game Boy instance and loads both the boot ROM // creates a new Game Boy instance and loads both the boot ROM
// and the initial game ROM to "start the engine" // and the initial game ROM to "start the engine"
let mut game_boy = GameBoy::new(); let game_boy = Arc::new(Mutex::new(Box::new(GameBoy::new())));
game_boy.load_boot_default(); game_boy.try_lock().unwrap().as_mut().load_boot_default();
// creates a new generic emulator structure loads the default // creates a new generic emulator structure loads the default
// ROM file and starts running it // 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.load_rom("../../res/roms/pocket.gb");
emulator.toggle_palette(); emulator.toggle_palette();
emulator.run(); emulator.run();
......
use crate::warnln; 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 { pub struct Apu {
ch1_timer: u16,
ch1_sequence: u8,
ch1_output: u8,
ch1_sweep_slope: u8, ch1_sweep_slope: u8,
ch1_sweep_increase: bool, ch1_sweep_increase: bool,
ch1_sweep_pace: u8, ch1_sweep_pace: u8,
...@@ -13,6 +23,9 @@ pub struct Apu { ...@@ -13,6 +23,9 @@ pub struct Apu {
ch1_sound_length: bool, ch1_sound_length: bool,
ch1_enabled: bool, ch1_enabled: bool,
ch2_timer: u16,
ch2_sequence: u8,
ch2_output: u8,
ch2_length_timer: u8, ch2_length_timer: u8,
ch2_wave_duty: u8, ch2_wave_duty: u8,
ch2_pace: u8, ch2_pace: u8,
...@@ -26,6 +39,9 @@ pub struct Apu { ...@@ -26,6 +39,9 @@ pub struct Apu {
impl Apu { impl Apu {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
ch1_timer: 0,
ch1_sequence: 0,
ch1_output: 0,
ch1_sweep_slope: 0x0, ch1_sweep_slope: 0x0,
ch1_sweep_increase: false, ch1_sweep_increase: false,
ch1_sweep_pace: 0x0, ch1_sweep_pace: 0x0,
...@@ -38,6 +54,9 @@ impl Apu { ...@@ -38,6 +54,9 @@ impl Apu {
ch1_sound_length: false, ch1_sound_length: false,
ch1_enabled: false, ch1_enabled: false,
ch2_timer: 0,
ch2_sequence: 0,
ch2_output: 0,
ch2_length_timer: 0x0, ch2_length_timer: 0x0,
ch2_wave_duty: 0x0, ch2_wave_duty: 0x0,
ch2_pace: 0x0, ch2_pace: 0x0,
...@@ -49,13 +68,10 @@ impl Apu { ...@@ -49,13 +68,10 @@ impl Apu {
} }
} }
pub fn clock(&mut self, cycles: u8) { pub fn clock(&mut self, cycles: u8, freq: u32) {
// @todo implement the clock and allow for the proper for _ in 0..cycles {
// writing of the output buffer at a fixed frequency self.cycle(freq);
}
} }
pub fn read(&mut self, addr: u16) -> u8 { pub fn read(&mut self, addr: u16) -> u8 {
...@@ -127,4 +143,29 @@ impl Apu { ...@@ -127,4 +143,29 @@ impl Apu {
_ => warnln!("Writing in unknown APU location 0x{:04x}", addr), _ => 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
}
} }
...@@ -53,6 +53,10 @@ pub struct Registers { ...@@ -53,6 +53,10 @@ pub struct Registers {
pub lyc: u8, pub lyc: u8,
} }
pub trait AudioProvider {
fn tick_apu(&mut self, freq: u32) -> u8;
}
#[cfg_attr(feature = "wasm", wasm_bindgen)] #[cfg_attr(feature = "wasm", wasm_bindgen)]
impl GameBoy { impl GameBoy {
#[cfg_attr(feature = "wasm", wasm_bindgen(constructor))] #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
...@@ -75,7 +79,6 @@ impl GameBoy { ...@@ -75,7 +79,6 @@ impl GameBoy {
pub fn clock(&mut self) -> u8 { pub fn clock(&mut self) -> u8 {
let cycles = self.cpu_clock(); let cycles = self.cpu_clock();
self.ppu_clock(cycles); self.ppu_clock(cycles);
self.apu_clock(cycles);
self.timer_clock(cycles); self.timer_clock(cycles);
cycles cycles
} }
...@@ -96,8 +99,8 @@ impl GameBoy { ...@@ -96,8 +99,8 @@ impl GameBoy {
self.ppu().clock(cycles) self.ppu().clock(cycles)
} }
pub fn apu_clock(&mut self, cycles: u8) { pub fn apu_clock(&mut self, cycles: u8, freq: u32) {
self.apu().clock(cycles) self.apu().clock(cycles, freq)
} }
pub fn timer_clock(&mut self, cycles: u8) { pub fn timer_clock(&mut self, cycles: u8) {
...@@ -372,6 +375,13 @@ pub fn hook_impl(info: &PanicInfo) { ...@@ -372,6 +375,13 @@ pub fn hook_impl(info: &PanicInfo) {
panic(message.as_str()); 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 { impl Default for GameBoy {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment