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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
|
<!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><code>sou_num_org</code>, <code>sou_num2_org</code> and <code>sou_num3_org</code> are assigned
<em>before</em> this test, so they <strong>are</strong> overwritten; what survives is
<code>sou_last_num</code> / <code>sou_last_num2</code>, which are written further down. A lone
punctuation mark therefore leaves the digraph history intact, but still clobbers the
context bytes a pending <a href="#mode2">mode 2</a> call will read —
<code>sou_num3_org</code> for its short-vowel lookup, <code>sou_num_org</code> for its dedup
comparison.</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="note">
<strong><code>i␣</code> is very nearly unreachable.</strong> Its <code>b</code> is <code>0x81</code>,
which is also a punctuation-flush trigger — so rule 3 below sets <code>r28 = 3</code> and
unconditionally reassigns <code>a</code>, discarding the <code>0x05</code>. It survives only when
the previous call's <code>b</code> was <em>also</em> punctuation, which forces <code>r28 = 1</code>
instead; <code>0x05</code> then falls through <code>Sou_TanboinHenkan</code>'s
<code>default: return a</code> and reaches playback unchanged. Every other digraph has a letter
in <code>b</code> and so never meets rule 3 — for those, "<code>r28 = 2</code> falls straight
through to playback" holds as written.
</div>
<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>The <em>intent</em> is to give the final letter of a word a chance to pair with the letter
before it instead of being dropped against the following space. What it does is narrower.
<code>a</code> is reloaded from <code>sou_last_num2</code> — the <em>previous</em> call's
<code>b</code> — and if <code>Sou_ConnectCheck</code> finds no merge it returns that byte
unchanged, so <code>Sou_TanboinHenkan</code> speaks the previous letter a second time and the
word-final letter is never voiced at all.</p>
<p>Worked example, <code>cat␣</code>. Window <code>(c, a, t)</code> doesn't merge, so
<code>r28 = 1</code> plays hard <code>c</code> and queues <code>a</code>; the deferred call plays
<code>a</code>. Window <code>(t, ␣, …)</code> then hits rule 3, reloads
<code>a = sou_last_num2 = 'a'</code>, fails to merge <code>at</code>, and plays <code>a</code>
again. The <code>t</code> makes no sound. The branch only pays off when the reloaded pair
<em>does</em> merge — then the merged code plays and the <code>0x53</code>–<code>0x76</code>
guard breaks out before <code>Sou_TanboinHenkan</code> can touch it.</p>
<p>Note the guard also drops the whole branch if <code>a > 0x77</code> before it starts, and
that nothing is queued on this path.</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 id="mode2">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>
|