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::{
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();
......
......@@ -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();
......
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
}
}
......@@ -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()
......
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