@kuralle-syrinx/browser-client 4.4.0 → 4.5.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 (52) hide show
  1. package/dist/agent-state.d.ts +35 -0
  2. package/dist/agent-state.d.ts.map +1 -0
  3. package/dist/agent-state.js +76 -0
  4. package/dist/agent-state.js.map +1 -0
  5. package/dist/audio.d.ts +61 -0
  6. package/dist/audio.d.ts.map +1 -0
  7. package/dist/audio.js +276 -0
  8. package/dist/audio.js.map +1 -0
  9. package/dist/browser-opus.d.ts +41 -0
  10. package/dist/browser-opus.d.ts.map +1 -0
  11. package/dist/browser-opus.js +81 -0
  12. package/dist/browser-opus.js.map +1 -0
  13. package/dist/index.d.ts +270 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +744 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/session-metrics.d.ts +28 -0
  18. package/dist/session-metrics.d.ts.map +1 -0
  19. package/dist/session-metrics.js +60 -0
  20. package/dist/session-metrics.js.map +1 -0
  21. package/dist/session-record.d.ts +112 -0
  22. package/dist/session-record.d.ts.map +1 -0
  23. package/dist/session-record.js +181 -0
  24. package/dist/session-record.js.map +1 -0
  25. package/dist/transport.d.ts +16 -0
  26. package/dist/transport.d.ts.map +1 -0
  27. package/dist/transport.js +3 -0
  28. package/dist/transport.js.map +1 -0
  29. package/dist/turn-recorder.d.ts +56 -0
  30. package/dist/turn-recorder.d.ts.map +1 -0
  31. package/dist/turn-recorder.js +168 -0
  32. package/dist/turn-recorder.js.map +1 -0
  33. package/dist/turn-timeline.d.ts +51 -0
  34. package/dist/turn-timeline.d.ts.map +1 -0
  35. package/dist/turn-timeline.js +92 -0
  36. package/dist/turn-timeline.js.map +1 -0
  37. package/dist/websocket-transport.d.ts +20 -0
  38. package/dist/websocket-transport.d.ts.map +1 -0
  39. package/dist/websocket-transport.js +92 -0
  40. package/dist/websocket-transport.js.map +1 -0
  41. package/package.json +35 -11
  42. package/src/agent-state.test.ts +143 -0
  43. package/src/audio.test.ts +130 -0
  44. package/src/index.test.ts +842 -0
  45. package/src/jitter-buffer.test.ts +275 -0
  46. package/src/session-metrics.test.ts +102 -0
  47. package/src/session-record.test.ts +219 -0
  48. package/src/studio-page.test.ts +69 -0
  49. package/src/transport.test.ts +233 -0
  50. package/src/turn-recorder.test.ts +138 -0
  51. package/src/turn-timeline.test.ts +151 -0
  52. package/index.html +0 -1073
@@ -0,0 +1,35 @@
1
+ import type { SyrinxStudioMessage } from "./index.js";
2
+ export type AgentState = "idle" | "listening" | "endpointing" | "thinking" | "speaking" | "interrupted";
3
+ export interface AgentStateSnapshot {
4
+ readonly state: AgentState;
5
+ /** ms the state has been held, from the caller's clock. Lets a UI flag a stuck state. */
6
+ readonly sinceMs: number;
7
+ readonly turnId?: string;
8
+ }
9
+ type AnyMessage = {
10
+ readonly type: string;
11
+ readonly [k: string]: unknown;
12
+ };
13
+ /**
14
+ * Fold one message into the state. Pure; the caller supplies the clock so this
15
+ * stays deterministic under test.
16
+ *
17
+ * Ordering note: `speech_started` wins over everything, because a caller talking
18
+ * is the highest-priority fact on the wire — that is also what makes barge-in
19
+ * legible (speaking → listening without waiting for the interrupt message).
20
+ */
21
+ export declare function nextAgentState(prev: AgentStateSnapshot, message: SyrinxStudioMessage | AnyMessage, atMs: number): AgentStateSnapshot;
22
+ export declare const INITIAL_AGENT_STATE: AgentStateSnapshot;
23
+ /** Fold a whole stream. Equivalent to repeated `nextAgentState`. */
24
+ export declare function deriveAgentState(messages: readonly {
25
+ readonly message: SyrinxStudioMessage | AnyMessage;
26
+ readonly atMs: number;
27
+ }[]): AgentStateSnapshot;
28
+ /**
29
+ * How long a state may plausibly be held before it indicates a stall. A UI should
30
+ * flag rather than hide these — a 30s "thinking" is a hung tool call, not patience.
31
+ */
32
+ export declare const STATE_STALL_THRESHOLD_MS: Readonly<Record<AgentState, number>>;
33
+ export declare function isStalled(snapshot: AgentStateSnapshot, nowMs: number): boolean;
34
+ export {};
35
+ //# sourceMappingURL=agent-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-state.d.ts","sourceRoot":"","sources":["../src/agent-state.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,WAAW,GACX,aAAa,GACb,UAAU,GACV,UAAU,GACV,aAAa,CAAC;AAElB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,KAAK,UAAU,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAE3E;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,mBAAmB,GAAG,UAAU,EACzC,IAAI,EAAE,MAAM,GACX,kBAAkB,CAoCpB;AAED,eAAO,MAAM,mBAAmB,EAAE,kBAAkD,CAAC;AAErF,oEAAoE;AACpE,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,SAAS;IAAE,QAAQ,CAAC,OAAO,EAAE,mBAAmB,GAAG,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GACjG,kBAAkB,CAEpB;AAED;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAOzE,CAAC;AAEF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9E"}
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // Conversational agent state, derived on the client from messages the server
4
+ // already sends. Distinct from `SessionState` in core, which is session
5
+ // *lifecycle* (uninitialized/ready/closed) — this is what the caller experiences:
6
+ // is it listening to me, thinking, or talking?
7
+ //
8
+ // A pause reads as "broken" without this. It is what a client renders as a
9
+ // listening indicator and what makes a turn timeline legible.
10
+ //
11
+ // Derived rather than sent: every signal already exists on the wire, so this
12
+ // needs no server change and works identically on Node and Workers.
13
+ /**
14
+ * Fold one message into the state. Pure; the caller supplies the clock so this
15
+ * stays deterministic under test.
16
+ *
17
+ * Ordering note: `speech_started` wins over everything, because a caller talking
18
+ * is the highest-priority fact on the wire — that is also what makes barge-in
19
+ * legible (speaking → listening without waiting for the interrupt message).
20
+ */
21
+ export function nextAgentState(prev, message, atMs) {
22
+ const type = typeof message.type === "string" ? message.type : "";
23
+ const turnId = typeof message.turnId === "string" ? message.turnId : prev.turnId;
24
+ const to = (state) => state === prev.state ? { ...prev, turnId } : { state, sinceMs: atMs, turnId };
25
+ switch (type) {
26
+ case "speech_started":
27
+ return to("listening");
28
+ case "speech_ended":
29
+ // Speech stopped, but the turn is not necessarily over — this is exactly
30
+ // the window where a premature endpoint cuts a caller off.
31
+ return to("endpointing");
32
+ case "stt_output":
33
+ case "turn_complete":
34
+ // A final transcript means the turn closed; the reasoner is now running.
35
+ return to("thinking");
36
+ case "agent_tool_call":
37
+ case "tool_call_started":
38
+ case "tool_call_delayed":
39
+ return to("thinking");
40
+ case "agent_chunk":
41
+ // Text is arriving but nothing is audible yet.
42
+ return prev.state === "speaking" ? to("speaking") : to("thinking");
43
+ case "tts_chunk":
44
+ return to("speaking");
45
+ case "agent_interrupted":
46
+ return to("interrupted");
47
+ case "tts_end":
48
+ case "agent_end":
49
+ // Only settle to idle from a talking state; an interrupt stands until the
50
+ // next turn starts, so the UI can show what happened.
51
+ return prev.state === "speaking" ? to("idle") : prev;
52
+ default:
53
+ return prev;
54
+ }
55
+ }
56
+ export const INITIAL_AGENT_STATE = { state: "idle", sinceMs: 0 };
57
+ /** Fold a whole stream. Equivalent to repeated `nextAgentState`. */
58
+ export function deriveAgentState(messages) {
59
+ return messages.reduce((s, { message, atMs }) => nextAgentState(s, message, atMs), INITIAL_AGENT_STATE);
60
+ }
61
+ /**
62
+ * How long a state may plausibly be held before it indicates a stall. A UI should
63
+ * flag rather than hide these — a 30s "thinking" is a hung tool call, not patience.
64
+ */
65
+ export const STATE_STALL_THRESHOLD_MS = {
66
+ idle: Number.POSITIVE_INFINITY,
67
+ listening: 60_000,
68
+ endpointing: 5_000,
69
+ thinking: 30_000,
70
+ speaking: 120_000,
71
+ interrupted: 10_000,
72
+ };
73
+ export function isStalled(snapshot, nowMs) {
74
+ return nowMs - snapshot.sinceMs > STATE_STALL_THRESHOLD_MS[snapshot.state];
75
+ }
76
+ //# sourceMappingURL=agent-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-state.js","sourceRoot":"","sources":["../src/agent-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import { type SyrinxAudioEnvelopeHeader } from "@kuralle-syrinx/core";
2
+ import type { BrowserOpusCodec } from "./browser-opus.js";
3
+ export interface ResampleFloat32Options {
4
+ readonly fromSampleRateHz: number;
5
+ readonly toSampleRateHz: number;
6
+ }
7
+ export interface EncodeBrowserAudioOptions extends ResampleFloat32Options {
8
+ readonly contextId?: string;
9
+ readonly sequence?: number;
10
+ }
11
+ export interface SyrinxAudioJsonFrame {
12
+ readonly type: "audio";
13
+ readonly audio: string;
14
+ readonly sampleRateHz: number;
15
+ readonly contextId?: string;
16
+ readonly sequence?: number;
17
+ }
18
+ export interface BrowserAssistantAudio {
19
+ readonly data: ArrayBuffer;
20
+ readonly metadata?: SyrinxAudioEnvelopeHeader;
21
+ }
22
+ export interface AudioJitterBufferOptions {
23
+ readonly targetBufferMs?: number;
24
+ readonly sampleRateHz: number;
25
+ }
26
+ export declare function resampleFloat32Linear(input: Float32Array, options: ResampleFloat32Options): Float32Array;
27
+ export declare function float32ToPcm16(samples: Float32Array): Int16Array;
28
+ export declare function encodeBrowserAudioFrame(input: Float32Array, options: EncodeBrowserAudioOptions): SyrinxAudioJsonFrame;
29
+ export declare function encodeBrowserAudioEnvelopeFrame(input: Float32Array, options: EncodeBrowserAudioOptions): Uint8Array;
30
+ export declare function pcm16FrameSampleCount(sampleRateHz: number, frameDurationMs?: number): number;
31
+ export declare function decodeBrowserAssistantAudio(input: ArrayBuffer | ArrayBufferView, opusCodec?: BrowserOpusCodec | null): BrowserAssistantAudio;
32
+ export declare function bytesToBase64(bytes: Uint8Array): string;
33
+ export declare class AudioJitterBuffer {
34
+ private readonly context;
35
+ private readonly scheduledFrames;
36
+ private nextScheduledTime;
37
+ private readonly targetBufferMs;
38
+ private readonly sampleRateHz;
39
+ private readonly contextIds;
40
+ private lastEnqueuedContextId;
41
+ private readonly playoutWindow;
42
+ /** True while assistant audio is scheduled or playing on the playout clock (not the generation clock). */
43
+ get isPlayingOut(): boolean;
44
+ /**
45
+ * Milliseconds of `contextId`'s audio that have actually played out of the
46
+ * speaker so far, clamped to what has been scheduled. This is the played-out
47
+ * clock the server needs to truncate assistant history to what was heard.
48
+ */
49
+ playedOutMs(contextId: string): number;
50
+ /** True once all of `contextId`'s scheduled audio has finished playing. */
51
+ isPlayoutComplete(contextId: string): boolean;
52
+ /** ContextId of the most recently scheduled assistant audio while any audio is still playing out. */
53
+ get activeContextId(): string | null;
54
+ constructor(context: AudioContext, options: AudioJitterBufferOptions);
55
+ enqueue(pcm16Data: ArrayBuffer, contextId?: string): void;
56
+ clear(contextId?: string): void;
57
+ get bufferedDurationMs(): number;
58
+ get activeContextIds(): readonly string[];
59
+ private recomputeNextScheduledTime;
60
+ }
61
+ //# sourceMappingURL=audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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"}
package/dist/audio.js ADDED
@@ -0,0 +1,276 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { decodeSyrinxAudioEnvelope, encodeSyrinxAudioEnvelope, hasSyrinxAudioEnvelope, } from "@kuralle-syrinx/core";
3
+ import { pcm16SamplesToBytes } from "@kuralle-syrinx/core/audio";
4
+ export function resampleFloat32Linear(input, options) {
5
+ const fromRate = readPositiveSampleRate(options.fromSampleRateHz, "fromSampleRateHz");
6
+ const toRate = readPositiveSampleRate(options.toSampleRateHz, "toSampleRateHz");
7
+ if (fromRate === toRate)
8
+ return input;
9
+ const outLength = Math.max(1, Math.round((input.length * toRate) / fromRate));
10
+ const output = new Float32Array(outLength);
11
+ const ratio = fromRate / toRate;
12
+ for (let i = 0; i < outLength; i += 1) {
13
+ const src = i * ratio;
14
+ const lo = Math.floor(src);
15
+ const hi = Math.min(input.length - 1, lo + 1);
16
+ const frac = src - lo;
17
+ output[i] = input[lo] * (1 - frac) + input[hi] * frac;
18
+ }
19
+ return output;
20
+ }
21
+ export function float32ToPcm16(samples) {
22
+ const pcm = new Int16Array(samples.length);
23
+ for (let i = 0; i < samples.length; i += 1) {
24
+ const clamped = Math.max(-1, Math.min(1, samples[i]));
25
+ pcm[i] = clamped < 0 ? Math.round(clamped * 32768) : Math.round(clamped * 32767);
26
+ }
27
+ return pcm;
28
+ }
29
+ export function encodeBrowserAudioFrame(input, options) {
30
+ const targetRate = readPositiveSampleRate(options.toSampleRateHz, "toSampleRateHz");
31
+ const resampled = resampleFloat32Linear(input, options);
32
+ const pcm = float32ToPcm16(resampled);
33
+ return {
34
+ type: "audio",
35
+ contextId: options.contextId,
36
+ sequence: options.sequence,
37
+ sampleRateHz: targetRate,
38
+ audio: bytesToBase64(new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength)),
39
+ };
40
+ }
41
+ export function encodeBrowserAudioEnvelopeFrame(input, options) {
42
+ const targetRate = readPositiveSampleRate(options.toSampleRateHz, "toSampleRateHz");
43
+ const resampled = resampleFloat32Linear(input, options);
44
+ const pcm = float32ToPcm16(resampled);
45
+ const audio = new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength);
46
+ return encodeSyrinxAudioEnvelope({
47
+ type: "audio",
48
+ contextId: options.contextId,
49
+ sampleRateHz: targetRate,
50
+ sequence: options.sequence,
51
+ encoding: "pcm_s16le",
52
+ channels: 1,
53
+ byteLength: audio.byteLength,
54
+ durationMs: Math.round((pcm.length / targetRate) * 1000),
55
+ }, audio);
56
+ }
57
+ export function pcm16FrameSampleCount(sampleRateHz, frameDurationMs = 20) {
58
+ const sampleRate = readPositiveSampleRate(sampleRateHz, "sampleRateHz");
59
+ if (!Number.isFinite(frameDurationMs) || frameDurationMs <= 0) {
60
+ throw new Error("frameDurationMs must be a positive number");
61
+ }
62
+ return Math.max(1, Math.round(sampleRate * (frameDurationMs / 1000)));
63
+ }
64
+ export function decodeBrowserAssistantAudio(input, opusCodec = null) {
65
+ const bytes = ArrayBuffer.isView(input)
66
+ ? new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
67
+ : new Uint8Array(input);
68
+ if (!hasSyrinxAudioEnvelope(bytes)) {
69
+ return { data: copyArrayBuffer(bytes) };
70
+ }
71
+ const envelope = decodeSyrinxAudioEnvelope(bytes);
72
+ if (envelope.header.encoding === "opus") {
73
+ if (!opusCodec) {
74
+ return { data: new ArrayBuffer(0), metadata: envelope.header };
75
+ }
76
+ const wireRate = envelope.header.sampleRateHz;
77
+ const decoded = opusCodec.decodeOpusFrame(envelope.audio);
78
+ const pcm = pcm16SamplesToBytes(decoded);
79
+ return {
80
+ data: copyArrayBuffer(pcm),
81
+ metadata: {
82
+ ...envelope.header,
83
+ encoding: "pcm_s16le",
84
+ sampleRateHz: wireRate,
85
+ byteLength: pcm.byteLength,
86
+ },
87
+ };
88
+ }
89
+ return {
90
+ data: copyArrayBuffer(envelope.audio),
91
+ metadata: envelope.header,
92
+ };
93
+ }
94
+ export function bytesToBase64(bytes) {
95
+ let binary = "";
96
+ for (let i = 0; i < bytes.length; i += 0x8000) {
97
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
98
+ }
99
+ return btoa(binary);
100
+ }
101
+ function readPositiveSampleRate(value, name) {
102
+ if (!Number.isFinite(value) || value <= 0)
103
+ throw new Error(`${name} must be a positive sample rate`);
104
+ return value;
105
+ }
106
+ function copyArrayBuffer(bytes) {
107
+ const copy = new Uint8Array(bytes.byteLength);
108
+ copy.set(bytes);
109
+ return copy.buffer;
110
+ }
111
+ export class AudioJitterBuffer {
112
+ context;
113
+ scheduledFrames = new Set();
114
+ nextScheduledTime = 0;
115
+ targetBufferMs;
116
+ sampleRateHz;
117
+ contextIds = new Set();
118
+ lastEnqueuedContextId = null;
119
+ // Per-context playout window on the AudioContext clock: the scheduledTime of the
120
+ // first frame (start) and the running end (start + all scheduled durations). Lets
121
+ // us report how many ms of a turn's audio the user has ACTUALLY heard — the
122
+ // authoritative signal for barge-in context truncation (B2).
123
+ playoutWindow = new Map();
124
+ /** True while assistant audio is scheduled or playing on the playout clock (not the generation clock). */
125
+ get isPlayingOut() {
126
+ return this.scheduledFrames.size > 0;
127
+ }
128
+ /**
129
+ * Milliseconds of `contextId`'s audio that have actually played out of the
130
+ * speaker so far, clamped to what has been scheduled. This is the played-out
131
+ * clock the server needs to truncate assistant history to what was heard.
132
+ */
133
+ playedOutMs(contextId) {
134
+ const window = this.playoutWindow.get(contextId);
135
+ if (!window)
136
+ return 0;
137
+ const elapsedMs = (this.context.currentTime - window.startTime) * 1000;
138
+ const scheduledMs = (window.endTime - window.startTime) * 1000;
139
+ return Math.max(0, Math.min(elapsedMs, scheduledMs));
140
+ }
141
+ /** True once all of `contextId`'s scheduled audio has finished playing. */
142
+ isPlayoutComplete(contextId) {
143
+ const window = this.playoutWindow.get(contextId);
144
+ if (!window)
145
+ return false;
146
+ return this.context.currentTime >= window.endTime;
147
+ }
148
+ /** ContextId of the most recently scheduled assistant audio while any audio is still playing out. */
149
+ get activeContextId() {
150
+ return this.scheduledFrames.size > 0 ? this.lastEnqueuedContextId : null;
151
+ }
152
+ constructor(context, options) {
153
+ this.context = context;
154
+ this.sampleRateHz = options.sampleRateHz;
155
+ this.targetBufferMs = options.targetBufferMs ?? 100;
156
+ }
157
+ enqueue(pcm16Data, contextId) {
158
+ try {
159
+ const pcm16Array = new Int16Array(pcm16Data);
160
+ // Skip empty or invalid audio data
161
+ if (pcm16Array.length === 0) {
162
+ return;
163
+ }
164
+ const float32Array = new Float32Array(pcm16Array.length);
165
+ // Convert PCM16 to Float32
166
+ for (let i = 0; i < pcm16Array.length; i++) {
167
+ float32Array[i] = pcm16Array[i] / (pcm16Array[i] < 0 ? 32768 : 32767);
168
+ }
169
+ const audioBuffer = this.context.createBuffer(1, float32Array.length, this.sampleRateHz);
170
+ audioBuffer.copyToChannel(float32Array, 0);
171
+ const now = this.context.currentTime;
172
+ // If this is the first frame or we've fallen behind, establish baseline
173
+ if (this.nextScheduledTime === 0 || this.nextScheduledTime < now) {
174
+ this.nextScheduledTime = now + (this.targetBufferMs / 1000);
175
+ }
176
+ const frame = {
177
+ buffer: audioBuffer,
178
+ scheduledTime: this.nextScheduledTime,
179
+ contextId,
180
+ source: null,
181
+ };
182
+ this.scheduledFrames.add(frame);
183
+ if (contextId) {
184
+ this.contextIds.add(contextId);
185
+ this.lastEnqueuedContextId = contextId;
186
+ const window = this.playoutWindow.get(contextId);
187
+ if (window) {
188
+ window.endTime = this.nextScheduledTime + audioBuffer.duration;
189
+ }
190
+ else {
191
+ this.playoutWindow.set(contextId, {
192
+ startTime: this.nextScheduledTime,
193
+ endTime: this.nextScheduledTime + audioBuffer.duration,
194
+ });
195
+ }
196
+ }
197
+ // Schedule the audio
198
+ const source = this.context.createBufferSource();
199
+ source.buffer = audioBuffer;
200
+ source.connect(this.context.destination);
201
+ frame.source = source;
202
+ source.start(frame.scheduledTime);
203
+ // Clean up when done
204
+ source.onended = () => {
205
+ this.scheduledFrames.delete(frame);
206
+ frame.source = null;
207
+ };
208
+ this.nextScheduledTime += audioBuffer.duration;
209
+ }
210
+ catch (error) {
211
+ console.warn("AudioJitterBuffer: Failed to enqueue audio frame:", error);
212
+ }
213
+ }
214
+ clear(contextId) {
215
+ if (contextId) {
216
+ for (const frame of this.scheduledFrames) {
217
+ if (frame.contextId === contextId) {
218
+ if (frame.source) {
219
+ try {
220
+ frame.source.stop();
221
+ }
222
+ catch {
223
+ // Ignore if already stopped
224
+ }
225
+ }
226
+ this.scheduledFrames.delete(frame);
227
+ }
228
+ }
229
+ this.contextIds.delete(contextId);
230
+ this.playoutWindow.delete(contextId);
231
+ if (this.lastEnqueuedContextId === contextId)
232
+ this.lastEnqueuedContextId = null;
233
+ this.recomputeNextScheduledTime();
234
+ }
235
+ else {
236
+ // Clear all frames
237
+ for (const frame of this.scheduledFrames) {
238
+ if (frame.source) {
239
+ try {
240
+ frame.source.stop();
241
+ }
242
+ catch {
243
+ // Ignore if already stopped
244
+ }
245
+ }
246
+ }
247
+ this.scheduledFrames.clear();
248
+ this.contextIds.clear();
249
+ this.lastEnqueuedContextId = null;
250
+ this.nextScheduledTime = 0;
251
+ }
252
+ }
253
+ get bufferedDurationMs() {
254
+ if (this.scheduledFrames.size === 0)
255
+ return 0;
256
+ const now = this.context.currentTime;
257
+ return Math.max(0, (this.nextScheduledTime - now) * 1000);
258
+ }
259
+ get activeContextIds() {
260
+ return [...this.contextIds];
261
+ }
262
+ recomputeNextScheduledTime() {
263
+ if (this.scheduledFrames.size === 0) {
264
+ this.nextScheduledTime = 0;
265
+ return;
266
+ }
267
+ let maxEnd = 0;
268
+ for (const frame of this.scheduledFrames) {
269
+ const end = frame.scheduledTime + frame.buffer.duration;
270
+ if (end > maxEnd)
271
+ maxEnd = end;
272
+ }
273
+ this.nextScheduledTime = maxEnd;
274
+ }
275
+ }
276
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ export declare const BROWSER_OPUS_SAMPLE_RATE_HZ = 48000;
2
+ export declare const BROWSER_OPUS_FRAME_DURATION_MS = 20;
3
+ export declare const BROWSER_SUPPORTED_INPUT_CODECS: readonly ["pcm_s16le", "opus"];
4
+ export type BrowserWireCodec = "pcm_s16le" | "opus";
5
+ type OpusEncoder = {
6
+ encode(pcm: Uint8Array): Uint8Array;
7
+ };
8
+ type OpusDecoder = {
9
+ decode(opus: Uint8Array): Uint8Array;
10
+ };
11
+ type OpusModule = {
12
+ readonly Encoder: new (options: {
13
+ channels: 1;
14
+ sample_rate: number;
15
+ application: "voip";
16
+ }) => OpusEncoder;
17
+ readonly Decoder: new (options: {
18
+ channels: 1;
19
+ sample_rate: number;
20
+ }) => OpusDecoder;
21
+ };
22
+ export interface BrowserOpusCodec {
23
+ readonly sampleRateHz: number;
24
+ readonly frameSamples: number;
25
+ encodePcm16Frame(samples: Int16Array, flush?: boolean): Uint8Array[];
26
+ decodeOpusFrame(wire: Uint8Array): Int16Array;
27
+ reset(): void;
28
+ }
29
+ export declare function loadBrowserOpusModule(): Promise<OpusModule>;
30
+ export declare function createBrowserOpusCodec(sampleRateHz?: number): Promise<BrowserOpusCodec>;
31
+ export declare function pickBrowserWireCodec(supportedInputCodecs: readonly string[] | undefined, opusAvailable: boolean): BrowserWireCodec;
32
+ export declare function encodeBrowserOpusEnvelope(opusPayload: Uint8Array, sampleRateHz: number, options: {
33
+ readonly contextId?: string;
34
+ readonly sequence?: number;
35
+ }): Uint8Array;
36
+ export declare function encodeBrowserPcmEnvelope(audio: Uint8Array, sampleRateHz: number, options: {
37
+ readonly contextId?: string;
38
+ readonly sequence?: number;
39
+ }): Uint8Array;
40
+ export {};
41
+ //# sourceMappingURL=browser-opus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-opus.d.ts","sourceRoot":"","sources":["../src/browser-opus.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAClD,eAAO,MAAM,8BAA8B,KAAK,CAAC;AACjD,eAAO,MAAM,8BAA8B,gCAAiC,CAAC;AAE7E,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,MAAM,CAAC;AAEpD,KAAK,WAAW,GAAG;IAAE,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAAA;CAAE,CAAC;AAC3D,KAAK,WAAW,GAAG;IAAE,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAAA;CAAE,CAAC;AAE5D,KAAK,UAAU,GAAG;IAChB,QAAQ,CAAC,OAAO,EAAE,KAAK,OAAO,EAAE;QAAE,QAAQ,EAAE,CAAC,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,KAAK,WAAW,CAAC;IAC1G,QAAQ,CAAC,OAAO,EAAE,KAAK,OAAO,EAAE;QAAE,QAAQ,EAAE,CAAC,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,KAAK,WAAW,CAAC;CACtF,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,UAAU,EAAE,CAAC;IACrE,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAC9C,KAAK,IAAI,IAAI,CAAC;CACf;AAID,wBAAgB,qBAAqB,IAAI,OAAO,CAAC,UAAU,CAAC,CAG3D;AAED,wBAAsB,sBAAsB,CAC1C,YAAY,SAA8B,GACzC,OAAO,CAAC,gBAAgB,CAAC,CAuC3B;AAED,wBAAgB,oBAAoB,CAClC,oBAAoB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EACnD,aAAa,EAAE,OAAO,GACrB,gBAAgB,CAGlB;AAED,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,UAAU,EACvB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACnE,UAAU,CAWZ;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,UAAU,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACnE,UAAU,CAWZ"}
@@ -0,0 +1,81 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { encodeSyrinxAudioEnvelope } from "@kuralle-syrinx/core";
3
+ import { pcm16BytesToSamples, pcm16SamplesToBytes } from "@kuralle-syrinx/core/audio";
4
+ export const BROWSER_OPUS_SAMPLE_RATE_HZ = 48_000;
5
+ export const BROWSER_OPUS_FRAME_DURATION_MS = 20;
6
+ export const BROWSER_SUPPORTED_INPUT_CODECS = ["pcm_s16le", "opus"];
7
+ let opusModulePromise = null;
8
+ export function loadBrowserOpusModule() {
9
+ opusModulePromise ??= import("@evan/opus");
10
+ return opusModulePromise;
11
+ }
12
+ export async function createBrowserOpusCodec(sampleRateHz = BROWSER_OPUS_SAMPLE_RATE_HZ) {
13
+ const { Encoder, Decoder } = await loadBrowserOpusModule();
14
+ const encoder = new Encoder({ channels: 1, sample_rate: sampleRateHz, application: "voip" });
15
+ const decoder = new Decoder({ channels: 1, sample_rate: sampleRateHz });
16
+ const frameSamples = Math.max(1, Math.round((sampleRateHz * BROWSER_OPUS_FRAME_DURATION_MS) / 1000));
17
+ let encodeRemainder = new Int16Array(0);
18
+ return {
19
+ sampleRateHz,
20
+ frameSamples,
21
+ reset() {
22
+ encodeRemainder = new Int16Array(0);
23
+ },
24
+ decodeOpusFrame(wire) {
25
+ return pcm16BytesToSamples(decoder.decode(wire));
26
+ },
27
+ encodePcm16Frame(samples, flush = false) {
28
+ const pending = new Int16Array(encodeRemainder.length + samples.length);
29
+ pending.set(encodeRemainder);
30
+ pending.set(samples, encodeRemainder.length);
31
+ const completeFrames = Math.floor(pending.length / frameSamples);
32
+ const frames = [];
33
+ for (let index = 0; index < completeFrames; index += 1) {
34
+ const frame = pending.subarray(index * frameSamples, (index + 1) * frameSamples);
35
+ frames.push(encoder.encode(pcm16SamplesToBytes(frame)));
36
+ }
37
+ const consumed = completeFrames * frameSamples;
38
+ const remainder = pending.subarray(consumed);
39
+ if (flush && remainder.length > 0) {
40
+ const padded = new Int16Array(frameSamples);
41
+ padded.set(remainder);
42
+ frames.push(encoder.encode(pcm16SamplesToBytes(padded)));
43
+ encodeRemainder = new Int16Array(0);
44
+ }
45
+ else {
46
+ encodeRemainder = new Int16Array(remainder);
47
+ }
48
+ return frames;
49
+ },
50
+ };
51
+ }
52
+ export function pickBrowserWireCodec(supportedInputCodecs, opusAvailable) {
53
+ if (opusAvailable && supportedInputCodecs?.includes("opus"))
54
+ return "opus";
55
+ return "pcm_s16le";
56
+ }
57
+ export function encodeBrowserOpusEnvelope(opusPayload, sampleRateHz, options) {
58
+ return encodeSyrinxAudioEnvelope({
59
+ type: "audio",
60
+ contextId: options.contextId,
61
+ sampleRateHz,
62
+ sequence: options.sequence,
63
+ encoding: "opus",
64
+ channels: 1,
65
+ byteLength: opusPayload.byteLength,
66
+ durationMs: BROWSER_OPUS_FRAME_DURATION_MS,
67
+ }, opusPayload);
68
+ }
69
+ export function encodeBrowserPcmEnvelope(audio, sampleRateHz, options) {
70
+ return encodeSyrinxAudioEnvelope({
71
+ type: "audio",
72
+ contextId: options.contextId,
73
+ sampleRateHz,
74
+ sequence: options.sequence,
75
+ encoding: "pcm_s16le",
76
+ channels: 1,
77
+ byteLength: audio.byteLength,
78
+ durationMs: Math.round((audio.byteLength / 2 / sampleRateHz) * 1000),
79
+ }, audio);
80
+ }
81
+ //# sourceMappingURL=browser-opus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-opus.js","sourceRoot":"","sources":["../src/browser-opus.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAE/B,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtF,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAClD,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC;AACjD,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,WAAW,EAAE,MAAM,CAAU,CAAC;AAoB7E,IAAI,iBAAiB,GAA+B,IAAI,CAAC;AAEzD,MAAM,UAAU,qBAAqB;IACnC,iBAAiB,KAAK,MAAM,CAAC,YAAY,CAAwB,CAAC;IAClE,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,YAAY,GAAG,2BAA2B;IAE1C,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,MAAM,qBAAqB,EAAE,CAAC;IAC3D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7F,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC,CAAC;IACxE,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,YAAY,GAAG,8BAA8B,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IACrG,IAAI,eAAe,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IAExC,OAAO;QACL,YAAY;QACZ,YAAY;QACZ,KAAK;YACH,eAAe,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;QACtC,CAAC;QACD,eAAe,CAAC,IAAgB;YAC9B,OAAO,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QACnD,CAAC;QACD,gBAAgB,CAAC,OAAmB,EAAE,KAAK,GAAG,KAAK;YACjD,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,eAAe,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;YACxE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;YAC7B,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;YAC7C,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC;YACjE,MAAM,MAAM,GAAiB,EAAE,CAAC;YAChC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,cAAc,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;gBACvD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,KAAK,GAAG,YAAY,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC;gBACjF,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC1D,CAAC;YACD,MAAM,QAAQ,GAAG,cAAc,GAAG,YAAY,CAAC;YAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAC7C,IAAI,KAAK,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAClC,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC;gBAC5C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACtB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBACzD,eAAe,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;YACtC,CAAC;iBAAM,CAAC;gBACN,eAAe,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;YAC9C,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,oBAAmD,EACnD,aAAsB;IAEtB,IAAI,aAAa,IAAI,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAC3E,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,WAAuB,EACvB,YAAoB,EACpB,OAAoE;IAEpE,OAAO,yBAAyB,CAAC;QAC/B,IAAI,EAAE,OAAO;QACb,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,YAAY;QACZ,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,QAAQ,EAAE,MAAM;QAChB,QAAQ,EAAE,CAAC;QACX,UAAU,EAAE,WAAW,CAAC,UAAU;QAClC,UAAU,EAAE,8BAA8B;KAC3C,EAAE,WAAW,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,KAAiB,EACjB,YAAoB,EACpB,OAAoE;IAEpE,OAAO,yBAAyB,CAAC;QAC/B,IAAI,EAAE,OAAO;QACb,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,YAAY;QACZ,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,QAAQ,EAAE,WAAW;QACrB,QAAQ,EAAE,CAAC;QACX,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,GAAG,YAAY,CAAC,GAAG,IAAI,CAAC;KACrE,EAAE,KAAK,CAAC,CAAC;AACZ,CAAC"}