@livekit/agents 1.0.27 → 1.0.31
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 -56
- package/dist/inference/tts.cjs.map +1 -1
- package/dist/inference/tts.d.cts +3 -0
- package/dist/inference/tts.d.ts +3 -0
- package/dist/inference/tts.d.ts.map +1 -1
- package/dist/inference/tts.js +173 -57
- package/dist/inference/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 +4 -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 +4 -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 -63
- package/src/utils.ts +25 -0
- package/src/voice/agent_activity.ts +7 -1
- package/src/voice/agent_session.ts +4 -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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/voice/background_audio.ts"],"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"],"mappings":"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","names":["BuiltinAudioClip"]}
|
|
1
|
+
{"version":3,"sources":["../../src/voice/background_audio.ts"],"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"],"mappings":"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","names":["BuiltinAudioClip"]}
|
package/dist/voice/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var import_agent_session = require("./agent_session.cjs");
|
|
|
30
30
|
__reExport(voice_exports, require("./avatar/index.cjs"), module.exports);
|
|
31
31
|
__reExport(voice_exports, require("./background_audio.cjs"), module.exports);
|
|
32
32
|
__reExport(voice_exports, require("./events.cjs"), module.exports);
|
|
33
|
+
var import_io = require("./io.cjs");
|
|
33
34
|
__reExport(voice_exports, require("./report.cjs"), module.exports);
|
|
34
35
|
__reExport(voice_exports, require("./room_io/index.cjs"), module.exports);
|
|
35
36
|
var import_run_context = require("./run_context.cjs");
|
package/dist/voice/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/voice/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport { Agent, StopResponse, type AgentOptions, type ModelSettings } from './agent.js';\nexport { AgentSession, type AgentSessionOptions } from './agent_session.js';\nexport * from './avatar/index.js';\nexport * from './background_audio.js';\nexport * from './events.js';\nexport * from './report.js';\nexport * from './room_io/index.js';\nexport { RunContext } from './run_context.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,mBAA2E;AAC3E,2BAAuD;AACvD,0BAAc,8BALd;AAMA,0BAAc,kCANd;AAOA,0BAAc,wBAPd;AAQA,0BAAc,
|
|
1
|
+
{"version":3,"sources":["../../src/voice/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport { Agent, StopResponse, type AgentOptions, type ModelSettings } from './agent.js';\nexport { AgentSession, type AgentSessionOptions } from './agent_session.js';\nexport * from './avatar/index.js';\nexport * from './background_audio.js';\nexport * from './events.js';\nexport { type TimedString } from './io.js';\nexport * from './report.js';\nexport * from './room_io/index.js';\nexport { RunContext } from './run_context.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,mBAA2E;AAC3E,2BAAuD;AACvD,0BAAc,8BALd;AAMA,0BAAc,kCANd;AAOA,0BAAc,wBAPd;AAQA,gBAAiC;AACjC,0BAAc,wBATd;AAUA,0BAAc,+BAVd;AAWA,yBAA2B;","names":[]}
|
package/dist/voice/index.d.cts
CHANGED
|
@@ -3,6 +3,7 @@ export { AgentSession, type AgentSessionOptions } from './agent_session.js';
|
|
|
3
3
|
export * from './avatar/index.js';
|
|
4
4
|
export * from './background_audio.js';
|
|
5
5
|
export * from './events.js';
|
|
6
|
+
export { type TimedString } from './io.js';
|
|
6
7
|
export * from './report.js';
|
|
7
8
|
export * from './room_io/index.js';
|
|
8
9
|
export { RunContext } from './run_context.js';
|
package/dist/voice/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { AgentSession, type AgentSessionOptions } from './agent_session.js';
|
|
|
3
3
|
export * from './avatar/index.js';
|
|
4
4
|
export * from './background_audio.js';
|
|
5
5
|
export * from './events.js';
|
|
6
|
+
export { type TimedString } from './io.js';
|
|
6
7
|
export * from './report.js';
|
|
7
8
|
export * from './room_io/index.js';
|
|
8
9
|
export { RunContext } from './run_context.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/voice/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/voice/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/voice/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { AgentSession } from "./agent_session.js";
|
|
|
3
3
|
export * from "./avatar/index.js";
|
|
4
4
|
export * from "./background_audio.js";
|
|
5
5
|
export * from "./events.js";
|
|
6
|
+
import {} from "./io.js";
|
|
6
7
|
export * from "./report.js";
|
|
7
8
|
export * from "./room_io/index.js";
|
|
8
9
|
import { RunContext } from "./run_context.js";
|
package/dist/voice/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/voice/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport { Agent, StopResponse, type AgentOptions, type ModelSettings } from './agent.js';\nexport { AgentSession, type AgentSessionOptions } from './agent_session.js';\nexport * from './avatar/index.js';\nexport * from './background_audio.js';\nexport * from './events.js';\nexport * from './report.js';\nexport * from './room_io/index.js';\nexport { RunContext } from './run_context.js';\n"],"mappings":"AAGA,SAAS,OAAO,oBAA2D;AAC3E,SAAS,oBAA8C;AACvD,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,kBAAkB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/voice/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport { Agent, StopResponse, type AgentOptions, type ModelSettings } from './agent.js';\nexport { AgentSession, type AgentSessionOptions } from './agent_session.js';\nexport * from './avatar/index.js';\nexport * from './background_audio.js';\nexport * from './events.js';\nexport { type TimedString } from './io.js';\nexport * from './report.js';\nexport * from './room_io/index.js';\nexport { RunContext } from './run_context.js';\n"],"mappings":"AAGA,SAAS,OAAO,oBAA2D;AAC3E,SAAS,oBAA8C;AACvD,cAAc;AACd,cAAc;AACd,cAAc;AACd,eAAiC;AACjC,cAAc;AACd,cAAc;AACd,SAAS,kBAAkB;","names":[]}
|
package/dist/voice/io.cjs
CHANGED
|
@@ -40,10 +40,11 @@ class AudioInput {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
class AudioOutput extends import_node_events.EventEmitter {
|
|
43
|
-
constructor(sampleRate, nextInChain) {
|
|
43
|
+
constructor(sampleRate, nextInChain, capabilities = { pause: false }) {
|
|
44
44
|
super();
|
|
45
45
|
this.sampleRate = sampleRate;
|
|
46
46
|
this.nextInChain = nextInChain;
|
|
47
|
+
this.capabilities = capabilities;
|
|
47
48
|
if (this.nextInChain) {
|
|
48
49
|
this.nextInChain.on(
|
|
49
50
|
AudioOutput.EVENT_PLAYBACK_FINISHED,
|
|
@@ -61,6 +62,14 @@ class AudioOutput extends import_node_events.EventEmitter {
|
|
|
61
62
|
interrupted: false
|
|
62
63
|
};
|
|
63
64
|
logger = (0, import_log.log)();
|
|
65
|
+
capabilities;
|
|
66
|
+
/**
|
|
67
|
+
* Whether this output and all outputs in the chain support pause/resume.
|
|
68
|
+
*/
|
|
69
|
+
get canPause() {
|
|
70
|
+
var _a;
|
|
71
|
+
return this.capabilities.pause && (((_a = this.nextInChain) == null ? void 0 : _a.canPause) ?? true);
|
|
72
|
+
}
|
|
64
73
|
/**
|
|
65
74
|
* Capture an audio frame for playback, frames can be pushed faster than real-time
|
|
66
75
|
*/
|
package/dist/voice/io.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/voice/io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport type { ChatContext } from '../llm/chat_context.js';\nimport type { ChatChunk } from '../llm/llm.js';\nimport type { ToolContext } from '../llm/tool_context.js';\nimport { log } from '../log.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport type { SpeechEvent } from '../stt/stt.js';\nimport { Future } from '../utils.js';\nimport type { ModelSettings } from './agent.js';\n\nexport type STTNode = (\n audio: ReadableStream<AudioFrame>,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<SpeechEvent | string> | null>;\n\nexport type LLMNode = (\n chatCtx: ChatContext,\n toolCtx: ToolContext,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<ChatChunk | string> | null>;\n\nexport type TTSNode = (\n text: ReadableStream<string>,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<AudioFrame> | null>;\n\nexport abstract class AudioInput {\n protected deferredStream: DeferredReadableStream<AudioFrame> =\n new DeferredReadableStream<AudioFrame>();\n\n get stream(): ReadableStream<AudioFrame> {\n return this.deferredStream.stream;\n }\n\n onAttached(): void {}\n\n onDetached(): void {}\n}\n\nexport abstract class AudioOutput extends EventEmitter {\n static readonly EVENT_PLAYBACK_FINISHED = 'playbackFinished';\n\n private playbackFinishedFuture: Future<void> = new Future();\n private _capturing: boolean = false;\n private playbackFinishedCount: number = 0;\n private playbackSegmentsCount: number = 0;\n private lastPlaybackEvent: PlaybackFinishedEvent = {\n playbackPosition: 0,\n interrupted: false,\n };\n protected logger = log();\n\n constructor(\n public sampleRate?: number,\n protected readonly nextInChain?: AudioOutput,\n ) {\n super();\n if (this.nextInChain) {\n this.nextInChain.on(AudioOutput.EVENT_PLAYBACK_FINISHED, (ev: PlaybackFinishedEvent) =>\n this.onPlaybackFinished(ev),\n );\n }\n }\n\n /**\n * Capture an audio frame for playback, frames can be pushed faster than real-time\n */\n async captureFrame(_frame: AudioFrame): Promise<void> {\n if (!this._capturing) {\n this._capturing = true;\n this.playbackSegmentsCount++;\n }\n }\n\n /**\n * Wait for the past audio segments to finish playing out.\n *\n * @returns The event that was emitted when the audio finished playing out (only the last segment information)\n */\n async waitForPlayout(): Promise<PlaybackFinishedEvent> {\n const target = this.playbackSegmentsCount;\n\n while (this.playbackFinishedCount < target) {\n await this.playbackFinishedFuture.await;\n this.playbackFinishedFuture = new Future();\n }\n\n return this.lastPlaybackEvent;\n }\n\n /**\n * Developers building audio sinks must call this method when a playback/segment is finished.\n * Segments are segmented by calls to flush() or clearBuffer()\n */\n onPlaybackFinished(options: PlaybackFinishedEvent) {\n if (this.playbackFinishedCount >= this.playbackSegmentsCount) {\n this.logger.warn('playback_finished called more times than playback segments were captured');\n return;\n }\n\n this.lastPlaybackEvent = options;\n this.playbackFinishedCount++;\n this.playbackFinishedFuture.resolve();\n this.emit(AudioOutput.EVENT_PLAYBACK_FINISHED, options);\n }\n\n flush(): void {\n this._capturing = false;\n }\n\n /**\n * Clear the buffer, stopping playback immediately\n */\n abstract clearBuffer(): void;\n\n onAttached(): void {\n if (this.nextInChain) {\n this.nextInChain.onAttached();\n }\n }\n\n onDetached(): void {\n if (this.nextInChain) {\n this.nextInChain.onDetached();\n }\n }\n\n /**\n * Pause the audio playback\n */\n pause(): void {\n if (this.nextInChain) {\n this.nextInChain.pause();\n }\n }\n\n /**\n * Resume the audio playback\n */\n resume(): void {\n if (this.nextInChain) {\n this.nextInChain.resume();\n }\n }\n}\n\nexport interface PlaybackFinishedEvent {\n // How much of the audio was played back\n playbackPosition: number;\n // Interrupted is True if playback was interrupted (clearBuffer() was called)\n interrupted: boolean;\n // Transcript synced with playback; may be partial if the audio was interrupted\n // When null, the transcript is not synchronized with the playback\n synchronizedTranscript?: string;\n}\n\nexport abstract class TextOutput {\n constructor(protected readonly nextInChain?: TextOutput) {}\n\n /**\n * Capture a text segment (Used by the output of LLM nodes)\n */\n abstract captureText(text: string): Promise<void>;\n\n /**\n * Mark the current text segment as complete (e.g LLM generation is complete)\n */\n abstract flush(): void;\n\n onAttached(): void {\n if (this.nextInChain) {\n this.nextInChain.onAttached();\n }\n }\n\n onDetached(): void {\n if (this.nextInChain) {\n this.nextInChain.onDetached();\n }\n }\n}\n\nexport class AgentInput {\n private _audioStream: AudioInput | null = null;\n // enabled by default\n private _audioEnabled: boolean = true;\n\n constructor(private readonly audioChanged: () => void) {}\n\n setAudioEnabled(enable: boolean): void {\n if (enable === this._audioEnabled) {\n return;\n }\n\n this._audioEnabled = enable;\n\n if (!this._audioStream) {\n return;\n }\n\n if (enable) {\n this._audioStream.onAttached();\n } else {\n this._audioStream.onDetached();\n }\n }\n\n get audioEnabled(): boolean {\n return this._audioEnabled;\n }\n\n get audio(): AudioInput | null {\n return this._audioStream;\n }\n\n set audio(stream: AudioInput | null) {\n this._audioStream = stream;\n this.audioChanged();\n }\n}\n\nexport class AgentOutput {\n private _audioSink: AudioOutput | null = null;\n private _transcriptionSink: TextOutput | null = null;\n private _audioEnabled: boolean = true;\n private _transcriptionEnabled: boolean = true;\n\n constructor(\n private readonly audioChanged: () => void,\n private readonly transcriptionChanged: () => void,\n ) {}\n\n setAudioEnabled(enabled: boolean): void {\n if (enabled === this._audioEnabled) {\n return;\n }\n\n this._audioEnabled = enabled;\n\n if (!this._audioSink) {\n return;\n }\n\n if (enabled) {\n this._audioSink.onAttached();\n } else {\n this._audioSink.onDetached();\n }\n }\n\n setTranscriptionEnabled(enabled: boolean): void {\n if (enabled === this._transcriptionEnabled) {\n return;\n }\n\n this._transcriptionEnabled = enabled;\n\n if (!this._transcriptionSink) {\n return;\n }\n\n if (enabled) {\n this._transcriptionSink.onAttached();\n } else {\n this._transcriptionSink.onDetached();\n }\n }\n\n get audioEnabled(): boolean {\n return this._audioEnabled;\n }\n\n get transcriptionEnabled(): boolean {\n return this._transcriptionEnabled;\n }\n\n get audio(): AudioOutput | null {\n return this._audioSink;\n }\n\n set audio(sink: AudioOutput | null) {\n if (sink === this._audioSink) {\n return;\n }\n\n if (this._audioSink) {\n this._audioSink.onDetached();\n }\n\n this._audioSink = sink;\n this.audioChanged();\n\n if (this._audioSink) {\n this._audioSink.onAttached();\n }\n }\n\n get transcription(): TextOutput | null {\n return this._transcriptionSink;\n }\n\n set transcription(sink: TextOutput | null) {\n if (sink === this._transcriptionSink) {\n return;\n }\n\n if (this._transcriptionSink) {\n this._transcriptionSink.onDetached();\n }\n\n this._transcriptionSink = sink;\n this.transcriptionChanged();\n\n if (this._transcriptionSink) {\n this._transcriptionSink.onAttached();\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/voice/io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport type { ChatContext } from '../llm/chat_context.js';\nimport type { ChatChunk } from '../llm/llm.js';\nimport type { ToolContext } from '../llm/tool_context.js';\nimport { log } from '../log.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport type { SpeechEvent } from '../stt/stt.js';\nimport { Future } from '../utils.js';\nimport type { ModelSettings } from './agent.js';\n\nexport type STTNode = (\n audio: ReadableStream<AudioFrame>,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<SpeechEvent | string> | null>;\n\nexport type LLMNode = (\n chatCtx: ChatContext,\n toolCtx: ToolContext,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<ChatChunk | string> | null>;\n\nexport type TTSNode = (\n text: ReadableStream<string>,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<AudioFrame> | null>;\n\n/**\n * A string with timing information for word-level alignment.\n */\nexport interface TimedString {\n text: string;\n startTime?: number; // seconds\n endTime?: number; // seconds\n}\n\nexport interface AudioOutputCapabilities {\n /** Whether this output supports pause/resume functionality */\n pause: boolean;\n}\n\nexport abstract class AudioInput {\n protected deferredStream: DeferredReadableStream<AudioFrame> =\n new DeferredReadableStream<AudioFrame>();\n\n get stream(): ReadableStream<AudioFrame> {\n return this.deferredStream.stream;\n }\n\n onAttached(): void {}\n\n onDetached(): void {}\n}\n\nexport abstract class AudioOutput extends EventEmitter {\n static readonly EVENT_PLAYBACK_FINISHED = 'playbackFinished';\n\n private playbackFinishedFuture: Future<void> = new Future();\n private _capturing: boolean = false;\n private playbackFinishedCount: number = 0;\n private playbackSegmentsCount: number = 0;\n private lastPlaybackEvent: PlaybackFinishedEvent = {\n playbackPosition: 0,\n interrupted: false,\n };\n protected logger = log();\n protected readonly capabilities: AudioOutputCapabilities;\n\n constructor(\n public sampleRate?: number,\n protected readonly nextInChain?: AudioOutput,\n capabilities: AudioOutputCapabilities = { pause: false },\n ) {\n super();\n this.capabilities = capabilities;\n if (this.nextInChain) {\n this.nextInChain.on(AudioOutput.EVENT_PLAYBACK_FINISHED, (ev: PlaybackFinishedEvent) =>\n this.onPlaybackFinished(ev),\n );\n }\n }\n\n /**\n * Whether this output and all outputs in the chain support pause/resume.\n */\n get canPause(): boolean {\n return this.capabilities.pause && (this.nextInChain?.canPause ?? true);\n }\n\n /**\n * Capture an audio frame for playback, frames can be pushed faster than real-time\n */\n async captureFrame(_frame: AudioFrame): Promise<void> {\n if (!this._capturing) {\n this._capturing = true;\n this.playbackSegmentsCount++;\n }\n }\n\n /**\n * Wait for the past audio segments to finish playing out.\n *\n * @returns The event that was emitted when the audio finished playing out (only the last segment information)\n */\n async waitForPlayout(): Promise<PlaybackFinishedEvent> {\n const target = this.playbackSegmentsCount;\n\n while (this.playbackFinishedCount < target) {\n await this.playbackFinishedFuture.await;\n this.playbackFinishedFuture = new Future();\n }\n\n return this.lastPlaybackEvent;\n }\n\n /**\n * Developers building audio sinks must call this method when a playback/segment is finished.\n * Segments are segmented by calls to flush() or clearBuffer()\n */\n onPlaybackFinished(options: PlaybackFinishedEvent) {\n if (this.playbackFinishedCount >= this.playbackSegmentsCount) {\n this.logger.warn('playback_finished called more times than playback segments were captured');\n return;\n }\n\n this.lastPlaybackEvent = options;\n this.playbackFinishedCount++;\n this.playbackFinishedFuture.resolve();\n this.emit(AudioOutput.EVENT_PLAYBACK_FINISHED, options);\n }\n\n flush(): void {\n this._capturing = false;\n }\n\n /**\n * Clear the buffer, stopping playback immediately\n */\n abstract clearBuffer(): void;\n\n onAttached(): void {\n if (this.nextInChain) {\n this.nextInChain.onAttached();\n }\n }\n\n onDetached(): void {\n if (this.nextInChain) {\n this.nextInChain.onDetached();\n }\n }\n\n /**\n * Pause the audio playback\n */\n pause(): void {\n if (this.nextInChain) {\n this.nextInChain.pause();\n }\n }\n\n /**\n * Resume the audio playback\n */\n resume(): void {\n if (this.nextInChain) {\n this.nextInChain.resume();\n }\n }\n}\n\nexport interface PlaybackFinishedEvent {\n // How much of the audio was played back\n playbackPosition: number;\n // Interrupted is True if playback was interrupted (clearBuffer() was called)\n interrupted: boolean;\n // Transcript synced with playback; may be partial if the audio was interrupted\n // When null, the transcript is not synchronized with the playback\n synchronizedTranscript?: string;\n}\n\nexport abstract class TextOutput {\n constructor(protected readonly nextInChain?: TextOutput) {}\n\n /**\n * Capture a text segment (Used by the output of LLM nodes)\n */\n abstract captureText(text: string): Promise<void>;\n\n /**\n * Mark the current text segment as complete (e.g LLM generation is complete)\n */\n abstract flush(): void;\n\n onAttached(): void {\n if (this.nextInChain) {\n this.nextInChain.onAttached();\n }\n }\n\n onDetached(): void {\n if (this.nextInChain) {\n this.nextInChain.onDetached();\n }\n }\n}\n\nexport class AgentInput {\n private _audioStream: AudioInput | null = null;\n // enabled by default\n private _audioEnabled: boolean = true;\n\n constructor(private readonly audioChanged: () => void) {}\n\n setAudioEnabled(enable: boolean): void {\n if (enable === this._audioEnabled) {\n return;\n }\n\n this._audioEnabled = enable;\n\n if (!this._audioStream) {\n return;\n }\n\n if (enable) {\n this._audioStream.onAttached();\n } else {\n this._audioStream.onDetached();\n }\n }\n\n get audioEnabled(): boolean {\n return this._audioEnabled;\n }\n\n get audio(): AudioInput | null {\n return this._audioStream;\n }\n\n set audio(stream: AudioInput | null) {\n this._audioStream = stream;\n this.audioChanged();\n }\n}\n\nexport class AgentOutput {\n private _audioSink: AudioOutput | null = null;\n private _transcriptionSink: TextOutput | null = null;\n private _audioEnabled: boolean = true;\n private _transcriptionEnabled: boolean = true;\n\n constructor(\n private readonly audioChanged: () => void,\n private readonly transcriptionChanged: () => void,\n ) {}\n\n setAudioEnabled(enabled: boolean): void {\n if (enabled === this._audioEnabled) {\n return;\n }\n\n this._audioEnabled = enabled;\n\n if (!this._audioSink) {\n return;\n }\n\n if (enabled) {\n this._audioSink.onAttached();\n } else {\n this._audioSink.onDetached();\n }\n }\n\n setTranscriptionEnabled(enabled: boolean): void {\n if (enabled === this._transcriptionEnabled) {\n return;\n }\n\n this._transcriptionEnabled = enabled;\n\n if (!this._transcriptionSink) {\n return;\n }\n\n if (enabled) {\n this._transcriptionSink.onAttached();\n } else {\n this._transcriptionSink.onDetached();\n }\n }\n\n get audioEnabled(): boolean {\n return this._audioEnabled;\n }\n\n get transcriptionEnabled(): boolean {\n return this._transcriptionEnabled;\n }\n\n get audio(): AudioOutput | null {\n return this._audioSink;\n }\n\n set audio(sink: AudioOutput | null) {\n if (sink === this._audioSink) {\n return;\n }\n\n if (this._audioSink) {\n this._audioSink.onDetached();\n }\n\n this._audioSink = sink;\n this.audioChanged();\n\n if (this._audioSink) {\n this._audioSink.onAttached();\n }\n }\n\n get transcription(): TextOutput | null {\n return this._transcriptionSink;\n }\n\n set transcription(sink: TextOutput | null) {\n if (sink === this._transcriptionSink) {\n return;\n }\n\n if (this._transcriptionSink) {\n this._transcriptionSink.onDetached();\n }\n\n this._transcriptionSink = sink;\n this.transcriptionChanged();\n\n if (this._transcriptionSink) {\n this._transcriptionSink.onAttached();\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/voice/io.d.cts
CHANGED
|
@@ -12,6 +12,18 @@ import type { ModelSettings } from './agent.js';
|
|
|
12
12
|
export type STTNode = (audio: ReadableStream<AudioFrame>, modelSettings: ModelSettings) => Promise<ReadableStream<SpeechEvent | string> | null>;
|
|
13
13
|
export type LLMNode = (chatCtx: ChatContext, toolCtx: ToolContext, modelSettings: ModelSettings) => Promise<ReadableStream<ChatChunk | string> | null>;
|
|
14
14
|
export type TTSNode = (text: ReadableStream<string>, modelSettings: ModelSettings) => Promise<ReadableStream<AudioFrame> | null>;
|
|
15
|
+
/**
|
|
16
|
+
* A string with timing information for word-level alignment.
|
|
17
|
+
*/
|
|
18
|
+
export interface TimedString {
|
|
19
|
+
text: string;
|
|
20
|
+
startTime?: number;
|
|
21
|
+
endTime?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface AudioOutputCapabilities {
|
|
24
|
+
/** Whether this output supports pause/resume functionality */
|
|
25
|
+
pause: boolean;
|
|
26
|
+
}
|
|
15
27
|
export declare abstract class AudioInput {
|
|
16
28
|
protected deferredStream: DeferredReadableStream<AudioFrame>;
|
|
17
29
|
get stream(): ReadableStream<AudioFrame>;
|
|
@@ -28,7 +40,12 @@ export declare abstract class AudioOutput extends EventEmitter {
|
|
|
28
40
|
private playbackSegmentsCount;
|
|
29
41
|
private lastPlaybackEvent;
|
|
30
42
|
protected logger: import("pino").Logger;
|
|
31
|
-
|
|
43
|
+
protected readonly capabilities: AudioOutputCapabilities;
|
|
44
|
+
constructor(sampleRate?: number | undefined, nextInChain?: AudioOutput | undefined, capabilities?: AudioOutputCapabilities);
|
|
45
|
+
/**
|
|
46
|
+
* Whether this output and all outputs in the chain support pause/resume.
|
|
47
|
+
*/
|
|
48
|
+
get canPause(): boolean;
|
|
32
49
|
/**
|
|
33
50
|
* Capture an audio frame for playback, frames can be pushed faster than real-time
|
|
34
51
|
*/
|
package/dist/voice/io.d.ts
CHANGED
|
@@ -12,6 +12,18 @@ import type { ModelSettings } from './agent.js';
|
|
|
12
12
|
export type STTNode = (audio: ReadableStream<AudioFrame>, modelSettings: ModelSettings) => Promise<ReadableStream<SpeechEvent | string> | null>;
|
|
13
13
|
export type LLMNode = (chatCtx: ChatContext, toolCtx: ToolContext, modelSettings: ModelSettings) => Promise<ReadableStream<ChatChunk | string> | null>;
|
|
14
14
|
export type TTSNode = (text: ReadableStream<string>, modelSettings: ModelSettings) => Promise<ReadableStream<AudioFrame> | null>;
|
|
15
|
+
/**
|
|
16
|
+
* A string with timing information for word-level alignment.
|
|
17
|
+
*/
|
|
18
|
+
export interface TimedString {
|
|
19
|
+
text: string;
|
|
20
|
+
startTime?: number;
|
|
21
|
+
endTime?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface AudioOutputCapabilities {
|
|
24
|
+
/** Whether this output supports pause/resume functionality */
|
|
25
|
+
pause: boolean;
|
|
26
|
+
}
|
|
15
27
|
export declare abstract class AudioInput {
|
|
16
28
|
protected deferredStream: DeferredReadableStream<AudioFrame>;
|
|
17
29
|
get stream(): ReadableStream<AudioFrame>;
|
|
@@ -28,7 +40,12 @@ export declare abstract class AudioOutput extends EventEmitter {
|
|
|
28
40
|
private playbackSegmentsCount;
|
|
29
41
|
private lastPlaybackEvent;
|
|
30
42
|
protected logger: import("pino").Logger;
|
|
31
|
-
|
|
43
|
+
protected readonly capabilities: AudioOutputCapabilities;
|
|
44
|
+
constructor(sampleRate?: number | undefined, nextInChain?: AudioOutput | undefined, capabilities?: AudioOutputCapabilities);
|
|
45
|
+
/**
|
|
46
|
+
* Whether this output and all outputs in the chain support pause/resume.
|
|
47
|
+
*/
|
|
48
|
+
get canPause(): boolean;
|
|
32
49
|
/**
|
|
33
50
|
* Capture an audio frame for playback, frames can be pushed faster than real-time
|
|
34
51
|
*/
|
package/dist/voice/io.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../src/voice/io.ts"],"names":[],"mappings":";;AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,OAAO,GAAG,CACpB,KAAK,EAAE,cAAc,CAAC,UAAU,CAAC,EACjC,aAAa,EAAE,aAAa,KACzB,OAAO,CAAC,cAAc,CAAC,WAAW,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;AAE1D,MAAM,MAAM,OAAO,GAAG,CACpB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,EACpB,aAAa,EAAE,aAAa,KACzB,OAAO,CAAC,cAAc,CAAC,SAAS,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;AAExD,MAAM,MAAM,OAAO,GAAG,CACpB,IAAI,EAAE,cAAc,CAAC,MAAM,CAAC,EAC5B,aAAa,EAAE,aAAa,KACzB,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC;AAEhD,8BAAsB,UAAU;IAC9B,SAAS,CAAC,cAAc,EAAE,sBAAsB,CAAC,UAAU,CAAC,CACjB;IAE3C,IAAI,MAAM,IAAI,cAAc,CAAC,UAAU,CAAC,CAEvC;IAED,UAAU,IAAI,IAAI;IAElB,UAAU,IAAI,IAAI;CACnB;AAED,8BAAsB,WAAY,SAAQ,YAAY;
|
|
1
|
+
{"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../src/voice/io.ts"],"names":[],"mappings":"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"}
|
package/dist/voice/io.js
CHANGED
|
@@ -13,10 +13,11 @@ class AudioInput {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
class AudioOutput extends EventEmitter {
|
|
16
|
-
constructor(sampleRate, nextInChain) {
|
|
16
|
+
constructor(sampleRate, nextInChain, capabilities = { pause: false }) {
|
|
17
17
|
super();
|
|
18
18
|
this.sampleRate = sampleRate;
|
|
19
19
|
this.nextInChain = nextInChain;
|
|
20
|
+
this.capabilities = capabilities;
|
|
20
21
|
if (this.nextInChain) {
|
|
21
22
|
this.nextInChain.on(
|
|
22
23
|
AudioOutput.EVENT_PLAYBACK_FINISHED,
|
|
@@ -34,6 +35,14 @@ class AudioOutput extends EventEmitter {
|
|
|
34
35
|
interrupted: false
|
|
35
36
|
};
|
|
36
37
|
logger = log();
|
|
38
|
+
capabilities;
|
|
39
|
+
/**
|
|
40
|
+
* Whether this output and all outputs in the chain support pause/resume.
|
|
41
|
+
*/
|
|
42
|
+
get canPause() {
|
|
43
|
+
var _a;
|
|
44
|
+
return this.capabilities.pause && (((_a = this.nextInChain) == null ? void 0 : _a.canPause) ?? true);
|
|
45
|
+
}
|
|
37
46
|
/**
|
|
38
47
|
* Capture an audio frame for playback, frames can be pushed faster than real-time
|
|
39
48
|
*/
|
package/dist/voice/io.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/voice/io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport type { ChatContext } from '../llm/chat_context.js';\nimport type { ChatChunk } from '../llm/llm.js';\nimport type { ToolContext } from '../llm/tool_context.js';\nimport { log } from '../log.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport type { SpeechEvent } from '../stt/stt.js';\nimport { Future } from '../utils.js';\nimport type { ModelSettings } from './agent.js';\n\nexport type STTNode = (\n audio: ReadableStream<AudioFrame>,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<SpeechEvent | string> | null>;\n\nexport type LLMNode = (\n chatCtx: ChatContext,\n toolCtx: ToolContext,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<ChatChunk | string> | null>;\n\nexport type TTSNode = (\n text: ReadableStream<string>,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<AudioFrame> | null>;\n\nexport abstract class AudioInput {\n protected deferredStream: DeferredReadableStream<AudioFrame> =\n new DeferredReadableStream<AudioFrame>();\n\n get stream(): ReadableStream<AudioFrame> {\n return this.deferredStream.stream;\n }\n\n onAttached(): void {}\n\n onDetached(): void {}\n}\n\nexport abstract class AudioOutput extends EventEmitter {\n static readonly EVENT_PLAYBACK_FINISHED = 'playbackFinished';\n\n private playbackFinishedFuture: Future<void> = new Future();\n private _capturing: boolean = false;\n private playbackFinishedCount: number = 0;\n private playbackSegmentsCount: number = 0;\n private lastPlaybackEvent: PlaybackFinishedEvent = {\n playbackPosition: 0,\n interrupted: false,\n };\n protected logger = log();\n\n constructor(\n public sampleRate?: number,\n protected readonly nextInChain?: AudioOutput,\n ) {\n super();\n if (this.nextInChain) {\n this.nextInChain.on(AudioOutput.EVENT_PLAYBACK_FINISHED, (ev: PlaybackFinishedEvent) =>\n this.onPlaybackFinished(ev),\n );\n }\n }\n\n /**\n * Capture an audio frame for playback, frames can be pushed faster than real-time\n */\n async captureFrame(_frame: AudioFrame): Promise<void> {\n if (!this._capturing) {\n this._capturing = true;\n this.playbackSegmentsCount++;\n }\n }\n\n /**\n * Wait for the past audio segments to finish playing out.\n *\n * @returns The event that was emitted when the audio finished playing out (only the last segment information)\n */\n async waitForPlayout(): Promise<PlaybackFinishedEvent> {\n const target = this.playbackSegmentsCount;\n\n while (this.playbackFinishedCount < target) {\n await this.playbackFinishedFuture.await;\n this.playbackFinishedFuture = new Future();\n }\n\n return this.lastPlaybackEvent;\n }\n\n /**\n * Developers building audio sinks must call this method when a playback/segment is finished.\n * Segments are segmented by calls to flush() or clearBuffer()\n */\n onPlaybackFinished(options: PlaybackFinishedEvent) {\n if (this.playbackFinishedCount >= this.playbackSegmentsCount) {\n this.logger.warn('playback_finished called more times than playback segments were captured');\n return;\n }\n\n this.lastPlaybackEvent = options;\n this.playbackFinishedCount++;\n this.playbackFinishedFuture.resolve();\n this.emit(AudioOutput.EVENT_PLAYBACK_FINISHED, options);\n }\n\n flush(): void {\n this._capturing = false;\n }\n\n /**\n * Clear the buffer, stopping playback immediately\n */\n abstract clearBuffer(): void;\n\n onAttached(): void {\n if (this.nextInChain) {\n this.nextInChain.onAttached();\n }\n }\n\n onDetached(): void {\n if (this.nextInChain) {\n this.nextInChain.onDetached();\n }\n }\n\n /**\n * Pause the audio playback\n */\n pause(): void {\n if (this.nextInChain) {\n this.nextInChain.pause();\n }\n }\n\n /**\n * Resume the audio playback\n */\n resume(): void {\n if (this.nextInChain) {\n this.nextInChain.resume();\n }\n }\n}\n\nexport interface PlaybackFinishedEvent {\n // How much of the audio was played back\n playbackPosition: number;\n // Interrupted is True if playback was interrupted (clearBuffer() was called)\n interrupted: boolean;\n // Transcript synced with playback; may be partial if the audio was interrupted\n // When null, the transcript is not synchronized with the playback\n synchronizedTranscript?: string;\n}\n\nexport abstract class TextOutput {\n constructor(protected readonly nextInChain?: TextOutput) {}\n\n /**\n * Capture a text segment (Used by the output of LLM nodes)\n */\n abstract captureText(text: string): Promise<void>;\n\n /**\n * Mark the current text segment as complete (e.g LLM generation is complete)\n */\n abstract flush(): void;\n\n onAttached(): void {\n if (this.nextInChain) {\n this.nextInChain.onAttached();\n }\n }\n\n onDetached(): void {\n if (this.nextInChain) {\n this.nextInChain.onDetached();\n }\n }\n}\n\nexport class AgentInput {\n private _audioStream: AudioInput | null = null;\n // enabled by default\n private _audioEnabled: boolean = true;\n\n constructor(private readonly audioChanged: () => void) {}\n\n setAudioEnabled(enable: boolean): void {\n if (enable === this._audioEnabled) {\n return;\n }\n\n this._audioEnabled = enable;\n\n if (!this._audioStream) {\n return;\n }\n\n if (enable) {\n this._audioStream.onAttached();\n } else {\n this._audioStream.onDetached();\n }\n }\n\n get audioEnabled(): boolean {\n return this._audioEnabled;\n }\n\n get audio(): AudioInput | null {\n return this._audioStream;\n }\n\n set audio(stream: AudioInput | null) {\n this._audioStream = stream;\n this.audioChanged();\n }\n}\n\nexport class AgentOutput {\n private _audioSink: AudioOutput | null = null;\n private _transcriptionSink: TextOutput | null = null;\n private _audioEnabled: boolean = true;\n private _transcriptionEnabled: boolean = true;\n\n constructor(\n private readonly audioChanged: () => void,\n private readonly transcriptionChanged: () => void,\n ) {}\n\n setAudioEnabled(enabled: boolean): void {\n if (enabled === this._audioEnabled) {\n return;\n }\n\n this._audioEnabled = enabled;\n\n if (!this._audioSink) {\n return;\n }\n\n if (enabled) {\n this._audioSink.onAttached();\n } else {\n this._audioSink.onDetached();\n }\n }\n\n setTranscriptionEnabled(enabled: boolean): void {\n if (enabled === this._transcriptionEnabled) {\n return;\n }\n\n this._transcriptionEnabled = enabled;\n\n if (!this._transcriptionSink) {\n return;\n }\n\n if (enabled) {\n this._transcriptionSink.onAttached();\n } else {\n this._transcriptionSink.onDetached();\n }\n }\n\n get audioEnabled(): boolean {\n return this._audioEnabled;\n }\n\n get transcriptionEnabled(): boolean {\n return this._transcriptionEnabled;\n }\n\n get audio(): AudioOutput | null {\n return this._audioSink;\n }\n\n set audio(sink: AudioOutput | null) {\n if (sink === this._audioSink) {\n return;\n }\n\n if (this._audioSink) {\n this._audioSink.onDetached();\n }\n\n this._audioSink = sink;\n this.audioChanged();\n\n if (this._audioSink) {\n this._audioSink.onAttached();\n }\n }\n\n get transcription(): TextOutput | null {\n return this._transcriptionSink;\n }\n\n set transcription(sink: TextOutput | null) {\n if (sink === this._transcriptionSink) {\n return;\n }\n\n if (this._transcriptionSink) {\n this._transcriptionSink.onDetached();\n }\n\n this._transcriptionSink = sink;\n this.transcriptionChanged();\n\n if (this._transcriptionSink) {\n this._transcriptionSink.onAttached();\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/voice/io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport type { ChatContext } from '../llm/chat_context.js';\nimport type { ChatChunk } from '../llm/llm.js';\nimport type { ToolContext } from '../llm/tool_context.js';\nimport { log } from '../log.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport type { SpeechEvent } from '../stt/stt.js';\nimport { Future } from '../utils.js';\nimport type { ModelSettings } from './agent.js';\n\nexport type STTNode = (\n audio: ReadableStream<AudioFrame>,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<SpeechEvent | string> | null>;\n\nexport type LLMNode = (\n chatCtx: ChatContext,\n toolCtx: ToolContext,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<ChatChunk | string> | null>;\n\nexport type TTSNode = (\n text: ReadableStream<string>,\n modelSettings: ModelSettings,\n) => Promise<ReadableStream<AudioFrame> | null>;\n\n/**\n * A string with timing information for word-level alignment.\n */\nexport interface TimedString {\n text: string;\n startTime?: number; // seconds\n endTime?: number; // seconds\n}\n\nexport interface AudioOutputCapabilities {\n /** Whether this output supports pause/resume functionality */\n pause: boolean;\n}\n\nexport abstract class AudioInput {\n protected deferredStream: DeferredReadableStream<AudioFrame> =\n new DeferredReadableStream<AudioFrame>();\n\n get stream(): ReadableStream<AudioFrame> {\n return this.deferredStream.stream;\n }\n\n onAttached(): void {}\n\n onDetached(): void {}\n}\n\nexport abstract class AudioOutput extends EventEmitter {\n static readonly EVENT_PLAYBACK_FINISHED = 'playbackFinished';\n\n private playbackFinishedFuture: Future<void> = new Future();\n private _capturing: boolean = false;\n private playbackFinishedCount: number = 0;\n private playbackSegmentsCount: number = 0;\n private lastPlaybackEvent: PlaybackFinishedEvent = {\n playbackPosition: 0,\n interrupted: false,\n };\n protected logger = log();\n protected readonly capabilities: AudioOutputCapabilities;\n\n constructor(\n public sampleRate?: number,\n protected readonly nextInChain?: AudioOutput,\n capabilities: AudioOutputCapabilities = { pause: false },\n ) {\n super();\n this.capabilities = capabilities;\n if (this.nextInChain) {\n this.nextInChain.on(AudioOutput.EVENT_PLAYBACK_FINISHED, (ev: PlaybackFinishedEvent) =>\n this.onPlaybackFinished(ev),\n );\n }\n }\n\n /**\n * Whether this output and all outputs in the chain support pause/resume.\n */\n get canPause(): boolean {\n return this.capabilities.pause && (this.nextInChain?.canPause ?? true);\n }\n\n /**\n * Capture an audio frame for playback, frames can be pushed faster than real-time\n */\n async captureFrame(_frame: AudioFrame): Promise<void> {\n if (!this._capturing) {\n this._capturing = true;\n this.playbackSegmentsCount++;\n }\n }\n\n /**\n * Wait for the past audio segments to finish playing out.\n *\n * @returns The event that was emitted when the audio finished playing out (only the last segment information)\n */\n async waitForPlayout(): Promise<PlaybackFinishedEvent> {\n const target = this.playbackSegmentsCount;\n\n while (this.playbackFinishedCount < target) {\n await this.playbackFinishedFuture.await;\n this.playbackFinishedFuture = new Future();\n }\n\n return this.lastPlaybackEvent;\n }\n\n /**\n * Developers building audio sinks must call this method when a playback/segment is finished.\n * Segments are segmented by calls to flush() or clearBuffer()\n */\n onPlaybackFinished(options: PlaybackFinishedEvent) {\n if (this.playbackFinishedCount >= this.playbackSegmentsCount) {\n this.logger.warn('playback_finished called more times than playback segments were captured');\n return;\n }\n\n this.lastPlaybackEvent = options;\n this.playbackFinishedCount++;\n this.playbackFinishedFuture.resolve();\n this.emit(AudioOutput.EVENT_PLAYBACK_FINISHED, options);\n }\n\n flush(): void {\n this._capturing = false;\n }\n\n /**\n * Clear the buffer, stopping playback immediately\n */\n abstract clearBuffer(): void;\n\n onAttached(): void {\n if (this.nextInChain) {\n this.nextInChain.onAttached();\n }\n }\n\n onDetached(): void {\n if (this.nextInChain) {\n this.nextInChain.onDetached();\n }\n }\n\n /**\n * Pause the audio playback\n */\n pause(): void {\n if (this.nextInChain) {\n this.nextInChain.pause();\n }\n }\n\n /**\n * Resume the audio playback\n */\n resume(): void {\n if (this.nextInChain) {\n this.nextInChain.resume();\n }\n }\n}\n\nexport interface PlaybackFinishedEvent {\n // How much of the audio was played back\n playbackPosition: number;\n // Interrupted is True if playback was interrupted (clearBuffer() was called)\n interrupted: boolean;\n // Transcript synced with playback; may be partial if the audio was interrupted\n // When null, the transcript is not synchronized with the playback\n synchronizedTranscript?: string;\n}\n\nexport abstract class TextOutput {\n constructor(protected readonly nextInChain?: TextOutput) {}\n\n /**\n * Capture a text segment (Used by the output of LLM nodes)\n */\n abstract captureText(text: string): Promise<void>;\n\n /**\n * Mark the current text segment as complete (e.g LLM generation is complete)\n */\n abstract flush(): void;\n\n onAttached(): void {\n if (this.nextInChain) {\n this.nextInChain.onAttached();\n }\n }\n\n onDetached(): void {\n if (this.nextInChain) {\n this.nextInChain.onDetached();\n }\n }\n}\n\nexport class AgentInput {\n private _audioStream: AudioInput | null = null;\n // enabled by default\n private _audioEnabled: boolean = true;\n\n constructor(private readonly audioChanged: () => void) {}\n\n setAudioEnabled(enable: boolean): void {\n if (enable === this._audioEnabled) {\n return;\n }\n\n this._audioEnabled = enable;\n\n if (!this._audioStream) {\n return;\n }\n\n if (enable) {\n this._audioStream.onAttached();\n } else {\n this._audioStream.onDetached();\n }\n }\n\n get audioEnabled(): boolean {\n return this._audioEnabled;\n }\n\n get audio(): AudioInput | null {\n return this._audioStream;\n }\n\n set audio(stream: AudioInput | null) {\n this._audioStream = stream;\n this.audioChanged();\n }\n}\n\nexport class AgentOutput {\n private _audioSink: AudioOutput | null = null;\n private _transcriptionSink: TextOutput | null = null;\n private _audioEnabled: boolean = true;\n private _transcriptionEnabled: boolean = true;\n\n constructor(\n private readonly audioChanged: () => void,\n private readonly transcriptionChanged: () => void,\n ) {}\n\n setAudioEnabled(enabled: boolean): void {\n if (enabled === this._audioEnabled) {\n return;\n }\n\n this._audioEnabled = enabled;\n\n if (!this._audioSink) {\n return;\n }\n\n if (enabled) {\n this._audioSink.onAttached();\n } else {\n this._audioSink.onDetached();\n }\n }\n\n setTranscriptionEnabled(enabled: boolean): void {\n if (enabled === this._transcriptionEnabled) {\n return;\n }\n\n this._transcriptionEnabled = enabled;\n\n if (!this._transcriptionSink) {\n return;\n }\n\n if (enabled) {\n this._transcriptionSink.onAttached();\n } else {\n this._transcriptionSink.onDetached();\n }\n }\n\n get audioEnabled(): boolean {\n return this._audioEnabled;\n }\n\n get transcriptionEnabled(): boolean {\n return this._transcriptionEnabled;\n }\n\n get audio(): AudioOutput | null {\n return this._audioSink;\n }\n\n set audio(sink: AudioOutput | null) {\n if (sink === this._audioSink) {\n return;\n }\n\n if (this._audioSink) {\n this._audioSink.onDetached();\n }\n\n this._audioSink = sink;\n this.audioChanged();\n\n if (this._audioSink) {\n this._audioSink.onAttached();\n }\n }\n\n get transcription(): TextOutput | null {\n return this._transcriptionSink;\n }\n\n set transcription(sink: TextOutput | null) {\n if (sink === this._transcriptionSink) {\n return;\n }\n\n if (this._transcriptionSink) {\n this._transcriptionSink.onDetached();\n }\n\n this._transcriptionSink = sink;\n this.transcriptionChanged();\n\n if (this._transcriptionSink) {\n this._transcriptionSink.onAttached();\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -370,7 +370,7 @@ class RecorderAudioOutput extends import_io.AudioOutput {
|
|
|
370
370
|
pauseWallTimes = [];
|
|
371
371
|
// [start, end] pairs
|
|
372
372
|
constructor(recorderIO, audioOutput, writeFn) {
|
|
373
|
-
super(audioOutput.sampleRate, audioOutput);
|
|
373
|
+
super(audioOutput.sampleRate, audioOutput, { pause: true });
|
|
374
374
|
this.recorderIO = recorderIO;
|
|
375
375
|
this.writeFn = writeFn;
|
|
376
376
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/voice/recorder_io/recorder_io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport ffmpegInstaller from '@ffmpeg-installer/ffmpeg';\nimport { Mutex } from '@livekit/mutex';\nimport { AudioFrame, AudioResampler } from '@livekit/rtc-node';\nimport ffmpeg from 'fluent-ffmpeg';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { PassThrough } from 'node:stream';\nimport type { ReadableStream } from 'node:stream/web';\nimport { TransformStream } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { isStreamReaderReleaseError } from '../../stream/deferred_stream.js';\nimport { type StreamChannel, createStreamChannel } from '../../stream/stream_channel.js';\nimport { Future, Task, cancelAndWait, delay } from '../../utils.js';\nimport type { AgentSession } from '../agent_session.js';\nimport { AudioInput, AudioOutput, type PlaybackFinishedEvent } from '../io.js';\n\nffmpeg.setFfmpegPath(ffmpegInstaller.path);\n\nconst WRITE_INTERVAL_MS = 2500;\nconst DEFAULT_SAMPLE_RATE = 48000;\n\nexport interface RecorderOptions {\n agentSession: AgentSession;\n sampleRate?: number;\n}\n\ninterface ResampleAndMixOptions {\n frames: AudioFrame[];\n resampler: AudioResampler | undefined;\n flush?: boolean;\n}\n\nexport class RecorderIO {\n private inRecord?: RecorderAudioInput;\n private outRecord?: RecorderAudioOutput;\n\n private inChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n private outChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n\n private session: AgentSession;\n private sampleRate: number;\n\n private _outputPath?: string;\n private forwardTask?: Task<void>;\n private encodeTask?: Task<void>;\n\n private closeFuture: Future<void> = new Future();\n private lock: Mutex = new Mutex();\n private started: boolean = false;\n\n // FFmpeg streaming state\n private pcmStream?: PassThrough;\n private ffmpegPromise?: Promise<void>;\n private inResampler?: AudioResampler;\n private outResampler?: AudioResampler;\n\n private logger = log();\n\n constructor(opts: RecorderOptions) {\n const { agentSession, sampleRate = DEFAULT_SAMPLE_RATE } = opts;\n\n this.session = agentSession;\n this.sampleRate = sampleRate;\n }\n\n async start(outputPath: string): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (this.started) return;\n\n if (!this.inRecord || !this.outRecord) {\n throw new Error(\n 'RecorderIO not properly initialized: both `recordInput()` and `recordOutput()` must be called before starting the recorder.',\n );\n }\n\n this._outputPath = outputPath;\n this.started = true;\n this.closeFuture = new Future();\n\n // Ensure output directory exists\n const dir = path.dirname(outputPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n this.forwardTask = Task.from(({ signal }) => this.forward(signal));\n this.encodeTask = Task.from(() => this.encode(), undefined, 'recorder_io_encode_task');\n } finally {\n unlock();\n }\n }\n\n async close(): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (!this.started) return;\n\n await this.inChan.close();\n await this.outChan.close();\n await this.closeFuture.await;\n await cancelAndWait([this.forwardTask!, this.encodeTask!]);\n\n this.started = false;\n } finally {\n unlock();\n }\n }\n\n recordInput(audioInput: AudioInput): RecorderAudioInput {\n this.inRecord = new RecorderAudioInput(this, audioInput);\n return this.inRecord;\n }\n\n recordOutput(audioOutput: AudioOutput): RecorderAudioOutput {\n this.outRecord = new RecorderAudioOutput(this, audioOutput, (buf) => this.writeCb(buf));\n return this.outRecord;\n }\n\n private writeCb(buf: AudioFrame[]): void {\n const inputBuf = this.inRecord!.takeBuf();\n this.inChan.write(inputBuf);\n this.outChan.write(buf);\n }\n\n get recording(): boolean {\n return this.started;\n }\n\n get outputPath(): string | undefined {\n return this._outputPath;\n }\n\n get recordingStartedAt(): number | undefined {\n // Use session start time to align with trace timestamps\n return this.session._startedAt;\n }\n\n /**\n * Forward task: periodically flush input buffer to encoder\n */\n private async forward(signal: AbortSignal): Promise<void> {\n while (!signal.aborted) {\n try {\n await delay(WRITE_INTERVAL_MS, { signal });\n } catch {\n // Aborted\n break;\n }\n\n if (this.outRecord!.hasPendingData) {\n // If the output is currently playing audio, wait for it to stay in sync\n continue;\n }\n\n // Flush input buffer\n const inputBuf = this.inRecord!.takeBuf();\n this.inChan\n .write(inputBuf)\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO input buffer'));\n this.outChan\n .write([])\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO output buffer'));\n }\n }\n\n /**\n * Start FFmpeg process for streaming encoding\n */\n private startFFmpeg(): void {\n if (this.pcmStream) return;\n\n this.pcmStream = new PassThrough();\n\n this.ffmpegPromise = new Promise<void>((resolve, reject) => {\n ffmpeg(this.pcmStream!)\n .inputFormat('s16le')\n .inputOptions([`-ar ${this.sampleRate}`, '-ac 2'])\n .audioCodec('libopus')\n .audioChannels(2)\n .audioFrequency(this.sampleRate)\n .format('ogg')\n .output(this._outputPath!)\n .on('end', () => {\n this.logger.debug('FFmpeg encoding finished');\n resolve();\n })\n .on('error', (err) => {\n // Ignore errors from intentional stream closure or SIGINT during shutdown\n if (\n err.message?.includes('Output stream closed') ||\n err.message?.includes('received signal 2') ||\n err.message?.includes('SIGKILL') ||\n err.message?.includes('SIGINT')\n ) {\n resolve();\n } else {\n this.logger.error({ err }, 'FFmpeg encoding error');\n reject(err);\n }\n })\n .run();\n });\n }\n\n /**\n * Resample and mix frames to mono Float32\n */\n private resampleAndMix(opts: ResampleAndMixOptions): {\n samples: Float32Array;\n resampler: AudioResampler | undefined;\n } {\n const INV_INT16 = 1.0 / 32768.0;\n const { frames, flush = false } = opts;\n let { resampler } = opts;\n\n if (frames.length === 0 && !flush) {\n return { samples: new Float32Array(0), resampler };\n }\n\n if (!resampler && frames.length > 0) {\n const firstFrame = frames[0]!;\n resampler = new AudioResampler(firstFrame.sampleRate, this.sampleRate, firstFrame.channels);\n }\n\n const resampledFrames: AudioFrame[] = [];\n for (const frame of frames) {\n if (resampler) {\n resampledFrames.push(...resampler.push(frame));\n }\n }\n\n if (flush && resampler) {\n resampledFrames.push(...resampler.flush());\n }\n\n const totalSamples = resampledFrames.reduce((acc, frame) => acc + frame.samplesPerChannel, 0);\n const samples = new Float32Array(totalSamples);\n\n let pos = 0;\n for (const frame of resampledFrames) {\n const data = frame.data;\n const numChannels = frame.channels;\n for (let i = 0; i < frame.samplesPerChannel; i++) {\n let sum = 0;\n for (let ch = 0; ch < numChannels; ch++) {\n sum += data[i * numChannels + ch]!;\n }\n samples[pos++] = (sum / numChannels) * INV_INT16;\n }\n }\n\n return { samples, resampler };\n }\n\n /**\n * Write PCM chunk to FFmpeg stream\n */\n private writePCM(leftSamples: Float32Array, rightSamples: Float32Array): void {\n if (!this.pcmStream) {\n this.startFFmpeg();\n }\n\n // Handle length mismatch by prepending silence\n if (leftSamples.length !== rightSamples.length) {\n const diff = Math.abs(leftSamples.length - rightSamples.length);\n if (leftSamples.length < rightSamples.length) {\n this.logger.warn(\n `Input is shorter by ${diff} samples; silence has been prepended to align the input channel.`,\n );\n const padded = new Float32Array(rightSamples.length);\n padded.set(leftSamples, diff);\n leftSamples = padded;\n } else {\n const padded = new Float32Array(leftSamples.length);\n padded.set(rightSamples, diff);\n rightSamples = padded;\n }\n }\n\n const maxLen = Math.max(leftSamples.length, rightSamples.length);\n if (maxLen <= 0) return;\n\n // Interleave stereo samples and convert back to Int16\n const stereoData = new Int16Array(maxLen * 2);\n for (let i = 0; i < maxLen; i++) {\n stereoData[i * 2] = Math.max(\n -32768,\n Math.min(32767, Math.round((leftSamples[i] ?? 0) * 32768)),\n );\n stereoData[i * 2 + 1] = Math.max(\n -32768,\n Math.min(32767, Math.round((rightSamples[i] ?? 0) * 32768)),\n );\n }\n\n this.pcmStream!.write(Buffer.from(stereoData.buffer));\n }\n\n /**\n * Encode task: read from channels, mix to stereo, stream to FFmpeg\n */\n private async encode(): Promise<void> {\n if (!this._outputPath) return;\n\n const inReader = this.inChan.stream().getReader();\n const outReader = this.outChan.stream().getReader();\n\n try {\n while (true) {\n const [inResult, outResult] = await Promise.all([inReader.read(), outReader.read()]);\n\n if (inResult.done || outResult.done) {\n break;\n }\n\n const inputBuf = inResult.value;\n const outputBuf = outResult.value;\n\n const inMixed = this.resampleAndMix({ frames: inputBuf, resampler: this.inResampler });\n this.inResampler = inMixed.resampler;\n\n const outMixed = this.resampleAndMix({\n frames: outputBuf,\n resampler: this.outResampler,\n flush: outputBuf.length > 0,\n });\n this.outResampler = outMixed.resampler;\n\n // Stream PCM data directly to FFmpeg\n this.writePCM(inMixed.samples, outMixed.samples);\n }\n\n // Close FFmpeg stream and wait for encoding to complete\n if (this.pcmStream) {\n this.pcmStream.end();\n await this.ffmpegPromise;\n }\n } catch (err) {\n this.logger.error({ err }, 'Error in encode task');\n } finally {\n inReader.releaseLock();\n outReader.releaseLock();\n\n if (!this.closeFuture.done) {\n this.closeFuture.resolve();\n }\n }\n }\n}\n\nclass RecorderAudioInput extends AudioInput {\n private source: AudioInput;\n private recorderIO: RecorderIO;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n\n constructor(recorderIO: RecorderIO, source: AudioInput) {\n super();\n this.recorderIO = recorderIO;\n this.source = source;\n\n // Set up the intercepting stream\n this.deferredStream.setSource(this.createInterceptingStream());\n }\n\n /**\n * Wall-clock time when the first frame was captured\n */\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n /**\n * Take accumulated frames and clear the buffer\n */\n takeBuf(): AudioFrame[] {\n const frames = this.accFrames;\n this.accFrames = [];\n return frames;\n }\n\n /**\n * Creates a stream that intercepts frames from the source,\n * accumulates them when recording, and passes them through unchanged.\n */\n private createInterceptingStream(): ReadableStream<AudioFrame> {\n const sourceStream = this.source.stream;\n const reader = sourceStream.getReader();\n\n const transform = new TransformStream<AudioFrame, AudioFrame>({\n transform: (frame, controller) => {\n // Accumulate frames when recording is active\n if (this.recorderIO.recording) {\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n this.accFrames.push(frame);\n }\n\n controller.enqueue(frame);\n },\n });\n\n const pump = async () => {\n const writer = transform.writable.getWriter();\n let sourceError: unknown;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n await writer.write(value);\n }\n } catch (e) {\n if (isStreamReaderReleaseError(e)) return;\n sourceError = e;\n } finally {\n if (sourceError) {\n writer.abort(sourceError);\n return;\n }\n\n writer.releaseLock();\n\n try {\n await transform.writable.close();\n } catch {\n // ignore \"WritableStream is closed\" errors\n }\n }\n };\n\n pump();\n\n return transform.readable;\n }\n\n onAttached(): void {\n this.source.onAttached();\n }\n\n onDetached(): void {\n this.source.onDetached();\n }\n}\n\nclass RecorderAudioOutput extends AudioOutput {\n private recorderIO: RecorderIO;\n private writeFn: (buf: AudioFrame[]) => void;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n\n // Pause tracking\n private currentPauseStart?: number;\n private pauseWallTimes: Array<[number, number]> = []; // [start, end] pairs\n\n constructor(\n recorderIO: RecorderIO,\n audioOutput: AudioOutput,\n writeFn: (buf: AudioFrame[]) => void,\n ) {\n super(audioOutput.sampleRate, audioOutput);\n this.recorderIO = recorderIO;\n this.writeFn = writeFn;\n }\n\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n get hasPendingData(): boolean {\n return this.accFrames.length > 0;\n }\n\n pause(): void {\n if (this.currentPauseStart === undefined && this.recorderIO.recording) {\n this.currentPauseStart = Date.now();\n }\n\n if (this.nextInChain) {\n this.nextInChain.pause();\n }\n }\n\n /**\n * Resume playback and record the pause interval\n */\n resume(): void {\n if (this.currentPauseStart !== undefined && this.recorderIO.recording) {\n this.pauseWallTimes.push([this.currentPauseStart, Date.now()]);\n this.currentPauseStart = undefined;\n }\n\n if (this.nextInChain) {\n this.nextInChain.resume();\n }\n }\n\n private resetPauseState(): void {\n this.currentPauseStart = undefined;\n this.pauseWallTimes = [];\n }\n\n onPlaybackFinished(options: PlaybackFinishedEvent): void {\n const finishTime = Date.now();\n\n super.onPlaybackFinished(options);\n\n if (!this.recorderIO.recording) {\n return;\n }\n\n if (this.currentPauseStart !== undefined) {\n this.pauseWallTimes.push([this.currentPauseStart, finishTime]);\n this.currentPauseStart = undefined;\n }\n\n if (this.accFrames.length === 0) {\n this.resetPauseState();\n return;\n }\n\n const playbackPosition = options.playbackPosition;\n\n const pauseEvents: Array<[number, number]> = [];\n\n if (this.pauseWallTimes.length > 0) {\n const totalPauseDuration = this.pauseWallTimes.reduce(\n (sum, [start, end]) => sum + (end - start),\n 0,\n );\n // Convert playbackPosition from seconds to milliseconds for wall time calculations\n const playbackStartTime = finishTime - playbackPosition * 1000 - totalPauseDuration;\n\n let accumulatedPause = 0;\n for (const [pauseStart, pauseEnd] of this.pauseWallTimes) {\n let position = (pauseStart - playbackStartTime - accumulatedPause) / 1000; // Convert to seconds\n const duration = (pauseEnd - pauseStart) / 1000; // Convert to seconds\n position = Math.max(0, Math.min(position, playbackPosition));\n pauseEvents.push([position, duration]);\n accumulatedPause += pauseEnd - pauseStart;\n }\n }\n\n const buf: AudioFrame[] = [];\n let accDur = 0;\n const sampleRate = this.accFrames[0]!.sampleRate;\n const numChannels = this.accFrames[0]!.channels;\n\n let pauseIdx = 0;\n let shouldBreak = false;\n\n for (const frame of this.accFrames) {\n let currentFrame = frame;\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n\n if (frameDuration + accDur > playbackPosition) {\n const [left] = splitFrame(currentFrame, playbackPosition - accDur);\n currentFrame = left;\n shouldBreak = true;\n }\n\n // Process any pauses before this frame starts\n while (pauseIdx < pauseEvents.length && pauseEvents[pauseIdx]![0] <= accDur) {\n const [, pauseDur] = pauseEvents[pauseIdx]!;\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n pauseIdx++;\n }\n\n // Process any pauses within this frame\n const currentFrameDuration = currentFrame.samplesPerChannel / currentFrame.sampleRate;\n while (\n pauseIdx < pauseEvents.length &&\n pauseEvents[pauseIdx]![0] < accDur + currentFrameDuration\n ) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n const [left, right] = splitFrame(currentFrame, pausePos - accDur);\n buf.push(left);\n accDur += left.samplesPerChannel / left.sampleRate;\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n currentFrame = right;\n pauseIdx++;\n }\n\n buf.push(currentFrame);\n accDur += currentFrame.samplesPerChannel / currentFrame.sampleRate;\n\n if (shouldBreak) {\n break;\n }\n }\n\n // Process remaining pauses\n while (pauseIdx < pauseEvents.length) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n if (pausePos <= playbackPosition) {\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n }\n pauseIdx++;\n }\n\n if (buf.length > 0) {\n this.writeFn(buf);\n }\n\n this.accFrames = [];\n this.resetPauseState();\n }\n\n async captureFrame(frame: AudioFrame): Promise<void> {\n await super.captureFrame(frame);\n\n if (this.recorderIO.recording) {\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n this.accFrames.push(frame);\n }\n\n if (this.nextInChain) {\n await this.nextInChain.captureFrame(frame);\n }\n }\n\n flush(): void {\n super.flush();\n\n if (this.nextInChain) {\n this.nextInChain.flush();\n }\n }\n\n clearBuffer(): void {\n if (this.nextInChain) {\n this.nextInChain.clearBuffer();\n }\n }\n}\n\n/**\n * Create a silent audio frame with the given duration\n */\nfunction createSilenceFrame(duration: number, sampleRate: number, numChannels: number): AudioFrame {\n const samples = Math.floor(duration * sampleRate);\n const data = new Int16Array(samples * numChannels); // Zero-filled by default\n return new AudioFrame(data, sampleRate, numChannels, samples);\n}\n\n/**\n * Split an audio frame at the given position (in seconds)\n * Returns [left, right] frames\n */\nfunction splitFrame(frame: AudioFrame, position: number): [AudioFrame, AudioFrame] {\n if (position <= 0) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [emptyFrame, frame];\n }\n\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n if (position >= frameDuration) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [frame, emptyFrame];\n }\n\n // samplesNeeded is samples per channel (i.e., sample count in time)\n const samplesNeeded = Math.floor(position * frame.sampleRate);\n // Int16Array: each element is one sample, interleaved by channel\n // So total elements = samplesPerChannel * channels\n const numChannels = frame.channels;\n\n const leftData = frame.data.slice(0, samplesNeeded * numChannels);\n const rightData = frame.data.slice(samplesNeeded * numChannels);\n\n const leftFrame = new AudioFrame(leftData, frame.sampleRate, frame.channels, samplesNeeded);\n\n const rightFrame = new AudioFrame(\n rightData,\n frame.sampleRate,\n frame.channels,\n frame.samplesPerChannel - samplesNeeded,\n );\n\n return [leftFrame, rightFrame];\n}\n"],"mappings":"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","names":["ffmpeg","ffmpegInstaller","path","fs"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/voice/recorder_io/recorder_io.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport ffmpegInstaller from '@ffmpeg-installer/ffmpeg';\nimport { Mutex } from '@livekit/mutex';\nimport { AudioFrame, AudioResampler } from '@livekit/rtc-node';\nimport ffmpeg from 'fluent-ffmpeg';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { PassThrough } from 'node:stream';\nimport type { ReadableStream } from 'node:stream/web';\nimport { TransformStream } from 'node:stream/web';\nimport { log } from '../../log.js';\nimport { isStreamReaderReleaseError } from '../../stream/deferred_stream.js';\nimport { type StreamChannel, createStreamChannel } from '../../stream/stream_channel.js';\nimport { Future, Task, cancelAndWait, delay } from '../../utils.js';\nimport type { AgentSession } from '../agent_session.js';\nimport { AudioInput, AudioOutput, type PlaybackFinishedEvent } from '../io.js';\n\nffmpeg.setFfmpegPath(ffmpegInstaller.path);\n\nconst WRITE_INTERVAL_MS = 2500;\nconst DEFAULT_SAMPLE_RATE = 48000;\n\nexport interface RecorderOptions {\n agentSession: AgentSession;\n sampleRate?: number;\n}\n\ninterface ResampleAndMixOptions {\n frames: AudioFrame[];\n resampler: AudioResampler | undefined;\n flush?: boolean;\n}\n\nexport class RecorderIO {\n private inRecord?: RecorderAudioInput;\n private outRecord?: RecorderAudioOutput;\n\n private inChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n private outChan: StreamChannel<AudioFrame[]> = createStreamChannel<AudioFrame[]>();\n\n private session: AgentSession;\n private sampleRate: number;\n\n private _outputPath?: string;\n private forwardTask?: Task<void>;\n private encodeTask?: Task<void>;\n\n private closeFuture: Future<void> = new Future();\n private lock: Mutex = new Mutex();\n private started: boolean = false;\n\n // FFmpeg streaming state\n private pcmStream?: PassThrough;\n private ffmpegPromise?: Promise<void>;\n private inResampler?: AudioResampler;\n private outResampler?: AudioResampler;\n\n private logger = log();\n\n constructor(opts: RecorderOptions) {\n const { agentSession, sampleRate = DEFAULT_SAMPLE_RATE } = opts;\n\n this.session = agentSession;\n this.sampleRate = sampleRate;\n }\n\n async start(outputPath: string): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (this.started) return;\n\n if (!this.inRecord || !this.outRecord) {\n throw new Error(\n 'RecorderIO not properly initialized: both `recordInput()` and `recordOutput()` must be called before starting the recorder.',\n );\n }\n\n this._outputPath = outputPath;\n this.started = true;\n this.closeFuture = new Future();\n\n // Ensure output directory exists\n const dir = path.dirname(outputPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n this.forwardTask = Task.from(({ signal }) => this.forward(signal));\n this.encodeTask = Task.from(() => this.encode(), undefined, 'recorder_io_encode_task');\n } finally {\n unlock();\n }\n }\n\n async close(): Promise<void> {\n const unlock = await this.lock.lock();\n\n try {\n if (!this.started) return;\n\n await this.inChan.close();\n await this.outChan.close();\n await this.closeFuture.await;\n await cancelAndWait([this.forwardTask!, this.encodeTask!]);\n\n this.started = false;\n } finally {\n unlock();\n }\n }\n\n recordInput(audioInput: AudioInput): RecorderAudioInput {\n this.inRecord = new RecorderAudioInput(this, audioInput);\n return this.inRecord;\n }\n\n recordOutput(audioOutput: AudioOutput): RecorderAudioOutput {\n this.outRecord = new RecorderAudioOutput(this, audioOutput, (buf) => this.writeCb(buf));\n return this.outRecord;\n }\n\n private writeCb(buf: AudioFrame[]): void {\n const inputBuf = this.inRecord!.takeBuf();\n this.inChan.write(inputBuf);\n this.outChan.write(buf);\n }\n\n get recording(): boolean {\n return this.started;\n }\n\n get outputPath(): string | undefined {\n return this._outputPath;\n }\n\n get recordingStartedAt(): number | undefined {\n // Use session start time to align with trace timestamps\n return this.session._startedAt;\n }\n\n /**\n * Forward task: periodically flush input buffer to encoder\n */\n private async forward(signal: AbortSignal): Promise<void> {\n while (!signal.aborted) {\n try {\n await delay(WRITE_INTERVAL_MS, { signal });\n } catch {\n // Aborted\n break;\n }\n\n if (this.outRecord!.hasPendingData) {\n // If the output is currently playing audio, wait for it to stay in sync\n continue;\n }\n\n // Flush input buffer\n const inputBuf = this.inRecord!.takeBuf();\n this.inChan\n .write(inputBuf)\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO input buffer'));\n this.outChan\n .write([])\n .catch((err) => this.logger.error({ err }, 'Error writing RecorderIO output buffer'));\n }\n }\n\n /**\n * Start FFmpeg process for streaming encoding\n */\n private startFFmpeg(): void {\n if (this.pcmStream) return;\n\n this.pcmStream = new PassThrough();\n\n this.ffmpegPromise = new Promise<void>((resolve, reject) => {\n ffmpeg(this.pcmStream!)\n .inputFormat('s16le')\n .inputOptions([`-ar ${this.sampleRate}`, '-ac 2'])\n .audioCodec('libopus')\n .audioChannels(2)\n .audioFrequency(this.sampleRate)\n .format('ogg')\n .output(this._outputPath!)\n .on('end', () => {\n this.logger.debug('FFmpeg encoding finished');\n resolve();\n })\n .on('error', (err) => {\n // Ignore errors from intentional stream closure or SIGINT during shutdown\n if (\n err.message?.includes('Output stream closed') ||\n err.message?.includes('received signal 2') ||\n err.message?.includes('SIGKILL') ||\n err.message?.includes('SIGINT')\n ) {\n resolve();\n } else {\n this.logger.error({ err }, 'FFmpeg encoding error');\n reject(err);\n }\n })\n .run();\n });\n }\n\n /**\n * Resample and mix frames to mono Float32\n */\n private resampleAndMix(opts: ResampleAndMixOptions): {\n samples: Float32Array;\n resampler: AudioResampler | undefined;\n } {\n const INV_INT16 = 1.0 / 32768.0;\n const { frames, flush = false } = opts;\n let { resampler } = opts;\n\n if (frames.length === 0 && !flush) {\n return { samples: new Float32Array(0), resampler };\n }\n\n if (!resampler && frames.length > 0) {\n const firstFrame = frames[0]!;\n resampler = new AudioResampler(firstFrame.sampleRate, this.sampleRate, firstFrame.channels);\n }\n\n const resampledFrames: AudioFrame[] = [];\n for (const frame of frames) {\n if (resampler) {\n resampledFrames.push(...resampler.push(frame));\n }\n }\n\n if (flush && resampler) {\n resampledFrames.push(...resampler.flush());\n }\n\n const totalSamples = resampledFrames.reduce((acc, frame) => acc + frame.samplesPerChannel, 0);\n const samples = new Float32Array(totalSamples);\n\n let pos = 0;\n for (const frame of resampledFrames) {\n const data = frame.data;\n const numChannels = frame.channels;\n for (let i = 0; i < frame.samplesPerChannel; i++) {\n let sum = 0;\n for (let ch = 0; ch < numChannels; ch++) {\n sum += data[i * numChannels + ch]!;\n }\n samples[pos++] = (sum / numChannels) * INV_INT16;\n }\n }\n\n return { samples, resampler };\n }\n\n /**\n * Write PCM chunk to FFmpeg stream\n */\n private writePCM(leftSamples: Float32Array, rightSamples: Float32Array): void {\n if (!this.pcmStream) {\n this.startFFmpeg();\n }\n\n // Handle length mismatch by prepending silence\n if (leftSamples.length !== rightSamples.length) {\n const diff = Math.abs(leftSamples.length - rightSamples.length);\n if (leftSamples.length < rightSamples.length) {\n this.logger.warn(\n `Input is shorter by ${diff} samples; silence has been prepended to align the input channel.`,\n );\n const padded = new Float32Array(rightSamples.length);\n padded.set(leftSamples, diff);\n leftSamples = padded;\n } else {\n const padded = new Float32Array(leftSamples.length);\n padded.set(rightSamples, diff);\n rightSamples = padded;\n }\n }\n\n const maxLen = Math.max(leftSamples.length, rightSamples.length);\n if (maxLen <= 0) return;\n\n // Interleave stereo samples and convert back to Int16\n const stereoData = new Int16Array(maxLen * 2);\n for (let i = 0; i < maxLen; i++) {\n stereoData[i * 2] = Math.max(\n -32768,\n Math.min(32767, Math.round((leftSamples[i] ?? 0) * 32768)),\n );\n stereoData[i * 2 + 1] = Math.max(\n -32768,\n Math.min(32767, Math.round((rightSamples[i] ?? 0) * 32768)),\n );\n }\n\n this.pcmStream!.write(Buffer.from(stereoData.buffer));\n }\n\n /**\n * Encode task: read from channels, mix to stereo, stream to FFmpeg\n */\n private async encode(): Promise<void> {\n if (!this._outputPath) return;\n\n const inReader = this.inChan.stream().getReader();\n const outReader = this.outChan.stream().getReader();\n\n try {\n while (true) {\n const [inResult, outResult] = await Promise.all([inReader.read(), outReader.read()]);\n\n if (inResult.done || outResult.done) {\n break;\n }\n\n const inputBuf = inResult.value;\n const outputBuf = outResult.value;\n\n const inMixed = this.resampleAndMix({ frames: inputBuf, resampler: this.inResampler });\n this.inResampler = inMixed.resampler;\n\n const outMixed = this.resampleAndMix({\n frames: outputBuf,\n resampler: this.outResampler,\n flush: outputBuf.length > 0,\n });\n this.outResampler = outMixed.resampler;\n\n // Stream PCM data directly to FFmpeg\n this.writePCM(inMixed.samples, outMixed.samples);\n }\n\n // Close FFmpeg stream and wait for encoding to complete\n if (this.pcmStream) {\n this.pcmStream.end();\n await this.ffmpegPromise;\n }\n } catch (err) {\n this.logger.error({ err }, 'Error in encode task');\n } finally {\n inReader.releaseLock();\n outReader.releaseLock();\n\n if (!this.closeFuture.done) {\n this.closeFuture.resolve();\n }\n }\n }\n}\n\nclass RecorderAudioInput extends AudioInput {\n private source: AudioInput;\n private recorderIO: RecorderIO;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n\n constructor(recorderIO: RecorderIO, source: AudioInput) {\n super();\n this.recorderIO = recorderIO;\n this.source = source;\n\n // Set up the intercepting stream\n this.deferredStream.setSource(this.createInterceptingStream());\n }\n\n /**\n * Wall-clock time when the first frame was captured\n */\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n /**\n * Take accumulated frames and clear the buffer\n */\n takeBuf(): AudioFrame[] {\n const frames = this.accFrames;\n this.accFrames = [];\n return frames;\n }\n\n /**\n * Creates a stream that intercepts frames from the source,\n * accumulates them when recording, and passes them through unchanged.\n */\n private createInterceptingStream(): ReadableStream<AudioFrame> {\n const sourceStream = this.source.stream;\n const reader = sourceStream.getReader();\n\n const transform = new TransformStream<AudioFrame, AudioFrame>({\n transform: (frame, controller) => {\n // Accumulate frames when recording is active\n if (this.recorderIO.recording) {\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n this.accFrames.push(frame);\n }\n\n controller.enqueue(frame);\n },\n });\n\n const pump = async () => {\n const writer = transform.writable.getWriter();\n let sourceError: unknown;\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n await writer.write(value);\n }\n } catch (e) {\n if (isStreamReaderReleaseError(e)) return;\n sourceError = e;\n } finally {\n if (sourceError) {\n writer.abort(sourceError);\n return;\n }\n\n writer.releaseLock();\n\n try {\n await transform.writable.close();\n } catch {\n // ignore \"WritableStream is closed\" errors\n }\n }\n };\n\n pump();\n\n return transform.readable;\n }\n\n onAttached(): void {\n this.source.onAttached();\n }\n\n onDetached(): void {\n this.source.onDetached();\n }\n}\n\nclass RecorderAudioOutput extends AudioOutput {\n private recorderIO: RecorderIO;\n private writeFn: (buf: AudioFrame[]) => void;\n private accFrames: AudioFrame[] = [];\n private _startedWallTime?: number;\n\n // Pause tracking\n private currentPauseStart?: number;\n private pauseWallTimes: Array<[number, number]> = []; // [start, end] pairs\n\n constructor(\n recorderIO: RecorderIO,\n audioOutput: AudioOutput,\n writeFn: (buf: AudioFrame[]) => void,\n ) {\n super(audioOutput.sampleRate, audioOutput, { pause: true });\n this.recorderIO = recorderIO;\n this.writeFn = writeFn;\n }\n\n get startedWallTime(): number | undefined {\n return this._startedWallTime;\n }\n\n get hasPendingData(): boolean {\n return this.accFrames.length > 0;\n }\n\n pause(): void {\n if (this.currentPauseStart === undefined && this.recorderIO.recording) {\n this.currentPauseStart = Date.now();\n }\n\n if (this.nextInChain) {\n this.nextInChain.pause();\n }\n }\n\n /**\n * Resume playback and record the pause interval\n */\n resume(): void {\n if (this.currentPauseStart !== undefined && this.recorderIO.recording) {\n this.pauseWallTimes.push([this.currentPauseStart, Date.now()]);\n this.currentPauseStart = undefined;\n }\n\n if (this.nextInChain) {\n this.nextInChain.resume();\n }\n }\n\n private resetPauseState(): void {\n this.currentPauseStart = undefined;\n this.pauseWallTimes = [];\n }\n\n onPlaybackFinished(options: PlaybackFinishedEvent): void {\n const finishTime = Date.now();\n\n super.onPlaybackFinished(options);\n\n if (!this.recorderIO.recording) {\n return;\n }\n\n if (this.currentPauseStart !== undefined) {\n this.pauseWallTimes.push([this.currentPauseStart, finishTime]);\n this.currentPauseStart = undefined;\n }\n\n if (this.accFrames.length === 0) {\n this.resetPauseState();\n return;\n }\n\n const playbackPosition = options.playbackPosition;\n\n const pauseEvents: Array<[number, number]> = [];\n\n if (this.pauseWallTimes.length > 0) {\n const totalPauseDuration = this.pauseWallTimes.reduce(\n (sum, [start, end]) => sum + (end - start),\n 0,\n );\n // Convert playbackPosition from seconds to milliseconds for wall time calculations\n const playbackStartTime = finishTime - playbackPosition * 1000 - totalPauseDuration;\n\n let accumulatedPause = 0;\n for (const [pauseStart, pauseEnd] of this.pauseWallTimes) {\n let position = (pauseStart - playbackStartTime - accumulatedPause) / 1000; // Convert to seconds\n const duration = (pauseEnd - pauseStart) / 1000; // Convert to seconds\n position = Math.max(0, Math.min(position, playbackPosition));\n pauseEvents.push([position, duration]);\n accumulatedPause += pauseEnd - pauseStart;\n }\n }\n\n const buf: AudioFrame[] = [];\n let accDur = 0;\n const sampleRate = this.accFrames[0]!.sampleRate;\n const numChannels = this.accFrames[0]!.channels;\n\n let pauseIdx = 0;\n let shouldBreak = false;\n\n for (const frame of this.accFrames) {\n let currentFrame = frame;\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n\n if (frameDuration + accDur > playbackPosition) {\n const [left] = splitFrame(currentFrame, playbackPosition - accDur);\n currentFrame = left;\n shouldBreak = true;\n }\n\n // Process any pauses before this frame starts\n while (pauseIdx < pauseEvents.length && pauseEvents[pauseIdx]![0] <= accDur) {\n const [, pauseDur] = pauseEvents[pauseIdx]!;\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n pauseIdx++;\n }\n\n // Process any pauses within this frame\n const currentFrameDuration = currentFrame.samplesPerChannel / currentFrame.sampleRate;\n while (\n pauseIdx < pauseEvents.length &&\n pauseEvents[pauseIdx]![0] < accDur + currentFrameDuration\n ) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n const [left, right] = splitFrame(currentFrame, pausePos - accDur);\n buf.push(left);\n accDur += left.samplesPerChannel / left.sampleRate;\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n currentFrame = right;\n pauseIdx++;\n }\n\n buf.push(currentFrame);\n accDur += currentFrame.samplesPerChannel / currentFrame.sampleRate;\n\n if (shouldBreak) {\n break;\n }\n }\n\n // Process remaining pauses\n while (pauseIdx < pauseEvents.length) {\n const [pausePos, pauseDur] = pauseEvents[pauseIdx]!;\n if (pausePos <= playbackPosition) {\n buf.push(createSilenceFrame(pauseDur, sampleRate, numChannels));\n }\n pauseIdx++;\n }\n\n if (buf.length > 0) {\n this.writeFn(buf);\n }\n\n this.accFrames = [];\n this.resetPauseState();\n }\n\n async captureFrame(frame: AudioFrame): Promise<void> {\n await super.captureFrame(frame);\n\n if (this.recorderIO.recording) {\n if (this._startedWallTime === undefined) {\n this._startedWallTime = Date.now();\n }\n this.accFrames.push(frame);\n }\n\n if (this.nextInChain) {\n await this.nextInChain.captureFrame(frame);\n }\n }\n\n flush(): void {\n super.flush();\n\n if (this.nextInChain) {\n this.nextInChain.flush();\n }\n }\n\n clearBuffer(): void {\n if (this.nextInChain) {\n this.nextInChain.clearBuffer();\n }\n }\n}\n\n/**\n * Create a silent audio frame with the given duration\n */\nfunction createSilenceFrame(duration: number, sampleRate: number, numChannels: number): AudioFrame {\n const samples = Math.floor(duration * sampleRate);\n const data = new Int16Array(samples * numChannels); // Zero-filled by default\n return new AudioFrame(data, sampleRate, numChannels, samples);\n}\n\n/**\n * Split an audio frame at the given position (in seconds)\n * Returns [left, right] frames\n */\nfunction splitFrame(frame: AudioFrame, position: number): [AudioFrame, AudioFrame] {\n if (position <= 0) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [emptyFrame, frame];\n }\n\n const frameDuration = frame.samplesPerChannel / frame.sampleRate;\n if (position >= frameDuration) {\n const emptyFrame = new AudioFrame(new Int16Array(0), frame.sampleRate, frame.channels, 0);\n return [frame, emptyFrame];\n }\n\n // samplesNeeded is samples per channel (i.e., sample count in time)\n const samplesNeeded = Math.floor(position * frame.sampleRate);\n // Int16Array: each element is one sample, interleaved by channel\n // So total elements = samplesPerChannel * channels\n const numChannels = frame.channels;\n\n const leftData = frame.data.slice(0, samplesNeeded * numChannels);\n const rightData = frame.data.slice(samplesNeeded * numChannels);\n\n const leftFrame = new AudioFrame(leftData, frame.sampleRate, frame.channels, samplesNeeded);\n\n const rightFrame = new AudioFrame(\n rightData,\n frame.sampleRate,\n frame.channels,\n frame.samplesPerChannel - samplesNeeded,\n );\n\n return [leftFrame, rightFrame];\n}\n"],"mappings":"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","names":["ffmpeg","ffmpegInstaller","path","fs"]}
|
|
@@ -337,7 +337,7 @@ class RecorderAudioOutput extends AudioOutput {
|
|
|
337
337
|
pauseWallTimes = [];
|
|
338
338
|
// [start, end] pairs
|
|
339
339
|
constructor(recorderIO, audioOutput, writeFn) {
|
|
340
|
-
super(audioOutput.sampleRate, audioOutput);
|
|
340
|
+
super(audioOutput.sampleRate, audioOutput, { pause: true });
|
|
341
341
|
this.recorderIO = recorderIO;
|
|
342
342
|
this.writeFn = writeFn;
|
|
343
343
|
}
|