aboutsummaryrefslogtreecommitdiff
path: root/docs/scheduling.html
blob: 07f1e523a66999485317daf5ba6c8a93f63f57b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<!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> and the NPC id is non-zero, that id
is forced to 0. This does <strong>not</strong> silence it — <code>Na_VoiceSe</code>'s ♪ escape
only bails on <code>voice == 0xFA</code>, and <code>melody_seq_offset[0]</code> /
<code>melody_seq_size[0]</code> are a real <code>0x120</code>-byte entry. The effect is to play
the figure on instrument&nbsp;0 rather than the speaker's own, presumably so one particular
line reads as a generic note instead of that character's timbre.</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>