- Fix frame counter running at 2× speed: clock_frame_counter now skips odd CPU cycles (APU cycle = CPU/2), so envelope, sweep, and length counters tick at the correct rate. Fixes sweep-driven whistle in Megaman II. - Switch audio sampling to per-CPU-cycle granularity in run_until_frame_complete_with_audio to eliminate square-wave harmonic aliasing caused by sampling only once per instruction. - Add IIR one-pole low-pass filter (~14 kHz) to AudioMixer to smooth abrupt level transitions (crackling) introduced by correct envelope timing. - Mute triangle channel when timer_period < 2 (≥27 kHz), which aliases into the audible range at 48 kHz. Real NES RC circuit removes these ultrasonics; emulator must suppress them explicitly. - Update all APU bus tests to use correct (doubled) CPU cycle counts.
335 lines
11 KiB
Rust
335 lines
11 KiB
Rust
use super::*;
|
|
|
|
#[test]
|
|
fn apu_frame_irq_asserts_in_4_step_mode() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
|
|
|
for _ in 0..29_832u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
|
|
assert!(bus.poll_irq(), "APU frame IRQ should assert in 4-step mode");
|
|
}
|
|
|
|
#[test]
|
|
fn reading_4015_clears_apu_frame_irq_flag() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
|
|
|
for _ in 0..29_832u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
|
|
let status = bus.read(0x4015);
|
|
assert_ne!(status & 0x40, 0, "frame IRQ bit should be set in status");
|
|
assert!(!bus.poll_irq(), "reading 4015 should clear frame IRQ");
|
|
}
|
|
|
|
#[test]
|
|
fn apu_frame_irq_inhibit_bit_disables_irq_and_clears_pending() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
|
for _ in 0..29_832u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert!(bus.poll_irq());
|
|
|
|
bus.write(0x4017, 0x40); // 4-step, IRQ inhibit
|
|
assert!(
|
|
!bus.poll_irq(),
|
|
"inhibit write should clear pending frame IRQ"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn writing_4015_does_not_acknowledge_apu_frame_irq() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
|
for _ in 0..29_832u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert!(bus.poll_irq(), "frame IRQ must be pending");
|
|
|
|
// Recreate pending frame IRQ and ensure $4015 write does not clear it.
|
|
for _ in 0..29_832u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
bus.write(0x4015, 0x00);
|
|
assert!(bus.poll_irq(), "writing $4015 must not clear frame IRQ");
|
|
|
|
// Reading $4015 still acknowledges frame IRQ as expected.
|
|
let _ = bus.read(0x4015);
|
|
assert!(!bus.poll_irq(), "reading $4015 should clear frame IRQ");
|
|
}
|
|
|
|
#[test]
|
|
fn apu_5step_mode_does_not_generate_frame_irq() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4017, 0x80); // 5-step mode
|
|
|
|
for _ in 0..20_000u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert!(!bus.poll_irq(), "5-step mode must not assert frame IRQ");
|
|
}
|
|
|
|
#[test]
|
|
fn apu_write_only_register_reads_return_cpu_open_bus() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4000, 0x12);
|
|
bus.write(0x0000, 0xAB);
|
|
assert_eq!(bus.read(0x0000), 0xAB);
|
|
|
|
assert_eq!(bus.read(0x4000), 0xAB);
|
|
assert_eq!(bus.read(0x400E), 0xAB);
|
|
}
|
|
|
|
#[test]
|
|
fn writing_4017_in_5step_mode_clocks_half_frame_after_delay() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4015, 0x01); // enable pulse1
|
|
bus.write(0x4000, 0x00); // length halt disabled
|
|
bus.write(0x4003, 0x18); // length index 3 => 2
|
|
assert_eq!(bus.apu.length_counters[0], 2);
|
|
|
|
bus.write(0x4017, 0x80); // switch to 5-step mode
|
|
assert_eq!(bus.apu.length_counters[0], 2);
|
|
for _ in 0..2u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert_eq!(bus.apu.length_counters[0], 2);
|
|
bus.clock_cpu(1); // reset delay complete (3 CPU cycles on even phase)
|
|
assert_eq!(bus.apu.length_counters[0], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn state_roundtrip_preserves_apu_frame_counter_fields() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.apu.frame_cycle = 777;
|
|
bus.apu.frame_mode_5step = true;
|
|
bus.apu.frame_irq_inhibit = true;
|
|
bus.apu.frame_irq_pending = true;
|
|
bus.apu.channel_enable_mask = 0x1F;
|
|
bus.apu.length_counters = [1, 2, 3, 4];
|
|
bus.apu.dmc_bytes_remaining = 99;
|
|
bus.apu.dmc_irq_enabled = true;
|
|
bus.apu.dmc_irq_pending = true;
|
|
bus.apu.dmc_cycle_counter = 1234;
|
|
bus.apu.envelope_divider = [9, 8, 7];
|
|
bus.apu.envelope_decay = [6, 5, 4];
|
|
bus.apu.envelope_start_flags = 0x05;
|
|
bus.apu.triangle_linear_counter = 3;
|
|
bus.apu.triangle_linear_reload_flag = true;
|
|
bus.apu.sweep_divider = [11, 12];
|
|
bus.apu.sweep_reload_flags = 0x03;
|
|
bus.apu.cpu_cycle_parity = true;
|
|
bus.apu.frame_reset_pending = true;
|
|
bus.apu.frame_reset_delay = 2;
|
|
bus.apu.pending_frame_mode_5step = true;
|
|
bus.apu.pending_frame_irq_inhibit = false;
|
|
|
|
let mut raw = Vec::new();
|
|
bus.save_state(&mut raw);
|
|
|
|
let mut restored = NativeBus::new(Box::new(StubMapper));
|
|
restored.load_state(&raw).expect("state should load");
|
|
assert_eq!(restored.apu.frame_cycle, 777);
|
|
assert!(restored.apu.frame_mode_5step);
|
|
assert!(restored.apu.frame_irq_inhibit);
|
|
assert!(restored.apu.frame_irq_pending);
|
|
assert_eq!(restored.apu.channel_enable_mask, 0x1F);
|
|
assert_eq!(restored.apu.length_counters, [1, 2, 3, 4]);
|
|
assert_eq!(restored.apu.dmc_bytes_remaining, 99);
|
|
assert!(restored.apu.dmc_irq_enabled);
|
|
assert!(restored.apu.dmc_irq_pending);
|
|
assert_eq!(restored.apu.dmc_cycle_counter, 1234);
|
|
assert_eq!(restored.apu.envelope_divider, [9, 8, 7]);
|
|
assert_eq!(restored.apu.envelope_decay, [6, 5, 4]);
|
|
assert_eq!(restored.apu.envelope_start_flags, 0x05);
|
|
assert_eq!(restored.apu.triangle_linear_counter, 3);
|
|
assert!(restored.apu.triangle_linear_reload_flag);
|
|
assert_eq!(restored.apu.sweep_divider, [11, 12]);
|
|
assert_eq!(restored.apu.sweep_reload_flags, 0x03);
|
|
assert!(restored.apu.cpu_cycle_parity);
|
|
assert!(restored.apu.frame_reset_pending);
|
|
assert_eq!(restored.apu.frame_reset_delay, 2);
|
|
assert!(restored.apu.pending_frame_mode_5step);
|
|
assert!(!restored.apu.pending_frame_irq_inhibit);
|
|
}
|
|
|
|
#[test]
|
|
fn apu_status_reflects_length_counters_and_disable_clears_them() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4015, 0x0F); // enable pulse1/pulse2/triangle/noise
|
|
bus.write(0x4003, 0xF8); // load pulse1 length index 31
|
|
bus.write(0x4007, 0xF8); // load pulse2
|
|
bus.write(0x400B, 0xF8); // load triangle
|
|
bus.write(0x400F, 0xF8); // load noise
|
|
|
|
let status = bus.read(0x4015);
|
|
assert_eq!(status & 0x0F, 0x0F);
|
|
|
|
bus.write(0x4015, 0x00);
|
|
let status2 = bus.read(0x4015);
|
|
assert_eq!(status2 & 0x0F, 0x00);
|
|
}
|
|
|
|
#[test]
|
|
fn apu_length_counter_decrements_on_half_frame_when_not_halted() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4015, 0x01); // enable pulse1
|
|
bus.write(0x4000, 0x00); // halt=0
|
|
bus.write(0x4003, 0x18); // length index 3 => value 2
|
|
|
|
assert_eq!(bus.apu.length_counters[0], 2);
|
|
for _ in 0..14_913u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert_eq!(bus.apu.length_counters[0], 1);
|
|
for _ in 0..14_916u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert_eq!(bus.apu.length_counters[0], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn dmc_irq_raises_and_is_reported_in_4015_status() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4010, 0x8F); // IRQ enable, no loop, fastest rate
|
|
bus.write(0x4013, 0x00); // sample length = 1 byte
|
|
bus.write(0x4015, 0x10); // enable DMC
|
|
|
|
for _ in 0..54u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert!(bus.poll_irq());
|
|
|
|
let status = bus.read(0x4015);
|
|
assert_ne!(status & 0x80, 0, "DMC IRQ should be visible in status");
|
|
assert!(bus.poll_irq(), "status read must not clear DMC IRQ");
|
|
bus.write(0x4015, 0x10);
|
|
assert!(!bus.poll_irq(), "writing 4015 acknowledges DMC IRQ");
|
|
}
|
|
|
|
#[test]
|
|
fn quarter_frame_clocks_triangle_linear_counter() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4008, 0x05); // control=0, reload value=5
|
|
bus.write(0x400B, 0x00); // set reload flag
|
|
|
|
for _ in 0..7_457u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert_eq!(bus.apu.triangle_linear_counter, 5);
|
|
assert!(!bus.apu.triangle_linear_reload_flag);
|
|
|
|
for _ in 0..7_456u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert_eq!(bus.apu.triangle_linear_counter, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn quarter_frame_envelope_start_reloads_decay() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4015, 0x01); // enable pulse1
|
|
bus.write(0x4000, 0x03); // envelope period=3
|
|
bus.write(0x4003, 0x00); // start envelope
|
|
assert_ne!(bus.apu.envelope_start_flags & 0x01, 0);
|
|
|
|
for _ in 0..7_457u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert_eq!(bus.apu.envelope_decay[0], 15);
|
|
assert_eq!(bus.apu.envelope_divider[0], 3);
|
|
assert_eq!(bus.apu.envelope_start_flags & 0x01, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn sweep_half_frame_updates_pulse_timer_period() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4002, 0x00); // timer low
|
|
bus.write(0x4003, 0x02); // timer high => period 0x200
|
|
bus.write(0x4001, 0x82); // enable, period=1, negate=0, shift=2
|
|
|
|
for _ in 0..14_913u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert_eq!(bus.apu.read(0x4002), 0x80);
|
|
assert_eq!(bus.apu.read(0x4003) & 0x07, 0x02);
|
|
}
|
|
|
|
#[test]
|
|
fn sweep_negative_pulse1_uses_ones_complement() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4002, 0x00); // period 0x200
|
|
bus.write(0x4003, 0x02);
|
|
bus.write(0x4001, 0x8A); // enable, period=1, negate=1, shift=2
|
|
|
|
for _ in 0..14_913u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
assert_eq!(bus.apu.read(0x4002), 0x7F);
|
|
assert_eq!(bus.apu.read(0x4003) & 0x07, 0x01);
|
|
}
|
|
|
|
#[test]
|
|
fn dmc_dma_fetches_sample_bytes_and_steals_cpu_cycles() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4010, 0x0F); // no IRQ, no loop, fastest period
|
|
bus.write(0x4012, 0x00); // sample start $C000
|
|
bus.write(0x4013, 0x00); // sample length = 1 byte
|
|
bus.write(0x4015, 0x10); // enable DMC (issues initial DMA request)
|
|
|
|
assert_eq!(bus.ppu_dot, 0);
|
|
bus.clock_cpu(1);
|
|
|
|
// 1 CPU cycle + 4-cycle DMA steal = 5 total CPU cycles => 15 PPU dots.
|
|
assert_eq!(bus.ppu_dot, 15);
|
|
assert_eq!(bus.apu.dmc_bytes_remaining, 0);
|
|
assert_eq!(bus.apu.dmc_current_addr, 0xC001);
|
|
assert!(bus.apu.dmc_sample_buffer_valid);
|
|
}
|
|
|
|
#[test]
|
|
fn dmc_playback_updates_output_level_from_sample_bits() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4011, 0x20); // initial DMC DAC level
|
|
bus.write(0x4010, 0x0F); // fastest DMC rate
|
|
bus.write(0x4012, 0x00); // sample start $C000
|
|
bus.write(0x4013, 0x00); // 1-byte sample
|
|
bus.write(0x4015, 0x10); // enable DMC
|
|
|
|
// Service initial DMA request.
|
|
bus.clock_cpu(1);
|
|
let initial = bus.apu.dmc_output_level;
|
|
|
|
// Stub mapper returns 0x00 sample byte, so each played bit drives output down by 2.
|
|
for _ in 0..600u32 {
|
|
bus.clock_cpu(1);
|
|
}
|
|
|
|
assert!(bus.apu.dmc_output_level < initial);
|
|
}
|
|
|
|
#[test]
|
|
fn pulse_channel_outputs_become_audible_after_setup() {
|
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
|
bus.write(0x4015, 0x01); // enable pulse1
|
|
bus.write(0x4000, 0b0101_1111); // 25% duty, constant volume=15
|
|
bus.write(0x4002, 0x08); // low timer period, not sweep-muted
|
|
bus.write(0x4003, 0x00); // reload length + reset duty sequencer
|
|
|
|
let mut saw_non_zero = false;
|
|
for _ in 0..64u32 {
|
|
bus.clock_cpu(1);
|
|
if bus.apu_channel_outputs().pulse1 > 0 {
|
|
saw_non_zero = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
assert!(saw_non_zero, "pulse1 never produced audible output");
|
|
}
|