realtime-voice-agents 2.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.
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/assets/elevator-jazz.ulaw +1 -0
- package/assets/keyboard-typing.ulaw +1 -0
- package/assets/lofi.ulaw +1 -0
- package/assets/ringing.ulaw +1 -0
- package/assets/thinking-hum.ulaw +1 -0
- package/dist/BackgroundAudioPlayer-iMcivjis.mjs +282 -0
- package/dist/BackgroundAudioPlayer-jfRULWKC.cjs +313 -0
- package/dist/BaseRealtimeProvider-BQigr5mB.mjs +62 -0
- package/dist/BaseRealtimeProvider-BehPNT1r.d.cts +239 -0
- package/dist/BaseRealtimeProvider-ClP8Wx1X.d.mts +239 -0
- package/dist/BaseRealtimeProvider-DI4pKtOb.cjs +91 -0
- package/dist/GeminiLiveProvider-DvkTgzjG.d.cts +101 -0
- package/dist/GeminiLiveProvider-x2nyx5aO.d.mts +101 -0
- package/dist/InMemorySessionStore-B0_i-DOU.cjs +34 -0
- package/dist/InMemorySessionStore-B5_rq61L.d.cts +83 -0
- package/dist/InMemorySessionStore-B5_rq61L.d.mts +83 -0
- package/dist/InMemorySessionStore-DYParOJO.mjs +29 -0
- package/dist/OpenAICompatibleProvider-Bdtl-UXH.mjs +402 -0
- package/dist/OpenAICompatibleProvider-NS4cKVQj.cjs +409 -0
- package/dist/audio.cjs +21 -0
- package/dist/audio.d.cts +165 -0
- package/dist/audio.d.mts +165 -0
- package/dist/audio.mjs +4 -0
- package/dist/env-DSnGaERV.cjs +19 -0
- package/dist/env-DUwUWTsg.mjs +14 -0
- package/dist/events-BUMYdETO.d.cts +24 -0
- package/dist/events-BUMYdETO.d.mts +24 -0
- package/dist/events-BxDTIKKq.cjs +48 -0
- package/dist/events-BylBSBW-.mjs +43 -0
- package/dist/gemini.cjs +401 -0
- package/dist/gemini.d.cts +41 -0
- package/dist/gemini.d.mts +41 -0
- package/dist/gemini.mjs +395 -0
- package/dist/index.cjs +2142 -0
- package/dist/index.d.cts +920 -0
- package/dist/index.d.mts +920 -0
- package/dist/index.mjs +2113 -0
- package/dist/mulaw--cwU2c9L.mjs +64 -0
- package/dist/mulaw-DLUObjdP.cjs +117 -0
- package/dist/openai.cjs +61 -0
- package/dist/openai.d.cts +121 -0
- package/dist/openai.d.mts +121 -0
- package/dist/openai.mjs +53 -0
- package/dist/presets-Bf75YXs5.d.cts +24 -0
- package/dist/presets-Bf75YXs5.d.mts +24 -0
- package/dist/rest-BYqiVOhe.mjs +265 -0
- package/dist/rest-BvUKut_k.cjs +300 -0
- package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
- package/dist/session-config-BVLl7-ha.mjs +74 -0
- package/dist/session-config-c8sOw1XL.cjs +85 -0
- package/dist/store.cjs +3 -0
- package/dist/store.d.cts +2 -0
- package/dist/store.d.mts +2 -0
- package/dist/store.mjs +2 -0
- package/dist/testing.cjs +544 -0
- package/dist/testing.d.cts +191 -0
- package/dist/testing.d.mts +191 -0
- package/dist/testing.mjs +538 -0
- package/dist/transcode-C9aJG7_W.cjs +215 -0
- package/dist/transcode-CsAp97G9.mjs +198 -0
- package/dist/transport-B_PJFIVd.d.cts +155 -0
- package/dist/transport-CEaLFV4E.d.mts +155 -0
- package/dist/twilio.cjs +8 -0
- package/dist/twilio.d.cts +3 -0
- package/dist/twilio.d.mts +3 -0
- package/dist/twilio.mjs +2 -0
- package/dist/twiml-z9LjoF4_.d.cts +67 -0
- package/dist/twiml-z9LjoF4_.d.mts +67 -0
- package/dist/xai.cjs +92 -0
- package/dist/xai.d.cts +35 -0
- package/dist/xai.d.mts +35 -0
- package/dist/xai.mjs +86 -0
- package/package.json +183 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const require_mulaw = require("./mulaw-DLUObjdP.cjs");
|
|
2
|
+
//#region src/audio/resampler.ts
|
|
3
|
+
function gcd(a, b) {
|
|
4
|
+
while (b !== 0) [a, b] = [b, a % b];
|
|
5
|
+
return a;
|
|
6
|
+
}
|
|
7
|
+
/** Modified Bessel function of the first kind, order zero (series expansion). */
|
|
8
|
+
function besselI0(x) {
|
|
9
|
+
let sum = 1;
|
|
10
|
+
let term = 1;
|
|
11
|
+
for (let k = 1; k < 50; k++) {
|
|
12
|
+
term *= (x / (2 * k)) ** 2;
|
|
13
|
+
sum += term;
|
|
14
|
+
if (term < sum * 1e-12) break;
|
|
15
|
+
}
|
|
16
|
+
return sum;
|
|
17
|
+
}
|
|
18
|
+
const bankCache = /* @__PURE__ */ new Map();
|
|
19
|
+
function designBank(L, M, opts) {
|
|
20
|
+
const key = `${L}/${M}/${opts.quality}/${opts.kaiserBeta}/${opts.rolloff}`;
|
|
21
|
+
const cached = bankCache.get(key);
|
|
22
|
+
if (cached) return cached;
|
|
23
|
+
let n = opts.quality * Math.max(L, M);
|
|
24
|
+
n = Math.ceil(n / L) * L;
|
|
25
|
+
const fc = opts.rolloff * .5 / Math.max(L, M);
|
|
26
|
+
const center = (n - 1) / 2;
|
|
27
|
+
const denom = besselI0(opts.kaiserBeta);
|
|
28
|
+
const proto = new Float64Array(n);
|
|
29
|
+
for (let i = 0; i < n; i++) {
|
|
30
|
+
const x = i - center;
|
|
31
|
+
const sinc = x === 0 ? 2 * fc : Math.sin(2 * Math.PI * fc * x) / (Math.PI * x);
|
|
32
|
+
const r = 2 * i / (n - 1) - 1;
|
|
33
|
+
const window = besselI0(opts.kaiserBeta * Math.sqrt(1 - r * r)) / denom;
|
|
34
|
+
proto[i] = sinc * window;
|
|
35
|
+
}
|
|
36
|
+
const tapsPerPhase = n / L;
|
|
37
|
+
const phases = [];
|
|
38
|
+
for (let p = 0; p < L; p++) {
|
|
39
|
+
const phase = new Float64Array(tapsPerPhase);
|
|
40
|
+
let sum = 0;
|
|
41
|
+
for (let t = 0; t < tapsPerPhase; t++) {
|
|
42
|
+
phase[t] = proto[p + t * L];
|
|
43
|
+
sum += phase[t];
|
|
44
|
+
}
|
|
45
|
+
if (sum !== 0) for (let t = 0; t < tapsPerPhase; t++) phase[t] /= sum;
|
|
46
|
+
phases.push(phase);
|
|
47
|
+
}
|
|
48
|
+
const bank = {
|
|
49
|
+
phases,
|
|
50
|
+
tapsPerPhase
|
|
51
|
+
};
|
|
52
|
+
bankCache.set(key, bank);
|
|
53
|
+
return bank;
|
|
54
|
+
}
|
|
55
|
+
var Resampler = class {
|
|
56
|
+
fromRate;
|
|
57
|
+
toRate;
|
|
58
|
+
L;
|
|
59
|
+
M;
|
|
60
|
+
bank;
|
|
61
|
+
/** Trailing input samples carried between chunks (tapsPerPhase − 1 long). */
|
|
62
|
+
hist;
|
|
63
|
+
/** Phase index of the next output sample. */
|
|
64
|
+
phase = 0;
|
|
65
|
+
/**
|
|
66
|
+
* Input index (relative to the start of the next [hist + chunk] buffer) that
|
|
67
|
+
* the next output sample is anchored on. Always ≥ tapsPerPhase − 1.
|
|
68
|
+
*/
|
|
69
|
+
kRel;
|
|
70
|
+
constructor(fromRate, toRate, options = {}) {
|
|
71
|
+
if (!Number.isInteger(fromRate) || !Number.isInteger(toRate) || fromRate <= 0 || toRate <= 0) throw new RangeError(`invalid sample rates: ${fromRate} -> ${toRate}`);
|
|
72
|
+
this.fromRate = fromRate;
|
|
73
|
+
this.toRate = toRate;
|
|
74
|
+
const g = gcd(fromRate, toRate);
|
|
75
|
+
this.L = toRate / g;
|
|
76
|
+
this.M = fromRate / g;
|
|
77
|
+
this.bank = designBank(this.L, this.M, {
|
|
78
|
+
quality: options.quality ?? 48,
|
|
79
|
+
kaiserBeta: options.kaiserBeta ?? 8,
|
|
80
|
+
rolloff: options.rolloff ?? .9
|
|
81
|
+
});
|
|
82
|
+
this.hist = new Int16Array(this.bank.tapsPerPhase - 1);
|
|
83
|
+
this.kRel = this.bank.tapsPerPhase - 1;
|
|
84
|
+
}
|
|
85
|
+
/** True when no rate conversion is needed (input is passed through). */
|
|
86
|
+
get isPassthrough() {
|
|
87
|
+
return this.L === 1 && this.M === 1;
|
|
88
|
+
}
|
|
89
|
+
process(input) {
|
|
90
|
+
if (this.isPassthrough) return input;
|
|
91
|
+
const { phases, tapsPerPhase } = this.bank;
|
|
92
|
+
const histLen = this.hist.length;
|
|
93
|
+
const buf = new Int16Array(histLen + input.length);
|
|
94
|
+
buf.set(this.hist, 0);
|
|
95
|
+
buf.set(input, histLen);
|
|
96
|
+
const maxOut = Math.ceil((buf.length - this.kRel) * this.L / this.M) + 1;
|
|
97
|
+
const out = new Int16Array(Math.max(maxOut, 0));
|
|
98
|
+
let produced = 0;
|
|
99
|
+
let k = this.kRel;
|
|
100
|
+
let p = this.phase;
|
|
101
|
+
while (k < buf.length) {
|
|
102
|
+
const coeffs = phases[p];
|
|
103
|
+
let acc = 0;
|
|
104
|
+
for (let t = 0; t < tapsPerPhase; t++) acc += coeffs[t] * buf[k - t];
|
|
105
|
+
let sample = Math.round(acc);
|
|
106
|
+
if (sample > 32767) sample = 32767;
|
|
107
|
+
else if (sample < -32768) sample = -32768;
|
|
108
|
+
out[produced++] = sample;
|
|
109
|
+
p += this.M;
|
|
110
|
+
k += p / this.L | 0;
|
|
111
|
+
p %= this.L;
|
|
112
|
+
}
|
|
113
|
+
const keepFrom = buf.length - histLen;
|
|
114
|
+
this.hist = buf.slice(keepFrom);
|
|
115
|
+
this.kRel = k - keepFrom;
|
|
116
|
+
this.phase = p;
|
|
117
|
+
return out.subarray(0, produced);
|
|
118
|
+
}
|
|
119
|
+
/** Drain the filter tail by pushing one phase-length of silence. */
|
|
120
|
+
flush() {
|
|
121
|
+
if (this.isPassthrough) return /* @__PURE__ */ new Int16Array(0);
|
|
122
|
+
return this.process(new Int16Array(this.bank.tapsPerPhase));
|
|
123
|
+
}
|
|
124
|
+
/** Clear all carried state (history, phase) for reuse on a fresh stream. */
|
|
125
|
+
reset() {
|
|
126
|
+
this.hist = new Int16Array(this.bank.tapsPerPhase - 1);
|
|
127
|
+
this.phase = 0;
|
|
128
|
+
this.kRel = this.bank.tapsPerPhase - 1;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/audio/transcode.ts
|
|
133
|
+
/**
|
|
134
|
+
* Per-call transcoders between the Twilio wire format (base64 μ-law 8 kHz) and
|
|
135
|
+
* provider PCM16 formats (Gemini: 16 kHz in, 24 kHz out).
|
|
136
|
+
*
|
|
137
|
+
* Both directions are stateful: the resampler carries FIR history across
|
|
138
|
+
* chunks, and the outbound transcoder carries a dangling odd byte so PCM16
|
|
139
|
+
* frames split mid-sample re-align instead of being truncated.
|
|
140
|
+
*/
|
|
141
|
+
const IS_LITTLE_ENDIAN = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
|
|
142
|
+
function pcm16ToBufferLE(pcm) {
|
|
143
|
+
if (IS_LITTLE_ENDIAN) return Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
|
144
|
+
const buf = Buffer.allocUnsafe(pcm.length * 2);
|
|
145
|
+
for (let i = 0; i < pcm.length; i++) buf.writeInt16LE(pcm[i], i * 2);
|
|
146
|
+
return buf;
|
|
147
|
+
}
|
|
148
|
+
function bufferLEToPcm16(buf) {
|
|
149
|
+
const samples = buf.length >> 1;
|
|
150
|
+
if (IS_LITTLE_ENDIAN && buf.byteOffset % 2 === 0) return new Int16Array(buf.buffer, buf.byteOffset, samples);
|
|
151
|
+
const pcm = new Int16Array(samples);
|
|
152
|
+
for (let i = 0; i < samples; i++) pcm[i] = buf.readInt16LE(i * 2);
|
|
153
|
+
return pcm;
|
|
154
|
+
}
|
|
155
|
+
/** Twilio → provider: base64 μ-law 8 kHz in, base64 PCM16LE at `outRate` out. */
|
|
156
|
+
var InboundTranscoder = class {
|
|
157
|
+
resampler;
|
|
158
|
+
constructor(outRate = 16e3, options) {
|
|
159
|
+
this.resampler = new Resampler(8e3, outRate, options);
|
|
160
|
+
}
|
|
161
|
+
process(base64Mulaw) {
|
|
162
|
+
const mulaw = Buffer.from(base64Mulaw, "base64");
|
|
163
|
+
const pcm = require_mulaw.mulawToPcm16(new Uint8Array(mulaw.buffer, mulaw.byteOffset, mulaw.length));
|
|
164
|
+
return pcm16ToBufferLE(this.resampler.process(pcm)).toString("base64");
|
|
165
|
+
}
|
|
166
|
+
reset() {
|
|
167
|
+
this.resampler.reset();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/** Provider → Twilio: base64 PCM16LE at `inRate` in, base64 μ-law 8 kHz out. */
|
|
171
|
+
var OutboundTranscoder = class {
|
|
172
|
+
resampler;
|
|
173
|
+
/** Dangling byte from a chunk that split a 16-bit sample. */
|
|
174
|
+
carry = null;
|
|
175
|
+
constructor(inRate = 24e3, options) {
|
|
176
|
+
this.resampler = new Resampler(inRate, 8e3, options);
|
|
177
|
+
}
|
|
178
|
+
process(base64Pcm16) {
|
|
179
|
+
let buf = Buffer.from(base64Pcm16, "base64");
|
|
180
|
+
if (this.carry) {
|
|
181
|
+
buf = Buffer.concat([this.carry, buf]);
|
|
182
|
+
this.carry = null;
|
|
183
|
+
}
|
|
184
|
+
if (buf.length % 2 !== 0) {
|
|
185
|
+
this.carry = Buffer.from(buf.subarray(buf.length - 1));
|
|
186
|
+
buf = buf.subarray(0, buf.length - 1);
|
|
187
|
+
}
|
|
188
|
+
const pcm = bufferLEToPcm16(buf);
|
|
189
|
+
const resampled = this.resampler.process(pcm);
|
|
190
|
+
return Buffer.from(require_mulaw.pcm16ToMulaw(resampled)).toString("base64");
|
|
191
|
+
}
|
|
192
|
+
reset() {
|
|
193
|
+
this.resampler.reset();
|
|
194
|
+
this.carry = null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
Object.defineProperty(exports, "InboundTranscoder", {
|
|
199
|
+
enumerable: true,
|
|
200
|
+
get: function() {
|
|
201
|
+
return InboundTranscoder;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
Object.defineProperty(exports, "OutboundTranscoder", {
|
|
205
|
+
enumerable: true,
|
|
206
|
+
get: function() {
|
|
207
|
+
return OutboundTranscoder;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
Object.defineProperty(exports, "Resampler", {
|
|
211
|
+
enumerable: true,
|
|
212
|
+
get: function() {
|
|
213
|
+
return Resampler;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { c as mulawToPcm16, l as pcm16ToMulaw } from "./mulaw--cwU2c9L.mjs";
|
|
2
|
+
//#region src/audio/resampler.ts
|
|
3
|
+
function gcd(a, b) {
|
|
4
|
+
while (b !== 0) [a, b] = [b, a % b];
|
|
5
|
+
return a;
|
|
6
|
+
}
|
|
7
|
+
/** Modified Bessel function of the first kind, order zero (series expansion). */
|
|
8
|
+
function besselI0(x) {
|
|
9
|
+
let sum = 1;
|
|
10
|
+
let term = 1;
|
|
11
|
+
for (let k = 1; k < 50; k++) {
|
|
12
|
+
term *= (x / (2 * k)) ** 2;
|
|
13
|
+
sum += term;
|
|
14
|
+
if (term < sum * 1e-12) break;
|
|
15
|
+
}
|
|
16
|
+
return sum;
|
|
17
|
+
}
|
|
18
|
+
const bankCache = /* @__PURE__ */ new Map();
|
|
19
|
+
function designBank(L, M, opts) {
|
|
20
|
+
const key = `${L}/${M}/${opts.quality}/${opts.kaiserBeta}/${opts.rolloff}`;
|
|
21
|
+
const cached = bankCache.get(key);
|
|
22
|
+
if (cached) return cached;
|
|
23
|
+
let n = opts.quality * Math.max(L, M);
|
|
24
|
+
n = Math.ceil(n / L) * L;
|
|
25
|
+
const fc = opts.rolloff * .5 / Math.max(L, M);
|
|
26
|
+
const center = (n - 1) / 2;
|
|
27
|
+
const denom = besselI0(opts.kaiserBeta);
|
|
28
|
+
const proto = new Float64Array(n);
|
|
29
|
+
for (let i = 0; i < n; i++) {
|
|
30
|
+
const x = i - center;
|
|
31
|
+
const sinc = x === 0 ? 2 * fc : Math.sin(2 * Math.PI * fc * x) / (Math.PI * x);
|
|
32
|
+
const r = 2 * i / (n - 1) - 1;
|
|
33
|
+
const window = besselI0(opts.kaiserBeta * Math.sqrt(1 - r * r)) / denom;
|
|
34
|
+
proto[i] = sinc * window;
|
|
35
|
+
}
|
|
36
|
+
const tapsPerPhase = n / L;
|
|
37
|
+
const phases = [];
|
|
38
|
+
for (let p = 0; p < L; p++) {
|
|
39
|
+
const phase = new Float64Array(tapsPerPhase);
|
|
40
|
+
let sum = 0;
|
|
41
|
+
for (let t = 0; t < tapsPerPhase; t++) {
|
|
42
|
+
phase[t] = proto[p + t * L];
|
|
43
|
+
sum += phase[t];
|
|
44
|
+
}
|
|
45
|
+
if (sum !== 0) for (let t = 0; t < tapsPerPhase; t++) phase[t] /= sum;
|
|
46
|
+
phases.push(phase);
|
|
47
|
+
}
|
|
48
|
+
const bank = {
|
|
49
|
+
phases,
|
|
50
|
+
tapsPerPhase
|
|
51
|
+
};
|
|
52
|
+
bankCache.set(key, bank);
|
|
53
|
+
return bank;
|
|
54
|
+
}
|
|
55
|
+
var Resampler = class {
|
|
56
|
+
fromRate;
|
|
57
|
+
toRate;
|
|
58
|
+
L;
|
|
59
|
+
M;
|
|
60
|
+
bank;
|
|
61
|
+
/** Trailing input samples carried between chunks (tapsPerPhase − 1 long). */
|
|
62
|
+
hist;
|
|
63
|
+
/** Phase index of the next output sample. */
|
|
64
|
+
phase = 0;
|
|
65
|
+
/**
|
|
66
|
+
* Input index (relative to the start of the next [hist + chunk] buffer) that
|
|
67
|
+
* the next output sample is anchored on. Always ≥ tapsPerPhase − 1.
|
|
68
|
+
*/
|
|
69
|
+
kRel;
|
|
70
|
+
constructor(fromRate, toRate, options = {}) {
|
|
71
|
+
if (!Number.isInteger(fromRate) || !Number.isInteger(toRate) || fromRate <= 0 || toRate <= 0) throw new RangeError(`invalid sample rates: ${fromRate} -> ${toRate}`);
|
|
72
|
+
this.fromRate = fromRate;
|
|
73
|
+
this.toRate = toRate;
|
|
74
|
+
const g = gcd(fromRate, toRate);
|
|
75
|
+
this.L = toRate / g;
|
|
76
|
+
this.M = fromRate / g;
|
|
77
|
+
this.bank = designBank(this.L, this.M, {
|
|
78
|
+
quality: options.quality ?? 48,
|
|
79
|
+
kaiserBeta: options.kaiserBeta ?? 8,
|
|
80
|
+
rolloff: options.rolloff ?? .9
|
|
81
|
+
});
|
|
82
|
+
this.hist = new Int16Array(this.bank.tapsPerPhase - 1);
|
|
83
|
+
this.kRel = this.bank.tapsPerPhase - 1;
|
|
84
|
+
}
|
|
85
|
+
/** True when no rate conversion is needed (input is passed through). */
|
|
86
|
+
get isPassthrough() {
|
|
87
|
+
return this.L === 1 && this.M === 1;
|
|
88
|
+
}
|
|
89
|
+
process(input) {
|
|
90
|
+
if (this.isPassthrough) return input;
|
|
91
|
+
const { phases, tapsPerPhase } = this.bank;
|
|
92
|
+
const histLen = this.hist.length;
|
|
93
|
+
const buf = new Int16Array(histLen + input.length);
|
|
94
|
+
buf.set(this.hist, 0);
|
|
95
|
+
buf.set(input, histLen);
|
|
96
|
+
const maxOut = Math.ceil((buf.length - this.kRel) * this.L / this.M) + 1;
|
|
97
|
+
const out = new Int16Array(Math.max(maxOut, 0));
|
|
98
|
+
let produced = 0;
|
|
99
|
+
let k = this.kRel;
|
|
100
|
+
let p = this.phase;
|
|
101
|
+
while (k < buf.length) {
|
|
102
|
+
const coeffs = phases[p];
|
|
103
|
+
let acc = 0;
|
|
104
|
+
for (let t = 0; t < tapsPerPhase; t++) acc += coeffs[t] * buf[k - t];
|
|
105
|
+
let sample = Math.round(acc);
|
|
106
|
+
if (sample > 32767) sample = 32767;
|
|
107
|
+
else if (sample < -32768) sample = -32768;
|
|
108
|
+
out[produced++] = sample;
|
|
109
|
+
p += this.M;
|
|
110
|
+
k += p / this.L | 0;
|
|
111
|
+
p %= this.L;
|
|
112
|
+
}
|
|
113
|
+
const keepFrom = buf.length - histLen;
|
|
114
|
+
this.hist = buf.slice(keepFrom);
|
|
115
|
+
this.kRel = k - keepFrom;
|
|
116
|
+
this.phase = p;
|
|
117
|
+
return out.subarray(0, produced);
|
|
118
|
+
}
|
|
119
|
+
/** Drain the filter tail by pushing one phase-length of silence. */
|
|
120
|
+
flush() {
|
|
121
|
+
if (this.isPassthrough) return /* @__PURE__ */ new Int16Array(0);
|
|
122
|
+
return this.process(new Int16Array(this.bank.tapsPerPhase));
|
|
123
|
+
}
|
|
124
|
+
/** Clear all carried state (history, phase) for reuse on a fresh stream. */
|
|
125
|
+
reset() {
|
|
126
|
+
this.hist = new Int16Array(this.bank.tapsPerPhase - 1);
|
|
127
|
+
this.phase = 0;
|
|
128
|
+
this.kRel = this.bank.tapsPerPhase - 1;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/audio/transcode.ts
|
|
133
|
+
/**
|
|
134
|
+
* Per-call transcoders between the Twilio wire format (base64 μ-law 8 kHz) and
|
|
135
|
+
* provider PCM16 formats (Gemini: 16 kHz in, 24 kHz out).
|
|
136
|
+
*
|
|
137
|
+
* Both directions are stateful: the resampler carries FIR history across
|
|
138
|
+
* chunks, and the outbound transcoder carries a dangling odd byte so PCM16
|
|
139
|
+
* frames split mid-sample re-align instead of being truncated.
|
|
140
|
+
*/
|
|
141
|
+
const IS_LITTLE_ENDIAN = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
|
|
142
|
+
function pcm16ToBufferLE(pcm) {
|
|
143
|
+
if (IS_LITTLE_ENDIAN) return Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
|
144
|
+
const buf = Buffer.allocUnsafe(pcm.length * 2);
|
|
145
|
+
for (let i = 0; i < pcm.length; i++) buf.writeInt16LE(pcm[i], i * 2);
|
|
146
|
+
return buf;
|
|
147
|
+
}
|
|
148
|
+
function bufferLEToPcm16(buf) {
|
|
149
|
+
const samples = buf.length >> 1;
|
|
150
|
+
if (IS_LITTLE_ENDIAN && buf.byteOffset % 2 === 0) return new Int16Array(buf.buffer, buf.byteOffset, samples);
|
|
151
|
+
const pcm = new Int16Array(samples);
|
|
152
|
+
for (let i = 0; i < samples; i++) pcm[i] = buf.readInt16LE(i * 2);
|
|
153
|
+
return pcm;
|
|
154
|
+
}
|
|
155
|
+
/** Twilio → provider: base64 μ-law 8 kHz in, base64 PCM16LE at `outRate` out. */
|
|
156
|
+
var InboundTranscoder = class {
|
|
157
|
+
resampler;
|
|
158
|
+
constructor(outRate = 16e3, options) {
|
|
159
|
+
this.resampler = new Resampler(8e3, outRate, options);
|
|
160
|
+
}
|
|
161
|
+
process(base64Mulaw) {
|
|
162
|
+
const mulaw = Buffer.from(base64Mulaw, "base64");
|
|
163
|
+
const pcm = mulawToPcm16(new Uint8Array(mulaw.buffer, mulaw.byteOffset, mulaw.length));
|
|
164
|
+
return pcm16ToBufferLE(this.resampler.process(pcm)).toString("base64");
|
|
165
|
+
}
|
|
166
|
+
reset() {
|
|
167
|
+
this.resampler.reset();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/** Provider → Twilio: base64 PCM16LE at `inRate` in, base64 μ-law 8 kHz out. */
|
|
171
|
+
var OutboundTranscoder = class {
|
|
172
|
+
resampler;
|
|
173
|
+
/** Dangling byte from a chunk that split a 16-bit sample. */
|
|
174
|
+
carry = null;
|
|
175
|
+
constructor(inRate = 24e3, options) {
|
|
176
|
+
this.resampler = new Resampler(inRate, 8e3, options);
|
|
177
|
+
}
|
|
178
|
+
process(base64Pcm16) {
|
|
179
|
+
let buf = Buffer.from(base64Pcm16, "base64");
|
|
180
|
+
if (this.carry) {
|
|
181
|
+
buf = Buffer.concat([this.carry, buf]);
|
|
182
|
+
this.carry = null;
|
|
183
|
+
}
|
|
184
|
+
if (buf.length % 2 !== 0) {
|
|
185
|
+
this.carry = Buffer.from(buf.subarray(buf.length - 1));
|
|
186
|
+
buf = buf.subarray(0, buf.length - 1);
|
|
187
|
+
}
|
|
188
|
+
const pcm = bufferLEToPcm16(buf);
|
|
189
|
+
const resampled = this.resampler.process(pcm);
|
|
190
|
+
return Buffer.from(pcm16ToMulaw(resampled)).toString("base64");
|
|
191
|
+
}
|
|
192
|
+
reset() {
|
|
193
|
+
this.resampler.reset();
|
|
194
|
+
this.carry = null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
export { OutboundTranscoder as n, Resampler as r, InboundTranscoder as t };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { t as TypedEmitter } from "./events-BUMYdETO.cjs";
|
|
2
|
+
//#region src/twilio/messages.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Twilio Media Streams wire messages.
|
|
5
|
+
* Reference: https://www.twilio.com/docs/voice/media-streams/websocket-messages
|
|
6
|
+
*
|
|
7
|
+
* Inbound (Twilio → server): connected, start, media, stop, mark, dtmf.
|
|
8
|
+
* Outbound (server → Twilio): media, mark, clear.
|
|
9
|
+
* All media payloads are base64 μ-law 8 kHz mono on `<Connect><Stream>` calls.
|
|
10
|
+
*/
|
|
11
|
+
interface TwilioConnectedEvent {
|
|
12
|
+
event: 'connected';
|
|
13
|
+
protocol?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
}
|
|
16
|
+
interface TwilioStartEvent {
|
|
17
|
+
event: 'start';
|
|
18
|
+
sequenceNumber?: string;
|
|
19
|
+
streamSid: string;
|
|
20
|
+
start: {
|
|
21
|
+
streamSid: string;
|
|
22
|
+
accountSid?: string;
|
|
23
|
+
callSid: string;
|
|
24
|
+
tracks?: string[];
|
|
25
|
+
mediaFormat?: {
|
|
26
|
+
encoding: string;
|
|
27
|
+
sampleRate: number;
|
|
28
|
+
channels: number;
|
|
29
|
+
};
|
|
30
|
+
/** `<Parameter name="…" value="…"/>` pairs from the TwiML `<Stream>`. */
|
|
31
|
+
customParameters?: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
interface TwilioMediaEvent {
|
|
35
|
+
event: 'media';
|
|
36
|
+
sequenceNumber?: string;
|
|
37
|
+
streamSid: string;
|
|
38
|
+
media: {
|
|
39
|
+
track?: 'inbound' | 'outbound';
|
|
40
|
+
chunk?: string;
|
|
41
|
+
/** Milliseconds since stream start — the honest wall clock of the call. */
|
|
42
|
+
timestamp?: string;
|
|
43
|
+
/** Base64 μ-law audio. */
|
|
44
|
+
payload: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
interface TwilioStopEvent {
|
|
48
|
+
event: 'stop';
|
|
49
|
+
sequenceNumber?: string;
|
|
50
|
+
streamSid: string;
|
|
51
|
+
stop?: {
|
|
52
|
+
accountSid?: string;
|
|
53
|
+
callSid?: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
interface TwilioMarkEvent {
|
|
57
|
+
event: 'mark';
|
|
58
|
+
sequenceNumber?: string;
|
|
59
|
+
streamSid: string;
|
|
60
|
+
mark: {
|
|
61
|
+
name: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
interface TwilioDtmfEvent {
|
|
65
|
+
event: 'dtmf';
|
|
66
|
+
streamSid: string;
|
|
67
|
+
sequenceNumber?: string;
|
|
68
|
+
dtmf: {
|
|
69
|
+
track?: string;
|
|
70
|
+
digit: string;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
type TwilioInboundMessage = TwilioConnectedEvent | TwilioStartEvent | TwilioMediaEvent | TwilioStopEvent | TwilioMarkEvent | TwilioDtmfEvent;
|
|
74
|
+
interface TwilioOutboundMedia {
|
|
75
|
+
event: 'media';
|
|
76
|
+
streamSid: string;
|
|
77
|
+
media: {
|
|
78
|
+
payload: string;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
interface TwilioOutboundMark {
|
|
82
|
+
event: 'mark';
|
|
83
|
+
streamSid: string;
|
|
84
|
+
mark: {
|
|
85
|
+
name: string;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
interface TwilioOutboundClear {
|
|
89
|
+
event: 'clear';
|
|
90
|
+
streamSid: string;
|
|
91
|
+
}
|
|
92
|
+
type TwilioOutboundMessage = TwilioOutboundMedia | TwilioOutboundMark | TwilioOutboundClear;
|
|
93
|
+
/**
|
|
94
|
+
* Parse a raw WebSocket frame into a Twilio inbound message.
|
|
95
|
+
* Returns null for non-JSON frames or unknown event types (Twilio may add
|
|
96
|
+
* events; unknown ones must not crash a live call).
|
|
97
|
+
*/
|
|
98
|
+
declare function parseTwilioMessage(raw: string | Buffer): TwilioInboundMessage | null;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/twilio/transport.d.ts
|
|
101
|
+
/** Minimal ws-shaped socket. `ws`-package sockets satisfy this directly. */
|
|
102
|
+
interface WebSocketLike {
|
|
103
|
+
send(data: string): void;
|
|
104
|
+
close(code?: number, reason?: string): void;
|
|
105
|
+
on(event: 'message', listener: (data: unknown, isBinary?: boolean) => void): unknown;
|
|
106
|
+
on(event: 'close', listener: (code?: number, reason?: unknown) => void): unknown;
|
|
107
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
108
|
+
readyState?: number;
|
|
109
|
+
}
|
|
110
|
+
interface TransportEvents extends Record<string, (...args: any[]) => void> {
|
|
111
|
+
start: (event: TwilioStartEvent) => void;
|
|
112
|
+
media: (event: TwilioMediaEvent) => void;
|
|
113
|
+
stop: (event: TwilioStopEvent) => void;
|
|
114
|
+
mark: (event: TwilioMarkEvent) => void;
|
|
115
|
+
dtmf: (event: TwilioDtmfEvent) => void;
|
|
116
|
+
close: (info: {
|
|
117
|
+
code?: number;
|
|
118
|
+
reason?: string;
|
|
119
|
+
}) => void;
|
|
120
|
+
error: (error: Error) => void;
|
|
121
|
+
}
|
|
122
|
+
interface AwaitStartOptions {
|
|
123
|
+
/** Max time to wait for the `start` frame. Default 5000. */
|
|
124
|
+
timeoutMs?: number;
|
|
125
|
+
/** Max non-start frames tolerated before giving up. Default 8. */
|
|
126
|
+
maxPreStartMessages?: number;
|
|
127
|
+
}
|
|
128
|
+
declare class TwilioMediaTransport extends TypedEmitter<TransportEvents> {
|
|
129
|
+
private readonly ws;
|
|
130
|
+
private streamSidValue;
|
|
131
|
+
private startEvent;
|
|
132
|
+
private closed;
|
|
133
|
+
constructor(ws: WebSocketLike);
|
|
134
|
+
get streamSid(): string | null;
|
|
135
|
+
get start(): TwilioStartEvent | null;
|
|
136
|
+
get isOpen(): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Resolve with the `start` frame, or reject on timeout / too many pre-start
|
|
139
|
+
* frames / socket close. Media frames arriving before `start` are counted
|
|
140
|
+
* but not delivered (there is no session to deliver them to yet).
|
|
141
|
+
*/
|
|
142
|
+
awaitStart(options?: AwaitStartOptions): Promise<TwilioStartEvent>;
|
|
143
|
+
/** Forward one media payload (base64 μ-law) to the caller. */
|
|
144
|
+
sendMedia(payload: string): void;
|
|
145
|
+
/** Interleave a named mark; Twilio echoes it once playout reaches it. */
|
|
146
|
+
sendMark(name: string): void;
|
|
147
|
+
/** Flush Twilio's playout buffer (barge-in). Pending marks echo back as discarded. */
|
|
148
|
+
sendClear(): void;
|
|
149
|
+
close(code?: number, reason?: string): void;
|
|
150
|
+
private handleMessage;
|
|
151
|
+
private requireStreamSid;
|
|
152
|
+
private sendRaw;
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
export { TwilioConnectedEvent as a, TwilioMarkEvent as c, TwilioOutboundMark as d, TwilioOutboundMedia as f, parseTwilioMessage as g, TwilioStopEvent as h, WebSocketLike as i, TwilioMediaEvent as l, TwilioStartEvent as m, TransportEvents as n, TwilioDtmfEvent as o, TwilioOutboundMessage as p, TwilioMediaTransport as r, TwilioInboundMessage as s, AwaitStartOptions as t, TwilioOutboundClear as u };
|