aboutsummaryrefslogtreecommitdiff
path: root/docs/scheduling.html
diff options
context:
space:
mode:
Diffstat (limited to 'docs/scheduling.html')
-rw-r--r--docs/scheduling.html147
1 files changed, 147 insertions, 0 deletions
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 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>3. Windowing &amp; timing — Animalese</title>
+<link rel="stylesheet" href="style.css">
+</head>
+<body>
+
+<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 3. Windowing &amp; timing</nav>
+
+<h1>3. Windowing &amp; timing</h1>
+<p class="lede">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.</p>
+
+<h2>The text cursor</h2>
+
+<p><code>mMsg_Main_Cursol_ControlCursol</code> (<code>src/game/m_msg_cursol.c_inc:1100</code>)
+runs once per game frame while a message is scrolling. Its loop:</p>
+
+<ol>
+ <li>Decrement <code>cursor_timer</code>; if still positive, do nothing this frame.
+ (Control code <code>mFont_CONT_CODE_CURSOR_SET_TIME</code>, value 3, sets it — this is
+ how messages pause mid-sentence.)</li>
+ <li>Skip newlines, message tags and control codes, executing their side effects.</li>
+ <li>On a printable character: advance <code>end_text_cursor_idx</code>
+ <strong>only if <code>mMsg_STATUS_FLAG_NOT_PAUSE_FRAME</code> is clear</strong>,
+ then toggle that flag, then call <code>mMsg_sound_CodeVoice(msg_p, cur_idx)</code>
+ and break.</li>
+</ol>
+
+<p>The flag toggle is the speed control: the index moves on alternate frames, so text
+advances at <strong>one character per two frames</strong>, roughly 33 characters/second
+at 60 fps.</p>
+
+<p>Holding <kbd>B</kbd> sets <code>mMsg_STATUS_FLAG_FAST_TEXT</code>, which zeroes
+<code>cursor_timer</code> and <em>clears</em> <code>NOT_PAUSE_FRAME</code> each frame, giving one
+character per frame. It also switches the reported message speed from 2 to 1
+(<code>mMsg_sound_MessageSpeedClear</code>), which the audio engine stores in
+<code>sou_message_speed</code>.</p>
+
+<h2>Building the (a, b, c) window</h2>
+
+<p><code>mMsg_sound_CodeVoice</code> (<code>m_msg_sound.c_inc:126</code>) converts the character
+and hands it to <code>mMsg_sound_voice_entry</code> (<code>:85</code>), which is the state
+machine:</p>
+
+<pre>if (idx == msg_p-&gt;voice_sfx_idx) return; // already seen this index
+if (idx == msg_p-&gt;end_text_cursor_idx) return; // this is a non-advancing frame
+msg_p-&gt;voice_sfx_idx = idx;
+
+if (status_flags &amp; 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;
+}</pre>
+
+<p>Two guards deserve attention. The <code>end_text_cursor_idx</code> 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 (<code>0x81</code>) is never allowed to be the <code>a</code> of a
+window, only the <code>b</code>; this is why utterances tend to break <em>after</em> a
+space rather than starting on one.</p>
+
+<p>The <code>c</code> lookahead reads one character past <code>b</code> and clamps a newline or a
+control code to <code>0xFF</code>. It exists for exactly two rules in the phonology:
+<code>you</code> and magic&nbsp;e.</p>
+
+<h2>Firing</h2>
+
+<p>Nothing plays from the message code. The trigger is in the draw pass —
+<code>mMsg_draw_font</code> (<code>src/game/m_msg_draw_font.c_inc:35</code>) decides whether
+this frame's glyph should speak:</p>
+
+<pre>if (status_flags &amp; VOICE_ENTRY) {
+ if (status_flags &amp; FLAG_18) voice = TRUE; // pair complete
+ else if (cursor_timer != 0 || force_voice_enable_flag) voice = TRUE;
+}</pre>
+
+<p>It then finds the line containing <code>voice_sfx_idx</code>, sets
+<code>mFont_SENTENCE_FLAG_VOICE_SE</code> on that line's <code>mFontSentence</code>, and clears
+both flags. <code>mFontSentence_gppDraw_main</code>
+(<code>src/game/m_font_main.c_inc:1078</code>) calls the speak function immediately after
+drawing the glyph at index <code>se_pos</code>, so audio is frame-locked to the character
+appearing on screen.</p>
+
+<p>The second branch is the escape hatch: if the cursor is paused (a
+<code>CURSOR_SET_TIME</code> code) or a control code returned
+<code>mMsg_RESULT_TRUE</code>, a half-built window with <code>b = c = 0xFF</code> fires anyway
+rather than being held indefinitely.</p>
+
+<h3>Scale → volume</h3>
+
+<p><code>mFontSentence_animal_voice_se</code> (<code>m_font_main.c_inc:951</code>) computes the
+<code>scale</code> argument from the glyph's own transform:</p>
+
+<pre>f_scale = sqrtf(scale.x² + scale.y²);
+scale = line_scale * f_scale * (32.0f / √2) + 0.5f; // clamped 0…255</pre>
+
+<p>32 is normal size, so a normally scaled glyph passes <code>scale == 32</code> and the
+volume adjustment is skipped entirely. (What happens for other values is
+<a href="quirks.html">not what it looks like</a>.)</p>
+
+<p>It also contains one very specific special case: if the character is ♪ at exactly
+normal scale in colour <code>rgba(50, 60, 50, 255)</code>, 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.</p>
+
+<h2>End of message</h2>
+
+<p><code>mMsg_sound_voice_endcode_set</code> (<code>m_msg_sound.c_inc:117</code>) sends
+<code>sAdo_VoiceSe(0x84, 0xFF, 0xFF, npc, 32, 0)</code> — a synthetic newline — and clears
+both window flags. It runs when the message ends, and also when the
+<code>mFont_CONT_CODE_SOUND_CUT</code> control code (value 81) appears, which additionally
+plays system SE <code>0x54</code> and suppresses the rest of the line's voice.</p>
+
+<h2>The resulting cadence</h2>
+
+<pre>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
+…</pre>
+
+<p>So: one <code>Na_VoiceSe</code> 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
+<a href="playback.html">two alternating subtracks</a>.</p>
+
+<div class="pager">
+ <span><a href="encoding.html">← 2. Text → voice code</a></span>
+ <span><a href="phonology.html">4. The phonology engine →</a></span>
+</div>
+
+</body>
+</html>