Everything a reimplementation needs to pull the samples and sequences out
of a dumped disc, byte by byte: the CISO container, the GameCube filesystem, the DOL,
the five audio tables, the bank blobs, and the VADPCM decoder. Every offset on this page
was verified against a real GAFE01 USA rev 0 image.
The chain, outermost to innermost:
CISO container (optional)
└ GameCube disc
├ main DOL ──► the five audio tables (compiled-in .data)
└ /audiorom.img ──► Audioseq · Audiobank · Audiowave regions
├ sequences 243/244/245 (dyntbl: phoneme code → instrument + note)
├ banks 156/157/158 (instrument → sample descriptor)
└ wave bank 5 (raw VADPCM frames)
All disc and audio structures are big-endian. The single exception is the CISO header's block size, which is little-endian.
A .ciso file is a raw disc image with all-zero blocks removed. The header
is 0x8000 bytes:
| Offset | Size | Field |
|---|---|---|
0x0000 | 4 | magic "CISO" (43 49 53 4F) |
0x0004 | 4 | block size, little-endian u32 (commonly 0x200000) |
0x0008 | 0x7FF8 | block map: one byte per logical block; 1 = stored, 0 = omitted (reads as zeros) |
Stored blocks follow the header back to back, in logical order. To read logical offset x:
b = x / block_size
if map[b] == 0: the bytes are zero
else: physical = 0x8000 + rank(b) * block_size + x % block_size
where rank(b) = number of set map entries before index b
Precomputing a prefix-sum over the map gives O(1) reads. A plain .iso /
.gcm is the same image without this layer — detect by checking for the
CISO magic and fall through to raw offsets if absent.
Only four header fields matter here:
| Offset | Size | Field | GAFE01 rev 0 value |
|---|---|---|---|
0x0000 | 6 | game code — verify "GAFE01" | |
0x0420 | 4 | offset of the main DOL | 0x0001E000 |
0x0424 | 4 | offset of the FST | 0x000FE500 |
0x0428 | 4 | size of the FST | 0x102 |
The filesystem table is an array of 12-byte entries followed by a string table:
entry {
u8 flags; // 0 = file, 1 = directory
u24 name_offset; // into the string table (NUL-terminated)
u32 offset; // file: absolute disc offset dir: parent index
u32 length; // file: byte length dir: index of next sibling
}
Entry 0 is the root; its length is the total entry count, and the string
table starts at fst + count * 12. Animal Crossing's FST has just 11 entries.
The one that matters:
audiorom.img offset 0x626D10 size 0x7EA760
The game opens it by name — Jac_InitARAM checks
Jac_CheckFile("/audiorom.img") and neosthread.c DMAs the whole file
into ARAM. Walk the FST by name rather than hardcoding the offset; the size, however, is
structural (it is the sum of the three region sizes below) and makes a good integrity
check. internal/dummyrom.c:68,
internal/neosthread.c:56
The audio tables are compiled into the main executable, not stored in
audiorom.img. A DOL starts with a 0x100-byte header:
0x00 u32 text_offset[7] 0x48 u32 text_address[7] 0x90 u32 text_size[7] 0x1C u32 data_offset[11] 0x64 u32 data_address[11] 0xAC u32 data_size[11] 0xD8 u32 bss_address 0xDC u32 bss_size 0xE0 u32 entry_point
To read virtual address va: find the section with
address ≤ va < address + size, then
file_offset = dol_offset + section_offset + (va − address). The GAFE01 rev 0
DOL's populated sections:
| Section | File offset (in DOL) | Virtual address | Size |
|---|---|---|---|
| text0 | 0x000100 | 0x80003100 | 0x02520 |
| text1 | 0x002620 | 0x800056C0 | 0xA40C0 |
| data0 | 0x0A66E0 | 0x80005620 | 0x00040 |
| data1 | 0x0A6720 | 0x80005660 | 0x00060 |
| data2 | 0x0A6780 | 0x800A9780 | 0x00040 |
| data3 | 0x0A67C0 | 0x800A97C0 | 0x00020 |
| data4 | 0x0A67E0 | 0x800A97E0 | 0x06080 |
| data5 | 0x0AC860 | 0x800AF860 | 0x32A20 |
| data6 | 0x0DF280 | 0x80217B80 | 0x00700 |
| data7 | 0x0DF980 | 0x80218BE0 | 0x00B40 |
The five tables all live in data5 (addresses from the ac-decomp symbol
map for GAFE01 rev 0; other revisions have different addresses):
| Symbol | Virtual address | Size | Entries | Disc offset |
|---|---|---|---|---|
AudioseqHeaderStart | 0x800CCA40 | 0xFA0 | 249 | 0x0E7A40 |
AudiobankHeaderStart | 0x800CD9E0 | 0xA00 | 159 | 0x0E89E0 |
AudiowaveHeaderStart | 0x800CE3E0 | 0x70 | 6 | 0x0E93E0 |
AudiodataHeaderStart | 0x800CE450 | 0x40 | 3 | 0x0E9450 |
AudiomapHeaderStart | 0x800CE490 | 0x3F0 | — | 0x0E9490 |
"Disc offset" is the absolute image offset:
0x1E000 + 0xAC860 + (va − 0x800AF860). The
ArcHeader/ArcEntry formats, the meaning of
param0/1/2, and the zero-size alias-entry rule are on
the soundfont page. An alternative to parsing the DOL is to
hardcode the table contents — they are plain data in
audioheaders.c of the decomp — but reading them from the image keeps the
tool self-contained and self-checking.
AudiodataHeaderStart carves the file into three regions. Their sizes sum
to exactly the file size:
| Region | Offset in file | Size | Absolute disc offset |
|---|---|---|---|
| Audioseq | 0x000000 | 0x0CF700 | 0x626D10 |
| Audiobank | 0x0CF700 | 0x067C80 | 0x6F6410 |
| Audiowave | 0x137380 | 0x6B33E0 | 0x75E090 |
Entry addresses in the sequence, bank and wave tables are offsets into their region. The animalese entries:
| Entry | Region offset | Size | Absolute disc offset | param0 | param1 |
|---|---|---|---|---|---|
| seq 243 | 0x0ABA60 | 0x660 | 0x6D2770 | ||
| seq 244 | 0x0AC0C0 | 0x480 | 0x6D2DD0 | ||
| seq 245 | 0x0AC540 | 0x660 | 0x6D3250 | ||
| bank 156 | 0x05C260 | 0x4500 | 0x752670 | 0x05FF | 0x7700 |
| bank 157 | 0x060760 | 0x3020 | 0x756B70 | 0x05FF | 0x5300 |
| bank 158 | 0x063780 | 0x4500 | 0x759B90 | 0x05FF | 0x7700 |
| wave bank 5 | 0x2C5DC0 | 0x3ED620 | 0xA23E50 |
The structures are defined on the soundfont page; this is
the walk, with bank 156's real numbers. All internal fields are offsets
relative to the bank blob's start, except sample, which is
relative to the wave bank's start.
bank 156 @ disc 0x752670
├ u32 drumListOffset = 0
├ u32 sfxListOffset = 0
├ u32 instrumentOffset[119] count from param1 >> 8
│ [0] = 0x3610
│ [1] = 0x3630 voicetables are 0x20 apart
│ …
│ [118] = 0x44D0
└ instrument 0 (phoneme code 0x01, short a) @ +0x3610:
rel=0 range=[0,127] adsr_decay_idx=250 envelope=0x3600
low = { 0, 0.0 } unused
norm = { wavetable 0x01F0, tuning 0.689062 }
high = { 0, 0.0 } unused
wavetable @ +0x1F0:
codec=0 (VADPCM) medium=0 size=0x750
sample = 0x341130 → disc 0xA23E50 + 0x341130 = 0xD64F80
loop = 0x250 → { start=0, end=3328, count=0, sample_end=0 }
book = 0x200 → { order=2, n_predictors=2, s16 codebook[32] }
envelope @ +0x3600: (1, 0) (1, 32700) (-1, 0) (0, 0)
Facts that hold across every instrument in all three voice banks, verified by exhaustive scan:
tuning is always 0.689062 = 22050 / 32000 — the recordings
are 22050 Hz and the mixer runs at 32000 Hz.codec = 0 (VADPCM), order = 2,
n_predictors = 2, loop.count = 0 (no looping);
loop.end equals the decoded sample count.(1,0) (1,32700) (-1,0) —
one envelope step at zero, one step rising to 32700/32768, then hang
(-1 = ADSR_HANG) until the note is released, then fade at
adsr_decay_table[250]. Effectively: instant attack, full sustain,
fast release.0x341130–0x385890, 157 0x385890–0x3AAAAC,
158 0x3AAAB0–0x3ED612 — each voice is a separate recording session.
(This is also what pins sample as wave-bank-relative: bank 158's last
sample ends 14 bytes short of the wave bank's exact end, and treating the offsets
as region-relative lands mid-stream in invalid VADPCM.)Each frame is 9 bytes → 16 samples. Total samples = size / 9 × 16.
byte 0: scale = header >> 4 (0–12 in these banks)
predictor = header & 0xF (0 or 1)
bytes 1–8: 16 nibbles, high first — sign-extend each from 4 bits,
then residual r[i] = nibble << scale
The codebook is stored as order × 8 signed 16-bit coefficients per
predictor. Expand each predictor into an 8-row × (order+8)-column matrix
M:
M[row][col] for col < order: book[predictor][col*8 + row]
M[row][order + i] = 2048 if i == row
= M[row − i − 1][order−1] if i < row
= 0 if i > row
Decode two 8-sample halves per frame. Each half takes an input vector of the previous
order (= 2) decoded samples followed by that half's 8
scaled residuals:
in = [ out[-2], out[-1], r[0..7] ] out[row] = floor( Σ M[row][k] · in[k] / 2048 ) for row 0..7
Initial state is two zero samples; state carries across frames for the length of the sample. The division is a floor (round toward −∞), not truncation.
out[n] = r[n] + (c₁·out[n−1] + c₂·out[n−2]) / 2048
with rounding at every step. The matrix form convolves the scaled residuals
and rounds once per output sample; cascading the rounding decodes audibly noisy audio.
Validation for this exact algorithm against this exact image: instrument 0 of bank
156 decodes to 3,328 samples (equal to its loop.end), peak amplitude 19,213,
nothing out of s16 range, and a smooth vowel envelope.
Sequences 243/244/245 are tiny programs in the JAudio-NES (ported Zelda 64) sequence bytecode. They are structurally identical; byte-for-byte they differ only in one handshake constant and in their note/timing parameters. Sequence 243 annotated:
; ---- group (master) script @ 0x00 ---- 0x00 db 7f master volume 127 0x02 dd 78 tempo 120 BPM 0x04 d7 00 03 enable subtracks 0 and 1 0x07 90 00 13 start subtrack 0 at 0x13 0x0A 91 00 13 start subtrack 1 at 0x13 ; same program, separate state 0x0D fd fd 00 delay 0x7D00 ticks 0x10 fb 00 0d branch → 0x0D ; idle forever ; ---- subtrack (channel) program @ 0x13, shared by both ---- 0x13 e9 88 SET_PRIORITY 0x88 0x15 c4 LARGE_NOTE_ON 0x16 dc 7f SET_PAN_WEIGHT 0x7F 0x18 ec RESET_VIBRATO loop: 0x19 fe delay 1 tick 0x1A cc 01 MACRO_SET: value ← 1 ; 2 in seq 244, 3 in seq 245 0x1C 71 PORT1_WRITE_MACRO_REG ; port[1] ← value (see below) 0x1D 60 MACRO_READ_PORT0 ; value ← port[0]; port[0] ← -1 0x1E f2 f9 branch → loop if value < 0 ; port empty 0x20 c2 00 26 SET_DYNTBL 0x0026 0x23 e4 DYNTBL_CALL ; call dyntbl[value] (skips -1) 0x24 f4 f3 branch → loop ; ---- dyntbl @ 0x26: 128 big-endian u16 script addresses ---- ; entry v is the routine for phoneme code v ; ---- per-phoneme routine (codes 0x01–0x52, 10 bytes each) ---- c1 <inst> VOICE_SET: instrument = code − 1 88 <addr16> NOTE0_SET_PC: start note layer at addr ff return to the spin loop ; layer: <note> <delay> <velocity> ff ; e.g. 6c 0d 6e ff
Semantics that matter for a reimplementation:
Nap_ReadSubPort(VOICE_GROUP, 0, 1) == sou_now_voice_seq polls — the
load-race gate on the gating page. It is the
only byte that differs between the three sequences' programs.MACRO_READ_PORT0 returns
the value and resets the port to −1, and both the branch (0xF2 branches
on negative) and DYNTBL_CALL (skips −1) treat −1 as "empty". So each
Sou_VoiceStart write triggers exactly one routine, at most one per
tick per subtrack.0x40–0x7F are note-on with
semitone = opcode − 0x40, then a varint delay and a velocity byte,
gate 0. Volume contribution is (velocity / 127)².
internal/track.c:1028 __SetNotefrequency_scale = PITCHTABLE[semitone] ×
tuning, where PITCHTABLE[i] = 2^((i − 48) / 12) — unity at 48.
internal/audiotable.c:321,
internal/track.c:975240 updates/s × tempo / max_tempo with
tempo = 120 × 48 = 5760 and max_tempo = 13726
(from 60000 × 48 × 4 / 48 / 16.713 / 1.04613,
internal/memory.c:998) — ≈ 100.7 ticks/s. When the layer's delay
expires the note releases through adsr_decay_table[250].Extracted from the shipped sequences. "Semitone" is relative to unity at 48, so 44 plays a sample at 2^(−4/12) ≈ 0.794× recorded speed, 41 at 0.667×, 39 at 0.595× — each bank's recordings are tuned differently, which is part of why the three voices sound distinct.
| Seq | Codes | Instrument | Semitone | Delay (ticks) | Velocity | Extras |
|---|---|---|---|---|---|---|
| 243 | 0x01–0x38 | code − 1 | 44 | 13 | 110 | |
0x39–0x52 | code − 1 | 44 | 15 | 110 | ||
0x53–0x76 | code − 1 | 44 | 28 | 95 | pitch sweep from semitone 41 (portamento c7 81 29 ff) | |
0x77 | 0x76 | 44 | 20 | 110 | ||
0x78–0x7F | silence (routine is an immediate ff) | |||||
| 244 | 0x01–0x38 | code − 1 | 41 | 16 | 110 | |
0x39–0x52 | code − 1 | 39 | 20 | 110 | ||
0x77 | 0x52 | 41 | 20 | 110 | the you substitute — see below | |
0x53–0x76, 0x78–0x7F | silence | |||||
| 245 | 0x01–0x38 | code − 1 | 39 | 17 | 110 | |
0x39–0x52, 0x77 | code − 1 | 39 | 20 | 110 | ||
0x53–0x76 | code − 1 | 44 | 28 | 95 | pitch sweep from semitone 41 | |
0x78–0x7F | silence | |||||
Two things this table reveals that the C code cannot:
0x53–0x76 play with portamento mode 1 sweeping from semitone 41 up to
44, longer (28 ticks) and quieter (velocity 95) than dialogue phonemes — each
keystroke's letter name lilts upward.you is deliberate, not clamped. Sequence
244 maps code 0x77 to instrument 0x52 — bank 157's 83rd
and last instrument, which sits above the phonology range
(0x01–0x52) and is reachable by nothing else. Bank 157 carries one
extra instrument specifically to voice you.For one phoneme code v spoken by spec s:
seq, bank = spec table (page 1) ; 243/156, 244/157, 245/158
inst, semitone, delay, velocity = per-code map above (dyntbl[v])
pcm = vadpcm_decode(bank.instrument[inst].normal.sample)
rate = 32000 × tuning × 2^((semitone − 48)/12)
× spec_pitch × emotion_pitch × punctuation_pitch ; pages 1 and 5
gain = (velocity/127)² × master(127/127) × spec_vol × emotion_vol
env = near-instant attack to 32700/32768, hold,
release after `delay` ticks via adsr_decay_table[250]
pan = centre, no reverb, no distance attenuation
Practically: decoding the sample and playing it at
22050 × 2^((semitone − 48 + 12·log₂(pitch_mults))/12) Hz with a
(velocity/127)² gain gets within a hair of the real thing; the envelope
release only matters when a sample outlasts its note delay. Timing of successive
syllables comes from the scheduler, not from the audio
engine — new syllables do not cut old ones off, they land on the alternating other
subtrack and overlap.
GAFE01,
the one ac-decomp matches). The FST walk, DOL walk and all structure layouts are
revision-independent; the DOL virtual addresses and disc offsets are not.
"NKIT v01" at disc offset 0x200; original size as a big-endian
u32 at 0x210) strips the ~1.4 GB of junk padding after the
real data without relocating it — and Animal Crossing's 11 files all end by
0xE11470, so every offset on this page still reads valid data (verified
against such an image). Two rules keep a loader safe on any variant: never derive
anything from total file size, and never read past the extents the FST declares.