From 39068fec2a620c36d7ce1e947fb01459bec51b15 Mon Sep 17 00:00:00 2001 From: transistor Date: Sun, 12 Dec 2021 15:20:09 -0800 Subject: [PATCH] Added audio support It's better than it was but there are still minor drop outs due to a buffer underrun I think (could be other timing issues related to the update loop or something else). Right now, the audio chips just have some code to produce sine waves for testing. --- frontends/moa-common/Cargo.toml | 2 + frontends/moa-common/src/audio.rs | 375 ++++++++++++++++++++++++++++++ frontends/moa-common/src/lib.rs | 4 + frontends/moa-minifb/Cargo.toml | 1 + frontends/moa-minifb/src/lib.rs | 19 +- src/host/audio.rs | 59 +++++ src/host/gfx.rs | 2 +- src/host/mod.rs | 4 +- src/host/traits.rs | 13 +- src/machines/genesis.rs | 15 +- src/peripherals/sn76489.rs | 89 +++++-- src/peripherals/ym2612.rs | 108 ++++++++- todo.txt | 39 ++-- 13 files changed, 663 insertions(+), 67 deletions(-) create mode 100644 frontends/moa-common/src/audio.rs create mode 100644 src/host/audio.rs diff --git a/frontends/moa-common/Cargo.toml b/frontends/moa-common/Cargo.toml index 35f26f6..2471792 100644 --- a/frontends/moa-common/Cargo.toml +++ b/frontends/moa-common/Cargo.toml @@ -5,8 +5,10 @@ edition = "2018" [features] tty = ["nix"] +audio = ["cpal"] [dependencies] moa = { path = "../../" } nix = { version = "0.23", optional = true } +cpal = { version = "0.13", optional = true } diff --git a/frontends/moa-common/src/audio.rs b/frontends/moa-common/src/audio.rs new file mode 100644 index 0000000..c1dae1d --- /dev/null +++ b/frontends/moa-common/src/audio.rs @@ -0,0 +1,375 @@ + +use moa::host::traits::{HostData, Audio}; +use cpal::{Data, Sample, Stream, SampleRate, SampleFormat, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}}; + + +const SAMPLE_RATE: usize = 48000; + + +#[derive(Clone)] +pub struct CircularBuffer { + pub inp: usize, + pub out: usize, + pub init: T, + pub buffer: Vec, +} + +impl CircularBuffer { + pub fn new(size: usize, init: T) -> Self { + Self { + inp: 0, + out: 0, + init, + buffer: vec![init; size], + } + } + + pub fn len(&self) -> usize { + self.buffer.len() + } + + pub fn clear(&mut self) { + self.inp = 0; + self.out = 0; + } + + pub fn resize(&mut self, newlen: usize) { + if self.buffer.len() != newlen { + self.buffer = vec![self.init; newlen]; + self.clear(); + } + } + + pub fn insert(&mut self, item: T) { + let next = self.next_in(); + if next != self.out { + self.buffer[self.inp] = item; + self.inp = next; + } + } + + pub fn drop_next(&mut self, mut count: usize) { + let avail = self.used_space(); + if count > avail { + count = avail; + } + + self.out += count; + if self.out >= self.buffer.len() { + self.out -= self.buffer.len(); + } + } + + pub fn is_full(&self) -> bool { + self.next_in() == self.out + } + + pub fn used_space(&self) -> usize { + if self.inp >= self.out { + self.inp - self.out + } else { + self.buffer.len() - self.out + self.inp + } + } + + fn next_in(&self) -> usize { + if self.inp + 1 < self.buffer.len() { + self.inp + 1 + } else { + 0 + } + } +} + +impl Iterator for CircularBuffer { + type Item = T; + + fn next(&mut self) -> Option { + if self.out == self.inp { + None + } else { + let value = self.buffer[self.out]; + self.out += 1; + if self.out >= self.buffer.len() { + self.out = 0; + } + Some(value) + } + } +} + + +pub struct AudioSource { + sample_rate: usize, + frame_size: usize, + sequence_num: usize, + mixer: HostData, + buffer: CircularBuffer, +} + +impl AudioSource { + pub fn new(mixer: HostData) -> Self { + let sample_rate = mixer.lock().sample_rate(); + let frame_size = mixer.lock().frame_size(); + let buffer = CircularBuffer::new(frame_size * 2, 0.0); + + Self { + sample_rate, + frame_size, + sequence_num: 0, + mixer, + buffer, + } + } + + pub fn fill_with(&mut self, samples: usize, iter: &mut Iterator) { + for i in 0..samples { + let sample = 0.25 * iter.next().unwrap(); + self.buffer.insert(sample); + self.buffer.insert(sample); + if self.buffer.is_full() { + break; + } + } + + if self.buffer.used_space() >= self.frame_size { + let mut locked_mixer = self.mixer.lock(); + + let mixer_sequence_num = locked_mixer.sequence_num(); + if mixer_sequence_num == self.sequence_num { + return; + } + self.sequence_num = mixer_sequence_num; + + for i in 0..locked_mixer.buffer.len() { + locked_mixer.buffer[i] += self.buffer.next().unwrap_or(0.0); + } + + self.frame_size = locked_mixer.frame_size(); + self.buffer.resize(self.frame_size * 2); + } + } +} + +impl Audio for AudioSource { + fn samples_per_second(&self) -> usize { + self.sample_rate + } + + fn write_samples(&mut self, samples: usize, iter: &mut Iterator) { + self.fill_with(samples, iter); + } +} + + +#[derive(Clone)] +pub struct AudioMixer { + sample_rate: usize, + //buffer: CircularBuffer, + buffer: Vec, + sequence_num: usize, +} + +impl AudioMixer { + pub fn new(sample_rate: usize) -> HostData { + HostData::new(AudioMixer { + sample_rate, + //buffer: CircularBuffer::new(1280 * 2, 0.0), + buffer: vec![0.0; 1280 * 2], + sequence_num: 0, + }) + } + + pub fn new_default() -> HostData { + AudioMixer::new(SAMPLE_RATE) + } + + pub fn sample_rate(&self) -> usize { + self.sample_rate + } + + pub fn frame_size(&self) -> usize { + self.buffer.len() + } + + pub fn sequence_num(&self) -> usize { + self.sequence_num + } + + pub fn resize_frame(&mut self, newlen: usize) { + if self.buffer.len() != newlen { + self.buffer = vec![0.0; newlen]; + } + } + + pub fn assembly_frame(&mut self, data: &mut [f32]) { + self.resize_frame(data.len()); + for i in 0..data.len() { + data[i] = Sample::from(&self.buffer[i]); + self.buffer[i] = 0.0; + } + self.sequence_num = self.sequence_num.wrapping_add(1); + +/* + let mut buffer = vec![0.0; data.len()]; + + for source in &self.sources { + let mut locked_source = source.lock(); + // TODO these are quick hacks to delay or shrink the buffer if it's too small or big + if locked_source.used_space() < data.len() { + continue; + } + let excess = locked_source.used_space() - (data.len() * 2); + if excess > 0 { + locked_source.drop_next(excess); + } + + for addr in buffer.iter_mut() { + *addr += locked_source.next().unwrap_or(0.0); + } + } + + for i in 0..data.len() { + let sample = buffer[i] / self.sources.len() as f32; + data[i] = Sample::from(&sample); + } +*/ + +/* + let mut locked_source = self.sources[1].lock(); + for i in 0..data.len() { + let sample = locked_source.next().unwrap_or(0.0); + data[i] = Sample::from(&sample); + } +*/ + } + + // TODO you need a way to add data to the mixer... the question is do you need to keep track of real time + // If you have a counter that calculates the amount of time until the next sample based on the size of + // the buffer given to the data_callback, then when submitting data, the audio sources can know that they + // the next place to write to is a given position in the mixer buffer (maybe not the start of the buffer). + + // But what do you do if there needs to be some skipping. If the source is generating data in 1 to 10 ms + // chunks according to simulated time, there might be a case where it tries to write too much data because + // it's running fast. (If it's running slow, you can insert silence) +} + + +pub struct AudioOutput { + stream: Stream, + mixer: HostData, +} + +impl AudioOutput { + pub fn create_audio_output(mixer: HostData) -> AudioOutput { + let device = cpal::default_host() + .default_output_device() + .expect("No sound output device available"); + + let config: StreamConfig = device + .supported_output_configs() + .expect("error while querying configs") + .find(|config| config.sample_format() == SampleFormat::F32 && config.channels() == 2) + .expect("no supported config?!") + .with_sample_rate(SampleRate(SAMPLE_RATE as u32)) + .into(); + + let channels = config.channels as usize; + + let data_callback = { + let mixer = mixer.clone(); + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + mixer.lock().assembly_frame(data); + +/* + let mut locked_mixer = mixer.lock(); + //println!(">>> {} into {}", locked_mixer.buffer.used_space(), data.len()); + + // TODO these are quick hacks to delay or shrink the buffer if it's too small or big + if locked_mixer.buffer.used_space() < data.len() { + return; + } + if locked_mixer.buffer.used_space() > data.len() * 2 { + for _ in 0..(locked_mixer.buffer.used_space() - (data.len() * 2)) { + locked_mixer.buffer.next(); + } + } + + for addr in data.iter_mut() { + let sample = locked_mixer.buffer.next().unwrap_or(0.0); + *addr = Sample::from(&sample); + } + //locked_mixer.buffer.clear(); +*/ + } + }; + + let stream = device.build_output_stream( + &config, + data_callback, + move |err| { + // react to errors here. + println!("ERROR"); + }, + ).unwrap(); + + stream.play().unwrap(); + + AudioOutput { + stream, + mixer, + } + } + + + /* + pub fn create_audio_output2(mut updater: Box) -> AudioOutput { + let device = cpal::default_host() + .default_output_device() + .expect("No sound output device available"); + + let config: StreamConfig = device + .supported_output_configs() + .expect("error while querying configs") + .find(|config| config.sample_format() == SampleFormat::F32 && config.channels() == 2) + .expect("no supported config?!") + .with_sample_rate(SampleRate(SAMPLE_RATE as u32)) + .into(); + + let channels = config.channels as usize; + let mixer = AudioMixer::new(SAMPLE_RATE); + + let data_callback = { + let mixer = mixer.clone(); + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let samples = data.len() / 2; + let mut buffer = vec![0.0; samples]; + updater.update_audio_frame(samples, mixer.lock().sample_rate(), &mut buffer); + + for (i, channels) in data.chunks_mut(2).enumerate() { + let sample = Sample::from(&buffer[i]); + channels[0] = sample; + channels[1] = sample; + } + } + }; + + let stream = device.build_output_stream( + &config, + data_callback, + move |err| { + // react to errors here. + println!("ERROR"); + }, + ).unwrap(); + + stream.play().unwrap(); + + AudioOutput { + stream, + mixer, + } + } + */ +} + diff --git a/frontends/moa-common/src/lib.rs b/frontends/moa-common/src/lib.rs index 72fdffd..d7491ff 100644 --- a/frontends/moa-common/src/lib.rs +++ b/frontends/moa-common/src/lib.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "tty")] pub mod tty; +#[cfg(feature = "audio")] +pub mod audio; + diff --git a/frontends/moa-minifb/Cargo.toml b/frontends/moa-minifb/Cargo.toml index 1aa10eb..59c3cde 100644 --- a/frontends/moa-minifb/Cargo.toml +++ b/frontends/moa-minifb/Cargo.toml @@ -8,6 +8,7 @@ default-run = "moa-genesis" [dependencies] moa = { path = "../../" } +moa-common = { path = "../moa-common/", features = ["audio"] } minifb = "0.19" clap = "3.0.0-beta.5" diff --git a/frontends/moa-minifb/src/lib.rs b/frontends/moa-minifb/src/lib.rs index 50fbab9..be3689b 100644 --- a/frontends/moa-minifb/src/lib.rs +++ b/frontends/moa-minifb/src/lib.rs @@ -8,9 +8,11 @@ use clap::{App, ArgMatches}; use moa::error::Error; use moa::system::System; -use moa::host::traits::{Host, ControllerUpdater, KeyboardUpdater, WindowUpdater}; +use moa::host::traits::{Host, HostData, ControllerUpdater, KeyboardUpdater, WindowUpdater, Audio}; use moa::host::controllers::{ControllerDevice, ControllerEvent}; +use moa_common::audio::{AudioOutput, AudioMixer, AudioSource}; + mod keys; mod controllers; @@ -77,6 +79,7 @@ pub struct MiniFrontendBuilder { pub window: Option>, pub controller: Option>, pub keyboard: Option>, + pub mixer: Option>, pub finalized: bool, } @@ -86,6 +89,7 @@ impl MiniFrontendBuilder { window: None, controller: None, keyboard: None, + mixer: Some(AudioMixer::new_default()), finalized: false, } } @@ -98,7 +102,8 @@ impl MiniFrontendBuilder { let window = std::mem::take(&mut self.window); let controller = std::mem::take(&mut self.controller); let keyboard = std::mem::take(&mut self.keyboard); - MiniFrontend::new(window, controller, keyboard) + let mixer = std::mem::take(&mut self.mixer); + MiniFrontend::new(window, controller, keyboard, mixer.unwrap()) } } @@ -130,6 +135,11 @@ impl Host for MiniFrontendBuilder { self.keyboard = Some(input); Ok(()) } + + fn create_audio_source(&mut self) -> Result, Error> { + let source = AudioSource::new(self.mixer.as_ref().unwrap().clone()); + Ok(Box::new(source)) + } } @@ -139,16 +149,18 @@ pub struct MiniFrontend { pub window: Option>, pub controller: Option>, pub keyboard: Option>, + pub audio: AudioOutput, } impl MiniFrontend { - pub fn new(window: Option>, controller: Option>, keyboard: Option>) -> Self { + pub fn new(window: Option>, controller: Option>, keyboard: Option>, mixer: HostData) -> Self { Self { buffer: vec![0; (WIDTH * HEIGHT) as usize], modifiers: 0, window, controller, keyboard, + audio: AudioOutput::create_audio_output(mixer), } } @@ -187,6 +199,7 @@ impl MiniFrontend { while window.is_open() && !window.is_key_down(Key::Escape) { if let Some(system) = system.as_mut() { system.run_for(16_600_000).unwrap(); + //system.run_until_break().unwrap(); } if let Some(keys) = window.get_keys_pressed(minifb::KeyRepeat::No) { diff --git a/src/host/audio.rs b/src/host/audio.rs new file mode 100644 index 0000000..8ac69e0 --- /dev/null +++ b/src/host/audio.rs @@ -0,0 +1,59 @@ + +use std::f32::consts::PI; + + +#[derive(Clone)] +pub struct SineWave { + pub frequency: f32, + pub sample_rate: usize, + pub position: usize, +} + +impl SineWave { + pub fn new(frequency: f32, sample_rate: usize) -> Self { + Self { + frequency, + sample_rate, + position: 0, + } + } +} + +impl Iterator for SineWave { + type Item = f32; + + fn next(&mut self) -> Option { + self.position += 1; + let result = (2.0 * PI * self.frequency * self.position as f32 / (self.sample_rate as f32)).sin(); + Some(result) + } +} + +#[derive(Clone)] +pub struct SquareWave { + pub frequency: f32, + pub sample_rate: usize, + pub position: usize, +} + +impl SquareWave { + pub fn new(frequency: f32, sample_rate: usize) -> Self { + Self { + frequency, + sample_rate, + position: 0, + } + } +} + +impl Iterator for SquareWave { + type Item = f32; + + fn next(&mut self) -> Option { + self.position += 1; + let samples_per_hz = self.sample_rate as f32 / self.frequency; + let result = if (self.position as f32 % samples_per_hz) < (samples_per_hz / 2.0) { 1.0 } else { -1.0 }; + Some(result) + } +} + diff --git a/src/host/gfx.rs b/src/host/gfx.rs index b5d71c9..2dabc80 100644 --- a/src/host/gfx.rs +++ b/src/host/gfx.rs @@ -57,7 +57,7 @@ pub struct FrameUpdateWrapper(Arc>); impl WindowUpdater for FrameUpdateWrapper { fn get_size(&mut self) -> (u32, u32) { - match self.0.lock() { + match self.0.lock() { Ok(frame) => (frame.width, frame.height), _ => (0, 0), } diff --git a/src/host/mod.rs b/src/host/mod.rs index 3c56048..475cd2f 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -1,10 +1,8 @@ pub mod traits; -#[cfg(feature = "tty")] -pub mod tty; - pub mod gfx; +pub mod audio; pub mod keys; pub mod controllers; diff --git a/src/host/traits.rs b/src/host/traits.rs index 046e795..f1fcc3f 100644 --- a/src/host/traits.rs +++ b/src/host/traits.rs @@ -10,7 +10,7 @@ pub trait Host { Err(Error::new("This frontend doesn't support PTYs")) } - fn add_window(&mut self, updater: Box) -> Result<(), Error> { + fn add_window(&mut self, _updater: Box) -> Result<(), Error> { Err(Error::new("This frontend doesn't support windows")) } @@ -21,8 +21,13 @@ pub trait Host { fn register_keyboard(&mut self, _input: Box) -> Result<(), Error> { Err(Error::new("This frontend doesn't support the keyboard")) } + + fn create_audio_source(&mut self) -> Result, Error> { + Err(Error::new("This frontend doesn't support the sound")) + } } + pub trait Tty { fn device_name(&self) -> String; fn read(&mut self) -> Option; @@ -32,6 +37,7 @@ pub trait Tty { pub trait WindowUpdater: Send { fn get_size(&mut self) -> (u32, u32); fn update_frame(&mut self, width: u32, height: u32, bitmap: &mut [u32]); + //fn update_frame(&mut self, draw_buffer: &mut dyn FnMut(u32, u32, &[u32])); } pub trait ControllerUpdater: Send { @@ -42,6 +48,11 @@ pub trait KeyboardUpdater: Send { fn update_keyboard(&mut self, key: Key, state: bool); } +pub trait Audio { + fn samples_per_second(&self) -> usize; + fn write_samples(&mut self, samples: usize, iter: &mut Iterator); +} + pub trait BlitableSurface { fn set_size(&mut self, width: u32, height: u32); fn blit>(&mut self, pos_x: u32, pos_y: u32, bitmap: B, width: u32, height: u32); diff --git a/src/machines/genesis.rs b/src/machines/genesis.rs index b85e5fe..009cd61 100644 --- a/src/machines/genesis.rs +++ b/src/machines/genesis.rs @@ -10,8 +10,8 @@ use crate::devices::{wrap_transmutable, Address, Addressable, Debuggable}; use crate::cpus::m68k::{M68k, M68kType}; use crate::cpus::z80::{Z80, Z80Type}; -use crate::peripherals::ym2612::YM2612; -use crate::peripherals::sn76489::SN76489; +use crate::peripherals::ym2612::Ym2612; +use crate::peripherals::sn76489::Sn76489; use crate::peripherals::genesis; use crate::peripherals::genesis::coprocessor::{CoprocessorBankRegister, CoprocessorBankArea}; @@ -63,8 +63,8 @@ pub fn build_genesis(host: &mut H, options: SegaGenesisOptions) -> Resu // Build the Coprocessor's Bus let bank_register = Signal::new(0); let coproc_ram = wrap_transmutable(MemoryBlock::new(vec![0; 0x00002000])); - let coproc_ym_sound = wrap_transmutable(YM2612::new()); - let coproc_sn_sound = wrap_transmutable(SN76489::new()); + let coproc_ym_sound = wrap_transmutable(Ym2612::create(host)?); + let coproc_sn_sound = wrap_transmutable(Sn76489::create(host)?); let coproc_register = wrap_transmutable(CoprocessorBankRegister::new(bank_register.clone())); let coproc_area = wrap_transmutable(CoprocessorBankArea::new(bank_register, system.bus.clone())); @@ -75,8 +75,10 @@ pub fn build_genesis(host: &mut H, options: SegaGenesisOptions) -> Resu coproc_bus.borrow_mut().insert(0x7f11, coproc_sn_sound.clone()); coproc_bus.borrow_mut().insert(0x8000, coproc_area); let coproc = Z80::new(Z80Type::Z80, 3_579_545, BusPort::new(0, 16, 8, coproc_bus.clone())); - let reset = coproc.reset.clone(); - let bus_request = coproc.bus_request.clone(); + let mut reset = coproc.reset.clone(); + let mut bus_request = coproc.bus_request.clone(); + reset.set(true); + bus_request.set(true); // Add coprocessor devices to the system bus so the 68000 can access them too system.add_addressable_device(0x00a00000, coproc_ram)?; @@ -86,7 +88,6 @@ pub fn build_genesis(host: &mut H, options: SegaGenesisOptions) -> Resu system.add_device("coproc", wrap_transmutable(coproc))?; - let controllers = genesis::controllers::GenesisController::create(host)?; let interrupt = controllers.get_interrupt_signal(); system.add_addressable_device(0x00a10000, wrap_transmutable(controllers)).unwrap(); diff --git a/src/peripherals/sn76489.rs b/src/peripherals/sn76489.rs index a2503f4..5535c2f 100644 --- a/src/peripherals/sn76489.rs +++ b/src/peripherals/sn76489.rs @@ -2,56 +2,103 @@ use crate::error::Error; use crate::system::System; use crate::devices::{ClockElapsed, Address, Addressable, Steppable, Transmutable}; +use crate::host::audio::{SineWave, SquareWave}; +use crate::host::traits::{Host, Audio}; + const DEV_NAME: &'static str = "sn76489"; -pub struct SN76489 { +/* +pub struct Sn76489Updater(HostData); +impl AudioUpdater for Sn76489Updater { + fn update_audio_frame(&mut self, samples: usize, sample_rate: usize, buffer: &mut [f32]) { + let mut sine = self.0.lock(); + //for i in 0..samples { + // buffer[i] = sine.next().unwrap(); + //} + } +} +*/ + + +pub struct Sn76489 { + pub regs: [u8; 8], + pub first_byte: Option, + pub source: Box, + pub sine: SquareWave, } -impl SN76489 { - pub fn new() -> Self { - Self { +impl Sn76489 { + pub fn create(host: &mut H) -> Result { + let source = host.create_audio_source()?; + let sine = SquareWave::new(600.0, source.samples_per_second()); - } + Ok(Self { + regs: [0; 8], + first_byte: None, + source, + sine, + }) } } -impl Addressable for SN76489 { +impl Steppable for Sn76489 { + fn step(&mut self, _system: &System) -> Result { + // TODO since you expect this step function to be called every 1ms of simulated time + // you could assume that you should produce (sample_rate / 1000) samples + + if self.sine.frequency > 200.0 { + self.sine.frequency -= 1.0; + } + + let rate = self.source.samples_per_second(); + self.source.write_samples(rate / 1000, &mut self.sine); + //println!("{}", self.sine.frequency); + Ok(1_000_000) // Every 1ms of simulated time + } +} + +impl Addressable for Sn76489 { fn len(&self) -> usize { 0x01 } fn read(&mut self, addr: Address, data: &mut [u8]) -> Result<(), Error> { - match addr { - _ => { - warning!("{}: !!! unhandled read from {:0x}", DEV_NAME, addr); - }, - } - debug!("{}: read from register {:x} of {:?}", DEV_NAME, addr, data); + warning!("{}: !!! device can't be read", DEV_NAME); Ok(()) } fn write(&mut self, addr: Address, data: &[u8]) -> Result<(), Error> { - debug!("{}: write to register {:x} with {:x}", DEV_NAME, addr, data[0]); - match addr { - _ => { - warning!("{}: !!! unhandled write {:0x} to {:0x}", DEV_NAME, data[0], addr); - }, + if addr != 0 { + warning!("{}: !!! unhandled write {:0x} to {:0x}", DEV_NAME, data[0], addr); + return Ok(()); } + + if (data[0] & 0x80) == 0 { + // TODO update noise byte + } else { + let reg = (data[0] & 0x70) >> 4; + if reg == 6 { + self.first_byte = Some(data[0]); + } else { + self.regs[reg as usize] = data[0] & 0x0F; + } + } + debug!("{}: write to register {:x} with {:x}", DEV_NAME, addr, data[0]); Ok(()) } } -impl Transmutable for SN76489 { +impl Transmutable for Sn76489 { fn as_addressable(&mut self) -> Option<&mut dyn Addressable> { Some(self) } - //fn as_steppable(&mut self) -> Option<&mut dyn Steppable> { - // Some(self) - //} + fn as_steppable(&mut self) -> Option<&mut dyn Steppable> { + Some(self) + } } diff --git a/src/peripherals/ym2612.rs b/src/peripherals/ym2612.rs index 9fadc0a..6e548bf 100644 --- a/src/peripherals/ym2612.rs +++ b/src/peripherals/ym2612.rs @@ -1,29 +1,106 @@ +use std::num::NonZeroU8; + use crate::error::Error; use crate::system::System; use crate::devices::{ClockElapsed, Address, Addressable, Steppable, Transmutable}; +use crate::host::audio::{SineWave, SquareWave}; +use crate::host::traits::{Host, Audio}; const DEV_NAME: &'static str = "ym2612"; -pub struct YM2612 { - +#[derive(Clone)] +pub struct Operator { + pub wave: SquareWave, } -impl YM2612 { - pub fn new() -> Self { +impl Operator { + pub fn new(sample_rate: usize) -> Self { Self { - + wave: SquareWave::new(400.0, sample_rate) } } } -impl Addressable for YM2612 { +#[derive(Clone)] +pub struct Channel { + pub operators: Vec, + pub on: u8, +} + +impl Channel { + pub fn new(sample_rate: usize) -> Self { + Self { + operators: vec![Operator::new(sample_rate); 4], + on: 0, + } + } +} + + + +pub struct Ym2612 { + pub source: Box, + pub selected_reg: Option, + + pub channels: Vec, +} + +impl Ym2612 { + pub fn create(host: &mut H) -> Result { + let source = host.create_audio_source()?; + let sample_rate = source.samples_per_second(); + Ok(Self { + source, + selected_reg: None, + channels: vec![Channel::new(sample_rate); 6], + }) + } + + pub fn set_register(&mut self, bank: u8, reg: usize, data: u8) { + match reg { + 0x28 => { + let ch = (data as usize) & 0x07; + self.channels[ch].on = data >> 4; + println!("Note: {}: {:x}", ch, self.channels[ch].on); + }, + _ => warning!("{}: !!! unhandled write to register {:0x} with {:0x}", DEV_NAME, reg, data), + } + } +} + +impl Steppable for Ym2612 { + fn step(&mut self, _system: &System) -> Result { + // TODO since you expect this step function to be called every 1ms of simulated time + // you could assume that you should produce (sample_rate / 1000) samples + + //if self.sine.frequency < 2000.0 { + // self.sine.frequency += 1.0; + //} + + //let rate = self.source.samples_per_second(); + //self.source.write_samples(rate / 1000, &mut self.sine); + //println!("{}", self.sine.frequency); + + //if self.on { + // let rate = self.source.samples_per_second(); + // self.source.write_samples(rate / 1000, &mut self.sine); + //} + Ok(1_000_000) // Every 1ms of simulated time + } +} + +impl Addressable for Ym2612 { fn len(&self) -> usize { 0x04 } fn read(&mut self, addr: Address, data: &mut [u8]) -> Result<(), Error> { match addr { + 0 | 1 | 2 | 3 => { + // Read the status byte (busy/overflow) + data[0] = 0; + } _ => { warning!("{}: !!! unhandled read from {:0x}", DEV_NAME, addr); }, @@ -35,6 +112,15 @@ impl Addressable for YM2612 { fn write(&mut self, addr: Address, data: &[u8]) -> Result<(), Error> { debug!("{}: write to register {:x} with {:x}", DEV_NAME, addr, data[0]); match addr { + 0 => { + self.selected_reg = NonZeroU8::new(data[0]); + }, + 1 => { + match self.selected_reg { + None => {}, + Some(reg) => self.set_register(0, reg.get() as usize, data[0]), + } + }, _ => { warning!("{}: !!! unhandled write {:0x} to {:0x}", DEV_NAME, data[0], addr); }, @@ -43,15 +129,13 @@ impl Addressable for YM2612 { } } - -impl Transmutable for YM2612 { +impl Transmutable for Ym2612 { fn as_addressable(&mut self) -> Option<&mut dyn Addressable> { Some(self) } - //fn as_steppable(&mut self) -> Option<&mut dyn Steppable> { - // Some(self) - //} + fn as_steppable(&mut self) -> Option<&mut dyn Steppable> { + Some(self) + } } - diff --git a/todo.txt b/todo.txt index e156bc1..07abb39 100644 --- a/todo.txt +++ b/todo.txt @@ -1,33 +1,28 @@ -* I'm trying to explore the alternative of having the frontend call a function and pass a closure that takes a frame (or buffer) and then draws - calls the update function from there... -* you can't put a closure into the WindowUpdate trait because you pass it in as Box which is a trait object, and you - can't mix generics and trait objects... -* you could maybe pass a closure in if you pass the updater as a generic WindowUpdater, although then you can only have one per application -* you could have a shared buffer that you submit to the frontend, and you both update it whenever (less synchronized which might not be good) +* for the mixer, it might be easier to have a buffer for each source, but then you'd need to have a list of all sources, even though + each source has a copy of the mixer as well... Likely there'd be a sub object in Source which is the buffer and anything else needed + by the mixer + +* I'm leaning towards having an object that data is written to by the device. The device can decide how often to update. The issue is + knowing what data to exclude or insert when mixing the incoming buffers +* Removing at a sample-level granularity would compress or lengthen the waveforms, so it would be better to mix/drop a whole chunk at + once (either predetermined by the audio system or determined by each device by the amount of samples it writes at once). The chunk + size could either be specified by the device in microseconds or something, or can be inferred by the sample_rate and the size of the + chunk. + +* how do you know how big an audio frame should be? How do other emulators do audio without stretching or compressing the waveforms, and + can/should I do mixing as well, given that I have 2 sources, and at least for those two, they should be connected to the same output * you could make the sound device be an object that is passed back to the simulation section like SimplePty. You need to either register a callback with the frontend sound system that is called when it needs data, or you write to a shared buffer which is passed back to the frontend when it needs it, or it has a copy it can use directly -* can you make some kind of signal that goes high when a frame has been drawn, and also all the system to pause execution at that point - -* for Signal/Register, you could possibly unify them, or you could distinguish them even more -* should you rename Register to AsyncSignal or something -* copy the callback over to Signal, or even make a trait that implements it for both? Or should you make a special object that is observable - which should always use the callback, and Signal would always be used when the callback wasn't used -* think more about what kinds of signals are used: - - one setter with multiple passive listeners - - one one-shot setter (no reset) with one active listener that resets the signal - - - * add sound * should you rename devices.rs traits.rs? -* rewrite the frame swapper thing to either not use the swapper or somethnig... it's just very sloppy and needs improving -* modify the frame swapper and frontend to avoid the extra buffer copy * add command line arguments to speed up or slow down either the frame rate limiter or the simulated time per frame * can you make the connections between things (like memory adapters), be expressed in a way that's more similar to the electrical design? like specifying that address pins 10-7 should be ignored/unconnected, pin 11 will connect to "chip select", etc +* should you add a unique ID to devices, such that they can be indexed, and their step functions can reset the next_run count and run them immediately * should you simulate bus arbitration? @@ -47,9 +42,15 @@ Debugger: Genesis/Mega Drive: + * the 68000/Z80 bank switching is probably buggy, and there's that other banking stuff in the 0xC00000 range, which isn't implemented at all + * add support for the H/V counters at 0xC00008 * need to implement the 1.5ms reset in the genesis controllers * fix ym7101 to better handle V/H interrupts (right now it sets and then the next step will clear, but it'd be nice if it could 'edge trigger') * make the ym7101 set/reset the v_int occurred flag based on the interrupt controller + * refactor to allow per-line horizontal scrolling, which might need a pattern iterator than only does a line at a time + * refactor ym7101 into multiple files perhaps. You can separate the DMA stuff, the address/interfacing parts, and the graphics state + * fix sprite/cell priorities so that they're drawn correctly + * add support for the sprite overflow flag (low priority) Macintosh: