feat(audio): implement PSG pulse channel 2

This commit is contained in:
0xrsydn 2026-10-07 02:30:16 +07:00
commit aee3621513
15 changed files with 457 additions and 89 deletions

View file

@ -1,4 +1,4 @@
//! Nominal Direct Sound and PSG pulse channel 1. No host audio output.
//! Nominal Direct Sound and PSG pulse channels 1–2. No host audio output.
mod pulse;
use pulse::Pulse;
@ -78,7 +78,7 @@ impl Fifo {
pub(crate) struct Audio {
enabled: bool,
psg: [u16; 17],
pulse: Pulse,
pulses: [Pulse; 2],
sequencer_phase: u16,
next_step: u8,
control: u16,
@ -99,10 +99,7 @@ impl Audio {
{
return Some("reserved PSG volume selection");
}
if self.enabled
&& matches!(address, 0x0400_006d | 0x0400_0075 | 0x0400_007d)
&& value & 0x80 != 0
{
if self.enabled && matches!(address, 0x0400_0075 | 0x0400_007d) && value & 0x80 != 0 {
Some("PSG channel trigger (synthesis not implemented)")
} else {
None
@ -113,9 +110,20 @@ impl Audio {
1 ^ usize::from((self.psg[8] >> 6) & 1)
}
/// Channel 2 has no sweep register. Its register gap must not alias any
/// pulse state, particularly the shared engine's sweep configuration.
fn pulse_register(address: u32) -> Option<(usize, u32)> {
match address {
SOUND_START..=0x0400_0065 => Some((0, address - SOUND_START)),
0x0400_0068..=0x0400_0069 => Some((1, address - 0x0400_0066)),
0x0400_006c..=0x0400_006d => Some((1, address - 0x0400_0068)),
_ => None,
}
}
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
if (SOUND_START..=SOUND_START + 5).contains(&address) {
return Some(self.pulse.read(address - SOUND_START));
if let Some((index, offset)) = Self::pulse_register(address) {
return Some(self.pulses[index].read(offset));
}
if (WAVE_RAM..FIFO_A).contains(&address) {
return Some(self.wave[self.wave_bank()][(address - WAVE_RAM) as usize]);
@ -123,9 +131,7 @@ impl Audio {
let value = match address & !1 {
SOUND_START..=0x0400_0080 => {
let mask = match address & !1 {
0x0400_0060 => 0x007f,
0x0400_0062 | 0x0400_0068 => 0xffc0,
0x0400_0064 | 0x0400_006c | 0x0400_0074 => 0x4000,
0x0400_0074 => 0x4000,
0x0400_0070 => 0x00e0,
0x0400_0072 => 0xe000,
0x0400_0078 => 0xff00,
@ -136,7 +142,11 @@ impl Audio {
self.psg[((address - SOUND_START) / 2) as usize] & mask
}
SOUNDCNT_H => self.control,
SOUNDCNT_X => (u16::from(self.enabled) << 7) | u16::from(self.pulse.active),
SOUNDCNT_X => {
(u16::from(self.enabled) << 7)
| u16::from(self.pulses[0].active)
| (u16::from(self.pulses[1].active) << 1)
}
SOUNDBIAS => self.bias,
0x0400_0086 | 0x0400_008a => 0,
_ => return None, // FIFO is write-only; open-bus readback is not modeled.
@ -156,9 +166,10 @@ impl Audio {
self.wave[self.wave_bank()][(address - WAVE_RAM) as usize] = value;
continue;
}
if self.enabled && (SOUND_START..=SOUND_START + 5).contains(&address) {
self.pulse
.write(address - SOUND_START, value, self.next_step);
if let Some((index, offset)) = Self::pulse_register(address) {
if self.enabled {
self.pulses[index].write(offset, value, self.next_step);
}
continue;
}
let shift = (address & 1) * 8;
@ -181,7 +192,7 @@ impl Audio {
SOUNDCNT_X if address & 1 == 0 => {
if self.enabled && value & 0x80 == 0 {
self.psg = [0; 17];
self.pulse = Pulse::default();
self.pulses = [Pulse::default(); 2];
for fifo in &mut self.fifo {
fifo.reset_queue();
}
@ -207,11 +218,15 @@ impl Audio {
}
while cycles != 0 {
let count = cycles.min(SEQUENCER_PERIOD - u32::from(self.sequencer_phase));
self.pulse.advance(count);
for pulse in &mut self.pulses {
pulse.advance(count);
}
let phase = u32::from(self.sequencer_phase) + count;
if phase == SEQUENCER_PERIOD {
self.sequencer_phase = 0;
self.pulse.clock(self.next_step);
for pulse in &mut self.pulses {
pulse.clock(self.next_step);
}
self.next_step = (self.next_step + 1) & 7;
} else {
self.sequencer_phase = phase as u16;
@ -256,10 +271,15 @@ impl Audio {
let psg_control = self.psg[16];
let ratio = [1, 2, 4, 0][usize::from(self.control & 3)];
for (side, level) in sides.iter_mut().enumerate() {
if psg_control & (1 << (8 + side * 4)) != 0 {
let volume = ((psg_control >> (side * 4)) & 7) as i16 + 1;
*level += (self.pulse.sample() * volume * ratio) >> 2;
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();
}
}
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 = (*level + (self.bias & 0x3ff) as i16).clamp(0, 1023) - 512;
}
StereoLevel {

View file

@ -1,4 +1,5 @@
//! Nominal channel 1 oscillator and modulation units. No host-clock dependency.
//! Shared nominal pulse oscillator and modulation units. No host-clock dependency.
//! Channel 2 never receives sweep-register writes, so its sweep remains disabled.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(super) struct Pulse {

View file

@ -10,7 +10,9 @@ fn sound(memory: &mut Memory, period: u16) {
memory.write16(SOUNDBIAS, 0x200).unwrap();
memory.write16(SOUNDCNT_H, 0x0304).unwrap();
memory.write16(0x04000062, 0xf080).unwrap();
memory.write16(0x04000064, 0x87ff).unwrap(); // Running pulse, unrouted; rollback must include it.
memory.write16(0x04000064, 0x87ff).unwrap();
memory.write16(0x04000068, 0xf080).unwrap();
memory.write16(0x0400006c, 0x87ff).unwrap(); // Both pulses run unrouted; rollback must include them.
memory
.write32(
TIMER_BASE,
@ -21,26 +23,57 @@ fn sound(memory: &mut Memory, period: u16) {
#[test]
fn pulse_status_load_observes_length_expiry_at_bus_completion() {
let (cpu, mut memory) = prepared(false, &[0xe5910000], &[(1, SOUNDCNT_X)]);
sound(&mut memory, 1000);
memory.write16(0x04000062, 0xf03f).unwrap();
memory.write16(0x04000064, 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().read16(SOUNDCNT_X).unwrap(), 0x80);
for (index, duty, frequency) in [(0, 0x04000062, 0x04000064), (1, 0x04000068, 0x0400006c)] {
let (cpu, mut memory) = prepared(false, &[0xe5910000], &[(1, SOUNDCNT_X)]);
sound(&mut memory, 1000);
memory.write16(duty, 0xf03f).unwrap();
memory.write16(frequency, 0xc7ff).unwrap();
memory.advance_cycles(32768 - 7);
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
let status = 0x83 ^ (1 << index); // The other channel remains active.
assert_eq!(machine.cpu().registers()[0], status);
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), status as u16);
}
}
#[test]
fn pulse_trigger_reloads_timer_at_store_completion() {
let (cpu, mut memory) = prepared(false, &[0xe5810000], &[(0, 0x87ff), (1, 0x04000064)]);
for (index, frequency) in [0x04000064, 0x0400006c].into_iter().enumerate() {
let (cpu, mut memory) = prepared(false, &[0xe5810000], &[(0, 0x87ff), (1, frequency)]);
sound(&mut memory, 1000);
memory
.write16(0x04000080, 0x77 | (0x1100 << index))
.unwrap();
memory.write16(SOUNDCNT_H, 2).unwrap();
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(machine.cycles(), 7);
machine.memory_mut().advance_cycles(15);
assert_eq!(machine.memory().audio_level().left, 120);
machine.memory_mut().advance_cycles(1);
assert_eq!(machine.memory().audio_level().left, -120);
}
}
#[test]
fn successful_dma_trigger_starts_channel2_after_destination_bus_cycles() {
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
sound(&mut memory, 1000);
memory.write16(0x04000080, 0x1177).unwrap();
memory.write16(0x04000080, 0x2277).unwrap();
memory.write16(SOUNDCNT_H, 2).unwrap();
memory.write8(0x04000069, 0).unwrap();
memory.write8(0x04000069, 0xf0).unwrap(); // DAC gate restored, still needs a trigger.
memory.write32(0x02000000, 0x87ff).unwrap();
memory.write32(DMA_BASE, 0x02000000).unwrap();
memory.write32(DMA_BASE + 4, 0x0400006c).unwrap();
memory.write32(DMA_BASE + 8, 0x84000001).unwrap();
let mut machine = Machine::new(cpu, memory);
machine.step().unwrap();
assert_eq!(machine.cycles(), 7);
assert_eq!(
machine.step().unwrap(),
crate::machine::StepKind::Dma { channel: 0 }
);
assert_eq!(machine.memory().read16(SOUNDCNT_X).unwrap(), 0x83);
machine.memory_mut().advance_cycles(15);
assert_eq!(machine.memory().audio_level().left, 120);
machine.memory_mut().advance_cycles(1);
@ -95,7 +128,7 @@ fn failed_cpu_load_rolls_back_timer_audio_and_pending_refill_requests() {
#[test]
fn failed_dma_source_and_destination_restore_audio_after_speculative_timer_edges() {
for (source, destination, word) in [(FIFO_A, 0x02001000, 0), (0x02000000, 0x0400006c, 0x8000)] {
for (source, destination, word) in [(FIFO_A, 0x02001000, 0), (0x02000000, 0x04000074, 0x8000)] {
let (cpu, mut memory) = prepared(false, &[0xe1a00000], &[]);
sound(&mut memory, 1);
memory.write32(FIFO_A, 0x04030201).unwrap();
@ -113,6 +146,35 @@ fn failed_dma_source_and_destination_restore_audio_after_speculative_timer_edges
}
}
#[test]
fn arm_block_store_rejects_later_wave_trigger_before_committing_channel2_trigger() {
let (cpu, mut memory) = prepared(
false,
&[0xe8a0001e],
&[
(0, 0x04000068),
(1, 0xf080),
(2, 0x87ff),
(3, 0),
(4, 0x8000),
],
); // STMIA r0!,{r1-r4}: channel2 configuration/trigger, wave configuration/trigger.
memory.write16(SOUNDCNT_X, 0x80).unwrap();
let audio = memory.io.audio;
let mut machine = Machine::new(cpu, memory);
let cpu = machine.cpu().clone();
for _ in 0..2 {
assert!(machine
.step()
.unwrap_err()
.to_string()
.contains("PSG channel trigger"));
assert_eq!(machine.memory().io.audio, audio);
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 0);
}
}
#[test]
fn batch_validation_simulates_sound_enable_and_disable_before_committing_any_write() {
let mut memory = Memory::new(vec![]).unwrap();
@ -121,7 +183,9 @@ fn batch_validation_simulates_sound_enable_and_disable_before_committing_any_wri
.write_words(&[
(0x02000000, 0x11223344),
(SOUNDCNT_X, 0x80),
(0x0400006c, 0x8000),
(0x04000068, 0xf080),
(0x0400006c, 0x87ff),
(0x04000074, 0x8000),
])
.is_err());
assert_eq!(memory.read32(0x02000000).unwrap(), 0);

View file

@ -20,7 +20,7 @@ const REGIONS: [(u32, u32, u32); 5] = [
const MARKER: u32 = 0xa55a_c33c;
#[test]
fn sound_reset_disables_running_pulse_only_when_selected() {
fn sound_reset_disables_running_pulses_only_when_selected() {
use gba_core::io::SOUNDCNT_X;
for thumb in [false, true] {
for flags in [0, 0x40] {
@ -29,10 +29,12 @@ fn sound_reset_disables_running_pulse_only_when_selected() {
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write16(0x04000062, 0xf080).unwrap();
bus.write16(0x04000064, 0x87ff).unwrap();
bus.write16(0x04000068, 0xf080).unwrap();
bus.write16(0x0400006c, 0x87fe).unwrap();
complete(&mut machine, return_pc);
assert_eq!(
machine.memory().read16(SOUNDCNT_X).unwrap(),
if flags == 0 { 0x81 } else { 0 }
if flags == 0 { 0x83 } else { 0 }
);
}
}

View file

@ -167,7 +167,7 @@ fn wave_banks_and_idle_channel_masks_preserve_explicit_limits() {
bus.write16(0x04000070, 0x40).unwrap();
assert_eq!(bus.read32(WAVE_RAM).unwrap(), 0);
bus.write32(WAVE_RAM, 0xaabbccdd).unwrap();
for address in [0x0400006c, 0x04000074, 0x0400007c] {
for address in [0x04000074, 0x0400007c] {
let previous = bus.read16(address).unwrap();
assert!(bus
.write16(address, 0xffff)

View file

@ -104,7 +104,7 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
#[test]
fn block_store_rejects_later_activation_before_earlier_control_writes() {
let code: [u32; 7] = [
0xe3a00301, 0xe2800068, 0xe59f1008, 0xe3a02902, 0xe8a00006, 0xeafffffe, 0x12345678,
0xe3a00301, 0xe2800070, 0xe59f1008, 0xe3a02902, 0xe8a00006, 0xeafffffe, 0x12345678,
];
let mut bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
bus.write16(SOUNDCNT_X, 0x80).unwrap();
@ -118,7 +118,7 @@ fn block_store_rejects_later_activation_before_earlier_control_writes() {
assert!(error.to_string().contains("PSG channel trigger"));
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.memory().read16(0x04000068).unwrap(), 0);
assert_eq!(machine.memory().read16(0x04000070).unwrap(), 0);
assert_eq!(machine.step(), Err(error));
}
@ -128,7 +128,7 @@ fn dma_activation_error_preserves_channel_progress_and_clocks() {
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write32(0x02000000, 0x8000).unwrap();
bus.write32(DMA_BASE, 0x02000000).unwrap();
bus.write32(DMA_BASE + 4, 0x0400006c).unwrap();
bus.write32(DMA_BASE + 4, 0x04000074).unwrap();
bus.write32(DMA_BASE + 8, 0x84000001).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
let before = machine.cpu().clone();

View file

@ -1,4 +1,6 @@
//! Original public-bus tests for PSG channel 1; no game code or audio assets.
//! Original public-bus tests for PSG pulse channels; no game code or audio assets.
#[path = "pulse_sound/channel2.rs"]
mod channel2;
use gba_core::{
audio::StereoLevel,
cpu::Cpu,

View file

@ -0,0 +1,263 @@
//! Original channel 2 mapping, isolation, and shared-mixer regressions.
use super::*;
const DUTY2: u32 = 0x04000068;
const FREQUENCY2: u32 = 0x0400006c;
fn channel2() -> Memory {
let mut bus = bus();
bus.write16(MIX, 0x2277).unwrap();
bus
}
fn start2(bus: &mut Memory) {
bus.write16(DUTY2, 0xf080).unwrap();
bus.write16(FREQUENCY2, 0x87ff).unwrap();
}
#[test]
fn channel2_register_widths_masks_and_status_do_not_alias_channel1() {
let mut bus = channel2();
bus.write32(DUTY2, 0xfffff0bf).unwrap(); // High halfword is unused, not frequency.
assert_eq!(bus.read32(DUTY2).unwrap(), 0xf080);
assert_eq!(bus.read16(DUTY).unwrap(), 0);
bus.write8(FREQUENCY2, 255).unwrap();
bus.write8(FREQUENCY2 + 1, 0x87).unwrap();
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
assert_eq!(bus.audio_level(), stereo(120));
bus.advance_cycles(15);
assert_eq!(bus.audio_level(), stereo(120));
bus.advance_cycles(1);
assert_eq!(bus.audio_level(), stereo(-120));
bus.write32(SOUNDCNT_X, 0x8f).unwrap(); // Both status bits are read-only.
assert_eq!(bus.read32(SOUNDCNT_X).unwrap(), 0x82);
bus.write32(FREQUENCY2, 0xffffc7ff).unwrap();
assert_eq!(bus.read32(FREQUENCY2).unwrap(), 0x4000);
assert_eq!(bus.read16(FREQUENCY).unwrap(), 0);
assert_eq!(bus.read16(SWEEP).unwrap(), 0);
}
#[test]
fn channel1_sweep_and_unused_register_gaps_cannot_enable_channel2_sweep() {
let mut dirty = channel2();
let mut clean = channel2();
for bus in [&mut dirty, &mut clean] {
start2(bus); // Frequency2047: any positive sweep would overflow.
bus.write16(SWEEP, 0x11).unwrap();
bus.write16(DUTY, 0xf080).unwrap();
bus.write16(FREQUENCY, 0x85dc).unwrap(); // Channel1 frequency1500 overflows on trigger.
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
}
for address in [0x04000066, 0x0400006a, 0x0400006e] {
dirty.write16(address, 0xffff).unwrap();
assert_eq!(dirty.read16(address).unwrap(), 0);
}
for cycles in [15, 1, 31, SEQUENCE * 4, 1000000] {
dirty.advance_cycles(cycles);
clean.advance_cycles(cycles);
assert_eq!(dirty.audio_level(), clean.audio_level());
assert_eq!(dirty.read16(SOUNDCNT_X).unwrap(), 0x82);
}
}
#[test]
fn pulse_channels_keep_independent_frequency_phase_retrigger_and_dac_state() {
let mut bus = bus();
bus.write16(MIX, 0x2177).unwrap(); // Channel1 right, channel2 left.
start(&mut bus);
bus.write16(DUTY2, 0x7080).unwrap();
bus.write16(FREQUENCY2, 0x87fe).unwrap(); // 32 clocks per edge rather than16.
assert_eq!(
bus.audio_level(),
StereoLevel {
left: 56,
right: 120
}
);
bus.advance_cycles(16);
assert_eq!(
bus.audio_level(),
StereoLevel {
left: 56,
right: -120
}
);
bus.write16(FREQUENCY, 0x87ff).unwrap(); // Must not restart channel2's timer.
bus.advance_cycles(16);
assert_eq!(
bus.audio_level(),
StereoLevel {
left: -56,
right: -120
}
);
bus.write8(DUTY + 1, 0).unwrap();
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
assert_eq!(
bus.audio_level(),
StereoLevel {
left: -56,
right: 0
}
);
bus.write8(DUTY2 + 1, 0).unwrap();
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
assert_eq!(bus.audio_level(), stereo(0));
}
#[test]
fn triggers_share_sequencer_phase_but_envelopes_remain_independent() {
let mut bus = bus();
bus.write16(MIX, 0x2177).unwrap();
bus.write16(DUTY, 0xc180).unwrap(); // Volume12, falling every envelope tick.
bus.write16(FREQUENCY, 0x87ff).unwrap();
bus.advance_cycles(SEQUENCE * 6);
bus.write16(DUTY2, 0x3980).unwrap(); // Volume3, rising every envelope tick.
bus.write16(FREQUENCY2, 0x87ff).unwrap();
bus.advance_cycles(SEQUENCE * 2); // Shared step7, not eight new steps after trigger2.
assert_eq!(
bus.audio_level(),
StereoLevel {
left: 32,
right: 88
}
);
bus.advance_cycles(SEQUENCE * 8);
assert_eq!(
bus.audio_level(),
StereoLevel {
left: 40,
right: 80
}
);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x83);
bus.write8(DUTY2 + 1, 0).unwrap();
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x81);
assert_eq!(bus.audio_level(), StereoLevel { left: 0, right: 80 });
}
#[test]
fn separate_length_counters_and_channel2_empty_reload_observe_shared_phase() {
let mut bus = bus();
bus.write16(MIX, 0).unwrap(); // Muting does not pause length.
bus.write16(DUTY, 0xf0bf).unwrap();
bus.write16(FREQUENCY, 0xc7ff).unwrap();
bus.write16(DUTY2, 0xf0be).unwrap();
bus.write16(FREQUENCY2, 0xc7ff).unwrap();
bus.advance_cycles(SEQUENCE);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
bus.advance_cycles(SEQUENCE * 2);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
bus.write16(FREQUENCY2, 0xc7ff).unwrap(); // Next step3: reload length63, not64.
bus.advance_cycles(SEQUENCE * 125);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
bus.advance_cycles(SEQUENCE);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
}
#[test]
fn mixer_sums_pulses_before_rounding_and_clips_only_after_direct_sound() {
let mut bus = bus();
bus.write16(MIX, 0x3300).unwrap(); // Both channels, both sides, master gain1.
bus.write16(DUTY, 0x2080).unwrap(); // +2 at duty position0.
bus.write16(FREQUENCY, 0x8000).unwrap();
bus.write16(DUTY2, 0x1000).unwrap(); // -1 at duty position0.
bus.write16(FREQUENCY2, 0x8000).unwrap();
bus.write16(SOUNDCNT_H, 0).unwrap();
assert_eq!(bus.audio_level(), stereo(0)); // (2 - 1) >>2, not (2 >>2) + (-1 >>2).
bus.write16(MIX, 0x2200).unwrap();
assert_eq!(bus.audio_level(), stereo(-1));
bus.write16(MIX, 0x3377).unwrap();
bus.write16(SOUNDCNT_H, 0x0306).unwrap();
start(&mut bus);
start2(&mut bus);
assert_eq!(bus.audio_level(), stereo(240));
bus.write32(FIFO_A, 0x7f7f7f7f).unwrap();
bus.write32(TIMER_BASE, 0x0080ffff).unwrap();
bus.advance_cycles(1);
assert_eq!(bus.audio_level(), stereo(511));
bus.write16(MIX, 0).unwrap();
assert_eq!(bus.audio_level(), stereo(508));
}
#[test]
fn disabled_writes_and_channel2_dac_gate_cannot_start_playback() {
let mut bus = channel2();
bus.write16(SOUNDCNT_X, 0).unwrap();
bus.write32(DUTY2, u32::MAX).unwrap();
bus.write32(FREQUENCY2, u32::MAX).unwrap();
assert_eq!(bus.read32(DUTY2).unwrap(), 0);
assert_eq!(bus.read32(FREQUENCY2).unwrap(), 0);
bus.write16(SOUNDCNT_X, 0x80).unwrap();
bus.write16(DUTY2, 0x0780).unwrap();
bus.write16(FREQUENCY2, 0x87ff).unwrap();
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80);
bus.write16(DUTY2, 0x0880).unwrap(); // Rising direction enables the logical DAC gate.
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x80); // Still needs trigger.
bus.write16(FREQUENCY2, 0x87ff).unwrap();
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
bus.advance_cycles(SEQUENCE * 64); // Zero initial volume and period: active but silent.
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
start(&mut bus);
start2(&mut bus);
bus.write16(SOUNDCNT_X, 0).unwrap();
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0);
assert_eq!(bus.read16(DUTY).unwrap(), 0);
assert_eq!(bus.read16(DUTY2).unwrap(), 0);
assert_eq!(bus.audio_level(), stereo(0));
}
#[test]
fn channel2_runs_during_halt_and_freezes_during_stop() {
for stop in [false, true] {
let mut bus = channel2();
start2(&mut bus);
bus.write16(FREQUENCY2, 0x87fd).unwrap(); // 48 clocks per edge.
bus.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
machine.step().unwrap();
assert_eq!(
machine.memory().audio_level(),
stereo(if stop { 120 } else { -120 })
);
if stop {
machine.memory_mut().advance_cycles(u32::MAX);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().audio_level(), stereo(120));
}
}
}
#[test]
fn both_channels_keep_phase_across_large_batches_and_row_capture() {
let mut bulk = bus();
let mut rows = bus();
let mut split = bus();
rows.set_scanline_rendering(true);
for bus in [&mut bulk, &mut rows, &mut split] {
bus.write16(MIX, 0x2177).unwrap();
bus.write16(SWEEP, 0x29).unwrap();
bus.write16(DUTY, 0xa340).unwrap();
bus.write16(FREQUENCY, 0x85ad).unwrap();
bus.write16(DUTY2, 0x29c0).unwrap();
bus.write16(FREQUENCY2, 0x87ab).unwrap();
}
for cycles in [1, 15, 32751, 262145, 77777] {
bulk.advance_cycles(cycles);
rows.advance_cycles(cycles);
for _ in 0..cycles {
split.advance_cycles(1);
}
assert_eq!(bulk.audio_level(), split.audio_level());
assert_eq!(bulk.audio_level(), rows.audio_level());
assert_eq!(
bulk.read16(SOUNDCNT_X).unwrap(),
split.read16(SOUNDCNT_X).unwrap()
);
}
let mut bus = channel2();
start2(&mut bus);
bus.advance_cycles(u32::MAX);
assert_eq!(bus.read16(SOUNDCNT_X).unwrap(), 0x82);
assert_eq!(bus.audio_level(), stereo(120));
}