modularise audio output
This commit is contained in:
parent
953964ff34
commit
7f62f15eab
|
@ -1,4 +1,6 @@
|
||||||
pub use crate::processor::memory::mmio::joypad::JoypadState;
|
pub use crate::processor::memory::mmio::joypad::JoypadState;
|
||||||
|
use async_ringbuf::{AsyncHeapConsumer, AsyncHeapProducer, AsyncHeapRb};
|
||||||
|
use futures::executor;
|
||||||
|
|
||||||
pub enum EmulatorMessage {
|
pub enum EmulatorMessage {
|
||||||
Stop,
|
Stop,
|
||||||
|
@ -15,3 +17,27 @@ pub trait Renderer {
|
||||||
|
|
||||||
fn set_rumble(&mut self, _rumbling: bool) {}
|
fn set_rumble(&mut self, _rumbling: bool) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AudioOutput {
|
||||||
|
pub sample_rate: u32,
|
||||||
|
pub send_rb: AsyncHeapProducer<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioOutput {
|
||||||
|
pub fn new(sample_rate: u32) -> (Self, AsyncHeapConsumer<f32>) {
|
||||||
|
let rb_len = sample_rate as usize / 60;
|
||||||
|
|
||||||
|
let rb = AsyncHeapRb::<f32>::new(rb_len);
|
||||||
|
let (mut send_rb, rx) = rb.split();
|
||||||
|
|
||||||
|
executor::block_on(send_rb.push_iter(vec![0.; rb_len - 1].into_iter())).unwrap();
|
||||||
|
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
sample_rate,
|
||||||
|
send_rb,
|
||||||
|
},
|
||||||
|
rx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
12
src/lib.rs
12
src/lib.rs
|
@ -10,7 +10,7 @@ use crate::{
|
||||||
processor::memory::Memory,
|
processor::memory::Memory,
|
||||||
util::{pause, print_cycles},
|
util::{pause, print_cycles},
|
||||||
};
|
};
|
||||||
use connect::{EmulatorMessage, Renderer};
|
use connect::{AudioOutput, EmulatorMessage, Renderer};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use processor::{memory::Rom, Cpu};
|
use processor::{memory::Rom, Cpu};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -51,6 +51,7 @@ pub fn init(
|
||||||
receiver: Receiver<EmulatorMessage>,
|
receiver: Receiver<EmulatorMessage>,
|
||||||
options: Options,
|
options: Options,
|
||||||
mut window: Box<dyn Renderer>,
|
mut window: Box<dyn Renderer>,
|
||||||
|
output: AudioOutput,
|
||||||
tile_window: Option<Box<dyn Renderer>>,
|
tile_window: Option<Box<dyn Renderer>>,
|
||||||
) {
|
) {
|
||||||
VERBOSE.set(options.verbose).unwrap();
|
VERBOSE.set(options.verbose).unwrap();
|
||||||
|
@ -92,7 +93,14 @@ pub fn init(
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cpu = Cpu::new(
|
let mut cpu = Cpu::new(
|
||||||
Memory::init(bootrom, rom, window, options.connect_serial, tile_window),
|
Memory::init(
|
||||||
|
bootrom,
|
||||||
|
rom,
|
||||||
|
window,
|
||||||
|
output,
|
||||||
|
options.connect_serial,
|
||||||
|
tile_window,
|
||||||
|
),
|
||||||
bootrom_enabled,
|
bootrom_enabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
52
src/main.rs
52
src/main.rs
|
@ -3,8 +3,13 @@
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
|
|
||||||
use clap::{ArgGroup, Parser};
|
use clap::{ArgGroup, Parser};
|
||||||
|
use cpal::{
|
||||||
|
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||||
|
Stream,
|
||||||
|
};
|
||||||
|
use futures::executor;
|
||||||
use gb_emu::{
|
use gb_emu::{
|
||||||
connect::{EmulatorMessage, JoypadState, Renderer},
|
connect::{AudioOutput, EmulatorMessage, JoypadState, Renderer},
|
||||||
util::scale_buffer,
|
util::scale_buffer,
|
||||||
};
|
};
|
||||||
use gilrs::{
|
use gilrs::{
|
||||||
|
@ -90,14 +95,59 @@ fn main() {
|
||||||
|
|
||||||
ctrlc::set_handler(move || sender.send(EmulatorMessage::Stop).unwrap()).unwrap();
|
ctrlc::set_handler(move || sender.send(EmulatorMessage::Stop).unwrap()).unwrap();
|
||||||
|
|
||||||
|
let (output, _stream) = create_audio_output();
|
||||||
|
|
||||||
gb_emu::init(
|
gb_emu::init(
|
||||||
receiver,
|
receiver,
|
||||||
options,
|
options,
|
||||||
Box::new(WindowRenderer::new(factor, Some(Gilrs::new().unwrap()))),
|
Box::new(WindowRenderer::new(factor, Some(Gilrs::new().unwrap()))),
|
||||||
|
output,
|
||||||
tile_window,
|
tile_window,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_audio_output() -> (AudioOutput, Stream) {
|
||||||
|
let host = cpal::default_host();
|
||||||
|
|
||||||
|
let device = host
|
||||||
|
.default_output_device()
|
||||||
|
.expect("no output device available");
|
||||||
|
|
||||||
|
let mut supported_configs_range = device
|
||||||
|
.supported_output_configs()
|
||||||
|
.expect("error while querying configs");
|
||||||
|
let config = supported_configs_range
|
||||||
|
.next()
|
||||||
|
.expect("no supported config?!")
|
||||||
|
.with_max_sample_rate();
|
||||||
|
|
||||||
|
let sample_rate = config.sample_rate().0;
|
||||||
|
|
||||||
|
let (output, mut rx) = AudioOutput::new(sample_rate);
|
||||||
|
|
||||||
|
let stream = device
|
||||||
|
.build_output_stream(
|
||||||
|
&config.config(),
|
||||||
|
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
||||||
|
for v in data {
|
||||||
|
match executor::block_on(rx.pop()) {
|
||||||
|
Some(a) => *v = a,
|
||||||
|
None => panic!("Audio queue disconnected!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |err| {
|
||||||
|
// react to errors here.
|
||||||
|
println!("audio error: {err}");
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
stream.play().unwrap();
|
||||||
|
|
||||||
|
(output, stream)
|
||||||
|
}
|
||||||
|
|
||||||
struct WindowRenderer {
|
struct WindowRenderer {
|
||||||
window: Option<Window>,
|
window: Option<Window>,
|
||||||
scaled_buf: Vec<u32>,
|
scaled_buf: Vec<u32>,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use self::mmio::{Apu, Gpu, Joypad, Serial, Timer};
|
use self::mmio::{Apu, Gpu, Joypad, Serial, Timer};
|
||||||
pub use self::rom::Rom;
|
pub use self::rom::Rom;
|
||||||
use crate::{
|
use crate::{
|
||||||
connect::{JoypadState, Renderer},
|
connect::{AudioOutput, JoypadState, Renderer},
|
||||||
processor::SplitRegister,
|
processor::SplitRegister,
|
||||||
verbose_println, Cpu,
|
verbose_println, Cpu,
|
||||||
};
|
};
|
||||||
|
@ -34,6 +34,7 @@ impl Memory {
|
||||||
bootrom: Option<Vec<u8>>,
|
bootrom: Option<Vec<u8>>,
|
||||||
rom: Rom,
|
rom: Rom,
|
||||||
window: Box<dyn Renderer>,
|
window: Box<dyn Renderer>,
|
||||||
|
output: AudioOutput,
|
||||||
connect_serial: bool,
|
connect_serial: bool,
|
||||||
tile_window: Option<Box<dyn Renderer>>,
|
tile_window: Option<Box<dyn Renderer>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -53,7 +54,7 @@ impl Memory {
|
||||||
dma_addr: 0xFF,
|
dma_addr: 0xFF,
|
||||||
joypad: Joypad::default(),
|
joypad: Joypad::default(),
|
||||||
gpu: Gpu::new(window, tile_window),
|
gpu: Gpu::new(window, tile_window),
|
||||||
apu: Apu::init_default(),
|
apu: Apu::new(output),
|
||||||
serial,
|
serial,
|
||||||
timers: Timer::init(),
|
timers: Timer::init(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
use self::types::{Channels, DacSample, Mixer, VinEnable, Volume};
|
use self::types::{Channels, DacSample, Mixer, VinEnable, Volume};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
connect::AudioOutput,
|
||||||
constants::CLOCK_SPEED,
|
constants::CLOCK_SPEED,
|
||||||
processor::memory::Address,
|
processor::memory::Address,
|
||||||
util::{get_bit, set_or_clear_bit},
|
util::{get_bit, set_or_clear_bit},
|
||||||
};
|
};
|
||||||
use async_ringbuf::{AsyncHeapProducer, AsyncHeapRb};
|
|
||||||
use cpal::{
|
|
||||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
|
||||||
Device, Stream, StreamConfig,
|
|
||||||
};
|
|
||||||
use futures::executor;
|
use futures::executor;
|
||||||
use itertools::izip;
|
use itertools::izip;
|
||||||
use samplerate::{ConverterType, Samplerate};
|
use samplerate::{ConverterType, Samplerate};
|
||||||
|
@ -44,39 +40,22 @@ pub struct Apu {
|
||||||
mixer: Mixer,
|
mixer: Mixer,
|
||||||
div_apu: u8,
|
div_apu: u8,
|
||||||
buffer: Vec<DacSample>,
|
buffer: Vec<DacSample>,
|
||||||
device: Device,
|
|
||||||
config: StreamConfig,
|
|
||||||
stream: Option<Stream>,
|
|
||||||
send_rb: Option<AsyncHeapProducer<f32>>,
|
|
||||||
converter: Samplerate,
|
converter: Samplerate,
|
||||||
|
output: AudioOutput,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Apu {
|
const CYCLES_PER_FRAME: usize = 70224;
|
||||||
fn default() -> Self {
|
|
||||||
let host = cpal::default_host();
|
|
||||||
|
|
||||||
let device = host
|
|
||||||
.default_output_device()
|
|
||||||
.expect("no output device available");
|
|
||||||
|
|
||||||
let mut supported_configs_range = device
|
|
||||||
.supported_output_configs()
|
|
||||||
.expect("error while querying configs");
|
|
||||||
let config = supported_configs_range
|
|
||||||
.next()
|
|
||||||
.expect("no supported config?!")
|
|
||||||
.with_max_sample_rate();
|
|
||||||
|
|
||||||
|
impl Apu {
|
||||||
|
pub fn new(output: AudioOutput) -> Self {
|
||||||
let converter = Samplerate::new(
|
let converter = Samplerate::new(
|
||||||
ConverterType::ZeroOrderHold,
|
ConverterType::ZeroOrderHold,
|
||||||
CLOCK_SPEED as u32,
|
CLOCK_SPEED as u32,
|
||||||
config.sample_rate().0,
|
output.sample_rate,
|
||||||
config.channels().into(),
|
2,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let config = config.config();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
apu_enable: true,
|
apu_enable: true,
|
||||||
channels: Channels::default(),
|
channels: Channels::default(),
|
||||||
|
@ -84,57 +63,10 @@ impl Default for Apu {
|
||||||
mixer: Mixer::default(),
|
mixer: Mixer::default(),
|
||||||
div_apu: 0,
|
div_apu: 0,
|
||||||
buffer: vec![],
|
buffer: vec![],
|
||||||
device,
|
|
||||||
config,
|
|
||||||
stream: None,
|
|
||||||
send_rb: None,
|
|
||||||
converter,
|
converter,
|
||||||
|
output,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const CYCLES_PER_FRAME: usize = 70224;
|
|
||||||
|
|
||||||
impl Apu {
|
|
||||||
pub fn init_default() -> Self {
|
|
||||||
let mut r = Self::default();
|
|
||||||
r.init();
|
|
||||||
r
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(&mut self) {
|
|
||||||
let sample_rate = self.config.sample_rate.0;
|
|
||||||
let rb_len = sample_rate as usize / 60;
|
|
||||||
|
|
||||||
let rb = AsyncHeapRb::<f32>::new(rb_len);
|
|
||||||
let (mut tx, mut rx) = rb.split();
|
|
||||||
|
|
||||||
executor::block_on(tx.push_iter(vec![0.; rb_len - 1].into_iter())).unwrap();
|
|
||||||
|
|
||||||
self.send_rb = Some(tx);
|
|
||||||
|
|
||||||
let stream = self
|
|
||||||
.device
|
|
||||||
.build_output_stream(
|
|
||||||
&self.config,
|
|
||||||
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
|
||||||
for v in data {
|
|
||||||
match executor::block_on(rx.pop()) {
|
|
||||||
Some(a) => *v = a,
|
|
||||||
None => panic!("Audio queue disconnected!"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
move |err| {
|
|
||||||
// react to errors here.
|
|
||||||
println!("audio error: {err}");
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
stream.play().unwrap();
|
|
||||||
self.stream = Some(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn div_apu_tick(&mut self) {
|
pub fn div_apu_tick(&mut self) {
|
||||||
self.div_apu = self.div_apu.wrapping_add(1);
|
self.div_apu = self.div_apu.wrapping_add(1);
|
||||||
|
@ -179,7 +111,6 @@ impl Apu {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_audio(&mut self) {
|
fn next_audio(&mut self) {
|
||||||
if let Some(ref mut send) = self.send_rb {
|
|
||||||
let converted = self
|
let converted = self
|
||||||
.converter
|
.converter
|
||||||
.process(
|
.process(
|
||||||
|
@ -190,8 +121,7 @@ impl Apu {
|
||||||
.collect::<Vec<f32>>(),
|
.collect::<Vec<f32>>(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
executor::block_on(send.push_slice(&converted)).unwrap();
|
executor::block_on(self.output.send_rb.push_slice(&converted)).unwrap();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_register(&self, addr: Address) -> u8 {
|
pub fn get_register(&self, addr: Address) -> u8 {
|
||||||
|
|
Loading…
Reference in a new issue