tci-client-node 0.1.2 → 0.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 (41) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +57 -11
  3. package/dist/audio/index.cjs +52 -11
  4. package/dist/audio/index.cjs.map +1 -1
  5. package/dist/audio/index.d.cts +17 -3
  6. package/dist/audio/index.d.ts +17 -3
  7. package/dist/audio/index.js +51 -11
  8. package/dist/audio/index.js.map +1 -1
  9. package/dist/dialect/index.cjs +349 -0
  10. package/dist/dialect/index.cjs.map +1 -0
  11. package/dist/dialect/index.d.cts +29 -0
  12. package/dist/dialect/index.d.ts +29 -0
  13. package/dist/dialect/index.js +310 -0
  14. package/dist/dialect/index.js.map +1 -0
  15. package/dist/{index-CK3XdXP3.d.cts → index-lbK6NGY4.d.cts} +1 -1
  16. package/dist/{index-Dfmrk2MR.d.ts → index-paj1AOJY.d.ts} +1 -1
  17. package/dist/index.cjs +1057 -217
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +106 -12
  20. package/dist/index.d.ts +106 -12
  21. package/dist/index.js +1041 -217
  22. package/dist/index.js.map +1 -1
  23. package/dist/protocol/index.cjs.map +1 -1
  24. package/dist/protocol/index.d.cts +1 -1
  25. package/dist/protocol/index.d.ts +1 -1
  26. package/dist/protocol/index.js.map +1 -1
  27. package/dist/testing/index.cjs +79 -14
  28. package/dist/testing/index.cjs.map +1 -1
  29. package/dist/testing/index.d.cts +6 -0
  30. package/dist/testing/index.d.ts +6 -0
  31. package/dist/testing/index.js +79 -14
  32. package/dist/testing/index.js.map +1 -1
  33. package/dist/transport/index.cjs +175 -0
  34. package/dist/transport/index.cjs.map +1 -0
  35. package/dist/transport/index.d.cts +37 -0
  36. package/dist/transport/index.d.ts +37 -0
  37. package/dist/transport/index.js +138 -0
  38. package/dist/transport/index.js.map +1 -0
  39. package/dist/types-1YXGwrgI.d.cts +65 -0
  40. package/dist/types-BtPh4Dbd.d.ts +65 -0
  41. package/package.json +15 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ - Add standard TCI IQ capability discovery and dialect-specific sample-rate lists.
6
+ - Track IQ sample-rate, receiver lifecycle, and DDS center-frequency state.
7
+ - Add owned IQ stream sessions with first-frame startup acknowledgement and sample-rate readback.
8
+ - Emit typed IQ frames and add interleaved I/Q decoding helpers.
9
+ - Extend the mock TCI server with standard IQ frames and lifecycle commands.
10
+
11
+ ## 0.2.0
12
+
13
+ - Add an extensible TCI transport, dialect registry, strict handshake, and protocol identity model.
14
+ - Support ExpertSDR TCI 1.4 through 2.0, AetherSDR's 1.5 hybrid protocol, Thetis 2.0, and observed legacy command shapes.
15
+ - Treat constrained drive acknowledgements as successful clamped writes and add active drive readback.
16
+ - Correct scalar versus per-channel `Stream.length` handling for mono, stereo, legacy, and modern streams.
17
+ - Track negotiated audio parameters and expose dialect-aware handshake diagnostics.
package/README.md CHANGED
@@ -6,16 +6,19 @@ TCI is a WebSocket protocol: text commands are used for CAT-style radio control,
6
6
 
7
7
  ## Status
8
8
 
9
- `0.1.x` focuses on the subset needed by application integrations:
9
+ `0.3.x` provides a dialect-aware client for application integrations:
10
10
 
11
- - WebSocket lifecycle and READY/startup state handling
11
+ - Strict WebSocket and READY/startup handshake validation
12
+ - Automatic ExpertSDR 1.4, 1.5-1.8, 1.9-2.0, AetherSDR, and Thetis dialect selection
13
+ - Manual and externally registered dialect implementations
12
14
  - Frequency, mode, PTT, tune, drive, split, and CW text/macros
13
15
  - RX and TX sensor state parsing
14
16
  - RX audio, TX audio, TX_CHRONO, and line-out stream frame parsing/building
17
+ - Standard IQ capability discovery, DDS tracking, sample-rate readback, and owned IQ stream sessions
15
18
  - Serial command queue with timeout, cancellation, and interleaved broadcast handling
16
19
  - Mock TCI server and fake WebSocket transport for integration tests
17
20
 
18
- Panadapter, IQ UI, skimmer, and spots APIs are intentionally out of scope for the first release, but the protocol layer is designed to be extended.
21
+ Panadapter rendering, skimmer DSP, and spots APIs remain application concerns; this package exposes the standard IQ transport without imposing a spectrum implementation.
19
22
 
20
23
  ## Install
21
24
 
@@ -48,10 +51,11 @@ client.on('rxAudioFrame', (frame) => {
48
51
  client.on('txChrono', (request) => {
49
52
  // The host application decides what to transmit.
50
53
  // Send silence if no TX audio is ready.
51
- client.sendTxAudioForChrono(request, new Float32Array(request.sampleCount * request.channels));
54
+ client.sendTxAudioForChrono(request, new Float32Array(request.sampleCount));
52
55
  });
53
56
 
54
- await client.connect();
57
+ const handshake = await client.connect();
58
+ console.log(handshake.identity, handshake.dialect.dialect.id);
55
59
  await client.setFrequency(14_074_000);
56
60
  await client.setMode('digu');
57
61
  await client.configureAudio({
@@ -64,20 +68,64 @@ await client.startAudio();
64
68
  await client.setPtt(true, { source: 'tci' });
65
69
  ```
66
70
 
71
+ ## IQ Streams
72
+
73
+ ```ts
74
+ import { decodeInterleavedIq } from 'tci-client-node';
75
+
76
+ const capabilities = client.getIqCapabilities();
77
+ if (capabilities.supported) {
78
+ const iq = await client.openIqStream({ receiver: 0, sampleRate: 96_000 });
79
+ iq.on('frame', (event) => {
80
+ const interleaved = decodeInterleavedIq(event.frame);
81
+ console.log(event.centerFrequency, event.sampleRate, interleaved.length / 2);
82
+ });
83
+
84
+ const applied = await iq.setSampleRate(48_000);
85
+ console.log(applied.requested, applied.applied);
86
+ await iq.close();
87
+ }
88
+ ```
89
+
90
+ `openIqStream()` resolves on the first valid IQ frame rather than relying on an `IQ_START` echo, because some compatible servers intentionally do not echo that command. The returned session owns the subscription and sends best-effort `IQ_STOP` when closed.
91
+
92
+ `connect()` resolves only after a valid `READY;` initialization sequence. A WebSocket that opens but does not provide enough TCI initialization evidence is rejected. Use `dialect: 'thetis-2.0'` or another dialect ID only when an incomplete server cannot be identified automatically.
93
+
94
+ ## Dialects
95
+
96
+ The high-level client delegates command shapes and binary stream semantics to a `TciDialect`. Built-in dialects are `expertsdr-1.4`, `expertsdr-1.5-1.8`, `expertsdr-1.9-2.0`, `aethersdr-1.5`, `thetis-2.0`, and `generic-observed`.
97
+
98
+ `generic-observed` derives legacy versus TRX-indexed `DRIVE` syntax from startup state. Applications can provide a custom dialect directly or use a custom `TciDialectRegistry`.
99
+
100
+ ## Power Writes
101
+
102
+ Servers may apply a local band or PA safety limit and broadcast a different drive value. Use the detailed result when the difference matters:
103
+
104
+ ```ts
105
+ const result = await client.setDriveWithResult(100);
106
+ // { requested: 100, applied: 50, outcome: 'clamped', acknowledgement: 'state' }
107
+ ```
108
+
109
+ The compatibility `setDrive()` method remains available and treats a clamped value as a successful write rather than a timeout.
110
+
67
111
  ## Subpath Exports
68
112
 
69
113
  - `tci-client-node`: `TciClient`, `createTciClient`, high-level radio/audio API, errors, and core types.
70
114
  - `tci-client-node/protocol`: text command parser/formatter, escaping helpers, and command queue.
71
115
  - `tci-client-node/audio`: stream frame parser/builder and sample conversion helpers.
116
+ - `tci-client-node/dialect`: dialect interfaces, built-ins, detection, and registry.
117
+ - `tci-client-node/transport`: transport interface and default WebSocket transport.
72
118
  - `tci-client-node/testing`: `MockTciServer` and `FakeWebSocket` helpers for tests.
73
119
 
74
120
  ## Audio Frames
75
121
 
76
122
  The official TCI `Stream` header is 16 little-endian `uint32` fields. In this package:
77
123
 
78
- - `sampleCount` maps to the official `Stream.length` field from the header.
124
+ - `headerSampleCount` is the raw `Stream.length` field.
125
+ - `sampleCount` is the canonical scalar value count across all channels.
126
+ - `frameCount` is `sampleCount / channels`.
79
127
  - `payloadLength` is the actual byte length after the 64-byte header. `TX_CHRONO` frames are valid with no payload.
80
- - `channels` is read from the TCI 1.9+ header. If a legacy 1.8-style frame has no channel field, the parser infers it from payload size.
128
+ - Modern TCI uses scalar length semantics. Legacy dialects use per-channel length semantics and may omit the channel field; the selected dialect supplies the required context.
81
129
 
82
130
  Supported sample types are `int16`, `int24`, `int32`, and `float32`.
83
131
 
@@ -111,10 +159,8 @@ The package is built with `tsup` and publishes ESM, CommonJS, and declaration fi
111
159
  Releases are published by GitHub Actions when a `v*` tag is pushed. The tag must
112
160
  match `package.json` exactly, for example `v0.1.0` for version `0.1.0`.
113
161
 
114
- The workflow mirrors the `icom-wlan-node` release shape: install with `npm ci`,
115
- typecheck, build, test, verify the package contents, and publish to npm using
116
- the `NPM_TOKEN` repository secret. Provenance is enabled through npm's
117
- `publishConfig`.
162
+ The workflow installs with `npm ci`, typechecks, builds, tests, verifies package
163
+ contents, and publishes through npm Trusted Publishing (OIDC) with provenance.
118
164
 
119
165
  ## References
120
166
 
@@ -25,6 +25,7 @@ __export(audio_exports, {
25
25
  TciStreamType: () => TciStreamType,
26
26
  buildStreamFrame: () => buildStreamFrame,
27
27
  buildTxAudioFrame: () => buildTxAudioFrame,
28
+ decodeInterleavedIq: () => decodeInterleavedIq,
28
29
  deinterleaveChannels: () => deinterleaveChannels,
29
30
  float32ToPcm16: () => float32ToPcm16,
30
31
  mixToMono: () => mixToMono,
@@ -68,7 +69,7 @@ var TciSampleType = /* @__PURE__ */ ((TciSampleType2) => {
68
69
  TciSampleType2[TciSampleType2["FLOAT32"] = 3] = "FLOAT32";
69
70
  return TciSampleType2;
70
71
  })(TciSampleType || {});
71
- function parseStreamFrame(input) {
72
+ function parseStreamFrame(input, options = {}) {
72
73
  const buffer = toBuffer(input);
73
74
  if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {
74
75
  throw new TciError("invalid-frame", `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);
@@ -77,15 +78,15 @@ function parseStreamFrame(input) {
77
78
  const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));
78
79
  const sampleType = normalizeSampleType(header[2]);
79
80
  const streamType = normalizeStreamType(header[6]);
80
- let channels = header[7];
81
+ let channels = header[7] || options.negotiatedChannels || 0;
81
82
  const bytesPerSample = sampleTypeBytes(sampleType);
82
- const sampleCount = header[5];
83
+ const headerSampleCount = header[5];
83
84
  const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
84
85
  if (channels <= 0) {
85
86
  if (streamType === 3 /* TX_CHRONO */ && actualPayloadLength === 0) {
86
87
  channels = 1;
87
88
  } else {
88
- const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / bytesPerSample : 1;
89
+ const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;
89
90
  if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
90
91
  throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
91
92
  }
@@ -97,16 +98,28 @@ function parseStreamFrame(input) {
97
98
  if (payloadLength % alignedFrameBytes !== 0) {
98
99
  throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
99
100
  }
101
+ const actualScalarCount = payloadLength / bytesPerSample;
102
+ const requestedSemantics = options.lengthSemantics ?? "auto";
103
+ const lengthSemantics = resolveLengthSemantics(
104
+ requestedSemantics,
105
+ streamType,
106
+ headerSampleCount,
107
+ actualScalarCount,
108
+ channels
109
+ );
110
+ const sampleCount = lengthSemantics === "per-channel" ? headerSampleCount * channels : headerSampleCount;
100
111
  if (streamType !== 3 /* TX_CHRONO */) {
101
- const expectedPerChannelPayloadLength = sampleCount * bytesPerSample * channels;
102
- const expectedScalarPayloadLength = sampleCount * bytesPerSample;
103
- if (payloadLength !== expectedPerChannelPayloadLength && payloadLength !== expectedScalarPayloadLength) {
112
+ const expectedPayloadLength = sampleCount * bytesPerSample;
113
+ if (payloadLength !== expectedPayloadLength) {
104
114
  throw new TciError(
105
115
  "invalid-frame",
106
- `TCI stream frame length mismatch: header says ${sampleCount} samples (${expectedPerChannelPayloadLength} payload bytes), got ${payloadLength}`
116
+ `TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`
107
117
  );
108
118
  }
109
119
  }
120
+ if (sampleCount % channels !== 0) {
121
+ throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
122
+ }
110
123
  return {
111
124
  receiver: header[0],
112
125
  sampleRate: header[1],
@@ -118,7 +131,10 @@ function parseStreamFrame(input) {
118
131
  channels,
119
132
  reserved: header.slice(8),
120
133
  payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
121
- sampleCount
134
+ headerSampleCount,
135
+ sampleCount,
136
+ frameCount: sampleCount / channels,
137
+ lengthSemantics
122
138
  };
123
139
  }
124
140
  function buildStreamFrame(options) {
@@ -132,11 +148,19 @@ function buildStreamFrame(options) {
132
148
  if (payload.byteLength % (bytesPerSample * channels) !== 0) {
133
149
  throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
134
150
  }
135
- const derivedSampleCount = payload.byteLength / bytesPerSample / channels;
151
+ const derivedSampleCount = payload.byteLength / bytesPerSample;
136
152
  const sampleCount = options.sampleCount ?? derivedSampleCount;
137
153
  if (!Number.isInteger(sampleCount) || sampleCount < 0) {
138
154
  throw new TciError("invalid-frame", `Invalid TCI sample count: ${sampleCount}`);
139
155
  }
156
+ if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {
157
+ throw new TciError("invalid-frame", `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);
158
+ }
159
+ if (sampleCount % channels !== 0) {
160
+ throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
161
+ }
162
+ const lengthSemantics = options.lengthSemantics === "per-channel" ? "per-channel" : "scalar";
163
+ const headerSampleCount = lengthSemantics === "per-channel" ? sampleCount / channels : sampleCount;
140
164
  const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
141
165
  const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
142
166
  const reserved = options.reserved ?? [];
@@ -146,7 +170,7 @@ function buildStreamFrame(options) {
146
170
  sampleType,
147
171
  options.codec ?? 0,
148
172
  options.crc ?? 0,
149
- sampleCount,
173
+ headerSampleCount,
150
174
  options.streamType,
151
175
  channels,
152
176
  ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0)
@@ -155,6 +179,13 @@ function buildStreamFrame(options) {
155
179
  payload.copy(frame, TCI_STREAM_HEADER_BYTES);
156
180
  return frame;
157
181
  }
182
+ function resolveLengthSemantics(requested, streamType, headerSampleCount, actualScalarCount, channels) {
183
+ if (requested !== "auto") return requested;
184
+ if (streamType === 3 /* TX_CHRONO */ && actualScalarCount === 0) return "scalar";
185
+ if (actualScalarCount === headerSampleCount) return "scalar";
186
+ if (actualScalarCount === headerSampleCount * channels) return "per-channel";
187
+ return "scalar";
188
+ }
158
189
  function buildTxAudioFrame(options) {
159
190
  return buildStreamFrame({ ...options, streamType: 2 /* TX_AUDIO_STREAM */ });
160
191
  }
@@ -239,6 +270,15 @@ function payloadToFloat32(frameOrPayload, sampleType) {
239
270
  }
240
271
  return output;
241
272
  }
273
+ function decodeInterleavedIq(frame) {
274
+ if (frame.streamType !== 0 /* IQ_STREAM */) {
275
+ throw new TciError("invalid-frame", "Expected a TCI IQ stream frame");
276
+ }
277
+ if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
278
+ throw new TciError("invalid-frame", `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);
279
+ }
280
+ return payloadToFloat32(frame);
281
+ }
242
282
  function samplesToPayload(samples, sampleType) {
243
283
  const type = normalizeSampleType(sampleType);
244
284
  const bytes = sampleTypeBytes(type);
@@ -341,6 +381,7 @@ function writeInt24(view, offset, value) {
341
381
  TciStreamType,
342
382
  buildStreamFrame,
343
383
  buildTxAudioFrame,
384
+ decodeInterleavedIq,
344
385
  deinterleaveChannels,
345
386
  float32ToPcm16,
346
387
  mixToMono,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/audio/index.ts","../../src/errors.ts","../../src/audio/streamFrame.ts"],"sourcesContent":["export * from './streamFrame.js';\n","export type TciErrorCode =\n | 'connect-timeout'\n | 'command-timeout'\n | 'not-connected'\n | 'disconnected'\n | 'protocol-error'\n | 'invalid-frame'\n | 'cancelled';\n\nexport class TciError extends Error {\n readonly code: TciErrorCode;\n readonly details?: unknown;\n\n constructor(code: TciErrorCode, message: string, details?: unknown) {\n super(message);\n this.name = 'TciError';\n this.code = code;\n this.details = details;\n }\n}\n\nexport function toTciError(error: unknown, fallbackCode: TciErrorCode = 'protocol-error'): TciError {\n if (error instanceof TciError) {\n return error;\n }\n if (error instanceof Error) {\n return new TciError(fallbackCode, error.message, error);\n }\n return new TciError(fallbackCode, String(error), error);\n}\n","import { TciError } from '../errors.js';\n\nexport const TCI_STREAM_HEADER_BYTES = 16 * 4;\n\nexport enum TciStreamType {\n IQ_STREAM = 0,\n RX_AUDIO_STREAM = 1,\n TX_AUDIO_STREAM = 2,\n TX_CHRONO = 3,\n LINEOUT_STREAM = 4,\n}\n\nexport enum TciSampleType {\n INT16 = 0,\n INT24 = 1,\n INT32 = 2,\n FLOAT32 = 3,\n}\n\nexport type TciSampleTypeName = 'int16' | 'int24' | 'int32' | 'float32';\n\nexport interface TciStreamFrame {\n receiver: number;\n sampleRate: number;\n sampleType: TciSampleType;\n codec: number;\n crc: number;\n /** Byte length of the payload following the 64-byte TCI stream header. */\n payloadLength: number;\n streamType: TciStreamType;\n channels: number;\n reserved: number[];\n payload: Buffer;\n /** Official Stream.length value: number of samples per channel in the payload. */\n sampleCount: number;\n}\n\nexport interface BuildStreamFrameOptions {\n receiver?: number;\n sampleRate: number;\n sampleType: TciSampleType | TciSampleTypeName;\n streamType: TciStreamType;\n channels: number;\n payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;\n samples?: Float32Array | readonly number[];\n /** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */\n sampleCount?: number;\n codec?: number;\n crc?: number;\n reserved?: readonly number[];\n}\n\nexport interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {\n receiver?: number;\n}\n\nexport function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView): TciStreamFrame {\n const buffer = toBuffer(input);\n if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {\n throw new TciError('invalid-frame', `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));\n const sampleType = normalizeSampleType(header[2]);\n const streamType = normalizeStreamType(header[6]);\n let channels = header[7];\n const bytesPerSample = sampleTypeBytes(sampleType);\n const sampleCount = header[5];\n const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;\n if (channels <= 0) {\n if (streamType === TciStreamType.TX_CHRONO && actualPayloadLength === 0) {\n channels = 1;\n } else {\n const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / bytesPerSample : 1;\n if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n channels = inferredChannels;\n }\n }\n const payloadLength = actualPayloadLength;\n const alignedFrameBytes = bytesPerSample * channels;\n if (payloadLength % alignedFrameBytes !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n\n if (streamType !== TciStreamType.TX_CHRONO) {\n const expectedPerChannelPayloadLength = sampleCount * bytesPerSample * channels;\n const expectedScalarPayloadLength = sampleCount * bytesPerSample;\n if (payloadLength !== expectedPerChannelPayloadLength && payloadLength !== expectedScalarPayloadLength) {\n throw new TciError(\n 'invalid-frame',\n `TCI stream frame length mismatch: header says ${sampleCount} samples (${expectedPerChannelPayloadLength} payload bytes), got ${payloadLength}`,\n );\n }\n }\n\n return {\n receiver: header[0],\n sampleRate: header[1],\n sampleType,\n codec: header[3],\n crc: header[4],\n payloadLength,\n streamType,\n channels,\n reserved: header.slice(8),\n payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),\n sampleCount,\n };\n}\n\nexport function buildStreamFrame(options: BuildStreamFrameOptions): Buffer {\n const sampleType = normalizeSampleType(options.sampleType);\n const payload = options.payload ? toBuffer(options.payload) : samplesToPayload(options.samples ?? [], sampleType);\n const channels = options.channels;\n if (channels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n const bytesPerSample = sampleTypeBytes(sampleType);\n if (payload.byteLength % (bytesPerSample * channels) !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n const derivedSampleCount = payload.byteLength / bytesPerSample / channels;\n const sampleCount = options.sampleCount ?? derivedSampleCount;\n if (!Number.isInteger(sampleCount) || sampleCount < 0) {\n throw new TciError('invalid-frame', `Invalid TCI sample count: ${sampleCount}`);\n }\n\n const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);\n const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);\n const reserved = options.reserved ?? [];\n const header = [\n options.receiver ?? 0,\n options.sampleRate,\n sampleType,\n options.codec ?? 0,\n options.crc ?? 0,\n sampleCount,\n options.streamType,\n channels,\n ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0),\n ];\n header.forEach((value, index) => view.setUint32(index * 4, value >>> 0, true));\n payload.copy(frame, TCI_STREAM_HEADER_BYTES);\n return frame;\n}\n\nexport function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer {\n return buildStreamFrame({ ...options, streamType: TciStreamType.TX_AUDIO_STREAM });\n}\n\nexport function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number {\n switch (normalizeSampleType(sampleType)) {\n case TciSampleType.INT16:\n return 2;\n case TciSampleType.INT24:\n return 3;\n case TciSampleType.INT32:\n case TciSampleType.FLOAT32:\n return 4;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName {\n switch (sampleType) {\n case TciSampleType.INT16:\n return 'int16';\n case TciSampleType.INT24:\n return 'int24';\n case TciSampleType.INT32:\n return 'int32';\n case TciSampleType.FLOAT32:\n return 'float32';\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType {\n if (typeof sampleType === 'string') {\n switch (sampleType.toLowerCase()) {\n case 'int16':\n return TciSampleType.INT16;\n case 'int24':\n return TciSampleType.INT24;\n case 'int32':\n return TciSampleType.INT32;\n case 'float32':\n return TciSampleType.FLOAT32;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n }\n if (sampleType >= TciSampleType.INT16 && sampleType <= TciSampleType.FLOAT32) {\n return sampleType as TciSampleType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n}\n\nexport function normalizeStreamType(streamType: TciStreamType | number): TciStreamType {\n if (streamType >= TciStreamType.IQ_STREAM && streamType <= TciStreamType.LINEOUT_STREAM) {\n return streamType as TciStreamType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI stream type: ${streamType}`);\n}\n\nexport function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array {\n const payload = isFrame(frameOrPayload) ? frameOrPayload.payload : toBuffer(frameOrPayload);\n const type = isFrame(frameOrPayload) ? frameOrPayload.sampleType : normalizeSampleType(sampleType ?? TciSampleType.FLOAT32);\n const bytes = sampleTypeBytes(type);\n if (payload.byteLength % bytes !== 0) {\n throw new TciError('invalid-frame', 'Payload length is not aligned to sample type');\n }\n\n const output = new Float32Array(payload.byteLength / bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < output.length; i += 1) {\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n output[i] = view.getInt16(offset, true) / 32768;\n break;\n case TciSampleType.INT24:\n output[i] = readInt24(view, offset) / 8388608;\n break;\n case TciSampleType.INT32:\n output[i] = view.getInt32(offset, true) / 2147483648;\n break;\n case TciSampleType.FLOAT32:\n output[i] = view.getFloat32(offset, true);\n break;\n }\n }\n return output;\n}\n\nexport function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer {\n const type = normalizeSampleType(sampleType);\n const bytes = sampleTypeBytes(type);\n const payload = Buffer.alloc(samples.length * bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < samples.length; i += 1) {\n const value = clampSample(samples[i] ?? 0);\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n view.setInt16(offset, Math.round(value * 32767), true);\n break;\n case TciSampleType.INT24:\n writeInt24(view, offset, Math.round(value * 8388607));\n break;\n case TciSampleType.INT32:\n view.setInt32(offset, Math.round(value * 2147483647), true);\n break;\n case TciSampleType.FLOAT32:\n view.setFloat32(offset, value, true);\n break;\n }\n }\n return payload;\n}\n\nexport function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array {\n if (input instanceof Int16Array) {\n const output = new Float32Array(input.length);\n for (let i = 0; i < input.length; i += 1) {\n output[i] = input[i] / 32768;\n }\n return output;\n }\n return payloadToFloat32(toBuffer(input), TciSampleType.INT16);\n}\n\nexport function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer {\n return samplesToPayload(samples, TciSampleType.INT16);\n}\n\nexport function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[] {\n if (channels <= 0 || samples.length % channels !== 0) {\n throw new TciError('invalid-frame', 'Cannot deinterleave samples with invalid channel count');\n }\n const frames = samples.length / channels;\n const outputs = Array.from({ length: channels }, () => new Float32Array(frames));\n for (let frame = 0; frame < frames; frame += 1) {\n for (let channel = 0; channel < channels; channel += 1) {\n outputs[channel][frame] = samples[frame * channels + channel];\n }\n }\n return outputs;\n}\n\nexport function mixToMono(samples: Float32Array, channels: number): Float32Array {\n if (channels === 1) {\n return samples;\n }\n const separated = deinterleaveChannels(samples, channels);\n const mono = new Float32Array(separated[0]?.length ?? 0);\n for (const channel of separated) {\n for (let i = 0; i < mono.length; i += 1) {\n mono[i] += channel[i] / channels;\n }\n }\n return mono;\n}\n\nfunction toBuffer(input: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView): Buffer {\n if (Buffer.isBuffer(input)) {\n return input;\n }\n if (input instanceof ArrayBuffer) {\n return Buffer.from(input);\n }\n if (ArrayBuffer.isView(input)) {\n return Buffer.from(input.buffer, input.byteOffset, input.byteLength);\n }\n return Buffer.from(input);\n}\n\nfunction isFrame(value: unknown): value is TciStreamFrame {\n return Boolean(value && typeof value === 'object' && 'payload' in value && 'sampleType' in value);\n}\n\nfunction clampSample(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n return Math.max(-1, Math.min(1, value));\n}\n\nfunction readInt24(view: DataView, offset: number): number {\n const value = view.getUint8(offset) | (view.getUint8(offset + 1) << 8) | (view.getUint8(offset + 2) << 16);\n return value & 0x800000 ? value | 0xff000000 : value;\n}\n\nfunction writeInt24(view: DataView, offset: number, value: number): void {\n const clamped = Math.max(-8388608, Math.min(8388607, value));\n view.setUint8(offset, clamped & 0xff);\n view.setUint8(offset + 1, (clamped >> 8) & 0xff);\n view.setUint8(offset + 2, (clamped >> 16) & 0xff);\n}\n"],"mappings":"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","names":["TciStreamType","TciSampleType"]}
1
+ {"version":3,"sources":["../../src/audio/index.ts","../../src/errors.ts","../../src/audio/streamFrame.ts"],"sourcesContent":["export * from './streamFrame.js';\n","export type TciErrorCode =\n | 'connect-timeout'\n | 'handshake-timeout'\n | 'invalid-handshake'\n | 'unknown-dialect'\n | 'command-timeout'\n | 'not-connected'\n | 'disconnected'\n | 'protocol-error'\n | 'invalid-frame'\n | 'cancelled';\n\nexport class TciError extends Error {\n readonly code: TciErrorCode;\n readonly details?: unknown;\n\n constructor(code: TciErrorCode, message: string, details?: unknown) {\n super(message);\n this.name = 'TciError';\n this.code = code;\n this.details = details;\n }\n}\n\nexport function toTciError(error: unknown, fallbackCode: TciErrorCode = 'protocol-error'): TciError {\n if (error instanceof TciError) {\n return error;\n }\n if (error instanceof Error) {\n return new TciError(fallbackCode, error.message, error);\n }\n return new TciError(fallbackCode, String(error), error);\n}\n","import { TciError } from '../errors.js';\nimport type { TciStreamLengthSemantics } from '../dialect/types.js';\n\nexport const TCI_STREAM_HEADER_BYTES = 16 * 4;\n\nexport enum TciStreamType {\n IQ_STREAM = 0,\n RX_AUDIO_STREAM = 1,\n TX_AUDIO_STREAM = 2,\n TX_CHRONO = 3,\n LINEOUT_STREAM = 4,\n}\n\nexport enum TciSampleType {\n INT16 = 0,\n INT24 = 1,\n INT32 = 2,\n FLOAT32 = 3,\n}\n\nexport type TciSampleTypeName = 'int16' | 'int24' | 'int32' | 'float32';\n\nexport interface TciStreamFrame {\n receiver: number;\n sampleRate: number;\n sampleType: TciSampleType;\n codec: number;\n crc: number;\n /** Byte length of the payload following the 64-byte TCI stream header. */\n payloadLength: number;\n streamType: TciStreamType;\n channels: number;\n reserved: number[];\n payload: Buffer;\n /** Raw Stream.length value from the wire header. */\n headerSampleCount: number;\n /** Canonical number of scalar sample values across all channels. */\n sampleCount: number;\n /** Number of sample frames, each containing `channels` scalar values. */\n frameCount: number;\n lengthSemantics: Exclude<TciStreamLengthSemantics, 'auto'>;\n}\n\nexport interface ParseStreamFrameOptions {\n lengthSemantics?: TciStreamLengthSemantics;\n negotiatedChannels?: number;\n}\n\nexport interface BuildStreamFrameOptions {\n receiver?: number;\n sampleRate: number;\n sampleType: TciSampleType | TciSampleTypeName;\n streamType: TciStreamType;\n channels: number;\n payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;\n samples?: Float32Array | readonly number[];\n /** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */\n sampleCount?: number;\n lengthSemantics?: TciStreamLengthSemantics;\n codec?: number;\n crc?: number;\n reserved?: readonly number[];\n}\n\nexport interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {\n receiver?: number;\n}\n\nexport function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView, options: ParseStreamFrameOptions = {}): TciStreamFrame {\n const buffer = toBuffer(input);\n if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {\n throw new TciError('invalid-frame', `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);\n const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));\n const sampleType = normalizeSampleType(header[2]);\n const streamType = normalizeStreamType(header[6]);\n let channels = header[7] || options.negotiatedChannels || 0;\n const bytesPerSample = sampleTypeBytes(sampleType);\n const headerSampleCount = header[5];\n const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;\n if (channels <= 0) {\n if (streamType === TciStreamType.TX_CHRONO && actualPayloadLength === 0) {\n channels = 1;\n } else {\n const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;\n if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n channels = inferredChannels;\n }\n }\n const payloadLength = actualPayloadLength;\n const alignedFrameBytes = bytesPerSample * channels;\n if (payloadLength % alignedFrameBytes !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n\n const actualScalarCount = payloadLength / bytesPerSample;\n const requestedSemantics = options.lengthSemantics ?? 'auto';\n const lengthSemantics = resolveLengthSemantics(\n requestedSemantics,\n streamType,\n headerSampleCount,\n actualScalarCount,\n channels,\n );\n const sampleCount = lengthSemantics === 'per-channel' ? headerSampleCount * channels : headerSampleCount;\n\n if (streamType !== TciStreamType.TX_CHRONO) {\n const expectedPayloadLength = sampleCount * bytesPerSample;\n if (payloadLength !== expectedPayloadLength) {\n throw new TciError(\n 'invalid-frame',\n `TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`,\n );\n }\n }\n\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n\n return {\n receiver: header[0],\n sampleRate: header[1],\n sampleType,\n codec: header[3],\n crc: header[4],\n payloadLength,\n streamType,\n channels,\n reserved: header.slice(8),\n payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),\n headerSampleCount,\n sampleCount,\n frameCount: sampleCount / channels,\n lengthSemantics,\n };\n}\n\nexport function buildStreamFrame(options: BuildStreamFrameOptions): Buffer {\n const sampleType = normalizeSampleType(options.sampleType);\n const payload = options.payload ? toBuffer(options.payload) : samplesToPayload(options.samples ?? [], sampleType);\n const channels = options.channels;\n if (channels <= 0) {\n throw new TciError('invalid-frame', `Invalid TCI channel count: ${channels}`);\n }\n const bytesPerSample = sampleTypeBytes(sampleType);\n if (payload.byteLength % (bytesPerSample * channels) !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\n }\n const derivedSampleCount = payload.byteLength / bytesPerSample;\n const sampleCount = options.sampleCount ?? derivedSampleCount;\n if (!Number.isInteger(sampleCount) || sampleCount < 0) {\n throw new TciError('invalid-frame', `Invalid TCI sample count: ${sampleCount}`);\n }\n if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {\n throw new TciError('invalid-frame', `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);\n }\n if (sampleCount % channels !== 0) {\n throw new TciError('invalid-frame', `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);\n }\n const lengthSemantics = options.lengthSemantics === 'per-channel' ? 'per-channel' : 'scalar';\n const headerSampleCount = lengthSemantics === 'per-channel' ? sampleCount / channels : sampleCount;\n\n const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);\n const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);\n const reserved = options.reserved ?? [];\n const header = [\n options.receiver ?? 0,\n options.sampleRate,\n sampleType,\n options.codec ?? 0,\n options.crc ?? 0,\n headerSampleCount,\n options.streamType,\n channels,\n ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0),\n ];\n header.forEach((value, index) => view.setUint32(index * 4, value >>> 0, true));\n payload.copy(frame, TCI_STREAM_HEADER_BYTES);\n return frame;\n}\n\nfunction resolveLengthSemantics(\n requested: TciStreamLengthSemantics,\n streamType: TciStreamType,\n headerSampleCount: number,\n actualScalarCount: number,\n channels: number,\n): Exclude<TciStreamLengthSemantics, 'auto'> {\n if (requested !== 'auto') return requested;\n if (streamType === TciStreamType.TX_CHRONO && actualScalarCount === 0) return 'scalar';\n if (actualScalarCount === headerSampleCount) return 'scalar';\n if (actualScalarCount === headerSampleCount * channels) return 'per-channel';\n return 'scalar';\n}\n\nexport function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer {\n return buildStreamFrame({ ...options, streamType: TciStreamType.TX_AUDIO_STREAM });\n}\n\nexport function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number {\n switch (normalizeSampleType(sampleType)) {\n case TciSampleType.INT16:\n return 2;\n case TciSampleType.INT24:\n return 3;\n case TciSampleType.INT32:\n case TciSampleType.FLOAT32:\n return 4;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName {\n switch (sampleType) {\n case TciSampleType.INT16:\n return 'int16';\n case TciSampleType.INT24:\n return 'int24';\n case TciSampleType.INT32:\n return 'int32';\n case TciSampleType.FLOAT32:\n return 'float32';\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n}\n\nexport function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType {\n if (typeof sampleType === 'string') {\n switch (sampleType.toLowerCase()) {\n case 'int16':\n return TciSampleType.INT16;\n case 'int24':\n return TciSampleType.INT24;\n case 'int32':\n return TciSampleType.INT32;\n case 'float32':\n return TciSampleType.FLOAT32;\n default:\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n }\n }\n if (sampleType >= TciSampleType.INT16 && sampleType <= TciSampleType.FLOAT32) {\n return sampleType as TciSampleType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI sample type: ${sampleType}`);\n}\n\nexport function normalizeStreamType(streamType: TciStreamType | number): TciStreamType {\n if (streamType >= TciStreamType.IQ_STREAM && streamType <= TciStreamType.LINEOUT_STREAM) {\n return streamType as TciStreamType;\n }\n throw new TciError('invalid-frame', `Unsupported TCI stream type: ${streamType}`);\n}\n\nexport function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array {\n const payload = isFrame(frameOrPayload) ? frameOrPayload.payload : toBuffer(frameOrPayload);\n const type = isFrame(frameOrPayload) ? frameOrPayload.sampleType : normalizeSampleType(sampleType ?? TciSampleType.FLOAT32);\n const bytes = sampleTypeBytes(type);\n if (payload.byteLength % bytes !== 0) {\n throw new TciError('invalid-frame', 'Payload length is not aligned to sample type');\n }\n\n const output = new Float32Array(payload.byteLength / bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < output.length; i += 1) {\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n output[i] = view.getInt16(offset, true) / 32768;\n break;\n case TciSampleType.INT24:\n output[i] = readInt24(view, offset) / 8388608;\n break;\n case TciSampleType.INT32:\n output[i] = view.getInt32(offset, true) / 2147483648;\n break;\n case TciSampleType.FLOAT32:\n output[i] = view.getFloat32(offset, true);\n break;\n }\n }\n return output;\n}\n\nexport function decodeInterleavedIq(frame: TciStreamFrame): Float32Array {\n if (frame.streamType !== TciStreamType.IQ_STREAM) {\n throw new TciError('invalid-frame', 'Expected a TCI IQ stream frame');\n }\n if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {\n throw new TciError('invalid-frame', `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);\n }\n return payloadToFloat32(frame);\n}\n\nexport function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer {\n const type = normalizeSampleType(sampleType);\n const bytes = sampleTypeBytes(type);\n const payload = Buffer.alloc(samples.length * bytes);\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);\n for (let i = 0; i < samples.length; i += 1) {\n const value = clampSample(samples[i] ?? 0);\n const offset = i * bytes;\n switch (type) {\n case TciSampleType.INT16:\n view.setInt16(offset, Math.round(value * 32767), true);\n break;\n case TciSampleType.INT24:\n writeInt24(view, offset, Math.round(value * 8388607));\n break;\n case TciSampleType.INT32:\n view.setInt32(offset, Math.round(value * 2147483647), true);\n break;\n case TciSampleType.FLOAT32:\n view.setFloat32(offset, value, true);\n break;\n }\n }\n return payload;\n}\n\nexport function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array {\n if (input instanceof Int16Array) {\n const output = new Float32Array(input.length);\n for (let i = 0; i < input.length; i += 1) {\n output[i] = input[i] / 32768;\n }\n return output;\n }\n return payloadToFloat32(toBuffer(input), TciSampleType.INT16);\n}\n\nexport function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer {\n return samplesToPayload(samples, TciSampleType.INT16);\n}\n\nexport function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[] {\n if (channels <= 0 || samples.length % channels !== 0) {\n throw new TciError('invalid-frame', 'Cannot deinterleave samples with invalid channel count');\n }\n const frames = samples.length / channels;\n const outputs = Array.from({ length: channels }, () => new Float32Array(frames));\n for (let frame = 0; frame < frames; frame += 1) {\n for (let channel = 0; channel < channels; channel += 1) {\n outputs[channel][frame] = samples[frame * channels + channel];\n }\n }\n return outputs;\n}\n\nexport function mixToMono(samples: Float32Array, channels: number): Float32Array {\n if (channels === 1) {\n return samples;\n }\n const separated = deinterleaveChannels(samples, channels);\n const mono = new Float32Array(separated[0]?.length ?? 0);\n for (const channel of separated) {\n for (let i = 0; i < mono.length; i += 1) {\n mono[i] += channel[i] / channels;\n }\n }\n return mono;\n}\n\nfunction toBuffer(input: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView): Buffer {\n if (Buffer.isBuffer(input)) {\n return input;\n }\n if (input instanceof ArrayBuffer) {\n return Buffer.from(input);\n }\n if (ArrayBuffer.isView(input)) {\n return Buffer.from(input.buffer, input.byteOffset, input.byteLength);\n }\n return Buffer.from(input);\n}\n\nfunction isFrame(value: unknown): value is TciStreamFrame {\n return Boolean(value && typeof value === 'object' && 'payload' in value && 'sampleType' in value);\n}\n\nfunction clampSample(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n return Math.max(-1, Math.min(1, value));\n}\n\nfunction readInt24(view: DataView, offset: number): number {\n const value = view.getUint8(offset) | (view.getUint8(offset + 1) << 8) | (view.getUint8(offset + 2) << 16);\n return value & 0x800000 ? value | 0xff000000 : value;\n}\n\nfunction writeInt24(view: DataView, offset: number, value: number): void {\n const clamped = Math.max(-8388608, Math.min(8388607, value));\n view.setUint8(offset, clamped & 0xff);\n view.setUint8(offset + 1, (clamped >> 8) & 0xff);\n view.setUint8(offset + 2, (clamped >> 16) & 0xff);\n}\n"],"mappings":"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","names":["TciStreamType","TciSampleType"]}
@@ -1,3 +1,6 @@
1
+ import { j as TciStreamLengthSemantics } from '../types-1YXGwrgI.cjs';
2
+ import '../text-BwCWY1k1.cjs';
3
+
1
4
  declare const TCI_STREAM_HEADER_BYTES: number;
2
5
  declare enum TciStreamType {
3
6
  IQ_STREAM = 0,
@@ -25,8 +28,17 @@ interface TciStreamFrame {
25
28
  channels: number;
26
29
  reserved: number[];
27
30
  payload: Buffer;
28
- /** Official Stream.length value: number of samples per channel in the payload. */
31
+ /** Raw Stream.length value from the wire header. */
32
+ headerSampleCount: number;
33
+ /** Canonical number of scalar sample values across all channels. */
29
34
  sampleCount: number;
35
+ /** Number of sample frames, each containing `channels` scalar values. */
36
+ frameCount: number;
37
+ lengthSemantics: Exclude<TciStreamLengthSemantics, 'auto'>;
38
+ }
39
+ interface ParseStreamFrameOptions {
40
+ lengthSemantics?: TciStreamLengthSemantics;
41
+ negotiatedChannels?: number;
30
42
  }
31
43
  interface BuildStreamFrameOptions {
32
44
  receiver?: number;
@@ -38,6 +50,7 @@ interface BuildStreamFrameOptions {
38
50
  samples?: Float32Array | readonly number[];
39
51
  /** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */
40
52
  sampleCount?: number;
53
+ lengthSemantics?: TciStreamLengthSemantics;
41
54
  codec?: number;
42
55
  crc?: number;
43
56
  reserved?: readonly number[];
@@ -45,7 +58,7 @@ interface BuildStreamFrameOptions {
45
58
  interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {
46
59
  receiver?: number;
47
60
  }
48
- declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView): TciStreamFrame;
61
+ declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView, options?: ParseStreamFrameOptions): TciStreamFrame;
49
62
  declare function buildStreamFrame(options: BuildStreamFrameOptions): Buffer;
50
63
  declare function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer;
51
64
  declare function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number;
@@ -53,10 +66,11 @@ declare function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName;
53
66
  declare function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType;
54
67
  declare function normalizeStreamType(streamType: TciStreamType | number): TciStreamType;
55
68
  declare function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array;
69
+ declare function decodeInterleavedIq(frame: TciStreamFrame): Float32Array;
56
70
  declare function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer;
57
71
  declare function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array;
58
72
  declare function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer;
59
73
  declare function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[];
60
74
  declare function mixToMono(samples: Float32Array, channels: number): Float32Array;
61
75
 
62
- export { type BuildStreamFrameOptions, type BuildTxAudioFrameOptions, TCI_STREAM_HEADER_BYTES, TciSampleType, type TciSampleTypeName, type TciStreamFrame, TciStreamType, buildStreamFrame, buildTxAudioFrame, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload };
76
+ export { type BuildStreamFrameOptions, type BuildTxAudioFrameOptions, type ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciSampleType, type TciSampleTypeName, type TciStreamFrame, TciStreamType, buildStreamFrame, buildTxAudioFrame, decodeInterleavedIq, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload };
@@ -1,3 +1,6 @@
1
+ import { j as TciStreamLengthSemantics } from '../types-BtPh4Dbd.js';
2
+ import '../text-BwCWY1k1.js';
3
+
1
4
  declare const TCI_STREAM_HEADER_BYTES: number;
2
5
  declare enum TciStreamType {
3
6
  IQ_STREAM = 0,
@@ -25,8 +28,17 @@ interface TciStreamFrame {
25
28
  channels: number;
26
29
  reserved: number[];
27
30
  payload: Buffer;
28
- /** Official Stream.length value: number of samples per channel in the payload. */
31
+ /** Raw Stream.length value from the wire header. */
32
+ headerSampleCount: number;
33
+ /** Canonical number of scalar sample values across all channels. */
29
34
  sampleCount: number;
35
+ /** Number of sample frames, each containing `channels` scalar values. */
36
+ frameCount: number;
37
+ lengthSemantics: Exclude<TciStreamLengthSemantics, 'auto'>;
38
+ }
39
+ interface ParseStreamFrameOptions {
40
+ lengthSemantics?: TciStreamLengthSemantics;
41
+ negotiatedChannels?: number;
30
42
  }
31
43
  interface BuildStreamFrameOptions {
32
44
  receiver?: number;
@@ -38,6 +50,7 @@ interface BuildStreamFrameOptions {
38
50
  samples?: Float32Array | readonly number[];
39
51
  /** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */
40
52
  sampleCount?: number;
53
+ lengthSemantics?: TciStreamLengthSemantics;
41
54
  codec?: number;
42
55
  crc?: number;
43
56
  reserved?: readonly number[];
@@ -45,7 +58,7 @@ interface BuildStreamFrameOptions {
45
58
  interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {
46
59
  receiver?: number;
47
60
  }
48
- declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView): TciStreamFrame;
61
+ declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView, options?: ParseStreamFrameOptions): TciStreamFrame;
49
62
  declare function buildStreamFrame(options: BuildStreamFrameOptions): Buffer;
50
63
  declare function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer;
51
64
  declare function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number;
@@ -53,10 +66,11 @@ declare function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName;
53
66
  declare function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType;
54
67
  declare function normalizeStreamType(streamType: TciStreamType | number): TciStreamType;
55
68
  declare function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array;
69
+ declare function decodeInterleavedIq(frame: TciStreamFrame): Float32Array;
56
70
  declare function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer;
57
71
  declare function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array;
58
72
  declare function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer;
59
73
  declare function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[];
60
74
  declare function mixToMono(samples: Float32Array, channels: number): Float32Array;
61
75
 
62
- export { type BuildStreamFrameOptions, type BuildTxAudioFrameOptions, TCI_STREAM_HEADER_BYTES, TciSampleType, type TciSampleTypeName, type TciStreamFrame, TciStreamType, buildStreamFrame, buildTxAudioFrame, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload };
76
+ export { type BuildStreamFrameOptions, type BuildTxAudioFrameOptions, type ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciSampleType, type TciSampleTypeName, type TciStreamFrame, TciStreamType, buildStreamFrame, buildTxAudioFrame, decodeInterleavedIq, deinterleaveChannels, float32ToPcm16, mixToMono, normalizeSampleType, normalizeStreamType, parseStreamFrame, payloadToFloat32, pcm16ToFloat32, sampleTypeBytes, sampleTypeName, samplesToPayload };
@@ -27,7 +27,7 @@ var TciSampleType = /* @__PURE__ */ ((TciSampleType2) => {
27
27
  TciSampleType2[TciSampleType2["FLOAT32"] = 3] = "FLOAT32";
28
28
  return TciSampleType2;
29
29
  })(TciSampleType || {});
30
- function parseStreamFrame(input) {
30
+ function parseStreamFrame(input, options = {}) {
31
31
  const buffer = toBuffer(input);
32
32
  if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {
33
33
  throw new TciError("invalid-frame", `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);
@@ -36,15 +36,15 @@ function parseStreamFrame(input) {
36
36
  const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));
37
37
  const sampleType = normalizeSampleType(header[2]);
38
38
  const streamType = normalizeStreamType(header[6]);
39
- let channels = header[7];
39
+ let channels = header[7] || options.negotiatedChannels || 0;
40
40
  const bytesPerSample = sampleTypeBytes(sampleType);
41
- const sampleCount = header[5];
41
+ const headerSampleCount = header[5];
42
42
  const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
43
43
  if (channels <= 0) {
44
44
  if (streamType === 3 /* TX_CHRONO */ && actualPayloadLength === 0) {
45
45
  channels = 1;
46
46
  } else {
47
- const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / bytesPerSample : 1;
47
+ const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;
48
48
  if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
49
49
  throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
50
50
  }
@@ -56,16 +56,28 @@ function parseStreamFrame(input) {
56
56
  if (payloadLength % alignedFrameBytes !== 0) {
57
57
  throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
58
58
  }
59
+ const actualScalarCount = payloadLength / bytesPerSample;
60
+ const requestedSemantics = options.lengthSemantics ?? "auto";
61
+ const lengthSemantics = resolveLengthSemantics(
62
+ requestedSemantics,
63
+ streamType,
64
+ headerSampleCount,
65
+ actualScalarCount,
66
+ channels
67
+ );
68
+ const sampleCount = lengthSemantics === "per-channel" ? headerSampleCount * channels : headerSampleCount;
59
69
  if (streamType !== 3 /* TX_CHRONO */) {
60
- const expectedPerChannelPayloadLength = sampleCount * bytesPerSample * channels;
61
- const expectedScalarPayloadLength = sampleCount * bytesPerSample;
62
- if (payloadLength !== expectedPerChannelPayloadLength && payloadLength !== expectedScalarPayloadLength) {
70
+ const expectedPayloadLength = sampleCount * bytesPerSample;
71
+ if (payloadLength !== expectedPayloadLength) {
63
72
  throw new TciError(
64
73
  "invalid-frame",
65
- `TCI stream frame length mismatch: header says ${sampleCount} samples (${expectedPerChannelPayloadLength} payload bytes), got ${payloadLength}`
74
+ `TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`
66
75
  );
67
76
  }
68
77
  }
78
+ if (sampleCount % channels !== 0) {
79
+ throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
80
+ }
69
81
  return {
70
82
  receiver: header[0],
71
83
  sampleRate: header[1],
@@ -77,7 +89,10 @@ function parseStreamFrame(input) {
77
89
  channels,
78
90
  reserved: header.slice(8),
79
91
  payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
80
- sampleCount
92
+ headerSampleCount,
93
+ sampleCount,
94
+ frameCount: sampleCount / channels,
95
+ lengthSemantics
81
96
  };
82
97
  }
83
98
  function buildStreamFrame(options) {
@@ -91,11 +106,19 @@ function buildStreamFrame(options) {
91
106
  if (payload.byteLength % (bytesPerSample * channels) !== 0) {
92
107
  throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
93
108
  }
94
- const derivedSampleCount = payload.byteLength / bytesPerSample / channels;
109
+ const derivedSampleCount = payload.byteLength / bytesPerSample;
95
110
  const sampleCount = options.sampleCount ?? derivedSampleCount;
96
111
  if (!Number.isInteger(sampleCount) || sampleCount < 0) {
97
112
  throw new TciError("invalid-frame", `Invalid TCI sample count: ${sampleCount}`);
98
113
  }
114
+ if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {
115
+ throw new TciError("invalid-frame", `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);
116
+ }
117
+ if (sampleCount % channels !== 0) {
118
+ throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
119
+ }
120
+ const lengthSemantics = options.lengthSemantics === "per-channel" ? "per-channel" : "scalar";
121
+ const headerSampleCount = lengthSemantics === "per-channel" ? sampleCount / channels : sampleCount;
99
122
  const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
100
123
  const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
101
124
  const reserved = options.reserved ?? [];
@@ -105,7 +128,7 @@ function buildStreamFrame(options) {
105
128
  sampleType,
106
129
  options.codec ?? 0,
107
130
  options.crc ?? 0,
108
- sampleCount,
131
+ headerSampleCount,
109
132
  options.streamType,
110
133
  channels,
111
134
  ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0)
@@ -114,6 +137,13 @@ function buildStreamFrame(options) {
114
137
  payload.copy(frame, TCI_STREAM_HEADER_BYTES);
115
138
  return frame;
116
139
  }
140
+ function resolveLengthSemantics(requested, streamType, headerSampleCount, actualScalarCount, channels) {
141
+ if (requested !== "auto") return requested;
142
+ if (streamType === 3 /* TX_CHRONO */ && actualScalarCount === 0) return "scalar";
143
+ if (actualScalarCount === headerSampleCount) return "scalar";
144
+ if (actualScalarCount === headerSampleCount * channels) return "per-channel";
145
+ return "scalar";
146
+ }
117
147
  function buildTxAudioFrame(options) {
118
148
  return buildStreamFrame({ ...options, streamType: 2 /* TX_AUDIO_STREAM */ });
119
149
  }
@@ -198,6 +228,15 @@ function payloadToFloat32(frameOrPayload, sampleType) {
198
228
  }
199
229
  return output;
200
230
  }
231
+ function decodeInterleavedIq(frame) {
232
+ if (frame.streamType !== 0 /* IQ_STREAM */) {
233
+ throw new TciError("invalid-frame", "Expected a TCI IQ stream frame");
234
+ }
235
+ if (frame.channels !== 2 || frame.sampleCount % 2 !== 0) {
236
+ throw new TciError("invalid-frame", `TCI IQ frame must contain interleaved I/Q pairs, got ${frame.channels} channels`);
237
+ }
238
+ return payloadToFloat32(frame);
239
+ }
201
240
  function samplesToPayload(samples, sampleType) {
202
241
  const type = normalizeSampleType(sampleType);
203
242
  const bytes = sampleTypeBytes(type);
@@ -299,6 +338,7 @@ export {
299
338
  TciStreamType,
300
339
  buildStreamFrame,
301
340
  buildTxAudioFrame,
341
+ decodeInterleavedIq,
302
342
  deinterleaveChannels,
303
343
  float32ToPcm16,
304
344
  mixToMono,