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 modulation;
|
||||||
|
mod noise;
|
||||||
mod pulse;
|
mod pulse;
|
||||||
|
use noise::Noise;
|
||||||
use pulse::Pulse;
|
use pulse::Pulse;
|
||||||
|
|
||||||
const SEQUENCER_PERIOD: u32 = 32768; // 16,777,216 Hz / 512 Hz.
|
const SEQUENCER_PERIOD: u32 = 32768; // 16,777,216 Hz / 512 Hz.
|
||||||
|
|
@ -80,6 +82,7 @@ pub(crate) struct Audio {
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
psg: [u16; 17],
|
psg: [u16; 17],
|
||||||
pulses: [Pulse; 2],
|
pulses: [Pulse; 2],
|
||||||
|
noise: Noise,
|
||||||
sequencer_phase: u16,
|
sequencer_phase: u16,
|
||||||
next_step: u8,
|
next_step: u8,
|
||||||
control: u16,
|
control: u16,
|
||||||
|
|
@ -100,7 +103,7 @@ impl Audio {
|
||||||
{
|
{
|
||||||
return Some("reserved PSG volume selection");
|
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)")
|
Some("PSG channel trigger (synthesis not implemented)")
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -123,6 +126,9 @@ impl Audio {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
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) {
|
if let Some((index, offset)) = Self::pulse_register(address) {
|
||||||
return Some(self.pulses[index].read(offset));
|
return Some(self.pulses[index].read(offset));
|
||||||
}
|
}
|
||||||
|
|
@ -135,8 +141,6 @@ impl Audio {
|
||||||
0x0400_0074 => 0x4000,
|
0x0400_0074 => 0x4000,
|
||||||
0x0400_0070 => 0x00e0,
|
0x0400_0070 => 0x00e0,
|
||||||
0x0400_0072 => 0xe000,
|
0x0400_0072 => 0xe000,
|
||||||
0x0400_0078 => 0xff00,
|
|
||||||
0x0400_007c => 0x40ff,
|
|
||||||
0x0400_0080 => 0xff77,
|
0x0400_0080 => 0xff77,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
|
@ -147,6 +151,7 @@ impl Audio {
|
||||||
(u16::from(self.enabled) << 7)
|
(u16::from(self.enabled) << 7)
|
||||||
| u16::from(self.pulses[0].modulation.active)
|
| u16::from(self.pulses[0].modulation.active)
|
||||||
| (u16::from(self.pulses[1].modulation.active) << 1)
|
| (u16::from(self.pulses[1].modulation.active) << 1)
|
||||||
|
| (u16::from(self.noise.modulation.active) << 3)
|
||||||
}
|
}
|
||||||
SOUNDBIAS => self.bias,
|
SOUNDBIAS => self.bias,
|
||||||
0x0400_0086 | 0x0400_008a => 0,
|
0x0400_0086 | 0x0400_008a => 0,
|
||||||
|
|
@ -173,6 +178,13 @@ impl Audio {
|
||||||
}
|
}
|
||||||
continue;
|
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 shift = (address & 1) * 8;
|
||||||
let merge = |old: u16| (old & !(255 << shift)) | (u16::from(value) << shift);
|
let merge = |old: u16| (old & !(255 << shift)) | (u16::from(value) << shift);
|
||||||
match address & !1 {
|
match address & !1 {
|
||||||
|
|
@ -194,6 +206,7 @@ impl Audio {
|
||||||
if self.enabled && value & 0x80 == 0 {
|
if self.enabled && value & 0x80 == 0 {
|
||||||
self.psg = [0; 17];
|
self.psg = [0; 17];
|
||||||
self.pulses = [Pulse::default(); 2];
|
self.pulses = [Pulse::default(); 2];
|
||||||
|
self.noise = Noise::default();
|
||||||
for fifo in &mut self.fifo {
|
for fifo in &mut self.fifo {
|
||||||
fifo.reset_queue();
|
fifo.reset_queue();
|
||||||
}
|
}
|
||||||
|
|
@ -222,12 +235,14 @@ impl Audio {
|
||||||
for pulse in &mut self.pulses {
|
for pulse in &mut self.pulses {
|
||||||
pulse.advance(count);
|
pulse.advance(count);
|
||||||
}
|
}
|
||||||
|
self.noise.advance(count);
|
||||||
let phase = u32::from(self.sequencer_phase) + count;
|
let phase = u32::from(self.sequencer_phase) + count;
|
||||||
if phase == SEQUENCER_PERIOD {
|
if phase == SEQUENCER_PERIOD {
|
||||||
self.sequencer_phase = 0;
|
self.sequencer_phase = 0;
|
||||||
for pulse in &mut self.pulses {
|
for pulse in &mut self.pulses {
|
||||||
pulse.clock(self.next_step);
|
pulse.clock(self.next_step);
|
||||||
}
|
}
|
||||||
|
self.noise.clock(self.next_step);
|
||||||
self.next_step = (self.next_step + 1) & 7;
|
self.next_step = (self.next_step + 1) & 7;
|
||||||
} else {
|
} else {
|
||||||
self.sequencer_phase = phase as u16;
|
self.sequencer_phase = phase as u16;
|
||||||
|
|
@ -278,6 +293,9 @@ impl Audio {
|
||||||
psg_sample += pulse.sample();
|
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;
|
let volume = ((psg_control >> (side * 4)) & 7) as i16 + 1;
|
||||||
// Sum PSG channels before scaling, so fractional bits are discarded once.
|
// Sum PSG channels before scaling, so fractional bits are discarded once.
|
||||||
*level += (psg_sample * volume * ratio) >> 2;
|
*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(0x04000062, 0xf080).unwrap();
|
||||||
memory.write16(0x04000064, 0x87ff).unwrap();
|
memory.write16(0x04000064, 0x87ff).unwrap();
|
||||||
memory.write16(0x04000068, 0xf080).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
|
memory
|
||||||
.write32(
|
.write32(
|
||||||
TIMER_BASE,
|
TIMER_BASE,
|
||||||
|
|
@ -22,16 +24,22 @@ fn sound(memory: &mut Memory, period: u16) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pulse_status_load_observes_length_expiry_at_bus_completion() {
|
fn psg_status_load_observes_length_expiry_at_bus_completion() {
|
||||||
for (index, duty, frequency) in [(0, 0x04000062, 0x04000064), (1, 0x04000068, 0x0400006c)] {
|
for (index, duty, frequency) in [
|
||||||
|
(0, 0x04000062, 0x04000064),
|
||||||
|
(1, 0x04000068, 0x0400006c),
|
||||||
|
(3, 0x04000078, 0x0400007c),
|
||||||
|
] {
|
||||||
let (cpu, mut memory) = prepared(false, &[0xe5910000], &[(1, SOUNDCNT_X)]);
|
let (cpu, mut memory) = prepared(false, &[0xe5910000], &[(1, SOUNDCNT_X)]);
|
||||||
sound(&mut memory, 1000);
|
sound(&mut memory, 1000);
|
||||||
memory.write16(duty, 0xf03f).unwrap();
|
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);
|
memory.advance_cycles(32768 - 7);
|
||||||
let mut machine = Machine::new(cpu, memory);
|
let mut machine = Machine::new(cpu, memory);
|
||||||
machine.step().unwrap();
|
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.cpu().registers()[0], status);
|
||||||
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), status as u16);
|
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), status as u16);
|
||||||
}
|
}
|
||||||
|
|
@ -57,27 +65,47 @@ fn pulse_trigger_reloads_timer_at_store_completion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn successful_dma_trigger_starts_channel2_after_destination_bus_cycles() {
|
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, 0x8877).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(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], &[]);
|
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||||
sound(&mut memory, 1000);
|
sound(&mut memory, 1000);
|
||||||
memory.write16(0x04000080, 0x2277).unwrap();
|
memory.write16(0x04000080, routing).unwrap();
|
||||||
memory.write16(SOUNDCNT_H, 2).unwrap();
|
memory.write16(SOUNDCNT_H, 2).unwrap();
|
||||||
memory.write8(0x04000069, 0).unwrap();
|
memory.write8(envelope, 0).unwrap();
|
||||||
memory.write8(0x04000069, 0xf0).unwrap(); // DAC gate restored, still needs a trigger.
|
memory.write8(envelope, 0xf0).unwrap(); // DAC gate restored, still needs a trigger.
|
||||||
memory.write32(0x02000000, 0x87ff).unwrap();
|
memory.write32(0x02000000, control).unwrap();
|
||||||
memory.write32(DMA_BASE, 0x02000000).unwrap();
|
memory.write32(DMA_BASE, 0x02000000).unwrap();
|
||||||
memory.write32(DMA_BASE + 4, 0x0400006c).unwrap();
|
memory.write32(DMA_BASE + 4, destination).unwrap();
|
||||||
memory.write32(DMA_BASE + 8, 0x84000001).unwrap();
|
memory.write32(DMA_BASE + 8, 0x84000001).unwrap();
|
||||||
let mut machine = Machine::new(cpu, memory);
|
let mut machine = Machine::new(cpu, memory);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
machine.step().unwrap(),
|
machine.step().unwrap(),
|
||||||
crate::machine::StepKind::Dma { channel: 0 }
|
crate::machine::StepKind::Dma { channel: 0 }
|
||||||
);
|
);
|
||||||
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), 0x83);
|
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), 0x8b);
|
||||||
machine.memory_mut().advance_cycles(15);
|
machine.memory_mut().advance_cycles(delay);
|
||||||
assert_eq!(machine.memory().audio_level().left, 120);
|
assert_eq!(machine.memory().audio_level().left, before);
|
||||||
machine.memory_mut().advance_cycles(1);
|
machine.memory_mut().advance_cycles(1);
|
||||||
assert_eq!(machine.memory().audio_level().left, -120);
|
assert_eq!(machine.memory().audio_level().left, after);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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]
|
#[test]
|
||||||
fn arm_block_store_rejects_later_wave_trigger_before_committing_channel2_trigger() {
|
fn arm_block_store_rejects_later_wave_trigger_before_committing_channel2_trigger() {
|
||||||
let (cpu, mut memory) = prepared(
|
let (cpu, mut memory) = prepared(
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const REGIONS: [(u32, u32, u32); 5] = [
|
||||||
const MARKER: u32 = 0xa55a_c33c;
|
const MARKER: u32 = 0xa55a_c33c;
|
||||||
|
|
||||||
#[test]
|
#[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;
|
use gba_core::io::SOUNDCNT_X;
|
||||||
for thumb in [false, true] {
|
for thumb in [false, true] {
|
||||||
for flags in [0, 0x40] {
|
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(0x04000064, 0x87ff).unwrap();
|
||||||
bus.write16(0x04000068, 0xf080).unwrap();
|
bus.write16(0x04000068, 0xf080).unwrap();
|
||||||
bus.write16(0x0400006c, 0x87fe).unwrap();
|
bus.write16(0x0400006c, 0x87fe).unwrap();
|
||||||
|
bus.write16(0x04000078, 0xf000).unwrap();
|
||||||
|
bus.write16(0x0400007c, 0x8000).unwrap();
|
||||||
complete(&mut machine, return_pc);
|
complete(&mut machine, return_pc);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
machine.memory().read16(SOUNDCNT_X).unwrap(),
|
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();
|
bus.write16(0x04000070, 0x40).unwrap();
|
||||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0);
|
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0);
|
||||||
bus.write32(WAVE_RAM, 0xaabbccdd).unwrap();
|
bus.write32(WAVE_RAM, 0xaabbccdd).unwrap();
|
||||||
for address in [0x04000074, 0x0400007c] {
|
let previous = bus.read16(0x04000074).unwrap();
|
||||||
let previous = bus.read16(address).unwrap();
|
|
||||||
assert!(bus
|
assert!(bus
|
||||||
.write16(address, 0xffff)
|
.write16(0x04000074, 0xffff)
|
||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
.to_string()
|
.to_string()
|
||||||
.contains("PSG channel trigger"));
|
.contains("PSG channel trigger"));
|
||||||
assert_eq!(bus.read16(address).unwrap(), previous);
|
assert_eq!(bus.read16(0x04000074).unwrap(), previous);
|
||||||
}
|
|
||||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0x11223344);
|
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0x11223344);
|
||||||
assert_eq!(bus.read16(0x04000062).unwrap(), 0);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,9 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/gba-core/src/audio.rs` | Direct Sound state, PSG sequencer, idle channel/wave banks, and instantaneous stereo mixing |
|
| `crates/gba-core/src/audio.rs` | Direct Sound state, PSG sequencer, idle channel/wave banks, and instantaneous stereo mixing |
|
||||||
| `crates/gba-core/src/audio/pulse.rs` | Shared pulse oscillator; sweep is mapped only for channel 1 |
|
| `crates/gba-core/src/audio/pulse.rs` | Shared pulse oscillator; sweep is mapped only for channel 1 |
|
||||||
| `crates/gba-core/src/audio/modulation.rs` | Shared PSG length, envelope, logical DAC gate, and activity state |
|
| `crates/gba-core/src/audio/modulation.rs` | Shared PSG length, envelope, logical DAC gate, and activity state |
|
||||||
|
| `crates/gba-core/src/audio/noise.rs` | Noise channel 4 divider, 7/15-bit counter, and bounded jump-ahead advancement |
|
||||||
|
| `crates/gba-core/src/audio/noise/tests.rs` | Counter periods, independent bit-array reference, divider fields, and clock batching |
|
||||||
|
| `crates/gba-core/tests/noise_sound.rs` | Noise registers, modulation, stereo mixing, independent status, HALT/STOP, and capture independence |
|
||||||
| `crates/gba-core/src/audio/pulse/tests.rs` | Exhaustive frequency/duty arithmetic and original modulation edge tests |
|
| `crates/gba-core/src/audio/pulse/tests.rs` | Exhaustive frequency/duty arithmetic and original modulation edge tests |
|
||||||
| `crates/gba-core/tests/pulse_sound.rs` | Register masks, stereo mixing, sequencer clocks, idle modes, batching, and explicit limits |
|
| `crates/gba-core/tests/pulse_sound.rs` | Register masks, stereo mixing, sequencer clocks, idle modes, batching, and explicit limits |
|
||||||
| `crates/gba-core/tests/pulse_sound/channel2.rs` | Channel 2 mapping, register gaps, independent state, shared clocks, and sum-before-rounding mixing |
|
| `crates/gba-core/tests/pulse_sound/channel2.rs` | Channel 2 mapping, register gaps, independent state, shared clocks, and sum-before-rounding mixing |
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Audio device subset
|
# Audio device subset
|
||||||
|
|
||||||
The core models Direct Sound A/B, bounded sound DMA, and programmable sound generator (PSG) pulse channels 1 and 2.
|
The core models Direct Sound A/B, bounded sound DMA, and programmable sound generator (PSG) pulse channels 1–2 and noise channel 4.
|
||||||
It does not produce desktop audio. PSG wave channel 3 and noise channel 4 remain unsupported.
|
It does not produce desktop audio. PSG wave channel 3 remains unsupported.
|
||||||
`Memory::audio_level()` exposes the current digital stereo level for deterministic inspection, not a continuous sample stream.
|
`Memory::audio_level()` exposes the current digital stereo level for deterministic inspection, not a continuous sample stream.
|
||||||
|
|
||||||
## Registers and PSG state
|
## Registers and PSG state
|
||||||
|
|
@ -11,9 +11,12 @@ It does not produce desktop audio. PSG wave channel 3 and noise channel 4 remain
|
||||||
| `0x04000060..0x04000065` | Pulse channel 1 sweep, duty, length, envelope, frequency, and trigger |
|
| `0x04000060..0x04000065` | Pulse channel 1 sweep, duty, length, envelope, frequency, and trigger |
|
||||||
| `0x04000068..0x04000069` | Pulse channel 2 duty, length, and envelope |
|
| `0x04000068..0x04000069` | Pulse channel 2 duty, length, and envelope |
|
||||||
| `0x0400006c..0x0400006d` | Pulse channel 2 frequency and trigger; no sweep unit |
|
| `0x0400006c..0x0400006d` | Pulse channel 2 frequency and trigger; no sweep unit |
|
||||||
| `0x04000070..0x04000081` | Idle channel 3–4 configuration and PSG mixer controls |
|
| `0x04000070..0x04000075` | Idle wave channel 3 configuration |
|
||||||
|
| `0x04000078..0x04000079` | Noise channel 4 length and envelope |
|
||||||
|
| `0x0400007c..0x0400007d` | Noise divider, counter width, shift, length enable, and trigger |
|
||||||
|
| `0x04000080..0x04000081` | PSG stereo routing and volume |
|
||||||
| `0x04000082` (`SOUNDCNT_H`) | Mixing, routing, timer selection, and FIFO reset strobes; readback mask `0x770f` |
|
| `0x04000082` (`SOUNDCNT_H`) | Mixing, routing, timer selection, and FIFO reset strobes; readback mask `0x770f` |
|
||||||
| `0x04000084` (`SOUNDCNT_X`) | Master enable in bit 7; read-only channel 1/2 activity in bits 0/1; channels 3–4 remain inactive |
|
| `0x04000084` (`SOUNDCNT_X`) | Master enable in bit 7; channel 1/2/4 activity in read-only bits 0/1/3; channel 3 remains inactive |
|
||||||
| `0x04000088` (`SOUNDBIAS`) | Bias/resolution configuration with mask `0xc3fe` |
|
| `0x04000088` (`SOUNDBIAS`) | Bias/resolution configuration with mask `0xc3fe` |
|
||||||
| `0x04000090..0x0400009f` | CPU wave RAM window, opposite the bank selected by `SOUND3CNT_L` bit 6 |
|
| `0x04000090..0x0400009f` | CPU wave RAM window, opposite the bank selected by `SOUND3CNT_L` bit 6 |
|
||||||
| `0x040000a0..0x040000a7` | FIFO A/B writes with byte, halfword, and word access boundaries preserved |
|
| `0x040000a0..0x040000a7` | FIFO A/B writes with byte, halfword, and word access boundaries preserved |
|
||||||
|
|
@ -28,10 +31,10 @@ The CPU then accesses wave bank 1. Idle bank selection can expose bank 0 after m
|
||||||
The original BIOS reset clears bank 1 and both FIFO queues. It retains bank 0 and any separate in-flight playback word.
|
The original BIOS reset clears bank 1 and both FIFO queues. It retains bank 0 and any separate in-flight playback word.
|
||||||
This is a functional reset subset, not a claim of complete firmware ordering or sound-reset equivalence.
|
This is a functional reset subset, not a claim of complete firmware ordering or sound-reset equivalence.
|
||||||
|
|
||||||
An enabled channel 3–4 trigger returns `MemoryError::UnsupportedIo` with the exact address and value.
|
An enabled wave channel 3 trigger returns `MemoryError::UnsupportedIo` with the exact address and value.
|
||||||
This applies even when the configured channel would be inaudible. All PSG writes are ignored while master sound is disabled.
|
This applies even when the configured channel would be inaudible. All PSG writes are ignored while master sound is disabled.
|
||||||
Reserved PSG volume selection 3 is rejected while master sound is enabled, including enable after disabled configuration.
|
Reserved PSG volume selection 3 is rejected while master sound is enabled, including enable after disabled configuration.
|
||||||
Idle channel 3–4 writes do not imply playback support.
|
Idle wave channel 3 writes do not imply playback support.
|
||||||
|
|
||||||
## Pulse channels 1 and 2
|
## Pulse channels 1 and 2
|
||||||
|
|
||||||
|
|
@ -84,6 +87,41 @@ First-trigger output suppression, trigger sub-divider alignment, envelope trigge
|
||||||
The model samples the selected duty position immediately after trigger and uses a full-period timer reload.
|
The model samples the selected duty position immediately after trigger and uses a full-period timer reload.
|
||||||
The consulted implementations differ on some initial phases and envelope/sweep edge cases; no GBA hardware audio recording was used as an oracle.
|
The consulted implementations differ on some initial phases and envelope/sweep edge cases; no GBA hardware audio recording was used as an oracle.
|
||||||
|
|
||||||
|
## Noise channel 4
|
||||||
|
|
||||||
|
Noise uses a deterministic linear-feedback shift register (LFSR), not a host random-number generator.
|
||||||
|
The nominal counter follows GBATEK's GBA Galois description:
|
||||||
|
|
||||||
|
| Width | Trigger seed | Feedback mask | Sequence period |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 15-bit | `0x4000` | `0x6000` | 32767 shifts |
|
||||||
|
| 7-bit | `0x0040` | `0x0060` | 127 shifts |
|
||||||
|
|
||||||
|
At each edge, the old low bit selects the signed output level.
|
||||||
|
The counter shifts right and, when that bit was one, XORs the feedback mask.
|
||||||
|
The period between edges is `(ratio == 0 ? 32 : 64 * ratio) << shift` system clocks.
|
||||||
|
All eight divider fields and sixteen shift fields use this GBA formula, including shifts 14 and 15.
|
||||||
|
No general-purpose timer or IRQ enable is required.
|
||||||
|
|
||||||
|
A trigger reloads the selected seed, full edge interval, envelope, and applicable length state.
|
||||||
|
The nominal held output starts low and changes at counter edges.
|
||||||
|
Live divider/shift writes preserve the remaining interval; the new period applies on the next reload.
|
||||||
|
Live width writes preserve the full counter, including transient upper bits, and select the new feedback mask.
|
||||||
|
They do not truncate or reseed the counter.
|
||||||
|
Exact hardware startup alignment and live-width transition behavior remain unverified.
|
||||||
|
The consulted mGBA implementation uses a different counter representation; this model does not claim matching initial sample phases.
|
||||||
|
|
||||||
|
Length, envelope, logical DAC gating, and activity use the same tested modulation implementation as the pulse channels.
|
||||||
|
All three channels receive the shared sequencer clocks but keep independent state.
|
||||||
|
Noise status uses SOUNDCNT_X bit 3. Envelope saturation alone does not clear it.
|
||||||
|
Master disable resets noise configuration and counter state. Muting its routing does not stop it.
|
||||||
|
The unused halfwords at `0x0400007a` and `0x0400007e` read zero and do not alias noise controls.
|
||||||
|
|
||||||
|
Large clock advances combine precomputed powers of the counter transition instead of shifting once per noise sample.
|
||||||
|
An independent bit-array reference checks these jump-ahead operations, including live-width transients and the final output carry.
|
||||||
|
Both complete sequence periods and maximum clock batches have deterministic regressions.
|
||||||
|
This is device-state advancement, not a sampled audio stream or hardware recording comparison.
|
||||||
|
|
||||||
## FIFO and timer behavior
|
## FIFO and timer behavior
|
||||||
|
|
||||||
Each channel has a seven-word queue, a separate four-byte playback word, and a held signed sample.
|
Each channel has a seven-word queue, a separate four-byte playback word, and a held signed sample.
|
||||||
|
|
@ -128,12 +166,12 @@ Sub-instruction DMA arbitration and exact refill latency are not modeled.
|
||||||
## Mixing and timing ownership
|
## Mixing and timing ownership
|
||||||
|
|
||||||
Each routed Direct Sound sample contributes `sample * 2` at 50% volume or `sample * 4` at 100% volume.
|
Each routed Direct Sound sample contributes `sample * 2` at 50% volume or `sample * 4` at 100% volume.
|
||||||
Each active pulse contributes a centered signed amplitude, `+volume` or `-volume`.
|
Each active pulse or noise channel contributes a centered signed amplitude, `+volume` or `-volume`.
|
||||||
`SOUNDCNT_L` routes channel 1 through bits 8/12 and channel 2 through bits 9/13, for right/left respectively.
|
`SOUNDCNT_L` routes channel 1 through bits 8/12, channel 2 through bits 9/13, and noise through bits 11/15, for right/left respectively.
|
||||||
The mixer sums routed PSG amplitudes on each side, then multiplies by that side's volume field plus one.
|
The mixer sums routed PSG amplitudes on each side, then multiplies by that side's volume field plus one.
|
||||||
`SOUNDCNT_H` applies the PSG ratio: 25%, 50%, or 100%, using one arithmetic right shift after summation.
|
`SOUNDCNT_H` applies the PSG ratio: 25%, 50%, or 100%, using one arithmetic right shift after summation.
|
||||||
Rounding each pulse separately would produce different low-volume levels and is not used.
|
Rounding each PSG channel separately would produce different low-volume levels and is not used.
|
||||||
At full volume, each pulse spans `-120..120` before mixing with Direct Sound.
|
At full volume, each supported PSG channel spans `-120..120` before mixing with Direct Sound.
|
||||||
This centered mixer follows GBATEK's signed range and the NanoBoyAdvance comparison; exact analog offset is not modeled.
|
This centered mixer follows GBATEK's signed range and the NanoBoyAdvance comparison; exact analog offset is not modeled.
|
||||||
The mixer adds all supported channels and the ten-bit bias, clips each side to `0..1023`, and subtracts 512.
|
The mixer adds all supported channels and the ten-bit bias, clips each side to `0..1023`, and subtracts 512.
|
||||||
`StereoLevel` therefore contains signed levels in `-512..511`. Master disable returns zero.
|
`StereoLevel` therefore contains signed levels in `-512..511`. Master disable returns zero.
|
||||||
|
|
@ -157,15 +195,20 @@ Pulse tests cover every frequency/duty pair, waveform batching, status, length,
|
||||||
They also check bus-completion status reads, retrigger timing, clock ownership, and rollback with both pulse channels running.
|
They also check bus-completion status reads, retrigger timing, clock ownership, and rollback with both pulse channels running.
|
||||||
Channel 2 tests cover register gaps, absence of sweep, independent state, shared sequencer phase, and sum-before-rounding mixing.
|
Channel 2 tests cover register gaps, absence of sweep, independent state, shared sequencer phase, and sum-before-rounding mixing.
|
||||||
Original CPU/DMA tests verify channel 2 activation and rollback when a later block-store value is unsupported.
|
Original CPU/DMA tests verify channel 2 activation and rollback when a later block-store value is unsupported.
|
||||||
BIOS reset tests verify that selected sound reset stops both channels, while unselected sound state remains active.
|
Noise tests cover both counter widths, divider/shift fields, complete sequences, jump-ahead arithmetic, retrigger, and live rate/width writes.
|
||||||
|
They also cover modulation, independent status, stereo mixing, HALT/STOP, and scanline-capture independence.
|
||||||
|
CPU/DMA tests include noise register writes at bus completion and failed-step rollback with a running counter.
|
||||||
|
BIOS reset tests verify that selected sound reset stops all supported PSG channels, while unselected sound state remains active.
|
||||||
|
|
||||||
These tests validate the documented nominal model, not hardware audio fidelity.
|
These tests validate the documented nominal model, not hardware audio fidelity.
|
||||||
Next work: noise channel 4, wave channel 3, a timestamped or fixed-rate output stream, PWM/mixer sampling, and a Darwin host backend.
|
Remaining audio work: wave channel 3, a timestamped or fixed-rate output stream, PWM/mixer sampling, and a Darwin host backend.
|
||||||
The local Emerald startup now passes both pulse triggers and stops at a noise channel 4 trigger.
|
The local Emerald startup now passes the pulse and noise triggers, then stops at GPIO serial interrupt control.
|
||||||
See [the local runtime result](../research/emerald-reset.md).
|
See [the local runtime result](../research/emerald-reset.md).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
- [GBATEK noise channel 4](https://mgba-emu.github.io/gbatek/): length/envelope registers, divider formula, Galois counter seeds, masks, and sequence periods.
|
||||||
|
- [NanoBoyAdvance noise at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/channel/noise_channel.cc) and [noise header](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/channel/noise_channel.hh): GBA divider scaling, counter feedback, trigger, and register behavior comparison.
|
||||||
- [GBATEK channels 1 and 2](https://mgba-emu.github.io/gbatek/): register fields, duty ratios, modulation rates, channel 2's register gap, and absence of sweep.
|
- [GBATEK channels 1 and 2](https://mgba-emu.github.io/gbatek/): register fields, duty ratios, modulation rates, channel 2's register gap, and absence of sweep.
|
||||||
- [Pan Docs audio details](https://gbdev.io/pandocs/Audio_details.html): duty counters, shadow sweep, length edge rules, and GBA digital-mixer differences. GB-only edge details are not treated as verified GBA measurements.
|
- [Pan Docs audio details](https://gbdev.io/pandocs/Audio_details.html): duty counters, shadow sweep, length edge rules, and GBA digital-mixer differences. GB-only edge details are not treated as verified GBA measurements.
|
||||||
- [GbdevWiki sound hardware](https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware): background sequencer and sweep descriptions.
|
- [GbdevWiki sound hardware](https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware): background sequencer and sweep descriptions.
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ Supported flags can be combined:
|
||||||
| 7 | Reset supported display, DMA, timer, and interrupt registers as described below |
|
| 7 | Reset supported display, DMA, timer, and interrupt registers as described below |
|
||||||
|
|
||||||
All low-byte flag combinations, including `r0=0xff`, are accepted within the supported device subset.
|
All low-byte flag combinations, including `r0=0xff`, are accepted within the supported device subset.
|
||||||
This is not full audio or serial support. [PSG channels 3–4](audio.md) and [serial transfers](serial.md) still fail explicitly.
|
This is not full audio or serial support. [wave channel 3](audio.md) and [serial transfers](serial.md) still fail explicitly.
|
||||||
Sound reset retains wave bank 0 and the separate in-flight playback word; exact firmware sound-reset equivalence is not established.
|
Sound reset retains wave bank 0 and the separate in-flight playback word; exact firmware sound-reset equivalence is not established.
|
||||||
The firmware always clears the low halfword of SIODATA32, even when bit 5 is clear; its upper halfword is preserved.
|
The firmware always clears the low halfword of SIODATA32, even when bit 5 is clear; its upper halfword is preserved.
|
||||||
|
|
||||||
|
|
@ -233,7 +233,7 @@ After the RAM clears, bit 5 clears SIOCNT and SIODATA8, writes RCNT=0x8000, and
|
||||||
Disconnected general-purpose inputs read high, so RCNT reads back as 0x800f.
|
Disconnected general-purpose inputs read high, so RCNT reads back as 0x800f.
|
||||||
Bit 6 disables SOUNDCNT_X, strobes both FIFO resets, clears SOUNDCNT_H, and sets SOUNDBIAS=0x0200.
|
Bit 6 disables SOUNDCNT_X, strobes both FIFO resets, clears SOUNDCNT_H, and sets SOUNDBIAS=0x0200.
|
||||||
It then clears accessible wave bank 1. Wave bank 0 is retained and can be selected again after master enable.
|
It then clears accessible wave bank 1. Wave bank 0 is retained and can be selected again after master enable.
|
||||||
Disabled PSG registers remain zero. Running pulse channels stop only when sound reset is selected.
|
Disabled PSG registers remain zero. Running pulse and noise channels stop only when sound reset is selected.
|
||||||
Reset does not produce audio, fabricate serial transfers, or bypass normal bus accesses.
|
Reset does not produce audio, fabricate serial transfers, or bypass normal bus accesses.
|
||||||
|
|
||||||
Bit 7 runs after the selected RAM and sound/serial resets:
|
Bit 7 runs after the selected RAM and sound/serial resets:
|
||||||
|
|
@ -511,5 +511,5 @@ Unsupported SWIs, unsupported exception vectors, and null/misaligned IRQ callbac
|
||||||
The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op.
|
The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op.
|
||||||
Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction.
|
Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction.
|
||||||
Prior boot/service steps remain committed on failure.
|
Prior boot/service steps remain committed on failure.
|
||||||
PSG channels 3–4, host audio output, serial transfers, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
|
PSG wave channel 3, host audio output, serial transfers, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
|
||||||
This subset is not sufficient for Pokémon Emerald compatibility.
|
This subset is not sufficient for Pokémon Emerald compatibility.
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ Failed steps discard staged counters, divider progress, timer/audio writes, FIFO
|
||||||
Display and other device clocks then advance once, without advancing timers again.
|
Display and other device clocks then advance once, without advancing timers again.
|
||||||
Display and remaining devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled.
|
Display and remaining devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled.
|
||||||
[Direct Sound](audio.md) consumes selected timer overflows in the timer transaction; sound DMA requests commit at step boundaries.
|
[Direct Sound](audio.md) consumes selected timer overflows in the timer transaction; sound DMA requests commit at step boundaries.
|
||||||
The same transaction advances both PSG pulse oscillators and their shared 512 Hz sequencer independently of the general-purpose timers.
|
The same transaction advances both PSG pulse oscillators, the noise counter, and their shared 512 Hz sequencer independently of the general-purpose timers.
|
||||||
Host clock advances and HALT idle batches retain ordinary bulk timer advancement; STOP idle does not advance timers.
|
Host clock advances and HALT idle batches retain ordinary bulk timer advancement; STOP idle does not advance timers.
|
||||||
|
|
||||||
The unchanged [prefetch cancellation probe](../research/prefetch-cancellation.md) now matches both published interval totals and actual timer samples.
|
The unchanged [prefetch cancellation probe](../research/prefetch-cancellation.md) now matches both published interval totals and actual timer samples.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ No ROM bytes, extracted code, or assets were added to the repository.
|
||||||
## General correction
|
## General correction
|
||||||
|
|
||||||
The reset correction introduced bounded sound and [disconnected serial](../hardware/serial.md) initialization models.
|
The reset correction introduced bounded sound and [disconnected serial](../hardware/serial.md) initialization models.
|
||||||
The [sound model](../hardware/audio.md) now also supports Direct Sound, bounded FIFO DMA, and both nominal pulse channels.
|
The [sound model](../hardware/audio.md) now also supports Direct Sound, bounded FIFO DMA, both pulse channels, and noise channel 4.
|
||||||
The BIOS performs reset through original ARM stores against that model.
|
The BIOS performs reset through original ARM stores against that model.
|
||||||
Bit 5 selects general-purpose input pins and clears supported serial reset registers.
|
Bit 5 selects general-purpose input pins and clears supported serial reset registers.
|
||||||
Bit 6 disables sound, resets FIFO queues, clears mixing configuration and accessible wave RAM, and sets the functional bias default.
|
Bit 6 disables sound, resets FIFO queues, clears mixing configuration and accessible wave RAM, and sets the functional bias default.
|
||||||
|
|
@ -32,16 +32,17 @@ direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --w
|
||||||
The earlier reset-only implementation stopped at master sound enable (`0x04000084`, PC `0x082e0518`).
|
The earlier reset-only implementation stopped at master sound enable (`0x04000084`, PC `0x082e0518`).
|
||||||
The Direct Sound increment then reached a PSG channel 1 trigger (`0x04000065`, PC `0x082e03bc`).
|
The Direct Sound increment then reached a PSG channel 1 trigger (`0x04000065`, PC `0x082e03bc`).
|
||||||
Channel 1 support reached the channel 2 trigger (`0x0400006d`, PC `0x082e03c0`).
|
Channel 1 support reached the channel 2 trigger (`0x0400006d`, PC `0x082e03c0`).
|
||||||
With both pulse channels implemented, both runs now stop at noise channel 4:
|
Both pulse channels then reached the noise trigger (`0x0400007d`, PC `0x082e03c4`).
|
||||||
|
With noise channel 4 implemented, both runs now stop at general-purpose input/output (GPIO) serial interrupt enable:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Steps: 321163; instructions: 320651; IRQ entries: 0; DMA units: 512; HALT idle: 0
|
Steps: 395420; instructions: 393690; IRQ entries: 2; DMA units: 1728; HALT idle: 0
|
||||||
Nominal cycles: 1038746
|
Nominal cycles: 1328057
|
||||||
PC=0x082e03c4
|
PC=0x082e2aac
|
||||||
unsupported PSG channel trigger (synthesis not implemented): write 0x80 at 0x0400007d
|
unsupported GPIO serial interrupt enable: write 0x01 at 0x04000135
|
||||||
```
|
```
|
||||||
|
|
||||||
The native run captures three startup frames. These do not establish a working title screen or gameplay.
|
The native run captures five startup frames. These do not establish a working title screen or gameplay.
|
||||||
Audio-device behavior is validated by original tests, not by audible game output.
|
Audio-device behavior is validated by original tests, not by audible game output.
|
||||||
The window run does not reach its requested frame limit and correctly exits with failure.
|
The window run does not reach its requested frame limit and correctly exits with failure.
|
||||||
Terminal execution is also diagnostic, not a compatibility pass.
|
Terminal execution is also diagnostic, not a compatibility pass.
|
||||||
|
|
@ -53,12 +54,15 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
|
||||||
Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
|
Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
|
||||||
The pinned Pong debug/release reports match.
|
The pinned Pong debug/release reports match.
|
||||||
|
|
||||||
The next observed startup requirement is noise channel 4. Wave channel 3 is also still missing.
|
The next observed requirement is [serial control-register behavior](../hardware/serial.md), specifically RCNT bit 8.
|
||||||
Both pulse channels now have independent nominal waveform, length, envelope, status, and stereo mixing behavior.
|
Check mode gating and interrupt-enable semantics before changing the current rejection.
|
||||||
Sweep applies only to channel 1. The two pulses share the sequencer without sharing their channel state.
|
This diagnostic alone does not establish that the game needs an active link transfer.
|
||||||
Direct Sound has FIFO clocks and nominal DMA requests. All supported channels use the instantaneous digital-level inspection interface.
|
|
||||||
A continuous sample stream and desktop output are still missing. Do not treat digital-level tests as audible-game validation.
|
Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels.
|
||||||
Do not suppress the PSG trigger diagnostic without implementing the requested device behavior.
|
Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests.
|
||||||
|
All supported channels use the instantaneous digital-level inspection interface.
|
||||||
|
Wave channel 3, a continuous sample stream, and desktop output are still missing.
|
||||||
|
Do not treat digital-level tests as audible-game validation or suppress remaining diagnostics without implementing their behavior.
|
||||||
Cartridge saves remain a separate required feature. SRAM alone will not support Emerald's Flash save protocol.
|
Cartridge saves remain a separate required feature. SRAM alone will not support Emerald's Flash save protocol.
|
||||||
|
|
||||||
Hardware and emulator references are recorded in the linked subsystem documents.
|
Hardware and emulator references are recorded in the linked subsystem documents.
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,8 @@ Update this file when a feature lands or a limit is removed.
|
||||||
- RegisterRamReset supports disabled sound and disconnected serial initialization, including the common `r0=0xff` request.
|
- RegisterRamReset supports disabled sound and disconnected serial initialization, including the common `r0=0xff` request.
|
||||||
- Direct Sound A/B support timer-driven FIFO playback, stereo digital levels, and DMA1/A or DMA2/B refill blocks.
|
- Direct Sound A/B support timer-driven FIFO playback, stereo digital levels, and DMA1/A or DMA2/B refill blocks.
|
||||||
- PSG pulse channels 1 and 2 support independent nominal frequency/duty playback, length, envelope, status, and stereo mixing. Channel 1 also supports sweep.
|
- PSG pulse channels 1 and 2 support independent nominal frequency/duty playback, length, envelope, status, and stereo mixing. Channel 1 also supports sweep.
|
||||||
- Channel 3–4 triggers and unsupported serial operations produce explicit address/value diagnostics with whole-access and block-store rollback.
|
- Noise channel 4 supports nominal 7/15-bit generation, divider clocks, shared length/envelope behavior, status, and stereo mixing.
|
||||||
|
- Wave channel 3 triggers and unsupported serial operations produce explicit address/value diagnostics with whole-access and block-store rollback.
|
||||||
- ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP.
|
- ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP.
|
||||||
- Optional ROM-window frame limits and an original input-test ROM generator.
|
- Optional ROM-window frame limits and an original input-test ROM generator.
|
||||||
- Headless JSON ROM suites with PC or VBlank completion, register/CPSR/memory/pixel assertions, scheduled buttons, bounded budgets, and structured failure reports.
|
- Headless JSON ROM suites with PC or VBlank completion, register/CPSR/memory/pixel assertions, scheduled buttons, bounded budgets, and structured failure reports.
|
||||||
|
|
@ -137,8 +138,8 @@ Horizontal window comparators retain four-cycle event history.
|
||||||
Sprite rows prepare one line ahead with a whole-row cycle40 sample and nominal per-row work limits.
|
Sprite rows prepare one line ahead with a whole-row cycle40 sample and nominal per-row work limits.
|
||||||
Individual fetch timing, hardware-accurate work cutoffs, video-bus contention, and per-pixel color composition remain unimplemented.
|
Individual fetch timing, hardware-accurate work cutoffs, video-bus contention, and per-pixel color composition remain unimplemented.
|
||||||
There is no host audio output or cartridge save support.
|
There is no host audio output or cartridge save support.
|
||||||
The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, idle channel 3–4 state, wave banking, and instantaneous digital mixing.
|
The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, noise channel 4, idle wave channel 3 state, and instantaneous digital mixing.
|
||||||
It does not yet provide a continuous sample stream. Channel 3–4 playback and unverified sound-DMA pairings remain diagnostic.
|
It does not yet provide a continuous sample stream. Wave channel 3 playback and unverified sound-DMA pairings remain diagnostic.
|
||||||
PSG timing and signed mixing are nominal; hardware startup edges and analog behavior remain unverified.
|
PSG timing and signed mixing are nominal; hardware startup edges and analog behavior remain unverified.
|
||||||
The [serial initialization subset](hardware/serial.md) supports idle normal data and disconnected general-purpose pins, not active transfers.
|
The [serial initialization subset](hardware/serial.md) supports idle normal data and disconnected general-purpose pins, not active transfers.
|
||||||
Mapped I/O covers DMA, timers, interrupts, WAITCNT, display control/status, background control/scroll/affine registers, window/effect registers, KEYINPUT/KEYCNT, POSTFLG, and HALTCNT.
|
Mapped I/O covers DMA, timers, interrupts, WAITCNT, display control/status, background control/scroll/affine registers, window/effect registers, KEYINPUT/KEYCNT, POSTFLG, and HALTCNT.
|
||||||
|
|
@ -150,7 +151,7 @@ ROM files can run in bounded terminal mode or a window with the original BIOS re
|
||||||
ROM windows have no demo-specific startup checks. Input is sampled between bounded execution slices.
|
ROM windows have no demo-specific startup checks. Input is sampled between bounded execution slices.
|
||||||
Audio, saves, and external BIOS loading remain unavailable. ROM loading does not establish commercial-game compatibility.
|
Audio, saves, and external BIOS loading remain unavailable. ROM loading does not establish commercial-game compatibility.
|
||||||
A local Emerald run now completes its all-device reset and master sound enable.
|
A local Emerald run now completes its all-device reset and master sound enable.
|
||||||
It also passes both pulse triggers, then stops explicitly at noise channel 4 (`0x0400007d`).
|
It also passes the pulse and noise triggers, then stops explicitly at GPIO serial interrupt enable (`0x04000135`).
|
||||||
No title-screen, gameplay, or audible-game compatibility is established.
|
No title-screen, gameplay, or audible-game compatibility is established.
|
||||||
Its captured startup frames do not establish title-screen or gameplay compatibility. See [the startup result](research/emerald-reset.md).
|
Its captured startup frames do not establish title-screen or gameplay compatibility. See [the startup result](research/emerald-reset.md).
|
||||||
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.
|
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,10 @@ The [pinned Pong homebrew](public-pong.md) passes scripted menus, ball movement,
|
||||||
The native Darwin window also starts and presents frames without a diagnostic.
|
The native Darwin window also starts and presents frames without a diagnostic.
|
||||||
This exposed and corrected missing firmware affine-scale initialization, without a game-specific patch.
|
This exposed and corrected missing firmware affine-scale initialization, without a game-specific patch.
|
||||||
Frame-based scenarios and the preparation adapter now make the result repeatable.
|
Frame-based scenarios and the preparation adapter now make the result repeatable.
|
||||||
A local Emerald run now passes its initial all-device reset, master sound enable, and both PSG pulse triggers.
|
A local Emerald run now passes its initial all-device reset, master sound enable, and pulse/noise triggers.
|
||||||
It stops at a noise channel 4 trigger. The core has tested Direct Sound and both pulse channels.
|
It stops at GPIO serial interrupt control. The core has tested Direct Sound, both pulse channels, and noise channel 4.
|
||||||
A continuous sample stream and desktop audio output remain missing.
|
Wave channel 3, a continuous sample stream, and desktop audio output remain missing.
|
||||||
See [the runtime result](research/emerald-reset.md). Remaining PSG channels are the next demonstrated startup requirement.
|
See [the runtime result](research/emerald-reset.md). Serial control-register semantics are the next demonstrated startup requirement.
|
||||||
The separate save requirement remains cartridge SRAM, followed by safe host save persistence.
|
The separate save requirement remains cartridge SRAM, followed by safe host save persistence.
|
||||||
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue