mirror of
https://github.com/transistorfet/moa.git
synced 2025-03-05 14:31:23 +00:00
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:
parent
69c94fa3af
commit
31faf70d97
27
docs/log.txt
27
docs/log.txt
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
18
todo.txt
18
todo.txt
@ -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...
|
||||
|
Loading…
x
Reference in New Issue
Block a user