feat(audio): implement PSG noise channel 4
This commit is contained in:
parent
ca2134f23c
commit
8ae7b3cf72
14 changed files with 774 additions and 75 deletions
|
|
@ -1,7 +1,9 @@
|
|||
//! Nominal Direct Sound and PSG pulse channels 1–2. No host audio output.
|
||||
//! Nominal Direct Sound, PSG pulse channels 1–2, and noise channel 4. No host audio output.
|
||||
|
||||
mod modulation;
|
||||
mod noise;
|
||||
mod pulse;
|
||||
use noise::Noise;
|
||||
use pulse::Pulse;
|
||||
|
||||
const SEQUENCER_PERIOD: u32 = 32768; // 16,777,216 Hz / 512 Hz.
|
||||
|
|
@ -80,6 +82,7 @@ pub(crate) struct Audio {
|
|||
enabled: bool,
|
||||
psg: [u16; 17],
|
||||
pulses: [Pulse; 2],
|
||||
noise: Noise,
|
||||
sequencer_phase: u16,
|
||||
next_step: u8,
|
||||
control: u16,
|
||||
|
|
@ -100,7 +103,7 @@ impl Audio {
|
|||
{
|
||||
return Some("reserved PSG volume selection");
|
||||
}
|
||||
if self.enabled && matches!(address, 0x0400_0075 | 0x0400_007d) && value & 0x80 != 0 {
|
||||
if self.enabled && address == 0x0400_0075 && value & 0x80 != 0 {
|
||||
Some("PSG channel trigger (synthesis not implemented)")
|
||||
} else {
|
||||
None
|
||||
|
|
@ -123,6 +126,9 @@ impl Audio {
|
|||
}
|
||||
|
||||
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
||||
if (0x0400_0078..=0x0400_007d).contains(&address) {
|
||||
return Some(self.noise.read(address - 0x0400_0078));
|
||||
}
|
||||
if let Some((index, offset)) = Self::pulse_register(address) {
|
||||
return Some(self.pulses[index].read(offset));
|
||||
}
|
||||
|
|
@ -135,8 +141,6 @@ impl Audio {
|
|||
0x0400_0074 => 0x4000,
|
||||
0x0400_0070 => 0x00e0,
|
||||
0x0400_0072 => 0xe000,
|
||||
0x0400_0078 => 0xff00,
|
||||
0x0400_007c => 0x40ff,
|
||||
0x0400_0080 => 0xff77,
|
||||
_ => 0,
|
||||
};
|
||||
|
|
@ -147,6 +151,7 @@ impl Audio {
|
|||
(u16::from(self.enabled) << 7)
|
||||
| u16::from(self.pulses[0].modulation.active)
|
||||
| (u16::from(self.pulses[1].modulation.active) << 1)
|
||||
| (u16::from(self.noise.modulation.active) << 3)
|
||||
}
|
||||
SOUNDBIAS => self.bias,
|
||||
0x0400_0086 | 0x0400_008a => 0,
|
||||
|
|
@ -173,6 +178,13 @@ impl Audio {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (0x0400_0078..=0x0400_007d).contains(&address) {
|
||||
if self.enabled {
|
||||
self.noise
|
||||
.write(address - 0x0400_0078, value, self.next_step);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let shift = (address & 1) * 8;
|
||||
let merge = |old: u16| (old & !(255 << shift)) | (u16::from(value) << shift);
|
||||
match address & !1 {
|
||||
|
|
@ -194,6 +206,7 @@ impl Audio {
|
|||
if self.enabled && value & 0x80 == 0 {
|
||||
self.psg = [0; 17];
|
||||
self.pulses = [Pulse::default(); 2];
|
||||
self.noise = Noise::default();
|
||||
for fifo in &mut self.fifo {
|
||||
fifo.reset_queue();
|
||||
}
|
||||
|
|
@ -222,12 +235,14 @@ impl Audio {
|
|||
for pulse in &mut self.pulses {
|
||||
pulse.advance(count);
|
||||
}
|
||||
self.noise.advance(count);
|
||||
let phase = u32::from(self.sequencer_phase) + count;
|
||||
if phase == SEQUENCER_PERIOD {
|
||||
self.sequencer_phase = 0;
|
||||
for pulse in &mut self.pulses {
|
||||
pulse.clock(self.next_step);
|
||||
}
|
||||
self.noise.clock(self.next_step);
|
||||
self.next_step = (self.next_step + 1) & 7;
|
||||
} else {
|
||||
self.sequencer_phase = phase as u16;
|
||||
|
|
@ -278,6 +293,9 @@ impl Audio {
|
|||
psg_sample += pulse.sample();
|
||||
}
|
||||
}
|
||||
if psg_control & (1 << (11 + side * 4)) != 0 {
|
||||
psg_sample += self.noise.sample();
|
||||
}
|
||||
let volume = ((psg_control >> (side * 4)) & 7) as i16 + 1;
|
||||
// Sum PSG channels before scaling, so fractional bits are discarded once.
|
||||
*level += (psg_sample * volume * ratio) >> 2;
|
||||
|
|
|
|||
144
crates/gba-core/src/audio/noise.rs
Normal file
144
crates/gba-core/src/audio/noise.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
//! Nominal GBA channel 4 polynomial counter. No random numbers or host clocks.
|
||||
use super::modulation::Modulation;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Noise {
|
||||
pub(super) modulation: Modulation,
|
||||
polynomial: u8,
|
||||
lfsr: u16,
|
||||
remaining: u32,
|
||||
high: bool,
|
||||
}
|
||||
|
||||
impl Noise {
|
||||
pub(super) fn read(&self, offset: u32) -> u8 {
|
||||
match offset {
|
||||
1 => self.modulation.envelope,
|
||||
4 => self.polynomial,
|
||||
5 => u8::from(self.modulation.length_enabled) << 6,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn short(&self) -> bool {
|
||||
self.polynomial & 8 != 0
|
||||
}
|
||||
|
||||
fn period(&self) -> u32 {
|
||||
let ratio = u32::from(self.polynomial & 7);
|
||||
let base = if ratio == 0 { 32 } else { 64 * ratio };
|
||||
base << (self.polynomial >> 4)
|
||||
}
|
||||
|
||||
pub(super) fn write(&mut self, offset: u32, value: u8, next_step: u8) {
|
||||
match offset {
|
||||
0 => self.modulation.write_length(value),
|
||||
1 => self.modulation.write_envelope(value),
|
||||
4 => self.polynomial = value, // Preserve counter and remaining interval on live writes.
|
||||
5 => {
|
||||
if self.modulation.write_control(value, next_step) {
|
||||
self.lfsr = if self.short() { 0x40 } else { 0x4000 };
|
||||
self.remaining = self.period();
|
||||
self.high = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn advance(&mut self, cycles: u32) {
|
||||
if !self.modulation.active || cycles == 0 {
|
||||
return;
|
||||
}
|
||||
if cycles < self.remaining {
|
||||
self.remaining -= cycles;
|
||||
return;
|
||||
}
|
||||
let rest = cycles - self.remaining;
|
||||
let period = self.period();
|
||||
let edges = 1 + rest / period;
|
||||
// The output is the carry of the final shift, not the final register's low bit.
|
||||
let before_last = jump(self.lfsr, edges - 1, self.short());
|
||||
self.high = before_last & 1 != 0;
|
||||
self.lfsr = shift(before_last, self.short());
|
||||
self.remaining = period - rest % period;
|
||||
}
|
||||
|
||||
pub(super) fn clock(&mut self, step: u8) {
|
||||
self.modulation.clock(step);
|
||||
}
|
||||
|
||||
pub(super) fn sample(&self) -> i16 {
|
||||
if !self.modulation.active {
|
||||
return 0;
|
||||
}
|
||||
if self.high {
|
||||
i16::from(self.modulation.volume)
|
||||
} else {
|
||||
-i16::from(self.modulation.volume)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn shift(state: u16, short: bool) -> u16 {
|
||||
(state >> 1)
|
||||
^ if state & 1 != 0 {
|
||||
if short {
|
||||
0x60
|
||||
} else {
|
||||
0x6000
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// Each column maps one input bit. Squaring the linear map doubles its step count.
|
||||
// This also handles transient upper bits after a live width change; reducing by
|
||||
// the 127-step short-mode period would incorrectly discard those transient bits.
|
||||
type Transform = [u16; 15];
|
||||
const JUMPS: [[Transform; 32]; 2] = [powers(false), powers(true)];
|
||||
|
||||
const fn apply(map: &Transform, mut state: u16) -> u16 {
|
||||
let mut result = 0;
|
||||
while state != 0 {
|
||||
result ^= map[state.trailing_zeros() as usize];
|
||||
state &= state - 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
const fn powers(short: bool) -> [Transform; 32] {
|
||||
let mut maps = [[0; 15]; 32];
|
||||
let mut bit = 0;
|
||||
while bit < 15 {
|
||||
maps[0][bit] = shift(1 << bit, short);
|
||||
bit += 1;
|
||||
}
|
||||
let mut power = 1;
|
||||
while power < 32 {
|
||||
bit = 0;
|
||||
while bit < 15 {
|
||||
maps[power][bit] = apply(&maps[power - 1], maps[power - 1][bit]);
|
||||
bit += 1;
|
||||
}
|
||||
power += 1;
|
||||
}
|
||||
maps
|
||||
}
|
||||
|
||||
fn jump(mut state: u16, mut count: u32, short: bool) -> u16 {
|
||||
let maps = &JUMPS[usize::from(short)];
|
||||
let mut power = 0;
|
||||
while count != 0 {
|
||||
if count & 1 != 0 {
|
||||
state = apply(&maps[power], state);
|
||||
}
|
||||
count >>= 1;
|
||||
power += 1;
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
172
crates/gba-core/src/audio/noise/tests.rs
Normal file
172
crates/gba-core/src/audio/noise/tests.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
//! Original counter references and deterministic clock-batching checks.
|
||||
use super::*;
|
||||
|
||||
fn start(polynomial: u8) -> Noise {
|
||||
let mut noise = Noise::default();
|
||||
noise.write(1, 0xf0, 0);
|
||||
noise.write(4, polynomial, 0);
|
||||
noise.write(5, 0x80, 0);
|
||||
noise
|
||||
}
|
||||
|
||||
// Independent bit-array form of the documented Galois counter.
|
||||
fn reference(state: u16, short: bool) -> (u16, bool) {
|
||||
let mut bits = [false; 15];
|
||||
for (index, bit) in bits.iter_mut().enumerate() {
|
||||
*bit = state & (1 << index) != 0;
|
||||
}
|
||||
let carry = bits[0];
|
||||
bits.copy_within(1..15, 0);
|
||||
bits[14] = false;
|
||||
if carry {
|
||||
let top = if short { 6 } else { 14 };
|
||||
bits[top] = !bits[top];
|
||||
bits[top - 1] = !bits[top - 1];
|
||||
}
|
||||
let value = bits
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(0, |value, (index, &bit)| value | (u16::from(bit) << index));
|
||||
(value, carry)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_counters_visit_every_nonzero_state_before_repeating() {
|
||||
for (short, width) in [(false, 15), (true, 7)] {
|
||||
let initial = 1 << (width - 1);
|
||||
let mut state = initial;
|
||||
let mut seen = vec![false; 1 << width];
|
||||
let mut positive = 0;
|
||||
for index in 0..(1 << width) - 1 {
|
||||
assert_ne!(state, 0);
|
||||
assert!(!seen[usize::from(state)]);
|
||||
seen[usize::from(state)] = true;
|
||||
let (expected, high) = reference(state, short);
|
||||
assert_eq!(shift(state, short), expected);
|
||||
if index < width {
|
||||
assert_eq!(high, index == width - 1);
|
||||
}
|
||||
positive += usize::from(high);
|
||||
state = expected;
|
||||
}
|
||||
assert_eq!(state, initial);
|
||||
assert_eq!(positive, 1 << (width - 1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_maps_match_each_state_and_iterated_random_batches_including_transient_upper_bits() {
|
||||
for short in [false, true] {
|
||||
for state in 0..=0x7fff {
|
||||
assert_eq!(jump(state, 1, short), reference(state, short).0);
|
||||
}
|
||||
let mut random = 0x4183u32;
|
||||
for _ in 0..200 {
|
||||
random = random.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
let state = (random & 0x7fff) as u16;
|
||||
let count = (random >> 16) & 4095;
|
||||
let mut expected = state;
|
||||
for _ in 0..count {
|
||||
expected = reference(expected, short).0;
|
||||
}
|
||||
assert_eq!(jump(state, count, short), expected);
|
||||
assert_eq!(
|
||||
jump(jump(state, count / 2, short), count - count / 2, short),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_divider_shift_width_fields_observe_exact_nominal_edges() {
|
||||
for polynomial in 0..=255u8 {
|
||||
let divisor = [32, 64, 128, 192, 256, 320, 384, 448][usize::from(polynomial & 7)];
|
||||
let period = divisor * (1u32 << (polynomial >> 4));
|
||||
for cycles in [0, period - 1, period, period * 19 + 7] {
|
||||
let mut noise = start(polynomial);
|
||||
noise.advance(cycles);
|
||||
let mut expected = if polynomial & 8 != 0 { 0x40 } else { 0x4000 };
|
||||
let mut high = false;
|
||||
for _ in 0..cycles / period {
|
||||
(expected, high) = reference(expected, polynomial & 8 != 0);
|
||||
}
|
||||
assert_eq!(noise.lfsr, expected);
|
||||
assert_eq!(noise.high, high);
|
||||
assert_eq!(noise.remaining, period - cycles % period);
|
||||
assert_eq!(noise.sample(), if high { 15 } else { -15 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maximum_clock_batch_matches_independent_cycle_reduction_and_last_carry() {
|
||||
for (polynomial, width) in [(0, 15), (8, 7)] {
|
||||
let mut noise = start(polynomial);
|
||||
noise.advance(u32::MAX);
|
||||
let edges = u32::MAX / 32;
|
||||
let mut expected = 1 << (width - 1);
|
||||
for _ in 0..(edges - 1) % ((1 << width) - 1) {
|
||||
expected = reference(expected, width == 7).0;
|
||||
}
|
||||
let (state, high) = reference(expected, width == 7);
|
||||
assert_eq!((noise.lfsr, noise.high), (state, high));
|
||||
assert_eq!(noise.remaining, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_width_and_rate_changes_preserve_counter_and_remaining_interval() {
|
||||
let mut noise = start(0);
|
||||
noise.advance(3 * 32 + 15);
|
||||
assert_eq!(noise.lfsr, 0x800);
|
||||
noise.write(4, 0x19, 0); // Short mode, new period128. Old interval has17 clocks left.
|
||||
let mut expected = noise.lfsr;
|
||||
noise.advance(16);
|
||||
assert_eq!(noise.lfsr, expected);
|
||||
noise.advance(1);
|
||||
expected = reference(expected, true).0;
|
||||
assert_eq!(noise.lfsr, expected); // Upper bits are not truncated or reseeded.
|
||||
assert_eq!(noise.remaining, 128);
|
||||
noise.advance(128 * 1000);
|
||||
for _ in 0..1000 {
|
||||
expected = reference(expected, true).0;
|
||||
}
|
||||
assert_eq!(noise.lfsr, expected);
|
||||
noise.write(4, 0, 0);
|
||||
noise.advance(128 + 32 * 25);
|
||||
for _ in 0..26 {
|
||||
expected = reference(expected, false).0;
|
||||
}
|
||||
assert_eq!(noise.lfsr, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_reseeds_counter_reloads_timer_and_resets_held_output() {
|
||||
for (polynomial, width) in [(0, 15), (8, 7)] {
|
||||
let mut noise = start(polynomial);
|
||||
noise.advance(width * 32);
|
||||
assert_eq!(noise.sample(), 15);
|
||||
noise.write(5, 0x80, 0);
|
||||
assert_eq!(noise.lfsr, 1 << (width - 1));
|
||||
assert_eq!(noise.remaining, 32);
|
||||
assert_eq!(noise.sample(), -15);
|
||||
noise.write(1, 0, 0);
|
||||
let before = noise;
|
||||
noise.advance(u32::MAX);
|
||||
assert_eq!(noise, before);
|
||||
assert_eq!(noise.sample(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_cycles_and_arbitrary_splits_preserve_full_noise_state() {
|
||||
let mut bulk = start(8);
|
||||
let mut split = bulk;
|
||||
for cycles in [0, 1, 31, 33, 4096, 1000000, u32::MAX] {
|
||||
bulk.advance(cycles);
|
||||
split.advance(cycles / 2);
|
||||
split.advance(cycles - cycles / 2);
|
||||
assert_eq!(bulk, split);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ fn sound(memory: &mut Memory, period: u16) {
|
|||
memory.write16(0x04000062, 0xf080).unwrap();
|
||||
memory.write16(0x04000064, 0x87ff).unwrap();
|
||||
memory.write16(0x04000068, 0xf080).unwrap();
|
||||
memory.write16(0x0400006c, 0x87ff).unwrap(); // Both pulses run unrouted; rollback must include them.
|
||||
memory.write16(0x0400006c, 0x87ff).unwrap();
|
||||
memory.write16(0x04000078, 0xf000).unwrap();
|
||||
memory.write16(0x0400007c, 0x8008).unwrap(); // All supported PSG channels run unrouted.
|
||||
memory
|
||||
.write32(
|
||||
TIMER_BASE,
|
||||
|
|
@ -22,16 +24,22 @@ fn sound(memory: &mut Memory, period: u16) {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn pulse_status_load_observes_length_expiry_at_bus_completion() {
|
||||
for (index, duty, frequency) in [(0, 0x04000062, 0x04000064), (1, 0x04000068, 0x0400006c)] {
|
||||
fn psg_status_load_observes_length_expiry_at_bus_completion() {
|
||||
for (index, duty, frequency) in [
|
||||
(0, 0x04000062, 0x04000064),
|
||||
(1, 0x04000068, 0x0400006c),
|
||||
(3, 0x04000078, 0x0400007c),
|
||||
] {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe5910000], &[(1, SOUNDCNT_X)]);
|
||||
sound(&mut memory, 1000);
|
||||
memory.write16(duty, 0xf03f).unwrap();
|
||||
memory.write16(frequency, 0xc7ff).unwrap();
|
||||
memory
|
||||
.write16(frequency, if index == 3 { 0xc000 } else { 0xc7ff })
|
||||
.unwrap();
|
||||
memory.advance_cycles(32768 - 7);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
let status = 0x83 ^ (1 << index); // The other channel remains active.
|
||||
let status = 0x8b ^ (1 << index); // The other channels remain active.
|
||||
assert_eq!(machine.cpu().registers()[0], status);
|
||||
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), status as u16);
|
||||
}
|
||||
|
|
@ -57,27 +65,47 @@ fn pulse_trigger_reloads_timer_at_store_completion() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn successful_dma_trigger_starts_channel2_after_destination_bus_cycles() {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
fn noise_trigger_reloads_counter_at_store_completion() {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe5810000], &[(0, 0x8008), (1, 0x0400007c)]);
|
||||
sound(&mut memory, 1000);
|
||||
memory.write16(0x04000080, 0x2277).unwrap();
|
||||
memory.write16(0x04000080, 0x8877).unwrap();
|
||||
memory.write16(SOUNDCNT_H, 2).unwrap();
|
||||
memory.write8(0x04000069, 0).unwrap();
|
||||
memory.write8(0x04000069, 0xf0).unwrap(); // DAC gate restored, still needs a trigger.
|
||||
memory.write32(0x02000000, 0x87ff).unwrap();
|
||||
memory.write32(DMA_BASE, 0x02000000).unwrap();
|
||||
memory.write32(DMA_BASE + 4, 0x0400006c).unwrap();
|
||||
memory.write32(DMA_BASE + 8, 0x84000001).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
assert_eq!(
|
||||
machine.step().unwrap(),
|
||||
crate::machine::StepKind::Dma { channel: 0 }
|
||||
);
|
||||
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), 0x83);
|
||||
machine.memory_mut().advance_cycles(15);
|
||||
assert_eq!(machine.memory().audio_level().left, 120);
|
||||
machine.memory_mut().advance_cycles(1);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cycles(), 7);
|
||||
machine.memory_mut().advance_cycles(7 * 32 - 1);
|
||||
assert_eq!(machine.memory().audio_level().left, -120);
|
||||
machine.memory_mut().advance_cycles(1);
|
||||
assert_eq!(machine.memory().audio_level().left, 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_dma_triggers_start_pulse_and_noise_after_destination_bus_cycles() {
|
||||
for (envelope, destination, routing, control, delay, before, after) in [
|
||||
(0x04000069, 0x0400006c, 0x2277, 0x87ff, 15, 120, -120),
|
||||
(0x04000079, 0x0400007c, 0x8877, 0x8008, 223, -120, 120),
|
||||
] {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
sound(&mut memory, 1000);
|
||||
memory.write16(0x04000080, routing).unwrap();
|
||||
memory.write16(SOUNDCNT_H, 2).unwrap();
|
||||
memory.write8(envelope, 0).unwrap();
|
||||
memory.write8(envelope, 0xf0).unwrap(); // DAC gate restored, still needs a trigger.
|
||||
memory.write32(0x02000000, control).unwrap();
|
||||
memory.write32(DMA_BASE, 0x02000000).unwrap();
|
||||
memory.write32(DMA_BASE + 4, destination).unwrap();
|
||||
memory.write32(DMA_BASE + 8, 0x84000001).unwrap();
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
assert_eq!(
|
||||
machine.step().unwrap(),
|
||||
crate::machine::StepKind::Dma { channel: 0 }
|
||||
);
|
||||
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), 0x8b);
|
||||
machine.memory_mut().advance_cycles(delay);
|
||||
assert_eq!(machine.memory().audio_level().left, before);
|
||||
machine.memory_mut().advance_cycles(1);
|
||||
assert_eq!(machine.memory().audio_level().left, after);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -146,6 +174,29 @@ fn failed_dma_source_and_destination_restore_audio_after_speculative_timer_edges
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_block_store_rejects_later_reserved_ratio_before_committing_noise_trigger() {
|
||||
let (cpu, mut memory) = prepared(
|
||||
false,
|
||||
&[0xe8a0000e],
|
||||
&[(0, 0x04000078), (1, 0xf000), (2, 0x8008), (3, 0x00030000)],
|
||||
); // STMIA r0!,{r1-r3}: noise configuration/trigger, then reserved PSG ratio.
|
||||
memory.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
let audio = memory.io.audio;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
for _ in 0..2 {
|
||||
assert!(machine
|
||||
.step()
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("reserved PSG volume selection"));
|
||||
assert_eq!(machine.memory().io.audio, audio);
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_block_store_rejects_later_wave_trigger_before_committing_channel2_trigger() {
|
||||
let (cpu, mut memory) = prepared(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const REGIONS: [(u32, u32, u32); 5] = [
|
|||
const MARKER: u32 = 0xa55a_c33c;
|
||||
|
||||
#[test]
|
||||
fn sound_reset_disables_running_pulses_only_when_selected() {
|
||||
fn sound_reset_disables_running_psg_channels_only_when_selected() {
|
||||
use gba_core::io::SOUNDCNT_X;
|
||||
for thumb in [false, true] {
|
||||
for flags in [0, 0x40] {
|
||||
|
|
@ -31,10 +31,12 @@ fn sound_reset_disables_running_pulses_only_when_selected() {
|
|||
bus.write16(0x04000064, 0x87ff).unwrap();
|
||||
bus.write16(0x04000068, 0xf080).unwrap();
|
||||
bus.write16(0x0400006c, 0x87fe).unwrap();
|
||||
bus.write16(0x04000078, 0xf000).unwrap();
|
||||
bus.write16(0x0400007c, 0x8000).unwrap();
|
||||
complete(&mut machine, return_pc);
|
||||
assert_eq!(
|
||||
machine.memory().read16(SOUNDCNT_X).unwrap(),
|
||||
if flags == 0 { 0x83 } else { 0 }
|
||||
if flags == 0 { 0x8b } else { 0 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,15 +167,13 @@ 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 [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);
|
||||
}
|
||||
let previous = bus.read16(0x04000074).unwrap();
|
||||
assert!(bus
|
||||
.write16(0x04000074, 0xffff)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("PSG channel trigger"));
|
||||
assert_eq!(bus.read16(0x04000074).unwrap(), previous);
|
||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0x11223344);
|
||||
assert_eq!(bus.read16(0x04000062).unwrap(), 0);
|
||||
|
|
|
|||
263
crates/gba-core/tests/noise_sound.rs
Normal file
263
crates/gba-core/tests/noise_sound.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
//! Original public-bus regressions for noise channel 4. No game audio or 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 ENVELOPE: u32 = 0x04000078;
|
||||
const CONTROL: u32 = 0x0400007c;
|
||||
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, 0x8877).unwrap();
|
||||
bus.write16(SOUNDCNT_H, 2).unwrap();
|
||||
bus
|
||||
}
|
||||
|
||||
fn start(bus: &mut Memory, short: bool) {
|
||||
bus.write16(ENVELOPE, 0xf000).unwrap();
|
||||
bus.write16(CONTROL, if short { 0x8008 } else { 0x8000 })
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn stereo(value: i16) -> StereoLevel {
|
||||
StereoLevel {
|
||||
left: value,
|
||||
right: value,
|
||||
}
|
||||
}
|
||||
|
||||
fn reference_level(cycles: u32, short: bool) -> i16 {
|
||||
let edges = cycles / 32;
|
||||
if edges == 0 {
|
||||
return -120;
|
||||
}
|
||||
let (mut state, mask, period) = if short {
|
||||
(0x40u16, 0x60, 127)
|
||||
} else {
|
||||
(0x4000, 0x6000, 32767)
|
||||
};
|
||||
for _ in 0..(edges - 1) % period {
|
||||
state = (state >> 1) ^ if state & 1 != 0 { mask } else { 0 };
|
||||
}
|
||||
if state & 1 != 0 {
|
||||
120
|
||||
} else {
|
||||
-120
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_masks_padding_readonly_status_and_first_carry_in_both_widths() {
|
||||
for (short, width) in [(false, 15), (true, 7)] {
|
||||
let mut bus = bus();
|
||||
bus.write32(ENVELOPE, 0xfffff0ff).unwrap(); // Unused bits/halfword cannot affect control.
|
||||
assert_eq!(bus.read32(ENVELOPE).unwrap(), 0xf000);
|
||||
assert_eq!(bus.read16(CONTROL).unwrap(), 0);
|
||||
bus.write8(CONTROL, if short { 8 } else { 0 }).unwrap();
|
||||
bus.write8(CONTROL + 1, 0xff).unwrap();
|
||||
assert_eq!(
|
||||
bus.read32(CONTROL).unwrap(),
|
||||
if short { 0x4008 } else { 0x4000 }
|
||||
);
|
||||
assert_eq!(bus.read32(SOUNDCNT_X).unwrap(), 0x88);
|
||||
bus.write16(SOUNDCNT_X, 0x8f).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x88);
|
||||
assert_eq!(bus.audio_level(), stereo(-120));
|
||||
bus.advance_cycles(width * 32 - 1);
|
||||
assert_eq!(bus.audio_level(), stereo(-120));
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(120));
|
||||
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_expiry_and_extra_clock_reload_do_not_need_a_general_timer() {
|
||||
let mut bus = bus();
|
||||
bus.write16(ENVELOPE, 0xf03e).unwrap();
|
||||
bus.write16(CONTROL, 0xc000).unwrap();
|
||||
bus.advance_cycles(SEQUENCE);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x88);
|
||||
bus.advance_cycles(SEQUENCE * 2);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
bus.write16(CONTROL, 0xc000).unwrap(); // Next step3: empty length reloads63.
|
||||
bus.advance_cycles(SEQUENCE * 125);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x88);
|
||||
bus.advance_cycles(SEQUENCE);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_changes_at_shared_step7_and_saturation_does_not_clear_status() {
|
||||
let mut bus = bus();
|
||||
bus.advance_cycles(SEQUENCE * 6);
|
||||
bus.write16(ENVELOPE, 0x3900).unwrap();
|
||||
bus.write16(CONTROL, 0x8008).unwrap();
|
||||
bus.advance_cycles(SEQUENCE * 2 - 1);
|
||||
assert_eq!(bus.audio_level().left.abs(), 24);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level().left.abs(), 32);
|
||||
bus.write16(ENVELOPE, 0xc100).unwrap();
|
||||
bus.write16(CONTROL, 0x8008).unwrap();
|
||||
bus.advance_cycles(SEQUENCE * 8 * 20);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x88);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn master_off_and_dac_gate_prevent_activation_until_reconfigured_and_triggered() {
|
||||
let mut bus = bus();
|
||||
start(&mut bus, false);
|
||||
bus.write8(ENVELOPE + 1, 0).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
bus.write8(ENVELOPE + 1, 0xf0).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
bus.write16(CONTROL, 0x8000).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x88);
|
||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||
bus.write32(ENVELOPE, u32::MAX).unwrap();
|
||||
bus.write32(CONTROL, u32::MAX).unwrap();
|
||||
assert_eq!(bus.read32(ENVELOPE).unwrap(), 0);
|
||||
assert_eq!(bus.read32(CONTROL).unwrap(), 0);
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
bus.write16(CONTROL, 0x8000).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_routing_gain_ratios_and_sum_before_rounding_include_noise() {
|
||||
let mut bus = bus();
|
||||
start(&mut bus, false);
|
||||
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, 0x8070).unwrap();
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: -120,
|
||||
right: 0
|
||||
}
|
||||
);
|
||||
bus.write16(MIX, 0x0800).unwrap();
|
||||
assert_eq!(
|
||||
bus.audio_level(),
|
||||
StereoLevel {
|
||||
left: 0,
|
||||
right: -15
|
||||
}
|
||||
);
|
||||
bus.write16(MIX, 0x9900).unwrap();
|
||||
bus.write16(SOUNDCNT_H, 0).unwrap();
|
||||
bus.write16(0x04000062, 0x2080).unwrap(); // Pulse1 +2.
|
||||
bus.write16(0x04000064, 0x8000).unwrap();
|
||||
bus.write16(ENVELOPE, 0x1000).unwrap(); // Noise -1.
|
||||
bus.write16(CONTROL, 0x8000).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(0)); // Sum1 >>2, not separately rounded0 + -1.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_supported_sources_mix_before_bias_clipping_and_noise_mute_does_not_stop_it() {
|
||||
for positive in [false, true] {
|
||||
let mut bus = bus();
|
||||
bus.write16(MIX, 0x9977).unwrap(); // Pulse1 and noise, both sides.
|
||||
bus.write16(SOUNDCNT_H, 0x0306).unwrap(); // Also Direct Sound A.
|
||||
bus.write16(0x04000062, if positive { 0xf080 } else { 0xf000 })
|
||||
.unwrap();
|
||||
bus.write16(0x04000064, 0x8000).unwrap();
|
||||
start(&mut bus, true);
|
||||
if positive {
|
||||
bus.advance_cycles(7 * 32);
|
||||
}
|
||||
bus.write32(FIFO_A, if positive { 0x7f7f7f7f } else { 0x80808080 })
|
||||
.unwrap();
|
||||
bus.write32(TIMER_BASE, 0x0080ffff).unwrap();
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.audio_level(), stereo(if positive { 511 } else { -512 }));
|
||||
bus.write16(MIX, 0).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x89);
|
||||
assert_eq!(bus.audio_level(), stereo(if positive { 508 } else { -512 }));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_status_and_length_are_independent_of_both_pulses() {
|
||||
let mut bus = bus();
|
||||
for (duty, frequency) in [(0x04000062, 0x04000064), (0x04000068, 0x0400006c)] {
|
||||
bus.write16(duty, 0xf080).unwrap();
|
||||
bus.write16(frequency, 0x87ff).unwrap();
|
||||
}
|
||||
bus.write16(ENVELOPE, 0xf03f).unwrap();
|
||||
bus.write16(CONTROL, 0xc000).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x8b);
|
||||
bus.advance_cycles(SEQUENCE);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x83);
|
||||
assert_eq!(bus.audio_level(), stereo(0)); // Pulses are not routed here.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halt_keeps_noise_and_modulation_running_but_stop_freezes_them() {
|
||||
for stop in [false, true] {
|
||||
let mut bus = bus();
|
||||
start(&mut bus, true);
|
||||
bus.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
let before = machine.cpu().clone();
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(
|
||||
machine.memory().audio_level(),
|
||||
stereo(reference_level(machine.cycles() as u32, true))
|
||||
);
|
||||
if stop {
|
||||
machine.memory_mut().advance_cycles(u32::MAX);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.memory().audio_level(), stereo(-120));
|
||||
} else {
|
||||
assert!(machine.cycles() > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batched_single_clock_and_row_capture_paths_agree_and_maximum_batches_are_bounded() {
|
||||
let mut bulk = bus();
|
||||
let mut rows = bus();
|
||||
let mut small = bus();
|
||||
rows.set_scanline_rendering(true);
|
||||
for bus in [&mut bulk, &mut rows, &mut small] {
|
||||
bus.write16(ENVELOPE, 0xc100).unwrap();
|
||||
bus.write16(CONTROL, 0x8008).unwrap();
|
||||
}
|
||||
for cycles in [1, 31, 4096, 32768, 262145] {
|
||||
bulk.advance_cycles(cycles);
|
||||
rows.advance_cycles(cycles);
|
||||
for _ in 0..cycles {
|
||||
small.advance_cycles(1);
|
||||
}
|
||||
assert_eq!(bulk.audio_level(), small.audio_level());
|
||||
assert_eq!(bulk.audio_level(), rows.audio_level());
|
||||
assert_eq!(
|
||||
bulk.read16(SOUNDCNT_X).unwrap(),
|
||||
rows.read16(SOUNDCNT_X).unwrap()
|
||||
);
|
||||
}
|
||||
for short in [false, true] {
|
||||
let mut bus = bus();
|
||||
start(&mut bus, short);
|
||||
bus.advance_cycles(u32::MAX);
|
||||
assert_eq!(bus.audio_level(), stereo(reference_level(u32::MAX, short)));
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x88);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue