@kuralle-syrinx/core 4.2.0 → 4.4.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/package.json +2 -2
- package/src/audio/alaw.ts +56 -0
- package/src/audio/g722.ts +329 -0
- package/src/audio/index.ts +12 -0
- package/src/audio/loudness.ts +87 -0
- package/src/idle-timeout.ts +1 -0
- package/src/index.ts +65 -3
- package/src/interaction-coordinator.ts +17 -2
- package/src/interaction-policy.ts +12 -0
- package/src/observability-observer.ts +8 -4
- package/src/observability.ts +30 -1
- package/src/packet-factories.ts +124 -3
- package/src/packets.ts +139 -1
- package/src/plugin-contract.ts +41 -0
- package/src/policies/rule-based.ts +17 -2
- package/src/pricing.ts +137 -0
- package/src/primary-speaker-gate.ts +23 -3
- package/src/reasoner.ts +28 -1
- package/src/spend-cap.ts +52 -0
- package/src/turn-arbiter.ts +34 -2
- package/src/voice-agent-session-util.ts +18 -0
- package/src/voice-agent-session.ts +469 -36
- package/src/voice-text.ts +69 -0
- package/src/audio/audio.test.ts +0 -285
- package/src/audio-envelope.test.ts +0 -167
- package/src/confidence-to-wait.test.ts +0 -21
- package/src/error-handler.test.ts +0 -56
- package/src/hedge-throwing-backend.test.ts +0 -46
- package/src/interaction-coordinator.test.ts +0 -425
- package/src/iu-ledger.test.ts +0 -276
- package/src/latency-filler.test.ts +0 -62
- package/src/observability-observer.test.ts +0 -245
- package/src/observability.test.ts +0 -85
- package/src/packet-factories.test.ts +0 -53
- package/src/pipeline-bus.g10.test.ts +0 -145
- package/src/pipeline-bus.test.ts +0 -211
- package/src/policies/defer.test.ts +0 -86
- package/src/policies/rule-based.test.ts +0 -269
- package/src/primary-speaker-gate.test.ts +0 -150
- package/src/provider-fallback.test.ts +0 -87
- package/src/reasoner-hedge.test.ts +0 -361
- package/src/reasoner-route.test.ts +0 -248
- package/src/reasoner.test.ts +0 -69
- package/src/retry.test.ts +0 -83
- package/src/route-throwing-spec.test.ts +0 -44
- package/src/tts-playout-clock.test.ts +0 -125
- package/src/turn-arbiter.characterization.test.ts +0 -477
- package/src/turn-arbiter.test.ts +0 -567
- package/src/voice-agent-session.test.ts +0 -3316
- package/src/voice-text.test.ts +0 -94
- package/tsconfig.json +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuralle-syrinx/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Voice pipeline kernel for Syrinx — pipeline bus, turn arbitration, playout clock, packets, and the framework-agnostic Reasoner seam",
|
|
6
6
|
"keywords": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dependencies": {},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "^5.7.0",
|
|
35
|
-
"vitest": "^2.
|
|
35
|
+
"vitest": "^3.2.6"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// ITU-T G.711 A-law (PCMA). Pure TypedArray — Workers-safe (no Buffer/node:).
|
|
4
|
+
// Unit-verified against known A-law encode/decode values + round-trip fidelity.
|
|
5
|
+
// Unverified against a live carrier trunk negotiation.
|
|
6
|
+
|
|
7
|
+
const ALAW_CLIP = 32635;
|
|
8
|
+
const ALAW_BIAS = 0x55;
|
|
9
|
+
|
|
10
|
+
export function decodeALawToPcm16(input: Uint8Array): Int16Array {
|
|
11
|
+
const output = new Int16Array(input.byteLength);
|
|
12
|
+
for (let i = 0; i < input.byteLength; i += 1) {
|
|
13
|
+
const alaw = input[i]! ^ ALAW_BIAS;
|
|
14
|
+
// A-law sign bit is inverted vs μ-law: 0x80 means positive.
|
|
15
|
+
const positive = (alaw & 0x80) !== 0;
|
|
16
|
+
const exponent = (alaw >> 4) & 0x07;
|
|
17
|
+
const mantissa = alaw & 0x0f;
|
|
18
|
+
let sample: number;
|
|
19
|
+
if (exponent === 0) {
|
|
20
|
+
sample = (mantissa << 4) + 8;
|
|
21
|
+
} else {
|
|
22
|
+
sample = ((mantissa << 4) + 0x108) << (exponent - 1);
|
|
23
|
+
}
|
|
24
|
+
output[i] = positive ? sample : -sample;
|
|
25
|
+
}
|
|
26
|
+
return output;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function encodePcm16ToALaw(input: Int16Array): Uint8Array {
|
|
30
|
+
const output = new Uint8Array(input.length);
|
|
31
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
32
|
+
output[i] = encodeSample(input[i]!);
|
|
33
|
+
}
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function encodeSample(sample: number): number {
|
|
38
|
+
// A-law: sign bit 0x80 = positive (opposite of μ-law).
|
|
39
|
+
let sign = 0x80;
|
|
40
|
+
let magnitude = sample;
|
|
41
|
+
if (magnitude < 0) {
|
|
42
|
+
sign = 0;
|
|
43
|
+
magnitude = -magnitude;
|
|
44
|
+
}
|
|
45
|
+
if (magnitude > ALAW_CLIP) magnitude = ALAW_CLIP;
|
|
46
|
+
|
|
47
|
+
let exponent = 7;
|
|
48
|
+
for (let mask = 0x4000; (magnitude & mask) === 0 && exponent > 0; mask >>= 1) {
|
|
49
|
+
exponent -= 1;
|
|
50
|
+
}
|
|
51
|
+
const mantissa =
|
|
52
|
+
exponent === 0
|
|
53
|
+
? (magnitude >> 4) & 0x0f
|
|
54
|
+
: (magnitude >> (exponent + 3)) & 0x0f;
|
|
55
|
+
return (sign | (exponent << 4) | mantissa) ^ ALAW_BIAS;
|
|
56
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// ITU-T G.722 64 kbit/s sub-band ADPCM (16 kHz PCM16 ↔ G.722).
|
|
4
|
+
// Pure TypedArray — Workers-safe (no Buffer / node: / process).
|
|
5
|
+
//
|
|
6
|
+
// HONESTY: spec-implemented (ITU-T G.722 algorithm tables + QMF + dual-band
|
|
7
|
+
// ADPCM), round-trip-tested on speech-like signals. NOT ITU-vector-certified
|
|
8
|
+
// (authoritative ITU G.722 test vectors were not embedded). Unverified against
|
|
9
|
+
// a live carrier trunk negotiation.
|
|
10
|
+
|
|
11
|
+
/** 16 kHz wideband PCM sample rate for G.722. */
|
|
12
|
+
export const G722_SAMPLE_RATE_HZ = 16_000;
|
|
13
|
+
|
|
14
|
+
/** 64 kbit/s = 1 byte per pair of 16 kHz samples. */
|
|
15
|
+
export const G722_BITRATE = 64_000;
|
|
16
|
+
|
|
17
|
+
// ── ITU-T G.722 tables ──────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const QMF_FWD = Object.freeze([
|
|
20
|
+
3, -11, 12, 32, -210, 951, 3876, -805, 362, -156, 53, -11,
|
|
21
|
+
]);
|
|
22
|
+
const QMF_REV = Object.freeze([
|
|
23
|
+
-11, 53, -156, 362, -805, 3876, 951, -210, 32, 12, -11, 3,
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const QM2 = Object.freeze([-7408, -1616, 7408, 1616]);
|
|
27
|
+
const QM4 = Object.freeze([
|
|
28
|
+
0, -20456, -12896, -8968, -6288, -4240, -2584, -1200, 20456, 12896, 8968, 6288, 4240, 2584, 1200, 0,
|
|
29
|
+
]);
|
|
30
|
+
const QM6 = Object.freeze([
|
|
31
|
+
-136, -136, -136, -136, -24808, -21904, -19008, -16704, -14984, -13512, -12280, -11192, -10232, -9360, -8576, -7856,
|
|
32
|
+
-7192, -6576, -6000, -5456, -4944, -4464, -4008, -3576, -3168, -2776, -2400, -2032, -1688, -1360, -1040, -728,
|
|
33
|
+
24808, 21904, 19008, 16704, 14984, 13512, 12280, 11192, 10232, 9360, 8576, 7856, 7192, 6576, 6000, 5456,
|
|
34
|
+
4944, 4464, 4008, 3576, 3168, 2776, 2400, 2032, 1688, 1360, 1040, 728, 432, 136, -432, -136,
|
|
35
|
+
]);
|
|
36
|
+
const Q6 = Object.freeze([
|
|
37
|
+
0, 35, 72, 110, 150, 190, 233, 276, 323, 370, 422, 473, 530, 587, 650, 714, 786, 858, 940, 1023, 1121, 1219, 1339, 1458,
|
|
38
|
+
1612, 1765, 1980, 2195, 2557, 2919, 0, 0,
|
|
39
|
+
]);
|
|
40
|
+
const ILB = Object.freeze([
|
|
41
|
+
2048, 2093, 2139, 2186, 2233, 2282, 2332, 2383, 2435, 2489, 2543, 2599, 2656, 2714, 2774, 2834, 2896, 2960, 3025, 3091,
|
|
42
|
+
3158, 3228, 3298, 3371, 3444, 3520, 3597, 3676, 3756, 3838, 3922, 4008,
|
|
43
|
+
]);
|
|
44
|
+
const ILN = Object.freeze([0, 63, 62, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 0]);
|
|
45
|
+
const ILP = Object.freeze([0, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 0]);
|
|
46
|
+
const IHN = Object.freeze([0, 1, 0]);
|
|
47
|
+
const IHP = Object.freeze([0, 3, 2]);
|
|
48
|
+
const WL = Object.freeze([-60, -30, 58, 172, 334, 538, 1198, 3042]);
|
|
49
|
+
const RL42 = Object.freeze([0, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1, 0]);
|
|
50
|
+
const WH = Object.freeze([0, -214, 798]);
|
|
51
|
+
const RH2 = Object.freeze([2, 1, 2, 1]);
|
|
52
|
+
|
|
53
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function saturate16(amp: number): number {
|
|
56
|
+
if (amp > 32767) return 32767;
|
|
57
|
+
if (amp < -32768) return -32768;
|
|
58
|
+
return amp | 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saturate15(amp: number): number {
|
|
62
|
+
if (amp > 16383) return 16383;
|
|
63
|
+
if (amp < -16384) return -16384;
|
|
64
|
+
return amp | 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function satAdd16(a: number, b: number): number {
|
|
68
|
+
return saturate16(a + b);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function satSub16(a: number, b: number): number {
|
|
72
|
+
return saturate16(a - b);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function circularDot(hist: Int16Array, coeffs: readonly number[], n: number, pos: number): number {
|
|
76
|
+
let z = 0;
|
|
77
|
+
for (let i = 0; i < n - pos; i += 1) z += hist[pos + i]! * coeffs[i]!;
|
|
78
|
+
for (let i = 0; i < pos; i += 1) z += hist[i]! * coeffs[n - pos + i]!;
|
|
79
|
+
return z;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface BandState {
|
|
83
|
+
nb: number;
|
|
84
|
+
det: number;
|
|
85
|
+
s: number;
|
|
86
|
+
sz: number;
|
|
87
|
+
r: number;
|
|
88
|
+
p: [number, number];
|
|
89
|
+
a: [number, number];
|
|
90
|
+
b: [number, number, number, number, number, number];
|
|
91
|
+
d: [number, number, number, number, number, number, number];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createBand(det: number): BandState {
|
|
95
|
+
return {
|
|
96
|
+
nb: 0,
|
|
97
|
+
det,
|
|
98
|
+
s: 0,
|
|
99
|
+
sz: 0,
|
|
100
|
+
r: 0,
|
|
101
|
+
p: [0, 0],
|
|
102
|
+
a: [0, 0],
|
|
103
|
+
b: [0, 0, 0, 0, 0, 0],
|
|
104
|
+
d: [0, 0, 0, 0, 0, 0, 0],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function block4(band: BandState, dx: number): void {
|
|
109
|
+
const r = satAdd16(band.s, dx);
|
|
110
|
+
const p = satAdd16(band.sz, dx);
|
|
111
|
+
|
|
112
|
+
// UPPOL2
|
|
113
|
+
let wd1 = saturate16(band.a[0] << 2);
|
|
114
|
+
let wd32 = (p ^ band.p[0]) & 0x8000 ? wd1 : -wd1;
|
|
115
|
+
if (wd32 > 32767) wd32 = 32767;
|
|
116
|
+
let wd3 =
|
|
117
|
+
(((p ^ band.p[1]) & 0x8000 ? -128 : 128) +
|
|
118
|
+
(wd32 >> 7) +
|
|
119
|
+
((band.a[1] * 32512) >> 15)) |
|
|
120
|
+
0;
|
|
121
|
+
if (Math.abs(wd3) > 12288) wd3 = wd3 < 0 ? -12288 : 12288;
|
|
122
|
+
const ap1 = wd3;
|
|
123
|
+
|
|
124
|
+
// UPPOL1
|
|
125
|
+
wd1 = (p ^ band.p[0]) & 0x8000 ? -192 : 192;
|
|
126
|
+
let wd2 = ((band.a[0] * 32640) >> 15) | 0;
|
|
127
|
+
let ap0 = satAdd16(wd1, wd2);
|
|
128
|
+
wd3 = satSub16(15360, ap1);
|
|
129
|
+
if (Math.abs(ap0) > wd3) ap0 = ap0 < 0 ? -wd3 : wd3;
|
|
130
|
+
|
|
131
|
+
// FILTEP
|
|
132
|
+
wd1 = satAdd16(r, r);
|
|
133
|
+
wd1 = ((ap0 * wd1) >> 15) | 0;
|
|
134
|
+
wd2 = satAdd16(band.r, band.r);
|
|
135
|
+
wd2 = ((ap1 * wd2) >> 15) | 0;
|
|
136
|
+
const sp = satAdd16(wd1, wd2);
|
|
137
|
+
band.r = r;
|
|
138
|
+
band.a[1] = ap1;
|
|
139
|
+
band.a[0] = ap0;
|
|
140
|
+
band.p[1] = band.p[0];
|
|
141
|
+
band.p[0] = p;
|
|
142
|
+
|
|
143
|
+
// UPZERO / DELAYA / FILTEZ
|
|
144
|
+
wd1 = dx === 0 ? 0 : 128;
|
|
145
|
+
band.d[0] = dx;
|
|
146
|
+
let sz = 0;
|
|
147
|
+
for (let i = 5; i >= 0; i -= 1) {
|
|
148
|
+
wd2 = (band.d[i + 1]! ^ dx) & 0x8000 ? -wd1 : wd1;
|
|
149
|
+
wd3 = ((band.b[i]! * 32640) >> 15) | 0;
|
|
150
|
+
band.b[i] = satAdd16(wd2, wd3);
|
|
151
|
+
wd3 = satAdd16(band.d[i]!, band.d[i]!);
|
|
152
|
+
sz += (band.b[i]! * wd3) >> 15;
|
|
153
|
+
band.d[i + 1] = band.d[i]!;
|
|
154
|
+
}
|
|
155
|
+
band.sz = saturate16(sz);
|
|
156
|
+
band.s = satAdd16(sp, band.sz);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── public state + API ──────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
export interface G722EncoderState {
|
|
162
|
+
readonly _kind: "g722-encoder";
|
|
163
|
+
ptr: number;
|
|
164
|
+
x: Int16Array;
|
|
165
|
+
y: Int16Array;
|
|
166
|
+
band: [BandState, BandState];
|
|
167
|
+
/** leftover sample when input length is odd */
|
|
168
|
+
pending: number | null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface G722DecoderState {
|
|
172
|
+
readonly _kind: "g722-decoder";
|
|
173
|
+
ptr: number;
|
|
174
|
+
x: Int16Array;
|
|
175
|
+
y: Int16Array;
|
|
176
|
+
band: [BandState, BandState];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function createG722EncoderState(): G722EncoderState {
|
|
180
|
+
return {
|
|
181
|
+
_kind: "g722-encoder",
|
|
182
|
+
ptr: 0,
|
|
183
|
+
x: new Int16Array(12),
|
|
184
|
+
y: new Int16Array(12),
|
|
185
|
+
band: [createBand(32), createBand(8)],
|
|
186
|
+
pending: null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createG722DecoderState(): G722DecoderState {
|
|
191
|
+
return {
|
|
192
|
+
_kind: "g722-decoder",
|
|
193
|
+
ptr: 0,
|
|
194
|
+
x: new Int16Array(12),
|
|
195
|
+
y: new Int16Array(12),
|
|
196
|
+
band: [createBand(32), createBand(8)],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Encode 16 kHz PCM16 → G.722 64 kbit/s bytes. Mutates `state`. Odd trailing sample is held for next call. */
|
|
201
|
+
export function encodeG722(state: G722EncoderState, pcm16: Int16Array): Uint8Array {
|
|
202
|
+
let offset = 0;
|
|
203
|
+
let samples = pcm16;
|
|
204
|
+
if (state.pending !== null) {
|
|
205
|
+
const merged = new Int16Array(pcm16.length + 1);
|
|
206
|
+
merged[0] = state.pending;
|
|
207
|
+
merged.set(pcm16, 1);
|
|
208
|
+
samples = merged;
|
|
209
|
+
state.pending = null;
|
|
210
|
+
}
|
|
211
|
+
if (samples.length % 2 !== 0) {
|
|
212
|
+
state.pending = samples[samples.length - 1]!;
|
|
213
|
+
samples = samples.subarray(0, samples.length - 1);
|
|
214
|
+
}
|
|
215
|
+
const out = new Uint8Array(samples.length >> 1);
|
|
216
|
+
let outIdx = 0;
|
|
217
|
+
|
|
218
|
+
while (offset < samples.length) {
|
|
219
|
+
state.x[state.ptr] = samples[offset++]!;
|
|
220
|
+
state.y[state.ptr] = samples[offset++]!;
|
|
221
|
+
state.ptr += 1;
|
|
222
|
+
if (state.ptr >= 12) state.ptr = 0;
|
|
223
|
+
|
|
224
|
+
const sumodd = circularDot(state.x, QMF_FWD, 12, state.ptr);
|
|
225
|
+
const sumeven = circularDot(state.y, QMF_REV, 12, state.ptr);
|
|
226
|
+
const xlow = ((sumeven + sumodd) >> 14) | 0;
|
|
227
|
+
const xhigh = ((sumeven - sumodd) >> 14) | 0;
|
|
228
|
+
|
|
229
|
+
// Low band
|
|
230
|
+
const el = satSub16(xlow, state.band[0].s);
|
|
231
|
+
let wd = el >= 0 ? el : ~el;
|
|
232
|
+
let i = 1;
|
|
233
|
+
for (; i < 30; i += 1) {
|
|
234
|
+
const wd1 = (Q6[i]! * state.band[0].det) >> 12;
|
|
235
|
+
if (wd < wd1) break;
|
|
236
|
+
}
|
|
237
|
+
const ilow = el < 0 ? ILN[i]! : ILP[i]!;
|
|
238
|
+
const ril = ilow >> 2;
|
|
239
|
+
let dlow = ((state.band[0].det * QM4[ril]!) >> 15) | 0;
|
|
240
|
+
const il4 = RL42[ril]!;
|
|
241
|
+
wd = ((state.band[0].nb * 127) >> 7) + WL[il4]!;
|
|
242
|
+
if (wd < 0) wd = 0;
|
|
243
|
+
else if (wd > 18432) wd = 18432;
|
|
244
|
+
state.band[0].nb = wd;
|
|
245
|
+
let wd1 = (state.band[0].nb >> 6) & 31;
|
|
246
|
+
let wd2 = 8 - (state.band[0].nb >> 11);
|
|
247
|
+
let wd3 = wd2 < 0 ? ILB[wd1]! << -wd2 : ILB[wd1]! >> wd2;
|
|
248
|
+
state.band[0].det = (wd3 << 2) | 0;
|
|
249
|
+
block4(state.band[0], dlow);
|
|
250
|
+
|
|
251
|
+
// High band
|
|
252
|
+
const eh = satSub16(xhigh, state.band[1].s);
|
|
253
|
+
wd = eh >= 0 ? eh : ~eh;
|
|
254
|
+
wd1 = (564 * state.band[1].det) >> 12;
|
|
255
|
+
const mih = wd >= wd1 ? 2 : 1;
|
|
256
|
+
const ihigh = eh < 0 ? IHN[mih]! : IHP[mih]!;
|
|
257
|
+
const dhigh = ((state.band[1].det * QM2[ihigh]!) >> 15) | 0;
|
|
258
|
+
const ih2 = RH2[ihigh]!;
|
|
259
|
+
wd = ((state.band[1].nb * 127) >> 7) + WH[ih2]!;
|
|
260
|
+
if (wd < 0) wd = 0;
|
|
261
|
+
else if (wd > 22528) wd = 22528;
|
|
262
|
+
state.band[1].nb = wd;
|
|
263
|
+
wd1 = (state.band[1].nb >> 6) & 31;
|
|
264
|
+
wd2 = 10 - (state.band[1].nb >> 11);
|
|
265
|
+
wd3 = wd2 < 0 ? ILB[wd1]! << -wd2 : ILB[wd1]! >> wd2;
|
|
266
|
+
state.band[1].det = (wd3 << 2) | 0;
|
|
267
|
+
block4(state.band[1], dhigh);
|
|
268
|
+
|
|
269
|
+
out[outIdx++] = ((ihigh << 6) | ilow) & 0xff;
|
|
270
|
+
}
|
|
271
|
+
return outIdx === out.length ? out : out.subarray(0, outIdx);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Decode G.722 64 kbit/s bytes → 16 kHz PCM16. Mutates `state`. */
|
|
275
|
+
export function decodeG722(state: G722DecoderState, g722: Uint8Array): Int16Array {
|
|
276
|
+
const out = new Int16Array(g722.byteLength * 2);
|
|
277
|
+
let outIdx = 0;
|
|
278
|
+
|
|
279
|
+
for (let j = 0; j < g722.byteLength; j += 1) {
|
|
280
|
+
const code = g722[j]!;
|
|
281
|
+
let wd1 = code & 0x3f;
|
|
282
|
+
const ihigh = (code >> 6) & 0x03;
|
|
283
|
+
let wd2 = QM6[wd1]!;
|
|
284
|
+
wd1 >>= 2;
|
|
285
|
+
|
|
286
|
+
// Low band INVQBL / RECONS / LIMIT
|
|
287
|
+
wd2 = ((state.band[0].det * wd2) >> 15) | 0;
|
|
288
|
+
const rlow = saturate15(state.band[0].s + wd2);
|
|
289
|
+
|
|
290
|
+
// Low band INVQAL + LOGSCL + SCALEL
|
|
291
|
+
wd2 = QM4[wd1]!;
|
|
292
|
+
const dlow = ((state.band[0].det * wd2) >> 15) | 0;
|
|
293
|
+
wd2 = RL42[wd1]!;
|
|
294
|
+
let nb = ((state.band[0].nb * 127) >> 7) + WL[wd2]!;
|
|
295
|
+
if (nb < 0) nb = 0;
|
|
296
|
+
else if (nb > 18432) nb = 18432;
|
|
297
|
+
state.band[0].nb = nb;
|
|
298
|
+
let s1 = (state.band[0].nb >> 6) & 31;
|
|
299
|
+
let s2 = 8 - (state.band[0].nb >> 11);
|
|
300
|
+
let s3 = s2 < 0 ? ILB[s1]! << -s2 : ILB[s1]! >> s2;
|
|
301
|
+
state.band[0].det = (s3 << 2) | 0;
|
|
302
|
+
block4(state.band[0], dlow);
|
|
303
|
+
|
|
304
|
+
// High band
|
|
305
|
+
wd2 = QM2[ihigh]!;
|
|
306
|
+
const dhigh = ((state.band[1].det * wd2) >> 15) | 0;
|
|
307
|
+
const rhigh = saturate15(dhigh + state.band[1].s);
|
|
308
|
+
wd2 = RH2[ihigh]!;
|
|
309
|
+
nb = ((state.band[1].nb * 127) >> 7) + WH[wd2]!;
|
|
310
|
+
if (nb < 0) nb = 0;
|
|
311
|
+
else if (nb > 22528) nb = 22528;
|
|
312
|
+
state.band[1].nb = nb;
|
|
313
|
+
s1 = (state.band[1].nb >> 6) & 31;
|
|
314
|
+
s2 = 10 - (state.band[1].nb >> 11);
|
|
315
|
+
s3 = s2 < 0 ? ILB[s1]! << -s2 : ILB[s1]! >> s2;
|
|
316
|
+
state.band[1].det = (s3 << 2) | 0;
|
|
317
|
+
block4(state.band[1], dhigh);
|
|
318
|
+
|
|
319
|
+
// QMF synthesis
|
|
320
|
+
state.x[state.ptr] = (rlow + rhigh) | 0;
|
|
321
|
+
state.y[state.ptr] = (rlow - rhigh) | 0;
|
|
322
|
+
state.ptr += 1;
|
|
323
|
+
if (state.ptr >= 12) state.ptr = 0;
|
|
324
|
+
// QMF introduces ~22-sample group delay; polarity follows the ITU analysis/synthesis pair.
|
|
325
|
+
out[outIdx++] = (circularDot(state.y, QMF_REV, 12, state.ptr) >> 11) | 0;
|
|
326
|
+
out[outIdx++] = (circularDot(state.x, QMF_FWD, 12, state.ptr) >> 11) | 0;
|
|
327
|
+
}
|
|
328
|
+
return outIdx === out.length ? out : out.subarray(0, outIdx);
|
|
329
|
+
}
|
package/src/audio/index.ts
CHANGED
|
@@ -2,4 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
export { pcm16BytesToSamples, pcm16SamplesToBytes, bigEndianPcm16BytesToSamples, pcm16SamplesToBigEndianBytes } from "./pcm.js";
|
|
4
4
|
export { decodeMuLawToPcm16, encodePcm16ToMuLaw } from "./mulaw.js";
|
|
5
|
+
export { decodeALawToPcm16, encodePcm16ToALaw } from "./alaw.js";
|
|
6
|
+
export {
|
|
7
|
+
createG722EncoderState,
|
|
8
|
+
createG722DecoderState,
|
|
9
|
+
encodeG722,
|
|
10
|
+
decodeG722,
|
|
11
|
+
G722_SAMPLE_RATE_HZ,
|
|
12
|
+
G722_BITRATE,
|
|
13
|
+
type G722EncoderState,
|
|
14
|
+
type G722DecoderState,
|
|
15
|
+
} from "./g722.js";
|
|
5
16
|
export { resamplePcm16, resamplePcm16Streaming, StreamingPcm16Resampler } from "./resample.js";
|
|
17
|
+
export { createLoudnessState, normalizeLoudness, type LoudnessConfig, type LoudnessState } from "./loudness.js";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
const INT16_MAX = 32767;
|
|
4
|
+
const DEFAULT_MAX_GAIN_STEP = 0.5;
|
|
5
|
+
const DEFAULT_RMS_SMOOTHING = 0.25;
|
|
6
|
+
const DEFAULT_CEILING = 30000;
|
|
7
|
+
|
|
8
|
+
export interface LoudnessConfig {
|
|
9
|
+
readonly targetRms: number;
|
|
10
|
+
readonly maxGainStep?: number;
|
|
11
|
+
readonly rmsSmoothing?: number;
|
|
12
|
+
readonly ceiling?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LoudnessState {
|
|
16
|
+
gain: number;
|
|
17
|
+
runningRms: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createLoudnessState(): LoudnessState {
|
|
21
|
+
return { gain: 1, runningRms: 0 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeLoudness(
|
|
25
|
+
pcm: Int16Array,
|
|
26
|
+
state: LoudnessState,
|
|
27
|
+
cfg: LoudnessConfig,
|
|
28
|
+
): Int16Array {
|
|
29
|
+
const targetRms = positiveBounded(cfg.targetRms, "targetRms", INT16_MAX);
|
|
30
|
+
const maxGainStep = positiveFinite(cfg.maxGainStep ?? DEFAULT_MAX_GAIN_STEP, "maxGainStep");
|
|
31
|
+
const rmsSmoothing = bounded(cfg.rmsSmoothing ?? DEFAULT_RMS_SMOOTHING, "rmsSmoothing", 0, 1);
|
|
32
|
+
const ceiling = positiveBounded(cfg.ceiling ?? DEFAULT_CEILING, "ceiling", INT16_MAX);
|
|
33
|
+
|
|
34
|
+
if (pcm.length === 0) return pcm;
|
|
35
|
+
|
|
36
|
+
const frameRms = rms(pcm);
|
|
37
|
+
if (frameRms > 0) {
|
|
38
|
+
state.runningRms =
|
|
39
|
+
state.runningRms > 0
|
|
40
|
+
? state.runningRms + (frameRms - state.runningRms) * rmsSmoothing
|
|
41
|
+
: frameRms;
|
|
42
|
+
const desiredGain = targetRms / state.runningRms;
|
|
43
|
+
const gainDelta = desiredGain - state.gain;
|
|
44
|
+
state.gain += Math.max(-maxGainStep, Math.min(maxGainStep, gainDelta));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const output = new Int16Array(pcm.length);
|
|
48
|
+
for (let i = 0; i < pcm.length; i += 1) {
|
|
49
|
+
output[i] = Math.round(softLimit(pcm[i]! * state.gain, ceiling));
|
|
50
|
+
}
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rms(pcm: Int16Array): number {
|
|
55
|
+
let sumSquares = 0;
|
|
56
|
+
for (const sample of pcm) sumSquares += sample * sample;
|
|
57
|
+
return Math.sqrt(sumSquares / pcm.length);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function softLimit(value: number, ceiling: number): number {
|
|
61
|
+
const magnitude = Math.abs(value);
|
|
62
|
+
const knee = ceiling * 0.75;
|
|
63
|
+
if (magnitude <= knee) return value;
|
|
64
|
+
const range = ceiling - knee;
|
|
65
|
+
const limited = knee + range * (1 - Math.exp(-(magnitude - knee) / range));
|
|
66
|
+
return Math.sign(value) * limited;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function positiveFinite(value: number, name: string): number {
|
|
70
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
71
|
+
throw new RangeError(`LoudnessConfig.${name} must be a positive finite number`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function positiveBounded(value: number, name: string, max: number): number {
|
|
77
|
+
const parsed = positiveFinite(value, name);
|
|
78
|
+
if (parsed > max) throw new RangeError(`LoudnessConfig.${name} must be at most ${String(max)}`);
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function bounded(value: number, name: string, min: number, max: number): number {
|
|
83
|
+
if (!Number.isFinite(value) || value <= min || value > max) {
|
|
84
|
+
throw new RangeError(`LoudnessConfig.${name} must be greater than ${String(min)} and at most ${String(max)}`);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
package/src/idle-timeout.ts
CHANGED
|
@@ -118,6 +118,7 @@ export class IdleTimeoutManager {
|
|
|
118
118
|
*/
|
|
119
119
|
extend(ms: number): void {
|
|
120
120
|
this.clearTimer();
|
|
121
|
+
if (this.config.durationMs <= 0) return;
|
|
121
122
|
this.timerScheduled = true;
|
|
122
123
|
this.scheduler.schedule("voice.idle_timeout", this.config.durationMs + ms, () => {
|
|
123
124
|
this.timerScheduled = false;
|
package/src/index.ts
CHANGED
|
@@ -33,11 +33,14 @@ export {
|
|
|
33
33
|
type SttPartialPacket,
|
|
34
34
|
type SttResultPacket,
|
|
35
35
|
type FinalizeSttPacket,
|
|
36
|
+
type SttReconfigurePacket,
|
|
36
37
|
type SttErrorPacket,
|
|
37
38
|
type EndOfSpeechAudioPacket,
|
|
38
39
|
type EndOfSpeechPacket,
|
|
39
40
|
type InterimEndOfSpeechPacket,
|
|
40
41
|
type EndOfSpeechRetractedPacket,
|
|
42
|
+
type TurnEndOwner,
|
|
43
|
+
type TurnEndReason,
|
|
41
44
|
type UserInputPacket,
|
|
42
45
|
} from "./packets.js";
|
|
43
46
|
|
|
@@ -50,6 +53,16 @@ export {
|
|
|
50
53
|
type TurnChangePacket,
|
|
51
54
|
} from "./packets.js";
|
|
52
55
|
|
|
56
|
+
// Telephony control packets (DTMF / transfer)
|
|
57
|
+
export {
|
|
58
|
+
type DtmfDigit,
|
|
59
|
+
type DtmfReceivedPacket,
|
|
60
|
+
type DtmfSendPacket,
|
|
61
|
+
type CallTransferMode,
|
|
62
|
+
type CallTransferPacket,
|
|
63
|
+
} from "./packets.js";
|
|
64
|
+
export { parseDtmfDigit, dtmfReceived, dtmfSend, callTransfer } from "./packet-factories.js";
|
|
65
|
+
|
|
53
66
|
// Pipeline packets — LLM
|
|
54
67
|
export {
|
|
55
68
|
type LlmDeltaPacket,
|
|
@@ -126,16 +139,41 @@ export {
|
|
|
126
139
|
export {
|
|
127
140
|
type MessageCreatePacket,
|
|
128
141
|
type ConversationMetricPacket,
|
|
142
|
+
type AcousticSignalPacket,
|
|
143
|
+
type AcousticSignal,
|
|
144
|
+
type TurnLocalizationPacket,
|
|
145
|
+
type UsageStage,
|
|
146
|
+
type UsageRecordedPacket,
|
|
129
147
|
type TurnBoundaryKind,
|
|
130
148
|
type TurnBoundaryEventPacket,
|
|
131
149
|
type ObservabilityPacket,
|
|
132
150
|
type PipelineErrorPacket,
|
|
133
151
|
} from "./packets.js";
|
|
134
152
|
|
|
153
|
+
// Metering: price catalog + spend-cap guard (standalone; session wires later)
|
|
154
|
+
export {
|
|
155
|
+
type SttPrice,
|
|
156
|
+
type LlmPrice,
|
|
157
|
+
type TtsPrice,
|
|
158
|
+
type PriceCatalog,
|
|
159
|
+
type CostResult,
|
|
160
|
+
DEFAULT_PRICE_CATALOG,
|
|
161
|
+
costOf,
|
|
162
|
+
} from "./pricing.js";
|
|
163
|
+
export {
|
|
164
|
+
SpendCapGuard,
|
|
165
|
+
type SpendCapConfig,
|
|
166
|
+
type SpendCapCheck,
|
|
167
|
+
} from "./spend-cap.js";
|
|
168
|
+
|
|
135
169
|
// Observability backbone (VE-07)
|
|
136
170
|
export {
|
|
137
171
|
monotonicNowMs,
|
|
138
172
|
type MetricTags,
|
|
173
|
+
type ObservabilityLayer,
|
|
174
|
+
type TurnLocalizationVerdict,
|
|
175
|
+
type TurnLocalizationSignals,
|
|
176
|
+
localizeTurn,
|
|
139
177
|
type SpanHandle,
|
|
140
178
|
type MetricsExporter,
|
|
141
179
|
noopMetricsExporter,
|
|
@@ -162,6 +200,8 @@ export {
|
|
|
162
200
|
type PluginConfig,
|
|
163
201
|
type EndpointingOwner,
|
|
164
202
|
type EndpointingCapability,
|
|
203
|
+
type SttReconfigure,
|
|
204
|
+
type SttReconfigurePartial,
|
|
165
205
|
requireStringConfig,
|
|
166
206
|
optionalStringConfig,
|
|
167
207
|
} from "./plugin-contract.js";
|
|
@@ -200,7 +240,13 @@ export {
|
|
|
200
240
|
type SyrinxAudioEnvelopeHeader,
|
|
201
241
|
} from "./audio-envelope.js";
|
|
202
242
|
|
|
203
|
-
export {
|
|
243
|
+
export {
|
|
244
|
+
StreamingPcm16Resampler,
|
|
245
|
+
createLoudnessState,
|
|
246
|
+
normalizeLoudness,
|
|
247
|
+
type LoudnessConfig,
|
|
248
|
+
type LoudnessState,
|
|
249
|
+
} from "./audio/index.js";
|
|
204
250
|
|
|
205
251
|
// Interaction policy seam (IP-C1)
|
|
206
252
|
export {
|
|
@@ -209,16 +255,27 @@ export {
|
|
|
209
255
|
isLifecycleInteractionPolicy,
|
|
210
256
|
type InteractionObservation,
|
|
211
257
|
type InteractionDecision,
|
|
258
|
+
type AcousticSignalObservation,
|
|
259
|
+
type AcousticSignalSink,
|
|
212
260
|
type WordTiming,
|
|
213
261
|
} from "./interaction-policy.js";
|
|
214
262
|
export { confidenceToWaitMs, type ConfidenceToWaitConfig } from "./confidence-to-wait.js";
|
|
215
263
|
export { RuleBasedInteractionPolicy } from "./policies/rule-based.js";
|
|
216
264
|
export { DeferInteractionPolicy } from "./policies/defer.js";
|
|
217
265
|
export { InteractionCoordinator, type InteractionCaps } from "./interaction-coordinator.js";
|
|
218
|
-
export {
|
|
266
|
+
export {
|
|
267
|
+
type InteractionBackchannelPacket,
|
|
268
|
+
type InteractionDuckPacket,
|
|
269
|
+
type InteractionResumePacket,
|
|
270
|
+
} from "./packets.js";
|
|
219
271
|
|
|
220
272
|
// VoiceAgentSession
|
|
221
|
-
export {
|
|
273
|
+
export {
|
|
274
|
+
VoiceAgentSession,
|
|
275
|
+
type VoiceAgentSessionConfig,
|
|
276
|
+
type VoiceAgentSessionEvents,
|
|
277
|
+
type SessionStageUsage,
|
|
278
|
+
} from "./voice-agent-session.js";
|
|
222
279
|
|
|
223
280
|
// Primary-speaker barge-in gate (VE-02)
|
|
224
281
|
export {
|
|
@@ -226,6 +283,7 @@ export {
|
|
|
226
283
|
extractSpeakerFingerprint,
|
|
227
284
|
fingerprintSimilarity,
|
|
228
285
|
type SpeakerFingerprint,
|
|
286
|
+
type PrimarySpeakerGateDecision,
|
|
229
287
|
type PrimarySpeakerGateConfig,
|
|
230
288
|
} from "./primary-speaker-gate.js";
|
|
231
289
|
export {
|
|
@@ -251,11 +309,15 @@ export {
|
|
|
251
309
|
type LatencyFillerFixture,
|
|
252
310
|
} from "./latency-filler-fixtures.js";
|
|
253
311
|
|
|
312
|
+
// Voice text: markdown/formatting normalization + leaked-tool-call guard before TTS
|
|
313
|
+
export { normalizeForSpeech, stripLeakedToolCalls } from "./voice-text.js";
|
|
314
|
+
|
|
254
315
|
// Reasoner seam (RFC §4.2)
|
|
255
316
|
export {
|
|
256
317
|
type Reasoner,
|
|
257
318
|
type ReasonerTurn,
|
|
258
319
|
type ReasonerMessage,
|
|
320
|
+
type ReasonerUsage,
|
|
259
321
|
type ReasoningPart,
|
|
260
322
|
} from "./reasoner.js";
|
|
261
323
|
|