feat(audio): capture transactional fixed-rate stereo samples

This commit is contained in:
0xrsydn 2026-10-07 07:16:42 +07:00
commit 902c892d3f
11 changed files with 382 additions and 15 deletions

View file

@ -0,0 +1,113 @@
//! Original fixed-rate sample probes; no host device or external recordings.
use gba_core::{
audio::{StereoLevel, AUDIO_QUEUE_CAPACITY, AUDIO_SAMPLE_RATE},
io::{FIFO_A, HALTCNT, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, TIMER_BASE},
memory::Memory,
};
fn bus() -> Memory {
let mut bus = Memory::new(vec![]).unwrap();
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write16(SOUNDBIAS, 0x200).unwrap();
bus.write16(SOUNDCNT_H, 0x0304).unwrap();
bus
}
fn drain(bus: &mut Memory) -> Vec<StereoLevel> {
let mut output = vec![StereoLevel::default(); AUDIO_QUEUE_CAPACITY];
let count = bus.drain_audio_samples(&mut output);
output.truncate(count);
output
}
#[test]
fn exact_rate_phase_toggle_and_drain_order() {
assert_eq!(AUDIO_SAMPLE_RATE * 512, 16777216);
let mut bus = bus();
bus.advance_cycles(100);
assert!(drain(&mut bus).is_empty());
bus.set_audio_capture(true);
bus.advance_cycles(511);
bus.set_audio_capture(true); // Idempotent; no phase reset.
assert!(drain(&mut bus).is_empty());
bus.advance_cycles(1);
bus.write16(SOUNDBIAS, 0x202).unwrap();
bus.advance_cycles(512);
let mut one = [StereoLevel::default(); 1];
assert_eq!(bus.drain_audio_samples(&mut one), 1);
assert_eq!(one[0].left, 0);
assert_eq!(bus.drain_audio_samples(&mut one), 1);
assert_eq!(one[0].left, 2);
assert_eq!(bus.drain_audio_samples(&mut one), 0);
bus.advance_cycles(511);
bus.set_audio_capture(false);
bus.set_audio_capture(true);
bus.advance_cycles(1);
assert!(drain(&mut bus).is_empty());
}
#[test]
fn timer_overflow_at_sample_boundary_precedes_sample_and_later_stores() {
let mut bus = bus();
bus.write32(FIFO_A, 0x04030201).unwrap();
bus.write32(TIMER_BASE, 0x0080fe00).unwrap(); // 512-clock timer.
bus.set_audio_capture(true);
bus.advance_cycles(4 * 512);
assert_eq!(
drain(&mut bus).iter().map(|s| s.left).collect::<Vec<_>>(),
[4, 8, 12, 16]
);
bus.advance_cycles(512);
assert_eq!(drain(&mut bus)[0].left, 0);
}
#[test]
fn psg_sample_clock_crosses_many_oscillator_edges_and_respects_halt_stop() {
let mut bus = bus();
bus.write16(SOUNDCNT_H, 2).unwrap();
bus.write16(0x04000080, 0x1177).unwrap();
bus.write16(0x04000062, 0xf080).unwrap();
bus.write16(0x04000064, 0x87e0).unwrap(); // One duty step every 512 cycles.
bus.set_audio_capture(true);
bus.write8(HALTCNT, 0).unwrap();
bus.advance_cycles(8 * 512);
assert_eq!(
drain(&mut bus).iter().map(|s| s.left).collect::<Vec<_>>(),
[-120, -120, -120, -120, 120, 120, 120, 120]
);
bus.write8(HALTCNT, 0x80).unwrap();
bus.advance_cycles(8192);
assert!(drain(&mut bus).is_empty());
}
#[test]
fn bulk_split_capture_and_no_capture_keep_hardware_state_and_samples_equivalent() {
let mut a = bus();
let mut b = bus();
let mut c = bus();
for bus in [&mut a, &mut b, &mut c] {
bus.write32(FIFO_A, 0x807f01ff).unwrap();
bus.write32(TIMER_BASE, 0x0080ff9c).unwrap();
}
a.set_audio_capture(true);
b.set_audio_capture(true);
b.set_scanline_rendering(true);
a.advance_cycles(100003);
c.advance_cycles(100003);
for _ in 0..100003 {
b.advance_cycles(1);
}
assert_eq!(drain(&mut a), drain(&mut b));
assert_eq!(a.audio_level(), c.audio_level());
assert_eq!(a.read32(TIMER_BASE), c.read32(TIMER_BASE));
assert_eq!(a.audio_dropped_samples(), 0);
}
#[test]
fn bounded_overflow_counts_drops_and_keeps_earliest_frames_without_clock_stall() {
let mut bus = bus();
bus.set_audio_capture(true);
bus.advance_cycles(u32::MAX);
assert_eq!(
bus.audio_dropped_samples(),
u64::from(u32::MAX) / 512 - AUDIO_QUEUE_CAPACITY as u64
);
assert_eq!(drain(&mut bus).len(), AUDIO_QUEUE_CAPACITY);
bus.advance_cycles(1);
assert_eq!(drain(&mut bus).len(), 1); // Overflow did not lose the phase.
bus.set_audio_capture(false);
assert_eq!(bus.audio_dropped_samples(), 0);
}