diff options
| author | Alexander Kassouni <alex@kassouni.net> | 2026-09-08 20:11:06 -0700 |
|---|---|---|
| committer | Alexander Kassouni <alex@kassouni.net> | 2026-09-08 20:11:06 -0700 |
| commit | 9ca1a1c1c7bf56e39b516c299b4e0d3efe59652b (patch) | |
| tree | 7e41c6f8424fbd021b515f6a95deffda61b4a142 /docs/disc.html | |
| parent | 24ec2a92b56ca4296fe65b70524e36a8e1c67c2e (diff) | |
better docs
Diffstat (limited to 'docs/disc.html')
| -rw-r--r-- | docs/disc.html | 413 |
1 files changed, 413 insertions, 0 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 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 < 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 Hz and the mixer runs at 32000 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 ms (mean ≈ 189 ms). The longest samples are the + editor's letter-name block — "zero" at 407 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 < 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</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 < 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</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 < 0xC0): + commands <code>0x40–0x7F</code> are note-on with + <code>semitone = opcode − 0x40</code>, then a varint delay and a velocity byte, + gate 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 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> 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 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 & corrections →</a></span> +</div> + +</body> +</html> |