Files
nesemu/src/native_core/bus/tests/apu.rs
se.cherkasov d8f41bc2c9 fix(apu): correct frame counter timing, add LP filter, mute aliased triangle
- 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.
2026-03-15 10:44:43 +03:00

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");
}