use crate::trace_apu;
use serde::{Deserialize, Serialize};
use super::channel1::{
EnvelopeClockState, pulse_duty0_max_freq_edge_output, pulse_trigger_fresh_delay_t,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Channel2 {
duty: u8,
length_load: u8,
init_volume: u8,
env_add: bool,
env_period: u8,
freq: u16,
length_en: bool,
active: bool,
dac_on: bool,
duty_pos: u8,
freq_timer: u16,
pub(crate) length_counter: u8,
volume: u8,
env_timer: u8,
#[serde(default)]
current_output: u8,
triggered_once: bool,
#[serde(default)]
first_sample_zero: bool,
#[serde(default)]
env_clock_state: EnvelopeClockState,
}
impl Default for Channel2 {
fn default() -> Self {
Self::new()
}
}
impl Channel2 {
pub fn new() -> Self {
Self {
duty: 0,
length_load: 0,
init_volume: 0,
env_add: false,
env_period: 0,
freq: 0,
length_en: false,
active: false,
dac_on: false,
duty_pos: 0,
freq_timer: 0,
length_counter: 0,
volume: 0,
env_timer: 0,
current_output: 0,
triggered_once: false,
first_sample_zero: false,
env_clock_state: EnvelopeClockState::default(),
}
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn length_en(&self) -> bool {
self.length_en
}
pub fn output(&self) -> f32 {
f32::from(self.digital_output()) / 15.0
}
pub fn digital_output(&self) -> u8 {
if !self.active || !self.dac_on {
return 0;
}
if let Some(output) = pulse_duty0_max_freq_edge_output(
self.duty,
self.freq,
self.first_sample_zero,
self.duty_pos,
self.freq_timer,
self.volume,
) {
return output;
}
self.current_output
}
fn update_current_output(&mut self) {
if !self.active || !self.dac_on || self.first_sample_zero {
self.current_output = 0;
return;
}
let bit = super::apu::DUTY_TABLE[self.duty as usize][self.duty_pos as usize];
self.current_output = if bit == 1 { self.volume } else { 0 };
}
pub fn tick(&mut self) {
if !self.active {
return;
}
let period = (2048 - self.freq) * 4;
if self.freq_timer == 0 {
self.freq_timer = period;
}
for _ in 0..4 {
self.freq_timer -= 1;
if self.freq_timer == 0 {
self.freq_timer = period;
if self.triggered_once {
let old_pos = self.duty_pos;
self.duty_pos = (self.duty_pos + 1) & 7;
self.first_sample_zero = false;
self.update_current_output();
trace_apu!(5; "GB APU CH2 tick duty_pos {} -> {} period=0x{:03X}", old_pos, self.duty_pos, self.freq);
}
}
}
}
pub fn clock_length(&mut self) {
if !self.length_en || self.length_counter == 0 {
return;
}
self.length_counter -= 1;
trace_apu!(3; "GB APU CH2 length_counter={} active={}", self.length_counter, self.length_counter > 0);
if self.length_counter == 0 {
self.active = false;
self.current_output = 0;
}
}
pub fn clock_envelope_decrement(&mut self) {
if self.env_period == 0 {
return;
}
if self.env_timer > 0 {
self.env_timer -= 1;
}
}
pub fn clock_envelope_secondary(&mut self) {
if !self.active || self.env_period == 0 {
return;
}
if self.env_timer == 0 {
self.env_timer = self.env_period;
self.env_clock_state.clock = true;
}
}
pub fn clock_envelope_primary(&mut self) {
if !self.env_clock_state.clock {
return;
}
self.env_clock_state.clock = false;
if self.env_clock_state.locked {
return;
}
let old_volume = self.volume;
if self.env_add && self.volume < 15 {
self.volume += 1;
} else if !self.env_add && self.volume > 0 {
self.volume -= 1;
}
if (self.env_add && self.volume == 15) || (!self.env_add && self.volume == 0) {
self.env_clock_state.locked = true;
}
if old_volume != self.volume {
trace_apu!(3; "GB APU CH2 envelope volume {} -> {}", old_volume, self.volume);
self.update_current_output();
}
}
pub fn clock_envelope(&mut self) {
self.clock_envelope_decrement();
self.clock_envelope_secondary();
self.clock_envelope_primary();
}
pub fn power_off(&mut self) {
self.duty = 0;
self.length_load = 0;
self.init_volume = 0;
self.env_add = false;
self.env_period = 0;
self.freq = 0;
self.length_en = false;
self.active = false;
self.dac_on = false;
self.duty_pos = 0;
self.freq_timer = 0;
self.length_counter = 0;
self.volume = 0;
self.env_timer = 0;
self.current_output = 0;
self.triggered_once = false;
self.first_sample_zero = false;
self.env_clock_state = EnvelopeClockState::default();
}
pub fn read_nr21(&self) -> u8 {
0x3F | ((self.duty & 0x03) << 6)
}
pub fn read_nr22(&self) -> u8 {
((self.init_volume & 0x0F) << 4) | (u8::from(self.env_add) << 3) | (self.env_period & 0x07)
}
pub fn read_nr24(&self) -> u8 {
0xBF | (u8::from(self.length_en) << 6)
}
pub fn write_nr21(&mut self, val: u8) {
trace_apu!(2; "GB APU CH2 write NR21=0x{:02X} duty={} length={}", val, (val >> 6) & 0x03, val & 0x3F);
self.duty = (val >> 6) & 0x03;
self.length_load = val & 0x3F;
self.length_counter = 64 - self.length_load;
}
pub fn write_nr22(&mut self, val: u8) {
trace_apu!(2; "GB APU CH2 write NR22=0x{:02X} volume={} env_add={} env_period={}",
val, (val >> 4) & 0x0F, (val & 0x08) != 0, val & 0x07);
let old_val = self.read_nr22();
self.init_volume = (val >> 4) & 0x0F;
self.env_add = val & 0x08 != 0;
self.env_period = val & 0x07;
self.dac_on = val & 0xF8 != 0;
if !self.dac_on {
self.active = false;
self.current_output = 0;
} else if self.active {
self.apply_nrx2_glitch(old_val, val);
self.update_current_output();
}
}
fn apply_nrx2_glitch(&mut self, old_val: u8, new_val: u8) {
let old_period = old_val & 0x07;
let new_period = new_val & 0x07;
let old_direction_add = (old_val & 0x08) != 0;
let new_direction_add = (new_val & 0x08) != 0;
if self.env_clock_state.clock {
self.env_timer = new_period;
}
let mut should_tick =
(new_period != 0) && (old_period == 0) && !self.env_clock_state.locked;
if (new_val & 0x0F) == 0x08 && (old_val & 0x0F) == 0x08 && !self.env_clock_state.locked {
should_tick = true;
}
let should_invert = old_direction_add != new_direction_add;
if should_invert {
let old_volume = self.volume;
if new_direction_add {
if old_period == 0 && !self.env_clock_state.locked {
self.volume ^= 0x0F;
} else {
self.volume = (0x0E_u8.wrapping_sub(self.volume)) & 0x0F;
}
should_tick = false;
} else {
self.volume = (0x10_u8.wrapping_sub(self.volume)) & 0x0F;
}
trace_apu!(3; "GB APU CH2 zombie invert volume {} -> {}", old_volume, self.volume);
}
if should_tick {
let old_volume = self.volume;
if new_direction_add {
self.volume = (self.volume + 1) & 0x0F;
} else {
self.volume = self.volume.wrapping_sub(1) & 0x0F;
}
trace_apu!(3; "GB APU CH2 zombie tick volume {} -> {}", old_volume, self.volume);
} else if new_period == 0 && self.env_clock_state.clock {
self.env_clock_state.clock = false;
}
}
pub fn write_nr23(&mut self, val: u8) {
self.freq = (self.freq & 0x0700) | u16::from(val);
trace_apu!(2; "GB APU CH2 write NR23=0x{:02X} freq=0x{:03X}", val, self.freq);
}
pub fn write_nr24(&mut self, val: u8, extra_clk: bool, lf_div: bool) {
self.write_nr24_with_apu_phase(val, extra_clk, lf_div, None);
}
pub fn write_nr24_with_apu_phase(
&mut self,
val: u8,
extra_clk: bool,
lf_div: bool,
double_speed_phase_bits: Option<u8>,
) {
self.write_nr24_with_apu_phase_and_length_quirk(
val,
extra_clk,
lf_div,
double_speed_phase_bits,
false,
);
}
pub fn write_nr24_with_apu_phase_and_length_quirk(
&mut self,
val: u8,
extra_clk: bool,
lf_div: bool,
double_speed_phase_bits: Option<u8>,
cgb_early_extra_length_clock: bool,
) {
trace_apu!(2; "GB APU CH2 write NR24=0x{:02X} trigger={} length_en={} freq_high={}",
val, (val & 0x80) != 0, (val & 0x40) != 0, val & 0x07);
let old_length_en = self.length_en;
self.length_en = val & 0x40 != 0;
self.freq = (self.freq & 0x00FF) | (u16::from(val & 0x07) << 8);
let clocks_length_on_extra = self.length_en || cgb_early_extra_length_clock;
if extra_clk && !old_length_en && clocks_length_on_extra && self.length_counter > 0 {
self.length_counter -= 1;
if self.length_counter == 0 {
self.active = false;
}
}
if val & 0x80 != 0 {
self.trigger(lf_div, double_speed_phase_bits);
if extra_clk && clocks_length_on_extra && self.length_counter == 64 {
self.length_counter = 63;
}
}
}
pub fn write_nr21_length_only(&mut self, val: u8) {
self.length_load = val & 0x3F;
self.length_counter = 64 - self.length_load;
}
fn trigger(&mut self, lf_div: bool, double_speed_phase_bits: Option<u8>) {
trace_apu!(1; "GB APU CH2 trigger freq=0x{:03X} volume={} lf_div={}", self.freq, self.init_volume, lf_div);
let was_active = self.active;
if !self.triggered_once {
self.first_sample_zero = true;
}
self.triggered_once = true;
if self.dac_on {
self.active = true;
}
if self.length_counter == 0 {
self.length_counter = 64;
}
let fresh_delay_t = pulse_trigger_fresh_delay_t(lf_div, double_speed_phase_bits);
let delay_t = if was_active {
fresh_delay_t.saturating_sub(2)
} else {
fresh_delay_t
};
let period = (2048 - self.freq) * 4;
self.freq_timer = period + delay_t;
self.volume = self.init_volume;
self.env_timer = self.env_period;
if was_active {
self.update_current_output();
} else {
self.current_output = 0;
}
self.env_clock_state = EnvelopeClockState::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn triggered_ch2() -> Channel2 {
let mut ch = Channel2::new();
ch.write_nr22(0xF0); ch.write_nr21(0x80); ch.write_nr24(0x80, false, false); ch
}
#[test]
fn test_duty_write_takes_effect_on_next_sample() {
let mut ch = Channel2::new();
ch.write_nr22(0x80);
ch.write_nr21(0xC0);
ch.write_nr23(0xFF);
ch.write_nr24(0x87, false, false);
for _ in 0..3 {
ch.tick();
}
assert_eq!(ch.duty_pos, 1);
assert_eq!(ch.digital_output(), 8);
ch.write_nr21(0x00);
assert_eq!(
ch.digital_output(),
8,
"duty writes should not affect the current square sample"
);
ch.tick();
assert_eq!(ch.duty_pos, 2);
assert_eq!(ch.digital_output(), 0);
}
#[test]
fn test_double_speed_phase_bits_set_initial_freq_timer() {
let mut phase0 = Channel2::new();
phase0.write_nr22(0xF0);
phase0.write_nr21(0x80);
phase0.write_nr23(0xFF);
phase0.write_nr24_with_apu_phase(0x87, false, false, Some(0b00));
let mut phase3 = Channel2::new();
phase3.write_nr22(0xF0);
phase3.write_nr21(0x80);
phase3.write_nr23(0xFF);
phase3.write_nr24_with_apu_phase(0x87, false, false, Some(0b11));
assert_eq!(phase0.freq_timer, 14);
assert_eq!(phase3.freq_timer, 12);
}
#[test]
fn test_trigger_makes_channel_active() {
let ch = triggered_ch2();
assert!(ch.is_active());
}
#[test]
fn test_dac_off_prevents_activation() {
let mut ch = Channel2::new();
ch.write_nr22(0x00);
ch.write_nr24(0x80, false, false);
assert!(!ch.is_active());
}
#[test]
fn test_length_counter_expiry_silences_when_enabled() {
let mut ch = Channel2::new();
ch.write_nr22(0xF0);
ch.write_nr21(0xFF); ch.write_nr24(0xC0, false, false); ch.clock_length();
assert!(!ch.is_active());
}
#[test]
fn test_length_counter_no_expire_when_disabled() {
let mut ch = Channel2::new();
ch.write_nr22(0xF0);
ch.write_nr21(0xFF);
ch.write_nr24(0x80, false, false);
ch.clock_length();
assert!(ch.is_active());
}
#[test]
fn test_envelope_decrements_volume() {
let mut ch = Channel2::new();
ch.write_nr22(0x71); ch.write_nr24(0x80, false, false);
ch.clock_envelope();
assert_eq!(ch.volume, 6);
}
#[test]
fn test_envelope_increments_volume() {
let mut ch = Channel2::new();
ch.write_nr22(0x79); ch.write_nr24(0x80, false, false);
ch.clock_envelope();
assert_eq!(ch.volume, 8);
}
#[test]
fn test_nr21_read_duty_bits() {
let mut ch = Channel2::new();
ch.write_nr21(0xC0); assert_eq!(ch.read_nr21() >> 6, 0b11);
}
#[test]
fn test_nr22_read_back() {
let mut ch = Channel2::new();
ch.write_nr22(0xF3);
assert_eq!(ch.read_nr22(), 0xF3);
}
#[test]
fn test_nr24_length_en_readable() {
let mut ch = Channel2::new();
ch.write_nr22(0xF0);
ch.write_nr24(0x40, false, false);
assert_eq!(ch.read_nr24() & 0x40, 0x40);
}
#[test]
fn test_output_zero_when_inactive() {
let ch = Channel2::new();
assert_eq!(ch.output(), 0.0);
}
#[test]
fn test_duty_phase_is_not_clocked_before_first_trigger() {
let mut ch = Channel2::new();
for _ in 0..4096 {
ch.tick();
}
assert_eq!(
ch.duty_pos, 0,
"duty phase should remain at reset position before first trigger"
);
ch.write_nr22(0xF0); ch.write_nr21(0x80); ch.write_nr24(0x80, false, false);
let start = ch.duty_pos;
for _ in 0..4096 {
ch.tick();
}
assert_ne!(
ch.duty_pos, start,
"duty phase should advance after the channel has been triggered"
);
}
#[test]
fn test_duty_phase_does_not_advance_while_stopped_by_dac() {
let mut ch = Channel2::new();
ch.write_nr22(0x80);
ch.write_nr21(0x80);
ch.write_nr24(0x80, false, false);
ch.duty_pos = 3;
ch.write_nr22(0x00);
assert!(!ch.is_active());
for _ in 0..16 {
ch.tick();
}
assert_eq!(
ch.duty_pos, 3,
"stopping via DAC must freeze the duty phase until restart"
);
ch.write_nr22(0x80);
ch.write_nr24(0x80, false, false);
assert_eq!(
ch.duty_pos, 3,
"restarting after DAC stop must preserve the stopped duty phase"
);
}
#[test]
fn test_tick_freq_timer_decrements_by_tcycles_within_mcycle() {
let mut ch = Channel2::new();
ch.write_nr22(0xF0);
ch.write_nr21(0x80);
ch.write_nr23(0xFF);
ch.write_nr24(0x87, false, false); ch.freq_timer = 6;
let duty_before = ch.duty_pos;
ch.tick();
assert_eq!(
ch.freq_timer, 2,
"freq_timer should decrement to 2 after one M-cycle"
);
assert_eq!(
ch.duty_pos, duty_before,
"duty_pos should not advance when timer > 0"
);
}
#[test]
fn test_tick_freq_timer_expires_mid_mcycle_and_reloads_with_remainder() {
let mut ch = Channel2::new();
ch.write_nr22(0xF0);
ch.write_nr21(0x80);
ch.write_nr23(0xFE);
ch.write_nr24(0x87, false, false); ch.freq_timer = 3;
let duty_before = ch.duty_pos;
ch.tick();
assert_eq!(
ch.freq_timer, 7,
"freq_timer should be period - remaining (8 - 1 = 7)"
);
assert_eq!(
ch.duty_pos,
(duty_before + 1) & 7,
"duty_pos should advance once"
);
}
#[test]
fn test_tick_freq_timer_expires_exactly_at_mcycle_boundary() {
let mut ch = Channel2::new();
ch.write_nr22(0xF0);
ch.write_nr21(0x80);
ch.write_nr23(0xFD);
ch.write_nr24(0x87, false, false); ch.freq_timer = 4;
let duty_before = ch.duty_pos;
ch.tick();
assert_eq!(
ch.freq_timer, 12,
"freq_timer should be exactly period after expiring at boundary"
);
assert_eq!(
ch.duty_pos,
(duty_before + 1) & 7,
"duty_pos should advance once"
);
}
#[test]
fn test_nrx2_zombie_tick_when_period_zero_to_nonzero_decrease() {
let mut ch = Channel2::new();
ch.write_nr22(0x70); ch.write_nr24(0x80, false, false); assert_eq!(ch.volume, 7);
ch.write_nr22(0x71); assert_eq!(ch.volume, 6, "zombie tick should decrement volume");
}
#[test]
fn test_nrx2_zombie_tick_when_period_zero_to_nonzero_increase() {
let mut ch = Channel2::new();
ch.write_nr22(0x78); ch.write_nr24(0x80, false, false); assert_eq!(ch.volume, 7);
ch.write_nr22(0x79); assert_eq!(ch.volume, 8, "zombie tick should increment volume");
}
#[test]
fn test_nrx2_zombie_no_tick_when_old_period_nonzero() {
let mut ch = Channel2::new();
ch.write_nr22(0x71); ch.write_nr24(0x80, false, false); assert_eq!(ch.volume, 7);
ch.write_nr22(0x72); assert_eq!(ch.volume, 7, "no zombie tick when old_period != 0");
}
#[test]
fn test_nrx2_zombie_x08_to_x08_ticks_add() {
let mut ch = Channel2::new();
ch.write_nr22(0x78); ch.write_nr24(0x80, false, false); assert_eq!(ch.volume, 7);
ch.write_nr22(0x78); assert_eq!(ch.volume, 8, "x08->x08 zombie tick should increment volume");
}
#[test]
fn test_nrx2_zombie_direction_switch_to_add_period_zero_xors() {
let mut ch = Channel2::new();
ch.write_nr22(0x70); ch.write_nr24(0x80, false, false); assert_eq!(ch.volume, 7);
ch.write_nr22(0x78); assert_eq!(
ch.volume, 8,
"direction switch to add with period=0 should XOR volume with 0x0F"
);
}
#[test]
fn test_nrx2_zombie_direction_switch_to_add_nonzero_period_subtracts() {
let mut ch = Channel2::new();
ch.write_nr22(0x71); ch.write_nr24(0x80, false, false); assert_eq!(ch.volume, 7);
ch.write_nr22(0x79); assert_eq!(
ch.volume, 7,
"direction switch to add with nonzero period should use (0x0E - vol) formula"
);
}
#[test]
fn test_nrx2_zombie_direction_switch_to_subtract() {
let mut ch = Channel2::new();
ch.write_nr22(0x79); ch.write_nr24(0x80, false, false); assert_eq!(ch.volume, 7);
ch.write_nr22(0x71); assert_eq!(
ch.volume, 9,
"direction switch to sub should use (0x10 - vol) formula"
);
}
#[test]
fn test_nrx2_zombie_reload_countdown_when_clock_active() {
let mut ch = Channel2::new();
ch.write_nr22(0x71); ch.write_nr24(0x80, false, false); ch.clock_envelope_decrement(); ch.clock_envelope_secondary(); assert!(
ch.env_clock_state.clock,
"clock should be armed after secondary"
);
ch.write_nr22(0x73); assert_eq!(
ch.env_timer, 3,
"env_timer should reload to new_period when clock is active"
);
}
#[test]
fn test_clock_envelope_decrement_only_decrements_timer() {
let mut ch = Channel2::new();
ch.write_nr22(0x71); ch.write_nr24(0x80, false, false); let vol_before = ch.volume;
ch.clock_envelope_decrement();
assert_eq!(ch.volume, vol_before, "decrement must not change volume");
assert_eq!(ch.env_timer, 0, "decrement should reduce env_timer to 0");
}
#[test]
fn test_clock_envelope_secondary_arms_clock_and_reloads_timer() {
let mut ch = Channel2::new();
ch.write_nr22(0x71); ch.write_nr24(0x80, false, false); let vol_before = ch.volume;
ch.clock_envelope_decrement(); assert!(
!ch.env_clock_state.clock,
"clock not yet armed before secondary"
);
ch.clock_envelope_secondary(); assert!(
ch.env_clock_state.clock,
"clock should be armed after secondary"
);
assert_eq!(
ch.env_timer, 1,
"timer should reload to period after secondary"
);
assert_eq!(ch.volume, vol_before, "secondary must not change volume");
}
#[test]
fn test_clock_envelope_primary_fires_volume_change_when_clock_set() {
let mut ch = Channel2::new();
ch.write_nr22(0x71); ch.write_nr24(0x80, false, false); ch.clock_envelope_decrement();
ch.clock_envelope_secondary();
assert_eq!(ch.volume, 7);
ch.clock_envelope_primary();
assert_eq!(
ch.volume, 6,
"primary should decrement volume when clock armed"
);
assert!(
!ch.env_clock_state.clock,
"clock flag should be cleared after primary"
);
}
#[test]
fn test_clock_envelope_primary_no_change_without_clock() {
let mut ch = Channel2::new();
ch.write_nr22(0x71); ch.write_nr24(0x80, false, false); let vol_before = ch.volume;
ch.clock_envelope_primary(); assert_eq!(
ch.volume, vol_before,
"primary must not change volume when clock not armed"
);
}
}