@absolutejs/voice 0.0.22-beta.673 → 0.0.22-beta.674
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/client/htmxBootstrap.js +61 -6
- package/dist/client/index.js +61 -6
- package/dist/core/types.d.ts +9 -3
- package/dist/testing/index.js +61 -6
- package/package.json +1 -1
|
@@ -2135,6 +2135,49 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2135
2135
|
let maxGapMs = 0;
|
|
2136
2136
|
let errorCount = 0;
|
|
2137
2137
|
let hasScheduledAnyChunk = false;
|
|
2138
|
+
let buffersScheduled = 0;
|
|
2139
|
+
let lastScheduledTurnId;
|
|
2140
|
+
const audioFormats = new Set;
|
|
2141
|
+
let lastAudioFormat = null;
|
|
2142
|
+
let formatChangeCount = 0;
|
|
2143
|
+
let incompleteFrameChunkCount = 0;
|
|
2144
|
+
let discardedPartialFrameBytes = 0;
|
|
2145
|
+
let pendingFrameBytes = new Uint8Array;
|
|
2146
|
+
let pendingFrameFormat = null;
|
|
2147
|
+
let pendingFrameTurnId;
|
|
2148
|
+
const formatKey = (format) => `${format.container}/${format.encoding}/${String(format.channels)}ch/${String(format.sampleRateHz)}hz`;
|
|
2149
|
+
const frameAlignedChunk = (chunk) => {
|
|
2150
|
+
const format = formatKey(chunk.format);
|
|
2151
|
+
audioFormats.add(format);
|
|
2152
|
+
if (lastAudioFormat !== null && lastAudioFormat !== format) {
|
|
2153
|
+
formatChangeCount += 1;
|
|
2154
|
+
}
|
|
2155
|
+
lastAudioFormat = format;
|
|
2156
|
+
if (pendingFrameBytes.byteLength > 0 && (pendingFrameFormat !== format || pendingFrameTurnId !== chunk.turnId)) {
|
|
2157
|
+
discardedPartialFrameBytes += pendingFrameBytes.byteLength;
|
|
2158
|
+
pendingFrameBytes = new Uint8Array;
|
|
2159
|
+
}
|
|
2160
|
+
const joined = pendingFrameBytes.byteLength === 0 ? chunk.chunk : (() => {
|
|
2161
|
+
const bytes = new Uint8Array(pendingFrameBytes.byteLength + chunk.chunk.byteLength);
|
|
2162
|
+
bytes.set(pendingFrameBytes);
|
|
2163
|
+
bytes.set(chunk.chunk, pendingFrameBytes.byteLength);
|
|
2164
|
+
return bytes;
|
|
2165
|
+
})();
|
|
2166
|
+
const frameBytes = Math.max(1, chunk.format.channels) * 2;
|
|
2167
|
+
const completeBytes = joined.byteLength - joined.byteLength % frameBytes;
|
|
2168
|
+
const remainderBytes = joined.byteLength - completeBytes;
|
|
2169
|
+
if (remainderBytes > 0) {
|
|
2170
|
+
incompleteFrameChunkCount += 1;
|
|
2171
|
+
pendingFrameBytes = joined.slice(completeBytes);
|
|
2172
|
+
pendingFrameFormat = format;
|
|
2173
|
+
pendingFrameTurnId = chunk.turnId;
|
|
2174
|
+
} else {
|
|
2175
|
+
pendingFrameBytes = new Uint8Array;
|
|
2176
|
+
pendingFrameFormat = null;
|
|
2177
|
+
pendingFrameTurnId = undefined;
|
|
2178
|
+
}
|
|
2179
|
+
return completeBytes > 0 ? { ...chunk, chunk: joined.subarray(0, completeBytes) } : null;
|
|
2180
|
+
};
|
|
2138
2181
|
const sampleConcurrency = () => {
|
|
2139
2182
|
if (activeAudioPlayers.size > observedPeakConcurrency) {
|
|
2140
2183
|
observedPeakConcurrency = activeAudioPlayers.size;
|
|
@@ -2254,7 +2297,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2254
2297
|
queueEndTime = audioContext.currentTime;
|
|
2255
2298
|
return audioContext;
|
|
2256
2299
|
};
|
|
2257
|
-
const scheduleBuffer = (context, buffer, rate) => {
|
|
2300
|
+
const scheduleBuffer = (context, buffer, rate, turnId) => {
|
|
2258
2301
|
const node = context.createBufferSource();
|
|
2259
2302
|
node.buffer = buffer;
|
|
2260
2303
|
if (node.playbackRate) {
|
|
@@ -2272,13 +2315,15 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2272
2315
|
};
|
|
2273
2316
|
const earliestStart = context.currentTime + lookaheadSeconds;
|
|
2274
2317
|
const startAt = Math.max(earliestStart, queueEndTime);
|
|
2275
|
-
if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
2318
|
+
if (hasScheduledAnyChunk && turnId !== undefined && turnId === lastScheduledTurnId && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
2276
2319
|
const gapMs = (earliestStart - queueEndTime) * 1000;
|
|
2277
2320
|
gapCount += 1;
|
|
2278
2321
|
totalGapMs += gapMs;
|
|
2279
2322
|
maxGapMs = Math.max(maxGapMs, gapMs);
|
|
2280
2323
|
}
|
|
2281
2324
|
hasScheduledAnyChunk = true;
|
|
2325
|
+
lastScheduledTurnId = turnId;
|
|
2326
|
+
buffersScheduled += 1;
|
|
2282
2327
|
scheduledDurationMs += buffer.duration / rate * 1000;
|
|
2283
2328
|
sampleConcurrency();
|
|
2284
2329
|
queueEndTime = startAt + buffer.duration / rate;
|
|
@@ -2291,10 +2336,13 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2291
2336
|
};
|
|
2292
2337
|
const scheduleChunk = async (chunk) => {
|
|
2293
2338
|
const context = await ensureAudioContext();
|
|
2294
|
-
const
|
|
2339
|
+
const alignedChunk = frameAlignedChunk(chunk);
|
|
2340
|
+
if (!alignedChunk)
|
|
2341
|
+
return;
|
|
2342
|
+
const buffer = decodePCM16LEChunk(context, alignedChunk);
|
|
2295
2343
|
if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
|
|
2296
2344
|
stretcher?.reset();
|
|
2297
|
-
scheduleBuffer(context, buffer, playbackRate);
|
|
2345
|
+
scheduleBuffer(context, buffer, playbackRate, chunk.turnId);
|
|
2298
2346
|
return;
|
|
2299
2347
|
}
|
|
2300
2348
|
const channels = Math.max(1, chunk.format.channels);
|
|
@@ -2315,13 +2363,14 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2315
2363
|
continue;
|
|
2316
2364
|
outBuffer.getChannelData(channelIndex).set(channelOut);
|
|
2317
2365
|
}
|
|
2318
|
-
scheduleBuffer(context, outBuffer, 1);
|
|
2366
|
+
scheduleBuffer(context, outBuffer, 1, chunk.turnId);
|
|
2319
2367
|
};
|
|
2320
2368
|
const stopQueuedPlayback = (options2) => {
|
|
2321
2369
|
for (const node of [...sourceNodes]) {
|
|
2322
2370
|
node.stop?.();
|
|
2323
2371
|
}
|
|
2324
2372
|
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
2373
|
+
lastScheduledTurnId = undefined;
|
|
2325
2374
|
if (options2?.forceClear) {
|
|
2326
2375
|
for (const node of sourceNodes) {
|
|
2327
2376
|
node.disconnect?.();
|
|
@@ -2416,13 +2465,19 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2416
2465
|
const chunksReceived = source.assistantAudio.length;
|
|
2417
2466
|
const chunksScheduled = state.processedChunkCount;
|
|
2418
2467
|
return {
|
|
2468
|
+
audioFormatCount: audioFormats.size,
|
|
2469
|
+
buffersScheduled,
|
|
2419
2470
|
chunksReceived,
|
|
2420
2471
|
chunksScheduled,
|
|
2472
|
+
discardedPartialFrameBytes,
|
|
2421
2473
|
errorCount,
|
|
2474
|
+
formatChangeCount,
|
|
2422
2475
|
gapCount,
|
|
2476
|
+
incompleteFrameChunkCount,
|
|
2423
2477
|
maxConcurrentPlayers: observedPeakConcurrency,
|
|
2424
2478
|
maxGapMs: Math.round(maxGapMs),
|
|
2425
|
-
|
|
2479
|
+
pendingPartialFrameBytes: pendingFrameBytes.byteLength,
|
|
2480
|
+
ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived && discardedPartialFrameBytes === 0 && pendingFrameBytes.byteLength === 0,
|
|
2426
2481
|
scheduledDurationMs: Math.round(scheduledDurationMs),
|
|
2427
2482
|
totalGapMs: Math.round(totalGapMs)
|
|
2428
2483
|
};
|
package/dist/client/index.js
CHANGED
|
@@ -886,6 +886,49 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
886
886
|
let maxGapMs = 0;
|
|
887
887
|
let errorCount = 0;
|
|
888
888
|
let hasScheduledAnyChunk = false;
|
|
889
|
+
let buffersScheduled = 0;
|
|
890
|
+
let lastScheduledTurnId;
|
|
891
|
+
const audioFormats = new Set;
|
|
892
|
+
let lastAudioFormat = null;
|
|
893
|
+
let formatChangeCount = 0;
|
|
894
|
+
let incompleteFrameChunkCount = 0;
|
|
895
|
+
let discardedPartialFrameBytes = 0;
|
|
896
|
+
let pendingFrameBytes = new Uint8Array;
|
|
897
|
+
let pendingFrameFormat = null;
|
|
898
|
+
let pendingFrameTurnId;
|
|
899
|
+
const formatKey = (format) => `${format.container}/${format.encoding}/${String(format.channels)}ch/${String(format.sampleRateHz)}hz`;
|
|
900
|
+
const frameAlignedChunk = (chunk) => {
|
|
901
|
+
const format = formatKey(chunk.format);
|
|
902
|
+
audioFormats.add(format);
|
|
903
|
+
if (lastAudioFormat !== null && lastAudioFormat !== format) {
|
|
904
|
+
formatChangeCount += 1;
|
|
905
|
+
}
|
|
906
|
+
lastAudioFormat = format;
|
|
907
|
+
if (pendingFrameBytes.byteLength > 0 && (pendingFrameFormat !== format || pendingFrameTurnId !== chunk.turnId)) {
|
|
908
|
+
discardedPartialFrameBytes += pendingFrameBytes.byteLength;
|
|
909
|
+
pendingFrameBytes = new Uint8Array;
|
|
910
|
+
}
|
|
911
|
+
const joined = pendingFrameBytes.byteLength === 0 ? chunk.chunk : (() => {
|
|
912
|
+
const bytes = new Uint8Array(pendingFrameBytes.byteLength + chunk.chunk.byteLength);
|
|
913
|
+
bytes.set(pendingFrameBytes);
|
|
914
|
+
bytes.set(chunk.chunk, pendingFrameBytes.byteLength);
|
|
915
|
+
return bytes;
|
|
916
|
+
})();
|
|
917
|
+
const frameBytes = Math.max(1, chunk.format.channels) * 2;
|
|
918
|
+
const completeBytes = joined.byteLength - joined.byteLength % frameBytes;
|
|
919
|
+
const remainderBytes = joined.byteLength - completeBytes;
|
|
920
|
+
if (remainderBytes > 0) {
|
|
921
|
+
incompleteFrameChunkCount += 1;
|
|
922
|
+
pendingFrameBytes = joined.slice(completeBytes);
|
|
923
|
+
pendingFrameFormat = format;
|
|
924
|
+
pendingFrameTurnId = chunk.turnId;
|
|
925
|
+
} else {
|
|
926
|
+
pendingFrameBytes = new Uint8Array;
|
|
927
|
+
pendingFrameFormat = null;
|
|
928
|
+
pendingFrameTurnId = undefined;
|
|
929
|
+
}
|
|
930
|
+
return completeBytes > 0 ? { ...chunk, chunk: joined.subarray(0, completeBytes) } : null;
|
|
931
|
+
};
|
|
889
932
|
const sampleConcurrency = () => {
|
|
890
933
|
if (activeAudioPlayers.size > observedPeakConcurrency) {
|
|
891
934
|
observedPeakConcurrency = activeAudioPlayers.size;
|
|
@@ -1005,7 +1048,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1005
1048
|
queueEndTime = audioContext.currentTime;
|
|
1006
1049
|
return audioContext;
|
|
1007
1050
|
};
|
|
1008
|
-
const scheduleBuffer = (context, buffer, rate) => {
|
|
1051
|
+
const scheduleBuffer = (context, buffer, rate, turnId) => {
|
|
1009
1052
|
const node = context.createBufferSource();
|
|
1010
1053
|
node.buffer = buffer;
|
|
1011
1054
|
if (node.playbackRate) {
|
|
@@ -1023,13 +1066,15 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1023
1066
|
};
|
|
1024
1067
|
const earliestStart = context.currentTime + lookaheadSeconds;
|
|
1025
1068
|
const startAt = Math.max(earliestStart, queueEndTime);
|
|
1026
|
-
if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
1069
|
+
if (hasScheduledAnyChunk && turnId !== undefined && turnId === lastScheduledTurnId && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
1027
1070
|
const gapMs = (earliestStart - queueEndTime) * 1000;
|
|
1028
1071
|
gapCount += 1;
|
|
1029
1072
|
totalGapMs += gapMs;
|
|
1030
1073
|
maxGapMs = Math.max(maxGapMs, gapMs);
|
|
1031
1074
|
}
|
|
1032
1075
|
hasScheduledAnyChunk = true;
|
|
1076
|
+
lastScheduledTurnId = turnId;
|
|
1077
|
+
buffersScheduled += 1;
|
|
1033
1078
|
scheduledDurationMs += buffer.duration / rate * 1000;
|
|
1034
1079
|
sampleConcurrency();
|
|
1035
1080
|
queueEndTime = startAt + buffer.duration / rate;
|
|
@@ -1042,10 +1087,13 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1042
1087
|
};
|
|
1043
1088
|
const scheduleChunk = async (chunk) => {
|
|
1044
1089
|
const context = await ensureAudioContext();
|
|
1045
|
-
const
|
|
1090
|
+
const alignedChunk = frameAlignedChunk(chunk);
|
|
1091
|
+
if (!alignedChunk)
|
|
1092
|
+
return;
|
|
1093
|
+
const buffer = decodePCM16LEChunk(context, alignedChunk);
|
|
1046
1094
|
if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
|
|
1047
1095
|
stretcher?.reset();
|
|
1048
|
-
scheduleBuffer(context, buffer, playbackRate);
|
|
1096
|
+
scheduleBuffer(context, buffer, playbackRate, chunk.turnId);
|
|
1049
1097
|
return;
|
|
1050
1098
|
}
|
|
1051
1099
|
const channels = Math.max(1, chunk.format.channels);
|
|
@@ -1066,13 +1114,14 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1066
1114
|
continue;
|
|
1067
1115
|
outBuffer.getChannelData(channelIndex).set(channelOut);
|
|
1068
1116
|
}
|
|
1069
|
-
scheduleBuffer(context, outBuffer, 1);
|
|
1117
|
+
scheduleBuffer(context, outBuffer, 1, chunk.turnId);
|
|
1070
1118
|
};
|
|
1071
1119
|
const stopQueuedPlayback = (options2) => {
|
|
1072
1120
|
for (const node of [...sourceNodes]) {
|
|
1073
1121
|
node.stop?.();
|
|
1074
1122
|
}
|
|
1075
1123
|
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
1124
|
+
lastScheduledTurnId = undefined;
|
|
1076
1125
|
if (options2?.forceClear) {
|
|
1077
1126
|
for (const node of sourceNodes) {
|
|
1078
1127
|
node.disconnect?.();
|
|
@@ -1167,13 +1216,19 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
1167
1216
|
const chunksReceived = source.assistantAudio.length;
|
|
1168
1217
|
const chunksScheduled = state.processedChunkCount;
|
|
1169
1218
|
return {
|
|
1219
|
+
audioFormatCount: audioFormats.size,
|
|
1220
|
+
buffersScheduled,
|
|
1170
1221
|
chunksReceived,
|
|
1171
1222
|
chunksScheduled,
|
|
1223
|
+
discardedPartialFrameBytes,
|
|
1172
1224
|
errorCount,
|
|
1225
|
+
formatChangeCount,
|
|
1173
1226
|
gapCount,
|
|
1227
|
+
incompleteFrameChunkCount,
|
|
1174
1228
|
maxConcurrentPlayers: observedPeakConcurrency,
|
|
1175
1229
|
maxGapMs: Math.round(maxGapMs),
|
|
1176
|
-
|
|
1230
|
+
pendingPartialFrameBytes: pendingFrameBytes.byteLength,
|
|
1231
|
+
ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived && discardedPartialFrameBytes === 0 && pendingFrameBytes.byteLength === 0,
|
|
1177
1232
|
scheduledDurationMs: Math.round(scheduledDurationMs),
|
|
1178
1233
|
totalGapMs: Math.round(totalGapMs)
|
|
1179
1234
|
};
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1624,17 +1624,23 @@ export type VoiceAudioPlayerSource = {
|
|
|
1624
1624
|
assistantAudio: VoiceStreamState["assistantAudio"];
|
|
1625
1625
|
subscribe: (subscriber: () => void) => () => void;
|
|
1626
1626
|
};
|
|
1627
|
-
/** Post-call playback integrity
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1627
|
+
/** Post-call playback-pipeline integrity. This proves what the browser player
|
|
1628
|
+
* received and scheduled; it does not claim that the resulting waveform
|
|
1629
|
+
* sounded clean. A browser-side replay is required for that stronger claim. */
|
|
1630
1630
|
export type VoiceAudioIntegrity = {
|
|
1631
1631
|
ok: boolean;
|
|
1632
1632
|
chunksReceived: number;
|
|
1633
1633
|
chunksScheduled: number;
|
|
1634
|
+
buffersScheduled: number;
|
|
1634
1635
|
scheduledDurationMs: number;
|
|
1635
1636
|
gapCount: number;
|
|
1636
1637
|
totalGapMs: number;
|
|
1637
1638
|
maxGapMs: number;
|
|
1639
|
+
audioFormatCount: number;
|
|
1640
|
+
formatChangeCount: number;
|
|
1641
|
+
incompleteFrameChunkCount: number;
|
|
1642
|
+
discardedPartialFrameBytes: number;
|
|
1643
|
+
pendingPartialFrameBytes: number;
|
|
1638
1644
|
maxConcurrentPlayers: number;
|
|
1639
1645
|
errorCount: number;
|
|
1640
1646
|
};
|
package/dist/testing/index.js
CHANGED
|
@@ -2228,6 +2228,49 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2228
2228
|
let maxGapMs = 0;
|
|
2229
2229
|
let errorCount = 0;
|
|
2230
2230
|
let hasScheduledAnyChunk = false;
|
|
2231
|
+
let buffersScheduled = 0;
|
|
2232
|
+
let lastScheduledTurnId;
|
|
2233
|
+
const audioFormats = new Set;
|
|
2234
|
+
let lastAudioFormat = null;
|
|
2235
|
+
let formatChangeCount = 0;
|
|
2236
|
+
let incompleteFrameChunkCount = 0;
|
|
2237
|
+
let discardedPartialFrameBytes = 0;
|
|
2238
|
+
let pendingFrameBytes = new Uint8Array;
|
|
2239
|
+
let pendingFrameFormat = null;
|
|
2240
|
+
let pendingFrameTurnId;
|
|
2241
|
+
const formatKey = (format) => `${format.container}/${format.encoding}/${String(format.channels)}ch/${String(format.sampleRateHz)}hz`;
|
|
2242
|
+
const frameAlignedChunk = (chunk) => {
|
|
2243
|
+
const format = formatKey(chunk.format);
|
|
2244
|
+
audioFormats.add(format);
|
|
2245
|
+
if (lastAudioFormat !== null && lastAudioFormat !== format) {
|
|
2246
|
+
formatChangeCount += 1;
|
|
2247
|
+
}
|
|
2248
|
+
lastAudioFormat = format;
|
|
2249
|
+
if (pendingFrameBytes.byteLength > 0 && (pendingFrameFormat !== format || pendingFrameTurnId !== chunk.turnId)) {
|
|
2250
|
+
discardedPartialFrameBytes += pendingFrameBytes.byteLength;
|
|
2251
|
+
pendingFrameBytes = new Uint8Array;
|
|
2252
|
+
}
|
|
2253
|
+
const joined = pendingFrameBytes.byteLength === 0 ? chunk.chunk : (() => {
|
|
2254
|
+
const bytes2 = new Uint8Array(pendingFrameBytes.byteLength + chunk.chunk.byteLength);
|
|
2255
|
+
bytes2.set(pendingFrameBytes);
|
|
2256
|
+
bytes2.set(chunk.chunk, pendingFrameBytes.byteLength);
|
|
2257
|
+
return bytes2;
|
|
2258
|
+
})();
|
|
2259
|
+
const frameBytes = Math.max(1, chunk.format.channels) * 2;
|
|
2260
|
+
const completeBytes = joined.byteLength - joined.byteLength % frameBytes;
|
|
2261
|
+
const remainderBytes = joined.byteLength - completeBytes;
|
|
2262
|
+
if (remainderBytes > 0) {
|
|
2263
|
+
incompleteFrameChunkCount += 1;
|
|
2264
|
+
pendingFrameBytes = joined.slice(completeBytes);
|
|
2265
|
+
pendingFrameFormat = format;
|
|
2266
|
+
pendingFrameTurnId = chunk.turnId;
|
|
2267
|
+
} else {
|
|
2268
|
+
pendingFrameBytes = new Uint8Array;
|
|
2269
|
+
pendingFrameFormat = null;
|
|
2270
|
+
pendingFrameTurnId = undefined;
|
|
2271
|
+
}
|
|
2272
|
+
return completeBytes > 0 ? { ...chunk, chunk: joined.subarray(0, completeBytes) } : null;
|
|
2273
|
+
};
|
|
2231
2274
|
const sampleConcurrency = () => {
|
|
2232
2275
|
if (activeAudioPlayers.size > observedPeakConcurrency) {
|
|
2233
2276
|
observedPeakConcurrency = activeAudioPlayers.size;
|
|
@@ -2347,7 +2390,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2347
2390
|
queueEndTime = audioContext.currentTime;
|
|
2348
2391
|
return audioContext;
|
|
2349
2392
|
};
|
|
2350
|
-
const scheduleBuffer = (context, buffer, rate) => {
|
|
2393
|
+
const scheduleBuffer = (context, buffer, rate, turnId) => {
|
|
2351
2394
|
const node = context.createBufferSource();
|
|
2352
2395
|
node.buffer = buffer;
|
|
2353
2396
|
if (node.playbackRate) {
|
|
@@ -2365,13 +2408,15 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2365
2408
|
};
|
|
2366
2409
|
const earliestStart = context.currentTime + lookaheadSeconds;
|
|
2367
2410
|
const startAt = Math.max(earliestStart, queueEndTime);
|
|
2368
|
-
if (hasScheduledAnyChunk && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
2411
|
+
if (hasScheduledAnyChunk && turnId !== undefined && turnId === lastScheduledTurnId && earliestStart > queueEndTime + GAP_EPSILON_S) {
|
|
2369
2412
|
const gapMs = (earliestStart - queueEndTime) * 1000;
|
|
2370
2413
|
gapCount += 1;
|
|
2371
2414
|
totalGapMs += gapMs;
|
|
2372
2415
|
maxGapMs = Math.max(maxGapMs, gapMs);
|
|
2373
2416
|
}
|
|
2374
2417
|
hasScheduledAnyChunk = true;
|
|
2418
|
+
lastScheduledTurnId = turnId;
|
|
2419
|
+
buffersScheduled += 1;
|
|
2375
2420
|
scheduledDurationMs += buffer.duration / rate * 1000;
|
|
2376
2421
|
sampleConcurrency();
|
|
2377
2422
|
queueEndTime = startAt + buffer.duration / rate;
|
|
@@ -2384,10 +2429,13 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2384
2429
|
};
|
|
2385
2430
|
const scheduleChunk = async (chunk) => {
|
|
2386
2431
|
const context = await ensureAudioContext();
|
|
2387
|
-
const
|
|
2432
|
+
const alignedChunk = frameAlignedChunk(chunk);
|
|
2433
|
+
if (!alignedChunk)
|
|
2434
|
+
return;
|
|
2435
|
+
const buffer = decodePCM16LEChunk(context, alignedChunk);
|
|
2388
2436
|
if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
|
|
2389
2437
|
stretcher?.reset();
|
|
2390
|
-
scheduleBuffer(context, buffer, playbackRate);
|
|
2438
|
+
scheduleBuffer(context, buffer, playbackRate, chunk.turnId);
|
|
2391
2439
|
return;
|
|
2392
2440
|
}
|
|
2393
2441
|
const channels = Math.max(1, chunk.format.channels);
|
|
@@ -2408,13 +2456,14 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2408
2456
|
continue;
|
|
2409
2457
|
outBuffer.getChannelData(channelIndex).set(channelOut);
|
|
2410
2458
|
}
|
|
2411
|
-
scheduleBuffer(context, outBuffer, 1);
|
|
2459
|
+
scheduleBuffer(context, outBuffer, 1, chunk.turnId);
|
|
2412
2460
|
};
|
|
2413
2461
|
const stopQueuedPlayback = (options2) => {
|
|
2414
2462
|
for (const node of [...sourceNodes]) {
|
|
2415
2463
|
node.stop?.();
|
|
2416
2464
|
}
|
|
2417
2465
|
queueEndTime = audioContext ? audioContext.currentTime : 0;
|
|
2466
|
+
lastScheduledTurnId = undefined;
|
|
2418
2467
|
if (options2?.forceClear) {
|
|
2419
2468
|
for (const node of sourceNodes) {
|
|
2420
2469
|
node.disconnect?.();
|
|
@@ -2509,13 +2558,19 @@ var createVoiceAudioPlayer = (source, options = {}) => {
|
|
|
2509
2558
|
const chunksReceived = source.assistantAudio.length;
|
|
2510
2559
|
const chunksScheduled = state.processedChunkCount;
|
|
2511
2560
|
return {
|
|
2561
|
+
audioFormatCount: audioFormats.size,
|
|
2562
|
+
buffersScheduled,
|
|
2512
2563
|
chunksReceived,
|
|
2513
2564
|
chunksScheduled,
|
|
2565
|
+
discardedPartialFrameBytes,
|
|
2514
2566
|
errorCount,
|
|
2567
|
+
formatChangeCount,
|
|
2515
2568
|
gapCount,
|
|
2569
|
+
incompleteFrameChunkCount,
|
|
2516
2570
|
maxConcurrentPlayers: observedPeakConcurrency,
|
|
2517
2571
|
maxGapMs: Math.round(maxGapMs),
|
|
2518
|
-
|
|
2572
|
+
pendingPartialFrameBytes: pendingFrameBytes.byteLength,
|
|
2573
|
+
ok: observedPeakConcurrency <= 1 && errorCount === 0 && chunksScheduled >= chunksReceived && discardedPartialFrameBytes === 0 && pendingFrameBytes.byteLength === 0,
|
|
2519
2574
|
scheduledDurationMs: Math.round(scheduledDurationMs),
|
|
2520
2575
|
totalGapMs: Math.round(totalGapMs)
|
|
2521
2576
|
};
|