Fixed many bugs in ym2612, and switched to square wave

- The envelope generator wasn't working as it should have, minor issues
  with the limits and whether to use 10-bit or 12-bit values (more to come)
- fixed issues with sustain level where it was always set to 0
- fixed release rate and levels to make them 5-bit and 10-bit numbers
  respectively, so they match the others
- switched from SineWave to SquareWave and this alone made it go from
  terrible and muddy to not that far off.  I probably need to completely
  change the output

- also included an attempt at removing HostData, still needed for an
  interrupt that is triggered by user input
This commit is contained in:
transistor 2023-04-24 20:47:52 -07:00
parent 69c94fa3af
commit 31faf70d97
5 changed files with 72 additions and 40 deletions

View File

@ -353,3 +353,30 @@ General Work
2023-04-22
- I'm back to trying to get the ym2612 emulation working properly for the Genesis
2023-04-23
- I managed to get the idea of the envelope implemented but it's clearly very broken and I don't
know why exactly. It could easily be the waveform generators or bugs or anything else. I have
already found and fixed a number of bugs, so there are probably more
2023-04-24
- I added a print statement to blastem to see how the envelope value was varying over time based on
the envelope state and what update cycle it was in. In moa, it shows the envelope output varying
over many many update cycles which I thought was maybe wrong, and the decay was only lasting one
cycle
- It turned out blastem showed the same output varying over many cycles, so that wasn't a problem,
but it showed the decay lasting many cycles too
- it also showed the envelope output being up to 4092 instead of ~1024 and I didn't notice until now
but there's a comment that blastem is using a 12 bit number in two parts, 8 bits and 4 bits, but I
don't yet know how it uses those numbers to get the output
- the decay lasting only one cycle turned out to be a bug where I was masking the wrong bits and
shifting them away, so it was always 0.
- there was also a bug in that the raw values from the registers needs to be adjusted to make it
comparable to the envelope, and the total level also needs to be adjusted (which I was already
doing but it's a 10 bit value)
- it looks like the release rate also needs to be adjusted. It should be shifted left and
incremented to make it the same as the other rates
- after messing with it, it still didn't seem to work, and it still doesn't work correctly, but
changing from a sine wave generator to a square wave generator made it go from being muddled and
mute sounding to sounding crisp and much closer to what it's supposed to sound like

View File

@ -1,5 +1,3 @@
//! Clock time and duration types for simulation with femtosecond accurancy
//!
use std::ops::{Add, AddAssign, Sub, SubAssign, Mul, MulAssign, Div, DivAssign};
@ -230,7 +228,11 @@ impl AddAssign<ClockDuration> for ClockTime {
}
}
/// Represents a frequency in Hz
///
/// Clocks are usually given as a frequency, but durations are needed when dealing with clocks
/// and clock durations. This type makes it easier to create a clock of a given frequency and
/// convert it to a `ClockDuration`
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Frequency {
hertz: u32,

View File

@ -6,7 +6,7 @@
//!
//! Resources:
//! - Registers: https://www.smspower.org/maxim/Documents/YM2612
//! - Envelope and rates: http://gendev.spritesmind.net/forum/viewtopic.php?p=5716#5716
//! - Envelope and rates: https://gendev.spritesmind.net/forum/viewtopic.php?t=386 [Nemesis]
use std::num::NonZeroU8;
use std::collections::VecDeque;
@ -14,7 +14,7 @@ use std::collections::VecDeque;
use moa_core::{debug, warn};
use moa_core::{System, Error, ClockTime, ClockDuration, Frequency, Address, Addressable, Steppable, Transmutable};
use moa_core::host::{Host, Audio};
use moa_audio::{SineWave, db_to_gain};
use moa_audio::{SquareWave, db_to_gain};
/// Table of shift values for each possible rate angle
@ -117,7 +117,7 @@ const DEV_NAME: &str = "ym2612";
const CHANNELS: usize = 6;
const OPERATORS: usize = 4;
const MAX_ENVELOPE: u16 = 0xFFC;
const MAX_ENVELOPE: u16 = 0x3FC;
type EnvelopeClock = u64;
@ -200,12 +200,13 @@ impl EnvelopeGenerator {
match self.envelope_state {
EnvelopeState::Attack => {
let new_envelope = self.envelope + increment * (((1024 - self.envelope) / 16) + 1);
if new_envelope < MAX_ENVELOPE {
self.envelope = new_envelope;
} else {
//let new_envelope = self.envelope + increment * (((1024 - self.envelope) / 16) + 1);
let new_envelope = ((!self.envelope * increment) >> 4) & 0xFFFC;
if new_envelope > self.envelope {
self.envelope_state = EnvelopeState::Decay;
self.envelope = 0;
} else {
self.envelope = new_envelope.min(MAX_ENVELOPE);
}
},
EnvelopeState::Decay |
@ -214,16 +215,15 @@ impl EnvelopeGenerator {
self.envelope = (self.envelope + increment).min(MAX_ENVELOPE);
},
}
if self.debug_name == "ch 3, op 2" {
println!("{}: {:?} {} {}", update_cycle, self.envelope_state, self.envelope, self.sustain_level);
}
}
}
fn get_db_at(&mut self) -> f32 {
let envelope = if self.envelope_state == EnvelopeState::Attack {
!self.envelope & 0x3FF
} else {
self.envelope
};
let attenuation_10bit = (envelope + self.total_level).min(MAX_ENVELOPE);
let attenuation_10bit = (self.envelope + self.total_level).min(MAX_ENVELOPE);
attenuation_10bit as f32 * 0.09375
}
}
@ -245,7 +245,7 @@ enum OperatorAlgorithm {
struct Operator {
#[allow(dead_code)]
debug_name: String,
wave: SineWave,
wave: SquareWave,
frequency: f32,
multiplier: f32,
envelope: EnvelopeGenerator,
@ -255,7 +255,7 @@ impl Operator {
fn new(debug_name: String, sample_rate: usize) -> Self {
Self {
debug_name: debug_name.clone(),
wave: SineWave::new(400.0, sample_rate),
wave: SquareWave::new(400.0, sample_rate),
frequency: 400.0,
multiplier: 1.0,
envelope: EnvelopeGenerator::new(debug_name),
@ -474,7 +474,7 @@ impl Ym2612 {
selected_reg_1: None,
clock_frequency,
envelope_clock_period: clock_frequency.period_duration() * 351,
envelope_clock_period: clock_frequency.period_duration() * 144 * 3, // Nemesis shows * 351? Not sure why the difference
channels: (0..CHANNELS).map(|i| Channel::new(format!("ch {}", i), sample_rate)).collect(),
channel_frequencies: [(0, 0); CHANNELS],
@ -585,10 +585,10 @@ impl Ym2612 {
reg if is_reg_range(reg, 0x40) => {
let (ch, op) = get_ch_op(bank, reg);
// The total level (attenuation) is 0-127, where 0 is the highest volume and 127
// is the lowest, in 0.75 dB intervals. The 7-bit value is multiplied by 8 to
// is the lowest, in 0.75 dB intervals. The 7-bit value is shifted left to
// convert it to a 10-bit attenuation for the envelope generator, which is an
// attenuation value in 0.09375 dB intervals
self.channels[ch].operators[op].set_total_level((data & 0x7F) as u16 * 8);
self.channels[ch].operators[op].set_total_level(((data & 0x7F) as u16) << 3);
},
reg if is_reg_range(reg, 0x50)
@ -641,18 +641,20 @@ impl Ym2612 {
let (ch, op) = get_ch_op(bank, reg);
let keycode = self.registers[0xA0 + get_ch_index(ch)] >> 1;
let rate_scaling = self.registers[0x50 + index] & 0xC0 >> 6;
let attack_rate = self.registers[0x50 + index] & 0x1F;
let first_decay_rate = self.registers[0x60 + index] & 0x1F;
let second_decay_rate = self.registers[0x70 + index] & 0x1F;
let release_rate = self.registers[0x80 + index] & 0x0F;
let release_rate = ((self.registers[0x80 + index] & 0x0F) << 1) + 1; // register is only 4 bits, so it's adjusted to 5-bits with 1 in the LSB
self.channels[ch].operators[op].set_rate(EnvelopeState::Attack, calculate_rate(attack_rate, rate_scaling, keycode));
self.channels[ch].operators[op].set_rate(EnvelopeState::Decay, calculate_rate(first_decay_rate, rate_scaling, keycode));
self.channels[ch].operators[op].set_rate(EnvelopeState::Sustain, calculate_rate(second_decay_rate, rate_scaling, keycode));
self.channels[ch].operators[op].set_rate(EnvelopeState::Release, calculate_rate(release_rate, rate_scaling, keycode));
let sustain_level = (self.registers[0x80 + index] & 0x0F) >> 4;
self.channels[ch].operators[op].set_sustain_level(sustain_level as u16);
let sustain_level = (self.registers[0x80 + index] as u16 & 0xF0) << 2; // register is 4 bits, so it's adjusted to match total_level's scale
let sustain_level = if sustain_level == (0xF0 << 2) { MAX_ENVELOPE } else { sustain_level }; // adjust the maximum storable value to be the max attenuation
self.channels[ch].operators[op].set_sustain_level(sustain_level);
}
}

View File

@ -1,4 +1,7 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicU16, Ordering};
use moa_core::{warn, info};
use moa_core::{System, Error, ClockTime, ClockDuration, Address, Addressable, Steppable, Transmutable};
use moa_core::host::{Host, ControllerUpdater, HostData, ControllerDevice, ControllerEvent};
@ -22,7 +25,7 @@ pub struct GenesisControllerPort {
/// Data contains bits:
/// 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
/// X | Y | Z | MODE | START | A | C | B | RIGHT | LEFT | DOWN | UP
buttons: HostData<u16>,
buttons: Arc<AtomicU16>,
ctrl: u8,
outputs: u8,
@ -34,7 +37,7 @@ pub struct GenesisControllerPort {
impl Default for GenesisControllerPort {
fn default() -> Self {
Self {
buttons: HostData::new(0xffff),
buttons: Arc::new(AtomicU16::new(0xffff)),
ctrl: 0,
outputs: 0,
th_count: 0,
@ -45,7 +48,7 @@ impl Default for GenesisControllerPort {
impl GenesisControllerPort {
pub fn get_data(&mut self) -> u8 {
let inputs = self.buttons.get();
let inputs = self.buttons.load(Ordering::Relaxed);
let th_state = (self.outputs & 0x40) != 0;
match (th_state, self.th_count) {
@ -85,7 +88,7 @@ impl GenesisControllerPort {
}
}
pub struct GenesisControllersUpdater(HostData<u16>, HostData<bool>);
pub struct GenesisControllersUpdater(Arc<AtomicU16>, HostData<bool>);
impl ControllerUpdater for GenesisControllersUpdater {
fn update_controller(&mut self, event: ControllerEvent) {
@ -102,8 +105,8 @@ impl ControllerUpdater for GenesisControllersUpdater {
_ => (0x0000, false),
};
let buttons = (self.0.get() & !mask) | (if !state { mask } else { 0 });
self.0.set(buttons);
let buttons = (self.0.load(Ordering::Acquire) & !mask) | (if !state { mask } else { 0 });
self.0.store(buttons, Ordering::Release);
if buttons != 0 {
self.1.set(true);
}

View File

@ -1,11 +1,16 @@
* remove the old timer stuff and replace with opentelemetry if it can be wasm compatible
* can you make the frontend more adaptive to the input that the devices are using
* change the name of the functions that take Host to be `with_host` or `register` or something
* need to re-add a mechanism for audio frame dialation, either based on speed, or somehow automatic, use clocks and make them aligned
* maybe I should make ClockDuration use picos as the base instead, and use u64 since that gives like 212 days or something instead of 5h
and should prevent split nanoseconds which is the main concern
* the audio needs to not run if nothing is using it or there's constant buffer underruns
* fix ym2612 sound generation
* sound doesn't work on a lot of games... is it a problem with the Z80 accessing the YM2612, or the lack of YM timers? or something else?
* add opentelemetry if it can be wasm compatible, or some kind of timing for giving an average framerate
* add doc strings everywhere
* get rustfmt, rustdoc, and clippy working in some kind of semi-automatic fashion
@ -25,16 +30,9 @@
* can you refactor the update timeout to put it in rust? Would that make it faster? (the tricky part is the closure)
* re-enable sound on webassembly, see if it works (it does not. Very lagged and jittery with what sounds like repeated frames)
* can you somehow speed up the memory accessing through the sim? The dyn Addressable is causing a fair amount of overhead
* can you somehow make devices have two step functions for running things at different times? (I'm thinking ym2612 audio gen vs timers)
* should you rename devices.rs traits.rs?
* you could refactor the instruction loop into a series of functions, and test if there's a performance difference with and without #[inline(always)]
* move parser into its own utils/ or /libs/ directory
* fix ym2612 sound generation
* need to re-add a mechanism for audio frame dialation, either based on speed, or somehow automatic
* sound doesn't work on a lot of games... is it a problem with the Z80 accessing the YM2612, or the lack of YM timers? or something else?
* the pixel format idea didn't work because of window resizing and the fact that the frame needs to be adjusted in size because the window can't always be resized...