feat(audio): implement nominal Direct Sound playback and FIFO DMA
This commit is contained in:
parent
894f524cda
commit
5888c4d292
21 changed files with 933 additions and 129 deletions
318
crates/gba-core/tests/direct_sound.rs
Normal file
318
crates/gba-core/tests/direct_sound.rs
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
//! Original Direct Sound tests. No game data or upstream test programs.
|
||||
use gba_core::{
|
||||
audio::StereoLevel,
|
||||
cpu::Cpu,
|
||||
dma::{DMA_BASE, DMA_STRIDE},
|
||||
io::{FIFO_A, FIFO_B, HALTCNT, IF, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, TIMER_BASE, WAVE_RAM},
|
||||
machine::{Machine, StepKind},
|
||||
memory::{Memory, ROM_START},
|
||||
};
|
||||
|
||||
fn bus() -> Memory {
|
||||
let mut bus = Memory::new(0xeaff_fffeu32.to_le_bytes().to_vec()).unwrap();
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
bus.write16(SOUNDBIAS, 0x200).unwrap();
|
||||
bus.write16(SOUNDCNT_H, 0x0304).unwrap(); // A, both sides, 100%, timer 0.
|
||||
bus
|
||||
}
|
||||
|
||||
fn timer(bus: &mut Memory, index: u32, period: u16, control: u16) {
|
||||
bus.write32(
|
||||
TIMER_BASE + index * 4,
|
||||
u32::from(0u16.wrapping_sub(period)) | u32::from(control) << 16,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn stereo(value: i16) -> StereoLevel {
|
||||
StereoLevel {
|
||||
left: value,
|
||||
right: value,
|
||||
}
|
||||
}
|
||||
|
||||
fn sound_dma(bus: &mut Memory, channel: u32, control: u16) {
|
||||
let base = DMA_BASE + channel * DMA_STRIDE;
|
||||
bus.write32(base, 0x0200_0000).unwrap();
|
||||
bus.write32(base + 4, FIFO_A + (channel - 1) * 4).unwrap();
|
||||
bus.write32(base + 8, u32::from(control | 0xb000) << 16 | 1)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_little_endian_samples_hold_between_edges_then_underflow_to_zero() {
|
||||
let mut bus = bus();
|
||||
bus.write32(FIFO_A, 0x7f80ff01).unwrap();
|
||||
timer(&mut bus, 0, 10, 0x80);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
for sample in [4, -4, -512, 508, 0, 0] {
|
||||
bus.advance_cycles(9);
|
||||
let previous = bus.audio_level();
|
||||
bus.advance_cycles(0);
|
||||
assert_eq!(bus.audio_level(), previous);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(sample));
|
||||
}
|
||||
assert!(bus.read32(FIFO_A).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_timer_selection_cascade_routing_volume_and_clipping() {
|
||||
let mut bus = bus();
|
||||
bus.write16(SOUNDCNT_H, 0x610c).unwrap(); // A right/timer0, B left/timer1, both 100%.
|
||||
bus.write32(FIFO_A, 0x7f7f7f7f).unwrap();
|
||||
bus.write32(FIFO_B, 0x80808080).unwrap();
|
||||
timer(&mut bus, 0, 2, 0x80);
|
||||
timer(&mut bus, 1, 2, 0x84); // Cascade: one B sample per two A samples.
|
||||
bus.advance_cycles(2);
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: 0,
|
||||
right: 508
|
||||
}
|
||||
);
|
||||
bus.advance_cycles(2);
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: -512,
|
||||
right: 508
|
||||
}
|
||||
);
|
||||
bus.write16(SOUNDCNT_H, 0x3300).unwrap(); // Both sides, both 50%, retain held samples.
|
||||
assert_eq!(bus.audio_level(), stereo(-2));
|
||||
bus.write16(SOUNDBIAS, 0).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(-512));
|
||||
bus.write16(SOUNDBIAS, 0x3fe).unwrap();
|
||||
bus.write16(SOUNDCNT_H, 0x0304).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(511));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn muted_routes_still_consume_but_master_disable_freezes_playback() {
|
||||
let mut bus = bus();
|
||||
bus.write16(SOUNDCNT_H, 4).unwrap();
|
||||
bus.write32(FIFO_A, 0x04030201).unwrap();
|
||||
timer(&mut bus, 0, 1, 0x80);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
bus.write16(SOUNDCNT_H, 0x0304).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(4));
|
||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||
bus.advance_cycles(100);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(8)); // In-flight word is separate from the reset queue.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_strobes_clear_queues_not_in_flight_word_or_held_level() {
|
||||
let mut bus = bus();
|
||||
bus.write32(FIFO_A, 0x04030201).unwrap();
|
||||
bus.write32(FIFO_A, 0x7f7f7f7f).unwrap();
|
||||
timer(&mut bus, 0, 1, 0x80);
|
||||
bus.advance_cycles(1);
|
||||
bus.write16(SOUNDCNT_H, 0x8b04).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_H).unwrap(), 0x0304);
|
||||
assert_eq!(bus.audio_level(), stereo(4));
|
||||
for sample in [8, 12, 16, 0] {
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(sample));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_writes_enqueue_words_and_preserve_other_lanes_on_reused_slots() {
|
||||
let mut bus = bus();
|
||||
timer(&mut bus, 0, 1, 0x80);
|
||||
// Visit each of the seven queue slots. Reuse the first with a byte write.
|
||||
for _ in 0..7 {
|
||||
bus.write32(FIFO_A, 0x04030201).unwrap();
|
||||
bus.advance_cycles(4);
|
||||
}
|
||||
bus.write8(FIFO_A + 1, 9).unwrap();
|
||||
for sample in [4, 36, 12, 16] {
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(sample));
|
||||
}
|
||||
bus.write16(FIFO_A + 2, 0x0807).unwrap();
|
||||
for sample in [4, 8, 28, 32] {
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(sample));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_queue_overflow_clears_queued_words() {
|
||||
let mut bus = bus();
|
||||
for _ in 0..8 {
|
||||
bus.write32(FIFO_A, 0x7f7f7f7f).unwrap();
|
||||
}
|
||||
timer(&mut bus, 0, 1, 0x80);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
bus.write32(FIFO_A, 1).unwrap();
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wave_banks_and_idle_psg_masks_work_without_claiming_psg_playback() {
|
||||
let mut bus = bus();
|
||||
bus.write16(0x04000062, 0xffff).unwrap();
|
||||
assert_eq!(bus.read16(0x04000062).unwrap(), 0xffc0);
|
||||
bus.write32(WAVE_RAM, 0x11223344).unwrap(); // CPU bank 1.
|
||||
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] {
|
||||
let previous = bus.read16(address).unwrap();
|
||||
assert!(bus
|
||||
.write16(address, 0xffff)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("PSG channel trigger"));
|
||||
assert_eq!(bus.read16(address).unwrap(), previous);
|
||||
}
|
||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0x11223344);
|
||||
assert_eq!(bus.read16(0x04000062).unwrap(), 0);
|
||||
bus.write16(0x04000064, 0xffff).unwrap(); // Disabled PSG writes are ignored.
|
||||
bus.write16(SOUNDCNT_X, 0xff).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
bus.write16(0x04000070, 0x40).unwrap();
|
||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0xaabbccdd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sound_dma_forces_four_words_and_fixed_destination_then_repeats_and_interrupts() {
|
||||
for channel in [1, 2] {
|
||||
let mut bus = bus();
|
||||
let fifo = FIFO_A + (channel - 1) * 4;
|
||||
let route = if channel == 1 { 0x0304 } else { 0x3008 };
|
||||
bus.write16(SOUNDCNT_H, route).unwrap();
|
||||
for index in 0..8 {
|
||||
bus.write32(0x02000000 + index * 4, 0x01010101 * (index + 1))
|
||||
.unwrap();
|
||||
}
|
||||
sound_dma(&mut bus, channel, 0x4200); // IRQ/repeat, programmed count1, halfword/increment destination.
|
||||
timer(&mut bus, 0, 100, 0x80);
|
||||
bus.advance_cycles(100); // Empty playback, request.
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
for _ in 0..4 {
|
||||
assert_eq!(
|
||||
machine.step().unwrap(),
|
||||
StepKind::Dma {
|
||||
channel: channel as usize
|
||||
}
|
||||
);
|
||||
}
|
||||
assert_ne!(
|
||||
machine.memory().read16(IF).unwrap() & (1 << (8 + channel)),
|
||||
0
|
||||
);
|
||||
assert_ne!(
|
||||
machine
|
||||
.memory()
|
||||
.read16(DMA_BASE + channel * DMA_STRIDE + 10)
|
||||
.unwrap()
|
||||
& 0x8000,
|
||||
0
|
||||
);
|
||||
// No fifth unit. Four queued words suppress the next request BEFORE the playback word loads.
|
||||
assert!(matches!(machine.step().unwrap(), StepKind::Instruction));
|
||||
let remaining = 100 - (machine.cycles() as u32 % 100);
|
||||
machine.memory_mut().advance_cycles(remaining);
|
||||
assert_eq!(machine.memory().audio_level(), stereo(4));
|
||||
assert!(matches!(machine.step().unwrap(), StepKind::Instruction));
|
||||
let remaining = 100 - (machine.cycles() as u32 % 100);
|
||||
machine.memory_mut().advance_cycles(remaining);
|
||||
for _ in 0..4 {
|
||||
assert_eq!(
|
||||
machine.step().unwrap(),
|
||||
StepKind::Dma {
|
||||
channel: channel as usize
|
||||
}
|
||||
);
|
||||
}
|
||||
// Drain without servicing further requests. The repeated block must continue the source,
|
||||
// use four-byte strides, and never advance the FIFO destination into the other channel.
|
||||
for offset in 0..30 {
|
||||
let remaining = 100 - (machine.cycles() as u32 % 100);
|
||||
machine.memory_mut().advance_cycles(remaining);
|
||||
assert_eq!(
|
||||
machine.memory().audio_level(),
|
||||
stereo(((offset + 2) / 4 + 1) * 4)
|
||||
);
|
||||
}
|
||||
assert!(machine.memory().read32(fifo).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonrepeat_sound_dma_disables_after_four_words() {
|
||||
let mut bus = bus();
|
||||
sound_dma(&mut bus, 1, 0);
|
||||
timer(&mut bus, 0, 1000, 0x80);
|
||||
bus.advance_cycles(1000);
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
for _ in 0..4 {
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 1 });
|
||||
}
|
||||
assert_eq!(
|
||||
machine.memory().read16(DMA_BASE + DMA_STRIDE + 10).unwrap() & 0x8000,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halt_clocks_samples_and_stop_freezes_them() {
|
||||
for stop in [false, true] {
|
||||
let mut bus = bus();
|
||||
bus.write32(FIFO_A, 0x04030201).unwrap();
|
||||
timer(&mut bus, 0, 10, 0x80);
|
||||
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 { 0 } else { 4 })
|
||||
);
|
||||
if stop {
|
||||
machine.memory_mut().advance_cycles(u32::MAX);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_device_batches_match_single_clocks_with_and_without_row_capture() {
|
||||
for capture in [false, true] {
|
||||
let mut bulk = bus();
|
||||
let mut small = bus();
|
||||
for bus in [&mut bulk, &mut small] {
|
||||
if capture {
|
||||
bus.set_scanline_rendering(true);
|
||||
}
|
||||
bus.write16(SOUNDCNT_H, 0x730c).unwrap();
|
||||
for index in 1..=7 {
|
||||
bus.write32(FIFO_A, index * 0x01010101).unwrap();
|
||||
bus.write32(FIFO_B, (index + 8) * 0x01010101).unwrap();
|
||||
}
|
||||
timer(bus, 0, 2, 0x80);
|
||||
timer(bus, 1, 3, 0x80);
|
||||
}
|
||||
for cycles in [1, 2, 3, 17, 39, 1000] {
|
||||
bulk.advance_cycles(cycles);
|
||||
for _ in 0..cycles {
|
||||
small.advance_cycles(1);
|
||||
}
|
||||
assert_eq!(bulk.audio_level(), small.audio_level());
|
||||
}
|
||||
bulk.set_scanline_rendering(false);
|
||||
bulk.advance_cycles(u32::MAX);
|
||||
assert_eq!(bulk.audio_level(), stereo(0));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue