A phoneme code becomes an instrument number, written into a port on a looping JAudio2 sequence. Everything expressive happens through two float multipliers.
By the time the rule engine finishes, punctuation codes are still in a.
The sou_voice_mode switch (game64.c_inc:2810) resolves them, and
splits on whether this is the player's voice:
switch (a) {
case 0x81: case 0x84: case 0x85: case 0x86:
Na_SysTrgStart(NA_SE_BEBE); a = 0; break;
}
Every punctuation code clicks. ? and ! fall through unmodified
into the phoneme path — the pitch and volume bumps below never apply to the player.
| Code | Char | Effect |
|---|---|---|
0x86 | click | NA_SE_BEBE, then mute |
0x81 0x84 0x85 | space, newline, mute | mute |
0x82 | ? | mute, and pitch × 1.24 / 1.30 / 1.13 for sou_now_voice_seq 1 / 2 / 3 — the small index Na_SpecChange sets alongside sequence 243 / 244 / 245, not the sequence number itself |
0x83 | ! | mute, and volume × 1.15 |
| anything else | plays; sets sou_now_boin (dead, see quirks) |
Both multipliers are applied to the spec's base 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.
Six emotion states are set by text control codes and stored in
sou_message_status:
| Control code | Value | State |
|---|---|---|
SET_MESSAGE_CONTENTS_NORMAL | 75 | VOICE_STATUS_NORMAL |
SET_MESSAGE_CONTENTS_ANGRY | 76 | VOICE_STATUS_ANGRY |
SET_MESSAGE_CONTENTS_SAD | 77 | VOICE_STATUS_SAD |
SET_MESSAGE_CONTENTS_FUN | 78 | VOICE_STATUS_FUN |
SET_MESSAGE_CONTENTS_SLEEPY | 79 | VOICE_STATUS_SLEEPY |
SET_MESSAGE_CONTENTS_GLOOMY | 97 | VOICE_STATUS_GLOOMY |
Each drives a short random-period modulation of _18 (pitch multiplier) and
_0C (volume multiplier), recomputed per utterance
(game64.c_inc:2880). It is skipped for punctuation
(r29 > 0x77) and for specs 5 and 6, in which case the previous values
persist.
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;
}
Mostly flat and slightly quiet, with two bursts near the top and bottom of each 10–13 syllable cycle — a jagged, shouty contour.
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;
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.
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;
}
Consistently bright with a dip at the end of each cycle.
pitch = 1.0; vol = 1.0;
Two subtracks in NA_GROUP4 alternate, toggled by
sou_voice_se_toguru on every call, so consecutive syllables can overlap
rather than cutting each other off:
void Sou_VoiceStart(u8 subtrack, u8 b) {
NA_COMMAND_AUDIO_SUBTRACK_SET_PORT(VOICE_GROUP, subtrack, 0, b);
}
The guard before it is if (a != 0 && a <= 0x77) — code 0 never plays,
which is why h is silent. The call is also skipped entirely when the raw
code was space, newline or mute (r29 != 0x85 && r29 != 0x81 &&
r29 != 0x84).
Each voice sequence's subtrack sits in a spin loop:
loop: delay 1 tick
MACRO_SET 1 ; publish "I am sequence 1/2/3" …
PORT1_WRITE_MACRO_REG ; … to port 1 (the load-race handshake)
MACRO_READ_PORT0 ; value ← port 0, port 0 ← -1
branch loop if value < 0 ; port empty
SET_DYNTBL 0x0026
DYNTBL_CALL ; call routine dyntbl[phoneme code]
branch loop
The dynamic table has 128 entries pointing at routines of the shape
C1 <instrument> / 88 <layer addr> / FF, where the note layer holds the
actual note, delay and velocity. Entry v selects instrument
v − 1; entry 0 is an immediate FF — nothing. The
full annotated disassembly, the per-code note/delay/velocity map, and the tick math are
on the disc page. Two headlines from it: dialogue phonemes play
at semitone 44 / 41 / 39 (sequences 243 / 244 / 245) against a pitch table with unity at
48, so each bank is pitched down 4–9 semitones from its recordings; and port reads are
destructive (the port resets to −1), which is why one Sou_VoiceStart write
triggers exactly one syllable.
Sou_VoiceMake (game64.c_inc:1174) runs for both subtracks once
per game frame, inside Na_GameFrame:
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);
pan is initialised to 0x3F (centre) and reverbVolume to 0,
and neither is ever written for voice subtracks. Animalese is always centred, dry,
and unaffected by the speaker's distance from the camera — unlike every other
positional SE in the game.
Because both subtracks share sou_voice_se[0] and [1] which are
always written together, an in-flight syllable's volume and pitch are retroactively
changed by the next syllable's settings.
graph.c runs game_main (including the draw pass that calls
Na_VoiceSe) and then sAdo_GameFrame. So within one frame:
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)
Sou_VoiceMake runs before Sou_SpecialRoutine10, so a
deferred syllable's parameters land one frame late.
From src/static/jaudio_NES/game/audioheaders.c:
| Bank | ROM addr | Size | param0 | param1 | Instruments |
|---|---|---|---|---|---|
| 156 | 0x0005C260 | 0x4500 | 0x05FF | 0x7700 | 119 |
| 157 | 0x00060760 | 0x3020 | 0x05FF | 0x5300 | 83 |
| 158 | 0x00063780 | 0x4500 | 0x05FF | 0x7700 | 119 |
param1 >> 8 is the instrument count, param0 >> 8 the sample
bank id (5 for all three), and all have zero drums and zero SFX. 119 instruments =
phoneme codes 0x01–0x77 exactly.
Bank 157 stopping at 83 is not an omission, and nothing is lost. The phonology
engine's maximum output is 0x52 plus 0x77; instruments 0–81
cover 0x01–0x52, and the missing
0x53–0x76 range is the
editor's letter-name block, which spec 3 can never reach.
For you (0x77), sequence 244's dynamic table explicitly routes
the code to instrument 82 — the bank's 83rd and last instrument, which
sits above the phonology range and is reachable by nothing else. Bank 157 carries one
extra instrument precisely to be its you.
Sample details are on the disc page: N64 VADPCM, tuning 0.689062 (22050 Hz at a 32000 Hz mixer), order 2, two predictors, no loop, 32–407 ms each (mean ≈ 190 ms).