diff options
Diffstat (limited to 'docs/scheduling.html')
| -rw-r--r-- | docs/scheduling.html | 147 |
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 & timing — Animalese</title> +<link rel="stylesheet" href="style.css"> +</head> +<body> + +<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 3. Windowing & timing</nav> + +<h1>3. Windowing & 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->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; +}</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 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 & VOICE_ENTRY) { + if (status_flags & 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> |