@pipecat-ai/daily-transport 0.3.4 → 0.3.6

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/dist/index.js CHANGED
@@ -28,35 +28,1541 @@ function $parcel$export(e, n, v, s) {
28
28
  }
29
29
  var $28bdeb35415d61e8$exports = {};
30
30
 
31
+ $parcel$export($28bdeb35415d61e8$exports, "DailyRTVIMessageType", () => $28bdeb35415d61e8$export$ef180de88fd317cc);
31
32
  $parcel$export($28bdeb35415d61e8$exports, "DailyTransport", () => $28bdeb35415d61e8$export$b1ca982aa1e488c1);
32
33
 
33
34
 
35
+ /**
36
+ * Raw wav audio file contents
37
+ * @typedef {Object} WavPackerAudioType
38
+ * @property {Blob} blob
39
+ * @property {string} url
40
+ * @property {number} channelCount
41
+ * @property {number} sampleRate
42
+ * @property {number} duration
43
+ */ /**
44
+ * Utility class for assembling PCM16 "audio/wav" data
45
+ * @class
46
+ */ class $a61750b8fbee4dae$export$13afda237b1c9846 {
47
+ /**
48
+ * Converts Float32Array of amplitude data to ArrayBuffer in Int16Array format
49
+ * @param {Float32Array} float32Array
50
+ * @returns {ArrayBuffer}
51
+ */ static floatTo16BitPCM(float32Array) {
52
+ const buffer = new ArrayBuffer(float32Array.length * 2);
53
+ const view = new DataView(buffer);
54
+ let offset = 0;
55
+ for(let i = 0; i < float32Array.length; i++, offset += 2){
56
+ let s = Math.max(-1, Math.min(1, float32Array[i]));
57
+ view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
58
+ }
59
+ return buffer;
60
+ }
61
+ /**
62
+ * Concatenates two ArrayBuffers
63
+ * @param {ArrayBuffer} leftBuffer
64
+ * @param {ArrayBuffer} rightBuffer
65
+ * @returns {ArrayBuffer}
66
+ */ static mergeBuffers(leftBuffer, rightBuffer) {
67
+ const tmpArray = new Uint8Array(leftBuffer.byteLength + rightBuffer.byteLength);
68
+ tmpArray.set(new Uint8Array(leftBuffer), 0);
69
+ tmpArray.set(new Uint8Array(rightBuffer), leftBuffer.byteLength);
70
+ return tmpArray.buffer;
71
+ }
72
+ /**
73
+ * Packs data into an Int16 format
74
+ * @private
75
+ * @param {number} size 0 = 1x Int16, 1 = 2x Int16
76
+ * @param {number} arg value to pack
77
+ * @returns
78
+ */ _packData(size, arg) {
79
+ return [
80
+ new Uint8Array([
81
+ arg,
82
+ arg >> 8
83
+ ]),
84
+ new Uint8Array([
85
+ arg,
86
+ arg >> 8,
87
+ arg >> 16,
88
+ arg >> 24
89
+ ])
90
+ ][size];
91
+ }
92
+ /**
93
+ * Packs audio into "audio/wav" Blob
94
+ * @param {number} sampleRate
95
+ * @param {{bitsPerSample: number, channels: Array<Float32Array>, data: Int16Array}} audio
96
+ * @returns {WavPackerAudioType}
97
+ */ pack(sampleRate, audio) {
98
+ if (!audio?.bitsPerSample) throw new Error(`Missing "bitsPerSample"`);
99
+ else if (!audio?.channels) throw new Error(`Missing "channels"`);
100
+ else if (!audio?.data) throw new Error(`Missing "data"`);
101
+ const { bitsPerSample: bitsPerSample, channels: channels, data: data } = audio;
102
+ const output = [
103
+ // Header
104
+ 'RIFF',
105
+ this._packData(1, 52),
106
+ 'WAVE',
107
+ // chunk 1
108
+ 'fmt ',
109
+ this._packData(1, 16),
110
+ this._packData(0, 1),
111
+ this._packData(0, channels.length),
112
+ this._packData(1, sampleRate),
113
+ this._packData(1, sampleRate * channels.length * bitsPerSample / 8),
114
+ this._packData(0, channels.length * bitsPerSample / 8),
115
+ this._packData(0, bitsPerSample),
116
+ // chunk 2
117
+ 'data',
118
+ this._packData(1, channels[0].length * channels.length * bitsPerSample / 8),
119
+ data
120
+ ];
121
+ const blob = new Blob(output, {
122
+ type: 'audio/mpeg'
123
+ });
124
+ const url = URL.createObjectURL(blob);
125
+ return {
126
+ blob: blob,
127
+ url: url,
128
+ channelCount: channels.length,
129
+ sampleRate: sampleRate,
130
+ duration: data.byteLength / (channels.length * sampleRate * 2)
131
+ };
132
+ }
133
+ }
134
+ globalThis.WavPacker = $a61750b8fbee4dae$export$13afda237b1c9846;
135
+
136
+
137
+ /**
138
+ * Constants for help with visualization
139
+ * Helps map frequency ranges from Fast Fourier Transform
140
+ * to human-interpretable ranges, notably music ranges and
141
+ * human vocal ranges.
142
+ */ // Eighth octave frequencies
143
+ const $e10a9de47f58e137$var$octave8Frequencies = [
144
+ 4186.01,
145
+ 4434.92,
146
+ 4698.63,
147
+ 4978.03,
148
+ 5274.04,
149
+ 5587.65,
150
+ 5919.91,
151
+ 6271.93,
152
+ 6644.88,
153
+ 7040.0,
154
+ 7458.62,
155
+ 7902.13
156
+ ];
157
+ // Labels for each of the above frequencies
158
+ const $e10a9de47f58e137$var$octave8FrequencyLabels = [
159
+ 'C',
160
+ 'C#',
161
+ 'D',
162
+ 'D#',
163
+ 'E',
164
+ 'F',
165
+ 'F#',
166
+ 'G',
167
+ 'G#',
168
+ 'A',
169
+ 'A#',
170
+ 'B'
171
+ ];
172
+ const $e10a9de47f58e137$export$776c63898ae5b636 = [];
173
+ const $e10a9de47f58e137$export$facd167cc27ea9b0 = [];
174
+ for(let i = 1; i <= 8; i++)for(let f = 0; f < $e10a9de47f58e137$var$octave8Frequencies.length; f++){
175
+ const freq = $e10a9de47f58e137$var$octave8Frequencies[f];
176
+ $e10a9de47f58e137$export$776c63898ae5b636.push(freq / Math.pow(2, 8 - i));
177
+ $e10a9de47f58e137$export$facd167cc27ea9b0.push($e10a9de47f58e137$var$octave8FrequencyLabels[f] + i);
178
+ }
179
+ /**
180
+ * Subset of the note frequencies between 32 and 2000 Hz
181
+ * 6 octave range: C1 to B6
182
+ */ const $e10a9de47f58e137$var$voiceFrequencyRange = [
183
+ 32.0,
184
+ 2000.0
185
+ ];
186
+ const $e10a9de47f58e137$export$dbc1581ed2cfa183 = $e10a9de47f58e137$export$776c63898ae5b636.filter((_, i)=>{
187
+ return $e10a9de47f58e137$export$776c63898ae5b636[i] > $e10a9de47f58e137$var$voiceFrequencyRange[0] && $e10a9de47f58e137$export$776c63898ae5b636[i] < $e10a9de47f58e137$var$voiceFrequencyRange[1];
188
+ });
189
+ const $e10a9de47f58e137$export$30a6f2881311088f = $e10a9de47f58e137$export$facd167cc27ea9b0.filter((_, i)=>{
190
+ return $e10a9de47f58e137$export$776c63898ae5b636[i] > $e10a9de47f58e137$var$voiceFrequencyRange[0] && $e10a9de47f58e137$export$776c63898ae5b636[i] < $e10a9de47f58e137$var$voiceFrequencyRange[1];
191
+ });
192
+
193
+
194
+ class $5853841ab58516d4$export$2c3136da0bf130f9 {
195
+ /**
196
+ * Retrieves frequency domain data from an AnalyserNode adjusted to a decibel range
197
+ * returns human-readable formatting and labels
198
+ * @param {AnalyserNode} analyser
199
+ * @param {number} sampleRate
200
+ * @param {Float32Array} [fftResult]
201
+ * @param {"frequency"|"music"|"voice"} [analysisType]
202
+ * @param {number} [minDecibels] default -100
203
+ * @param {number} [maxDecibels] default -30
204
+ * @returns {AudioAnalysisOutputType}
205
+ */ static getFrequencies(analyser, sampleRate, fftResult, analysisType = 'frequency', minDecibels = -100, maxDecibels = -30) {
206
+ if (!fftResult) {
207
+ fftResult = new Float32Array(analyser.frequencyBinCount);
208
+ analyser.getFloatFrequencyData(fftResult);
209
+ }
210
+ const nyquistFrequency = sampleRate / 2;
211
+ const frequencyStep = 1 / fftResult.length * nyquistFrequency;
212
+ let outputValues;
213
+ let frequencies;
214
+ let labels;
215
+ if (analysisType === 'music' || analysisType === 'voice') {
216
+ const useFrequencies = analysisType === 'voice' ? (0, $e10a9de47f58e137$export$dbc1581ed2cfa183) : (0, $e10a9de47f58e137$export$776c63898ae5b636);
217
+ const aggregateOutput = Array(useFrequencies.length).fill(minDecibels);
218
+ for(let i = 0; i < fftResult.length; i++){
219
+ const frequency = i * frequencyStep;
220
+ const amplitude = fftResult[i];
221
+ for(let n = useFrequencies.length - 1; n >= 0; n--)if (frequency > useFrequencies[n]) {
222
+ aggregateOutput[n] = Math.max(aggregateOutput[n], amplitude);
223
+ break;
224
+ }
225
+ }
226
+ outputValues = aggregateOutput;
227
+ frequencies = analysisType === 'voice' ? (0, $e10a9de47f58e137$export$dbc1581ed2cfa183) : (0, $e10a9de47f58e137$export$776c63898ae5b636);
228
+ labels = analysisType === 'voice' ? (0, $e10a9de47f58e137$export$30a6f2881311088f) : (0, $e10a9de47f58e137$export$facd167cc27ea9b0);
229
+ } else {
230
+ outputValues = Array.from(fftResult);
231
+ frequencies = outputValues.map((_, i)=>frequencyStep * i);
232
+ labels = frequencies.map((f)=>`${f.toFixed(2)} Hz`);
233
+ }
234
+ // We normalize to {0, 1}
235
+ const normalizedOutput = outputValues.map((v)=>{
236
+ return Math.max(0, Math.min((v - minDecibels) / (maxDecibels - minDecibels), 1));
237
+ });
238
+ const values = new Float32Array(normalizedOutput);
239
+ return {
240
+ values: values,
241
+ frequencies: frequencies,
242
+ labels: labels
243
+ };
244
+ }
245
+ /**
246
+ * Creates a new AudioAnalysis instance for an HTMLAudioElement
247
+ * @param {HTMLAudioElement} audioElement
248
+ * @param {AudioBuffer|null} [audioBuffer] If provided, will cache all frequency domain data from the buffer
249
+ * @returns {AudioAnalysis}
250
+ */ constructor(audioElement, audioBuffer = null){
251
+ this.fftResults = [];
252
+ if (audioBuffer) {
253
+ /**
254
+ * Modified from
255
+ * https://stackoverflow.com/questions/75063715/using-the-web-audio-api-to-analyze-a-song-without-playing
256
+ *
257
+ * We do this to populate FFT values for the audio if provided an `audioBuffer`
258
+ * The reason to do this is that Safari fails when using `createMediaElementSource`
259
+ * This has a non-zero RAM cost so we only opt-in to run it on Safari, Chrome is better
260
+ */ const { length: length, sampleRate: sampleRate } = audioBuffer;
261
+ const offlineAudioContext = new OfflineAudioContext({
262
+ length: length,
263
+ sampleRate: sampleRate
264
+ });
265
+ const source = offlineAudioContext.createBufferSource();
266
+ source.buffer = audioBuffer;
267
+ const analyser = offlineAudioContext.createAnalyser();
268
+ analyser.fftSize = 8192;
269
+ analyser.smoothingTimeConstant = 0.1;
270
+ source.connect(analyser);
271
+ // limit is :: 128 / sampleRate;
272
+ // but we just want 60fps - cuts ~1s from 6MB to 1MB of RAM
273
+ const renderQuantumInSeconds = 1 / 60;
274
+ const durationInSeconds = length / sampleRate;
275
+ const analyze = (index)=>{
276
+ const suspendTime = renderQuantumInSeconds * index;
277
+ if (suspendTime < durationInSeconds) offlineAudioContext.suspend(suspendTime).then(()=>{
278
+ const fftResult = new Float32Array(analyser.frequencyBinCount);
279
+ analyser.getFloatFrequencyData(fftResult);
280
+ this.fftResults.push(fftResult);
281
+ analyze(index + 1);
282
+ });
283
+ if (index === 1) offlineAudioContext.startRendering();
284
+ else offlineAudioContext.resume();
285
+ };
286
+ source.start(0);
287
+ analyze(1);
288
+ this.audio = audioElement;
289
+ this.context = offlineAudioContext;
290
+ this.analyser = analyser;
291
+ this.sampleRate = sampleRate;
292
+ this.audioBuffer = audioBuffer;
293
+ } else {
294
+ const audioContext = new AudioContext();
295
+ const track = audioContext.createMediaElementSource(audioElement);
296
+ const analyser = audioContext.createAnalyser();
297
+ analyser.fftSize = 8192;
298
+ analyser.smoothingTimeConstant = 0.1;
299
+ track.connect(analyser);
300
+ analyser.connect(audioContext.destination);
301
+ this.audio = audioElement;
302
+ this.context = audioContext;
303
+ this.analyser = analyser;
304
+ this.sampleRate = this.context.sampleRate;
305
+ this.audioBuffer = null;
306
+ }
307
+ }
308
+ /**
309
+ * Gets the current frequency domain data from the playing audio track
310
+ * @param {"frequency"|"music"|"voice"} [analysisType]
311
+ * @param {number} [minDecibels] default -100
312
+ * @param {number} [maxDecibels] default -30
313
+ * @returns {AudioAnalysisOutputType}
314
+ */ getFrequencies(analysisType = 'frequency', minDecibels = -100, maxDecibels = -30) {
315
+ let fftResult = null;
316
+ if (this.audioBuffer && this.fftResults.length) {
317
+ const pct = this.audio.currentTime / this.audio.duration;
318
+ const index = Math.min(pct * this.fftResults.length | 0, this.fftResults.length - 1);
319
+ fftResult = this.fftResults[index];
320
+ }
321
+ return $5853841ab58516d4$export$2c3136da0bf130f9.getFrequencies(this.analyser, this.sampleRate, fftResult, analysisType, minDecibels, maxDecibels);
322
+ }
323
+ /**
324
+ * Resume the internal AudioContext if it was suspended due to the lack of
325
+ * user interaction when the AudioAnalysis was instantiated.
326
+ * @returns {Promise<true>}
327
+ */ async resumeIfSuspended() {
328
+ if (this.context.state === 'suspended') await this.context.resume();
329
+ return true;
330
+ }
331
+ }
332
+ globalThis.AudioAnalysis = $5853841ab58516d4$export$2c3136da0bf130f9;
333
+
334
+
335
+ const $d6d3743ef65c7519$export$50b76700e2b15e9 = `
336
+ class StreamProcessor extends AudioWorkletProcessor {
337
+ constructor() {
338
+ super();
339
+ this.hasStarted = false;
340
+ this.hasInterrupted = false;
341
+ this.outputBuffers = [];
342
+ this.bufferLength = 128;
343
+ this.write = { buffer: new Float32Array(this.bufferLength), trackId: null };
344
+ this.writeOffset = 0;
345
+ this.trackSampleOffsets = {};
346
+ this.port.onmessage = (event) => {
347
+ if (event.data) {
348
+ const payload = event.data;
349
+ if (payload.event === 'write') {
350
+ const int16Array = payload.buffer;
351
+ const float32Array = new Float32Array(int16Array.length);
352
+ for (let i = 0; i < int16Array.length; i++) {
353
+ float32Array[i] = int16Array[i] / 0x8000; // Convert Int16 to Float32
354
+ }
355
+ this.writeData(float32Array, payload.trackId);
356
+ } else if (
357
+ payload.event === 'offset' ||
358
+ payload.event === 'interrupt'
359
+ ) {
360
+ const requestId = payload.requestId;
361
+ const trackId = this.write.trackId;
362
+ const offset = this.trackSampleOffsets[trackId] || 0;
363
+ this.port.postMessage({
364
+ event: 'offset',
365
+ requestId,
366
+ trackId,
367
+ offset,
368
+ });
369
+ if (payload.event === 'interrupt') {
370
+ this.hasInterrupted = true;
371
+ }
372
+ } else {
373
+ throw new Error(\`Unhandled event "\${payload.event}"\`);
374
+ }
375
+ }
376
+ };
377
+ }
378
+
379
+ writeData(float32Array, trackId = null) {
380
+ let { buffer } = this.write;
381
+ let offset = this.writeOffset;
382
+ for (let i = 0; i < float32Array.length; i++) {
383
+ buffer[offset++] = float32Array[i];
384
+ if (offset >= buffer.length) {
385
+ this.outputBuffers.push(this.write);
386
+ this.write = { buffer: new Float32Array(this.bufferLength), trackId };
387
+ buffer = this.write.buffer;
388
+ offset = 0;
389
+ }
390
+ }
391
+ this.writeOffset = offset;
392
+ return true;
393
+ }
394
+
395
+ process(inputs, outputs, parameters) {
396
+ const output = outputs[0];
397
+ const outputChannelData = output[0];
398
+ const outputBuffers = this.outputBuffers;
399
+ if (this.hasInterrupted) {
400
+ this.port.postMessage({ event: 'stop' });
401
+ return false;
402
+ } else if (outputBuffers.length) {
403
+ this.hasStarted = true;
404
+ const { buffer, trackId } = outputBuffers.shift();
405
+ for (let i = 0; i < outputChannelData.length; i++) {
406
+ outputChannelData[i] = buffer[i] || 0;
407
+ }
408
+ if (trackId) {
409
+ this.trackSampleOffsets[trackId] =
410
+ this.trackSampleOffsets[trackId] || 0;
411
+ this.trackSampleOffsets[trackId] += buffer.length;
412
+ }
413
+ return true;
414
+ } else if (this.hasStarted) {
415
+ this.port.postMessage({ event: 'stop' });
416
+ return false;
417
+ } else {
418
+ return true;
419
+ }
420
+ }
421
+ }
422
+
423
+ registerProcessor('stream_processor', StreamProcessor);
424
+ `;
425
+ const $d6d3743ef65c7519$var$script = new Blob([
426
+ $d6d3743ef65c7519$export$50b76700e2b15e9
427
+ ], {
428
+ type: 'application/javascript'
429
+ });
430
+ const $d6d3743ef65c7519$var$src = URL.createObjectURL($d6d3743ef65c7519$var$script);
431
+ const $d6d3743ef65c7519$export$bfa8c596114d74df = $d6d3743ef65c7519$var$src;
432
+
433
+
434
+
435
+ class $1e7ce9484a3c4077$export$9698d62c78b8f366 {
436
+ /**
437
+ * Creates a new WavStreamPlayer instance
438
+ * @param {{sampleRate?: number}} options
439
+ * @returns {WavStreamPlayer}
440
+ */ constructor({ sampleRate: sampleRate = 44100 } = {}){
441
+ this.scriptSrc = (0, $d6d3743ef65c7519$export$bfa8c596114d74df);
442
+ this.sampleRate = sampleRate;
443
+ this.context = null;
444
+ this.stream = null;
445
+ this.analyser = null;
446
+ this.trackSampleOffsets = {};
447
+ this.interruptedTrackIds = {};
448
+ }
449
+ /**
450
+ * Connects the audio context and enables output to speakers
451
+ * @returns {Promise<true>}
452
+ */ async connect() {
453
+ this.context = new AudioContext({
454
+ sampleRate: this.sampleRate
455
+ });
456
+ if (this._speakerID) this.context.setSinkId(this._speakerID);
457
+ if (this.context.state === "suspended") await this.context.resume();
458
+ try {
459
+ await this.context.audioWorklet.addModule(this.scriptSrc);
460
+ } catch (e) {
461
+ console.error(e);
462
+ throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`);
463
+ }
464
+ const analyser = this.context.createAnalyser();
465
+ analyser.fftSize = 8192;
466
+ analyser.smoothingTimeConstant = 0.1;
467
+ this.analyser = analyser;
468
+ return true;
469
+ }
470
+ /**
471
+ * Gets the current frequency domain data from the playing track
472
+ * @param {"frequency"|"music"|"voice"} [analysisType]
473
+ * @param {number} [minDecibels] default -100
474
+ * @param {number} [maxDecibels] default -30
475
+ * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType}
476
+ */ getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) {
477
+ if (!this.analyser) throw new Error("Not connected, please call .connect() first");
478
+ return (0, $5853841ab58516d4$export$2c3136da0bf130f9).getFrequencies(this.analyser, this.sampleRate, null, analysisType, minDecibels, maxDecibels);
479
+ }
480
+ /**
481
+ * @param {string} speaker deviceId
482
+ */ async updateSpeaker(speaker) {
483
+ const _prevSpeaker = this._speakerID;
484
+ this._speakerID = speaker;
485
+ if (this.context) try {
486
+ if (speaker === "default") await this.context.setSinkId();
487
+ else await this.context.setSinkId(speaker);
488
+ } catch (e) {
489
+ console.error(`Could not set sinkId to ${speaker}: ${e}`);
490
+ this._speakerID = _prevSpeaker;
491
+ }
492
+ }
493
+ /**
494
+ * Starts audio streaming
495
+ * @private
496
+ * @returns {Promise<true>}
497
+ */ _start() {
498
+ const streamNode = new AudioWorkletNode(this.context, "stream_processor");
499
+ streamNode.connect(this.context.destination);
500
+ streamNode.port.onmessage = (e)=>{
501
+ const { event: event } = e.data;
502
+ if (event === "stop") {
503
+ streamNode.disconnect();
504
+ this.stream = null;
505
+ } else if (event === "offset") {
506
+ const { requestId: requestId, trackId: trackId, offset: offset } = e.data;
507
+ const currentTime = offset / this.sampleRate;
508
+ this.trackSampleOffsets[requestId] = {
509
+ trackId: trackId,
510
+ offset: offset,
511
+ currentTime: currentTime
512
+ };
513
+ }
514
+ };
515
+ this.analyser.disconnect();
516
+ streamNode.connect(this.analyser);
517
+ this.stream = streamNode;
518
+ return true;
519
+ }
520
+ /**
521
+ * Adds 16BitPCM data to the currently playing audio stream
522
+ * You can add chunks beyond the current play point and they will be queued for play
523
+ * @param {ArrayBuffer|Int16Array} arrayBuffer
524
+ * @param {string} [trackId]
525
+ * @returns {Int16Array}
526
+ */ add16BitPCM(arrayBuffer, trackId = "default") {
527
+ if (typeof trackId !== "string") throw new Error(`trackId must be a string`);
528
+ else if (this.interruptedTrackIds[trackId]) return;
529
+ if (!this.stream) this._start();
530
+ let buffer;
531
+ if (arrayBuffer instanceof Int16Array) buffer = arrayBuffer;
532
+ else if (arrayBuffer instanceof ArrayBuffer) buffer = new Int16Array(arrayBuffer);
533
+ else throw new Error(`argument must be Int16Array or ArrayBuffer`);
534
+ this.stream.port.postMessage({
535
+ event: "write",
536
+ buffer: buffer,
537
+ trackId: trackId
538
+ });
539
+ return buffer;
540
+ }
541
+ /**
542
+ * Gets the offset (sample count) of the currently playing stream
543
+ * @param {boolean} [interrupt]
544
+ * @returns {{trackId: string|null, offset: number, currentTime: number}}
545
+ */ async getTrackSampleOffset(interrupt = false) {
546
+ if (!this.stream) return null;
547
+ const requestId = crypto.randomUUID();
548
+ this.stream.port.postMessage({
549
+ event: interrupt ? "interrupt" : "offset",
550
+ requestId: requestId
551
+ });
552
+ let trackSampleOffset;
553
+ while(!trackSampleOffset){
554
+ trackSampleOffset = this.trackSampleOffsets[requestId];
555
+ await new Promise((r)=>setTimeout(()=>r(), 1));
556
+ }
557
+ const { trackId: trackId } = trackSampleOffset;
558
+ if (interrupt && trackId) this.interruptedTrackIds[trackId] = true;
559
+ return trackSampleOffset;
560
+ }
561
+ /**
562
+ * Strips the current stream and returns the sample offset of the audio
563
+ * @param {boolean} [interrupt]
564
+ * @returns {{trackId: string|null, offset: number, currentTime: number}}
565
+ */ async interrupt() {
566
+ return this.getTrackSampleOffset(true);
567
+ }
568
+ }
569
+ globalThis.WavStreamPlayer = $1e7ce9484a3c4077$export$9698d62c78b8f366;
570
+
571
+
572
+ const $2cf6a2d8a6d031bc$var$AudioProcessorWorklet = `
573
+ class AudioProcessor extends AudioWorkletProcessor {
574
+
575
+ constructor() {
576
+ super();
577
+ this.port.onmessage = this.receive.bind(this);
578
+ this.initialize();
579
+ }
580
+
581
+ initialize() {
582
+ this.foundAudio = false;
583
+ this.recording = false;
584
+ this.chunks = [];
585
+ }
586
+
587
+ /**
588
+ * Concatenates sampled chunks into channels
589
+ * Format is chunk[Left[], Right[]]
590
+ */
591
+ readChannelData(chunks, channel = -1, maxChannels = 9) {
592
+ let channelLimit;
593
+ if (channel !== -1) {
594
+ if (chunks[0] && chunks[0].length - 1 < channel) {
595
+ throw new Error(
596
+ \`Channel \${channel} out of range: max \${chunks[0].length}\`
597
+ );
598
+ }
599
+ channelLimit = channel + 1;
600
+ } else {
601
+ channel = 0;
602
+ channelLimit = Math.min(chunks[0] ? chunks[0].length : 1, maxChannels);
603
+ }
604
+ const channels = [];
605
+ for (let n = channel; n < channelLimit; n++) {
606
+ const length = chunks.reduce((sum, chunk) => {
607
+ return sum + chunk[n].length;
608
+ }, 0);
609
+ const buffers = chunks.map((chunk) => chunk[n]);
610
+ const result = new Float32Array(length);
611
+ let offset = 0;
612
+ for (let i = 0; i < buffers.length; i++) {
613
+ result.set(buffers[i], offset);
614
+ offset += buffers[i].length;
615
+ }
616
+ channels[n] = result;
617
+ }
618
+ return channels;
619
+ }
620
+
621
+ /**
622
+ * Combines parallel audio data into correct format,
623
+ * channels[Left[], Right[]] to float32Array[LRLRLRLR...]
624
+ */
625
+ formatAudioData(channels) {
626
+ if (channels.length === 1) {
627
+ // Simple case is only one channel
628
+ const float32Array = channels[0].slice();
629
+ const meanValues = channels[0].slice();
630
+ return { float32Array, meanValues };
631
+ } else {
632
+ const float32Array = new Float32Array(
633
+ channels[0].length * channels.length
634
+ );
635
+ const meanValues = new Float32Array(channels[0].length);
636
+ for (let i = 0; i < channels[0].length; i++) {
637
+ const offset = i * channels.length;
638
+ let meanValue = 0;
639
+ for (let n = 0; n < channels.length; n++) {
640
+ float32Array[offset + n] = channels[n][i];
641
+ meanValue += channels[n][i];
642
+ }
643
+ meanValues[i] = meanValue / channels.length;
644
+ }
645
+ return { float32Array, meanValues };
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Converts 32-bit float data to 16-bit integers
651
+ */
652
+ floatTo16BitPCM(float32Array) {
653
+ const buffer = new ArrayBuffer(float32Array.length * 2);
654
+ const view = new DataView(buffer);
655
+ let offset = 0;
656
+ for (let i = 0; i < float32Array.length; i++, offset += 2) {
657
+ let s = Math.max(-1, Math.min(1, float32Array[i]));
658
+ view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
659
+ }
660
+ return buffer;
661
+ }
662
+
663
+ /**
664
+ * Retrieves the most recent amplitude values from the audio stream
665
+ * @param {number} channel
666
+ */
667
+ getValues(channel = -1) {
668
+ const channels = this.readChannelData(this.chunks, channel);
669
+ const { meanValues } = this.formatAudioData(channels);
670
+ return { meanValues, channels };
671
+ }
672
+
673
+ /**
674
+ * Exports chunks as an audio/wav file
675
+ */
676
+ export() {
677
+ const channels = this.readChannelData(this.chunks);
678
+ const { float32Array, meanValues } = this.formatAudioData(channels);
679
+ const audioData = this.floatTo16BitPCM(float32Array);
680
+ return {
681
+ meanValues: meanValues,
682
+ audio: {
683
+ bitsPerSample: 16,
684
+ channels: channels,
685
+ data: audioData,
686
+ },
687
+ };
688
+ }
689
+
690
+ receive(e) {
691
+ const { event, id } = e.data;
692
+ let receiptData = {};
693
+ switch (event) {
694
+ case 'start':
695
+ this.recording = true;
696
+ break;
697
+ case 'stop':
698
+ this.recording = false;
699
+ break;
700
+ case 'clear':
701
+ this.initialize();
702
+ break;
703
+ case 'export':
704
+ receiptData = this.export();
705
+ break;
706
+ case 'read':
707
+ receiptData = this.getValues();
708
+ break;
709
+ default:
710
+ break;
711
+ }
712
+ // Always send back receipt
713
+ this.port.postMessage({ event: 'receipt', id, data: receiptData });
714
+ }
715
+
716
+ sendChunk(chunk) {
717
+ const channels = this.readChannelData([chunk]);
718
+ const { float32Array, meanValues } = this.formatAudioData(channels);
719
+ const rawAudioData = this.floatTo16BitPCM(float32Array);
720
+ const monoAudioData = this.floatTo16BitPCM(meanValues);
721
+ this.port.postMessage({
722
+ event: 'chunk',
723
+ data: {
724
+ mono: monoAudioData,
725
+ raw: rawAudioData,
726
+ },
727
+ });
728
+ }
729
+
730
+ process(inputList, outputList, parameters) {
731
+ // Copy input to output (e.g. speakers)
732
+ // Note that this creates choppy sounds with Mac products
733
+ const sourceLimit = Math.min(inputList.length, outputList.length);
734
+ for (let inputNum = 0; inputNum < sourceLimit; inputNum++) {
735
+ const input = inputList[inputNum];
736
+ const output = outputList[inputNum];
737
+ const channelCount = Math.min(input.length, output.length);
738
+ for (let channelNum = 0; channelNum < channelCount; channelNum++) {
739
+ input[channelNum].forEach((sample, i) => {
740
+ output[channelNum][i] = sample;
741
+ });
742
+ }
743
+ }
744
+ const inputs = inputList[0];
745
+ // There's latency at the beginning of a stream before recording starts
746
+ // Make sure we actually receive audio data before we start storing chunks
747
+ let sliceIndex = 0;
748
+ if (!this.foundAudio) {
749
+ for (const channel of inputs) {
750
+ sliceIndex = 0; // reset for each channel
751
+ if (this.foundAudio) {
752
+ break;
753
+ }
754
+ if (channel) {
755
+ for (const value of channel) {
756
+ if (value !== 0) {
757
+ // find only one non-zero entry in any channel
758
+ this.foundAudio = true;
759
+ break;
760
+ } else {
761
+ sliceIndex++;
762
+ }
763
+ }
764
+ }
765
+ }
766
+ }
767
+ if (inputs && inputs[0] && this.foundAudio && this.recording) {
768
+ // We need to copy the TypedArray, because the \`process\`
769
+ // internals will reuse the same buffer to hold each input
770
+ const chunk = inputs.map((input) => input.slice(sliceIndex));
771
+ this.chunks.push(chunk);
772
+ this.sendChunk(chunk);
773
+ }
774
+ return true;
775
+ }
776
+ }
777
+
778
+ registerProcessor('audio_processor', AudioProcessor);
779
+ `;
780
+ const $2cf6a2d8a6d031bc$var$script = new Blob([
781
+ $2cf6a2d8a6d031bc$var$AudioProcessorWorklet
782
+ ], {
783
+ type: 'application/javascript'
784
+ });
785
+ const $2cf6a2d8a6d031bc$var$src = URL.createObjectURL($2cf6a2d8a6d031bc$var$script);
786
+ const $2cf6a2d8a6d031bc$export$1f65f50a8cbff43c = $2cf6a2d8a6d031bc$var$src;
787
+
788
+
789
+
790
+
791
+ class $0d471c38435bdab6$export$439b217ca659a877 {
792
+ /**
793
+ * Create a new WavRecorder instance
794
+ * @param {{sampleRate?: number, outputToSpeakers?: boolean, debug?: boolean}} [options]
795
+ * @returns {WavRecorder}
796
+ */ constructor({ sampleRate: sampleRate = 44100, outputToSpeakers: outputToSpeakers = false, debug: debug = false } = {}){
797
+ // Script source
798
+ this.scriptSrc = (0, $2cf6a2d8a6d031bc$export$1f65f50a8cbff43c);
799
+ // Config
800
+ this.sampleRate = sampleRate;
801
+ this.outputToSpeakers = outputToSpeakers;
802
+ this.debug = !!debug;
803
+ this._deviceChangeCallback = null;
804
+ this._devices = [];
805
+ this.deviceSelection = null;
806
+ // State variables
807
+ this.stream = null;
808
+ this.processor = null;
809
+ this.source = null;
810
+ this.node = null;
811
+ this.recording = false;
812
+ // Event handling with AudioWorklet
813
+ this._lastEventId = 0;
814
+ this.eventReceipts = {};
815
+ this.eventTimeout = 5000;
816
+ // Process chunks of audio
817
+ this._chunkProcessor = ()=>{};
818
+ this._chunkProcessorSize = void 0;
819
+ this._chunkProcessorBuffer = {
820
+ raw: new ArrayBuffer(0),
821
+ mono: new ArrayBuffer(0)
822
+ };
823
+ }
824
+ /**
825
+ * Decodes audio data from multiple formats to a Blob, url, Float32Array and AudioBuffer
826
+ * @param {Blob|Float32Array|Int16Array|ArrayBuffer|number[]} audioData
827
+ * @param {number} sampleRate
828
+ * @param {number} fromSampleRate
829
+ * @returns {Promise<DecodedAudioType>}
830
+ */ static async decode(audioData, sampleRate = 44100, fromSampleRate = -1) {
831
+ const context = new AudioContext({
832
+ sampleRate: sampleRate
833
+ });
834
+ let arrayBuffer;
835
+ let blob;
836
+ if (audioData instanceof Blob) {
837
+ if (fromSampleRate !== -1) throw new Error(`Can not specify "fromSampleRate" when reading from Blob`);
838
+ blob = audioData;
839
+ arrayBuffer = await blob.arrayBuffer();
840
+ } else if (audioData instanceof ArrayBuffer) {
841
+ if (fromSampleRate !== -1) throw new Error(`Can not specify "fromSampleRate" when reading from ArrayBuffer`);
842
+ arrayBuffer = audioData;
843
+ blob = new Blob([
844
+ arrayBuffer
845
+ ], {
846
+ type: 'audio/wav'
847
+ });
848
+ } else {
849
+ let float32Array;
850
+ let data;
851
+ if (audioData instanceof Int16Array) {
852
+ data = audioData;
853
+ float32Array = new Float32Array(audioData.length);
854
+ for(let i = 0; i < audioData.length; i++)float32Array[i] = audioData[i] / 0x8000;
855
+ } else if (audioData instanceof Float32Array) float32Array = audioData;
856
+ else if (audioData instanceof Array) float32Array = new Float32Array(audioData);
857
+ else throw new Error(`"audioData" must be one of: Blob, Float32Arrray, Int16Array, ArrayBuffer, Array<number>`);
858
+ if (fromSampleRate === -1) throw new Error(`Must specify "fromSampleRate" when reading from Float32Array, In16Array or Array`);
859
+ else if (fromSampleRate < 3000) throw new Error(`Minimum "fromSampleRate" is 3000 (3kHz)`);
860
+ if (!data) data = (0, $a61750b8fbee4dae$export$13afda237b1c9846).floatTo16BitPCM(float32Array);
861
+ const audio = {
862
+ bitsPerSample: 16,
863
+ channels: [
864
+ float32Array
865
+ ],
866
+ data: data
867
+ };
868
+ const packer = new (0, $a61750b8fbee4dae$export$13afda237b1c9846)();
869
+ const result = packer.pack(fromSampleRate, audio);
870
+ blob = result.blob;
871
+ arrayBuffer = await blob.arrayBuffer();
872
+ }
873
+ const audioBuffer = await context.decodeAudioData(arrayBuffer);
874
+ const values = audioBuffer.getChannelData(0);
875
+ const url = URL.createObjectURL(blob);
876
+ return {
877
+ blob: blob,
878
+ url: url,
879
+ values: values,
880
+ audioBuffer: audioBuffer
881
+ };
882
+ }
883
+ /**
884
+ * Logs data in debug mode
885
+ * @param {...any} arguments
886
+ * @returns {true}
887
+ */ log() {
888
+ if (this.debug) this.log(...arguments);
889
+ return true;
890
+ }
891
+ /**
892
+ * Retrieves the current sampleRate for the recorder
893
+ * @returns {number}
894
+ */ getSampleRate() {
895
+ return this.sampleRate;
896
+ }
897
+ /**
898
+ * Retrieves the current status of the recording
899
+ * @returns {"ended"|"paused"|"recording"}
900
+ */ getStatus() {
901
+ if (!this.processor) return 'ended';
902
+ else if (!this.recording) return 'paused';
903
+ else return 'recording';
904
+ }
905
+ /**
906
+ * Sends an event to the AudioWorklet
907
+ * @private
908
+ * @param {string} name
909
+ * @param {{[key: string]: any}} data
910
+ * @param {AudioWorkletNode} [_processor]
911
+ * @returns {Promise<{[key: string]: any}>}
912
+ */ async _event(name, data = {}, _processor = null) {
913
+ _processor = _processor || this.processor;
914
+ if (!_processor) throw new Error('Can not send events without recording first');
915
+ const message = {
916
+ event: name,
917
+ id: this._lastEventId++,
918
+ data: data
919
+ };
920
+ _processor.port.postMessage(message);
921
+ const t0 = new Date().valueOf();
922
+ while(!this.eventReceipts[message.id]){
923
+ if (new Date().valueOf() - t0 > this.eventTimeout) throw new Error(`Timeout waiting for "${name}" event`);
924
+ await new Promise((res)=>setTimeout(()=>res(true), 1));
925
+ }
926
+ const payload = this.eventReceipts[message.id];
927
+ delete this.eventReceipts[message.id];
928
+ return payload;
929
+ }
930
+ /**
931
+ * Sets device change callback, remove if callback provided is `null`
932
+ * @param {(Array<MediaDeviceInfo & {default: boolean}>): void|null} callback
933
+ * @returns {true}
934
+ */ listenForDeviceChange(callback) {
935
+ if (callback === null && this._deviceChangeCallback) {
936
+ navigator.mediaDevices.removeEventListener('devicechange', this._deviceChangeCallback);
937
+ this._deviceChangeCallback = null;
938
+ } else if (callback !== null) {
939
+ // Basically a debounce; we only want this called once when devices change
940
+ // And we only want the most recent callback() to be executed
941
+ // if a few are operating at the same time
942
+ let lastId = 0;
943
+ let lastDevices = [];
944
+ const serializeDevices = (devices)=>devices.map((d)=>d.deviceId).sort().join(',');
945
+ const cb = async ()=>{
946
+ let id = ++lastId;
947
+ const devices = await this.listDevices();
948
+ if (id === lastId) {
949
+ if (serializeDevices(lastDevices) !== serializeDevices(devices)) {
950
+ lastDevices = devices;
951
+ callback(devices.slice());
952
+ }
953
+ }
954
+ };
955
+ navigator.mediaDevices.addEventListener('devicechange', cb);
956
+ cb();
957
+ this._deviceChangeCallback = cb;
958
+ }
959
+ return true;
960
+ }
961
+ /**
962
+ * Manually request permission to use the microphone
963
+ * @returns {Promise<true>}
964
+ */ async requestPermission() {
965
+ const permissionStatus = await navigator.permissions.query({
966
+ name: 'microphone'
967
+ });
968
+ if (permissionStatus.state === 'denied') window.alert('You must grant microphone access to use this feature.');
969
+ else if (permissionStatus.state === 'prompt') try {
970
+ const stream = await navigator.mediaDevices.getUserMedia({
971
+ audio: true
972
+ });
973
+ const tracks = stream.getTracks();
974
+ tracks.forEach((track)=>track.stop());
975
+ } catch (e) {
976
+ window.alert('You must grant microphone access to use this feature.');
977
+ }
978
+ return true;
979
+ }
980
+ /**
981
+ * List all eligible devices for recording, will request permission to use microphone
982
+ * @returns {Promise<Array<MediaDeviceInfo & {default: boolean}>>}
983
+ */ async listDevices() {
984
+ if (!navigator.mediaDevices || !('enumerateDevices' in navigator.mediaDevices)) throw new Error('Could not request user devices');
985
+ await this.requestPermission();
986
+ const devices = await navigator.mediaDevices.enumerateDevices();
987
+ const audioDevices = devices.filter((device)=>device.kind === 'audioinput');
988
+ return audioDevices;
989
+ // const defaultDeviceIndex = audioDevices.findIndex(
990
+ // (device) => device.deviceId === 'default'
991
+ // );
992
+ // const deviceList = [];
993
+ // if (defaultDeviceIndex !== -1) {
994
+ // let defaultDevice = audioDevices.splice(defaultDeviceIndex, 1)[0];
995
+ // let existingIndex = audioDevices.findIndex(
996
+ // (device) => device.groupId === defaultDevice.groupId
997
+ // );
998
+ // if (existingIndex !== -1) {
999
+ // defaultDevice = audioDevices.splice(existingIndex, 1)[0];
1000
+ // }
1001
+ // defaultDevice.default = true;
1002
+ // deviceList.push(defaultDevice);
1003
+ // }
1004
+ // return deviceList.concat(audioDevices);
1005
+ }
1006
+ /**
1007
+ * Begins a recording session and requests microphone permissions if not already granted
1008
+ * Microphone recording indicator will appear on browser tab but status will be "paused"
1009
+ * @param {string} [deviceId] if no device provided, default device will be used
1010
+ * @returns {Promise<true>}
1011
+ */ async begin(deviceId) {
1012
+ if (this.processor) throw new Error(`Already connected: please call .end() to start a new session`);
1013
+ if (!navigator.mediaDevices || !('getUserMedia' in navigator.mediaDevices)) throw new Error('Could not request user media');
1014
+ deviceId = deviceId ?? this.deviceSelection?.deviceId;
1015
+ try {
1016
+ const config = {
1017
+ audio: true
1018
+ };
1019
+ if (deviceId) config.audio = {
1020
+ deviceId: {
1021
+ exact: deviceId
1022
+ }
1023
+ };
1024
+ this.stream = await navigator.mediaDevices.getUserMedia(config);
1025
+ } catch (err) {
1026
+ throw new Error('Could not start media stream');
1027
+ }
1028
+ this.listDevices().then((devices)=>{
1029
+ deviceId = this.stream.getAudioTracks()[0].getSettings().deviceId;
1030
+ console.log('find current device', devices, deviceId, this.stream.getAudioTracks()[0].getSettings());
1031
+ this.deviceSelection = devices.find((d)=>d.deviceId === deviceId);
1032
+ console.log('current device', this.deviceSelection);
1033
+ });
1034
+ const context = new AudioContext({
1035
+ sampleRate: this.sampleRate
1036
+ });
1037
+ const source = context.createMediaStreamSource(this.stream);
1038
+ // Load and execute the module script.
1039
+ try {
1040
+ await context.audioWorklet.addModule(this.scriptSrc);
1041
+ } catch (e) {
1042
+ console.error(e);
1043
+ throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`);
1044
+ }
1045
+ const processor = new AudioWorkletNode(context, 'audio_processor');
1046
+ processor.port.onmessage = (e)=>{
1047
+ const { event: event, id: id, data: data } = e.data;
1048
+ if (event === 'receipt') this.eventReceipts[id] = data;
1049
+ else if (event === 'chunk') {
1050
+ if (this._chunkProcessorSize) {
1051
+ const buffer = this._chunkProcessorBuffer;
1052
+ this._chunkProcessorBuffer = {
1053
+ raw: (0, $a61750b8fbee4dae$export$13afda237b1c9846).mergeBuffers(buffer.raw, data.raw),
1054
+ mono: (0, $a61750b8fbee4dae$export$13afda237b1c9846).mergeBuffers(buffer.mono, data.mono)
1055
+ };
1056
+ if (this._chunkProcessorBuffer.mono.byteLength >= this._chunkProcessorSize) {
1057
+ this._chunkProcessor(this._chunkProcessorBuffer);
1058
+ this._chunkProcessorBuffer = {
1059
+ raw: new ArrayBuffer(0),
1060
+ mono: new ArrayBuffer(0)
1061
+ };
1062
+ }
1063
+ } else this._chunkProcessor(data);
1064
+ }
1065
+ };
1066
+ const node = source.connect(processor);
1067
+ const analyser = context.createAnalyser();
1068
+ analyser.fftSize = 8192;
1069
+ analyser.smoothingTimeConstant = 0.1;
1070
+ node.connect(analyser);
1071
+ if (this.outputToSpeakers) {
1072
+ // eslint-disable-next-line no-console
1073
+ console.warn("Warning: Output to speakers may affect sound quality,\nespecially due to system audio feedback preventative measures.\nuse only for debugging");
1074
+ analyser.connect(context.destination);
1075
+ }
1076
+ this.source = source;
1077
+ this.node = node;
1078
+ this.analyser = analyser;
1079
+ this.processor = processor;
1080
+ console.log('begin completed');
1081
+ return true;
1082
+ }
1083
+ /**
1084
+ * Gets the current frequency domain data from the recording track
1085
+ * @param {"frequency"|"music"|"voice"} [analysisType]
1086
+ * @param {number} [minDecibels] default -100
1087
+ * @param {number} [maxDecibels] default -30
1088
+ * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType}
1089
+ */ getFrequencies(analysisType = 'frequency', minDecibels = -100, maxDecibels = -30) {
1090
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1091
+ return (0, $5853841ab58516d4$export$2c3136da0bf130f9).getFrequencies(this.analyser, this.sampleRate, null, analysisType, minDecibels, maxDecibels);
1092
+ }
1093
+ /**
1094
+ * Pauses the recording
1095
+ * Keeps microphone stream open but halts storage of audio
1096
+ * @returns {Promise<true>}
1097
+ */ async pause() {
1098
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1099
+ else if (!this.recording) throw new Error('Already paused: please call .record() first');
1100
+ if (this._chunkProcessorBuffer.raw.byteLength) this._chunkProcessor(this._chunkProcessorBuffer);
1101
+ this.log('Pausing ...');
1102
+ await this._event('stop');
1103
+ this.recording = false;
1104
+ return true;
1105
+ }
1106
+ /**
1107
+ * Start recording stream and storing to memory from the connected audio source
1108
+ * @param {(data: { mono: Int16Array; raw: Int16Array }) => any} [chunkProcessor]
1109
+ * @param {number} [chunkSize] chunkProcessor will not be triggered until this size threshold met in mono audio
1110
+ * @returns {Promise<true>}
1111
+ */ async record(chunkProcessor = ()=>{}, chunkSize = 8192) {
1112
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1113
+ else if (this.recording) throw new Error('Already recording: please call .pause() first');
1114
+ else if (typeof chunkProcessor !== 'function') throw new Error(`chunkProcessor must be a function`);
1115
+ this._chunkProcessor = chunkProcessor;
1116
+ this._chunkProcessorSize = chunkSize;
1117
+ this._chunkProcessorBuffer = {
1118
+ raw: new ArrayBuffer(0),
1119
+ mono: new ArrayBuffer(0)
1120
+ };
1121
+ this.log('Recording ...');
1122
+ await this._event('start');
1123
+ this.recording = true;
1124
+ return true;
1125
+ }
1126
+ /**
1127
+ * Clears the audio buffer, empties stored recording
1128
+ * @returns {Promise<true>}
1129
+ */ async clear() {
1130
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1131
+ await this._event('clear');
1132
+ return true;
1133
+ }
1134
+ /**
1135
+ * Reads the current audio stream data
1136
+ * @returns {Promise<{meanValues: Float32Array, channels: Array<Float32Array>}>}
1137
+ */ async read() {
1138
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1139
+ this.log('Reading ...');
1140
+ const result = await this._event('read');
1141
+ return result;
1142
+ }
1143
+ /**
1144
+ * Saves the current audio stream to a file
1145
+ * @param {boolean} [force] Force saving while still recording
1146
+ * @returns {Promise<import('./wav_packer.js').WavPackerAudioType>}
1147
+ */ async save(force = false) {
1148
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1149
+ if (!force && this.recording) throw new Error('Currently recording: please call .pause() first, or call .save(true) to force');
1150
+ this.log('Exporting ...');
1151
+ const exportData = await this._event('export');
1152
+ const packer = new (0, $a61750b8fbee4dae$export$13afda237b1c9846)();
1153
+ const result = packer.pack(this.sampleRate, exportData.audio);
1154
+ return result;
1155
+ }
1156
+ /**
1157
+ * Ends the current recording session and saves the result
1158
+ * @returns {Promise<import('./wav_packer.js').WavPackerAudioType>}
1159
+ */ async end() {
1160
+ if (!this.processor) throw new Error('Session ended: please call .begin() first');
1161
+ const _processor = this.processor;
1162
+ this.log('Stopping ...');
1163
+ await this._event('stop');
1164
+ this.recording = false;
1165
+ const tracks = this.stream.getTracks();
1166
+ tracks.forEach((track)=>track.stop());
1167
+ this.log('Exporting ...');
1168
+ const exportData = await this._event('export', {}, _processor);
1169
+ this.processor.disconnect();
1170
+ this.source.disconnect();
1171
+ this.node.disconnect();
1172
+ this.analyser.disconnect();
1173
+ this.stream = null;
1174
+ this.processor = null;
1175
+ this.source = null;
1176
+ this.node = null;
1177
+ const packer = new (0, $a61750b8fbee4dae$export$13afda237b1c9846)();
1178
+ const result = packer.pack(this.sampleRate, exportData.audio);
1179
+ return result;
1180
+ }
1181
+ /**
1182
+ * Performs a full cleanup of WavRecorder instance
1183
+ * Stops actively listening via microphone and removes existing listeners
1184
+ * @returns {Promise<true>}
1185
+ */ async quit() {
1186
+ this.listenForDeviceChange(null);
1187
+ // we do not reset this on end so that selections persist across starts
1188
+ this.deviceSelection = null;
1189
+ if (this.processor) await this.end();
1190
+ return true;
1191
+ }
1192
+ }
1193
+ globalThis.WavRecorder = $0d471c38435bdab6$export$439b217ca659a877;
1194
+
1195
+
1196
+
1197
+
1198
+
1199
+ class $7cef7a69bdf8f84d$export$2934cf2d25c67a48 {
1200
+ /**
1201
+ * Create a new MediaStreamRecorder instance
1202
+ * @param {{sampleRate?: number, outputToSpeakers?: boolean, debug?: boolean}} [options]
1203
+ * @returns {MediaStreamRecorder}
1204
+ */ constructor({ sampleRate: sampleRate = 44100, outputToSpeakers: outputToSpeakers = false, debug: debug = false } = {}){
1205
+ // Script source
1206
+ this.scriptSrc = (0, $2cf6a2d8a6d031bc$export$1f65f50a8cbff43c);
1207
+ // Config
1208
+ this.sampleRate = sampleRate;
1209
+ this.outputToSpeakers = outputToSpeakers;
1210
+ this.debug = !!debug;
1211
+ // State variables
1212
+ this.stream = null;
1213
+ this.processor = null;
1214
+ this.source = null;
1215
+ this.node = null;
1216
+ this.recording = false;
1217
+ // Event handling with AudioWorklet
1218
+ this._lastEventId = 0;
1219
+ this.eventReceipts = {};
1220
+ this.eventTimeout = 5000;
1221
+ // Process chunks of audio
1222
+ this._chunkProcessor = ()=>{};
1223
+ this._chunkProcessorSize = void 0;
1224
+ this._chunkProcessorBuffer = {
1225
+ raw: new ArrayBuffer(0),
1226
+ mono: new ArrayBuffer(0)
1227
+ };
1228
+ }
1229
+ /**
1230
+ * Logs data in debug mode
1231
+ * @param {...any} arguments
1232
+ * @returns {true}
1233
+ */ log() {
1234
+ if (this.debug) this.log(...arguments);
1235
+ return true;
1236
+ }
1237
+ /**
1238
+ * Retrieves the current sampleRate for the recorder
1239
+ * @returns {number}
1240
+ */ getSampleRate() {
1241
+ return this.sampleRate;
1242
+ }
1243
+ /**
1244
+ * Retrieves the current status of the recording
1245
+ * @returns {"ended"|"paused"|"recording"}
1246
+ */ getStatus() {
1247
+ if (!this.processor) return "ended";
1248
+ else if (!this.recording) return "paused";
1249
+ else return "recording";
1250
+ }
1251
+ /**
1252
+ * Sends an event to the AudioWorklet
1253
+ * @private
1254
+ * @param {string} name
1255
+ * @param {{[key: string]: any}} data
1256
+ * @param {AudioWorkletNode} [_processor]
1257
+ * @returns {Promise<{[key: string]: any}>}
1258
+ */ async _event(name, data = {}, _processor = null) {
1259
+ _processor = _processor || this.processor;
1260
+ if (!_processor) throw new Error("Can not send events without recording first");
1261
+ const message = {
1262
+ event: name,
1263
+ id: this._lastEventId++,
1264
+ data: data
1265
+ };
1266
+ _processor.port.postMessage(message);
1267
+ const t0 = new Date().valueOf();
1268
+ while(!this.eventReceipts[message.id]){
1269
+ if (new Date().valueOf() - t0 > this.eventTimeout) throw new Error(`Timeout waiting for "${name}" event`);
1270
+ await new Promise((res)=>setTimeout(()=>res(true), 1));
1271
+ }
1272
+ const payload = this.eventReceipts[message.id];
1273
+ delete this.eventReceipts[message.id];
1274
+ return payload;
1275
+ }
1276
+ /**
1277
+ * Begins a recording session for the given audioTrack
1278
+ * Microphone recording indicator will appear on browser tab but status will be "paused"
1279
+ * @param {MediaStreamTrack} [audioTrack] if no device provided, default device will be used
1280
+ * @returns {Promise<true>}
1281
+ */ async begin(audioTrack) {
1282
+ if (this.processor) throw new Error(`Already connected: please call .end() to start a new session`);
1283
+ if (!audioTrack || audioTrack.kind !== "audio") throw new Error("No audio track provided");
1284
+ this.stream = new MediaStream([
1285
+ audioTrack
1286
+ ]);
1287
+ const context = new AudioContext({
1288
+ sampleRate: this.sampleRate
1289
+ });
1290
+ const source = context.createMediaStreamSource(this.stream);
1291
+ // Load and execute the module script.
1292
+ try {
1293
+ await context.audioWorklet.addModule(this.scriptSrc);
1294
+ } catch (e) {
1295
+ console.error(e);
1296
+ throw new Error(`Could not add audioWorklet module: ${this.scriptSrc}`);
1297
+ }
1298
+ const processor = new AudioWorkletNode(context, "audio_processor");
1299
+ processor.port.onmessage = (e)=>{
1300
+ const { event: event, id: id, data: data } = e.data;
1301
+ if (event === "receipt") this.eventReceipts[id] = data;
1302
+ else if (event === "chunk") {
1303
+ if (this._chunkProcessorSize) {
1304
+ const buffer = this._chunkProcessorBuffer;
1305
+ this._chunkProcessorBuffer = {
1306
+ raw: (0, $a61750b8fbee4dae$export$13afda237b1c9846).mergeBuffers(buffer.raw, data.raw),
1307
+ mono: (0, $a61750b8fbee4dae$export$13afda237b1c9846).mergeBuffers(buffer.mono, data.mono)
1308
+ };
1309
+ if (this._chunkProcessorBuffer.mono.byteLength >= this._chunkProcessorSize) {
1310
+ this._chunkProcessor(this._chunkProcessorBuffer);
1311
+ this._chunkProcessorBuffer = {
1312
+ raw: new ArrayBuffer(0),
1313
+ mono: new ArrayBuffer(0)
1314
+ };
1315
+ }
1316
+ } else this._chunkProcessor(data);
1317
+ }
1318
+ };
1319
+ const node = source.connect(processor);
1320
+ const analyser = context.createAnalyser();
1321
+ analyser.fftSize = 8192;
1322
+ analyser.smoothingTimeConstant = 0.1;
1323
+ node.connect(analyser);
1324
+ if (this.outputToSpeakers) {
1325
+ // eslint-disable-next-line no-console
1326
+ console.warn("Warning: Output to speakers may affect sound quality,\nespecially due to system audio feedback preventative measures.\nuse only for debugging");
1327
+ analyser.connect(context.destination);
1328
+ }
1329
+ this.source = source;
1330
+ this.node = node;
1331
+ this.analyser = analyser;
1332
+ this.processor = processor;
1333
+ return true;
1334
+ }
1335
+ /**
1336
+ * Gets the current frequency domain data from the recording track
1337
+ * @param {"frequency"|"music"|"voice"} [analysisType]
1338
+ * @param {number} [minDecibels] default -100
1339
+ * @param {number} [maxDecibels] default -30
1340
+ * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType}
1341
+ */ getFrequencies(analysisType = "frequency", minDecibels = -100, maxDecibels = -30) {
1342
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1343
+ return (0, $5853841ab58516d4$export$2c3136da0bf130f9).getFrequencies(this.analyser, this.sampleRate, null, analysisType, minDecibels, maxDecibels);
1344
+ }
1345
+ /**
1346
+ * Pauses the recording
1347
+ * Keeps microphone stream open but halts storage of audio
1348
+ * @returns {Promise<true>}
1349
+ */ async pause() {
1350
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1351
+ else if (!this.recording) throw new Error("Already paused: please call .record() first");
1352
+ if (this._chunkProcessorBuffer.raw.byteLength) this._chunkProcessor(this._chunkProcessorBuffer);
1353
+ this.log("Pausing ...");
1354
+ await this._event("stop");
1355
+ this.recording = false;
1356
+ return true;
1357
+ }
1358
+ /**
1359
+ * Start recording stream and storing to memory from the connected audio source
1360
+ * @param {(data: { mono: Int16Array; raw: Int16Array }) => any} [chunkProcessor]
1361
+ * @param {number} [chunkSize] chunkProcessor will not be triggered until this size threshold met in mono audio
1362
+ * @returns {Promise<true>}
1363
+ */ async record(chunkProcessor = ()=>{}, chunkSize = 8192) {
1364
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1365
+ else if (this.recording) throw new Error("Already recording: HELLO please call .pause() first");
1366
+ else if (typeof chunkProcessor !== "function") throw new Error(`chunkProcessor must be a function`);
1367
+ this._chunkProcessor = chunkProcessor;
1368
+ this._chunkProcessorSize = chunkSize;
1369
+ this._chunkProcessorBuffer = {
1370
+ raw: new ArrayBuffer(0),
1371
+ mono: new ArrayBuffer(0)
1372
+ };
1373
+ this.log("Recording ...");
1374
+ await this._event("start");
1375
+ this.recording = true;
1376
+ return true;
1377
+ }
1378
+ /**
1379
+ * Clears the audio buffer, empties stored recording
1380
+ * @returns {Promise<true>}
1381
+ */ async clear() {
1382
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1383
+ await this._event("clear");
1384
+ return true;
1385
+ }
1386
+ /**
1387
+ * Reads the current audio stream data
1388
+ * @returns {Promise<{meanValues: Float32Array, channels: Array<Float32Array>}>}
1389
+ */ async read() {
1390
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1391
+ this.log("Reading ...");
1392
+ const result = await this._event("read");
1393
+ return result;
1394
+ }
1395
+ /**
1396
+ * Saves the current audio stream to a file
1397
+ * @param {boolean} [force] Force saving while still recording
1398
+ * @returns {Promise<import('./wav_packer.js').WavPackerAudioType>}
1399
+ */ async save(force = false) {
1400
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1401
+ if (!force && this.recording) throw new Error("Currently recording: please call .pause() first, or call .save(true) to force");
1402
+ this.log("Exporting ...");
1403
+ const exportData = await this._event("export");
1404
+ const packer = new (0, $a61750b8fbee4dae$export$13afda237b1c9846)();
1405
+ const result = packer.pack(this.sampleRate, exportData.audio);
1406
+ return result;
1407
+ }
1408
+ /**
1409
+ * Ends the current recording session and saves the result
1410
+ * @returns {Promise<import('./wav_packer.js').WavPackerAudioType>}
1411
+ */ async end() {
1412
+ if (!this.processor) throw new Error("Session ended: please call .begin() first");
1413
+ const _processor = this.processor;
1414
+ this.log("Stopping ...");
1415
+ await this._event("stop");
1416
+ this.recording = false;
1417
+ this.log("Exporting ...");
1418
+ const exportData = await this._event("export", {}, _processor);
1419
+ this.processor.disconnect();
1420
+ this.source.disconnect();
1421
+ this.node.disconnect();
1422
+ this.analyser.disconnect();
1423
+ this.stream = null;
1424
+ this.processor = null;
1425
+ this.source = null;
1426
+ this.node = null;
1427
+ const packer = new (0, $a61750b8fbee4dae$export$13afda237b1c9846)();
1428
+ const result = packer.pack(this.sampleRate, exportData.audio);
1429
+ return result;
1430
+ }
1431
+ /**
1432
+ * Performs a full cleanup of WavRecorder instance
1433
+ * Stops actively listening via microphone and removes existing listeners
1434
+ * @returns {Promise<true>}
1435
+ */ async quit() {
1436
+ this.listenForDeviceChange(null);
1437
+ if (this.processor) await this.end();
1438
+ return true;
1439
+ }
1440
+ }
1441
+ globalThis.WavRecorder = WavRecorder;
1442
+
1443
+
1444
+
1445
+
1446
+ var $28bdeb35415d61e8$export$ef180de88fd317cc;
1447
+ (function(DailyRTVIMessageType) {
1448
+ DailyRTVIMessageType["AUDIO_BUFFERING_STARTED"] = "audio-buffering-started";
1449
+ DailyRTVIMessageType["AUDIO_BUFFERING_STOPPED"] = "audio-buffering-stopped";
1450
+ })($28bdeb35415d61e8$export$ef180de88fd317cc || ($28bdeb35415d61e8$export$ef180de88fd317cc = {}));
1451
+ class $28bdeb35415d61e8$var$DailyCallWrapper {
1452
+ constructor(daily){
1453
+ this._daily = daily;
1454
+ this._proxy = new Proxy(this._daily, {
1455
+ get: (target, prop, receiver)=>{
1456
+ if (typeof target[prop] === "function") {
1457
+ let errMsg;
1458
+ switch(String(prop)){
1459
+ // Disable methods that modify the lifecycle of the call. These operations
1460
+ // should be performed via the RTVI client in order to keep state in sync.
1461
+ case "preAuth":
1462
+ errMsg = `Calls to preAuth() are disabled. Please use Transport.preAuth()`;
1463
+ break;
1464
+ case "startCamera":
1465
+ errMsg = `Calls to startCamera() are disabled. Please use RTVIClient.initDevices() or Transport.initDevices()`;
1466
+ break;
1467
+ case "join":
1468
+ errMsg = `Calls to join() are disabled. Please use RTVIClient.connect()`;
1469
+ break;
1470
+ case "leave":
1471
+ errMsg = `Calls to leave() are disabled. Please use RTVIClient.disconnect()`;
1472
+ break;
1473
+ case "destroy":
1474
+ errMsg = `Calls to destroy() are disabled.`;
1475
+ break;
1476
+ }
1477
+ if (errMsg) return ()=>{
1478
+ throw new Error(errMsg);
1479
+ };
1480
+ // Forward other method calls
1481
+ return (...args)=>{
1482
+ return target[prop](...args);
1483
+ };
1484
+ }
1485
+ // Forward property access
1486
+ return Reflect.get(target, prop, receiver);
1487
+ }
1488
+ });
1489
+ }
1490
+ get proxy() {
1491
+ return this._proxy;
1492
+ }
1493
+ }
34
1494
  class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclientjs.Transport) {
35
- constructor({ dailyFactoryOptions: dailyFactoryOptions = {} } = {}){
1495
+ constructor({ dailyFactoryOptions: dailyFactoryOptions = {}, bufferLocalAudioUntilBotReady: bufferLocalAudioUntilBotReady = false } = {}){
36
1496
  super();
37
1497
  this._botId = "";
38
1498
  this._selectedCam = {};
39
1499
  this._selectedMic = {};
40
1500
  this._selectedSpeaker = {};
1501
+ this._currentAudioTrack = null;
1502
+ this._audioQueue = [];
1503
+ this._callbacks = {};
41
1504
  this._dailyFactoryOptions = dailyFactoryOptions;
42
- }
43
- initialize(options, messageHandler) {
44
- this._callbacks = options.callbacks ?? {};
45
- this._onMessage = messageHandler;
46
- const existingInstance = (0, ($parcel$interopDefault($jrPAn$dailycodailyjs))).getCallInstance();
47
- if (existingInstance) existingInstance.destroy();
1505
+ this._bufferLocalAudioUntilBotReady = bufferLocalAudioUntilBotReady;
48
1506
  this._daily = (0, ($parcel$interopDefault($jrPAn$dailycodailyjs))).createCallObject({
49
1507
  ...this._dailyFactoryOptions,
50
- // Default is cam off
51
- startVideoOff: options.enableCam != true,
52
- // Default is mic on
53
- startAudioOff: options.enableMic == false,
54
1508
  allowMultipleCallInstances: true
55
1509
  });
1510
+ this._dailyWrapper = new $28bdeb35415d61e8$var$DailyCallWrapper(this._daily);
1511
+ }
1512
+ setupRecorder() {
1513
+ this._mediaStreamRecorder = new (0, $7cef7a69bdf8f84d$export$2934cf2d25c67a48)({
1514
+ sampleRate: $28bdeb35415d61e8$export$b1ca982aa1e488c1.RECORDER_SAMPLE_RATE
1515
+ });
1516
+ }
1517
+ handleUserAudioStream(data) {
1518
+ this._audioQueue.push(data);
1519
+ }
1520
+ flushAudioQueue() {
1521
+ const batchSize = 10; // Number of buffers to send in one message
1522
+ if (this._audioQueue.length === 0) return;
1523
+ (0, $jrPAn$pipecataiclientjs.logger).debug(`Will flush audio queue: ${this._audioQueue.length}`);
1524
+ while(this._audioQueue.length > 0){
1525
+ const batch = [];
1526
+ // Collect up to `batchSize` items
1527
+ while(batch.length < batchSize && this._audioQueue.length > 0){
1528
+ const queuedData = this._audioQueue.shift();
1529
+ if (queuedData) batch.push(queuedData);
1530
+ }
1531
+ if (batch.length > 0) this._sendAudioBatch(batch);
1532
+ }
1533
+ }
1534
+ _sendAudioBatch(dataBatch) {
1535
+ const encodedBatch = dataBatch.map((data)=>{
1536
+ const pcmByteArray = new Uint8Array(data);
1537
+ return btoa(String.fromCharCode(...pcmByteArray));
1538
+ });
1539
+ const rtviMessage = {
1540
+ id: "raw-audio-batch",
1541
+ label: "rtvi-ai",
1542
+ type: "raw-audio-batch",
1543
+ data: {
1544
+ base64AudioBatch: encodedBatch,
1545
+ sampleRate: $28bdeb35415d61e8$export$b1ca982aa1e488c1.RECORDER_SAMPLE_RATE,
1546
+ numChannels: 1
1547
+ }
1548
+ };
1549
+ this.sendMessage(rtviMessage);
1550
+ }
1551
+ initialize(options, messageHandler) {
1552
+ if (this._bufferLocalAudioUntilBotReady) this.setupRecorder();
1553
+ this._callbacks = options.callbacks ?? {};
1554
+ this._onMessage = messageHandler;
1555
+ if (this._dailyFactoryOptions.startVideoOff == null || options.enableCam != null) // Default is cam off
1556
+ this._dailyFactoryOptions.startVideoOff = !(options.enableCam ?? false);
1557
+ if (this._dailyFactoryOptions.startAudioOff == null || options.enableMic != null) // Default is mic on
1558
+ this._dailyFactoryOptions.startAudioOff = !(options.enableMic ?? true);
56
1559
  this.attachEventListeners();
57
1560
  this.state = "disconnected";
58
1561
  (0, $jrPAn$pipecataiclientjs.logger).debug("[RTVI Transport] Initialized");
59
1562
  }
1563
+ get dailyCallClient() {
1564
+ return this._dailyWrapper.proxy;
1565
+ }
60
1566
  get state() {
61
1567
  return this._state;
62
1568
  }
@@ -127,7 +1633,7 @@ class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclie
127
1633
  return this._daily.localScreenAudio() || this._daily.localScreenVideo();
128
1634
  }
129
1635
  tracks() {
130
- const participants = this._daily?.participants() ?? {};
1636
+ const participants = this._daily.participants() ?? {};
131
1637
  const bot = participants?.[this._botId];
132
1638
  const tracks = {
133
1639
  local: {
@@ -143,6 +1649,26 @@ class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclie
143
1649
  };
144
1650
  return tracks;
145
1651
  }
1652
+ async startRecording() {
1653
+ try {
1654
+ (0, $jrPAn$pipecataiclientjs.logger).info("[RTVI Transport] Initializing recording");
1655
+ await this._mediaStreamRecorder.record((data)=>{
1656
+ this.handleUserAudioStream(data.mono);
1657
+ }, $28bdeb35415d61e8$export$b1ca982aa1e488c1.RECORDER_CHUNK_SIZE);
1658
+ this._onMessage({
1659
+ type: $28bdeb35415d61e8$export$ef180de88fd317cc.AUDIO_BUFFERING_STARTED,
1660
+ data: {}
1661
+ });
1662
+ (0, $jrPAn$pipecataiclientjs.logger).info("[RTVI Transport] Recording Initialized");
1663
+ } catch (e) {
1664
+ const err = e;
1665
+ if (!err.message.includes("Already recording")) (0, $jrPAn$pipecataiclientjs.logger).error("Error starting recording", e);
1666
+ }
1667
+ }
1668
+ async preAuth(dailyFactoryOptions) {
1669
+ this._dailyFactoryOptions = dailyFactoryOptions;
1670
+ await this._daily.preAuth(dailyFactoryOptions);
1671
+ }
146
1672
  async initDevices() {
147
1673
  if (!this._daily) throw new (0, $jrPAn$pipecataiclientjs.RTVIError)("Transport instance not initialized");
148
1674
  this.state = "initializing";
@@ -151,14 +1677,14 @@ class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclie
151
1677
  const cams = devices.filter((d)=>d.kind === "videoinput");
152
1678
  const mics = devices.filter((d)=>d.kind === "audioinput");
153
1679
  const speakers = devices.filter((d)=>d.kind === "audiooutput");
1680
+ this._selectedCam = infos.camera;
1681
+ this._selectedMic = infos.mic;
1682
+ this._selectedSpeaker = infos.speaker;
154
1683
  this._callbacks.onAvailableCamsUpdated?.(cams);
155
1684
  this._callbacks.onAvailableMicsUpdated?.(mics);
156
1685
  this._callbacks.onAvailableSpeakersUpdated?.(speakers);
157
- this._selectedCam = infos.camera;
158
1686
  this._callbacks.onCamUpdated?.(infos.camera);
159
- this._selectedMic = infos.mic;
160
1687
  this._callbacks.onMicUpdated?.(infos.mic);
161
- this._selectedSpeaker = infos.speaker;
162
1688
  this._callbacks.onSpeakerUpdated?.(infos.speaker);
163
1689
  // Instantiate audio observers
164
1690
  if (!this._daily.isLocalAudioLevelObserverRunning()) await this._daily.startLocalAudioLevelObserver(100);
@@ -169,14 +1695,15 @@ class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclie
169
1695
  if (!this._daily) throw new (0, $jrPAn$pipecataiclientjs.RTVIError)("Transport instance not initialized");
170
1696
  if (abortController.signal.aborted) return;
171
1697
  this.state = "connecting";
1698
+ const opts = this._dailyFactoryOptions;
1699
+ opts.url = authBundle.room_url ?? opts.url;
1700
+ if (authBundle.token != null) opts.token = authBundle.token;
172
1701
  try {
173
- await this._daily.join({
174
- url: authBundle.room_url,
175
- token: authBundle.token
176
- });
1702
+ await this._daily.join(opts);
177
1703
  const room = await this._daily.room();
178
1704
  if (room && "id" in room) this._expiry = room.config?.exp;
179
1705
  } catch (e) {
1706
+ (0, $jrPAn$pipecataiclientjs.logger).error("Failed to join room", e);
180
1707
  this.state = "error";
181
1708
  throw new (0, $jrPAn$pipecataiclientjs.TransportStartError)();
182
1709
  }
@@ -187,16 +1714,30 @@ class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclie
187
1714
  async sendReadyMessage() {
188
1715
  return new Promise((resolve)=>{
189
1716
  (async ()=>{
190
- this._daily.on("track-started", (ev)=>{
1717
+ const readyHandler = (ev)=>{
191
1718
  if (!ev.participant?.local) {
192
1719
  this.state = "ready";
1720
+ this.flushAudioQueue();
193
1721
  this.sendMessage((0, $jrPAn$pipecataiclientjs.RTVIMessage).clientReady());
1722
+ this.stopRecording();
1723
+ this._daily.off("track-started", readyHandler);
194
1724
  resolve();
195
1725
  }
196
- });
1726
+ };
1727
+ this._daily.on("track-started", readyHandler);
197
1728
  })();
198
1729
  });
199
1730
  }
1731
+ stopRecording() {
1732
+ if (this._mediaStreamRecorder && this._mediaStreamRecorder.getStatus() !== "ended") {
1733
+ // disconnecting, we don't need to record anymore
1734
+ this._mediaStreamRecorder.end();
1735
+ this._onMessage({
1736
+ type: $28bdeb35415d61e8$export$ef180de88fd317cc.AUDIO_BUFFERING_STOPPED,
1737
+ data: {}
1738
+ });
1739
+ }
1740
+ }
200
1741
  attachEventListeners() {
201
1742
  this._daily.on("available-devices-updated", this.handleAvailableDevicesUpdated.bind(this));
202
1743
  this._daily.on("selected-devices-updated", this.handleSelectedDevicesUpdated.bind(this));
@@ -211,10 +1752,13 @@ class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclie
211
1752
  this._daily.on("nonfatal-error", this.handleNonFatalError.bind(this));
212
1753
  }
213
1754
  async disconnect() {
1755
+ this.state = "disconnecting";
214
1756
  this._daily.stopLocalAudioLevelObserver();
215
1757
  this._daily.stopRemoteParticipantsAudioLevelObserver();
1758
+ this._audioQueue = [];
1759
+ this._currentAudioTrack = null;
1760
+ this.stopRecording();
216
1761
  await this._daily.leave();
217
- await this._daily.destroy();
218
1762
  }
219
1763
  sendMessage(message) {
220
1764
  this._daily.sendAppMessage(message, "*");
@@ -246,9 +1790,34 @@ class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclie
246
1790
  this._callbacks.onSpeakerUpdated?.(ev.devices.speaker);
247
1791
  }
248
1792
  }
1793
+ async handleLocalAudioTrack(track) {
1794
+ if (this.state == "ready" || !this._bufferLocalAudioUntilBotReady) return;
1795
+ const status = this._mediaStreamRecorder.getStatus();
1796
+ switch(status){
1797
+ case "ended":
1798
+ await this._mediaStreamRecorder.begin(track);
1799
+ await this.startRecording();
1800
+ break;
1801
+ case "paused":
1802
+ await this.startRecording();
1803
+ break;
1804
+ case "recording":
1805
+ default:
1806
+ if (this._currentAudioTrack !== track) {
1807
+ await this._mediaStreamRecorder.end();
1808
+ await this._mediaStreamRecorder.begin(track);
1809
+ await this.startRecording();
1810
+ } else (0, $jrPAn$pipecataiclientjs.logger).warn("track-started event received for current track and already recording");
1811
+ break;
1812
+ }
1813
+ this._currentAudioTrack = track;
1814
+ }
249
1815
  handleTrackStarted(ev) {
250
1816
  if (ev.type === "screenAudio" || ev.type === "screenVideo") this._callbacks.onScreenTrackStarted?.(ev.track, ev.participant ? $28bdeb35415d61e8$var$dailyParticipantToParticipant(ev.participant) : undefined);
251
- else this._callbacks.onTrackStarted?.(ev.track, ev.participant ? $28bdeb35415d61e8$var$dailyParticipantToParticipant(ev.participant) : undefined);
1817
+ else {
1818
+ if (ev.participant?.local && ev.track.kind === "audio") this.handleLocalAudioTrack(ev.track);
1819
+ this._callbacks.onTrackStarted?.(ev.track, ev.participant ? $28bdeb35415d61e8$var$dailyParticipantToParticipant(ev.participant) : undefined);
1820
+ }
252
1821
  }
253
1822
  handleTrackStopped(ev) {
254
1823
  if (ev.type === "screenAudio" || ev.type === "screenVideo") this._callbacks.onScreenTrackStopped?.(ev.track, ev.participant ? $28bdeb35415d61e8$var$dailyParticipantToParticipant(ev.participant) : undefined);
@@ -281,7 +1850,7 @@ class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclie
281
1850
  }
282
1851
  }
283
1852
  handleLeftMeeting() {
284
- this.state = "disconnecting";
1853
+ this.state = "disconnected";
285
1854
  this._botId = "";
286
1855
  this._callbacks.onDisconnected?.();
287
1856
  }
@@ -293,6 +1862,8 @@ class $28bdeb35415d61e8$export$b1ca982aa1e488c1 extends (0, $jrPAn$pipecataiclie
293
1862
  }
294
1863
  }
295
1864
  }
1865
+ $28bdeb35415d61e8$export$b1ca982aa1e488c1.RECORDER_SAMPLE_RATE = 16000;
1866
+ $28bdeb35415d61e8$export$b1ca982aa1e488c1.RECORDER_CHUNK_SIZE = 512;
296
1867
  const $28bdeb35415d61e8$var$dailyParticipantToParticipant = (p)=>({
297
1868
  id: p.user_id,
298
1869
  local: p.local,