diff options
Diffstat (limited to 'docs/phonology.html')
| -rw-r--r-- | docs/phonology.html | 235 |
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 && b == 0xFF && 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' && 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="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>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 > 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 && 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>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> |