From 46227e2003de28202cc4e82bca4f7f4745476c4e Mon Sep 17 00:00:00 2001 From: Alexander Kassouni Date: Sat, 5 Sep 2026 17:51:13 -0700 Subject: init --- docs/playback.html | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 docs/playback.html (limited to 'docs/playback.html') diff --git a/docs/playback.html b/docs/playback.html new file mode 100644 index 0000000..1cf9464 --- /dev/null +++ b/docs/playback.html @@ -0,0 +1,196 @@ + + + + + +5. Playback — Animalese + + + + + + +

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 sequence 243 / 244 / 245
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:

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

+ +

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

+ +
+ ← 4. The phonology engine + 6. The soundfont lookup → +
+ + + -- cgit v1.3.1