From ce630baa5d2ab97dfa6aee5a0104b97d3920cb6e Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 9 Jul 2021 01:25:52 -0500 Subject: [PATCH] feat(snd): implement audio playback using rodio --- Cargo.lock | 21 ++++++++++++ Cargo.toml | 1 + src/bus.rs | 6 +++- src/cpu.rs | 5 +++ src/emu.rs | 3 +- src/lib.rs | 1 + src/main.rs | 18 +++++++++- src/sound.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++--- 8 files changed, 141 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da276b5..465798d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -468,6 +468,26 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + [[package]] name = "d3d12" version = "0.3.2" @@ -675,6 +695,7 @@ dependencies = [ "anyhow", "bitfield", "clap", + "crossbeam-channel", "egui", "egui_wgpu_backend", "egui_winit_platform", diff --git a/Cargo.toml b/Cargo.toml index a01f855..6a09da2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ egui = "^0.10" egui_wgpu_backend = { git="https://github.com/hasenbanck/egui_wgpu_backend.git", rev="9d03ad345d15d1e44165849b242d3562fdf3e859" } egui_winit_platform = { git="https://github.com/hasenbanck/egui_winit_platform.git", rev="17298250e9721e8bf2c1d4a17b3e22777f8cb2e8" } rodio = "^0.14" +crossbeam-channel = "^0.5" [profile.release] debug = true diff --git a/src/bus.rs b/src/bus.rs index cfde3e2..60960d2 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -4,7 +4,7 @@ use crate::interrupt::{Interrupt, InterruptFlag}; use crate::joypad::Joypad; use crate::ppu::{Ppu, PpuMode}; use crate::serial::Serial; -use crate::sound::Sound; +use crate::sound::{SampleSender, Sound}; use crate::timer::Timer; use crate::work_ram::{VariableWorkRam, WorkRam}; use std::{fs::File, io::Read}; @@ -66,6 +66,10 @@ impl Bus { self.cartridge.as_ref()?.title() } + pub(crate) fn pass_audio_src(&mut self, sender: SampleSender) { + self.snd.set_audio_src(sender) + } + pub(crate) fn clock(&mut self) { self.ppu.clock(); self.timer.clock(); diff --git a/src/cpu.rs b/src/cpu.rs index f062ce6..efc56b6 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -3,6 +3,7 @@ use crate::instruction::{Cycle, Instruction}; use crate::interrupt::{InterruptEnable, InterruptFlag}; use crate::joypad::Joypad; use crate::ppu::Ppu; +use crate::sound::SampleSender; use crate::timer::Timer; use bitfield::bitfield; use std::fmt::{Display, Formatter, Result as FmtResult}; @@ -44,6 +45,10 @@ impl Cpu { }) } + pub fn set_audio_src(&mut self, sender: SampleSender) { + self.bus.pass_audio_src(sender) + } + pub(crate) fn ime(&self) -> ImeState { self.ime } diff --git a/src/emu.rs b/src/emu.rs index 3d0aee2..140d438 100644 --- a/src/emu.rs +++ b/src/emu.rs @@ -4,11 +4,12 @@ use crate::joypad; use crate::ppu::Ppu; use anyhow::Result; use gilrs::Gilrs; +use rodio::OutputStreamHandle; use std::time::Duration; pub const SM83_CYCLE_TIME: Duration = Duration::from_nanos(1_000_000_000 / SM83_CLOCK_SPEED); pub const CYCLES_IN_FRAME: Cycle = Cycle::new(456 * 154); // 456 Cycles times 154 scanlines -const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz +pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz const DEFAULT_TITLE: &str = "DMG-01 Emulator"; pub fn init(boot_path: Option<&str>, rom_path: &str) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 53aecf3..9713f19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub use gui::Egui; pub use instruction::Cycle; +pub use sound::AudioSenderReceiver; pub const GB_WIDTH: usize = 160; pub const GB_HEIGHT: usize = 144; diff --git a/src/main.rs b/src/main.rs index 84ba79e..591b7f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ use anyhow::{anyhow, Result}; use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; -use gb::{Cycle, Egui, GB_HEIGHT, GB_WIDTH}; +use gb::{AudioSenderReceiver, Cycle, Egui, GB_HEIGHT, GB_WIDTH}; use gilrs::Gilrs; use pixels::{Pixels, SurfaceTexture}; +use rodio::{OutputStream, Sink}; use std::time::Instant; use winit::dpi::LogicalSize; use winit::event::{Event, VirtualKeyCode}; use winit::event_loop::{ControlFlow, EventLoop}; +use winit::platform::windows::WindowBuilderExtWindows; use winit::window::{Window, WindowBuilder}; use winit_input_helper::WinitInputHelper; @@ -65,6 +67,19 @@ fn main() -> Result<()> { (pixels, egui) }; + let (send, recv) = AudioSenderReceiver::new(); + game_boy.set_audio_src(send); + + // Initialize Audio + let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio"); + let sink = Sink::try_new(&stream_handle).expect("Initialize Audio Sink"); + + std::thread::spawn(move || { + sink.append(recv); + + sink.sleep_until_end(); + }); + let mut now = Instant::now(); let mut cycle_count: Cycle = Default::default(); @@ -135,5 +150,6 @@ fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result { .with_resizable(true) .with_decorations(true) .with_transparent(false) + .with_drag_and_drop(false) // OleInitialize failed error if this is set to true .build(event_loop)?) } diff --git a/src/sound.rs b/src/sound.rs index 491dff5..7358896 100644 --- a/src/sound.rs +++ b/src/sound.rs @@ -1,8 +1,15 @@ use bitfield::bitfield; +use crossbeam_channel::{Receiver, Sender}; +use rodio::Source; + +use crate::emu::SM83_CLOCK_SPEED; +use crate::Cycle; const WAVE_PATTERN_RAM_LEN: usize = 0x10; +const SAMPLE_RATE: u32 = 4800; // Hz +const SAMPLE_RATE_IN_CYCLES: Cycle = Cycle::new((SM83_CLOCK_SPEED / SAMPLE_RATE as u64) as u32); -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Default)] pub(crate) struct Sound { pub(crate) ctrl: SoundControl, /// Tone & Sweep @@ -17,14 +24,15 @@ pub(crate) struct Sound { // Frame Sequencer frame_seq_state: FrameSequencerState, div_prev: Option, + + sender: Option, + cycle: Cycle, } impl Sound { pub(crate) fn clock(&mut self, div: u16) { use FrameSequencerState::*; - - // Decrement Channel 2 Frequency Timer - let ch2_amplitude = self.ch2.clock(); + self.cycle += 1; // the 5th bit of the high byte let bit_5 = (div >> 13 & 0x01) as u8; @@ -52,6 +60,24 @@ impl Sound { } self.div_prev = Some(bit_5); + + // TODO: Should the FrameSequencer be run first? + if self.cycle > SAMPLE_RATE_IN_CYCLES { + // Sample the APU + self.cycle %= SAMPLE_RATE_IN_CYCLES; + + let left_sample = self.ch2.clock(); + let right_sample = self.ch2.clock(); + + if let Some(send) = self.sender.as_ref() { + send.add_sample(left_sample); + send.add_sample(right_sample); + } + } + } + + pub(crate) fn set_audio_src(&mut self, sender: SampleSender) { + self.sender = Some(sender); } fn handle_length(&mut self) { @@ -1000,3 +1026,62 @@ impl From for u8 { ctrl.0 } } + +pub struct AudioSenderReceiver; + +impl AudioSenderReceiver { + pub fn new() -> (SampleSender, SampleReceiver) { + let (send, recv) = crossbeam_channel::unbounded(); + + (SampleSender { send }, SampleReceiver { recv }) + } +} + +#[derive(Debug, Clone)] +pub struct SampleSender { + send: Sender, +} + +impl SampleSender { + fn add_sample(&self, sample: u8) { + self.send + .send(sample) + .expect("Send audio sample across threads"); + } +} + +pub struct SampleReceiver { + recv: Receiver, +} + +impl Iterator for SampleReceiver { + type Item = f32; + + fn next(&mut self) -> Option { + // TODO: Should this never return none? + self.recv.recv().ok().map(|num| num as f32) + } +} + +impl Source for SampleReceiver { + fn current_frame_len(&self) -> Option { + // A frame changes when the samples rate or + // number of channels change. This will never happen, so + // we return + None + } + + fn channels(&self) -> u16 { + // The Gameboy supports two channels + 2 + } + + fn sample_rate(&self) -> u32 { + SAMPLE_RATE + } + + fn total_duration(&self) -> Option { + // The duration of this source is infinite + None + } +}