5. Playback

A phoneme code becomes an instrument number, written into a port on a looping JAudio2 sequence. Everything expressive happens through two float multipliers.

Punctuation, applied at play time

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:

Specs 5 and 6 — the player

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.

All other specs

CodeCharEffect
0x86clickNA_SE_BEBE, then mute
0x81 0x84 0x85space, newline, mutemute
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 elseplays; 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.

The emotion LFO

Six emotion states are set by text control codes and stored in sou_message_status:

Control codeValueState
SET_MESSAGE_CONTENTS_NORMAL75VOICE_STATUS_NORMAL
SET_MESSAGE_CONTENTS_ANGRY76VOICE_STATUS_ANGRY
SET_MESSAGE_CONTENTS_SAD77VOICE_STATUS_SAD
SET_MESSAGE_CONTENTS_FUN78VOICE_STATUS_FUN
SET_MESSAGE_CONTENTS_SLEEPY79VOICE_STATUS_SLEEPY
SET_MESSAGE_CONTENTS_GLOOMY97VOICE_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.

Angry (1)

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.

Sad (2), Sleepy (4), Gloomy (5)

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.

Fun (3)

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.

Normal (0)

pitch = 1.0;  vol = 1.0;

Reaching the DSP

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).

The sequence side

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.

Per-frame parameter push

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.

Frame ordering

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.

Banks

From src/static/jaudio_NES/game/audioheaders.c:

BankROM addrSizeparam0param1Instruments
1560x0005C2600x45000x05FF0x7700119
1570x000607600x30200x05FF0x530083
1580x000637800x45000x05FF0x7700119

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).

← 4. The phonology engine 6. The soundfont lookup →