aboutsummaryrefslogtreecommitdiff
path: root/docs/playback.html
diff options
context:
space:
mode:
authorAlexander Kassouni <alex@kassouni.net>2026-09-05 17:51:13 -0700
committerAlexander Kassouni <alex@kassouni.net>2026-09-05 17:51:13 -0700
commit46227e2003de28202cc4e82bca4f7f4745476c4e (patch)
treefe450ac33ffa13747613c3cfdf36ea24b3036c46 /docs/playback.html
init
Diffstat (limited to 'docs/playback.html')
-rw-r--r--docs/playback.html196
1 files changed, 196 insertions, 0 deletions
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 &gt; 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() &gt;&gt; 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() &gt;&gt; 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() &gt;&gt; 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 &amp;&amp; a &lt;= 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 &amp;&amp; r29 != 0x81 &amp;&amp;
+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 &lt;instrument&gt; / 88 &lt;note&gt; / FF</code>. Entry <em>v</em> selects instrument
+<strong><em>v</em> − 1</strong>. Entry 0 is an immediate <code>FF</code> — nothing.</p>
+
+<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 &gt;&gt; 8</code> is the instrument count, <code>param0 &gt;&gt; 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>