@kongyo2/kongyoroid 1.0.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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/cache.d.ts +36 -0
  4. package/dist/cache.d.ts.map +1 -0
  5. package/dist/cache.js +109 -0
  6. package/dist/cache.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +502 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/concurrency.d.ts +14 -0
  12. package/dist/concurrency.d.ts.map +1 -0
  13. package/dist/concurrency.js +107 -0
  14. package/dist/concurrency.js.map +1 -0
  15. package/dist/errors.d.ts +35 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +81 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/formant.d.ts +34 -0
  20. package/dist/formant.d.ts.map +1 -0
  21. package/dist/formant.js +622 -0
  22. package/dist/formant.js.map +1 -0
  23. package/dist/index.d.ts +40 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +23 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/io.d.ts +6 -0
  28. package/dist/io.d.ts.map +1 -0
  29. package/dist/io.js +144 -0
  30. package/dist/io.js.map +1 -0
  31. package/dist/kongyoroid.d.ts +82 -0
  32. package/dist/kongyoroid.d.ts.map +1 -0
  33. package/dist/kongyoroid.js +275 -0
  34. package/dist/kongyoroid.js.map +1 -0
  35. package/dist/limits.d.ts +18 -0
  36. package/dist/limits.d.ts.map +1 -0
  37. package/dist/limits.js +15 -0
  38. package/dist/limits.js.map +1 -0
  39. package/dist/mora.d.ts +28 -0
  40. package/dist/mora.d.ts.map +1 -0
  41. package/dist/mora.js +295 -0
  42. package/dist/mora.js.map +1 -0
  43. package/dist/notation.d.ts +15 -0
  44. package/dist/notation.d.ts.map +1 -0
  45. package/dist/notation.js +114 -0
  46. package/dist/notation.js.map +1 -0
  47. package/dist/pitch.d.ts +5 -0
  48. package/dist/pitch.d.ts.map +1 -0
  49. package/dist/pitch.js +36 -0
  50. package/dist/pitch.js.map +1 -0
  51. package/dist/player.d.ts +9 -0
  52. package/dist/player.d.ts.map +1 -0
  53. package/dist/player.js +81 -0
  54. package/dist/player.js.map +1 -0
  55. package/dist/request.d.ts +12 -0
  56. package/dist/request.d.ts.map +1 -0
  57. package/dist/request.js +115 -0
  58. package/dist/request.js.map +1 -0
  59. package/dist/schema.d.ts +9 -0
  60. package/dist/schema.d.ts.map +1 -0
  61. package/dist/schema.js +267 -0
  62. package/dist/schema.js.map +1 -0
  63. package/dist/score.d.ts +37 -0
  64. package/dist/score.d.ts.map +1 -0
  65. package/dist/score.js +166 -0
  66. package/dist/score.js.map +1 -0
  67. package/dist/text.d.ts +3 -0
  68. package/dist/text.d.ts.map +1 -0
  69. package/dist/text.js +54 -0
  70. package/dist/text.js.map +1 -0
  71. package/dist/types.d.ts +121 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/types.js +2 -0
  74. package/dist/types.js.map +1 -0
  75. package/dist/validate.d.ts +13 -0
  76. package/dist/validate.d.ts.map +1 -0
  77. package/dist/validate.js +59 -0
  78. package/dist/validate.js.map +1 -0
  79. package/dist/version.d.ts +3 -0
  80. package/dist/version.d.ts.map +1 -0
  81. package/dist/version.js +3 -0
  82. package/dist/version.js.map +1 -0
  83. package/dist/voicevox/api.d.ts +79 -0
  84. package/dist/voicevox/api.d.ts.map +1 -0
  85. package/dist/voicevox/api.js +176 -0
  86. package/dist/voicevox/api.js.map +1 -0
  87. package/dist/voicevox/client.d.ts +64 -0
  88. package/dist/voicevox/client.d.ts.map +1 -0
  89. package/dist/voicevox/client.js +321 -0
  90. package/dist/voicevox/client.js.map +1 -0
  91. package/dist/voicevox/dictionary.d.ts +21 -0
  92. package/dist/voicevox/dictionary.d.ts.map +1 -0
  93. package/dist/voicevox/dictionary.js +41 -0
  94. package/dist/voicevox/dictionary.js.map +1 -0
  95. package/dist/voicevox/song.d.ts +12 -0
  96. package/dist/voicevox/song.d.ts.map +1 -0
  97. package/dist/voicevox/song.js +45 -0
  98. package/dist/voicevox/song.js.map +1 -0
  99. package/dist/voicevox/speech.d.ts +17 -0
  100. package/dist/voicevox/speech.d.ts.map +1 -0
  101. package/dist/voicevox/speech.js +100 -0
  102. package/dist/voicevox/speech.js.map +1 -0
  103. package/dist/voicevox/styles.d.ts +15 -0
  104. package/dist/voicevox/styles.d.ts.map +1 -0
  105. package/dist/voicevox/styles.js +103 -0
  106. package/dist/voicevox/styles.js.map +1 -0
  107. package/dist/wav.d.ts +18 -0
  108. package/dist/wav.d.ts.map +1 -0
  109. package/dist/wav.js +198 -0
  110. package/dist/wav.js.map +1 -0
  111. package/examples/batch.jsonl +4 -0
  112. package/examples/song-notes.json +24 -0
  113. package/examples/song.json +12 -0
  114. package/examples/speech-kana.json +6 -0
  115. package/examples/speech.json +8 -0
  116. package/package.json +86 -0
@@ -0,0 +1,622 @@
1
+ import { setImmediate as yieldToEventLoop } from "node:timers/promises";
2
+ import { checkAbort, invalid } from "./errors.js";
3
+ import { LIMITS } from "./limits.js";
4
+ import { kanaToUnits } from "./mora.js";
5
+ import { parseKanaNotation } from "./notation.js";
6
+ import { midiToHz } from "./pitch.js";
7
+ import { encodePcm16, wavHeader } from "./wav.js";
8
+ function vowel(f, amp) {
9
+ return { f, bw: [80, 110, 170, 230], amp, voiced: 1, noise: 0.02, level: 1, kind: "vowel", duration: 0.1 };
10
+ }
11
+ function whisper(base) {
12
+ return { ...base, voiced: 0, noise: 1, level: 0.22, bw: [140, 180, 240, 300] };
13
+ }
14
+ function nasal(f, duration) {
15
+ return {
16
+ f,
17
+ bw: [90, 220, 280, 320],
18
+ amp: [1, 0.22, 0.09, 0.03],
19
+ voiced: 1,
20
+ noise: 0.01,
21
+ level: 0.72,
22
+ kind: "nasal",
23
+ duration,
24
+ };
25
+ }
26
+ function stop(f, amp, voiced, level, duration) {
27
+ return { f, bw: [220, 320, 420, 520], amp, voiced, noise: 1, level, kind: "stop", duration };
28
+ }
29
+ function fricative(f, bw, amp, voiced, level, duration, kind = "fricative") {
30
+ return { f, bw, amp, voiced, noise: 1, level, kind, duration };
31
+ }
32
+ function glide(f, amp) {
33
+ return { f, bw: [70, 120, 180, 240], amp, voiced: 1, noise: 0.02, level: 0.82, kind: "glide", duration: 0.045 };
34
+ }
35
+ function palatal(base) {
36
+ return { ...base, f: [base.f[0], Math.max(base.f[1], 2100), Math.max(base.f[2], 2900), base.f[3]] };
37
+ }
38
+ function labial(base) {
39
+ return { ...base, f: [base.f[0], Math.min(base.f[1], 900), base.f[2], base.f[3]] };
40
+ }
41
+ const A = vowel([750, 1200, 2600, 3500], [1, 0.5, 0.25, 0.1]);
42
+ const I = vowel([290, 2250, 3000, 3600], [1, 0.32, 0.2, 0.08]);
43
+ const U = vowel([330, 1300, 2350, 3400], [1, 0.28, 0.16, 0.06]);
44
+ const E = vowel([500, 1900, 2550, 3500], [1, 0.45, 0.25, 0.1]);
45
+ const O = vowel([520, 900, 2500, 3400], [1, 0.45, 0.18, 0.07]);
46
+ const SILENCE = {
47
+ f: [500, 1500, 2500, 3500],
48
+ bw: [200, 200, 200, 200],
49
+ amp: [0, 0, 0, 0],
50
+ voiced: 0,
51
+ noise: 0,
52
+ level: 0,
53
+ kind: "silence",
54
+ duration: 0.08,
55
+ };
56
+ const K = stop([1500, 2500, 3500, 4500], [0.4, 1, 0.6, 0.3], 0, 0.7, 0.065);
57
+ const G = stop([400, 1800, 2600, 3400], [0.7, 1, 0.5, 0.25], 0.45, 0.65, 0.05);
58
+ const T = stop([1000, 3500, 4500, 5500], [0.3, 1, 0.7, 0.4], 0, 0.7, 0.06);
59
+ const D = stop([350, 2000, 3000, 4000], [0.8, 1, 0.5, 0.25], 0.5, 0.65, 0.045);
60
+ const P = stop([600, 1300, 2500, 3500], [1, 0.6, 0.3, 0.15], 0, 0.55, 0.065);
61
+ const B = stop([300, 1000, 2300, 3300], [1, 0.6, 0.3, 0.15], 0.55, 0.6, 0.045);
62
+ const S = fricative([4500, 6000, 7500, 9000], [800, 900, 1000, 1200], [0.6, 1, 0.8, 0.5], 0, 0.45, 0.085);
63
+ const SH = fricative([2600, 3800, 5200, 7000], [500, 600, 800, 1000], [0.8, 1, 0.7, 0.4], 0, 0.5, 0.085);
64
+ const H = fricative([900, 1900, 2800, 3600], [300, 400, 500, 600], [0.7, 0.8, 0.5, 0.3], 0, 0.32, 0.07);
65
+ const F = fricative([1100, 2100, 3200, 4500], [600, 700, 800, 900], [0.7, 0.8, 0.6, 0.4], 0, 0.3, 0.075);
66
+ const NASAL_N = nasal([280, 1300, 2300, 3300], 0.055);
67
+ const M = nasal([250, 950, 2200, 3200], 0.06);
68
+ const R = {
69
+ f: [380, 1400, 2300, 3300],
70
+ bw: [100, 150, 200, 250],
71
+ amp: [1, 0.45, 0.2, 0.08],
72
+ voiced: 1,
73
+ noise: 0.02,
74
+ level: 0.6,
75
+ kind: "flap",
76
+ duration: 0.026,
77
+ };
78
+ const Y = glide([280, 2100, 2900, 3500], [1, 0.35, 0.2, 0.08]);
79
+ const W = glide([300, 700, 2300, 3300], [1, 0.4, 0.15, 0.06]);
80
+ const TIMBRES = {
81
+ a: A,
82
+ i: I,
83
+ u: U,
84
+ e: E,
85
+ o: O,
86
+ A: whisper(A),
87
+ I: whisper(I),
88
+ U: whisper(U),
89
+ E: whisper(E),
90
+ O: whisper(O),
91
+ N: nasal([280, 1100, 2300, 3300], 0.085),
92
+ cl: SILENCE,
93
+ pau: SILENCE,
94
+ k: K,
95
+ ky: palatal(K),
96
+ kw: labial(K),
97
+ g: G,
98
+ gy: palatal(G),
99
+ gw: labial(G),
100
+ t: T,
101
+ ty: palatal(T),
102
+ d: D,
103
+ dy: palatal(D),
104
+ p: P,
105
+ py: palatal(P),
106
+ b: B,
107
+ by: palatal(B),
108
+ s: S,
109
+ sh: SH,
110
+ z: fricative(S.f, S.bw, S.amp, 0.6, 0.5, 0.07),
111
+ j: fricative(SH.f, SH.bw, SH.amp, 0.6, 0.5, 0.075, "affricate"),
112
+ ch: fricative(SH.f, SH.bw, SH.amp, 0, 0.55, 0.085, "affricate"),
113
+ ts: fricative(S.f, S.bw, S.amp, 0, 0.5, 0.085, "affricate"),
114
+ h: H,
115
+ hy: palatal(H),
116
+ f: F,
117
+ v: fricative(F.f, F.bw, F.amp, 0.7, 0.45, 0.06),
118
+ n: NASAL_N,
119
+ ny: palatal(NASAL_N),
120
+ m: M,
121
+ my: palatal(M),
122
+ r: R,
123
+ ry: palatal(R),
124
+ y: Y,
125
+ w: W,
126
+ };
127
+ const PALATALIZED = new Set(["ky", "gy", "ty", "dy", "py", "by", "hy", "ny", "my", "ry"]);
128
+ const LABIALIZED = new Set(["kw", "gw"]);
129
+ const BASE_F0 = 150;
130
+ function isVoicedSustain(phoneme) {
131
+ const kind = TIMBRES[phoneme].kind;
132
+ return (kind === "vowel" || kind === "nasal" || kind === "glide" || kind === "flap") && TIMBRES[phoneme].voiced > 0;
133
+ }
134
+ function finalize(drafts, sampleRate, plan) {
135
+ const segments = [];
136
+ let cursor = 0;
137
+ let seconds = 0;
138
+ for (const [index, draft] of drafts.entries()) {
139
+ seconds += draft.seconds;
140
+ const end = Math.round(seconds * sampleRate);
141
+ const frames = end - cursor;
142
+ if (frames <= 0)
143
+ continue;
144
+ cursor = end;
145
+ const previous = drafts[index - 1];
146
+ const next = drafts[index + 1];
147
+ segments.push({
148
+ phoneme: draft.phoneme,
149
+ frames,
150
+ f0Start: draft.f0Start,
151
+ f0End: draft.f0End,
152
+ amplitude: draft.amplitude,
153
+ vibrato: draft.vibrato,
154
+ rampIn: previous === undefined || !isVoicedSustain(previous.phoneme),
155
+ rampOut: next === undefined || !isVoicedSustain(next.phoneme),
156
+ });
157
+ }
158
+ if (cursor > LIMITS.formantSeconds * sampleRate) {
159
+ invalid("$", `Audio would last ${(cursor / sampleRate).toFixed(1)} s; the formant engine renders at most ${LIMITS.formantSeconds} s per request.`, "Split the input into shorter requests, or use the voicevox engine.");
160
+ }
161
+ return { ...plan, segments, frames: cursor };
162
+ }
163
+ function consonantDrafts(consonant, seconds, f0Start, f0End) {
164
+ if (consonant === null)
165
+ return [];
166
+ const out = [{ phoneme: consonant, seconds, f0Start, f0End, amplitude: 1, vibrato: false }];
167
+ if (PALATALIZED.has(consonant))
168
+ out.push({ phoneme: "y", seconds: 0.03, f0Start: f0End, f0End, amplitude: 0.9, vibrato: false });
169
+ if (LABIALIZED.has(consonant))
170
+ out.push({ phoneme: "w", seconds: 0.03, f0Start: f0End, f0End, amplitude: 0.9, vibrato: false });
171
+ return out;
172
+ }
173
+ function shapePitch(hz, pitch, intonation) {
174
+ const mean = Math.log(BASE_F0 * 1.08);
175
+ const scaled = mean + (Math.log(hz) - mean) * intonation;
176
+ return Math.exp(scaled * 2 ** pitch);
177
+ }
178
+ function heuristicPhrases(units) {
179
+ const phrases = [];
180
+ let moras = [];
181
+ const flush = (pauseWeight, interrogative) => {
182
+ if (moras.length === 0) {
183
+ const last = phrases.at(-1);
184
+ if (last !== undefined && pauseWeight > 0) {
185
+ phrases[phrases.length - 1] = {
186
+ phrase: { ...last.phrase, pause: true, interrogative: last.phrase.interrogative || interrogative },
187
+ pauseWeight: last.pauseWeight + pauseWeight,
188
+ };
189
+ }
190
+ return;
191
+ }
192
+ phrases.push({ phrase: { moras, accent: moras.length, pause: pauseWeight > 0, interrogative }, pauseWeight });
193
+ moras = [];
194
+ };
195
+ for (const unit of units) {
196
+ if (unit.kind === "pause") {
197
+ flush(unit.weight, unit.text === "?");
198
+ continue;
199
+ }
200
+ moras.push({ text: unit.text, consonant: unit.consonant, vowel: unit.vowel });
201
+ if (moras.length >= 7)
202
+ flush(0, false);
203
+ }
204
+ flush(0, false);
205
+ return phrases;
206
+ }
207
+ export function planSpeech(request, sampleRate) {
208
+ const phrases = request.kana === undefined
209
+ ? heuristicPhrases(kanaToUnits(request.text, "$.text"))
210
+ : parseKanaNotation(request.kana, "$.kana").map((phrase) => ({ phrase, pauseWeight: 1 }));
211
+ if (phrases.length === 0)
212
+ invalid("$.text", "Text contains no readable kana.");
213
+ const speed = request.speed;
214
+ const drafts = [];
215
+ const rest = (seconds) => {
216
+ drafts.push({ phoneme: "pau", seconds, f0Start: 0, f0End: 0, amplitude: 0, vibrato: false });
217
+ };
218
+ rest(request.prePause);
219
+ let previousHz = shapePitch(BASE_F0, request.pitch, request.intonation);
220
+ for (const [phraseIndex, { phrase, pauseWeight }] of phrases.entries()) {
221
+ const declination = Math.max(0.85, 1 - 0.04 * phraseIndex);
222
+ const count = phrase.moras.length;
223
+ for (const [moraIndex, mora] of phrase.moras.entries()) {
224
+ const position = moraIndex + 1;
225
+ const high = phrase.accent === 1 ? position === 1 : position >= 2 && position <= phrase.accent;
226
+ const slope = high ? 1.22 - (0.1 * moraIndex) / Math.max(1, count - 1) : 0.97;
227
+ const targetHz = shapePitch(BASE_F0 * slope * declination, request.pitch, request.intonation);
228
+ const last = moraIndex === count - 1;
229
+ const endHz = request.upspeak && phrase.interrogative && last ? targetHz * 1.35 : targetHz * 0.985;
230
+ const timbre = TIMBRES[mora.vowel];
231
+ const consonant = mora.consonant;
232
+ const consonantSeconds = consonant === null ? 0 : TIMBRES[consonant].duration / speed;
233
+ drafts.push(...consonantDrafts(consonant, consonantSeconds, previousHz, targetHz));
234
+ const vowelSeconds = (mora.vowel === "cl"
235
+ ? 0.08
236
+ : mora.vowel === "N"
237
+ ? 0.085
238
+ : timbre.kind === "vowel" && timbre.voiced === 0
239
+ ? 0.07
240
+ : consonant === null
241
+ ? 0.12
242
+ : 0.095) / speed;
243
+ drafts.push({
244
+ phoneme: mora.vowel,
245
+ seconds: vowelSeconds,
246
+ f0Start: targetHz,
247
+ f0End: endHz,
248
+ amplitude: 1,
249
+ vibrato: false,
250
+ });
251
+ previousHz = endHz;
252
+ }
253
+ if (phrase.pause) {
254
+ const base = request.pauseLength ?? 0.18 * pauseWeight;
255
+ rest((base * request.pauseScale) / speed);
256
+ }
257
+ else if (phraseIndex < phrases.length - 1) {
258
+ rest(0.02 / speed);
259
+ }
260
+ }
261
+ rest(request.postPause);
262
+ return finalize(drafts, sampleRate, {
263
+ sampleRate,
264
+ seed: request.seed,
265
+ volume: request.volume,
266
+ vibratoDepth: 0,
267
+ vibratoRate: 0,
268
+ });
269
+ }
270
+ export function planSong(request, sampleRate) {
271
+ const secondsPerBeat = 60 / request.tempo;
272
+ const notes = request.notes;
273
+ const drafts = [];
274
+ const rest = (seconds) => {
275
+ drafts.push({ phoneme: "pau", seconds, f0Start: 0, f0End: 0, amplitude: 0, vibrato: false });
276
+ };
277
+ const leadIn = notes[0]?.key === null ? 0 : request.leadIn;
278
+ const durations = notes.map((note) => note.beats * secondsPerBeat);
279
+ const consonantOf = (index) => {
280
+ const note = notes[index];
281
+ if (note === undefined || note.key === null)
282
+ return { consonant: null, seconds: 0, fromOwn: false };
283
+ const units = kanaToUnits(note.lyric, `$.notes[${index}].lyric`);
284
+ const unit = units[0];
285
+ if (unit === undefined || unit.kind !== "mora" || unit.consonant === null) {
286
+ return { consonant: null, seconds: 0, fromOwn: false };
287
+ }
288
+ const nominal = TIMBRES[unit.consonant].duration + (PALATALIZED.has(unit.consonant) || LABIALIZED.has(unit.consonant) ? 0.03 : 0);
289
+ const fromOwn = index === 0 && leadIn <= 0;
290
+ const available = index === 0 ? (fromOwn ? (durations[0] ?? 0) * 0.4 : leadIn) : (durations[index - 1] ?? 0) * 0.4;
291
+ return { consonant: unit.consonant, seconds: Math.min(nominal, available), fromOwn };
292
+ };
293
+ let previousHz = 0;
294
+ const first = consonantOf(0);
295
+ if (leadIn > 0)
296
+ rest(leadIn - first.seconds);
297
+ for (const [index, note] of notes.entries()) {
298
+ const duration = durations[index] ?? 0;
299
+ const own = consonantOf(index);
300
+ const following = consonantOf(index + 1);
301
+ if (note.key === null) {
302
+ rest(Math.max(0, duration - following.seconds));
303
+ previousHz = 0;
304
+ continue;
305
+ }
306
+ const hz = midiToHz(note.key);
307
+ const vowelUnit = kanaToUnits(note.lyric, `$.notes[${index}].lyric`)[0];
308
+ const vowelPhoneme = vowelUnit !== undefined && vowelUnit.kind === "mora" ? vowelUnit.vowel : "a";
309
+ if (own.consonant !== null && own.seconds > 0) {
310
+ const glideSeconds = PALATALIZED.has(own.consonant) || LABIALIZED.has(own.consonant) ? Math.min(0.03, own.seconds * 0.4) : 0;
311
+ const parts = consonantDrafts(own.consonant, own.seconds - glideSeconds, previousHz || hz, hz);
312
+ for (const part of parts)
313
+ if (part.phoneme === "y" || part.phoneme === "w")
314
+ part.seconds = glideSeconds;
315
+ drafts.push(...parts);
316
+ }
317
+ const vowelSeconds = Math.max(0, duration - following.seconds - (own.fromOwn ? own.seconds : 0));
318
+ const previous = drafts.at(-1);
319
+ if (previous !== undefined &&
320
+ previous.phoneme === vowelPhoneme &&
321
+ previous.vibrato &&
322
+ previous.f0End === hz &&
323
+ own.consonant === null) {
324
+ previous.seconds += vowelSeconds;
325
+ }
326
+ else {
327
+ drafts.push({
328
+ phoneme: vowelPhoneme,
329
+ seconds: vowelSeconds,
330
+ f0Start: previousHz === 0 ? hz : previousHz,
331
+ f0End: hz,
332
+ amplitude: 1,
333
+ vibrato: true,
334
+ });
335
+ }
336
+ previousHz = hz;
337
+ }
338
+ if (notes.at(-1)?.key !== null)
339
+ rest(request.leadOut);
340
+ return finalize(drafts, sampleRate, {
341
+ sampleRate,
342
+ seed: request.seed,
343
+ volume: request.volume,
344
+ vibratoDepth: request.vibratoDepth ?? 25,
345
+ vibratoRate: request.vibratoRate,
346
+ });
347
+ }
348
+ class Resonator {
349
+ a1 = 0;
350
+ a2 = 0;
351
+ b = 0;
352
+ y1 = 0;
353
+ y2 = 0;
354
+ x1 = 0;
355
+ x2 = 0;
356
+ tune(frequency, width, rate) {
357
+ const radius = Math.exp((-Math.PI * width) / rate);
358
+ this.a1 = 2 * radius * Math.cos((2 * Math.PI * Math.min(rate * 0.45, frequency)) / rate);
359
+ this.a2 = radius * radius;
360
+ this.b = (1 - radius) * 0.5;
361
+ }
362
+ step(x) {
363
+ const y = this.b * (x - this.x2) + this.a1 * this.y1 - this.a2 * this.y2;
364
+ this.x2 = this.x1;
365
+ this.x1 = x;
366
+ this.y2 = this.y1;
367
+ this.y1 = y;
368
+ return y;
369
+ }
370
+ }
371
+ function polyBlep(t, step) {
372
+ if (t < step) {
373
+ const x = t / step;
374
+ return x + x - x * x - 1;
375
+ }
376
+ if (t > 1 - step) {
377
+ const x = (t - 1) / step;
378
+ return x * x + x + x + 1;
379
+ }
380
+ return 0;
381
+ }
382
+ const CONTROL = 32;
383
+ class FormantRenderer {
384
+ plan;
385
+ filters = [new Resonator(), new Resonator(), new Resonator(), new Resonator()];
386
+ frequencies = new Float64Array([500, 1500, 2500, 3500]);
387
+ widths = new Float64Array([100, 150, 200, 250]);
388
+ gains = new Float64Array(4);
389
+ segmentIndex = 0;
390
+ segmentStart = 0;
391
+ cursor = 0;
392
+ phase = 0;
393
+ random;
394
+ source = 0;
395
+ dcIn = 0;
396
+ dcOut = 0;
397
+ pitchStep = 0;
398
+ voiced = 0;
399
+ noise = 0;
400
+ level = 0;
401
+ envelope = 0;
402
+ lastHz = BASE_F0;
403
+ constructor(plan) {
404
+ this.plan = plan;
405
+ this.random = plan.seed >>> 0 || 1;
406
+ }
407
+ nextRandom() {
408
+ let r = this.random;
409
+ r ^= r << 13;
410
+ r ^= r >>> 17;
411
+ r ^= r << 5;
412
+ this.random = r >>> 0;
413
+ return this.random / 2_147_483_648 - 1;
414
+ }
415
+ control(segment, local, rate) {
416
+ const timbre = TIMBRES[segment.phoneme];
417
+ const progress = local / Math.max(1, segment.frames);
418
+ const seconds = local / rate;
419
+ const tau = timbre.kind === "vowel" || timbre.kind === "nasal" || timbre.kind === "glide" ? 0.014 : 0.006;
420
+ const mix = 1 - Math.exp(-CONTROL / (rate * tau));
421
+ for (let band = 0; band < 4; band++) {
422
+ const f = this.frequencies[band] ?? 500;
423
+ const w = this.widths[band] ?? 100;
424
+ const targetF = timbre.f[band] ?? f;
425
+ const targetW = timbre.bw[band] ?? w;
426
+ const nextF = f + (targetF - f) * mix;
427
+ const nextW = w + (targetW - w) * mix;
428
+ this.frequencies[band] = nextF;
429
+ this.widths[band] = nextW;
430
+ this.gains[band] = timbre.amp[band] ?? 0;
431
+ this.filters[band]?.tune(nextF, nextW, rate);
432
+ }
433
+ let hz = segment.f0Start > 0 && segment.f0End > 0
434
+ ? segment.f0Start + (segment.f0End - segment.f0Start) * Math.min(1, progress * 3)
435
+ : this.lastHz;
436
+ if (segment.f0End > 0)
437
+ this.lastHz = segment.f0End;
438
+ if (segment.vibrato && this.plan.vibratoDepth > 0) {
439
+ const fade = Math.min(1, Math.max(0, (seconds - 0.12) / 0.18));
440
+ const cents = this.plan.vibratoDepth * fade * Math.sin(2 * Math.PI * this.plan.vibratoRate * (this.cursor / rate));
441
+ hz *= 2 ** (cents / 1200);
442
+ }
443
+ hz *= 1 + 0.004 * this.nextRandom();
444
+ this.pitchStep = hz / rate;
445
+ let voiced = timbre.voiced;
446
+ let noise = timbre.noise;
447
+ let level = timbre.level;
448
+ switch (timbre.kind) {
449
+ case "stop": {
450
+ const closure = 0.6;
451
+ if (progress < closure) {
452
+ noise = 0;
453
+ voiced = timbre.voiced > 0 ? 0.12 : 0;
454
+ level = timbre.voiced > 0 ? 0.3 : 0;
455
+ }
456
+ else {
457
+ const burst = (progress - closure) / (1 - closure);
458
+ noise = Math.exp(-burst * 5);
459
+ voiced = timbre.voiced * Math.min(1, burst * 3);
460
+ }
461
+ break;
462
+ }
463
+ case "affricate":
464
+ if (progress < 0.3) {
465
+ noise = 0;
466
+ voiced = timbre.voiced * 0.2;
467
+ level *= 0.3;
468
+ }
469
+ break;
470
+ case "flap":
471
+ level *= 0.5 + 0.5 * Math.abs(Math.cos(Math.PI * progress));
472
+ break;
473
+ default:
474
+ break;
475
+ }
476
+ this.voiced = voiced;
477
+ this.noise = noise;
478
+ this.level = level * segment.amplitude;
479
+ }
480
+ fill(buffer) {
481
+ const plan = this.plan;
482
+ const rate = plan.sampleRate;
483
+ const attack = Math.max(1, Math.round(rate * 0.008));
484
+ const release = Math.max(1, Math.round(rate * 0.014));
485
+ for (let i = 0; i < buffer.length; i++, this.cursor++) {
486
+ let segment = plan.segments[this.segmentIndex];
487
+ while (segment !== undefined && this.cursor >= this.segmentStart + segment.frames) {
488
+ this.segmentStart += segment.frames;
489
+ this.segmentIndex += 1;
490
+ segment = plan.segments[this.segmentIndex];
491
+ }
492
+ if (segment === undefined) {
493
+ buffer[i] = 0;
494
+ continue;
495
+ }
496
+ const local = this.cursor - this.segmentStart;
497
+ if (local % CONTROL === 0 || local === 0)
498
+ this.control(segment, local, rate);
499
+ const saw = 2 * this.phase - 1 - polyBlep(this.phase, this.pitchStep);
500
+ this.phase += this.pitchStep;
501
+ this.phase -= Math.floor(this.phase);
502
+ this.source += 0.3 * (saw - this.source);
503
+ const excitation = this.source * this.voiced + this.nextRandom() * this.noise * 0.6;
504
+ let filtered = 0;
505
+ for (let band = 0; band < 4; band++) {
506
+ filtered += (this.gains[band] ?? 0) * (this.filters[band]?.step(excitation) ?? 0);
507
+ }
508
+ const dc = filtered - this.dcIn + 0.995 * this.dcOut;
509
+ this.dcIn = filtered;
510
+ this.dcOut = dc;
511
+ const target = (segment.rampIn && local < attack ? local / attack : 1) *
512
+ (segment.rampOut && segment.frames - local < release ? (segment.frames - local) / release : 1);
513
+ this.envelope += 0.2 * (target - this.envelope);
514
+ buffer[i] = Math.tanh(dc * 7) * this.level * this.envelope;
515
+ }
516
+ }
517
+ }
518
+ const LEVEL_TARGET = { vowel: 0.3, nasal: 0.22, glide: 0.26 };
519
+ function levelGain(segment, block, sung) {
520
+ const kind = TIMBRES[segment.phoneme].kind;
521
+ if ((kind !== "vowel" && kind !== "nasal" && kind !== "glide") || TIMBRES[segment.phoneme].voiced === 0)
522
+ return 1;
523
+ let sum = 0;
524
+ for (let i = 0; i < block.length; i++)
525
+ sum += (block[i] ?? 0) ** 2;
526
+ const rms = Math.sqrt(sum / Math.max(1, block.length));
527
+ if (rms < 1e-4)
528
+ return 1;
529
+ const ratio = (LEVEL_TARGET[kind] * segment.amplitude) / rms;
530
+ return Math.min(2, Math.max(0.5, ratio ** (sung ? 0.85 : 0.5)));
531
+ }
532
+ function applyLevel(block, gain, previousGain, ramp, volume) {
533
+ for (let i = 0; i < block.length; i++) {
534
+ const blend = i < ramp ? i / ramp : 1;
535
+ const g = previousGain + (gain - previousGain) * blend;
536
+ const value = (block[i] ?? 0) * g * volume;
537
+ block[i] = volume > 1 ? Math.tanh(value) : value;
538
+ }
539
+ }
540
+ export function* renderChunks(plan, options = {}) {
541
+ const renderer = new FormantRenderer(plan);
542
+ const ramp = Math.max(1, Math.round(plan.sampleRate * 0.006));
543
+ const sung = plan.vibratoDepth > 0;
544
+ let previousGain = 1;
545
+ for (const segment of plan.segments) {
546
+ checkAbort(options.signal);
547
+ const block = new Float32Array(segment.frames);
548
+ renderer.fill(block);
549
+ const gain = levelGain(segment, block, sung);
550
+ applyLevel(block, gain, previousGain, ramp, plan.volume);
551
+ previousGain = gain;
552
+ yield block;
553
+ }
554
+ }
555
+ export async function renderInto(plan, sink, options = {}) {
556
+ const renderer = new FormantRenderer(plan);
557
+ const ramp = Math.max(1, Math.round(plan.sampleRate * 0.006));
558
+ const sung = plan.vibratoDepth > 0;
559
+ const step = Math.max(1024, Math.round(plan.sampleRate / 4));
560
+ let previousGain = 1;
561
+ const fillFrom = async (block, offset) => {
562
+ if (offset >= block.length)
563
+ return;
564
+ renderer.fill(block.subarray(offset, Math.min(block.length, offset + step)));
565
+ await yieldToEventLoop();
566
+ checkAbort(options.signal);
567
+ return fillFrom(block, offset + step);
568
+ };
569
+ const next = async (index) => {
570
+ const segment = plan.segments[index];
571
+ if (segment === undefined)
572
+ return;
573
+ checkAbort(options.signal);
574
+ const block = new Float32Array(segment.frames);
575
+ await fillFrom(block, 0);
576
+ const gain = levelGain(segment, block, sung);
577
+ applyLevel(block, gain, previousGain, ramp, plan.volume);
578
+ previousGain = gain;
579
+ sink(block);
580
+ return next(index + 1);
581
+ };
582
+ await next(0);
583
+ }
584
+ export function renderPlan(plan, options = {}) {
585
+ const pcm = new Float32Array(plan.frames);
586
+ let offset = 0;
587
+ for (const chunk of renderChunks(plan, options)) {
588
+ pcm.set(chunk, offset);
589
+ offset += chunk.length;
590
+ }
591
+ return pcm;
592
+ }
593
+ export function encodePlanSync(plan, options = {}) {
594
+ const out = new Uint8Array(44 + plan.frames * 2);
595
+ out.set(wavHeader(plan.frames, plan.sampleRate));
596
+ let offset = 44;
597
+ for (const chunk of renderChunks(plan, options)) {
598
+ encodePcm16(chunk, out, offset);
599
+ offset += chunk.length * 2;
600
+ }
601
+ return out;
602
+ }
603
+ export async function encodePlan(plan, options = {}) {
604
+ const out = new Uint8Array(44 + plan.frames * 2);
605
+ out.set(wavHeader(plan.frames, plan.sampleRate));
606
+ let offset = 44;
607
+ await renderInto(plan, (chunk) => {
608
+ encodePcm16(chunk, out, offset);
609
+ offset += chunk.length * 2;
610
+ }, options);
611
+ return out;
612
+ }
613
+ export function planRequest(request, sampleRate) {
614
+ return request.kind === "speech" ? planSpeech(request, sampleRate) : planSong(request, sampleRate);
615
+ }
616
+ export function renderFormantSync(request, sampleRate, options = {}) {
617
+ return encodePlanSync(planRequest(request, sampleRate), options);
618
+ }
619
+ export async function renderFormant(request, sampleRate, options = {}) {
620
+ return encodePlan(planRequest(request, sampleRate), options);
621
+ }
622
+ //# sourceMappingURL=formant.js.map