pub mod channel1;
pub mod channel2;
pub mod channel3;
pub mod channel4;
use channel1::Channel1;
use channel2::Channel2;
use channel3::Channel3;
use channel4::Channel4;
const DMG_MCYCLES_PER_SEC: f32 = 1_048_576.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], ];
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,
}
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,
}
}
pub fn set_sample_rate(&mut self, rate: f32) {
self.cycles_per_sample = DMG_MCYCLES_PER_SEC / rate;
}
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;
let flags = FS_TABLE[self.fs_step as usize];
if flags & 0x01 != 0 {
self.ch1.clock_length();
self.ch2.clock_length();
self.ch3.clock_length();
self.ch4.clock_length();
}
if flags & 0x02 != 0 {
self.ch1.clock_sweep();
}
if flags & 0x04 != 0 {
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(&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;
(left_mix * left_volume + right_mix * right_volume) / 2.0
}
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 => self.nr50 = val,
0xFF25 => 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 {
self.ch1.power_off();
self.ch2.power_off();
self.ch3.power_off();
self.ch4.power_off();
self.nr50 = 0x00;
self.nr51 = 0x00;
} else if !was_powered && self.powered {
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"
);
}
}