narrator-ts 0.0.0 → 0.1.1

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -3
  3. package/dist/aros-resource/index.d.ts +39 -0
  4. package/dist/aros-resource/index.js +282 -0
  5. package/dist/narrator/contour.d.ts +90 -0
  6. package/dist/narrator/contour.js +235 -0
  7. package/dist/narrator/duration.d.ts +39 -0
  8. package/dist/narrator/duration.js +259 -0
  9. package/dist/narrator/frames.d.ts +140 -0
  10. package/dist/narrator/frames.js +428 -0
  11. package/dist/narrator/index.d.ts +15 -0
  12. package/dist/narrator/index.js +13 -0
  13. package/dist/narrator/interpolate.d.ts +183 -0
  14. package/dist/narrator/interpolate.js +467 -0
  15. package/dist/narrator/onset.d.ts +25 -0
  16. package/dist/narrator/onset.js +73 -0
  17. package/dist/narrator/parse.d.ts +67 -0
  18. package/dist/narrator/parse.js +184 -0
  19. package/dist/narrator/prosody.d.ts +355 -0
  20. package/dist/narrator/prosody.js +1121 -0
  21. package/dist/narrator/render.d.ts +64 -0
  22. package/dist/narrator/render.js +273 -0
  23. package/dist/narrator/rewrite.d.ts +55 -0
  24. package/dist/narrator/rewrite.js +155 -0
  25. package/dist/narrator/speak.d.ts +120 -0
  26. package/dist/narrator/speak.js +206 -0
  27. package/dist/narrator/stress.d.ts +36 -0
  28. package/dist/narrator/stress.js +147 -0
  29. package/dist/narrator/voice.d.ts +66 -0
  30. package/dist/narrator/voice.js +78 -0
  31. package/dist/translator/engines.d.ts +37 -0
  32. package/dist/translator/engines.js +26 -0
  33. package/dist/translator/index.d.ts +3 -0
  34. package/dist/translator/index.js +2 -0
  35. package/dist/translator/translate.d.ts +10 -0
  36. package/dist/translator/translate.js +363 -0
  37. package/dist/translator/types.d.ts +70 -0
  38. package/dist/translator/types.js +31 -0
  39. package/package.json +66 -4
  40. package/reference/README.md +168 -0
  41. package/reference/formants.json +18 -0
  42. package/reference/nrl-7948.json +434 -0
  43. package/reference/nrl-table.json +1 -0
  44. package/reference/voice-free.json +11775 -0
  45. package/reference/wordlist.txt +10 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * narrator.device's phoneme parser — the front of the front half.
3
+ *
4
+ * Ported from hunk+0xf68 of build 33.2. It turns the input string into three
5
+ * parallel byte arrays, and every later stage works from those rather than
6
+ * from the text, so getting this exactly right is a precondition for getting
7
+ * durations or frames right. See research/02-narrator.md.
8
+ *
9
+ * The arrays start at index 4, not 0. Slots 0-3 are a lead-in the later
10
+ * stages look backwards into, with slot 2 seeded to QX; a port that starts at
11
+ * zero will produce plausible phonemes and wrong everything else.
12
+ */
13
+ /** Attribute bits this stage tests. Others are used further down. */
14
+ export const ATTR = {
15
+ /** Bit 0 (0x100a): a stress digit may follow this phoneme. */
16
+ STRESSABLE: 1 << 0,
17
+ /** Bit 25 (0x10de): this phoneme already ends an utterance. */
18
+ TERMINAL: 1 << 25,
19
+ /** Bit 26 (0x103a): a pause — space, `.`, `?`, `,` or `-`. */
20
+ PAUSE: 1 << 26,
21
+ /** Bit 27 (0x1032): rejected wherever it appears. */
22
+ ILLEGAL: 1 << 27,
23
+ };
24
+ /** Phoneme indices the parser names directly. */
25
+ const P = { SPACE: 0, PERIOD: 1, QUERY: 2, DASH: 4, OPEN: 5, CLOSE: 6, QX: 0x15 };
26
+ /** Where the real phonemes start; 0-3 are the lead-in. */
27
+ export const LEAD_IN = 4;
28
+ /** The arrays are 0x200 bytes, which is also the phoneme limit. */
29
+ export const MAX_PHONEMES = 0x200;
30
+ /** Written into all three arrays after the last phoneme. */
31
+ export const TERMINATOR = 0xff;
32
+ /**
33
+ * Parse a phoneme string the way the device does.
34
+ *
35
+ * `input` is bytes rather than a string because the device works in Latin-1
36
+ * and scans two characters at a time, including the byte past the end.
37
+ */
38
+ export function parse(input, table, length = input.length) {
39
+ const phonemes = new Uint8Array(MAX_PHONEMES + 2);
40
+ const stress = new Uint8Array(MAX_PHONEMES + 2);
41
+ const flags = new Uint8Array(MAX_PHONEMES + 2);
42
+ // 0x100a and friends index the attribute table by phoneme. The digits sit
43
+ // past its end and are peeled off as stress before any lookup, so anything
44
+ // out of range here would be a bug rather than a value.
45
+ const attrOf = (p) => table.attrs[p] ?? 0;
46
+ // 0xf8a-0xf96: the lead-in. Only slot 2 is non-zero.
47
+ phonemes[2] = P.QX;
48
+ // 0xf68: an empty request produces nothing at all, not an empty utterance.
49
+ if (length <= 0)
50
+ return { phonemes, stress, flags, count: 0, consumed: 0 };
51
+ let at = 0; // A0, the read position
52
+ let n = LEAD_IN; // D3, the write position
53
+ stress[n] = 0; // 0xfa8, the loop's entry clear
54
+ flags[n] = 0;
55
+ const byte = (i) => (i < input.length ? input[i] : 0);
56
+ for (;;) {
57
+ const c0 = byte(at);
58
+ // 0xfb2/0xfba: NUL or '#' ends the input regardless of io_Length.
59
+ if (c0 === 0 || c0 === 0x23)
60
+ break;
61
+ // 0xfbe-0xfe2: match two characters, then fall back to one. The table is
62
+ // words, so a one-character name is that character followed by NUL — and
63
+ // the retry masks the low byte off rather than re-reading.
64
+ let word = (c0 << 8) | byte(at + 1);
65
+ let taken = 2;
66
+ let idx = table.names.findIndex((_, i) => wordAt(table, i) === word);
67
+ if (idx < 0) {
68
+ if (taken === 1)
69
+ return fail(at);
70
+ word &= 0xff00;
71
+ taken = 1;
72
+ idx = table.names.findIndex((_, i) => wordAt(table, i) === word);
73
+ if (idx < 0)
74
+ return fail(at);
75
+ }
76
+ // 0xfea-0x1022: a stress digit. It attaches to the phoneme *already*
77
+ // written, not to the next one.
78
+ if (word >= 0x3000 && word <= 0x3900) {
79
+ at += taken;
80
+ // 0x3000 is a bare '0', which is the default and simply disappears.
81
+ if (word !== 0x3000) {
82
+ if (!(attrOf(phonemes[n - 1]) & ATTR.STRESSABLE))
83
+ return fail(at - taken);
84
+ stress[n - 1] = 0x30 | ((word >> 8) & 0x0f);
85
+ }
86
+ if (!more())
87
+ break;
88
+ continue;
89
+ }
90
+ at += taken;
91
+ const attr = attrOf(idx);
92
+ // 0x1032, and note the position: A0 is advanced at 0x1026, *before* this
93
+ // test, so the offset reported points past the phoneme rather than at it.
94
+ // The digit and no-match rejections above report the character itself.
95
+ if (attr & ATTR.ILLEGAL)
96
+ return fail(at);
97
+ if (attr & ATTR.PAUSE) {
98
+ // 0x103a-0x1062: pauses collapse. A run of them keeps the *last*, so
99
+ // "a . b" and "a. b" agree — except that a plain space after any pause
100
+ // is dropped instead of replacing it.
101
+ if (attrOf(phonemes[n - 1]) & ATTR.PAUSE) {
102
+ if (n <= LEAD_IN || idx === P.SPACE) {
103
+ if (!more())
104
+ break;
105
+ continue;
106
+ }
107
+ // Overwrite the previous pause. Note its stress and flag bytes are
108
+ // *not* re-cleared — the loop only clears the slot after the one it
109
+ // just wrote — so they survive into the replacement.
110
+ n--;
111
+ }
112
+ }
113
+ else if (idx === P.OPEN) {
114
+ // 0x106a: emphasis brackets set a bit and store no phoneme. Neither
115
+ // path takes the io_Length check at the bottom of the loop.
116
+ flags[n] |= 0x20;
117
+ continue;
118
+ }
119
+ else if (idx === P.CLOSE) {
120
+ flags[n - 1] |= 0x10;
121
+ continue;
122
+ }
123
+ // 0x1084: store.
124
+ phonemes[n] = idx;
125
+ n++;
126
+ // 0x108a: '.' and '?' end the utterance even mid-string.
127
+ if (idx === P.PERIOD || idx === P.QUERY)
128
+ break;
129
+ if (n >= MAX_PHONEMES)
130
+ return fail(at);
131
+ if (!more())
132
+ break;
133
+ stress[n] = 0;
134
+ flags[n] = 0;
135
+ }
136
+ // 0x10cc. A trailing space is dropped, and then the result is *still*
137
+ // considered for a '-' — the device falls through from one to the other
138
+ // rather than choosing between them, so "AA4 " ends up a phoneme longer
139
+ // than it started rather than a phoneme shorter.
140
+ //
141
+ // Note there is no lower bound on the decrement. With nothing but a pause
142
+ // for input the write index walks back into the lead-in, and the length
143
+ // test at the end then rejects the whole utterance — which is why a lone
144
+ // "." produces silence rather than a pause.
145
+ let dash = true;
146
+ if (phonemes[n - 1] === P.SPACE)
147
+ n--;
148
+ else if (attrOf(phonemes[n - 1]) & ATTR.TERMINAL)
149
+ dash = false;
150
+ // 0x10e4: `ble` against 3, not against the lead-in.
151
+ if (dash && n > 3) {
152
+ phonemes[n] = P.DASH;
153
+ stress[n] = 0;
154
+ flags[n] = 0;
155
+ n++;
156
+ }
157
+ phonemes[n] = TERMINATOR;
158
+ stress[n] = TERMINATOR;
159
+ flags[n] = TERMINATOR;
160
+ n++;
161
+ // 0x110e: how far the scan got, which is what the driver advances by.
162
+ const consumed = at;
163
+ // 0x1118: four or fewer means the lead-in and nothing else.
164
+ if (n <= LEAD_IN)
165
+ return { phonemes, stress, flags, count: 0, consumed };
166
+ return { phonemes, stress, flags, count: n, consumed };
167
+ /** 0x10aa: stop once io_Length is consumed, whatever the buffer holds. */
168
+ function more() {
169
+ if (at >= length)
170
+ return false;
171
+ stress[n] = 0;
172
+ flags[n] = 0;
173
+ return true;
174
+ }
175
+ /** 0x10c2: the device reports a 1-based character offset, not a code. */
176
+ function fail(pos) {
177
+ return { phonemes, stress, flags, count: 0, consumed: at, error: pos + 1 };
178
+ }
179
+ }
180
+ /** The table as the scan sees it: two bytes, NUL-padded, big-endian. */
181
+ function wordAt(table, i) {
182
+ const s = table.names[i] ?? '';
183
+ return ((s.charCodeAt(0) || 0) << 8) | (s.charCodeAt(1) || 0);
184
+ }
@@ -0,0 +1,355 @@
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 type { Attrs } from './rewrite.js';
25
+ /** Values written into `arr4`, the per-syllable descriptor. */
26
+ export declare const SYLLABLE: {
27
+ /** Bits 0-3: the scaled stress digit. */
28
+ readonly LEVEL: 15;
29
+ /** 0x20: a primary stress. */
30
+ readonly PRIMARY: 32;
31
+ /** 0x40: the last syllable of the utterance. */
32
+ readonly LAST: 64;
33
+ /** 0x80: a pause follows this syllable. */
34
+ readonly PAUSE: 128;
35
+ };
36
+ /** `hunk+0x1f02` gives up at this many syllables — `arr4` is 0x80 bytes. */
37
+ export declare const MAX_SYLLABLES = 128;
38
+ /** The workspace scalars this stage keeps, by their `A5` offsets. */
39
+ export interface ProsodyCounters {
40
+ /** `A5+0x88`: which pass round the loop this is. */
41
+ pass: number;
42
+ /** `A5+0x8a`: primary stresses in this phrase. */
43
+ stresses: number;
44
+ /** `A5+0x8c`: syllables in this phrase. */
45
+ syllables: number;
46
+ /** `A5+0x8e`: the first primary stress, or the syllable count if none. */
47
+ first: number;
48
+ /** `A5+0x90`: boundaries seen. */
49
+ boundaries: number;
50
+ /** `A5+0x9e`: the last primary stress. */
51
+ last: number;
52
+ /** `A5+0xa0`: syllables across the whole utterance. */
53
+ total: number;
54
+ }
55
+ export interface ProsodyState {
56
+ phonemes: Uint8Array;
57
+ stress: Uint8Array;
58
+ flags: Uint8Array;
59
+ /** `A5+0x7c`, `+0x84` and `+0x80` — cursors, which each pass moves on. */
60
+ atPhoneme: number;
61
+ atStress: number;
62
+ atFlag: number;
63
+ /** The eight `0x80`-byte arrays at `A5+0x6e8`. */
64
+ arr: Uint8Array[];
65
+ /**
66
+ * `A5+0x5c`..`+0x78` hold a cursor into each of the eight, and they all
67
+ * advance together — `hunk+0x2160` moves them past each phrase once it has
68
+ * consumed it. So a routine here indexes from this rather than from zero,
69
+ * and phrase two of an utterance writes where phrase one left off.
70
+ */
71
+ arrAt: number;
72
+ counters: ProsodyCounters;
73
+ }
74
+ /**
75
+ * hunk+0x1f02. Walk the next phrase and write one descriptor per syllable.
76
+ *
77
+ * A syllable here is a phoneme the stress spreader marked. Its level comes
78
+ * from the stress digit the writer typed, scaled by 199/128 — so a `4` becomes
79
+ * 6 — and a further 2 if the phoneme is inside a spread. Anything above 4
80
+ * counts as a primary stress and is remembered as such.
81
+ *
82
+ * That scaling is the one place the digits 0-9 stop being a rank and become a
83
+ * number the synthesizer does arithmetic with.
84
+ *
85
+ * Returns the syllable count, which the four routines after this one read out
86
+ * of `D4` rather than recomputing, or `null` if the array overflowed.
87
+ */
88
+ export declare function scanPhrase(state: ProsodyState, attrs: Attrs): number | null;
89
+ /**
90
+ * hunk+0x1fd8. Walk the same phrase again and record where it breaks, then
91
+ * move the cursors past it.
92
+ *
93
+ * `arr3` gets `0x90` at a dash, and that is all it gets in this build.
94
+ *
95
+ * The other two markers, `2` and `0x0e`, come from bits 4 and 5 of the flag
96
+ * byte, and **nothing in 33.2 ever sets them**. The parser zeroes the array,
97
+ * the rewrite engine zeroes what it inserts, and the stress spreader writes
98
+ * only `0x80` and `0x40`; every flag byte reaching here across the whole
99
+ * corpus is one of 0, 0x40, 0x80 or 0xc0. So the marker-shuffling pass at the
100
+ * end of this routine — which walks to the nearest primary stress and moves a
101
+ * marker onto it, backwards for `0x0e` and forwards for `2` — can never run.
102
+ *
103
+ * It is ported anyway. It is real code with a clear meaning, it is what the
104
+ * routine says, and 37.7 is a rewrite that may well feed it.
105
+ */
106
+ export declare function markBoundaries(state: ProsodyState, attrs: Attrs): number;
107
+ /**
108
+ * hunk+0x20bc. Set bit 0 on every syllable of the phrase.
109
+ *
110
+ * `D4` is not loaded here — it is still the count `scanPhrase` left behind.
111
+ */
112
+ export declare function markVoiced(state: ProsodyState, syllables: number): void;
113
+ /**
114
+ * hunk+0x20d0. Record what the phrase ended with — `.` is 4, `?` is 8 —
115
+ * backwards from the end until a syllable that a pause follows.
116
+ *
117
+ * A comma is neither, so it leaves nothing; the difference between a statement
118
+ * and a question is decided here and read by {@link markCadence}.
119
+ */
120
+ export declare function markPunctuation(state: ProsodyState, syllables: number): void;
121
+ /**
122
+ * hunk+0x210a. Give the phrase its cadence: `0xb0` normally, `0x30` after a
123
+ * question, and a `4` on the last primary stress.
124
+ *
125
+ * It does nothing at all when the phrase ended on the punctuation itself —
126
+ * the cursors are past it by then, so `(-1,A0)` is the `.` or `?` and this
127
+ * returns. Only a phrase that ran out some other way gets a cadence.
128
+ *
129
+ * Which means **this** rise never fires. It is chosen by `arr5[n - 1] == 8`,
130
+ * and 8 is what {@link markPunctuation} writes for a `?`; but a phrase that
131
+ * ended in `?` has already returned two lines above, and in any case
132
+ * {@link markVoiced} has put bit 0 into every entry first, so the value is 9
133
+ * and never 8. Two independent reasons, either enough on its own.
134
+ *
135
+ * That is not to say 33.2 cannot ask a question. It does — in
136
+ * {@link linkSyllables}, which reads the same `arr5` byte and inverts the
137
+ * final fall into a rise, and which the corpus drives. This particular rise is
138
+ * the dead one. Ported as written rather than "fixed".
139
+ */
140
+ export declare function markCadence(state: ProsodyState, syllables: number): void;
141
+ /**
142
+ * hunk+0x1ee0. One pass round the driver's loop: find the next phrase and
143
+ * describe it, or report that there is none left.
144
+ *
145
+ * The driver at `hunk+0x832` calls this, then `hunk+0x2160` to turn the
146
+ * description into pitch values, and goes round again until this returns
147
+ * false. `false` is the device's `Z` exit and comes straight from the
148
+ * `move.w D4,(A5+0x8c)` that ends {@link scanPhrase} — a syllable count of
149
+ * zero sets it, which is why the routine ends by storing the count rather
150
+ * than by testing anything.
151
+ *
152
+ * Note the count the last three routines see is {@link markBoundaries}'s, not
153
+ * {@link scanPhrase}'s. Both count phonemes the spreader marked, but the scan
154
+ * can walk past some of them looking for a stress digit, so they are not
155
+ * guaranteed to agree, and the device leaves whichever ran last in `D4`.
156
+ */
157
+ export declare function nextPhrase(state: ProsodyState, attrs: Attrs): boolean;
158
+ /**
159
+ * hunk+0x21b8. The pitch the phrase's first stressed syllable starts on.
160
+ *
161
+ * This is **declination**: the value falls as the utterance goes on. It is
162
+ * built from how many phrases have been spoken (`pass`) and how many
163
+ * boundaries have been crossed, not from anything about the syllable itself,
164
+ * so the sixth phrase of a paragraph starts lower than the first — which is
165
+ * what a speaker running out of breath actually does.
166
+ *
167
+ * The result is clamped to 125..165, and returned because `hunk+0x220c` picks
168
+ * it up out of `D0` rather than reading it back.
169
+ */
170
+ export declare function phrasePitch(state: ProsodyState): number;
171
+ /**
172
+ * hunk+0x220c. Give every stressed syllable of the phrase its peak pitch.
173
+ *
174
+ * {@link phrasePitch} has put a value on the first one; this walks the rest and
175
+ * steps down from it, landing on 110 at the last — 115 if the phrase ended in
176
+ * a question mark, which is the only effect a `?` has on this build's contour
177
+ * and the only place the question survives at all.
178
+ *
179
+ * Three passes, in one routine:
180
+ *
181
+ * 1. **0x224c** — share the whole fall out over the phrase's primary stresses,
182
+ * but give the first and the last 19/128 of a step extra and take it back
183
+ * evenly from the ones in between. So the pitch drops fastest at each end
184
+ * of the phrase and coasts through the middle, which is what declination
185
+ * actually looks like. Below four stresses there is no middle to take it
186
+ * from and every step is equal.
187
+ * 2. **0x2274** — backwards, pulling each stressed syllable 51/128 of the way
188
+ * towards a neighbour. Dead in 33.2: the neighbour is chosen by the low two
189
+ * bits of `arr5`, {@link markVoiced} puts a 1 in every entry of it before
190
+ * this runs, and 1 is the "leave it alone" case.
191
+ * 3. **0x22d6** — scale by how stressed each syllable is. A level above 8 is
192
+ * pushed further above 110 and one below 8 pulled back towards it, in
193
+ * proportion to how far above 110 it already sits, so the contrast between
194
+ * a strong and a weak stress opens up at the top of the phrase and closes
195
+ * at the bottom.
196
+ *
197
+ * `pitch` is {@link phrasePitch}'s result, which the device leaves in `D0`.
198
+ */
199
+ export declare function syllablePitch(state: ProsodyState, pitch: number): void;
200
+ /**
201
+ * hunk+0x230c. How far each stressed syllable climbs to the peak
202
+ * {@link syllablePitch} gave it, and how far it drops away again.
203
+ *
204
+ * `arr6` is the climb and `arr7` the drop, both as distances *below* the peak,
205
+ * and `hunk+0x2642` finally subtracts them to get the syllable's onset and its
206
+ * end. Both are proportional to how far the peak sits above 110, so a syllable
207
+ * that has already fallen to the floor of the phrase gets no contour at all
208
+ * and the utterance flattens out as it ends.
209
+ *
210
+ * The shape is set by the low nibble of the cadence byte, read as a *signed*
211
+ * nibble: the climb is `(26·cadence + 128)/128` of that distance and the drop
212
+ * is `(cadence − 1)·26/128` of it. A cadence of 4 — what {@link markCadence}
213
+ * puts on the last primary stress of a phrase — makes the climb nearly twice
214
+ * the default and the drop three times it, which is the sentence-final fall.
215
+ *
216
+ * A negative nibble would invert the climb, and none is reachable: the only
217
+ * values anything puts in that nibble are 0 and the 4 {@link markCadence}
218
+ * writes, the 2 and the 0x0e of {@link markBoundaries} both coming from flag
219
+ * bits nothing in 33.2 sets.
220
+ *
221
+ * The drop is clipped at zero rather than allowed to go negative here, so at
222
+ * this stage a syllable can end level with its peak but never above it. Only
223
+ * {@link linkSyllables} lifts it above, and only for a question.
224
+ */
225
+ export declare function syllableRange(state: ProsodyState): void;
226
+ /**
227
+ * hunk+0x23ce. Reconcile each stressed syllable with the next one, and give
228
+ * the phrase its final punctuation.
229
+ *
230
+ * Two halves. The first walks the primary stresses backwards in pairs and
231
+ * adjusts both by how far apart they are:
232
+ *
233
+ * - **Back to back** (0x23f8) — both contours shrink to 77/128, the earlier
234
+ * peak drops 26/128 of its height and the later one rises by the same, and
235
+ * then whatever gap is left between where the earlier syllable ends and the
236
+ * later one starts is closed outright. Two stresses in a row have no room
237
+ * for two full contours, so they are flattened and butted together.
238
+ * - **Anything further apart** (0x2496) — both contours *grow*, by 19, 32 or
239
+ * 38 parts in 128 as the gap is one, two or more syllables, and the peaks
240
+ * move apart rather than together. With room between them each stress gets
241
+ * its own excursion, and the more room the bigger.
242
+ *
243
+ * The second half (0x2574) is the punctuation, on any stressed syllable a
244
+ * pause follows:
245
+ *
246
+ * - **A full stop** ends the syllable a flat 75 below its peak.
247
+ * - **A question** shortens the climb by 102/128 of the drop and then rebuilds
248
+ * the drop from the *highest* peak anywhere earlier in the phrase, times
249
+ * 154/128. That is larger than this syllable's own peak, so the drop comes
250
+ * out negative and the syllable ends *above* where it peaked.
251
+ *
252
+ * So 33.2 does speak a question differently — here, in arithmetic, rather than
253
+ * through the rise flag in {@link markCadence} that no input can select.
254
+ */
255
+ export declare function linkSyllables(state: ProsodyState): void;
256
+ /**
257
+ * hunk+0x25f8. Deepen the drop at a phrase boundary.
258
+ *
259
+ * The high nibble of the cadence byte is what {@link markCadence} and
260
+ * {@link markBoundaries} put there: `0xb0` on the phrase's last syllable and
261
+ * `0x90` on the one before a dash. Either adds 38/128 to that syllable's drop,
262
+ * so the pitch falls half again as far at the end of a phrase as it does
263
+ * inside one. That, and not the cadence flag, is what a comma sounds like.
264
+ *
265
+ * The other arm takes 102/128 *off* the drop instead, and is unreachable: it
266
+ * wants the high nibble non-zero with bit 7 clear, and the only two values
267
+ * that can put anything in that nibble both set bit 7. Its two possible
268
+ * sources — the `2` and the `0x0e` of {@link markBoundaries} — need flag bits
269
+ * nothing in 33.2 sets.
270
+ */
271
+ export declare function boundaryFall(state: ProsodyState): void;
272
+ /**
273
+ * hunk+0x2642. Turn the peaks and their two distances into three real pitches
274
+ * per syllable, and fill in every syllable that has not got one.
275
+ *
276
+ * Up to here only the primary stresses have been touched, and each is held as
277
+ * a peak with a climb and a drop hanging off it. This is where that becomes
278
+ * `arr0`, `arr1`, `arr2` — an onset, a peak and an end — for *every* syllable
279
+ * of the phrase, which is what `hunk+0x1a8e` needs.
280
+ *
281
+ * Four passes:
282
+ *
283
+ * 1. **0x265e** — subtract: onset is peak minus climb, end is peak minus drop.
284
+ * 2. **0x2680** — take the stresses in pairs and fill the gap between them.
285
+ * If the earlier syllable ends below where the later one starts, the two
286
+ * are moved half the distance towards each other first, so the line never
287
+ * has to climb backwards. Then the run between them glides down through
288
+ * {@link GLIDE}.
289
+ * 3. **0x277e** — everything *before* the phrase's first stress sits flat at
290
+ * 110, with its peak lifted by twice its own stress level. An unstressed
291
+ * lead-in is spoken at the bottom of the range.
292
+ * 4. **0x27a8** — everything *after* the last stress. Here the step is not
293
+ * from a table: it is the whole remaining distance down to 110, divided
294
+ * evenly by however many syllables are left, so the tail always lands on
295
+ * the floor whatever its length. A full stop aims 35 lower still and ends
296
+ * on 75; a comma or nothing ends on 110; a question does something else
297
+ * entirely and ends on 154/128 of the phrase's highest peak.
298
+ */
299
+ export declare function fillContours(state: ProsodyState): void;
300
+ /**
301
+ * hunk+0x2864. Squeeze each stressed syllable's contour by the consonants
302
+ * around it — the last thing the pitch loop does.
303
+ *
304
+ * This is **consonantal F0 perturbation**, and it is the most linguistically
305
+ * literate thing in the device after {@link intrinsicPitch}. A consonant
306
+ * disturbs the pitch of the vowel next to it, and how much depends on whether
307
+ * it is voiced: a voiceless one perturbs far more, because the larynx has to
308
+ * stop and restart. Both ends of the syllable are treated separately.
309
+ *
310
+ * At the **onset** (0x28a0), only if the syllable begins with a consonant: the
311
+ * start of the contour is moved up towards the peak by 26/128 of the climb
312
+ * after a voiced consonant and 102/128 after a voiceless one — so a syllable
313
+ * beginning `p` or `t` has almost no room to rise and one beginning `m` or `b`
314
+ * keeps nearly all of it. A voiceless onset also lifts the whole contour by
315
+ * 26/128 of its height first, which is the well-attested raising of F0 after a
316
+ * voiceless stop.
317
+ *
318
+ * At the **end** (0x291a), by what the syllable runs into. The device walks
319
+ * forward past the stress digit to the first phoneme that settles it:
320
+ *
321
+ * | | |
322
+ * |---|---|
323
+ * | 26/128 | a vowel or the glottal stop — the fall carries on into it |
324
+ * | 86/128 | a voiceless phoneme first — most of the fall is eaten |
325
+ * | 64/128 | a pause follows, or the next syllable is stressed too |
326
+ *
327
+ * Voiced consonants in between are stepped over, since they do not interrupt
328
+ * the pitch. And a phrase whose last syllable is stressed is left alone, there
329
+ * being nothing after it to run into.
330
+ *
331
+ * Each adjustment moves the endpoint and shrinks the distance by the same
332
+ * amount, so the peak never moves and the three stay consistent.
333
+ */
334
+ export declare function coarticulatePitch(state: ProsodyState, attrs: Attrs): void;
335
+ /**
336
+ * hunk+0x2160. One phrase's worth of pitch, and then move the cursors past it.
337
+ *
338
+ * The driver at `hunk+0x832` alternates {@link nextPhrase} with this until
339
+ * there is no phrase left, so between them they are the whole of the tune.
340
+ *
341
+ * Two things are skipped rather than guarded inside the routines themselves.
342
+ * `mode` — the monotone robot voice — skips all seven, since
343
+ * {@link assignPitch} is going to write one flat period over everything
344
+ * anyway; and a phrase with no primary stress in it skips the first four,
345
+ * which are the ones that need somewhere to hang a contour, leaving the last
346
+ * three to fill it in flat.
347
+ *
348
+ * The seven pass registers between them and the device never reloads what one
349
+ * of them clobbers, so they have to run in this order. `phrasePitch` is the
350
+ * only one whose result is threaded by hand, in `D0`.
351
+ *
352
+ * Ends by adding the syllable count to all eight array cursors at once, which
353
+ * is what makes the next phrase write where this one stopped.
354
+ */
355
+ export declare function pitchLoopBody(state: ProsodyState, attrs: Attrs, mode: number): void;