@unctad-ai/voice-agent-core 0.1.1
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/config/defaults.d.ts +128 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +169 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/contexts/SiteConfigContext.d.ts +7 -0
- package/dist/contexts/SiteConfigContext.d.ts.map +1 -0
- package/dist/contexts/SiteConfigContext.js +14 -0
- package/dist/contexts/SiteConfigContext.js.map +1 -0
- package/dist/hooks/useAudioPlayback.d.ts +18 -0
- package/dist/hooks/useAudioPlayback.d.ts.map +1 -0
- package/dist/hooks/useAudioPlayback.js +482 -0
- package/dist/hooks/useAudioPlayback.js.map +1 -0
- package/dist/hooks/useTenVAD.d.ts +42 -0
- package/dist/hooks/useTenVAD.d.ts.map +1 -0
- package/dist/hooks/useTenVAD.js +318 -0
- package/dist/hooks/useTenVAD.js.map +1 -0
- package/dist/hooks/useVoiceAgent.d.ts +50 -0
- package/dist/hooks/useVoiceAgent.d.ts.map +1 -0
- package/dist/hooks/useVoiceAgent.js +1005 -0
- package/dist/hooks/useVoiceAgent.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/services/voiceApi.d.ts +22 -0
- package/dist/services/voiceApi.d.ts.map +1 -0
- package/dist/services/voiceApi.js +93 -0
- package/dist/services/voiceApi.js.map +1 -0
- package/dist/types/config.d.ts +53 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +2 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/errors.d.ts +2 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +2 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/settings.d.ts +26 -0
- package/dist/types/settings.d.ts.map +1 -0
- package/dist/types/settings.js +2 -0
- package/dist/types/settings.js.map +1 -0
- package/dist/types/voice.d.ts +20 -0
- package/dist/types/voice.d.ts.map +1 -0
- package/dist/types/voice.js +11 -0
- package/dist/types/voice.js.map +1 -0
- package/dist/utils/audioUtils.d.ts +2 -0
- package/dist/utils/audioUtils.d.ts.map +1 -0
- package/dist/utils/audioUtils.js +30 -0
- package/dist/utils/audioUtils.js.map +1 -0
- package/dist/utils/wavParser.d.ts +27 -0
- package/dist/utils/wavParser.d.ts.map +1 -0
- package/dist/utils/wavParser.js +75 -0
- package/dist/utils/wavParser.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { parseWavHeader, pcmToFloat32 } from '../utils/wavParser';
|
|
3
|
+
import { WAV_HEADER_SIZE, TTS_STREAM_CHUNK_MS } from '../config/defaults';
|
|
4
|
+
export function useAudioPlayback({ onPlaybackEnd, volumeRef, speedRef, } = {}) {
|
|
5
|
+
const audioCtxRef = useRef(null);
|
|
6
|
+
const sourceRef = useRef(null);
|
|
7
|
+
const streamingSourcesRef = useRef([]);
|
|
8
|
+
const analyserRef = useRef(null);
|
|
9
|
+
const gainRef = useRef(null);
|
|
10
|
+
const [analyserNode, setAnalyserNode] = useState(null);
|
|
11
|
+
const onPlaybackEndRef = useRef(onPlaybackEnd);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
onPlaybackEndRef.current = onPlaybackEnd;
|
|
14
|
+
}, [onPlaybackEnd]);
|
|
15
|
+
const playPromiseResolveRef = useRef(null);
|
|
16
|
+
/** When true, suppress onPlaybackEnd in onended — explicit stop, not natural end */
|
|
17
|
+
const stoppingRef = useRef(false);
|
|
18
|
+
/** Next scheduled time for streaming chunk scheduling */
|
|
19
|
+
const streamNextTimeRef = useRef(0);
|
|
20
|
+
const getContext = useCallback(() => {
|
|
21
|
+
if (!audioCtxRef.current) {
|
|
22
|
+
try {
|
|
23
|
+
audioCtxRef.current = new AudioContext();
|
|
24
|
+
gainRef.current = audioCtxRef.current.createGain();
|
|
25
|
+
gainRef.current.gain.value = volumeRef?.current ?? 1;
|
|
26
|
+
analyserRef.current = audioCtxRef.current.createAnalyser();
|
|
27
|
+
analyserRef.current.fftSize = 256;
|
|
28
|
+
// Chain: source → gain → analyser → destination
|
|
29
|
+
gainRef.current.connect(analyserRef.current);
|
|
30
|
+
analyserRef.current.connect(audioCtxRef.current.destination);
|
|
31
|
+
setAnalyserNode(analyserRef.current);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
console.error('[useAudioPlayback] Failed to create AudioContext:', err);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return audioCtxRef.current;
|
|
39
|
+
}, []);
|
|
40
|
+
/** Call on first user gesture (e.g. FAB click) to eagerly create AudioContext.
|
|
41
|
+
* Mobile Safari requires AudioContext creation inside a user-triggered event. */
|
|
42
|
+
const initContext = useCallback(() => {
|
|
43
|
+
const ctx = getContext();
|
|
44
|
+
if (!ctx)
|
|
45
|
+
console.warn('[useAudioPlayback] initContext: AudioContext unavailable');
|
|
46
|
+
}, [getContext]);
|
|
47
|
+
/** Smoothly ramp gain to new volume (15ms ramp, no clicks) */
|
|
48
|
+
const applyVolume = useCallback((v) => {
|
|
49
|
+
const ctx = audioCtxRef.current;
|
|
50
|
+
const gain = gainRef.current;
|
|
51
|
+
if (ctx && gain) {
|
|
52
|
+
gain.gain.setTargetAtTime(v, ctx.currentTime, 0.015);
|
|
53
|
+
}
|
|
54
|
+
}, []);
|
|
55
|
+
const playAudio = useCallback(async (audioBuffer) => {
|
|
56
|
+
const ctx = getContext();
|
|
57
|
+
if (!ctx)
|
|
58
|
+
throw new Error('AudioContext unavailable — cannot play audio');
|
|
59
|
+
if (ctx.state === 'suspended')
|
|
60
|
+
await ctx.resume();
|
|
61
|
+
// Stop any current playback and resolve pending promise
|
|
62
|
+
try {
|
|
63
|
+
sourceRef.current?.stop();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// ignore
|
|
67
|
+
}
|
|
68
|
+
playPromiseResolveRef.current?.();
|
|
69
|
+
playPromiseResolveRef.current = null;
|
|
70
|
+
const buffer = await ctx.decodeAudioData(audioBuffer.slice(0));
|
|
71
|
+
const source = ctx.createBufferSource();
|
|
72
|
+
source.buffer = buffer;
|
|
73
|
+
if (speedRef)
|
|
74
|
+
source.playbackRate.value = speedRef.current;
|
|
75
|
+
source.connect(gainRef.current);
|
|
76
|
+
sourceRef.current = source;
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
playPromiseResolveRef.current = resolve;
|
|
79
|
+
source.onended = () => {
|
|
80
|
+
sourceRef.current = null;
|
|
81
|
+
playPromiseResolveRef.current = null;
|
|
82
|
+
if (!stoppingRef.current) {
|
|
83
|
+
onPlaybackEndRef.current?.();
|
|
84
|
+
}
|
|
85
|
+
resolve();
|
|
86
|
+
};
|
|
87
|
+
source.start();
|
|
88
|
+
});
|
|
89
|
+
}, [getContext, speedRef]);
|
|
90
|
+
const stopAudio = useCallback(() => {
|
|
91
|
+
// Suppress onPlaybackEnd during explicit stop — the caller manages state.
|
|
92
|
+
stoppingRef.current = true;
|
|
93
|
+
// Resume context if suspended — prevents stuck state after barge-in.
|
|
94
|
+
// source.stop() works on a suspended context (it marks the source for
|
|
95
|
+
// stopping; the audio thread processes it when resumed), but resuming
|
|
96
|
+
// ensures onended fires promptly for cleanup.
|
|
97
|
+
const ctx = audioCtxRef.current;
|
|
98
|
+
if (ctx?.state === 'suspended') {
|
|
99
|
+
ctx.resume(); // fire-and-forget
|
|
100
|
+
}
|
|
101
|
+
// Stop buffered playback source
|
|
102
|
+
try {
|
|
103
|
+
sourceRef.current?.stop();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// If stop() throws, onended won't fire — resolve the hanging promise
|
|
107
|
+
}
|
|
108
|
+
sourceRef.current = null;
|
|
109
|
+
// Stop all streaming playback sources
|
|
110
|
+
for (const src of streamingSourcesRef.current) {
|
|
111
|
+
try {
|
|
112
|
+
src.stop();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// ignore — may already have ended
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
streamingSourcesRef.current = [];
|
|
119
|
+
playPromiseResolveRef.current?.();
|
|
120
|
+
playPromiseResolveRef.current = null;
|
|
121
|
+
stoppingRef.current = false;
|
|
122
|
+
}, []);
|
|
123
|
+
/**
|
|
124
|
+
* Suspend audio playback for two-phase barge-in.
|
|
125
|
+
* Uses AudioContext.suspend() to freeze the entire audio graph in place —
|
|
126
|
+
* all scheduled sources pause mid-playback. Works identically for streaming
|
|
127
|
+
* (CosyVoice, Chatterbox, Resemble) and buffered (Pocket TTS) providers.
|
|
128
|
+
* New chunks from the TTS stream continue to be scheduled on the frozen
|
|
129
|
+
* context and will play seamlessly when resumePlayback() unfreezes it.
|
|
130
|
+
*/
|
|
131
|
+
const suspendPlayback = useCallback(() => {
|
|
132
|
+
const ctx = audioCtxRef.current;
|
|
133
|
+
if (ctx && ctx.state === 'running') {
|
|
134
|
+
ctx.suspend(); // fire-and-forget — resolves in ~microseconds on audio thread
|
|
135
|
+
}
|
|
136
|
+
}, []);
|
|
137
|
+
/**
|
|
138
|
+
* Resume audio playback after a false barge-in.
|
|
139
|
+
* Unfreezes the AudioContext — all paused and newly scheduled sources
|
|
140
|
+
* continue playing from exactly where they stopped. Zero content loss.
|
|
141
|
+
*/
|
|
142
|
+
const resumePlayback = useCallback(() => {
|
|
143
|
+
const ctx = audioCtxRef.current;
|
|
144
|
+
if (ctx && ctx.state === 'suspended') {
|
|
145
|
+
ctx.resume(); // fire-and-forget
|
|
146
|
+
}
|
|
147
|
+
}, []);
|
|
148
|
+
/**
|
|
149
|
+
* Stream audio chunks for gapless playback as they arrive.
|
|
150
|
+
* Parses the WAV header from the first bytes, then converts PCM chunks
|
|
151
|
+
* into AudioBuffers scheduled for back-to-back playback.
|
|
152
|
+
*
|
|
153
|
+
* During AudioContext.suspend() (barge-in), chunks continue to be parsed
|
|
154
|
+
* and scheduled normally — they simply won't produce audio until the
|
|
155
|
+
* context is resumed. This eliminates all buffering complexity.
|
|
156
|
+
*/
|
|
157
|
+
const playStreamingAudio = useCallback(async (chunks, signal) => {
|
|
158
|
+
const ctx = getContext();
|
|
159
|
+
if (!ctx)
|
|
160
|
+
throw new Error('AudioContext unavailable — cannot play streaming audio');
|
|
161
|
+
if (ctx.state === 'suspended')
|
|
162
|
+
await ctx.resume();
|
|
163
|
+
// Stop any current playback
|
|
164
|
+
try {
|
|
165
|
+
sourceRef.current?.stop();
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// ignore
|
|
169
|
+
}
|
|
170
|
+
for (const src of streamingSourcesRef.current) {
|
|
171
|
+
try {
|
|
172
|
+
src.stop();
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
/* ignore */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
streamingSourcesRef.current = [];
|
|
179
|
+
playPromiseResolveRef.current?.();
|
|
180
|
+
playPromiseResolveRef.current = null;
|
|
181
|
+
if (signal?.aborted) {
|
|
182
|
+
throw signal.reason ?? new DOMException('Aborted', 'AbortError');
|
|
183
|
+
}
|
|
184
|
+
// Deferred resolve/reject — the promise settles when the last AudioBufferSourceNode ends
|
|
185
|
+
let resolvePlayback;
|
|
186
|
+
let rejectPlayback;
|
|
187
|
+
const playbackPromise = new Promise((resolve, reject) => {
|
|
188
|
+
resolvePlayback = resolve;
|
|
189
|
+
rejectPlayback = reject;
|
|
190
|
+
});
|
|
191
|
+
playPromiseResolveRef.current = resolvePlayback;
|
|
192
|
+
const onAbort = () => {
|
|
193
|
+
for (const src of streamingSourcesRef.current) {
|
|
194
|
+
try {
|
|
195
|
+
src.stop();
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* ignore */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
streamingSourcesRef.current = [];
|
|
202
|
+
playPromiseResolveRef.current = null;
|
|
203
|
+
rejectPlayback(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
204
|
+
};
|
|
205
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
206
|
+
try {
|
|
207
|
+
let headerBuf = new Uint8Array(0);
|
|
208
|
+
let header = null;
|
|
209
|
+
let pcmCarry = new Uint8Array(0); // leftover PCM bytes between chunks
|
|
210
|
+
let firstChunk = true;
|
|
211
|
+
let bytesPerChunk = 0; // set after header is parsed
|
|
212
|
+
// Sample-exact scheduling: track total samples as integer to avoid
|
|
213
|
+
// floating-point drift from repeated audioBuffer.duration additions.
|
|
214
|
+
// 0.15s (150ms at 24kHz = 3600 samples) is NOT exactly representable
|
|
215
|
+
// in float64; adding it ~47 times for a 7s clip accumulates error
|
|
216
|
+
// that creates micro-gaps/overlaps → audible clicks.
|
|
217
|
+
let scheduleStartTime = 0;
|
|
218
|
+
let totalSamplesScheduled = 0;
|
|
219
|
+
let sampleRate = 0;
|
|
220
|
+
streamNextTimeRef.current = 0;
|
|
221
|
+
for await (const chunk of chunks) {
|
|
222
|
+
if (signal?.aborted)
|
|
223
|
+
break;
|
|
224
|
+
if (!header) {
|
|
225
|
+
// Accumulate bytes until we have the full WAV header
|
|
226
|
+
const merged = new Uint8Array(headerBuf.length + chunk.length);
|
|
227
|
+
merged.set(headerBuf);
|
|
228
|
+
merged.set(chunk, headerBuf.length);
|
|
229
|
+
headerBuf = merged;
|
|
230
|
+
if (headerBuf.length < WAV_HEADER_SIZE)
|
|
231
|
+
continue;
|
|
232
|
+
header = parseWavHeader(headerBuf.slice(0, WAV_HEADER_SIZE));
|
|
233
|
+
sampleRate = header.sampleRate;
|
|
234
|
+
// Compute chunk size from target duration — adapts to any sample rate
|
|
235
|
+
const samplesPerChunk = Math.floor((sampleRate * TTS_STREAM_CHUNK_MS) / 1000);
|
|
236
|
+
bytesPerChunk = samplesPerChunk * header.bytesPerSample;
|
|
237
|
+
// Remaining bytes after header are PCM data
|
|
238
|
+
const remaining = headerBuf.slice(WAV_HEADER_SIZE);
|
|
239
|
+
if (remaining.length > 0) {
|
|
240
|
+
pcmCarry = remaining;
|
|
241
|
+
}
|
|
242
|
+
headerBuf = new Uint8Array(0); // free
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// Append chunk to PCM carry buffer
|
|
246
|
+
const merged = new Uint8Array(pcmCarry.length + chunk.length);
|
|
247
|
+
merged.set(pcmCarry);
|
|
248
|
+
merged.set(chunk, pcmCarry.length);
|
|
249
|
+
pcmCarry = merged;
|
|
250
|
+
}
|
|
251
|
+
// Schedule AudioBuffers from accumulated PCM.
|
|
252
|
+
// During AudioContext.suspend(), these sources are created and
|
|
253
|
+
// scheduled normally — they just won't produce audio until resumed.
|
|
254
|
+
while (pcmCarry.length >= bytesPerChunk) {
|
|
255
|
+
const pcmSlice = pcmCarry.slice(0, bytesPerChunk);
|
|
256
|
+
pcmCarry = pcmCarry.slice(bytesPerChunk);
|
|
257
|
+
const float32 = pcmToFloat32(pcmSlice, header);
|
|
258
|
+
const audioBuffer = ctx.createBuffer(1, float32.length, sampleRate);
|
|
259
|
+
audioBuffer.copyToChannel(float32, 0);
|
|
260
|
+
const source = ctx.createBufferSource();
|
|
261
|
+
source.buffer = audioBuffer;
|
|
262
|
+
if (speedRef)
|
|
263
|
+
source.playbackRate.value = speedRef.current;
|
|
264
|
+
source.connect(gainRef.current);
|
|
265
|
+
streamingSourcesRef.current.push(source);
|
|
266
|
+
// Clean up each source when it finishes — keeps the array bounded
|
|
267
|
+
// to only currently-playing/scheduled sources during suspension.
|
|
268
|
+
source.onended = () => {
|
|
269
|
+
const idx = streamingSourcesRef.current.indexOf(source);
|
|
270
|
+
if (idx !== -1)
|
|
271
|
+
streamingSourcesRef.current.splice(idx, 1);
|
|
272
|
+
};
|
|
273
|
+
if (firstChunk) {
|
|
274
|
+
// 25ms lookahead: by the time source.start() runs, ctx.currentTime
|
|
275
|
+
// has advanced past the read value. Starting in the past causes the
|
|
276
|
+
// buffer to play immediately, overlapping the next scheduled buffer → click.
|
|
277
|
+
scheduleStartTime = ctx.currentTime + 0.025;
|
|
278
|
+
streamNextTimeRef.current = scheduleStartTime;
|
|
279
|
+
firstChunk = false;
|
|
280
|
+
}
|
|
281
|
+
source.start(streamNextTimeRef.current);
|
|
282
|
+
totalSamplesScheduled += float32.length;
|
|
283
|
+
const effectiveSpeed = speedRef?.current ?? 1;
|
|
284
|
+
streamNextTimeRef.current =
|
|
285
|
+
scheduleStartTime + totalSamplesScheduled / (sampleRate * effectiveSpeed);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// --- Generator exhausted ---
|
|
289
|
+
// Flush remaining PCM (last partial chunk)
|
|
290
|
+
if (header && pcmCarry.length >= header.bytesPerSample) {
|
|
291
|
+
const alignedLen = Math.floor(pcmCarry.length / header.bytesPerSample) * header.bytesPerSample;
|
|
292
|
+
const pcmSlice = pcmCarry.slice(0, alignedLen);
|
|
293
|
+
const float32 = pcmToFloat32(pcmSlice, header);
|
|
294
|
+
const audioBuffer = ctx.createBuffer(1, float32.length, sampleRate);
|
|
295
|
+
audioBuffer.copyToChannel(float32, 0);
|
|
296
|
+
const source = ctx.createBufferSource();
|
|
297
|
+
source.buffer = audioBuffer;
|
|
298
|
+
if (speedRef)
|
|
299
|
+
source.playbackRate.value = speedRef.current;
|
|
300
|
+
source.connect(gainRef.current);
|
|
301
|
+
streamingSourcesRef.current.push(source);
|
|
302
|
+
source.onended = () => {
|
|
303
|
+
const idx = streamingSourcesRef.current.indexOf(source);
|
|
304
|
+
if (idx !== -1)
|
|
305
|
+
streamingSourcesRef.current.splice(idx, 1);
|
|
306
|
+
};
|
|
307
|
+
if (firstChunk) {
|
|
308
|
+
scheduleStartTime = ctx.currentTime + 0.025;
|
|
309
|
+
streamNextTimeRef.current = scheduleStartTime;
|
|
310
|
+
}
|
|
311
|
+
source.start(streamNextTimeRef.current);
|
|
312
|
+
totalSamplesScheduled += float32.length;
|
|
313
|
+
const effectiveSpeed = speedRef?.current ?? 1;
|
|
314
|
+
streamNextTimeRef.current =
|
|
315
|
+
scheduleStartTime + totalSamplesScheduled / (sampleRate * effectiveSpeed);
|
|
316
|
+
}
|
|
317
|
+
// Wire playback-end detection on the last scheduled source.
|
|
318
|
+
// Each source's onended already handles cleanup (splice from array).
|
|
319
|
+
// The last source additionally fires onPlaybackEndRef and resolves.
|
|
320
|
+
const sources = streamingSourcesRef.current;
|
|
321
|
+
const lastSource = sources.length > 0 ? sources[sources.length - 1] : null;
|
|
322
|
+
if (lastSource) {
|
|
323
|
+
const existingOnEnded = lastSource.onended;
|
|
324
|
+
lastSource.onended = (ev) => {
|
|
325
|
+
// Run the cleanup handler first (splice from array)
|
|
326
|
+
existingOnEnded?.call(lastSource, ev);
|
|
327
|
+
playPromiseResolveRef.current = null;
|
|
328
|
+
signal?.removeEventListener('abort', onAbort);
|
|
329
|
+
if (!stoppingRef.current) {
|
|
330
|
+
onPlaybackEndRef.current?.();
|
|
331
|
+
}
|
|
332
|
+
resolvePlayback();
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// No audio was produced
|
|
337
|
+
playPromiseResolveRef.current = null;
|
|
338
|
+
signal?.removeEventListener('abort', onAbort);
|
|
339
|
+
if (!stoppingRef.current) {
|
|
340
|
+
onPlaybackEndRef.current?.();
|
|
341
|
+
}
|
|
342
|
+
resolvePlayback();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
signal?.removeEventListener('abort', onAbort);
|
|
347
|
+
for (const src of streamingSourcesRef.current) {
|
|
348
|
+
try {
|
|
349
|
+
src.stop();
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
/* ignore */
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
streamingSourcesRef.current = [];
|
|
356
|
+
playPromiseResolveRef.current = null;
|
|
357
|
+
rejectPlayback(err);
|
|
358
|
+
}
|
|
359
|
+
return playbackPromise;
|
|
360
|
+
}, [getContext]);
|
|
361
|
+
/**
|
|
362
|
+
* Play a sequence of audio buffers back-to-back (sentence-level pipelined TTS).
|
|
363
|
+
* All promises are started in parallel but played in order.
|
|
364
|
+
* Only fires onPlaybackEnd after the last buffer finishes.
|
|
365
|
+
*/
|
|
366
|
+
const playAudioSequence = useCallback(async (audioPromises, signal) => {
|
|
367
|
+
if (audioPromises.length === 0)
|
|
368
|
+
return;
|
|
369
|
+
const ctx = getContext();
|
|
370
|
+
if (!ctx)
|
|
371
|
+
throw new Error('AudioContext unavailable — cannot play audio sequence');
|
|
372
|
+
if (ctx.state === 'suspended')
|
|
373
|
+
await ctx.resume();
|
|
374
|
+
// Stop any current playback
|
|
375
|
+
try {
|
|
376
|
+
sourceRef.current?.stop();
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// ignore
|
|
380
|
+
}
|
|
381
|
+
for (const src of streamingSourcesRef.current) {
|
|
382
|
+
try {
|
|
383
|
+
src.stop();
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
/* ignore */
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
streamingSourcesRef.current = [];
|
|
390
|
+
playPromiseResolveRef.current?.();
|
|
391
|
+
playPromiseResolveRef.current = null;
|
|
392
|
+
for (let i = 0; i < audioPromises.length; i++) {
|
|
393
|
+
if (signal?.aborted)
|
|
394
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
395
|
+
const arrayBuf = await audioPromises[i];
|
|
396
|
+
if (signal?.aborted)
|
|
397
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
398
|
+
const buffer = await ctx.decodeAudioData(arrayBuf.slice(0));
|
|
399
|
+
if (signal?.aborted)
|
|
400
|
+
throw new DOMException('Aborted', 'AbortError');
|
|
401
|
+
const source = ctx.createBufferSource();
|
|
402
|
+
source.buffer = buffer;
|
|
403
|
+
if (speedRef)
|
|
404
|
+
source.playbackRate.value = speedRef.current;
|
|
405
|
+
source.connect(gainRef.current);
|
|
406
|
+
sourceRef.current = source;
|
|
407
|
+
const isLast = i === audioPromises.length - 1;
|
|
408
|
+
await new Promise((resolve, reject) => {
|
|
409
|
+
playPromiseResolveRef.current = resolve;
|
|
410
|
+
const onAbort = () => {
|
|
411
|
+
try {
|
|
412
|
+
source.stop();
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
/* ignore */
|
|
416
|
+
}
|
|
417
|
+
sourceRef.current = null;
|
|
418
|
+
playPromiseResolveRef.current = null;
|
|
419
|
+
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
420
|
+
};
|
|
421
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
422
|
+
source.onended = () => {
|
|
423
|
+
signal?.removeEventListener('abort', onAbort);
|
|
424
|
+
sourceRef.current = null;
|
|
425
|
+
playPromiseResolveRef.current = null;
|
|
426
|
+
if (isLast && !stoppingRef.current) {
|
|
427
|
+
onPlaybackEndRef.current?.();
|
|
428
|
+
}
|
|
429
|
+
resolve();
|
|
430
|
+
};
|
|
431
|
+
source.start();
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}, [getContext]);
|
|
435
|
+
const getAmplitude = useCallback(() => {
|
|
436
|
+
const analyser = analyserRef.current;
|
|
437
|
+
if (!analyser)
|
|
438
|
+
return 0;
|
|
439
|
+
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
440
|
+
analyser.getByteFrequencyData(data);
|
|
441
|
+
let sum = 0;
|
|
442
|
+
for (let i = 0; i < data.length; i++) {
|
|
443
|
+
sum += data[i];
|
|
444
|
+
}
|
|
445
|
+
return sum / (data.length * 255);
|
|
446
|
+
}, []);
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
return () => {
|
|
449
|
+
try {
|
|
450
|
+
sourceRef.current?.stop();
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// ignore
|
|
454
|
+
}
|
|
455
|
+
for (const src of streamingSourcesRef.current) {
|
|
456
|
+
try {
|
|
457
|
+
src.stop();
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
/* ignore */
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
streamingSourcesRef.current = [];
|
|
464
|
+
playPromiseResolveRef.current?.();
|
|
465
|
+
playPromiseResolveRef.current = null;
|
|
466
|
+
audioCtxRef.current?.close();
|
|
467
|
+
};
|
|
468
|
+
}, []);
|
|
469
|
+
return {
|
|
470
|
+
playAudio,
|
|
471
|
+
playAudioSequence,
|
|
472
|
+
playStreamingAudio,
|
|
473
|
+
stopAudio,
|
|
474
|
+
suspendPlayback,
|
|
475
|
+
resumePlayback,
|
|
476
|
+
getAmplitude,
|
|
477
|
+
initContext,
|
|
478
|
+
applyVolume,
|
|
479
|
+
analyser: analyserNode,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
//# sourceMappingURL=useAudioPlayback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAudioPlayback.js","sourceRoot":"","sources":["../../src/hooks/useAudioPlayback.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTenVAD — React hook wrapping TEN VAD (WebAssembly) for browser voice
|
|
3
|
+
* activity detection. Drop-in replacement for `useMicVAD` from
|
|
4
|
+
* `@ricky0123/vad-react` with the same callback/state contract.
|
|
5
|
+
*
|
|
6
|
+
* Architecture:
|
|
7
|
+
* AudioContext (16 kHz) → AudioWorkletNode (ten-vad-processor.js)
|
|
8
|
+
* → postMessage Float32 chunks → main-thread WASM inference
|
|
9
|
+
* → speech segmentation state machine → callbacks
|
|
10
|
+
*/
|
|
11
|
+
export interface UseTenVADOptions {
|
|
12
|
+
/** Start capturing immediately on mount (default false) */
|
|
13
|
+
startOnLoad?: boolean;
|
|
14
|
+
/** Hop size in samples for each VAD frame at 16 kHz (default 256 = 16 ms) */
|
|
15
|
+
hopSize?: number;
|
|
16
|
+
/** VAD probability threshold [0-1] (default 0.5) */
|
|
17
|
+
threshold?: number;
|
|
18
|
+
/** Probability above which speech is considered to have started */
|
|
19
|
+
positiveSpeechThreshold?: number;
|
|
20
|
+
/** Probability below which speech is considered to have ended */
|
|
21
|
+
negativeSpeechThreshold?: number;
|
|
22
|
+
/** How long (ms) speech must stay below negative threshold before segment ends */
|
|
23
|
+
redemptionMs?: number;
|
|
24
|
+
/** Minimum speech duration (ms) to fire onSpeechEnd; shorter = onVADMisfire */
|
|
25
|
+
minSpeechMs?: number;
|
|
26
|
+
/** Audio to keep before speech onset (ms) */
|
|
27
|
+
preSpeechPadMs?: number;
|
|
28
|
+
onSpeechStart?: () => void;
|
|
29
|
+
onSpeechEnd?: (audio: Float32Array) => void;
|
|
30
|
+
onVADMisfire?: () => void;
|
|
31
|
+
onFrameProcessed?: (probabilities: {
|
|
32
|
+
isSpeech: number;
|
|
33
|
+
rms: number;
|
|
34
|
+
}) => void;
|
|
35
|
+
}
|
|
36
|
+
export declare function useTenVAD(options?: UseTenVADOptions): {
|
|
37
|
+
loading: boolean;
|
|
38
|
+
errored: false | object;
|
|
39
|
+
start: () => Promise<void>;
|
|
40
|
+
pause: () => void;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=useTenVAD.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTenVAD.d.ts","sourceRoot":"","sources":["../../src/hooks/useTenVAD.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA2BH,MAAM,WAAW,gBAAgB;IAC/B,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,iEAAiE;IACjE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,gBAAgB,CAAC,EAAE,CAAC,aAAa,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC/E;AAsBD,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB;;;;;EAyWvD"}
|