diff --git a/emulator/peripherals/yamaha/src/ym2612.rs b/emulator/peripherals/yamaha/src/ym2612.rs index 536dfae..9f4665a 100644 --- a/emulator/peripherals/yamaha/src/ym2612.rs +++ b/emulator/peripherals/yamaha/src/ym2612.rs @@ -120,6 +120,41 @@ const RATE_TABLE: &[u16] = &[ 8, 8, 8, 8, 8, 8, 8, 8, ]; +const DETUNE_TABLE: &[u8] = &[ + 0, 0, 1, 2, + 0, 0, 1, 2, + 0, 0, 1, 2, + 0, 0, 1, 2, + 0, 1, 2, 2, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 3, + 0, 1, 2, 4, + 0, 1, 3, 4, + 0, 1, 3, 4, + 0, 1, 3, 5, + 0, 2, 4, 5, + 0, 2, 4, 6, + 0, 2, 4, 6, + 0, 2, 5, 7, + 0, 2, 5, 8, + 0, 3, 6, 8, + 0, 3, 6, 9, + 0, 3, 7, 10, + 0, 4, 8, 11, + 0, 4, 8, 12, + 0, 4, 9, 13, + 0, 5, 10, 14, + 0, 5, 11, 16, + 0, 6, 12, 17, + 0, 6, 13, 19, + 0, 7, 14, 20, + 0, 8, 16, 22, + 0, 8, 16, 22, + 0, 8, 16, 22, + 0, 8, 16, 22, +]; + const SIN_TABLE_SIZE: usize = 512; const POW_TABLE_SIZE: usize = 1 << 13; @@ -302,23 +337,35 @@ impl PhaseGenerator { } fn calculate_phase_increment(&mut self) { + // Start with the Fnumber let increment = self.fnumber as u32; + // Shift according to the block (octave) let increment = if self.block == 0 { increment >> 1 } else { increment << self.block - 1 }; - // TODO detune - //let inc = + // Apply detune + let keycode = get_keycode(self.block, self.fnumber); + let sign = self.detune >> 2; + let detune_index = (self.detune & 0x03) as usize; + let detune = DETUNE_TABLE[keycode * 4 + detune_index] as u32; + let increment = if sign == 0 { + increment + detune + } else { + increment - detune + }.min(0x1FFFF); + // Apply multiple let increment = if self.multiple == 0 { increment >> 1 } else { (increment * self.multiple).min(MAX_PHASE) }; + // Cache the value for use later, since it only changes when the input registers are set self.increment = increment; } @@ -329,6 +376,26 @@ impl PhaseGenerator { } } +/// Map the upper 5 bits of the fnumber to the lower 2 bits of the keycode +/// +/// The upper of the two bits is bit 11 of the fnumber, and the lower bit is follows +/// the formula F11 & (F10 | F9 | F8) | !F11 & (F10 & F9 & F8), where the bit numbers +/// of the fnumber value start from 1 instead of 0. It's easier to map this with an +/// lookup table than to calculate this. +/// +/// K1 = F11 +/// K0 = F11 & (F10 | F9 | F8) | !F11 & (F10 & F9 & F8) +const FNUMBER_TO_KEYCODE: &[u8] = &[ + 0, 0, 0, 0, 0, 0, 0, 1, + 2, 3, 3, 3, 3, 3, 3, 3, +]; + +/// Generate the keycode required for detune calculations using the block and fnumber +fn get_keycode(block: u8, fnumber: u16) -> usize { + ((block as usize) << 2) | FNUMBER_TO_KEYCODE[(fnumber as usize) >> 7] as usize +} + + #[derive(Copy, Clone, Debug)] enum OperatorAlgorithm { A0, @@ -341,7 +408,6 @@ enum OperatorAlgorithm { A7, } - #[derive(Clone)] struct Operator { #[allow(dead_code)] @@ -391,11 +457,11 @@ impl Operator { let phase = self.phase.update_phase(fm_clock); let mod_phase = phase + modulator; - - let mut output = POW_TABLE[(SIN_TABLE[(mod_phase & 0x1FF) as usize] + envelope) as usize]; -//if self.debug_name == "ch 3, op 1" { -//print!("{:4x}", output); +//if self.debug_name == "ch 2, op 0" { +//println!("{:4x} {:4x} {:4x} {:4x}", phase, self.phase.increment, modulator, envelope); //} + let mut output = POW_TABLE[(SIN_TABLE[(mod_phase & 0x1FF) as usize] + envelope) as usize]; + if mod_phase & 0x200 != 0 { output = (output as i16 * -1) as u16 } @@ -404,36 +470,38 @@ impl Operator { } } -#[inline] -fn fnumber_to_frequency(block: u8, fnumber: u16) -> f32 { - (fnumber as f32 * 0.0264) * 2_u32.pow(block as u32) as f32 -} #[derive(Clone)] struct Channel { #[allow(dead_code)] debug_name: String, + enabled: (bool, bool), operators: Vec, + algorithm: OperatorAlgorithm, + key_state: u8, next_key_clock: FmClock, next_key_state: u8, - base_frequency: f32, - algorithm: OperatorAlgorithm, } impl Channel { fn new(debug_name: String) -> Self { Self { debug_name: debug_name.clone(), + enabled: (true, true), operators: (0..OPERATORS).map(|i| Operator::new(format!("{}, op {}", debug_name, i))).collect(), + algorithm: OperatorAlgorithm::A0, + key_state: 0, next_key_clock: 0, next_key_state: 0, - base_frequency: 0.0, - algorithm: OperatorAlgorithm::A0, } } + fn set_enabled(&mut self, left: bool, right: bool) { + self.enabled = (left, right); + } + fn change_key_state(&mut self, fm_clock: FmClock, key: u8) { self.next_key_clock = fm_clock; self.next_key_state = key; @@ -449,20 +517,25 @@ impl Channel { } } - fn get_sample(&mut self, clocks: (FmClock, EnvelopeClock)) -> f32 { + fn get_sample(&mut self, clocks: (FmClock, EnvelopeClock)) -> (f32, f32) { self.check_key_change(clocks); - let mut output = self.get_algorithm_output(clocks); + let output = self.get_algorithm_output(clocks); let output = if output & 0x2000 == 0 { output as i16 } else { (output | 0xC000) as i16 }; -//if self.debug_name == "ch 0" { -//println!("{}", output); + + //let output = output * 2 / 3; +//if self.debug_name == "ch 2" { +//println!("{:6x}", output); //} let output = output as f32 / (1 << 14) as f32; - output + + let left = if self.enabled.0 { output } else { 0.0 }; + let right = if self.enabled.1 { output } else { 0.0 }; + (left, right) } fn get_algorithm_output(&mut self, clocks: (FmClock, EnvelopeClock)) -> u16 { @@ -649,11 +722,11 @@ impl Ym2612 { let mut sample = 0.0; for ch in 0..(CHANNELS - 1) { - sample += self.channels[ch].get_sample(clocks); + sample += self.channels[ch].get_sample(clocks).0; } if !self.dac.enabled { - sample += self.channels[CHANNELS - 1].get_sample(clocks); + sample += self.channels[CHANNELS - 1].get_sample(clocks).0; } sample @@ -744,6 +817,7 @@ impl Ym2612 { reg if (0xB0..=0xB2).contains(®) => { let ch = get_ch(bank, reg); + // TODO add feedback values self.channels[ch].algorithm = match data & 0x07 { 0 => OperatorAlgorithm::A0, 1 => OperatorAlgorithm::A1, @@ -757,6 +831,12 @@ impl Ym2612 { }; }, + reg if (0xB4..=0xB6).contains(®) => { + let ch = get_ch(bank, reg - 4); + // TODO add AMS and FMS (which only apply to the LFO) + self.channels[ch].set_enabled(data & 0x80 != 0, data & 0x40 != 0); + }, + _ => { warn!("{}: !!! unhandled write to register {:0x} with {:0x}", DEV_NAME, reg, data); }, diff --git a/todo.txt b/todo.txt index acd11bb..29c917a 100644 --- a/todo.txt +++ b/todo.txt @@ -2,6 +2,9 @@ * the first 512 entries are 0 for some reason, in the log table, but otherwise seems ok * you need to scale the output sample to be +/- 1.0 instead of 0-1.0 +* AudioFrame (and possibly the mixer and source) should be moved to the core, it should probably have the sample rate +* need to be able to support stereo output eventually + * 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