aboutsummaryrefslogtreecommitdiff
path: root/docs/disc.html
blob: 5f4a4ae99556fdfc1e9cc5b1d6fbc4ca53a30aa8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>8. Walking the disc image — Animalese</title>
<link rel="stylesheet" href="style.css">
</head>
<body>

<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 8. Walking the disc image</nav>

<h1>8. Walking the disc image</h1>
<p class="lede">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 <code>GAFE01</code> USA rev&nbsp;0 image.</p>

<p>The chain, outermost to innermost:</p>

<pre>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)</pre>

<p>All disc and audio structures are <strong>big-endian</strong>. The single exception is
the CISO header's block size, which is little-endian.</p>

<h2>1. The CISO container</h2>

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

<table>
<tr><th>Offset</th><th>Size</th><th>Field</th></tr>
<tr><td><code>0x0000</code></td><td>4</td><td>magic <code>"CISO"</code> (<code>43 49 53 4F</code>)</td></tr>
<tr><td><code>0x0004</code></td><td>4</td><td>block size, <strong>little-endian</strong> u32 (commonly <code>0x200000</code>)</td></tr>
<tr><td><code>0x0008</code></td><td><code>0x7FF8</code></td><td>block map: one byte per logical block; <code>1</code> = stored, <code>0</code> = omitted (reads as zeros)</td></tr>
</table>

<p>Stored blocks follow the header back to back, in logical order. To read logical
offset <em>x</em>:</p>

<pre>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</pre>

<p>Precomputing a prefix-sum over the map gives O(1) reads. A plain <code>.iso</code> /
<code>.gcm</code> is the same image without this layer — detect by checking for the
<code>CISO</code> magic and fall through to raw offsets if absent.</p>

<h2>2. The GameCube disc</h2>

<p>Only four header fields matter here:</p>

<table>
<tr><th>Offset</th><th>Size</th><th>Field</th><th>GAFE01 rev 0 value</th></tr>
<tr><td><code>0x0000</code></td><td>6</td><td>game code — verify <code>"GAFE01"</code></td><td></td></tr>
<tr><td><code>0x0420</code></td><td>4</td><td>offset of the main DOL</td><td><code>0x0001E000</code></td></tr>
<tr><td><code>0x0424</code></td><td>4</td><td>offset of the FST</td><td><code>0x000FE500</code></td></tr>
<tr><td><code>0x0428</code></td><td>4</td><td>size of the FST</td><td><code>0x102</code></td></tr>
</table>

<h3>The FST</h3>

<p>The filesystem table is an array of 12-byte entries followed by a string table:</p>

<pre>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
}</pre>

<p>Entry 0 is the root; its <code>length</code> is the total entry count, and the string
table starts at <code>fst + count * 12</code>. Animal Crossing's FST has just 11 entries.
The one that matters:</p>

<pre>audiorom.img    offset 0x626D10    size 0x7EA760</pre>

<p>The game opens it by name — <code>Jac_InitARAM</code> checks
<code>Jac_CheckFile("/audiorom.img")</code> and <code>neosthread.c</code> 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. <span class="src"><code>internal/dummyrom.c:68</code>,
<code>internal/neosthread.c:56</code></span></p>

<h2>3. The DOL and the five tables</h2>

<p>The audio tables are compiled into the main executable, not stored in
<code>audiorom.img</code>. A DOL starts with a <code>0x100</code>-byte header:</p>

<pre>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</pre>

<p>To read virtual address <em>va</em>: find the section with
<code>address ≤ va &lt; address + size</code>, then
<code>file_offset = dol_offset + section_offset + (va − address)</code>. The GAFE01 rev 0
DOL's populated sections:</p>

<table>
<tr><th>Section</th><th>File offset (in DOL)</th><th>Virtual address</th><th>Size</th></tr>
<tr><td>text0</td><td><code>0x000100</code></td><td><code>0x80003100</code></td><td><code>0x02520</code></td></tr>
<tr><td>text1</td><td><code>0x002620</code></td><td><code>0x800056C0</code></td><td><code>0xA40C0</code></td></tr>
<tr><td>data0</td><td><code>0x0A66E0</code></td><td><code>0x80005620</code></td><td><code>0x00040</code></td></tr>
<tr><td>data1</td><td><code>0x0A6720</code></td><td><code>0x80005660</code></td><td><code>0x00060</code></td></tr>
<tr><td>data2</td><td><code>0x0A6780</code></td><td><code>0x800A9780</code></td><td><code>0x00040</code></td></tr>
<tr><td>data3</td><td><code>0x0A67C0</code></td><td><code>0x800A97C0</code></td><td><code>0x00020</code></td></tr>
<tr><td>data4</td><td><code>0x0A67E0</code></td><td><code>0x800A97E0</code></td><td><code>0x06080</code></td></tr>
<tr><td>data5</td><td><code>0x0AC860</code></td><td><code>0x800AF860</code></td><td><code>0x32A20</code></td></tr>
<tr><td>data6</td><td><code>0x0DF280</code></td><td><code>0x80217B80</code></td><td><code>0x00700</code></td></tr>
<tr><td>data7</td><td><code>0x0DF980</code></td><td><code>0x80218BE0</code></td><td><code>0x00B40</code></td></tr>
</table>

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

<table>
<tr><th>Symbol</th><th>Virtual address</th><th>Size</th><th>Entries</th><th>Disc offset</th></tr>
<tr><td><code>AudioseqHeaderStart</code></td><td><code>0x800CCA40</code></td><td><code>0xFA0</code></td><td>249</td><td><code>0x0E7A40</code></td></tr>
<tr><td><code>AudiobankHeaderStart</code></td><td><code>0x800CD9E0</code></td><td><code>0xA00</code></td><td>159</td><td><code>0x0E89E0</code></td></tr>
<tr><td><code>AudiowaveHeaderStart</code></td><td><code>0x800CE3E0</code></td><td><code>0x70</code></td><td>6</td><td><code>0x0E93E0</code></td></tr>
<tr><td><code>AudiodataHeaderStart</code></td><td><code>0x800CE450</code></td><td><code>0x40</code></td><td>3</td><td><code>0x0E9450</code></td></tr>
<tr><td><code>AudiomapHeaderStart</code></td><td><code>0x800CE490</code></td><td><code>0x3F0</code></td><td>—</td><td><code>0x0E9490</code></td></tr>
</table>

<p>"Disc offset" is the absolute image offset:
<code>0x1E000 + 0xAC860 + (va − 0x800AF860)</code>. The
<code>ArcHeader</code>/<code>ArcEntry</code> formats, the meaning of
<code>param0/1/2</code>, and the zero-size alias-entry rule are on
<a href="soundfont.html">the soundfont page</a>. An alternative to parsing the DOL is to
hardcode the table contents — they are plain data in
<code>audioheaders.c</code> of the decomp — but reading them from the image keeps the
tool self-contained and self-checking.</p>

<h2>4. audiorom.img</h2>

<p><code>AudiodataHeaderStart</code> carves the file into three regions. Their sizes sum
to exactly the file size:</p>

<table>
<tr><th>Region</th><th>Offset in file</th><th>Size</th><th>Absolute disc offset</th></tr>
<tr><td>Audioseq</td><td><code>0x000000</code></td><td><code>0x0CF700</code></td><td><code>0x626D10</code></td></tr>
<tr><td>Audiobank</td><td><code>0x0CF700</code></td><td><code>0x067C80</code></td><td><code>0x6F6410</code></td></tr>
<tr><td>Audiowave</td><td><code>0x137380</code></td><td><code>0x6B33E0</code></td><td><code>0x75E090</code></td></tr>
</table>

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

<table>
<tr><th>Entry</th><th>Region offset</th><th>Size</th><th>Absolute disc offset</th><th>param0</th><th>param1</th></tr>
<tr><td>seq 243</td><td><code>0x0ABA60</code></td><td><code>0x660</code></td><td><code>0x6D2770</code></td><td></td><td></td></tr>
<tr><td>seq 244</td><td><code>0x0AC0C0</code></td><td><code>0x480</code></td><td><code>0x6D2DD0</code></td><td></td><td></td></tr>
<tr><td>seq 245</td><td><code>0x0AC540</code></td><td><code>0x660</code></td><td><code>0x6D3250</code></td><td></td><td></td></tr>
<tr><td>bank 156</td><td><code>0x05C260</code></td><td><code>0x4500</code></td><td><code>0x752670</code></td><td><code>0x05FF</code></td><td><code>0x7700</code></td></tr>
<tr><td>bank 157</td><td><code>0x060760</code></td><td><code>0x3020</code></td><td><code>0x756B70</code></td><td><code>0x05FF</code></td><td><code>0x5300</code></td></tr>
<tr><td>bank 158</td><td><code>0x063780</code></td><td><code>0x4500</code></td><td><code>0x759B90</code></td><td><code>0x05FF</code></td><td><code>0x7700</code></td></tr>
<tr><td>wave bank 5</td><td><code>0x2C5DC0</code></td><td><code>0x3ED620</code></td><td><code>0xA23E50</code></td><td></td><td></td></tr>
</table>

<h2>5. Walking a bank blob</h2>

<p>The structures are defined on <a href="soundfont.html">the soundfont page</a>; this is
the walk, with bank 156's real numbers. All internal fields are offsets
<strong>relative to the bank blob's start</strong>, except <code>sample</code>, which is
relative to the <strong>wave bank's start</strong>.</p>

<pre>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)</pre>

<p>Facts that hold across every instrument in all three voice banks, verified by
exhaustive scan:</p>

<ul>
  <li>Only the <em>normal</em> wtstr slot is populated; low/high are null.</li>
  <li><code>tuning</code> is always <code>0.689062</code> = 22050 / 32000 — the recordings
      are 22050&nbsp;Hz and the mixer runs at 32000&nbsp;Hz.</li>
  <li><code>codec</code> = 0 (VADPCM), <code>order</code> = 2,
      <code>n_predictors</code> = 2, <code>loop.count</code> = 0 (no looping);
      <code>loop.end</code> equals the decoded sample count.</li>
  <li>Every instrument shares the same envelope: <code>(1,0) (1,32700) (-1,0)</code> —
      one envelope step at zero, one step rising to 32700/32768, then hang
      (<code>-1</code> = <code>ADSR_HANG</code>) until the note is released, then fade at
      <code>adsr_decay_table[250]</code>. Effectively: instant attack, full sustain,
      fast release.</li>
  <li>Durations run 32–407&nbsp;ms (mean ≈ 189&nbsp;ms). The longest samples are the
      editor's letter-name block — "zero" at 407&nbsp;ms is a whole word.</li>
  <li>The three banks' samples tile wave bank 5 contiguously and share nothing:
      156 owns <code>0x341130–0x385890</code>, 157 <code>0x385890–0x3AAAAC</code>,
      158 <code>0x3AAAB0–0x3ED612</code> — each voice is a separate recording session.
      (This is also what pins <code>sample</code> 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.)</li>
</ul>

<h2>6. Decoding VADPCM</h2>

<p>Each frame is 9 bytes → 16 samples. Total samples = <code>size / 9 × 16</code>.</p>

<pre>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</pre>

<p>The codebook is stored as <code>order × 8</code> signed 16-bit coefficients per
predictor. Expand each predictor into an 8-row × (order+8)-column matrix
<code>M</code>:</p>

<pre>M[row][col]          for col &lt; order:   book[predictor][col*8 + row]
M[row][order + i]    = 2048                     if i == row
                     = M[row − i − 1][order−1]  if i &lt; row
                     = 0                        if i &gt; row</pre>

<p>Decode two 8-sample halves per frame. Each half takes an input vector of the previous
<code>order</code> (= 2) <em>decoded</em> samples followed by that half's 8
<em>scaled residuals</em>:</p>

<pre>in = [ out[-2], out[-1], r[0..7] ]
out[row] = floor( Σ  M[row][k] · in[k]  / 2048 )      for row 0..7</pre>

<p>Initial state is two zero samples; state carries across frames for the length of the
sample. The division is a <strong>floor</strong> (round toward −∞), not truncation.</p>

<div class="note">
<strong>The one easy mistake</strong> is feeding decoded outputs back per-sample inside a
half — i.e. running <code>out[n] = r[n] + (c₁·out[n−1] + c₂·out[n−2]) / 2048</code>
with rounding at every step. The matrix form convolves the <em>scaled residuals</em>
and rounds once per output sample; cascading the rounding decodes audibly noisy audio.
</div>

<p>Validation for this exact algorithm against this exact image: instrument 0 of bank
156 decodes to 3,328 samples (equal to its <code>loop.end</code>), peak amplitude 19,213,
nothing out of s16 range, and a smooth vowel envelope.</p>

<h2>7. The voice sequences, disassembled</h2>

<p>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:</p>

<pre>; ---- 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 &lt; 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 &lt;inst&gt;      VOICE_SET: instrument = code − 1
88 &lt;addr16&gt;    NOTE0_SET_PC: start note layer at addr
ff             return to the spin loop
; layer:
&lt;note&gt; &lt;delay&gt; &lt;velocity&gt; ff       ; e.g. 6c 0d 6e ff</pre>

<p>Semantics that matter for a reimplementation:</p>

<ul>
  <li><strong>The port-1 handshake.</strong> Every iteration the subtrack publishes its
      identity (1/2/3) to its port 1. This is what
      <code>Nap_ReadSubPort(VOICE_GROUP, 0, 1) == sou_now_voice_seq</code> polls — the
      load-race gate on <a href="gating.html">the gating page</a>. It is the
      <em>only</em> byte that differs between the three sequences' programs.</li>
  <li><strong>Port reads are destructive.</strong> <code>MACRO_READ_PORT0</code> returns
      the value and resets the port to −1, and both the branch (<code>0xF2</code> branches
      on negative) and <code>DYNTBL_CALL</code> (skips −1) treat −1 as "empty". So each
      <code>Sou_VoiceStart</code> write triggers exactly one routine, at most one per
      tick per subtrack.</li>
  <li><strong>Note command decode</strong> (large-note mode, opcode &lt; 0xC0):
      commands <code>0x40–0x7F</code> are note-on with
      <code>semitone = opcode − 0x40</code>, then a varint delay and a velocity byte,
      gate&nbsp;0. Volume contribution is <code>(velocity / 127)²</code>.
      <span class="src"><code>internal/track.c:1028 __SetNote</code></span></li>
  <li><strong>Pitch.</strong> <code>frequency_scale = PITCHTABLE[semitone] ×
      tuning</code>, where <code>PITCHTABLE[i] = 2^((i − 48) / 12)</code> — unity at 48.
      <span class="src"><code>internal/audiotable.c:321</code>,
      <code>internal/track.c:975</code></span></li>
  <li><strong>Ticks.</strong> One sequence tick ≈ 9.9&nbsp;ms: ticks fire at
      <code>240 updates/s × tempo / max_tempo</code> with
      <code>tempo = 120 × 48 = 5760</code> and <code>max_tempo = 13726</code>
      (from <code>60000 × 48 × 4 / 48 / 16.713 / 1.04613</code>,
      <code>internal/memory.c:998</code>) — ≈ 100.7 ticks/s. When the layer's delay
      expires the note releases through <code>adsr_decay_table[250]</code>.</li>
</ul>

<h3>The complete per-code parameter map</h3>

<p>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.</p>

<table>
<tr><th>Seq</th><th>Codes</th><th>Instrument</th><th>Semitone</th><th>Delay (ticks)</th><th>Velocity</th><th>Extras</th></tr>
<tr><td rowspan="5">243</td>
    <td><code>0x01–0x38</code></td><td>code − 1</td><td>44</td><td>13</td><td>110</td><td></td></tr>
<tr><td><code>0x39–0x52</code></td><td>code − 1</td><td>44</td><td>15</td><td>110</td><td></td></tr>
<tr><td><code>0x53–0x76</code></td><td>code − 1</td><td>44</td><td>28</td><td>95</td><td>pitch sweep from semitone 41 (portamento <code>c7 81 29 ff</code>)</td></tr>
<tr><td><code>0x77</code></td><td>0x76</td><td>44</td><td>20</td><td>110</td><td></td></tr>
<tr><td><code>0x78–0x7F</code></td><td colspan="4">silence (routine is an immediate <code>ff</code>)</td><td></td></tr>
<tr><td rowspan="4">244</td>
    <td><code>0x01–0x38</code></td><td>code − 1</td><td>41</td><td>16</td><td>110</td><td></td></tr>
<tr><td><code>0x39–0x52</code></td><td>code − 1</td><td>39</td><td>20</td><td>110</td><td></td></tr>
<tr><td><code>0x77</code></td><td><strong>0x52</strong></td><td>41</td><td>20</td><td>110</td><td>the <code>you</code> substitute — see below</td></tr>
<tr><td><code>0x53–0x76</code>, <code>0x78–0x7F</code></td><td colspan="4">silence</td><td></td></tr>
<tr><td rowspan="4">245</td>
    <td><code>0x01–0x38</code></td><td>code − 1</td><td>39</td><td>17</td><td>110</td><td></td></tr>
<tr><td><code>0x39–0x52</code>, <code>0x77</code></td><td>code − 1</td><td>39</td><td>20</td><td>110</td><td></td></tr>
<tr><td><code>0x53–0x76</code></td><td>code − 1</td><td>44</td><td>28</td><td>95</td><td>pitch sweep from semitone 41</td></tr>
<tr><td><code>0x78–0x7F</code></td><td colspan="4">silence</td><td></td></tr>
</table>

<p>Two things this table reveals that the C code cannot:</p>

<ul>
  <li><strong>The editor letter names have a rising intonation.</strong> Codes
      <code>0x53–0x76</code> 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.</li>
  <li><strong>Spec 3's <code>you</code> is deliberate, not clamped.</strong> Sequence
      244 maps code <code>0x77</code> to instrument <code>0x52</code> — bank 157's 83rd
      and last instrument, which sits <em>above</em> the phonology range
      (<code>0x01–0x52</code>) and is reachable by nothing else. Bank 157 carries one
      extra instrument specifically to voice <code>you</code>.</li>
</ul>

<h2>8. Reassembling audible animalese</h2>

<p>For one phoneme code <em>v</em> spoken by spec <em>s</em>:</p>

<pre>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</pre>

<p>Practically: decoding the sample and playing it at
<code>22050 × 2^((semitone − 48 + 12·log₂(pitch_mults))/12)</code>&nbsp;Hz with a
<code>(velocity/127)²</code> 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 <a href="scheduling.html">scheduler</a>, not from the audio
engine — new syllables do not cut old ones off, they land on the alternating other
subtrack and overlap.</p>

<div class="note">
<strong>Scope.</strong> These offsets are for the USA rev 0 image (<code>GAFE01</code>,
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.
</div>

<div class="note">
<strong>NKit-shrunk images work unmodified.</strong> An NKit-shrunk rip (marker
<code>"NKIT v01"</code> at disc offset <code>0x200</code>; original size as a big-endian
u32 at <code>0x210</code>) strips the ~1.4&nbsp;GB of junk padding <em>after</em> the
real data without relocating it — and Animal Crossing's 11 files all end by
<code>0xE11470</code>, 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.
</div>

<div class="pager">
  <span><a href="editor.html">← 7. The editor path</a></span>
  <span><a href="quirks.html">9. Quirks &amp; corrections →</a></span>
</div>

</body>
</html>