@livekit/agents 1.0.25 → 1.0.30
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/connection_pool.cjs +242 -0
- package/dist/connection_pool.cjs.map +1 -0
- package/dist/connection_pool.d.cts +123 -0
- package/dist/connection_pool.d.ts +123 -0
- package/dist/connection_pool.d.ts.map +1 -0
- package/dist/connection_pool.js +218 -0
- package/dist/connection_pool.js.map +1 -0
- package/dist/connection_pool.test.cjs +256 -0
- package/dist/connection_pool.test.cjs.map +1 -0
- package/dist/connection_pool.test.js +255 -0
- package/dist/connection_pool.test.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/inference/tts.cjs +172 -58
- package/dist/inference/tts.cjs.map +1 -1
- package/dist/inference/tts.d.cts +3 -1
- package/dist/inference/tts.d.ts +3 -1
- package/dist/inference/tts.d.ts.map +1 -1
- package/dist/inference/tts.js +173 -59
- package/dist/inference/tts.js.map +1 -1
- package/dist/tts/stream_adapter.cjs +6 -3
- package/dist/tts/stream_adapter.cjs.map +1 -1
- package/dist/tts/stream_adapter.d.cts +1 -1
- package/dist/tts/stream_adapter.d.ts +1 -1
- package/dist/tts/stream_adapter.d.ts.map +1 -1
- package/dist/tts/stream_adapter.js +6 -3
- package/dist/tts/stream_adapter.js.map +1 -1
- package/dist/tts/tts.cjs +26 -15
- package/dist/tts/tts.cjs.map +1 -1
- package/dist/tts/tts.d.cts +7 -4
- package/dist/tts/tts.d.ts +7 -4
- package/dist/tts/tts.d.ts.map +1 -1
- package/dist/tts/tts.js +26 -15
- package/dist/tts/tts.js.map +1 -1
- package/dist/utils.cjs +20 -0
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +7 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +19 -0
- package/dist/utils.js.map +1 -1
- package/dist/voice/agent_activity.cjs +3 -1
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +3 -1
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/agent_session.cjs +6 -1
- package/dist/voice/agent_session.cjs.map +1 -1
- package/dist/voice/agent_session.d.ts.map +1 -1
- package/dist/voice/agent_session.js +6 -1
- package/dist/voice/agent_session.js.map +1 -1
- package/dist/voice/avatar/datastream_io.cjs +1 -1
- package/dist/voice/avatar/datastream_io.cjs.map +1 -1
- package/dist/voice/avatar/datastream_io.js +1 -1
- package/dist/voice/avatar/datastream_io.js.map +1 -1
- package/dist/voice/background_audio.cjs +77 -37
- package/dist/voice/background_audio.cjs.map +1 -1
- package/dist/voice/background_audio.d.cts +10 -3
- package/dist/voice/background_audio.d.ts +10 -3
- package/dist/voice/background_audio.d.ts.map +1 -1
- package/dist/voice/background_audio.js +78 -37
- package/dist/voice/background_audio.js.map +1 -1
- package/dist/voice/index.cjs +1 -0
- package/dist/voice/index.cjs.map +1 -1
- package/dist/voice/index.d.cts +1 -0
- package/dist/voice/index.d.ts +1 -0
- package/dist/voice/index.d.ts.map +1 -1
- package/dist/voice/index.js +1 -0
- package/dist/voice/index.js.map +1 -1
- package/dist/voice/io.cjs +10 -1
- package/dist/voice/io.cjs.map +1 -1
- package/dist/voice/io.d.cts +18 -1
- package/dist/voice/io.d.ts +18 -1
- package/dist/voice/io.d.ts.map +1 -1
- package/dist/voice/io.js +10 -1
- package/dist/voice/io.js.map +1 -1
- package/dist/voice/recorder_io/recorder_io.cjs +1 -1
- package/dist/voice/recorder_io/recorder_io.cjs.map +1 -1
- package/dist/voice/recorder_io/recorder_io.js +1 -1
- package/dist/voice/recorder_io/recorder_io.js.map +1 -1
- package/dist/voice/room_io/_output.cjs +1 -1
- package/dist/voice/room_io/_output.cjs.map +1 -1
- package/dist/voice/room_io/_output.js +1 -1
- package/dist/voice/room_io/_output.js.map +1 -1
- package/dist/voice/transcription/synchronizer.cjs +1 -1
- package/dist/voice/transcription/synchronizer.cjs.map +1 -1
- package/dist/voice/transcription/synchronizer.js +1 -1
- package/dist/voice/transcription/synchronizer.js.map +1 -1
- package/dist/worker.cjs +4 -6
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +4 -6
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/src/connection_pool.test.ts +346 -0
- package/src/connection_pool.ts +307 -0
- package/src/index.ts +1 -0
- package/src/inference/tts.ts +206 -65
- package/src/tts/stream_adapter.ts +10 -3
- package/src/tts/tts.ts +41 -18
- package/src/utils.ts +25 -0
- package/src/voice/agent_activity.ts +7 -1
- package/src/voice/agent_session.ts +6 -1
- package/src/voice/avatar/datastream_io.ts +1 -1
- package/src/voice/background_audio.ts +95 -55
- package/src/voice/index.ts +1 -0
- package/src/voice/io.ts +24 -0
- package/src/voice/recorder_io/recorder_io.ts +1 -1
- package/src/voice/room_io/_output.ts +1 -1
- package/src/voice/transcription/synchronizer.ts +1 -1
- package/src/worker.ts +4 -7
|
@@ -74,8 +74,11 @@ class PlayHandle {
|
|
|
74
74
|
class BackgroundAudioPlayer {
|
|
75
75
|
ambientSound;
|
|
76
76
|
thinkingSound;
|
|
77
|
+
streamTimeoutMs;
|
|
77
78
|
playTasks = [];
|
|
78
79
|
audioSource = new import_rtc_node.AudioSource(48e3, 1, AUDIO_SOURCE_BUFFER_MS);
|
|
80
|
+
audioMixer;
|
|
81
|
+
mixerTask;
|
|
79
82
|
room;
|
|
80
83
|
agentSession;
|
|
81
84
|
publication;
|
|
@@ -83,15 +86,20 @@ class BackgroundAudioPlayer {
|
|
|
83
86
|
republishTask;
|
|
84
87
|
ambientHandle;
|
|
85
88
|
thinkingHandle;
|
|
89
|
+
closed = true;
|
|
86
90
|
// TODO (Brian): add lock
|
|
87
91
|
#logger = (0, import_log.log)();
|
|
88
92
|
constructor(options) {
|
|
89
|
-
const { ambientSound, thinkingSound } = options || {};
|
|
93
|
+
const { ambientSound, thinkingSound, streamTimeoutMs = 200 } = options || {};
|
|
90
94
|
this.ambientSound = ambientSound;
|
|
91
95
|
this.thinkingSound = thinkingSound;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
this.streamTimeoutMs = streamTimeoutMs;
|
|
97
|
+
this.audioMixer = new import_rtc_node.AudioMixer(48e3, 1, {
|
|
98
|
+
blocksize: 4800,
|
|
99
|
+
// 100ms at 48kHz
|
|
100
|
+
capacity: 1,
|
|
101
|
+
streamTimeoutMs: this.streamTimeoutMs
|
|
102
|
+
});
|
|
95
103
|
}
|
|
96
104
|
/**
|
|
97
105
|
* Select a sound from a list of background sound based on probability weights
|
|
@@ -190,7 +198,16 @@ class BackgroundAudioPlayer {
|
|
|
190
198
|
this.room = room;
|
|
191
199
|
this.agentSession = agentSession;
|
|
192
200
|
this.trackPublishOptions = trackPublishOptions;
|
|
201
|
+
this.closed = false;
|
|
193
202
|
await this.publishTrack();
|
|
203
|
+
this.mixerTask = import_utils.Task.from(async () => {
|
|
204
|
+
try {
|
|
205
|
+
await this.runMixerTask();
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (this.closed) return;
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
194
211
|
this.room.on("reconnected", this.onReconnected);
|
|
195
212
|
(_a = this.agentSession) == null ? void 0 : _a.on(import_events.AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);
|
|
196
213
|
if (!this.ambientSound) return;
|
|
@@ -205,11 +222,16 @@ class BackgroundAudioPlayer {
|
|
|
205
222
|
*/
|
|
206
223
|
async close() {
|
|
207
224
|
var _a, _b, _c, _d;
|
|
225
|
+
this.closed = true;
|
|
208
226
|
await (0, import_utils.cancelAndWait)(this.playTasks, TASK_TIMEOUT_MS);
|
|
209
227
|
if (this.republishTask) {
|
|
210
228
|
await this.republishTask.cancelAndWait(TASK_TIMEOUT_MS);
|
|
211
229
|
}
|
|
230
|
+
await this.audioMixer.aclose();
|
|
212
231
|
await this.audioSource.close();
|
|
232
|
+
if (this.mixerTask) {
|
|
233
|
+
await this.mixerTask.cancelAndWait(TASK_TIMEOUT_MS);
|
|
234
|
+
}
|
|
213
235
|
(_a = this.agentSession) == null ? void 0 : _a.off(import_events.AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);
|
|
214
236
|
(_b = this.room) == null ? void 0 : _b.off("reconnected", this.onReconnected);
|
|
215
237
|
if (this.publication && this.publication.sid) {
|
|
@@ -250,6 +272,11 @@ class BackgroundAudioPlayer {
|
|
|
250
272
|
async republishTrackTask() {
|
|
251
273
|
await this.publishTrack();
|
|
252
274
|
}
|
|
275
|
+
async runMixerTask() {
|
|
276
|
+
for await (const frame of this.audioMixer) {
|
|
277
|
+
await this.audioSource.captureFrame(frame);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
253
280
|
onAgentStateChanged = (ev) => {
|
|
254
281
|
var _a;
|
|
255
282
|
if (!this.thinkingSound) {
|
|
@@ -259,10 +286,38 @@ class BackgroundAudioPlayer {
|
|
|
259
286
|
if (this.thinkingHandle && !this.thinkingHandle.done()) {
|
|
260
287
|
return;
|
|
261
288
|
}
|
|
289
|
+
const normalized = this.normalizeSoundSource(this.thinkingSound);
|
|
290
|
+
if (normalized) {
|
|
291
|
+
const { source, volume } = normalized;
|
|
292
|
+
const selectedSound = { source, volume, probability: 1 };
|
|
293
|
+
this.thinkingHandle = this.play(selectedSound, typeof source === "string");
|
|
294
|
+
}
|
|
262
295
|
} else {
|
|
263
296
|
(_a = this.thinkingHandle) == null ? void 0 : _a.stop();
|
|
264
297
|
}
|
|
265
298
|
};
|
|
299
|
+
// Note: Python uses numpy, TS uses typed arrays for equivalent logic
|
|
300
|
+
applyVolumeToFrame(frame, volume) {
|
|
301
|
+
const int16Data = new Int16Array(
|
|
302
|
+
frame.data.buffer,
|
|
303
|
+
frame.data.byteOffset,
|
|
304
|
+
frame.data.byteLength / 2
|
|
305
|
+
);
|
|
306
|
+
const float32Data = new Float32Array(int16Data.length);
|
|
307
|
+
for (let i = 0; i < int16Data.length; i++) {
|
|
308
|
+
float32Data[i] = int16Data[i];
|
|
309
|
+
}
|
|
310
|
+
const volumeFactor = 10 ** Math.log10(volume);
|
|
311
|
+
for (let i = 0; i < float32Data.length; i++) {
|
|
312
|
+
float32Data[i] *= volumeFactor;
|
|
313
|
+
}
|
|
314
|
+
const outputData = new Int16Array(float32Data.length);
|
|
315
|
+
for (let i = 0; i < float32Data.length; i++) {
|
|
316
|
+
const clipped = Math.max(-32768, Math.min(32767, float32Data[i]));
|
|
317
|
+
outputData[i] = Math.round(clipped);
|
|
318
|
+
}
|
|
319
|
+
return new import_rtc_node.AudioFrame(outputData, frame.sampleRate, frame.channels, frame.samplesPerChannel);
|
|
320
|
+
}
|
|
266
321
|
async playTask({
|
|
267
322
|
playHandle,
|
|
268
323
|
sound,
|
|
@@ -273,45 +328,30 @@ class BackgroundAudioPlayer {
|
|
|
273
328
|
if (isBuiltinAudioClip(sound)) {
|
|
274
329
|
sound = getBuiltinAudioPath(sound);
|
|
275
330
|
}
|
|
331
|
+
let audioStream;
|
|
276
332
|
if (typeof sound === "string") {
|
|
277
|
-
|
|
333
|
+
audioStream = loop ? (0, import_audio.loopAudioFramesFromFile)(sound, { abortSignal: signal }) : (0, import_audio.audioFramesFromFile)(sound, { abortSignal: signal });
|
|
334
|
+
} else {
|
|
335
|
+
audioStream = sound;
|
|
278
336
|
}
|
|
279
|
-
|
|
280
|
-
|
|
337
|
+
const applyVolume = this.applyVolumeToFrame.bind(this);
|
|
338
|
+
async function* genWrapper() {
|
|
339
|
+
for await (const frame of audioStream) {
|
|
281
340
|
if (signal.aborted || playHandle.done()) break;
|
|
282
|
-
|
|
283
|
-
if (volume !== 1) {
|
|
284
|
-
const int16Data = new Int16Array(
|
|
285
|
-
frame.data.buffer,
|
|
286
|
-
frame.data.byteOffset,
|
|
287
|
-
frame.data.byteLength / 2
|
|
288
|
-
);
|
|
289
|
-
const float32Data = new Float32Array(int16Data.length);
|
|
290
|
-
for (let i = 0; i < int16Data.length; i++) {
|
|
291
|
-
float32Data[i] = int16Data[i];
|
|
292
|
-
}
|
|
293
|
-
const volumeFactor = 10 ** Math.log10(volume);
|
|
294
|
-
for (let i = 0; i < float32Data.length; i++) {
|
|
295
|
-
float32Data[i] *= volumeFactor;
|
|
296
|
-
}
|
|
297
|
-
const outputData = new Int16Array(float32Data.length);
|
|
298
|
-
for (let i = 0; i < float32Data.length; i++) {
|
|
299
|
-
const clipped = Math.max(-32768, Math.min(32767, float32Data[i]));
|
|
300
|
-
outputData[i] = Math.round(clipped);
|
|
301
|
-
}
|
|
302
|
-
processedFrame = new import_rtc_node.AudioFrame(
|
|
303
|
-
outputData,
|
|
304
|
-
frame.sampleRate,
|
|
305
|
-
frame.channels,
|
|
306
|
-
frame.samplesPerChannel
|
|
307
|
-
);
|
|
308
|
-
} else {
|
|
309
|
-
processedFrame = frame;
|
|
310
|
-
}
|
|
311
|
-
await this.audioSource.captureFrame(processedFrame);
|
|
341
|
+
yield volume !== 1 ? applyVolume(frame, volume) : frame;
|
|
312
342
|
}
|
|
343
|
+
playHandle._markPlayoutDone();
|
|
344
|
+
}
|
|
345
|
+
const gen = genWrapper();
|
|
346
|
+
try {
|
|
347
|
+
this.audioMixer.addStream(gen);
|
|
348
|
+
await playHandle.waitForPlayout();
|
|
313
349
|
} finally {
|
|
350
|
+
this.audioMixer.removeStream(gen);
|
|
314
351
|
playHandle._markPlayoutDone();
|
|
352
|
+
if (playHandle.done()) {
|
|
353
|
+
await gen.return(void 0);
|
|
354
|
+
}
|
|
315
355
|
}
|
|
316
356
|
}
|
|
317
357
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/voice/background_audio.ts","../../../node_modules/.pnpm/tsup@8.4.0_@microsoft+api-extractor@7.43.7_@types+node@22.15.30__postcss@8.4.38_tsx@4.20.4_typescript@5.4.5/node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n AudioFrame,\n AudioSource,\n LocalAudioTrack,\n type LocalTrackPublication,\n type Room,\n TrackPublishOptions,\n} from '@livekit/rtc-node';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { audioFramesFromFile, loopAudioFramesFromFile } from '../audio.js';\nimport { log } from '../log.js';\nimport { Future, Task, cancelAndWait } from '../utils.js';\nimport type { AgentSession } from './agent_session.js';\nimport { AgentSessionEventTypes, type AgentStateChangedEvent } from './events.js';\n\nconst TASK_TIMEOUT_MS = 500;\n\nexport enum BuiltinAudioClip {\n OFFICE_AMBIENCE = 'office-ambience.ogg',\n KEYBOARD_TYPING = 'keyboard-typing.ogg',\n KEYBOARD_TYPING2 = 'keyboard-typing2.ogg',\n}\n\nexport function isBuiltinAudioClip(\n source: AudioSourceType | AudioConfig | AudioConfig[],\n): source is BuiltinAudioClip {\n return (\n typeof source === 'string' &&\n Object.values(BuiltinAudioClip).includes(source as BuiltinAudioClip)\n );\n}\n\nexport function getBuiltinAudioPath(clip: BuiltinAudioClip): string {\n const resourcesPath = join(dirname(fileURLToPath(import.meta.url)), '../../resources');\n return join(resourcesPath, clip);\n}\n\nexport type AudioSourceType = string | BuiltinAudioClip | AsyncIterable<AudioFrame>;\n\nexport interface AudioConfig {\n source: AudioSourceType;\n volume?: number;\n probability?: number;\n}\n\nexport interface BackgroundAudioPlayerOptions {\n /**\n * Ambient sound to play continuously in the background.\n * Can be a file path, BuiltinAudioClip, or AudioConfig.\n * File paths will be looped automatically.\n */\n ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];\n\n /**\n * Sound to play when the agent is thinking.\n * TODO (Brian): Implement thinking sound when AudioMixer becomes available\n */\n thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];\n\n /**\n * Stream timeout in milliseconds\n * @defaultValue 200\n */\n streamTimeoutMs?: number;\n}\n\nexport interface BackgroundAudioStartOptions {\n room: Room;\n agentSession?: AgentSession;\n trackPublishOptions?: TrackPublishOptions;\n}\n\n// Queue size for AudioSource buffer (400ms)\n// Kept small to avoid abrupt cutoffs when removing sounds\nconst AUDIO_SOURCE_BUFFER_MS = 400;\n\nexport class PlayHandle {\n private doneFuture = new Future<void>();\n private stopFuture = new Future<void>();\n\n done(): boolean {\n return this.doneFuture.done;\n }\n\n stop(): void {\n if (this.done()) return;\n\n if (!this.stopFuture.done) {\n this.stopFuture.resolve();\n }\n\n this._markPlayoutDone();\n }\n\n async waitForPlayout(): Promise<void> {\n return this.doneFuture.await;\n }\n\n _markPlayoutDone(): void {\n if (!this.doneFuture.done) {\n this.doneFuture.resolve();\n }\n }\n}\n\n/**\n * Manages background audio playback for LiveKit agent sessions\n *\n * This class handles playing ambient sounds and manages audio track publishing.\n * It supports:\n * - Continuous ambient sound playback with looping\n * - Volume control and probability-based sound selection\n * - Integration with LiveKit rooms and agent sessions\n *\n * Note: Thinking sound not yet supported\n *\n * @example\n * ```typescript\n * const player = new BackgroundAudioPlayer({\n * ambientSound: { source: BuiltinAudioClip.OFFICE_AMBIENCE, volume: 0.8 },\n * });\n *\n * await player.start({ room, agentSession });\n * ```\n */\nexport class BackgroundAudioPlayer {\n private ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];\n private thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];\n\n private playTasks: Task<void>[] = [];\n private audioSource = new AudioSource(48000, 1, AUDIO_SOURCE_BUFFER_MS);\n\n private room?: Room;\n private agentSession?: AgentSession;\n private publication?: LocalTrackPublication;\n private trackPublishOptions?: TrackPublishOptions;\n private republishTask?: Task<void>;\n\n private ambientHandle?: PlayHandle;\n private thinkingHandle?: PlayHandle;\n\n // TODO (Brian): add lock\n\n #logger = log();\n\n constructor(options?: BackgroundAudioPlayerOptions) {\n const { ambientSound, thinkingSound } = options || {};\n\n this.ambientSound = ambientSound;\n this.thinkingSound = thinkingSound;\n\n if (this.thinkingSound) {\n this.#logger.warn('thinkingSound is not yet supported');\n // TODO: Implement thinking sound when AudioMixer becomes available\n }\n }\n\n /**\n * Select a sound from a list of background sound based on probability weights\n * Return undefined if no sound is selected (when sum of probabilities < 1.0).\n */\n private selectSoundFromList(sounds: AudioConfig[]): AudioConfig | undefined {\n const totalProbability = sounds.reduce((sum, sound) => sum + (sound.probability ?? 1.0), 0);\n\n if (totalProbability <= 0) {\n return undefined;\n }\n\n if (totalProbability < 1.0 && Math.random() > totalProbability) {\n return undefined;\n }\n\n const normalizeFactor = totalProbability <= 1.0 ? 1.0 : totalProbability;\n const r = Math.random() * Math.min(totalProbability, 1.0);\n let cumulative = 0.0;\n\n for (const sound of sounds) {\n const prob = sound.probability ?? 1.0;\n if (prob <= 0) {\n continue;\n }\n\n const normProb = prob / normalizeFactor;\n cumulative += normProb;\n\n if (r <= cumulative) {\n return sound;\n }\n }\n\n return sounds[sounds.length - 1];\n }\n\n private normalizeSoundSource(\n source?: AudioSourceType | AudioConfig | AudioConfig[],\n ): { source: AudioSourceType; volume: number } | undefined {\n if (source === undefined) {\n return undefined;\n }\n\n if (typeof source === 'string') {\n return {\n source: this.normalizeBuiltinAudio(source),\n volume: 1.0,\n };\n }\n\n if (Array.isArray(source)) {\n const selected = this.selectSoundFromList(source);\n if (selected === undefined) {\n return undefined;\n }\n\n return {\n source: selected.source,\n volume: selected.volume ?? 1.0,\n };\n }\n\n if (typeof source === 'object' && 'source' in source) {\n return {\n source: this.normalizeBuiltinAudio(source.source),\n volume: source.volume ?? 1.0,\n };\n }\n\n return { source, volume: 1.0 };\n }\n\n private normalizeBuiltinAudio(source: AudioSourceType): AudioSourceType {\n if (isBuiltinAudioClip(source)) {\n return getBuiltinAudioPath(source);\n }\n return source;\n }\n\n play(audio: AudioSourceType | AudioConfig | AudioConfig[], loop = false): PlayHandle {\n const normalized = this.normalizeSoundSource(audio);\n if (normalized === undefined) {\n const handle = new PlayHandle();\n handle._markPlayoutDone();\n return handle;\n }\n\n const { source, volume } = normalized;\n const playHandle = new PlayHandle();\n\n const task = Task.from(async ({ signal }) => {\n await this.playTask({ playHandle, sound: source, volume, loop, signal });\n });\n\n task.addDoneCallback(() => {\n playHandle._markPlayoutDone();\n this.playTasks.splice(this.playTasks.indexOf(task), 1);\n });\n\n this.playTasks.push(task);\n return playHandle;\n }\n\n /**\n * Start the background audio system, publishing the audio track\n * and beginning playback of any configured ambient sound.\n *\n * If `ambientSound` is provided (and contains file paths), they will loop\n * automatically. If `ambientSound` contains AsyncIterators, they are assumed\n * to be already infinite or looped.\n *\n * @param options - Options for starting background audio playback\n */\n async start(options: BackgroundAudioStartOptions): Promise<void> {\n const { room, agentSession, trackPublishOptions } = options;\n this.room = room;\n this.agentSession = agentSession;\n this.trackPublishOptions = trackPublishOptions;\n\n await this.publishTrack();\n\n // TODO (Brian): check job context is not fake\n\n // TODO (Brian): start audio mixer task\n this.room.on('reconnected', this.onReconnected);\n\n this.agentSession?.on(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n\n if (!this.ambientSound) return;\n\n const normalized = this.normalizeSoundSource(this.ambientSound);\n if (!normalized) return;\n\n const { source, volume } = normalized;\n const selectedSound: AudioConfig = { source, volume, probability: 1.0 };\n this.ambientHandle = this.play(selectedSound, typeof source === 'string');\n }\n\n /**\n * Close and cleanup the background audio system\n */\n async close(): Promise<void> {\n await cancelAndWait(this.playTasks, TASK_TIMEOUT_MS);\n\n if (this.republishTask) {\n await this.republishTask.cancelAndWait(TASK_TIMEOUT_MS);\n }\n\n // TODO (Brian): cancel audio mixer task and close audio mixer\n\n await this.audioSource.close();\n\n this.agentSession?.off(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n this.room?.off('reconnected', this.onReconnected);\n\n if (this.publication && this.publication.sid) {\n await this.room?.localParticipant?.unpublishTrack(this.publication.sid);\n }\n }\n\n /**\n * Get the current track publication\n */\n getPublication(): LocalTrackPublication | undefined {\n return this.publication;\n }\n\n private async publishTrack(): Promise<void> {\n if (this.publication !== undefined) {\n return;\n }\n\n const track = LocalAudioTrack.createAudioTrack('background_audio', this.audioSource);\n\n if (this.room?.localParticipant === undefined) {\n throw new Error('Local participant not available');\n }\n\n const publication = await this.room.localParticipant.publishTrack(\n track,\n this.trackPublishOptions ?? new TrackPublishOptions(),\n );\n\n this.publication = publication;\n this.#logger.debug(`Background audio track published: ${this.publication.sid}`);\n }\n\n private onReconnected = (): void => {\n if (this.republishTask) {\n this.republishTask.cancel();\n }\n\n this.publication = undefined;\n this.republishTask = Task.from(async () => {\n await this.republishTrackTask();\n });\n };\n\n private async republishTrackTask(): Promise<void> {\n // TODO (Brian): add lock protection when implementing lock\n await this.publishTrack();\n }\n\n private onAgentStateChanged = (ev: AgentStateChangedEvent): void => {\n if (!this.thinkingSound) {\n return;\n }\n\n if (ev.newState === 'thinking') {\n if (this.thinkingHandle && !this.thinkingHandle.done()) {\n return;\n }\n\n // TODO (Brian): play thinking sound and assign to thinkingHandle\n } else {\n this.thinkingHandle?.stop();\n }\n };\n\n private async playTask({\n playHandle,\n sound,\n volume,\n loop,\n signal,\n }: {\n playHandle: PlayHandle;\n sound: AudioSourceType;\n volume: number;\n loop: boolean;\n signal: AbortSignal;\n }): Promise<void> {\n if (isBuiltinAudioClip(sound)) {\n sound = getBuiltinAudioPath(sound);\n }\n\n if (typeof sound === 'string') {\n sound = loop\n ? loopAudioFramesFromFile(sound, { abortSignal: signal })\n : audioFramesFromFile(sound, { abortSignal: signal });\n }\n\n try {\n for await (const frame of sound) {\n if (signal.aborted || playHandle.done()) break;\n\n let processedFrame: AudioFrame;\n\n if (volume !== 1.0) {\n const int16Data = new Int16Array(\n frame.data.buffer,\n frame.data.byteOffset,\n frame.data.byteLength / 2,\n );\n const float32Data = new Float32Array(int16Data.length);\n\n for (let i = 0; i < int16Data.length; i++) {\n float32Data[i] = int16Data[i]!;\n }\n\n const volumeFactor = 10 ** Math.log10(volume);\n for (let i = 0; i < float32Data.length; i++) {\n float32Data[i]! *= volumeFactor;\n }\n\n const outputData = new Int16Array(float32Data.length);\n for (let i = 0; i < float32Data.length; i++) {\n const clipped = Math.max(-32768, Math.min(32767, float32Data[i]!));\n outputData[i] = Math.round(clipped);\n }\n\n processedFrame = new AudioFrame(\n outputData,\n frame.sampleRate,\n frame.channels,\n frame.samplesPerChannel,\n );\n } else {\n processedFrame = frame;\n }\n\n // TODO (Brian): use AudioMixer to add/remove frame streams\n await this.audioSource.captureFrame(processedFrame);\n }\n } finally {\n // TODO: the waitForPlayout() may be innaccurate by 400ms\n playHandle._markPlayoutDone();\n }\n }\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":"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","names":["BuiltinAudioClip"]}
|
|
1
|
+
{"version":3,"sources":["../../src/voice/background_audio.ts","../../../node_modules/.pnpm/tsup@8.4.0_@microsoft+api-extractor@7.43.7_@types+node@22.15.30__postcss@8.4.38_tsx@4.20.4_typescript@5.4.5/node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n AudioFrame,\n AudioMixer,\n AudioSource,\n LocalAudioTrack,\n type LocalTrackPublication,\n type Room,\n TrackPublishOptions,\n} from '@livekit/rtc-node';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { audioFramesFromFile, loopAudioFramesFromFile } from '../audio.js';\nimport { log } from '../log.js';\nimport { Future, Task, cancelAndWait } from '../utils.js';\nimport type { AgentSession } from './agent_session.js';\nimport { AgentSessionEventTypes, type AgentStateChangedEvent } from './events.js';\n\nconst TASK_TIMEOUT_MS = 500;\n\nexport enum BuiltinAudioClip {\n OFFICE_AMBIENCE = 'office-ambience.ogg',\n KEYBOARD_TYPING = 'keyboard-typing.ogg',\n KEYBOARD_TYPING2 = 'keyboard-typing2.ogg',\n}\n\nexport function isBuiltinAudioClip(\n source: AudioSourceType | AudioConfig | AudioConfig[],\n): source is BuiltinAudioClip {\n return (\n typeof source === 'string' &&\n Object.values(BuiltinAudioClip).includes(source as BuiltinAudioClip)\n );\n}\n\nexport function getBuiltinAudioPath(clip: BuiltinAudioClip): string {\n const resourcesPath = join(dirname(fileURLToPath(import.meta.url)), '../../resources');\n return join(resourcesPath, clip);\n}\n\nexport type AudioSourceType = string | BuiltinAudioClip | AsyncIterable<AudioFrame>;\n\nexport interface AudioConfig {\n source: AudioSourceType;\n volume?: number;\n probability?: number;\n}\n\nexport interface BackgroundAudioPlayerOptions {\n /**\n * Ambient sound to play continuously in the background.\n * Can be a file path, BuiltinAudioClip, or AudioConfig.\n * File paths will be looped automatically.\n */\n ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];\n\n /**\n * Sound to play when the agent is thinking.\n * Plays when agent state changes to 'thinking' and stops when it changes to other states.\n */\n thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];\n\n /**\n * Stream timeout in milliseconds\n * @defaultValue 200\n */\n streamTimeoutMs?: number;\n}\n\nexport interface BackgroundAudioStartOptions {\n room: Room;\n agentSession?: AgentSession;\n trackPublishOptions?: TrackPublishOptions;\n}\n\n// Queue size for AudioSource buffer (400ms)\n// Kept small to avoid abrupt cutoffs when removing sounds\nconst AUDIO_SOURCE_BUFFER_MS = 400;\n\nexport class PlayHandle {\n private doneFuture = new Future<void>();\n private stopFuture = new Future<void>();\n\n done(): boolean {\n return this.doneFuture.done;\n }\n\n stop(): void {\n if (this.done()) return;\n\n if (!this.stopFuture.done) {\n this.stopFuture.resolve();\n }\n\n this._markPlayoutDone();\n }\n\n async waitForPlayout(): Promise<void> {\n return this.doneFuture.await;\n }\n\n _markPlayoutDone(): void {\n if (!this.doneFuture.done) {\n this.doneFuture.resolve();\n }\n }\n}\n\n/**\n * Manages background audio playback for LiveKit agent sessions\n *\n * This class handles playing ambient sounds and manages audio track publishing.\n * It supports:\n * - Continuous ambient sound playback with looping\n * - Thinking sound playback during agent processing\n * - Multiple simultaneous audio streams via AudioMixer\n * - Volume control and probability-based sound selection\n * - Integration with LiveKit rooms and agent sessions\n *\n * @example\n * ```typescript\n * const player = new BackgroundAudioPlayer({\n * ambientSound: { source: BuiltinAudioClip.OFFICE_AMBIENCE, volume: 0.8 },\n * thinkingSound: { source: BuiltinAudioClip.KEYBOARD_TYPING, volume: 0.6 },\n * });\n *\n * await player.start({ room, agentSession });\n * ```\n */\nexport class BackgroundAudioPlayer {\n private ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];\n private thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];\n private streamTimeoutMs: number;\n\n private playTasks: Task<void>[] = [];\n private audioSource = new AudioSource(48000, 1, AUDIO_SOURCE_BUFFER_MS);\n private audioMixer: AudioMixer;\n private mixerTask?: Task<void>;\n\n private room?: Room;\n private agentSession?: AgentSession;\n private publication?: LocalTrackPublication;\n private trackPublishOptions?: TrackPublishOptions;\n private republishTask?: Task<void>;\n\n private ambientHandle?: PlayHandle;\n private thinkingHandle?: PlayHandle;\n\n private closed = true;\n\n // TODO (Brian): add lock\n\n #logger = log();\n\n constructor(options?: BackgroundAudioPlayerOptions) {\n const { ambientSound, thinkingSound, streamTimeoutMs = 200 } = options || {};\n\n this.ambientSound = ambientSound;\n this.thinkingSound = thinkingSound;\n this.streamTimeoutMs = streamTimeoutMs;\n\n this.audioMixer = new AudioMixer(48000, 1, {\n blocksize: 4800, // 100ms at 48kHz\n capacity: 1,\n streamTimeoutMs: this.streamTimeoutMs,\n });\n }\n\n /**\n * Select a sound from a list of background sound based on probability weights\n * Return undefined if no sound is selected (when sum of probabilities < 1.0).\n */\n private selectSoundFromList(sounds: AudioConfig[]): AudioConfig | undefined {\n const totalProbability = sounds.reduce((sum, sound) => sum + (sound.probability ?? 1.0), 0);\n\n if (totalProbability <= 0) {\n return undefined;\n }\n\n if (totalProbability < 1.0 && Math.random() > totalProbability) {\n return undefined;\n }\n\n const normalizeFactor = totalProbability <= 1.0 ? 1.0 : totalProbability;\n const r = Math.random() * Math.min(totalProbability, 1.0);\n let cumulative = 0.0;\n\n for (const sound of sounds) {\n const prob = sound.probability ?? 1.0;\n if (prob <= 0) {\n continue;\n }\n\n const normProb = prob / normalizeFactor;\n cumulative += normProb;\n\n if (r <= cumulative) {\n return sound;\n }\n }\n\n return sounds[sounds.length - 1];\n }\n\n private normalizeSoundSource(\n source?: AudioSourceType | AudioConfig | AudioConfig[],\n ): { source: AudioSourceType; volume: number } | undefined {\n if (source === undefined) {\n return undefined;\n }\n\n if (typeof source === 'string') {\n return {\n source: this.normalizeBuiltinAudio(source),\n volume: 1.0,\n };\n }\n\n if (Array.isArray(source)) {\n const selected = this.selectSoundFromList(source);\n if (selected === undefined) {\n return undefined;\n }\n\n return {\n source: selected.source,\n volume: selected.volume ?? 1.0,\n };\n }\n\n if (typeof source === 'object' && 'source' in source) {\n return {\n source: this.normalizeBuiltinAudio(source.source),\n volume: source.volume ?? 1.0,\n };\n }\n\n return { source, volume: 1.0 };\n }\n\n private normalizeBuiltinAudio(source: AudioSourceType): AudioSourceType {\n if (isBuiltinAudioClip(source)) {\n return getBuiltinAudioPath(source);\n }\n return source;\n }\n\n play(audio: AudioSourceType | AudioConfig | AudioConfig[], loop = false): PlayHandle {\n const normalized = this.normalizeSoundSource(audio);\n if (normalized === undefined) {\n const handle = new PlayHandle();\n handle._markPlayoutDone();\n return handle;\n }\n\n const { source, volume } = normalized;\n const playHandle = new PlayHandle();\n\n const task = Task.from(async ({ signal }) => {\n await this.playTask({ playHandle, sound: source, volume, loop, signal });\n });\n\n task.addDoneCallback(() => {\n playHandle._markPlayoutDone();\n this.playTasks.splice(this.playTasks.indexOf(task), 1);\n });\n\n this.playTasks.push(task);\n return playHandle;\n }\n\n /**\n * Start the background audio system, publishing the audio track\n * and beginning playback of any configured ambient sound.\n *\n * If `ambientSound` is provided (and contains file paths), they will loop\n * automatically. If `ambientSound` contains AsyncIterators, they are assumed\n * to be already infinite or looped.\n *\n * @param options - Options for starting background audio playback\n */\n async start(options: BackgroundAudioStartOptions): Promise<void> {\n const { room, agentSession, trackPublishOptions } = options;\n this.room = room;\n this.agentSession = agentSession;\n this.trackPublishOptions = trackPublishOptions;\n\n this.closed = false;\n\n await this.publishTrack();\n\n // TODO (Brian): check job context is not fake\n\n this.mixerTask = Task.from(async () => {\n try {\n await this.runMixerTask();\n } catch (err) {\n if (this.closed) return; // expected when AudioSource is closed\n throw err;\n }\n });\n\n this.room.on('reconnected', this.onReconnected);\n\n this.agentSession?.on(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n if (!this.ambientSound) return;\n\n const normalized = this.normalizeSoundSource(this.ambientSound);\n if (!normalized) return;\n\n const { source, volume } = normalized;\n const selectedSound: AudioConfig = { source, volume, probability: 1.0 };\n this.ambientHandle = this.play(selectedSound, typeof source === 'string');\n }\n\n /**\n * Close and cleanup the background audio system\n */\n async close(): Promise<void> {\n this.closed = true;\n\n await cancelAndWait(this.playTasks, TASK_TIMEOUT_MS);\n\n if (this.republishTask) {\n await this.republishTask.cancelAndWait(TASK_TIMEOUT_MS);\n }\n\n await this.audioMixer.aclose();\n await this.audioSource.close();\n\n if (this.mixerTask) {\n await this.mixerTask.cancelAndWait(TASK_TIMEOUT_MS);\n }\n\n this.agentSession?.off(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);\n this.room?.off('reconnected', this.onReconnected);\n\n if (this.publication && this.publication.sid) {\n await this.room?.localParticipant?.unpublishTrack(this.publication.sid);\n }\n }\n\n /**\n * Get the current track publication\n */\n getPublication(): LocalTrackPublication | undefined {\n return this.publication;\n }\n\n private async publishTrack(): Promise<void> {\n if (this.publication !== undefined) {\n return;\n }\n\n const track = LocalAudioTrack.createAudioTrack('background_audio', this.audioSource);\n\n if (this.room?.localParticipant === undefined) {\n throw new Error('Local participant not available');\n }\n\n const publication = await this.room.localParticipant.publishTrack(\n track,\n this.trackPublishOptions ?? new TrackPublishOptions(),\n );\n\n this.publication = publication;\n this.#logger.debug(`Background audio track published: ${this.publication.sid}`);\n }\n\n private onReconnected = (): void => {\n if (this.republishTask) {\n this.republishTask.cancel();\n }\n\n this.publication = undefined;\n this.republishTask = Task.from(async () => {\n await this.republishTrackTask();\n });\n };\n\n private async republishTrackTask(): Promise<void> {\n // TODO (Brian): add lock protection when implementing lock\n await this.publishTrack();\n }\n\n private async runMixerTask(): Promise<void> {\n for await (const frame of this.audioMixer) {\n await this.audioSource.captureFrame(frame);\n }\n }\n\n private onAgentStateChanged = (ev: AgentStateChangedEvent): void => {\n if (!this.thinkingSound) {\n return;\n }\n\n if (ev.newState === 'thinking') {\n if (this.thinkingHandle && !this.thinkingHandle.done()) {\n return;\n }\n\n const normalized = this.normalizeSoundSource(this.thinkingSound);\n if (normalized) {\n const { source, volume } = normalized;\n const selectedSound: AudioConfig = { source, volume, probability: 1.0 };\n // Loop thinking sound while in thinking state (same as ambient)\n this.thinkingHandle = this.play(selectedSound, typeof source === 'string');\n }\n } else {\n this.thinkingHandle?.stop();\n }\n };\n\n // Note: Python uses numpy, TS uses typed arrays for equivalent logic\n private applyVolumeToFrame(frame: AudioFrame, volume: number): AudioFrame {\n const int16Data = new Int16Array(\n frame.data.buffer,\n frame.data.byteOffset,\n frame.data.byteLength / 2,\n );\n const float32Data = new Float32Array(int16Data.length);\n\n for (let i = 0; i < int16Data.length; i++) {\n float32Data[i] = int16Data[i]!;\n }\n\n const volumeFactor = 10 ** Math.log10(volume);\n for (let i = 0; i < float32Data.length; i++) {\n float32Data[i]! *= volumeFactor;\n }\n\n const outputData = new Int16Array(float32Data.length);\n for (let i = 0; i < float32Data.length; i++) {\n const clipped = Math.max(-32768, Math.min(32767, float32Data[i]!));\n outputData[i] = Math.round(clipped);\n }\n\n return new AudioFrame(outputData, frame.sampleRate, frame.channels, frame.samplesPerChannel);\n }\n\n private async playTask({\n playHandle,\n sound,\n volume,\n loop,\n signal,\n }: {\n playHandle: PlayHandle;\n sound: AudioSourceType;\n volume: number;\n loop: boolean;\n signal: AbortSignal;\n }): Promise<void> {\n if (isBuiltinAudioClip(sound)) {\n sound = getBuiltinAudioPath(sound);\n }\n\n let audioStream: AsyncIterable<AudioFrame>;\n if (typeof sound === 'string') {\n audioStream = loop\n ? loopAudioFramesFromFile(sound, { abortSignal: signal })\n : audioFramesFromFile(sound, { abortSignal: signal });\n } else {\n audioStream = sound;\n }\n\n const applyVolume = this.applyVolumeToFrame.bind(this);\n async function* genWrapper(): AsyncGenerator<AudioFrame> {\n for await (const frame of audioStream) {\n if (signal.aborted || playHandle.done()) break;\n yield volume !== 1.0 ? applyVolume(frame, volume) : frame;\n }\n playHandle._markPlayoutDone();\n }\n\n const gen = genWrapper();\n try {\n this.audioMixer.addStream(gen);\n await playHandle.waitForPlayout();\n } finally {\n this.audioMixer.removeStream(gen);\n playHandle._markPlayoutDone();\n\n if (playHandle.done()) {\n await gen.return(undefined);\n }\n }\n }\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":"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","names":["BuiltinAudioClip"]}
|
|
@@ -22,7 +22,7 @@ export interface BackgroundAudioPlayerOptions {
|
|
|
22
22
|
ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];
|
|
23
23
|
/**
|
|
24
24
|
* Sound to play when the agent is thinking.
|
|
25
|
-
*
|
|
25
|
+
* Plays when agent state changes to 'thinking' and stops when it changes to other states.
|
|
26
26
|
*/
|
|
27
27
|
thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];
|
|
28
28
|
/**
|
|
@@ -50,15 +50,16 @@ export declare class PlayHandle {
|
|
|
50
50
|
* This class handles playing ambient sounds and manages audio track publishing.
|
|
51
51
|
* It supports:
|
|
52
52
|
* - Continuous ambient sound playback with looping
|
|
53
|
+
* - Thinking sound playback during agent processing
|
|
54
|
+
* - Multiple simultaneous audio streams via AudioMixer
|
|
53
55
|
* - Volume control and probability-based sound selection
|
|
54
56
|
* - Integration with LiveKit rooms and agent sessions
|
|
55
57
|
*
|
|
56
|
-
* Note: Thinking sound not yet supported
|
|
57
|
-
*
|
|
58
58
|
* @example
|
|
59
59
|
* ```typescript
|
|
60
60
|
* const player = new BackgroundAudioPlayer({
|
|
61
61
|
* ambientSound: { source: BuiltinAudioClip.OFFICE_AMBIENCE, volume: 0.8 },
|
|
62
|
+
* thinkingSound: { source: BuiltinAudioClip.KEYBOARD_TYPING, volume: 0.6 },
|
|
62
63
|
* });
|
|
63
64
|
*
|
|
64
65
|
* await player.start({ room, agentSession });
|
|
@@ -68,8 +69,11 @@ export declare class BackgroundAudioPlayer {
|
|
|
68
69
|
#private;
|
|
69
70
|
private ambientSound?;
|
|
70
71
|
private thinkingSound?;
|
|
72
|
+
private streamTimeoutMs;
|
|
71
73
|
private playTasks;
|
|
72
74
|
private audioSource;
|
|
75
|
+
private audioMixer;
|
|
76
|
+
private mixerTask?;
|
|
73
77
|
private room?;
|
|
74
78
|
private agentSession?;
|
|
75
79
|
private publication?;
|
|
@@ -77,6 +81,7 @@ export declare class BackgroundAudioPlayer {
|
|
|
77
81
|
private republishTask?;
|
|
78
82
|
private ambientHandle?;
|
|
79
83
|
private thinkingHandle?;
|
|
84
|
+
private closed;
|
|
80
85
|
constructor(options?: BackgroundAudioPlayerOptions);
|
|
81
86
|
/**
|
|
82
87
|
* Select a sound from a list of background sound based on probability weights
|
|
@@ -108,7 +113,9 @@ export declare class BackgroundAudioPlayer {
|
|
|
108
113
|
private publishTrack;
|
|
109
114
|
private onReconnected;
|
|
110
115
|
private republishTrackTask;
|
|
116
|
+
private runMixerTask;
|
|
111
117
|
private onAgentStateChanged;
|
|
118
|
+
private applyVolumeToFrame;
|
|
112
119
|
private playTask;
|
|
113
120
|
}
|
|
114
121
|
//# sourceMappingURL=background_audio.d.ts.map
|
|
@@ -22,7 +22,7 @@ export interface BackgroundAudioPlayerOptions {
|
|
|
22
22
|
ambientSound?: AudioSourceType | AudioConfig | AudioConfig[];
|
|
23
23
|
/**
|
|
24
24
|
* Sound to play when the agent is thinking.
|
|
25
|
-
*
|
|
25
|
+
* Plays when agent state changes to 'thinking' and stops when it changes to other states.
|
|
26
26
|
*/
|
|
27
27
|
thinkingSound?: AudioSourceType | AudioConfig | AudioConfig[];
|
|
28
28
|
/**
|
|
@@ -50,15 +50,16 @@ export declare class PlayHandle {
|
|
|
50
50
|
* This class handles playing ambient sounds and manages audio track publishing.
|
|
51
51
|
* It supports:
|
|
52
52
|
* - Continuous ambient sound playback with looping
|
|
53
|
+
* - Thinking sound playback during agent processing
|
|
54
|
+
* - Multiple simultaneous audio streams via AudioMixer
|
|
53
55
|
* - Volume control and probability-based sound selection
|
|
54
56
|
* - Integration with LiveKit rooms and agent sessions
|
|
55
57
|
*
|
|
56
|
-
* Note: Thinking sound not yet supported
|
|
57
|
-
*
|
|
58
58
|
* @example
|
|
59
59
|
* ```typescript
|
|
60
60
|
* const player = new BackgroundAudioPlayer({
|
|
61
61
|
* ambientSound: { source: BuiltinAudioClip.OFFICE_AMBIENCE, volume: 0.8 },
|
|
62
|
+
* thinkingSound: { source: BuiltinAudioClip.KEYBOARD_TYPING, volume: 0.6 },
|
|
62
63
|
* });
|
|
63
64
|
*
|
|
64
65
|
* await player.start({ room, agentSession });
|
|
@@ -68,8 +69,11 @@ export declare class BackgroundAudioPlayer {
|
|
|
68
69
|
#private;
|
|
69
70
|
private ambientSound?;
|
|
70
71
|
private thinkingSound?;
|
|
72
|
+
private streamTimeoutMs;
|
|
71
73
|
private playTasks;
|
|
72
74
|
private audioSource;
|
|
75
|
+
private audioMixer;
|
|
76
|
+
private mixerTask?;
|
|
73
77
|
private room?;
|
|
74
78
|
private agentSession?;
|
|
75
79
|
private publication?;
|
|
@@ -77,6 +81,7 @@ export declare class BackgroundAudioPlayer {
|
|
|
77
81
|
private republishTask?;
|
|
78
82
|
private ambientHandle?;
|
|
79
83
|
private thinkingHandle?;
|
|
84
|
+
private closed;
|
|
80
85
|
constructor(options?: BackgroundAudioPlayerOptions);
|
|
81
86
|
/**
|
|
82
87
|
* Select a sound from a list of background sound based on probability weights
|
|
@@ -108,7 +113,9 @@ export declare class BackgroundAudioPlayer {
|
|
|
108
113
|
private publishTrack;
|
|
109
114
|
private onReconnected;
|
|
110
115
|
private republishTrackTask;
|
|
116
|
+
private runMixerTask;
|
|
111
117
|
private onAgentStateChanged;
|
|
118
|
+
private applyVolumeToFrame;
|
|
112
119
|
private playTask;
|
|
113
120
|
}
|
|
114
121
|
//# sourceMappingURL=background_audio.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"background_audio.d.ts","sourceRoot":"","sources":["../../src/voice/background_audio.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,UAAU,
|
|
1
|
+
{"version":3,"file":"background_audio.d.ts","sourceRoot":"","sources":["../../src/voice/background_audio.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AudioFrame,
|
|
3
|
+
AudioMixer,
|
|
3
4
|
AudioSource,
|
|
4
5
|
LocalAudioTrack,
|
|
5
6
|
TrackPublishOptions
|
|
@@ -50,8 +51,11 @@ class PlayHandle {
|
|
|
50
51
|
class BackgroundAudioPlayer {
|
|
51
52
|
ambientSound;
|
|
52
53
|
thinkingSound;
|
|
54
|
+
streamTimeoutMs;
|
|
53
55
|
playTasks = [];
|
|
54
56
|
audioSource = new AudioSource(48e3, 1, AUDIO_SOURCE_BUFFER_MS);
|
|
57
|
+
audioMixer;
|
|
58
|
+
mixerTask;
|
|
55
59
|
room;
|
|
56
60
|
agentSession;
|
|
57
61
|
publication;
|
|
@@ -59,15 +63,20 @@ class BackgroundAudioPlayer {
|
|
|
59
63
|
republishTask;
|
|
60
64
|
ambientHandle;
|
|
61
65
|
thinkingHandle;
|
|
66
|
+
closed = true;
|
|
62
67
|
// TODO (Brian): add lock
|
|
63
68
|
#logger = log();
|
|
64
69
|
constructor(options) {
|
|
65
|
-
const { ambientSound, thinkingSound } = options || {};
|
|
70
|
+
const { ambientSound, thinkingSound, streamTimeoutMs = 200 } = options || {};
|
|
66
71
|
this.ambientSound = ambientSound;
|
|
67
72
|
this.thinkingSound = thinkingSound;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
this.streamTimeoutMs = streamTimeoutMs;
|
|
74
|
+
this.audioMixer = new AudioMixer(48e3, 1, {
|
|
75
|
+
blocksize: 4800,
|
|
76
|
+
// 100ms at 48kHz
|
|
77
|
+
capacity: 1,
|
|
78
|
+
streamTimeoutMs: this.streamTimeoutMs
|
|
79
|
+
});
|
|
71
80
|
}
|
|
72
81
|
/**
|
|
73
82
|
* Select a sound from a list of background sound based on probability weights
|
|
@@ -166,7 +175,16 @@ class BackgroundAudioPlayer {
|
|
|
166
175
|
this.room = room;
|
|
167
176
|
this.agentSession = agentSession;
|
|
168
177
|
this.trackPublishOptions = trackPublishOptions;
|
|
178
|
+
this.closed = false;
|
|
169
179
|
await this.publishTrack();
|
|
180
|
+
this.mixerTask = Task.from(async () => {
|
|
181
|
+
try {
|
|
182
|
+
await this.runMixerTask();
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (this.closed) return;
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
170
188
|
this.room.on("reconnected", this.onReconnected);
|
|
171
189
|
(_a = this.agentSession) == null ? void 0 : _a.on(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);
|
|
172
190
|
if (!this.ambientSound) return;
|
|
@@ -181,11 +199,16 @@ class BackgroundAudioPlayer {
|
|
|
181
199
|
*/
|
|
182
200
|
async close() {
|
|
183
201
|
var _a, _b, _c, _d;
|
|
202
|
+
this.closed = true;
|
|
184
203
|
await cancelAndWait(this.playTasks, TASK_TIMEOUT_MS);
|
|
185
204
|
if (this.republishTask) {
|
|
186
205
|
await this.republishTask.cancelAndWait(TASK_TIMEOUT_MS);
|
|
187
206
|
}
|
|
207
|
+
await this.audioMixer.aclose();
|
|
188
208
|
await this.audioSource.close();
|
|
209
|
+
if (this.mixerTask) {
|
|
210
|
+
await this.mixerTask.cancelAndWait(TASK_TIMEOUT_MS);
|
|
211
|
+
}
|
|
189
212
|
(_a = this.agentSession) == null ? void 0 : _a.off(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged);
|
|
190
213
|
(_b = this.room) == null ? void 0 : _b.off("reconnected", this.onReconnected);
|
|
191
214
|
if (this.publication && this.publication.sid) {
|
|
@@ -226,6 +249,11 @@ class BackgroundAudioPlayer {
|
|
|
226
249
|
async republishTrackTask() {
|
|
227
250
|
await this.publishTrack();
|
|
228
251
|
}
|
|
252
|
+
async runMixerTask() {
|
|
253
|
+
for await (const frame of this.audioMixer) {
|
|
254
|
+
await this.audioSource.captureFrame(frame);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
229
257
|
onAgentStateChanged = (ev) => {
|
|
230
258
|
var _a;
|
|
231
259
|
if (!this.thinkingSound) {
|
|
@@ -235,10 +263,38 @@ class BackgroundAudioPlayer {
|
|
|
235
263
|
if (this.thinkingHandle && !this.thinkingHandle.done()) {
|
|
236
264
|
return;
|
|
237
265
|
}
|
|
266
|
+
const normalized = this.normalizeSoundSource(this.thinkingSound);
|
|
267
|
+
if (normalized) {
|
|
268
|
+
const { source, volume } = normalized;
|
|
269
|
+
const selectedSound = { source, volume, probability: 1 };
|
|
270
|
+
this.thinkingHandle = this.play(selectedSound, typeof source === "string");
|
|
271
|
+
}
|
|
238
272
|
} else {
|
|
239
273
|
(_a = this.thinkingHandle) == null ? void 0 : _a.stop();
|
|
240
274
|
}
|
|
241
275
|
};
|
|
276
|
+
// Note: Python uses numpy, TS uses typed arrays for equivalent logic
|
|
277
|
+
applyVolumeToFrame(frame, volume) {
|
|
278
|
+
const int16Data = new Int16Array(
|
|
279
|
+
frame.data.buffer,
|
|
280
|
+
frame.data.byteOffset,
|
|
281
|
+
frame.data.byteLength / 2
|
|
282
|
+
);
|
|
283
|
+
const float32Data = new Float32Array(int16Data.length);
|
|
284
|
+
for (let i = 0; i < int16Data.length; i++) {
|
|
285
|
+
float32Data[i] = int16Data[i];
|
|
286
|
+
}
|
|
287
|
+
const volumeFactor = 10 ** Math.log10(volume);
|
|
288
|
+
for (let i = 0; i < float32Data.length; i++) {
|
|
289
|
+
float32Data[i] *= volumeFactor;
|
|
290
|
+
}
|
|
291
|
+
const outputData = new Int16Array(float32Data.length);
|
|
292
|
+
for (let i = 0; i < float32Data.length; i++) {
|
|
293
|
+
const clipped = Math.max(-32768, Math.min(32767, float32Data[i]));
|
|
294
|
+
outputData[i] = Math.round(clipped);
|
|
295
|
+
}
|
|
296
|
+
return new AudioFrame(outputData, frame.sampleRate, frame.channels, frame.samplesPerChannel);
|
|
297
|
+
}
|
|
242
298
|
async playTask({
|
|
243
299
|
playHandle,
|
|
244
300
|
sound,
|
|
@@ -249,45 +305,30 @@ class BackgroundAudioPlayer {
|
|
|
249
305
|
if (isBuiltinAudioClip(sound)) {
|
|
250
306
|
sound = getBuiltinAudioPath(sound);
|
|
251
307
|
}
|
|
308
|
+
let audioStream;
|
|
252
309
|
if (typeof sound === "string") {
|
|
253
|
-
|
|
310
|
+
audioStream = loop ? loopAudioFramesFromFile(sound, { abortSignal: signal }) : audioFramesFromFile(sound, { abortSignal: signal });
|
|
311
|
+
} else {
|
|
312
|
+
audioStream = sound;
|
|
254
313
|
}
|
|
255
|
-
|
|
256
|
-
|
|
314
|
+
const applyVolume = this.applyVolumeToFrame.bind(this);
|
|
315
|
+
async function* genWrapper() {
|
|
316
|
+
for await (const frame of audioStream) {
|
|
257
317
|
if (signal.aborted || playHandle.done()) break;
|
|
258
|
-
|
|
259
|
-
if (volume !== 1) {
|
|
260
|
-
const int16Data = new Int16Array(
|
|
261
|
-
frame.data.buffer,
|
|
262
|
-
frame.data.byteOffset,
|
|
263
|
-
frame.data.byteLength / 2
|
|
264
|
-
);
|
|
265
|
-
const float32Data = new Float32Array(int16Data.length);
|
|
266
|
-
for (let i = 0; i < int16Data.length; i++) {
|
|
267
|
-
float32Data[i] = int16Data[i];
|
|
268
|
-
}
|
|
269
|
-
const volumeFactor = 10 ** Math.log10(volume);
|
|
270
|
-
for (let i = 0; i < float32Data.length; i++) {
|
|
271
|
-
float32Data[i] *= volumeFactor;
|
|
272
|
-
}
|
|
273
|
-
const outputData = new Int16Array(float32Data.length);
|
|
274
|
-
for (let i = 0; i < float32Data.length; i++) {
|
|
275
|
-
const clipped = Math.max(-32768, Math.min(32767, float32Data[i]));
|
|
276
|
-
outputData[i] = Math.round(clipped);
|
|
277
|
-
}
|
|
278
|
-
processedFrame = new AudioFrame(
|
|
279
|
-
outputData,
|
|
280
|
-
frame.sampleRate,
|
|
281
|
-
frame.channels,
|
|
282
|
-
frame.samplesPerChannel
|
|
283
|
-
);
|
|
284
|
-
} else {
|
|
285
|
-
processedFrame = frame;
|
|
286
|
-
}
|
|
287
|
-
await this.audioSource.captureFrame(processedFrame);
|
|
318
|
+
yield volume !== 1 ? applyVolume(frame, volume) : frame;
|
|
288
319
|
}
|
|
320
|
+
playHandle._markPlayoutDone();
|
|
321
|
+
}
|
|
322
|
+
const gen = genWrapper();
|
|
323
|
+
try {
|
|
324
|
+
this.audioMixer.addStream(gen);
|
|
325
|
+
await playHandle.waitForPlayout();
|
|
289
326
|
} finally {
|
|
327
|
+
this.audioMixer.removeStream(gen);
|
|
290
328
|
playHandle._markPlayoutDone();
|
|
329
|
+
if (playHandle.done()) {
|
|
330
|
+
await gen.return(void 0);
|
|
331
|
+
}
|
|
291
332
|
}
|
|
292
333
|
}
|
|
293
334
|
}
|