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.
- package/LICENSE +21 -0
- package/README.md +131 -3
- package/dist/aros-resource/index.d.ts +39 -0
- package/dist/aros-resource/index.js +282 -0
- 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 +66 -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,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;
|