feat(audio): implement PSG noise channel 4

This commit is contained in:
0xrsydn 2026-10-07 02:51:46 +07:00
commit 8ae7b3cf72
14 changed files with 774 additions and 75 deletions

View file

@ -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;

View 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;

View 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);
}
}

View file

@ -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(