@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.
Files changed (49) hide show
  1. package/package.json +2 -2
  2. package/src/audio/alaw.ts +56 -0
  3. package/src/audio/g722.ts +329 -0
  4. package/src/audio/index.ts +12 -0
  5. package/src/audio/loudness.ts +87 -0
  6. package/src/idle-timeout.ts +1 -0
  7. package/src/index.ts +63 -3
  8. package/src/interaction-policy.ts +12 -0
  9. package/src/observability-observer.ts +8 -4
  10. package/src/observability.ts +30 -1
  11. package/src/packet-factories.ts +110 -2
  12. package/src/packets.ts +116 -1
  13. package/src/plugin-contract.ts +41 -0
  14. package/src/policies/rule-based.ts +17 -2
  15. package/src/pricing.ts +137 -0
  16. package/src/primary-speaker-gate.ts +23 -3
  17. package/src/reasoner.ts +28 -1
  18. package/src/spend-cap.ts +52 -0
  19. package/src/turn-arbiter.ts +34 -2
  20. package/src/voice-agent-session.ts +453 -34
  21. package/src/voice-text.ts +69 -0
  22. package/src/audio/audio.test.ts +0 -285
  23. package/src/audio-envelope.test.ts +0 -167
  24. package/src/confidence-to-wait.test.ts +0 -21
  25. package/src/error-handler.test.ts +0 -56
  26. package/src/hedge-throwing-backend.test.ts +0 -46
  27. package/src/interaction-coordinator.test.ts +0 -425
  28. package/src/iu-ledger.test.ts +0 -276
  29. package/src/latency-filler.test.ts +0 -62
  30. package/src/observability-observer.test.ts +0 -245
  31. package/src/observability.test.ts +0 -85
  32. package/src/packet-factories.test.ts +0 -53
  33. package/src/pipeline-bus.g10.test.ts +0 -145
  34. package/src/pipeline-bus.test.ts +0 -211
  35. package/src/policies/defer.test.ts +0 -86
  36. package/src/policies/rule-based.test.ts +0 -269
  37. package/src/primary-speaker-gate.test.ts +0 -150
  38. package/src/provider-fallback.test.ts +0 -87
  39. package/src/reasoner-hedge.test.ts +0 -361
  40. package/src/reasoner-route.test.ts +0 -248
  41. package/src/reasoner.test.ts +0 -69
  42. package/src/retry.test.ts +0 -83
  43. package/src/route-throwing-spec.test.ts +0 -44
  44. package/src/tts-playout-clock.test.ts +0 -125
  45. package/src/turn-arbiter.characterization.test.ts +0 -477
  46. package/src/turn-arbiter.test.ts +0 -567
  47. package/src/voice-agent-session.test.ts +0 -3316
  48. package/src/voice-text.test.ts +0 -94
  49. package/tsconfig.json +0 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuralle-syrinx/core",
3
- "version": "4.2.0",
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.1.0"
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
+ }
@@ -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
+ }
@@ -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 { StreamingPcm16Resampler } from "./audio/index.js";
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 { type InteractionBackchannelPacket } from "./packets.js";
264
+ export {
265
+ type InteractionBackchannelPacket,
266
+ type InteractionDuckPacket,
267
+ type InteractionResumePacket,
268
+ } from "./packets.js";
219
269
 
220
270
  // VoiceAgentSession
221
- export { VoiceAgentSession, type VoiceAgentSessionConfig, type VoiceAgentSessionEvents } from "./voice-agent-session.js";
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). */