feat(audio): implement nominal Direct Sound playback and FIFO DMA

This commit is contained in:
0xrsydn 2026-10-07 01:35:55 +07:00
commit 5888c4d292
21 changed files with 933 additions and 129 deletions

View file

@ -1,49 +1,112 @@
# Sound initialization subset
# Direct Sound subset
Sound synthesis and host audio output are not implemented.
The core now models disabled sound-register state so firmware can perform a functional sound reset.
This is not a silent-audio compatibility mode. Enabling sound returns an explicit diagnostic.
The core models timer-driven Direct Sound A/B playback and a bounded sound-DMA configuration.
It does not produce desktop audio. Programmable sound generator (PSG) synthesis remains unsupported.
`Memory::audio_level()` exposes the current digital stereo level for deterministic inspection, not a continuous sample stream.
## Supported state
## Registers and inactive PSG state
| Address | Behavior |
| --- | --- |
| `0x04000060..0x04000081` | Programmable sound generator (PSG) registers remain zero; writes have no effect while master sound is disabled |
| `0x04000082` (`SOUNDCNT_H`) | Retains mixing/routing/timer configuration with readback mask `0x770f` |
| `0x04000084` (`SOUNDCNT_X`) | Reads zero; status flags are read-only; a write setting bit 7 fails |
| `0x04000088` (`SOUNDBIAS`) | Retains bias/resolution configuration with mask `0xc3fe`; no analog output is generated |
| `0x04000090..0x0400009f` | Read/write wave RAM in the CPU-accessible inactive bank |
| `0x04000060..0x04000081` | Idle PSG configuration and documented read masks; writes are ignored while master sound is disabled |
| `0x04000082` (`SOUNDCNT_H`) | Mixing, routing, timer selection, and FIFO reset strobes; readback mask `0x770f` |
| `0x04000084` (`SOUNDCNT_X`) | Master enable in bit 7; read-only PSG activity flags remain zero |
| `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 |
Unused mapped bytes through `0x0400008b` read zero and ignore writes.
The FIFO reset strobes in `SOUNDCNT_H` do not latch. No FIFO data can be queued in this subset.
FIFO addresses and the remaining sound-region holes stay unmapped.
All sound configuration starts at zero in raw memory. RegisterRamReset bit 6 sets the functional bias default to `0x0200`.
Unused mapped register bytes read zero and ignore writes.
FIFO reads and the `0x0400008c..0x0400008f` gap remain explicit unmapped-access errors; no FIFO open-bus value is invented.
Raw memory starts with zero sound state. RegisterRamReset bit 6 sets the functional bias default to `0x0200`.
With master sound disabled, `SOUND3CNT_L` stays zero. The CPU accesses wave bank 1.
Bank 0 cannot be selected or played and remains zero. Disabling master sound does not erase wave RAM.
The BIOS reset explicitly clears the accessible wave RAM through ordinary stores.
Future bank switching and channel playback need a complete sound engine, not a larger register array.
Master disable clears PSG configuration and the FIFO queues, but retains wave RAM and Direct Sound routing/bias.
The CPU then accesses wave bank 1. Idle bank selection can expose bank 0 after master enable.
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.
## Diagnostics and atomicity
An enabled PSG channel trigger returns `MemoryError::UnsupportedIo` with the exact address and value.
This applies even when the configured channel would be inaudible. Disabled PSG trigger writes are ignored.
Length, envelope, sweep, frequency, and wave-bank playback clocks are not implemented.
Do not interpret successful idle configuration writes as PSG synthesis.
A write setting `SOUNDCNT_X` bit 7 returns `MemoryError::UnsupportedIo`, including the address, byte value, and operation.
Status bits and unused bits alone do not enable sound.
Validation occurs before any bytes in the access are written.
Block stores validate every register value before committing earlier writes. Failed CPU and DMA steps remain retryable.
## FIFO and timer behavior
Original tests cover disabled-register writes, masks, byte lanes, wave retention, and BIOS reset selection.
They also check rejected sound activation through byte/word accesses, ARM block stores, and DMA without partial state changes.
Each channel has a seven-word queue, a separate four-byte playback word, and a held signed sample.
Timer 0 or timer 1 overflow clocks the selected channel while master sound is enabled.
Timer 1 cascade overflows work. Timer IRQ enable and stereo routing do not gate sample consumption.
Timers 2 and 3 cannot clock Direct Sound.
## Remaining work
On each selected overflow:
Implement channel clocks, length/envelope/sweep state, wave banking, FIFO consumption, timer-triggered sound DMA, and mixing.
Add a host-independent sample interface before connecting a desktop audio backend.
Do not acknowledge active sound requests without implementing their device behavior.
1. Request sound DMA if the queue contains at most three words, before loading a playback word.
2. Load a queued word if the playback word is empty.
3. Latch the next signed eight-bit sample in little-endian order.
The held sample remains unchanged between overflows. An empty playback word supplies zero on the next overflow.
This underflow policy follows both consulted emulator implementations; the cited hardware report leaves it uncertain.
A bulk advance consumes all available samples and then reaches zero without looping over every empty overflow.
A partial FIFO write updates the addressed lanes of the next queue slot and enqueues that whole word.
Other lanes retain that slot's previous bytes. A write to a full queue clears the queue.
Reset strobes clear queues without changing the held sample or remaining playback bytes.
Queue clearing also zeros stored queue slots in this nominal model.
Exact stale-lane behavior across overflow/reset remains unverified on hardware.
HALT continues timer/audio progress. STOP freezes it.
Master disable stops sample consumption; route disable only mutes the mixer path.
## Sound DMA
Special timing supports DMA1 to FIFO A and DMA2 to FIFO B.
Each request transfers four 32-bit words with a fixed destination, regardless of programmed count, width, or destination mode.
Source address control, channel priority, repeat enable, completion IRQ, and retained DMA data still apply.
Without repeat, the channel disables after the fourth word. With repeat, it waits for another timer request.
Other channel/FIFO pairings and non-FIFO destinations remain explicit `DmaError::UnsupportedControl` cases.
The references disagree about request routing outside the supported pairing; hardware reports permit non-FIFO destinations.
The core does not silently guess those configurations. Video capture and Game Pak DRQ remain unsupported.
Requests become visible at successful machine-step boundaries. Active DMA blocks ignore further requests.
Device-only bulk advances coalesce unserviced requests; they do not execute or replay DMA blocks.
Sub-instruction DMA arbitration and exact refill latency are not modeled.
## Mixing and timing ownership
Each routed sample contributes `sample * 2` at 50% volume or `sample * 4` at 100% volume.
The mixer adds 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.
Bias resolution bits retain their values but do not yet drive pulse-width modulation (PWM) sampling or quantization.
There is no resampling, analog filtering, sample queue, or host output backend.
Polling once per video frame will not reconstruct audio.
Timer and sound state share the staged CPU/DMA clock.
Reads and writes observe their nominal bus-completion phase, after earlier timer overflows.
Successful steps commit once. Failed steps discard FIFO consumption, held samples, register writes, and refill requests.
Block-store validation simulates sound enable/disable in a temporary state before committing any RAM or I/O writes.
Other devices retain their existing scheduling limits.
## Validation and remaining work
Original tests cover signed samples, byte order, queue capacity, partial writes, resets, underflow, stereo routing, and clipping.
They also cover cascaded timers, DMA block/repeat behavior, source progress, completion IRQs, HALT/STOP, and clock batching.
CPU/DMA failure tests verify transactional rollback and sound writes at bus completion.
BIOS tests cover disabled FIFO reset and the supported wave-bank policy.
These tests validate the documented nominal model, not hardware audio fidelity.
Next work: PSG clocks and synthesis, a timestamped or fixed-rate output stream, PWM/mixer sampling, and a Darwin host backend.
The local Emerald startup now passes master enable and stops at a PSG channel 1 trigger.
See [the local runtime result](../research/emerald-reset.md).
## References
- [GBATEK sound control registers](https://problemkaputt.de/gbatek-gba-sound-control-registers.htm): disabled PSG behavior, master enable, masks, FIFO reset strobes, and bias.
- [GBATEK](https://mgba-emu.github.io/gbatek/): channel 3 wave-bank access and register layout.
- [mGBA BIOS service implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional reset defaults. No upstream implementation was copied.
- [GBATEK Direct Sound](https://problemkaputt.de/gbatek-gba-sound-channel-a-and-b-dma-sound.htm): registers, timer selection, signed samples, and DMA blocks.
- [GBATEK sound control](https://problemkaputt.de/gbatek-gba-sound-control-registers.htm): master enable, read masks, reset strobes, routing, and bias.
- [GBATEK mirror](https://mgba-emu.github.io/gbatek/): wave-bank access and register layout.
- [gbadev Direct Sound overview](https://gbadev.net/gbadoc/audio/directsound.html): introductory FIFO/timer/DMA behavior; not authority for queue edge cases.
- [Gericom's hardware findings, mGBA issue 1847](https://github.com/mgba-emu/mgba/issues/1847): seven-word queue, separate playback buffer, request order, partial writes, and overflow observations.
- [mGBA audio at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/audio.c): playback, reset, mixing, and destination-based request routing comparison.
- [NanoBoyAdvance APU at 55b5cf0a](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/apu.cc): master gating, playback word, underflow, and mixing.
- [NanoBoyAdvance FIFO](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/channel/fifo.hh), [registers](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/apu/registers.cc), and [DMA](https://github.com/nba-emu/NanoBoyAdvance/blob/55b5cf0ae3d929582ac5bfd486558173502b8354/src/nba/src/hw/dma/dma.cc): queue/reset policy and channel-based request routing comparison.
- [mGBA BIOS at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional reset defaults.
Reset values are source-backed. Exact Nintendo firmware ordering, timing, and analog behavior are not established.
The Rust implementation and regression programs are original. No upstream implementation or external test ROM was copied into the repository.

View file

@ -216,11 +216,12 @@ Supported flags can be combined:
| 3 | Clear all 96 KiB of video RAM: `0x06000000..0x06017fff` |
| 4 | Clear sprite attribute memory (OAM): `0x07000000..0x070003ff` |
| 5 | Reset supported idle serial state and select general-purpose inputs |
| 6 | Reset disabled sound state, mixing control, bias, and accessible wave RAM |
| 6 | Disable sound, reset FIFO queues and mixing control, set bias, and clear accessible wave bank 1 |
| 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. [Sound activation](audio.md) and [serial transfers](serial.md) still fail explicitly.
This is not full audio or serial support. [PSG playback](audio.md) and [serial transfers](serial.md) still fail explicitly.
Sound reset retains wave bank 0 and the separate in-flight playback word; exact firmware sound-reset equivalence is not established.
The firmware always clears the low halfword of SIODATA32, even when bit 5 is clear; its upper halfword is preserved.
Every supported request first writes `DISPCNT=0x0080`, including requests with no selected flags.
@ -509,5 +510,5 @@ Unsupported SWIs, unsupported exception vectors, and null/misaligned IRQ callbac
The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op.
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.
Active sound, serial transfers, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
PSG playback, host audio output, serial transfers, HardReset, and serial/Game Pak STOP wake-up remain unimplemented.
This subset is not sufficient for Pokémon Emerald compatibility.

View file

@ -59,7 +59,9 @@ Exact startup/completion placement remains nominal, not independently verified p
See [CPU resume timing](cpu.md#nominal-cpu-resume-after-dma) for failure, idle, and refill rules.
Startup scheduling delays, channel-resumption costs, a full CPU fetch pipeline, and display-bus contention remain unmodeled.
Special timing modes (sound FIFO/video capture), Game Pak DRQ, and prohibited source mode 3 return `DmaError::UnsupportedControl`.
Special timing supports DMA1/FIFO A and DMA2/FIFO B with four-word blocks, forced word width, and fixed destinations.
Timer-driven requests, repeat, and completion IRQs are described in [Direct Sound](audio.md).
Other special-mode pairings, video capture, Game Pak DRQ, and prohibited source mode 3 return `DmaError::UnsupportedControl`.
DMA reads below work RAM reuse known channel data as described below. They never expose BIOS bytes.
DMA writes to DMA registers return `DmaError::RegisterDestination`; self-modifying transfers are not supported.
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.

View file

@ -7,7 +7,7 @@
Otherwise it services one ready DMA data unit, keeping the CPU paused.
If HALT is waiting and no DMA is ready, it advances device clocks to the next event without executing CPU code.
Otherwise, it samples the IRQ line, including the CPU's interrupt mask, then enters IRQ mode or executes one instruction.
Each successful step advances display and timer clocks by its nominal cost.
Each successful step advances display, timer, and Direct Sound clocks by its nominal cost.
`Machine::last_timing()` reports separate code, data, internal, and idle cycle totals for the last successful step.
`StepTiming::idle_cycles` counts device-clock cycles during HALT; it does not count CPU work.
IRQ entry is a separate step; the vector instruction runs on the following step.
@ -15,7 +15,7 @@ Entry charges the discarded incoming-state PC+8/PC+4 fetch and the ARM vector pa
DMA resume can make the discarded fetch non-sequential. Missing discarded bytes do not prevent IRQ entry.
See [refill timing](../research/refill-fetch-timing.md) for history effects and remaining limits.
A device event during a step can trigger IRQ entry once no DMA is ready and CPU masks allow delivery.
Timer accesses use ordered bus-completion phases. Other device writes still take effect before their bulk clock update.
Timer and sound accesses use ordered bus-completion phases. Other device writes still take effect before their bulk clock update.
`StepKind::Dma { channel }` identifies a DMA unit. `StepKind::HaltIdle` identifies one bounded HALT clock advance.
`StepKind::StopIdle` identifies a stopped system with no clock progress.
The other step kinds remain `Instruction` and `IrqEntry`.
@ -34,10 +34,11 @@ The transaction also owns timer IF bits. Reads see overflows through the current
A later internal cycle can raise an acknowledged timer request again. Non-timer IF bits retain their existing behavior.
IRQ delivery still waits until the next machine step. A committed timer request can wake HALT, but not STOP.
Successful steps commit timers, the shared divider phase, and timer IF bits.
Failed steps discard staged counters, divider progress, reload/control writes, and requests.
Successful steps commit timers, the shared divider phase, timer IF bits, and Direct Sound state.
Failed steps discard staged counters, divider progress, timer/audio writes, FIFO consumption, and requests.
Display and other device clocks then advance once, without advancing timers again.
Those devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled.
Display and remaining devices still use the bulk scheduler. Mid-instruction display events, general bus arbitration, and IRQ synchronization delays remain unmodeled.
[Direct Sound](audio.md) consumes selected timer overflows in the timer transaction; sound DMA requests commit at step boundaries.
Host clock advances and HALT idle batches retain ordinary bulk timer advancement; STOP idle does not advance timers.
The unchanged [prefetch cancellation probe](../research/prefetch-cancellation.md) now matches both published interval totals and actual timer samples.
@ -92,7 +93,8 @@ See [prescaler evidence and original tests](../research/timer-prescaler.md).
Hardware startup and register-write delays are still not modeled. Writes retain the immediate bus-completion policy.
The shared-phase tests validate this bounded model, not absolute enable/reload edges on hardware.
Audio events and interrupt sources other than timers, display events, DMA completion, and keypad input are not implemented.
Timers 0 and 1 can clock Direct Sound playback and request FIFO DMA independently of timer IRQ enable.
Interrupt sources other than timers, display events, DMA completion, and keypad input are not implemented.
`--timer-demo` executes original ARM code that configures Timer 0, IE, and IME.
The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`.
@ -228,7 +230,7 @@ The current desktop demos do not enter STOP; the frontend does not yet provide a
Only keypad wake-up is implemented. General-purpose serial and Game Pak wake sources require their missing external devices.
Oscillator restart delays, exact entry/wake edges, and hardware-specific IF behavior remain unverified.
There is no host-thread sleep, wall-clock accounting, or audio clock implementation.
There is no host-thread sleep or wall-clock accounting. STOP freezes the emulated timer-driven audio clock.
The graphics demo now uses the optional original BIOS replacement's VBlankIntrWait service.
Its IRQ callback acknowledges VBlank and updates the BIOS RAM flag before the service returns.