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
|
|
@ -77,6 +77,39 @@ fn raw_original_program_runs_with_a_bounded_report_without_changing_the_file() {
|
|||
assert_eq!(fs::read(path).unwrap(), original);
|
||||
}
|
||||
|
||||
fn wave_setup() -> Vec<u32> {
|
||||
// Original ARM halfword stores and a status load; no external audio or assets.
|
||||
let mut code = Vec::new();
|
||||
for (address, value) in [
|
||||
(0x04000084, 0x80),
|
||||
(0x04000090, 0xf012), // CPU bank 1 before selecting playback bank 1.
|
||||
(0x04000070, 0xc0),
|
||||
(0x04000072, 0x2000),
|
||||
(0x04000074, 0x87ff),
|
||||
] {
|
||||
code.extend([0xe59f0000, 0xea000000, address]);
|
||||
code.extend([0xe59f1000, 0xea000000, value]);
|
||||
code.push(0xe1c010b0); // STRH r1,[r0]
|
||||
}
|
||||
code.extend([0xe59f0000, 0xea000000, 0x04000084, 0xe1d020b0, 0xeafffffe]);
|
||||
code
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_wave_program_can_activate_channel3_without_host_audio() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.rom(&wave_setup());
|
||||
let original = fs::read(&path).unwrap();
|
||||
let output = run(&path, "1000");
|
||||
assert!(output.status.success(), "{}", stderr(&output));
|
||||
assert!(
|
||||
stdout(&output).contains("r2=0x00000084"),
|
||||
"{}",
|
||||
stdout(&output)
|
||||
);
|
||||
assert_eq!(fs::read(path).unwrap(), original);
|
||||
}
|
||||
|
||||
fn joybus_setup() -> Vec<u32> {
|
||||
// Original ARM: select Joybus, enable IRQ, supply a reply, inspect local status/data.
|
||||
vec![
|
||||
|
|
@ -458,6 +491,11 @@ fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors()
|
|||
);
|
||||
assert!(stdout(&output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&output).contains("Result: window frame limit reached"));
|
||||
let wave_path = fixture.rom(&wave_setup());
|
||||
let wave_output = window(&wave_path, false, None);
|
||||
assert!(wave_output.status.success(), "{}", stderr(&wave_output));
|
||||
assert!(stdout(&wave_output).contains("Captured ROM frames: 3"));
|
||||
assert!(stdout(&wave_output).contains("r2=0x00000084"));
|
||||
let joybus_path = fixture.rom(&joybus_setup());
|
||||
let joybus_output = window(&joybus_path, false, None);
|
||||
assert!(joybus_output.status.success(), "{}", stderr(&joybus_output));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -64,9 +64,13 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/src/cpu/fetch_history_tests.rs` | Shared sample consumption, no execution-entry rereads, and failed-step retry |
|
||||
| `crates/gba-core/src/cpu/fetch_boundary_tests.rs` | Actual fetch-region snapshots, split refills, IWRAM lane requirements, ROM-window boundaries, and nominal timing |
|
||||
| `crates/gba-core/src/io.rs` | I/O registers, timers, HALT/STOP wake-up, next-event bounds, and IRQ latches |
|
||||
| `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, 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/modulation.rs` | Shared PSG length, envelope, logical DAC gate, and activity state |
|
||||
| `crates/gba-core/src/audio/wave.rs` | Single-bank wave samples, rotating RAM, length/gate state, and fractional gain |
|
||||
| `crates/gba-core/src/audio/wave/tests.rs` | Every sample rate/bank, nibble-order reference, full rotations, batches, volumes, and length edges |
|
||||
| `crates/gba-core/tests/wave_sound.rs` | Wave masks, bank access, stereo gain, status, HALT/STOP, reset, and explicit unsupported modes |
|
||||
| `crates/gba-core/src/memory/wave_step_tests.rs` | Wave trigger/status bus phases, rotated RAM transactions, ARM/Thumb/DMA, and rollback |
|
||||
| `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 |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Audio device subset
|
||||
|
||||
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 remains unsupported.
|
||||
The core models Direct Sound A/B, bounded sound DMA, and programmable sound generator (PSG) channels 1–4.
|
||||
Wave channel 3 supports single-bank playback only. The core does not produce desktop audio.
|
||||
`Memory::audio_level()` exposes the current digital stereo level for deterministic inspection, not a continuous sample stream.
|
||||
|
||||
## Registers and PSG state
|
||||
|
|
@ -11,12 +11,12 @@ It does not produce desktop audio. PSG wave channel 3 remains unsupported.
|
|||
| `0x04000060..0x04000065` | Pulse channel 1 sweep, duty, length, envelope, frequency, and trigger |
|
||||
| `0x04000068..0x04000069` | Pulse channel 2 duty, length, and envelope |
|
||||
| `0x0400006c..0x0400006d` | Pulse channel 2 frequency and trigger; no sweep unit |
|
||||
| `0x04000070..0x04000075` | Idle wave channel 3 configuration |
|
||||
| `0x04000070..0x04000075` | Wave channel 3 gate, bank, length, volume, frequency, and trigger |
|
||||
| `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` |
|
||||
| `0x04000084` (`SOUNDCNT_X`) | Master enable in bit 7; channel 1/2/4 activity in read-only bits 0/1/3; channel 3 remains inactive |
|
||||
| `0x04000084` (`SOUNDCNT_X`) | Master enable in bit 7; channel 1–4 activity in read-only bits 0–3 |
|
||||
| `0x04000088` (`SOUNDBIAS`) | Bias/resolution configuration with mask `0xc3fe` |
|
||||
| `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 |
|
||||
|
|
@ -31,10 +31,9 @@ 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.
|
||||
This is a functional reset subset, not a claim of complete firmware ordering or sound-reset equivalence.
|
||||
|
||||
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.
|
||||
Unsupported two-bank wave playback and active wave-bank changes return `MemoryError::UnsupportedIo` with the exact address and value.
|
||||
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.
|
||||
Idle wave channel 3 writes do not imply playback support.
|
||||
|
||||
## Pulse channels 1 and 2
|
||||
|
||||
|
|
@ -122,6 +121,56 @@ An independent bit-array reference checks these jump-ahead operations, including
|
|||
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.
|
||||
|
||||
## Wave channel 3: single-bank playback
|
||||
|
||||
The selected bank supplies 32 four-bit samples. The CPU reads and writes the opposite bank, including during playback.
|
||||
SOUND3CNT_L retains gate, bank, and dimension bits with mask `0xe0`.
|
||||
The volume register retains bits 13–15. Frequency, length-load, and trigger fields are write-only.
|
||||
The unused halfword at `0x04000076` reads zero and ignores writes.
|
||||
|
||||
Each nominal sample period is `8 * (2048 - frequency)` system clocks.
|
||||
Playback consumes each byte's high nibble before its low nibble, then proceeds to the next byte.
|
||||
The selected bank rotates by one nibble per sample; it is not immutable storage addressed by a hidden pointer.
|
||||
Stopping playback and exposing that bank therefore shows rotated data.
|
||||
Complete 32-sample rotations restore the bank layout but still update the held output sample.
|
||||
Clock batches compute the final rotation and sample directly, without looping once per sample.
|
||||
|
||||
A trigger reloads a full sample period and clears the held output to nominal silence until the first edge.
|
||||
It cannot restore data already rotated in wave RAM. Retrigger uses the bank's current layout.
|
||||
Frequency writes affect the next reload, not the remaining sample interval.
|
||||
Clearing the gate stops activity. Setting the gate again requires a trigger to resume.
|
||||
A trigger with the gate clear is accepted but cannot activate playback, including with an idle two-bank configuration.
|
||||
Mute volume and disabled routes do not stop playback or clear activity.
|
||||
Master disable resets channel configuration/state but preserves both wave RAM banks in their current layouts.
|
||||
|
||||
The length counter loads `256 - length_field` and uses the shared 256 Hz length clocks.
|
||||
It follows the same nominal extra-clock/reload rule as other PSG channels, with maximum length 256 rather than 64.
|
||||
A nonempty counter survives retrigger. Enabled expiry clears SOUNDCNT_X bit 2.
|
||||
HALT continues sample and length clocks; STOP freezes them. No timer or IRQ enable is required.
|
||||
|
||||
The centered full-volume amplitude is `2 * (sample - 8)`, from -16 through +14.
|
||||
Volume selects mute, 100%, 50%, or 25%. Force-volume overrides all selections with 75%.
|
||||
The mixer retains quarter-units until after combining PSG sources, stereo volume, and the overall PSG ratio.
|
||||
This avoids separately rounding fractional wave amplitudes. It follows the signed NanoBoyAdvance mixer comparison, not an analog recording.
|
||||
|
||||
### Evidence limits
|
||||
|
||||
Only dimension zero playback is implemented. Idle dimension-one configuration is writable, but triggering it with the gate enabled is diagnostic.
|
||||
Selecting dimension one or changing the selected bank during active playback also remains diagnostic; clear the gate first.
|
||||
These failures retain register lanes, RAM rotation, activity, and clocks through ordinary audio transactions.
|
||||
|
||||
GBATEK describes rotating wave RAM and selected-bank-first two-bank playback.
|
||||
The pinned mGBA implementation rotates storage, while NanoBoyAdvance uses a sample pointer and changes bank at wrap.
|
||||
Their two-bank starts, observable bank state, and retrigger behavior differ.
|
||||
This increment does not choose an unverified two-bank model merely to accept more register writes.
|
||||
The single-bank order/rate follows GBATEK; trigger phase, held-sample startup, live frequency/volume edges, and length quirks remain nominal.
|
||||
There is no Game Boy wave-corruption model or physical GBA waveform validation.
|
||||
|
||||
Original tests cover every frequency and both single-bank selections, independent nibble rotation, complete rotations, maximum batches, and volume settings.
|
||||
Integration checks cover masks, gating, status, stereo mixing/clipping, length, HALT/STOP, master reset, and BIOS reset.
|
||||
ARM/Thumb/DMA tests verify trigger bus phases, status at length expiry, rotated RAM readback, and failed-step/block-store isolation.
|
||||
Original terminal and native-window programs activate channel 3 without external game assets or host audio.
|
||||
|
||||
## FIFO and timer behavior
|
||||
|
||||
Each channel has a seven-word queue, a separate four-byte playback word, and a held signed sample.
|
||||
|
|
@ -167,11 +216,12 @@ Sub-instruction DMA arbitration and exact refill latency are not modeled.
|
|||
|
||||
Each routed Direct Sound sample contributes `sample * 2` at 50% volume or `sample * 4` at 100% volume.
|
||||
Each active pulse or noise channel contributes a centered signed amplitude, `+volume` or `-volume`.
|
||||
`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.
|
||||
`SOUNDCNT_L` routes channel 1 through bits 8/12, channel 2 through bits 9/13, wave through bits 10/14, and noise through bits 11/15.
|
||||
Each pair selects right/left respectively.
|
||||
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.
|
||||
Rounding each PSG channel separately would produce different low-volume levels and is not used.
|
||||
At full volume, each supported PSG channel spans `-120..120` before mixing with Direct Sound.
|
||||
At full stereo volume, each pulse/noise channel spans `-120..120`; wave spans `-128..112`, before mixing with Direct Sound.
|
||||
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.
|
||||
`StereoLevel` therefore contains signed levels in `-512..511`. Master disable returns zero.
|
||||
|
|
@ -201,12 +251,16 @@ CPU/DMA tests include noise register writes at bus completion and failed-step ro
|
|||
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.
|
||||
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 the pulse and noise triggers, then stops at GPIO serial interrupt control.
|
||||
Remaining audio work: two-bank wave playback, verified live bank changes, a continuous output stream, PWM/mixer sampling, and a Darwin host backend.
|
||||
A local Emerald input probe exposed a wave trigger with the gate disabled. This is now accepted without activating playback.
|
||||
The same scheduled-input probe passes that point and completes its extended frame budget; audible output and gameplay remain unverified.
|
||||
See [the local runtime result](../research/emerald-reset.md).
|
||||
|
||||
## References
|
||||
|
||||
- [GBATEK wave channel 3](https://problemkaputt.de/gbatek-gba-sound-channel-3-wave-output.htm): bank selection, nibble order, rotating RAM, sample rate, length, and force volume.
|
||||
- [NanoBoyAdvance wave at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/channel/wave_channel.cc) and [header](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/channel/wave_channel.hh): signed gain and rate comparison; pointer-based RAM and two-bank/retrigger differences are not copied.
|
||||
|
||||
- [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.
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ Supported flags can be combined:
|
|||
| 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.
|
||||
This is not full audio or serial support. [wave channel 3](audio.md) and [serial transfers](serial.md) still fail explicitly.
|
||||
This is not full audio or serial support. [Two-bank wave playback](audio.md) and [connected serial transfers](serial.md) remain unsupported.
|
||||
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.
|
||||
|
||||
|
|
@ -544,6 +544,6 @@ 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.
|
||||
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.
|
||||
PSG wave channel 3, host audio output, connected serial links, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
|
||||
Two-bank wave playback, host audio output, connected serial links, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
|
||||
Disconnected normal serial transfers use the [bounded serial model](serial.md); cancel active transfers before BIOS serial reset.
|
||||
This subset is not sufficient for Pokémon Emerald compatibility.
|
||||
|
|
|
|||
|
|
@ -114,9 +114,9 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
|
|||
Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
|
||||
The pinned Pong debug/release reports match.
|
||||
|
||||
Next, inspect visible startup/menu progress and exercise button presses and releases in a bounded run.
|
||||
Identify any waiting loop or new diagnostic before selecting another compatibility correction.
|
||||
Do not treat frame-limit success as a title-screen or gameplay pass.
|
||||
A subsequent [scheduled-input probe](emerald-wave.md) exposed a wave-channel trigger with its gate disabled.
|
||||
The general single-bank wave model now passes that point and completes an extended Start/A input schedule.
|
||||
Visible menu semantics and gameplay still require verification; frame-limit success alone cannot establish either.
|
||||
Flash programming, erase, busy behavior, and safe persistence remain required before claiming save compatibility.
|
||||
RTC persistence and cartridge IRQ behavior remain separate missing features.
|
||||
Ordinary ROM writes remain read-only. Without `--rtc`, the earlier GPIO diagnostic is still expected.
|
||||
|
|
@ -125,7 +125,7 @@ This result does not establish gameplay, linked transfers, or cartridge/serial G
|
|||
Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels.
|
||||
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.
|
||||
Wave channel 3 now supports single-bank playback. Two-bank playback, 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.
|
||||
|
||||
|
|
|
|||
97
docs/research/emerald-wave.md
Normal file
97
docs/research/emerald-wave.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Local Emerald input probe: wave trigger
|
||||
|
||||
## Claim and method
|
||||
|
||||
The earlier 600-frame native run established execution without a diagnostic, not gameplay.
|
||||
This probe checks whether scheduled input changes captured output and exposes another unsupported operation.
|
||||
The game remains a local user-supplied file. No ROM bytes, assets, screenshots, or extracted instructions are committed or submitted to Jev.
|
||||
|
||||
A temporary host driver uses public core APIs:
|
||||
|
||||
1. Boot the local ROM with `bios::boot` and select `CartridgeHardware::Rtc` and `SaveDevice::Flash128`.
|
||||
2. Set deterministic RTC components to `[24, 1, 1, 1, 12, 0, 0]`. Do not advance this clock during the probe.
|
||||
3. Enable scanline rendering and call `Machine::run_until_vblank(400000)` once per requested frame.
|
||||
4. Apply each input snapshot after the named VBlank, before the next machine step.
|
||||
5. Require `present_frame` to return a complete image. Stop on any execution/render diagnostic.
|
||||
6. Compare captured pixels locally against a fresh no-input run with identical hardware/time configuration.
|
||||
|
||||
The initial budget is 900 VBlanks. Each frame has its own machine-step bound; this is not the CLI's total step budget.
|
||||
A follow-up extends the limit to 1,320 VBlanks. The temporary driver rejects requests above 1,500 VBlanks.
|
||||
These are local diagnostic runs, not committed ROM-suite assertions or a commercial compatibility test.
|
||||
The current manifest versions cannot select this cartridge hardware, so `--test-suite` cannot reproduce this setup directly.
|
||||
|
||||
| VBlank | Buttons held after the event |
|
||||
| --- | --- |
|
||||
| 0 | None |
|
||||
| 600 | Start |
|
||||
| 610 | None |
|
||||
| 720 | Start |
|
||||
| 730 | None |
|
||||
| 840 | Start |
|
||||
| 850 | None |
|
||||
| 960 | A, extended run only |
|
||||
| 970 | None |
|
||||
| 1080 | A, extended run only |
|
||||
| 1090 | None |
|
||||
| 1200 | A, extended run only |
|
||||
| 1210 | None |
|
||||
|
||||
## Observed failure
|
||||
|
||||
Before wave support, the first Start press changed captured output.
|
||||
Execution then stopped before frame 638:
|
||||
|
||||
```text
|
||||
PC=0x082e104c
|
||||
successful steps through frame 637: 62597533
|
||||
unsupported PSG channel trigger (synthesis not implemented): write 0x86 at 0x04000075
|
||||
SOUND3CNT_L readback: 0
|
||||
SOUND3CNT_H readback: 0
|
||||
```
|
||||
|
||||
The wave gate was disabled. A trigger in this state must not activate channel 3.
|
||||
Rejecting every trigger was an overly strict feature limit, not evidence that the game needed audible wave output at that moment.
|
||||
The correction implements general [single-bank wave behavior](../hardware/audio.md#wave-channel-3-single-bank-playback), including the inactive trigger case.
|
||||
It does not patch this game's instruction or skip its write.
|
||||
|
||||
## Result after the correction
|
||||
|
||||
The unchanged initial schedule reaches 900 frames and 89,199,123 successful machine steps without a diagnostic.
|
||||
The extended schedule reaches 1,320 frames and 128,588,887 successful machine steps, ending at PC `0x080008ca`.
|
||||
The no-input control also reaches 900 frames without a diagnostic.
|
||||
|
||||
Local pixel comparisons against that control show:
|
||||
|
||||
| Captured frame | Different pixels out of 38,400 |
|
||||
| --- | --- |
|
||||
| 600, before input | 0 |
|
||||
| 620 | 38,400 |
|
||||
| 660 | 38,232 |
|
||||
| 900 | 38,400 |
|
||||
|
||||
This establishes input-dependent execution and different captured output under fixed device configuration.
|
||||
It does not establish readable menus, correct scene rendering, release behavior inside the game, or gameplay.
|
||||
Local optical character recognition did not identify reliable menu text and supplied no semantic acceptance criterion.
|
||||
No visual classifier or Jev call was used. No title-screen claim follows from color counts or pixel differences.
|
||||
The file's cartridge revision was not inferred from its filename.
|
||||
|
||||
Original regressions separately test active wave synthesis, nibble rotation, lengths, gains, gating, bus phases, and rollback.
|
||||
Workspace debug/release tests, public ARM/Thumb/memory/BIOS suites, pinned Pong scenarios, and native Darwin checks pass.
|
||||
The pinned Pong debug/release reports match. Native ROM-window tests include an original wave-activation program.
|
||||
A separate 600-frame Emerald native run still reaches its frame limit without input or a diagnostic.
|
||||
|
||||
## Remaining requirements
|
||||
|
||||
- Verify visible menu text and gameplay through direct window testing or explicit independently justified assertions.
|
||||
- Implement continuous sample production and a Darwin output backend before claiming audible sound.
|
||||
- Research two-bank wave playback and live bank changes separately. Both remain diagnostic during active playback.
|
||||
- Implement Flash programming/erase and safe persistence before claiming saving.
|
||||
|
||||
For a manual native check, use:
|
||||
|
||||
```sh
|
||||
direnv exec . cargo run --locked --release -- --rom roms/pokemon-emerald.gba --rtc --save-type flash128 --window
|
||||
```
|
||||
|
||||
Enter maps to Start. Z maps to A. Escape exits. This command does not supply the deterministic probe schedule or fixed RTC.
|
||||
Hardware sources and implementation comparisons are recorded in [audio references](../hardware/audio.md#references).
|
||||
|
|
@ -89,7 +89,8 @@ Update this file when a feature lands or a limit is removed.
|
|||
- 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.
|
||||
- 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.
|
||||
- Wave channel 3 supports single-bank nibble playback, rotating RAM, volume/force-volume, length, activity status, and digital mixing.
|
||||
- Two-bank wave playback, active bank changes, and unsupported serial operations remain diagnostic 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.
|
||||
- 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.
|
||||
|
|
@ -151,8 +152,8 @@ Flash programming, erase, busy polling, wider accesses, unverified aliases, and
|
|||
The optional [cartridge GPIO/RTC subset](hardware/cartridge-gpio.md) supports calendar/control transfers and explicit clock updates, selected with `--rtc` in ROM modes.
|
||||
RTC persistence, interrupts, invalid-date correction, physical subsecond timing, GPIO aliases, and byte writes remain unsupported or unverified.
|
||||
The core uses deterministic caller time. Desktop RTC runs use host UTC/elapsed time and are not fully deterministic.
|
||||
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. Wave channel 3 playback and unverified sound-DMA pairings remain diagnostic.
|
||||
The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, single-bank wave channel 3, noise channel 4, and instantaneous digital mixing.
|
||||
It does not yet provide a continuous sample stream. Two-bank wave playback, active wave-bank changes, and unverified sound-DMA pairings remain diagnostic.
|
||||
PSG timing and signed mixing are nominal; hardware startup edges and analog behavior remain unverified.
|
||||
The [serial subset](hardware/serial.md) supports disconnected normal 8/32-bit transfers, external-clock waiting, idle multiplayer configuration, and general-purpose pins.
|
||||
Internal clocks shift high input at nominal rates, clear busy, and request completion IRQs. External requests retain busy without clock edges.
|
||||
|
|
@ -177,7 +178,10 @@ It now also completes internally clocked normal transfers and selects idle multi
|
|||
With explicit `--rtc` selection, it also passes Game Pak GPIO configuration and RTC calendar reads.
|
||||
With `--save-type flash128`, it also passes Flash identification and reaches a later startup check.
|
||||
It now passes disconnected Joybus setup and completes a bounded 600-frame native run without a diagnostic error.
|
||||
This run uses no scripted input or pixel assertions; menu progress and gameplay remain unverified.
|
||||
That run used no scripted input or pixel assertions.
|
||||
A later [fixed-time input probe](research/emerald-wave.md) found a wave trigger with its gate disabled.
|
||||
Wave support now passes that point and reaches 1,320 frames with scheduled Start/A presses and releases.
|
||||
Output differs from a no-input control, but readable menus and gameplay remain unverified.
|
||||
Flash writing and persistence remain unimplemented.
|
||||
Without `--rtc`, cartridge GPIO writes still return read-only diagnostics.
|
||||
No title-screen, gameplay, or audible-game compatibility is established.
|
||||
|
|
|
|||
|
|
@ -79,7 +79,11 @@ ARM/Thumb probes verify RAM byte reads, ROM command writes, all save wait settin
|
|||
Wide accesses, DMA, instruction fetches, and CPU reads outside work RAM retain explicit diagnostics.
|
||||
An original desktop ROM installs its Flash byte reader in IWRAM; terminal/native tests check explicit selection and unchanged files.
|
||||
They verify explicit activation errors and atomic byte/word/block-store/DMA failure behavior.
|
||||
The optional [local Emerald startup check](research/emerald-reset.md) now reaches sound activation but still fails deliberately there.
|
||||
Wave tests cover every frequency and bank, independent nibble rotation, full rotations, bounded maximum batches, and fractional gains.
|
||||
They also check inactive triggers, gates, length, status, master/BIOS reset, HALT/STOP, live-write limits, and CPU/DMA rollback.
|
||||
Original terminal/native programs activate wave playback without external assets or host audio.
|
||||
The optional [local Emerald input probe](research/emerald-wave.md) now completes its scheduled Start/A frame budget.
|
||||
Frame completion and different pixels are not assertions about readable menus, correct graphics, or gameplay.
|
||||
[Jev evidence review](jev-debugging.md) is optional and never changes deterministic test verdicts.
|
||||
Its Python tests mock the provider and verify schemas, response validation, credential handling, non-overwriting logs, and failure behavior.
|
||||
Live classifier smoke probes are separate from offline tests and must not become mandatory CI checks.
|
||||
|
|
|
|||
|
|
@ -44,9 +44,12 @@ It also passes normal serial transfers and idle multiplayer configuration.
|
|||
With `--rtc --save-type flash128`, it passes Game Pak GPIO setup, RTC reads, and Flash identification.
|
||||
It also passes disconnected Joybus configuration and completes a bounded 600-frame native run without a diagnostic error.
|
||||
That run does not verify screen contents, button responses, or gameplay.
|
||||
The core has tested Direct Sound, both pulse channels, and noise channel 4.
|
||||
Wave channel 3, a continuous sample stream, and desktop audio output remain missing.
|
||||
See [the runtime result](research/emerald-reset.md). Next, verify visible menu progress and button responses rather than treating frame counts as playability.
|
||||
A [fixed-time input probe](research/emerald-wave.md) then exposed an unsupported wave trigger with its gate disabled.
|
||||
Single-bank wave support now passes that point and completes 1,320 frames with scheduled Start/A input.
|
||||
Captured output differs from a no-input control; readable menus and gameplay remain unverified.
|
||||
The core has tested Direct Sound, both pulse channels, single-bank wave channel 3, and noise channel 4.
|
||||
Two-bank wave playback, a continuous sample stream, and desktop audio output remain missing.
|
||||
See [the runtime result](research/emerald-reset.md). Next, verify visible menu progress rather than treating frame counts as playability.
|
||||
The separate save requirement includes Flash programming/erase and safe host persistence. Identification/read support is not successful saving.
|
||||
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