8. Walking the disc image

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.

1. The CISO container

A .ciso file is a raw disc image with all-zero blocks removed. The header is 0x8000 bytes:

OffsetSizeField
0x00004magic "CISO" (43 49 53 4F)
0x00044block size, little-endian u32 (commonly 0x200000)
0x00080x7FF8block 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.

2. The GameCube disc

Only four header fields matter here:

OffsetSizeFieldGAFE01 rev 0 value
0x00006game code — verify "GAFE01"
0x04204offset of the main DOL0x0001E000
0x04244offset of the FST0x000FE500
0x04284size of the FST0x102

The FST

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

3. The DOL and the five tables

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:

SectionFile offset (in DOL)Virtual addressSize
text00x0001000x800031000x02520
text10x0026200x800056C00xA40C0
data00x0A66E00x800056200x00040
data10x0A67200x800056600x00060
data20x0A67800x800A97800x00040
data30x0A67C00x800A97C00x00020
data40x0A67E00x800A97E00x06080
data50x0AC8600x800AF8600x32A20
data60x0DF2800x80217B800x00700
data70x0DF9800x80218BE00x00B40

The five tables all live in data5 (addresses from the ac-decomp symbol map for GAFE01 rev 0; other revisions have different addresses):

SymbolVirtual addressSizeEntriesDisc offset
AudioseqHeaderStart0x800CCA400xFA02490x0E7A40
AudiobankHeaderStart0x800CD9E00xA001590x0E89E0
AudiowaveHeaderStart0x800CE3E00x7060x0E93E0
AudiodataHeaderStart0x800CE4500x4030x0E9450
AudiomapHeaderStart0x800CE4900x3F0—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.

4. audiorom.img

AudiodataHeaderStart carves the file into three regions. Their sizes sum to exactly the file size:

RegionOffset in fileSizeAbsolute disc offset
Audioseq0x0000000x0CF7000x626D10
Audiobank0x0CF7000x067C800x6F6410
Audiowave0x1373800x6B33E00x75E090

Entry addresses in the sequence, bank and wave tables are offsets into their region. The animalese entries:

EntryRegion offsetSizeAbsolute disc offsetparam0param1
seq 2430x0ABA600x6600x6D2770
seq 2440x0AC0C00x4800x6D2DD0
seq 2450x0AC5400x6600x6D3250
bank 1560x05C2600x45000x7526700x05FF0x7700
bank 1570x0607600x30200x756B700x05FF0x5300
bank 1580x0637800x45000x759B900x05FF0x7700
wave bank 50x2C5DC00x3ED6200xA23E50

5. Walking a bank blob

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:

6. Decoding 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.

The one easy mistake is feeding decoded outputs back per-sample inside a half — i.e. running 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.

7. The voice sequences, disassembled

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:

The complete per-code parameter map

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.

SeqCodesInstrumentSemitoneDelay (ticks)VelocityExtras
243 0x01–0x38code − 14413110
0x39–0x52code − 14415110
0x53–0x76code − 1442895pitch sweep from semitone 41 (portamento c7 81 29 ff)
0x770x764420110
0x78–0x7Fsilence (routine is an immediate ff)
244 0x01–0x38code − 14116110
0x39–0x52code − 13920110
0x770x524120110the you substitute — see below
0x53–0x76, 0x78–0x7Fsilence
245 0x01–0x38code − 13917110
0x39–0x52, 0x77code − 13920110
0x53–0x76code − 1442895pitch sweep from semitone 41
0x78–0x7Fsilence

Two things this table reveals that the C code cannot:

8. Reassembling audible animalese

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.

Scope. These offsets are for the USA rev 0 image (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-shrunk images work unmodified. An NKit-shrunk rip (marker "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.
← 7. The editor path 9. Quirks & corrections →