@ai-ecoverse/pi-audio 1.26.0 → 1.27.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 +1 -1
- package/dist/echo.js +46 -2
- package/dist/echo.js.map +2 -2
- package/dist/index.js.map +2 -2
- package/dist/types/echo.d.ts +6 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/webrtc-aec/index.d.ts +19 -0
- package/dist/webrtc-aec/index.js +80 -0
- package/dist/webrtc-aec/index.js.map +7 -0
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
35
35
|
|
|
36
36
|
`@ai-ecoverse/pi-audio/elevenlabs` registers ElevenLabs speech adapters when `registerElevenLabs()` is called. `scribe_v2` posts multipart audio to `/speech-to-text`. `scribe_v2_realtime` streams base64 audio on `wss://…/speech-to-text/realtime` and commits turns with `turn_prediction` (`turnDetection: 'semantic'`). `eleven_flash_v2_5` and `eleven_multilingual_v2` post JSON to `/text-to-speech/{voice}/with-timestamps` for a string and stream text on `stream-input` for an async iterable, with character alignment. `eleven_v3` and `eleven_v4` stay on the HTTP path (`textStreaming: false`). Scribe prices are $0.22 per hour for batch and $0.39 per hour for realtime.
|
|
37
37
|
|
|
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`.
|
|
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`. `@ai-ecoverse/pi-audio/webrtc-aec` adapts `@ai-ecoverse/wasm-webrtc-audio-processing` (optional peer, WebRTC AEC3) into that canceller. `createWebRtcEcho({ wasm, sampleRate })` instantiates the module the caller compiled, which is what an AudioWorklet can do. `createNodeWebRtcEcho({ sampleRate })` loads the package's wasm on Node. Mic and reference chunks are converted to mono float at 16 kHz or 48 kHz, and any non-finite sample is replaced with 0 before `cancel`, including after the canceller's own NaN fix. `stats()` supplies `delayMs` and echo return loss enhancement. `calibrateEcho` takes that canceller as its last argument: it cancels the captured pair, keeps the measured residual, and uses those stats for `delayMs` and `cancellationDb` when they are finite. `voiceTurns` passes `options.echo` through `openEchoMic` before `listen`.
|
|
39
39
|
|
|
40
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
41
|
|
package/dist/echo.js
CHANGED
|
@@ -58,7 +58,7 @@ function measureEcho(reference, mic, sampleRate) {
|
|
|
58
58
|
recommendation: recommend(residualDb)
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
-
async function calibrateEcho(out, mic, probe, durationMs, signal) {
|
|
61
|
+
async function calibrateEcho(out, mic, probe, durationMs, signal, canceller) {
|
|
62
62
|
if (signal.aborted) throw new VoiceRequestError("aborted", "aborted");
|
|
63
63
|
out.bed.play(probe, 0);
|
|
64
64
|
const rate = { value: 0 };
|
|
@@ -68,11 +68,55 @@ async function calibrateEcho(out, mic, probe, durationMs, signal) {
|
|
|
68
68
|
const pair = await Promise.all([heard, reference]);
|
|
69
69
|
let used = rate.value;
|
|
70
70
|
if (used <= 0) used = 1;
|
|
71
|
-
|
|
71
|
+
const cleaned = applyCanceller(pair[0], pair[1], used, canceller);
|
|
72
|
+
return withStats(measureEcho(pair[1], cleaned.samples, used), cleaned.stats);
|
|
72
73
|
} finally {
|
|
73
74
|
out.bed.stop(0);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
function applyCanceller(mic, reference, sampleRate, canceller) {
|
|
78
|
+
if (canceller === void 0) return { samples: mic };
|
|
79
|
+
const cancelled = canceller.cancel(
|
|
80
|
+
floatChunk(mic, sampleRate),
|
|
81
|
+
floatChunk(reference, sampleRate)
|
|
82
|
+
);
|
|
83
|
+
return { samples: heardSamples(cancelled, mic), stats: readStats(canceller) };
|
|
84
|
+
}
|
|
85
|
+
function readStats(canceller) {
|
|
86
|
+
if (canceller.stats === void 0) return void 0;
|
|
87
|
+
return canceller.stats();
|
|
88
|
+
}
|
|
89
|
+
function heardSamples(chunk, fallback) {
|
|
90
|
+
const decoded = samplesOf(chunk);
|
|
91
|
+
if (decoded.length === 0) return fallback;
|
|
92
|
+
return Float32Array.from(decoded);
|
|
93
|
+
}
|
|
94
|
+
function floatChunk(samples, sampleRate) {
|
|
95
|
+
const data = new Uint8Array(samples.length * 4);
|
|
96
|
+
const view = new DataView(data.buffer);
|
|
97
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
98
|
+
view.setFloat32(index * 4, samples[index], true);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
data,
|
|
102
|
+
format: { encoding: "pcmf32", sampleRate, channels: 1 },
|
|
103
|
+
timestamp: 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function withStats(report, stats) {
|
|
107
|
+
if (stats === void 0) return report;
|
|
108
|
+
return {
|
|
109
|
+
delayMs: chosen(stats.delayMs, report.delayMs),
|
|
110
|
+
cancellationDb: chosen(stats.cancellationDb, report.cancellationDb),
|
|
111
|
+
residualDb: report.residualDb,
|
|
112
|
+
recommendation: report.recommendation
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function chosen(value, fallback) {
|
|
116
|
+
if (value === null) return fallback;
|
|
117
|
+
if (!Number.isFinite(value)) return fallback;
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
76
120
|
function quiet() {
|
|
77
121
|
return { delayMs: 0, cancellationDb: 80, residualDb: -80, recommendation: "interrupt" };
|
|
78
122
|
}
|
package/dist/echo.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/echo.ts"],
|
|
4
|
-
"sourcesContent": ["import { type AudioChunk, type AudioSource, chunkSeconds } from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { AudioOut, Cue } from './soundscape.ts';\nimport type { EchoRecommendation } from './turns.ts';\n\nexport type EchoMode = 'browser' | 'wasm' | 'none';\n\nexport interface EchoCanceller {\n cancel(mic: AudioChunk, reference: AudioChunk): AudioChunk;\n}\n\nexport interface EchoControl {\n mode: EchoMode;\n reference: AudioSource;\n canceller?: EchoCanceller;\n}\n\nexport interface EchoReport {\n delayMs: number;\n cancellationDb: number;\n residualDb: number;\n recommendation: EchoRecommendation;\n}\n\nexport interface MicOpen {\n echoCancellation: boolean | 'all';\n playback: 'webaudio' | 'rtc-loopback';\n}\n\nconst floor = 1e-8;\n\nexport function micOpen(mode: EchoMode, useAll: boolean): MicOpen {\n if (mode === 'browser') {\n let echoCancellation: boolean | 'all' = true;\n if (useAll) echoCancellation = 'all';\n return { echoCancellation, playback: 'rtc-loopback' };\n }\n return { echoCancellation: false, playback: 'webaudio' };\n}\n\nexport function echoRuntime(hasMediaDevices: boolean): 'node' | 'browser' {\n if (hasMediaDevices) return 'browser';\n return 'node';\n}\n\nexport function assertNodeEcho(mode: EchoMode, runtime: 'node' | 'browser'): void {\n if (runtime === 'node' && mode === 'browser') {\n throw new VoiceRequestError('error', 'Node echo control is wasm or none');\n }\n}\n\nexport function chooseCapture(tried: EchoMode, report: EchoReport): EchoMode {\n if (tried === 'none') return 'none';\n if (tried === 'wasm') return 'wasm';\n if (report.cancellationDb < 12) return 'wasm';\n return 'browser';\n}\n\nexport function openEchoMic(control: EchoControl, mic: AudioSource): AudioSource {\n if (control.mode === 'none' || control.mode === 'browser') return mic;\n const canceller = control.canceller;\n if (canceller === undefined) {\n throw new VoiceRequestError('error', 'wasm echo control needs a canceller');\n }\n return cancelStream(mic, control.reference, canceller);\n}\n\nexport function measureEcho(\n reference: Float32Array,\n mic: Float32Array,\n sampleRate: number\n): EchoReport {\n if (reference.length === 0) return quiet();\n if (mic.length === 0) return quiet();\n let rate = sampleRate;\n if (rate <= 0) rate = 1;\n const maxLag = Math.min(mic.length - 1, Math.max(0, Math.round(rate * 0.25)));\n let bestLag = 0;\n let best = -1;\n let bestGain = 0;\n for (let lag = 0; lag <= maxLag; lag += 1) {\n const scored = scoreLag(reference, mic, lag);\n if (scored.score > best) {\n best = scored.score;\n bestLag = lag;\n bestGain = scored.gain;\n }\n }\n const leak = Math.abs(bestGain);\n const residualDb = amplitudeDb(leak);\n return {\n delayMs: (bestLag / rate) * 1000,\n cancellationDb: -residualDb,\n residualDb,\n recommendation: recommend(residualDb),\n };\n}\n\nexport async function calibrateEcho(\n out: AudioOut,\n mic: AudioSource,\n probe: Cue,\n durationMs: number,\n signal: AbortSignal\n): Promise<EchoReport> {\n if (signal.aborted) throw new VoiceRequestError('aborted', 'aborted');\n out.bed.play(probe, 0);\n const rate = { value: 0 };\n try {\n const heard = readSamples(mic, durationMs, signal, rate);\n const reference = readSamples(out.reference, durationMs, signal, rate);\n const pair = await Promise.all([heard, reference]);\n let used = rate.value;\n if (used <= 0) used = 1;\n
|
|
5
|
-
"mappings": "AAAA,SAA4C,oBAAoB;AAChE,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import { type AudioChunk, type AudioSource, chunkSeconds } from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { AudioOut, Cue } from './soundscape.ts';\nimport type { EchoRecommendation } from './turns.ts';\n\nexport type EchoMode = 'browser' | 'wasm' | 'none';\n\nexport interface EchoStats {\n delayMs: number | null;\n cancellationDb: number | null;\n}\n\nexport interface EchoCanceller {\n cancel(mic: AudioChunk, reference: AudioChunk): AudioChunk;\n stats?(): EchoStats;\n}\n\nexport interface EchoControl {\n mode: EchoMode;\n reference: AudioSource;\n canceller?: EchoCanceller;\n}\n\nexport interface EchoReport {\n delayMs: number;\n cancellationDb: number;\n residualDb: number;\n recommendation: EchoRecommendation;\n}\n\nexport interface MicOpen {\n echoCancellation: boolean | 'all';\n playback: 'webaudio' | 'rtc-loopback';\n}\n\nconst floor = 1e-8;\n\nexport function micOpen(mode: EchoMode, useAll: boolean): MicOpen {\n if (mode === 'browser') {\n let echoCancellation: boolean | 'all' = true;\n if (useAll) echoCancellation = 'all';\n return { echoCancellation, playback: 'rtc-loopback' };\n }\n return { echoCancellation: false, playback: 'webaudio' };\n}\n\nexport function echoRuntime(hasMediaDevices: boolean): 'node' | 'browser' {\n if (hasMediaDevices) return 'browser';\n return 'node';\n}\n\nexport function assertNodeEcho(mode: EchoMode, runtime: 'node' | 'browser'): void {\n if (runtime === 'node' && mode === 'browser') {\n throw new VoiceRequestError('error', 'Node echo control is wasm or none');\n }\n}\n\nexport function chooseCapture(tried: EchoMode, report: EchoReport): EchoMode {\n if (tried === 'none') return 'none';\n if (tried === 'wasm') return 'wasm';\n if (report.cancellationDb < 12) return 'wasm';\n return 'browser';\n}\n\nexport function openEchoMic(control: EchoControl, mic: AudioSource): AudioSource {\n if (control.mode === 'none' || control.mode === 'browser') return mic;\n const canceller = control.canceller;\n if (canceller === undefined) {\n throw new VoiceRequestError('error', 'wasm echo control needs a canceller');\n }\n return cancelStream(mic, control.reference, canceller);\n}\n\nexport function measureEcho(\n reference: Float32Array,\n mic: Float32Array,\n sampleRate: number\n): EchoReport {\n if (reference.length === 0) return quiet();\n if (mic.length === 0) return quiet();\n let rate = sampleRate;\n if (rate <= 0) rate = 1;\n const maxLag = Math.min(mic.length - 1, Math.max(0, Math.round(rate * 0.25)));\n let bestLag = 0;\n let best = -1;\n let bestGain = 0;\n for (let lag = 0; lag <= maxLag; lag += 1) {\n const scored = scoreLag(reference, mic, lag);\n if (scored.score > best) {\n best = scored.score;\n bestLag = lag;\n bestGain = scored.gain;\n }\n }\n const leak = Math.abs(bestGain);\n const residualDb = amplitudeDb(leak);\n return {\n delayMs: (bestLag / rate) * 1000,\n cancellationDb: -residualDb,\n residualDb,\n recommendation: recommend(residualDb),\n };\n}\n\nexport async function calibrateEcho(\n out: AudioOut,\n mic: AudioSource,\n probe: Cue,\n durationMs: number,\n signal: AbortSignal,\n canceller?: EchoCanceller\n): Promise<EchoReport> {\n if (signal.aborted) throw new VoiceRequestError('aborted', 'aborted');\n out.bed.play(probe, 0);\n const rate = { value: 0 };\n try {\n const heard = readSamples(mic, durationMs, signal, rate);\n const reference = readSamples(out.reference, durationMs, signal, rate);\n const pair = await Promise.all([heard, reference]);\n let used = rate.value;\n if (used <= 0) used = 1;\n const cleaned = applyCanceller(pair[0], pair[1], used, canceller);\n return withStats(measureEcho(pair[1], cleaned.samples, used), cleaned.stats);\n } finally {\n out.bed.stop(0);\n }\n}\n\nfunction applyCanceller(\n mic: Float32Array,\n reference: Float32Array,\n sampleRate: number,\n canceller: EchoCanceller | undefined\n): { samples: Float32Array; stats?: EchoStats } {\n if (canceller === undefined) return { samples: mic };\n const cancelled = canceller.cancel(\n floatChunk(mic, sampleRate),\n floatChunk(reference, sampleRate)\n );\n return { samples: heardSamples(cancelled, mic), stats: readStats(canceller) };\n}\n\nfunction readStats(canceller: EchoCanceller): EchoStats | undefined {\n if (canceller.stats === undefined) return undefined;\n return canceller.stats();\n}\n\nfunction heardSamples(chunk: AudioChunk, fallback: Float32Array): Float32Array {\n const decoded = samplesOf(chunk);\n if (decoded.length === 0) return fallback;\n return Float32Array.from(decoded);\n}\n\nfunction floatChunk(samples: Float32Array, sampleRate: number): AudioChunk {\n const data = new Uint8Array(samples.length * 4);\n const view = new DataView(data.buffer);\n for (let index = 0; index < samples.length; index += 1) {\n view.setFloat32(index * 4, samples[index] as number, true);\n }\n return {\n data,\n format: { encoding: 'pcmf32', sampleRate, channels: 1 },\n timestamp: 0,\n };\n}\n\nfunction withStats(report: EchoReport, stats: EchoStats | undefined): EchoReport {\n if (stats === undefined) return report;\n return {\n delayMs: chosen(stats.delayMs, report.delayMs),\n cancellationDb: chosen(stats.cancellationDb, report.cancellationDb),\n residualDb: report.residualDb,\n recommendation: report.recommendation,\n };\n}\n\nfunction chosen(value: number | null, fallback: number): number {\n if (value === null) return fallback;\n if (!Number.isFinite(value)) return fallback;\n return value;\n}\n\nfunction quiet(): EchoReport {\n return { delayMs: 0, cancellationDb: 80, residualDb: -80, recommendation: 'interrupt' };\n}\n\nfunction recommend(residualDb: number): EchoRecommendation {\n if (residualDb <= -30) return 'interrupt';\n if (residualDb <= -12) return 'onWords';\n return 'headphones';\n}\n\nfunction scoreLag(\n reference: Float32Array,\n mic: Float32Array,\n lag: number\n): { score: number; gain: number } {\n let dot = 0;\n let energy = 0;\n const length = Math.min(reference.length, mic.length - lag);\n for (let index = 0; index < length; index += 1) {\n const ref = reference[index] as number;\n const heard = mic[index + lag] as number;\n dot += ref * heard;\n energy += ref * ref;\n }\n if (energy <= floor) return { score: -1, gain: 0 };\n return { score: Math.abs(dot), gain: dot / energy };\n}\n\nfunction amplitudeDb(amplitude: number): number {\n let safe = amplitude;\n if (safe <= floor) safe = floor;\n return 20 * Math.log10(safe);\n}\n\nasync function* cancelStream(\n mic: AudioSource,\n reference: AudioSource,\n canceller: EchoCanceller\n): AudioSource {\n const refs = reference[Symbol.asyncIterator]();\n for await (const chunk of mic) {\n const next = await refs.next();\n if (next.done) {\n yield chunk;\n continue;\n }\n yield canceller.cancel(chunk, next.value);\n }\n}\n\nasync function readSamples(\n source: AudioSource,\n durationMs: number,\n signal: AbortSignal,\n rate: { value: number }\n): Promise<Float32Array> {\n const samples: number[] = [];\n let heardMs = 0;\n for await (const chunk of source) {\n if (signal.aborted) throw new VoiceRequestError('aborted', 'aborted');\n if (rate.value === 0) rate.value = chunk.format.sampleRate;\n for (const sample of samplesOf(chunk)) samples.push(sample);\n heardMs += chunkSeconds(chunk) * 1000;\n if (heardMs >= durationMs) break;\n }\n return Float32Array.from(samples);\n}\n\nfunction samplesOf(chunk: AudioChunk): number[] {\n if (chunk.format.encoding === 'pcm16') return pcm16Samples(chunk.data);\n if (chunk.format.encoding === 'pcmf32') return pcmf32Samples(chunk.data);\n return [];\n}\n\nfunction pcm16Samples(data: Uint8Array): number[] {\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n const samples: number[] = [];\n const count = Math.floor(data.byteLength / 2);\n for (let index = 0; index < count; index += 1) {\n samples.push(view.getInt16(index * 2, true) / 32768);\n }\n return samples;\n}\n\nfunction pcmf32Samples(data: Uint8Array): number[] {\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n const samples: number[] = [];\n const count = Math.floor(data.byteLength / 4);\n for (let index = 0; index < count; index += 1) {\n samples.push(view.getFloat32(index * 4, true));\n }\n return samples;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
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 {
|
|
5
|
-
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAE3B,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
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 {\n EchoCanceller,\n EchoControl,\n EchoMode,\n EchoReport,\n EchoStats,\n MicOpen,\n} 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;AASP;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/types/echo.d.ts
CHANGED
|
@@ -2,8 +2,13 @@ import { type AudioChunk, type AudioSource } from './audio.ts';
|
|
|
2
2
|
import type { AudioOut, Cue } from './soundscape.ts';
|
|
3
3
|
import type { EchoRecommendation } from './turns.ts';
|
|
4
4
|
export type EchoMode = 'browser' | 'wasm' | 'none';
|
|
5
|
+
export interface EchoStats {
|
|
6
|
+
delayMs: number | null;
|
|
7
|
+
cancellationDb: number | null;
|
|
8
|
+
}
|
|
5
9
|
export interface EchoCanceller {
|
|
6
10
|
cancel(mic: AudioChunk, reference: AudioChunk): AudioChunk;
|
|
11
|
+
stats?(): EchoStats;
|
|
7
12
|
}
|
|
8
13
|
export interface EchoControl {
|
|
9
14
|
mode: EchoMode;
|
|
@@ -26,4 +31,4 @@ export declare function assertNodeEcho(mode: EchoMode, runtime: 'node' | 'browse
|
|
|
26
31
|
export declare function chooseCapture(tried: EchoMode, report: EchoReport): EchoMode;
|
|
27
32
|
export declare function openEchoMic(control: EchoControl, mic: AudioSource): AudioSource;
|
|
28
33
|
export declare function measureEcho(reference: Float32Array, mic: Float32Array, sampleRate: number): EchoReport;
|
|
29
|
-
export declare function calibrateEcho(out: AudioOut, mic: AudioSource, probe: Cue, durationMs: number, signal: AbortSignal): Promise<EchoReport>;
|
|
34
|
+
export declare function calibrateEcho(out: AudioOut, mic: AudioSource, probe: Cue, durationMs: number, signal: AbortSignal, canceller?: EchoCanceller): Promise<EchoReport>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { blankUsage } from './blank-usage.ts';
|
|
|
4
4
|
export type { CaptionHistoryEntry } from './captions.ts';
|
|
5
5
|
export { captionsFromHistory } from './captions.ts';
|
|
6
6
|
export { convertAudio, convertAudioSource, decodeAudioBase64, encodeAudioBase64, } from './convert.ts';
|
|
7
|
-
export type { EchoCanceller, EchoControl, EchoMode, EchoReport, MicOpen } from './echo.ts';
|
|
7
|
+
export type { EchoCanceller, EchoControl, EchoMode, EchoReport, EchoStats, 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
10
|
export type { Mixer, OpenMixerOptions } from './mixer.ts';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type WasmSource } from '@ai-ecoverse/wasm-webrtc-audio-processing';
|
|
2
|
+
import type { EchoCanceller, EchoStats } from '../echo.ts';
|
|
3
|
+
export interface WebRtcEchoOptions {
|
|
4
|
+
sampleRate: 16000 | 48000;
|
|
5
|
+
noiseSuppression?: boolean | 'low' | 'moderate' | 'high' | 'veryHigh';
|
|
6
|
+
vad?: boolean | 0 | 1 | 2 | 3;
|
|
7
|
+
wasm: WasmSource;
|
|
8
|
+
}
|
|
9
|
+
export interface WebRtcEcho extends EchoCanceller {
|
|
10
|
+
readonly sampleRate: 16000 | 48000;
|
|
11
|
+
readonly latencySamples: number;
|
|
12
|
+
stats(): EchoStats;
|
|
13
|
+
destroy(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface NodeWebRtcModule {
|
|
16
|
+
wasmModule(): WebAssembly.Module;
|
|
17
|
+
}
|
|
18
|
+
export declare function createWebRtcEcho(options: WebRtcEchoOptions): WebRtcEcho;
|
|
19
|
+
export declare function createNodeWebRtcEcho(options: Omit<WebRtcEchoOptions, 'wasm'>, load?: () => Promise<NodeWebRtcModule>): Promise<WebRtcEcho>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { create } from "@ai-ecoverse/wasm-webrtc-audio-processing";
|
|
2
|
+
import { convertAudio } from "../convert.js";
|
|
3
|
+
import { VoiceRequestError } from "../errors.js";
|
|
4
|
+
function createWebRtcEcho(options) {
|
|
5
|
+
const aec = create({
|
|
6
|
+
wasm: options.wasm,
|
|
7
|
+
sampleRate: options.sampleRate,
|
|
8
|
+
noiseSuppression: options.noiseSuppression,
|
|
9
|
+
vad: options.vad
|
|
10
|
+
});
|
|
11
|
+
return adapt(aec, options.sampleRate);
|
|
12
|
+
}
|
|
13
|
+
async function createNodeWebRtcEcho(options, load = loadNodeWebRtc) {
|
|
14
|
+
let loaded;
|
|
15
|
+
try {
|
|
16
|
+
loaded = await load();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (missingModule(error)) {
|
|
19
|
+
throw new VoiceRequestError("error", "WebRTC echo canceller is unavailable");
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
return createWebRtcEcho({ ...options, wasm: loaded.wasmModule() });
|
|
24
|
+
}
|
|
25
|
+
async function loadNodeWebRtc() {
|
|
26
|
+
return import("@ai-ecoverse/wasm-webrtc-audio-processing/node");
|
|
27
|
+
}
|
|
28
|
+
function missingModule(error) {
|
|
29
|
+
if (!(error instanceof Error)) return false;
|
|
30
|
+
if (!("code" in error)) return false;
|
|
31
|
+
return error.code === "ERR_MODULE_NOT_FOUND";
|
|
32
|
+
}
|
|
33
|
+
function adapt(aec, sampleRate) {
|
|
34
|
+
return {
|
|
35
|
+
sampleRate,
|
|
36
|
+
latencySamples: aec.latencySamples,
|
|
37
|
+
cancel(mic, reference) {
|
|
38
|
+
const cleaned = aec.cancel(floatsOf(mic, sampleRate), floatsOf(reference, sampleRate));
|
|
39
|
+
return pcmChunk(cleaned, mic, sampleRate);
|
|
40
|
+
},
|
|
41
|
+
stats() {
|
|
42
|
+
const live = aec.stats();
|
|
43
|
+
return { delayMs: live.delayMs, cancellationDb: live.erleDb };
|
|
44
|
+
},
|
|
45
|
+
destroy() {
|
|
46
|
+
aec.destroy();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function floatsOf(chunk, sampleRate) {
|
|
51
|
+
const pcm = convertAudio(chunk, { encoding: "pcmf32", sampleRate, channels: 1 });
|
|
52
|
+
const length = Math.floor(pcm.data.byteLength / 4);
|
|
53
|
+
const samples = new Float32Array(length);
|
|
54
|
+
const view = new DataView(pcm.data.buffer, pcm.data.byteOffset, pcm.data.byteLength);
|
|
55
|
+
for (let index = 0; index < length; index += 1) {
|
|
56
|
+
samples[index] = finiteSample(view.getFloat32(index * 4, true));
|
|
57
|
+
}
|
|
58
|
+
return samples;
|
|
59
|
+
}
|
|
60
|
+
function finiteSample(sample) {
|
|
61
|
+
if (Number.isFinite(sample)) return sample;
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
function pcmChunk(samples, mic, sampleRate) {
|
|
65
|
+
const data = new Uint8Array(samples.byteLength);
|
|
66
|
+
data.set(new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength));
|
|
67
|
+
return convertAudio(
|
|
68
|
+
{
|
|
69
|
+
data,
|
|
70
|
+
format: { encoding: "pcmf32", sampleRate, channels: 1 },
|
|
71
|
+
timestamp: mic.timestamp
|
|
72
|
+
},
|
|
73
|
+
mic.format
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
createNodeWebRtcEcho,
|
|
78
|
+
createWebRtcEcho
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/webrtc-aec/index.ts"],
|
|
4
|
+
"sourcesContent": ["import { create, type WasmSource } from '@ai-ecoverse/wasm-webrtc-audio-processing';\nimport type { AudioChunk } from '../audio.ts';\nimport { convertAudio } from '../convert.ts';\nimport type { EchoCanceller, EchoStats } from '../echo.ts';\nimport { VoiceRequestError } from '../errors.ts';\n\ntype Aec = ReturnType<typeof create>;\n\nexport interface WebRtcEchoOptions {\n sampleRate: 16000 | 48000;\n noiseSuppression?: boolean | 'low' | 'moderate' | 'high' | 'veryHigh';\n vad?: boolean | 0 | 1 | 2 | 3;\n wasm: WasmSource;\n}\n\nexport interface WebRtcEcho extends EchoCanceller {\n readonly sampleRate: 16000 | 48000;\n readonly latencySamples: number;\n stats(): EchoStats;\n destroy(): void;\n}\n\nexport interface NodeWebRtcModule {\n wasmModule(): WebAssembly.Module;\n}\n\nexport function createWebRtcEcho(options: WebRtcEchoOptions): WebRtcEcho {\n const aec = create({\n wasm: options.wasm,\n sampleRate: options.sampleRate,\n noiseSuppression: options.noiseSuppression,\n vad: options.vad,\n });\n return adapt(aec, options.sampleRate);\n}\n\nexport async function createNodeWebRtcEcho(\n options: Omit<WebRtcEchoOptions, 'wasm'>,\n load: () => Promise<NodeWebRtcModule> = loadNodeWebRtc\n): Promise<WebRtcEcho> {\n let loaded: NodeWebRtcModule;\n try {\n loaded = await load();\n } catch (error) {\n if (missingModule(error)) {\n throw new VoiceRequestError('error', 'WebRTC echo canceller is unavailable');\n }\n throw error;\n }\n return createWebRtcEcho({ ...options, wasm: loaded.wasmModule() });\n}\n\nasync function loadNodeWebRtc(): Promise<NodeWebRtcModule> {\n return import('@ai-ecoverse/wasm-webrtc-audio-processing/node');\n}\n\nfunction missingModule(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n if (!('code' in error)) return false;\n return error.code === 'ERR_MODULE_NOT_FOUND';\n}\n\nfunction adapt(aec: Aec, sampleRate: 16000 | 48000): WebRtcEcho {\n return {\n sampleRate,\n latencySamples: aec.latencySamples,\n cancel(mic: AudioChunk, reference: AudioChunk): AudioChunk {\n const cleaned = aec.cancel(floatsOf(mic, sampleRate), floatsOf(reference, sampleRate));\n return pcmChunk(cleaned, mic, sampleRate);\n },\n stats(): EchoStats {\n const live = aec.stats();\n return { delayMs: live.delayMs, cancellationDb: live.erleDb };\n },\n destroy(): void {\n aec.destroy();\n },\n };\n}\n\nfunction floatsOf(chunk: AudioChunk, sampleRate: 16000 | 48000): Float32Array {\n const pcm = convertAudio(chunk, { encoding: 'pcmf32', sampleRate, channels: 1 });\n const length = Math.floor(pcm.data.byteLength / 4);\n const samples = new Float32Array(length);\n const view = new DataView(pcm.data.buffer, pcm.data.byteOffset, pcm.data.byteLength);\n for (let index = 0; index < length; index += 1) {\n samples[index] = finiteSample(view.getFloat32(index * 4, true));\n }\n return samples;\n}\n\nfunction finiteSample(sample: number): number {\n if (Number.isFinite(sample)) return sample;\n return 0;\n}\n\nfunction pcmChunk(samples: Float32Array, mic: AudioChunk, sampleRate: 16000 | 48000): AudioChunk {\n const data = new Uint8Array(samples.byteLength);\n data.set(new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength));\n return convertAudio(\n {\n data,\n format: { encoding: 'pcmf32', sampleRate, channels: 1 },\n timestamp: mic.timestamp,\n },\n mic.format\n );\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,cAA+B;AAExC,SAAS,oBAAoB;AAE7B,SAAS,yBAAyB;AAsB3B,SAAS,iBAAiB,SAAwC;AACvE,QAAM,MAAM,OAAO;AAAA,IACjB,MAAM,QAAQ;AAAA,IACd,YAAY,QAAQ;AAAA,IACpB,kBAAkB,QAAQ;AAAA,IAC1B,KAAK,QAAQ;AAAA,EACf,CAAC;AACD,SAAO,MAAM,KAAK,QAAQ,UAAU;AACtC;AAEA,eAAsB,qBACpB,SACA,OAAwC,gBACnB;AACrB,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,KAAK;AAAA,EACtB,SAAS,OAAO;AACd,QAAI,cAAc,KAAK,GAAG;AACxB,YAAM,IAAI,kBAAkB,SAAS,sCAAsC;AAAA,IAC7E;AACA,UAAM;AAAA,EACR;AACA,SAAO,iBAAiB,EAAE,GAAG,SAAS,MAAM,OAAO,WAAW,EAAE,CAAC;AACnE;AAEA,eAAe,iBAA4C;AACzD,SAAO,OAAO,gDAAgD;AAChE;AAEA,SAAS,cAAc,OAAyB;AAC9C,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,MAAI,EAAE,UAAU,OAAQ,QAAO;AAC/B,SAAO,MAAM,SAAS;AACxB;AAEA,SAAS,MAAM,KAAU,YAAuC;AAC9D,SAAO;AAAA,IACL;AAAA,IACA,gBAAgB,IAAI;AAAA,IACpB,OAAO,KAAiB,WAAmC;AACzD,YAAM,UAAU,IAAI,OAAO,SAAS,KAAK,UAAU,GAAG,SAAS,WAAW,UAAU,CAAC;AACrF,aAAO,SAAS,SAAS,KAAK,UAAU;AAAA,IAC1C;AAAA,IACA,QAAmB;AACjB,YAAM,OAAO,IAAI,MAAM;AACvB,aAAO,EAAE,SAAS,KAAK,SAAS,gBAAgB,KAAK,OAAO;AAAA,IAC9D;AAAA,IACA,UAAgB;AACd,UAAI,QAAQ;AAAA,IACd;AAAA,EACF;AACF;AAEA,SAAS,SAAS,OAAmB,YAAyC;AAC5E,QAAM,MAAM,aAAa,OAAO,EAAE,UAAU,UAAU,YAAY,UAAU,EAAE,CAAC;AAC/E,QAAM,SAAS,KAAK,MAAM,IAAI,KAAK,aAAa,CAAC;AACjD,QAAM,UAAU,IAAI,aAAa,MAAM;AACvC,QAAM,OAAO,IAAI,SAAS,IAAI,KAAK,QAAQ,IAAI,KAAK,YAAY,IAAI,KAAK,UAAU;AACnF,WAAS,QAAQ,GAAG,QAAQ,QAAQ,SAAS,GAAG;AAC9C,YAAQ,KAAK,IAAI,aAAa,KAAK,WAAW,QAAQ,GAAG,IAAI,CAAC;AAAA,EAChE;AACA,SAAO;AACT;AAEA,SAAS,aAAa,QAAwB;AAC5C,MAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AACpC,SAAO;AACT;AAEA,SAAS,SAAS,SAAuB,KAAiB,YAAuC;AAC/F,QAAM,OAAO,IAAI,WAAW,QAAQ,UAAU;AAC9C,OAAK,IAAI,IAAI,WAAW,QAAQ,QAAQ,QAAQ,YAAY,QAAQ,UAAU,CAAC;AAC/E,SAAO;AAAA,IACL;AAAA,MACE;AAAA,MACA,QAAQ,EAAE,UAAU,UAAU,YAAY,UAAU,EAAE;AAAA,MACtD,WAAW,IAAI;AAAA,IACjB;AAAA,IACA,IAAI;AAAA,EACN;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/pi-audio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -89,6 +89,10 @@
|
|
|
89
89
|
"./brain": {
|
|
90
90
|
"types": "./dist/types/brain/index.d.ts",
|
|
91
91
|
"default": "./dist/brain/index.js"
|
|
92
|
+
},
|
|
93
|
+
"./webrtc-aec": {
|
|
94
|
+
"types": "./dist/types/webrtc-aec/index.d.ts",
|
|
95
|
+
"default": "./dist/webrtc-aec/index.js"
|
|
92
96
|
}
|
|
93
97
|
},
|
|
94
98
|
"files": [
|
|
@@ -110,6 +114,7 @@
|
|
|
110
114
|
},
|
|
111
115
|
"devDependencies": {
|
|
112
116
|
"@ai-ecoverse/slicc-shared-web": "1.21.1",
|
|
117
|
+
"@ai-ecoverse/wasm-webrtc-audio-processing": "2.1.0-1",
|
|
113
118
|
"@biomejs/biome": "2.5.15",
|
|
114
119
|
"@earendil-works/pi-ai": "1.1.0",
|
|
115
120
|
"@earendil-works/pi-durable": "1.1.0",
|
|
@@ -123,12 +128,16 @@
|
|
|
123
128
|
"typescript": "7.0.2"
|
|
124
129
|
},
|
|
125
130
|
"peerDependencies": {
|
|
131
|
+
"@ai-ecoverse/wasm-webrtc-audio-processing": ">=2.1.0-1 <2.2.0",
|
|
126
132
|
"@earendil-works/pi-ai": "1.1.0",
|
|
127
133
|
"@earendil-works/pi-durable": "^1.1.0",
|
|
128
134
|
"node-web-audio-api": "^2.2.0",
|
|
129
135
|
"web-audio-api": "^1.6.0"
|
|
130
136
|
},
|
|
131
137
|
"peerDependenciesMeta": {
|
|
138
|
+
"@ai-ecoverse/wasm-webrtc-audio-processing": {
|
|
139
|
+
"optional": true
|
|
140
|
+
},
|
|
132
141
|
"@earendil-works/pi-durable": {
|
|
133
142
|
"optional": true
|
|
134
143
|
},
|