@kuralle-syrinx/core 4.2.0 → 4.3.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 +63 -3
- 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 +110 -2
- package/src/packets.ts +116 -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.ts +453 -34
- 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.3.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,6 +33,7 @@ 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,
|
|
@@ -50,6 +51,16 @@ export {
|
|
|
50
51
|
type TurnChangePacket,
|
|
51
52
|
} from "./packets.js";
|
|
52
53
|
|
|
54
|
+
// Telephony control packets (DTMF / transfer)
|
|
55
|
+
export {
|
|
56
|
+
type DtmfDigit,
|
|
57
|
+
type DtmfReceivedPacket,
|
|
58
|
+
type DtmfSendPacket,
|
|
59
|
+
type CallTransferMode,
|
|
60
|
+
type CallTransferPacket,
|
|
61
|
+
} from "./packets.js";
|
|
62
|
+
export { parseDtmfDigit, dtmfReceived, dtmfSend, callTransfer } from "./packet-factories.js";
|
|
63
|
+
|
|
53
64
|
// Pipeline packets — LLM
|
|
54
65
|
export {
|
|
55
66
|
type LlmDeltaPacket,
|
|
@@ -126,16 +137,41 @@ export {
|
|
|
126
137
|
export {
|
|
127
138
|
type MessageCreatePacket,
|
|
128
139
|
type ConversationMetricPacket,
|
|
140
|
+
type AcousticSignalPacket,
|
|
141
|
+
type AcousticSignal,
|
|
142
|
+
type TurnLocalizationPacket,
|
|
143
|
+
type UsageStage,
|
|
144
|
+
type UsageRecordedPacket,
|
|
129
145
|
type TurnBoundaryKind,
|
|
130
146
|
type TurnBoundaryEventPacket,
|
|
131
147
|
type ObservabilityPacket,
|
|
132
148
|
type PipelineErrorPacket,
|
|
133
149
|
} from "./packets.js";
|
|
134
150
|
|
|
151
|
+
// Metering: price catalog + spend-cap guard (standalone; session wires later)
|
|
152
|
+
export {
|
|
153
|
+
type SttPrice,
|
|
154
|
+
type LlmPrice,
|
|
155
|
+
type TtsPrice,
|
|
156
|
+
type PriceCatalog,
|
|
157
|
+
type CostResult,
|
|
158
|
+
DEFAULT_PRICE_CATALOG,
|
|
159
|
+
costOf,
|
|
160
|
+
} from "./pricing.js";
|
|
161
|
+
export {
|
|
162
|
+
SpendCapGuard,
|
|
163
|
+
type SpendCapConfig,
|
|
164
|
+
type SpendCapCheck,
|
|
165
|
+
} from "./spend-cap.js";
|
|
166
|
+
|
|
135
167
|
// Observability backbone (VE-07)
|
|
136
168
|
export {
|
|
137
169
|
monotonicNowMs,
|
|
138
170
|
type MetricTags,
|
|
171
|
+
type ObservabilityLayer,
|
|
172
|
+
type TurnLocalizationVerdict,
|
|
173
|
+
type TurnLocalizationSignals,
|
|
174
|
+
localizeTurn,
|
|
139
175
|
type SpanHandle,
|
|
140
176
|
type MetricsExporter,
|
|
141
177
|
noopMetricsExporter,
|
|
@@ -162,6 +198,8 @@ export {
|
|
|
162
198
|
type PluginConfig,
|
|
163
199
|
type EndpointingOwner,
|
|
164
200
|
type EndpointingCapability,
|
|
201
|
+
type SttReconfigure,
|
|
202
|
+
type SttReconfigurePartial,
|
|
165
203
|
requireStringConfig,
|
|
166
204
|
optionalStringConfig,
|
|
167
205
|
} from "./plugin-contract.js";
|
|
@@ -200,7 +238,13 @@ export {
|
|
|
200
238
|
type SyrinxAudioEnvelopeHeader,
|
|
201
239
|
} from "./audio-envelope.js";
|
|
202
240
|
|
|
203
|
-
export {
|
|
241
|
+
export {
|
|
242
|
+
StreamingPcm16Resampler,
|
|
243
|
+
createLoudnessState,
|
|
244
|
+
normalizeLoudness,
|
|
245
|
+
type LoudnessConfig,
|
|
246
|
+
type LoudnessState,
|
|
247
|
+
} from "./audio/index.js";
|
|
204
248
|
|
|
205
249
|
// Interaction policy seam (IP-C1)
|
|
206
250
|
export {
|
|
@@ -209,16 +253,27 @@ export {
|
|
|
209
253
|
isLifecycleInteractionPolicy,
|
|
210
254
|
type InteractionObservation,
|
|
211
255
|
type InteractionDecision,
|
|
256
|
+
type AcousticSignalObservation,
|
|
257
|
+
type AcousticSignalSink,
|
|
212
258
|
type WordTiming,
|
|
213
259
|
} from "./interaction-policy.js";
|
|
214
260
|
export { confidenceToWaitMs, type ConfidenceToWaitConfig } from "./confidence-to-wait.js";
|
|
215
261
|
export { RuleBasedInteractionPolicy } from "./policies/rule-based.js";
|
|
216
262
|
export { DeferInteractionPolicy } from "./policies/defer.js";
|
|
217
263
|
export { InteractionCoordinator, type InteractionCaps } from "./interaction-coordinator.js";
|
|
218
|
-
export {
|
|
264
|
+
export {
|
|
265
|
+
type InteractionBackchannelPacket,
|
|
266
|
+
type InteractionDuckPacket,
|
|
267
|
+
type InteractionResumePacket,
|
|
268
|
+
} from "./packets.js";
|
|
219
269
|
|
|
220
270
|
// VoiceAgentSession
|
|
221
|
-
export {
|
|
271
|
+
export {
|
|
272
|
+
VoiceAgentSession,
|
|
273
|
+
type VoiceAgentSessionConfig,
|
|
274
|
+
type VoiceAgentSessionEvents,
|
|
275
|
+
type SessionStageUsage,
|
|
276
|
+
} from "./voice-agent-session.js";
|
|
222
277
|
|
|
223
278
|
// Primary-speaker barge-in gate (VE-02)
|
|
224
279
|
export {
|
|
@@ -226,6 +281,7 @@ export {
|
|
|
226
281
|
extractSpeakerFingerprint,
|
|
227
282
|
fingerprintSimilarity,
|
|
228
283
|
type SpeakerFingerprint,
|
|
284
|
+
type PrimarySpeakerGateDecision,
|
|
229
285
|
type PrimarySpeakerGateConfig,
|
|
230
286
|
} from "./primary-speaker-gate.js";
|
|
231
287
|
export {
|
|
@@ -251,11 +307,15 @@ export {
|
|
|
251
307
|
type LatencyFillerFixture,
|
|
252
308
|
} from "./latency-filler-fixtures.js";
|
|
253
309
|
|
|
310
|
+
// Voice text: markdown/formatting normalization + leaked-tool-call guard before TTS
|
|
311
|
+
export { normalizeForSpeech, stripLeakedToolCalls } from "./voice-text.js";
|
|
312
|
+
|
|
254
313
|
// Reasoner seam (RFC §4.2)
|
|
255
314
|
export {
|
|
256
315
|
type Reasoner,
|
|
257
316
|
type ReasonerTurn,
|
|
258
317
|
type ReasonerMessage,
|
|
318
|
+
type ReasonerUsage,
|
|
259
319
|
type ReasoningPart,
|
|
260
320
|
} from "./reasoner.js";
|
|
261
321
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
|
|
3
|
+
import type { AcousticSignal } from "./packets.js";
|
|
4
|
+
|
|
3
5
|
export interface WordTiming {
|
|
4
6
|
readonly word: string;
|
|
5
7
|
readonly startMs: number;
|
|
@@ -85,9 +87,19 @@ export type InteractionDecision =
|
|
|
85
87
|
| { readonly kind: "hold" }
|
|
86
88
|
| { readonly kind: "interrupt"; readonly interruptedContextId: string };
|
|
87
89
|
|
|
90
|
+
export interface AcousticSignalObservation {
|
|
91
|
+
readonly contextId: string;
|
|
92
|
+
readonly timestampMs: number;
|
|
93
|
+
readonly signal: AcousticSignal;
|
|
94
|
+
readonly payload?: Readonly<Record<string, unknown>>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type AcousticSignalSink = (signal: AcousticSignalObservation) => void;
|
|
98
|
+
|
|
88
99
|
export interface InteractionPolicy {
|
|
89
100
|
observe(obs: InteractionObservation): readonly InteractionDecision[];
|
|
90
101
|
reset(contextId: string): void;
|
|
102
|
+
setAcousticSignalSink?(sink: AcousticSignalSink): void;
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
/** Optional lifecycle for externally supplied model policies (session-owned). */
|