aboutsummaryrefslogtreecommitdiff
path: root/docs/phonology.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/phonology.html
init
Diffstat (limited to 'docs/phonology.html')
-rw-r--r--docs/phonology.html235
1 files changed, 235 insertions, 0 deletions
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 &amp;&amp; b == 0xFF &amp;&amp; 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' &amp;&amp; b &gt; 0x77 &amp;&amp; class(last_num) == vowel &amp;&amp; class(last_num2) == consonant) {
+ a = b; r28 = 4; // drop the e, speak the punctuation slot
+}
+if (b == 'e' &amp;&amp; c &gt; 0x77 &amp;&amp; class(last_num2) == vowel &amp;&amp; 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 &lt; 0x53 || a &gt; 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 &gt; 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 &amp;&amp; class(b) == consonant &amp;&amp; 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' &amp;&amp; sou_num3_org &gt; 0x77 &amp;&amp;
+ class(sou_last_num2) == vowel &amp;&amp; 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 &amp;&amp; sou_num_org == sou_last_num) i = 1;
+…
+if (spec == 5 || spec == 6 || r29 &gt; 0x77) i = 0;
+if (i == 1) a = Nap_GetRandom() &gt;&gt; 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 &amp; timing</a></span>
+ <span><a href="playback.html">5. Playback →</a></span>
+</div>
+
+</body>
+</html>