pub mod channel1;
pub mod channel2;
pub mod channel3;
pub mod channel4;
use crate::trace_apu;
use serde::{Deserialize, Serialize};
use channel1::Channel1;
use channel2::Channel2;
use channel3::Channel3;
use channel4::Channel4;
const DMG_MCYCLES_PER_SEC: f32 = 1_048_576.0;
const HP_CUTOFF_HZ: f32 = 7.0;
const FS_MCYCLES_PER_STEP: u16 = 2048;
#[rustfmt::skip]
const FS_TABLE: [u8; 8] = [
0b001, 0b000, 0b011, 0b000, 0b001, 0b000, 0b011, 0b100, ];
const DUTY_TABLE: [[u8; 8]; 4] = [
[0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 1, 1, 1, 1], [0, 1, 1, 1, 1, 1, 1, 0], ];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Apu {
ch1: Channel1,
ch2: Channel2,
ch3: Channel3,
ch4: Channel4,
nr50: u8,
nr51: u8,
powered: bool,
fs_timer: u16,
fs_step: u8,
sample_acc: f32,
cycles_per_sample: f32,
pending_sample: Option<f32>,
is_cgb: bool,
#[serde(default)]
hp_prev_in: f32,
#[serde(default)]
hp_prev_out: f32,
#[serde(skip, default = "Apu::default_hp_rc")]
hp_rc: f32,
}
impl Apu {
pub fn new(is_cgb: bool) -> Self {
Self {
ch1: Channel1::new(),
ch2: Channel2::new(),
ch3: Channel3::new_with_mode(is_cgb),
ch4: Channel4::new(),
nr50: 0x00,
nr51: 0x00,
powered: false,
fs_timer: FS_MCYCLES_PER_STEP,
fs_step: 0,
sample_acc: 0.0,
cycles_per_sample: DMG_MCYCLES_PER_SEC / 44_100.0,
pending_sample: None,
is_cgb,
hp_prev_in: 0.0,
hp_prev_out: 0.0,
hp_rc: Self::compute_hp_rc(44_100.0),
}
}
fn compute_hp_rc(sample_rate: f32) -> f32 {
sample_rate / (sample_rate + 2.0 * std::f32::consts::PI * HP_CUTOFF_HZ)
}
fn default_hp_rc() -> f32 {
Self::compute_hp_rc(44_100.0)
}
pub fn set_sample_rate(&mut self, rate: f32) {
self.cycles_per_sample = DMG_MCYCLES_PER_SEC / rate;
self.hp_rc = Self::compute_hp_rc(rate);
}
pub fn sample_rate(&self) -> f32 {
DMG_MCYCLES_PER_SEC / self.cycles_per_sample
}
pub fn sample_ready(&self) -> bool {
self.pending_sample.is_some()
}
pub fn take_sample(&mut self) -> Option<f32> {
self.pending_sample.take()
}
pub fn tick(&mut self, m_cycles: u8) {
for _ in 0..m_cycles {
self.tick_one();
}
}
fn tick_one(&mut self) {
self.fs_timer -= 1;
if self.fs_timer == 0 {
self.fs_timer = FS_MCYCLES_PER_STEP;
trace_apu!(3; "GB APU FS step={} length={} sweep={} envelope={}",
self.fs_step,
(FS_TABLE[self.fs_step as usize] & 0x01) != 0,
(FS_TABLE[self.fs_step as usize] & 0x02) != 0,
(FS_TABLE[self.fs_step as usize] & 0x04) != 0);
let flags = FS_TABLE[self.fs_step as usize];
if flags & 0x01 != 0 {
trace_apu!(4; "GB APU clock length counters");
self.ch1.clock_length();
self.ch2.clock_length();
self.ch3.clock_length();
self.ch4.clock_length();
}
if flags & 0x02 != 0 {
trace_apu!(4; "GB APU clock CH1 sweep");
self.ch1.clock_sweep();
}
if flags & 0x04 != 0 {
trace_apu!(4; "GB APU clock envelopes");
self.ch1.clock_envelope();
self.ch2.clock_envelope();
self.ch4.clock_envelope();
}
self.fs_step = (self.fs_step + 1) & 7;
}
self.ch1.tick();
self.ch2.tick();
self.ch3.tick();
self.ch4.tick();
self.sample_acc += 1.0;
if self.sample_acc >= self.cycles_per_sample {
self.sample_acc -= self.cycles_per_sample;
if self.pending_sample.is_none() {
self.pending_sample = Some(self.mix());
}
}
}
fn mix(&mut self) -> f32 {
if !self.powered {
return 0.0;
}
let samples = [
self.ch1.output(),
self.ch2.output(),
self.ch3.output(),
self.ch4.output(),
];
let left_mix = Self::mix_terminal(samples, self.nr51 >> 4);
let right_mix = Self::mix_terminal(samples, self.nr51 & 0x0F);
let left_volume = ((self.nr50 >> 4) & 0x07) as f32 / 7.0;
let right_volume = (self.nr50 & 0x07) as f32 / 7.0;
let raw = (left_mix * left_volume + right_mix * right_volume) / 2.0;
let hp_out = self.hp_rc * (self.hp_prev_out + raw - self.hp_prev_in);
self.hp_prev_in = raw;
self.hp_prev_out = hp_out;
let final_out = hp_out.clamp(-1.0, 1.0);
trace_apu!(5; "GB APU mix ch1={:.3} ch2={:.3} ch3={:.3} ch4={:.3} left={:.3} right={:.3} raw={:.3} hp={:.3} out={:.3}",
samples[0], samples[1], samples[2], samples[3],
left_mix * left_volume, right_mix * right_volume,
raw, hp_out, final_out);
final_out
}
fn mix_terminal(samples: [f32; 4], enable_mask: u8) -> f32 {
samples
.iter()
.enumerate()
.map(|(i, &s)| if enable_mask & (1 << i) != 0 { s } else { 0.0 })
.sum::<f32>()
/ 4.0
}
pub fn read_register(&self, addr: u16) -> u8 {
match addr {
0xFF10 => self.ch1.read_nr10(),
0xFF11 => self.ch1.read_nr11(),
0xFF12 => self.ch1.read_nr12(),
0xFF13 => 0xFF, 0xFF14 => self.ch1.read_nr14(),
0xFF15 => 0xFF, 0xFF16 => self.ch2.read_nr21(),
0xFF17 => self.ch2.read_nr22(),
0xFF18 => 0xFF, 0xFF19 => self.ch2.read_nr24(),
0xFF1A => self.ch3.read_nr30(),
0xFF1B => 0xFF, 0xFF1C => self.ch3.read_nr32(),
0xFF1D => 0xFF, 0xFF1E => self.ch3.read_nr34(),
0xFF1F => 0xFF, 0xFF20 => 0xFF, 0xFF21 => self.ch4.read_nr42(),
0xFF22 => self.ch4.read_nr43(),
0xFF23 => self.ch4.read_nr44(),
0xFF24 => self.nr50,
0xFF25 => self.nr51,
0xFF26 => self.read_nr52(),
0xFF30..=0xFF3F => self.ch3.read_wave_ram(addr),
_ => 0xFF,
}
}
fn read_nr52(&self) -> u8 {
let mut nr52 = 0x70;
nr52 |= u8::from(self.powered) << 7;
nr52 |= u8::from(self.ch1.is_active());
nr52 |= u8::from(self.ch2.is_active()) << 1;
nr52 |= u8::from(self.ch3.is_active()) << 2;
nr52 |= u8::from(self.ch4.is_active()) << 3;
nr52
}
pub fn write_register(&mut self, addr: u16, val: u8) {
if addr == 0xFF26 {
self.write_nr52(val);
return;
}
if (0xFF30..=0xFF3F).contains(&addr) {
self.ch3.write_wave_ram(addr, val);
return;
}
if !self.powered {
if !self.is_cgb {
match addr {
0xFF11 => self.ch1.write_nr11_length_only(val),
0xFF16 => self.ch2.write_nr21_length_only(val),
0xFF1B => self.ch3.write_nr31_length_only(val),
0xFF20 => self.ch4.write_nr41_length_only(val),
_ => {}
}
}
return;
}
let extra_clk = FS_TABLE[self.fs_step as usize] & 0x01 == 0;
match addr {
0xFF10 => self.ch1.write_nr10(val),
0xFF11 => self.ch1.write_nr11(val),
0xFF12 => self.ch1.write_nr12(val),
0xFF13 => self.ch1.write_nr13(val),
0xFF14 => self.ch1.write_nr14(val, extra_clk),
0xFF15 => {}
0xFF16 => self.ch2.write_nr21(val),
0xFF17 => self.ch2.write_nr22(val),
0xFF18 => self.ch2.write_nr23(val),
0xFF19 => self.ch2.write_nr24(val, extra_clk),
0xFF1A => self.ch3.write_nr30(val),
0xFF1B => self.ch3.write_nr31(val),
0xFF1C => self.ch3.write_nr32(val),
0xFF1D => self.ch3.write_nr33(val),
0xFF1E => self.ch3.write_nr34(val, extra_clk),
0xFF1F => {}
0xFF20 => self.ch4.write_nr41(val),
0xFF21 => self.ch4.write_nr42(val),
0xFF22 => self.ch4.write_nr43(val),
0xFF23 => self.ch4.write_nr44(val, extra_clk),
0xFF24 => {
trace_apu!(2; "GB APU write NR50=0x{:02X} left_vol={} right_vol={}", val, (val >> 4) & 0x07, val & 0x07);
self.nr50 = val;
}
0xFF25 => {
trace_apu!(2; "GB APU write NR51=0x{:02X} left_en=0x{:X} right_en=0x{:X}", val, val >> 4, val & 0x0F);
self.nr51 = val;
}
_ => {}
}
}
fn write_nr52(&mut self, val: u8) {
let was_powered = self.powered;
self.powered = val & 0x80 != 0;
if was_powered && !self.powered {
trace_apu!(1; "GB APU power off");
self.ch1.power_off();
self.ch2.power_off();
self.ch3.power_off();
self.ch4.power_off();
self.nr50 = 0x00;
self.nr51 = 0x00;
self.hp_prev_in = 0.0;
self.hp_prev_out = 0.0;
} else if !was_powered && self.powered {
trace_apu!(1; "GB APU power on");
self.fs_step = 0;
self.fs_timer = FS_MCYCLES_PER_STEP;
if self.is_cgb {
self.ch1.length_counter = 0;
self.ch2.length_counter = 0;
self.ch3.length_counter = 0;
self.ch4.length_counter = 0;
}
}
}
pub fn read_wave_ram(&self, addr: u16) -> u8 {
self.ch3.read_wave_ram(addr)
}
}
impl Default for Apu {
fn default() -> Self {
Self::new(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn powered_apu() -> Apu {
let mut apu = Apu::new(false);
apu.write_register(0xFF26, 0x80); apu
}
#[test]
fn test_nr52_power_on_bit_readable() {
let apu = powered_apu();
assert_eq!(apu.read_register(0xFF26) & 0x80, 0x80);
}
#[test]
fn test_nr52_power_off_bit_readable() {
let apu = Apu::new(false);
assert_eq!(apu.read_register(0xFF26) & 0x80, 0x00);
}
#[test]
fn test_nr52_unused_bits_read_as_1() {
let apu = Apu::new(false);
assert_eq!(apu.read_register(0xFF26) & 0x70, 0x70);
}
#[test]
fn test_power_off_clears_nr50() {
let mut apu = powered_apu();
apu.write_register(0xFF24, 0x77);
assert_eq!(apu.read_register(0xFF24), 0x77);
apu.write_register(0xFF26, 0x00); assert_eq!(apu.nr50, 0x00);
}
#[test]
fn test_power_off_clears_nr51() {
let mut apu = powered_apu();
apu.write_register(0xFF25, 0xF3);
apu.write_register(0xFF26, 0x00);
assert_eq!(apu.nr51, 0x00);
}
#[test]
fn test_power_off_ignores_nr50_write() {
let mut apu = Apu::new(false); apu.write_register(0xFF24, 0x77);
assert_eq!(apu.nr50, 0x00, "NR50 write must be ignored when APU is off");
}
#[test]
fn test_power_on_allows_nr50_write() {
let mut apu = powered_apu();
apu.write_register(0xFF24, 0x77);
assert_eq!(apu.nr50, 0x77);
}
#[test]
fn test_nr51_write_read_roundtrip() {
let mut apu = powered_apu();
apu.write_register(0xFF25, 0xAB);
assert_eq!(apu.read_register(0xFF25), 0xAB);
}
#[test]
fn test_sample_not_ready_initially() {
let apu = Apu::new(false);
assert!(!apu.sample_ready());
}
#[test]
fn test_sample_ready_after_enough_ticks() {
let mut apu = Apu::new(false);
apu.set_sample_rate(44_100.0);
apu.tick(30);
assert!(apu.sample_ready(), "expected a sample after 30 M-cycles");
}
#[test]
fn test_take_sample_clears_ready_flag() {
let mut apu = Apu::new(false);
apu.tick(30);
apu.take_sample();
assert!(!apu.sample_ready());
}
#[test]
fn test_sample_is_silent_when_powered_off() {
let mut apu = Apu::new(false);
apu.tick(30);
let s = apu.take_sample().unwrap_or(0.0);
assert_eq!(s, 0.0);
}
#[test]
fn test_frame_sequencer_advances_after_2048_mcycles() {
let mut apu = powered_apu();
assert_eq!(apu.fs_step, 0);
apu.tick(100); for _ in 0..1948u16 {
apu.tick(1);
}
assert_eq!(apu.fs_step, 1, "fs_step must be 1 after 2048 M-cycles");
}
#[test]
fn test_frame_sequencer_wraps_at_8() {
let mut apu = powered_apu();
for _ in 0u32..16_384 {
apu.tick(1);
}
assert_eq!(apu.fs_step, 0, "fs_step must wrap back to 0 after 8 steps");
}
#[test]
fn test_registers_return_cleared_values_when_powered_off() {
let mut apu = powered_apu();
apu.write_register(0xFF26, 0x00); assert_eq!(
apu.read_register(0xFF10),
0x80,
"NR10 must return cleared 0x80 when powered off (not 0xFF)"
);
assert_eq!(
apu.read_register(0xFF12),
0x00,
"NR12 must return cleared 0x00 when powered off (not 0xFF)"
);
}
#[test]
fn test_nr52_ch1_active_bit_reflects_channel1_state() {
let apu = Apu::new(false);
assert_eq!(apu.read_register(0xFF26) & 0x01, 0x00);
}
fn powered_cgb_apu() -> Apu {
let mut apu = Apu::new(true);
apu.write_register(0xFF26, 0x80); apu
}
#[test]
fn test_cgb_power_off_rejects_nr41_length_write() {
let mut apu = powered_cgb_apu();
apu.write_register(0xFF20, 0x3F); apu.write_register(0xFF26, 0x00); apu.write_register(0xFF20, 0x3F); assert_eq!(
apu.ch4.length_counter, 0,
"CGB must reject length writes when APU is off"
);
}
#[test]
fn test_cgb_power_off_rejects_nr11_length_write() {
let mut apu = Apu::new(true);
apu.write_register(0xFF11, 0x3F); assert_eq!(
apu.ch1.length_counter, 0,
"CGB must reject CH1 length writes when APU is off"
);
}
#[test]
fn test_cgb_power_off_rejects_nr21_length_write() {
let mut apu = Apu::new(true);
apu.write_register(0xFF16, 0x3F);
assert_eq!(
apu.ch2.length_counter, 0,
"CGB must reject CH2 length writes when APU is off"
);
}
#[test]
fn test_cgb_power_off_rejects_nr31_length_write() {
let mut apu = Apu::new(true);
apu.write_register(0xFF1B, 0xFF); assert_eq!(
apu.ch3.length_counter, 0,
"CGB must reject CH3 length writes when APU is off"
);
}
#[test]
fn test_dmg_power_off_allows_nr41_length_write() {
let mut apu = Apu::new(false);
apu.write_register(0xFF20, 0x3F); assert_eq!(
apu.ch4.length_counter, 1,
"DMG must allow length writes when APU is off"
);
}
#[test]
fn test_cgb_power_on_resets_length_counters() {
let mut apu = powered_cgb_apu();
apu.write_register(0xFF11, 0x00); apu.write_register(0xFF16, 0x00); apu.write_register(0xFF1B, 0x00); apu.write_register(0xFF20, 0x00); assert_eq!(apu.ch1.length_counter, 64);
assert_eq!(apu.ch4.length_counter, 64);
apu.write_register(0xFF26, 0x00);
apu.write_register(0xFF26, 0x80);
assert_eq!(
apu.ch1.length_counter, 0,
"CGB power-on must reset CH1 length"
);
assert_eq!(
apu.ch2.length_counter, 0,
"CGB power-on must reset CH2 length"
);
assert_eq!(
apu.ch3.length_counter, 0,
"CGB power-on must reset CH3 length"
);
assert_eq!(
apu.ch4.length_counter, 0,
"CGB power-on must reset CH4 length"
);
}
#[test]
fn test_mix_hp_filter_produces_negative_transient_when_input_drops() {
let mut apu = powered_apu();
apu.hp_prev_in = 1.0;
apu.hp_prev_out = 0.5;
let sample = apu.mix();
assert!(
sample < 0.0,
"HP filter must produce a negative transient when input drops from high to zero, got {sample}"
);
}
#[test]
fn test_mix_hp_filter_output_stays_in_bipolar_range() {
let mut apu = powered_apu();
apu.write_register(0xFF11, 0x80); apu.write_register(0xFF12, 0xF0); apu.write_register(0xFF14, 0x80);
apu.write_register(0xFF16, 0x80); apu.write_register(0xFF17, 0xF0); apu.write_register(0xFF19, 0x80);
apu.write_register(0xFF1A, 0x80); apu.write_register(0xFF1C, 0x20); apu.write_register(0xFF1E, 0x80);
apu.write_register(0xFF21, 0xF0); apu.write_register(0xFF23, 0x80);
apu.write_register(0xFF25, 0xFF);
apu.write_register(0xFF24, 0x77);
let mut violations = 0u32;
for _ in 0..60_000 {
apu.tick(1);
if let Some(s) = apu.take_sample()
&& !(-1.0..=1.0).contains(&s)
{
violations += 1;
}
}
assert_eq!(
violations, 0,
"mix() must never exceed [-1.0, 1.0]; found {violations} violation(s)"
);
}
#[test]
fn test_mix_active_channel_produces_negative_samples_after_filter_converges() {
let mut apu = powered_apu();
apu.write_register(0xFF11, 0x80); apu.write_register(0xFF12, 0xF0); apu.write_register(0xFF13, 0x80); apu.write_register(0xFF14, 0x84); apu.write_register(0xFF25, 0x01); apu.write_register(0xFF24, 0x07);
let mut found_negative = false;
for _ in 0..71_340 {
apu.tick(1);
if let Some(s) = apu.take_sample()
&& s < 0.0
{
found_negative = true;
break;
}
}
assert!(
found_negative,
"HP filter must produce negative samples once DC is removed from an oscillating channel"
);
}
}