use crate::trace_apu;
use serde::{Deserialize, Serialize};
use super::channel1::EnvelopeClockState;
const DIVISORS: [u32; 8] = [8, 16, 32, 48, 64, 80, 96, 112];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Channel4 {
init_volume: u8,
env_add: bool,
env_period: u8,
clock_shift: u8,
lfsr_7bit: bool,
divisor_code: u8,
length_load: u8,
length_en: bool,
active: bool,
dac_on: bool,
lfsr: u16,
freq_timer: u32,
pub(crate) length_counter: u8,
volume: u8,
env_timer: u8,
#[serde(default)]
env_clock_state: EnvelopeClockState,
#[serde(default)]
counter: u16,
#[serde(default)]
counter_countdown: u16,
#[serde(default)]
alignment: u32,
#[serde(default)]
countdown_reloaded: bool,
#[serde(default)]
counter_active: bool,
}
impl Default for Channel4 {
fn default() -> Self {
Self::new()
}
}
impl Channel4 {
pub fn new() -> Self {
Self {
init_volume: 0,
env_add: false,
env_period: 0,
clock_shift: 0,
lfsr_7bit: false,
divisor_code: 0,
length_load: 0,
length_en: false,
active: false,
dac_on: false,
lfsr: 0x7FFF,
freq_timer: 0,
length_counter: 0,
volume: 0,
env_timer: 0,
env_clock_state: EnvelopeClockState::default(),
counter: 0,
counter_countdown: 0,
alignment: 0,
countdown_reloaded: false,
counter_active: false,
}
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn length_en(&self) -> bool {
self.length_en
}
pub fn output(&self) -> f32 {
if !self.active || !self.dac_on {
return 0.0;
}
if self.lfsr & 0x01 == 0 {
self.volume as f32 / 15.0
} else {
0.0
}
}
pub fn digital_output(&self) -> u8 {
if !self.active || !self.dac_on {
return 0;
}
if self.lfsr & 0x01 == 0 {
self.volume
} else {
0
}
}
fn freq_timer_period(&self) -> u32 {
DIVISORS[self.divisor_code as usize] << self.clock_shift
}
pub fn tick(&mut self) {
let period = self.freq_timer_period();
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;
trace_apu!(5; "GB APU CH4 tick timer expired, clocking LFSR");
self.clock_lfsr();
}
}
self.alignment = self.alignment.wrapping_add(2);
if self.counter_active {
self.tick_counter_model();
}
}
fn tick_counter_model(&mut self) {
let reload = self.counter_reload();
if self.counter_countdown > 2 {
self.counter_countdown -= 2;
self.countdown_reloaded = false;
} else if self.counter_countdown == 2 {
self.counter = (self.counter + 1) & 0x3FFF;
self.counter_countdown = reload;
self.countdown_reloaded = true;
} else if self.counter_countdown == 1 {
self.counter = (self.counter + 1) & 0x3FFF;
self.counter_countdown = reload - 1;
self.countdown_reloaded = false;
} else {
self.counter_countdown = reload;
}
}
fn counter_reload(&self) -> u16 {
if self.divisor_code == 0 {
2
} else {
u16::from(self.divisor_code) * 4
}
}
pub fn clock_lfsr(&mut self) {
let xor = (self.lfsr & 0x01) ^ ((self.lfsr >> 1) & 0x01);
self.lfsr >>= 1;
self.lfsr |= xor << 14;
if self.lfsr_7bit {
self.lfsr &= !(1 << 6);
self.lfsr |= xor << 6;
}
trace_apu!(5; "GB APU CH4 LFSR shift mode={} lfsr=0x{:04X}",
if self.lfsr_7bit { "7-bit" } else { "15-bit" }, self.lfsr);
}
pub fn clock_length(&mut self) {
if !self.length_en || self.length_counter == 0 {
return;
}
self.length_counter -= 1;
trace_apu!(3; "GB APU CH4 length_counter={} active={}", self.length_counter, self.length_counter > 0);
if self.length_counter == 0 {
self.active = false;
}
}
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 CH4 envelope volume {} -> {}", old_volume, self.volume);
}
}
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.init_volume = 0;
self.env_add = false;
self.env_period = 0;
self.clock_shift = 0;
self.lfsr_7bit = false;
self.divisor_code = 0;
self.length_load = 0;
self.length_en = false;
self.active = false;
self.dac_on = false;
self.freq_timer = 0;
self.length_counter = 0;
self.volume = 0;
self.env_timer = 0;
self.env_clock_state = EnvelopeClockState::default();
self.reset_counter_model();
}
fn reset_counter_model(&mut self) {
self.counter = 0;
self.counter_countdown = 0;
self.alignment = 0;
self.countdown_reloaded = false;
self.counter_active = false;
}
pub fn read_nr42(&self) -> u8 {
((self.init_volume & 0x0F) << 4) | (u8::from(self.env_add) << 3) | (self.env_period & 0x07)
}
pub fn read_nr43(&self) -> u8 {
((self.clock_shift & 0x0F) << 4)
| (u8::from(self.lfsr_7bit) << 3)
| (self.divisor_code & 0x07)
}
pub fn read_nr44(&self) -> u8 {
0xBF | (u8::from(self.length_en) << 6)
}
pub fn write_nr41(&mut self, val: u8) {
trace_apu!(2; "GB APU CH4 write NR41=0x{:02X} length={}", val, val & 0x3F);
self.length_load = val & 0x3F;
self.length_counter = 64 - self.length_load;
}
pub fn write_nr42(&mut self, val: u8) {
trace_apu!(2; "GB APU CH4 write NR42=0x{:02X} volume={} env_add={} env_period={}",
val, (val >> 4) & 0x0F, (val & 0x08) != 0, val & 0x07);
let old_val = self.read_nr42();
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;
} else if self.active {
self.apply_nrx2_glitch(old_val, val);
}
}
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 CH4 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 CH4 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_nr43(&mut self, val: u8) {
trace_apu!(2; "GB APU CH4 write NR43=0x{:02X} shift={} mode={} divisor={}",
val, (val >> 4) & 0x0F, if (val & 0x08) != 0 { "7-bit" } else { "15-bit" }, val & 0x07);
if self.countdown_reloaded && self.counter_active {
let new_div = val & 0x07;
let new_divisor = if new_div == 0 {
2u16
} else {
u16::from(new_div) * 4
};
self.counter_countdown = if new_divisor == 2 || self.alignment & 2 != 0 {
new_divisor
} else {
new_divisor + 2
};
}
self.clock_shift = (val >> 4) & 0x0F;
self.lfsr_7bit = val & 0x08 != 0;
self.divisor_code = val & 0x07;
if self.counter_active {
self.recompute_freq_timer_from_counter();
}
}
fn recompute_freq_timer_from_counter(&mut self) {
let shift = u32::from(self.clock_shift);
let reload = u32::from(self.counter_reload());
let half_period = 1u32 << shift;
let full_period = half_period << 1;
let pos = u32::from(self.counter) & (full_period - 1);
let distance = if pos < half_period {
half_period - pos
} else {
full_period - pos + half_period
};
self.freq_timer = u32::from(self.counter_countdown) * 2 + (distance - 1) * reload * 2;
}
pub fn write_nr44(&mut self, val: u8, extra_clk: bool) {
self.write_nr44_with_apu_phase(val, extra_clk, None);
}
pub fn write_nr44_with_apu_phase(
&mut self,
val: u8,
extra_clk: bool,
double_speed_phase_bits: Option<u8>,
) {
self.write_nr44_with_apu_phase_and_length_quirk(
val,
extra_clk,
double_speed_phase_bits,
false,
);
}
pub fn write_nr44_with_apu_phase_and_length_quirk(
&mut self,
val: u8,
extra_clk: bool,
double_speed_phase_bits: Option<u8>,
cgb_early_extra_length_clock: bool,
) {
trace_apu!(2; "GB APU CH4 write NR44=0x{:02X} trigger={} length_en={}",
val, (val & 0x80) != 0, (val & 0x40) != 0);
let old_length_en = self.length_en;
self.length_en = val & 0x40 != 0;
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(double_speed_phase_bits);
if extra_clk && clocks_length_on_extra && self.length_counter == 64 {
self.length_counter = 63;
}
}
}
pub fn write_nr41_length_only(&mut self, val: u8) {
self.length_load = val & 0x3F;
self.length_counter = 64 - self.length_load;
}
fn trigger(&mut self, double_speed_phase_bits: Option<u8>) {
trace_apu!(1; "GB APU CH4 trigger volume={} shift={} mode={} divisor={}",
self.init_volume, self.clock_shift,
if self.lfsr_7bit { "7-bit" } else { "15-bit" }, self.divisor_code);
let was_active = self.active;
if self.dac_on {
self.active = true;
}
if self.length_counter == 0 {
self.length_counter = 64;
}
let period = self.freq_timer_period();
let freq_timer = if let Some(phase_bits) = double_speed_phase_bits {
let delay_t = 6u32 - 2 * u32::from(phase_bits & 1);
let delay_t = if was_active {
delay_t.saturating_sub(2)
} else {
delay_t
};
period + delay_t
} else {
match self.read_nr43() {
0x09 => {
if self.freq_timer <= 4 { 16 } else { 20 }
}
0x18 => 16, 0x0A => {
if self.freq_timer >= 20 { 24 } else { 20 }
}
0x28 => 24, 0x0B => {
if self.freq_timer >= 36 { 32 } else { 28 }
}
0x0C | 0x1A => {
if self.freq_timer >= 52 { 40 } else { 36 }
}
0x29 => {
if self.freq_timer >= 52 { 40 } else { 44 }
}
0x38 => 40, _ => {
if was_active {
period * 2 + 4
} else {
period + 4
}
}
}
};
self.freq_timer = freq_timer;
self.volume = self.init_volume;
self.env_timer = self.env_period;
self.lfsr = 0x7FFF;
self.env_clock_state = EnvelopeClockState::default();
let div = self.divisor_code;
let mut initial_cd = if div == 0 {
6u16
} else {
u16::from(div) * 4 + 6
};
if div > 1 {
if self.alignment & 2 != 0 {
initial_cd = initial_cd.saturating_sub(2);
} else {
initial_cd = initial_cd.saturating_sub(4);
}
}
self.counter_countdown = initial_cd;
self.countdown_reloaded = false;
self.counter_active = true;
}
pub fn apu_init(&mut self) {
self.reset_counter_model();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn triggered_ch4() -> Channel4 {
let mut ch = Channel4::new();
ch.write_nr42(0xF1); ch.write_nr44(0x80, false); ch
}
#[test]
fn test_trigger_makes_channel_active() {
assert!(triggered_ch4().is_active());
}
#[test]
fn test_dac_off_prevents_activation() {
let mut ch = Channel4::new();
ch.write_nr42(0x00);
ch.write_nr44(0x80, false);
assert!(!ch.is_active());
}
#[test]
fn test_trigger_resets_lfsr_to_7fff() {
assert_eq!(triggered_ch4().lfsr, 0x7FFF);
}
#[test]
fn test_length_expiry_silences_when_enabled() {
let mut ch = Channel4::new();
ch.write_nr42(0xF1);
ch.write_nr41(0x3F); ch.write_nr44(0xC0, false); ch.clock_length();
assert!(!ch.is_active());
}
#[test]
fn test_length_no_expire_when_disabled() {
let mut ch = Channel4::new();
ch.write_nr42(0xF1);
ch.write_nr41(0x3F);
ch.write_nr44(0x80, false);
ch.clock_length();
assert!(ch.is_active());
}
#[test]
fn test_envelope_decrements_volume() {
let mut ch = Channel4::new();
ch.write_nr42(0x71); ch.write_nr44(0x80, false);
ch.clock_envelope();
assert_eq!(ch.volume, 6);
}
#[test]
fn test_envelope_increments_volume() {
let mut ch = Channel4::new();
ch.write_nr42(0x79); ch.write_nr44(0x80, false);
ch.clock_envelope();
assert_eq!(ch.volume, 8);
}
#[test]
fn test_15bit_lfsr_one_clock() {
let mut ch = Channel4::new();
ch.write_nr42(0xF0);
ch.write_nr43(0x00); ch.write_nr44(0x80, false); ch.clock_lfsr();
assert_eq!(ch.lfsr, 0x3FFF);
}
#[test]
fn test_7bit_lfsr_sets_bit6_to_xor() {
let mut ch = Channel4::new();
ch.write_nr42(0xF0);
ch.write_nr43(0x08); ch.write_nr44(0x80, false);
ch.clock_lfsr();
assert_eq!(ch.lfsr & (1 << 6), 0);
}
#[test]
fn test_15bit_and_7bit_produce_different_patterns() {
let mut ch15 = Channel4::new();
ch15.write_nr42(0xF0);
ch15.write_nr43(0x00);
ch15.write_nr44(0x80, false);
let mut ch7 = Channel4::new();
ch7.write_nr42(0xF0);
ch7.write_nr43(0x08);
ch7.write_nr44(0x80, false);
let bits15: Vec<u8> = (0..32)
.map(|_| {
ch15.clock_lfsr();
(ch15.lfsr & 1) as u8
})
.collect();
let bits7: Vec<u8> = (0..32)
.map(|_| {
ch7.clock_lfsr();
(ch7.lfsr & 1) as u8
})
.collect();
assert_ne!(bits15, bits7);
}
#[test]
fn test_nr42_read_back() {
let mut ch = Channel4::new();
ch.write_nr42(0xF3);
assert_eq!(ch.read_nr42(), 0xF3);
}
#[test]
fn test_nr43_read_back() {
let mut ch = Channel4::new();
ch.write_nr43(0xAB);
assert_eq!(ch.read_nr43(), 0xAB);
}
#[test]
fn test_nr44_reads_length_en() {
let mut ch = Channel4::new();
ch.write_nr42(0xF0);
ch.write_nr44(0x40, false);
assert_eq!(ch.read_nr44() & 0x40, 0x40);
}
#[test]
fn test_output_zero_when_inactive() {
assert_eq!(Channel4::new().output(), 0.0);
}
#[test]
fn test_double_speed_phase_sets_initial_noise_timer() {
let mut phase0 = Channel4::new();
phase0.write_nr42(0xF0);
phase0.write_nr43(0x00);
phase0.write_nr44_with_apu_phase(0x80, false, Some(0));
let mut phase1 = Channel4::new();
phase1.write_nr42(0xF0);
phase1.write_nr43(0x00);
phase1.write_nr44_with_apu_phase(0x80, false, Some(1));
assert_eq!(phase0.freq_timer, 14);
assert_eq!(phase1.freq_timer, 12);
}
#[test]
fn test_double_speed_phase_uses_shifted_noise_period() {
let mut ch = Channel4::new();
ch.write_nr42(0xF0);
ch.write_nr43(0x40); ch.write_nr44_with_apu_phase(0x80, false, Some(0));
assert_eq!(ch.freq_timer, 134);
}
#[test]
fn test_normal_speed_frequency_alignment_startup_timer() {
let mut ch = Channel4::new();
ch.write_nr42(0xF0);
ch.write_nr43(0x09);
ch.freq_timer = 4;
ch.write_nr44(0x80, false);
assert_eq!(ch.freq_timer, 16);
}
#[test]
fn test_normal_speed_default_startup_timer_uses_shifted_period() {
let mut ch = Channel4::new();
ch.write_nr42(0xF0);
ch.write_nr43(0x40); ch.write_nr44(0x80, false);
assert_eq!(ch.freq_timer, 132);
}
#[test]
fn test_tick_freq_timer_decrements_by_tcycles_within_mcycle() {
let mut ch = Channel4::new();
ch.write_nr42(0xF0);
ch.write_nr43(0x00); ch.write_nr44(0x80, false); ch.freq_timer = 6;
let lfsr_before = ch.lfsr;
ch.tick();
assert_eq!(
ch.freq_timer, 2,
"freq_timer should decrement to 2 after one M-cycle"
);
assert_eq!(ch.lfsr, lfsr_before, "LFSR should not clock when timer > 0");
}
#[test]
fn test_tick_freq_timer_expires_mid_mcycle_and_reloads_with_remainder() {
let mut ch = Channel4::new();
ch.write_nr42(0xF0);
ch.write_nr43(0x00); ch.write_nr44(0x80, false); ch.freq_timer = 3;
ch.tick();
assert_eq!(
ch.freq_timer, 7,
"freq_timer should be period - remaining (8 - 1 = 7)"
);
assert_eq!(
ch.lfsr, 0x3FFF,
"LFSR should clock once when timer expires mid M-cycle"
);
}
#[test]
fn test_tick_freq_timer_expires_exactly_at_mcycle_boundary() {
let mut ch = Channel4::new();
ch.write_nr42(0xF0);
ch.write_nr43(0x00); ch.write_nr44(0x80, false); ch.freq_timer = 4;
ch.tick();
assert_eq!(
ch.freq_timer, 8,
"freq_timer should be exactly period after expiring at boundary"
);
assert_eq!(ch.lfsr, 0x3FFF, "LFSR should clock once");
}
}