SWI 0x1C — SoundDriverMain¶
- Entry:
0x00001DC4(THUMB; SWI table stores0x00001DC5) - Status: verified (hardware-checked 2026-07-08: cycle counts and return registers measured on real GBA; struct layouts remain from static analysis)
Summary¶
The per-frame PCM software mixer. Called once every game frame (after SoundDriverVSync). It runs the attached music-player/update hooks, then mixes all active PCM mixer voices (up to 12) — applying each voice's playback position, pitch/frequency step, and left/right envelope volumes — into the SoundArea PCM output buffer that the FIFO DMA streams to the APU. Handles sample looping and one-shot end, and (optionally) the reverb feedback pass.
Parameters¶
| Reg | In | Meaning |
|---|---|---|
| — | — | Operates on the global SoundArea at [0x03007FF0]; no explicit args |
Returns¶
| Reg | Out | Meaning |
|---|---|---|
| — | — | PCM buffer for the current cycle filled; voice states advanced |
Clobbered registers¶
Saves/restores r4–r11 and returns cleanly (large register file used by the inner mix loop).
Hardware audit (2026-07-08, canary r4–r12 + CPSR snapshot): caller-visible clobbers: r0/r1 garbage — they returned holding the caller's r8/r9 canary values, i.e. internal register shuffling leaks r8/r9 contents into r0/r1. r2, r4–r12, r13, and CPSR (flags and mode) came back bit-identical on every tested path. r11/r12 are explained by the SWI dispatcher (it pushes {r11, r12, lr} — see 10_irq_boot_and_iwram.md) and CPSR by the SPSR restore on return; r2 and r4–r10 must be preserved or restored by the routine itself — where a static note above claims r2 is destroyed, the hardware disagrees at the caller level. Raw data: results/clobber_audit_pass2.csv.
Algorithm¶
- Load SoundArea via
[0x03007FF0]; if identity word ≠0x68736d53, return immediately (re-entrancy / not-initialized guard). Otherwise bump the identity word to mark "busy". - If the MPlayMain enable flag (SoundArea+0x20) is set, invoke the installed music sequencer hook (SoundArea+0x24). Then invoke the main update hook (SoundArea+0x28, a no-op stub until a player is attached). These hooks generate/refresh per-voice parameters before mixing.
- Compute the mix-buffer write pointer from the mix buffer base (SoundArea+
0x350, the DMA1 source) offset by the current DMA sub-frame counter × pcmSamplesPerVBlank (SoundArea+0x10). This implements a multi-frame double/ring buffer synchronised with SoundDriverVSync's DMA restart period (SoundArea+0x0B). - For each of the 12 PCM channels (SoundArea +
0x50+ i*0x40): - Skip if the channel status byte marks it inactive.
- Read the channel's sample source pointer, current fractional playback position, frequency step, and per-side (left/right) volume/envelope bytes.
- Generate
pcmSamplesPerVBlankoutput samples: advance the position by the frequency step per sample, fetch the source sample, scale by the left and right volumes, and accumulate into the interleaved/paired mix buffer. The inner sample loop is heavily unrolled and dispatched through a computed jump for speed. - On reaching the sample end, either wrap to the loop point or mark the channel released/off (one-shot).
- Advance/decay the channel's envelope state.
- After all voices are mixed, clamp/store the result and (if reverb is enabled) apply the reverb feedback mix.
- Restore the identity word and saved registers; return.
Buffer / DMA relationship¶
- Mix buffer A base = SoundArea+
0x350=DMA1SAD; buffer B base = SoundArea+0x980=DMA2SAD(see SoundDriverInit). Each is ~0x630bytes. - The mixer writes ahead of the DMA read position; SoundDriverVSync restarts the DMA
every
pcmDmaPeriod(SoundArea+0x0B) frames so the two stay phase-locked.
Edge cases & known bugs¶
- No-op if the SoundArea identity word is wrong (also protects against re-entry from an interrupt while the mixer is running).
- Inactive/over-count channels beyond
maxChansare skipped by their status byte.
Sound channel struct (0x40 bytes, static analysis)¶
Skeleton extracted from SoundDriverMain's mixing loop (static analysis (Ghidra 12.1.2)). The mixer processes up to 12 channels of 0x40 bytes each (base at SoundArea+0x50, stride 0x40; the 4 hardware Direct-Sound channels are a separate array reached via SoundArea+0x1C).
| Off | Field (inferred) |
|---|---|
| 0x00 | status / key-on flags (bit7 = start, bit6 = stop/release, bit2, low bits = envelope phase) |
| 0x02 | raw right volume |
| 0x03 | raw left volume |
| 0x05 | envelope attack rate |
| 0x07 | envelope sustain/decay rate |
| 0x0A | computed right output volume |
| 0x0B | computed left output volume |
| 0x0C | current envelope level |
| 0x0D | envelope phase counter |
| 0x18 | current sample value / loop field |
| 0x1C | fractional sample position (reset to 0 on key-on) |
| 0x24 | WaveData pointer |
| 0x28 | play-position pointer (init = WaveData+0x10) |
Field bit-level semantics are partially inferred; the envelope math is level = (rate × level) >> 8 per step with the 0x02/0x03 volumes folded into 0x0A/0x0B.
Cycle count¶
Hardware-measured net CPU cycles (worker-ROM harness, TM0/TM1 cascade at F/1,
13-cycle baseline subtracted; identical across 3 runs — see
02_hardware_verification_checklist.md § Measurement setup).
1310 cycles with the driver initialized and idle (no active channels, no sequencer players). Scales up with active voices; idle value is the floor.
Open questions (need hardware verification)¶
- Exact per-channel (0x40-byte) field offsets (position, step, loop point, envelope, L/R volume) — inferred from the mix loop but not yet byte-verified.
- Whether output is true stereo (separate A/B buffers) or the second buffer mirrors the first; and the exact reverb algorithm.
- Whether it must run with IRQs disabled (the busy-lock suggests re-entrancy concern).
GBATEK cross-reference¶
Expands GBATEK's one-line SoundDriverMain entry into the mixer's actual structure:
hook invocation, per-voice mixing into the 0x350/0x980 buffers, and the DMA
phase-lock via the frame counter.