@ai-ecoverse/pi-audio 1.23.0 → 1.25.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/README.md +4 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +2 -2
- package/dist/mixer.js +337 -0
- package/dist/mixer.js.map +7 -0
- package/dist/models.js.map +2 -2
- package/dist/node-host.js +132 -0
- package/dist/node-host.js.map +7 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/mixer.d.ts +15 -0
- package/dist/types/models.d.ts +1 -1
- package/dist/types/node-host.d.ts +34 -0
- package/dist/types/wavespeed/catalog.d.ts +1 -0
- package/dist/types/wavespeed/index.d.ts +1 -0
- package/dist/types/wavespeed/speak.d.ts +4 -0
- package/dist/types/wavespeed/transcribe.d.ts +4 -0
- package/dist/types/wavespeed/wire.d.ts +25 -0
- package/dist/types/worklet/convert.d.ts +9 -6
- package/dist/types/worklet/release.d.ts +13 -0
- package/dist/types/worklet/tap.d.ts +25 -0
- package/dist/wavespeed/catalog.js +60 -0
- package/dist/wavespeed/catalog.js.map +7 -0
- package/dist/wavespeed/index.js +5 -0
- package/dist/wavespeed/index.js.map +7 -0
- package/dist/wavespeed/speak.js +66 -0
- package/dist/wavespeed/speak.js.map +7 -0
- package/dist/wavespeed/transcribe.js +85 -0
- package/dist/wavespeed/transcribe.js.map +7 -0
- package/dist/wavespeed/wire.js +138 -0
- package/dist/wavespeed/wire.js.map +7 -0
- package/dist/worklet/convert.js +23 -0
- package/dist/worklet/convert.js.map +2 -2
- package/dist/worklet/release.js +42 -0
- package/dist/worklet/release.js.map +7 -0
- package/dist/worklet/tap.js +97 -0
- package/dist/worklet/tap.js.map +7 -0
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
23
23
|
|
|
24
24
|
`@ai-ecoverse/pi-audio/durable` appends a `voice.truncate` entry for a `VoiceCut`. The entry has no model messages. Its `edits` replace the target assistant message with the text heard so far and keep tool calls. The stored entry stays intact, and pi-durable derives the shorter message for the next model request. `@earendil-works/pi-durable` is an optional peer (`^1.1.0`); the root package does not import it. `VoiceCut.entryId` is the decimal string of pi-durable's numeric entry id.
|
|
25
25
|
|
|
26
|
+
`@ai-ecoverse/pi-audio/wavespeed` registers WaveSpeed adapters when `registerWaveSpeed()` is called. Both are batch only. `wavespeed-ai/openai-whisper` submits JSON to `/wavespeed-ai/openai-whisper` and polls `/predictions/{id}/result`. Timestamps stay off, so the catalog price is $0.001 per second (`perInputMinute` 0.06). A byte input is uploaded to `/media/upload/binary` first. `minimax/speech-2.6-turbo` is the same submit/poll path for MP3 (`textStreaming: false`, `streaming: false`) at $0.06 per thousand characters. `voice_id` is a caller id (`cloning: true`). These models are not for live voice.
|
|
27
|
+
|
|
26
28
|
`@ai-ecoverse/pi-audio/cloudflare` registers Workers AI speech adapters when `registerCloudflare()` is called. One Cloudflare token calls `POST /client/v4/accounts/{account_id}/ai/run/{model}`. Put the account in `baseUrl` or the `cf-account-id` header. Streaming models open `wss://gateway.ai.cloudflare.com/v1/{account}/{gateway}/workers-ai?model=…`. The gateway id is the `cf-aig-gateway-id` header, or `default`. `@cf/deepgram/nova-3` streams (`turnDetection: vad`, catalog price $0.0092 per minute). `@cf/deepgram/flux` streams with semantic end of turn ($0.0077 per minute) and has no batch transcribe. `@cf/openai/whisper-large-v3-turbo` is batch only ($0.000513 per minute). `@cf/deepgram/aura-1` ($0.015 per thousand characters), `aura-2-en`, and `aura-2-es` ($0.03) speak MP3 over HTTP and linear16 over the socket (`textStreaming: true`). `@cf/myshell-ai/melotts` is batch MP3 priced at $0.000205 per audio minute. `@cf/pipecat-ai/smart-turn-v2` is not a transcript model, so it is not registered.
|
|
27
29
|
|
|
28
30
|
`@ai-ecoverse/pi-audio/google` registers Gemini speech adapters when `registerGoogle()` is called. `gemini-3.8-flash-tts` and `gemini-3.8-flash-lite-tts` post JSON to `models/{id}:generateContent` with `responseModalities: ["AUDIO"]` and a prebuilt `voiceName`. Unary audio is WAV at 24 kHz unless the format asks for `AUDIO_L16`, `AUDIO_MULAW`, or `AUDIO_ALAW`. `textStreaming` is false. Output audio is priced from 25 tokens per second: $0.0135 per minute for Flash and $0.009 per minute for Flash-Lite through December 31, 2026. Text input is $0.50 per million tokens and is not on `audioCost`. Auth is `x-goog-api-key`.
|
|
@@ -35,6 +37,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
35
37
|
|
|
36
38
|
`calibrateEcho(out, mic, probe, durationMs, signal)` plays a probe and aligns the mic with `AudioOut.reference`. `delayMs` is the lag. `cancellationDb` is how far the leaked copy sits below the reference, and `residualDb` is that leak. A quiet leak recommends `interrupt`, a moderate one `onWords`, and a loud one `headphones`. `chooseCapture` keeps `none` and `wasm`, and moves `browser` to `wasm` when cancellation is under 12 dB. `micOpen('browser')` asks for `echoCancellation` and `playback: 'rtc-loopback'`. `wasm` and `none` turn browser cancellation off and use `webaudio`. `assertNodeEcho` rejects `browser` on Node. `openEchoMic` leaves `browser` and `none` audio as captured. `wasm` runs an injected `EchoCanceller` against `reference`. The homescoop `@ai-ecoverse/wasm-*` echo-canceller package does not exist yet, so this package does not import one. `voiceTurns` passes `options.echo` through `openEchoMic` before `listen`.
|
|
37
39
|
|
|
40
|
+
`openNodeAudio({ sink: 'none' })` builds that mix on Node. `node-web-audio-api` (optional peer, BSD-3) is the `AudioContext`, created with `sinkId: { type: 'none' }` when the machine has no output device. `web-audio-api` (the audiojs package, MIT) is the pure-JS fallback: it accepts the same sink value but does not render unless a speaker or a stream is attached, so a silent sink is a discarded stream. Hosts pass the microphone as an `AudioSource`. The package does not open `node-web-audio-api`'s mic, and Node echo mode stays `wasm` or `none` with `playback: 'webaudio'`. Finished worklets post `pi-audio-release` and `bindWorkletNode` disconnects them. Returning `false` from `process` does not stop a processor in node-web-audio-api (issue 202). `openMixer(context)` is the same graph on a context the host already opened.
|
|
41
|
+
|
|
38
42
|
`followBed(bed, soundscape)` cues a `BedChannel` from `VoicePhase`. A loop waits `graceMs` before it starts, so a short gap stays silent. `keep` leaves the current loop running across chained tools. Speech ducks the bed to `duckUnderSpeech`; listening ducks it to `duckUnderUser`. `voiceTurns` does this when `options.soundscape` is set and `out` is an `AudioOut` (`speech`, `bed`, `reference`, `playback`). A plain `AudioSink` still receives only the spoken audio.
|
|
39
43
|
|
|
40
44
|
`voiceTurns` captions include `agent_spoken`. `upTo` follows `AudioSink` playback: per character when the chunk has alignment, and evenly across the sentence when it does not. `captionsFromHistory(entries)` rebuilds user captions from entries whose `data.source` is `voice`, and agent captions from the assistant text. A `voice.truncate` entry adds `agent_spoken` and `agent_cut` at `heardUpTo`, so the rest of the original text stays available as unheard.
|
package/dist/index.js
CHANGED
|
@@ -17,11 +17,13 @@ import {
|
|
|
17
17
|
openEchoMic
|
|
18
18
|
} from "./echo.js";
|
|
19
19
|
import { VoiceRequestError } from "./errors.js";
|
|
20
|
+
import { openMixer } from "./mixer.js";
|
|
20
21
|
import {
|
|
21
22
|
getVoiceModel,
|
|
22
23
|
listVoiceModels,
|
|
23
24
|
registerVoiceModel
|
|
24
25
|
} from "./models.js";
|
|
26
|
+
import { openNodeAudio } from "./node-host.js";
|
|
25
27
|
import { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from "./realtime.js";
|
|
26
28
|
import {
|
|
27
29
|
connectRealtime,
|
|
@@ -40,15 +42,21 @@ import { TranscriptionEventStream } from "./transcription.js";
|
|
|
40
42
|
import { voiceTurns } from "./turns.js";
|
|
41
43
|
import { addAudioCounters, priceAudio } from "./usage.js";
|
|
42
44
|
import { energyVad } from "./vad.js";
|
|
45
|
+
import { bindWorkletNode, WORKLET_RELEASE } from "./worklet/release.js";
|
|
46
|
+
import { AUDIO_TAP_PROCESSOR, audioTapWorkletUrl } from "./worklet/tap.js";
|
|
43
47
|
export {
|
|
48
|
+
AUDIO_TAP_PROCESSOR,
|
|
44
49
|
RealtimeLedger,
|
|
45
50
|
SpeechEventStream,
|
|
46
51
|
TranscriptionEventStream,
|
|
47
52
|
VoiceRequestError,
|
|
53
|
+
WORKLET_RELEASE,
|
|
48
54
|
addAudioCounters,
|
|
49
55
|
askAgent,
|
|
50
56
|
assertNodeEcho,
|
|
57
|
+
audioTapWorkletUrl,
|
|
51
58
|
bindThinker,
|
|
59
|
+
bindWorkletNode,
|
|
52
60
|
blankUsage,
|
|
53
61
|
calibrateEcho,
|
|
54
62
|
captionsFromHistory,
|
|
@@ -68,6 +76,8 @@ export {
|
|
|
68
76
|
micOpen,
|
|
69
77
|
mintRealtimeToken,
|
|
70
78
|
openEchoMic,
|
|
79
|
+
openMixer,
|
|
80
|
+
openNodeAudio,
|
|
71
81
|
planDelivery,
|
|
72
82
|
planTruncation,
|
|
73
83
|
priceAudio,
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport type { CaptionHistoryEntry } from './captions.ts';\nexport { captionsFromHistory } from './captions.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport type { EchoCanceller, EchoControl, EchoMode, EchoReport, MicOpen } from './echo.ts';\nexport {\n assertNodeEcho,\n calibrateEcho,\n chooseCapture,\n echoRuntime,\n measureEcho,\n micOpen,\n openEchoMic,\n} from './echo.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n DeliveryPlan,\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeSocket,\n RealtimeToken,\n TruncationPlan,\n} from './realtime.ts';\nexport { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { AudioOut, BedChannel, Cue, CueSource, Soundscape } from './soundscape.ts';\nexport { followBed } from './soundscape.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type { VoiceAgentEvent, VoiceMessageChange } from './spoken.ts';\nexport type {\n BindThinkerOptions,\n BoundThinker,\n Thinker,\n ThinkerCaption,\n ThinkerPhase,\n ThinkerUpdate,\n} from './talker.ts';\nexport { askAgent, bindThinker, talkerTools } from './talker.ts';\nexport type { RealtimeConstraints } from './tokens.ts';\nexport { mintRealtimeToken } from './tokens.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n BargeIn,\n CaptionEvent,\n EchoRecommendation,\n VoiceConversation,\n VoiceCut,\n VoiceModels,\n VoicePhase,\n VoiceSession,\n VoiceTurnMeta,\n VoiceTurnOptions,\n} from './turns.ts';\nexport { voiceTurns } from './turns.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\n"],
|
|
5
|
-
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAE3B,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport type { CaptionHistoryEntry } from './captions.ts';\nexport { captionsFromHistory } from './captions.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport type { EchoCanceller, EchoControl, EchoMode, EchoReport, MicOpen } from './echo.ts';\nexport {\n assertNodeEcho,\n calibrateEcho,\n chooseCapture,\n echoRuntime,\n measureEcho,\n micOpen,\n openEchoMic,\n} from './echo.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type { Mixer, OpenMixerOptions } from './mixer.ts';\nexport { openMixer } from './mixer.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n NodeAudioEngine,\n NodeAudioModule,\n NodeAudioOptions,\n NodeAudioSession,\n} from './node-host.ts';\nexport { openNodeAudio } from './node-host.ts';\nexport type {\n DeliveryPlan,\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeSocket,\n RealtimeToken,\n TruncationPlan,\n} from './realtime.ts';\nexport { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { AudioOut, BedChannel, Cue, CueSource, Soundscape } from './soundscape.ts';\nexport { followBed } from './soundscape.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type { VoiceAgentEvent, VoiceMessageChange } from './spoken.ts';\nexport type {\n BindThinkerOptions,\n BoundThinker,\n Thinker,\n ThinkerCaption,\n ThinkerPhase,\n ThinkerUpdate,\n} from './talker.ts';\nexport { askAgent, bindThinker, talkerTools } from './talker.ts';\nexport type { RealtimeConstraints } from './tokens.ts';\nexport { mintRealtimeToken } from './tokens.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n BargeIn,\n CaptionEvent,\n EchoRecommendation,\n VoiceConversation,\n VoiceCut,\n VoiceModels,\n VoicePhase,\n VoiceSession,\n VoiceTurnMeta,\n VoiceTurnOptions,\n} from './turns.ts';\nexport { voiceTurns } from './turns.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\nexport { bindWorkletNode, WORKLET_RELEASE } from './worklet/release.ts';\nexport { AUDIO_TAP_PROCESSOR, audioTapWorkletUrl } from './worklet/tap.ts';\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAE3B,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAElC,SAAS,iBAAiB;AAiB1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAOP,SAAS,qBAAqB;AAY9B,SAAS,cAAc,gBAAgB,gBAAgB,sBAAsB;AAC7E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,iBAAiB;AAE1B,SAAS,yBAAyB;AAUlC,SAAS,UAAU,aAAa,mBAAmB;AAEnD,SAAS,yBAAyB;AASlC,SAAS,gCAAgC;AAazC,SAAS,kBAAkB;AAS3B,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB,uBAAuB;AACjD,SAAS,qBAAqB,0BAA0B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
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
|
+
}
|
package/dist/models.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/models.ts"],
|
|
4
|
-
"sourcesContent": ["import type { BaseModel } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from './audio.ts';\nimport type { RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost } from './usage.ts';\n\nexport type KnownTranscriptionApi =\n | 'openai-transcriptions'\n | 'openai-realtime-transcription'\n | 'xai-stt'\n | 'elevenlabs-scribe'\n | 'mistral-transcriptions'\n | 'fish-asr'\n | 'cloudflare-ai-run';\n\nexport type TranscriptionApi = KnownTranscriptionApi | (string & {});\n\nexport type KnownSpeechApi =\n | 'openai-speech'\n | 'xai-tts'\n | 'elevenlabs-tts'\n | 'mistral-speech'\n | 'fish-tts'\n | 'google-tts'\n | 'cloudflare-ai-run'\n | 'wavespeed';\n\nexport type SpeechApi = KnownSpeechApi | (string & {});\n\nexport type KnownRealtimeApi =\n | 'openai-realtime'\n | 'xai-realtime'\n | 'gemini-live'\n | 'elevenlabs-agents'\n | 'fish-agents';\n\nexport type RealtimeApi = KnownRealtimeApi | (string & {});\n\nexport type Delivery = 'interrupt' | 'when_idle' | 'silent';\n\nexport type DeliverySupport = 'native' | 'approximate' | 'unsupported';\n\nexport interface VoiceInfo {\n id: string;\n name: string;\n languages?: string[];\n}\n\nexport interface TranscriptionModel<TApi extends TranscriptionApi>\n extends Omit<BaseModel<TApi>, 'input'> {\n type: 'transcription';\n streaming: boolean;\n turnDetection: 'none' | 'vad' | 'semantic';\n inputFormats: AudioFormat[];\n languages?: string[];\n diarization?: boolean;\n audioCost: TranscriptionAudioCost;\n}\n\nexport interface SpeechModel<TApi extends SpeechApi> extends Omit<BaseModel<TApi>, 'input'> {\n type: 'speech';\n textStreaming: boolean;\n outputFormats: AudioFormat[];\n voices?: VoiceInfo[];\n cloning?: boolean;\n audioCost: SpeechAudioCost;\n}\n\nexport interface RealtimeModel<TApi extends RealtimeApi> extends Omit<BaseModel<TApi>, 'input'> {\n type: 'realtime';\n transports: ('websocket' | 'webrtc')[];\n toolCalls: 'blocking' | 'async';\n inputFormat: AudioFormat;\n outputFormat: AudioFormat;\n video?: { maxFps: number };\n maxSessionMs?: number;\n resumable: boolean;\n ephemeralAuth: boolean;\n voices: VoiceInfo[];\n cascade?: { stt: string; llm: string; tts: string };\n provisioning: 'none' | 'agent';\n delivery: Record<Delivery, DeliverySupport>;\n truncation: 'client' | 'server' | 'none';\n liveToolUpdate: boolean;\n toolTimeoutMs?: { min: number; max: number };\n audioCost: RealtimeAudioCost;\n}\n\ndeclare module '@earendil-works/pi-ai' {\n interface ModelTypeMap {\n transcription: TranscriptionModel<TranscriptionApi>;\n speech: SpeechModel<SpeechApi>;\n realtime: RealtimeModel<RealtimeApi>;\n }\n}\n\nexport type VoiceModel =\n | TranscriptionModel<TranscriptionApi>\n | SpeechModel<SpeechApi>\n | RealtimeModel<RealtimeApi>;\n\nexport type VoiceModelType = VoiceModel['type'];\n\nconst catalog = new Map<string, VoiceModel>();\n\nfunction catalogKey(type: string, provider: string, id: string): string {\n return `${type}\\0${provider}\\0${id}`;\n}\n\nexport function registerVoiceModel(model: VoiceModel): () => void {\n const key = catalogKey(model.type, model.provider, model.id);\n catalog.set(key, model);\n return () => {\n if (catalog.get(key) === model) catalog.delete(key);\n };\n}\n\nexport function getVoiceModel<TType extends VoiceModelType>(\n type: TType,\n provider: string,\n id: string\n): Extract<VoiceModel, { type: TType }> | undefined {\n return catalog.get(catalogKey(type, provider, id)) as\n | Extract<VoiceModel, { type: TType }>\n | undefined;\n}\n\nexport function listVoiceModels<TType extends VoiceModelType>(\n type: TType,\n provider?: string\n): Extract<VoiceModel, { type: TType }>[] {\n const found: Extract<VoiceModel, { type: TType }>[] = [];\n for (const model of catalog.values()) {\n if (model.type !== type) continue;\n if (provider !== undefined && model.provider !== provider) continue;\n found.push(model as Extract<VoiceModel, { type: TType }>);\n }\n return found;\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { BaseModel } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from './audio.ts';\nimport type { RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost } from './usage.ts';\n\nexport type KnownTranscriptionApi =\n | 'openai-transcriptions'\n | 'openai-realtime-transcription'\n | 'xai-stt'\n | 'elevenlabs-scribe'\n | 'mistral-transcriptions'\n | 'fish-asr'\n | 'cloudflare-ai-run'\n | 'wavespeed';\n\nexport type TranscriptionApi = KnownTranscriptionApi | (string & {});\n\nexport type KnownSpeechApi =\n | 'openai-speech'\n | 'xai-tts'\n | 'elevenlabs-tts'\n | 'mistral-speech'\n | 'fish-tts'\n | 'google-tts'\n | 'cloudflare-ai-run'\n | 'wavespeed';\n\nexport type SpeechApi = KnownSpeechApi | (string & {});\n\nexport type KnownRealtimeApi =\n | 'openai-realtime'\n | 'xai-realtime'\n | 'gemini-live'\n | 'elevenlabs-agents'\n | 'fish-agents';\n\nexport type RealtimeApi = KnownRealtimeApi | (string & {});\n\nexport type Delivery = 'interrupt' | 'when_idle' | 'silent';\n\nexport type DeliverySupport = 'native' | 'approximate' | 'unsupported';\n\nexport interface VoiceInfo {\n id: string;\n name: string;\n languages?: string[];\n}\n\nexport interface TranscriptionModel<TApi extends TranscriptionApi>\n extends Omit<BaseModel<TApi>, 'input'> {\n type: 'transcription';\n streaming: boolean;\n turnDetection: 'none' | 'vad' | 'semantic';\n inputFormats: AudioFormat[];\n languages?: string[];\n diarization?: boolean;\n audioCost: TranscriptionAudioCost;\n}\n\nexport interface SpeechModel<TApi extends SpeechApi> extends Omit<BaseModel<TApi>, 'input'> {\n type: 'speech';\n textStreaming: boolean;\n outputFormats: AudioFormat[];\n voices?: VoiceInfo[];\n cloning?: boolean;\n audioCost: SpeechAudioCost;\n}\n\nexport interface RealtimeModel<TApi extends RealtimeApi> extends Omit<BaseModel<TApi>, 'input'> {\n type: 'realtime';\n transports: ('websocket' | 'webrtc')[];\n toolCalls: 'blocking' | 'async';\n inputFormat: AudioFormat;\n outputFormat: AudioFormat;\n video?: { maxFps: number };\n maxSessionMs?: number;\n resumable: boolean;\n ephemeralAuth: boolean;\n voices: VoiceInfo[];\n cascade?: { stt: string; llm: string; tts: string };\n provisioning: 'none' | 'agent';\n delivery: Record<Delivery, DeliverySupport>;\n truncation: 'client' | 'server' | 'none';\n liveToolUpdate: boolean;\n toolTimeoutMs?: { min: number; max: number };\n audioCost: RealtimeAudioCost;\n}\n\ndeclare module '@earendil-works/pi-ai' {\n interface ModelTypeMap {\n transcription: TranscriptionModel<TranscriptionApi>;\n speech: SpeechModel<SpeechApi>;\n realtime: RealtimeModel<RealtimeApi>;\n }\n}\n\nexport type VoiceModel =\n | TranscriptionModel<TranscriptionApi>\n | SpeechModel<SpeechApi>\n | RealtimeModel<RealtimeApi>;\n\nexport type VoiceModelType = VoiceModel['type'];\n\nconst catalog = new Map<string, VoiceModel>();\n\nfunction catalogKey(type: string, provider: string, id: string): string {\n return `${type}\\0${provider}\\0${id}`;\n}\n\nexport function registerVoiceModel(model: VoiceModel): () => void {\n const key = catalogKey(model.type, model.provider, model.id);\n catalog.set(key, model);\n return () => {\n if (catalog.get(key) === model) catalog.delete(key);\n };\n}\n\nexport function getVoiceModel<TType extends VoiceModelType>(\n type: TType,\n provider: string,\n id: string\n): Extract<VoiceModel, { type: TType }> | undefined {\n return catalog.get(catalogKey(type, provider, id)) as\n | Extract<VoiceModel, { type: TType }>\n | undefined;\n}\n\nexport function listVoiceModels<TType extends VoiceModelType>(\n type: TType,\n provider?: string\n): Extract<VoiceModel, { type: TType }>[] {\n const found: Extract<VoiceModel, { type: TType }>[] = [];\n for (const model of catalog.values()) {\n if (model.type !== type) continue;\n if (provider !== undefined && model.provider !== provider) continue;\n found.push(model as Extract<VoiceModel, { type: TType }>);\n }\n return found;\n}\n"],
|
|
5
|
+
"mappings": "AAsGA,MAAM,UAAU,oBAAI,IAAwB;AAE5C,SAAS,WAAW,MAAc,UAAkB,IAAoB;AACtE,SAAO,GAAG,IAAI,KAAK,QAAQ,KAAK,EAAE;AACpC;AAEO,SAAS,mBAAmB,OAA+B;AAChE,QAAM,MAAM,WAAW,MAAM,MAAM,MAAM,UAAU,MAAM,EAAE;AAC3D,UAAQ,IAAI,KAAK,KAAK;AACtB,SAAO,MAAM;AACX,QAAI,QAAQ,IAAI,GAAG,MAAM,MAAO,SAAQ,OAAO,GAAG;AAAA,EACpD;AACF;AAEO,SAAS,cACd,MACA,UACA,IACkD;AAClD,SAAO,QAAQ,IAAI,WAAW,MAAM,UAAU,EAAE,CAAC;AAGnD;AAEO,SAAS,gBACd,MACA,UACwC;AACxC,QAAM,QAAgD,CAAC;AACvD,aAAW,SAAS,QAAQ,OAAO,GAAG;AACpC,QAAI,MAAM,SAAS,KAAM;AACzB,QAAI,aAAa,UAAa,MAAM,aAAa,SAAU;AAC3D,UAAM,KAAK,KAA6C;AAAA,EAC1D;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
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
|