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

3. Windowing & timing

+

The phonology engine needs three characters at once. Getting them there +takes two cursor steps, a pair of status flags, and a trigger that fires from the font +renderer rather than the message logic.

+ +

The text cursor

+ +

mMsg_Main_Cursol_ControlCursol (src/game/m_msg_cursol.c_inc:1100) +runs once per game frame while a message is scrolling. Its loop:

+ +
    +
  1. Decrement cursor_timer; if still positive, do nothing this frame. + (Control code mFont_CONT_CODE_CURSOR_SET_TIME, value 3, sets it — this is + how messages pause mid-sentence.)
  2. +
  3. Skip newlines, message tags and control codes, executing their side effects.
  4. +
  5. On a printable character: advance end_text_cursor_idx + only if mMsg_STATUS_FLAG_NOT_PAUSE_FRAME is clear, + then toggle that flag, then call mMsg_sound_CodeVoice(msg_p, cur_idx) + and break.
  6. +
+ +

The flag toggle is the speed control: the index moves on alternate frames, so text +advances at one character per two frames, roughly 33 characters/second +at 60 fps.

+ +

Holding B sets mMsg_STATUS_FLAG_FAST_TEXT, which zeroes +cursor_timer and clears NOT_PAUSE_FRAME each frame, giving one +character per frame. It also switches the reported message speed from 2 to 1 +(mMsg_sound_MessageSpeedClear), which the audio engine stores in +sou_message_speed.

+ +

Building the (a, b, c) window

+ +

mMsg_sound_CodeVoice (m_msg_sound.c_inc:126) converts the character +and hands it to mMsg_sound_voice_entry (:85), which is the state +machine:

+ +
if (idx == msg_p->voice_sfx_idx) return;          // already seen this index
+if (idx == msg_p->end_text_cursor_idx) return;    // this is a non-advancing frame
+msg_p->voice_sfx_idx = idx;
+
+if (status_flags & VOICE_ENTRY) {
+    // second character of the pair
+    voice2_idx = voice_no;                        // b
+    voice3_idx = lookahead(idx + code_size);      // c, or 0xFF at a newline
+    status_flags |= FLAG_18;
+}
+else if (voice_no != 0x81) {                      // a space cannot open a pair
+    voice_idx  = voice_no;                        // a
+    voice2_idx = voice3_idx = 0xFF;
+    animal_voice_idx = npc_voice_id;
+    status_flags |= VOICE_ENTRY;
+}
+ +

Two guards deserve attention. The end_text_cursor_idx comparison is what +makes the every-other-frame toggle actually halve the entry rate — on a +non-advancing frame the pre-advance index equals the post-advance index and the call +is dropped. And a space (0x81) is never allowed to be the a of a +window, only the b; this is why utterances tend to break after a +space rather than starting on one.

+ +

The c lookahead reads one character past b and clamps a newline or a +control code to 0xFF. It exists for exactly two rules in the phonology: +you and magic e.

+ +

Firing

+ +

Nothing plays from the message code. The trigger is in the draw pass — +mMsg_draw_font (src/game/m_msg_draw_font.c_inc:35) decides whether +this frame's glyph should speak:

+ +
if (status_flags & VOICE_ENTRY) {
+    if (status_flags & FLAG_18)                  voice = TRUE;   // pair complete
+    else if (cursor_timer != 0 || force_voice_enable_flag) voice = TRUE;
+}
+ +

It then finds the line containing voice_sfx_idx, sets +mFont_SENTENCE_FLAG_VOICE_SE on that line's mFontSentence, and clears +both flags. mFontSentence_gppDraw_main +(src/game/m_font_main.c_inc:1078) calls the speak function immediately after +drawing the glyph at index se_pos, so audio is frame-locked to the character +appearing on screen.

+ +

The second branch is the escape hatch: if the cursor is paused (a +CURSOR_SET_TIME code) or a control code returned +mMsg_RESULT_TRUE, a half-built window with b = c = 0xFF fires anyway +rather than being held indefinitely.

+ +

Scale → volume

+ +

mFontSentence_animal_voice_se (m_font_main.c_inc:951) computes the +scale argument from the glyph's own transform:

+ +
f_scale = sqrtf(scale.x² + scale.y²);
+scale   = line_scale * f_scale * (32.0f / √2) + 0.5f;   // clamped 0…255
+ +

32 is normal size, so a normally scaled glyph passes scale == 32 and the +volume adjustment is skipped entirely. (What happens for other values is +not what it looks like.)

+ +

It also contains one very specific special case: if the character is ♪ at exactly +normal scale in colour rgba(50, 60, 50, 255), the NPC id is forced to 0, +suppressing the melody. This is presumably a specific piece of dialogue where a +literal music note glyph should stay silent.

+ +

End of message

+ +

mMsg_sound_voice_endcode_set (m_msg_sound.c_inc:117) sends +sAdo_VoiceSe(0x84, 0xFF, 0xFF, npc, 32, 0) — a synthetic newline — and clears +both window flags. It runs when the message ends, and also when the +mFont_CONT_CODE_SOUND_CUT control code (value 81) appears, which additionally +plays system SE 0x54 and suppresses the rest of the line's voice.

+ +

The resulting cadence

+ +
frame  0   char A   entry: a = A                     (nothing plays)
+frame  1   —        non-advancing frame, dropped
+frame  2   char B   entry: b = B, c = lookahead(C)
+                    draw:  sAdo_VoiceSe(A, B, C, …)   ← one call
+frame  3   —        Sou_SpecialRoutine10 replays B as mode 2
+frame  4   char C   entry: a = C
+…
+ +

So: one Na_VoiceSe call per two characters, one pair every four frames +(~66 ms), with the deferred second syllable one game frame (~17 ms) behind the first. +Samples run about 190 ms, so syllables overlap heavily — which is why there are +two alternating subtracks.

+ +
+ ← 2. Text → voice code + 4. The phonology engine → +
+ + + -- cgit v1.3.1