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/playback.html | |
init
Diffstat (limited to 'docs/playback.html')
| -rw-r--r-- | docs/playback.html | 196 |
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 > 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> |