@ai-ecoverse/pi-audio 1.22.0 → 1.24.0

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/mixer.js ADDED
@@ -0,0 +1,337 @@
1
+ import { chunkSeconds } from "./audio.js";
2
+ import { convertAudio } from "./convert.js";
3
+ import { echoRuntime } from "./echo.js";
4
+ import { VoiceRequestError } from "./errors.js";
5
+ import { bindWorkletNode, isWorkletRelease } from "./worklet/release.js";
6
+ import { AUDIO_TAP_PROCESSOR, loadAudioTap } from "./worklet/tap.js";
7
+ async function openMixer(context, options = {}) {
8
+ const playback = playbackOf(options);
9
+ const mix = context.createGain();
10
+ const speechGain = context.createGain();
11
+ const bedGain = context.createGain();
12
+ speechGain.connect(mix);
13
+ bedGain.connect(mix);
14
+ await loadAudioTap(context.audioWorklet, options.tapUrl);
15
+ const tap = new (workletNode(options))(context, AUDIO_TAP_PROCESSOR, {
16
+ numberOfInputs: 1,
17
+ numberOfOutputs: 1,
18
+ channelCount: 2,
19
+ outputChannelCount: [2]
20
+ });
21
+ mix.connect(tap);
22
+ tap.connect(context.destination);
23
+ const queue = new ChunkQueue();
24
+ const release = watchTap(tap, queue, context);
25
+ const speech = new SpeechOut(context, speechGain);
26
+ const bed = new BedPlayer(context, bedGain);
27
+ return {
28
+ context,
29
+ speech,
30
+ bed,
31
+ reference: queue,
32
+ playback,
33
+ close() {
34
+ release();
35
+ bed.close();
36
+ speech.flush();
37
+ queue.close();
38
+ disconnect(speechGain);
39
+ disconnect(mix);
40
+ }
41
+ };
42
+ }
43
+ class ChunkQueue {
44
+ #chunks = [];
45
+ #waiters = [];
46
+ #closed = false;
47
+ push(chunk) {
48
+ if (this.#closed) return;
49
+ const waiter = this.#waiters.shift();
50
+ if (waiter) {
51
+ waiter({ value: chunk, done: false });
52
+ return;
53
+ }
54
+ if (this.#chunks.length === 32) this.#chunks.shift();
55
+ this.#chunks.push(chunk);
56
+ }
57
+ close() {
58
+ if (this.#closed) return;
59
+ this.#closed = true;
60
+ const waiters = this.#waiters.splice(0);
61
+ for (const waiter of waiters) waiter({ value: void 0, done: true });
62
+ }
63
+ [Symbol.asyncIterator]() {
64
+ return this;
65
+ }
66
+ next() {
67
+ const queued = this.#chunks.shift();
68
+ if (queued) return Promise.resolve({ value: queued, done: false });
69
+ if (this.#closed) return Promise.resolve({ value: void 0, done: true });
70
+ return new Promise((resolve) => {
71
+ this.#waiters.push(resolve);
72
+ });
73
+ }
74
+ }
75
+ class SpeechOut {
76
+ #context;
77
+ #gain;
78
+ #listeners = /* @__PURE__ */ new Set();
79
+ #sources = [];
80
+ #timers = [];
81
+ #origin = -1;
82
+ #writtenMs = 0;
83
+ #nextAt = 0;
84
+ constructor(context, gain) {
85
+ this.#context = context;
86
+ this.#gain = gain;
87
+ }
88
+ get playedMs() {
89
+ if (this.#origin < 0) return 0;
90
+ const elapsed = (this.#context.currentTime - this.#origin) * 1e3;
91
+ if (elapsed <= 0) return 0;
92
+ if (elapsed > this.#writtenMs) return this.#writtenMs;
93
+ return elapsed;
94
+ }
95
+ write(chunk, tag) {
96
+ const channels = planar(chunk);
97
+ const frames = channels[0].length;
98
+ if (frames === 0) return;
99
+ const buffer = this.#context.createBuffer(channels.length, frames, chunk.format.sampleRate);
100
+ writeBuffer(buffer, channels);
101
+ const source = this.#context.createBufferSource();
102
+ source.buffer = buffer;
103
+ source.connect(this.#gain);
104
+ const start = Math.max(this.#context.currentTime, this.#nextAt);
105
+ source.start(start);
106
+ this.#nextAt = start + chunkSeconds(chunk);
107
+ if (this.#origin < 0) this.#origin = start;
108
+ this.#writtenMs += chunkSeconds(chunk) * 1e3;
109
+ if (tag) this.#emit(tag, start);
110
+ source.onended = () => disconnect(source);
111
+ this.#sources.push(source);
112
+ }
113
+ flush() {
114
+ this.#stopSources();
115
+ this.#clearTimers();
116
+ this.#origin = -1;
117
+ this.#writtenMs = 0;
118
+ this.#nextAt = this.#context.currentTime;
119
+ }
120
+ onPlayback(listener) {
121
+ this.#listeners.add(listener);
122
+ return () => {
123
+ this.#listeners.delete(listener);
124
+ };
125
+ }
126
+ #emit(tag, start) {
127
+ const delay = (start - this.#context.currentTime) * 1e3;
128
+ const fire = () => {
129
+ for (const listener of this.#listeners) listener(tag, 0);
130
+ };
131
+ if (delay <= 0) {
132
+ fire();
133
+ return;
134
+ }
135
+ this.#timers.push(setTimeout(fire, delay));
136
+ }
137
+ #stopSources() {
138
+ const sources = this.#sources.splice(0);
139
+ for (const source of sources) {
140
+ source.stop();
141
+ disconnect(source);
142
+ }
143
+ }
144
+ #clearTimers() {
145
+ for (const timer of this.#timers.splice(0)) clearTimeout(timer);
146
+ }
147
+ }
148
+ class BedPlayer {
149
+ #context;
150
+ #output;
151
+ #loops = [];
152
+ #shots = [];
153
+ #timers = [];
154
+ #generation = 0;
155
+ constructor(context, output) {
156
+ this.#context = context;
157
+ this.#output = output;
158
+ }
159
+ play(cue, fadeMs) {
160
+ if (cue.kind === "keep" || cue.kind === "silence") return;
161
+ if (cue.kind === "loop") this.#clear(this.#loops, 0);
162
+ const gain = this.#context.createGain();
163
+ ramp(gain.gain, cue.gain, this.#context.currentTime, fadeMs);
164
+ gain.connect(this.#output);
165
+ const token = this.#generation;
166
+ const node = this.#start(cue, gain, token);
167
+ if (!node) return;
168
+ this.#keep(cue.kind, node);
169
+ }
170
+ duck(gain, rampMs) {
171
+ ramp(this.#output.gain, gain, this.#context.currentTime, rampMs);
172
+ }
173
+ stop(fadeMs) {
174
+ this.#generation += 1;
175
+ this.#clear(this.#loops, fadeMs);
176
+ this.#clear(this.#shots, fadeMs);
177
+ }
178
+ close() {
179
+ this.stop(0);
180
+ for (const timer of this.#timers.splice(0)) clearTimeout(timer);
181
+ disconnect(this.#output);
182
+ }
183
+ #start(cue, gain, token) {
184
+ const source = cue.source;
185
+ if ("url" in source) {
186
+ void this.#loadUrl(source.url, gain, cue.kind, token).catch(() => disconnect(gain));
187
+ return void 0;
188
+ }
189
+ if ("buffer" in source) return this.#buffer(source.buffer, gain, cue.kind);
190
+ return this.#synth(source, gain);
191
+ }
192
+ #synth(source, gain) {
193
+ const node = source.synth(this.#context);
194
+ node.connect(gain);
195
+ startNode(node);
196
+ return node;
197
+ }
198
+ #buffer(buffer, gain, kind) {
199
+ const source = this.#context.createBufferSource();
200
+ source.buffer = buffer;
201
+ source.loop = kind === "loop";
202
+ source.connect(gain);
203
+ source.start();
204
+ if (kind === "oneShot") source.onended = () => disconnect(source);
205
+ return source;
206
+ }
207
+ async #loadUrl(url, gain, kind, token) {
208
+ const response = await fetch(url).catch(() => void 0);
209
+ if (response === void 0 || !response.ok || token !== this.#generation) {
210
+ disconnect(gain);
211
+ return;
212
+ }
213
+ const bytes = await response.arrayBuffer();
214
+ if (token !== this.#generation) {
215
+ disconnect(gain);
216
+ return;
217
+ }
218
+ const buffer = await decoded(this.#context, bytes);
219
+ if (buffer === void 0 || token !== this.#generation) {
220
+ disconnect(gain);
221
+ return;
222
+ }
223
+ this.#keep(kind, this.#buffer(buffer, gain, kind));
224
+ }
225
+ #keep(kind, node) {
226
+ if (kind === "loop") this.#loops.push(node);
227
+ else this.#shots.push(node);
228
+ }
229
+ #clear(list, fadeMs) {
230
+ const nodes = list.splice(0);
231
+ for (const node of nodes) this.#end(node, fadeMs);
232
+ }
233
+ #end(node, fadeMs) {
234
+ const stoppable = node;
235
+ if (fadeMs <= 0) {
236
+ if (typeof stoppable.stop === "function") stoppable.stop();
237
+ disconnect(node);
238
+ return;
239
+ }
240
+ if (typeof stoppable.stop === "function") {
241
+ stoppable.stop(this.#context.currentTime + fadeMs / 1e3);
242
+ }
243
+ this.#timers.push(setTimeout(() => disconnect(node), fadeMs));
244
+ }
245
+ }
246
+ function playbackOf(options) {
247
+ const playback = options.playback ?? "webaudio";
248
+ const runtime = options.runtime ?? echoRuntime(hasMediaDevices());
249
+ if (runtime === "node" && playback === "rtc-loopback") {
250
+ throw new VoiceRequestError("error", "Node playback is webaudio");
251
+ }
252
+ return playback;
253
+ }
254
+ function hasMediaDevices() {
255
+ return typeof navigator !== "undefined" && navigator.mediaDevices !== void 0;
256
+ }
257
+ function workletNode(options) {
258
+ if (options.workletNode) return options.workletNode;
259
+ const ctor = globalThis.AudioWorkletNode;
260
+ if (ctor) return ctor;
261
+ throw new VoiceRequestError("error", "AudioWorkletNode is not available");
262
+ }
263
+ function watchTap(tap, queue, context) {
264
+ tap.port.onmessage = (event) => {
265
+ const samples = readSamples(event.data);
266
+ if (!samples) return;
267
+ queue.push({
268
+ data: new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength).slice(),
269
+ format: { encoding: "pcmf32", sampleRate: context.sampleRate, channels: 1 },
270
+ timestamp: context.currentTime * 1e3
271
+ });
272
+ };
273
+ return bindWorkletNode(tap);
274
+ }
275
+ function readSamples(data) {
276
+ if (isWorkletRelease(data)) return void 0;
277
+ if (typeof data !== "object" || data === null || !("samples" in data)) return void 0;
278
+ if (!(data.samples instanceof Float32Array) || data.samples.length === 0) return void 0;
279
+ return data.samples;
280
+ }
281
+ function planar(chunk) {
282
+ const pcm = chunk.format.encoding === "pcmf32" ? chunk : convertAudio(chunk, { ...chunk.format, encoding: "pcmf32" });
283
+ const channels = pcm.format.channels;
284
+ const view = new DataView(pcm.data.buffer, pcm.data.byteOffset, pcm.data.byteLength);
285
+ const frames = Math.floor(pcm.data.byteLength / (4 * channels));
286
+ const out = [];
287
+ for (let channel = 0; channel < channels; channel += 1) {
288
+ const samples = new Float32Array(frames);
289
+ for (let frame = 0; frame < frames; frame += 1) {
290
+ samples[frame] = view.getFloat32((frame * channels + channel) * 4, true);
291
+ }
292
+ out.push(samples);
293
+ }
294
+ return out;
295
+ }
296
+ function writeBuffer(buffer, channels) {
297
+ if (typeof buffer.copyToChannel === "function") {
298
+ for (let index = 0; index < channels.length; index += 1) {
299
+ buffer.copyToChannel(channels[index], index);
300
+ }
301
+ return;
302
+ }
303
+ for (let index = 0; index < channels.length; index += 1) {
304
+ buffer.getChannelData(index).set(channels[index]);
305
+ }
306
+ }
307
+ function ramp(param, value, when, fadeMs) {
308
+ param.cancelScheduledValues(when);
309
+ if (fadeMs <= 0) {
310
+ param.setValueAtTime(value, when);
311
+ return;
312
+ }
313
+ param.setValueAtTime(param.value, when);
314
+ param.linearRampToValueAtTime(value, when + fadeMs / 1e3);
315
+ }
316
+ function startNode(node) {
317
+ const startable = node;
318
+ if (typeof startable.start === "function") startable.start();
319
+ }
320
+ async function decoded(context, bytes) {
321
+ try {
322
+ return await context.decodeAudioData(bytes);
323
+ } catch {
324
+ return void 0;
325
+ }
326
+ }
327
+ function disconnect(node) {
328
+ try {
329
+ node.disconnect();
330
+ } catch {
331
+ return;
332
+ }
333
+ }
334
+ export {
335
+ openMixer
336
+ };
337
+ //# sourceMappingURL=mixer.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/mixer.ts"],
4
+ "sourcesContent": ["import { type AudioChunk, type AudioSink, type AudioTag, chunkSeconds } from './audio.ts';\nimport { convertAudio } from './convert.ts';\nimport { echoRuntime } from './echo.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { AudioOut, Cue, CueSource } from './soundscape.ts';\nimport { bindWorkletNode, isWorkletRelease, type WorkletNodeLike } from './worklet/release.ts';\nimport { AUDIO_TAP_PROCESSOR, loadAudioTap } from './worklet/tap.ts';\n\nexport interface Mixer extends AudioOut {\n close(): void;\n}\n\nexport interface MixerWorkletNode extends WorkletNodeLike {\n connect(destination: AudioNode): AudioNode;\n}\n\nexport interface OpenMixerOptions {\n playback?: 'webaudio' | 'rtc-loopback';\n runtime?: 'node' | 'browser';\n tapUrl?: string;\n workletNode?: new (\n context: BaseAudioContext,\n name: string,\n options?: AudioWorkletNodeOptions\n ) => MixerWorkletNode;\n}\n\nexport async function openMixer(\n context: BaseAudioContext,\n options: OpenMixerOptions = {}\n): Promise<Mixer> {\n const playback = playbackOf(options);\n const mix = context.createGain();\n const speechGain = context.createGain();\n const bedGain = context.createGain();\n speechGain.connect(mix);\n bedGain.connect(mix);\n await loadAudioTap(context.audioWorklet, options.tapUrl);\n const tap = new (workletNode(options))(context, AUDIO_TAP_PROCESSOR, {\n numberOfInputs: 1,\n numberOfOutputs: 1,\n channelCount: 2,\n outputChannelCount: [2],\n });\n mix.connect(tap as unknown as AudioNode);\n tap.connect(context.destination);\n const queue = new ChunkQueue();\n const release = watchTap(tap, queue, context);\n const speech = new SpeechOut(context, speechGain);\n const bed = new BedPlayer(context, bedGain);\n return {\n context,\n speech,\n bed,\n reference: queue,\n playback,\n close() {\n release();\n bed.close();\n speech.flush();\n queue.close();\n disconnect(speechGain);\n disconnect(mix);\n },\n };\n}\n\nclass ChunkQueue implements AsyncIterable<AudioChunk> {\n readonly #chunks: AudioChunk[] = [];\n readonly #waiters: Array<(result: IteratorResult<AudioChunk>) => void> = [];\n #closed = false;\n\n push(chunk: AudioChunk): void {\n if (this.#closed) return;\n const waiter = this.#waiters.shift();\n if (waiter) {\n waiter({ value: chunk, done: false });\n return;\n }\n if (this.#chunks.length === 32) this.#chunks.shift();\n this.#chunks.push(chunk);\n }\n\n close(): void {\n if (this.#closed) return;\n this.#closed = true;\n const waiters = this.#waiters.splice(0);\n for (const waiter of waiters) waiter({ value: undefined, done: true });\n }\n\n [Symbol.asyncIterator](): AsyncIterator<AudioChunk> {\n return this;\n }\n\n next(): Promise<IteratorResult<AudioChunk>> {\n const queued = this.#chunks.shift();\n if (queued) return Promise.resolve({ value: queued, done: false });\n if (this.#closed) return Promise.resolve({ value: undefined, done: true });\n return new Promise((resolve) => {\n this.#waiters.push(resolve);\n });\n }\n}\n\nclass SpeechOut implements AudioSink {\n readonly #context: BaseAudioContext;\n readonly #gain: GainNode;\n readonly #listeners = new Set<(tag: AudioTag, offsetMs: number) => void>();\n readonly #sources: AudioBufferSourceNode[] = [];\n readonly #timers: Array<ReturnType<typeof setTimeout>> = [];\n #origin = -1;\n #writtenMs = 0;\n #nextAt = 0;\n\n constructor(context: BaseAudioContext, gain: GainNode) {\n this.#context = context;\n this.#gain = gain;\n }\n\n get playedMs(): number {\n if (this.#origin < 0) return 0;\n const elapsed = (this.#context.currentTime - this.#origin) * 1000;\n if (elapsed <= 0) return 0;\n if (elapsed > this.#writtenMs) return this.#writtenMs;\n return elapsed;\n }\n\n write(chunk: AudioChunk, tag?: AudioTag): void {\n const channels = planar(chunk);\n const frames = channels[0].length;\n if (frames === 0) return;\n const buffer = this.#context.createBuffer(channels.length, frames, chunk.format.sampleRate);\n writeBuffer(buffer, channels);\n const source = this.#context.createBufferSource();\n source.buffer = buffer;\n source.connect(this.#gain);\n const start = Math.max(this.#context.currentTime, this.#nextAt);\n source.start(start);\n this.#nextAt = start + chunkSeconds(chunk);\n if (this.#origin < 0) this.#origin = start;\n this.#writtenMs += chunkSeconds(chunk) * 1000;\n if (tag) this.#emit(tag, start);\n source.onended = () => disconnect(source);\n this.#sources.push(source);\n }\n\n flush(): void {\n this.#stopSources();\n this.#clearTimers();\n this.#origin = -1;\n this.#writtenMs = 0;\n this.#nextAt = this.#context.currentTime;\n }\n\n onPlayback(listener: (tag: AudioTag, offsetMs: number) => void): () => void {\n this.#listeners.add(listener);\n return () => {\n this.#listeners.delete(listener);\n };\n }\n\n #emit(tag: AudioTag, start: number): void {\n const delay = (start - this.#context.currentTime) * 1000;\n const fire = () => {\n for (const listener of this.#listeners) listener(tag, 0);\n };\n if (delay <= 0) {\n fire();\n return;\n }\n this.#timers.push(setTimeout(fire, delay));\n }\n\n #stopSources(): void {\n const sources = this.#sources.splice(0);\n for (const source of sources) {\n source.stop();\n disconnect(source);\n }\n }\n\n #clearTimers(): void {\n for (const timer of this.#timers.splice(0)) clearTimeout(timer);\n }\n}\n\nclass BedPlayer {\n readonly #context: BaseAudioContext;\n readonly #output: GainNode;\n readonly #loops: AudioNode[] = [];\n readonly #shots: AudioNode[] = [];\n readonly #timers: Array<ReturnType<typeof setTimeout>> = [];\n #generation = 0;\n\n constructor(context: BaseAudioContext, output: GainNode) {\n this.#context = context;\n this.#output = output;\n }\n\n play(cue: Cue, fadeMs: number): void {\n if (cue.kind === 'keep' || cue.kind === 'silence') return;\n if (cue.kind === 'loop') this.#clear(this.#loops, 0);\n const gain = this.#context.createGain();\n ramp(gain.gain, cue.gain, this.#context.currentTime, fadeMs);\n gain.connect(this.#output);\n const token = this.#generation;\n const node = this.#start(cue, gain, token);\n if (!node) return;\n this.#keep(cue.kind, node);\n }\n\n duck(gain: number, rampMs: number): void {\n ramp(this.#output.gain, gain, this.#context.currentTime, rampMs);\n }\n\n stop(fadeMs: number): void {\n this.#generation += 1;\n this.#clear(this.#loops, fadeMs);\n this.#clear(this.#shots, fadeMs);\n }\n\n close(): void {\n this.stop(0);\n for (const timer of this.#timers.splice(0)) clearTimeout(timer);\n disconnect(this.#output);\n }\n\n #start(\n cue: Extract<Cue, { kind: 'loop' | 'oneShot' }>,\n gain: GainNode,\n token: number\n ): AudioNode | undefined {\n const source = cue.source;\n if ('url' in source) {\n void this.#loadUrl(source.url, gain, cue.kind, token).catch(() => disconnect(gain));\n return undefined;\n }\n if ('buffer' in source) return this.#buffer(source.buffer, gain, cue.kind);\n return this.#synth(source, gain);\n }\n\n #synth(source: Extract<CueSource, { synth: unknown }>, gain: GainNode): AudioNode {\n const node = source.synth(this.#context);\n node.connect(gain);\n startNode(node);\n return node;\n }\n\n #buffer(buffer: AudioBuffer, gain: GainNode, kind: 'loop' | 'oneShot'): AudioNode {\n const source = this.#context.createBufferSource();\n source.buffer = buffer;\n source.loop = kind === 'loop';\n source.connect(gain);\n source.start();\n if (kind === 'oneShot') source.onended = () => disconnect(source);\n return source;\n }\n\n async #loadUrl(\n url: string,\n gain: GainNode,\n kind: 'loop' | 'oneShot',\n token: number\n ): Promise<void> {\n const response = await fetch(url).catch(() => undefined);\n if (response === undefined || !response.ok || token !== this.#generation) {\n disconnect(gain);\n return;\n }\n const bytes = await response.arrayBuffer();\n if (token !== this.#generation) {\n disconnect(gain);\n return;\n }\n const buffer = await decoded(this.#context, bytes);\n if (buffer === undefined || token !== this.#generation) {\n disconnect(gain);\n return;\n }\n this.#keep(kind, this.#buffer(buffer, gain, kind));\n }\n\n #keep(kind: 'loop' | 'oneShot', node: AudioNode): void {\n if (kind === 'loop') this.#loops.push(node);\n else this.#shots.push(node);\n }\n\n #clear(list: AudioNode[], fadeMs: number): void {\n const nodes = list.splice(0);\n for (const node of nodes) this.#end(node, fadeMs);\n }\n\n #end(node: AudioNode, fadeMs: number): void {\n const stoppable = node as AudioNode & { stop?: (when?: number) => void };\n if (fadeMs <= 0) {\n if (typeof stoppable.stop === 'function') stoppable.stop();\n disconnect(node);\n return;\n }\n if (typeof stoppable.stop === 'function') {\n stoppable.stop(this.#context.currentTime + fadeMs / 1000);\n }\n this.#timers.push(setTimeout(() => disconnect(node), fadeMs));\n }\n}\n\nfunction playbackOf(options: OpenMixerOptions): 'webaudio' | 'rtc-loopback' {\n const playback = options.playback ?? 'webaudio';\n const runtime = options.runtime ?? echoRuntime(hasMediaDevices());\n if (runtime === 'node' && playback === 'rtc-loopback') {\n throw new VoiceRequestError('error', 'Node playback is webaudio');\n }\n return playback;\n}\n\nfunction hasMediaDevices(): boolean {\n return typeof navigator !== 'undefined' && navigator.mediaDevices !== undefined;\n}\n\nfunction workletNode(options: OpenMixerOptions): NonNullable<OpenMixerOptions['workletNode']> {\n if (options.workletNode) return options.workletNode;\n const ctor = (globalThis as { AudioWorkletNode?: OpenMixerOptions['workletNode'] })\n .AudioWorkletNode;\n if (ctor) return ctor;\n throw new VoiceRequestError('error', 'AudioWorkletNode is not available');\n}\n\nfunction watchTap(tap: WorkletNodeLike, queue: ChunkQueue, context: BaseAudioContext): () => void {\n tap.port.onmessage = (event) => {\n const samples = readSamples(event.data);\n if (!samples) return;\n queue.push({\n data: new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength).slice(),\n format: { encoding: 'pcmf32', sampleRate: context.sampleRate, channels: 1 },\n timestamp: context.currentTime * 1000,\n });\n };\n return bindWorkletNode(tap);\n}\n\nfunction readSamples(data: unknown): Float32Array | undefined {\n if (isWorkletRelease(data)) return undefined;\n if (typeof data !== 'object' || data === null || !('samples' in data)) return undefined;\n if (!(data.samples instanceof Float32Array) || data.samples.length === 0) return undefined;\n return data.samples;\n}\n\nfunction planar(chunk: AudioChunk): Float32Array[] {\n const pcm =\n chunk.format.encoding === 'pcmf32'\n ? chunk\n : convertAudio(chunk, { ...chunk.format, encoding: 'pcmf32' });\n const channels = pcm.format.channels;\n const view = new DataView(pcm.data.buffer, pcm.data.byteOffset, pcm.data.byteLength);\n const frames = Math.floor(pcm.data.byteLength / (4 * channels));\n const out: Float32Array[] = [];\n for (let channel = 0; channel < channels; channel += 1) {\n const samples = new Float32Array(frames);\n for (let frame = 0; frame < frames; frame += 1) {\n samples[frame] = view.getFloat32((frame * channels + channel) * 4, true);\n }\n out.push(samples);\n }\n return out;\n}\n\nfunction writeBuffer(buffer: AudioBuffer, channels: Float32Array[]): void {\n if (typeof buffer.copyToChannel === 'function') {\n for (let index = 0; index < channels.length; index += 1) {\n buffer.copyToChannel(channels[index] as Float32Array<ArrayBuffer>, index);\n }\n return;\n }\n for (let index = 0; index < channels.length; index += 1) {\n buffer.getChannelData(index).set(channels[index] as Float32Array<ArrayBuffer>);\n }\n}\n\nfunction ramp(param: AudioParam, value: number, when: number, fadeMs: number): void {\n param.cancelScheduledValues(when);\n if (fadeMs <= 0) {\n param.setValueAtTime(value, when);\n return;\n }\n param.setValueAtTime(param.value, when);\n param.linearRampToValueAtTime(value, when + fadeMs / 1000);\n}\n\nfunction startNode(node: AudioNode): void {\n const startable = node as AudioNode & { start?: (when?: number) => void };\n if (typeof startable.start === 'function') startable.start();\n}\n\nasync function decoded(\n context: BaseAudioContext,\n bytes: ArrayBuffer\n): Promise<AudioBuffer | undefined> {\n try {\n return await context.decodeAudioData(bytes);\n } catch {\n return undefined;\n }\n}\n\nfunction disconnect(node: { disconnect(): void }): void {\n try {\n node.disconnect();\n } catch {\n return;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,132 @@
1
+ import { PassThrough } from "node:stream";
2
+ import { VoiceRequestError } from "./errors.js";
3
+ import { openMixer } from "./mixer.js";
4
+ const TAP_NAME = "pi-audio-tap.js";
5
+ const AUDIO_TAP_CLASSIC = [
6
+ "class PiAudioTap extends AudioWorkletProcessor {",
7
+ " constructor() {",
8
+ " super();",
9
+ " this.released = false;",
10
+ " this.posted = false;",
11
+ " var port = this.port;",
12
+ " if (port) {",
13
+ " port.onmessage = function (event) {",
14
+ ' if (event.data && event.data.type === "pi-audio-release") this.released = true;',
15
+ " }.bind(this);",
16
+ " }",
17
+ " }",
18
+ " process(inputs, outputs) {",
19
+ " if (this.released) {",
20
+ " if (!this.posted) {",
21
+ " this.posted = true;",
22
+ ' if (this.port) this.port.postMessage({ type: "pi-audio-release" });',
23
+ " }",
24
+ " return true;",
25
+ " }",
26
+ " var input = (inputs && inputs[0]) || [];",
27
+ " var output = (outputs && outputs[0]) || [];",
28
+ " for (var channel = 0; channel < output.length; channel++) {",
29
+ " var dst = output[channel];",
30
+ " if (!dst) continue;",
31
+ " var src = input[channel] || input[0];",
32
+ " if (!src) { dst.fill(0); continue; }",
33
+ " if (src.length === dst.length) dst.set(src);",
34
+ " else {",
35
+ " var count = Math.min(src.length, dst.length);",
36
+ " for (var frame = 0; frame < count; frame++) dst[frame] = src[frame];",
37
+ " }",
38
+ " }",
39
+ " var heard = input[0];",
40
+ " if (heard && heard.length && this.port) {",
41
+ " var copy = new Float32Array(heard.length);",
42
+ " copy.set(heard);",
43
+ " this.port.postMessage({ samples: copy });",
44
+ " }",
45
+ " return true;",
46
+ " }",
47
+ "}",
48
+ "registerProcessor('pi-audio-tap', PiAudioTap);",
49
+ ""
50
+ ].join("\n");
51
+ async function loadNodeAudioEngine(engine) {
52
+ if (engine === "node-web-audio-api") {
53
+ return await import("node-web-audio-api");
54
+ }
55
+ return await import("web-audio-api");
56
+ }
57
+ async function openNodeAudio(options = {}) {
58
+ const engine = options.engine ?? "auto";
59
+ const sink = options.sink ?? "none";
60
+ const load = options.load ?? loadNodeAudioEngine;
61
+ const names = engine === "auto" ? ["node-web-audio-api", "web-audio-api"] : [engine];
62
+ const tapUrl = await writeTap();
63
+ let failure;
64
+ for (const name of names) {
65
+ try {
66
+ return await startEngine(name, sink, options.sampleRate, load, tapUrl);
67
+ } catch (error) {
68
+ failure = error;
69
+ if (engine !== "auto") break;
70
+ }
71
+ }
72
+ throw new VoiceRequestError("error", failureText(failure));
73
+ }
74
+ async function startEngine(engine, sink, sampleRate, load, tapUrl) {
75
+ const loaded = await load(engine);
76
+ const built = await nodeContextOptions(engine, sink, sampleRate);
77
+ const context = new loaded.AudioContext(built.options);
78
+ try {
79
+ const out = await openMixer(context, {
80
+ playback: "webaudio",
81
+ runtime: "node",
82
+ tapUrl,
83
+ workletNode: loaded.AudioWorkletNode
84
+ });
85
+ if (context.state === "suspended") await context.resume();
86
+ return {
87
+ engine,
88
+ sink,
89
+ context,
90
+ out,
91
+ close: () => closeSession(context, out, built.stream)
92
+ };
93
+ } catch (error) {
94
+ await closeSession(context, void 0, built.stream);
95
+ throw error;
96
+ }
97
+ }
98
+ async function nodeContextOptions(engine, sink, sampleRate) {
99
+ const options = {};
100
+ if (sampleRate !== void 0) options.sampleRate = sampleRate;
101
+ if (sink === "default") return { options };
102
+ if (engine === "node-web-audio-api") {
103
+ options.sinkId = { type: "none" };
104
+ return { options };
105
+ }
106
+ const stream = new PassThrough();
107
+ stream.resume();
108
+ options.sinkId = stream;
109
+ return { options, stream };
110
+ }
111
+ async function closeSession(context, out, stream) {
112
+ if (out) out.close();
113
+ if (stream) stream.destroy();
114
+ await context.close();
115
+ }
116
+ function failureText(error) {
117
+ if (error instanceof Error) return error.message;
118
+ return "Node audio engine is unavailable";
119
+ }
120
+ async function writeTap() {
121
+ const { writeFileSync } = await import("node:fs");
122
+ const { tmpdir } = await import("node:os");
123
+ const { join } = await import("node:path");
124
+ const file = join(tmpdir(), TAP_NAME);
125
+ writeFileSync(file, AUDIO_TAP_CLASSIC);
126
+ return file;
127
+ }
128
+ export {
129
+ loadNodeAudioEngine,
130
+ openNodeAudio
131
+ };
132
+ //# sourceMappingURL=node-host.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/node-host.ts"],
4
+ "sourcesContent": ["import { PassThrough } from 'node:stream';\nimport { VoiceRequestError } from './errors.ts';\nimport { type Mixer, type OpenMixerOptions, openMixer } from './mixer.ts';\n\nexport type NodeAudioEngine = 'node-web-audio-api' | 'web-audio-api';\n\nexport interface NodeAudioModule {\n AudioContext: new (options?: Record<string, unknown>) => NodeAudioContext;\n AudioWorkletNode: NonNullable<OpenMixerOptions['workletNode']>;\n}\n\nexport interface NodeAudioContext {\n readonly state: AudioContextState;\n resume(): Promise<void>;\n close(): Promise<void>;\n readonly sampleRate: number;\n readonly currentTime: number;\n readonly destination: AudioDestinationNode;\n readonly audioWorklet: AudioWorklet;\n createGain(): GainNode;\n createBuffer(channels: number, length: number, sampleRate: number): AudioBuffer;\n createBufferSource(): AudioBufferSourceNode;\n decodeAudioData(buffer: ArrayBuffer): Promise<AudioBuffer>;\n}\n\nexport interface NodeAudioOptions {\n engine?: NodeAudioEngine | 'auto';\n sink?: 'none' | 'default';\n sampleRate?: number;\n load?: (engine: NodeAudioEngine) => Promise<NodeAudioModule>;\n}\n\nexport interface NodeAudioSession {\n readonly engine: NodeAudioEngine;\n readonly sink: 'none' | 'default';\n readonly context: BaseAudioContext;\n readonly out: Mixer;\n close(): Promise<void>;\n}\n\nconst TAP_NAME = 'pi-audio-tap.js';\n\nconst AUDIO_TAP_CLASSIC = [\n 'class PiAudioTap extends AudioWorkletProcessor {',\n ' constructor() {',\n ' super();',\n ' this.released = false;',\n ' this.posted = false;',\n ' var port = this.port;',\n ' if (port) {',\n ' port.onmessage = function (event) {',\n ' if (event.data && event.data.type === \"pi-audio-release\") this.released = true;',\n ' }.bind(this);',\n ' }',\n ' }',\n ' process(inputs, outputs) {',\n ' if (this.released) {',\n ' if (!this.posted) {',\n ' this.posted = true;',\n ' if (this.port) this.port.postMessage({ type: \"pi-audio-release\" });',\n ' }',\n ' return true;',\n ' }',\n ' var input = (inputs && inputs[0]) || [];',\n ' var output = (outputs && outputs[0]) || [];',\n ' for (var channel = 0; channel < output.length; channel++) {',\n ' var dst = output[channel];',\n ' if (!dst) continue;',\n ' var src = input[channel] || input[0];',\n ' if (!src) { dst.fill(0); continue; }',\n ' if (src.length === dst.length) dst.set(src);',\n ' else {',\n ' var count = Math.min(src.length, dst.length);',\n ' for (var frame = 0; frame < count; frame++) dst[frame] = src[frame];',\n ' }',\n ' }',\n ' var heard = input[0];',\n ' if (heard && heard.length && this.port) {',\n ' var copy = new Float32Array(heard.length);',\n ' copy.set(heard);',\n ' this.port.postMessage({ samples: copy });',\n ' }',\n ' return true;',\n ' }',\n '}',\n \"registerProcessor('pi-audio-tap', PiAudioTap);\",\n '',\n].join('\\n');\n\nexport async function loadNodeAudioEngine(engine: NodeAudioEngine): Promise<NodeAudioModule> {\n if (engine === 'node-web-audio-api') {\n return (await import('node-web-audio-api')) as unknown as NodeAudioModule;\n }\n return (await import('web-audio-api')) as unknown as NodeAudioModule;\n}\n\nexport async function openNodeAudio(options: NodeAudioOptions = {}): Promise<NodeAudioSession> {\n const engine = options.engine ?? 'auto';\n const sink = options.sink ?? 'none';\n const load = options.load ?? loadNodeAudioEngine;\n const names: NodeAudioEngine[] =\n engine === 'auto' ? ['node-web-audio-api', 'web-audio-api'] : [engine];\n const tapUrl = await writeTap();\n let failure: unknown;\n for (const name of names) {\n try {\n return await startEngine(name, sink, options.sampleRate, load, tapUrl);\n } catch (error) {\n failure = error;\n if (engine !== 'auto') break;\n }\n }\n throw new VoiceRequestError('error', failureText(failure));\n}\n\nasync function startEngine(\n engine: NodeAudioEngine,\n sink: 'none' | 'default',\n sampleRate: number | undefined,\n load: (engine: NodeAudioEngine) => Promise<NodeAudioModule>,\n tapUrl: string\n): Promise<NodeAudioSession> {\n const loaded = await load(engine);\n const built = await nodeContextOptions(engine, sink, sampleRate);\n const context = new loaded.AudioContext(built.options);\n try {\n const out = await openMixer(context as unknown as BaseAudioContext, {\n playback: 'webaudio',\n runtime: 'node',\n tapUrl,\n workletNode: loaded.AudioWorkletNode,\n });\n if (context.state === 'suspended') await context.resume();\n return {\n engine,\n sink,\n context: context as unknown as BaseAudioContext,\n out,\n close: () => closeSession(context, out, built.stream),\n };\n } catch (error) {\n await closeSession(context, undefined, built.stream);\n throw error;\n }\n}\n\nasync function nodeContextOptions(\n engine: NodeAudioEngine,\n sink: 'none' | 'default',\n sampleRate: number | undefined\n): Promise<{ options: Record<string, unknown>; stream?: PassThrough }> {\n const options: Record<string, unknown> = {};\n if (sampleRate !== undefined) options.sampleRate = sampleRate;\n if (sink === 'default') return { options };\n if (engine === 'node-web-audio-api') {\n options.sinkId = { type: 'none' };\n return { options };\n }\n const stream = new PassThrough();\n stream.resume();\n options.sinkId = stream;\n return { options, stream };\n}\n\nasync function closeSession(\n context: NodeAudioContext,\n out: Mixer | undefined,\n stream: PassThrough | undefined\n): Promise<void> {\n if (out) out.close();\n if (stream) stream.destroy();\n await context.close();\n}\n\nfunction failureText(error: unknown): string {\n if (error instanceof Error) return error.message;\n return 'Node audio engine is unavailable';\n}\n\nasync function writeTap(): Promise<string> {\n const { writeFileSync } = await import('node:fs');\n const { tmpdir } = await import('node:os');\n const { join } = await import('node:path');\n const file = join(tmpdir(), TAP_NAME);\n writeFileSync(file, AUDIO_TAP_CLASSIC);\n return file;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,4 @@
1
+ import type { JsonObject } from '@earendil-works/pi-ai';
2
+ import type { CustomLlm } from './hosted-agent.ts';
3
+ export declare function elevenLabsCustomLlm(llm: CustomLlm): JsonObject;
4
+ export declare function fishCustomLlm(llm: CustomLlm): JsonObject;
@@ -2,6 +2,7 @@ import type { RealtimeModel } from '../models.ts';
2
2
  import type { ElevenLabsOptions } from './session.ts';
3
3
  export declare const elevenLabsAgent: RealtimeModel<'elevenlabs-agents'>;
4
4
  export declare function registerElevenLabsAgents(options?: ElevenLabsOptions): () => void;
5
+ export { elevenLabsCustomLlm } from '../custom-llm.ts';
5
6
  export type { CustomLlm, HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';
6
7
  export { hashHostedSpec, memoryHostedCache } from '../hosted-agent.ts';
7
8
  export type { ProvisionOptions } from './provision.ts';
@@ -2,6 +2,7 @@ import type { RealtimeModel } from '../models.ts';
2
2
  import type { FishOptions } from './session.ts';
3
3
  export declare const fishAgent: RealtimeModel<'fish-agents'>;
4
4
  export declare function registerFishAgents(options?: FishOptions): () => void;
5
+ export { fishCustomLlm } from '../custom-llm.ts';
5
6
  export type { CustomLlm, HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';
6
7
  export { hashHostedSpec, memoryHostedCache } from '../hosted-agent.ts';
7
8
  export type { ProvisionOptions } from './provision.ts';
@@ -7,8 +7,12 @@ export { convertAudio, convertAudioSource, decodeAudioBase64, encodeAudioBase64,
7
7
  export type { EchoCanceller, EchoControl, EchoMode, EchoReport, MicOpen } from './echo.ts';
8
8
  export { assertNodeEcho, calibrateEcho, chooseCapture, echoRuntime, measureEcho, micOpen, openEchoMic, } from './echo.ts';
9
9
  export { VoiceRequestError } from './errors.ts';
10
+ export type { Mixer, OpenMixerOptions } from './mixer.ts';
11
+ export { openMixer } from './mixer.ts';
10
12
  export type { Delivery, DeliverySupport, KnownRealtimeApi, KnownSpeechApi, KnownTranscriptionApi, RealtimeApi, RealtimeModel, SpeechApi, SpeechModel, TranscriptionApi, TranscriptionModel, VoiceInfo, VoiceModel, VoiceModelType, } from './models.ts';
11
13
  export { getVoiceModel, listVoiceModels, registerVoiceModel, } from './models.ts';
14
+ export type { NodeAudioEngine, NodeAudioModule, NodeAudioOptions, NodeAudioSession, } from './node-host.ts';
15
+ export { openNodeAudio } from './node-host.ts';
12
16
  export type { DeliveryPlan, RealtimeApiProvider, RealtimeContext, RealtimeEvent, RealtimeOptions, RealtimeSession, RealtimeSocket, RealtimeToken, TruncationPlan, } from './realtime.ts';
13
17
  export { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';
14
18
  export { connectRealtime, listen, registerRealtimeApiProvider, registerSpeechApiProvider, registerTranscriptionApiProvider, speak, transcribe, } from './registry.ts';
@@ -29,3 +33,5 @@ export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAud
29
33
  export { addAudioCounters, priceAudio } from './usage.ts';
30
34
  export type { VadHook, VadUpdate } from './vad.ts';
31
35
  export { energyVad } from './vad.ts';
36
+ export { bindWorkletNode, WORKLET_RELEASE } from './worklet/release.ts';
37
+ export { AUDIO_TAP_PROCESSOR, audioTapWorkletUrl } from './worklet/tap.ts';
@@ -0,0 +1,15 @@
1
+ import type { AudioOut } from './soundscape.ts';
2
+ import { type WorkletNodeLike } from './worklet/release.ts';
3
+ export interface Mixer extends AudioOut {
4
+ close(): void;
5
+ }
6
+ export interface MixerWorkletNode extends WorkletNodeLike {
7
+ connect(destination: AudioNode): AudioNode;
8
+ }
9
+ export interface OpenMixerOptions {
10
+ playback?: 'webaudio' | 'rtc-loopback';
11
+ runtime?: 'node' | 'browser';
12
+ tapUrl?: string;
13
+ workletNode?: new (context: BaseAudioContext, name: string, options?: AudioWorkletNodeOptions) => MixerWorkletNode;
14
+ }
15
+ export declare function openMixer(context: BaseAudioContext, options?: OpenMixerOptions): Promise<Mixer>;
@@ -0,0 +1,34 @@
1
+ import { type Mixer, type OpenMixerOptions } from './mixer.ts';
2
+ export type NodeAudioEngine = 'node-web-audio-api' | 'web-audio-api';
3
+ export interface NodeAudioModule {
4
+ AudioContext: new (options?: Record<string, unknown>) => NodeAudioContext;
5
+ AudioWorkletNode: NonNullable<OpenMixerOptions['workletNode']>;
6
+ }
7
+ export interface NodeAudioContext {
8
+ readonly state: AudioContextState;
9
+ resume(): Promise<void>;
10
+ close(): Promise<void>;
11
+ readonly sampleRate: number;
12
+ readonly currentTime: number;
13
+ readonly destination: AudioDestinationNode;
14
+ readonly audioWorklet: AudioWorklet;
15
+ createGain(): GainNode;
16
+ createBuffer(channels: number, length: number, sampleRate: number): AudioBuffer;
17
+ createBufferSource(): AudioBufferSourceNode;
18
+ decodeAudioData(buffer: ArrayBuffer): Promise<AudioBuffer>;
19
+ }
20
+ export interface NodeAudioOptions {
21
+ engine?: NodeAudioEngine | 'auto';
22
+ sink?: 'none' | 'default';
23
+ sampleRate?: number;
24
+ load?: (engine: NodeAudioEngine) => Promise<NodeAudioModule>;
25
+ }
26
+ export interface NodeAudioSession {
27
+ readonly engine: NodeAudioEngine;
28
+ readonly sink: 'none' | 'default';
29
+ readonly context: BaseAudioContext;
30
+ readonly out: Mixer;
31
+ close(): Promise<void>;
32
+ }
33
+ export declare function loadNodeAudioEngine(engine: NodeAudioEngine): Promise<NodeAudioModule>;
34
+ export declare function openNodeAudio(options?: NodeAudioOptions): Promise<NodeAudioSession>;