@liforma/client 0.3.0 → 0.4.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 (123) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +14 -1
  3. package/dist/byo/audioCapture.d.ts +24 -0
  4. package/dist/byo/audioCapture.d.ts.map +1 -0
  5. package/dist/byo/index.d.ts +8 -0
  6. package/dist/byo/index.d.ts.map +1 -0
  7. package/dist/byo/index.js +307 -0
  8. package/dist/byo/index.js.map +1 -0
  9. package/dist/byo/mic.d.ts +27 -0
  10. package/dist/byo/mic.d.ts.map +1 -0
  11. package/dist/byo/pcm.d.ts +33 -0
  12. package/dist/byo/pcm.d.ts.map +1 -0
  13. package/dist/byo/pcmCarry.d.ts +17 -0
  14. package/dist/byo/pcmCarry.d.ts.map +1 -0
  15. package/dist/byo/resample.d.ts +18 -0
  16. package/dist/byo/resample.d.ts.map +1 -0
  17. package/dist/byo/turn.d.ts +39 -0
  18. package/dist/byo/turn.d.ts.map +1 -0
  19. package/dist/byo/types.d.ts +74 -0
  20. package/dist/byo/types.d.ts.map +1 -0
  21. package/dist/deepgram/connectDeepgramAgent.d.ts +51 -0
  22. package/dist/deepgram/connectDeepgramAgent.d.ts.map +1 -0
  23. package/dist/deepgram/deepgramAgentMessages.d.ts +31 -0
  24. package/dist/deepgram/deepgramAgentMessages.d.ts.map +1 -0
  25. package/dist/deepgram/index.d.ts +4 -0
  26. package/dist/deepgram/index.d.ts.map +1 -0
  27. package/dist/deepgram/index.js +142 -0
  28. package/dist/deepgram/index.js.map +1 -0
  29. package/dist/elevenlabs/connectElevenLabsAgent.d.ts +23 -0
  30. package/dist/elevenlabs/connectElevenLabsAgent.d.ts.map +1 -0
  31. package/dist/elevenlabs/index.d.ts +4 -0
  32. package/dist/elevenlabs/index.d.ts.map +1 -0
  33. package/dist/elevenlabs/index.js +89 -0
  34. package/dist/elevenlabs/index.js.map +1 -0
  35. package/dist/google/connectGeminiLive.d.ts +34 -0
  36. package/dist/google/connectGeminiLive.d.ts.map +1 -0
  37. package/dist/google/index.d.ts +4 -0
  38. package/dist/google/index.d.ts.map +1 -0
  39. package/dist/google/index.js +98 -0
  40. package/dist/google/index.js.map +1 -0
  41. package/dist/livekit/connectLiveKitAgent.d.ts +46 -0
  42. package/dist/livekit/connectLiveKitAgent.d.ts.map +1 -0
  43. package/dist/livekit/index.d.ts +3 -0
  44. package/dist/livekit/index.d.ts.map +1 -0
  45. package/dist/livekit/index.js +113 -0
  46. package/dist/livekit/index.js.map +1 -0
  47. package/dist/next/byo/audioCapture.d.ts +24 -0
  48. package/dist/next/byo/audioCapture.d.ts.map +1 -0
  49. package/dist/next/byo/pcm.d.ts +33 -0
  50. package/dist/next/byo/pcm.d.ts.map +1 -0
  51. package/dist/next/byo/resample.d.ts +18 -0
  52. package/dist/next/byo/resample.d.ts.map +1 -0
  53. package/dist/next/index.js +675 -474
  54. package/dist/next/index.js.map +1 -1
  55. package/dist/next/v2/embed/iframeHost.d.ts +16 -1
  56. package/dist/next/v2/embed/iframeHost.d.ts.map +1 -1
  57. package/dist/next/v2/embed/protocol.d.ts +41 -1
  58. package/dist/next/v2/embed/protocol.d.ts.map +1 -1
  59. package/dist/next/v2/experience.d.ts +14 -2
  60. package/dist/next/v2/experience.d.ts.map +1 -1
  61. package/dist/next/v2/speech/captureMediaStreamTrack.d.ts +1 -1
  62. package/dist/next/v2/speech/captureMediaStreamTrack.d.ts.map +1 -1
  63. package/dist/next/v2/speech/speechController.d.ts.map +1 -1
  64. package/dist/next/v2/speech/types.d.ts +13 -0
  65. package/dist/next/v2/speech/types.d.ts.map +1 -1
  66. package/dist/next/widget/mountExperienceWidget.d.ts.map +1 -1
  67. package/dist/next/widget/styles.d.ts +1 -1
  68. package/dist/next/widget/styles.d.ts.map +1 -1
  69. package/dist/npm/byo/audioCapture.d.ts +24 -0
  70. package/dist/npm/byo/audioCapture.d.ts.map +1 -0
  71. package/dist/npm/byo/pcm.d.ts +33 -0
  72. package/dist/npm/byo/pcm.d.ts.map +1 -0
  73. package/dist/npm/byo/resample.d.ts +18 -0
  74. package/dist/npm/byo/resample.d.ts.map +1 -0
  75. package/dist/npm/index.js +700 -499
  76. package/dist/npm/index.js.map +1 -1
  77. package/dist/npm/v2/embed/iframeHost.d.ts +16 -1
  78. package/dist/npm/v2/embed/iframeHost.d.ts.map +1 -1
  79. package/dist/npm/v2/embed/protocol.d.ts +41 -1
  80. package/dist/npm/v2/embed/protocol.d.ts.map +1 -1
  81. package/dist/npm/v2/experience.d.ts +14 -2
  82. package/dist/npm/v2/experience.d.ts.map +1 -1
  83. package/dist/npm/v2/speech/captureMediaStreamTrack.d.ts +1 -1
  84. package/dist/npm/v2/speech/captureMediaStreamTrack.d.ts.map +1 -1
  85. package/dist/npm/v2/speech/speechController.d.ts.map +1 -1
  86. package/dist/npm/v2/speech/types.d.ts +13 -0
  87. package/dist/npm/v2/speech/types.d.ts.map +1 -1
  88. package/dist/npm/widget/mountExperienceWidget.d.ts.map +1 -1
  89. package/dist/npm/widget/styles.d.ts +1 -1
  90. package/dist/npm/widget/styles.d.ts.map +1 -1
  91. package/dist/openai/connectOpenAiRealtime.d.ts +41 -0
  92. package/dist/openai/connectOpenAiRealtime.d.ts.map +1 -0
  93. package/dist/openai/connectOpenAiRealtimeWebRtc.d.ts +37 -0
  94. package/dist/openai/connectOpenAiRealtimeWebRtc.d.ts.map +1 -0
  95. package/dist/openai/index.d.ts +5 -0
  96. package/dist/openai/index.d.ts.map +1 -0
  97. package/dist/openai/index.js +282 -0
  98. package/dist/openai/index.js.map +1 -0
  99. package/dist/react/byo/audioCapture.d.ts +24 -0
  100. package/dist/react/byo/audioCapture.d.ts.map +1 -0
  101. package/dist/react/byo/pcm.d.ts +33 -0
  102. package/dist/react/byo/pcm.d.ts.map +1 -0
  103. package/dist/react/byo/resample.d.ts +18 -0
  104. package/dist/react/byo/resample.d.ts.map +1 -0
  105. package/dist/react/index.js +696 -495
  106. package/dist/react/index.js.map +1 -1
  107. package/dist/react/v2/embed/iframeHost.d.ts +16 -1
  108. package/dist/react/v2/embed/iframeHost.d.ts.map +1 -1
  109. package/dist/react/v2/embed/protocol.d.ts +41 -1
  110. package/dist/react/v2/embed/protocol.d.ts.map +1 -1
  111. package/dist/react/v2/experience.d.ts +14 -2
  112. package/dist/react/v2/experience.d.ts.map +1 -1
  113. package/dist/react/v2/speech/captureMediaStreamTrack.d.ts +1 -1
  114. package/dist/react/v2/speech/captureMediaStreamTrack.d.ts.map +1 -1
  115. package/dist/react/v2/speech/speechController.d.ts.map +1 -1
  116. package/dist/react/v2/speech/types.d.ts +13 -0
  117. package/dist/react/v2/speech/types.d.ts.map +1 -1
  118. package/dist/react/widget/mountExperienceWidget.d.ts.map +1 -1
  119. package/dist/react/widget/styles.d.ts +1 -1
  120. package/dist/react/widget/styles.d.ts.map +1 -1
  121. package/dist/sdk/v2/client.js +37 -10
  122. package/dist/sdk/v2/client.js.map +1 -1
  123. package/package.json +55 -7
@@ -0,0 +1,282 @@
1
+ import { MAX_PCM_CHUNK_BYTES as e, base64ToPcmBytes as t, createTurnSession as n, floatToPcm16Le as r, pcmBytesToBase64 as i, pcmBytesToBase64 as a, resampleFloat32 as o, shouldCaptureMic as s, startPcmMic as c, writePcmInChunks as l } from "@liforma/client/byo";
2
+ //#region src/openai/connectOpenAiRealtime.ts
3
+ var u = "gpt-realtime-2.1", d = "marin", f = 24e3;
4
+ async function p(e, t) {
5
+ let { ephemeralKey: r, model: i = u, voice: o = d, instructions: l = "You are a helpful voice assistant. Keep replies short.", autoResponse: p = !0, captureMic: m = !0, mediaStream: h, onLog: g, onDisconnect: _, onError: v } = t, y = r.trim();
6
+ if (!y) throw Error("Pass ephemeralKey from your server mint route");
7
+ let b = (e, t = "info") => g?.(e, t), x = f, S = null, C = !1, w = !1, T = null, E = n(e, {
8
+ sampleRate: x,
9
+ isEnded: () => C,
10
+ onWriteError: (e) => v?.(e instanceof Error ? e.message : String(e))
11
+ });
12
+ function D(e) {
13
+ !S || S.readyState !== WebSocket.OPEN || S.send(JSON.stringify(e));
14
+ }
15
+ function O(e) {
16
+ let t;
17
+ try {
18
+ t = JSON.parse(e);
19
+ } catch {
20
+ return;
21
+ }
22
+ switch (t.type) {
23
+ case "session.created":
24
+ D({
25
+ type: "session.update",
26
+ session: {
27
+ type: "realtime",
28
+ model: i,
29
+ output_modalities: ["audio"],
30
+ instructions: l,
31
+ audio: {
32
+ input: {
33
+ format: {
34
+ type: "audio/pcm",
35
+ rate: x
36
+ },
37
+ turn_detection: { type: "semantic_vad" }
38
+ },
39
+ output: {
40
+ format: {
41
+ type: "audio/pcm",
42
+ rate: x
43
+ },
44
+ voice: o
45
+ }
46
+ }
47
+ }
48
+ });
49
+ break;
50
+ case "session.updated":
51
+ p && !w && (w = !0, D({ type: "response.create" }), b("Session ready — requesting first assistant turn"));
52
+ break;
53
+ case "response.output_audio.delta":
54
+ typeof t.delta == "string" && t.delta && E.writePcmBase64(t.delta);
55
+ break;
56
+ case "response.output_audio_transcript.delta":
57
+ typeof t.delta == "string" && t.delta && E.setTranscript(t.delta, "append");
58
+ break;
59
+ case "response.output_audio_transcript.done":
60
+ typeof t.transcript == "string" && t.transcript.trim() && E.setTranscript(t.transcript, "replace");
61
+ break;
62
+ case "response.done":
63
+ case "response.output_audio.done":
64
+ E.closeActiveTurn();
65
+ break;
66
+ case "input_audio_buffer.speech_started":
67
+ E.bargeIn();
68
+ break;
69
+ case "error": {
70
+ let e = String(t.error?.message ?? "OpenAI Realtime error");
71
+ v?.(e), b(e, "warn");
72
+ break;
73
+ }
74
+ default: break;
75
+ }
76
+ }
77
+ let k = `wss://api.openai.com/v1/realtime?model=${encodeURIComponent(i)}`;
78
+ if (S = new WebSocket(k, ["realtime", `openai-insecure-api-key.${y}`]), await new Promise((e, t) => {
79
+ if (!S) {
80
+ t(/* @__PURE__ */ Error("WebSocket failed to construct"));
81
+ return;
82
+ }
83
+ let n = S, r = () => {
84
+ a(), e();
85
+ }, i = () => {
86
+ a(), t(/* @__PURE__ */ Error("OpenAI Realtime WebSocket failed to open"));
87
+ }, a = () => {
88
+ n.removeEventListener("open", r), n.removeEventListener("error", i);
89
+ };
90
+ n.addEventListener("open", r), n.addEventListener("error", i);
91
+ }), S.addEventListener("message", (e) => {
92
+ typeof e.data == "string" && O(e.data);
93
+ }), S.addEventListener("close", () => {
94
+ T?.stop(), T = null, S = null, !C && (E.abandonActiveTurn(), b("OpenAI Realtime disconnected"), _?.());
95
+ }), s({
96
+ captureMic: m,
97
+ mediaStream: h
98
+ })) try {
99
+ T = await c({
100
+ targetSampleRate: x,
101
+ mediaStream: h,
102
+ onLog: b,
103
+ shouldSend: () => !C && S != null && S.readyState === WebSocket.OPEN,
104
+ onPcm: (e) => {
105
+ D({
106
+ type: "input_audio_buffer.append",
107
+ audio: a(e)
108
+ });
109
+ }
110
+ });
111
+ } catch (e) {
112
+ S.close();
113
+ let t = e instanceof Error ? e.message : String(e);
114
+ throw Error(`Microphone access failed: ${t}`);
115
+ }
116
+ return b("Realtime connected. Speak into your mic — the avatar should lip-sync."), {
117
+ isConnected: () => {
118
+ let e = S;
119
+ return e != null && e.readyState === WebSocket.OPEN && !C;
120
+ },
121
+ async end() {
122
+ if (C) return;
123
+ C = !0, await E.closeActiveTurn(), T?.stop(), T = null;
124
+ let e = S;
125
+ if (S = null, e && e.readyState === WebSocket.OPEN) try {
126
+ e.close();
127
+ } catch (e) {
128
+ console.error(e);
129
+ }
130
+ }
131
+ };
132
+ }
133
+ //#endregion
134
+ //#region src/openai/connectOpenAiRealtimeWebRtc.ts
135
+ async function m(e, t) {
136
+ let { ephemeralKey: n, model: r = u, voice: i = d, instructions: a = "You are a helpful voice assistant. Keep replies short.", autoResponse: o = !0, captureMic: c = !0, mediaStream: l, sampleRate: p = f, onLog: m, onDisconnect: h, onError: g } = t, _ = n.trim();
137
+ if (!_) throw Error("Pass ephemeralKey from your server mint route");
138
+ let v = (e, t = "info") => m?.(e, t), y = !1, b = !1, x = !1, S = null, C = null, w = "", T = null, E = !l, D = new RTCPeerConnection(), O = D.createDataChannel("oai-events");
139
+ function k(e) {
140
+ O.readyState === "open" && O.send(JSON.stringify(e));
141
+ }
142
+ function A(t) {
143
+ if (y) return;
144
+ C = t;
145
+ let n = w.trim();
146
+ S = e.speech.createUtterance({
147
+ track: t,
148
+ sampleRate: p,
149
+ queue: "replace-active",
150
+ ...n ? { transcript: n } : {}
151
+ });
152
+ }
153
+ function j(e, t) {
154
+ let n = t === "replace" ? e.trim() : `${w}${e}`.trim();
155
+ n && (w = n, S && S.setTranscript(n).catch(() => void 0));
156
+ }
157
+ function M() {
158
+ w = "";
159
+ let t = S;
160
+ S = null, t ? t.cancel() : e.speech.interrupt({ scope: "active" }), C && !y && C.readyState === "live" && A(C);
161
+ }
162
+ function N(e) {
163
+ let t;
164
+ try {
165
+ t = JSON.parse(e);
166
+ } catch {
167
+ return;
168
+ }
169
+ switch (t.type) {
170
+ case "session.created":
171
+ k({
172
+ type: "session.update",
173
+ session: {
174
+ type: "realtime",
175
+ model: r,
176
+ output_modalities: ["audio"],
177
+ instructions: a,
178
+ audio: { output: { voice: i } }
179
+ }
180
+ });
181
+ break;
182
+ case "session.updated":
183
+ o && !b && (b = !0, k({ type: "response.create" }), v("WebRTC session ready — requesting first assistant turn"));
184
+ break;
185
+ case "response.output_audio_transcript.delta":
186
+ typeof t.delta == "string" && t.delta && j(t.delta, "append");
187
+ break;
188
+ case "response.output_audio_transcript.done":
189
+ typeof t.transcript == "string" && t.transcript.trim() && j(t.transcript, "replace");
190
+ break;
191
+ case "input_audio_buffer.speech_started":
192
+ M();
193
+ break;
194
+ case "error": {
195
+ let e = String(t.error?.message ?? "OpenAI Realtime error");
196
+ g?.(e), v(e, "warn");
197
+ break;
198
+ }
199
+ default: break;
200
+ }
201
+ }
202
+ if (D.ontrack = (e) => {
203
+ let t = e.track;
204
+ !t || t.kind !== "audio" || (v("Remote OpenAI audio track received — bridging to avatar"), A(t), t.addEventListener("ended", () => {
205
+ C === t && (C = null);
206
+ let e = S;
207
+ S = null, e?.close({ history: "none" }).catch(() => void 0);
208
+ }));
209
+ }, D.onconnectionstatechange = () => {
210
+ if (D.connectionState === "connected") {
211
+ x = !0;
212
+ return;
213
+ }
214
+ if (D.connectionState === "failed" || D.connectionState === "disconnected" || D.connectionState === "closed") {
215
+ if (x = !1, y) return;
216
+ let e = S;
217
+ S = null, e?.cancel().catch(() => void 0), v("OpenAI Realtime WebRTC disconnected"), h?.();
218
+ }
219
+ }, O.addEventListener("message", (e) => {
220
+ typeof e.data == "string" && N(e.data);
221
+ }), s({
222
+ captureMic: c,
223
+ mediaStream: l
224
+ })) try {
225
+ T = l ?? await navigator.mediaDevices.getUserMedia({ audio: {
226
+ channelCount: 1,
227
+ echoCancellation: !0,
228
+ noiseSuppression: !0
229
+ } });
230
+ for (let e of T.getAudioTracks()) D.addTrack(e, T);
231
+ v("Mic track added to WebRTC peer connection");
232
+ } catch (e) {
233
+ D.close();
234
+ let t = e instanceof Error ? e.message : String(e);
235
+ throw Error(`Microphone access failed: ${t}`);
236
+ }
237
+ let P = await D.createOffer();
238
+ await D.setLocalDescription(P);
239
+ let F = await fetch("https://api.openai.com/v1/realtime/calls", {
240
+ method: "POST",
241
+ body: P.sdp ?? "",
242
+ headers: {
243
+ Authorization: `Bearer ${_}`,
244
+ "Content-Type": "application/sdp"
245
+ }
246
+ });
247
+ if (!F.ok) {
248
+ if (D.close(), E && T) for (let e of T.getTracks()) e.stop();
249
+ let e = await F.text().catch(() => "");
250
+ throw Error(`OpenAI Realtime WebRTC SDP exchange failed (${F.status})${e ? `: ${e}` : ""}`);
251
+ }
252
+ let I = await F.text();
253
+ return await D.setRemoteDescription({
254
+ type: "answer",
255
+ sdp: I
256
+ }), v("Realtime WebRTC connected. Speak into your mic — the avatar should lip-sync."), {
257
+ peerConnection: D,
258
+ isConnected: () => x && !y && D.connectionState === "connected",
259
+ async end() {
260
+ if (y) return;
261
+ y = !0;
262
+ let e = S;
263
+ if (S = null, e) try {
264
+ await e.cancel();
265
+ } catch {}
266
+ try {
267
+ O.close();
268
+ } catch {}
269
+ try {
270
+ D.close();
271
+ } catch (e) {
272
+ console.error(e);
273
+ }
274
+ if (E && T) for (let e of T.getTracks()) e.stop();
275
+ T = null, x = !1;
276
+ }
277
+ };
278
+ }
279
+ //#endregion
280
+ export { u as DEFAULT_OPENAI_REALTIME_MODEL, d as DEFAULT_OPENAI_REALTIME_VOICE, e as MAX_PCM_CHUNK_BYTES, f as OPENAI_REALTIME_PCM_RATE, t as base64ToPcmBytes, p as connectOpenAiRealtime, m as connectOpenAiRealtimeWebRtc, r as floatToPcm16Le, i as pcmBytesToBase64, o as resampleFloat32, l as writePcmInChunks };
281
+
282
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/openai/connectOpenAiRealtime.ts","../../src/openai/connectOpenAiRealtimeWebRtc.ts"],"sourcesContent":["/**\n * OpenAI Realtime (WebSocket) → Liforma BYO voice helper.\n *\n * Mount a Liforma Experience (typically `mode=\"presenter\"`, `speechInputMode=\"off\"`),\n * wait until the player has started (audio unlocked), mint an ephemeral client secret\n * on your server, then call this helper.\n *\n * Why WebSocket here: per-turn PCM + transcript match the ElevenLabs createUtterance\n * pattern. OpenAI’s preferred browser media path is WebRTC — see docs for that path.\n *\n * Docs: https://docs.liforma.ai/avatar-experiences/bring-your-own-voice/openai\n */\n\nimport {\n\tcreateTurnSession,\n\tpcmBytesToBase64,\n\tshouldCaptureMic,\n\tstartPcmMic,\n\ttype ByoBridge,\n\ttype ByoConnectCallbacks,\n\ttype ByoMicCaptureOptions,\n\ttype ByoPcmExperience,\n\ttype PcmMicSession\n} from '@liforma/client/byo';\n\nexport type OpenAiRealtimeExperience = ByoPcmExperience;\n\n/** Default Realtime model when none is passed. */\nexport const DEFAULT_OPENAI_REALTIME_MODEL = 'gpt-realtime-2.1';\n\n/** Default OpenAI Realtime voice when none is passed. */\nexport const DEFAULT_OPENAI_REALTIME_VOICE = 'marin';\n\n/** OpenAI Realtime PCM rate used by this helper. */\nexport const OPENAI_REALTIME_PCM_RATE = 24_000;\n\nexport type ConnectOpenAiRealtimeOptions = ByoConnectCallbacks &\n\tByoMicCaptureOptions & {\n\t\t/** Ephemeral client secret from your backend (never ship OPENAI_API_KEY to browsers). */\n\t\treadonly ephemeralKey: string;\n\t\treadonly model?: string;\n\t\treadonly voice?: string;\n\t\t/** Session instructions applied via `session.update`. */\n\t\treadonly instructions?: string;\n\t\t/**\n\t\t * When true (default), send `response.create` after `session.updated` so the\n\t\t * assistant speaks first. Set false if the host will drive turns itself.\n\t\t */\n\t\treadonly autoResponse?: boolean;\n\t};\n\nexport type OpenAiRealtimeBridge = ByoBridge;\n\n/**\n * Start OpenAI Realtime over WebSocket and pipe agent PCM + transcript into\n * `experience.speech.createUtterance` so the Liforma avatar talks.\n * Captures the host mic by default (Liforma mic stays off).\n */\nexport async function connectOpenAiRealtime(\n\texperience: OpenAiRealtimeExperience,\n\toptions: ConnectOpenAiRealtimeOptions\n): Promise<OpenAiRealtimeBridge> {\n\tconst {\n\t\tephemeralKey,\n\t\tmodel = DEFAULT_OPENAI_REALTIME_MODEL,\n\t\tvoice = DEFAULT_OPENAI_REALTIME_VOICE,\n\t\tinstructions = 'You are a helpful voice assistant. Keep replies short.',\n\t\tautoResponse = true,\n\t\tcaptureMic = true,\n\t\tmediaStream,\n\t\tonLog,\n\t\tonDisconnect,\n\t\tonError\n\t} = options;\n\n\tconst key = ephemeralKey.trim();\n\tif (!key) throw new Error('Pass ephemeralKey from your server mint route');\n\n\tconst log = (line: string, kind: 'info' | 'warn' = 'info') => onLog?.(line, kind);\n\tconst sampleRate = OPENAI_REALTIME_PCM_RATE;\n\n\tlet ws: WebSocket | null = null;\n\tlet ended = false;\n\tlet greeted = false;\n\tlet mic: PcmMicSession | null = null;\n\n\tconst turns = createTurnSession(experience, {\n\t\tsampleRate,\n\t\tisEnded: () => ended,\n\t\tonWriteError: (err) => onError?.(err instanceof Error ? err.message : String(err))\n\t});\n\n\tfunction send(event: Record<string, unknown>): void {\n\t\tif (!ws || ws.readyState !== WebSocket.OPEN) return;\n\t\tws.send(JSON.stringify(event));\n\t}\n\n\tfunction handleServerEvent(raw: string): void {\n\t\tlet event: {\n\t\t\ttype?: string;\n\t\t\tdelta?: string;\n\t\t\ttranscript?: string;\n\t\t\terror?: { message?: string };\n\t\t};\n\t\ttry {\n\t\t\tevent = JSON.parse(raw) as typeof event;\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tswitch (event.type) {\n\t\t\tcase 'session.created':\n\t\t\t\tsend({\n\t\t\t\t\ttype: 'session.update',\n\t\t\t\t\tsession: {\n\t\t\t\t\t\ttype: 'realtime',\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\toutput_modalities: ['audio'],\n\t\t\t\t\t\tinstructions,\n\t\t\t\t\t\taudio: {\n\t\t\t\t\t\t\tinput: {\n\t\t\t\t\t\t\t\tformat: { type: 'audio/pcm', rate: sampleRate },\n\t\t\t\t\t\t\t\tturn_detection: { type: 'semantic_vad' }\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\toutput: {\n\t\t\t\t\t\t\t\tformat: { type: 'audio/pcm', rate: sampleRate },\n\t\t\t\t\t\t\t\tvoice\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tbreak;\n\n\t\t\tcase 'session.updated':\n\t\t\t\tif (autoResponse && !greeted) {\n\t\t\t\t\tgreeted = true;\n\t\t\t\t\tsend({ type: 'response.create' });\n\t\t\t\t\tlog('Session ready — requesting first assistant turn');\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'response.output_audio.delta':\n\t\t\t\tif (typeof event.delta === 'string' && event.delta) {\n\t\t\t\t\tturns.writePcmBase64(event.delta);\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'response.output_audio_transcript.delta':\n\t\t\t\tif (typeof event.delta === 'string' && event.delta) {\n\t\t\t\t\tturns.setTranscript(event.delta, 'append');\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'response.output_audio_transcript.done':\n\t\t\t\tif (typeof event.transcript === 'string' && event.transcript.trim()) {\n\t\t\t\t\tturns.setTranscript(event.transcript, 'replace');\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'response.done':\n\t\t\tcase 'response.output_audio.done':\n\t\t\t\tvoid turns.closeActiveTurn();\n\t\t\t\tbreak;\n\n\t\t\tcase 'input_audio_buffer.speech_started':\n\t\t\t\tturns.bargeIn();\n\t\t\t\tbreak;\n\n\t\t\tcase 'error': {\n\t\t\t\tconst message = String(event.error?.message ?? 'OpenAI Realtime error');\n\t\t\t\tonError?.(message);\n\t\t\t\tlog(message, 'warn');\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst url = `wss://api.openai.com/v1/realtime?model=${encodeURIComponent(model)}`;\n\tws = new WebSocket(url, ['realtime', `openai-insecure-api-key.${key}`]);\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tif (!ws) {\n\t\t\treject(new Error('WebSocket failed to construct'));\n\t\t\treturn;\n\t\t}\n\t\tconst socket = ws;\n\t\tconst onOpen = () => {\n\t\t\tcleanup();\n\t\t\tresolve();\n\t\t};\n\t\tconst onErrorEvt = () => {\n\t\t\tcleanup();\n\t\t\treject(new Error('OpenAI Realtime WebSocket failed to open'));\n\t\t};\n\t\tconst cleanup = () => {\n\t\t\tsocket.removeEventListener('open', onOpen);\n\t\t\tsocket.removeEventListener('error', onErrorEvt);\n\t\t};\n\t\tsocket.addEventListener('open', onOpen);\n\t\tsocket.addEventListener('error', onErrorEvt);\n\t});\n\n\tws.addEventListener('message', (msg) => {\n\t\tif (typeof msg.data === 'string') handleServerEvent(msg.data);\n\t});\n\n\tws.addEventListener('close', () => {\n\t\tmic?.stop();\n\t\tmic = null;\n\t\tws = null;\n\t\t// Skip after intentional end() — turn was already closed cleanly.\n\t\tif (ended) return;\n\t\tturns.abandonActiveTurn();\n\t\tlog('OpenAI Realtime disconnected');\n\t\tonDisconnect?.();\n\t});\n\n\tif (shouldCaptureMic({ captureMic, mediaStream })) {\n\t\ttry {\n\t\t\tmic = await startPcmMic({\n\t\t\t\ttargetSampleRate: sampleRate,\n\t\t\t\tmediaStream,\n\t\t\t\tonLog: log,\n\t\t\t\tshouldSend: () => !ended && ws != null && ws.readyState === WebSocket.OPEN,\n\t\t\t\tonPcm: (pcm) => {\n\t\t\t\t\tsend({\n\t\t\t\t\t\ttype: 'input_audio_buffer.append',\n\t\t\t\t\t\taudio: pcmBytesToBase64(pcm)\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tws.close();\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tthrow new Error(`Microphone access failed: ${message}`);\n\t\t}\n\t}\n\n\tlog('Realtime connected. Speak into your mic — the avatar should lip-sync.');\n\n\treturn {\n\t\tisConnected: () => {\n\t\t\tconst socket = ws;\n\t\t\treturn socket != null && socket.readyState === WebSocket.OPEN && !ended;\n\t\t},\n\t\tasync end() {\n\t\t\tif (ended) return;\n\t\t\tended = true;\n\t\t\tawait turns.closeActiveTurn();\n\t\t\tmic?.stop();\n\t\t\tmic = null;\n\t\t\tconst active = ws;\n\t\t\tws = null;\n\t\t\tif (active && active.readyState === WebSocket.OPEN) {\n\t\t\t\ttry {\n\t\t\t\t\tactive.close();\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(err);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n}\n","/**\n * OpenAI Realtime (WebRTC) → Liforma BYO voice helper.\n *\n * Preferred browser media path: remote audio track → `createUtterance({ track })`,\n * transcript side-channel on the `oai-events` data channel for force-align lipsync.\n * Do **not** also attach the remote track to an `<audio>` element.\n *\n * Docs: https://docs.liforma.ai/avatar-experiences/bring-your-own-voice/openai\n */\n\nimport {\n\tshouldCaptureMic,\n\ttype ByoBridge,\n\ttype ByoConnectCallbacks,\n\ttype ByoMicCaptureOptions,\n\ttype ByoPcmExperience,\n\ttype ByoSpeechUtterance\n} from '@liforma/client/byo';\nimport {\n\tDEFAULT_OPENAI_REALTIME_MODEL,\n\tDEFAULT_OPENAI_REALTIME_VOICE,\n\tOPENAI_REALTIME_PCM_RATE,\n\ttype OpenAiRealtimeExperience\n} from './connectOpenAiRealtime';\n\nexport type ConnectOpenAiRealtimeWebRtcOptions = ByoConnectCallbacks &\n\tByoMicCaptureOptions & {\n\t\t/** Ephemeral client secret from your backend (never ship OPENAI_API_KEY to browsers). */\n\t\treadonly ephemeralKey: string;\n\t\treadonly model?: string;\n\t\treadonly voice?: string;\n\t\t/** Session instructions applied via data-channel `session.update`. */\n\t\treadonly instructions?: string;\n\t\t/**\n\t\t * When true (default), send `response.create` after `session.updated` so the\n\t\t * assistant speaks first.\n\t\t */\n\t\treadonly autoResponse?: boolean;\n\t\t/** Sample-rate hint for track capture (OpenAI Realtime audio is typically 24 kHz). */\n\t\treadonly sampleRate?: number;\n\t};\n\nexport type OpenAiRealtimeWebRtcBridge = ByoBridge & {\n\treadonly peerConnection: RTCPeerConnection;\n};\n\n/**\n * Start OpenAI Realtime over WebRTC and pipe the remote audio track + transcript\n * into `experience.speech.createUtterance` so the Liforma avatar talks.\n */\nexport async function connectOpenAiRealtimeWebRtc(\n\texperience: OpenAiRealtimeExperience,\n\toptions: ConnectOpenAiRealtimeWebRtcOptions\n): Promise<OpenAiRealtimeWebRtcBridge> {\n\tconst {\n\t\tephemeralKey,\n\t\tmodel = DEFAULT_OPENAI_REALTIME_MODEL,\n\t\tvoice = DEFAULT_OPENAI_REALTIME_VOICE,\n\t\tinstructions = 'You are a helpful voice assistant. Keep replies short.',\n\t\tautoResponse = true,\n\t\tcaptureMic = true,\n\t\tmediaStream,\n\t\tsampleRate = OPENAI_REALTIME_PCM_RATE,\n\t\tonLog,\n\t\tonDisconnect,\n\t\tonError\n\t} = options;\n\n\tconst key = ephemeralKey.trim();\n\tif (!key) throw new Error('Pass ephemeralKey from your server mint route');\n\n\tconst log = (line: string, kind: 'info' | 'warn' = 'info') => onLog?.(line, kind);\n\n\tlet ended = false;\n\tlet greeted = false;\n\tlet connected = false;\n\tlet utterance: ByoSpeechUtterance | null = null;\n\tlet remoteTrack: MediaStreamTrack | null = null;\n\tlet activeTranscript = '';\n\tlet localStream: MediaStream | null = null;\n\tconst stopLocalTracks = !mediaStream;\n\n\tconst pc = new RTCPeerConnection();\n\tconst dc = pc.createDataChannel('oai-events');\n\n\tfunction send(event: Record<string, unknown>): void {\n\t\tif (dc.readyState !== 'open') return;\n\t\tdc.send(JSON.stringify(event));\n\t}\n\n\tfunction openUtteranceForTrack(track: MediaStreamTrack): void {\n\t\tif (ended) return;\n\t\tremoteTrack = track;\n\t\tconst seed = activeTranscript.trim();\n\t\tutterance = experience.speech.createUtterance({\n\t\t\ttrack,\n\t\t\tsampleRate,\n\t\t\tqueue: 'replace-active',\n\t\t\t...(seed ? { transcript: seed } : {})\n\t\t});\n\t}\n\n\tfunction setTranscript(text: string, mode: 'replace' | 'append'): void {\n\t\tconst next =\n\t\t\tmode === 'replace' ? text.trim() : `${activeTranscript}${text}`.trim();\n\t\tif (!next) return;\n\t\tactiveTranscript = next;\n\t\tif (!utterance) return;\n\t\tvoid utterance.setTranscript(next).catch(() => undefined);\n\t}\n\n\tfunction bargeIn(): void {\n\t\tactiveTranscript = '';\n\t\tconst current = utterance;\n\t\tutterance = null;\n\t\tvoid (current\n\t\t\t? current.cancel()\n\t\t\t: experience.speech.interrupt({ scope: 'active' }));\n\t\t// Continuous remote track — reopen so the next agent audio still lip-syncs.\n\t\tif (remoteTrack && !ended && remoteTrack.readyState === 'live') {\n\t\t\topenUtteranceForTrack(remoteTrack);\n\t\t}\n\t}\n\n\tfunction handleServerEvent(raw: string): void {\n\t\tlet event: {\n\t\t\ttype?: string;\n\t\t\tdelta?: string;\n\t\t\ttranscript?: string;\n\t\t\terror?: { message?: string };\n\t\t};\n\t\ttry {\n\t\t\tevent = JSON.parse(raw) as typeof event;\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tswitch (event.type) {\n\t\t\tcase 'session.created':\n\t\t\t\tsend({\n\t\t\t\t\ttype: 'session.update',\n\t\t\t\t\tsession: {\n\t\t\t\t\t\ttype: 'realtime',\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\toutput_modalities: ['audio'],\n\t\t\t\t\t\tinstructions,\n\t\t\t\t\t\taudio: {\n\t\t\t\t\t\t\toutput: { voice }\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tbreak;\n\n\t\t\tcase 'session.updated':\n\t\t\t\tif (autoResponse && !greeted) {\n\t\t\t\t\tgreeted = true;\n\t\t\t\t\tsend({ type: 'response.create' });\n\t\t\t\t\tlog('WebRTC session ready — requesting first assistant turn');\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'response.output_audio_transcript.delta':\n\t\t\t\tif (typeof event.delta === 'string' && event.delta) {\n\t\t\t\t\tsetTranscript(event.delta, 'append');\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'response.output_audio_transcript.done':\n\t\t\t\tif (typeof event.transcript === 'string' && event.transcript.trim()) {\n\t\t\t\t\tsetTranscript(event.transcript, 'replace');\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase 'input_audio_buffer.speech_started':\n\t\t\t\tbargeIn();\n\t\t\t\tbreak;\n\n\t\t\tcase 'error': {\n\t\t\t\tconst message = String(event.error?.message ?? 'OpenAI Realtime error');\n\t\t\t\tonError?.(message);\n\t\t\t\tlog(message, 'warn');\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tpc.ontrack = (ev) => {\n\t\tconst track = ev.track;\n\t\tif (!track || track.kind !== 'audio') return;\n\t\tlog('Remote OpenAI audio track received — bridging to avatar');\n\t\topenUtteranceForTrack(track);\n\t\ttrack.addEventListener('ended', () => {\n\t\t\tif (remoteTrack === track) remoteTrack = null;\n\t\t\tconst current = utterance;\n\t\t\tutterance = null;\n\t\t\tvoid current?.close({ history: 'none' }).catch(() => undefined);\n\t\t});\n\t};\n\n\tpc.onconnectionstatechange = () => {\n\t\tif (pc.connectionState === 'connected') {\n\t\t\tconnected = true;\n\t\t\treturn;\n\t\t}\n\t\tif (\n\t\t\tpc.connectionState === 'failed' ||\n\t\t\tpc.connectionState === 'disconnected' ||\n\t\t\tpc.connectionState === 'closed'\n\t\t) {\n\t\t\tconnected = false;\n\t\t\tif (ended) return;\n\t\t\tconst current = utterance;\n\t\t\tutterance = null;\n\t\t\tvoid current?.cancel().catch(() => undefined);\n\t\t\tlog('OpenAI Realtime WebRTC disconnected');\n\t\t\tonDisconnect?.();\n\t\t}\n\t};\n\n\tdc.addEventListener('message', (ev) => {\n\t\tif (typeof ev.data === 'string') handleServerEvent(ev.data);\n\t});\n\n\tif (shouldCaptureMic({ captureMic, mediaStream })) {\n\t\ttry {\n\t\t\tlocalStream =\n\t\t\t\tmediaStream ??\n\t\t\t\t(await navigator.mediaDevices.getUserMedia({\n\t\t\t\t\taudio: {\n\t\t\t\t\t\tchannelCount: 1,\n\t\t\t\t\t\techoCancellation: true,\n\t\t\t\t\t\tnoiseSuppression: true\n\t\t\t\t\t}\n\t\t\t\t}));\n\t\t\tfor (const track of localStream.getAudioTracks()) {\n\t\t\t\tpc.addTrack(track, localStream);\n\t\t\t}\n\t\t\tlog('Mic track added to WebRTC peer connection');\n\t\t} catch (err) {\n\t\t\tpc.close();\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tthrow new Error(`Microphone access failed: ${message}`);\n\t\t}\n\t}\n\n\tconst offer = await pc.createOffer();\n\tawait pc.setLocalDescription(offer);\n\n\tconst sdpResponse = await fetch('https://api.openai.com/v1/realtime/calls', {\n\t\tmethod: 'POST',\n\t\tbody: offer.sdp ?? '',\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${key}`,\n\t\t\t'Content-Type': 'application/sdp'\n\t\t}\n\t});\n\n\tif (!sdpResponse.ok) {\n\t\tpc.close();\n\t\tif (stopLocalTracks && localStream) {\n\t\t\tfor (const track of localStream.getTracks()) track.stop();\n\t\t}\n\t\tconst detail = await sdpResponse.text().catch(() => '');\n\t\tthrow new Error(\n\t\t\t`OpenAI Realtime WebRTC SDP exchange failed (${sdpResponse.status})${detail ? `: ${detail}` : ''}`\n\t\t);\n\t}\n\n\tconst answerSdp = await sdpResponse.text();\n\tawait pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });\n\n\tlog('Realtime WebRTC connected. Speak into your mic — the avatar should lip-sync.');\n\n\treturn {\n\t\tpeerConnection: pc,\n\t\tisConnected: () => connected && !ended && pc.connectionState === 'connected',\n\t\tasync end() {\n\t\t\tif (ended) return;\n\t\t\tended = true;\n\t\t\tconst current = utterance;\n\t\t\tutterance = null;\n\t\t\tif (current) {\n\t\t\t\ttry {\n\t\t\t\t\tawait current.cancel();\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tdc.close();\n\t\t\t} catch {\n\t\t\t\t/* ignore */\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tpc.close();\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(err);\n\t\t\t}\n\t\t\tif (stopLocalTracks && localStream) {\n\t\t\t\tfor (const track of localStream.getTracks()) track.stop();\n\t\t\t}\n\t\t\tlocalStream = null;\n\t\t\tconnected = false;\n\t\t}\n\t};\n}\n\n/** @deprecated Prefer OpenAiRealtimeExperience — same duck type. */\nexport type OpenAiRealtimeWebRtcExperience = ByoPcmExperience;\n"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared Web Audio capture: AudioWorklet when available, ScriptProcessor fallback.
3
+ * Used by BYO mic uplink and MediaStreamTrack → PCM piping.
4
+ */
5
+ export type AudioCaptureSession = {
6
+ stop(): void;
7
+ readonly fromSampleRate: number;
8
+ readonly usingWorklet: boolean;
9
+ };
10
+ export type StartAudioCaptureOptions = {
11
+ readonly mediaStream: MediaStream;
12
+ readonly targetSampleRate: number;
13
+ /** Called with pcm_s16le mono at targetSampleRate. */
14
+ readonly onPcm: (bytes: Uint8Array) => void;
15
+ /** When false, frames are dropped. */
16
+ readonly shouldSend?: () => boolean;
17
+ readonly onLog?: (line: string, kind?: 'info' | 'warn') => void;
18
+ };
19
+ /**
20
+ * Capture a MediaStream as pcm_s16le at targetSampleRate.
21
+ * Prefers AudioWorklet; falls back to ScriptProcessorNode.
22
+ */
23
+ export declare function startAudioCapture(options: StartAudioCaptureOptions): Promise<AudioCaptureSession>;
24
+ //# sourceMappingURL=audioCapture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audioCapture.d.ts","sourceRoot":"","sources":["../../../src/byo/audioCapture.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,MAAM,MAAM,mBAAmB,GAAG;IACjC,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACtC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC5C,sCAAsC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;CAChE,CAAC;AAsCF;;;GAGG;AACH,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,wBAAwB,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAoI9B"}
@@ -0,0 +1,33 @@
1
+ /** Liforma `speech.write` rejects chunks larger than 64 KiB. */
2
+ export declare const MAX_PCM_CHUNK_BYTES: number;
3
+ /**
4
+ * Playback / chunk guidance for custom BYO bridges (mirrors speech controller limits).
5
+ * Prefer ~20–200 ms PCM frames; the player starts after an adaptive ~80–250 ms jitter
6
+ * buffer (default 150 ms) based on chunk arrival variance.
7
+ */
8
+ export declare const BYO_PLAYBACK_GUIDANCE: {
9
+ readonly RECOMMENDED_CHUNK_MS_MIN: 20;
10
+ readonly RECOMMENDED_CHUNK_MS_MAX: 200;
11
+ readonly PLAYBACK_JITTER_BUFFER_MS: 150;
12
+ readonly PLAYBACK_JITTER_BUFFER_MS_MIN: 80;
13
+ readonly PLAYBACK_JITTER_BUFFER_MS_MAX: 250;
14
+ };
15
+ export declare function base64ToPcmBytes(b64: string): Uint8Array;
16
+ export declare function pcmBytesToBase64(bytes: Uint8Array): string;
17
+ export declare function floatToPcm16Le(input: Float32Array): Uint8Array;
18
+ /** Naive linear resample Float32 → target rate. */
19
+ export declare function resampleFloat32(input: Float32Array, fromRate: number, toRate: number): Float32Array;
20
+ /**
21
+ * Parse vendor format strings like `pcm_16000` / `pcm_24000`.
22
+ * Returns null when the format is missing or not PCM.
23
+ */
24
+ export declare function parsePcmSampleRate(format: string | null | undefined): number | null;
25
+ /**
26
+ * Write even-length PCM, splitting at MAX_PCM_CHUNK_BYTES on even boundaries.
27
+ * Callers that may receive odd-length frames should run bytes through
28
+ * {@link takeEvenPcmBytes} first so samples are not dropped across chunks.
29
+ */
30
+ export declare function writePcmInChunks(utterance: {
31
+ write: (chunk: ArrayBuffer | ArrayBufferView) => Promise<unknown>;
32
+ }, bytes: Uint8Array): Promise<void>;
33
+ //# sourceMappingURL=pcm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pcm.d.ts","sourceRoot":"","sources":["../../../src/byo/pcm.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,eAAO,MAAM,mBAAmB,QAAY,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;;;;;CAMxB,CAAC;AAEX,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAKxD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAO1D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,UAAU,CAQ9D;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,CAanG;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAKnF;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACrC,SAAS,EAAE;IAAE,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,GAAG,eAAe,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAChF,KAAK,EAAE,UAAU,GACf,OAAO,CAAC,IAAI,CAAC,CAmBf"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Stateful cubic (Catmull-Rom) resampler for continuous PCM capture streams.
3
+ * Better than naive linear for uplink mic / track capture.
4
+ */
5
+ export type FloatResampler = {
6
+ /** Push a mono float buffer; returns samples at the target rate (may be empty). */
7
+ push(input: Float32Array): Float32Array;
8
+ /** Flush any remaining fractional sample (usually silence). */
9
+ flush(): Float32Array;
10
+ readonly fromRate: number;
11
+ readonly toRate: number;
12
+ };
13
+ /**
14
+ * Create a continuous resampler. When rates match, push() returns the input as-is
15
+ * (no copy) when possible.
16
+ */
17
+ export declare function createCubicResampler(fromRate: number, toRate: number): FloatResampler;
18
+ //# sourceMappingURL=resample.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resample.d.ts","sourceRoot":"","sources":["../../../src/byo/resample.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC5B,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAAC;IACxC,+DAA+D;IAC/D,KAAK,IAAI,YAAY,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB,CAAC;AAUF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAkErF"}