diff options
| author | Alexander Kassouni <alex@kassouni.net> | 2026-09-05 17:51:13 -0700 |
|---|---|---|
| committer | Alexander Kassouni <alex@kassouni.net> | 2026-09-05 17:51:13 -0700 |
| commit | 46227e2003de28202cc4e82bca4f7f4745476c4e (patch) | |
| tree | fe450ac33ffa13747613c3cfdf36ea24b3036c46 /docs | |
init
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/editor.html | 116 | ||||
| -rw-r--r-- | docs/encoding.html | 136 | ||||
| -rw-r--r-- | docs/gating.html | 180 | ||||
| -rw-r--r-- | docs/index.html | 101 | ||||
| -rw-r--r-- | docs/phonology.html | 235 | ||||
| -rw-r--r-- | docs/playback.html | 196 | ||||
| -rw-r--r-- | docs/quirks.html | 157 | ||||
| -rw-r--r-- | docs/reference.html | 217 | ||||
| -rw-r--r-- | docs/scheduling.html | 147 | ||||
| -rw-r--r-- | docs/soundfont.html | 226 | ||||
| -rw-r--r-- | docs/style.css | 83 |
11 files changed, 1794 insertions, 0 deletions
diff --git a/docs/editor.html b/docs/editor.html new file mode 100644 index 0000000..8e46242 --- /dev/null +++ b/docs/editor.html @@ -0,0 +1,116 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>7. The editor path — Animalese</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + +<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 7. The editor path</nav> + +<h1>7. The editor path</h1> +<p class="lede">There is a third calling convention into <code>Na_VoiceSe</code> that skips +the phonology engine entirely, and it explains what a third of the voice samples are +for.</p> + +<h2>Mode 1</h2> + +<p>The text editor overlay — used for letters, town and player names, patterns, and +anywhere else the player types — speaks each keystroke from +<code>src/game/m_editor_ovl.c:712</code>:</p> + +<pre>if (command == mED_COMMAND_OUTPUT_CODE) { + int voice = mMsg_sound_voice_get_for_editor(editor_ovl->now_code); + + if (voice == 0x85 || voice == 0x80 || voice == 0x81 || + voice == 0x82 || voice == 0x83 || voice == -1) + voice = 0x86; + + sAdo_VoiceSe(voice, 0xFF, 0xFF, 0, 32, 1); // ← mode 1 +} +else if (command == mED_COMMAND_EXCHANGE_CODE) { // cursor moved onto a character + if (exchange_code != -1 && mED_exchange_se_list[exchange_code]) + sAdo_VoiceSe(mMsg_sound_voice_get_for_editor(exchange_code), 0xFF, 0xFF, 0, 32, 1); + else + sAdo_SysTrgStart(0x36); +}</pre> + +<p>After both filters, only three things survive: letters (<code>0x5D</code>–<code>0x76</code>), +digits (<code>0x53</code>–<code>0x5C</code>), and the click (<code>0x86</code>).</p> + +<h2>What mode 1 does inside Na_VoiceSe</h2> + +<p><code>Na_VoiceSe</code>'s two <code>switch (f)</code> blocks handle only cases 0 and 2. +With <code>f == 1</code> both fall through, so:</p> + +<ul> + <li>no digraph merge, no vowel-length table, no silent e;</li> + <li><code>sou_num_org</code> etc. are never written, so <code>sou_last_num = + sou_num_org</code> further down just rewrites stale values — the message-path + history is not corrupted, but it is not updated either;</li> + <li><code>r28</code> is left uninitialised and never read;</li> + <li>nothing is queued for <code>Sou_SpecialRoutine10</code>;</li> + <li><strong><code>a</code> arrives at the playback stage as the raw voice code.</strong></li> +</ul> + +<p>The editor always runs at spec 5 or 6 (see <a href="gating.html">gating</a>), which +routes <code>0x86</code> to <code>NA_SE_BEBE</code> and passes everything else through. The +final guard is <code>if (a != 0 && a <= 0x77)</code>, and letters and digits both +satisfy it. So:</p> + +<pre>typing 'a' → Sou_VoiceStart(subtrack, 0x5D) → instrument 0x5C +typing 'z' → Sou_VoiceStart(subtrack, 0x76) → instrument 0x75 +typing '0' → Sou_VoiceStart(subtrack, 0x53) → instrument 0x52</pre> + +<h2>What this tells us about the sample set</h2> + +<p>Trace the reachable phoneme codes across the whole engine:</p> + +<table> +<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>0x77</code></td><td>118</td><td>the <code>you</code> digraph, and nothing else</td></tr> +</table> + +<p>The <code>0x53</code>–<code>0x76</code> block is exactly 36 codes: ten digits then +twenty-six letters, in the same order as the voice-code alphabet, and it is +unreachable through any dialogue. The obvious reading is that these 36 samples are the +<strong>spoken names of the digits and letters</strong> — what you hear when typing a name +one character at a time.</p> + +<div class="note"> +<strong>Confidence.</strong> The code path is unambiguous; the interpretation of the samples +as letter names is inference. It is testable: extract instruments 82–117 from bank 156 +and listen. It also predicts that bank 157, which stops at 83 instruments, is a +phonology-only bank — and that is exactly what it is. +</div> + +<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> + +<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> + +<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. +</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> +</div> + +</body> +</html> diff --git a/docs/encoding.html b/docs/encoding.html new file mode 100644 index 0000000..8b57261 --- /dev/null +++ b/docs/encoding.html @@ -0,0 +1,136 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>2. Text → voice code — Animalese</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + +<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 2. Text → voice code</nav> + +<h1>2. Text → voice code</h1> +<p class="lede">A flat 256-entry table turns the game's own character encoding into a +34-value "voice code" alphabet. This is the only place text is looked at.</p> + +<h2>The game's character set</h2> + +<p>Messages are stored one byte per character in a custom encoding +(<code>include/m_font.h</code>). It is ASCII-compatible over exactly three ranges — +<code>0-9</code> at 48–57, <code>A-Z</code> at 65–90, <code>a-z</code> at 97–122 — and +diverges everywhere else. Notable non-ASCII placements:</p> + +<table> +<tr><th>Code</th><th>Character</th><th>Note</th></tr> +<tr><td>0–31</td><td>uppercase accented Latin, <code>¡ ¿ Ç Ð Ñ ß Þ</code></td><td>where ASCII control codes would be</td></tr> +<tr><td>43</td><td><code>♥</code></td><td>where ASCII has <code>+</code></td></tr> +<tr><td>47</td><td><code>♪</code></td><td>where ASCII has <code>/</code></td></tr> +<tr><td>59</td><td>droplet symbol</td><td>where ASCII has <code>;</code></td></tr> +<tr><td>92</td><td>annoyed symbol</td><td>where ASCII has <code>\</code></td></tr> +<tr><td>127</td><td><code>CHAR_CONTROL_CODE</code></td><td>escape byte; a control code follows</td></tr> +<tr><td>128</td><td><code>CHAR_MESSAGE_TAG</code></td><td>escape byte</td></tr> +<tr><td>210, 211</td><td>short space, wide space</td><td>layout-only spaces</td></tr> +</table> + +<h2>The voice table</h2> + +<p><code>mMsg_sound_voice_get(code)</code> (<code>src/game/m_msg_sound.c_inc:1</code>) is a +256-byte lookup with two hard-coded exceptions:</p> + +<pre>if (code == CHAR_CONTROL_CODE || code == CHAR_MESSAGE_TAG) + return -1; // no sound, and no state change +return voice_array[code];</pre> + +<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> +slots.</p> + +<h3>Voice code space</h3> + +<table> +<tr><th>Voice code</th><th>Meaning</th></tr> +<tr><td><code>0x53</code>–<code>0x5C</code></td><td>digits <code>0</code>–<code>9</code></td></tr> +<tr><td><code>0x5D</code>–<code>0x76</code></td><td>letters <code>a</code>–<code>z</code></td></tr> +<tr><td><code>0x80</code></td><td><code>♪</code> — plays the speaker's melody, bypasses everything</td></tr> +<tr><td><code>0x81</code></td><td>space</td></tr> +<tr><td><code>0x82</code></td><td><code>?</code> — raises pitch</td></tr> +<tr><td><code>0x83</code></td><td><code>!</code> — raises volume</td></tr> +<tr><td><code>0x84</code></td><td>newline (injected, never in the table)</td></tr> +<tr><td><code>0x85</code></td><td>mute — the catch-all for punctuation and symbols</td></tr> +<tr><td><code>0x86</code></td><td>click — plays <code>NA_SE_BEBE</code></td></tr> +<tr><td><code>0xFF</code></td><td>empty slot in a window</td></tr> +</table> + +<p>The split at <code>0x77</code> matters: <code>Sou_BoinShiinCheck</code> and several rules in +<code>Na_VoiceSe</code> test <code>> 0x77</code> to mean "this is punctuation, not a letter".</p> + +<h3>Case and accent folding</h3> + +<p>Upper and lower case map to the same code, and every accented Latin letter folds onto +its base letter. Some of the folds are worth knowing about:</p> + +<table> +<tr><th>Character(s)</th><th>Folds to</th></tr> +<tr><td><code>À Á  à Šà á â ã ä å</code>, <code>ª</code> (feminine ordinal)</td><td><code>a</code></td></tr> +<tr><td><code>ß</code> (eszett)</td><td><code>b</code> — a glyph-shape fold, not a phonetic one</td></tr> +<tr><td><code>Ç ç</code></td><td><code>c</code></td></tr> +<tr><td><code>Ð ð</code></td><td><code>d</code></td></tr> +<tr><td><code>È É Ê Ë è é ê ë</code></td><td><code>e</code></td></tr> +<tr><td><code>Ì Í Î Ï ì í î ï</code></td><td><code>i</code></td></tr> +<tr><td><code>Ñ ñ</code></td><td><code>n</code></td></tr> +<tr><td><code>Ò Ó Ô Õ Ö ò ó ô õ ö</code>, <code>º</code> (masculine ordinal)</td><td><code>o</code></td></tr> +<tr><td><code>Þ þ</code> (thorn)</td><td><code>p</code></td></tr> +<tr><td><code>Ù Ú Û Ü ù ú û ü</code></td><td><code>u</code></td></tr> +<tr><td><code>Ý ý ÿ</code></td><td><code>y</code></td></tr> +<tr><td><code>Œ œ</code></td><td><em>mute</em> — the only accented Latin pair that is not folded</td></tr> +</table> + +<h3>Everything else</h3> + +<ul> + <li>Space (32), short space (210), wide space (211), and code 255 → <code>0x81</code>.</li> + <li>Interpunct (133) → <code>0x86</code>, the click. It is the only character in the whole + table that maps to the click code.</li> + <li>All other punctuation, symbols, and the entire block from 155 upward → + <code>0x85</code>, mute.</li> +</ul> + +<h2>Two wrappers</h2> + +<h3>mMsg_sound_voice_get2</h3> + +<pre>if (code == CHAR_NEW_LINE) return 0x84; +return mMsg_sound_voice_get(code);</pre> + +<p>Newline has no entry in the table; the wrapper injects <code>0x84</code>. This is the +function the scheduler actually calls, so <code>0x84</code> reaches the engine while the +raw table never produces it. <span class="src"><code>m_msg_sound.c_inc:44</code></span></p> + +<h3>mMsg_sound_voice_get_for_editor</h3> + +<pre>sound = mMsg_sound_voice_get(code); +switch (sound) { + case 0x80: case 0x82: case 0x83: case 0x85: return 0x86; + default: return sound; +}</pre> + +<p>In the text editor, ♪, <code>?</code>, <code>!</code> and every mute character all become +the click. See <a href="editor.html">the editor path</a>. +<span class="src"><code>m_msg_sound.c_inc:26</code></span></p> + +<div class="note"> +<strong>Only these 34 values leave the text layer.</strong> Word boundaries, capitalisation, +sentence structure and the actual letters beyond <code>a-z0-9</code> are all gone by +the time audio sees anything. Whatever "understanding of English" the animalese has, +it lives entirely in the digraph table on the next page. +</div> + +<div class="pager"> + <span><a href="gating.html">← 1. Gating</a></span> + <span><a href="scheduling.html">3. Windowing & timing →</a></span> +</div> + +</body> +</html> diff --git a/docs/gating.html b/docs/gating.html new file mode 100644 index 0000000..9e78ac0 --- /dev/null +++ b/docs/gating.html @@ -0,0 +1,180 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>1. Gating & voice selection — Animalese</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + +<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 1. Gating & voice selection</nav> + +<h1>1. Gating & voice selection</h1> +<p class="lede">Before a single character is converted, two decisions are made: whether +this message speaks at all, and with whose voice.</p> + +<h2>Voice mode</h2> + +<p>There are three voice modes, pushed to the audio engine by +<code>sAdo_SetVoiceMode</code> and stored in <code>sou_voice_mode</code>:</p> + +<table> +<tr><th>Mode</th><th>Name</th><th>Behaviour</th></tr> +<tr><td><code>0</code></td><td><code>VOICE_MODE_ANIMALESE</code></td><td>full phonology, samples from the voice bank</td></tr> +<tr><td><code>1</code></td><td><code>VOICE_MODE_CLICK</code></td><td>every non-space character plays <code>NA_SE_BEBE</code>, one blip</td></tr> +<tr><td><code>2</code></td><td><code>VOICE_MODE_SILENT</code></td><td>nothing</td></tr> +</table> + +<p>The mode is not simply the player's config setting. It is the product of the setting +and of <em>who is talking</em> — <code>mMsg_sound_voice_mode_get</code>:</p> + +<pre>mode_type = Save_Get(config).voice_mode // 0 animalese, 1 bebese, 2 silent +if (msg has VOICE_SILENT flag) mode_type = SILENT + +if (mMsg_check_sound_animal(msg)) + mode = { ANIMALESE, CLICK, SILENT }[mode_type] // an animal is speaking +else if (mode_type == SILENT) + mode = SILENT +else + mode = CLICK // narration, signs, menus</pre> + +<p class="src">Source: <code>src/game/m_msg_sound.c_inc:259</code></p> + +<p>So the config's "animalese" setting only unlocks animalese for messages that pass +<code>mMsg_check_sound_animal</code>. Everything else — item descriptions, bulletin +boards, system prompts — is click mode no matter what.</p> + +<h3>What counts as an animal talking</h3> + +<p><code>mMsg_check_sound_animal</code> (<code>m_msg_sound.c_inc:66</code>) returns true when the +<code>VOICE_CLICK</code> flag is clear <em>and</em> any of:</p> + +<ul> + <li><code>msg_p->show_actor_name</code> — the window has a speaker nameplate;</li> + <li><code>mMsg_check_sound_special</code> — message number in + <code>0x1743–0x1756</code> (event start), <code>0x1799–0x17AC</code> (event end), + or <code>0x3A8A–0x3AF0</code> (Dial-a-Psychic);</li> + <li><code>mMsg_check_sound_shasho</code> — message number in <code>0x35AF–0x35BE</code> + (the conductor).</li> +</ul> + +<p>The special ranges exist so that disembodied event narration still gets a voice. +They are forced to <strong>spec 4</strong> regardless of any actor +(<code>mMsg_sound_spec_change_voice_force</code>, <code>:288</code>).</p> + +<h2>Spec: which of the three voices</h2> + +<p>The speaker maps to a "spec" (2–9), and the spec selects a sequence and bank. +<code>Na_SpecChange</code> (<code>game64.c_inc:3459</code>) is the switch, and it is a no-op +if the spec is unchanged — otherwise it stops the voice sequence, clears the stay +cache, and starts a new one:</p> + +<table> +<tr><th>Spec</th><th>Sequence</th><th>Bank</th><th>Instruments</th><th>Used by</th></tr> +<tr><td>2, 5, 7, 9</td><td>243</td><td>156</td><td>119</td><td>default villagers, most specials, male player, Timmy/Tommy, Farley</td></tr> +<tr><td>3</td><td>244</td><td>157</td><td>83</td><td>Wendell, Pete, Booker, Jack</td></tr> +<tr><td>4, 6, 8</td><td>245</td><td>158</td><td>119</td><td>Pelly, Phyllis, Mabel, Sable, Blanca, female player, Joan, event narration</td></tr> +</table> + +<p>Spec also selects a base timbre inside <code>Na_VoiceSe</code> +(<code>game64.c_inc:2760</code>), reapplied on every call:</p> + +<table> +<tr><th>Spec</th><th>Volume (<code>_08</code>)</th><th>Pitch (<code>_14</code>)</th><th>Character</th></tr> +<tr><td>2, 3, 4</td><td>0.65</td><td>1.00</td><td>baseline</td></tr> +<tr><td>5, 6</td><td>0.90</td><td>1.00</td><td>player (louder, unmodulated)</td></tr> +<tr><td>7</td><td>0.90</td><td>1.30</td><td>squeaky — Timmy & Tommy</td></tr> +<tr><td>8</td><td>0.90</td><td>0.75</td><td>low — Joan</td></tr> +<tr><td>9</td><td>0.70</td><td>0.65</td><td>very low — Farley</td></tr> +</table> + +<p>Note that <strong>spec is orthogonal to bank</strong>: specs 5, 7 and 9 all use bank 156 and +differ only in this pitch/volume pair. There are three sample sets and eight voices.</p> + +<h3>Villagers</h3> + +<p><code>mNpc_GetNpcSoundSpec</code> (<code>src/game/m_npc.c:4116</code>) indexes by personality:</p> + +<pre>static int spec_table[mNpc_LOOKS_NUM] = { 4, 4, 2, 2, 3, 4 }; +// normal(girl) 4 | peppy 4 | lazy(boy) 2 | jock 2 | cranky 3 | snooty 4</pre> + +<p>An out-of-range <code>looks</code> falls back to <code>mNpc_LOOKS_BOY</code> (spec 2).</p> + +<h3>Special NPCs</h3> + +<p>Non-villagers fall through to <code>mNpc_GetNpcSoundSpecNotAnimal</code>, a linear scan of +<code>l_sp_actor_name</code> (<code>m_npc.c:32</code>). The default when not found is spec 2. +Everything in that table is spec 2 except:</p> + +<table> +<tr><th>Spec</th><th>NPCs</th></tr> +<tr><td>3</td><td>Wendell, Pete, Booker, Jack</td></tr> +<tr><td>4</td><td>Pelly, Phyllis, Mabel, Sable, Blanca</td></tr> +<tr><td>8</td><td>Joan</td></tr> +<tr><td>9</td><td>Farley</td></tr> +</table> + +<h3>The player's own voice: specs 5 and 6</h3> + +<p>Specs 5 and 6 never appear in any NPC table. They are set from the submenu +(<code>src/game/m_submenu.c:364</code>) when a menu that can type text opens:</p> + +<pre>if ((menu_type == mSM_OVL_LEDIT && param0 == 0) || + Common_Get(now_private)->gender == mPr_SEX_MALE) { + sAdo_SpecChange(5); // male +} else { + sAdo_SpecChange(6); // female +} +sAdo_SetVoiceMode(0); // force animalese on regardless of config</pre> + +<p>Two things follow. First, letter-writing and name entry force animalese even when the +player has set the config to click or silent. Second, specs 5 and 6 are treated as a +distinct class throughout <code>Na_VoiceSe</code>:</p> + +<ul> + <li>every punctuation code (<code>0x81</code>, <code>0x84</code>, <code>0x85</code>, + <code>0x86</code>) plays <code>NA_SE_BEBE</code> instead of being silently dropped — + the typing click;</li> + <li>the <code>?</code> pitch bump and <code>!</code> volume bump never apply;</li> + <li>the emotion LFO is skipped entirely;</li> + <li>anti-repetition substitution is disabled.</li> +</ul> + +<p>See <a href="editor.html">the editor path</a> for what actually gets spoken there.</p> + +<h3>Timmy and Tommy</h3> + +<p>Two text control codes toggle a temporary spec override, used for Nook's nephews:</p> + +<table> +<tr><th>Code</th><th>Value</th><th>Effect</th></tr> +<tr><td><code>mFont_CONT_CODE_VOICE_TRUE</code></td><td>92 (0x5C)</td><td>restore <code>msg_p->spec</code></td></tr> +<tr><td><code>mFont_CONT_CODE_VOICE_FALSE</code></td><td>93 (0x5D)</td><td>switch to spec 7 (pitch × 1.3)</td></tr> +</table> + +<p>Both guard on the actor being <code>SP_NPC_MAMEDANUKI0/1</code>, so the codes are inert in +any other message. <span class="src"><code>m_msg_sound.c_inc:317–340</code></span></p> + +<h2>Hard gates inside Na_VoiceSe</h2> + +<p>Four early returns drop the call regardless of everything above +(<code>game64.c_inc:2724–2735</code>):</p> + +<ul> + <li><code>voice > 0x12B</code> — NPC voice id out of range (299 max);</li> + <li>animalese mode and <code>Nap_ReadSubPort(VOICE_GROUP, 0, 1) != sou_now_voice_seq</code> + — the voice sequence has not finished loading yet, so early characters of the + first line after a spec change are silently dropped;</li> + <li><code>sou_scene_mode == 0</code> — audio not in a playing scene;</li> + <li><code>a == 0x85 && b == 0xFF && c == 0xFF</code> — a lone mute character, + handled at the very top before any state is touched.</li> +</ul> + +<div class="pager"> + <span><a href="index.html">← Index</a></span> + <span><a href="encoding.html">2. Text → voice code →</a></span> +</div> + +</body> +</html> diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..d6a0817 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,101 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Animalese pipeline — Animal Crossing (GC)</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + +<h1>Animalese: the text → speech pipeline</h1> +<p class="lede">How <em>Animal Crossing</em> (GameCube, <code>GAFE01</code>, USA rev 0) turns a +message string into villager babble. Every claim here is traced to a function in the +<a href="https://github.com/Prakxo/ac-decomp">ac-decomp</a> tree.</p> + +<h2>The short version</h2> + +<pre> +message text (game charset, 1 byte/char) + │ + │ 1. mMsg_sound_voice_get() 256-entry table + ▼ char code ──► voice code (0x53–0x86) +gating + voice selection + │ 2. mMsg_sound_voice_mode_get() animalese / click / silent + │ mNpc_GetNpcSoundSpec() speaker ──► spec 2–9 + ▼ Na_SpecChange() spec ──► sequence 243/244/245 +windowing + │ 3. mMsg_sound_voice_entry() collect (a, b, c) over two cursor steps + ▼ ~1 character per 2 game frames +draw-time trigger + │ 4. mFontSentence_animal_voice_se() glyph scale ──► volume + ▼ sAdo_VoiceSe(a, b, c, npc, scale, mode) +phonology + │ 5. Na_VoiceSe() digraphs, long vowels, silent e + ▼ ──► phoneme code 0x01–0x77 +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 +</pre> + +<p>A phoneme code is literally a <strong>soundfont instrument number plus one</strong>. The whole +"engine" is a lookup table with about fifty special cases; the character comes almost +entirely from the samples.</p> + +<h2>Pages</h2> + +<ul class="toc"> + <li><a href="gating.html">1. Gating & voice selection</a> + <span>When animalese plays at all, and which of the three voices you hear. + Specs 2–9, the click fallback, and the player's own voice.</span></li> + + <li><a href="encoding.html">2. Text → voice code</a> + <span>The game's character set, the 256-entry voice table, case folding, + accent folding, and the eight punctuation codes.</span></li> + + <li><a href="scheduling.html">3. Windowing & timing</a> + <span>How three characters get bundled into one call, why it fires from the + font renderer, and the every-other-frame cursor.</span></li> + + <li><a href="phonology.html">4. The phonology engine</a> + <span><code>Na_VoiceSe</code> in full: the 55-entry digraph table, short/long + vowel tables, silent e, magic e, and punctuation flush.</span></li> + + <li><a href="playback.html">5. Playback</a> + <span>Sequences, banks, the two alternating subtracks, per-spec timbre, + the emotion LFO, and the deferred second syllable.</span></li> + + <li><a href="soundfont.html">6. The soundfont lookup</a> + <span>Sequences, banks and instruments as data: the four archive tables, the + bank blob, load-time relocation, and where the PCM actually lives.</span></li> + + <li><a href="editor.html">7. The editor path</a> + <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> + <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> +</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> + +<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> + +<p class="src">Verified against ac-decomp at commit <code>09ca8e8b</code> (DOL and REL 100 % linked).</p> + +</body> +</html> diff --git a/docs/phonology.html b/docs/phonology.html new file mode 100644 index 0000000..ebbc872 --- /dev/null +++ b/docs/phonology.html @@ -0,0 +1,235 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>4. The phonology engine — Animalese</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + +<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 4. The phonology engine</nav> + +<h1>4. The phonology engine</h1> +<p class="lede"><code>Na_VoiceSe</code> — <code>src/static/jaudio_NES/game/game64.c_inc:2598</code>. +Given three voice codes it produces one phoneme code now and optionally queues a second +for the next frame.</p> + +<pre>void Na_VoiceSe(u8 a, u8 b, u8 c, s16 voice, u8 scale, u8 f)</pre> + +<p><code>f</code> is the calling mode: <strong>0</strong> is the normal message path, +<strong>2</strong> is the deferred second syllable, <strong>1</strong> is the +<a href="editor.html">editor path</a> which skips this page entirely.</p> + +<h2>Vowel / consonant classification</h2> + +<p><code>Sou_BoinShiinCheck</code> (<code>:2564</code>) — <em>boin</em> = vowel, +<em>shiin</em> = consonant:</p> + +<table> +<tr><th>Returns</th><th>Class</th><th>Members</th></tr> +<tr><td><code>0</code></td><td>vowel</td><td><code>a e i o u</code> <strong>and <code>n</code></strong></td></tr> +<tr><td><code>1</code></td><td>consonant</td><td>the other 20 letters</td></tr> +<tr><td><code>2</code></td><td>neither</td><td>digits and every punctuation code</td></tr> +</table> + +<p><code>n</code> being a vowel is deliberate — it is a syllabic nasal in Japanese, and this +engine was written by Japanese programmers modelling English through a kana-shaped +lens. It changes when the silent-<em>e</em> and magic-<em>e</em> rules fire, and it is +the single most surprising thing in the engine.</p> + +<h2>Rule order for mode 0</h2> + +<p>State is carried in <code>sou_last_num</code> and <code>sou_last_num2</code>, the previous +call's raw <code>a</code> and <code>b</code>. The originals are also stashed in +<code>sou_num_org</code>/<code>sou_num2_org</code>/<code>sou_num3_org</code> before anything is +rewritten.</p> + +<h3>0. Lone mute</h3> +<pre>if (a == 0x85 && b == 0xFF && c == 0xFF) return;</pre> +<p>Returns before touching any state, so a lone punctuation mark does not disturb the +previous-character history.</p> + +<h3>1. Silent e (<code>:2612</code>)</h3> +<pre>if (a == 'e' && b > 0x77 && class(last_num) == vowel && class(last_num2) == consonant) { + a = b; r28 = 4; // drop the e, speak the punctuation slot +} +if (b == 'e' && c > 0x77 && class(last_num2) == vowel && class(a) == consonant) { + b = 0; r28 = 4; // drop the e, queue nothing +}</pre> +<p>A word-final <em>e</em> preceded by vowel + consonant is dropped: <em>make</em>, +<em>time</em>, <em>nose</em>. Because <code>n</code> classifies as a vowel, <em>done</em> and +<em>gone</em> also trip it. Setting <code>r28 = 4</code> skips the digraph pass entirely.</p> + +<h3>2. Digraph merge (<code>:2623</code>)</h3> +<pre>a = Sou_ConnectCheck(a, b, c); +r28 = (a == sou_num_org) ? 1 : 2; // 2 = merged, both letters consumed</pre> +<p>If a digraph matched, <code>r28 = 2</code> and control falls straight through to +playback: the merged phoneme plays and <strong>nothing is queued</strong>, so both letters +are consumed by one sample.</p> + +<h4>The digraph table — <code>Sou_ConnectCheck</code> (<code>:2316</code>)</h4> +<p>53 entries. All are two characters except <code>you</code>, the only rule that reads <code>c</code>; one pairs a letter with a space rather than another letter (<code>i␣</code>).</p> + +<div class="grid2"> +<code>ai</code>→0x0F<br><code>al</code>→0x02<br><code>an</code>→0x39<br><code>ar</code>→0x16<br><code>as</code>→0x3A<br> +<code>be</code>→0x3B<br><code>by</code>→0x3C<br> +<code>ch</code>→0x08<br> +<code>do</code>→0x3D<br> +<code>ea</code>→0x15<br><code>er</code>→0x0D<br> +<code>fa</code>→0x35<br><code>fo</code>→0x34<br> +<code>ge</code>→0x17<br><code>go</code>→0x3E<br> +<code>ha</code>→0x3F<br><code>he</code>→0x40<br><code>hi</code>→0x41<br><code>ho</code>→0x42<br> +<code>i␣</code>→0x05<br><code>if</code>→0x43<br><code>in</code>→0x44<br><code>ir</code>→0x0C<br><code>is</code>→0x45<br><code>it</code>→0x46<br> +<code>ks</code>→0x31<br> +<code>me</code>→0x47<br><code>my</code>→0x48<br> +<code>na</code>→0x38<br><code>ng</code>→0x1C<br><code>no</code>→0x49<br> +<code>of</code>→0x4A<br><code>oh</code>→0x4B<br><code>ok</code>→0x4C<br><code>on</code>→0x4D<br><code>oo</code>→0x2A<br><code>or</code>→0x2C<br><code>ou</code>→0x20<br><code>ow</code>→0x06<br><code>oy</code>→0x1F<br> +<code>re</code>→0x0E<br> +<code>se</code>→0x37<br><code>sh</code>→0x24<br><code>si</code>→0x36<br><code>so</code>→0x4F<br><code>su</code>→0x25<br> +<code>th</code>→0x28<br><code>to</code>→0x50<br> +<code>up</code>→0x51<br> +<code>we</code>→0x52<br><code>wo</code>→0x33<br> +<code>you</code>→0x77<br> +<code>ze</code>→0x32 +</div> + +<p>The design is transparent. Fourteen entries are genuine English digraphs — +<code>ch sh th ng oo ow ou oy ai ea ar er ir or</code>. The other thirty-nine are the +openings of common English words: <code>he ho hi ha me my no on of oh ok so to up we +by do go is it in if as an al na se si su fa fo ge ze ks you</code>. The engine does not +model English phonetics; it memorises the most frequent English word-starts and gives +each one a recorded syllable.</p> + +<h3>3. Punctuation flush (<code>:2630</code>)</h3> +<pre>if (b is punctuation) { + r28 = 3; + if (sou_last_num2 was also punctuation) r28 = 1; // don't flush twice +}</pre> +<p>When a word ends, <code>r28 = 3</code> re-runs the merge one character further back:</p> +<pre>a = Sou_ConnectCheck(sou_last_num2, sou_num_org, sou_num2_org); +if (a < 0x53 || a > 0x76) break; // it merged, or wasn't a letter — leave it +a = Sou_TanboinHenkan(a, b);</pre> +<p>This gives the final letter of a word a chance to pair with the letter before it +instead of being dropped against the following space. Note the guard also drops the +whole branch if <code>a > 0x77</code> before it starts.</p> + +<h3>4. Vowel length (<code>:2658</code>, for <code>r28 == 1</code> or <code>4</code>)</h3> +<pre>if (class(a) == vowel && class(b) == consonant && c == 'e') + a = Sou_ChouboinHenkan(a, b); // long — magic e, as in cake +else + a = Sou_TanboinHenkan(a, b); // short + +sou_num2_request = b; // queue the second syllable +sou_num2_animal_id = voice; +sou_num2_scale = scale;</pre> + +<p><em>Chouboin</em> = long vowel, <em>tanboin</em> = short vowel. The two tables are +identical except for five entries:</p> + +<table> +<tr><th></th><th><code>a</code></th><th><code>i</code></th><th><code>u</code></th><th><code>e</code></th><th><code>o</code></th><th><code>n</code></th></tr> +<tr><td>short</td><td>0x01</td><td>0x14</td><td>0x29</td><td>0x0A</td><td>0x1D</td><td>0x1B</td></tr> +<tr><td>long</td><td>0x02</td><td>0x15</td><td>0x2A</td><td>0x0E</td><td>0x1E</td><td>0x1B</td></tr> +</table> + +<p><code>n</code> is the same in both — it is only classified as a vowel, it has no long form. +Note the aliasing: long <code>a</code> (0x02) is the same sample as digraph <code>al</code>, +long <code>i</code> (0x15) is <code>ea</code> and consonant <code>y</code>, long <code>e</code> +(0x0E) is <code>re</code>, long <code>u</code> (0x2A) is <code>oo</code>.</p> + +<h4>Consonants (both tables)</h4> +<table> +<tr><th>Letter</th><th>Code</th><th>Letter</th><th>Code</th><th>Letter</th><th>Code</th><th>Letter</th><th>Code</th></tr> +<tr><td><code>b</code></td><td>0x07</td><td><code>g</code></td><td>0x12</td><td><code>m</code></td><td>0x1A</td><td><code>t</code></td><td>0x26</td></tr> +<tr><td><code>c</code></td><td>0x23 / 0x18</td><td><code>h</code></td><td>0x00</td><td><code>p</code></td><td>0x21</td><td><code>v</code></td><td>0x2D</td></tr> +<tr><td><code>d</code></td><td>0x09</td><td><code>j</code></td><td>0x17</td><td><code>q</code></td><td>0x18</td><td><code>w</code></td><td>0x2E</td></tr> +<tr><td><code>f</code></td><td>0x11</td><td><code>k</code></td><td>0x18</td><td><code>r</code></td><td>0x22</td><td><code>x</code></td><td>0x31</td></tr> +<tr><td></td><td></td><td><code>l</code></td><td>0x19</td><td><code>s</code></td><td>0x23</td><td><code>y</code></td><td>0x15</td></tr> +<tr><td></td><td></td><td></td><td></td><td></td><td></td><td><code>z</code></td><td>0x30</td></tr> +</table> + +<p>Only one context rule exists: <code>c</code> is soft (0x23, same as <code>s</code>) when the +<em>next</em> letter is <code>i</code> or <code>e</code>, hard (0x18, same as <code>k</code>) +otherwise. <code>h</code> maps to 0x00, which has no instrument — <strong><code>h</code> is +silent</strong> unless it merges into a digraph.</p> + +<h4>Digits</h4> +<table> +<tr><th><code>0</code></th><th><code>1</code></th><th><code>2</code></th><th><code>3</code></th><th><code>4</code></th><th><code>5</code></th><th><code>6</code></th><th><code>7</code></th><th><code>8</code></th><th><code>9</code></th></tr> +<tr><td>0x32</td><td>0x33</td><td>0x26</td><td>0x27</td><td>0x34</td><td>0x35</td><td>0x36</td><td>0x37</td><td>0x10</td><td>0x38</td></tr> +</table> +<p>Eight of the ten reuse a code the letter tables also produce, and the overlaps line up +with the first syllable of the spoken number: <code>0</code>=<code>ze</code>(ro), +<code>1</code>=<code>wo</code>(one), <code>2</code>=<code>t</code>(wo), +<code>4</code>=<code>fo</code>(ur), <code>5</code>=<code>fa</code>(five), +<code>6</code>=<code>si</code>(x), <code>7</code>=<code>se</code>(ven), +<code>9</code>=<code>na</code>(nine). Only <code>3</code> (0x27) and <code>8</code> (0x10) +get codes nothing else reaches. The digraph names here are just labels for shared +samples — the recording itself presumably says the number.</p> + +<h2>The deferred second syllable — mode 2</h2> + +<p><code>Sou_SpecialRoutine10</code> (<code>:1708</code>) runs once per game frame inside +<code>Na_GameFrame</code>:</p> + +<pre>if (sou_Na_VoiceSeFlag == 1) { + sou_Na_VoiceSeFlag = 0; // a real call happened this frame +} else if (sou_num2_request) { + Na_VoiceSe(sou_num2_request, 0, 0, sou_num2_animal_id, sou_num2_scale, 2); + sou_num2_request = 0; +}</pre> + +<p>So the queued <code>b</code> plays on the first frame where no fresh call arrived. Mode 2 +is a stripped-down path:</p> + +<pre>if (sou_voice_mode == 1) return; // click mode: no second syllable +if (a == 0xFF) return; +if (a == 'e' && sou_num3_org > 0x77 && + class(sou_last_num2) == vowel && class(sou_num_org) == consonant) + a = 0; // silent-e check again +else + a = Sou_TanboinHenkan(a, sou_num3_org); // always short</pre> + +<p>The queued syllable never gets the long-vowel treatment and never re-checks digraphs, +which is why the second half of a pair always sounds flatter than the first.</p> + +<h2>Anti-repetition</h2> + +<pre>// mode 0 +if (sou_num_org == sou_last_num2 && sou_num_org == sou_last_num) i = 1; +… +if (spec == 5 || spec == 6 || r29 > 0x77) i = 0; +if (i == 1) a = Nap_GetRandom() >> 2; // 0x00–0x3F</pre> + +<p>Three identical voice codes in a row — <code>aaa</code>, <code>mmm</code>, <code>zzz</code> — +substitute a uniformly random phoneme from the first 64. This is the only path that +reaches many of the codes never produced by any rule, and it is why repeated-letter +interjections in dialogue sound like nonsense rather than a stutter. It is disabled +for the player's voice (specs 5 and 6).</p> + +<h2>♪ — the melody escape</h2> + +<pre>if (a == 0x80) { + if (voice == 0xFA) return; + switch (sou_voice_mode) { + case 0: Na_MelodyVoice(voice); break; // animalese + case 1: Na_SysTrgStart(NA_SE_BEBE); break; // click + case 2: break; // silent + } + return; +}</pre> + +<p><code>Na_MelodyVoice</code> (<code>src/static/jaudio_NES/game/melody.c:727</code>) plays a +fixed 16-note pattern <code>{13, 14 × 15}</code> on subtrack 6 at volume 1.3, using the +speaker's <code>npc_voice_id</code> to select which chunk of sequence 248 to copy in. The +id is the NPC's instrument type (<code>draw_data.voice_type</code>), the same one used for +the town tune — so ♪ plays "their" instrument rather than a phoneme.</p> + +<div class="pager"> + <span><a href="scheduling.html">← 3. Windowing & timing</a></span> + <span><a href="playback.html">5. Playback →</a></span> +</div> + +</body> +</html> diff --git a/docs/playback.html b/docs/playback.html new file mode 100644 index 0000000..1cf9464 --- /dev/null +++ b/docs/playback.html @@ -0,0 +1,196 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>5. Playback — Animalese</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + +<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 5. Playback</nav> + +<h1>5. Playback</h1> +<p class="lede">A phoneme code becomes an instrument number, written into a port on a +looping JAudio2 sequence. Everything expressive happens through two float multipliers.</p> + +<h2>Punctuation, applied at play time</h2> + +<p>By the time the rule engine finishes, punctuation codes are still in <code>a</code>. +The <code>sou_voice_mode</code> switch (<code>game64.c_inc:2810</code>) resolves them, and +splits on whether this is the player's voice:</p> + +<h3>Specs 5 and 6 — the player</h3> +<pre>switch (a) { + case 0x81: case 0x84: case 0x85: case 0x86: + Na_SysTrgStart(NA_SE_BEBE); a = 0; break; +}</pre> +<p>Every punctuation code clicks. <code>?</code> and <code>!</code> fall through unmodified +into the phoneme path — the pitch and volume bumps below never apply to the player.</p> + +<h3>All other specs</h3> +<table> +<tr><th>Code</th><th>Char</th><th>Effect</th></tr> +<tr><td><code>0x86</code></td><td>click</td><td><code>NA_SE_BEBE</code>, then mute</td></tr> +<tr><td><code>0x81</code> <code>0x84</code> <code>0x85</code></td><td>space, newline, mute</td><td>mute</td></tr> +<tr><td><code>0x82</code></td><td><code>?</code></td><td>mute, and pitch × 1.24 / 1.30 / 1.13 for sequence 243 / 244 / 245</td></tr> +<tr><td><code>0x83</code></td><td><code>!</code></td><td>mute, and volume × 1.15</td></tr> +<tr><td>anything else</td><td></td><td>plays; sets <code>sou_now_boin</code> (dead, see <a href="quirks.html">quirks</a>)</td></tr> +</table> + +<p>Both multipliers are applied to the spec's <em>base</em> value, which was just reset at +the top of the call, so they last exactly one utterance. The pitch factor differing per +sequence is a per-bank tuning correction, not an expressive choice.</p> + +<h2>The emotion LFO</h2> + +<p>Six emotion states are set by text control codes and stored in +<code>sou_message_status</code>:</p> + +<table> +<tr><th>Control code</th><th>Value</th><th>State</th></tr> +<tr><td><code>SET_MESSAGE_CONTENTS_NORMAL</code></td><td>75</td><td><code>VOICE_STATUS_NORMAL</code></td></tr> +<tr><td><code>SET_MESSAGE_CONTENTS_ANGRY</code></td><td>76</td><td><code>VOICE_STATUS_ANGRY</code></td></tr> +<tr><td><code>SET_MESSAGE_CONTENTS_SAD</code></td><td>77</td><td><code>VOICE_STATUS_SAD</code></td></tr> +<tr><td><code>SET_MESSAGE_CONTENTS_FUN</code></td><td>78</td><td><code>VOICE_STATUS_FUN</code></td></tr> +<tr><td><code>SET_MESSAGE_CONTENTS_SLEEPY</code></td><td>79</td><td><code>VOICE_STATUS_SLEEPY</code></td></tr> +<tr><td><code>SET_MESSAGE_CONTENTS_GLOOMY</code></td><td>97</td><td><code>VOICE_STATUS_GLOOMY</code></td></tr> +</table> + +<p>Each drives a short random-period modulation of <code>_18</code> (pitch multiplier) and +<code>_0C</code> (volume multiplier), recomputed per utterance +(<code>game64.c_inc:2880</code>). It is skipped for punctuation +(<code>r29 > 0x77</code>) and for specs 5 and 6, in which case the previous values +persist.</p> + +<h3>Angry (1)</h3> +<pre>if (counter == 0) counter = (rand() >> 6) + 10; // 10–13 +else counter--; +switch (counter) { + case 0: case 1: case 2: pitch = 1.25, vol = 1.25; break; + case 3: case 4: pitch = 1.20, vol = 1.20; break; + case 9: case 10: pitch = 1.25, vol = 1.25; break; + default: pitch = 0.90, vol = 1.05; break; +}</pre> +<p>Mostly flat and slightly quiet, with two bursts near the top and bottom of each +10–13 syllable cycle — a jagged, shouty contour.</p> + +<h3>Sad (2), Sleepy (4), Gloomy (5)</h3> +<pre>if (counter == 0) { effect = (rand() >> 6) + 14; counter = 1; } // 14–17 +else if (counter == effect) { counter = 0; sad_toguru ^= 1; } +else counter++; + +pitch = (sad_toguru ? 1.1 : 1.0) - counter * 0.022; +vol = (sad_toguru ? 1.1 : 1.0) - counter * 0.025;</pre> +<p>A sawtooth: pitch and volume slide down over 14–17 syllables, then jump back up, +alternating between two baselines. The three states share the code exactly — they +differ only in what the writers chose to tag.</p> + +<h3>Fun (3)</h3> +<pre>if (counter == 0) counter = (rand() >> 6) + 10; +else counter--; +switch (counter) { + case 0: case 1: pitch = 1.05, vol = 1.05; break; + case 2: pitch = 0.90, vol = 1.15; break; + default: pitch = 1.15, vol = 1.05; break; +}</pre> +<p>Consistently bright with a dip at the end of each cycle.</p> + +<h3>Normal (0)</h3> +<pre>pitch = 1.0; vol = 1.0;</pre> + +<h2>Reaching the DSP</h2> + +<p>Two subtracks in <code>NA_GROUP4</code> alternate, toggled by +<code>sou_voice_se_toguru</code> on every call, so consecutive syllables can overlap +rather than cutting each other off:</p> + +<pre>void Sou_VoiceStart(u8 subtrack, u8 b) { + NA_COMMAND_AUDIO_SUBTRACK_SET_PORT(VOICE_GROUP, subtrack, 0, b); +}</pre> + +<p>The guard before it is <code>if (a != 0 && a <= 0x77)</code> — code 0 never plays, +which is why <code>h</code> is silent. The call is also skipped entirely when the raw +code was space, newline or mute (<code>r29 != 0x85 && r29 != 0x81 && +r29 != 0x84</code>).</p> + +<h3>The sequence side</h3> + +<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> + +<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> + +<h3>Per-frame parameter push</h3> + +<p><code>Sou_VoiceMake</code> (<code>game64.c_inc:1174</code>) runs for both subtracks once +per game frame, inside <code>Na_GameFrame</code>:</p> + +<pre>volumeScale = _08 * _0C; // spec base × emotion volume +frequencyScale = _14 * _18; // spec base × emotion pitch +NA_COMMAND_AUDIO_SUBTRACK_SET_VOL_SCALE (VOICE_GROUP, index, volumeScale); +NA_COMMAND_AUDIO_SUBTRACK_SET_FREQ_SCALE(VOICE_GROUP, index, frequencyScale); +NA_COMMAND_AUDIO_SUBTRACK_SET_PAN (VOICE_GROUP, index, pan); +NA_COMMAND_AUDIO_SUBTRACK_REVERB_VOLUME (VOICE_GROUP, index, reverbVolume);</pre> + +<p><code>pan</code> is initialised to <code>0x3F</code> (centre) and <code>reverbVolume</code> to 0, +and neither is ever written for voice subtracks. <strong>Animalese is always centred, dry, +and unaffected by the speaker's distance from the camera</strong> — unlike every other +positional SE in the game.</p> + +<p>Because both subtracks share <code>sou_voice_se[0]</code> and <code>[1]</code> which are +always written together, an in-flight syllable's volume and pitch are retroactively +changed by the next syllable's settings.</p> + +<h3>Frame ordering</h3> + +<p><code>graph.c</code> runs <code>game_main</code> (including the draw pass that calls +<code>Na_VoiceSe</code>) and <em>then</em> <code>sAdo_GameFrame</code>. So within one frame:</p> + +<pre>game_main → mFontSentence_animal_voice_se → Na_VoiceSe (sets params, writes port) +Na_GameFrame → Sou_VoiceMake (pushes params to the DSP) + → Sou_SpecialRoutine10 (fires any queued second syllable)</pre> + +<p><code>Sou_VoiceMake</code> runs <em>before</em> <code>Sou_SpecialRoutine10</code>, so a +deferred syllable's parameters land one frame late.</p> + +<h2>Banks</h2> + +<p>From <code>src/static/jaudio_NES/game/audioheaders.c</code>:</p> + +<table> +<tr><th>Bank</th><th>ROM addr</th><th>Size</th><th>param0</th><th>param1</th><th>Instruments</th></tr> +<tr><td>156</td><td><code>0x0005C260</code></td><td><code>0x4500</code></td><td><code>0x05FF</code></td><td><code>0x7700</code></td><td>119</td></tr> +<tr><td>157</td><td><code>0x00060760</code></td><td><code>0x3020</code></td><td><code>0x05FF</code></td><td><code>0x5300</code></td><td>83</td></tr> +<tr><td>158</td><td><code>0x00063780</code></td><td><code>0x4500</code></td><td><code>0x05FF</code></td><td><code>0x7700</code></td><td>119</td></tr> +</table> + +<p><code>param1 >> 8</code> is the instrument count, <code>param0 >> 8</code> the sample +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>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> + +<div class="pager"> + <span><a href="phonology.html">← 4. The phonology engine</a></span> + <span><a href="soundfont.html">6. The soundfont lookup →</a></span> +</div> + +</body> +</html> diff --git a/docs/quirks.html b/docs/quirks.html new file mode 100644 index 0000000..bb860c9 --- /dev/null +++ b/docs/quirks.html @@ -0,0 +1,157 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>8. 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> + +<h1>8. 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> + +<h2>Bugs that shipped</h2> + +<h3>Glyph scale → volume is a dead store</h3> +<pre>if (scale > 0x20) { + scale = scale - 0x20; + f31 = scale; + f31 = f31 * 0.15f; + f30 = sou_voice_se[0]._08 + f31; // computed… + f30 = 4.f; // …and immediately discarded + sou_voice_se[0]._08 = f30; +} else { + scale = 0x1f - scale; + f31 = scale * 0.02f; + f30 = sou_voice_se[0]._08 - f31; // computed… + f30 = 0.4f; // …and immediately discarded + sou_voice_se[0]._08 = f30; +}</pre> +<p class="src"><code>game64.c_inc:2994</code></p> +<p>The intent was clearly a smooth scale-to-volume ramp. What ships is a step function: +any glyph larger than normal slams volume to <strong>4.0</strong> (a 6× jump over the +0.65 baseline), anything smaller to <strong>0.4</strong>. Normal-size text passes +<code>scale == 32</code> and skips the block entirely, so this is only audible on +deliberately scaled dialogue.</p> +<p>This is in the matching decomp, so retail does it. A faithful port needs it; a clean +library does not.</p> + +<h3>The mode-2 dedup check is tautological</h3> +<pre>case 2: { + if (sou_num_org == sou_num_org && sou_num_org == sou_last_num2) i = 1; +}</pre> +<p class="src"><code>game64.c_inc:2711</code></p> +<p>The first comparison is always true. The intent was presumably a three-in-a-row check +mirroring mode 0; the effect is a two-in-a-row check. Deferred syllables therefore +trigger the random substitution more often than intended.</p> + +<h3>SHIIN2BOIN is dead, and would read out of bounds</h3> +<pre>const u8 SHIIN2BOIN[] = { 0, 1, 2, 3, 4, 5, 1, 2, … }; // 86 entries +… +sou_now_boin = SHIIN2BOIN[a]; // a can be up to 0x77 = 119</pre> +<p class="src"><code>game64.c_inc:236</code>, written at <code>:2863</code></p> +<p><code>sou_now_boin</code> is written here and initialised in <code>Sou_InitAudio</code>, and +<strong>never read anywhere in the tree</strong>. Same for <code>sou_nobasu_count</code> +("elongation count"), which is only ever set to 0. Both are vestigial mouth-shape / +vowel tracking, presumably live in the N64 <em>Dōbutsu no Mori</em> code this was ported +from. Because the table is 86 entries and <code>a</code> can reach 119, the write reads +past the array — harmless only because the result is discarded.</p> +<p>The table also has an obvious data oddity: entry 45 is the value <code>45</code>, where +every neighbour is 0–5.</p> + +<h2>Design choices that surprise</h2> + +<ul> + <li><strong><code>n</code> is a vowel.</strong> <code>Sou_BoinShiinCheck</code> groups it with + <code>a e i o u</code>. This changes when silent e and magic e fire, so + <em>done</em>, <em>gone</em> and <em>none</em> get the silent-<em>e</em> drop.</li> + + <li><strong><code>h</code> is silent.</strong> <code>Sou_TanboinHenkan('h')</code> returns + <code>0x00</code>, and code 0 has no instrument (guarded by + <code>if (a != 0 && a <= 0x77)</code>). Standalone <code>h</code> makes no sound; + it only speaks through <code>ch sh th ha he hi ho oh</code>.</li> + + <li><strong>A space can never open a window.</strong> <code>mMsg_sound_voice_entry</code> + refuses <code>voice_no == 0x81</code> in the "first character" branch, so word-initial + pairing always starts on a letter.</li> + + <li><strong>Animalese is never panned or attenuated.</strong> <code>pan</code> stays at + <code>0x3F</code> and <code>reverbVolume</code> at 0 for the life of the program. A + villager at the far edge of the screen is exactly as loud and as centred as one in + your face — the only positional SE in the game that ignores position.</li> + + <li><strong>The two subtracks share one parameter set.</strong> <code>sou_voice_se[0]</code> + and <code>[1]</code> are always written together, and <code>Sou_VoiceMake</code> pushes + both every frame — so a new syllable retroactively changes the volume and pitch + of the one still ringing.</li> + + <li><strong>Voice mode is not the config setting.</strong> Animalese only ever plays for + messages that pass <code>mMsg_check_sound_animal</code>; and the submenu forces + <code>sAdo_SetVoiceMode(0)</code> regardless of config, so letter-writing always + speaks even for players who set the game to silent.</li> + + <li><strong>Sequence-load races drop characters.</strong> After a spec change, + <code>Na_VoiceSe</code> returns early until + <code>Nap_ReadSubPort(VOICE_GROUP, 0, 1) == sou_now_voice_seq</code>. The first + syllables of the first line with a new speaker can be silently lost.</li> +</ul> + +<h2>Corrections to earlier notes</h2> + +<p>The following supersede statements in <code>../extraction.md</code> as originally written.</p> + +<table> +<tr><th>Was</th><th>Actually</th></tr> + +<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> + +<tr><td>"43 of the 119 codes are unreachable through normal text … only ever hit by the +anti-repetition random substitution."</td> +<td>36 of them (<code>0x53</code>–<code>0x76</code>) are reachable — through the mode-1 +editor path — and the random substitution can <em>not</em> reach them, since it only +produces <code>0x00</code>–<code>0x3F</code>.</td></tr> + +<tr><td>"<code>Sou_SpecialRoutine10</code> replays it on the next <em>audio</em> frame."</td> +<td>It runs inside <code>Na_GameFrame</code>, which <code>graph.c</code> calls once per +<em>game</em> frame. Same ~17 ms at 60 fps, but the mechanism is the game loop, and it +halves under <code>SetGameFrame(2)</code>.</td></tr> + +<tr><td>"♪ … calls <code>Na_MelodyVoice(npc_id)</code> — the villager's tune."</td> +<td>It plays a fixed 16-note pattern <code>{13, 14 × 15}</code>, not a tune. The per-NPC +part is <em>which instrument</em>: <code>npc_voice_id</code> selects a chunk of sequence +248 to copy in. It is the speaker's timbre playing a constant figure.</td></tr> + +<tr><td>Specs 5 and 6 listed only in the spec→sequence table, unattributed.</td> +<td>They are the <strong>player's own voice</strong> in text-entry menus — 5 male, 6 +female — set from <code>m_submenu.c:364</code>, and are special-cased throughout +<code>Na_VoiceSe</code>.</td></tr> + +<tr><td>Voice mode not covered.</td> +<td>Three modes gate the whole pipeline, and the gate depends on the message, not just +the config. See <a href="gating.html">gating</a>.</td></tr> + +<tr><td>Mode <code>f == 1</code> not covered.</td> +<td>A third calling convention that bypasses phonology entirely. See +<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 55 digraph entries, both vowel tables, the per-spec +timbre values, and the spec→sequence→bank mapping.</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> +</div> + +</body> +</html> diff --git a/docs/reference.html b/docs/reference.html new file mode 100644 index 0000000..5b86ebb --- /dev/null +++ b/docs/reference.html @@ -0,0 +1,217 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>9. 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> + +<h1>9. Reference tables</h1> +<p class="lede">Everything needed to reimplement the pipeline, plus an index into the +decomp.</p> + +<h2>Voice codes</h2> + +<table> +<tr><th>Code</th><th>Char</th><th>Class</th></tr> +<tr><td><code>0x53</code>–<code>0x5C</code></td><td><code>0</code>–<code>9</code></td><td>2 (neither)</td></tr> +<tr><td><code>0x5D</code> <code>0x61</code> <code>0x65</code> <code>0x6A</code> <code>0x6B</code> <code>0x71</code></td><td><code>a e i n o u</code></td><td>0 (vowel)</td></tr> +<tr><td>other of <code>0x5D</code>–<code>0x76</code></td><td><code>b c d f g h j k l m p q r s t v w x y z</code></td><td>1 (consonant)</td></tr> +<tr><td><code>0x80</code>–<code>0x86</code>, <code>0xFF</code></td><td>♪ ␣ ? ! ⏎ mute click empty</td><td>2 (neither)</td></tr> +</table> + +<p>Letter <em>L</em> ↔ code: <code>0x5D + (L − 'a')</code>. Digit <em>D</em> ↔ code: +<code>0x53 + D</code>.</p> + +<h2>Phoneme codes</h2> + +<p>Phoneme code <em>v</em> plays soundfont instrument <em>v</em> − 1. Code 0 is +silence. Codes marked † are produced by more than one rule and therefore share a +sample.</p> + +<h3>Short vowels (<code>Sou_TanboinHenkan</code>)</h3> +<table> +<tr><th><code>a</code></th><th><code>i</code></th><th><code>u</code></th><th><code>e</code></th><th><code>o</code></th><th><code>n</code></th></tr> +<tr><td>0x01</td><td>0x14</td><td>0x29</td><td>0x0A</td><td>0x1D</td><td>0x1B</td></tr> +</table> + +<h3>Long vowels (<code>Sou_ChouboinHenkan</code>)</h3> +<table> +<tr><th><code>a</code></th><th><code>i</code></th><th><code>u</code></th><th><code>e</code></th><th><code>o</code></th><th><code>n</code></th></tr> +<tr><td>0x02 †</td><td>0x15 †</td><td>0x2A †</td><td>0x0E †</td><td>0x1E</td><td>0x1B</td></tr> +</table> +<p>Used only when <code>class(a) == vowel && class(b) == consonant && c == 'e'</code>. +The tables are otherwise byte-identical.</p> + +<h3>Consonants (both tables)</h3> +<table> +<tr><th>Letter</th><th>Code</th><th>Notes</th></tr> +<tr><td><code>b</code></td><td>0x07</td><td></td></tr> +<tr><td><code>c</code></td><td>0x23 / 0x18</td><td>soft (= <code>s</code>) if next letter is <code>i</code> or <code>e</code>, else hard (= <code>k</code>)</td></tr> +<tr><td><code>d</code></td><td>0x09</td><td></td></tr> +<tr><td><code>f</code></td><td>0x11</td><td></td></tr> +<tr><td><code>g</code></td><td>0x12</td><td></td></tr> +<tr><td><code>h</code></td><td>0x00</td><td><strong>silent</strong> — no instrument</td></tr> +<tr><td><code>j</code></td><td>0x17</td><td>shares <code>ge</code></td></tr> +<tr><td><code>k</code> <code>q</code></td><td>0x18</td><td></td></tr> +<tr><td><code>l</code></td><td>0x19</td><td></td></tr> +<tr><td><code>m</code></td><td>0x1A</td><td></td></tr> +<tr><td><code>p</code></td><td>0x21</td><td></td></tr> +<tr><td><code>r</code></td><td>0x22</td><td></td></tr> +<tr><td><code>s</code></td><td>0x23</td><td></td></tr> +<tr><td><code>t</code></td><td>0x26</td><td>shares digit <code>2</code></td></tr> +<tr><td><code>v</code></td><td>0x2D</td><td></td></tr> +<tr><td><code>w</code></td><td>0x2E</td><td></td></tr> +<tr><td><code>x</code></td><td>0x31</td><td>shares <code>ks</code></td></tr> +<tr><td><code>y</code></td><td>0x15</td><td>shares long <code>i</code> and <code>ea</code></td></tr> +<tr><td><code>z</code></td><td>0x30</td><td></td></tr> +</table> + +<h3>Digits (both tables)</h3> +<table> +<tr><th><code>0</code></th><th><code>1</code></th><th><code>2</code></th><th><code>3</code></th><th><code>4</code></th><th><code>5</code></th><th><code>6</code></th><th><code>7</code></th><th><code>8</code></th><th><code>9</code></th></tr> +<tr><td>0x32</td><td>0x33</td><td>0x26</td><td>0x27</td><td>0x34</td><td>0x35</td><td>0x36</td><td>0x37</td><td>0x10</td><td>0x38</td></tr> +</table> + +<h3>Digraphs (<code>Sou_ConnectCheck</code>) — 53 entries</h3> +<table> +<tr><th>Pair</th><th>Code</th><th>Pair</th><th>Code</th><th>Pair</th><th>Code</th><th>Pair</th><th>Code</th></tr> +<tr><td><code>ai</code></td><td>0x0F</td><td><code>go</code></td><td>0x3E</td><td><code>no</code></td><td>0x49</td><td><code>si</code></td><td>0x36</td></tr> +<tr><td><code>al</code></td><td>0x02</td><td><code>ha</code></td><td>0x3F</td><td><code>of</code></td><td>0x4A</td><td><code>so</code></td><td>0x4F</td></tr> +<tr><td><code>an</code></td><td>0x39</td><td><code>he</code></td><td>0x40</td><td><code>oh</code></td><td>0x4B</td><td><code>su</code></td><td>0x25</td></tr> +<tr><td><code>ar</code></td><td>0x16</td><td><code>hi</code></td><td>0x41</td><td><code>ok</code></td><td>0x4C</td><td><code>th</code></td><td>0x28</td></tr> +<tr><td><code>as</code></td><td>0x3A</td><td><code>ho</code></td><td>0x42</td><td><code>on</code></td><td>0x4D</td><td><code>to</code></td><td>0x50</td></tr> +<tr><td><code>be</code></td><td>0x3B</td><td><code>i␣</code></td><td>0x05</td><td><code>oo</code></td><td>0x2A</td><td><code>up</code></td><td>0x51</td></tr> +<tr><td><code>by</code></td><td>0x3C</td><td><code>if</code></td><td>0x43</td><td><code>or</code></td><td>0x2C</td><td><code>we</code></td><td>0x52</td></tr> +<tr><td><code>ch</code></td><td>0x08</td><td><code>in</code></td><td>0x44</td><td><code>ou</code></td><td>0x20</td><td><code>wo</code></td><td>0x33</td></tr> +<tr><td><code>do</code></td><td>0x3D</td><td><code>ir</code></td><td>0x0C</td><td><code>ow</code></td><td>0x06</td><td><code>you</code></td><td>0x77</td></tr> +<tr><td><code>ea</code></td><td>0x15</td><td><code>is</code></td><td>0x45</td><td><code>oy</code></td><td>0x1F</td><td><code>ze</code></td><td>0x32</td></tr> +<tr><td><code>er</code></td><td>0x0D</td><td><code>it</code></td><td>0x46</td><td><code>re</code></td><td>0x0E</td><td></td><td></td></tr> +<tr><td><code>fa</code></td><td>0x35</td><td><code>ks</code></td><td>0x31</td><td><code>se</code></td><td>0x37</td><td></td><td></td></tr> +<tr><td><code>fo</code></td><td>0x34</td><td><code>me</code></td><td>0x47</td><td><code>sh</code></td><td>0x24</td><td></td><td></td></tr> +<tr><td><code>ge</code></td><td>0x17</td><td><code>my</code></td><td>0x48</td><td><code>na</code></td><td>0x38</td><td></td><td></td></tr> +<tr><td></td><td></td><td><code>ng</code></td><td>0x1C</td><td></td><td></td><td></td><td></td></tr> +</table> +<p>Of the 119 codes <code>0x01</code>–<code>0x77</code>, the phonology engine produces 76. +The 43 it never produces are seven isolated gaps — +<code>0x03 0x04 0x0B 0x13 0x2B 0x2F 0x4E</code> — plus the 36-code block +<code>0x53</code>–<code>0x76</code>. The anti-repetition substitution can reach the first six +of the gaps (it only emits <code>0x00</code>–<code>0x3F</code>); the block is reachable only +via <a href="editor.html">the editor path</a>; <code>0x4E</code> appears to be reachable by +nothing at all.</p> + +<h2>Specs</h2> + +<table> +<tr><th>Spec</th><th>Seq</th><th>Bank</th><th>Vol</th><th>Pitch</th><th>Speaker</th></tr> +<tr><td>2</td><td>243</td><td>156</td><td>0.65</td><td>1.00</td><td>lazy & jock villagers; most special NPCs (default)</td></tr> +<tr><td>3</td><td>244</td><td>157</td><td>0.65</td><td>1.00</td><td>cranky villagers; Wendell, Pete, Booker, Jack</td></tr> +<tr><td>4</td><td>245</td><td>158</td><td>0.65</td><td>1.00</td><td>normal, peppy, snooty villagers; Pelly, Phyllis, Mabel, Sable, Blanca; event narration</td></tr> +<tr><td>5</td><td>243</td><td>156</td><td>0.90</td><td>1.00</td><td>player, male — text editor only</td></tr> +<tr><td>6</td><td>245</td><td>158</td><td>0.90</td><td>1.00</td><td>player, female — text editor only</td></tr> +<tr><td>7</td><td>243</td><td>156</td><td>0.90</td><td>1.30</td><td>Timmy & Tommy (via control code 93)</td></tr> +<tr><td>8</td><td>245</td><td>158</td><td>0.90</td><td>0.75</td><td>Joan</td></tr> +<tr><td>9</td><td>243</td><td>156</td><td>0.70</td><td>0.65</td><td>Farley</td></tr> +</table> + +<h2>Text control codes that affect voice</h2> + +<table> +<tr><th>Value</th><th>Name</th><th>Effect</th></tr> +<tr><td>3</td><td><code>CURSOR_SET_TIME</code></td><td>pauses the cursor; lets a half-built window fire early</td></tr> +<tr><td>75–79, 97</td><td><code>SET_MESSAGE_CONTENTS_*</code></td><td>emotion state (normal / angry / sad / fun / sleepy / gloomy)</td></tr> +<tr><td>81</td><td><code>SOUND_CUT</code></td><td>plays system SE <code>0x54</code>, sends the end code, suppresses voice</td></tr> +<tr><td>84</td><td><code>SET_CHAR_SCALE</code></td><td>changes glyph scale → the <a href="quirks.html">volume step</a></td></tr> +<tr><td>90</td><td><code>SET_LINE_SCALE</code></td><td>same, per line</td></tr> +<tr><td>92</td><td><code>VOICE_TRUE</code></td><td>restore spec (Timmy/Tommy only)</td></tr> +<tr><td>93</td><td><code>VOICE_FALSE</code></td><td>spec 7 (Timmy/Tommy only)</td></tr> +</table> + +<h2>Source index</h2> + +<p>Paths relative to the ac-decomp root. Line numbers are for commit +<code>09ca8e8b</code>.</p> + +<h3>Message layer</h3> +<table> +<tr><td><code>src/game/m_msg_sound.c_inc:1</code></td><td><code>mMsg_sound_voice_get</code> — the 256-entry table</td></tr> +<tr><td><code>​:26</code></td><td><code>mMsg_sound_voice_get_for_editor</code></td></tr> +<tr><td><code>​:44</code></td><td><code>mMsg_sound_voice_get2</code> — newline injection</td></tr> +<tr><td><code>​:52</code></td><td><code>mMsg_check_sound_special</code> — forced-voice message ranges</td></tr> +<tr><td><code>​:66</code></td><td><code>mMsg_check_sound_animal</code></td></tr> +<tr><td><code>​:85</code></td><td><code>mMsg_sound_voice_entry</code> — the (a, b, c) window</td></tr> +<tr><td><code>​:117</code></td><td><code>mMsg_sound_voice_endcode_set</code></td></tr> +<tr><td><code>​:126</code></td><td><code>mMsg_sound_CodeVoice</code></td></tr> +<tr><td><code>​:259</code></td><td><code>mMsg_sound_voice_mode_get</code></td></tr> +<tr><td><code>​:288</code></td><td><code>mMsg_sound_spec_change_voice_force</code></td></tr> +<tr><td><code>​:317</code></td><td><code>mMsg_sound_spec_change_true/false</code> — Timmy/Tommy</td></tr> +<tr><td><code>src/game/m_msg_cursol.c_inc:1100</code></td><td><code>mMsg_Main_Cursol_ControlCursol</code> — the cursor loop</td></tr> +<tr><td><code>​:1184</code></td><td>the <code>NOT_PAUSE_FRAME</code> toggle</td></tr> +<tr><td><code>src/game/m_msg_draw_font.c_inc:35</code></td><td>voice-fire decision</td></tr> +<tr><td><code>src/game/m_font_main.c_inc:951</code></td><td><code>mFontSentence_animal_voice_se</code> — scale, and the call</td></tr> +<tr><td><code>​:1078</code></td><td>the draw-loop trigger</td></tr> +<tr><td><code>src/game/m_npc.c:32</code></td><td><code>l_sp_actor_name</code> — special-NPC spec table</td></tr> +<tr><td><code>​:4116</code></td><td><code>mNpc_GetNpcSoundSpec</code></td></tr> +<tr><td><code>src/game/m_submenu.c:364</code></td><td>player voice, specs 5 / 6</td></tr> +<tr><td><code>src/game/m_editor_ovl.c:712</code></td><td>the mode-1 editor path</td></tr> +</table> + +<h3>Bridge</h3> +<table> +<tr><td><code>src/audio.c:96</code></td><td><code>sAdo_VoiceSe</code></td></tr> +<tr><td><code>​:152</code></td><td><code>sAdo_SetVoiceMode</code></td></tr> +<tr><td><code>​:216</code></td><td><code>sAdo_SpecChange</code></td></tr> +<tr><td><code>include/audio.h:45</code></td><td><code>VOICE_STATUS_*</code></td></tr> +<tr><td><code>include/audio_defs.h:10</code></td><td><code>VOICE_MODE_*</code></td></tr> +</table> + +<h3>Audio engine — <code>src/static/jaudio_NES/game/game64.c_inc</code></h3> +<table> +<tr><td><code>:22</code></td><td><code>VOICE_GROUP</code> = <code>NA_GROUP4</code></td></tr> +<tr><td><code>:97</code></td><td><code>SOU_VOICE_SE</code> struct</td></tr> +<tr><td><code>:236</code></td><td><code>SHIIN2BOIN</code> (dead)</td></tr> +<tr><td><code>:311</code></td><td><code>sou_voice_se_init</code> — pan <code>0x3F</code>, reverb 0</td></tr> +<tr><td><code>:820</code></td><td><code>Sou_VoiceStart</code></td></tr> +<tr><td><code>:1174</code></td><td><code>Sou_VoiceMake</code></td></tr> +<tr><td><code>:1708</code></td><td><code>Sou_SpecialRoutine10</code> — deferred syllable</td></tr> +<tr><td><code>:1852</code></td><td><code>Na_GameFrame</code></td></tr> +<tr><td><code>:2146</code></td><td><code>Sou_TanboinHenkan</code></td></tr> +<tr><td><code>:2231</code></td><td><code>Sou_ChouboinHenkan</code></td></tr> +<tr><td><code>:2316</code></td><td><code>Sou_ConnectCheck</code></td></tr> +<tr><td><code>:2564</code></td><td><code>Sou_BoinShiinCheck</code></td></tr> +<tr><td><code>:2598</code></td><td><code>Na_VoiceSe</code></td></tr> +<tr><td><code>:3319</code></td><td><code>Na_SetVoiceMode</code></td></tr> +<tr><td><code>:3459</code></td><td><code>Na_SpecChange</code></td></tr> +<tr><td><code>melody.c:727</code></td><td><code>Na_MelodyVoice</code> — the ♪ handler</td></tr> +<tr><td><code>audioheaders.c:4073</code></td><td>bank 156–158 entries</td></tr> +<tr><td><code>audioheaders.c:4176</code></td><td><code>AudiodataHeaderStart</code> — the three archives</td></tr> +<tr><td><code>audioheaders.c:4717</code></td><td>seq 243–245 → bank 156–158</td></tr> +</table> + +<h3>Soundfont / bank loading</h3> +<table> +<tr><td><code>include/jaudio_NES/audiostruct.h:74</code></td><td><code>ArcEntry</code> / <code>ArcHeader</code></td></tr> +<tr><td><code>​:95</code></td><td><code>adpcmloop</code>, <code>adpcmbook</code>, <code>smzwavetable</code>, <code>wtstr</code></td></tr> +<tr><td><code>​:348</code></td><td><code>voicetable</code> — the instrument</td></tr> +<tr><td><code>​:373</code></td><td><code>voiceinfo</code> — per-bank runtime info</td></tr> +<tr><td><code>src/static/jaudio_NES/internal/system.c:772</code></td><td><code>__Link_BankNum</code> — alias entries</td></tr> +<tr><td><code>​:813</code></td><td><code>Nas_BankOfsToAddr_Inner</code> — offset → pointer</td></tr> +<tr><td><code>​:1171</code></td><td><code>__SetVlute</code> — <code>param0/1/2</code> decode</td></tr> +<tr><td><code>​:1692</code></td><td><code>__WaveTouch</code> — sample vs bank relocation</td></tr> +<tr><td><code>internal/channel.c:354</code></td><td><code>NoteToVoice</code></td></tr> +<tr><td><code>​:368</code></td><td><code>ProgToVp</code> — instrument index → <code>voicetable</code></td></tr> +<tr><td><code>internal/track.c:1159</code></td><td><code>Nas_ProgramChanger</code></td></tr> +<tr><td><code>​:1399</code></td><td><code>SUBTRACK_CMD_SET_INSTRUMENT_BANK</code> — reverse bank-list index</td></tr> +</table> + +<div class="pager"> + <span><a href="quirks.html">← 8. Quirks & corrections</a></span> + <span><a href="index.html">Index</a></span> +</div> + +</body> +</html> diff --git a/docs/scheduling.html b/docs/scheduling.html new file mode 100644 index 0000000..581dff6 --- /dev/null +++ b/docs/scheduling.html @@ -0,0 +1,147 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>3. Windowing & timing — Animalese</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + +<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 3. Windowing & timing</nav> + +<h1>3. Windowing & timing</h1> +<p class="lede">The phonology engine needs three characters at once. Getting them there +takes two cursor steps, a pair of status flags, and a trigger that fires from the font +renderer rather than the message logic.</p> + +<h2>The text cursor</h2> + +<p><code>mMsg_Main_Cursol_ControlCursol</code> (<code>src/game/m_msg_cursol.c_inc:1100</code>) +runs once per game frame while a message is scrolling. Its loop:</p> + +<ol> + <li>Decrement <code>cursor_timer</code>; if still positive, do nothing this frame. + (Control code <code>mFont_CONT_CODE_CURSOR_SET_TIME</code>, value 3, sets it — this is + how messages pause mid-sentence.)</li> + <li>Skip newlines, message tags and control codes, executing their side effects.</li> + <li>On a printable character: advance <code>end_text_cursor_idx</code> + <strong>only if <code>mMsg_STATUS_FLAG_NOT_PAUSE_FRAME</code> is clear</strong>, + then toggle that flag, then call <code>mMsg_sound_CodeVoice(msg_p, cur_idx)</code> + and break.</li> +</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 +at 60 fps.</p> + +<p>Holding <kbd>B</kbd> sets <code>mMsg_STATUS_FLAG_FAST_TEXT</code>, which zeroes +<code>cursor_timer</code> and <em>clears</em> <code>NOT_PAUSE_FRAME</code> each frame, giving one +character per frame. It also switches the reported message speed from 2 to 1 +(<code>mMsg_sound_MessageSpeedClear</code>), which the audio engine stores in +<code>sou_message_speed</code>.</p> + +<h2>Building the (a, b, c) window</h2> + +<p><code>mMsg_sound_CodeVoice</code> (<code>m_msg_sound.c_inc:126</code>) converts the character +and hands it to <code>mMsg_sound_voice_entry</code> (<code>:85</code>), which is the state +machine:</p> + +<pre>if (idx == msg_p->voice_sfx_idx) return; // already seen this index +if (idx == msg_p->end_text_cursor_idx) return; // this is a non-advancing frame +msg_p->voice_sfx_idx = idx; + +if (status_flags & VOICE_ENTRY) { + // second character of the pair + voice2_idx = voice_no; // b + voice3_idx = lookahead(idx + code_size); // c, or 0xFF at a newline + status_flags |= FLAG_18; +} +else if (voice_no != 0x81) { // a space cannot open a pair + voice_idx = voice_no; // a + voice2_idx = voice3_idx = 0xFF; + animal_voice_idx = npc_voice_id; + status_flags |= VOICE_ENTRY; +}</pre> + +<p>Two guards deserve attention. The <code>end_text_cursor_idx</code> comparison is what +makes the every-other-frame toggle actually halve the entry rate — on a +non-advancing frame the pre-advance index equals the post-advance index and the call +is dropped. And a space (<code>0x81</code>) is never allowed to be the <code>a</code> of a +window, only the <code>b</code>; this is why utterances tend to break <em>after</em> a +space rather than starting on one.</p> + +<p>The <code>c</code> lookahead reads one character past <code>b</code> and clamps a newline or a +control code to <code>0xFF</code>. It exists for exactly two rules in the phonology: +<code>you</code> and magic e.</p> + +<h2>Firing</h2> + +<p>Nothing plays from the message code. The trigger is in the draw pass — +<code>mMsg_draw_font</code> (<code>src/game/m_msg_draw_font.c_inc:35</code>) decides whether +this frame's glyph should speak:</p> + +<pre>if (status_flags & VOICE_ENTRY) { + if (status_flags & FLAG_18) voice = TRUE; // pair complete + else if (cursor_timer != 0 || force_voice_enable_flag) voice = TRUE; +}</pre> + +<p>It then finds the line containing <code>voice_sfx_idx</code>, sets +<code>mFont_SENTENCE_FLAG_VOICE_SE</code> on that line's <code>mFontSentence</code>, and clears +both flags. <code>mFontSentence_gppDraw_main</code> +(<code>src/game/m_font_main.c_inc:1078</code>) calls the speak function immediately after +drawing the glyph at index <code>se_pos</code>, so audio is frame-locked to the character +appearing on screen.</p> + +<p>The second branch is the escape hatch: if the cursor is paused (a +<code>CURSOR_SET_TIME</code> code) or a control code returned +<code>mMsg_RESULT_TRUE</code>, a half-built window with <code>b = c = 0xFF</code> fires anyway +rather than being held indefinitely.</p> + +<h3>Scale → volume</h3> + +<p><code>mFontSentence_animal_voice_se</code> (<code>m_font_main.c_inc:951</code>) computes the +<code>scale</code> argument from the glyph's own transform:</p> + +<pre>f_scale = sqrtf(scale.x² + scale.y²); +scale = line_scale * f_scale * (32.0f / √2) + 0.5f; // clamped 0…255</pre> + +<p>32 is normal size, so a normally scaled glyph passes <code>scale == 32</code> and the +volume adjustment is skipped entirely. (What happens for other values is +<a href="quirks.html">not what it looks like</a>.)</p> + +<p>It also contains one very specific special case: if the character is ♪ at exactly +normal scale in colour <code>rgba(50, 60, 50, 255)</code>, the NPC id is forced to 0, +suppressing the melody. This is presumably a specific piece of dialogue where a +literal music note glyph should stay silent.</p> + +<h2>End of message</h2> + +<p><code>mMsg_sound_voice_endcode_set</code> (<code>m_msg_sound.c_inc:117</code>) sends +<code>sAdo_VoiceSe(0x84, 0xFF, 0xFF, npc, 32, 0)</code> — a synthetic newline — and clears +both window flags. It runs when the message ends, and also when the +<code>mFont_CONT_CODE_SOUND_CUT</code> control code (value 81) appears, which additionally +plays system SE <code>0x54</code> and suppresses the rest of the line's voice.</p> + +<h2>The resulting cadence</h2> + +<pre>frame 0 char A entry: a = A (nothing plays) +frame 1 — non-advancing frame, dropped +frame 2 char B entry: b = B, c = lookahead(C) + draw: sAdo_VoiceSe(A, B, C, …) ← one call +frame 3 — Sou_SpecialRoutine10 replays B as mode 2 +frame 4 char C entry: a = C +…</pre> + +<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 +<a href="playback.html">two alternating subtracks</a>.</p> + +<div class="pager"> + <span><a href="encoding.html">← 2. Text → voice code</a></span> + <span><a href="phonology.html">4. The phonology engine →</a></span> +</div> + +</body> +</html> diff --git a/docs/soundfont.html b/docs/soundfont.html new file mode 100644 index 0000000..c07caf9 --- /dev/null +++ b/docs/soundfont.html @@ -0,0 +1,226 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>6. The soundfont lookup — Animalese</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + +<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 6. The soundfont lookup</nav> + +<h1>6. The soundfont lookup</h1> +<p class="lede">How a phoneme code becomes bytes on disk. Sequence and bank ids are +address lookups; instrument numbers are not — they resolve through offsets stored +inside the bank blob itself.</p> + +<h2>Four tables, not one</h2> + +<p>Everything is driven by tables in <code>src/static/jaudio_NES/game/audioheaders.c</code>, +which is compiled into the DOL. <strong>None of it lives in +<code>audiorom.img</code></strong> — the image is pure payload with no table of contents, +which is why extraction needs the decomp.</p> + +<table> +<tr><th>Table</th><th>Entries</th><th>Indexes into</th></tr> +<tr><td><code>AudiodataHeaderStart</code></td><td>3</td><td><code>audiorom.img</code> — where the three regions are</td></tr> +<tr><td><code>AudioseqHeaderStart</code></td><td>249</td><td>the Audioseq region</td></tr> +<tr><td><code>AudiobankHeaderStart</code></td><td>159</td><td>the Audiobank region</td></tr> +<tr><td><code>AudiowaveHeaderStart</code></td><td>6</td><td>the Audiowave region</td></tr> +</table> + +<p>All four are <code>ArcHeader</code>s over a common entry type +(<code>include/jaudio_NES/audiostruct.h:74</code>):</p> + +<pre>typedef struct ArcEntry_ { + u32 addr; /* offset within this region */ + s32 size; + s8 medium; /* MEDIUM_RAM 0, DISK 1, CART 2, DISK_DRIVE 3 */ + s8 cacheType; + s16 param0, param1, param2; +} ArcEntry; /* 0x10 */</pre> + +<p><code>addr</code> and <code>size</code> are the address spec. <code>param0</code>–<code>param2</code> +are unused for sequences and waves. For <strong>banks</strong> they carry metadata the +loader needs <em>before</em> it can parse the blob, since the blob has no header of its +own — <code>__SetVlute</code> (<code>internal/system.c:1171</code>):</p> + +<pre>vinfo->wave_bank_id0 = (entry->param0 >> 8) & 0xFF; +vinfo->wave_bank_id1 = (entry->param0 >> 0) & 0xFF; +vinfo->num_instruments = (entry->param1 >> 8) & 0xFF; +vinfo->num_drums = (entry->param1 >> 0) & 0xFF; +vinfo->num_sfx = entry->param2;</pre> + +<p>So bank 156's <code>param0 = 0x05FF</code> means "my samples are in wave bank 5, there is +no second wave bank (<code>0xFF</code>)", and <code>param1 = 0x7700</code> means "119 +instruments, 0 drums". <strong>The instrument count is a number in a table entry, not an +address.</strong></p> + +<div class="note"> +<strong>Alias entries.</strong> <code>__Link_BankNum</code> +(<code>internal/system.c:772</code>) reads +<code>if (header->entries[id].size == 0) id = header->entries[id].addr;</code> +— a zero-size entry is not empty, its <code>addr</code> field is reinterpreted as +<em>another entry's id</em>. Any tool walking these tables has to follow the redirect or +it will read garbage for the aliased ids. +</div> + +<h2>Sequence → bank is a fifth lookup</h2> + +<p><code>AudiomapHeaderStart</code> (<code>audioheaders.c:4221</code>) is a <code>u8[]</code> that +indexes itself:</p> + +<pre>u16 offset[seq_id] // absolute offset into this same array + at that offset: u8 count, then u8 bank_id[count]</pre> + +<p>The voice sequences sit at the tail: <code>1, 0x9C</code> / <code>1, 0x9D</code> / +<code>1, 0x9E</code> — one bank each, 156 / 157 / 158.</p> + +<p>The sequence's own <code>set_bank</code> command does not carry a bank id. It carries an +index into <em>its own</em> list, counted from the end +(<code>internal/track.c:1399</code>):</p> + +<pre>cmdArgU16 = ((u16*)AG.map_header)[grp->seq_id]; +lo_bits = ((u8*)AG.map_header)[cmdArgU16]; // count +// inverse lookup, 1 = last bank, 2 = second to last, … +cmdArgU8 = ((u8*)AG.map_header)[cmdArgU16 + lo_bits - cmdArgU8]; +if (Nas_SzCacheCheck(BANK_TABLE, CACHE_EITHER, cmdArgU8)) + subtrack->bank_id = cmdArgU8;</pre> + +<p>That indirection lets a sequence be re-pointed at a different bank by editing the map +alone. For the voice sequences the list has one entry, so the byte is always 1.</p> + +<h2>Inside the bank blob</h2> + +<p>Pure offsets, all relative to the blob's own start:</p> + +<pre>u32 drumListOffset +u32 sfxListOffset +u32 instrumentOffset[num_instruments] ← count came from param1</pre> + +<p>Each instrument is a <code>voicetable</code> +(<code>audiostruct.h:348</code>, 0x20 bytes):</p> + +<pre>typedef struct voicetable_ { + u8 is_relocated; + u8 normal_range_low; + u8 normal_range_high; + u8 adsr_decay_idx; + envdat* envelope; /* offset → pointer */ + wtstr low_pitch_tuned_sample; /* { smzwavetable*, f32 tuning } */ + wtstr normal_pitch_tuned_sample; + wtstr high_pitch_tuned_sample; +} voicetable;</pre> + +<p>and each sample descriptor a <code>smzwavetable</code> (0x10):</p> + +<pre>typedef struct smzwavetable_ { + u32 bit31 : 1; + u32 codec : 3; /* 0 = CODEC_ADPCM (N64 VADPCM) */ + u32 medium : 2; + u32 bit26 : 1; + u32 is_relocated : 1; + u32 size : 24; + u8* sample; /* offset into the WAVE bank */ + adpcmloop* loop; /* offset within THIS bank */ + adpcmbook* book; /* offset within THIS bank */ +} smzwavetable;</pre> + +<p>with <code>adpcmbook = { s32 order, s32 n_predictors, s16 codebook[] }</code> and +<code>adpcmloop = { u32 loop_start, loop_end, count, sample_end, s16 state[16] if count > 0 }</code>.</p> + +<h2>Relocation at load time</h2> + +<p><code>Nas_BankOfsToAddr_Inner</code> (<code>internal/system.c:813</code>) walks the blob once +and rewrites every offset into a real pointer in place, guarded by +<code>is_relocated</code> because an instrument can appear in the list more than once. +Its per-sample helper <code>__WaveTouch</code> (<code>:1692</code>) contains the one asymmetry +that matters:</p> + +<pre>wavetable->loop = (adpcmloop*)((u32)wavetable->loop + ram_addr); // + bank base +wavetable->book = (adpcmbook*)((u32)wavetable->book + ram_addr); // + bank base + +switch (wavetable->medium) { + case MEDIUM_RAM: wavetable->sample += (u32)wave_media->wave0_p; break; // + WAVE base + case MEDIUM_DISK: wavetable->sample += (u32)wave_media->wave1_p; break; + case MEDIUM_CART: case MEDIUM_DISK_DRIVE: break; // already absolute +}</pre> + +<p><strong>Loop points and the VADPCM codebook live in the bank; the PCM lives in a +different region entirely.</strong> That is the whole reason <code>wave_bank_id0</code> exists — +it names which of the six wave banks to add. <code>wave0_p</code> / <code>wave1_p</code> +correspond to the two halves of <code>param0</code>.</p> + +<p>Two small quirks in the same function: the relocator clamps to +<strong>126 instruments</strong> regardless of what <code>param1</code> claimed (no voice bank +comes close), and it decides "not yet relocated" by testing +<code>(u32)wavetable <= OS_BASE_CACHED</code> — small values are offsets, large ones are +already pointers.</p> + +<h2>The runtime chain, for one syllable</h2> + +<pre>Sou_VoiceStart(subtrack, 0x28) write phoneme 0x28 ("th") to port 0 + ↓ +sequence 243 spins on the port, then set_dyntbl / dyntbl_call +dyntbl[0x28] → routine "C1 27 / 88 <note> / FF" + ↓ C1 = set instrument 0x27 ← where code − 1 happens +Nas_ProgramChanger(subtrack, 0x27, …) track.c:1159 + ↓ +ProgToVp(subtrack->bank_id, 0x27) channel.c:368 + ↓ if (inst >= voice_info[prog].num_instruments) → error, NULL + voice_info[156].instruments[0x27] → voicetable* + ↓ +NoteToVoice(vtbl, note) channel.c:354 + ↓ note < normal_range_low → low sample + ↓ note <= normal_range_high → normal sample + ↓ else → high sample +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> + +<p>Every voice instrument uses only the <em>normal</em> slot, so <code>NoteToVoice</code> is a +formality here.</p> + +<h2>Worked example — bank 156</h2> + +<pre>audiorom.img AudiodataHeaderStart +├ Audioseq @ 0x000000 len 0x0CF700 entry 0 +├ Audiobank @ 0x0CF700 len 0x067C80 entry 1 +│ └ bank 156 @ +0x05C260 → 0x12B960 len 0x4500 AudiobankHeaderStart[156] +│ ├ u32[0] drumList = 0 (num_drums = 0) +│ ├ u32[1] sfxList = 0 (num_sfx = 0) +│ ├ u32[2..120] 119 instrument offsets +│ └ voicetables · envelopes · smzwavetables · loops · codebooks +└ Audiowave @ 0x137380 len 0x6B33E0 entry 2 + └ wave bank 5 @ +0x2C5DC0 → 0x3FD140 len 0x3ED620 AudiowaveHeaderStart[5] + └ raw VADPCM frames; smzwavetable.sample is an offset into here</pre> + +<p>All three voice banks share <code>param0 = 0x05FF</code>, so all animalese samples — +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 +<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 +<code>is_relocated</code> is a load-time scratch flag rather than anything meaningful in +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> + +<div class="pager"> + <span><a href="playback.html">← 5. Playback</a></span> + <span><a href="editor.html">7. The editor path →</a></span> +</div> + +</body> +</html> diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..78917cf --- /dev/null +++ b/docs/style.css @@ -0,0 +1,83 @@ +/* minimal shared styling */ +:root { + --fg: #1a1a1a; + --dim: #666; + --rule: #d8d8d8; + --bg: #fdfdfc; + --code-bg: #f3f3f1; + --accent: #7a4a12; +} +@media (prefers-color-scheme: dark) { + :root { + --fg: #e4e2de; + --dim: #999; + --rule: #3a3a3a; + --bg: #17181a; + --code-bg: #202225; + --accent: #d8a76a; + } +} + +html { -webkit-text-size-adjust: 100%; } +body { + max-width: 46rem; + margin: 0 auto; + padding: 2.5rem 1.25rem 6rem; + background: var(--bg); + color: var(--fg); + font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; +} + +h1, h2, h3 { line-height: 1.25; font-weight: 600; } +h1 { font-size: 1.65rem; margin: 0 0 .25rem; } +h2 { font-size: 1.2rem; margin: 2.5rem 0 .75rem; padding-top: .75rem; border-top: 1px solid var(--rule); } +h3 { font-size: 1rem; margin: 1.75rem 0 .5rem; } + +a { color: var(--accent); } +a:hover { text-decoration: none; } + +nav.crumb { font-size: .85rem; color: var(--dim); margin-bottom: 2rem; } +nav.crumb a { color: var(--dim); } + +p.lede { color: var(--dim); margin: 0 0 2rem; } + +code, kbd { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .875em; } +:not(pre) > code { background: var(--code-bg); padding: .1em .35em; border-radius: 3px; } + +pre { + background: var(--code-bg); + padding: .85rem 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: .8125rem; + line-height: 1.5; +} + +table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: .875rem; display: block; overflow-x: auto; } +th, td { text-align: left; padding: .35rem .7rem .35rem 0; border-bottom: 1px solid var(--rule); vertical-align: top; } +th { font-weight: 600; white-space: nowrap; } +td code { white-space: nowrap; } + +ul, ol { padding-left: 1.3rem; } +li { margin: .3rem 0; } + +.src { font-size: .8125rem; color: var(--dim); } +.src code { background: none; padding: 0; } + +.note { + border-left: 3px solid var(--accent); + padding: .1rem 0 .1rem .9rem; + margin: 1.25rem 0; + color: var(--dim); +} +.note strong { color: var(--fg); } + +.toc { list-style: none; padding: 0; } +.toc li { margin: .9rem 0; } +.toc a { font-weight: 600; } +.toc span { display: block; color: var(--dim); font-size: .9rem; font-weight: 400; } + +.pager { margin-top: 3.5rem; padding-top: 1rem; border-top: 1px solid var(--rule); font-size: .9rem; display: flex; justify-content: space-between; gap: 1rem; } + +.grid2 { columns: 2; column-gap: 2rem; font-size: .875rem; } +@media (max-width: 40rem) { .grid2 { columns: 1; } } |