narrator-ts 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +113 -3
- package/dist/narrator/contour.d.ts +90 -0
- package/dist/narrator/contour.js +235 -0
- package/dist/narrator/duration.d.ts +39 -0
- package/dist/narrator/duration.js +259 -0
- package/dist/narrator/frames.d.ts +140 -0
- package/dist/narrator/frames.js +428 -0
- package/dist/narrator/index.d.ts +15 -0
- package/dist/narrator/index.js +13 -0
- package/dist/narrator/interpolate.d.ts +183 -0
- package/dist/narrator/interpolate.js +467 -0
- package/dist/narrator/onset.d.ts +25 -0
- package/dist/narrator/onset.js +73 -0
- package/dist/narrator/parse.d.ts +67 -0
- package/dist/narrator/parse.js +184 -0
- package/dist/narrator/prosody.d.ts +355 -0
- package/dist/narrator/prosody.js +1121 -0
- package/dist/narrator/render.d.ts +64 -0
- package/dist/narrator/render.js +273 -0
- package/dist/narrator/rewrite.d.ts +55 -0
- package/dist/narrator/rewrite.js +155 -0
- package/dist/narrator/speak.d.ts +120 -0
- package/dist/narrator/speak.js +206 -0
- package/dist/narrator/stress.d.ts +36 -0
- package/dist/narrator/stress.js +147 -0
- package/dist/narrator/voice.d.ts +66 -0
- package/dist/narrator/voice.js +78 -0
- package/dist/translator/engines.d.ts +37 -0
- package/dist/translator/engines.js +26 -0
- package/dist/translator/index.d.ts +3 -0
- package/dist/translator/index.js +2 -0
- package/dist/translator/translate.d.ts +10 -0
- package/dist/translator/translate.js +363 -0
- package/dist/translator/types.d.ts +70 -0
- package/dist/translator/types.js +31 -0
- package/package.json +57 -4
- package/reference/README.md +168 -0
- package/reference/formants.json +18 -0
- package/reference/nrl-7948.json +434 -0
- package/reference/nrl-table.json +1 -0
- package/reference/voice-free.json +11775 -0
- package/reference/wordlist.txt +10 -0
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* narrator.device's prosody pass — hunk+0x1ee0 of build 33.2, and the five
|
|
3
|
+
* routines it calls.
|
|
4
|
+
*
|
|
5
|
+
* This is the last stage of the front half to be read, and it is what decides
|
|
6
|
+
* the *tune*. Everything before it works phoneme by phoneme; this works
|
|
7
|
+
* syllable by syllable, one phrase at a time, and it is called round a loop —
|
|
8
|
+
* `hunk+0x832` runs it, then `hunk+0x2160`, until it reports no phrase left.
|
|
9
|
+
*
|
|
10
|
+
* The five build up three arrays indexed by syllable:
|
|
11
|
+
*
|
|
12
|
+
* | | | |
|
|
13
|
+
* |---|---|---|
|
|
14
|
+
* | `0x1f02` | {@link scanPhrase} | `arr4`: how stressed each syllable is |
|
|
15
|
+
* | `0x1fd8` | {@link markBoundaries} | `arr3`: where the phrase breaks are |
|
|
16
|
+
* | `0x20bc` | {@link markVoiced} | `arr5`: seed every syllable with 1 |
|
|
17
|
+
* | `0x20d0` | {@link markPunctuation} | `arr5`: `.` is 4, `?` is 8 |
|
|
18
|
+
* | `0x210a` | {@link markCadence} | `arr3`: the final fall, or the rise |
|
|
19
|
+
*
|
|
20
|
+
* They are five separate `bsr`s that **share `D4`**, the syllable count, in a
|
|
21
|
+
* register — none of the last four sets it up. So they are one routine split
|
|
22
|
+
* five ways rather than five routines, and the count is threaded here.
|
|
23
|
+
*/
|
|
24
|
+
import { TERMINATOR } from './parse.js';
|
|
25
|
+
/** Attribute bits this stage tests. */
|
|
26
|
+
const ATTR = {
|
|
27
|
+
/** Bit 19: `.`, `?` and `,` — the things that end a phrase outright. */
|
|
28
|
+
PHRASE_BREAK: 1 << 19,
|
|
29
|
+
/** Bit 26: not spoken, so a boundary between syllables. */
|
|
30
|
+
BOUNDARY: 1 << 26,
|
|
31
|
+
};
|
|
32
|
+
/** Bits of the stress byte. */
|
|
33
|
+
const STRESS = {
|
|
34
|
+
/** 0x80: the spreader marked this phoneme as a syllable's start. */
|
|
35
|
+
MARK: 0x80,
|
|
36
|
+
/** 0x20: it carries stress. */
|
|
37
|
+
STRESSED: 0x20,
|
|
38
|
+
/** 0x40: it is inside a spread. */
|
|
39
|
+
SPREAD: 0x40,
|
|
40
|
+
};
|
|
41
|
+
/** Values written into `arr4`, the per-syllable descriptor. */
|
|
42
|
+
export const SYLLABLE = {
|
|
43
|
+
/** Bits 0-3: the scaled stress digit. */
|
|
44
|
+
LEVEL: 0x0f,
|
|
45
|
+
/** 0x20: a primary stress. */
|
|
46
|
+
PRIMARY: 0x20,
|
|
47
|
+
/** 0x40: the last syllable of the utterance. */
|
|
48
|
+
LAST: 0x40,
|
|
49
|
+
/** 0x80: a pause follows this syllable. */
|
|
50
|
+
PAUSE: 0x80,
|
|
51
|
+
};
|
|
52
|
+
/** Phoneme indices this stage names outright. */
|
|
53
|
+
const FULL_STOP = 1;
|
|
54
|
+
const QUESTION = 2;
|
|
55
|
+
const DASH = 4;
|
|
56
|
+
/** `hunk+0x1f02` gives up at this many syllables — `arr4` is 0x80 bytes. */
|
|
57
|
+
export const MAX_SYLLABLES = 0x80;
|
|
58
|
+
/**
|
|
59
|
+
* The arrays this stage touches, by the names it uses them under.
|
|
60
|
+
*
|
|
61
|
+
* A syllable's pitch is three numbers: where it starts, how high it gets, and
|
|
62
|
+
* where it ends. `hunk+0x1a8e` reads them out of `arr0`, `arr1` and `arr2` in
|
|
63
|
+
* that order and pins the frames accordingly, so `arr1` is the *peak* despite
|
|
64
|
+
* sitting between the other two in memory. All three are frequencies, and a
|
|
65
|
+
* larger one is a higher note.
|
|
66
|
+
*
|
|
67
|
+
* The whole pitch loop's body builds `arr1` first and holds the other two as
|
|
68
|
+
* distances below it — `arr6` how far the syllable climbs from its start,
|
|
69
|
+
* `arr7` how far it drops to its end. `hunk+0x2642` finally subtracts them.
|
|
70
|
+
* A negative distance is therefore a syllable that goes *up*, which is how a
|
|
71
|
+
* question is spoken.
|
|
72
|
+
*/
|
|
73
|
+
/** `arr0`: the pitch the syllable starts on. */
|
|
74
|
+
const ONSET = 0;
|
|
75
|
+
const PEAK = 1;
|
|
76
|
+
/** `arr2`: the pitch it ends on. */
|
|
77
|
+
const END = 2;
|
|
78
|
+
const CADENCE = 3;
|
|
79
|
+
const DESCRIPTOR = 4;
|
|
80
|
+
const VOICING = 5;
|
|
81
|
+
/** `arr6`: how far the syllable climbs from its onset to its peak. */
|
|
82
|
+
const CLIMB = 6;
|
|
83
|
+
/** `arr7`: how far it drops from its peak to its end. */
|
|
84
|
+
const DROP = 7;
|
|
85
|
+
/**
|
|
86
|
+
* hunk+0x1f02. Walk the next phrase and write one descriptor per syllable.
|
|
87
|
+
*
|
|
88
|
+
* A syllable here is a phoneme the stress spreader marked. Its level comes
|
|
89
|
+
* from the stress digit the writer typed, scaled by 199/128 — so a `4` becomes
|
|
90
|
+
* 6 — and a further 2 if the phoneme is inside a spread. Anything above 4
|
|
91
|
+
* counts as a primary stress and is remembered as such.
|
|
92
|
+
*
|
|
93
|
+
* That scaling is the one place the digits 0-9 stop being a rank and become a
|
|
94
|
+
* number the synthesizer does arithmetic with.
|
|
95
|
+
*
|
|
96
|
+
* Returns the syllable count, which the four routines after this one read out
|
|
97
|
+
* of `D4` rather than recomputing, or `null` if the array overflowed.
|
|
98
|
+
*/
|
|
99
|
+
export function scanPhrase(state, attrs) {
|
|
100
|
+
const { phonemes, stress, flags, counters } = state;
|
|
101
|
+
const arr4 = state.arr[DESCRIPTOR].subarray(state.arrAt);
|
|
102
|
+
let p = state.atPhoneme;
|
|
103
|
+
let s = state.atStress;
|
|
104
|
+
let f = state.atFlag;
|
|
105
|
+
let n = 0; // D4, the syllable being written
|
|
106
|
+
let firstPrimary = -1; // D5
|
|
107
|
+
counters.stresses = 0;
|
|
108
|
+
for (;;) {
|
|
109
|
+
// ------------------------------------------------------------ 0x1f24
|
|
110
|
+
const phoneme = phonemes[p++];
|
|
111
|
+
let mark = stress[s++];
|
|
112
|
+
f++;
|
|
113
|
+
let extra = 0; // D6
|
|
114
|
+
if (phoneme === TERMINATOR) {
|
|
115
|
+
// 0x1fc0: the utterance ends here.
|
|
116
|
+
arr4[n - 1] |= SYLLABLE.LAST;
|
|
117
|
+
if (counters.stresses === 0)
|
|
118
|
+
firstPrimary = n;
|
|
119
|
+
counters.first = firstPrimary & 0xffff;
|
|
120
|
+
counters.syllables = n;
|
|
121
|
+
return n;
|
|
122
|
+
}
|
|
123
|
+
const a = attrs[phoneme] ?? 0;
|
|
124
|
+
if (a & ATTR.BOUNDARY) {
|
|
125
|
+
// 0x1f42: a space or a pause. The syllable before it is flagged, and a
|
|
126
|
+
// `.`, `?` or `,` ends the phrase outright.
|
|
127
|
+
counters.boundaries = (counters.boundaries + 1) & 0xffff;
|
|
128
|
+
arr4[n - 1] |= SYLLABLE.PAUSE;
|
|
129
|
+
if (a & ATTR.PHRASE_BREAK) {
|
|
130
|
+
arr4[n - 1] |= SYLLABLE.LAST;
|
|
131
|
+
if (counters.stresses === 0)
|
|
132
|
+
firstPrimary = n;
|
|
133
|
+
counters.first = firstPrimary & 0xffff;
|
|
134
|
+
counters.syllables = n;
|
|
135
|
+
return n;
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// 0x1f58: only a marked phoneme opens a syllable.
|
|
140
|
+
if (!(mark & STRESS.MARK))
|
|
141
|
+
continue;
|
|
142
|
+
// `D1` is still the whole stress byte here, and 0x1f9e masks it to the
|
|
143
|
+
// low nibble at the end — so an unstressed syllable keeps the digit as
|
|
144
|
+
// typed and only a stressed one gets the scaled value below.
|
|
145
|
+
let level = mark;
|
|
146
|
+
if (mark & STRESS.STRESSED) {
|
|
147
|
+
// 0x1f68: the digit sits in the low nibble, but the spreader may have
|
|
148
|
+
// put the mark on a phoneme in front of the one that carries it, so
|
|
149
|
+
// walk forward to the first non-zero digit.
|
|
150
|
+
level = mark & 0x0f;
|
|
151
|
+
while (level === 0) {
|
|
152
|
+
f++;
|
|
153
|
+
p++;
|
|
154
|
+
mark = stress[s++];
|
|
155
|
+
level = mark & 0x0f;
|
|
156
|
+
}
|
|
157
|
+
// 0x1f76: x199/128, and +2 inside a spread.
|
|
158
|
+
level = ((level * 0xc7) & 0xffff) >> 7;
|
|
159
|
+
if (stress[s - 1] & STRESS.SPREAD)
|
|
160
|
+
level += 2;
|
|
161
|
+
if (((level << 24) >> 24) > 4) {
|
|
162
|
+
counters.stresses = (counters.stresses + 1) & 0xffff;
|
|
163
|
+
counters.last = n;
|
|
164
|
+
extra = SYLLABLE.PRIMARY;
|
|
165
|
+
if (firstPrimary < 0)
|
|
166
|
+
firstPrimary = n;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// 0x1f9e: only the low nibble of the scaled level survives.
|
|
170
|
+
arr4[n] = ((level & 0x0f) | extra) & 0xff;
|
|
171
|
+
n++;
|
|
172
|
+
counters.total = (counters.total + 1) & 0xffff;
|
|
173
|
+
// 0x1fb0: `arr4` is 0x80 bytes and this is where it gives up.
|
|
174
|
+
if (counters.total === MAX_SYLLABLES)
|
|
175
|
+
return null;
|
|
176
|
+
// The cursors are deliberately not written back: 0x1f02 reads them and
|
|
177
|
+
// leaves them alone, and `markBoundaries` is what moves them on.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* hunk+0x1fd8. Walk the same phrase again and record where it breaks, then
|
|
182
|
+
* move the cursors past it.
|
|
183
|
+
*
|
|
184
|
+
* `arr3` gets `0x90` at a dash, and that is all it gets in this build.
|
|
185
|
+
*
|
|
186
|
+
* The other two markers, `2` and `0x0e`, come from bits 4 and 5 of the flag
|
|
187
|
+
* byte, and **nothing in 33.2 ever sets them**. The parser zeroes the array,
|
|
188
|
+
* the rewrite engine zeroes what it inserts, and the stress spreader writes
|
|
189
|
+
* only `0x80` and `0x40`; every flag byte reaching here across the whole
|
|
190
|
+
* corpus is one of 0, 0x40, 0x80 or 0xc0. So the marker-shuffling pass at the
|
|
191
|
+
* end of this routine — which walks to the nearest primary stress and moves a
|
|
192
|
+
* marker onto it, backwards for `0x0e` and forwards for `2` — can never run.
|
|
193
|
+
*
|
|
194
|
+
* It is ported anyway. It is real code with a clear meaning, it is what the
|
|
195
|
+
* routine says, and 37.7 is a rewrite that may well feed it.
|
|
196
|
+
*/
|
|
197
|
+
export function markBoundaries(state, attrs) {
|
|
198
|
+
const { phonemes, stress, flags } = state;
|
|
199
|
+
const arr3 = state.arr[CADENCE].subarray(state.arrAt);
|
|
200
|
+
const arr4 = state.arr[DESCRIPTOR].subarray(state.arrAt);
|
|
201
|
+
let p = state.atPhoneme;
|
|
202
|
+
let s = state.atStress;
|
|
203
|
+
let f = state.atFlag;
|
|
204
|
+
let n = 0; // D4 again, counted the same way
|
|
205
|
+
for (;;) {
|
|
206
|
+
// ------------------------------------------------------------ 0x1ff6
|
|
207
|
+
const phoneme = phonemes[p++];
|
|
208
|
+
const mark = stress[s++];
|
|
209
|
+
const flag = flags[f++];
|
|
210
|
+
if (phoneme === TERMINATOR) {
|
|
211
|
+
// 0x2054: back the cursors up over the terminator so the next pass
|
|
212
|
+
// starts on it and stops immediately.
|
|
213
|
+
p--;
|
|
214
|
+
s--;
|
|
215
|
+
f--;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
if (phoneme === DASH) {
|
|
219
|
+
arr3[n - 1] |= 0x90;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const a = attrs[phoneme] ?? 0;
|
|
223
|
+
// 0x2018: a real phrase break leaves the cursors *past* it, so the next
|
|
224
|
+
// pass round the loop begins after the punctuation.
|
|
225
|
+
if (a & ATTR.PHRASE_BREAK)
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
// 0x201e
|
|
229
|
+
if (mark & STRESS.MARK)
|
|
230
|
+
n++;
|
|
231
|
+
if (flag & 0x20) {
|
|
232
|
+
arr3[n - 1] = 2;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!(flag & 0x10))
|
|
236
|
+
continue;
|
|
237
|
+
// 0x2038: the low nibble as a signed nibble.
|
|
238
|
+
const v = ((arr3[n - 1] & 0x0f) << 4) >> 4;
|
|
239
|
+
if (v === 0 || v <= -2)
|
|
240
|
+
arr3[n - 1] |= 0x0e;
|
|
241
|
+
}
|
|
242
|
+
state.atPhoneme = p;
|
|
243
|
+
state.atStress = s;
|
|
244
|
+
state.atFlag = f;
|
|
245
|
+
// ---------------------------------------------------------------- 0x2066
|
|
246
|
+
let carry = 0; // D7
|
|
247
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
248
|
+
arr4[i] |= carry;
|
|
249
|
+
let at = i;
|
|
250
|
+
let step;
|
|
251
|
+
if (arr3[i] === 2) {
|
|
252
|
+
step = 1;
|
|
253
|
+
carry = 0;
|
|
254
|
+
}
|
|
255
|
+
else if (arr3[i] === 0x0e) {
|
|
256
|
+
step = -1;
|
|
257
|
+
carry = 0x10;
|
|
258
|
+
arr4[i] |= carry;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
// 0x2092: walk until a primary stress, then move the marker onto it.
|
|
264
|
+
for (;;) {
|
|
265
|
+
if (arr4[at] & SYLLABLE.PRIMARY) {
|
|
266
|
+
arr3[at] = arr3[i];
|
|
267
|
+
if (at !== i)
|
|
268
|
+
arr3[i] = 0;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
at += step;
|
|
272
|
+
if (at < 0 || at > n) {
|
|
273
|
+
arr3[i] = 0;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return n;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* hunk+0x20bc. Set bit 0 on every syllable of the phrase.
|
|
282
|
+
*
|
|
283
|
+
* `D4` is not loaded here — it is still the count `scanPhrase` left behind.
|
|
284
|
+
*/
|
|
285
|
+
export function markVoiced(state, syllables) {
|
|
286
|
+
const arr5 = state.arr[VOICING].subarray(state.arrAt);
|
|
287
|
+
for (let i = syllables - 1; i >= 0; i--)
|
|
288
|
+
arr5[i] |= 1;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* hunk+0x20d0. Record what the phrase ended with — `.` is 4, `?` is 8 —
|
|
292
|
+
* backwards from the end until a syllable that a pause follows.
|
|
293
|
+
*
|
|
294
|
+
* A comma is neither, so it leaves nothing; the difference between a statement
|
|
295
|
+
* and a question is decided here and read by {@link markCadence}.
|
|
296
|
+
*/
|
|
297
|
+
export function markPunctuation(state, syllables) {
|
|
298
|
+
const arr4 = state.arr[DESCRIPTOR].subarray(state.arrAt);
|
|
299
|
+
const arr5 = state.arr[VOICING].subarray(state.arrAt);
|
|
300
|
+
const ended = state.phonemes[state.atPhoneme - 1];
|
|
301
|
+
let code = 0;
|
|
302
|
+
if (ended === FULL_STOP)
|
|
303
|
+
code = 4;
|
|
304
|
+
if (ended === QUESTION)
|
|
305
|
+
code = 8;
|
|
306
|
+
for (let i = syllables - 1; i >= 0; i--) {
|
|
307
|
+
arr5[i] |= code;
|
|
308
|
+
// `dbne`: the syllable a pause follows is included, then it stops.
|
|
309
|
+
if (arr4[i] & SYLLABLE.PAUSE)
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* hunk+0x210a. Give the phrase its cadence: `0xb0` normally, `0x30` after a
|
|
315
|
+
* question, and a `4` on the last primary stress.
|
|
316
|
+
*
|
|
317
|
+
* It does nothing at all when the phrase ended on the punctuation itself —
|
|
318
|
+
* the cursors are past it by then, so `(-1,A0)` is the `.` or `?` and this
|
|
319
|
+
* returns. Only a phrase that ran out some other way gets a cadence.
|
|
320
|
+
*
|
|
321
|
+
* Which means **this** rise never fires. It is chosen by `arr5[n - 1] == 8`,
|
|
322
|
+
* and 8 is what {@link markPunctuation} writes for a `?`; but a phrase that
|
|
323
|
+
* ended in `?` has already returned two lines above, and in any case
|
|
324
|
+
* {@link markVoiced} has put bit 0 into every entry first, so the value is 9
|
|
325
|
+
* and never 8. Two independent reasons, either enough on its own.
|
|
326
|
+
*
|
|
327
|
+
* That is not to say 33.2 cannot ask a question. It does — in
|
|
328
|
+
* {@link linkSyllables}, which reads the same `arr5` byte and inverts the
|
|
329
|
+
* final fall into a rise, and which the corpus drives. This particular rise is
|
|
330
|
+
* the dead one. Ported as written rather than "fixed".
|
|
331
|
+
*/
|
|
332
|
+
export function markCadence(state, syllables) {
|
|
333
|
+
const arr3 = state.arr[CADENCE].subarray(state.arrAt);
|
|
334
|
+
const arr4 = state.arr[DESCRIPTOR].subarray(state.arrAt);
|
|
335
|
+
const arr5 = state.arr[VOICING].subarray(state.arrAt);
|
|
336
|
+
const { counters } = state;
|
|
337
|
+
const ended = state.phonemes[state.atPhoneme - 1];
|
|
338
|
+
if (ended === FULL_STOP || ended === QUESTION)
|
|
339
|
+
return;
|
|
340
|
+
// 0x212e: a question ends up rising instead of falling.
|
|
341
|
+
arr3[syllables - 1] |= arr5[syllables - 1] === 8 ? 0x30 : 0xb0;
|
|
342
|
+
if (counters.stresses === 0)
|
|
343
|
+
return;
|
|
344
|
+
// 0x2144: back to the last primary stress, and mark it. `dbne` can leave
|
|
345
|
+
// the index at -1, and the device would then write the byte before `arr3`;
|
|
346
|
+
// a phrase with `stresses` non-zero always has one, so it does not happen.
|
|
347
|
+
let i = syllables - 1;
|
|
348
|
+
for (;;) {
|
|
349
|
+
if (arr4[i] & SYLLABLE.PRIMARY)
|
|
350
|
+
break;
|
|
351
|
+
if (--i < 0)
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
arr3[i] = ((arr3[i] & 0xf0) | 4) & 0xff;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* hunk+0x1ee0. One pass round the driver's loop: find the next phrase and
|
|
358
|
+
* describe it, or report that there is none left.
|
|
359
|
+
*
|
|
360
|
+
* The driver at `hunk+0x832` calls this, then `hunk+0x2160` to turn the
|
|
361
|
+
* description into pitch values, and goes round again until this returns
|
|
362
|
+
* false. `false` is the device's `Z` exit and comes straight from the
|
|
363
|
+
* `move.w D4,(A5+0x8c)` that ends {@link scanPhrase} — a syllable count of
|
|
364
|
+
* zero sets it, which is why the routine ends by storing the count rather
|
|
365
|
+
* than by testing anything.
|
|
366
|
+
*
|
|
367
|
+
* Note the count the last three routines see is {@link markBoundaries}'s, not
|
|
368
|
+
* {@link scanPhrase}'s. Both count phonemes the spreader marked, but the scan
|
|
369
|
+
* can walk past some of them looking for a stress digit, so they are not
|
|
370
|
+
* guaranteed to agree, and the device leaves whichever ran last in `D4`.
|
|
371
|
+
*/
|
|
372
|
+
export function nextPhrase(state, attrs) {
|
|
373
|
+
state.counters.pass = (state.counters.pass + 1) & 0xffff;
|
|
374
|
+
const found = scanPhrase(state, attrs);
|
|
375
|
+
// 0x1eea: the overflow exit. The driver treats it as an error and abandons
|
|
376
|
+
// the utterance rather than truncating it.
|
|
377
|
+
if (found === null)
|
|
378
|
+
return false;
|
|
379
|
+
if (found === 0)
|
|
380
|
+
return false;
|
|
381
|
+
const syllables = markBoundaries(state, attrs);
|
|
382
|
+
markVoiced(state, syllables);
|
|
383
|
+
markPunctuation(state, syllables);
|
|
384
|
+
markCadence(state, syllables);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* hunk+0x21b8. The pitch the phrase's first stressed syllable starts on.
|
|
389
|
+
*
|
|
390
|
+
* This is **declination**: the value falls as the utterance goes on. It is
|
|
391
|
+
* built from how many phrases have been spoken (`pass`) and how many
|
|
392
|
+
* boundaries have been crossed, not from anything about the syllable itself,
|
|
393
|
+
* so the sixth phrase of a paragraph starts lower than the first — which is
|
|
394
|
+
* what a speaker running out of breath actually does.
|
|
395
|
+
*
|
|
396
|
+
* The result is clamped to 125..165, and returned because `hunk+0x220c` picks
|
|
397
|
+
* it up out of `D0` rather than reading it back.
|
|
398
|
+
*/
|
|
399
|
+
export function phrasePitch(state) {
|
|
400
|
+
const { counters } = state;
|
|
401
|
+
const arr1 = state.arr[PEAK].subarray(state.arrAt);
|
|
402
|
+
// 0x21bc: `divu.w`, so this is (boundaries + 4) / 3, truncated.
|
|
403
|
+
const spread = Math.floor(((counters.boundaries + 4) & 0xffff) / 3);
|
|
404
|
+
// 0x21c2: `(6 - pass) * 2`, floored at zero once six phrases have gone by.
|
|
405
|
+
let rise = ((6 - counters.pass) << 1) & 0xffff;
|
|
406
|
+
if (rise & 0x8000)
|
|
407
|
+
rise = 0;
|
|
408
|
+
let peak = ((rise * spread) & 0xffff) + 0x7b;
|
|
409
|
+
peak = (peak - ((counters.pass << 3) & 0xffff)) & 0xffff;
|
|
410
|
+
// 0x21f2: a floor of 125 and a ceiling of 165, in that order.
|
|
411
|
+
const signed = (peak << 16) >> 16;
|
|
412
|
+
if (signed <= 0x7d)
|
|
413
|
+
peak = 0x7d;
|
|
414
|
+
if (((peak << 16) >> 16) >= 0xa5)
|
|
415
|
+
peak = 0xa5;
|
|
416
|
+
arr1[counters.first] = peak & 0xff;
|
|
417
|
+
return peak;
|
|
418
|
+
}
|
|
419
|
+
/** `ext.w` and `ext.l` — sign-extend the low byte, and the low word. */
|
|
420
|
+
const sb = (v) => ((v & 0xff) << 24) >> 24;
|
|
421
|
+
const sw = (v) => ((v & 0xffff) << 16) >> 16;
|
|
422
|
+
/** `muls.w`: both operands are low words, and the result is a longword. */
|
|
423
|
+
const muls = (a, b) => sw(a) * sw(b);
|
|
424
|
+
/**
|
|
425
|
+
* The rounding idiom the rest of the pitch loop's body is built out of:
|
|
426
|
+
* `bpl` on the product, then `addi.w #$40` or `subi.w #$40`, then `asr.w #7`.
|
|
427
|
+
*
|
|
428
|
+
* It is a divide by 128 rounding to nearest and away from zero, and it is
|
|
429
|
+
* written out longhand at every one of the eleven places it appears. The sign
|
|
430
|
+
* test is on the whole longword the `muls.w` produced; the rounding and the
|
|
431
|
+
* shift are on its low word only.
|
|
432
|
+
*/
|
|
433
|
+
const round7 = (v) => sw(sw(v) + (v < 0 ? -0x40 : 0x40)) >> 7;
|
|
434
|
+
/**
|
|
435
|
+
* hunk+0x220c. Give every stressed syllable of the phrase its peak pitch.
|
|
436
|
+
*
|
|
437
|
+
* {@link phrasePitch} has put a value on the first one; this walks the rest and
|
|
438
|
+
* steps down from it, landing on 110 at the last — 115 if the phrase ended in
|
|
439
|
+
* a question mark, which is the only effect a `?` has on this build's contour
|
|
440
|
+
* and the only place the question survives at all.
|
|
441
|
+
*
|
|
442
|
+
* Three passes, in one routine:
|
|
443
|
+
*
|
|
444
|
+
* 1. **0x224c** — share the whole fall out over the phrase's primary stresses,
|
|
445
|
+
* but give the first and the last 19/128 of a step extra and take it back
|
|
446
|
+
* evenly from the ones in between. So the pitch drops fastest at each end
|
|
447
|
+
* of the phrase and coasts through the middle, which is what declination
|
|
448
|
+
* actually looks like. Below four stresses there is no middle to take it
|
|
449
|
+
* from and every step is equal.
|
|
450
|
+
* 2. **0x2274** — backwards, pulling each stressed syllable 51/128 of the way
|
|
451
|
+
* towards a neighbour. Dead in 33.2: the neighbour is chosen by the low two
|
|
452
|
+
* bits of `arr5`, {@link markVoiced} puts a 1 in every entry of it before
|
|
453
|
+
* this runs, and 1 is the "leave it alone" case.
|
|
454
|
+
* 3. **0x22d6** — scale by how stressed each syllable is. A level above 8 is
|
|
455
|
+
* pushed further above 110 and one below 8 pulled back towards it, in
|
|
456
|
+
* proportion to how far above 110 it already sits, so the contrast between
|
|
457
|
+
* a strong and a weak stress opens up at the top of the phrase and closes
|
|
458
|
+
* at the bottom.
|
|
459
|
+
*
|
|
460
|
+
* `pitch` is {@link phrasePitch}'s result, which the device leaves in `D0`.
|
|
461
|
+
*/
|
|
462
|
+
export function syllablePitch(state, pitch) {
|
|
463
|
+
const { counters } = state;
|
|
464
|
+
const arr1 = state.arr[PEAK].subarray(state.arrAt);
|
|
465
|
+
const arr4 = state.arr[DESCRIPTOR].subarray(state.arrAt);
|
|
466
|
+
const arr5 = state.arr[VOICING].subarray(state.arrAt);
|
|
467
|
+
// 0x2214: what the phrase falls to. `arr5` bits 2-3 are the punctuation
|
|
468
|
+
// {@link markPunctuation} recorded, and 8 is a question mark.
|
|
469
|
+
const floor = (arr5[counters.syllables - 1] & 0x0c) === 8 ? 0x73 : 0x6e;
|
|
470
|
+
// 0x222c: `sub.b` then `divu.w`. `hunk+0x2160` has already checked the
|
|
471
|
+
// phrase has a primary stress in it, which is what stops the divide by zero.
|
|
472
|
+
const drop = Math.floor(((pitch - floor) & 0xff) / counters.stresses) & 0xffff;
|
|
473
|
+
let big = drop;
|
|
474
|
+
let small = drop;
|
|
475
|
+
if (sw(counters.stresses) >= 4) {
|
|
476
|
+
// 0x2238: `mulu.w` writes a longword and `lsr.w` shifts only half of it,
|
|
477
|
+
// so this is the low word of the product, not the product.
|
|
478
|
+
const extra = ((drop * 0x13) & 0xffff) >> 7;
|
|
479
|
+
big = (drop + extra) & 0xffff;
|
|
480
|
+
small = (drop - Math.floor(((extra << 1) & 0xffff) / (counters.stresses - 3))) & 0xffff;
|
|
481
|
+
}
|
|
482
|
+
// ------------------------------------------------------------------ 0x224c
|
|
483
|
+
// The device tests at the bottom, so this runs once even when the first
|
|
484
|
+
// stress is the last syllable — it reads one descriptor past the phrase.
|
|
485
|
+
// That byte belongs to a syllable not yet written, so it is zero and nothing
|
|
486
|
+
// comes of it; in the device it is the first byte of `arr5`.
|
|
487
|
+
let step = big & 0xff;
|
|
488
|
+
let level = arr1[counters.first];
|
|
489
|
+
let i = (counters.first + 1) & 0xffff;
|
|
490
|
+
for (;;) {
|
|
491
|
+
if (arr4[i] & SYLLABLE.PRIMARY) {
|
|
492
|
+
// 0x225e: the last primary stress takes the big step too.
|
|
493
|
+
if (i === counters.last)
|
|
494
|
+
step = big & 0xff;
|
|
495
|
+
level = (level - step) & 0xff;
|
|
496
|
+
arr1[i] = level;
|
|
497
|
+
step = small & 0xff;
|
|
498
|
+
}
|
|
499
|
+
i = (i + 1) & 0xffff;
|
|
500
|
+
if (sw(counters.syllables) <= sw(i))
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
// ------------------------------------------------------------------ 0x2274
|
|
504
|
+
/** The last stressed syllable this pass looked at — `D2`, and it moves down. */
|
|
505
|
+
let after = counters.last;
|
|
506
|
+
for (let j = counters.last;; j--) {
|
|
507
|
+
if (arr4[j] & SYLLABLE.PRIMARY) {
|
|
508
|
+
const towards = arr5[j] & 3;
|
|
509
|
+
if (towards === 0) {
|
|
510
|
+
// 0x22b6: away from the syllable after it, so the two spread apart.
|
|
511
|
+
let d = sb(arr1[j] - arr1[after]);
|
|
512
|
+
if (d >= 0)
|
|
513
|
+
d = -d;
|
|
514
|
+
arr1[j] = (arr1[j] + (((d * 0x33) >> 7) & 0xff)) & 0xff;
|
|
515
|
+
}
|
|
516
|
+
else if (towards !== 1) {
|
|
517
|
+
// 0x2290: towards the previous stressed syllable. Reaching the first
|
|
518
|
+
// one ends the pass outright rather than skipping it.
|
|
519
|
+
if (sw(j) <= sw(counters.first))
|
|
520
|
+
break;
|
|
521
|
+
// 0x229a: `dbne`, so a run with no stress in it leaves the index at
|
|
522
|
+
// -1 and the device reads the byte before `arr1`. It cannot happen —
|
|
523
|
+
// `first` is a primary stress by construction and `j` is past it.
|
|
524
|
+
let k = (j - 1) & 0xffff;
|
|
525
|
+
while (!(arr4[k] & SYLLABLE.PRIMARY)) {
|
|
526
|
+
k = (k - 1) & 0xffff;
|
|
527
|
+
if (k === 0xffff)
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
let d = sb(arr1[j] - arr1[k]);
|
|
531
|
+
if (d < 0)
|
|
532
|
+
d = -d;
|
|
533
|
+
arr1[j] = (arr1[j] + (((d * 0x33) >> 7) & 0xff)) & 0xff;
|
|
534
|
+
}
|
|
535
|
+
after = j;
|
|
536
|
+
}
|
|
537
|
+
if (j === 0)
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
// ------------------------------------------------------------------ 0x22d6
|
|
541
|
+
for (let k = counters.last;; k--) {
|
|
542
|
+
const descriptor = arr4[k];
|
|
543
|
+
if (descriptor & SYLLABLE.PRIMARY) {
|
|
544
|
+
const weight = sw((descriptor & SYLLABLE.LEVEL) - 8);
|
|
545
|
+
// `mulu.w`, so a syllable that has already fallen below 110 wraps to a
|
|
546
|
+
// large positive here instead of going negative — the routine assumes
|
|
547
|
+
// the fall lands on the floor and never undershoots it.
|
|
548
|
+
const above = ((((arr1[k] - 0x6e) & 0xffff) * 0x0d) & 0xffff) >>> 7;
|
|
549
|
+
arr1[k] = (arr1[k] + weight * sw(above)) & 0xff;
|
|
550
|
+
}
|
|
551
|
+
if (k === 0)
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* hunk+0x230c. How far each stressed syllable climbs to the peak
|
|
557
|
+
* {@link syllablePitch} gave it, and how far it drops away again.
|
|
558
|
+
*
|
|
559
|
+
* `arr6` is the climb and `arr7` the drop, both as distances *below* the peak,
|
|
560
|
+
* and `hunk+0x2642` finally subtracts them to get the syllable's onset and its
|
|
561
|
+
* end. Both are proportional to how far the peak sits above 110, so a syllable
|
|
562
|
+
* that has already fallen to the floor of the phrase gets no contour at all
|
|
563
|
+
* and the utterance flattens out as it ends.
|
|
564
|
+
*
|
|
565
|
+
* The shape is set by the low nibble of the cadence byte, read as a *signed*
|
|
566
|
+
* nibble: the climb is `(26·cadence + 128)/128` of that distance and the drop
|
|
567
|
+
* is `(cadence − 1)·26/128` of it. A cadence of 4 — what {@link markCadence}
|
|
568
|
+
* puts on the last primary stress of a phrase — makes the climb nearly twice
|
|
569
|
+
* the default and the drop three times it, which is the sentence-final fall.
|
|
570
|
+
*
|
|
571
|
+
* A negative nibble would invert the climb, and none is reachable: the only
|
|
572
|
+
* values anything puts in that nibble are 0 and the 4 {@link markCadence}
|
|
573
|
+
* writes, the 2 and the 0x0e of {@link markBoundaries} both coming from flag
|
|
574
|
+
* bits nothing in 33.2 sets.
|
|
575
|
+
*
|
|
576
|
+
* The drop is clipped at zero rather than allowed to go negative here, so at
|
|
577
|
+
* this stage a syllable can end level with its peak but never above it. Only
|
|
578
|
+
* {@link linkSyllables} lifts it above, and only for a question.
|
|
579
|
+
*/
|
|
580
|
+
export function syllableRange(state) {
|
|
581
|
+
const { counters } = state;
|
|
582
|
+
const arr1 = state.arr[PEAK].subarray(state.arrAt);
|
|
583
|
+
const arr3 = state.arr[CADENCE].subarray(state.arrAt);
|
|
584
|
+
const arr4 = state.arr[DESCRIPTOR].subarray(state.arrAt);
|
|
585
|
+
const arr6 = state.arr[CLIMB].subarray(state.arrAt);
|
|
586
|
+
const arr7 = state.arr[DROP].subarray(state.arrAt);
|
|
587
|
+
for (let i = counters.last;; i--) {
|
|
588
|
+
if (arr4[i] & SYLLABLE.PRIMARY) {
|
|
589
|
+
// 0x232e: everything below is a fraction of this.
|
|
590
|
+
const above = sw(arr1[i] - 0x6e);
|
|
591
|
+
// 0x233a: the low nibble, sign-extended by hand.
|
|
592
|
+
const nibble = arr3[i] & 0x0f;
|
|
593
|
+
const cadence = sw(nibble | (nibble & 0x08 ? 0xfff0 : 0));
|
|
594
|
+
// 0x2346: two shifts of seven with a `muls.w` between them, so the
|
|
595
|
+
// 51/128 is applied to a value already divided by 128.
|
|
596
|
+
let climb = muls(cadence, 0x1a) + 0x80;
|
|
597
|
+
climb = muls(climb, above) >>> 7;
|
|
598
|
+
arr6[i] = (muls(climb, 0x33) >>> 7) & 0xff;
|
|
599
|
+
// 0x235e: `neg.b` and `bpl`, so the clip is on the byte.
|
|
600
|
+
const drop = (-(muls(muls(cadence - 1, above), 0x1a) >>> 7)) & 0xff;
|
|
601
|
+
arr7[i] = drop & 0x80 ? 0 : drop;
|
|
602
|
+
// 0x2374: a further −38/128 on both, for a syllable carrying the marker
|
|
603
|
+
// `hunk+0x1fd8` moves onto a primary stress and no cadence of its own.
|
|
604
|
+
// Dead in 33.2 for the same reason that marker is: bit 4 of the flag
|
|
605
|
+
// byte is what puts it there, and nothing ever sets it.
|
|
606
|
+
if (arr4[i] & 0x10 && (cadence & 0xff) === 0) {
|
|
607
|
+
arr6[i] = (arr6[i] + round7(muls(sb(arr6[i]), -38))) & 0xff;
|
|
608
|
+
arr7[i] = (arr7[i] + round7(muls(sb(arr7[i]), -38))) & 0xff;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (i === 0)
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
// 0x23c0: the phrase's first stressed syllable climbs the whole distance
|
|
615
|
+
// rather than a fraction of it, so it is the one that starts down at 110 and
|
|
616
|
+
// reaches the peak the declination picked.
|
|
617
|
+
arr6[counters.first] = (arr1[counters.first] - 0x6e) & 0xff;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* hunk+0x23ce. Reconcile each stressed syllable with the next one, and give
|
|
621
|
+
* the phrase its final punctuation.
|
|
622
|
+
*
|
|
623
|
+
* Two halves. The first walks the primary stresses backwards in pairs and
|
|
624
|
+
* adjusts both by how far apart they are:
|
|
625
|
+
*
|
|
626
|
+
* - **Back to back** (0x23f8) — both contours shrink to 77/128, the earlier
|
|
627
|
+
* peak drops 26/128 of its height and the later one rises by the same, and
|
|
628
|
+
* then whatever gap is left between where the earlier syllable ends and the
|
|
629
|
+
* later one starts is closed outright. Two stresses in a row have no room
|
|
630
|
+
* for two full contours, so they are flattened and butted together.
|
|
631
|
+
* - **Anything further apart** (0x2496) — both contours *grow*, by 19, 32 or
|
|
632
|
+
* 38 parts in 128 as the gap is one, two or more syllables, and the peaks
|
|
633
|
+
* move apart rather than together. With room between them each stress gets
|
|
634
|
+
* its own excursion, and the more room the bigger.
|
|
635
|
+
*
|
|
636
|
+
* The second half (0x2574) is the punctuation, on any stressed syllable a
|
|
637
|
+
* pause follows:
|
|
638
|
+
*
|
|
639
|
+
* - **A full stop** ends the syllable a flat 75 below its peak.
|
|
640
|
+
* - **A question** shortens the climb by 102/128 of the drop and then rebuilds
|
|
641
|
+
* the drop from the *highest* peak anywhere earlier in the phrase, times
|
|
642
|
+
* 154/128. That is larger than this syllable's own peak, so the drop comes
|
|
643
|
+
* out negative and the syllable ends *above* where it peaked.
|
|
644
|
+
*
|
|
645
|
+
* So 33.2 does speak a question differently — here, in arithmetic, rather than
|
|
646
|
+
* through the rise flag in {@link markCadence} that no input can select.
|
|
647
|
+
*/
|
|
648
|
+
export function linkSyllables(state) {
|
|
649
|
+
const { counters } = state;
|
|
650
|
+
const arr1 = state.arr[PEAK].subarray(state.arrAt);
|
|
651
|
+
const arr3 = state.arr[CADENCE].subarray(state.arrAt);
|
|
652
|
+
const arr4 = state.arr[DESCRIPTOR].subarray(state.arrAt);
|
|
653
|
+
const arr5 = state.arr[VOICING].subarray(state.arrAt);
|
|
654
|
+
const arr6 = state.arr[CLIMB].subarray(state.arrAt);
|
|
655
|
+
const arr7 = state.arr[DROP].subarray(state.arrAt);
|
|
656
|
+
// ------------------------------------------------------------------ 0x23e0
|
|
657
|
+
/** `D1`: the stressed syllable above this one, which only it moves on. */
|
|
658
|
+
let next = counters.last;
|
|
659
|
+
for (let i = counters.last - 1; i >= 0; i--) {
|
|
660
|
+
if (!(arr4[i] & SYLLABLE.PRIMARY))
|
|
661
|
+
continue;
|
|
662
|
+
// 0x23ea: `next - i - 2`, so zero means exactly one syllable between them
|
|
663
|
+
// and there is nothing to reconcile.
|
|
664
|
+
const gap = sw(next - i - 2);
|
|
665
|
+
if (gap < 0) {
|
|
666
|
+
arr6[i] = (arr6[i] + round7(muls(sb(arr6[i]), -0x33))) & 0xff;
|
|
667
|
+
arr6[next] = (arr6[next] + round7(muls(sb(arr6[next]), -0x33))) & 0xff;
|
|
668
|
+
arr1[i] = (arr1[i] + round7(muls(arr1[i] - 0x6e, -0x1a))) & 0xff;
|
|
669
|
+
arr1[next] = (arr1[next] + round7(muls(arr1[next] - 0x6e, 0x1a))) & 0xff;
|
|
670
|
+
// 0x2474: what is left between this syllable's low and the next one's,
|
|
671
|
+
// all in bytes, and `bpl` on the byte.
|
|
672
|
+
const d = (arr1[i] - arr7[i] - arr1[next] + arr6[next]) & 0xff;
|
|
673
|
+
if (sb(d) < 0)
|
|
674
|
+
arr6[next] = (arr6[next] - d) & 0xff;
|
|
675
|
+
else
|
|
676
|
+
arr7[i] = (arr7[i] + d) & 0xff;
|
|
677
|
+
}
|
|
678
|
+
else if (gap > 0) {
|
|
679
|
+
// 0x2498, 0x24e6 and 0x2512: three ladders of the same shape, each
|
|
680
|
+
// reading the gap back off the stack.
|
|
681
|
+
const widen = gap === 1 ? 0x13 : gap === 2 ? 0x20 : 0x26;
|
|
682
|
+
arr6[i] = (arr6[i] + round7(muls(sb(arr6[i]), widen))) & 0xff;
|
|
683
|
+
arr6[next] = (arr6[next] + round7(muls(sb(arr6[next]), widen))) & 0xff;
|
|
684
|
+
arr1[next] = (arr1[next] + round7(muls(arr1[next] - 0x6e, gap === 1 ? -0x13 : -0x20))) & 0xff;
|
|
685
|
+
arr1[i] = (arr1[i] + round7(muls(arr1[i] - 0x6e, gap === 1 ? 0x0d : 0x13))) & 0xff;
|
|
686
|
+
// 0x2542: bit 4 again, and dead for the same reason. When it is set a
|
|
687
|
+
// cadence nibble of zero, or a negative one, skips what follows.
|
|
688
|
+
const marked = arr4[next] & 0x10;
|
|
689
|
+
const nibble = arr3[next] & 0x0f;
|
|
690
|
+
if (!(marked && (nibble === 0 || nibble & 0x08)) && gap >= 2) {
|
|
691
|
+
// 0x2562: far enough apart, and the later swing is set outright.
|
|
692
|
+
arr6[next] = (arr1[next] - 0x69) & 0xff;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
next = i;
|
|
696
|
+
}
|
|
697
|
+
// ------------------------------------------------------------------ 0x2574
|
|
698
|
+
for (let i = counters.last;; i--) {
|
|
699
|
+
const descriptor = arr4[i];
|
|
700
|
+
if (descriptor & SYLLABLE.PRIMARY && descriptor & SYLLABLE.PAUSE) {
|
|
701
|
+
const punctuation = arr5[i] & 0x0c;
|
|
702
|
+
if (punctuation === 8) {
|
|
703
|
+
// 0x25ac: a question.
|
|
704
|
+
let d1 = round7(muls(sb(arr7[i]), 0x66));
|
|
705
|
+
arr6[i] = (arr6[i] + d1) & 0xff;
|
|
706
|
+
// 0x25ca: the highest peak from here back to the start of the
|
|
707
|
+
// phrase — `cmp.w` with `bgt`, so this keeps the larger.
|
|
708
|
+
//
|
|
709
|
+
// `D1` is not cleared first: only its low byte is replaced each time
|
|
710
|
+
// round, and its high byte is left over from the rounding above. When
|
|
711
|
+
// that rounding came out negative the high byte is 0xff, every
|
|
712
|
+
// comparison is against a negative word, and the highest stays this
|
|
713
|
+
// syllable's own peak however low it is.
|
|
714
|
+
let d7 = arr1[i];
|
|
715
|
+
for (let k = i - 1; k >= 0; k--) {
|
|
716
|
+
d1 = (d1 & 0xff00) | arr1[k];
|
|
717
|
+
if (sw(d7) <= sw(d1))
|
|
718
|
+
d7 = (d7 & 0xff00) | (d1 & 0xff);
|
|
719
|
+
}
|
|
720
|
+
// 0x25e2: 154/128 of it, which is more than this syllable's own
|
|
721
|
+
// peak — so the drop comes out negative and the syllable rises.
|
|
722
|
+
arr7[i] = (arr1[i] - ((((d7 & 0xffff) * 0x9a) & 0xffff) >> 7)) & 0xff;
|
|
723
|
+
}
|
|
724
|
+
else if (punctuation === 4) {
|
|
725
|
+
// 0x259c: a full stop ends a flat 75 below the peak.
|
|
726
|
+
arr7[i] = (arr1[i] - 0x4b) & 0xff;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (i === 0)
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* hunk+0x25f8. Deepen the drop at a phrase boundary.
|
|
735
|
+
*
|
|
736
|
+
* The high nibble of the cadence byte is what {@link markCadence} and
|
|
737
|
+
* {@link markBoundaries} put there: `0xb0` on the phrase's last syllable and
|
|
738
|
+
* `0x90` on the one before a dash. Either adds 38/128 to that syllable's drop,
|
|
739
|
+
* so the pitch falls half again as far at the end of a phrase as it does
|
|
740
|
+
* inside one. That, and not the cadence flag, is what a comma sounds like.
|
|
741
|
+
*
|
|
742
|
+
* The other arm takes 102/128 *off* the drop instead, and is unreachable: it
|
|
743
|
+
* wants the high nibble non-zero with bit 7 clear, and the only two values
|
|
744
|
+
* that can put anything in that nibble both set bit 7. Its two possible
|
|
745
|
+
* sources — the `2` and the `0x0e` of {@link markBoundaries} — need flag bits
|
|
746
|
+
* nothing in 33.2 sets.
|
|
747
|
+
*/
|
|
748
|
+
export function boundaryFall(state) {
|
|
749
|
+
const arr3 = state.arr[CADENCE].subarray(state.arrAt);
|
|
750
|
+
const arr7 = state.arr[DROP].subarray(state.arrAt);
|
|
751
|
+
// `D4`, which hunk+0x2160 loaded from A5+0x8c at the top and none of the
|
|
752
|
+
// seven reloads — so it is scanPhrase's count, not markBoundaries'.
|
|
753
|
+
for (let i = state.counters.syllables - 1; i >= 0; i--) {
|
|
754
|
+
const mark = arr3[i] & 0xf0;
|
|
755
|
+
if (mark === 0)
|
|
756
|
+
continue;
|
|
757
|
+
const by = mark & 0x80 ? 0x26 : -0x66;
|
|
758
|
+
arr7[i] = (arr7[i] + round7(muls(sb(arr7[i]), by))) & 0xff;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* The step table at `hunk+0x29ca`, in 128ths, and the three windows into it.
|
|
763
|
+
*
|
|
764
|
+
* A run of unstressed syllables between two stresses glides from where the
|
|
765
|
+
* first one ended down towards where the second one starts, and this is how
|
|
766
|
+
* much of the step each syllable of the run takes. The factors *compound* —
|
|
767
|
+
* each is applied to what the last one left — so a long run's glide is steep
|
|
768
|
+
* at first and then flattens out.
|
|
769
|
+
*
|
|
770
|
+
* Whatever the run's length, only the first three syllables of it glide. The
|
|
771
|
+
* device picks a shorter window for a shorter run rather than stretching one
|
|
772
|
+
* window across it, so a single unstressed syllable takes the whole step in
|
|
773
|
+
* one go and everything past the third sits flat.
|
|
774
|
+
*/
|
|
775
|
+
const GLIDE = [0x3a, 0x64, 0x49, 0x4d, 0x56, 0x80];
|
|
776
|
+
const GLIDE_WINDOW = [[5, 6], [3, 5], [0, 3]];
|
|
777
|
+
/**
|
|
778
|
+
* hunk+0x29a2. Walk a run of syllables, each starting where the last one
|
|
779
|
+
* ended and dropping a little further.
|
|
780
|
+
*
|
|
781
|
+
* `arr0` gets the previous syllable's end, so the pitch is continuous across
|
|
782
|
+
* the run; `arr2` gets that minus the step; and `arr1` sits 5 above `arr2`, so
|
|
783
|
+
* every one of these syllables has the same shallow contour rather than a flat
|
|
784
|
+
* line. Returns the index it stopped on.
|
|
785
|
+
*/
|
|
786
|
+
function glide(arr0, arr1, arr2, at, step, factors) {
|
|
787
|
+
for (const factor of factors) {
|
|
788
|
+
arr0[at + 1] = arr2[at];
|
|
789
|
+
// 0x29aa: `mulu.w` then `lsr.w #7`, so the step is scaled before it is
|
|
790
|
+
// used and the next syllable inherits the smaller one.
|
|
791
|
+
step = (((step & 0xffff) * factor) & 0xffff) >>> 7;
|
|
792
|
+
at++;
|
|
793
|
+
arr2[at] = (arr0[at] - step) & 0xff;
|
|
794
|
+
arr1[at] = (arr2[at] + 5) & 0xff;
|
|
795
|
+
}
|
|
796
|
+
return at;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* hunk+0x2642. Turn the peaks and their two distances into three real pitches
|
|
800
|
+
* per syllable, and fill in every syllable that has not got one.
|
|
801
|
+
*
|
|
802
|
+
* Up to here only the primary stresses have been touched, and each is held as
|
|
803
|
+
* a peak with a climb and a drop hanging off it. This is where that becomes
|
|
804
|
+
* `arr0`, `arr1`, `arr2` — an onset, a peak and an end — for *every* syllable
|
|
805
|
+
* of the phrase, which is what `hunk+0x1a8e` needs.
|
|
806
|
+
*
|
|
807
|
+
* Four passes:
|
|
808
|
+
*
|
|
809
|
+
* 1. **0x265e** — subtract: onset is peak minus climb, end is peak minus drop.
|
|
810
|
+
* 2. **0x2680** — take the stresses in pairs and fill the gap between them.
|
|
811
|
+
* If the earlier syllable ends below where the later one starts, the two
|
|
812
|
+
* are moved half the distance towards each other first, so the line never
|
|
813
|
+
* has to climb backwards. Then the run between them glides down through
|
|
814
|
+
* {@link GLIDE}.
|
|
815
|
+
* 3. **0x277e** — everything *before* the phrase's first stress sits flat at
|
|
816
|
+
* 110, with its peak lifted by twice its own stress level. An unstressed
|
|
817
|
+
* lead-in is spoken at the bottom of the range.
|
|
818
|
+
* 4. **0x27a8** — everything *after* the last stress. Here the step is not
|
|
819
|
+
* from a table: it is the whole remaining distance down to 110, divided
|
|
820
|
+
* evenly by however many syllables are left, so the tail always lands on
|
|
821
|
+
* the floor whatever its length. A full stop aims 35 lower still and ends
|
|
822
|
+
* on 75; a comma or nothing ends on 110; a question does something else
|
|
823
|
+
* entirely and ends on 154/128 of the phrase's highest peak.
|
|
824
|
+
*/
|
|
825
|
+
export function fillContours(state) {
|
|
826
|
+
const { counters } = state;
|
|
827
|
+
const arr0 = state.arr[ONSET].subarray(state.arrAt);
|
|
828
|
+
const arr1 = state.arr[PEAK].subarray(state.arrAt);
|
|
829
|
+
const arr2 = state.arr[END].subarray(state.arrAt);
|
|
830
|
+
const arr3 = state.arr[CADENCE].subarray(state.arrAt);
|
|
831
|
+
const arr4 = state.arr[DESCRIPTOR].subarray(state.arrAt);
|
|
832
|
+
const arr5 = state.arr[VOICING].subarray(state.arrAt);
|
|
833
|
+
const arr6 = state.arr[CLIMB].subarray(state.arrAt);
|
|
834
|
+
const arr7 = state.arr[DROP].subarray(state.arrAt);
|
|
835
|
+
// ------------------------------------------------------------------ 0x265e
|
|
836
|
+
for (let i = counters.last;; i--) {
|
|
837
|
+
if (arr4[i] & SYLLABLE.PRIMARY) {
|
|
838
|
+
arr0[i] = (arr1[i] - arr6[i]) & 0xff;
|
|
839
|
+
arr2[i] = (arr1[i] - arr7[i]) & 0xff;
|
|
840
|
+
}
|
|
841
|
+
if (i === 0)
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
// ------------------------------------------------------------------ 0x2680
|
|
845
|
+
let scan = counters.first;
|
|
846
|
+
let remaining = counters.stresses;
|
|
847
|
+
for (;;) {
|
|
848
|
+
let at = scan;
|
|
849
|
+
let step = 0;
|
|
850
|
+
// 0x268a: one gap fewer than there are stresses.
|
|
851
|
+
remaining = sw(remaining - 1);
|
|
852
|
+
if (remaining <= 0)
|
|
853
|
+
break;
|
|
854
|
+
// 0x2694: forward to the next primary stress.
|
|
855
|
+
do {
|
|
856
|
+
scan = (scan + 1) & 0xffff;
|
|
857
|
+
} while (!(arr4[scan] & SYLLABLE.PRIMARY));
|
|
858
|
+
// 0x269e: syllables strictly between the two. None, and there is nothing
|
|
859
|
+
// to fill in.
|
|
860
|
+
const between = sw(scan - at - 1);
|
|
861
|
+
if (between === 0)
|
|
862
|
+
continue;
|
|
863
|
+
// 0x26a6: does the earlier one end below where the later one starts?
|
|
864
|
+
step = (arr2[at] - arr0[scan]) & 0xff;
|
|
865
|
+
if (sb(step) < 0) {
|
|
866
|
+
// 0x26b0: move both half way towards the other, adjusting the climb and
|
|
867
|
+
// the drop to match so the peaks stay where they are.
|
|
868
|
+
const half = ((-step) & 0xff) >>> 1;
|
|
869
|
+
arr0[scan] = (arr0[scan] - half) & 0xff;
|
|
870
|
+
arr6[scan] = (arr6[scan] + half) & 0xff;
|
|
871
|
+
arr2[at] = (arr2[at] + half) & 0xff;
|
|
872
|
+
arr7[at] = (arr7[at] - half) & 0xff;
|
|
873
|
+
step = 0;
|
|
874
|
+
}
|
|
875
|
+
// 0x26ca: a cadence below 8 on a syllable carrying the moved marker
|
|
876
|
+
// spreads the step evenly instead of using the table. Dead, as everywhere
|
|
877
|
+
// else that marker is tested — nothing sets flag bit 4.
|
|
878
|
+
if (sb(arr3[at] & 0x0f) < 8 && arr4[at] & 0x10) {
|
|
879
|
+
// 0x26e4
|
|
880
|
+
step = Math.floor(step / between);
|
|
881
|
+
for (let k = 0; k < between; k++) {
|
|
882
|
+
arr0[at + 1] = arr2[at];
|
|
883
|
+
at++;
|
|
884
|
+
arr2[at] = (arr0[at] - step) & 0xff;
|
|
885
|
+
arr1[at] = (arr2[at] + 5) & 0xff;
|
|
886
|
+
}
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
// 0x270c: one of the three windows, by how long the run is.
|
|
890
|
+
if (between >= 3) {
|
|
891
|
+
// 0x2718: a long run does not glide towards the next stress at all —
|
|
892
|
+
// it glides towards 105, and only for its first three syllables.
|
|
893
|
+
step = (arr2[at] - 0x69) & 0xff;
|
|
894
|
+
}
|
|
895
|
+
const [from, to] = GLIDE_WINDOW[Math.min(between, 3) - 1];
|
|
896
|
+
at = glide(arr0, arr1, arr2, at, step, GLIDE.slice(from, to));
|
|
897
|
+
// 0x273a: the rest of a long run holds the pitch the glide left off on,
|
|
898
|
+
// its peak lifted by twice its own stress level.
|
|
899
|
+
for (let k = 0; k < between - 3; k++) {
|
|
900
|
+
const held = arr2[at];
|
|
901
|
+
at++;
|
|
902
|
+
arr0[at] = held;
|
|
903
|
+
arr2[at] = held;
|
|
904
|
+
arr1[at] = (held + ((arr4[at] & 0x0f) << 1)) & 0xff;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// ------------------------------------------------------------------ 0x277e
|
|
908
|
+
for (let i = counters.first - 1; i >= 0; i--) {
|
|
909
|
+
arr1[i] = (0x6e + ((arr4[i] & 0x0f) << 1)) & 0xff;
|
|
910
|
+
arr0[i] = 0x6e;
|
|
911
|
+
arr2[i] = 0x6e;
|
|
912
|
+
}
|
|
913
|
+
// ------------------------------------------------------------------ 0x27a8
|
|
914
|
+
const last = counters.syllables - 1;
|
|
915
|
+
// 0x27b2: a phrase ending on a stress has no tail, and is already done.
|
|
916
|
+
if (arr4[last] & SYLLABLE.PRIMARY)
|
|
917
|
+
return;
|
|
918
|
+
const punctuation = arr5[last] & 0x0c;
|
|
919
|
+
// 0x27c8: back to the last stressed syllable, or to 0 if there is none.
|
|
920
|
+
let at = 0;
|
|
921
|
+
for (let i = last - 1; i >= 0; i--) {
|
|
922
|
+
if (arr4[i] & SYLLABLE.PRIMARY) {
|
|
923
|
+
at = i;
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const count = sw(last - at);
|
|
928
|
+
if (count !== 0) {
|
|
929
|
+
// 0x27da: `moveq` then `subi.w`, so a syllable already below 110 wraps to
|
|
930
|
+
// a large positive and the tail climbs instead of falling.
|
|
931
|
+
let step = (arr2[at] - 0x6e) & 0xffff;
|
|
932
|
+
// 0x27e4: a full stop aims 35 further down than the others.
|
|
933
|
+
if (punctuation === 4)
|
|
934
|
+
step = (step + 35) & 0xffff;
|
|
935
|
+
step = Math.floor(step / count);
|
|
936
|
+
for (let k = 0; k < count; k++) {
|
|
937
|
+
arr0[at + 1] = arr2[at];
|
|
938
|
+
at++;
|
|
939
|
+
arr2[at] = (arr0[at] - step) & 0xff;
|
|
940
|
+
arr1[at] = (arr2[at] + 5) & 0xff;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// ------------------------------------------------------------------ 0x2812
|
|
944
|
+
if (punctuation === 8) {
|
|
945
|
+
// A question. The last syllable ends on 154/128 of the phrase's highest
|
|
946
|
+
// peak — above everything in it, so the utterance finishes going up.
|
|
947
|
+
let highest = 0;
|
|
948
|
+
for (let i = counters.syllables - 1; i >= 0; i--) {
|
|
949
|
+
if (sw(highest) <= arr1[i])
|
|
950
|
+
highest = arr1[i];
|
|
951
|
+
}
|
|
952
|
+
highest = (((highest & 0xffff) * 0x9a) & 0xffff) >>> 7;
|
|
953
|
+
arr2[last] = highest & 0xff;
|
|
954
|
+
arr1[last] = (highest + 5) & 0xff;
|
|
955
|
+
// 0x2852: and no drop, so nothing later pulls it back down.
|
|
956
|
+
arr7[last] = 0;
|
|
957
|
+
// 0x2856: with more than one syllable it starts where the one before it
|
|
958
|
+
// ended, so the rise is the whole of this syllable rather than a jump.
|
|
959
|
+
if (sw(counters.syllables) > 1)
|
|
960
|
+
arr0[last] = arr2[last - 1];
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
// 0x2818: a full stop ends on 75, anything else on 110.
|
|
964
|
+
arr2[at] = 0x4b;
|
|
965
|
+
if (punctuation === 4)
|
|
966
|
+
return;
|
|
967
|
+
arr2[at] = 0x6e;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* hunk+0x2864. Squeeze each stressed syllable's contour by the consonants
|
|
971
|
+
* around it — the last thing the pitch loop does.
|
|
972
|
+
*
|
|
973
|
+
* This is **consonantal F0 perturbation**, and it is the most linguistically
|
|
974
|
+
* literate thing in the device after {@link intrinsicPitch}. A consonant
|
|
975
|
+
* disturbs the pitch of the vowel next to it, and how much depends on whether
|
|
976
|
+
* it is voiced: a voiceless one perturbs far more, because the larynx has to
|
|
977
|
+
* stop and restart. Both ends of the syllable are treated separately.
|
|
978
|
+
*
|
|
979
|
+
* At the **onset** (0x28a0), only if the syllable begins with a consonant: the
|
|
980
|
+
* start of the contour is moved up towards the peak by 26/128 of the climb
|
|
981
|
+
* after a voiced consonant and 102/128 after a voiceless one — so a syllable
|
|
982
|
+
* beginning `p` or `t` has almost no room to rise and one beginning `m` or `b`
|
|
983
|
+
* keeps nearly all of it. A voiceless onset also lifts the whole contour by
|
|
984
|
+
* 26/128 of its height first, which is the well-attested raising of F0 after a
|
|
985
|
+
* voiceless stop.
|
|
986
|
+
*
|
|
987
|
+
* At the **end** (0x291a), by what the syllable runs into. The device walks
|
|
988
|
+
* forward past the stress digit to the first phoneme that settles it:
|
|
989
|
+
*
|
|
990
|
+
* | | |
|
|
991
|
+
* |---|---|
|
|
992
|
+
* | 26/128 | a vowel or the glottal stop — the fall carries on into it |
|
|
993
|
+
* | 86/128 | a voiceless phoneme first — most of the fall is eaten |
|
|
994
|
+
* | 64/128 | a pause follows, or the next syllable is stressed too |
|
|
995
|
+
*
|
|
996
|
+
* Voiced consonants in between are stepped over, since they do not interrupt
|
|
997
|
+
* the pitch. And a phrase whose last syllable is stressed is left alone, there
|
|
998
|
+
* being nothing after it to run into.
|
|
999
|
+
*
|
|
1000
|
+
* Each adjustment moves the endpoint and shrinks the distance by the same
|
|
1001
|
+
* amount, so the peak never moves and the three stay consistent.
|
|
1002
|
+
*/
|
|
1003
|
+
export function coarticulatePitch(state, attrs) {
|
|
1004
|
+
const { counters, phonemes, stress } = state;
|
|
1005
|
+
const arr0 = state.arr[ONSET].subarray(state.arrAt);
|
|
1006
|
+
const arr1 = state.arr[PEAK].subarray(state.arrAt);
|
|
1007
|
+
const arr2 = state.arr[END].subarray(state.arrAt);
|
|
1008
|
+
const arr4 = state.arr[DESCRIPTOR].subarray(state.arrAt);
|
|
1009
|
+
const arr6 = state.arr[CLIMB].subarray(state.arrAt);
|
|
1010
|
+
const arr7 = state.arr[DROP].subarray(state.arrAt);
|
|
1011
|
+
/** Bit 1 of the attributes, and bit 9 — a consonant, and voiced. */
|
|
1012
|
+
const CONSONANT = 1 << 1;
|
|
1013
|
+
const VOICED = 1 << 9;
|
|
1014
|
+
const VOWEL = 1 << 0;
|
|
1015
|
+
/** Phoneme 47, the glottal stop. */
|
|
1016
|
+
const GLOTTAL = 0x2f;
|
|
1017
|
+
// The three cursors, which walk backwards together one syllable at a time.
|
|
1018
|
+
let p = state.atPhoneme - 1;
|
|
1019
|
+
let s = state.atStress - 1;
|
|
1020
|
+
for (let i = counters.syllables - 1; i >= 0; i--) {
|
|
1021
|
+
// 0x287e: back to the phoneme the spreader marked as this syllable's
|
|
1022
|
+
// start. `tst.b -(A2)` and `bpl`, so it is looking for bit 7.
|
|
1023
|
+
do {
|
|
1024
|
+
p--;
|
|
1025
|
+
s--;
|
|
1026
|
+
} while (!(stress[s] & STRESS.MARK));
|
|
1027
|
+
if (!(arr4[i] & SYLLABLE.PRIMARY))
|
|
1028
|
+
continue;
|
|
1029
|
+
// ---------------------------------------------------------------- 0x28a0
|
|
1030
|
+
const onset = attrs[phonemes[p]] ?? 0;
|
|
1031
|
+
if (onset & CONSONANT) {
|
|
1032
|
+
let by = 0x1a;
|
|
1033
|
+
if (!(onset & VOICED)) {
|
|
1034
|
+
// 0x28b8: a voiceless consonant lifts the whole syllable first, peak,
|
|
1035
|
+
// climb and drop alike, so its shape is kept as it rises.
|
|
1036
|
+
const lift = round7(muls(arr1[i] - 0x6e, 0x1a));
|
|
1037
|
+
arr1[i] = (arr1[i] + lift) & 0xff;
|
|
1038
|
+
arr7[i] = (arr7[i] + lift) & 0xff;
|
|
1039
|
+
arr6[i] = (arr6[i] + lift) & 0xff;
|
|
1040
|
+
by = 0x66;
|
|
1041
|
+
}
|
|
1042
|
+
const d = round7(muls(sb(arr6[i]), by));
|
|
1043
|
+
arr0[i] = (arr0[i] + d) & 0xff;
|
|
1044
|
+
arr6[i] = (arr6[i] - d) & 0xff;
|
|
1045
|
+
}
|
|
1046
|
+
// ---------------------------------------------------------------- 0x291a
|
|
1047
|
+
let by;
|
|
1048
|
+
if (arr4[i] & SYLLABLE.PAUSE || arr4[i + 1] & SYLLABLE.PRIMARY) {
|
|
1049
|
+
// 0x295c: and the phrase's last syllable has nothing to run into.
|
|
1050
|
+
if (i === counters.syllables - 1)
|
|
1051
|
+
continue;
|
|
1052
|
+
by = 0x40;
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
// 0x292a: forward past the syllable's own stress digit first.
|
|
1056
|
+
let k = 0;
|
|
1057
|
+
while ((stress[s + k++] & 0x0f) === 0)
|
|
1058
|
+
;
|
|
1059
|
+
// 0x293c: then on to the first phoneme that settles it, stepping over
|
|
1060
|
+
// voiced consonants on the way.
|
|
1061
|
+
for (;;) {
|
|
1062
|
+
const next = phonemes[p + k];
|
|
1063
|
+
if (next === GLOTTAL) {
|
|
1064
|
+
by = 0x1a;
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
const a = attrs[next] ?? 0;
|
|
1068
|
+
if (a & VOWEL) {
|
|
1069
|
+
by = 0x1a;
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
if (!(a & VOICED)) {
|
|
1073
|
+
by = 0x56;
|
|
1074
|
+
break;
|
|
1075
|
+
}
|
|
1076
|
+
k++;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
// 0x2974
|
|
1080
|
+
const d = round7(muls(sb(arr7[i]), by));
|
|
1081
|
+
arr2[i] = (arr2[i] + d) & 0xff;
|
|
1082
|
+
arr7[i] = (arr7[i] - d) & 0xff;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* hunk+0x2160. One phrase's worth of pitch, and then move the cursors past it.
|
|
1087
|
+
*
|
|
1088
|
+
* The driver at `hunk+0x832` alternates {@link nextPhrase} with this until
|
|
1089
|
+
* there is no phrase left, so between them they are the whole of the tune.
|
|
1090
|
+
*
|
|
1091
|
+
* Two things are skipped rather than guarded inside the routines themselves.
|
|
1092
|
+
* `mode` — the monotone robot voice — skips all seven, since
|
|
1093
|
+
* {@link assignPitch} is going to write one flat period over everything
|
|
1094
|
+
* anyway; and a phrase with no primary stress in it skips the first four,
|
|
1095
|
+
* which are the ones that need somewhere to hang a contour, leaving the last
|
|
1096
|
+
* three to fill it in flat.
|
|
1097
|
+
*
|
|
1098
|
+
* The seven pass registers between them and the device never reloads what one
|
|
1099
|
+
* of them clobbers, so they have to run in this order. `phrasePitch` is the
|
|
1100
|
+
* only one whose result is threaded by hand, in `D0`.
|
|
1101
|
+
*
|
|
1102
|
+
* Ends by adding the syllable count to all eight array cursors at once, which
|
|
1103
|
+
* is what makes the next phrase write where this one stopped.
|
|
1104
|
+
*/
|
|
1105
|
+
export function pitchLoopBody(state, attrs, mode) {
|
|
1106
|
+
const { counters } = state;
|
|
1107
|
+
// 0x2184
|
|
1108
|
+
if (mode === 0) {
|
|
1109
|
+
// 0x218a: nothing to hang a contour on.
|
|
1110
|
+
if (counters.stresses !== 0) {
|
|
1111
|
+
syllablePitch(state, phrasePitch(state));
|
|
1112
|
+
syllableRange(state);
|
|
1113
|
+
linkSyllables(state);
|
|
1114
|
+
}
|
|
1115
|
+
boundaryFall(state);
|
|
1116
|
+
fillContours(state);
|
|
1117
|
+
coarticulatePitch(state, attrs);
|
|
1118
|
+
}
|
|
1119
|
+
// 0x21aa: `add.l D4,(A0)+` eight times.
|
|
1120
|
+
state.arrAt += counters.syllables;
|
|
1121
|
+
}
|