feat(snd): implement audio playback using rodio
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
0fa818a1a6
commit
ce630baa5d
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
18
src/main.rs
18
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<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)?)
|
||||
}
|
||||
|
|
93
src/sound.rs
93
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<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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue