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 | |
| parent | 24ec2a92b56ca4296fe65b70524e36a8e1c67c2e (diff) | |
better docs
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 107 | ||||
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | docs/disc.html | 413 | ||||
| -rw-r--r-- | docs/editor.html | 31 | ||||
| -rw-r--r-- | docs/encoding.html | 3 | ||||
| -rw-r--r-- | docs/index.html | 30 | ||||
| -rw-r--r-- | docs/playback.html | 47 | ||||
| -rw-r--r-- | docs/quirks.html | 29 | ||||
| -rw-r--r-- | docs/reference.html | 74 | ||||
| -rw-r--r-- | docs/scheduling.html | 4 | ||||
| -rw-r--r-- | docs/soundfont.html | 20 | ||||
| -rw-r--r-- | src/iso/fs.rs | 7 | ||||
| -rw-r--r-- | src/iso/loader.rs | 25 | ||||
| -rw-r--r-- | src/iso/mod.rs | 3 | ||||
| -rw-r--r-- | src/main.rs | 13 |
16 files changed, 740 insertions, 71 deletions
@@ -1,3 +1,4 @@ /target /result /.direnv +*.iso @@ -6,7 +6,11 @@ version = 4 name = "animalese" version = "0.1.0" dependencies = [ + "byteorder", "clap", + "digest-io", + "hex-literal", + "sha2", ] [[package]] @@ -60,6 +64,27 @@ dependencies = [ ] [[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] name = "clap" version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -106,18 +131,83 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "digest-io" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de63d600bc7fab91180bc17385f29b342468dc8ef2af09dceba450a293de3da" +dependencies = [ + "digest", +] + +[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -142,6 +232,17 @@ dependencies = [ ] [[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -159,6 +260,12 @@ dependencies = [ ] [[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4,4 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] +byteorder = "1.5.0" clap = { version = "4.6.6", features = ["derive"] } +digest-io = "0.1.0" +hex-literal = "1.1.0" +sha2 = "0.11.0" 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> 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() >> 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 & 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 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 & 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 & 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 % linked) and against a dumped <code>GAFE01</code> USA rev 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 < 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 <instrument> / 88 <note> / 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 <instrument> / 88 <layer addr> / 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 Hz at a 32000 Hz mixer), order 2, two predictors, no loop, +32–407 ms each (mean ≈ 190 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 & corrections — Animalese</title> +<title>9. Quirks, dead code & corrections — Animalese</title> <link rel="stylesheet" href="style.css"> </head> <body> -<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 8. Quirks & corrections</nav> +<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 9. Quirks & corrections</nav> -<h1>8. Quirks, dead code & corrections</h1> +<h1>9. Quirks, dead code & 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>​: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>​:1028</code></td><td><code>__SetNote</code> — note byte → semitone/delay/velocity</td></tr> +<tr><td><code>​:780</code></td><td><code>__SetVoice</code> — <code>frequency_scale = PITCHTABLE[semitone] × tuning</code>, portamento</td></tr> +<tr><td><code>​:1746</code></td><td><code>MACRO_READ_PORT0</code> — destructive port read (resets to −1)</td></tr> +<tr><td><code>​: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 & corrections</a></span> + <span><a href="quirks.html">← 9. Quirks & 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 << 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 << 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> diff --git a/src/iso/fs.rs b/src/iso/fs.rs new file mode 100644 index 0000000..1f90851 --- /dev/null +++ b/src/iso/fs.rs @@ -0,0 +1,7 @@ +#[repr(C)] +pub struct FileTableEntry { + flags: u8, + name_offset: u24, // pointer to string table + offset: u32, + length: u32, +} diff --git a/src/iso/loader.rs b/src/iso/loader.rs new file mode 100644 index 0000000..b8c61da --- /dev/null +++ b/src/iso/loader.rs @@ -0,0 +1,25 @@ + +use std::{path::Path}; +use sha2::{Sha256}; +use std::fs::File; +use hex_literal::hex; +use digest_io::HashReader; +use std::io::BufReader; + +pub struct GameIso { + filepath: bool +} + +impl GameIso { + pub fn new<P: AsRef<Path>>(path: P) -> () { + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + + let mut hasher: HashReader<Sha256, BufReader<File>> = HashReader::<Sha256, BufReader<File>>::new(reader); + hasher.hash_to_end(); + let hash = hasher.finalize(); + let s: String = hash.iter().map(|b| format!("{:02x}", b)).collect(); + + assert!(hash == hex!("a08ad2654831ab298071bdcdf727945efcfdd50d2b0e3512a3d361ee7b18296d")); + } +} diff --git a/src/iso/mod.rs b/src/iso/mod.rs new file mode 100644 index 0000000..2d546d9 --- /dev/null +++ b/src/iso/mod.rs @@ -0,0 +1,3 @@ +mod loader; + +pub use loader::GameIso; diff --git a/src/main.rs b/src/main.rs index 44d0887..573b8db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,27 @@ +use std::path::PathBuf; use clap::Parser; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { #[arg(short, long)] - volume: f64, + volume: Option<f64>, #[arg(short, long)] - timbre: f64, + timbre: Option<f64>, + + #[arg(long, value_name = "CISO filepath")] + iso_path: PathBuf, } mod phonemes; mod tokenizer; +mod iso; fn main() { - let _args = Args::parse(); + let args = Args::parse(); + + let game = iso::GameIso::new(args.iso_path); println!("Hello, world!"); } |