aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/disc.html413
-rw-r--r--docs/editor.html31
-rw-r--r--docs/encoding.html3
-rw-r--r--docs/index.html30
-rw-r--r--docs/playback.html47
-rw-r--r--docs/quirks.html29
-rw-r--r--docs/reference.html74
-rw-r--r--docs/scheduling.html4
-rw-r--r--docs/soundfont.html20
9 files changed, 583 insertions, 68 deletions
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 @@
+<!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>
diff --git a/docs/editor.html b/docs/editor.html
index 8e46242..22349d5 100644
--- a/docs/editor.html
+++ b/docs/editor.html
@@ -72,7 +72,7 @@ typing '0' → Sou_VoiceStart(subtrack, 0x53) → instrument 0x52</pre>
<tr><th>Code range</th><th>Instruments</th><th>Reached by</th></tr>
<tr><td><code>0x01</code>–<code>0x52</code></td><td>0–81</td><td>the phonology engine — short/long vowels, consonants, all 52 two-character digraphs plus <code>you</code></td></tr>
<tr><td><code>0x01</code>–<code>0x40</code></td><td></td><td>also the anti-repetition random substitution (<code>rand() &gt;&gt; 2</code>)</td></tr>
-<tr><td><code>0x53</code>–<code>0x76</code></td><td>82–117</td><td><strong>only mode 1</strong> — one entry per digit and letter, in order</td></tr>
+<tr><td><code>0x53</code>–<code>0x76</code></td><td>82–117</td><td><strong>only mode 1</strong> — one entry per digit and letter, in order (on bank 157, instrument 82 is instead the <code>you</code> substitute, see below)</td></tr>
<tr><td><code>0x77</code></td><td>118</td><td>the <code>you</code> digraph, and nothing else</td></tr>
</table>
@@ -92,24 +92,31 @@ phonology-only bank — and that is exactly what it is.
<h2>Why bank 157 is short</h2>
<p>Spec 3 (Wendell, Pete, Booker, Jack) uses sequence 244 / bank 157, which has 83
-instruments — codes <code>0x01</code>–<code>0x53</code>. That covers the entire phonology
-output range (<code>≤ 0x52</code>) with one to spare, and omits the letter-name block
-that spec 3 can never reach, since the editor is always spec 5 or 6.</p>
+instruments — one more than the phonology output range
+(<code>0x01</code>–<code>0x52</code> → instruments 0–81) — and omits the letter-name
+block that spec 3 can never reach, since the editor is always spec 5 or 6.</p>
-<p>The one real casualty is <code>you</code> (<code>0x77</code>): the sequence's dynamic
-table clamps out-of-range entries to the last routine, so a spec-3 voice saying "you"
-plays instrument 82 instead. That is one wrong syllable on a common word, in four
-NPCs' voices.</p>
+<p>The 83rd instrument is not spare. Sequence 244's dynamic table maps codes
+<code>0x53</code>–<code>0x76</code> to explicit silence and maps <code>you</code>
+(<code>0x77</code>) to <strong>instrument 82</strong> — a sample reachable through no
+other path, played at a lower note than the other two voices'
+<code>you</code> (semitone 41 vs 44; see <a href="disc.html">the disc page</a>). So bank
+157 carries a dedicated <code>you</code> substitute, and spec-3 voices lack nothing
+audible. Whether instrument 82 is an actual "you" recording is testable the same way as
+the letter names: extract it and listen.</p>
<div class="note">
-This corrects an earlier reading of <code>extraction.md</code> §2.4, which described bank 157
-as "collapsing every digraph sample above <code>0x53</code> onto one sound". Nothing above
-<code>0x53</code> is a digraph — the block is not phonology at all.
+This corrects two earlier readings. Working notes once described bank 157 as
+"collapsing every digraph sample above <code>0x53</code> onto one sound" — nothing above
+<code>0x53</code> is a digraph, the block is not phonology at all. A later revision of
+these pages then claimed the dynamic table "clamps out-of-range entries to the last
+routine" — it does not; the mapping of <code>0x77</code> → instrument 82 is an explicit
+table entry, verified on disc.
</div>
<div class="pager">
<span><a href="soundfont.html">← 6. The soundfont lookup</a></span>
- <span><a href="quirks.html">8. Quirks &amp; corrections →</a></span>
+ <span><a href="disc.html">8. Walking the disc image →</a></span>
</div>
</body>
diff --git a/docs/encoding.html b/docs/encoding.html
index 8b57261..bc5bcc7 100644
--- a/docs/encoding.html
+++ b/docs/encoding.html
@@ -42,6 +42,9 @@ diverges everywhere else. Notable non-ASCII placements:</p>
return -1; // no sound, and no state change
return voice_array[code];</pre>
+<p>The full 256-byte array is reproduced verbatim in the
+<a href="reference.html">reference tables</a>; this page describes its shape.</p>
+
<p>Those are the only two entries in the array holding <code>0xFF</code>, so
<code>0xFF</code> is never actually returned from this function — it arrives at
<code>Na_VoiceSe</code> only as a padding value for the unused <code>b</code>/<code>c</code>
diff --git a/docs/index.html b/docs/index.html
index 7017da5..86f2f9f 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -37,7 +37,7 @@ playback
│ 6. Sou_VoiceStart() write port 0 of a JAudio2 subtrack
│ sequence dyntbl ──► instrument (code − 1)
▼ 7. ProgToVp(bank, inst) ──► NoteToVoice() ──► smzwavetable
-DSP ──► one ~190 ms VADPCM sample, 22050 Hz
+DSP ──► one 30–400 ms VADPCM sample, 22050 Hz, pitched down 4–9 semitones
</pre>
<p>A phoneme code is literally a <strong>soundfont instrument number plus one</strong>. The whole
@@ -75,27 +75,33 @@ entirely from the samples.</p>
<span>Mode&nbsp;1 — an undocumented third calling convention that bypasses
phonology entirely, and what it says about samples 0x53–0x76.</span></li>
- <li><a href="quirks.html">8. Quirks, dead code &amp; corrections</a>
+ <li><a href="disc.html">8. Walking the disc image</a>
+ <span>CISO, the GameCube filesystem, the DOL, the bank blobs, the VADPCM
+ decoder, and the voice sequences disassembled — every offset needed to
+ extract and replay the samples from a dump.</span></li>
+
+ <li><a href="quirks.html">9. Quirks, dead code &amp; corrections</a>
<span>The dead volume store, the tautological dedup, vestigial mouth-shape
tracking, and corrections to earlier notes.</span></li>
- <li><a href="reference.html">9. Reference tables</a>
- <span>Everything in one place: voice codes, digraphs, phoneme tables,
- control codes, source line index.</span></li>
+ <li><a href="reference.html">10. Reference tables</a>
+ <span>Everything in one place: the full voice table, digraphs, phoneme
+ tables, control codes, disc offsets, source line index.</span></li>
</ul>
<h2>Scope</h2>
-<p>These pages document the <em>runtime</em> pipeline as decompiled. Extracting the
-actual audio samples from a disc image is covered separately in
-<code>../extraction.md</code>, which also carries the audio table-of-contents,
-bank/sample formats, and VADPCM decoding notes.</p>
+<p>Pages 1–7 document the <em>runtime</em> pipeline as decompiled. Page 8 documents the
+<em>data</em>: how to walk a dumped disc image down to individual VADPCM frames, with
+the audio table-of-contents, bank/sample formats, sequence disassembly and decoding
+notes. Together they are intended to be a complete reference for a reimplementation.</p>
<div class="note">
<strong>No game assets here.</strong> Everything on these pages is derived from decompiled
-source. Reproducing audible animalese needs a legally dumped copy of the game.
-</div>
+source and structural analysis. Reproducing audible animalese needs a legally dumped
+copy of the game.</div>
-<p class="src">Verified against ac-decomp at commit <code>09ca8e8b</code> (DOL and REL 100 % linked).</p>
+<p class="src">Verified against ac-decomp at commit <code>09ca8e8b</code> (DOL and REL
+100&nbsp;% linked) and against a dumped <code>GAFE01</code> USA rev&nbsp;0 disc image.</p>
</body>
</html>
diff --git a/docs/playback.html b/docs/playback.html
index 73be9a2..4addb30 100644
--- a/docs/playback.html
+++ b/docs/playback.html
@@ -118,16 +118,25 @@ r29 != 0x84</code>).</p>
<p>Each voice sequence's subtrack sits in a spin loop:</p>
-<pre>L_0019: delay 1 frame
- macro_read_port0
- branch_rel_not_eq_zero L_0019 ; wait until Sou_VoiceStart writes the port
- set_dyntbl D_0026
- dyntbl_call ; index the table with the phoneme code
- branch L_0019</pre>
+<pre>loop: delay 1 tick
+ MACRO_SET 1 ; publish "I am sequence 1/2/3" …
+ PORT1_WRITE_MACRO_REG ; … to port 1 (the load-race handshake)
+ MACRO_READ_PORT0 ; value ← port 0, port 0 ← -1
+ branch loop if value &lt; 0 ; port empty
+ SET_DYNTBL 0x0026
+ DYNTBL_CALL ; call routine dyntbl[phoneme code]
+ branch loop</pre>
-<p>The dynamic table has 128 entries pointing at 10-byte routines
-<code>C1 &lt;instrument&gt; / 88 &lt;note&gt; / FF</code>. Entry <em>v</em> selects instrument
-<strong><em>v</em> − 1</strong>. Entry 0 is an immediate <code>FF</code> — nothing.</p>
+<p>The dynamic table has 128 entries pointing at routines of the shape
+<code>C1 &lt;instrument&gt; / 88 &lt;layer addr&gt; / FF</code>, where the note layer holds the
+actual note, delay and velocity. Entry <em>v</em> selects instrument
+<strong><em>v</em> − 1</strong>; entry 0 is an immediate <code>FF</code> — nothing. The
+full annotated disassembly, the per-code note/delay/velocity map, and the tick math are
+on <a href="disc.html">the disc page</a>. Two headlines from it: dialogue phonemes play
+at semitone 44 / 41 / 39 (sequences 243 / 244 / 245) against a pitch table with unity at
+48, so each bank is pitched down 4–9 semitones from its recordings; and port reads are
+destructive (the port resets to −1), which is why one <code>Sou_VoiceStart</code> write
+triggers exactly one syllable.</p>
<h3>Per-frame parameter push</h3>
@@ -177,15 +186,19 @@ deferred syllable's parameters land one frame late.</p>
bank id (5 for all three), and all have zero drums and zero SFX. 119 instruments =
phoneme codes <code>0x01</code>–<code>0x77</code> exactly.</p>
-<p>Bank 157 stopping at 83 is not an omission: instruments 0–82 cover codes
-<code>0x01</code>–<code>0x53</code>, and the phonology engine never produces a code above
-<code>0x52</code> apart from <code>0x77</code>. The missing range
-<code>0x53</code>–<code>0x76</code> is the <a href="editor.html">editor's letter-name block</a>,
-which spec 3 can never reach. The only thing spec-3 voices genuinely lack is
-<code>you</code>.</p>
+<p>Bank 157 stopping at 83 is not an omission, and nothing is lost. The phonology
+engine's maximum output is <code>0x52</code> plus <code>0x77</code>; instruments 0–81
+cover <code>0x01</code>–<code>0x52</code>, and the missing
+<code>0x53</code>–<code>0x76</code> range is the
+<a href="editor.html">editor's letter-name block</a>, which spec 3 can never reach.
+For <code>you</code> (<code>0x77</code>), sequence 244's dynamic table explicitly routes
+the code to <strong>instrument 82</strong> — the bank's 83rd and last instrument, which
+sits above the phonology range and is reachable by nothing else. Bank 157 carries one
+extra instrument precisely to be its <code>you</code>.</p>
-<p>Sample details are in <code>../extraction.md</code>: N64 VADPCM, tuning 0.6891
-(22050 Hz), order 2, two predictors, no loop, ~190 ms each.</p>
+<p>Sample details are on <a href="disc.html">the disc page</a>: N64 VADPCM, tuning
+0.689062 (22050&nbsp;Hz at a 32000&nbsp;Hz mixer), order 2, two predictors, no loop,
+32–407&nbsp;ms each (mean ≈ 190&nbsp;ms).</p>
<div class="pager">
<span><a href="phonology.html">← 4. The phonology engine</a></span>
diff --git a/docs/quirks.html b/docs/quirks.html
index c46fdb2..b9b1246 100644
--- a/docs/quirks.html
+++ b/docs/quirks.html
@@ -3,14 +3,14 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>8. Quirks, dead code &amp; corrections — Animalese</title>
+<title>9. Quirks, dead code &amp; corrections — Animalese</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
-<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 8. Quirks &amp; corrections</nav>
+<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 9. Quirks &amp; corrections</nav>
-<h1>8. Quirks, dead code &amp; corrections</h1>
+<h1>9. Quirks, dead code &amp; corrections</h1>
<p class="lede">Things a faithful port has to reproduce, things a clean reimplementation
should drop, and where earlier notes were wrong.</p>
@@ -102,7 +102,9 @@ every neighbour is 0–5.</p>
<h2>Corrections to earlier notes</h2>
-<p>The following supersede statements in <code>../extraction.md</code> as originally written.</p>
+<p>The following supersede statements from earlier working notes
+(<code>extraction.md</code>, since retired — its surviving content lives on
+<a href="disc.html">the disc page</a>).</p>
<table>
<tr><th>Was</th><th>Actually</th></tr>
@@ -110,9 +112,10 @@ every neighbour is 0–5.</p>
<tr><td>"Bank 157 has only 83 instruments … that voice collapses every digraph sample
above <code>0x53</code> onto one sound."</td>
<td>Nothing above <code>0x53</code> is a digraph. The phonology engine's maximum output is
-<code>0x52</code>, plus <code>0x77</code>. Bank 157 covers the phonology range exactly; the
-missing block is the <a href="editor.html">editor's letter names</a>, unreachable at spec 3.
-The only real loss is <code>you</code>.</td></tr>
+<code>0x52</code>, plus <code>0x77</code>. Bank 157 covers the phonology range with one
+instrument to spare, the missing block is the <a href="editor.html">editor's letter
+names</a> (unreachable at spec 3), and nothing is lost: sequence 244 explicitly maps
+<code>you</code> to that 83rd instrument — a dedicated substitute, not a clamp.</td></tr>
<tr><td>"43 of the 119 codes are unreachable through normal text … only ever hit by the
anti-repetition random substitution."</td>
@@ -144,13 +147,15 @@ the config. See <a href="gating.html">gating</a>.</td></tr>
<a href="editor.html">the editor path</a>.</td></tr>
</table>
-<p>Everything else in <code>extraction.md</code> §1 was re-verified against the current tree
-and holds: the voice table, the 53 digraph entries, both vowel tables, the per-spec
-timbre values, and the spec→sequence→bank mapping.</p>
+<p>Everything else in the retired notes was re-verified against the current tree and
+holds: the voice table, the 53 digraph entries, both vowel tables, the per-spec timbre
+values, and the spec→sequence→bank mapping. The table contents, bank layouts and
+sequence bytecode were additionally verified byte-for-byte against a dumped disc image
+(see <a href="disc.html">the disc page</a>).</p>
<div class="pager">
- <span><a href="editor.html">← 7. The editor path</a></span>
- <span><a href="reference.html">9. Reference tables →</a></span>
+ <span><a href="disc.html">← 8. Walking the disc image</a></span>
+ <span><a href="reference.html">10. Reference tables →</a></span>
</div>
</body>
diff --git a/docs/reference.html b/docs/reference.html
index 5b86ebb..1abf660 100644
--- a/docs/reference.html
+++ b/docs/reference.html
@@ -3,17 +3,42 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>9. Reference tables — Animalese</title>
+<title>10. Reference tables — Animalese</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
-<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 9. Reference tables</nav>
+<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 10. Reference tables</nav>
-<h1>9. Reference tables</h1>
+<h1>10. Reference tables</h1>
<p class="lede">Everything needed to reimplement the pipeline, plus an index into the
decomp.</p>
+<h2>The character → voice code table</h2>
+
+<p><code>mMsg_sound_voice_get</code>'s full 256-byte array, indexed by the game character
+code (row × 16 + column). <code>0x7F</code> and <code>0x80</code> hold <code>0xFF</code>
+but the function returns −1 for them before the lookup.</p>
+
+<pre> x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF
+0x00 85 85 5D 5D 5D 5D 5D 5D 5F 61 61 61 61 65 65 65
+0x10 65 60 6A 6B 6B 6B 6B 6B 85 71 71 71 71 5E 6C 5D
+0x20 81 83 85 5D 5D 85 85 85 85 85 85 85 85 85 85 80
+0x30 53 54 55 56 57 58 59 5A 5B 5C 85 85 85 85 85 82
+0x40 85 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B
+0x50 6C 6D 6E 6F 70 71 72 73 74 75 76 5D 85 5D 5D 85
+0x60 5F 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B
+0x70 6C 6D 6E 6F 70 71 72 73 74 75 76 61 61 61 61 FF
+0x80 FF 65 65 65 65 86 60 6A 6B 6B 6B 6B 6B 85 71 71
+0x90 85 71 71 75 75 6C 75 85 85 5D 6B 85 85 85 85 85
+0xA0 85 85 85 85 85 85 85 85 85 85 85 85 85 85 85 85
+0xB0 85 85 85 85 85 85 85 85 85 85 85 85 85 85 85 85
+0xC0 85 85 85 85 85 85 85 85 85 85 85 85 85 85 85 85
+0xD0 85 85 81 81 85 85 85 85 85 85 85 85 85 85 85 85
+0xE0 85 85 85 85 85 85 85 85 85 85 85 85 85 85 85 85
+0xF0 85 85 85 85 85 85 85 85 85 85 85 85 85 85 85 81</pre>
+<p class="src">Source: <code>src/game/m_msg_sound.c_inc:2</code>, verified verbatim.</p>
+
<h2>Voice codes</h2>
<table>
@@ -118,6 +143,32 @@ nothing at all.</p>
<tr><td>9</td><td>243</td><td>156</td><td>0.70</td><td>0.65</td><td>Farley</td></tr>
</table>
+<h2>Per-code playback parameters</h2>
+
+<p>What each phoneme code actually plays — instrument, semitone, note delay, velocity,
+per sequence — is tabulated on <a href="disc.html">the disc page</a>, extracted from the
+shipped sequence data. The headline constants: dialogue phonemes play at semitone 44
+(seq 243), 41/39 (seq 244) or 39 (seq 245) against unity-at-48
+(<code>PITCHTABLE[i] = 2^((i−48)/12)</code>), velocity 110 (gain
+<code>(110/127)² ≈ 0.75</code>); editor letter names at semitone 44 with a rising sweep
+from 41, velocity 95; all samples tuned 0.689062 = 22050/32000.</p>
+
+<h2>Disc image quick reference (GAFE01 USA rev 0)</h2>
+
+<table>
+<tr><th>Thing</th><th>Where</th></tr>
+<tr><td>main DOL</td><td>disc <code>0x1E000</code> (from header <code>0x420</code>)</td></tr>
+<tr><td>FST</td><td>disc <code>0xFE500</code>, size <code>0x102</code> (from header <code>0x424/0x428</code>)</td></tr>
+<tr><td><code>/audiorom.img</code></td><td>disc <code>0x626D10</code>, size <code>0x7EA760</code></td></tr>
+<tr><td>the five audio tables</td><td>DOL <code>.data5</code>; VAs <code>0x800CCA40</code> (seq), <code>0x800CD9E0</code> (bank), <code>0x800CE3E0</code> (wave), <code>0x800CE450</code> (data), <code>0x800CE490</code> (map)</td></tr>
+<tr><td>seq 243 / 244 / 245</td><td>disc <code>0x6D2770</code> / <code>0x6D2DD0</code> / <code>0x6D3250</code>, sizes <code>0x660</code> / <code>0x480</code> / <code>0x660</code></td></tr>
+<tr><td>bank 156 / 157 / 158</td><td>disc <code>0x752670</code> / <code>0x756B70</code> / <code>0x759B90</code></td></tr>
+<tr><td>wave bank 5</td><td>disc <code>0xA23E50</code>, size <code>0x3ED620</code>; <code>smzwavetable.sample</code> is relative to this</td></tr>
+</table>
+
+<p>Full derivations, structure layouts and the VADPCM decoder are on
+<a href="disc.html">the disc page</a>.</p>
+
<h2>Text control codes that affect voice</h2>
<table>
@@ -208,8 +259,23 @@ nothing at all.</p>
<tr><td><code>&#8203;:1399</code></td><td><code>SUBTRACK_CMD_SET_INSTRUMENT_BANK</code> — reverse bank-list index</td></tr>
</table>
+<h3>Sequence playback internals</h3>
+<table>
+<tr><td><code>include/jaudio_NES/audiocommon.h:405</code></td><td>common / group / subtrack / note command opcodes</td></tr>
+<tr><td><code>internal/track.c:543</code></td><td><code>Nas_NoteSeq</code> — note layer interpreter</td></tr>
+<tr><td><code>&#8203;:1028</code></td><td><code>__SetNote</code> — note byte → semitone/delay/velocity</td></tr>
+<tr><td><code>&#8203;:780</code></td><td><code>__SetVoice</code> — <code>frequency_scale = PITCHTABLE[semitone] × tuning</code>, portamento</td></tr>
+<tr><td><code>&#8203;:1746</code></td><td><code>MACRO_READ_PORT0</code> — destructive port read (resets to −1)</td></tr>
+<tr><td><code>&#8203;:1877</code></td><td><code>GRP_CMD_SET_TEMPO</code> — tempo = byte × 48 tatums</td></tr>
+<tr><td><code>internal/audiotable.c:321</code></td><td><code>PITCHTABLE</code> — <code>2^((i−48)/12)</code>, unity at 48</td></tr>
+<tr><td><code>internal/memory.c:998</code></td><td><code>max_tempo</code> derivation (tick rate)</td></tr>
+<tr><td><code>internal/audioconst.c:32</code></td><td><code>AGC</code> — <code>timeBase = 48</code></td></tr>
+<tr><td><code>internal/dummyrom.c:68</code></td><td><code>Jac_InitARAM</code> — opens <code>/audiorom.img</code></td></tr>
+<tr><td><code>internal/neosthread.c:56</code></td><td><code>DVDT_LoadtoARAM("/audiorom.img", …)</code></td></tr>
+</table>
+
<div class="pager">
- <span><a href="quirks.html">← 8. Quirks &amp; corrections</a></span>
+ <span><a href="quirks.html">← 9. Quirks &amp; corrections</a></span>
<span><a href="index.html">Index</a></span>
</div>
diff --git a/docs/scheduling.html b/docs/scheduling.html
index 07f1e52..844b5f8 100644
--- a/docs/scheduling.html
+++ b/docs/scheduling.html
@@ -32,7 +32,7 @@ runs once per game frame while a message is scrolling. Its loop:</p>
</ol>
<p>The flag toggle is the speed control: the index moves on alternate frames, so text
-advances at <strong>one character per two frames</strong>, roughly 33 characters/second
+advances at <strong>one character per two frames</strong> — 30 characters/second
at 60 fps.</p>
<p>Holding <kbd>B</kbd> sets <code>mMsg_STATUS_FLAG_FAST_TEXT</code>, which zeroes
@@ -138,7 +138,7 @@ frame 4 char C entry: a = C
<p>So: one <code>Na_VoiceSe</code> call per two characters, one pair every four frames
(~66 ms), with the deferred second syllable one game frame (~17 ms) behind the first.
-Samples run about 190 ms, so syllables overlap heavily — which is why there are
+Samples average about 190 ms, so syllables overlap heavily — which is why there are
<a href="playback.html">two alternating subtracks</a>.</p>
<div class="pager">
diff --git a/docs/soundfont.html b/docs/soundfont.html
index c07caf9..436071d 100644
--- a/docs/soundfont.html
+++ b/docs/soundfont.html
@@ -178,11 +178,13 @@ NoteToVoice(vtbl, note) channel.c:354
wtstr { smzwavetable*, tuning } → sample, loop, book, codec, size</pre>
<p><code>ProgToVp</code> is the only consumer of the instrument number, and it is a plain
-array index bounded by <code>num_instruments</code>. This is exactly why
-<code>you</code> (instrument 118) cannot work on bank 157's 83 instruments: the bounds
-check fails, <code>audio_error_flags</code> is set to
-<code>0x03000000 + (prog &lt;&lt; 8) + inst</code>, and nothing plays — though in practice the
-sequence's dynamic table has already clamped the index before it gets there.</p>
+array index bounded by <code>num_instruments</code>: on an out-of-range instrument the
+bounds check fails, <code>audio_error_flags</code> is set to
+<code>0x03000000 + (prog &lt;&lt; 8) + inst</code>, and nothing plays. In retail data the
+guard never trips — each sequence's dynamic table only names instruments its own bank
+has. In particular <code>you</code> on bank 157 is not an error case: sequence 244 maps
+code <code>0x77</code> to instrument 82, the last instrument of the bank
+(see <a href="disc.html">the disc page</a>).</p>
<p>Every voice instrument uses only the <em>normal</em> slot, so <code>NoteToVoice</code> is a
formality here.</p>
@@ -205,7 +207,7 @@ formality here.</p>
119 + 83 + 119 instrument slots — point into that one 4 MB wave bank.</p>
<div class="note">
-<strong>Field naming.</strong> <code>../extraction.md</code> §2.2 guessed
+<strong>Field naming.</strong> Earlier working notes guessed
<code>u8 loaded, rangeLo, rangeHi, releaseRate</code> for the instrument header. The decomp
calls them <code>is_relocated</code>, <code>normal_range_low</code>,
<code>normal_range_high</code>, <code>adsr_decay_idx</code>. Same layout, and
@@ -213,9 +215,9 @@ calls them <code>is_relocated</code>, <code>normal_range_low</code>,
the file.
</div>
-<p>Decoding the VADPCM frames themselves is covered in <code>../extraction.md</code> §2.3 —
-including the one easy mistake, which is convolving the decoded outputs instead of the
-scaled residuals.</p>
+<p>Decoding the VADPCM frames themselves is covered on
+<a href="disc.html">the disc page</a> — including the one easy mistake, which is
+convolving the decoded outputs instead of the scaled residuals.</p>
<div class="pager">
<span><a href="playback.html">← 5. Playback</a></span>