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:
L_0019: delay 1 frame
macro_read_port0
branch_rel_not_eq_zero L_0019 ; wait until Sou_VoiceStart writes the port
set_dyntbl D_0026
dyntbl_call ; index the table with the phoneme code
branch L_0019
The dynamic table has 128 entries pointing at 10-byte routines
C1 <instrument> / 88 <note> / FF. Entry v selects instrument
v − 1. Entry 0 is an immediate FF — nothing.
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: instruments 0–82 cover codes
0x01–0x53, and the phonology engine never produces a code above
0x52 apart from 0x77. The missing range
0x53–0x76 is the editor's letter-name block,
which spec 3 can never reach. The only thing spec-3 voices genuinely lack is
you.
Sample details are in ../extraction.md: N64 VADPCM, tuning 0.6891
(22050 Hz), order 2, two predictors, no loop, ~190 ms each.