feat(audio): add single-bank wave channel playback
This commit is contained in:
parent
7546d17630
commit
f496f12ea3
20 changed files with 860 additions and 44 deletions
|
|
@ -1,10 +1,12 @@
|
|||
//! Nominal Direct Sound, PSG pulse channels 1–2, and noise channel 4. No host audio output.
|
||||
//! Nominal Direct Sound and PSG channels, including single-bank wave playback. No host output.
|
||||
|
||||
mod modulation;
|
||||
mod noise;
|
||||
mod pulse;
|
||||
mod wave;
|
||||
use noise::Noise;
|
||||
use pulse::Pulse;
|
||||
use wave::Wave;
|
||||
|
||||
const SEQUENCER_PERIOD: u32 = 32768; // 16,777,216 Hz / 512 Hz.
|
||||
|
||||
|
|
@ -83,6 +85,7 @@ pub(crate) struct Audio {
|
|||
psg: [u16; 17],
|
||||
pulses: [Pulse; 2],
|
||||
noise: Noise,
|
||||
wave_channel: Wave,
|
||||
sequencer_phase: u16,
|
||||
next_step: u8,
|
||||
control: u16,
|
||||
|
|
@ -103,15 +106,15 @@ impl Audio {
|
|||
{
|
||||
return Some("reserved PSG volume selection");
|
||||
}
|
||||
if self.enabled && address == 0x0400_0075 && value & 0x80 != 0 {
|
||||
Some("PSG channel trigger (synthesis not implemented)")
|
||||
if self.enabled && (0x0400_0070..=0x0400_0075).contains(&address) {
|
||||
self.wave_channel.unsupported(address - 0x0400_0070, value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn wave_bank(&self) -> usize {
|
||||
1 ^ usize::from((self.psg[8] >> 6) & 1)
|
||||
1 ^ self.wave_channel.bank()
|
||||
}
|
||||
|
||||
/// Channel 2 has no sweep register. Its register gap must not alias any
|
||||
|
|
@ -126,6 +129,9 @@ impl Audio {
|
|||
}
|
||||
|
||||
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
||||
if (0x0400_0070..=0x0400_0075).contains(&address) {
|
||||
return Some(self.wave_channel.read(address - 0x0400_0070));
|
||||
}
|
||||
if (0x0400_0078..=0x0400_007d).contains(&address) {
|
||||
return Some(self.noise.read(address - 0x0400_0078));
|
||||
}
|
||||
|
|
@ -138,9 +144,6 @@ impl Audio {
|
|||
let value = match address & !1 {
|
||||
SOUND_START..=0x0400_0080 => {
|
||||
let mask = match address & !1 {
|
||||
0x0400_0074 => 0x4000,
|
||||
0x0400_0070 => 0x00e0,
|
||||
0x0400_0072 => 0xe000,
|
||||
0x0400_0080 => 0xff77,
|
||||
_ => 0,
|
||||
};
|
||||
|
|
@ -151,6 +154,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.wave_channel.active) << 2)
|
||||
| (u16::from(self.noise.modulation.active) << 3)
|
||||
}
|
||||
SOUNDBIAS => self.bias,
|
||||
|
|
@ -178,6 +182,13 @@ impl Audio {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (0x0400_0070..=0x0400_0075).contains(&address) {
|
||||
if self.enabled {
|
||||
self.wave_channel
|
||||
.write(address - 0x0400_0070, value, self.next_step);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (0x0400_0078..=0x0400_007d).contains(&address) {
|
||||
if self.enabled {
|
||||
self.noise
|
||||
|
|
@ -207,6 +218,7 @@ impl Audio {
|
|||
self.psg = [0; 17];
|
||||
self.pulses = [Pulse::default(); 2];
|
||||
self.noise = Noise::default();
|
||||
self.wave_channel = Wave::default();
|
||||
for fifo in &mut self.fifo {
|
||||
fifo.reset_queue();
|
||||
}
|
||||
|
|
@ -236,6 +248,7 @@ impl Audio {
|
|||
pulse.advance(count);
|
||||
}
|
||||
self.noise.advance(count);
|
||||
self.wave_channel.advance(count, &mut self.wave);
|
||||
let phase = u32::from(self.sequencer_phase) + count;
|
||||
if phase == SEQUENCER_PERIOD {
|
||||
self.sequencer_phase = 0;
|
||||
|
|
@ -243,6 +256,7 @@ impl Audio {
|
|||
pulse.clock(self.next_step);
|
||||
}
|
||||
self.noise.clock(self.next_step);
|
||||
self.wave_channel.clock(self.next_step);
|
||||
self.next_step = (self.next_step + 1) & 7;
|
||||
} else {
|
||||
self.sequencer_phase = phase as u16;
|
||||
|
|
@ -290,15 +304,18 @@ impl Audio {
|
|||
let mut psg_sample = 0;
|
||||
for (index, pulse) in self.pulses.iter().enumerate() {
|
||||
if psg_control & (1 << (8 + side * 4 + index)) != 0 {
|
||||
psg_sample += pulse.sample();
|
||||
psg_sample += pulse.sample() * 4;
|
||||
}
|
||||
}
|
||||
if psg_control & (1 << (11 + side * 4)) != 0 {
|
||||
psg_sample += self.noise.sample();
|
||||
psg_sample += self.noise.sample() * 4;
|
||||
}
|
||||
if psg_control & (1 << (10 + side * 4)) != 0 {
|
||||
psg_sample += self.wave_channel.sample_quarters();
|
||||
}
|
||||
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;
|
||||
*level += (psg_sample * volume * ratio) >> 4;
|
||||
*level = (*level + (self.bias & 0x3ff) as i16).clamp(0, 1023) - 512;
|
||||
}
|
||||
StereoLevel {
|
||||
|
|
|
|||
133
crates/gba-core/src/audio/wave.rs
Normal file
133
crates/gba-core/src/audio/wave.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
//! Nominal single-bank wave playback. RAM rotates in high-nibble-first order.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct Wave {
|
||||
control: u8,
|
||||
volume: u8,
|
||||
frequency: u16,
|
||||
length: u16,
|
||||
length_enabled: bool,
|
||||
pub(super) active: bool,
|
||||
remaining: u32,
|
||||
sample: Option<u8>,
|
||||
}
|
||||
|
||||
impl Wave {
|
||||
pub(super) fn bank(&self) -> usize {
|
||||
usize::from((self.control >> 6) & 1)
|
||||
}
|
||||
|
||||
pub(super) fn unsupported(&self, offset: u32, value: u8) -> Option<&'static str> {
|
||||
if (offset == 5 && value & 0x80 != 0 && self.control & 0xa0 == 0xa0)
|
||||
|| (offset == 0 && self.active && value & 0xa0 == 0xa0)
|
||||
{
|
||||
return Some("64-sample wave playback");
|
||||
}
|
||||
if offset == 0 && self.active && value & 0x80 != 0 && (value ^ self.control) & 0x40 != 0 {
|
||||
return Some("wave bank change during playback");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn read(&self, offset: u32) -> u8 {
|
||||
match offset {
|
||||
0 => self.control,
|
||||
3 => self.volume,
|
||||
5 => u8::from(self.length_enabled) << 6,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn period(&self) -> u32 {
|
||||
8 * (2048 - u32::from(self.frequency))
|
||||
}
|
||||
|
||||
pub(super) fn write(&mut self, offset: u32, value: u8, next_step: u8) {
|
||||
match offset {
|
||||
0 => {
|
||||
self.control = value & 0xe0;
|
||||
if value & 0x80 == 0 {
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
2 => self.length = 256 - u16::from(value),
|
||||
3 => self.volume = value & 0xe0,
|
||||
4 => self.frequency = (self.frequency & 0x700) | u16::from(value),
|
||||
5 => {
|
||||
self.frequency = (self.frequency & 255) | (u16::from(value & 7) << 8);
|
||||
let extra_clock = next_step & 1 != 0;
|
||||
if !self.length_enabled && value & 0x40 != 0 && extra_clock {
|
||||
self.clock_length();
|
||||
}
|
||||
self.length_enabled = value & 0x40 != 0;
|
||||
if value & 0x80 != 0 {
|
||||
self.active = self.control & 0x80 != 0;
|
||||
if self.length == 0 {
|
||||
self.length = 256 - u16::from(self.length_enabled && extra_clock);
|
||||
}
|
||||
self.remaining = self.period();
|
||||
// Nominal startup policy: silence until the first full sample period.
|
||||
// Retrigger cannot restore RAM that playback has already rotated.
|
||||
self.sample = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn clock_length(&mut self) {
|
||||
if self.length != 0 {
|
||||
self.length -= 1;
|
||||
if self.length == 0 {
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clock(&mut self, step: u8) {
|
||||
if step & 1 == 0 && self.length_enabled {
|
||||
self.clock_length();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn advance(&mut self, cycles: u32, banks: &mut [[u8; 16]; 2]) {
|
||||
if !self.active || cycles == 0 {
|
||||
return;
|
||||
}
|
||||
if cycles < self.remaining {
|
||||
self.remaining -= cycles;
|
||||
return;
|
||||
}
|
||||
let rest = cycles - self.remaining;
|
||||
let edges = 1 + rest / self.period();
|
||||
self.remaining = self.period() - rest % self.period();
|
||||
let data = &mut banks[self.bank()];
|
||||
let old = *data;
|
||||
let digit = |index: usize| {
|
||||
let byte = old[(index % 32) / 2];
|
||||
(byte >> if index & 1 == 0 { 4 } else { 0 }) & 15
|
||||
};
|
||||
self.sample = Some(digit(((edges - 1) % 32) as usize));
|
||||
let rotation = (edges % 32) as usize;
|
||||
for (index, byte) in data.iter_mut().enumerate() {
|
||||
*byte = (digit(index * 2 + rotation) << 4) | digit(index * 2 + rotation + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Quarter-units of the pulse/noise amplitude. Keep fractional wave volume
|
||||
/// until after routing, master volume, and PSG ratio have been combined.
|
||||
pub(super) fn sample_quarters(&self) -> i16 {
|
||||
if !self.active {
|
||||
return 0;
|
||||
}
|
||||
let Some(sample) = self.sample else { return 0 };
|
||||
let gain = if self.volume & 0x80 != 0 {
|
||||
3
|
||||
} else {
|
||||
[0, 4, 2, 1][usize::from(self.volume >> 5)]
|
||||
};
|
||||
(i16::from(sample) - 8) * 2 * gain
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
132
crates/gba-core/src/audio/wave/tests.rs
Normal file
132
crates/gba-core/src/audio/wave/tests.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
use super::Wave;
|
||||
|
||||
fn start(bank: u8, frequency: u16) -> Wave {
|
||||
let mut wave = Wave::default();
|
||||
wave.write(0, 0x80 | (bank << 6), 0);
|
||||
wave.write(3, 0x20, 0);
|
||||
wave.write(4, frequency as u8, 0);
|
||||
wave.write(5, 0x80 | (frequency >> 8) as u8, 0);
|
||||
wave
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_frequency_and_bank_follow_high_nibble_first_rotation() {
|
||||
for frequency in 0..2048 {
|
||||
for bank in 0..2 {
|
||||
let mut wave = start(bank, frequency);
|
||||
let mut ram = [[0xab; 16]; 2];
|
||||
let mut digits: Vec<u8> = (0..32).map(|i| (i * 7 % 16) as u8).collect();
|
||||
for (index, byte) in ram[bank as usize].iter_mut().enumerate() {
|
||||
*byte = digits[index * 2] * 16 + digits[index * 2 + 1];
|
||||
}
|
||||
let period = (2048 - u32::from(frequency)) * 8;
|
||||
assert_eq!(wave.sample_quarters(), 0);
|
||||
for _ in 0..65 {
|
||||
let before = ram;
|
||||
wave.advance(period - 1, &mut ram);
|
||||
assert_eq!(ram, before);
|
||||
let output = digits.remove(0);
|
||||
digits.push(output);
|
||||
wave.advance(1, &mut ram);
|
||||
assert_eq!(wave.sample_quarters(), (i16::from(output) - 8) * 8);
|
||||
for (index, byte) in ram[bank as usize].iter().enumerate() {
|
||||
assert_eq!(*byte, digits[index * 2] * 16 + digits[index * 2 + 1]);
|
||||
}
|
||||
assert_eq!(ram[1 - bank as usize], [0xab; 16]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_advance_matches_single_clocks_and_full_rotations_keep_the_last_sample() {
|
||||
for frequency in [0, 1024, 2047] {
|
||||
let mut fast = start(0, frequency);
|
||||
let mut slow = fast;
|
||||
let mut a = [[0x12; 16], [0xab; 16]];
|
||||
let mut b = a;
|
||||
fast.advance(100003, &mut a);
|
||||
for _ in 0..100003 {
|
||||
slow.advance(1, &mut b);
|
||||
}
|
||||
assert_eq!((fast, a), (slow, b));
|
||||
}
|
||||
let mut wave = start(0, 2047);
|
||||
let mut ram = [[0x12; 16]; 2];
|
||||
wave.advance(256, &mut ram);
|
||||
assert_eq!(wave.sample, Some(2));
|
||||
assert_eq!(ram, [[0x12; 16]; 2]);
|
||||
wave.advance(u32::MAX, &mut ram);
|
||||
assert_eq!(wave.sample, Some(1)); // floor(MAX/8) is odd.
|
||||
assert_eq!(wave.remaining, 1);
|
||||
assert_eq!(ram[0], [0x21; 16]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volume_mute_and_force_gain_preserve_fractional_units_and_activity() {
|
||||
let mut wave = start(0, 2047);
|
||||
for digit in 0..16 {
|
||||
let mut ram = [[digit * 17; 16]; 2];
|
||||
wave.advance(8, &mut ram);
|
||||
for control in 0..8 {
|
||||
wave.write(3, control << 5, 0);
|
||||
let gain = if control >= 4 {
|
||||
3
|
||||
} else {
|
||||
[0, 4, 2, 1][control as usize]
|
||||
};
|
||||
assert_eq!(wave.sample_quarters(), (i16::from(digit) - 8) * 2 * gain);
|
||||
assert!(wave.active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_frequency_length_and_gate_have_independent_effects() {
|
||||
let mut wave = start(0, 2047);
|
||||
let mut ram = [[0x12; 16]; 2];
|
||||
wave.advance(3, &mut ram);
|
||||
wave.write(4, 0, 0); // Change next reload, not the remaining five cycles.
|
||||
assert_eq!(wave.remaining, 5);
|
||||
wave.advance(5, &mut ram);
|
||||
assert_eq!(wave.sample, Some(1));
|
||||
assert_eq!(wave.remaining, 2048);
|
||||
wave.write(5, 0x87, 0); // Retrigger: fresh period, rotated RAM is not restored.
|
||||
assert_eq!(wave.sample, None);
|
||||
wave.advance(2048, &mut ram);
|
||||
assert_eq!(wave.sample, Some(2));
|
||||
wave.write(0, 0, 0);
|
||||
assert!(!wave.active);
|
||||
let before = ram;
|
||||
wave.advance(1000000, &mut ram);
|
||||
assert_eq!(ram, before);
|
||||
wave.write(0, 0x80, 0);
|
||||
assert!(!wave.active); // Gate enable alone is not a trigger.
|
||||
wave.write(2, 255, 0);
|
||||
wave.write(5, 0xc0, 0);
|
||||
assert_eq!(wave.length, 1);
|
||||
wave.clock(1);
|
||||
assert!(wave.active);
|
||||
wave.clock(2);
|
||||
assert!(!wave.active);
|
||||
wave.write(5, 0xc0, 1);
|
||||
assert_eq!(wave.length, 255);
|
||||
assert!(wave.active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_enable_extra_clock_and_muted_trigger_do_not_reset_ram() {
|
||||
let mut wave = start(0, 2047);
|
||||
wave.write(2, 255, 0);
|
||||
wave.write(5, 0x47, 1);
|
||||
assert_eq!(wave.length, 0);
|
||||
assert!(!wave.active);
|
||||
wave.write(0, 0, 0);
|
||||
wave.write(5, 0xc7, 1);
|
||||
assert_eq!(wave.length, 255);
|
||||
assert!(!wave.active);
|
||||
for _ in 0..255 {
|
||||
wave.clock(0);
|
||||
}
|
||||
assert_eq!(wave.length, 0);
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@ mod serial_step_tests;
|
|||
mod timer_step_tests;
|
||||
#[cfg(test)]
|
||||
mod timing_event_tests;
|
||||
#[cfg(test)]
|
||||
mod wave_step_tests;
|
||||
pub(crate) use fetch::InstructionFetch;
|
||||
use iwram_bus::IwramBus;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ fn sound(memory: &mut Memory, period: u16) {
|
|||
memory.write16(0x04000068, 0xf080).unwrap();
|
||||
memory.write16(0x0400006c, 0x87ff).unwrap();
|
||||
memory.write16(0x04000078, 0xf000).unwrap();
|
||||
memory.write16(0x0400007c, 0x8008).unwrap(); // All supported PSG channels run unrouted.
|
||||
memory.write16(0x0400007c, 0x8008).unwrap(); // Pulse and noise channels run unrouted.
|
||||
memory
|
||||
.write32(
|
||||
TIMER_BASE,
|
||||
|
|
@ -160,6 +160,7 @@ fn failed_dma_source_and_destination_restore_audio_after_speculative_timer_edges
|
|||
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
sound(&mut memory, 1);
|
||||
memory.write32(FIFO_A, 0x04030201).unwrap();
|
||||
memory.write16(0x04000070, 0xa0).unwrap();
|
||||
memory.write32(0x02000000, word).unwrap();
|
||||
memory.write32(DMA_BASE, source).unwrap();
|
||||
memory.write32(DMA_BASE + 4, destination).unwrap();
|
||||
|
|
@ -206,7 +207,7 @@ fn arm_block_store_rejects_later_wave_trigger_before_committing_channel2_trigger
|
|||
(0, 0x04000068),
|
||||
(1, 0xf080),
|
||||
(2, 0x87ff),
|
||||
(3, 0),
|
||||
(3, 0xa0),
|
||||
(4, 0x8000),
|
||||
],
|
||||
); // STMIA r0!,{r1-r4}: channel2 configuration/trigger, wave configuration/trigger.
|
||||
|
|
@ -219,7 +220,7 @@ fn arm_block_store_rejects_later_wave_trigger_before_committing_channel2_trigger
|
|||
.step()
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("PSG channel trigger"));
|
||||
.contains("64-sample wave playback"));
|
||||
assert_eq!(machine.memory().io.audio, audio);
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
|
|
@ -236,6 +237,7 @@ fn batch_validation_simulates_sound_enable_and_disable_before_committing_any_wri
|
|||
(SOUNDCNT_X, 0x80),
|
||||
(0x04000068, 0xf080),
|
||||
(0x0400006c, 0x87ff),
|
||||
(0x04000070, 0xa0),
|
||||
(0x04000074, 0x8000),
|
||||
])
|
||||
.is_err());
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ fn dma_start_excludes_source_cycles_and_failed_destination_discards_serial_progr
|
|||
if fail {
|
||||
start(&mut memory, 60);
|
||||
memory.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
memory.write16(0x04000070, 0xa0).unwrap(); // Unsupported two-bank wave playback.
|
||||
}
|
||||
memory
|
||||
.write32(0x02000000, if fail { 0x8000 } else { 0x4083 })
|
||||
|
|
|
|||
112
crates/gba-core/src/memory/wave_step_tests.rs
Normal file
112
crates/gba-core/src/memory/wave_step_tests.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Original wave clocks, RAM rotation, and access-phase rollback probes.
|
||||
use super::{timing_event_tests::prepared, *};
|
||||
use crate::{
|
||||
io::{SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, WAVE_RAM},
|
||||
machine::{Machine, StepKind},
|
||||
};
|
||||
|
||||
fn setup(bus: &mut Memory) {
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
bus.write16(SOUNDBIAS, 0x200).unwrap();
|
||||
bus.write16(SOUNDCNT_H, 2).unwrap();
|
||||
bus.write16(0x04000080, 0x4477).unwrap();
|
||||
bus.write16(0x04000070, 0x40).unwrap();
|
||||
bus.write32(WAVE_RAM, 0x123456f0).unwrap(); // Bank 0, high nibble first.
|
||||
bus.write16(0x04000070, 0x80).unwrap();
|
||||
bus.write16(0x04000072, 0x2000).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arm_thumb_and_dma_triggers_start_after_the_store_bus_phase() {
|
||||
for kind in 0..3 {
|
||||
let (cpu, mut memory) = prepared(
|
||||
kind == 1,
|
||||
&[if kind == 1 { 0x6008 } else { 0xe5810000 }],
|
||||
&[(0, 0x87ff), (1, 0x04000074)],
|
||||
);
|
||||
setup(&mut memory);
|
||||
if kind == 2 {
|
||||
memory.write32(0x02000000, 0x87ff).unwrap();
|
||||
memory.write32(DMA_BASE, 0x02000000).unwrap();
|
||||
memory.write32(DMA_BASE + 4, 0x04000074).unwrap();
|
||||
memory.write32(DMA_BASE + 8, 0x84000001).unwrap();
|
||||
}
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
assert_eq!(
|
||||
machine.step().unwrap(),
|
||||
if kind == 2 {
|
||||
StepKind::Dma { channel: 0 }
|
||||
} else {
|
||||
StepKind::Instruction
|
||||
}
|
||||
);
|
||||
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), 0x84);
|
||||
machine.memory_mut().advance_cycles(7);
|
||||
assert_eq!(machine.memory().audio_level().left, 0);
|
||||
machine.memory_mut().advance_cycles(1);
|
||||
assert_eq!(machine.memory().audio_level().left, 112);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_expiry_at_load_bus_completion_is_visible_in_status() {
|
||||
let (cpu, mut memory) = prepared(false, &[0xe5910000], &[(1, SOUNDCNT_X)]);
|
||||
setup(&mut memory);
|
||||
memory.write16(0x04000072, 0x20ff).unwrap();
|
||||
memory.write16(0x04000074, 0xc7ff).unwrap();
|
||||
memory.advance_cycles(32768 - 7);
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.cpu().registers()[0], 0x80);
|
||||
assert_eq!(machine.memory().audio_level().left, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_block_access_rolls_back_rotated_ram_samples_configuration_and_clocks() {
|
||||
for store in [false, true] {
|
||||
let (cpu, mut memory) = if store {
|
||||
prepared(
|
||||
false,
|
||||
&[0xe8a0003e],
|
||||
&[
|
||||
(0, 0x04000070),
|
||||
(1, 0x20000080),
|
||||
(2, 0x87ff),
|
||||
(3, 0),
|
||||
(4, 0),
|
||||
(5, 0x00030000),
|
||||
],
|
||||
)
|
||||
} else {
|
||||
prepared(false, &[0xe8910005], &[(1, SOUNDBIAS)])
|
||||
};
|
||||
setup(&mut memory);
|
||||
memory.write16(0x04000074, 0x87ff).unwrap();
|
||||
memory.advance_cycles(7);
|
||||
let audio = memory.io.audio;
|
||||
let mut machine = Machine::new(cpu, memory);
|
||||
let cpu = machine.cpu().clone();
|
||||
for _ in 0..2 {
|
||||
assert!(machine.step().is_err());
|
||||
assert_eq!(machine.memory().io.audio, audio);
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_bank_switch_reads_the_current_rotated_ram_not_committed_storage() {
|
||||
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||
setup(&mut memory);
|
||||
memory.write16(0x04000074, 0x87ff).unwrap();
|
||||
memory.advance_cycles(8);
|
||||
let audio = memory.io.audio;
|
||||
memory.begin_timer_step();
|
||||
memory.write16(0x04000070, 0x40).unwrap(); // Stop and expose played bank.
|
||||
assert_eq!(memory.read32(WAVE_RAM).unwrap(), 0x20416305);
|
||||
memory.write32(WAVE_RAM, 0xaabbccdd).unwrap();
|
||||
assert_eq!(memory.read32(WAVE_RAM).unwrap(), 0xaabbccdd);
|
||||
memory.discard_timer_step();
|
||||
assert_eq!(memory.io.audio, audio);
|
||||
}
|
||||
|
|
@ -31,12 +31,15 @@ fn sound_reset_disables_running_psg_channels_only_when_selected() {
|
|||
bus.write16(0x04000064, 0x87ff).unwrap();
|
||||
bus.write16(0x04000068, 0xf080).unwrap();
|
||||
bus.write16(0x0400006c, 0x87fe).unwrap();
|
||||
bus.write16(0x04000070, 0x80).unwrap();
|
||||
bus.write16(0x04000072, 0x2000).unwrap();
|
||||
bus.write16(0x04000074, 0x87fd).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 { 0x8b } else { 0 }
|
||||
if flags == 0 { 0x8f } else { 0 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,12 +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();
|
||||
bus.write16(0x04000070, 0xe0).unwrap(); // Unimplemented two-bank playback.
|
||||
let previous = bus.read16(0x04000074).unwrap();
|
||||
assert!(bus
|
||||
.write16(0x04000074, 0xffff)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("PSG channel trigger"));
|
||||
.contains("64-sample wave playback"));
|
||||
assert_eq!(bus.read16(0x04000074).unwrap(), previous);
|
||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0x11223344);
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
|
|||
#[test]
|
||||
fn block_store_rejects_later_activation_before_earlier_control_writes() {
|
||||
let code: [u32; 7] = [
|
||||
0xe3a00301, 0xe2800070, 0xe59f1008, 0xe3a02902, 0xe8a00006, 0xeafffffe, 0x12345678,
|
||||
0xe3a00301, 0xe2800070, 0xe59f1008, 0xe3a02902, 0xe8a00006, 0xeafffffe, 0x123400a0,
|
||||
];
|
||||
let mut bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
|
|
@ -108,7 +108,7 @@ fn block_store_rejects_later_activation_before_earlier_control_writes() {
|
|||
let before = machine.cpu().clone();
|
||||
let cycles = machine.cycles();
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("PSG channel trigger"));
|
||||
assert!(error.to_string().contains("64-sample wave playback"));
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
assert_eq!(machine.memory().read16(0x04000070).unwrap(), 0);
|
||||
|
|
@ -119,6 +119,7 @@ fn block_store_rejects_later_activation_before_earlier_control_writes() {
|
|||
fn dma_activation_error_preserves_channel_progress_and_clocks() {
|
||||
let mut bus = memory();
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
bus.write16(0x04000070, 0xa0).unwrap();
|
||||
bus.write32(0x02000000, 0x8000).unwrap();
|
||||
bus.write32(DMA_BASE, 0x02000000).unwrap();
|
||||
bus.write32(DMA_BASE + 4, 0x04000074).unwrap();
|
||||
|
|
@ -126,7 +127,7 @@ fn dma_activation_error_preserves_channel_progress_and_clocks() {
|
|||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
let before = machine.cpu().clone();
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains("PSG channel trigger"));
|
||||
assert!(error.to_string().contains("64-sample wave playback"));
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
|
|
|
|||
208
crates/gba-core/tests/wave_sound.rs
Normal file
208
crates/gba-core/tests/wave_sound.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
//! Original channel 3 tests. Digital levels are not host audio or hardware recordings.
|
||||
use gba_core::{
|
||||
audio::StereoLevel,
|
||||
io::{HALTCNT, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, WAVE_RAM},
|
||||
memory::Memory,
|
||||
};
|
||||
const CONTROL: u32 = 0x04000070;
|
||||
const LENGTH: u32 = 0x04000072;
|
||||
const FREQUENCY: u32 = 0x04000074;
|
||||
const ROUTE: u32 = 0x04000080;
|
||||
|
||||
fn bus() -> Memory {
|
||||
let mut bus = Memory::new(vec![]).unwrap();
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
bus.write16(SOUNDBIAS, 0x200).unwrap();
|
||||
bus.write16(SOUNDCNT_H, 2).unwrap();
|
||||
bus.write16(ROUTE, 0x4477).unwrap();
|
||||
bus
|
||||
}
|
||||
fn start(bus: &mut Memory, bank: u16, data: u8) {
|
||||
bus.write16(CONTROL, (bank ^ 1) << 6).unwrap();
|
||||
for offset in 0..16 {
|
||||
bus.write8(WAVE_RAM + offset, data).unwrap();
|
||||
}
|
||||
bus.write16(CONTROL, 0x80 | (bank << 6)).unwrap();
|
||||
bus.write16(LENGTH, 0x2000).unwrap();
|
||||
bus.write16(FREQUENCY, 0x87ff).unwrap();
|
||||
}
|
||||
fn stereo(level: i16) -> StereoLevel {
|
||||
StereoLevel {
|
||||
left: level,
|
||||
right: level,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_masks_gate_status_and_unused_halfword_do_not_alias_other_channels() {
|
||||
let mut bus = bus();
|
||||
bus.write16(CONTROL, 0xffff).unwrap();
|
||||
bus.write16(LENGTH, 0xffff).unwrap();
|
||||
bus.write16(FREQUENCY, 0x7fff).unwrap(); // No trigger, 64-sample configuration is idle.
|
||||
bus.write16(FREQUENCY + 2, 0xffff).unwrap();
|
||||
assert_eq!(bus.read16(CONTROL).unwrap(), 0xe0);
|
||||
assert_eq!(bus.read16(LENGTH).unwrap(), 0xe000);
|
||||
assert_eq!(bus.read16(FREQUENCY).unwrap(), 0x4000);
|
||||
assert_eq!(bus.read16(FREQUENCY + 2).unwrap(), 0);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
bus.write16(CONTROL, 0).unwrap();
|
||||
bus.write16(FREQUENCY, 0x87ff).unwrap(); // A trigger with the gate off cannot play.
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
start(&mut bus, 0, 0xf0);
|
||||
bus.write16(SOUNDCNT_X, 0xff).unwrap(); // Status is read-only.
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x84);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
bus.advance_cycles(8);
|
||||
assert_eq!(bus.audio_level(), stereo(112));
|
||||
bus.advance_cycles(8);
|
||||
assert_eq!(bus.audio_level(), stereo(-128));
|
||||
bus.write16(CONTROL, 0).unwrap();
|
||||
bus.write16(CONTROL, 0x80).unwrap();
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_bank_access_and_readback_follow_rotated_storage_without_touching_the_other_bank() {
|
||||
for bank in [0, 1] {
|
||||
let mut bus = bus();
|
||||
start(&mut bus, bank, 0x12);
|
||||
bus.write32(WAVE_RAM, 0xabcdef90).unwrap(); // Other bank remains writable during playback.
|
||||
bus.advance_cycles(8);
|
||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0xabcdef90);
|
||||
bus.write16(CONTROL, (bank ^ 1) << 6).unwrap(); // Stop and expose the played bank.
|
||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0x21212121);
|
||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
bus.write16(CONTROL, (bank ^ 1) << 6).unwrap();
|
||||
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0x21212121);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wave_gain_routes_and_psg_ratio_use_one_final_signed_rounding() {
|
||||
let mut bus = bus();
|
||||
start(&mut bus, 1, 0x99); // Centered amplitude +2 before volume.
|
||||
bus.advance_cycles(8);
|
||||
for ratio in 0..3 {
|
||||
bus.write16(SOUNDCNT_H, ratio).unwrap();
|
||||
for setting in 0..8 {
|
||||
bus.write8(LENGTH + 1, setting << 5).unwrap();
|
||||
let gain = if setting >= 4 {
|
||||
3
|
||||
} else {
|
||||
[0, 4, 2, 1][setting as usize]
|
||||
};
|
||||
for volume in 0..8 {
|
||||
bus.write16(ROUTE, 0x4400 | volume | (volume << 4)).unwrap();
|
||||
let expected = (2 * gain * (volume as i16 + 1) * (1 << ratio)) >> 4;
|
||||
assert_eq!(bus.audio_level(), stereo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
bus.write16(ROUTE, 0x0400).unwrap();
|
||||
assert_eq!(bus.audio_level(), StereoLevel { left: 0, right: 1 }); // +1.5 rounds once.
|
||||
bus.write16(ROUTE, 0).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x84);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wave_fraction_combines_with_pulse_before_rounding_and_direct_sound_bias_clipping() {
|
||||
let mut bus = bus();
|
||||
start(&mut bus, 0, 0x77); // -2 amplitude.
|
||||
bus.advance_cycles(8);
|
||||
bus.write16(LENGTH, 0x8000).unwrap(); // -1.5 at forced 75%.
|
||||
bus.write16(0x04000062, 0x1080).unwrap(); // Pulse amplitude +1 at duty position zero.
|
||||
bus.write16(0x04000064, 0x8000).unwrap();
|
||||
bus.write16(ROUTE, 0x5500).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(-1)); // (-1.5 + 1), not individually rounded by a different rule.
|
||||
bus.write16(SOUNDBIAS, 0).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(-512));
|
||||
bus.write16(SOUNDBIAS, 0x3fe).unwrap();
|
||||
bus.write16(0x04000062, 0xf080).unwrap();
|
||||
bus.write16(0x04000064, 0x8000).unwrap();
|
||||
bus.write16(ROUTE, 0x5577).unwrap();
|
||||
assert_eq!(bus.audio_level(), stereo(511));
|
||||
// Fractional wave and pulse contributions must not be rounded separately.
|
||||
bus.write16(SOUNDBIAS, 0x200).unwrap();
|
||||
start(&mut bus, 0, 0x99);
|
||||
bus.advance_cycles(8);
|
||||
bus.write16(LENGTH, 0x8000).unwrap(); // +1.5.
|
||||
bus.write16(0x04000062, 0x1080).unwrap();
|
||||
bus.write16(0x04000064, 0x8000).unwrap(); // +1.
|
||||
bus.write16(ROUTE, 0x5500).unwrap();
|
||||
bus.write16(SOUNDCNT_H, 1).unwrap(); // 50%: (+1.5 + 1)/2, rounded once.
|
||||
assert_eq!(bus.audio_level(), stereo(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_shared_clock_halt_stop_and_master_disable_have_distinct_effects() {
|
||||
let mut bus = bus();
|
||||
start(&mut bus, 0, 0xf0);
|
||||
bus.write16(LENGTH, 0x20ff).unwrap(); // One length tick.
|
||||
bus.write16(FREQUENCY, 0xc7ff).unwrap();
|
||||
bus.write8(HALTCNT, 0).unwrap();
|
||||
bus.advance_cycles(32767);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x84);
|
||||
bus.advance_cycles(1);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
assert_eq!(bus.audio_level(), stereo(0));
|
||||
start(&mut bus, 0, 0xf0);
|
||||
bus.advance_cycles(8);
|
||||
bus.write8(HALTCNT, 0x80).unwrap();
|
||||
let before = bus.audio_level();
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.audio_level(), before);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x84);
|
||||
bus.write16(SOUNDCNT_X, 0).unwrap();
|
||||
bus.write32(CONTROL, u32::MAX).unwrap(); // Disabled writes ignored.
|
||||
bus.write16(FREQUENCY, 0xffff).unwrap();
|
||||
bus.write16(SOUNDCNT_X, 0x80).unwrap();
|
||||
assert_eq!(bus.read32(CONTROL).unwrap(), 0);
|
||||
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_live_bank_changes_and_two_bank_playback_are_retryable_and_atomic() {
|
||||
let mut bus = bus();
|
||||
start(&mut bus, 0, 0x12);
|
||||
bus.advance_cycles(8);
|
||||
let before = bus.audio_level();
|
||||
for value in [0xc0, 0xa0, 0xe0] {
|
||||
let error = bus.write32(CONTROL, 0xe0000000 | value).unwrap_err();
|
||||
assert_eq!(bus.write32(CONTROL, 0xe0000000 | value), Err(error));
|
||||
assert_eq!(bus.read16(CONTROL).unwrap(), 0x80);
|
||||
assert_eq!(bus.read16(LENGTH).unwrap(), 0x2000);
|
||||
assert_eq!(bus.audio_level(), before);
|
||||
}
|
||||
bus.write16(CONTROL, 0).unwrap();
|
||||
bus.write16(CONTROL, 0xa0).unwrap();
|
||||
let error = bus.write16(FREQUENCY, 0xffff).unwrap_err();
|
||||
assert!(error.to_string().contains("64-sample wave playback"));
|
||||
assert_eq!(bus.write16(FREQUENCY, 0xffff), Err(error));
|
||||
assert_eq!(bus.read16(FREQUENCY).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batched_clocks_match_small_chunks_with_and_without_scanline_capture() {
|
||||
let mut a = bus();
|
||||
let mut b = bus();
|
||||
for bus in [&mut a, &mut b] {
|
||||
start(bus, 1, 0x12);
|
||||
}
|
||||
b.set_scanline_rendering(true);
|
||||
a.advance_cycles(100003);
|
||||
for _ in 0..100003 {
|
||||
b.advance_cycles(1);
|
||||
}
|
||||
assert_eq!(a.audio_level(), b.audio_level());
|
||||
for bus in [&mut a, &mut b] {
|
||||
bus.write16(CONTROL, 0).unwrap();
|
||||
}
|
||||
for offset in 0..16 {
|
||||
assert_eq!(a.read8(WAVE_RAM + offset), b.read8(WAVE_RAM + offset));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue