use std::cell::RefCell;
use std::rc::Rc;
use super::dmc::Dmc;
use super::frame_counter::FrameCounter;
use super::noise::Noise;
use super::pulse::Pulse;
use super::triangle::Triangle;
use crate::nes::apu::dmc::DmcState;
use crate::nes::apu::noise::NoiseState;
use crate::nes::apu::pulse::PulseState;
use crate::nes::apu::triangle::TriangleState;
use crate::nes::console::TimingMode;
use crate::trace_apu;
use ringbuf::HeapRb;
use ringbuf::traits::{Consumer, Observer, RingBuffer};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FrameCounterState {
pub cycle_counter: u32,
pub mode: bool,
pub irq_inhibit: bool,
pub irq_flag: bool,
pub irq_assert_cycles_remaining: u8,
pub block_frame_counter: bool,
pub five_step_extra_cycle: bool,
pub pending_write: Option<u8>,
pub write_delay: u8,
pub pending_write_on_odd_cpu_cycle: bool,
pub pending_immediate_quarter: bool,
pub pending_immediate_half: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ApuState {
pub frame_counter: FrameCounterState,
pub pulse1: PulseState,
pub pulse2: PulseState,
pub triangle: TriangleState,
pub noise: NoiseState,
pub dmc: DmcState,
pub sample_accumulator: f32,
pub cycles_per_sample: f32,
pub pending_samples: Vec<f32>,
pub pulse1_enabled: bool,
pub pulse2_enabled: bool,
pub triangle_enabled: bool,
pub noise_enabled: bool,
pub dmc_enabled: bool,
pub apu_cycle: u32,
pub cpu_cycle: u64,
pub last_4017_write: u8,
}
const MAX_PENDING_SAMPLES: usize = 16_384;
const STATUS_PULSE1: u8 = 1 << 0;
const STATUS_PULSE2: u8 = 1 << 1;
const STATUS_TRIANGLE: u8 = 1 << 2;
const STATUS_NOISE: u8 = 1 << 3;
const STATUS_DMC: u8 = 1 << 4;
const STATUS_FRAME_IRQ: u8 = 1 << 6;
const STATUS_DMC_IRQ: u8 = 1 << 7;
#[rustfmt::skip]
#[allow(clippy::excessive_precision)]
const PULSE_TABLE: [f32; 31] = [
0.0, 0.011609139, 0.022937592, 0.033999473, 0.044808503, 0.055377416, 0.065718144,
0.075841725, 0.085758299, 0.095477104, 0.105006486, 0.114354908, 0.123530001,
0.132538617, 0.141387892, 0.150083256, 0.158630435, 0.167034455, 0.175300646,
0.183433647, 0.191437408, 0.199316200, 0.207074609, 0.214716494, 0.222245022,
0.229663670, 0.236976123, 0.244186282, 0.251297271, 0.258312434, 0.265235335,
];
#[rustfmt::skip]
#[allow(clippy::excessive_precision)]
const TND_TABLE: [f32; 203] = [
0.000000000, 0.006699824, 0.013345020, 0.019936254, 0.026474180, 0.032959443, 0.039392675,
0.045774502, 0.052105535, 0.058386381, 0.064617632, 0.070799874, 0.076933683, 0.083019626,
0.089058261, 0.095050137, 0.100995796, 0.106895770, 0.112750584, 0.118560753, 0.124326788,
0.130049188, 0.135728448, 0.141365053, 0.146959482, 0.152512207, 0.158023692, 0.163494395,
0.168924767, 0.174315252, 0.179666289, 0.184978308, 0.190251735, 0.195486988, 0.200684482,
0.205844623, 0.210967811, 0.216054444, 0.221104910, 0.226119593, 0.231098874, 0.236043125,
0.240952715, 0.245828007, 0.250669358, 0.255477124, 0.260251651, 0.264993283, 0.269702358,
0.274379212, 0.279024174, 0.283637568, 0.288219716, 0.292770934, 0.297291534, 0.301781823,
0.306242106, 0.310672683, 0.315073849, 0.319445896, 0.323789113, 0.328103783, 0.332390186,
0.336648601, 0.340879300, 0.345082552, 0.349258625, 0.353407780, 0.357530277, 0.361626373,
0.365696320, 0.369740367, 0.373758762, 0.377751747, 0.381719563, 0.385662446, 0.389580632,
0.393474351, 0.397343833, 0.401189302, 0.405010981, 0.408809091, 0.412583848, 0.416335468,
0.420064163, 0.423770142, 0.427453612, 0.431114778, 0.434753841, 0.438371001, 0.441966456,
0.445540399, 0.449093024, 0.452624521, 0.456135077, 0.459624878, 0.463094108, 0.466542949,
0.469971578, 0.473380175, 0.476768913, 0.480137965, 0.483487503, 0.486817696, 0.490128711,
0.493420713, 0.496693865, 0.499948329, 0.503184264, 0.506401828, 0.509601178, 0.512782466,
0.515945847, 0.519091470, 0.522219486, 0.525330040, 0.528423279, 0.531499348, 0.534558388,
0.537600541, 0.540625946, 0.543634742, 0.546627063, 0.549603047, 0.552562825, 0.555506530,
0.558434293, 0.561346242, 0.564242506, 0.567123210, 0.569988481, 0.572838441, 0.575673213,
0.578492918, 0.581297676, 0.584087605, 0.586862823, 0.589623445, 0.592369587, 0.595101363,
0.597818884, 0.600522262, 0.603211607, 0.605887028, 0.608548633, 0.611196528, 0.613830820,
0.616451613, 0.619059010, 0.621653114, 0.624234026, 0.626801846, 0.629356675, 0.631898610,
0.634427748, 0.636944186, 0.639448020, 0.641939344, 0.644418251, 0.646884834, 0.649339185,
0.651781395, 0.654211552, 0.656629747, 0.659036068, 0.661430601, 0.663813433, 0.666184650,
0.668544336, 0.670892576, 0.673229451, 0.675555046, 0.677869441, 0.680172716, 0.682464952,
0.684746229, 0.687016623, 0.689276214, 0.691525078, 0.693763291, 0.695990928, 0.698208065,
0.700414776, 0.702611133, 0.704797210, 0.706973079, 0.709138811, 0.711294476, 0.713440145,
0.715575887, 0.717701770, 0.719817864, 0.721924234, 0.724020949, 0.726108075, 0.728185676,
0.730253819, 0.732312567, 0.734361984, 0.736402134, 0.738433080, 0.740454883, 0.742467605,
];
pub type SharedApu = Rc<RefCell<Apu>>;
pub struct Apu {
tv_system: TimingMode,
frame_counter: FrameCounter,
pulse1: Pulse,
pulse2: Pulse,
triangle: Triangle,
noise: Noise,
dmc: Dmc,
sample_accumulator: f32,
cycles_per_sample: f32,
pending_samples: HeapRb<f32>,
pulse1_enabled: bool,
pulse2_enabled: bool,
triangle_enabled: bool,
noise_enabled: bool,
dmc_enabled: bool,
apu_cycle: u32,
cpu_cycle: u64,
last_4017_write: u8,
}
impl Apu {
pub fn new() -> Self {
Self::new_with_tv_system(TimingMode::Ntsc)
}
pub fn new_with_tv_system(tv_system: TimingMode) -> Self {
const DEFAULT_SAMPLE_RATE: f32 = 44100.0;
let pending_samples = HeapRb::<f32>::new(MAX_PENDING_SAMPLES);
let cpu_clock = tv_system.cpu_clock_hz();
let mut apu = Self {
tv_system,
frame_counter: FrameCounter::new_with_tv_system(tv_system),
pulse1: Pulse::new(true), pulse2: Pulse::new(false), triangle: Triangle::new(),
noise: Noise::new_with_tv_system(tv_system),
dmc: Dmc::new_with_tv_system(tv_system),
sample_accumulator: 0.0,
cycles_per_sample: cpu_clock / DEFAULT_SAMPLE_RATE,
pending_samples,
pulse1_enabled: true,
pulse2_enabled: true,
triangle_enabled: true,
noise_enabled: true,
dmc_enabled: true,
apu_cycle: 0,
cpu_cycle: 0,
last_4017_write: 0x00,
};
apu.frame_counter.write_register(0x00);
for _ in 0..9 {
apu.frame_counter.clock();
}
apu
}
#[cfg(test)]
fn new_for_testing() -> Self {
const DEFAULT_SAMPLE_RATE: f32 = 44100.0;
let pending_samples = HeapRb::<f32>::new(MAX_PENDING_SAMPLES);
let cpu_clock = TimingMode::Ntsc.cpu_clock_hz();
let mut apu = Self {
tv_system: TimingMode::Ntsc,
frame_counter: FrameCounter::new(),
pulse1: Pulse::new(true),
pulse2: Pulse::new(false),
triangle: Triangle::new(),
noise: Noise::new(),
dmc: Dmc::new(),
sample_accumulator: 0.0,
cycles_per_sample: cpu_clock / DEFAULT_SAMPLE_RATE,
pending_samples,
pulse1_enabled: true,
pulse2_enabled: true,
triangle_enabled: true,
noise_enabled: true,
dmc_enabled: true,
apu_cycle: 0,
cpu_cycle: 0,
last_4017_write: 0x00,
};
apu.frame_counter.write_register(0x00);
apu
}
pub fn apu_cycle(&self) -> u32 {
self.apu_cycle
}
pub fn reset(&mut self, cpu_cycle: u64, soft_reset: bool) {
trace_apu!(1; "reset cpu_cycle={} soft_reset={} last_4017_write=0x{:02X}", cpu_cycle, soft_reset, self.last_4017_write);
self.frame_counter.reset();
self.pulse1.reset();
self.pulse2.reset();
self.triangle.reset();
self.noise.reset();
self.dmc.reset();
self.sample_accumulator = 0.0;
self.clear_pending_samples();
self.apu_cycle = 0;
if !soft_reset {
self.last_4017_write = 0x00;
self.frame_counter.write_register(0x00);
for _ in 0..1 {
self.frame_counter.clock();
}
return;
}
let write_delay = if cpu_cycle.is_multiple_of(2) { 4 } else { 3 };
self.frame_counter
.queue_delayed_write(self.last_4017_write, write_delay);
let total_clocks = u32::from(write_delay) + 1;
for _ in 0..total_clocks {
self.frame_counter.clock();
}
}
pub fn debug_frame_counter_cycle(&self) -> u32 {
self.frame_counter.get_cycle_counter()
}
#[cfg(test)]
pub fn pulse1(&self) -> &Pulse {
&self.pulse1
}
pub fn pulse1_mut(&mut self) -> &mut Pulse {
&mut self.pulse1
}
#[cfg(test)]
pub fn pulse2(&self) -> &Pulse {
&self.pulse2
}
pub fn pulse2_mut(&mut self) -> &mut Pulse {
&mut self.pulse2
}
#[cfg(test)]
pub fn frame_counter(&self) -> &FrameCounter {
&self.frame_counter
}
#[cfg(test)]
pub fn frame_counter_mut(&mut self) -> &mut FrameCounter {
&mut self.frame_counter
}
pub fn write_frame_counter(&mut self, value: u8) {
self.last_4017_write = value;
let write_delay = if self.apu_cycle.is_multiple_of(2) {
3
} else {
4
};
let write_on_odd_cpu_cycle = !self.apu_cycle.is_multiple_of(2);
trace_apu!(1; "write $4017 value=0x{:02X} apu_cycle={} delay={} odd_cpu_cycle={}", value, self.apu_cycle, write_delay, write_on_odd_cpu_cycle);
self.frame_counter.queue_delayed_write_with_jitter(
value,
write_delay,
write_on_odd_cpu_cycle,
);
}
#[cfg(test)]
pub fn triangle(&self) -> &Triangle {
&self.triangle
}
pub fn triangle_mut(&mut self) -> &mut Triangle {
&mut self.triangle
}
#[cfg(test)]
pub fn noise(&self) -> &Noise {
&self.noise
}
pub fn noise_mut(&mut self) -> &mut Noise {
&mut self.noise
}
#[cfg(test)]
pub fn dmc(&self) -> &Dmc {
&self.dmc
}
pub fn dmc_mut(&mut self) -> &mut Dmc {
&mut self.dmc
}
#[cfg(test)]
pub fn clock(&mut self) {
self.clock_with_expansion(0.0);
}
pub fn clock_with_expansion(&mut self, expansion_audio: f32) {
trace_apu!(
5; "tick apu_cycle={} frame_counter_cycle={}",
self.apu_cycle,
self.frame_counter.get_cycle_counter()
);
let (quarter_frame, half_frame) = self.frame_counter.clock();
if quarter_frame || half_frame {
trace_apu!(
3; "frame_counter clock quarter={} half={} cycle={} cpu_cycle={} apu_cycle={}",
quarter_frame,
half_frame,
self.frame_counter.get_cycle_counter(),
self.cpu_cycle,
self.apu_cycle
);
}
if quarter_frame {
self.pulse1.clock_envelope();
self.pulse2.clock_envelope();
self.triangle.clock_linear_counter_with_reload();
self.noise.clock_envelope();
}
if half_frame {
self.pulse1.clock_length_counter();
self.pulse1.clock_sweep();
self.pulse2.clock_length_counter();
self.pulse2.clock_sweep();
self.triangle.clock_length_counter();
self.noise.clock_length_counter();
}
self.pulse1.apply_pending_length_reload();
self.pulse2.apply_pending_length_reload();
self.triangle.apply_pending_length_reload();
self.noise.apply_pending_length_reload();
self.pulse1.apply_pending_length_halt();
self.pulse2.apply_pending_length_halt();
self.triangle.apply_pending_length_halt();
self.noise.apply_pending_length_halt();
if self.apu_cycle.is_multiple_of(2) {
self.pulse1.clock_timer();
self.pulse2.clock_timer();
self.noise.clock_timer();
}
self.triangle.clock_timer();
self.apu_cycle = self.apu_cycle.wrapping_add(1);
self.cpu_cycle = self.cpu_cycle.wrapping_add(1);
self.dmc.process_clock();
self.dmc.clock_timer();
self.sample_accumulator += 1.0;
if self.sample_accumulator >= self.cycles_per_sample {
self.sample_accumulator -= self.cycles_per_sample;
self.push_pending_sample(self.mix() + expansion_audio.max(0.0));
}
}
pub fn sample_ready(&self) -> bool {
!self.pending_samples.is_empty()
}
pub fn get_sample(&mut self) -> Option<f32> {
self.pending_samples.try_pop()
}
pub fn poll_irq(&self) -> bool {
self.frame_counter.get_irq_flag() || self.dmc.get_irq_flag()
}
pub fn read_status(&mut self, open_bus: u8) -> u8 {
let mut status = 0;
if self.pulse1.is_length_counter_enabled() && self.pulse1.get_length_counter() > 0 {
status |= STATUS_PULSE1;
}
if self.pulse2.is_length_counter_enabled() && self.pulse2.get_length_counter() > 0 {
status |= STATUS_PULSE2;
}
if self.triangle.is_length_counter_enabled() && self.triangle.get_length_counter() > 0 {
status |= STATUS_TRIANGLE;
}
if self.noise.is_length_counter_enabled() && self.noise.get_length_counter() > 0 {
status |= STATUS_NOISE;
}
if self.dmc.has_bytes_remaining() {
status |= STATUS_DMC;
}
if self.frame_counter.get_irq_flag() {
status |= STATUS_FRAME_IRQ;
}
if self.dmc.get_irq_flag() {
status |= STATUS_DMC_IRQ;
}
status |= open_bus & (1 << 5);
self.frame_counter.clear_irq_flag();
trace_apu!(
3; "read $4015 status=0b{:08b} open_bus=0x{:02X}",
status,
open_bus
);
status
}
pub fn write_enable(&mut self, value: u8) {
trace_apu!(1; "write $4015 value=0x{:02X}", value);
let pulse1_enabled = value & STATUS_PULSE1 != 0;
if !pulse1_enabled {
self.pulse1.clear_length_counter();
}
self.pulse1.set_length_counter_enabled(pulse1_enabled);
let pulse2_enabled = value & STATUS_PULSE2 != 0;
if !pulse2_enabled {
self.pulse2.clear_length_counter();
}
self.pulse2.set_length_counter_enabled(pulse2_enabled);
let triangle_enabled = value & STATUS_TRIANGLE != 0;
if !triangle_enabled {
self.triangle.clear_length_counter();
}
self.triangle.set_length_counter_enabled(triangle_enabled);
let noise_enabled = value & STATUS_NOISE != 0;
if !noise_enabled {
self.noise.clear_length_counter();
}
self.noise.set_length_counter_enabled(noise_enabled);
self.dmc
.set_enabled(value & STATUS_DMC != 0, self.cpu_cycle);
self.dmc.clear_irq_flag();
}
pub fn mix(&self) -> f32 {
let pulse1 = if self.pulse1_enabled {
self.pulse1.output() as usize
} else {
0
};
let pulse2 = if self.pulse2_enabled {
self.pulse2.output() as usize
} else {
0
};
let triangle = if self.triangle_enabled {
self.triangle.output() as usize
} else {
0
};
let noise = if self.noise_enabled {
self.noise.output() as usize
} else {
0
};
let dmc = if self.dmc_enabled {
self.dmc.output() as usize
} else {
0
};
let pulse_index = pulse1 + pulse2;
let pulse_out = if pulse_index < PULSE_TABLE.len() {
PULSE_TABLE[pulse_index]
} else {
0.0
};
let tnd_index = 3 * triangle + 2 * noise + dmc;
let tnd_out = if tnd_index < TND_TABLE.len() {
TND_TABLE[tnd_index]
} else {
0.0
};
trace_apu!(5; "Mixing pulse1,pulse2,triangle,noise,dmc=({}, {}, {}, {}, {}) into {}", pulse1, pulse2, triangle, noise, dmc, pulse_out + tnd_out);
pulse_out + tnd_out
}
pub fn set_sample_rate(&mut self, sample_rate: f32) {
self.cycles_per_sample = self.tv_system.cpu_clock_hz() / sample_rate;
self.sample_accumulator = 0.0;
self.clear_pending_samples();
}
fn push_pending_sample(&mut self, sample: f32) {
self.pending_samples.push_overwrite(sample);
}
fn clear_pending_samples(&mut self) {
self.pending_samples.clear();
}
fn pending_samples_snapshot(&self) -> Vec<f32> {
let start = self.pending_samples.read_index();
let end = self.pending_samples.write_index();
let (first, second) = unsafe { self.pending_samples.unsafe_slices(start, end) };
let mut samples = Vec::with_capacity(self.pending_samples.occupied_len());
samples.extend(
first
.iter()
.map(|value| unsafe { *value.assume_init_ref() }),
);
samples.extend(
second
.iter()
.map(|value| unsafe { *value.assume_init_ref() }),
);
samples
}
fn restore_pending_samples(&mut self, samples: &[f32]) {
self.clear_pending_samples();
for &sample in samples {
self.push_pending_sample(sample);
}
}
#[cfg(test)]
pub(crate) fn push_sample_for_test(&mut self, sample: f32) {
self.push_pending_sample(sample);
}
pub fn set_pulse1_enabled(&mut self, enabled: bool) {
self.pulse1_enabled = enabled;
}
pub fn set_pulse2_enabled(&mut self, enabled: bool) {
self.pulse2_enabled = enabled;
}
pub fn set_triangle_enabled(&mut self, enabled: bool) {
self.triangle_enabled = enabled;
}
pub fn set_noise_enabled(&mut self, enabled: bool) {
self.noise_enabled = enabled;
}
pub fn set_dmc_enabled(&mut self, enabled: bool) {
self.dmc_enabled = enabled;
}
pub fn capture_state(&self) -> ApuState {
ApuState {
frame_counter: FrameCounterState {
cycle_counter: self.frame_counter.get_cycle_counter(),
mode: self.frame_counter.get_mode(),
irq_inhibit: self.frame_counter.get_irq_inhibit(),
irq_flag: self.frame_counter.get_irq_flag(),
irq_assert_cycles_remaining: self.frame_counter.irq_assert_cycles_remaining(),
block_frame_counter: self.frame_counter.block_frame_counter(),
five_step_extra_cycle: self.frame_counter.five_step_extra_cycle(),
pending_write: self.frame_counter.pending_write(),
write_delay: self.frame_counter.write_delay(),
pending_write_on_odd_cpu_cycle: self.frame_counter.pending_write_on_odd_cpu_cycle(),
pending_immediate_quarter: self.frame_counter.pending_immediate_clock().0,
pending_immediate_half: self.frame_counter.pending_immediate_clock().1,
},
pulse1: self.pulse1.capture_state(),
pulse2: self.pulse2.capture_state(),
triangle: self.triangle.capture_state(),
noise: self.noise.capture_state(),
dmc: self.dmc.capture_state(),
sample_accumulator: self.sample_accumulator,
cycles_per_sample: self.cycles_per_sample,
pending_samples: self.pending_samples_snapshot(),
pulse1_enabled: self.pulse1_enabled,
pulse2_enabled: self.pulse2_enabled,
triangle_enabled: self.triangle_enabled,
noise_enabled: self.noise_enabled,
dmc_enabled: self.dmc_enabled,
apu_cycle: self.apu_cycle,
cpu_cycle: self.cpu_cycle,
last_4017_write: self.last_4017_write,
}
}
pub fn restore_state(&mut self, state: &ApuState) {
self.frame_counter.restore_state(
state.frame_counter.cycle_counter,
state.frame_counter.mode,
state.frame_counter.irq_inhibit,
state.frame_counter.irq_flag,
state.frame_counter.irq_assert_cycles_remaining,
state.frame_counter.block_frame_counter,
state.frame_counter.five_step_extra_cycle,
state.frame_counter.pending_write,
state.frame_counter.write_delay,
state.frame_counter.pending_write_on_odd_cpu_cycle,
(
state.frame_counter.pending_immediate_quarter,
state.frame_counter.pending_immediate_half,
),
);
self.pulse1.restore_state(&state.pulse1);
self.pulse2.restore_state(&state.pulse2);
self.triangle.restore_state(&state.triangle);
self.noise.restore_state(&state.noise);
self.dmc.restore_state(&state.dmc);
self.apu_cycle = state.apu_cycle;
self.cpu_cycle = state.cpu_cycle;
self.last_4017_write = state.last_4017_write;
self.sample_accumulator = state.sample_accumulator;
self.cycles_per_sample = state.cycles_per_sample;
self.restore_pending_samples(&state.pending_samples);
self.pulse1_enabled = state.pulse1_enabled;
self.pulse2_enabled = state.pulse2_enabled;
self.triangle_enabled = state.triangle_enabled;
self.noise_enabled = state.noise_enabled;
self.dmc_enabled = state.dmc_enabled;
}
}
impl Default for Apu {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unusual_byte_groupings)]
mod tests {
use super::*;
fn write_pulse1_length(apu: &mut Apu, value: u8) {
apu.pulse1_mut().write_length_counter_timer_high(value);
apu.pulse1_mut().apply_pending_length_reload();
}
fn write_pulse2_length(apu: &mut Apu, value: u8) {
apu.pulse2_mut().write_length_counter_timer_high(value);
apu.pulse2_mut().apply_pending_length_reload();
}
fn write_triangle_length(apu: &mut Apu, value: u8) {
apu.triangle_mut().write_length_counter_timer_high(value);
apu.triangle_mut().apply_pending_length_reload();
}
fn load_triangle_length(apu: &mut Apu, index: u8) {
apu.triangle_mut().load_length_counter(index);
apu.triangle_mut().apply_pending_length_reload();
}
fn write_noise_length(apu: &mut Apu, value: u8) {
apu.noise_mut().write_length(value);
apu.noise_mut().apply_pending_length_reload();
}
#[test]
fn test_clock_with_expansion_adds_to_mix() {
let mut apu = Apu::new_for_testing();
for _ in 0..128 {
apu.clock_with_expansion(0.1);
if apu.sample_ready() {
break;
}
}
let sample = apu.get_sample().expect("expected a sample");
assert!((sample - 0.1).abs() < 0.0001, "sample was {sample}");
}
#[test]
fn test_apu_save_state_roundtrip_includes_internal_state() {
let mut apu = Apu::new_for_testing();
apu.sample_accumulator = 7.25;
apu.cycles_per_sample = 123.0;
apu.pulse1_enabled = false;
apu.pulse2_enabled = true;
apu.triangle_enabled = false;
apu.noise_enabled = true;
apu.dmc_enabled = false;
apu.apu_cycle = 1234;
apu.cpu_cycle = 5678;
apu.last_4017_write = 0xC0;
apu.push_sample_for_test(0.1);
apu.push_sample_for_test(0.2);
apu.frame_counter
.queue_delayed_write_with_jitter(0x80, 3, true);
apu.frame_counter
.debug_set_pending_immediate_clock(true, false);
apu.frame_counter.debug_set_irq_assert_cycles_remaining(2);
apu.frame_counter.debug_set_five_step_extra_cycle(true);
apu.dmc.debug_set_dma_pending(true);
apu.dmc.debug_set_transfer_start_delay(2);
apu.pulse1_mut().write_control(0x30); apu.pulse1_mut().apply_pending_length_halt(); apu.pulse1_mut().write_control(0x10);
let state = apu.capture_state();
let mut restored = Apu::new_for_testing();
restored.restore_state(&state);
assert!((restored.sample_accumulator - 7.25).abs() < 1e-6);
assert!((restored.cycles_per_sample - 123.0).abs() < 1e-6);
assert!(!restored.pulse1_enabled);
assert!(restored.pulse2_enabled);
assert!(!restored.triangle_enabled);
assert!(restored.noise_enabled);
assert!(!restored.dmc_enabled);
assert_eq!(restored.apu_cycle, 1234);
assert_eq!(restored.cpu_cycle, 5678);
assert_eq!(restored.last_4017_write, 0xC0);
assert_eq!(restored.frame_counter.debug_pending_write(), Some(0x80));
assert_eq!(restored.frame_counter.debug_write_delay(), 3);
assert!(
restored
.frame_counter
.debug_pending_write_on_odd_cpu_cycle()
);
assert_eq!(
restored.frame_counter.debug_pending_immediate_clock(),
(true, false)
);
assert_eq!(
restored.frame_counter.debug_irq_assert_cycles_remaining(),
2
);
assert!(restored.frame_counter.debug_five_step_extra_cycle());
assert!(restored.dmc.dma_pending());
assert_eq!(restored.dmc.debug_transfer_start_delay(), 2);
assert!(restored.pulse1().debug_length_counter_halt());
assert_eq!(
restored.pulse1().debug_length_counter_pending_halt(),
Some(false)
);
let sample1 = restored.get_sample();
let sample2 = restored.get_sample();
let sample3 = restored.get_sample();
assert!(matches!(sample1, Some(value) if (value - 0.1).abs() < 1e-6));
assert!(matches!(sample2, Some(value) if (value - 0.2).abs() < 1e-6));
assert!(sample3.is_none());
}
#[test]
fn test_4017_write_takes_effect_after_3_cycles_when_during_apu_cycle() {
let mut apu = Apu::new_for_testing();
for _ in 0..100 {
apu.clock();
}
let before = apu.frame_counter().get_cycle_counter();
assert!(before >= 100);
apu.write_frame_counter(0x80);
for _ in 0..2 {
apu.clock();
assert!(
apu.frame_counter().get_cycle_counter() >= before,
"Frame counter should not have been reset yet"
);
}
apu.clock();
assert!(
apu.frame_counter().get_cycle_counter() < 10,
"Frame counter should have been reset by the delayed $4017 write"
);
}
#[test]
fn test_4017_write_takes_effect_after_4_cycles_when_between_apu_cycles() {
let mut apu = Apu::new_for_testing();
apu.clock();
for _ in 0..100 {
apu.clock();
}
let before = apu.frame_counter().get_cycle_counter();
assert!(before >= 100);
apu.write_frame_counter(0x80);
for _ in 0..3 {
apu.clock();
assert!(
apu.frame_counter().get_cycle_counter() >= before,
"Frame counter should not have been reset yet"
);
}
apu.clock();
assert!(
apu.frame_counter().get_cycle_counter() < 10,
"Frame counter should have been reset by the delayed $4017 write"
);
}
#[test]
fn test_4017_odd_jitter_delays_effective_reset_by_one_cycle() {
let mut apu = Apu::new_for_testing();
apu.clock();
apu.write_frame_counter(0x00);
for _ in 0..4 {
apu.clock();
}
assert_eq!(apu.frame_counter().get_cycle_counter(), 0);
}
#[test]
fn test_length_halt_change_applies_after_half_frame_clock() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
apu.pulse1_mut().write_control(0x10); write_pulse1_length(&mut apu, 0x18);
while apu.frame_counter().get_cycle_counter() < 14912 {
apu.clock();
}
apu.pulse1_mut().write_control(0x30); apu.clock();
assert_eq!(
apu.pulse1().get_length_counter(),
1,
"halt should apply after the half-frame length clock"
);
}
#[test]
fn test_length_halt_applies_before_immediate_half_frame_on_4017_write() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
apu.pulse1_mut().write_control(0x10); write_pulse1_length(&mut apu, 0x18); assert_eq!(apu.pulse1().get_length_counter(), 2);
apu.pulse1_mut().write_control(0x30); apu.frame_counter_mut().write_register(0x80); apu.clock();
assert_eq!(
apu.pulse1().get_length_counter(),
1,
"halt should apply after the immediate half-frame length clock"
);
}
#[test]
fn test_length_reload_during_half_frame_with_nonzero_counter_is_ignored() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
apu.pulse1_mut().write_control(0x10); write_pulse1_length(&mut apu, 0x38);
while apu.frame_counter().get_cycle_counter() < 14912 {
apu.clock();
}
apu.pulse1_mut().write_length_counter_timer_high(0x18); apu.clock();
assert_eq!(
apu.pulse1().get_length_counter(),
5,
"reload during length clock with nonzero counter should be ignored"
);
}
#[test]
fn test_length_reload_during_half_frame_with_zero_counter_is_allowed() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
apu.pulse1_mut().write_control(0x10); write_pulse1_length(&mut apu, 0x38);
apu.write_enable(0x00); apu.write_enable(STATUS_PULSE1);
while apu.frame_counter().get_cycle_counter() < 14912 {
apu.clock();
}
apu.pulse1_mut().write_length_counter_timer_high(0x18); apu.clock();
assert_eq!(
apu.pulse1().get_length_counter(),
2,
"reload during length clock with zero counter should be allowed"
);
}
#[test]
fn test_soft_reset_positions_frame_counter_1_cycle_after_effective_4017_write_even_cpu_cycle() {
let mut apu = Apu::new_for_testing();
apu.reset(0, true);
assert_eq!(apu.frame_counter().get_cycle_counter(), 1);
}
#[test]
fn test_soft_reset_positions_frame_counter_1_cycle_after_effective_4017_write_odd_cpu_cycle() {
let mut apu = Apu::new_for_testing();
apu.reset(1, true);
assert_eq!(apu.frame_counter().get_cycle_counter(), 1);
}
#[test]
fn test_power_on_reset_advances_frame_counter_to_1_cycles_after_effective_4017_write() {
let mut apu = Apu::new_for_testing();
apu.reset(0, false);
assert_eq!(apu.frame_counter().get_cycle_counter(), 1);
}
#[test]
fn test_apu_reset_distinguishes_power_on_from_soft_reset() {
let mut apu = Apu::new_for_testing();
apu.write_frame_counter(0x80);
apu.reset(0, true);
assert!(
apu.frame_counter().get_mode(),
"Soft reset should keep the last-written $4017 mode"
);
apu.reset(0, false);
assert!(
!apu.frame_counter().get_mode(),
"Power-on reset should behave as if $4017 was written with $00"
);
}
#[test]
fn test_apu_new() {
let apu = Apu::new_for_testing();
assert_eq!(apu.frame_counter().get_cycle_counter(), 0);
assert_eq!(apu.pulse1().output(), 0);
assert_eq!(apu.pulse2().output(), 0);
assert_eq!(apu.triangle().output(), 0); assert_eq!(apu.noise().output(), 0); }
#[test]
fn test_frame_counter_advances() {
let mut apu = Apu::new_for_testing();
assert_eq!(apu.frame_counter().get_cycle_counter(), 0);
apu.clock();
assert_eq!(apu.frame_counter().get_cycle_counter(), 1);
for _ in 0..100 {
apu.clock();
}
assert_eq!(apu.frame_counter().get_cycle_counter(), 101);
}
#[test]
fn test_envelope_gets_clocked() {
let mut apu = Apu::new_for_testing();
apu.pulse1_mut().write_control(0b0000_0000); apu.pulse1_mut().write_length_counter_timer_high(0xFF);
assert!(apu.pulse1().get_envelope_start_flag());
for _ in 0..7457 {
apu.clock();
}
assert!(!apu.pulse1().get_envelope_start_flag());
}
#[test]
fn test_length_counter_gets_clocked() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
apu.pulse1_mut().write_control(0b0000_0000); write_pulse1_length(&mut apu, 0b00010_000);
let initial_length = apu.pulse1().get_length_counter();
assert_eq!(initial_length, 20);
for _ in 0..14913 {
apu.clock();
}
assert_eq!(apu.pulse1().get_length_counter(), 19);
}
#[test]
fn test_sweep_gets_clocked() {
let mut apu = Apu::new_for_testing();
apu.pulse1_mut().write_sweep(0b1000_0001);
assert!(apu.pulse1().get_sweep_reload());
for _ in 0..14913 {
apu.clock();
}
assert!(!apu.pulse1().get_sweep_reload());
}
#[test]
fn test_sweep_immediate_half_frame_updates_period() {
let mut apu = Apu::new_for_testing();
apu.pulse1_mut().write_sweep(0b1000_1001); apu.pulse1_mut().write_timer_low(16);
apu.pulse1_mut().write_timer_high(0);
apu.write_frame_counter(0b1100_0000);
for _ in 0..4 {
apu.clock();
}
assert_eq!(apu.pulse1().get_timer_period(), 16);
}
#[test]
fn test_frame_counter_mode_change() {
let mut apu = Apu::new_for_testing();
assert!(!apu.frame_counter().get_mode());
apu.frame_counter_mut().write_register(0b1000_0000);
assert!(apu.frame_counter().get_mode());
apu.frame_counter_mut().write_register(0b0000_0000);
assert!(!apu.frame_counter().get_mode());
}
#[test]
fn test_both_pulse_channels_get_clocked() {
let mut apu = Apu::new_for_testing();
apu.write_enable(0b0001_1111); write_pulse1_length(&mut apu, 0xFF);
write_pulse2_length(&mut apu, 0xFF);
assert!(apu.pulse1().get_envelope_start_flag());
assert!(apu.pulse2().get_envelope_start_flag());
for _ in 0..7457 {
apu.clock();
}
assert!(!apu.pulse1().get_envelope_start_flag());
assert!(!apu.pulse2().get_envelope_start_flag());
}
#[test]
fn test_pulse1_uses_ones_complement_for_sweep() {
let mut apu = Apu::new_for_testing();
apu.pulse1_mut().write_timer_low(20);
apu.pulse1_mut().write_timer_high(0);
apu.pulse1_mut().write_sweep(0b1000_1001);
assert_eq!(apu.pulse1().get_sweep_target_period(), 9);
}
#[test]
fn test_pulse2_uses_twos_complement_for_sweep() {
let mut apu = Apu::new_for_testing();
apu.pulse2_mut().write_timer_low(20);
apu.pulse2_mut().write_timer_high(0);
apu.pulse2_mut().write_sweep(0b1000_1001);
assert_eq!(apu.pulse2().get_sweep_target_period(), 10);
}
#[test]
fn test_triangle_linear_counter_gets_clocked() {
let mut apu = Apu::new_for_testing();
apu.triangle_mut().write_linear_counter(0x7F); write_triangle_length(&mut apu, 0x08);
assert!(apu.triangle_mut().is_linear_counter_reload_flag_set());
for _ in 0..7457 {
apu.clock();
}
assert_eq!(apu.triangle().get_linear_counter(), 127);
assert!(!apu.triangle().is_linear_counter_reload_flag_set());
for _ in 0..7456 {
apu.clock();
}
assert_eq!(apu.triangle().get_linear_counter(), 126);
}
#[test]
fn test_triangle_linear_counter_clocks_only_on_quarter_frames_in_5_step_mode() {
let mut apu = Apu::new_for_testing();
apu.write_frame_counter(0b1000_0000);
let mut prev = apu.debug_frame_counter_cycle();
for _ in 0..10 {
apu.clock();
let now = apu.debug_frame_counter_cycle();
if now == 0 && prev != 0 {
break;
}
prev = now;
}
assert_eq!(apu.debug_frame_counter_cycle(), 0);
apu.write_enable(STATUS_TRIANGLE);
apu.triangle_mut().write_linear_counter(0b0000_0011); write_triangle_length(&mut apu, 0x00); assert!(apu.triangle().is_linear_counter_reload_flag_set());
for _ in 0..7456 {
apu.clock();
}
assert_eq!(apu.triangle().get_linear_counter(), 0);
assert!(apu.triangle().is_linear_counter_reload_flag_set());
apu.clock();
assert_eq!(apu.triangle().get_linear_counter(), 3);
assert!(!apu.triangle().is_linear_counter_reload_flag_set());
for _ in 0..7456 {
apu.clock();
}
assert_eq!(apu.triangle().get_linear_counter(), 2);
}
#[test]
fn test_triangle_length_counter_gets_clocked() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_TRIANGLE);
load_triangle_length(&mut apu, 5);
assert_eq!(apu.triangle().get_length_counter(), 4);
for _ in 0..14913 {
apu.clock();
}
assert_eq!(apu.triangle().get_length_counter(), 3);
}
#[test]
fn test_triangle_length_counter_clocks_only_on_half_frames_in_5_step_mode() {
let mut apu = Apu::new_for_testing();
apu.write_frame_counter(0b1000_0000);
let mut prev = apu.debug_frame_counter_cycle();
for _ in 0..10 {
apu.clock();
let now = apu.debug_frame_counter_cycle();
if now == 0 && prev != 0 {
break;
}
prev = now;
}
assert_eq!(apu.debug_frame_counter_cycle(), 0);
apu.write_enable(STATUS_TRIANGLE);
load_triangle_length(&mut apu, 3); assert_eq!(apu.triangle().get_length_counter(), 2);
for _ in 0..7457 {
apu.clock();
}
assert_eq!(apu.triangle().get_length_counter(), 2);
for _ in 0..7456 {
apu.clock();
}
assert_eq!(apu.triangle().get_length_counter(), 1);
for _ in 0..7458 {
apu.clock();
}
assert_eq!(apu.triangle().get_length_counter(), 1);
for _ in 0..7458 {
apu.clock();
}
assert_eq!(apu.triangle().get_length_counter(), 1);
for _ in 0..7452 {
apu.clock();
}
assert_eq!(apu.triangle().get_length_counter(), 0);
}
#[test]
fn test_noise_channel_integrated() {
let apu = Apu::new_for_testing();
assert_eq!(apu.noise().output(), 0); }
#[test]
fn test_noise_envelope_gets_clocked() {
let mut apu = Apu::new_for_testing();
apu.noise_mut().write_envelope(0b0000_0101); write_noise_length(&mut apu, 0xFF);
for _ in 0..7457 {
apu.clock();
}
}
#[test]
fn test_noise_length_counter_gets_clocked() {
let mut apu = Apu::new_for_testing();
apu.noise_mut().write_envelope(0b0000_0000); write_noise_length(&mut apu, 0b00010_000);
for _ in 0..14913 {
apu.clock();
}
}
#[test]
fn test_dmc_channel_accessible() {
let apu = Apu::new_for_testing();
assert_eq!(apu.dmc().output(), 0);
}
#[test]
fn test_dmc_channel_mutable() {
let mut apu = Apu::new_for_testing();
apu.dmc_mut().write_direct_load(0b0100_0000); assert_eq!(apu.dmc().output(), 64);
}
#[test]
fn test_dmc_timer_gets_clocked() {
let mut apu = Apu::new_for_testing();
apu.dmc_mut().write_flags_and_rate(0b0000_0000); apu.dmc_mut().write_direct_load(0b0000_0000);
for _ in 0..427 {
apu.clock();
}
apu.clock();
}
#[test]
fn test_status_all_channels_inactive() {
let mut apu = Apu::new_for_testing();
assert_eq!(apu.read_status(0), 0b0000_0000);
}
#[test]
fn test_status_pulse1_active() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
write_pulse1_length(&mut apu, 0b00001_000); assert_eq!(apu.read_status(0) & 0b0000_0001, 0b0000_0001);
}
#[test]
fn test_status_pulse2_active() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE2);
write_pulse2_length(&mut apu, 0b00001_000); assert_eq!(apu.read_status(0) & 0b0000_0010, 0b0000_0010);
}
#[test]
fn test_status_triangle_active() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_TRIANGLE);
load_triangle_length(&mut apu, 1); assert_eq!(apu.read_status(0) & 0b0000_0100, 0b0000_0100);
}
#[test]
fn test_status_noise_active() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_NOISE);
write_noise_length(&mut apu, 0b00001_000);
assert_eq!(apu.read_status(0) & 0b0000_1000, 0b0000_1000);
}
#[test]
fn test_status_all_channels_active() {
let mut apu = Apu::new_for_testing();
apu.write_enable(0b0001_1111);
write_pulse1_length(&mut apu, 0b00001_000);
write_pulse2_length(&mut apu, 0b00001_000);
load_triangle_length(&mut apu, 1);
write_noise_length(&mut apu, 0b00001_000);
assert_eq!(apu.read_status(0) & 0b0000_1111, 0b0000_1111);
}
#[test]
fn test_enable_disable_pulse1() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
write_pulse1_length(&mut apu, 0b00001_000);
assert_eq!(apu.read_status(0) & STATUS_PULSE1, STATUS_PULSE1);
apu.write_enable(0b0000_0000);
assert_eq!(apu.read_status(0) & STATUS_PULSE1, 0);
}
#[test]
fn test_enable_pulse1_with_enable_bit() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
write_pulse1_length(&mut apu, 0b00001_000);
assert_eq!(apu.read_status(0) & STATUS_PULSE1, STATUS_PULSE1);
}
#[test]
fn test_enable_all_channels() {
let mut apu = Apu::new_for_testing();
apu.write_enable(0b0001_1111);
write_pulse1_length(&mut apu, 0b00001_000);
write_pulse2_length(&mut apu, 0b00001_000);
load_triangle_length(&mut apu, 1);
write_noise_length(&mut apu, 0b00001_000);
assert_eq!(apu.read_status(0) & 0b0000_1111, 0b0000_1111);
}
#[test]
fn test_disable_clears_length_counters() {
let mut apu = Apu::new_for_testing();
apu.write_enable(0b0001_1111);
write_pulse1_length(&mut apu, 0b00001_000);
write_pulse2_length(&mut apu, 0b00001_000);
load_triangle_length(&mut apu, 1);
write_noise_length(&mut apu, 0b00001_000);
assert_eq!(apu.read_status(0) & 0b0000_1111, 0b0000_1111);
apu.write_enable(0b0000_0000);
assert_eq!(apu.read_status(0) & 0b0000_1111, 0b0000_0000);
}
#[test]
fn test_enable_dmc_restarts_sample_when_empty() {
let mut apu = Apu::new_for_testing();
apu.dmc_mut().write_sample_address(0x00); apu.dmc_mut().write_sample_length(0x01);
apu.write_enable(STATUS_DMC);
assert_eq!(apu.read_status(0) & STATUS_DMC, STATUS_DMC);
}
#[test]
fn test_disable_dmc_clears_bytes_remaining_after_disable_delay() {
let mut apu = Apu::new_for_testing();
apu.dmc_mut().write_sample_address(0x00);
apu.dmc_mut().write_sample_length(0x01);
apu.write_enable(STATUS_DMC);
assert!(apu.dmc().has_bytes_remaining());
assert_eq!(apu.read_status(0) & STATUS_DMC, STATUS_DMC);
apu.write_enable(0b0000_0000);
assert!(apu.dmc().has_bytes_remaining());
assert_eq!(apu.read_status(0) & STATUS_DMC, STATUS_DMC);
apu.clock();
assert!(apu.dmc().has_bytes_remaining());
assert_eq!(apu.read_status(0) & STATUS_DMC, STATUS_DMC);
apu.clock();
assert!(!apu.dmc().has_bytes_remaining());
assert_eq!(apu.read_status(0) & STATUS_DMC, 0);
}
#[test]
fn test_write_enable_clears_dmc_interrupt() {
let mut apu = Apu::new_for_testing();
apu.dmc_mut().write_flags_and_rate(0b1000_0000); apu.dmc_mut().write_sample_address(0x00);
apu.dmc_mut().write_sample_length(0x00);
apu.write_enable(0b0000_0000);
assert_eq!(apu.read_status(0) & STATUS_DMC_IRQ, 0);
}
#[test]
fn test_mixer_all_channels_silent() {
let apu = Apu::new_for_testing();
let output = apu.mix();
assert_eq!(output, 0.0);
}
#[test]
fn test_mixer_pulse_only() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
apu.pulse1_mut().write_control(0b1111_1111); apu.pulse1_mut().write_timer_low(0x08); write_pulse1_length(&mut apu, 0b00001_000); let output = apu.mix();
assert!(output > 0.0);
assert!(output <= 1.0);
}
#[test]
fn test_mixer_isolation_triangle_only_no_bleed_from_muted_channels() {
let mut triangle_only = Apu::new_for_testing();
triangle_only.set_pulse1_enabled(false);
triangle_only.set_pulse2_enabled(false);
triangle_only.set_noise_enabled(false);
triangle_only.set_dmc_enabled(false);
triangle_only.set_triangle_enabled(true);
triangle_only.write_enable(STATUS_TRIANGLE);
triangle_only.triangle_mut().write_linear_counter(0x7F);
triangle_only.triangle_mut().trigger_linear_counter_reload();
write_triangle_length(&mut triangle_only, 0b00001_000);
let baseline = triangle_only.mix();
assert!(baseline > 0.0, "Expected non-zero triangle-only mix output");
let mut loud_but_muted = Apu::new_for_testing();
loud_but_muted.set_pulse1_enabled(false);
loud_but_muted.set_pulse2_enabled(false);
loud_but_muted.set_noise_enabled(false);
loud_but_muted.set_dmc_enabled(false);
loud_but_muted.set_triangle_enabled(true);
loud_but_muted.write_enable(STATUS_TRIANGLE);
loud_but_muted.triangle_mut().write_linear_counter(0x7F);
loud_but_muted
.triangle_mut()
.trigger_linear_counter_reload();
write_triangle_length(&mut loud_but_muted, 0b00001_000);
loud_but_muted.pulse1_mut().write_control(0b1111_1111);
loud_but_muted.pulse1_mut().write_timer_low(0x08);
write_pulse1_length(&mut loud_but_muted, 0b00001_000);
loud_but_muted.pulse2_mut().write_control(0b1111_1111);
loud_but_muted.pulse2_mut().write_timer_low(0x08);
write_pulse2_length(&mut loud_but_muted, 0b00001_000);
loud_but_muted.noise_mut().write_envelope(0b0011_1111);
write_noise_length(&mut loud_but_muted, 0xFF);
loud_but_muted.dmc_mut().write_direct_load(0b0111_1111);
let with_muted_channels_configured = loud_but_muted.mix();
assert!(
(with_muted_channels_configured - baseline).abs() < 1e-9,
"Muted channels must not affect mixer output"
);
}
#[test]
fn test_mixer_output_range() {
let mut apu = Apu::new_for_testing();
apu.pulse1_mut().write_control(0b1111_1111); apu.pulse1_mut().write_timer_low(0x08); write_pulse1_length(&mut apu, 0b00001_000);
apu.pulse2_mut().write_control(0b1111_1111); apu.pulse2_mut().write_timer_low(0x08); write_pulse2_length(&mut apu, 0b00001_000);
apu.triangle_mut().write_linear_counter(0xFF);
write_triangle_length(&mut apu, 0xFF);
apu.noise_mut().write_envelope(0b0011_1111);
write_noise_length(&mut apu, 0xFF);
apu.dmc_mut().write_direct_load(0b0111_1111);
let output = apu.mix();
assert!(output >= 0.0);
assert!(output <= 1.0);
}
#[test]
fn test_mixer_formula_pulse() {
let apu = Apu::new_for_testing();
let output = apu.mix();
assert_eq!(output, 0.0);
}
#[test]
fn test_mixer_combines_channels() {
let mut apu = Apu::new_for_testing();
apu.pulse1_mut().write_control(0b1111_0101); apu.pulse1_mut().write_timer_low(0x08); write_pulse1_length(&mut apu, 0b00001_000);
let pulse_only = apu.mix();
apu.dmc_mut().write_direct_load(0b0010_0000); let pulse_and_dmc = apu.mix();
assert!(pulse_and_dmc >= pulse_only);
}
#[test]
fn test_sample_generation_no_sample_initially() {
let apu = Apu::new_for_testing();
assert!(!apu.sample_ready());
}
#[test]
fn test_sample_generation_after_clocking() {
let mut apu = Apu::new_for_testing();
for _ in 0..41 {
apu.clock();
}
assert!(apu.sample_ready());
}
#[test]
fn test_sample_generation_retrieves_sample() {
let mut apu = Apu::new_for_testing();
for _ in 0..41 {
apu.clock();
}
assert!(apu.sample_ready());
let sample = apu.get_sample();
assert!(sample.is_some());
assert!(!apu.sample_ready());
}
#[test]
fn test_sample_generation_uses_mixer_output() {
let mut apu = Apu::new_for_testing();
apu.write_enable(STATUS_PULSE1);
apu.pulse1_mut().write_control(0b1011_1111); apu.pulse1_mut().write_timer_low(0x08); write_pulse1_length(&mut apu, 0b00001_000);
let mut non_zero_found = false;
for _ in 0..200 {
apu.clock();
if let Some(sample) = apu.get_sample()
&& sample > 0.0
{
non_zero_found = true;
assert!(sample <= 1.0);
}
}
assert!(
non_zero_found,
"Expected at least one non-zero sample with 50% duty cycle"
);
}
#[test]
fn test_sample_generation_timing() {
let mut apu = Apu::new_for_testing();
let mut sample_count = 0;
for _ in 0..1789 {
apu.clock();
if apu.sample_ready() {
apu.get_sample();
sample_count += 1;
}
}
assert!((43..=45).contains(&sample_count));
}
#[test]
fn test_sample_generation_configurable_rate() {
let mut apu = Apu::new_for_testing();
apu.set_sample_rate(48000.0);
let mut sample_count = 0;
for _ in 0..1789 {
apu.clock();
if apu.sample_ready() {
apu.get_sample();
sample_count += 1;
}
}
assert!((47..=49).contains(&sample_count));
}
#[test]
fn test_sample_generation_does_not_drop_samples_when_not_polled_each_cycle() {
let mut apu = Apu::new_for_testing();
for _ in 0..1789 {
apu.clock();
}
let mut sample_count = 0;
while apu.sample_ready() {
apu.get_sample();
sample_count += 1;
}
assert!((43..=45).contains(&sample_count));
}
#[test]
fn test_sample_generation_pending_queue_is_bounded() {
let mut apu = Apu::new_for_testing();
let samples_to_generate = MAX_PENDING_SAMPLES + 100;
for _ in 0..(samples_to_generate * 41) {
apu.clock();
}
let mut drained = 0usize;
while apu.sample_ready() {
apu.get_sample();
drained += 1;
}
assert_eq!(drained, MAX_PENDING_SAMPLES);
}
#[test]
fn test_sample_generation_pending_queue_drops_oldest() {
let mut apu = Apu::new_for_testing();
for i in 0..(MAX_PENDING_SAMPLES + 2) {
apu.push_sample_for_test(i as f32);
}
let first = apu.get_sample().expect("sample should be available");
assert_eq!(first, 2.0);
}
}