feat(audio): implement PSG pulse channel 1

This commit is contained in:
0xrsydn 2026-10-07 01:53:42 +07:00
commit 9a646651da
18 changed files with 850 additions and 55 deletions

View file

@ -1,4 +1,9 @@
//! Nominal Direct Sound device state. No host audio or PSG synthesis.
//! Nominal Direct Sound and PSG pulse channel 1. No host audio output.
mod pulse;
use pulse::Pulse;
const SEQUENCER_PERIOD: u32 = 32768; // 16,777,216 Hz / 512 Hz.
pub const SOUND_START: u32 = 0x0400_0060;
pub const SOUNDCNT_H: u32 = 0x0400_0082;
@ -73,6 +78,9 @@ impl Fifo {
pub(crate) struct Audio {
enabled: bool,
psg: [u16; 17],
pulse: Pulse,
sequencer_phase: u16,
next_step: u8,
control: u16,
bias: u16,
wave: [[u8; 16]; 2],
@ -86,11 +94,13 @@ impl Audio {
}
pub(crate) fn unsupported(&self, address: u32, value: u8) -> Option<&'static str> {
if (self.enabled && address == SOUNDCNT_H && value & 3 == 3)
|| (address == SOUNDCNT_X && value & 0x80 != 0 && self.control & 3 == 3)
{
return Some("reserved PSG volume selection");
}
if self.enabled
&& matches!(
address,
0x0400_0065 | 0x0400_006d | 0x0400_0075 | 0x0400_007d
)
&& matches!(address, 0x0400_006d | 0x0400_0075 | 0x0400_007d)
&& value & 0x80 != 0
{
Some("PSG channel trigger (synthesis not implemented)")
@ -104,6 +114,9 @@ impl Audio {
}
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
if (SOUND_START..=SOUND_START + 5).contains(&address) {
return Some(self.pulse.read(address - SOUND_START));
}
if (WAVE_RAM..FIFO_A).contains(&address) {
return Some(self.wave[self.wave_bank()][(address - WAVE_RAM) as usize]);
}
@ -123,7 +136,7 @@ impl Audio {
self.psg[((address - SOUND_START) / 2) as usize] & mask
}
SOUNDCNT_H => self.control,
SOUNDCNT_X => u16::from(self.enabled) << 7,
SOUNDCNT_X => (u16::from(self.enabled) << 7) | u16::from(self.pulse.active),
SOUNDBIAS => self.bias,
0x0400_0086 | 0x0400_008a => 0,
_ => return None, // FIFO is write-only; open-bus readback is not modeled.
@ -143,6 +156,11 @@ impl Audio {
self.wave[self.wave_bank()][(address - WAVE_RAM) as usize] = value;
continue;
}
if self.enabled && (SOUND_START..=SOUND_START + 5).contains(&address) {
self.pulse
.write(address - SOUND_START, value, self.next_step);
continue;
}
let shift = (address & 1) * 8;
let merge = |old: u16| (old & !(255 << shift)) | (u16::from(value) << shift);
match address & !1 {
@ -163,10 +181,14 @@ impl Audio {
SOUNDCNT_X if address & 1 == 0 => {
if self.enabled && value & 0x80 == 0 {
self.psg = [0; 17];
self.pulse = Pulse::default();
for fifo in &mut self.fifo {
fifo.reset_queue();
}
}
if !self.enabled && value & 0x80 != 0 {
self.next_step = 0;
}
self.enabled = value & 0x80 != 0;
}
SOUNDBIAS => self.bias = merge(self.bias) & 0xc3fe,
@ -175,6 +197,29 @@ impl Audio {
}
}
/// Free-running 512 Hz divider; master enable resets the sequencer step,
/// not the divider phase. STOP is gated by the memory bus before this call.
pub(crate) fn advance(&mut self, mut cycles: u32) {
if !self.enabled {
self.sequencer_phase = ((u64::from(self.sequencer_phase) + u64::from(cycles))
% u64::from(SEQUENCER_PERIOD)) as u16;
return;
}
while cycles != 0 {
let count = cycles.min(SEQUENCER_PERIOD - u32::from(self.sequencer_phase));
self.pulse.advance(count);
let phase = u32::from(self.sequencer_phase) + count;
if phase == SEQUENCER_PERIOD {
self.sequencer_phase = 0;
self.pulse.clock(self.next_step);
self.next_step = (self.next_step + 1) & 7;
} else {
self.sequencer_phase = phase as u16;
}
cycles -= count;
}
}
pub(crate) fn timer_overflows(&mut self, timer: usize, count: u64) {
if !self.enabled || timer > 1 {
return;
@ -208,7 +253,13 @@ impl Audio {
}
}
}
for level in &mut sides {
let psg_control = self.psg[16];
let ratio = [1, 2, 4, 0][usize::from(self.control & 3)];
for (side, level) in sides.iter_mut().enumerate() {
if psg_control & (1 << (8 + side * 4)) != 0 {
let volume = ((psg_control >> (side * 4)) & 7) as i16 + 1;
*level += (self.pulse.sample() * volume * ratio) >> 2;
}
*level = (*level + (self.bias & 0x3ff) as i16).clamp(0, 1023) - 512;
}
StereoLevel {

View file

@ -0,0 +1,207 @@
//! Nominal channel 1 oscillator and modulation units. No host-clock dependency.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(super) struct Pulse {
sweep: u8,
duty: u8,
envelope: u8,
frequency: u16,
length_enabled: bool,
length: u8,
pub(super) active: bool,
phase: u8,
remaining: u32,
volume: u8,
envelope_ticks: u8,
envelope_running: bool,
sweep_shadow: u16,
sweep_ticks: u8,
sweep_running: bool,
subtracted: bool,
}
impl Pulse {
pub(super) fn read(&self, offset: u32) -> u8 {
match offset {
0 => self.sweep,
2 => self.duty << 6,
3 => self.envelope,
5 => u8::from(self.length_enabled) << 6,
_ => 0,
}
}
fn period(&self) -> u32 {
16 * (2048 - u32::from(self.frequency))
}
fn dac_enabled(&self) -> bool {
self.envelope & 0xf8 != 0
}
fn envelope_can_run(&self) -> bool {
self.envelope & 7 != 0
&& if self.envelope & 8 != 0 {
self.volume < 15
} else {
self.volume > 0
}
}
fn sweep_period(&self) -> u8 {
let period = (self.sweep >> 4) & 7;
if period == 0 {
8
} else {
period
}
}
fn calculate_sweep(&mut self) -> u16 {
let delta = self.sweep_shadow >> (self.sweep & 7);
if self.sweep & 8 != 0 {
self.subtracted = true;
self.sweep_shadow - delta
} else {
self.sweep_shadow + delta
}
}
pub(super) fn write(&mut self, offset: u32, value: u8, next_step: u8) {
match offset {
0 => {
if self.subtracted && self.sweep & 8 != 0 && value & 8 == 0 {
self.active = false;
}
self.sweep = value & 0x7f;
}
2 => {
self.duty = value >> 6;
self.length = 64 - (value & 63);
}
3 => {
let was_running = self.envelope_running;
self.envelope = value;
if !self.dac_enabled() {
self.active = false;
}
self.envelope_running = self.envelope_can_run();
if !was_running && self.envelope_running {
self.envelope_ticks = value & 7;
}
}
4 => self.frequency = (self.frequency & 0x700) | u16::from(value),
5 => {
self.frequency = (self.frequency & 255) | (u16::from(value & 7) << 8);
let extra_length_clock = next_step & 1 != 0;
if !self.length_enabled && value & 0x40 != 0 && extra_length_clock {
self.clock_length();
}
self.length_enabled = value & 0x40 != 0;
if value & 0x80 != 0 {
self.active = self.dac_enabled();
if self.length == 0 {
self.length = 64;
if self.length_enabled && extra_length_clock {
self.length -= 1;
}
}
// Retrigger reloads the oscillator timer but preserves duty position.
// Sub-divider alignment and hardware startup latency are not modeled.
self.remaining = self.period();
self.volume = self.envelope >> 4;
self.envelope_ticks = if self.envelope & 7 == 0 {
8
} else {
self.envelope & 7
};
self.envelope_running = self.envelope_can_run();
self.sweep_shadow = self.frequency;
self.sweep_ticks = self.sweep_period();
self.sweep_running = self.sweep & 0x77 != 0;
self.subtracted = false;
if self.sweep & 7 != 0 && self.calculate_sweep() > 2047 {
self.active = false;
}
}
}
_ => {}
}
}
/// Batch oscillator edges until the next modulation event or bus access.
pub(super) fn advance(&mut self, cycles: u32) {
if !self.active || cycles == 0 {
return;
}
if cycles < self.remaining {
self.remaining -= cycles;
return;
}
let rest = cycles - self.remaining;
let period = self.period();
self.phase = ((u32::from(self.phase) + 1 + rest / period) & 7) as u8;
self.remaining = period - rest % period;
}
fn clock_length(&mut self) {
if self.length != 0 {
self.length -= 1;
if self.length == 0 {
self.active = false;
}
}
}
pub(super) fn clock(&mut self, step: u8) {
if step & 1 == 0 && self.length_enabled {
self.clock_length();
}
if step & 3 == 2 && self.sweep_running {
self.sweep_ticks -= 1;
if self.sweep_ticks == 0 {
self.sweep_ticks = self.sweep_period();
if self.sweep & 0x70 != 0 {
let next = self.calculate_sweep();
if next > 2047 {
self.active = false;
} else if self.sweep & 7 != 0 {
self.frequency = next;
self.sweep_shadow = next;
if self.calculate_sweep() > 2047 {
self.active = false;
}
}
}
}
}
if step == 7 && self.active && self.envelope_running {
self.envelope_ticks -= 1;
if self.envelope_ticks == 0 {
if self.envelope & 8 != 0 {
self.volume += 1;
} else {
self.volume -= 1;
}
self.envelope_ticks = self.envelope & 7;
self.envelope_running = self.envelope_can_run();
}
}
}
/// Signed pulse amplitude before PSG master volume and stereo routing.
pub(super) fn sample(&self) -> i16 {
if !self.active {
return 0;
}
let high = [0x80u8, 0x81, 0xe1, 0x7e][usize::from(self.duty)] & (1 << self.phase) != 0;
if high {
i16::from(self.volume)
} else {
-i16::from(self.volume)
}
}
}
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,205 @@
//! Original arithmetic and state-transition regressions, not imported hardware tests.
use super::*;
fn start(frequency: u16, duty: u8, envelope: u8, sweep: u8) -> Pulse {
let mut pulse = Pulse::default();
pulse.write(0, sweep, 0);
pulse.write(2, duty << 6, 0);
pulse.write(3, envelope, 0);
pulse.write(4, frequency as u8, 0);
pulse.write(5, 0x80 | (frequency >> 8) as u8, 0);
pulse
}
#[test]
fn all_frequencies_and_duties_match_independent_edge_arithmetic() {
let high_phases: [&[u8]; 4] = [&[7], &[0, 7], &[0, 5, 6, 7], &[1, 2, 3, 4, 5, 6]];
for frequency in 0..2048 {
let period = 16 * (2048 - u32::from(frequency));
for duty in 0..4 {
for cycles in [0, period - 1, period, period * 8, u32::MAX] {
let mut pulse = start(frequency, duty, 0xf0, 0);
pulse.advance(cycles);
let phase = ((cycles / period) % 8) as u8;
assert_eq!(pulse.phase, phase);
assert_eq!(pulse.remaining, period - cycles % period);
assert_eq!(
pulse.sample(),
if high_phases[duty as usize].contains(&phase) {
15
} else {
-15
}
);
}
}
}
}
#[test]
fn frequency_writes_preserve_current_interval_and_trigger_preserves_duty_phase() {
let mut pulse = start(2046, 2, 0xf0, 0); // 32 clocks per edge.
pulse.advance(31);
pulse.write(4, 255, 0); // Next period is 16 clocks; current interval has one left.
pulse.advance(1);
assert_eq!(pulse.phase, 1);
assert_eq!(pulse.remaining, 16);
pulse.advance(7);
pulse.write(5, 0x87, 0);
assert_eq!(pulse.phase, 1);
assert_eq!(pulse.remaining, 16);
pulse.advance(15);
assert_eq!(pulse.phase, 1);
pulse.advance(1);
assert_eq!(pulse.phase, 2);
}
#[test]
fn length_load_enable_extra_clock_expiry_and_zero_reload() {
let mut pulse = start(1000, 2, 0xf0, 0);
pulse.write(2, 63, 0); // One remaining length tick.
pulse.clock(0); // Disabled length does not expire.
assert!(pulse.active);
pulse.write(5, 0x43, 1); // Enable before a step that does not clock length.
assert_eq!(pulse.length, 0);
assert!(!pulse.active);
pulse.write(5, 0xc3, 1); // Trigger with empty length reloads 63 in this phase.
assert_eq!(pulse.length, 63);
assert!(pulse.active);
for _ in 0..62 {
pulse.clock(0);
}
assert!(pulse.active);
pulse.clock(0);
assert!(!pulse.active);
pulse.clock(0);
assert_eq!(pulse.length, 0);
pulse.write(5, 0xc3, 0);
assert_eq!(pulse.length, 64);
}
#[test]
fn simultaneous_length_enable_and_trigger_reload_after_extra_expiry() {
let mut pulse = start(1000, 0, 0xf0, 0);
pulse.write(2, 63, 0);
pulse.write(5, 0xc3, 1);
assert_eq!(pulse.length, 63);
assert!(pulse.active);
pulse.write(2, 62, 0);
pulse.write(5, 0xc3, 1); // Already enabled: do not add another extra decrement.
assert_eq!(pulse.length, 2);
}
#[test]
fn envelopes_step_at_selected_period_saturate_and_do_not_clear_status() {
for envelope in [0x39, 0xc1] {
let mut pulse = start(0, 1, envelope, 0);
for tick in 1..=20 {
pulse.clock(7);
let expected = if envelope & 8 != 0 {
(3 + tick).min(15)
} else {
12u8.saturating_sub(tick)
};
assert_eq!(pulse.volume, expected);
assert!(pulse.active);
}
}
let mut pulse = start(0, 1, 0x53, 0);
pulse.clock(7);
pulse.clock(7);
assert_eq!(pulse.volume, 5);
pulse.clock(7);
assert_eq!(pulse.volume, 4);
pulse.write(3, 0xa0, 0); // Period zero holds current volume until retrigger.
for _ in 0..16 {
pulse.clock(7);
}
assert_eq!(pulse.volume, 4);
pulse.write(5, 0x80, 0);
assert_eq!(pulse.volume, 10);
}
#[test]
fn live_envelope_configuration_retains_volume_and_dac_off_requires_retrigger() {
let mut pulse = start(0, 1, 0x52, 0);
pulse.clock(7);
pulse.write(3, 0xa9, 0); // Change direction/period, not current volume or remaining tick.
assert_eq!(pulse.volume, 5);
pulse.clock(7);
assert_eq!(pulse.volume, 6);
pulse.write(3, 0, 0);
assert!(!pulse.active);
assert_eq!(pulse.sample(), 0);
pulse.write(3, 0x08, 0); // Rising envelope with initial zero has DAC enabled.
assert!(!pulse.active);
pulse.write(5, 0x80, 0);
assert!(pulse.active);
assert_eq!(pulse.volume, 0);
pulse.write(3, 7, 0); // Period alone cannot enable DAC.
pulse.write(5, 0x80, 0);
assert!(!pulse.active);
}
#[test]
fn sweep_checks_trigger_overflow_and_second_calculation_after_update() {
assert!(!start(1500, 1, 0xf0, 0x11).active);
assert!(!start(1500, 1, 0xf0, 1).active); // Time zero still checks at trigger.
let mut pulse = start(1000, 1, 0xf0, 0x11);
assert!(pulse.active);
pulse.clock(2);
assert_eq!(pulse.frequency, 1500);
assert!(!pulse.active); // Next calculated 2250 disables before another sweep tick.
}
#[test]
fn sweep_uses_shadow_frequency_and_preserves_live_write_until_next_update() {
let mut pulse = start(400, 1, 0xf0, 0x21);
pulse.write(4, 200, 0);
assert_eq!(pulse.frequency, 456);
pulse.clock(2);
assert_eq!(pulse.frequency, 456);
pulse.clock(6);
assert_eq!(pulse.frequency, 600); // 400 + 400/2, not based on 456.
assert_eq!(pulse.read(4), 0); // Frequency is write-only.
assert_eq!(pulse.read(5), 0);
}
#[test]
fn sweep_zero_period_shift_zero_and_subtraction_direction_rules() {
let mut pulse = start(500, 1, 0xf0, 1);
for _ in 0..32 {
pulse.clock(2);
}
assert_eq!(pulse.frequency, 500); // Time zero suppresses periodic changes.
let mut pulse = start(1200, 1, 0xf0, 0x10);
assert!(pulse.active); // Shift zero skips trigger-time overflow check.
pulse.clock(2);
assert!(!pulse.active); // Timed overflow check still applies.
let mut pulse = start(1000, 1, 0xf0, 0x19);
pulse.clock(2);
assert_eq!(pulse.frequency, 500);
pulse.write(0, 0x11, 0);
assert!(!pulse.active); // Clearing negate after subtraction disables the channel.
let mut pulse = start(1000, 1, 0xf0, 0x18);
pulse.write(0, 0x10, 0); // No subtraction calculation has happened.
assert!(pulse.active);
}
#[test]
fn split_clock_advances_match_one_batch() {
let mut random = 0x5a17u32;
let mut bulk = start(1783, 3, 0xa2, 0x29);
let mut split = bulk;
for step in 0..1000u32 {
random = random.wrapping_mul(1664525).wrapping_add(1013904223);
let cycles = random & 0xffff;
bulk.advance(cycles);
for part in [cycles / 3, cycles / 3, cycles - (cycles / 3) * 2] {
split.advance(part);
}
bulk.clock((step & 7) as u8);
split.clock((step & 7) as u8);
assert_eq!(bulk, split);
}
}

View file

@ -438,6 +438,7 @@ impl Io {
audio: &mut Audio,
cycles: u32,
) -> u16 {
audio.advance(cycles);
let mut pending = 0;
let mut overflows = 0;
for (index, timer) in timers.iter_mut().enumerate() {

View file

@ -1,4 +1,4 @@
//! Staged timer and Direct Sound clocks for one instruction, IRQ entry, or DMA unit.
//! Staged timer and audio clocks for one instruction, IRQ entry, or DMA unit.
//! Other devices remain on the instruction-boundary scheduler.
use super::{Io, Timer, IF, TIMER_BASE};

View file

@ -165,7 +165,7 @@ impl Memory {
Ok(memory)
}
/// Current committed Direct Sound mixer level. This is not a host sample stream.
/// Current committed audio mixer level. This is not a host sample stream.
pub fn audio_level(&self) -> StereoLevel {
self.io.audio.level()
}

View file

@ -9,6 +9,8 @@ fn sound(memory: &mut Memory, period: u16) {
memory.write16(SOUNDCNT_X, 0x80).unwrap();
memory.write16(SOUNDBIAS, 0x200).unwrap();
memory.write16(SOUNDCNT_H, 0x0304).unwrap();
memory.write16(0x04000062, 0xf080).unwrap();
memory.write16(0x04000064, 0x87ff).unwrap(); // Running pulse, unrouted; rollback must include it.
memory
.write32(
TIMER_BASE,
@ -17,6 +19,34 @@ fn sound(memory: &mut Memory, period: u16) {
.unwrap();
}
#[test]
fn pulse_status_load_observes_length_expiry_at_bus_completion() {
let (cpu, mut memory) = prepared(false, &[0xe5910000], &[(1, SOUNDCNT_X)]);
sound(&mut memory, 1000);
memory.write16(0x04000062, 0xf03f).unwrap();
memory.write16(0x04000064, 0xc7ff).unwrap();
memory.advance_cycles(32768 - 7);
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(machine.cpu().registers()[0], 0x80);
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), 0x80);
}
#[test]
fn pulse_trigger_reloads_timer_at_store_completion() {
let (cpu, mut memory) = prepared(false, &[0xe5810000], &[(0, 0x87ff), (1, 0x04000064)]);
sound(&mut memory, 1000);
memory.write16(0x04000080, 0x1177).unwrap();
memory.write16(SOUNDCNT_H, 2).unwrap();
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(machine.cycles(), 7);
machine.memory_mut().advance_cycles(15);
assert_eq!(machine.memory().audio_level().left, 120);
machine.memory_mut().advance_cycles(1);
assert_eq!(machine.memory().audio_level().left, -120);
}
#[test]
fn fifo_store_becomes_visible_after_its_bus_completion_not_at_instruction_start() {
let (cpu, mut memory) = prepared(
@ -65,7 +95,7 @@ fn failed_cpu_load_rolls_back_timer_audio_and_pending_refill_requests() {
#[test]
fn failed_dma_source_and_destination_restore_audio_after_speculative_timer_edges() {
for (source, destination, word) in [(FIFO_A, 0x02001000, 0), (0x02000000, 0x04000064, 0x8000)] {
for (source, destination, word) in [(FIFO_A, 0x02001000, 0), (0x02000000, 0x0400006c, 0x8000)] {
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
sound(&mut memory, 1);
memory.write32(FIFO_A, 0x04030201).unwrap();
@ -91,7 +121,7 @@ fn batch_validation_simulates_sound_enable_and_disable_before_committing_any_wri
.write_words(&[
(0x02000000, 0x11223344),
(SOUNDCNT_X, 0x80),
(0x04000064, 0x8000),
(0x0400006c, 0x8000),
])
.is_err());
assert_eq!(memory.read32(0x02000000).unwrap(), 0);

View file

@ -19,6 +19,25 @@ const REGIONS: [(u32, u32, u32); 5] = [
];
const MARKER: u32 = 0xa55a_c33c;
#[test]
fn sound_reset_disables_running_pulse_only_when_selected() {
use gba_core::io::SOUNDCNT_X;
for thumb in [false, true] {
for flags in [0, 0x40] {
let (mut machine, return_pc) = prepare(flags, thumb, 0x1f);
let bus = machine.memory_mut();
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write16(0x04000062, 0xf080).unwrap();
bus.write16(0x04000064, 0x87ff).unwrap();
complete(&mut machine, return_pc);
assert_eq!(
machine.memory().read16(SOUNDCNT_X).unwrap(),
if flags == 0 { 0x81 } else { 0 }
);
}
}
}
#[test]
fn sound_reset_clears_disabled_fifo_queues_and_only_accessible_wave_bank() {
use gba_core::io::{FIFO_A, SOUNDCNT_H, SOUNDCNT_X, TIMER_BASE, WAVE_RAM};

View file

@ -159,7 +159,7 @@ fn full_queue_overflow_clears_queued_words() {
}
#[test]
fn wave_banks_and_idle_psg_masks_work_without_claiming_psg_playback() {
fn wave_banks_and_idle_channel_masks_preserve_explicit_limits() {
let mut bus = bus();
bus.write16(0x04000062, 0xffff).unwrap();
assert_eq!(bus.read16(0x04000062).unwrap(), 0xffc0);
@ -167,7 +167,7 @@ fn wave_banks_and_idle_psg_masks_work_without_claiming_psg_playback() {
bus.write16(0x04000070, 0x40).unwrap();
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0);
bus.write32(WAVE_RAM, 0xaabbccdd).unwrap();
for address in [0x04000064, 0x0400006c, 0x04000074, 0x0400007c] {
for address in [0x0400006c, 0x04000074, 0x0400007c] {
let previous = bus.read16(address).unwrap();
assert!(bus
.write16(address, 0xffff)

View file

@ -104,7 +104,7 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
#[test]
fn block_store_rejects_later_activation_before_earlier_control_writes() {
let code: [u32; 7] = [
0xe3a00301, 0xe2800060, 0xe59f1008, 0xe3a02902, 0xe8a00006, 0xeafffffe, 0x12345678,
0xe3a00301, 0xe2800068, 0xe59f1008, 0xe3a02902, 0xe8a00006, 0xeafffffe, 0x12345678,
];
let mut bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
bus.write16(SOUNDCNT_X, 0x80).unwrap();
@ -118,7 +118,7 @@ fn block_store_rejects_later_activation_before_earlier_control_writes() {
assert!(error.to_string().contains("PSG channel trigger"));
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(0x04000060).unwrap(), 0);
assert_eq!(machine.memory().read16(0x04000068).unwrap(), 0);
assert_eq!(machine.step(), Err(error));
}
@ -128,7 +128,7 @@ fn dma_activation_error_preserves_channel_progress_and_clocks() {
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write32(0x02000000, 0x8000).unwrap();
bus.write32(DMA_BASE, 0x02000000).unwrap();
bus.write32(DMA_BASE + 4, 0x04000064).unwrap();
bus.write32(DMA_BASE + 4, 0x0400006c).unwrap();
bus.write32(DMA_BASE + 8, 0x84000001).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
let before = machine.cpu().clone();

View file

@ -0,0 +1,213 @@
//! Original public-bus tests for PSG channel 1; no game code or audio assets.
use gba_core::{
audio::StereoLevel,
cpu::Cpu,
io::{FIFO_A, HALTCNT, IF, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, TIMER_BASE},
machine::Machine,
memory::{Memory, ROM_START},
};
const SWEEP: u32 = 0x04000060;
const DUTY: u32 = 0x04000062;
const FREQUENCY: u32 = 0x04000064;
const MIX: u32 = 0x04000080;
const SEQUENCE: u32 = 32768;
fn bus() -> Memory {
let mut bus = Memory::new(0xeafffffeu32.to_le_bytes().to_vec()).unwrap();
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write16(SOUNDBIAS, 0x200).unwrap();
bus.write16(MIX, 0x1177).unwrap(); // Channel 1, both sides, maximum volume.
bus.write16(SOUNDCNT_H, 2).unwrap(); // PSG 100%.
bus
}
fn start(bus: &mut Memory) {
bus.write16(DUTY, 0xf080).unwrap(); // 50% duty, volume15, no envelope, length64.
bus.write16(FREQUENCY, 0x87ff).unwrap(); // Frequency2047, trigger; length disabled.
}
fn stereo(value: i16) -> StereoLevel {
StereoLevel {
left: value,
right: value,
}
}
#[test]
fn register_widths_masks_status_and_wave_edges_without_general_timers() {
let mut bus = bus();
bus.write32(SWEEP, 0xf0bf0080).unwrap(); // Sweep bit7 ignored; duty50%, length1.
assert_eq!(bus.read32(SWEEP).unwrap(), 0xf0800000);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
bus.write8(FREQUENCY, 255).unwrap();
bus.write8(FREQUENCY + 1, 0x87).unwrap();
assert_eq!(bus.read32(FREQUENCY).unwrap(), 0);
assert_eq!(bus.read32(SOUNDCNT_X).unwrap(), 0x81);
bus.write16(SOUNDCNT_X, 0x80).unwrap(); // Status cannot be cleared by a write.
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x81);
assert_eq!(bus.audio_level(), stereo(120));
bus.advance_cycles(15);
assert_eq!(bus.audio_level(), stereo(120));
bus.advance_cycles(1);
assert_eq!(bus.audio_level(), stereo(-120));
bus.advance_cycles(64);
assert_eq!(bus.audio_level(), stereo(120));
assert_eq!(bus.read16(IF).unwrap(), 0); // PSG clocks do not generate IRQs.
}
#[test]
fn psg_stereo_volume_ratios_and_direct_sound_share_bias_and_clipping() {
let mut bus = bus();
start(&mut bus);
for (ratio, expected) in [(0, 30), (1, 60), (2, 120)] {
bus.write16(SOUNDCNT_H, ratio).unwrap();
assert_eq!(bus.audio_level(), stereo(expected));
}
bus.write16(MIX, 0x1070).unwrap();
assert_eq!(
bus.audio_level(),
StereoLevel {
left: 120,
right: 0
}
);
bus.write16(MIX, 0x0100).unwrap(); // Volume field zero means one, not mute.
assert_eq!(bus.audio_level(), StereoLevel { left: 0, right: 15 });
bus.write16(MIX, 0x1177).unwrap();
bus.write16(SOUNDCNT_H, 0x0306).unwrap();
bus.write32(FIFO_A, 0x7f7f7f7f).unwrap();
bus.write32(TIMER_BASE, 0x0080ffff).unwrap();
bus.advance_cycles(1);
assert_eq!(bus.audio_level(), stereo(511)); // 508 + 120 clips after mixing.
bus.write8(DUTY + 1, 0).unwrap();
assert_eq!(bus.audio_level(), stereo(508)); // DAC off removes only pulse contribution.
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
}
#[test]
fn length_clocks_at_256_hz_and_status_expires_independently_of_routing() {
let mut bus = bus();
bus.write16(MIX, 0).unwrap();
bus.write16(DUTY, 0xf0be).unwrap(); // Two length ticks.
bus.write16(FREQUENCY, 0xc7ff).unwrap();
bus.advance_cycles(SEQUENCE - 1);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x81);
bus.advance_cycles(1); // Step0, first length tick.
bus.advance_cycles(SEQUENCE * 2 - 1);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x81);
bus.advance_cycles(1); // Step2, second length tick.
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
assert_eq!(bus.read16(FREQUENCY).unwrap(), 0x4000);
}
#[test]
fn envelope_64_hz_and_sweep_128_hz_use_distinct_sequencer_steps() {
let mut bus = bus();
bus.write16(DUTY, 0x3980).unwrap(); // Volume3, increase every envelope tick.
bus.write16(FREQUENCY, 0x87ff).unwrap();
bus.advance_cycles(SEQUENCE * 8 - 1);
assert_eq!(bus.audio_level().left.abs(), 24);
bus.advance_cycles(1); // Step7.
assert_eq!(bus.audio_level().left.abs(), 32);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x81);
let mut bus = self::bus();
bus.write16(SWEEP, 0x11).unwrap();
bus.write16(DUTY, 0xf080).unwrap();
bus.write16(FREQUENCY, 0x83e8).unwrap(); // 1000 ->1500; second check overflows2250.
bus.advance_cycles(SEQUENCE * 3 - 1);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x81);
bus.advance_cycles(1); // Step2, first sweep clock.
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
assert_eq!(bus.audio_level(), stereo(0));
}
#[test]
fn sequencer_divider_runs_while_master_off_and_enable_resets_only_step() {
let mut bus = Memory::new(vec![]).unwrap();
bus.advance_cycles(SEQUENCE - 1);
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write16(DUTY, 0xf03f).unwrap();
bus.write16(FREQUENCY, 0xc000).unwrap();
bus.advance_cycles(1); // Not one whole new interval after enable.
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
bus.write16(SOUNDCNT_X, 0).unwrap();
assert_eq!(bus.read16(DUTY).unwrap(), 0);
bus.write16(DUTY, 0xffff).unwrap();
bus.write16(FREQUENCY, 0xffff).unwrap();
bus.write16(SOUNDCNT_X, 0x80).unwrap();
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
assert_eq!(bus.read16(DUTY).unwrap(), 0);
}
#[test]
fn reserved_psg_ratio_errors_are_atomic_including_enable_after_disabled_configuration() {
let mut bus = bus();
start(&mut bus);
let before = bus.audio_level();
let error = bus.write32(MIX, 0x00030000).unwrap_err();
assert!(error.to_string().contains("reserved PSG volume selection"));
assert_eq!(bus.read16(MIX).unwrap(), 0x1177);
assert_eq!(bus.read16(SOUNDCNT_H).unwrap(), 2);
assert_eq!(bus.audio_level(), before);
bus.write16(SOUNDCNT_X, 0).unwrap();
bus.write16(SOUNDCNT_H, 3).unwrap(); // Disabled sound still retains these bits.
assert!(bus.write16(SOUNDCNT_X, 0x80).is_err());
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0);
}
#[test]
fn halt_keeps_pulse_running_and_stop_freezes_it() {
for stop in [false, true] {
let mut bus = bus();
start(&mut bus);
bus.write16(FREQUENCY, 0x87fd).unwrap(); // 48 clocks per duty step.
bus.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
let cpu = machine.cpu().clone();
machine.step().unwrap();
assert_eq!(machine.cpu(), &cpu);
assert_eq!(
machine.memory().audio_level(),
stereo(if stop { 120 } else { -120 })
);
if stop {
machine.memory_mut().advance_cycles(u32::MAX);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().audio_level(), stereo(120));
}
}
}
#[test]
fn clock_batching_and_row_capture_do_not_change_psg_result() {
let mut bulk = bus();
let mut rows = bus();
let mut split = bus();
rows.set_scanline_rendering(true);
for bus in [&mut bulk, &mut rows, &mut split] {
bus.write16(SWEEP, 0x29).unwrap();
bus.write16(DUTY, 0xa340).unwrap();
bus.write16(FREQUENCY, 0x85ad).unwrap();
}
for cycles in [1, 15, 32751, 262145, 77777] {
bulk.advance_cycles(cycles);
rows.advance_cycles(cycles);
for _ in 0..cycles {
split.advance_cycles(1);
}
assert_eq!(bulk.audio_level(), split.audio_level());
assert_eq!(bulk.audio_level(), rows.audio_level());
assert_eq!(
bulk.read16(SOUNDCNT_X).unwrap(),
split.read16(SOUNDCNT_X).unwrap()
);
}
// Maximum batch must remain bounded and preserve an indefinite channel.
let mut bus = bus();
start(&mut bus);
bus.advance_cycles(u32::MAX);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x81);
assert_eq!(bus.audio_level(), stereo(120)); // floor(MAX/16) modulo8 = 7.
}