1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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 <code>sou_now_voice_seq</code> 1 / 2 / 3 — the small index <code>Na_SpecChange</code> sets alongside sequence 243 / 244 / 245, not the sequence number itself</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>
|