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