From 9ca1a1c1c7bf56e39b516c299b4e0d3efe59652b Mon Sep 17 00:00:00 2001 From: Alexander Kassouni Date: Tue, 8 Sep 2026 20:11:06 -0700 Subject: better docs --- docs/disc.html | 413 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 docs/disc.html (limited to 'docs/disc.html') diff --git a/docs/disc.html b/docs/disc.html new file mode 100644 index 0000000..5f4a4ae --- /dev/null +++ b/docs/disc.html @@ -0,0 +1,413 @@ + + + + + +8. Walking the disc image — Animalese + + + + + + +

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
2430x01–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)
2440x01–0x38code − 14116110
0x39–0x52code − 13920110
0x770x524120110the you substitute — see below
0x53–0x76, 0x78–0x7Fsilence
2450x01–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 → +
+ + + -- cgit v1.3.1