feat(audio): implement PSG pulse channel 2
This commit is contained in:
parent
9a646651da
commit
aee3621513
15 changed files with 457 additions and 89 deletions
|
|
@ -20,7 +20,7 @@ const REGIONS: [(u32, u32, u32); 5] = [
|
|||
const MARKER: u32 = 0xa55a_c33c;
|
||||
|
||||
#[test]
|
||||
fn sound_reset_disables_running_pulse_only_when_selected() {
|
||||
fn sound_reset_disables_running_pulses_only_when_selected() {
|
||||
use gba_core::io::SOUNDCNT_X;
|
||||
for thumb in [false, true] {
|
||||
for flags in [0, 0x40] {
|
||||
|
|
@ -29,10 +29,12 @@ fn sound_reset_disables_running_pulse_only_when_selected() {
|
|||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
bus.write16(0x04000062, 0xf080).unwrap();
|
||||
bus.write16(0x04000064, 0x87ff).unwrap();
|
||||
bus.write16(0x04000068, 0xf080).unwrap();
|
||||
bus.write16(0x0400006c, 0x87fe).unwrap();
|
||||
complete(&mut machine, return_pc);
|
||||
assert_eq!(
|
||||
machine.memory().read16(SOUNDCNT_X).unwrap(),
|
||||
if flags == 0 { 0x81 } else { 0 }
|
||||
if flags == 0 { 0x83 } else { 0 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ fn wave_banks_and_idle_channel_masks_preserve_explicit_limits() {
|
|||
bus.write16(0x04000070, 0x40).unwrap();
|
||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0);
|
||||
bus.write32(WAVE_RAM, 0xaabbccdd).unwrap();
|
||||
for address in [0x0400006c, 0x04000074, 0x0400007c] {
|
||||
for address in [0x04000074, 0x0400007c] {
|
||||
let previous = bus.read16(address).unwrap();
|
||||
assert!(bus
|
||||
.write16(address, 0xffff)
|
||||
|
|
|
|||
|
|
@ -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, 0xe2800068, 0xe59f1008, 0xe3a02902, 0xe8a00006, 0xeafffffe, 0x12345678,
|
||||
0xe3a00301, 0xe2800070, 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(0x04000068).unwrap(), 0);
|
||||
assert_eq!(machine.memory().read16(0x04000070).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, 0x0400006c).unwrap();
|
||||
bus.write32(DMA_BASE + 4, 0x04000074).unwrap();
|
||||
bus.write32(DMA_BASE + 8, 0x84000001).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
let before = machine.cpu().clone();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Original public-bus tests for PSG channel 1; no game code or audio assets.
|
||||
//! Original public-bus tests for PSG pulse channels; no game code or audio assets.
|
||||
#[path = "pulse_sound/channel2.rs"]
|
||||
mod channel2;
|
||||
use gba_core::{
|
||||
audio::StereoLevel,
|
||||
cpu::Cpu,
|
||||
|
|
|
|||
263
crates/gba-core/tests/pulse_sound/channel2.rs
Normal file
263
crates/gba-core/tests/pulse_sound/channel2.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
//! Original channel 2 mapping, isolation, and shared-mixer regressions.
|
||||
use super::*;
|
||||
|
||||
const DUTY2: u32 = 0x04000068;
|
||||
const FREQUENCY2: u32 = 0x0400006c;
|
||||
|
||||
fn channel2() -> Memory {
|
||||
let mut bus = bus();
|
||||
bus.write16(MIX, 0x2277).unwrap();
|
||||
bus
|
||||
}
|
||||
|
||||
fn start2(bus: &mut Memory) {
|
||||
bus.write16(DUTY2, 0xf080).unwrap();
|
||||
bus.write16(FREQUENCY2, 0x87ff).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel2_register_widths_masks_and_status_do_not_alias_channel1() {
|
||||
let mut bus = channel2();
|
||||
bus.write32(DUTY2, 0xfffff0bf).unwrap(); // High halfword is unused, not frequency.
|
||||
assert_eq!(bus.read32(DUTY2).unwrap(), 0xf080);
|
||||
assert_eq!(bus.read16(DUTY).unwrap(), 0);
|
||||
bus.write8(FREQUENCY2, 255).unwrap();
|
||||
bus.write8(FREQUENCY2 + 1, 0x87).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
|
||||
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.write32(SOUNDCNT_X, 0x8f).unwrap(); // Both status bits are read-only.
|
||||
assert_eq!(bus.read32(SOUNDCNT_X).unwrap(), 0x82);
|
||||
bus.write32(FREQUENCY2, 0xffffc7ff).unwrap();
|
||||
assert_eq!(bus.read32(FREQUENCY2).unwrap(), 0x4000);
|
||||
assert_eq!(bus.read16(FREQUENCY).unwrap(), 0);
|
||||
assert_eq!(bus.read16(SWEEP).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel1_sweep_and_unused_register_gaps_cannot_enable_channel2_sweep() {
|
||||
let mut dirty = channel2();
|
||||
let mut clean = channel2();
|
||||
for bus in [&mut dirty, &mut clean] {
|
||||
start2(bus); // Frequency2047: any positive sweep would overflow.
|
||||
bus.write16(SWEEP, 0x11).unwrap();
|
||||
bus.write16(DUTY, 0xf080).unwrap();
|
||||
bus.write16(FREQUENCY, 0x85dc).unwrap(); // Channel1 frequency1500 overflows on trigger.
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
|
||||
}
|
||||
for address in [0x04000066, 0x0400006a, 0x0400006e] {
|
||||
dirty.write16(address, 0xffff).unwrap();
|
||||
assert_eq!(dirty.read16(address).unwrap(), 0);
|
||||
}
|
||||
for cycles in [15, 1, 31, SEQUENCE * 4, 1000000] {
|
||||
dirty.advance_cycles(cycles);
|
||||
clean.advance_cycles(cycles);
|
||||
assert_eq!(dirty.audio_level(), clean.audio_level());
|
||||
assert_eq!(dirty.read16(SOUNDCNT_X).unwrap(), 0x82);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pulse_channels_keep_independent_frequency_phase_retrigger_and_dac_state() {
|
||||
let mut bus = bus();
|
||||
bus.write16(MIX, 0x2177).unwrap(); // Channel1 right, channel2 left.
|
||||
start(&mut bus);
|
||||
bus.write16(DUTY2, 0x7080).unwrap();
|
||||
bus.write16(FREQUENCY2, 0x87fe).unwrap(); // 32 clocks per edge rather than16.
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: 56,
|
||||
right: 120
|
||||
}
|
||||
);
|
||||
bus.advance_cycles(16);
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: 56,
|
||||
right: -120
|
||||
}
|
||||
);
|
||||
bus.write16(FREQUENCY, 0x87ff).unwrap(); // Must not restart channel2's timer.
|
||||
bus.advance_cycles(16);
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: -56,
|
||||
right: -120
|
||||
}
|
||||
);
|
||||
bus.write8(DUTY + 1, 0).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: -56,
|
||||
right: 0
|
||||
}
|
||||
);
|
||||
bus.write8(DUTY2 + 1, 0).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triggers_share_sequencer_phase_but_envelopes_remain_independent() {
|
||||
let mut bus = bus();
|
||||
bus.write16(MIX, 0x2177).unwrap();
|
||||
bus.write16(DUTY, 0xc180).unwrap(); // Volume12, falling every envelope tick.
|
||||
bus.write16(FREQUENCY, 0x87ff).unwrap();
|
||||
bus.advance_cycles(SEQUENCE * 6);
|
||||
bus.write16(DUTY2, 0x3980).unwrap(); // Volume3, rising every envelope tick.
|
||||
bus.write16(FREQUENCY2, 0x87ff).unwrap();
|
||||
bus.advance_cycles(SEQUENCE * 2); // Shared step7, not eight new steps after trigger2.
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: 32,
|
||||
right: 88
|
||||
}
|
||||
);
|
||||
bus.advance_cycles(SEQUENCE * 8);
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: 40,
|
||||
right: 80
|
||||
}
|
||||
);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x83);
|
||||
bus.write8(DUTY2 + 1, 0).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x81);
|
||||
assert_eq!(bus.audio_level(), StereoLevel { left: 0, right: 80 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separate_length_counters_and_channel2_empty_reload_observe_shared_phase() {
|
||||
let mut bus = bus();
|
||||
bus.write16(MIX, 0).unwrap(); // Muting does not pause length.
|
||||
bus.write16(DUTY, 0xf0bf).unwrap();
|
||||
bus.write16(FREQUENCY, 0xc7ff).unwrap();
|
||||
bus.write16(DUTY2, 0xf0be).unwrap();
|
||||
bus.write16(FREQUENCY2, 0xc7ff).unwrap();
|
||||
bus.advance_cycles(SEQUENCE);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
|
||||
bus.advance_cycles(SEQUENCE * 2);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
bus.write16(FREQUENCY2, 0xc7ff).unwrap(); // Next step3: reload length63, not64.
|
||||
bus.advance_cycles(SEQUENCE * 125);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
|
||||
bus.advance_cycles(SEQUENCE);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixer_sums_pulses_before_rounding_and_clips_only_after_direct_sound() {
|
||||
let mut bus = bus();
|
||||
bus.write16(MIX, 0x3300).unwrap(); // Both channels, both sides, master gain1.
|
||||
bus.write16(DUTY, 0x2080).unwrap(); // +2 at duty position0.
|
||||
bus.write16(FREQUENCY, 0x8000).unwrap();
|
||||
bus.write16(DUTY2, 0x1000).unwrap(); // -1 at duty position0.
|
||||
bus.write16(FREQUENCY2, 0x8000).unwrap();
|
||||
bus.write16(SOUNDCNT_H, 0).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(0)); // (2 - 1) >>2, not (2 >>2) + (-1 >>2).
|
||||
bus.write16(MIX, 0x2200).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(-1));
|
||||
bus.write16(MIX, 0x3377).unwrap();
|
||||
bus.write16(SOUNDCNT_H, 0x0306).unwrap();
|
||||
start(&mut bus);
|
||||
start2(&mut bus);
|
||||
assert_eq!(bus.audio_level(), stereo(240));
|
||||
bus.write32(FIFO_A, 0x7f7f7f7f).unwrap();
|
||||
bus.write32(TIMER_BASE, 0x0080ffff).unwrap();
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(511));
|
||||
bus.write16(MIX, 0).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(508));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_writes_and_channel2_dac_gate_cannot_start_playback() {
|
||||
let mut bus = channel2();
|
||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||
bus.write32(DUTY2, u32::MAX).unwrap();
|
||||
bus.write32(FREQUENCY2, u32::MAX).unwrap();
|
||||
assert_eq!(bus.read32(DUTY2).unwrap(), 0);
|
||||
assert_eq!(bus.read32(FREQUENCY2).unwrap(), 0);
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
bus.write16(DUTY2, 0x0780).unwrap();
|
||||
bus.write16(FREQUENCY2, 0x87ff).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
bus.write16(DUTY2, 0x0880).unwrap(); // Rising direction enables the logical DAC gate.
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80); // Still needs trigger.
|
||||
bus.write16(FREQUENCY2, 0x87ff).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
|
||||
bus.advance_cycles(SEQUENCE * 64); // Zero initial volume and period: active but silent.
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
|
||||
start(&mut bus);
|
||||
start2(&mut bus);
|
||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0);
|
||||
assert_eq!(bus.read16(DUTY).unwrap(), 0);
|
||||
assert_eq!(bus.read16(DUTY2).unwrap(), 0);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel2_runs_during_halt_and_freezes_during_stop() {
|
||||
for stop in [false, true] {
|
||||
let mut bus = channel2();
|
||||
start2(&mut bus);
|
||||
bus.write16(FREQUENCY2, 0x87fd).unwrap(); // 48 clocks per edge.
|
||||
bus.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
machine.step().unwrap();
|
||||
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 both_channels_keep_phase_across_large_batches_and_row_capture() {
|
||||
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(MIX, 0x2177).unwrap();
|
||||
bus.write16(SWEEP, 0x29).unwrap();
|
||||
bus.write16(DUTY, 0xa340).unwrap();
|
||||
bus.write16(FREQUENCY, 0x85ad).unwrap();
|
||||
bus.write16(DUTY2, 0x29c0).unwrap();
|
||||
bus.write16(FREQUENCY2, 0x87ab).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()
|
||||
);
|
||||
}
|
||||
let mut bus = channel2();
|
||||
start2(&mut bus);
|
||||
bus.advance_cycles(u32::MAX);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
|
||||
assert_eq!(bus.audio_level(), stereo(120));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue