@pipedog/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio/resamplers/engine.d.ts +60 -0
- package/dist/audio/resamplers/engine.js +89 -0
- package/dist/audio/resamplers/sinc_engine.d.ts +10 -0
- package/dist/audio/resamplers/sinc_engine.js +235 -0
- package/dist/audio/resamplers/soxr_resampler.d.ts +10 -6
- package/dist/audio/resamplers/soxr_resampler.js +17 -7
- package/dist/audio/resamplers/soxr_stream_resampler.d.ts +9 -6
- package/dist/audio/resamplers/soxr_stream_resampler.js +24 -11
- package/dist/audio/turn/smart_turn/local_smart_turn_v3.js +16 -8
- package/dist/audio/vad/vad_analyzer.d.ts +11 -3
- package/dist/audio/vad/vad_analyzer.js +72 -34
- package/dist/pipeline/worker.d.ts +7 -0
- package/dist/pipeline/worker.js +40 -23
- package/dist/utils/file_storage.d.ts +37 -21
- package/dist/utils/file_storage.js +87 -9
- package/dist/utils/frame_queue.js +2 -2
- package/dist/utils/image.d.ts +0 -1
- package/dist/utils/image.js +37 -4
- package/dist/utils/jpeg_encoder.d.ts +14 -0
- package/dist/utils/jpeg_encoder.js +315 -0
- package/dist/utils/text/transforms/acronyms.d.ts +21 -0
- package/dist/utils/text/transforms/acronyms.js +31 -0
- package/dist/utils/text/transforms/currency.d.ts +16 -0
- package/dist/utils/text/transforms/currency.js +56 -0
- package/dist/utils/text/transforms/dates.d.ts +18 -0
- package/dist/utils/text/transforms/dates.js +102 -0
- package/dist/utils/text/transforms/email.d.ts +17 -0
- package/dist/utils/text/transforms/email.js +35 -0
- package/dist/utils/text/transforms/index.d.ts +37 -0
- package/dist/utils/text/transforms/index.js +42 -0
- package/dist/utils/text/transforms/numbers.d.ts +25 -0
- package/dist/utils/text/transforms/numbers.js +59 -0
- package/dist/utils/text/transforms/percentages.d.ts +16 -0
- package/dist/utils/text/transforms/percentages.js +32 -0
- package/dist/utils/text/transforms/phone.d.ts +21 -0
- package/dist/utils/text/transforms/phone.js +39 -0
- package/dist/utils/text/transforms/pronunciations.js +2 -1
- package/dist/utils/text/transforms/replacements.d.ts +39 -0
- package/dist/utils/text/transforms/replacements.js +46 -0
- package/dist/utils/text/transforms/strip_markdown.d.ts +25 -0
- package/dist/utils/text/transforms/strip_markdown.js +60 -0
- package/dist/utils/text/transforms/units.d.ts +22 -0
- package/dist/utils/text/transforms/units.js +81 -0
- package/dist/utils/text/transforms/voice_formatter.d.ts +86 -0
- package/dist/utils/text/transforms/voice_formatter.js +98 -0
- package/dist/utils/text/unicode_digits.d.ts +3 -0
- package/dist/utils/text/unicode_digits.js +34 -0
- package/dist/utils/tracing/service_decorators.js +2 -2
- package/dist/utils/tracing/setup.d.ts +18 -6
- package/dist/utils/tracing/setup.js +63 -13
- package/dist/utils/tracing/turn_trace_observer.d.ts +5 -0
- package/dist/utils/tracing/turn_trace_observer.js +18 -4
- package/dist/utils/yaml.d.ts +151 -36
- package/dist/utils/yaml.js +527 -12
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workers/proxy/index.d.ts +3 -0
- package/dist/workers/proxy/index.js +9 -0
- package/dist/workers/proxy/websocket/client.d.ts +110 -0
- package/dist/workers/proxy/websocket/client.js +196 -0
- package/dist/workers/proxy/websocket/index.d.ts +3 -0
- package/dist/workers/proxy/websocket/index.js +10 -0
- package/package.json +18 -3
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { SoxrQuality } from "./base_audio_resampler.ts";
|
|
2
|
+
/** Options of a streaming resampler (as `soxr.ResampleStream(...)`). */
|
|
3
|
+
export interface ResamplerStreamOptions {
|
|
4
|
+
inRate: number;
|
|
5
|
+
outRate: number;
|
|
6
|
+
numChannels: number;
|
|
7
|
+
quality: SoxrQuality;
|
|
8
|
+
}
|
|
9
|
+
/** A stateful streaming resampler; stands in for `soxr.ResampleStream`. */
|
|
10
|
+
export interface ResamplerStream {
|
|
11
|
+
/**
|
|
12
|
+
* Resample a chunk of interleaved samples, keeping the filter history for
|
|
13
|
+
* the next chunk. With `last` the audio held in the filter is appended and
|
|
14
|
+
* the stream starts over.
|
|
15
|
+
*/
|
|
16
|
+
resampleChunk<T extends Int16Array | Float32Array>(chunk: T, last?: boolean): T;
|
|
17
|
+
/** Discard the filter history (`soxr.ResampleStream.clear()`). */
|
|
18
|
+
clear(): void;
|
|
19
|
+
/** Free any native/WASM state. Optional. */
|
|
20
|
+
destroy?(): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A resampling engine. Structurally identical to what
|
|
24
|
+
* `createSoxrCompatibleEngine()` from `@pipedog/resampler-wasm` returns.
|
|
25
|
+
*/
|
|
26
|
+
export interface ResamplerEngine {
|
|
27
|
+
/** Stands in for `soxr.resample(audio, in_rate, out_rate, quality=...)` (mono). */
|
|
28
|
+
resample<T extends Int16Array | Float32Array>(audio: T, inRate: number, outRate: number, quality: SoxrQuality): T;
|
|
29
|
+
/** Stands in for `soxr.ResampleStream(in_rate=, out_rate=, num_channels=, quality=)`. */
|
|
30
|
+
createStream(options: ResamplerStreamOptions): ResamplerStream;
|
|
31
|
+
}
|
|
32
|
+
type EngineLoader = () => Promise<ResamplerEngine>;
|
|
33
|
+
/**
|
|
34
|
+
* Install the engine every SOXR resampler uses from now on (streams already
|
|
35
|
+
* created keep theirs). Pass `null` to go back to the default
|
|
36
|
+
* (`@pipedog/resampler-wasm`, with the pure-TypeScript fallback).
|
|
37
|
+
*/
|
|
38
|
+
export declare function setResamplerEngine(engine: ResamplerEngine | null): void;
|
|
39
|
+
/**
|
|
40
|
+
* The engine in use, loading the default one on first call.
|
|
41
|
+
*
|
|
42
|
+
* Resolves to the injected engine if there is one, else the WASM engine, else
|
|
43
|
+
* (after one warning) the pure-TypeScript fallback. Never rejects.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getResamplerEngine(): Promise<ResamplerEngine>;
|
|
46
|
+
/**
|
|
47
|
+
* The engine in use, without loading anything: the injected engine, else the
|
|
48
|
+
* default one if it has finished loading, else `null`. Lets synchronous code
|
|
49
|
+
* (e.g. `soxrResample`) use the engine once `getResamplerEngine()` resolved.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getLoadedResamplerEngine(): ResamplerEngine | null;
|
|
52
|
+
/**
|
|
53
|
+
* Replace how the default engine is loaded and forget the loaded one. For
|
|
54
|
+
* tests (e.g. simulating a WASM load failure).
|
|
55
|
+
*
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
export declare function _resetResamplerEngineForTests(loader?: EngineLoader): void;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) 2026, pipedog contributors
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
/**
|
|
4
|
+
* The resampling engine behind `SOXRAudioResampler` and
|
|
5
|
+
* `SOXRStreamAudioResampler`.
|
|
6
|
+
*
|
|
7
|
+
* Upstream calls python-soxr (`soxr.resample()` and `soxr.ResampleStream`).
|
|
8
|
+
* Here those calls go through a small engine interface, so the actual
|
|
9
|
+
* implementation can be swapped:
|
|
10
|
+
*
|
|
11
|
+
* - By default the engine is `@pipedog/resampler-wasm` (rubato windowed sinc
|
|
12
|
+
* compiled to WebAssembly), loaded lazily on first use. On Cloudflare
|
|
13
|
+
* Workers the package's "workerd" export condition imports the `.wasm` as a
|
|
14
|
+
* module (workerd forbids compiling WASM from bytes); elsewhere it is
|
|
15
|
+
* fetched/read next to the package and instantiated asynchronously.
|
|
16
|
+
* - If the WASM engine cannot be loaded, core logs one warning and falls back
|
|
17
|
+
* to a pure-TypeScript windowed-sinc engine (`sinc_engine.ts`).
|
|
18
|
+
* - Applications can inject their own engine (for example a native libsoxr
|
|
19
|
+
* binding) with `setResamplerEngine()`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { setResamplerEngine } from "@pipedog/core/audio/resamplers/engine";
|
|
24
|
+
* setResamplerEngine(myNativeSoxrEngine);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { logger } from "../../utils/logger.js";
|
|
28
|
+
import { createSincResamplerEngine } from "./sinc_engine.js";
|
|
29
|
+
/** Load `@pipedog/resampler-wasm` and wrap it in the soxr-shaped interface. */
|
|
30
|
+
async function loadWasmEngine() {
|
|
31
|
+
const wasm = await import("@pipedog/resampler-wasm");
|
|
32
|
+
return wasm.createSoxrCompatibleEngine(await wasm.initResampler());
|
|
33
|
+
}
|
|
34
|
+
let injected = null;
|
|
35
|
+
let loaded = null;
|
|
36
|
+
let loading = null;
|
|
37
|
+
let defaultLoader = loadWasmEngine;
|
|
38
|
+
/**
|
|
39
|
+
* Install the engine every SOXR resampler uses from now on (streams already
|
|
40
|
+
* created keep theirs). Pass `null` to go back to the default
|
|
41
|
+
* (`@pipedog/resampler-wasm`, with the pure-TypeScript fallback).
|
|
42
|
+
*/
|
|
43
|
+
export function setResamplerEngine(engine) {
|
|
44
|
+
injected = engine;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The engine in use, loading the default one on first call.
|
|
48
|
+
*
|
|
49
|
+
* Resolves to the injected engine if there is one, else the WASM engine, else
|
|
50
|
+
* (after one warning) the pure-TypeScript fallback. Never rejects.
|
|
51
|
+
*/
|
|
52
|
+
export async function getResamplerEngine() {
|
|
53
|
+
if (injected)
|
|
54
|
+
return injected;
|
|
55
|
+
if (loaded)
|
|
56
|
+
return loaded;
|
|
57
|
+
loading ??= defaultLoader().then((engine) => {
|
|
58
|
+
loaded = engine;
|
|
59
|
+
return engine;
|
|
60
|
+
}, (e) => {
|
|
61
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
62
|
+
logger.warning(`Could not load the WASM resampler (@pipedog/resampler-wasm): ${reason}. ` +
|
|
63
|
+
"Falling back to the pure-TypeScript windowed-sinc resampler (lower quality, slower).");
|
|
64
|
+
loaded = createSincResamplerEngine();
|
|
65
|
+
return loaded;
|
|
66
|
+
});
|
|
67
|
+
return injected ?? (await loading);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The engine in use, without loading anything: the injected engine, else the
|
|
71
|
+
* default one if it has finished loading, else `null`. Lets synchronous code
|
|
72
|
+
* (e.g. `soxrResample`) use the engine once `getResamplerEngine()` resolved.
|
|
73
|
+
*/
|
|
74
|
+
export function getLoadedResamplerEngine() {
|
|
75
|
+
return injected ?? loaded;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Replace how the default engine is loaded and forget the loaded one. For
|
|
79
|
+
* tests (e.g. simulating a WASM load failure).
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export function _resetResamplerEngineForTests(loader = loadWasmEngine) {
|
|
84
|
+
injected = null;
|
|
85
|
+
loaded = null;
|
|
86
|
+
loading = null;
|
|
87
|
+
defaultLoader = loader;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ResamplerEngine } from "./engine.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Create the pure-TypeScript windowed-sinc engine.
|
|
4
|
+
*
|
|
5
|
+
* Applications normally never call this: core falls back to it on its own
|
|
6
|
+
* when the WASM engine cannot load. It can also be installed explicitly with
|
|
7
|
+
* `setResamplerEngine(createSincResamplerEngine())`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createSincResamplerEngine(): ResamplerEngine;
|
|
10
|
+
//# sourceMappingURL=sinc_engine.d.ts.map
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Copyright (c) 2026, pipedog contributors
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
const SPECS = {
|
|
4
|
+
QQ: { passband: 0.8, attenuation: 50 },
|
|
5
|
+
LQ: { passband: 0.676, attenuation: 96 },
|
|
6
|
+
MQ: { passband: 0.91, attenuation: 96 },
|
|
7
|
+
HQ: { passband: 0.91, attenuation: 120 },
|
|
8
|
+
VHQ: { passband: 0.91, attenuation: 140 },
|
|
9
|
+
};
|
|
10
|
+
/** Kaiser window design (Kaiser's formulas for beta and length). */
|
|
11
|
+
function design({ passband, attenuation }) {
|
|
12
|
+
const transition = 1 - passband; // in units of the lower Nyquist frequency
|
|
13
|
+
const cutoff = (1 + passband) / 2;
|
|
14
|
+
const beta = attenuation > 50
|
|
15
|
+
? 0.1102 * (attenuation - 8.7)
|
|
16
|
+
: 0.5842 * (attenuation - 21) ** 0.4 + 0.07886 * (attenuation - 21);
|
|
17
|
+
// Total length in samples of the lower rate, then in zero crossings per side.
|
|
18
|
+
const length = (attenuation - 7.95) / (2.285 * Math.PI * transition);
|
|
19
|
+
const zeros = Math.ceil((length / 2) * cutoff);
|
|
20
|
+
return { zeros, cutoff, beta };
|
|
21
|
+
}
|
|
22
|
+
const PRESETS = Object.fromEntries(Object.entries(SPECS).map(([q, spec]) => [q, design(spec)]));
|
|
23
|
+
/** Table points per zero crossing (linear interpolation error ~ -118 dB). */
|
|
24
|
+
const OVERSAMPLE = 1024;
|
|
25
|
+
/** Zeroth-order modified Bessel function of the first kind. */
|
|
26
|
+
function besselI0(x) {
|
|
27
|
+
let sum = 1;
|
|
28
|
+
let term = 1;
|
|
29
|
+
const q = (x * x) / 4;
|
|
30
|
+
for (let k = 1; k < 200; k++) {
|
|
31
|
+
term *= q / (k * k);
|
|
32
|
+
sum += term;
|
|
33
|
+
if (term < sum * 1e-17)
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
return sum;
|
|
37
|
+
}
|
|
38
|
+
const TABLES = new Map();
|
|
39
|
+
/** sinc(x) * kaiser(x / zeros) for x in [0, zeros], OVERSAMPLE points per unit (plus guard). */
|
|
40
|
+
function kernelTable(quality) {
|
|
41
|
+
let table = TABLES.get(quality);
|
|
42
|
+
if (table)
|
|
43
|
+
return table;
|
|
44
|
+
const { zeros, beta } = PRESETS[quality];
|
|
45
|
+
const n = zeros * OVERSAMPLE;
|
|
46
|
+
table = new Float32Array(n + 2);
|
|
47
|
+
const i0b = besselI0(beta);
|
|
48
|
+
for (let i = 0; i <= n; i++) {
|
|
49
|
+
const x = i / OVERSAMPLE;
|
|
50
|
+
const r = x / zeros;
|
|
51
|
+
const w = besselI0(beta * Math.sqrt(Math.max(0, 1 - r * r))) / i0b;
|
|
52
|
+
const s = i === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
|
|
53
|
+
table[i] = s * w;
|
|
54
|
+
}
|
|
55
|
+
TABLES.set(quality, table);
|
|
56
|
+
return table;
|
|
57
|
+
}
|
|
58
|
+
function checkRates(inRate, outRate) {
|
|
59
|
+
if (!(inRate > 0) || !(outRate > 0) || !Number.isFinite(inRate) || !Number.isFinite(outRate)) {
|
|
60
|
+
throw new Error(`invalid sample rates: ${inRate}->${outRate}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** One channel of a streaming windowed-sinc resampler. */
|
|
64
|
+
class SincChannel {
|
|
65
|
+
#step; // input samples per output sample
|
|
66
|
+
#scale; // 2 * fc (fc in cycles per input sample)
|
|
67
|
+
#halfWidth; // kernel half width in input samples
|
|
68
|
+
#table;
|
|
69
|
+
#tableScale; // table index per input-sample distance
|
|
70
|
+
/** Input history; #buf[0] is absolute input index #base. */
|
|
71
|
+
#buf = new Float64Array(256);
|
|
72
|
+
#len = 0;
|
|
73
|
+
#base = 0;
|
|
74
|
+
/** Total input samples seen and next output index (absolute). */
|
|
75
|
+
#totalIn = 0;
|
|
76
|
+
#nextOut = 0;
|
|
77
|
+
constructor(inRate, outRate, quality) {
|
|
78
|
+
const preset = PRESETS[quality];
|
|
79
|
+
this.#step = inRate / outRate;
|
|
80
|
+
const fc = 0.5 * Math.min(1, outRate / inRate) * preset.cutoff;
|
|
81
|
+
this.#scale = 2 * fc;
|
|
82
|
+
this.#halfWidth = preset.zeros / this.#scale;
|
|
83
|
+
this.#table = kernelTable(quality);
|
|
84
|
+
this.#tableScale = this.#scale * OVERSAMPLE;
|
|
85
|
+
}
|
|
86
|
+
#append(samples, start, stride, count) {
|
|
87
|
+
if (this.#len + count > this.#buf.length) {
|
|
88
|
+
const grown = new Float64Array(Math.max(this.#buf.length * 2, this.#len + count));
|
|
89
|
+
grown.set(this.#buf.subarray(0, this.#len));
|
|
90
|
+
this.#buf = grown;
|
|
91
|
+
}
|
|
92
|
+
for (let i = 0; i < count; i++) {
|
|
93
|
+
this.#buf[this.#len + i] = samples[start + i * stride];
|
|
94
|
+
}
|
|
95
|
+
this.#len += count;
|
|
96
|
+
this.#totalIn += count;
|
|
97
|
+
}
|
|
98
|
+
#sampleAt(t) {
|
|
99
|
+
const lo = Math.ceil(t - this.#halfWidth);
|
|
100
|
+
const hi = Math.floor(t + this.#halfWidth);
|
|
101
|
+
const buf = this.#buf;
|
|
102
|
+
const base = this.#base;
|
|
103
|
+
const table = this.#table;
|
|
104
|
+
const ts = this.#tableScale;
|
|
105
|
+
const tableMax = table.length - 2;
|
|
106
|
+
let acc = 0;
|
|
107
|
+
const from = Math.max(lo, base);
|
|
108
|
+
const to = Math.min(hi, base + this.#len - 1);
|
|
109
|
+
for (let n = from; n <= to; n++) {
|
|
110
|
+
const pos = Math.abs(t - n) * ts;
|
|
111
|
+
const i = pos | 0;
|
|
112
|
+
if (i > tableMax)
|
|
113
|
+
continue;
|
|
114
|
+
const frac = pos - i;
|
|
115
|
+
const k = table[i] + (table[i + 1] - table[i]) * frac;
|
|
116
|
+
acc += buf[n - base] * k;
|
|
117
|
+
}
|
|
118
|
+
return acc * this.#scale;
|
|
119
|
+
}
|
|
120
|
+
/** Number of outputs computable now (or, with `end`, the remaining outputs of the stream). */
|
|
121
|
+
#available(end) {
|
|
122
|
+
if (end) {
|
|
123
|
+
const total = Math.round(this.#totalIn / this.#step);
|
|
124
|
+
return Math.max(0, total - this.#nextOut);
|
|
125
|
+
}
|
|
126
|
+
// Output k needs inputs up to floor(k*step + halfWidth) < totalIn.
|
|
127
|
+
const last = Math.floor((this.#totalIn - 1 - this.#halfWidth) / this.#step);
|
|
128
|
+
return Math.max(0, last + 1 - this.#nextOut);
|
|
129
|
+
}
|
|
130
|
+
/** Feed `count` samples (strided) and write the outputs produced to `out` (strided). */
|
|
131
|
+
process(samples, start, stride, count, end) {
|
|
132
|
+
if (count > 0)
|
|
133
|
+
this.#append(samples, start, stride, count);
|
|
134
|
+
const n = this.#available(end);
|
|
135
|
+
const out = new Float64Array(n);
|
|
136
|
+
for (let i = 0; i < n; i++) {
|
|
137
|
+
out[i] = this.#sampleAt((this.#nextOut + i) * this.#step);
|
|
138
|
+
}
|
|
139
|
+
this.#nextOut += n;
|
|
140
|
+
if (end) {
|
|
141
|
+
this.clear();
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// Drop history no future output can reach.
|
|
145
|
+
const keepFrom = Math.floor(this.#nextOut * this.#step - this.#halfWidth) - 1;
|
|
146
|
+
const drop = Math.min(this.#len, keepFrom - this.#base);
|
|
147
|
+
if (drop > 0) {
|
|
148
|
+
this.#buf.copyWithin(0, drop, this.#len);
|
|
149
|
+
this.#len -= drop;
|
|
150
|
+
this.#base += drop;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
clear() {
|
|
156
|
+
this.#len = 0;
|
|
157
|
+
this.#base = 0;
|
|
158
|
+
this.#totalIn = 0;
|
|
159
|
+
this.#nextOut = 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function toInt16(x) {
|
|
163
|
+
const r = Math.round(x);
|
|
164
|
+
return r > 32767 ? 32767 : r < -32768 ? -32768 : r;
|
|
165
|
+
}
|
|
166
|
+
/** A streaming windowed-sinc resampler over interleaved samples. */
|
|
167
|
+
class SincStream {
|
|
168
|
+
#channels;
|
|
169
|
+
#numChannels;
|
|
170
|
+
/** Samples of an incomplete frame carried to the next call. */
|
|
171
|
+
#pending = [];
|
|
172
|
+
constructor(options) {
|
|
173
|
+
checkRates(options.inRate, options.outRate);
|
|
174
|
+
const numChannels = options.numChannels;
|
|
175
|
+
if (!Number.isInteger(numChannels) || numChannels < 1) {
|
|
176
|
+
throw new Error(`invalid channel count: ${numChannels}`);
|
|
177
|
+
}
|
|
178
|
+
this.#numChannels = numChannels;
|
|
179
|
+
this.#channels = Array.from({ length: numChannels }, () => new SincChannel(options.inRate, options.outRate, options.quality));
|
|
180
|
+
}
|
|
181
|
+
resampleChunk(chunk, last = false) {
|
|
182
|
+
const c = this.#numChannels;
|
|
183
|
+
let input = chunk;
|
|
184
|
+
if (this.#pending.length > 0) {
|
|
185
|
+
input = [...this.#pending, ...chunk];
|
|
186
|
+
this.#pending = [];
|
|
187
|
+
}
|
|
188
|
+
const frames = Math.floor(input.length / c);
|
|
189
|
+
const rest = input.length - frames * c;
|
|
190
|
+
if (rest > 0 && !last) {
|
|
191
|
+
this.#pending = Array.from({ length: rest }, (_, i) => input[frames * c + i]);
|
|
192
|
+
}
|
|
193
|
+
const outs = this.#channels.map((ch, i) => ch.process(input, i, c, frames, last));
|
|
194
|
+
const outFrames = outs[0]?.length ?? 0;
|
|
195
|
+
const isFloat = chunk instanceof Float32Array;
|
|
196
|
+
const out = isFloat ? new Float32Array(outFrames * c) : new Int16Array(outFrames * c);
|
|
197
|
+
for (let ch = 0; ch < c; ch++) {
|
|
198
|
+
const data = outs[ch];
|
|
199
|
+
for (let f = 0; f < outFrames; f++) {
|
|
200
|
+
const v = data[f];
|
|
201
|
+
out[f * c + ch] = isFloat ? v : toInt16(v);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (last)
|
|
205
|
+
this.#pending = [];
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
clear() {
|
|
209
|
+
this.#pending = [];
|
|
210
|
+
for (const ch of this.#channels)
|
|
211
|
+
ch.clear();
|
|
212
|
+
}
|
|
213
|
+
destroy() { }
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Create the pure-TypeScript windowed-sinc engine.
|
|
217
|
+
*
|
|
218
|
+
* Applications normally never call this: core falls back to it on its own
|
|
219
|
+
* when the WASM engine cannot load. It can also be installed explicitly with
|
|
220
|
+
* `setResamplerEngine(createSincResamplerEngine())`.
|
|
221
|
+
*/
|
|
222
|
+
export function createSincResamplerEngine() {
|
|
223
|
+
return {
|
|
224
|
+
resample(audio, inRate, outRate, quality) {
|
|
225
|
+
if (inRate === outRate)
|
|
226
|
+
return audio;
|
|
227
|
+
const stream = new SincStream({ inRate, outRate, numChannels: 1, quality });
|
|
228
|
+
return stream.resampleChunk(audio, true);
|
|
229
|
+
},
|
|
230
|
+
createStream(options) {
|
|
231
|
+
return new SincStream(options);
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=sinc_engine.js.map
|
|
@@ -8,20 +8,24 @@
|
|
|
8
8
|
* 1. For batch processing of complete audio files
|
|
9
9
|
* 2. When you have all the audio data available at once
|
|
10
10
|
*
|
|
11
|
-
* PORTING NOTE: libsoxr
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* PORTING NOTE: libsoxr is not used. `soxr.resample()` is replaced by the
|
|
12
|
+
* resampler engine from `engine.ts`: `@pipedog/resampler-wasm` (rubato
|
|
13
|
+
* windowed sinc in WebAssembly) by default, a pure-TypeScript windowed-sinc
|
|
14
|
+
* fallback if that cannot load, or whatever `setResamplerEngine()` installed.
|
|
15
|
+
* Output lengths follow soxr's; the samples differ slightly because the
|
|
16
|
+
* filters differ.
|
|
15
17
|
*/
|
|
16
18
|
import { BaseAudioResampler, type SoxrQuality } from "./base_audio_resampler.ts";
|
|
17
19
|
/**
|
|
18
20
|
* Stand-in for `soxr.resample()`: resample a whole mono signal.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
22
|
+
* Synchronous, like soxr's: it uses the engine already loaded (or injected)
|
|
23
|
+
* and throws if there is none yet. `await getResamplerEngine()` first when
|
|
24
|
+
* calling it outside `SOXRAudioResampler.resample()`.
|
|
21
25
|
*
|
|
22
26
|
* @internal
|
|
23
27
|
*/
|
|
24
|
-
export declare function soxrResample<T extends Int16Array | Float32Array>(
|
|
28
|
+
export declare function soxrResample<T extends Int16Array | Float32Array>(audio: T, inRate: number, outRate: number, quality: SoxrQuality): T;
|
|
25
29
|
export interface SOXRAudioResamplerOptions {
|
|
26
30
|
/**
|
|
27
31
|
* SOXR quality preset. Higher quality means higher CPU cost. One of "VHQ"
|
|
@@ -13,21 +13,30 @@
|
|
|
13
13
|
* 1. For batch processing of complete audio files
|
|
14
14
|
* 2. When you have all the audio data available at once
|
|
15
15
|
*
|
|
16
|
-
* PORTING NOTE: libsoxr
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* PORTING NOTE: libsoxr is not used. `soxr.resample()` is replaced by the
|
|
17
|
+
* resampler engine from `engine.ts`: `@pipedog/resampler-wasm` (rubato
|
|
18
|
+
* windowed sinc in WebAssembly) by default, a pure-TypeScript windowed-sinc
|
|
19
|
+
* fallback if that cannot load, or whatever `setResamplerEngine()` installed.
|
|
20
|
+
* Output lengths follow soxr's; the samples differ slightly because the
|
|
21
|
+
* filters differ.
|
|
20
22
|
*/
|
|
21
23
|
import { BaseAudioResampler } from "./base_audio_resampler.js";
|
|
24
|
+
import { getLoadedResamplerEngine, getResamplerEngine } from "./engine.js";
|
|
22
25
|
/**
|
|
23
26
|
* Stand-in for `soxr.resample()`: resample a whole mono signal.
|
|
24
27
|
*
|
|
25
|
-
*
|
|
28
|
+
* Synchronous, like soxr's: it uses the engine already loaded (or injected)
|
|
29
|
+
* and throws if there is none yet. `await getResamplerEngine()` first when
|
|
30
|
+
* calling it outside `SOXRAudioResampler.resample()`.
|
|
26
31
|
*
|
|
27
32
|
* @internal
|
|
28
33
|
*/
|
|
29
|
-
export function soxrResample(
|
|
30
|
-
|
|
34
|
+
export function soxrResample(audio, inRate, outRate, quality) {
|
|
35
|
+
const engine = getLoadedResamplerEngine();
|
|
36
|
+
if (!engine) {
|
|
37
|
+
throw new Error("no resampler engine loaded yet: `await getResamplerEngine()` (audio/resamplers/engine) first");
|
|
38
|
+
}
|
|
39
|
+
return engine.resample(audio, inRate, outRate, quality);
|
|
31
40
|
}
|
|
32
41
|
/**
|
|
33
42
|
* Audio resampler implementation using the SoX resampler library.
|
|
@@ -53,6 +62,7 @@ export class SOXRAudioResampler extends BaseAudioResampler {
|
|
|
53
62
|
if (inRate === outRate) {
|
|
54
63
|
return audio;
|
|
55
64
|
}
|
|
65
|
+
await getResamplerEngine();
|
|
56
66
|
const audioData = bytesToInt16(audio);
|
|
57
67
|
const resampledAudio = soxrResample(audioData, inRate, outRate, this._quality);
|
|
58
68
|
return new Uint8Array(resampledAudio.buffer, resampledAudio.byteOffset, resampledAudio.byteLength);
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { BaseAudioResampler, type SoxrQuality } from "./base_audio_resampler.ts";
|
|
2
|
+
import { type ResamplerEngine, type ResamplerStream } from "./engine.ts";
|
|
2
3
|
/**
|
|
3
|
-
* Stand-in for `soxr.ResampleStream` (mono, int16)
|
|
4
|
-
*
|
|
5
|
-
* STUB: libsoxr is not available in JavaScript yet; constructing one throws.
|
|
4
|
+
* Stand-in for `soxr.ResampleStream` (mono, int16): a stream of the resampler
|
|
5
|
+
* engine (the loaded/injected one unless `engine` is given).
|
|
6
6
|
*
|
|
7
7
|
* @internal
|
|
8
8
|
*/
|
|
9
9
|
export declare class SoxrResampleStream {
|
|
10
|
-
|
|
10
|
+
protected _stream: ResamplerStream;
|
|
11
|
+
constructor(options: {
|
|
11
12
|
inRate: number;
|
|
12
13
|
outRate: number;
|
|
13
14
|
numChannels: number;
|
|
14
15
|
quality: SoxrQuality;
|
|
15
|
-
});
|
|
16
|
-
resampleChunk(
|
|
16
|
+
}, engine?: ResamplerEngine | null);
|
|
17
|
+
resampleChunk(chunk: Int16Array, last?: boolean): Int16Array;
|
|
17
18
|
clear(): void;
|
|
19
|
+
/** Free the engine's state (WASM memory). */
|
|
20
|
+
destroy(): void;
|
|
18
21
|
}
|
|
19
22
|
export interface SOXRStreamAudioResamplerOptions {
|
|
20
23
|
/**
|
|
@@ -16,30 +16,40 @@
|
|
|
16
16
|
* 4. When you need to reuse the same resampler configuration multiple times,
|
|
17
17
|
* as it saves initialization overhead
|
|
18
18
|
*
|
|
19
|
-
* PORTING NOTE: libsoxr
|
|
20
|
-
* stream
|
|
21
|
-
*
|
|
19
|
+
* PORTING NOTE: libsoxr is not used. `soxr.ResampleStream` is replaced by a
|
|
20
|
+
* stream from the resampler engine (`engine.ts`): `@pipedog/resampler-wasm`
|
|
21
|
+
* by default, a pure-TypeScript windowed-sinc fallback if that cannot load,
|
|
22
|
+
* or whatever `setResamplerEngine()` installed. `resample()` loads the engine
|
|
23
|
+
* on first use.
|
|
22
24
|
*/
|
|
23
25
|
import { time } from "@pipedog/asyncio";
|
|
24
26
|
import { BaseAudioResampler } from "./base_audio_resampler.js";
|
|
27
|
+
import { getLoadedResamplerEngine, getResamplerEngine, } from "./engine.js";
|
|
25
28
|
import { bytesToInt16 } from "./soxr_resampler.js";
|
|
26
29
|
const EMPTY_AUDIO = new Int16Array(0);
|
|
27
30
|
/**
|
|
28
|
-
* Stand-in for `soxr.ResampleStream` (mono, int16)
|
|
29
|
-
*
|
|
30
|
-
* STUB: libsoxr is not available in JavaScript yet; constructing one throws.
|
|
31
|
+
* Stand-in for `soxr.ResampleStream` (mono, int16): a stream of the resampler
|
|
32
|
+
* engine (the loaded/injected one unless `engine` is given).
|
|
31
33
|
*
|
|
32
34
|
* @internal
|
|
33
35
|
*/
|
|
34
36
|
export class SoxrResampleStream {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
_stream;
|
|
38
|
+
constructor(options, engine = getLoadedResamplerEngine()) {
|
|
39
|
+
if (!engine) {
|
|
40
|
+
throw new Error("no resampler engine loaded yet: `await getResamplerEngine()` (audio/resamplers/engine) first");
|
|
41
|
+
}
|
|
42
|
+
this._stream = engine.createStream(options);
|
|
37
43
|
}
|
|
38
|
-
resampleChunk(
|
|
39
|
-
|
|
44
|
+
resampleChunk(chunk, last = false) {
|
|
45
|
+
return this._stream.resampleChunk(chunk, last);
|
|
40
46
|
}
|
|
41
47
|
clear() {
|
|
42
|
-
|
|
48
|
+
this._stream.clear();
|
|
49
|
+
}
|
|
50
|
+
/** Free the engine's state (WASM memory). */
|
|
51
|
+
destroy() {
|
|
52
|
+
this._stream.destroy?.();
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
function int16ToBytes(samples) {
|
|
@@ -115,6 +125,9 @@ export class SOXRStreamAudioResampler extends BaseAudioResampler {
|
|
|
115
125
|
if (inRate === outRate) {
|
|
116
126
|
return audio;
|
|
117
127
|
}
|
|
128
|
+
if (this._soxrStream === null) {
|
|
129
|
+
await getResamplerEngine();
|
|
130
|
+
}
|
|
118
131
|
const stream = this._maybeInitializeSoxStream(inRate, outRate);
|
|
119
132
|
const audioData = bytesToInt16(audio);
|
|
120
133
|
const resampledAudio = stream.resampleChunk(audioData);
|
|
@@ -11,18 +11,21 @@
|
|
|
11
11
|
*
|
|
12
12
|
* PORTING NOTES:
|
|
13
13
|
* - There is no ONNX runtime in core. The analyzer takes an injected
|
|
14
|
-
* `createSession`
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - The model is not
|
|
18
|
-
* file name and is handed to `createSession` as is
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* `createSession`; `createLocalSmartTurnAnalyzerV3()` in
|
|
15
|
+
* `@pipedog/audio-smart-turn` wires it to `@pipedog/onnx` (onnxruntime-node
|
|
16
|
+
* or onnxruntime-web) and the model. Without one, the default factory throws.
|
|
17
|
+
* - The model is not in core; `smartTurnModelPath` defaults to the upstream
|
|
18
|
+
* file name and is handed to `createSession` as is (`@pipedog/audio-smart-turn`
|
|
19
|
+
* ships the file).
|
|
20
|
+
* - Resampling to 16 kHz uses the resampler engine (`audio/resamplers/engine`)
|
|
21
|
+
* instead of soxr. `_predictEndpoint` makes sure it is loaded before the
|
|
22
|
+
* (synchronous) `_resampleToModelRate` runs.
|
|
21
23
|
* - `PIPECAT_SMART_TURN_LOG_DATA` WAV dumps need a filesystem, which core
|
|
22
24
|
* doesn't use; the dump is a stub that logs a warning.
|
|
23
25
|
*/
|
|
24
26
|
import { envTruthy } from "../../../utils/env.js";
|
|
25
27
|
import { logger } from "../../../utils/logger.js";
|
|
28
|
+
import { getResamplerEngine } from "../../resamplers/engine.js";
|
|
26
29
|
import { soxrResample } from "../../resamplers/soxr_resampler.js";
|
|
27
30
|
import { computeWhisperLogMelFeatures, WHISPER_NUM_FRAMES, WHISPER_NUM_MELS, } from "./_whisper_features.js";
|
|
28
31
|
import { BaseSmartTurn, } from "./base_smart_turn.js";
|
|
@@ -33,7 +36,8 @@ import { BaseSmartTurn, } from "./base_smart_turn.js";
|
|
|
33
36
|
*/
|
|
34
37
|
export const MODEL_SAMPLE_RATE = 16000;
|
|
35
38
|
const notPortedSessionFactory = async () => {
|
|
36
|
-
throw new Error("LocalSmartTurnAnalyzerV3:
|
|
39
|
+
throw new Error("LocalSmartTurnAnalyzerV3: core has no ONNX runtime; use createLocalSmartTurnAnalyzerV3() " +
|
|
40
|
+
"from @pipedog/audio-smart-turn, or pass `createSession`");
|
|
37
41
|
};
|
|
38
42
|
function truncateAudioToLastNSeconds(audioArray, nSeconds = 8, sampleRate = MODEL_SAMPLE_RATE) {
|
|
39
43
|
// Truncate audio to last n seconds or pad with zeros to meet n seconds.
|
|
@@ -113,6 +117,10 @@ export class LocalSmartTurnAnalyzerV3 extends BaseSmartTurn {
|
|
|
113
117
|
const audioForLogging = audioArray;
|
|
114
118
|
const actualRate = this._sampleRate || MODEL_SAMPLE_RATE;
|
|
115
119
|
// Resample to 16 kHz if the pipeline uses a different sample rate
|
|
120
|
+
if (actualRate !== MODEL_SAMPLE_RATE) {
|
|
121
|
+
// soxr is synchronous; our engine may still need loading.
|
|
122
|
+
await getResamplerEngine();
|
|
123
|
+
}
|
|
116
124
|
audioArray = this._resampleToModelRate(audioArray);
|
|
117
125
|
// Truncate to 8 seconds (keeping the end) or pad to 8 seconds
|
|
118
126
|
audioArray = truncateAudioToLastNSeconds(audioArray, 8);
|
|
@@ -54,6 +54,7 @@ export interface VADAnalyzerOptions {
|
|
|
54
54
|
* on every runtime we target, so the analysis runs inline.
|
|
55
55
|
*/
|
|
56
56
|
export declare abstract class VADAnalyzer {
|
|
57
|
+
#private;
|
|
57
58
|
protected _initSampleRate: number | null;
|
|
58
59
|
protected _sampleRate: number;
|
|
59
60
|
protected _params: VADParams;
|
|
@@ -85,10 +86,13 @@ export declare abstract class VADAnalyzer {
|
|
|
85
86
|
/**
|
|
86
87
|
* Calculate voice activity confidence for the given audio buffer.
|
|
87
88
|
*
|
|
89
|
+
* May return a promise: JavaScript ONNX runtimes only run models
|
|
90
|
+
* asynchronously. Upstream calls this on the analyzer's model thread.
|
|
91
|
+
*
|
|
88
92
|
* @param buffer Audio buffer to analyze.
|
|
89
93
|
* @returns Voice confidence score between 0.0 and 1.0.
|
|
90
94
|
*/
|
|
91
|
-
abstract voiceConfidence(buffer: Uint8Array): number
|
|
95
|
+
abstract voiceConfidence(buffer: Uint8Array): number | Promise<number>;
|
|
92
96
|
/**
|
|
93
97
|
* Set the sample rate for audio processing.
|
|
94
98
|
*
|
|
@@ -113,8 +117,12 @@ export declare abstract class VADAnalyzer {
|
|
|
113
117
|
* @returns Current VAD state after processing the buffer.
|
|
114
118
|
*/
|
|
115
119
|
analyzeAudio(buffer: Uint8Array): Promise<VADState>;
|
|
116
|
-
/**
|
|
117
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Analyze audio buffer and return current VAD state.
|
|
122
|
+
*
|
|
123
|
+
* Synchronous when `voiceConfidence` is; otherwise returns a promise.
|
|
124
|
+
*/
|
|
125
|
+
protected _runAnalyzer(buffer: Uint8Array): VADState | Promise<VADState>;
|
|
118
126
|
/**
|
|
119
127
|
* Release the analyzer's resources.
|
|
120
128
|
*
|