cantor-digitalis 0.0.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 +15 -0
- package/README.md +164 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1486 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/anti-resonance.d.ts +58 -0
- package/dist/nodes/anti-resonance.d.ts.map +1 -0
- package/dist/nodes/formant-bank.d.ts +76 -0
- package/dist/nodes/formant-bank.d.ts.map +1 -0
- package/dist/nodes/formant-resonator.d.ts +67 -0
- package/dist/nodes/formant-resonator.d.ts.map +1 -0
- package/dist/nodes/gain.d.ts +16 -0
- package/dist/nodes/gain.d.ts.map +1 -0
- package/dist/nodes/glottal-flow-derivative.d.ts +119 -0
- package/dist/nodes/glottal-flow-derivative.d.ts.map +1 -0
- package/dist/nodes/glottal-formant.d.ts +64 -0
- package/dist/nodes/glottal-formant.d.ts.map +1 -0
- package/dist/nodes/noise-source.d.ts +51 -0
- package/dist/nodes/noise-source.d.ts.map +1 -0
- package/dist/nodes/pulse-train.d.ts +51 -0
- package/dist/nodes/pulse-train.d.ts.map +1 -0
- package/dist/nodes/spectral-tilt.d.ts +69 -0
- package/dist/nodes/spectral-tilt.d.ts.map +1 -0
- package/dist/nodes/types.d.ts +10 -0
- package/dist/nodes/types.d.ts.map +1 -0
- package/dist/nodes/vocal-tract.d.ts +83 -0
- package/dist/nodes/vocal-tract.d.ts.map +1 -0
- package/dist/nodes/voice.d.ts +102 -0
- package/dist/nodes/voice.d.ts.map +1 -0
- package/dist/parameters/index.d.ts +111 -0
- package/dist/parameters/index.d.ts.map +1 -0
- package/dist/parameters/vowels.d.ts +28 -0
- package/dist/parameters/vowels.d.ts.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1486 @@
|
|
|
1
|
+
var lt = Object.defineProperty;
|
|
2
|
+
var ht = (a, t, e) => t in a ? lt(a, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : a[t] = e;
|
|
3
|
+
var r = (a, t, e) => ht(a, typeof t != "symbol" ? t + "" : t, e);
|
|
4
|
+
class w {
|
|
5
|
+
constructor(t, e) {
|
|
6
|
+
r(this, "ctx");
|
|
7
|
+
r(this, "gainNode");
|
|
8
|
+
r(this, "in");
|
|
9
|
+
r(this, "out");
|
|
10
|
+
this.ctx = t, this.gainNode = e, this.in = e, this.out = e;
|
|
11
|
+
}
|
|
12
|
+
static async create(t, e) {
|
|
13
|
+
const i = t.createGain();
|
|
14
|
+
return i.gain.setValueAtTime(e.gain, t.currentTime), new w(t, i);
|
|
15
|
+
}
|
|
16
|
+
update(t) {
|
|
17
|
+
this.gainNode.gain.setTargetAtTime(t.gain, this.ctx.currentTime, 0.02);
|
|
18
|
+
}
|
|
19
|
+
destroy() {
|
|
20
|
+
this.gainNode.disconnect();
|
|
21
|
+
}
|
|
22
|
+
get gain() {
|
|
23
|
+
return this.gainNode.gain;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const dt = `
|
|
27
|
+
class PulseTrainProcessor extends AudioWorkletProcessor {
|
|
28
|
+
static get parameterDescriptors() {
|
|
29
|
+
return [
|
|
30
|
+
{ name: "f0", defaultValue: 220, minValue: 20, maxValue: 20000, automationRate: "a-rate" },
|
|
31
|
+
{ name: "jitterDepth", defaultValue: 0, minValue: 0, maxValue: 0.3, automationRate: "k-rate" },
|
|
32
|
+
{ name: "shimmerDepth", defaultValue: 0, minValue: 0, maxValue: 1, automationRate: "k-rate" }
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
constructor() {
|
|
37
|
+
super();
|
|
38
|
+
// Phase accumulator (0 to 1)
|
|
39
|
+
this.phase = 0;
|
|
40
|
+
|
|
41
|
+
// Current perturbation values (updated each period)
|
|
42
|
+
this.currentJitter = 1; // Multiplier for f0 (1 = no jitter)
|
|
43
|
+
this.currentShimmer = 1; // Multiplier for amplitude (1 = no shimmer)
|
|
44
|
+
|
|
45
|
+
// Box-Muller spare for Gaussian random
|
|
46
|
+
this.hasSpare = false;
|
|
47
|
+
this.spare = 0;
|
|
48
|
+
|
|
49
|
+
// Cached harmonic count for band-limiting
|
|
50
|
+
this.harmonicCount = 0;
|
|
51
|
+
this.lastF0ForHarmonics = 220;
|
|
52
|
+
this.updateHarmonicCount(220);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update the number of harmonics to sum for band-limited impulse.
|
|
57
|
+
* We include harmonics up to Nyquist to avoid aliasing.
|
|
58
|
+
*/
|
|
59
|
+
updateHarmonicCount(f0) {
|
|
60
|
+
const nyquist = sampleRate / 2;
|
|
61
|
+
this.harmonicCount = Math.max(1, Math.floor(nyquist / f0));
|
|
62
|
+
this.lastF0ForHarmonics = f0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate a Gaussian-distributed random number using Box-Muller transform.
|
|
67
|
+
* Returns values with mean 0 and standard deviation 1.
|
|
68
|
+
*/
|
|
69
|
+
gaussianRandom() {
|
|
70
|
+
if (this.hasSpare) {
|
|
71
|
+
this.hasSpare = false;
|
|
72
|
+
return this.spare;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let u, v, s;
|
|
76
|
+
do {
|
|
77
|
+
u = Math.random() * 2 - 1;
|
|
78
|
+
v = Math.random() * 2 - 1;
|
|
79
|
+
s = u * u + v * v;
|
|
80
|
+
} while (s >= 1 || s === 0);
|
|
81
|
+
|
|
82
|
+
const mul = Math.sqrt(-2 * Math.log(s) / s);
|
|
83
|
+
this.spare = v * mul;
|
|
84
|
+
this.hasSpare = true;
|
|
85
|
+
return u * mul;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate new perturbation values at a period boundary.
|
|
90
|
+
* Jitter affects the next period's frequency.
|
|
91
|
+
* Shimmer affects the current pulse's amplitude.
|
|
92
|
+
*/
|
|
93
|
+
generatePerturbations(jitterDepth, shimmerDepth) {
|
|
94
|
+
// Jitter: Gaussian perturbation of f0, clipped to ±jitterDepth
|
|
95
|
+
// Using Gaussian makes small perturbations more likely than large ones
|
|
96
|
+
const jitterRaw = this.gaussianRandom() * jitterDepth * 0.5;
|
|
97
|
+
const jitterClamped = Math.max(-jitterDepth, Math.min(jitterDepth, jitterRaw));
|
|
98
|
+
this.currentJitter = 1 + jitterClamped;
|
|
99
|
+
|
|
100
|
+
// Shimmer: Gaussian perturbation of amplitude, clipped to [0, 2]
|
|
101
|
+
// At shimmerDepth=1, amplitude can vary from 0 to 2 (±100%)
|
|
102
|
+
const shimmerRaw = this.gaussianRandom() * shimmerDepth * 0.5;
|
|
103
|
+
const shimmerClamped = Math.max(-shimmerDepth, Math.min(shimmerDepth, shimmerRaw));
|
|
104
|
+
this.currentShimmer = 1 + shimmerClamped;
|
|
105
|
+
|
|
106
|
+
// Clamp shimmer to non-negative (amplitude can't be negative)
|
|
107
|
+
this.currentShimmer = Math.max(0, this.currentShimmer);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compute a band-limited impulse using additive synthesis.
|
|
112
|
+
* Sums cosines at all harmonic frequencies up to Nyquist.
|
|
113
|
+
*
|
|
114
|
+
* The impulse is normalized so that its peak amplitude is approximately 1.
|
|
115
|
+
*/
|
|
116
|
+
computeBandLimitedImpulse(phase) {
|
|
117
|
+
// Sum all harmonics: Σ cos(2π * k * phase) for k = 1 to harmonicCount
|
|
118
|
+
// This produces a Dirac comb approximation
|
|
119
|
+
let sum = 0;
|
|
120
|
+
const twoPiPhase = 2 * Math.PI * phase;
|
|
121
|
+
for (let k = 1; k <= this.harmonicCount; k++) {
|
|
122
|
+
sum += Math.cos(k * twoPiPhase);
|
|
123
|
+
}
|
|
124
|
+
// Normalize by harmonic count to keep amplitude reasonable
|
|
125
|
+
return sum / this.harmonicCount;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process(inputs, outputs, parameters) {
|
|
129
|
+
const output = outputs[0]?.[0];
|
|
130
|
+
if (!output) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const dt = 1 / sampleRate;
|
|
135
|
+
// k-rate parameters (single value per block)
|
|
136
|
+
const jitterDepth = parameters.jitterDepth[0];
|
|
137
|
+
const shimmerDepth = parameters.shimmerDepth[0];
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < output.length; i++) {
|
|
140
|
+
// a-rate: per-sample value for f0
|
|
141
|
+
const f0 = parameters.f0.length > 1 ? parameters.f0[i] : parameters.f0[0];
|
|
142
|
+
|
|
143
|
+
// Compute effective f0 with current jitter
|
|
144
|
+
const f0Effective = f0 * this.currentJitter;
|
|
145
|
+
|
|
146
|
+
// Advance phase
|
|
147
|
+
this.phase += f0Effective * dt;
|
|
148
|
+
|
|
149
|
+
// Check for period boundary (phase wrapped)
|
|
150
|
+
if (this.phase >= 1) {
|
|
151
|
+
this.phase -= 1;
|
|
152
|
+
// Generate new perturbations for the next period
|
|
153
|
+
this.generatePerturbations(jitterDepth, shimmerDepth);
|
|
154
|
+
// Update harmonic count if f0 changed significantly (>10%)
|
|
155
|
+
if (Math.abs(f0 - this.lastF0ForHarmonics) / this.lastF0ForHarmonics > 0.1) {
|
|
156
|
+
this.updateHarmonicCount(f0);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Generate band-limited impulse and apply shimmer
|
|
161
|
+
output[i] = this.currentShimmer * this.computeBandLimitedImpulse(this.phase);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
registerProcessor('pulse-train-processor', PulseTrainProcessor);
|
|
169
|
+
`;
|
|
170
|
+
let Q = !1;
|
|
171
|
+
async function pt(a) {
|
|
172
|
+
if (Q)
|
|
173
|
+
return;
|
|
174
|
+
const t = new Blob([dt], { type: "application/javascript" }), e = URL.createObjectURL(t);
|
|
175
|
+
try {
|
|
176
|
+
await a.audioWorklet.addModule(e), Q = !0;
|
|
177
|
+
} finally {
|
|
178
|
+
URL.revokeObjectURL(e);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
class V {
|
|
182
|
+
constructor(t, e, i) {
|
|
183
|
+
r(this, "ctx");
|
|
184
|
+
r(this, "workletNode");
|
|
185
|
+
r(this, "gain");
|
|
186
|
+
r(this, "in");
|
|
187
|
+
r(this, "out");
|
|
188
|
+
this.ctx = t, this.workletNode = e, this.gain = i, this.out = i.out, this.in = null;
|
|
189
|
+
}
|
|
190
|
+
/** Fundamental frequency AudioParam (a-rate, 20-20000 Hz) */
|
|
191
|
+
get f0() {
|
|
192
|
+
return this.workletNode.parameters.get("f0");
|
|
193
|
+
}
|
|
194
|
+
/** Jitter depth AudioParam (k-rate, 0-0.3) */
|
|
195
|
+
get jitterDepth() {
|
|
196
|
+
return this.workletNode.parameters.get("jitterDepth");
|
|
197
|
+
}
|
|
198
|
+
/** Shimmer depth AudioParam (k-rate, 0-1) */
|
|
199
|
+
get shimmerDepth() {
|
|
200
|
+
return this.workletNode.parameters.get("shimmerDepth");
|
|
201
|
+
}
|
|
202
|
+
static async create(t, e) {
|
|
203
|
+
await pt(t);
|
|
204
|
+
const i = new AudioWorkletNode(t, "pulse-train-processor"), s = await w.create(t, { gain: 0 });
|
|
205
|
+
i.connect(s.in);
|
|
206
|
+
const n = new V(t, i, s);
|
|
207
|
+
return n.update(e), n;
|
|
208
|
+
}
|
|
209
|
+
update(t) {
|
|
210
|
+
this.f0.setTargetAtTime(t.f0, this.ctx.currentTime, 0.02), this.jitterDepth.setTargetAtTime(t.jitterDepth, this.ctx.currentTime, 0.02), this.shimmerDepth.setTargetAtTime(t.shimmerDepth, this.ctx.currentTime, 0.02);
|
|
211
|
+
}
|
|
212
|
+
destroy() {
|
|
213
|
+
this.workletNode.disconnect(), this.gain.destroy();
|
|
214
|
+
}
|
|
215
|
+
start() {
|
|
216
|
+
this.gain.update({ gain: 1 });
|
|
217
|
+
}
|
|
218
|
+
stop() {
|
|
219
|
+
this.gain.update({ gain: 0 });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const mt = `
|
|
223
|
+
class GlottalFormantProcessor extends AudioWorkletProcessor {
|
|
224
|
+
static get parameterDescriptors() {
|
|
225
|
+
return [
|
|
226
|
+
{ name: "Fg", defaultValue: 110, minValue: 20, maxValue: 2000, automationRate: "a-rate" },
|
|
227
|
+
{ name: "Bg", defaultValue: 50, minValue: 10, maxValue: 500, automationRate: "a-rate" },
|
|
228
|
+
{ name: "Ag", defaultValue: 1, minValue: 0, maxValue: 10, automationRate: "a-rate" }
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
constructor() {
|
|
233
|
+
super();
|
|
234
|
+
// Filter state (delay line)
|
|
235
|
+
this.x1 = 0;
|
|
236
|
+
this.x2 = 0;
|
|
237
|
+
this.y1 = 0;
|
|
238
|
+
this.y2 = 0;
|
|
239
|
+
|
|
240
|
+
// Cached coefficient values to avoid recomputing when unchanged
|
|
241
|
+
this.lastFg = -1;
|
|
242
|
+
this.lastBg = -1;
|
|
243
|
+
this.R = 0;
|
|
244
|
+
this.cosTheta = 0;
|
|
245
|
+
this.a1 = 0;
|
|
246
|
+
this.a2 = 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
process(inputs, outputs, parameters) {
|
|
250
|
+
const input = inputs[0]?.[0];
|
|
251
|
+
const output = outputs[0]?.[0];
|
|
252
|
+
|
|
253
|
+
if (!input || !output) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const Ts = 1 / sampleRate;
|
|
258
|
+
// k-rate parameters (single value per block)
|
|
259
|
+
const Fg = parameters.Fg[0];
|
|
260
|
+
const Bg = parameters.Bg[0];
|
|
261
|
+
|
|
262
|
+
// Recompute filter coefficients if Fg or Bg changed
|
|
263
|
+
if (Fg !== this.lastFg || Bg !== this.lastBg) {
|
|
264
|
+
this.R = Math.exp(-Math.PI * Bg * Ts);
|
|
265
|
+
const theta = 2 * Math.PI * Fg * Ts;
|
|
266
|
+
this.cosTheta = Math.cos(theta);
|
|
267
|
+
this.a1 = -2 * this.R * this.cosTheta;
|
|
268
|
+
this.a2 = this.R * this.R;
|
|
269
|
+
this.lastFg = Fg;
|
|
270
|
+
this.lastBg = Bg;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (let i = 0; i < input.length; i++) {
|
|
274
|
+
// a-rate: per-sample value for Ag
|
|
275
|
+
const Ag = parameters.Ag.length > 1 ? parameters.Ag[i] : parameters.Ag[0];
|
|
276
|
+
|
|
277
|
+
const x0 = input[i];
|
|
278
|
+
|
|
279
|
+
// Numerator coefficients (feedforward) depend on Ag
|
|
280
|
+
// From: -Ag * z^{-1} * (1 - z^{-1}) = -Ag*z^{-1} + Ag*z^{-2}
|
|
281
|
+
const b1 = -Ag;
|
|
282
|
+
const b2 = Ag;
|
|
283
|
+
|
|
284
|
+
// Direct Form I biquad (b0 is always 0)
|
|
285
|
+
const y0 = b1 * this.x1 + b2 * this.x2
|
|
286
|
+
- this.a1 * this.y1 - this.a2 * this.y2;
|
|
287
|
+
|
|
288
|
+
// Update delay line
|
|
289
|
+
this.x2 = this.x1;
|
|
290
|
+
this.x1 = x0;
|
|
291
|
+
this.y2 = this.y1;
|
|
292
|
+
this.y1 = y0;
|
|
293
|
+
|
|
294
|
+
output[i] = y0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
registerProcessor('glottal-formant-processor', GlottalFormantProcessor);
|
|
302
|
+
`;
|
|
303
|
+
let z = !1;
|
|
304
|
+
async function ft(a) {
|
|
305
|
+
if (z)
|
|
306
|
+
return;
|
|
307
|
+
const t = new Blob([mt], { type: "application/javascript" }), e = URL.createObjectURL(t);
|
|
308
|
+
try {
|
|
309
|
+
await a.audioWorklet.addModule(e), z = !0;
|
|
310
|
+
} finally {
|
|
311
|
+
URL.revokeObjectURL(e);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
class D {
|
|
315
|
+
constructor(t, e) {
|
|
316
|
+
r(this, "ctx");
|
|
317
|
+
r(this, "workletNode");
|
|
318
|
+
r(this, "in");
|
|
319
|
+
r(this, "out");
|
|
320
|
+
this.ctx = t, this.workletNode = e, this.in = e, this.out = e;
|
|
321
|
+
}
|
|
322
|
+
/** Glottal formant centre frequency AudioParam (a-rate, 20-2000 Hz) */
|
|
323
|
+
get Fg() {
|
|
324
|
+
return this.workletNode.parameters.get("Fg");
|
|
325
|
+
}
|
|
326
|
+
/** Glottal formant bandwidth AudioParam (a-rate, 10-500 Hz) */
|
|
327
|
+
get Bg() {
|
|
328
|
+
return this.workletNode.parameters.get("Bg");
|
|
329
|
+
}
|
|
330
|
+
/** Source amplitude AudioParam (a-rate, 0-10) */
|
|
331
|
+
get Ag() {
|
|
332
|
+
return this.workletNode.parameters.get("Ag");
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Creates a new GlottalFormant node.
|
|
336
|
+
*
|
|
337
|
+
* The AudioWorklet module is registered automatically on first use.
|
|
338
|
+
*/
|
|
339
|
+
static async create(t, e) {
|
|
340
|
+
await ft(t);
|
|
341
|
+
const i = new AudioWorkletNode(t, "glottal-formant-processor"), s = new D(t, i);
|
|
342
|
+
return s.update(e), s;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Updates the glottal formant parameters.
|
|
346
|
+
* Sets AudioParams via setTargetAtTime for smooth transitions.
|
|
347
|
+
*/
|
|
348
|
+
update(t) {
|
|
349
|
+
this.Fg.setTargetAtTime(t.Fg, this.ctx.currentTime, 0.02), this.Bg.setTargetAtTime(t.Bg, this.ctx.currentTime, 0.02), this.Ag.setTargetAtTime(t.Ag, this.ctx.currentTime, 0.02);
|
|
350
|
+
}
|
|
351
|
+
destroy() {
|
|
352
|
+
this.workletNode.disconnect();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const gt = `
|
|
356
|
+
class SpectralTiltProcessor extends AudioWorkletProcessor {
|
|
357
|
+
static get parameterDescriptors() {
|
|
358
|
+
return [
|
|
359
|
+
{ name: "Tl1", defaultValue: 0, minValue: 0, maxValue: 50, automationRate: "k-rate" },
|
|
360
|
+
{ name: "Tl2", defaultValue: 0, minValue: 0, maxValue: 30, automationRate: "k-rate" }
|
|
361
|
+
];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
constructor() {
|
|
365
|
+
super();
|
|
366
|
+
// Filter coefficients for two stages
|
|
367
|
+
this.a1 = 0; // First stage pole
|
|
368
|
+
this.a2 = 0; // Second stage pole
|
|
369
|
+
this.g1 = 1; // First stage gain (1 - a1)
|
|
370
|
+
this.g2 = 1; // Second stage gain (1 - a2)
|
|
371
|
+
|
|
372
|
+
// Filter state (one delay element per stage)
|
|
373
|
+
this.y1_prev = 0; // Previous output of stage 1
|
|
374
|
+
this.y2_prev = 0; // Previous output of stage 2
|
|
375
|
+
|
|
376
|
+
// Cache last parameter values for change detection
|
|
377
|
+
this.lastTl1 = -1;
|
|
378
|
+
this.lastTl2 = -1;
|
|
379
|
+
|
|
380
|
+
// Pre-compute cosOmega since it's constant for a given sample rate
|
|
381
|
+
const Ts = 1 / sampleRate;
|
|
382
|
+
this.cosOmega = Math.cos(2 * Math.PI * 3000 * Ts);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Compute the pole coefficient for a single stage.
|
|
387
|
+
* Returns [a, g] where a is the pole and g = 1 - a is the gain.
|
|
388
|
+
*/
|
|
389
|
+
computeStageCoefficients(Tl) {
|
|
390
|
+
// Bypass if attenuation is zero or negative
|
|
391
|
+
if (Tl <= 0) {
|
|
392
|
+
return [0, 1];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Power ratio at 3000 Hz (10^(Tl/10) is the attenuation factor)
|
|
396
|
+
const attenuationRatio = Math.pow(10, Tl / 10);
|
|
397
|
+
|
|
398
|
+
// Compute ν from Section 3.2.2
|
|
399
|
+
// νᵢ = 1 + (1 - cos(ω)) / (10^(Tlᵢ/10) - 1)
|
|
400
|
+
const nu = 1 + (1 - this.cosOmega) / (attenuationRatio - 1);
|
|
401
|
+
|
|
402
|
+
// Compute pole coefficient: a = ν - √(ν² - 1)
|
|
403
|
+
// This ensures 0 < a < 1 for stability
|
|
404
|
+
const a = nu - Math.sqrt(nu * nu - 1);
|
|
405
|
+
const g = 1 - a;
|
|
406
|
+
|
|
407
|
+
return [a, g];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
process(inputs, outputs, parameters) {
|
|
411
|
+
const input = inputs[0]?.[0];
|
|
412
|
+
const output = outputs[0]?.[0];
|
|
413
|
+
|
|
414
|
+
if (!input || !output) {
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// k-rate parameters (single value per block)
|
|
419
|
+
const Tl1 = parameters.Tl1[0];
|
|
420
|
+
const Tl2 = parameters.Tl2[0];
|
|
421
|
+
|
|
422
|
+
// Recompute coefficients if parameters changed
|
|
423
|
+
if (Tl1 !== this.lastTl1) {
|
|
424
|
+
[this.a1, this.g1] = this.computeStageCoefficients(Tl1);
|
|
425
|
+
this.lastTl1 = Tl1;
|
|
426
|
+
}
|
|
427
|
+
if (Tl2 !== this.lastTl2) {
|
|
428
|
+
[this.a2, this.g2] = this.computeStageCoefficients(Tl2);
|
|
429
|
+
this.lastTl2 = Tl2;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
for (let i = 0; i < input.length; i++) {
|
|
433
|
+
// Stage 1: y1[n] = g1 * x[n] + a1 * y1[n-1]
|
|
434
|
+
const y1 = this.g1 * input[i] + this.a1 * this.y1_prev;
|
|
435
|
+
this.y1_prev = y1;
|
|
436
|
+
|
|
437
|
+
// Stage 2: y2[n] = g2 * y1[n] + a2 * y2[n-1]
|
|
438
|
+
const y2 = this.g2 * y1 + this.a2 * this.y2_prev;
|
|
439
|
+
this.y2_prev = y2;
|
|
440
|
+
|
|
441
|
+
output[i] = y2;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
registerProcessor('spectral-tilt-processor', SpectralTiltProcessor);
|
|
449
|
+
`;
|
|
450
|
+
let O = !1;
|
|
451
|
+
async function yt(a) {
|
|
452
|
+
if (O)
|
|
453
|
+
return;
|
|
454
|
+
const t = new Blob([gt], { type: "application/javascript" }), e = URL.createObjectURL(t);
|
|
455
|
+
try {
|
|
456
|
+
await a.audioWorklet.addModule(e), O = !0;
|
|
457
|
+
} finally {
|
|
458
|
+
URL.revokeObjectURL(e);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
class N {
|
|
462
|
+
constructor(t, e) {
|
|
463
|
+
r(this, "ctx");
|
|
464
|
+
r(this, "workletNode");
|
|
465
|
+
r(this, "in");
|
|
466
|
+
r(this, "out");
|
|
467
|
+
this.ctx = t, this.workletNode = e, this.in = e, this.out = e;
|
|
468
|
+
}
|
|
469
|
+
/** First stage spectral tilt attenuation AudioParam (k-rate, 0-50 dB at 3kHz) */
|
|
470
|
+
get Tl1() {
|
|
471
|
+
return this.workletNode.parameters.get("Tl1");
|
|
472
|
+
}
|
|
473
|
+
/** Second stage spectral tilt attenuation AudioParam (k-rate, 0-30 dB at 3kHz) */
|
|
474
|
+
get Tl2() {
|
|
475
|
+
return this.workletNode.parameters.get("Tl2");
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Creates a new SpectralTilt node.
|
|
479
|
+
*
|
|
480
|
+
* The AudioWorklet module is registered automatically on first use.
|
|
481
|
+
*/
|
|
482
|
+
static async create(t, e) {
|
|
483
|
+
await yt(t);
|
|
484
|
+
const i = new AudioWorkletNode(t, "spectral-tilt-processor"), s = new N(t, i);
|
|
485
|
+
return s.update(e), s;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Updates the spectral tilt parameters.
|
|
489
|
+
* Sets AudioParams via setTargetAtTime for smooth transitions.
|
|
490
|
+
*/
|
|
491
|
+
update(t) {
|
|
492
|
+
this.Tl1.setTargetAtTime(t.Tl1, this.ctx.currentTime, 0.02), this.Tl2.setTargetAtTime(t.Tl2, this.ctx.currentTime, 0.02);
|
|
493
|
+
}
|
|
494
|
+
destroy() {
|
|
495
|
+
this.workletNode.disconnect();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const wt = `
|
|
499
|
+
class NoiseSourceProcessor extends AudioWorkletProcessor {
|
|
500
|
+
static get parameterDescriptors() {
|
|
501
|
+
return [
|
|
502
|
+
{ name: "An", defaultValue: 0, minValue: 0, maxValue: 1, automationRate: "a-rate" }
|
|
503
|
+
];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
constructor() {
|
|
507
|
+
super();
|
|
508
|
+
// Box-Muller spare value
|
|
509
|
+
this.hasSpare = false;
|
|
510
|
+
this.spare = 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Generate a Gaussian-distributed random number using Box-Muller transform.
|
|
515
|
+
* Returns values with mean 0 and standard deviation 1.
|
|
516
|
+
*/
|
|
517
|
+
gaussianRandom() {
|
|
518
|
+
if (this.hasSpare) {
|
|
519
|
+
this.hasSpare = false;
|
|
520
|
+
return this.spare;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Generate two uniform random numbers in (0, 1)
|
|
524
|
+
let u, v, s;
|
|
525
|
+
do {
|
|
526
|
+
u = Math.random() * 2 - 1;
|
|
527
|
+
v = Math.random() * 2 - 1;
|
|
528
|
+
s = u * u + v * v;
|
|
529
|
+
} while (s >= 1 || s === 0);
|
|
530
|
+
|
|
531
|
+
const mul = Math.sqrt(-2 * Math.log(s) / s);
|
|
532
|
+
this.spare = v * mul;
|
|
533
|
+
this.hasSpare = true;
|
|
534
|
+
return u * mul;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
process(inputs, outputs, parameters) {
|
|
538
|
+
const output = outputs[0]?.[0];
|
|
539
|
+
|
|
540
|
+
if (!output) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
for (let i = 0; i < output.length; i++) {
|
|
545
|
+
// a-rate: per-sample value for An
|
|
546
|
+
const An = parameters.An.length > 1 ? parameters.An[i] : parameters.An[0];
|
|
547
|
+
output[i] = An * this.gaussianRandom();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
registerProcessor('noise-source-processor', NoiseSourceProcessor);
|
|
555
|
+
`;
|
|
556
|
+
let W = !1;
|
|
557
|
+
async function bt(a) {
|
|
558
|
+
if (W)
|
|
559
|
+
return;
|
|
560
|
+
const t = new Blob([wt], { type: "application/javascript" }), e = URL.createObjectURL(t);
|
|
561
|
+
try {
|
|
562
|
+
await a.audioWorklet.addModule(e), W = !0;
|
|
563
|
+
} finally {
|
|
564
|
+
URL.revokeObjectURL(e);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
class P {
|
|
568
|
+
constructor(t, e, i, s) {
|
|
569
|
+
r(this, "ctx");
|
|
570
|
+
r(this, "workletNode");
|
|
571
|
+
r(this, "highpassFilter");
|
|
572
|
+
r(this, "lowpassFilter");
|
|
573
|
+
r(this, "in", null);
|
|
574
|
+
// Noise source has no input
|
|
575
|
+
r(this, "out");
|
|
576
|
+
this.ctx = t, this.workletNode = e, this.highpassFilter = i, this.lowpassFilter = s, this.out = s;
|
|
577
|
+
}
|
|
578
|
+
/** Noise amplitude AudioParam (a-rate, 0-1) */
|
|
579
|
+
get An() {
|
|
580
|
+
return this.workletNode.parameters.get("An");
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Creates a new NoiseSource node.
|
|
584
|
+
*
|
|
585
|
+
* The AudioWorklet module is registered automatically on first use.
|
|
586
|
+
*/
|
|
587
|
+
static async create(t, e) {
|
|
588
|
+
await bt(t);
|
|
589
|
+
const i = new AudioWorkletNode(t, "noise-source-processor"), s = t.createBiquadFilter();
|
|
590
|
+
s.type = "highpass", s.frequency.setValueAtTime(1e3, t.currentTime), s.Q.setValueAtTime(Math.SQRT1_2, t.currentTime);
|
|
591
|
+
const n = t.createBiquadFilter();
|
|
592
|
+
n.type = "lowpass", n.frequency.setValueAtTime(6e3, t.currentTime), n.Q.setValueAtTime(Math.SQRT1_2, t.currentTime), i.connect(s), s.connect(n);
|
|
593
|
+
const o = new P(t, i, s, n);
|
|
594
|
+
return o.update(e), o;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Updates the noise source amplitude.
|
|
598
|
+
* Sets AudioParam via setTargetAtTime for smooth transitions.
|
|
599
|
+
*/
|
|
600
|
+
update(t) {
|
|
601
|
+
this.An.setTargetAtTime(t.An, this.ctx.currentTime, 0.02);
|
|
602
|
+
}
|
|
603
|
+
destroy() {
|
|
604
|
+
this.workletNode.disconnect(), this.highpassFilter.disconnect(), this.lowpassFilter.disconnect();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
class S {
|
|
608
|
+
constructor(t, e, i, s, n, o) {
|
|
609
|
+
r(this, "pulseTrain");
|
|
610
|
+
r(this, "glottalFormant");
|
|
611
|
+
r(this, "spectralTilt");
|
|
612
|
+
r(this, "noiseSource");
|
|
613
|
+
r(this, "noiseModulator");
|
|
614
|
+
// Multiplies noise by GFD signal
|
|
615
|
+
r(this, "outputGain");
|
|
616
|
+
r(this, "in", null);
|
|
617
|
+
// Source node has no input
|
|
618
|
+
r(this, "out");
|
|
619
|
+
this.pulseTrain = t, this.glottalFormant = e, this.spectralTilt = i, this.noiseSource = s, this.noiseModulator = n, this.outputGain = o, this.out = o.out;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Creates a new GlottalFlowDerivative node.
|
|
623
|
+
*
|
|
624
|
+
* Sets up the voiced path (pulse train → glottal formant → spectral tilt)
|
|
625
|
+
* and the noise modulation path where noise is multiplied by the GFD signal.
|
|
626
|
+
*/
|
|
627
|
+
static async create(t, e) {
|
|
628
|
+
const [i, s, n, o, c] = await Promise.all([
|
|
629
|
+
V.create(t, {
|
|
630
|
+
f0: e.f0,
|
|
631
|
+
jitterDepth: e.jitterDepth,
|
|
632
|
+
shimmerDepth: e.shimmerDepth
|
|
633
|
+
}),
|
|
634
|
+
D.create(t, { Fg: e.Fg, Bg: e.Bg, Ag: e.Ag }),
|
|
635
|
+
N.create(t, { Tl1: e.Tl1, Tl2: e.Tl2 }),
|
|
636
|
+
P.create(t, { An: e.An }),
|
|
637
|
+
w.create(t, { gain: 1 })
|
|
638
|
+
]), l = t.createGain();
|
|
639
|
+
return l.gain.setValueAtTime(0, t.currentTime), i.out.connect(s.in), s.out.connect(n.in), n.out.connect(c.in), n.out.connect(l.gain), o.out.connect(l), l.connect(c.in), new S(
|
|
640
|
+
i,
|
|
641
|
+
s,
|
|
642
|
+
n,
|
|
643
|
+
o,
|
|
644
|
+
l,
|
|
645
|
+
c
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Updates all glottal flow derivative parameters.
|
|
650
|
+
* Each sub-node receives its relevant parameters.
|
|
651
|
+
*/
|
|
652
|
+
update(t) {
|
|
653
|
+
this.pulseTrain.update({
|
|
654
|
+
f0: t.f0,
|
|
655
|
+
jitterDepth: t.jitterDepth,
|
|
656
|
+
shimmerDepth: t.shimmerDepth
|
|
657
|
+
}), this.glottalFormant.update({ Fg: t.Fg, Bg: t.Bg, Ag: t.Ag }), this.spectralTilt.update({ Tl1: t.Tl1, Tl2: t.Tl2 }), this.noiseSource.update({ An: t.An });
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Returns the pulse train node for direct AudioParam access.
|
|
661
|
+
*
|
|
662
|
+
* Example usage:
|
|
663
|
+
* gfd.pulseTrainNode.f0.setValueAtTime(440, ctx.currentTime);
|
|
664
|
+
* gfd.pulseTrainNode.jitterDepth.linearRampToValueAtTime(0.1, ctx.currentTime + 0.5);
|
|
665
|
+
*/
|
|
666
|
+
get pulseTrainNode() {
|
|
667
|
+
return this.pulseTrain;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Returns the glottal formant node for direct AudioParam access.
|
|
671
|
+
*
|
|
672
|
+
* Example usage:
|
|
673
|
+
* gfd.glottalFormantNode.Fg.setValueAtTime(220, ctx.currentTime);
|
|
674
|
+
* gfd.glottalFormantNode.Ag.setTargetAtTime(0.5, ctx.currentTime, 0.1);
|
|
675
|
+
*/
|
|
676
|
+
get glottalFormantNode() {
|
|
677
|
+
return this.glottalFormant;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Returns the spectral tilt node for direct AudioParam access.
|
|
681
|
+
*
|
|
682
|
+
* Example usage:
|
|
683
|
+
* gfd.spectralTiltNode.Tl1.setValueAtTime(10, ctx.currentTime);
|
|
684
|
+
*/
|
|
685
|
+
get spectralTiltNode() {
|
|
686
|
+
return this.spectralTilt;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Returns the noise source node for direct AudioParam access.
|
|
690
|
+
*
|
|
691
|
+
* Example usage:
|
|
692
|
+
* gfd.noiseSourceNode.An.setValueAtTime(0.3, ctx.currentTime);
|
|
693
|
+
*/
|
|
694
|
+
get noiseSourceNode() {
|
|
695
|
+
return this.noiseSource;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Starts the voiced excitation (pulse train).
|
|
699
|
+
*/
|
|
700
|
+
start() {
|
|
701
|
+
this.pulseTrain.start();
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Stops the voiced excitation (pulse train).
|
|
705
|
+
* Note: Noise source continues to produce output based on An parameter.
|
|
706
|
+
*/
|
|
707
|
+
stop() {
|
|
708
|
+
this.pulseTrain.stop();
|
|
709
|
+
}
|
|
710
|
+
destroy() {
|
|
711
|
+
this.pulseTrain.destroy(), this.glottalFormant.destroy(), this.spectralTilt.destroy(), this.noiseSource.destroy(), this.noiseModulator.disconnect(), this.outputGain.destroy();
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const Tt = `
|
|
715
|
+
class FormantResonatorProcessor extends AudioWorkletProcessor {
|
|
716
|
+
static get parameterDescriptors() {
|
|
717
|
+
return [
|
|
718
|
+
{ name: "F", defaultValue: 500, minValue: 100, maxValue: 8000, automationRate: "a-rate" },
|
|
719
|
+
{ name: "B", defaultValue: 100, minValue: 20, maxValue: 1000, automationRate: "a-rate" },
|
|
720
|
+
{ name: "A", defaultValue: 1, minValue: 0, maxValue: 10, automationRate: "a-rate" }
|
|
721
|
+
];
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
constructor() {
|
|
725
|
+
super();
|
|
726
|
+
// Filter state (delay line)
|
|
727
|
+
this.x1 = 0;
|
|
728
|
+
this.x2 = 0;
|
|
729
|
+
this.y1 = 0;
|
|
730
|
+
this.y2 = 0;
|
|
731
|
+
|
|
732
|
+
// Cached parameter values for change detection
|
|
733
|
+
this.lastF = -1;
|
|
734
|
+
this.lastB = -1;
|
|
735
|
+
this.lastA = -1;
|
|
736
|
+
|
|
737
|
+
// Cached coefficients
|
|
738
|
+
this.b0 = 0;
|
|
739
|
+
this.b2 = 0;
|
|
740
|
+
this.a1 = 0;
|
|
741
|
+
this.a2 = 0;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
process(inputs, outputs, parameters) {
|
|
745
|
+
const input = inputs[0]?.[0];
|
|
746
|
+
const output = outputs[0]?.[0];
|
|
747
|
+
|
|
748
|
+
if (!input || !output) {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// a-rate parameters (per-sample values)
|
|
753
|
+
const F = parameters.F[0];
|
|
754
|
+
const B = parameters.B[0];
|
|
755
|
+
const A = parameters.A[0];
|
|
756
|
+
|
|
757
|
+
// Recompute coefficients if any parameter changed
|
|
758
|
+
if (F !== this.lastF || B !== this.lastB || A !== this.lastA) {
|
|
759
|
+
const Ts = 1 / sampleRate;
|
|
760
|
+
|
|
761
|
+
// Pole radius and angle
|
|
762
|
+
const R = Math.exp(-Math.PI * B * Ts);
|
|
763
|
+
const theta = 2 * Math.PI * F * Ts;
|
|
764
|
+
|
|
765
|
+
// Gain scaling factor
|
|
766
|
+
const g = 1 - R;
|
|
767
|
+
|
|
768
|
+
// Numerator coefficients (feedforward)
|
|
769
|
+
// From: A * g * (1 - R*z^{-2}) = A*g + 0*z^{-1} - A*g*R*z^{-2}
|
|
770
|
+
this.b0 = A * g;
|
|
771
|
+
this.b2 = -A * g * R;
|
|
772
|
+
|
|
773
|
+
// Denominator coefficients (feedback)
|
|
774
|
+
// From: 1 - 2R*cos(θ)*z^{-1} + R²*z^{-2}
|
|
775
|
+
this.a1 = -2 * R * Math.cos(theta);
|
|
776
|
+
this.a2 = R * R;
|
|
777
|
+
|
|
778
|
+
this.lastF = F;
|
|
779
|
+
this.lastB = B;
|
|
780
|
+
this.lastA = A;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
for (let i = 0; i < input.length; i++) {
|
|
784
|
+
const x0 = input[i];
|
|
785
|
+
|
|
786
|
+
// Direct Form I biquad (b1 is always 0)
|
|
787
|
+
const y0 = this.b0 * x0 + this.b2 * this.x2
|
|
788
|
+
- this.a1 * this.y1 - this.a2 * this.y2;
|
|
789
|
+
|
|
790
|
+
// Update delay line
|
|
791
|
+
this.x2 = this.x1;
|
|
792
|
+
this.x1 = x0;
|
|
793
|
+
this.y2 = this.y1;
|
|
794
|
+
this.y1 = y0;
|
|
795
|
+
|
|
796
|
+
output[i] = y0;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
registerProcessor('formant-resonator-processor', FormantResonatorProcessor);
|
|
804
|
+
`;
|
|
805
|
+
let $ = !1;
|
|
806
|
+
async function Ft(a) {
|
|
807
|
+
if ($)
|
|
808
|
+
return;
|
|
809
|
+
const t = new Blob([Tt], { type: "application/javascript" }), e = URL.createObjectURL(t);
|
|
810
|
+
try {
|
|
811
|
+
await a.audioWorklet.addModule(e), $ = !0;
|
|
812
|
+
} finally {
|
|
813
|
+
URL.revokeObjectURL(e);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
class C {
|
|
817
|
+
constructor(t, e) {
|
|
818
|
+
r(this, "ctx");
|
|
819
|
+
r(this, "workletNode");
|
|
820
|
+
r(this, "in");
|
|
821
|
+
r(this, "out");
|
|
822
|
+
this.ctx = t, this.workletNode = e, this.in = e, this.out = e;
|
|
823
|
+
}
|
|
824
|
+
/** Formant centre frequency AudioParam (a-rate, 100-8000 Hz) */
|
|
825
|
+
get F() {
|
|
826
|
+
return this.workletNode.parameters.get("F");
|
|
827
|
+
}
|
|
828
|
+
/** Formant bandwidth AudioParam (a-rate, 20-1000 Hz) */
|
|
829
|
+
get B() {
|
|
830
|
+
return this.workletNode.parameters.get("B");
|
|
831
|
+
}
|
|
832
|
+
/** Formant amplitude AudioParam (a-rate, 0-10) */
|
|
833
|
+
get A() {
|
|
834
|
+
return this.workletNode.parameters.get("A");
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Creates a new FormantResonator node.
|
|
838
|
+
*
|
|
839
|
+
* The AudioWorklet module is registered automatically on first use.
|
|
840
|
+
*/
|
|
841
|
+
static async create(t, e) {
|
|
842
|
+
await Ft(t);
|
|
843
|
+
const i = new AudioWorkletNode(t, "formant-resonator-processor"), s = new C(t, i);
|
|
844
|
+
return s.update(e), s;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Updates the formant resonator parameters.
|
|
848
|
+
* Sets AudioParams via setTargetAtTime for smooth transitions.
|
|
849
|
+
*/
|
|
850
|
+
update(t) {
|
|
851
|
+
this.F.setTargetAtTime(t.F, this.ctx.currentTime, 0.02), this.B.setTargetAtTime(t.B, this.ctx.currentTime, 0.02), this.A.setTargetAtTime(t.A, this.ctx.currentTime, 0.02);
|
|
852
|
+
}
|
|
853
|
+
destroy() {
|
|
854
|
+
this.workletNode.disconnect();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
class j {
|
|
858
|
+
constructor(t, e, i) {
|
|
859
|
+
r(this, "resonators");
|
|
860
|
+
r(this, "inputGain");
|
|
861
|
+
r(this, "outputGain");
|
|
862
|
+
r(this, "in");
|
|
863
|
+
r(this, "out");
|
|
864
|
+
this.resonators = t, this.inputGain = e, this.outputGain = i, this.in = e, this.out = i;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Creates a new FormantBank node with the specified formants.
|
|
868
|
+
*
|
|
869
|
+
* All formant resonators are created in parallel for efficiency.
|
|
870
|
+
*/
|
|
871
|
+
static async create(t, e) {
|
|
872
|
+
const i = t.createGain();
|
|
873
|
+
i.gain.setValueAtTime(1, t.currentTime);
|
|
874
|
+
const s = t.createGain();
|
|
875
|
+
s.gain.setValueAtTime(1, t.currentTime);
|
|
876
|
+
const n = await Promise.all(
|
|
877
|
+
e.formants.map((o) => C.create(t, o))
|
|
878
|
+
);
|
|
879
|
+
for (const o of n)
|
|
880
|
+
i.connect(o.in), o.out.connect(s);
|
|
881
|
+
return new j(n, i, s);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Updates all formant parameters.
|
|
885
|
+
*
|
|
886
|
+
* The formants array must have the same length as the original.
|
|
887
|
+
* Each resonator receives its corresponding parameters.
|
|
888
|
+
*
|
|
889
|
+
* @throws Error if the formants array length doesn't match
|
|
890
|
+
*/
|
|
891
|
+
update(t) {
|
|
892
|
+
if (t.formants.length !== this.resonators.length)
|
|
893
|
+
throw new Error(
|
|
894
|
+
`FormantBank: Cannot change number of formants. Expected ${this.resonators.length}, got ${t.formants.length}. Destroy and recreate the node to change formant count.`
|
|
895
|
+
);
|
|
896
|
+
for (let e = 0; e < this.resonators.length; e++)
|
|
897
|
+
this.resonators[e].update(t.formants[e]);
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Returns the number of formant resonators in this bank.
|
|
901
|
+
*/
|
|
902
|
+
get count() {
|
|
903
|
+
return this.resonators.length;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Returns the array of formant resonators for direct AudioParam access.
|
|
907
|
+
*
|
|
908
|
+
* Example usage:
|
|
909
|
+
* formantBank.formants[0].F.setValueAtTime(800, ctx.currentTime);
|
|
910
|
+
* formantBank.formants[1].B.linearRampToValueAtTime(100, ctx.currentTime + 0.1);
|
|
911
|
+
*/
|
|
912
|
+
get formants() {
|
|
913
|
+
return this.resonators;
|
|
914
|
+
}
|
|
915
|
+
destroy() {
|
|
916
|
+
this.inputGain.disconnect(), this.outputGain.disconnect();
|
|
917
|
+
for (const t of this.resonators)
|
|
918
|
+
t.destroy();
|
|
919
|
+
this.resonators = [];
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const Rt = `
|
|
923
|
+
class AntiResonanceProcessor extends AudioWorkletProcessor {
|
|
924
|
+
static get parameterDescriptors() {
|
|
925
|
+
return [
|
|
926
|
+
{ name: "F", defaultValue: 4700, minValue: 1000, maxValue: 10000, automationRate: "k-rate" },
|
|
927
|
+
{ name: "Q", defaultValue: 2.5, minValue: 0.5, maxValue: 20, automationRate: "k-rate" }
|
|
928
|
+
];
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
constructor() {
|
|
932
|
+
super();
|
|
933
|
+
// Filter state (delay line)
|
|
934
|
+
this.x1 = 0;
|
|
935
|
+
this.x2 = 0;
|
|
936
|
+
this.y1 = 0;
|
|
937
|
+
this.y2 = 0;
|
|
938
|
+
|
|
939
|
+
// Cached parameter values for change detection
|
|
940
|
+
this.lastF = -1;
|
|
941
|
+
this.lastQ = -1;
|
|
942
|
+
|
|
943
|
+
// Cached coefficients
|
|
944
|
+
this.b0 = 1;
|
|
945
|
+
this.b1 = 0;
|
|
946
|
+
this.b2 = 1;
|
|
947
|
+
this.a1 = 0;
|
|
948
|
+
this.a2 = 0;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
process(inputs, outputs, parameters) {
|
|
952
|
+
const input = inputs[0]?.[0];
|
|
953
|
+
const output = outputs[0]?.[0];
|
|
954
|
+
|
|
955
|
+
if (!input || !output) {
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// k-rate parameters (single value per block)
|
|
960
|
+
const F = parameters.F[0];
|
|
961
|
+
const Q = parameters.Q[0];
|
|
962
|
+
|
|
963
|
+
// Recompute coefficients if any parameter changed
|
|
964
|
+
if (F !== this.lastF || Q !== this.lastQ) {
|
|
965
|
+
const Ts = 1 / sampleRate;
|
|
966
|
+
const omega = 2 * Math.PI * F * Ts;
|
|
967
|
+
|
|
968
|
+
// Intermediate coefficients from the paper
|
|
969
|
+
const alpha = Math.sin(omega) / (2 * Q);
|
|
970
|
+
const beta = -2 * Math.cos(omega);
|
|
971
|
+
|
|
972
|
+
// Normalize by dividing by (1 + alpha) so that a0 = 1
|
|
973
|
+
const a0 = 1 + alpha;
|
|
974
|
+
|
|
975
|
+
// Numerator coefficients (feedforward)
|
|
976
|
+
// From: (1 + β*z⁻¹ + z⁻²) / a0
|
|
977
|
+
this.b0 = 1 / a0;
|
|
978
|
+
this.b1 = beta / a0;
|
|
979
|
+
this.b2 = 1 / a0;
|
|
980
|
+
|
|
981
|
+
// Denominator coefficients (feedback), normalized
|
|
982
|
+
// From: ((1 + α) + β*z⁻¹ + (1 - α)*z⁻²) / a0
|
|
983
|
+
// a0/a0 = 1 (implicit), a1 = β/a0, a2 = (1 - α)/a0
|
|
984
|
+
this.a1 = beta / a0;
|
|
985
|
+
this.a2 = (1 - alpha) / a0;
|
|
986
|
+
|
|
987
|
+
this.lastF = F;
|
|
988
|
+
this.lastQ = Q;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
for (let i = 0; i < input.length; i++) {
|
|
992
|
+
const x0 = input[i];
|
|
993
|
+
|
|
994
|
+
// Direct Form I biquad
|
|
995
|
+
const y0 = this.b0 * x0 + this.b1 * this.x1 + this.b2 * this.x2
|
|
996
|
+
- this.a1 * this.y1 - this.a2 * this.y2;
|
|
997
|
+
|
|
998
|
+
// Update delay line
|
|
999
|
+
this.x2 = this.x1;
|
|
1000
|
+
this.x1 = x0;
|
|
1001
|
+
this.y2 = this.y1;
|
|
1002
|
+
this.y1 = y0;
|
|
1003
|
+
|
|
1004
|
+
output[i] = y0;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
registerProcessor('anti-resonance-processor', AntiResonanceProcessor);
|
|
1012
|
+
`;
|
|
1013
|
+
let I = !1;
|
|
1014
|
+
async function At(a) {
|
|
1015
|
+
if (I)
|
|
1016
|
+
return;
|
|
1017
|
+
const t = new Blob([Rt], { type: "application/javascript" }), e = URL.createObjectURL(t);
|
|
1018
|
+
try {
|
|
1019
|
+
await a.audioWorklet.addModule(e), I = !0;
|
|
1020
|
+
} finally {
|
|
1021
|
+
URL.revokeObjectURL(e);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
class G {
|
|
1025
|
+
constructor(t, e) {
|
|
1026
|
+
r(this, "ctx");
|
|
1027
|
+
r(this, "workletNode");
|
|
1028
|
+
r(this, "in");
|
|
1029
|
+
r(this, "out");
|
|
1030
|
+
this.ctx = t, this.workletNode = e, this.in = e, this.out = e;
|
|
1031
|
+
}
|
|
1032
|
+
/** Anti-formant centre frequency AudioParam (k-rate, 1000-10000 Hz) */
|
|
1033
|
+
get F() {
|
|
1034
|
+
return this.workletNode.parameters.get("F");
|
|
1035
|
+
}
|
|
1036
|
+
/** Anti-formant quality factor AudioParam (k-rate, 0.5-20) */
|
|
1037
|
+
get Q() {
|
|
1038
|
+
return this.workletNode.parameters.get("Q");
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Creates a new AntiResonance node.
|
|
1042
|
+
*
|
|
1043
|
+
* The AudioWorklet module is registered automatically on first use.
|
|
1044
|
+
*/
|
|
1045
|
+
static async create(t, e) {
|
|
1046
|
+
await At(t);
|
|
1047
|
+
const i = new AudioWorkletNode(t, "anti-resonance-processor"), s = new G(t, i);
|
|
1048
|
+
return s.update(e), s;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Updates the anti-resonance parameters.
|
|
1052
|
+
* Sets AudioParams via setTargetAtTime for smooth transitions.
|
|
1053
|
+
*/
|
|
1054
|
+
update(t) {
|
|
1055
|
+
this.F.setTargetAtTime(t.F, this.ctx.currentTime, 0.02), this.Q.setTargetAtTime(t.Q, this.ctx.currentTime, 0.02);
|
|
1056
|
+
}
|
|
1057
|
+
destroy() {
|
|
1058
|
+
this.workletNode.disconnect();
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
class L {
|
|
1062
|
+
constructor(t, e) {
|
|
1063
|
+
r(this, "formantBank");
|
|
1064
|
+
r(this, "antiResonance");
|
|
1065
|
+
r(this, "in");
|
|
1066
|
+
r(this, "out");
|
|
1067
|
+
this.formantBank = t, this.antiResonance = e, this.in = t.in, this.out = e.out;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Creates a new VocalTract node.
|
|
1071
|
+
*
|
|
1072
|
+
* Sets up the parallel formant bank followed by the anti-resonance filter.
|
|
1073
|
+
*/
|
|
1074
|
+
static async create(t, e) {
|
|
1075
|
+
const [i, s] = await Promise.all([
|
|
1076
|
+
j.create(t, { formants: e.formants }),
|
|
1077
|
+
G.create(t, { F: e.F_BQ, Q: e.Q_BQ })
|
|
1078
|
+
]);
|
|
1079
|
+
return i.out.connect(s.in), new L(i, s);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Updates all vocal tract parameters.
|
|
1083
|
+
*
|
|
1084
|
+
* @throws Error if the formants array length doesn't match the original
|
|
1085
|
+
*/
|
|
1086
|
+
update(t) {
|
|
1087
|
+
this.formantBank.update({ formants: t.formants }), this.antiResonance.update({ F: t.F_BQ, Q: t.Q_BQ });
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Returns the number of formant resonators in the vocal tract.
|
|
1091
|
+
*/
|
|
1092
|
+
get formantCount() {
|
|
1093
|
+
return this.formantBank.count;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Returns the array of formant resonators for direct AudioParam access.
|
|
1097
|
+
*
|
|
1098
|
+
* Example usage:
|
|
1099
|
+
* vocalTract.formants[0].F.setValueAtTime(800, ctx.currentTime);
|
|
1100
|
+
* vocalTract.formants[2].B.linearRampToValueAtTime(100, ctx.currentTime + 0.1);
|
|
1101
|
+
*/
|
|
1102
|
+
get formants() {
|
|
1103
|
+
return this.formantBank.formants;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Returns the anti-resonance node for direct AudioParam access.
|
|
1107
|
+
*
|
|
1108
|
+
* Example usage:
|
|
1109
|
+
* vocalTract.antiResonanceNode.F.setValueAtTime(4500, ctx.currentTime);
|
|
1110
|
+
* vocalTract.antiResonanceNode.Q.setValueAtTime(3, ctx.currentTime);
|
|
1111
|
+
*/
|
|
1112
|
+
get antiResonanceNode() {
|
|
1113
|
+
return this.antiResonance;
|
|
1114
|
+
}
|
|
1115
|
+
destroy() {
|
|
1116
|
+
this.formantBank.destroy(), this.antiResonance.destroy();
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
class E {
|
|
1120
|
+
constructor(t, e, i) {
|
|
1121
|
+
r(this, "glottalFlowDerivative");
|
|
1122
|
+
r(this, "vocalTract");
|
|
1123
|
+
r(this, "outputGainNode");
|
|
1124
|
+
r(this, "in", null);
|
|
1125
|
+
// Source node has no input
|
|
1126
|
+
r(this, "out");
|
|
1127
|
+
this.glottalFlowDerivative = t, this.vocalTract = e, this.outputGainNode = i, this.out = i.out;
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Creates a new Voice synthesizer node.
|
|
1131
|
+
*
|
|
1132
|
+
* Sets up the complete source-filter synthesis pipeline.
|
|
1133
|
+
*/
|
|
1134
|
+
static async create(t, e) {
|
|
1135
|
+
const [i, s, n] = await Promise.all([
|
|
1136
|
+
S.create(t, e),
|
|
1137
|
+
L.create(t, e),
|
|
1138
|
+
w.create(t, { gain: e.outputGain ?? 1 })
|
|
1139
|
+
]);
|
|
1140
|
+
return i.out.connect(s.in), s.out.connect(n.in), new E(i, s, n);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Updates all voice parameters.
|
|
1144
|
+
*
|
|
1145
|
+
* @throws Error if the formants array length doesn't match the original
|
|
1146
|
+
*/
|
|
1147
|
+
update(t) {
|
|
1148
|
+
this.glottalFlowDerivative.update(t), this.vocalTract.update(t), t.outputGain !== void 0 && this.outputGainNode.update({ gain: t.outputGain });
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Starts the voice (begins voiced excitation).
|
|
1152
|
+
*/
|
|
1153
|
+
start() {
|
|
1154
|
+
this.glottalFlowDerivative.start();
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Stops the voice (stops voiced excitation).
|
|
1158
|
+
* Note: Noise component continues based on An parameter.
|
|
1159
|
+
*/
|
|
1160
|
+
stop() {
|
|
1161
|
+
this.glottalFlowDerivative.stop();
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Returns the number of formant resonators in the vocal tract.
|
|
1165
|
+
*/
|
|
1166
|
+
get formantCount() {
|
|
1167
|
+
return this.vocalTract.formantCount;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Returns the glottal flow derivative node for direct AudioParam access.
|
|
1171
|
+
*
|
|
1172
|
+
* Example usage:
|
|
1173
|
+
* voice.source.pulseTrainNode.f0.setValueAtTime(440, ctx.currentTime);
|
|
1174
|
+
* voice.source.glottalFormantNode.Ag.setTargetAtTime(0.5, ctx.currentTime, 0.1);
|
|
1175
|
+
*/
|
|
1176
|
+
get source() {
|
|
1177
|
+
return this.glottalFlowDerivative;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Returns the vocal tract node for direct AudioParam access.
|
|
1181
|
+
*
|
|
1182
|
+
* Example usage:
|
|
1183
|
+
* voice.tract.formants[0].F.linearRampToValueAtTime(800, ctx.currentTime + 0.1);
|
|
1184
|
+
* voice.tract.antiResonanceNode.F.setValueAtTime(4500, ctx.currentTime);
|
|
1185
|
+
*/
|
|
1186
|
+
get tract() {
|
|
1187
|
+
return this.vocalTract;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Returns the output gain node for direct AudioParam access.
|
|
1191
|
+
*
|
|
1192
|
+
* Example usage:
|
|
1193
|
+
* voice.outputGain.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 1);
|
|
1194
|
+
*/
|
|
1195
|
+
get outputGain() {
|
|
1196
|
+
return this.outputGainNode;
|
|
1197
|
+
}
|
|
1198
|
+
destroy() {
|
|
1199
|
+
this.glottalFlowDerivative.destroy(), this.vocalTract.destroy(), this.outputGainNode.destroy();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const p = [0, 0.5, 1], m = [0, 1 / 3, 2 / 3, 1], F = [
|
|
1203
|
+
["u", "y", "i"],
|
|
1204
|
+
// H = 0
|
|
1205
|
+
["o", "œ", "e"],
|
|
1206
|
+
// H = 1/3
|
|
1207
|
+
["ɔ", "ø", "ɛ"],
|
|
1208
|
+
// H = 2/3
|
|
1209
|
+
["a", "a", "a"]
|
|
1210
|
+
// H = 1 (all map to /a/)
|
|
1211
|
+
];
|
|
1212
|
+
function R(a) {
|
|
1213
|
+
return qt.find((t) => t.ipa === a);
|
|
1214
|
+
}
|
|
1215
|
+
function B(a, t, e) {
|
|
1216
|
+
return a + (t - a) * e;
|
|
1217
|
+
}
|
|
1218
|
+
function vt(a, t, e) {
|
|
1219
|
+
return {
|
|
1220
|
+
frequency: B(a.frequency, t.frequency, e),
|
|
1221
|
+
amplitude: B(a.amplitude, t.amplitude, e),
|
|
1222
|
+
bandwidth: B(a.bandwidth, t.bandwidth, e)
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function x(a, t, e) {
|
|
1226
|
+
return a.map((i, s) => vt(i, t[s], e));
|
|
1227
|
+
}
|
|
1228
|
+
function kt(a, t) {
|
|
1229
|
+
a = Math.max(0, Math.min(1, a)), t = Math.max(0, Math.min(1, t));
|
|
1230
|
+
let e = 0, i = 1;
|
|
1231
|
+
for (let u = 0; u < m.length - 1; u++)
|
|
1232
|
+
if (t >= m[u] && t <= m[u + 1]) {
|
|
1233
|
+
e = u, i = u + 1;
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
let s = 0, n = 1;
|
|
1237
|
+
for (let u = 0; u < p.length - 1; u++)
|
|
1238
|
+
if (a >= p[u] && a <= p[u + 1]) {
|
|
1239
|
+
s = u, n = u + 1;
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
const o = R(F[e][s]), c = R(F[e][n]), l = R(F[i][s]), A = R(F[i][n]), f = m[i] - m[e], b = p[n] - p[s], v = f > 0 ? (t - m[e]) / f : 0, d = b > 0 ? (a - p[s]) / b : 0, k = x(o.formants, c.formants, d), h = x(l.formants, A.formants, d);
|
|
1243
|
+
return x(k, h, v);
|
|
1244
|
+
}
|
|
1245
|
+
const qt = [
|
|
1246
|
+
{
|
|
1247
|
+
ipa: "i",
|
|
1248
|
+
v: 1,
|
|
1249
|
+
h: 0,
|
|
1250
|
+
formants: [
|
|
1251
|
+
{ frequency: 215, amplitude: -10, bandwidth: 10 },
|
|
1252
|
+
{ frequency: 1900, amplitude: -10, bandwidth: 18 },
|
|
1253
|
+
{ frequency: 2630, amplitude: -8, bandwidth: 20 },
|
|
1254
|
+
{ frequency: 3170, amplitude: -4, bandwidth: 30 },
|
|
1255
|
+
{ frequency: 3710, amplitude: -15, bandwidth: 40 },
|
|
1256
|
+
{ frequency: 6340, amplitude: -15, bandwidth: 150 }
|
|
1257
|
+
]
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
ipa: "e",
|
|
1261
|
+
v: 1,
|
|
1262
|
+
h: 1 / 3,
|
|
1263
|
+
formants: [
|
|
1264
|
+
{ frequency: 410, amplitude: -1, bandwidth: 10 },
|
|
1265
|
+
{ frequency: 2e3, amplitude: -3, bandwidth: 15 },
|
|
1266
|
+
{ frequency: 2570, amplitude: -2, bandwidth: 20 },
|
|
1267
|
+
{ frequency: 2980, amplitude: -2, bandwidth: 30 },
|
|
1268
|
+
{ frequency: 3900, amplitude: -5, bandwidth: 40 },
|
|
1269
|
+
{ frequency: 5960, amplitude: -15, bandwidth: 150 }
|
|
1270
|
+
]
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
ipa: "ɛ",
|
|
1274
|
+
v: 1,
|
|
1275
|
+
h: 2 / 3,
|
|
1276
|
+
formants: [
|
|
1277
|
+
{ frequency: 590, amplitude: 0, bandwidth: 10 },
|
|
1278
|
+
{ frequency: 1700, amplitude: -4, bandwidth: 15 },
|
|
1279
|
+
{ frequency: 2540, amplitude: -5, bandwidth: 30 },
|
|
1280
|
+
{ frequency: 2800, amplitude: -12, bandwidth: 50 },
|
|
1281
|
+
{ frequency: 3900, amplitude: -24, bandwidth: 40 },
|
|
1282
|
+
{ frequency: 5600, amplitude: -15, bandwidth: 150 }
|
|
1283
|
+
]
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
ipa: "y",
|
|
1287
|
+
v: 0.5,
|
|
1288
|
+
h: 0,
|
|
1289
|
+
formants: [
|
|
1290
|
+
{ frequency: 250, amplitude: -12, bandwidth: 10 },
|
|
1291
|
+
{ frequency: 1750, amplitude: -9, bandwidth: 10 },
|
|
1292
|
+
{ frequency: 2160, amplitude: -14, bandwidth: 20 },
|
|
1293
|
+
{ frequency: 3060, amplitude: -11, bandwidth: 30 },
|
|
1294
|
+
{ frequency: 3900, amplitude: -11, bandwidth: 40 },
|
|
1295
|
+
{ frequency: 6120, amplitude: -15, bandwidth: 150 }
|
|
1296
|
+
]
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
ipa: "œ",
|
|
1300
|
+
v: 0.5,
|
|
1301
|
+
h: 1 / 3,
|
|
1302
|
+
formants: [
|
|
1303
|
+
{ frequency: 350, amplitude: -6, bandwidth: 10 },
|
|
1304
|
+
{ frequency: 1350, amplitude: -3, bandwidth: 10 },
|
|
1305
|
+
{ frequency: 2250, amplitude: -8, bandwidth: 20 },
|
|
1306
|
+
{ frequency: 3170, amplitude: -8, bandwidth: 30 },
|
|
1307
|
+
{ frequency: 3900, amplitude: -10, bandwidth: 40 },
|
|
1308
|
+
{ frequency: 6340, amplitude: -15, bandwidth: 150 }
|
|
1309
|
+
]
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
ipa: "ø",
|
|
1313
|
+
v: 0.5,
|
|
1314
|
+
h: 2 / 3,
|
|
1315
|
+
formants: [
|
|
1316
|
+
{ frequency: 620, amplitude: -3, bandwidth: 10 },
|
|
1317
|
+
{ frequency: 1300, amplitude: -3, bandwidth: 10 },
|
|
1318
|
+
{ frequency: 2520, amplitude: -3, bandwidth: 20 },
|
|
1319
|
+
{ frequency: 3310, amplitude: -7, bandwidth: 30 },
|
|
1320
|
+
{ frequency: 3900, amplitude: -14, bandwidth: 40 },
|
|
1321
|
+
{ frequency: 6620, amplitude: -15, bandwidth: 150 }
|
|
1322
|
+
]
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
ipa: "u",
|
|
1326
|
+
v: 0,
|
|
1327
|
+
h: 0,
|
|
1328
|
+
formants: [
|
|
1329
|
+
{ frequency: 290, amplitude: -6, bandwidth: 10 },
|
|
1330
|
+
{ frequency: 750, amplitude: -8, bandwidth: 10 },
|
|
1331
|
+
{ frequency: 2300, amplitude: -13, bandwidth: 20 },
|
|
1332
|
+
{ frequency: 3080, amplitude: -8, bandwidth: 30 },
|
|
1333
|
+
{ frequency: 3900, amplitude: -9, bandwidth: 40 },
|
|
1334
|
+
{ frequency: 6160, amplitude: -15, bandwidth: 150 }
|
|
1335
|
+
]
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
ipa: "o",
|
|
1339
|
+
v: 0,
|
|
1340
|
+
h: 1 / 3,
|
|
1341
|
+
formants: [
|
|
1342
|
+
{ frequency: 440, amplitude: -6, bandwidth: 10 },
|
|
1343
|
+
{ frequency: 750, amplitude: -1, bandwidth: 12 },
|
|
1344
|
+
{ frequency: 2160, amplitude: -10, bandwidth: 20 },
|
|
1345
|
+
{ frequency: 2860, amplitude: -6, bandwidth: 30 },
|
|
1346
|
+
{ frequency: 3900, amplitude: -28, bandwidth: 40 },
|
|
1347
|
+
{ frequency: 5720, amplitude: -15, bandwidth: 150 }
|
|
1348
|
+
]
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
ipa: "ɔ",
|
|
1352
|
+
v: 0,
|
|
1353
|
+
h: 2 / 3,
|
|
1354
|
+
formants: [
|
|
1355
|
+
{ frequency: 610, amplitude: -3, bandwidth: 10 },
|
|
1356
|
+
{ frequency: 950, amplitude: 0, bandwidth: 12 },
|
|
1357
|
+
{ frequency: 2510, amplitude: -12, bandwidth: 20 },
|
|
1358
|
+
{ frequency: 2830, amplitude: -15, bandwidth: 30 },
|
|
1359
|
+
{ frequency: 3900, amplitude: -20, bandwidth: 40 },
|
|
1360
|
+
{ frequency: 5660, amplitude: -15, bandwidth: 150 }
|
|
1361
|
+
]
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
ipa: "a",
|
|
1365
|
+
v: 0.5,
|
|
1366
|
+
h: 1,
|
|
1367
|
+
formants: [
|
|
1368
|
+
{ frequency: 700, amplitude: 0, bandwidth: 13 },
|
|
1369
|
+
{ frequency: 1200, amplitude: 0, bandwidth: 13 },
|
|
1370
|
+
{ frequency: 2500, amplitude: -5, bandwidth: 40 },
|
|
1371
|
+
{ frequency: 2800, amplitude: -7, bandwidth: 60 },
|
|
1372
|
+
{ frequency: 3600, amplitude: -24, bandwidth: 40 },
|
|
1373
|
+
{ frequency: 5600, amplitude: -15, bandwidth: 150 }
|
|
1374
|
+
]
|
|
1375
|
+
}
|
|
1376
|
+
], Mt = {
|
|
1377
|
+
harmonicCoincidenceAttenuation: !0,
|
|
1378
|
+
formantFrequencyScaling: !0,
|
|
1379
|
+
f1Tuning: !0,
|
|
1380
|
+
f2Tuning: !0,
|
|
1381
|
+
antiResonanceScaling: !0
|
|
1382
|
+
}, y = 0.2, H = 0.2, _ = 4700, Bt = 2.5;
|
|
1383
|
+
function xt(a) {
|
|
1384
|
+
return 440 * Math.pow(2, (a - 69) / 12);
|
|
1385
|
+
}
|
|
1386
|
+
function Vt(a) {
|
|
1387
|
+
return Math.pow(10, a / 20);
|
|
1388
|
+
}
|
|
1389
|
+
function Dt(a, t, e) {
|
|
1390
|
+
const i = e === 1 ? 0.903 - 0.426 * t : 0.978 - 0.279 * t;
|
|
1391
|
+
return a <= 0.5 ? Math.pow(10, -2 * (1 - i) * a) : Math.pow(10, 2 * i * (1 - a) - 1);
|
|
1392
|
+
}
|
|
1393
|
+
function Nt(a, t) {
|
|
1394
|
+
const e = t === 1 ? 0.66 : 0.55;
|
|
1395
|
+
return a <= 0.5 ? 0.5 + 2 * (e - 0.5) * a : 0.9 - 2 * (0.9 - e) * (1 - a);
|
|
1396
|
+
}
|
|
1397
|
+
function Pt(a, t, e) {
|
|
1398
|
+
const i = Math.PI * (1 - e), s = Math.tan(Math.max(0.01, Math.min(Math.PI - 0.01, i)));
|
|
1399
|
+
return a / (t * Math.abs(s));
|
|
1400
|
+
}
|
|
1401
|
+
function St(a, t) {
|
|
1402
|
+
return t === 1 ? {
|
|
1403
|
+
Tl1: 27 - 21 * a,
|
|
1404
|
+
Tl2: 11 - 11 * a
|
|
1405
|
+
} : {
|
|
1406
|
+
Tl1: 45 - 36 * a,
|
|
1407
|
+
Tl2: 20 - 18.5 * a
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function Ct(a, t) {
|
|
1411
|
+
return a <= y ? 0 : ((1 - H) * ((a - y) / (1 - y)) + H) / t;
|
|
1412
|
+
}
|
|
1413
|
+
function jt(a, t) {
|
|
1414
|
+
return t > y ? a : 1.5 * t * a;
|
|
1415
|
+
}
|
|
1416
|
+
function Gt(a) {
|
|
1417
|
+
return 1.7 * a + 0.5;
|
|
1418
|
+
}
|
|
1419
|
+
function Lt(a) {
|
|
1420
|
+
return 125e-6 * a + 0.975;
|
|
1421
|
+
}
|
|
1422
|
+
function Ut(a, t, e) {
|
|
1423
|
+
if (e >= 3)
|
|
1424
|
+
return 0;
|
|
1425
|
+
const i = 15 + 85 * ((a - 50) / 1450), s = [10, 15, 25][e], o = Math.round(t / a) * a, c = Math.abs(o - t);
|
|
1426
|
+
return c < i ? (1 - c / i) * s : 0;
|
|
1427
|
+
}
|
|
1428
|
+
function Ot(a, t = {}) {
|
|
1429
|
+
const e = { ...Mt, ...t }, {
|
|
1430
|
+
pitch: i,
|
|
1431
|
+
pitchOffset: s,
|
|
1432
|
+
vocalEffort: n,
|
|
1433
|
+
vowelHeight: o,
|
|
1434
|
+
vowelBackness: c,
|
|
1435
|
+
tenseness: l,
|
|
1436
|
+
breathiness: A,
|
|
1437
|
+
roughness: f,
|
|
1438
|
+
vocalTractSize: b,
|
|
1439
|
+
isFalsetto: v
|
|
1440
|
+
} = a, d = v ? 2 : 1, k = s + 35 * i, h = xt(k), u = Dt(l, n, d), J = Nt(l, d), K = h / (2 * u), X = Pt(h, u, J), Y = Ct(n, u), { Tl1: Z, Tl2: tt } = St(n, d), et = jt(A, n), at = f * 0.3, it = f * 1, U = Gt(b), st = Lt(h), rt = kt(c, o).map((g, q) => {
|
|
1441
|
+
const M = e.formantFrequencyScaling ? st * U : 1;
|
|
1442
|
+
let T = M * g.frequency;
|
|
1443
|
+
if (q === 0 && e.f1Tuning) {
|
|
1444
|
+
const ct = M * g.frequency + 140 / (1 - y) * n - 70;
|
|
1445
|
+
T = Math.max(h + 50, ct);
|
|
1446
|
+
}
|
|
1447
|
+
q === 1 && e.f2Tuning && (T = Math.max(2 * h + 50, M * g.frequency));
|
|
1448
|
+
const ot = e.harmonicCoincidenceAttenuation ? Ut(h, T, q) : 0, ut = Vt(g.amplitude - ot);
|
|
1449
|
+
return {
|
|
1450
|
+
F: T,
|
|
1451
|
+
B: g.bandwidth,
|
|
1452
|
+
A: ut
|
|
1453
|
+
};
|
|
1454
|
+
}), nt = e.antiResonanceScaling ? _ * U : _;
|
|
1455
|
+
return {
|
|
1456
|
+
f0: h,
|
|
1457
|
+
Fg: K,
|
|
1458
|
+
Bg: X,
|
|
1459
|
+
Ag: Y,
|
|
1460
|
+
Tl1: Z,
|
|
1461
|
+
Tl2: tt,
|
|
1462
|
+
An: et,
|
|
1463
|
+
jitterDepth: at,
|
|
1464
|
+
shimmerDepth: it,
|
|
1465
|
+
formants: rt,
|
|
1466
|
+
F_BQ: nt,
|
|
1467
|
+
Q_BQ: Bt
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
export {
|
|
1471
|
+
G as AntiResonance,
|
|
1472
|
+
j as FormantBank,
|
|
1473
|
+
C as FormantResonator,
|
|
1474
|
+
w as Gain,
|
|
1475
|
+
S as GlottalFlowDerivative,
|
|
1476
|
+
D as GlottalFormant,
|
|
1477
|
+
P as NoiseSource,
|
|
1478
|
+
V as PulseTrain,
|
|
1479
|
+
N as SpectralTilt,
|
|
1480
|
+
L as VocalTract,
|
|
1481
|
+
E as Voice,
|
|
1482
|
+
Ot as generateSynthParams,
|
|
1483
|
+
kt as interpolateFormants,
|
|
1484
|
+
qt as vowels
|
|
1485
|
+
};
|
|
1486
|
+
//# sourceMappingURL=index.js.map
|