Na_VoiceSe — src/static/jaudio_NES/game/game64.c_inc:2598.
Given three voice codes it produces one phoneme code now and optionally queues a second
for the next frame.
void Na_VoiceSe(u8 a, u8 b, u8 c, s16 voice, u8 scale, u8 f)
f is the calling mode: 0 is the normal message path,
2 is the deferred second syllable, 1 is the
editor path which skips this page entirely.
Sou_BoinShiinCheck (:2564) — boin = vowel,
shiin = consonant:
| Returns | Class | Members |
|---|---|---|
0 | vowel | a e i o u and n |
1 | consonant | the other 20 letters |
2 | neither | digits and every punctuation code |
n 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-e and magic-e rules fire, and it is
the single most surprising thing in the engine.
State is carried in sou_last_num and sou_last_num2, the previous
call's raw a and b. The originals are also stashed in
sou_num_org/sou_num2_org/sou_num3_org before anything is
rewritten.
if (a == 0x85 && b == 0xFF && c == 0xFF) return;
sou_num_org, sou_num2_org and sou_num3_org are assigned
before this test, so they are overwritten; what survives is
sou_last_num / sou_last_num2, which are written further down. A lone
punctuation mark therefore leaves the digraph history intact, but still clobbers the
context bytes a pending mode 2 call will read —
sou_num3_org for its short-vowel lookup, sou_num_org for its dedup
comparison.
:2612)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
}
A word-final e preceded by vowel + consonant is dropped: make,
time, nose. Because n classifies as a vowel, done and
gone also trip it. Setting r28 = 4 skips the digraph pass entirely.
:2623)a = Sou_ConnectCheck(a, b, c); r28 = (a == sou_num_org) ? 1 : 2; // 2 = merged, both letters consumed
If a digraph matched, r28 = 2 and control falls straight through to
playback: the merged phoneme plays and nothing is queued, so both letters
are consumed by one sample.
Sou_ConnectCheck (:2316)53 entries. All are two characters except you, the only rule that reads c; one pairs a letter with a space rather than another letter (i␣).
i␣ is very nearly unreachable. Its b is 0x81,
which is also a punctuation-flush trigger — so rule 3 below sets r28 = 3 and
unconditionally reassigns a, discarding the 0x05. It survives only when
the previous call's b was also punctuation, which forces r28 = 1
instead; 0x05 then falls through Sou_TanboinHenkan's
default: return a and reaches playback unchanged. Every other digraph has a letter
in b and so never meets rule 3 — for those, "r28 = 2 falls straight
through to playback" holds as written.
ai→0x0Fal→0x02an→0x39ar→0x16as→0x3Abe→0x3Bby→0x3Cch→0x08do→0x3Dea→0x15er→0x0Dfa→0x35fo→0x34ge→0x17go→0x3Eha→0x3Fhe→0x40hi→0x41ho→0x42i␣→0x05if→0x43in→0x44ir→0x0Cis→0x45it→0x46ks→0x31me→0x47my→0x48na→0x38ng→0x1Cno→0x49of→0x4Aoh→0x4Bok→0x4Con→0x4Doo→0x2Aor→0x2Cou→0x20ow→0x06oy→0x1Fre→0x0Ese→0x37sh→0x24si→0x36so→0x4Fsu→0x25th→0x28to→0x50up→0x51we→0x52wo→0x33you→0x77ze→0x32
The design is transparent. Fourteen entries are genuine English digraphs —
ch sh th ng oo ow ou oy ai ea ar er ir or. The other thirty-nine are the
openings of common English words: 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. The engine does not
model English phonetics; it memorises the most frequent English word-starts and gives
each one a recorded syllable.
:2630)if (b is punctuation) {
r28 = 3;
if (sou_last_num2 was also punctuation) r28 = 1; // don't flush twice
}
When a word ends, r28 = 3 re-runs the merge one character further back:
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);
The intent 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.
a is reloaded from sou_last_num2 — the previous call's
b — and if Sou_ConnectCheck finds no merge it returns that byte
unchanged, so Sou_TanboinHenkan speaks the previous letter a second time and the
word-final letter is never voiced at all.
Worked example, cat␣. Window (c, a, t) doesn't merge, so
r28 = 1 plays hard c and queues a; the deferred call plays
a. Window (t, ␣, …) then hits rule 3, reloads
a = sou_last_num2 = 'a', fails to merge at, and plays a
again. The t makes no sound. The branch only pays off when the reloaded pair
does merge — then the merged code plays and the 0x53–0x76
guard breaks out before Sou_TanboinHenkan can touch it.
Note the guard also drops the whole branch if a > 0x77 before it starts, and
that nothing is queued on this path.
:2658, for r28 == 1 or 4)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;
Chouboin = long vowel, tanboin = short vowel. The two tables are identical except for five entries:
a | i | u | e | o | n | |
|---|---|---|---|---|---|---|
| short | 0x01 | 0x14 | 0x29 | 0x0A | 0x1D | 0x1B |
| long | 0x02 | 0x15 | 0x2A | 0x0E | 0x1E | 0x1B |
n is the same in both — it is only classified as a vowel, it has no long form.
Note the aliasing: long a (0x02) is the same sample as digraph al,
long i (0x15) is ea and consonant y, long e
(0x0E) is re, long u (0x2A) is oo.
| Letter | Code | Letter | Code | Letter | Code | Letter | Code |
|---|---|---|---|---|---|---|---|
b | 0x07 | g | 0x12 | m | 0x1A | t | 0x26 |
c | 0x23 / 0x18 | h | 0x00 | p | 0x21 | v | 0x2D |
d | 0x09 | j | 0x17 | q | 0x18 | w | 0x2E |
f | 0x11 | k | 0x18 | r | 0x22 | x | 0x31 |
l | 0x19 | s | 0x23 | y | 0x15 | ||
z | 0x30 |
Only one context rule exists: c is soft (0x23, same as s) when the
next letter is i or e, hard (0x18, same as k)
otherwise. h maps to 0x00, which has no instrument — h is
silent unless it merges into a digraph.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| 0x32 | 0x33 | 0x26 | 0x27 | 0x34 | 0x35 | 0x36 | 0x37 | 0x10 | 0x38 |
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: 0=ze(ro),
1=wo(one), 2=t(wo),
4=fo(ur), 5=fa(five),
6=si(x), 7=se(ven),
9=na(nine). Only 3 (0x27) and 8 (0x10)
get codes nothing else reaches. The digraph names here are just labels for shared
samples — the recording itself presumably says the number.
Sou_SpecialRoutine10 (:1708) runs once per game frame inside
Na_GameFrame:
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;
}
So the queued b plays on the first frame where no fresh call arrived. Mode 2
is a stripped-down path:
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
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.
// 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
Three identical voice codes in a row — aaa, mmm, zzz —
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).
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;
}
Na_MelodyVoice (src/static/jaudio_NES/game/melody.c:727) plays a
fixed 16-note pattern {13, 14 × 15} on subtrack 6 at volume 1.3, using the
speaker's npc_voice_id to select which chunk of sequence 248 to copy in. The
id is the NPC's instrument type (draw_data.voice_type), the same one used for
the town tune — so ♪ plays "their" instrument rather than a phoneme.