feat(snd): implement audio playback using rodio
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Rekai Nyangadzayi Musuka 2021-07-09 01:25:52 -05:00
parent 0fa818a1a6
commit ce630baa5d
8 changed files with 141 additions and 7 deletions

21
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -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();

View File

@ -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
}

View File

@ -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<SM83> {

View File

@ -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;

View File

@ -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<Window> {
.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)?)
}

View File

@ -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<u8>,
sender: Option<SampleSender>,
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<ChannelControl> 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<u8>,
}
impl SampleSender {
fn add_sample(&self, sample: u8) {
self.send
.send(sample)
.expect("Send audio sample across threads");
}
}
pub struct SampleReceiver {
recv: Receiver<u8>,
}
impl Iterator for SampleReceiver {
type Item = f32;
fn next(&mut self) -> Option<Self::Item> {
// 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<usize> {
// 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<std::time::Duration> {
// The duration of this source is infinite
None
}
}