tci-client-node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 boybook
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # tci-client-node
2
+
3
+ A pure TypeScript client for the Expert Electronics TCI (Transceiver Control Interface) protocol used by SunSDR and ExpertSDR.
4
+
5
+ TCI is a WebSocket protocol: text commands are used for CAT-style radio control, and binary WebSocket frames carry audio/IQ stream blocks. This package therefore does not require a native Node.js addon.
6
+
7
+ ## Status
8
+
9
+ `0.1.x` focuses on the subset needed by application integrations:
10
+
11
+ - WebSocket lifecycle and READY/startup state handling
12
+ - Frequency, mode, PTT, tune, drive, split, and CW text/macros
13
+ - RX and TX sensor state parsing
14
+ - RX audio, TX audio, TX_CHRONO, and line-out stream frame parsing/building
15
+ - Serial command queue with timeout, cancellation, and interleaved broadcast handling
16
+ - Mock TCI server and fake WebSocket transport for integration tests
17
+
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.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install tci-client-node
24
+ ```
25
+
26
+ ## Basic Usage
27
+
28
+ ```ts
29
+ import { TciClient } from 'tci-client-node';
30
+
31
+ const client = new TciClient({
32
+ url: 'ws://127.0.0.1:40001',
33
+ receiver: 0,
34
+ trx: 0,
35
+ vfo: 0,
36
+ connectTimeoutMs: 5000,
37
+ commandTimeoutMs: 1000,
38
+ });
39
+
40
+ client.on('state', (state) => {
41
+ console.log(state.connected, state.ready, state.frequencies);
42
+ });
43
+
44
+ client.on('rxAudioFrame', (frame) => {
45
+ console.log(frame.sampleRate, frame.channels, frame.sampleCount);
46
+ });
47
+
48
+ client.on('txChrono', (request) => {
49
+ // The host application decides what to transmit.
50
+ // Send silence if no TX audio is ready.
51
+ client.sendTxAudio({
52
+ receiver: request.receiver,
53
+ sampleRate: request.sampleRate,
54
+ sampleType: request.sampleType,
55
+ channels: request.channels,
56
+ samples: new Float32Array(request.sampleCount * request.channels),
57
+ });
58
+ });
59
+
60
+ await client.connect();
61
+ await client.setFrequency(14_074_000);
62
+ await client.setMode('digu');
63
+ await client.configureAudio({
64
+ sampleRate: 12_000,
65
+ sampleType: 'float32',
66
+ channels: 1,
67
+ samplesPerFrame: 512,
68
+ });
69
+ await client.startAudio();
70
+ await client.setPtt(true, { source: 'tci' });
71
+ ```
72
+
73
+ ## Subpath Exports
74
+
75
+ - `tci-client-node`: `TciClient`, `createTciClient`, high-level radio/audio API, errors, and core types.
76
+ - `tci-client-node/protocol`: text command parser/formatter, escaping helpers, and command queue.
77
+ - `tci-client-node/audio`: stream frame parser/builder and sample conversion helpers.
78
+ - `tci-client-node/testing`: `MockTciServer` and `FakeWebSocket` helpers for tests.
79
+
80
+ ## Audio Frames
81
+
82
+ The official TCI `Stream` header is 16 little-endian `uint32` fields. In this package:
83
+
84
+ - `sampleCount` maps to the official `Stream.length` field, meaning samples per channel.
85
+ - `payloadLength` is the derived byte length after applying sample type and channel count.
86
+ - `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.
87
+
88
+ Supported sample types are `int16`, `int24`, `int32`, and `float32`.
89
+
90
+ ## Testing Utilities
91
+
92
+ ```ts
93
+ import { MockTciServer } from 'tci-client-node/testing';
94
+
95
+ const server = new MockTciServer();
96
+ await server.start();
97
+
98
+ const client = new TciClient({ url: server.url() });
99
+ await client.connect();
100
+
101
+ server.sendRxAudioFrame({ samples: new Float32Array([0, 0.5, -0.5]) });
102
+ ```
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ npm install
108
+ npm test
109
+ npm run typecheck
110
+ npm run build
111
+ ```
112
+
113
+ The package is built with `tsup` and publishes ESM, CommonJS, and declaration files.
114
+
115
+ ## Releases
116
+
117
+ Releases are published by GitHub Actions when a `v*` tag is pushed. The tag must
118
+ match `package.json` exactly, for example `v0.1.0` for version `0.1.0`.
119
+
120
+ The workflow mirrors the `icom-wlan-node` release shape: install with `npm ci`,
121
+ typecheck, build, test, verify the package contents, and publish to npm using
122
+ the `NPM_TOKEN` repository secret. Provenance is enabled through npm's
123
+ `publishConfig`.
124
+
125
+ ## References
126
+
127
+ - [ExpertSDR3 TCI protocol](https://github.com/ExpertSDR3/TCI)
128
+ - [ftl/tci](https://github.com/ftl/tci)
129
+ - [ftl/tciadapter](https://github.com/ftl/tciadapter)
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1,343 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/audio/index.ts
21
+ var audio_exports = {};
22
+ __export(audio_exports, {
23
+ TCI_STREAM_HEADER_BYTES: () => TCI_STREAM_HEADER_BYTES,
24
+ TciSampleType: () => TciSampleType,
25
+ TciStreamType: () => TciStreamType,
26
+ buildStreamFrame: () => buildStreamFrame,
27
+ buildTxAudioFrame: () => buildTxAudioFrame,
28
+ deinterleaveChannels: () => deinterleaveChannels,
29
+ float32ToPcm16: () => float32ToPcm16,
30
+ mixToMono: () => mixToMono,
31
+ normalizeSampleType: () => normalizeSampleType,
32
+ normalizeStreamType: () => normalizeStreamType,
33
+ parseStreamFrame: () => parseStreamFrame,
34
+ payloadToFloat32: () => payloadToFloat32,
35
+ pcm16ToFloat32: () => pcm16ToFloat32,
36
+ sampleTypeBytes: () => sampleTypeBytes,
37
+ sampleTypeName: () => sampleTypeName,
38
+ samplesToPayload: () => samplesToPayload
39
+ });
40
+ module.exports = __toCommonJS(audio_exports);
41
+
42
+ // src/errors.ts
43
+ var TciError = class extends Error {
44
+ code;
45
+ details;
46
+ constructor(code, message, details) {
47
+ super(message);
48
+ this.name = "TciError";
49
+ this.code = code;
50
+ this.details = details;
51
+ }
52
+ };
53
+
54
+ // src/audio/streamFrame.ts
55
+ var TCI_STREAM_HEADER_BYTES = 16 * 4;
56
+ var TciStreamType = /* @__PURE__ */ ((TciStreamType2) => {
57
+ TciStreamType2[TciStreamType2["IQ_STREAM"] = 0] = "IQ_STREAM";
58
+ TciStreamType2[TciStreamType2["RX_AUDIO_STREAM"] = 1] = "RX_AUDIO_STREAM";
59
+ TciStreamType2[TciStreamType2["TX_AUDIO_STREAM"] = 2] = "TX_AUDIO_STREAM";
60
+ TciStreamType2[TciStreamType2["TX_CHRONO"] = 3] = "TX_CHRONO";
61
+ TciStreamType2[TciStreamType2["LINEOUT_STREAM"] = 4] = "LINEOUT_STREAM";
62
+ return TciStreamType2;
63
+ })(TciStreamType || {});
64
+ var TciSampleType = /* @__PURE__ */ ((TciSampleType2) => {
65
+ TciSampleType2[TciSampleType2["INT16"] = 0] = "INT16";
66
+ TciSampleType2[TciSampleType2["INT24"] = 1] = "INT24";
67
+ TciSampleType2[TciSampleType2["INT32"] = 2] = "INT32";
68
+ TciSampleType2[TciSampleType2["FLOAT32"] = 3] = "FLOAT32";
69
+ return TciSampleType2;
70
+ })(TciSampleType || {});
71
+ function parseStreamFrame(input) {
72
+ const buffer = toBuffer(input);
73
+ if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {
74
+ throw new TciError("invalid-frame", `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);
75
+ }
76
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
77
+ const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));
78
+ const sampleType = normalizeSampleType(header[2]);
79
+ let channels = header[7];
80
+ const bytesPerSample = sampleTypeBytes(sampleType);
81
+ const sampleCount = header[5];
82
+ const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
83
+ if (channels <= 0) {
84
+ const inferredChannels = sampleCount > 0 ? actualPayloadLength / sampleCount / bytesPerSample : 1;
85
+ if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
86
+ throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
87
+ }
88
+ channels = inferredChannels;
89
+ }
90
+ const payloadLength = sampleCount * bytesPerSample * channels;
91
+ const expectedLength = TCI_STREAM_HEADER_BYTES + payloadLength;
92
+ if (buffer.byteLength !== expectedLength) {
93
+ throw new TciError(
94
+ "invalid-frame",
95
+ `TCI stream frame length mismatch: header says ${sampleCount} samples (${payloadLength} payload bytes), got ${buffer.byteLength - TCI_STREAM_HEADER_BYTES}`
96
+ );
97
+ }
98
+ if (payloadLength % (bytesPerSample * channels) !== 0) {
99
+ throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
100
+ }
101
+ return {
102
+ receiver: header[0],
103
+ sampleRate: header[1],
104
+ sampleType,
105
+ codec: header[3],
106
+ crc: header[4],
107
+ payloadLength,
108
+ streamType: normalizeStreamType(header[6]),
109
+ channels,
110
+ reserved: header.slice(8),
111
+ payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
112
+ sampleCount
113
+ };
114
+ }
115
+ function buildStreamFrame(options) {
116
+ const sampleType = normalizeSampleType(options.sampleType);
117
+ const payload = options.payload ? toBuffer(options.payload) : samplesToPayload(options.samples ?? [], sampleType);
118
+ const channels = options.channels;
119
+ if (channels <= 0) {
120
+ throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
121
+ }
122
+ const bytesPerSample = sampleTypeBytes(sampleType);
123
+ if (payload.byteLength % (bytesPerSample * channels) !== 0) {
124
+ throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
125
+ }
126
+ const sampleCount = payload.byteLength / bytesPerSample / channels;
127
+ const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
128
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
129
+ const reserved = options.reserved ?? [];
130
+ const header = [
131
+ options.receiver ?? 0,
132
+ options.sampleRate,
133
+ sampleType,
134
+ options.codec ?? 0,
135
+ options.crc ?? 0,
136
+ sampleCount,
137
+ options.streamType,
138
+ channels,
139
+ ...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0)
140
+ ];
141
+ header.forEach((value, index) => view.setUint32(index * 4, value >>> 0, true));
142
+ payload.copy(frame, TCI_STREAM_HEADER_BYTES);
143
+ return frame;
144
+ }
145
+ function buildTxAudioFrame(options) {
146
+ return buildStreamFrame({ ...options, streamType: 2 /* TX_AUDIO_STREAM */ });
147
+ }
148
+ function sampleTypeBytes(sampleType) {
149
+ switch (normalizeSampleType(sampleType)) {
150
+ case 0 /* INT16 */:
151
+ return 2;
152
+ case 1 /* INT24 */:
153
+ return 3;
154
+ case 2 /* INT32 */:
155
+ case 3 /* FLOAT32 */:
156
+ return 4;
157
+ default:
158
+ throw new TciError("invalid-frame", `Unsupported TCI sample type: ${sampleType}`);
159
+ }
160
+ }
161
+ function sampleTypeName(sampleType) {
162
+ switch (sampleType) {
163
+ case 0 /* INT16 */:
164
+ return "int16";
165
+ case 1 /* INT24 */:
166
+ return "int24";
167
+ case 2 /* INT32 */:
168
+ return "int32";
169
+ case 3 /* FLOAT32 */:
170
+ return "float32";
171
+ default:
172
+ throw new TciError("invalid-frame", `Unsupported TCI sample type: ${sampleType}`);
173
+ }
174
+ }
175
+ function normalizeSampleType(sampleType) {
176
+ if (typeof sampleType === "string") {
177
+ switch (sampleType.toLowerCase()) {
178
+ case "int16":
179
+ return 0 /* INT16 */;
180
+ case "int24":
181
+ return 1 /* INT24 */;
182
+ case "int32":
183
+ return 2 /* INT32 */;
184
+ case "float32":
185
+ return 3 /* FLOAT32 */;
186
+ default:
187
+ throw new TciError("invalid-frame", `Unsupported TCI sample type: ${sampleType}`);
188
+ }
189
+ }
190
+ if (sampleType >= 0 /* INT16 */ && sampleType <= 3 /* FLOAT32 */) {
191
+ return sampleType;
192
+ }
193
+ throw new TciError("invalid-frame", `Unsupported TCI sample type: ${sampleType}`);
194
+ }
195
+ function normalizeStreamType(streamType) {
196
+ if (streamType >= 0 /* IQ_STREAM */ && streamType <= 4 /* LINEOUT_STREAM */) {
197
+ return streamType;
198
+ }
199
+ throw new TciError("invalid-frame", `Unsupported TCI stream type: ${streamType}`);
200
+ }
201
+ function payloadToFloat32(frameOrPayload, sampleType) {
202
+ const payload = isFrame(frameOrPayload) ? frameOrPayload.payload : toBuffer(frameOrPayload);
203
+ const type = isFrame(frameOrPayload) ? frameOrPayload.sampleType : normalizeSampleType(sampleType ?? 3 /* FLOAT32 */);
204
+ const bytes = sampleTypeBytes(type);
205
+ if (payload.byteLength % bytes !== 0) {
206
+ throw new TciError("invalid-frame", "Payload length is not aligned to sample type");
207
+ }
208
+ const output = new Float32Array(payload.byteLength / bytes);
209
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
210
+ for (let i = 0; i < output.length; i += 1) {
211
+ const offset = i * bytes;
212
+ switch (type) {
213
+ case 0 /* INT16 */:
214
+ output[i] = view.getInt16(offset, true) / 32768;
215
+ break;
216
+ case 1 /* INT24 */:
217
+ output[i] = readInt24(view, offset) / 8388608;
218
+ break;
219
+ case 2 /* INT32 */:
220
+ output[i] = view.getInt32(offset, true) / 2147483648;
221
+ break;
222
+ case 3 /* FLOAT32 */:
223
+ output[i] = view.getFloat32(offset, true);
224
+ break;
225
+ }
226
+ }
227
+ return output;
228
+ }
229
+ function samplesToPayload(samples, sampleType) {
230
+ const type = normalizeSampleType(sampleType);
231
+ const bytes = sampleTypeBytes(type);
232
+ const payload = Buffer.alloc(samples.length * bytes);
233
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
234
+ for (let i = 0; i < samples.length; i += 1) {
235
+ const value = clampSample(samples[i] ?? 0);
236
+ const offset = i * bytes;
237
+ switch (type) {
238
+ case 0 /* INT16 */:
239
+ view.setInt16(offset, Math.round(value * 32767), true);
240
+ break;
241
+ case 1 /* INT24 */:
242
+ writeInt24(view, offset, Math.round(value * 8388607));
243
+ break;
244
+ case 2 /* INT32 */:
245
+ view.setInt32(offset, Math.round(value * 2147483647), true);
246
+ break;
247
+ case 3 /* FLOAT32 */:
248
+ view.setFloat32(offset, value, true);
249
+ break;
250
+ }
251
+ }
252
+ return payload;
253
+ }
254
+ function pcm16ToFloat32(input) {
255
+ if (input instanceof Int16Array) {
256
+ const output = new Float32Array(input.length);
257
+ for (let i = 0; i < input.length; i += 1) {
258
+ output[i] = input[i] / 32768;
259
+ }
260
+ return output;
261
+ }
262
+ return payloadToFloat32(toBuffer(input), 0 /* INT16 */);
263
+ }
264
+ function float32ToPcm16(samples) {
265
+ return samplesToPayload(samples, 0 /* INT16 */);
266
+ }
267
+ function deinterleaveChannels(samples, channels) {
268
+ if (channels <= 0 || samples.length % channels !== 0) {
269
+ throw new TciError("invalid-frame", "Cannot deinterleave samples with invalid channel count");
270
+ }
271
+ const frames = samples.length / channels;
272
+ const outputs = Array.from({ length: channels }, () => new Float32Array(frames));
273
+ for (let frame = 0; frame < frames; frame += 1) {
274
+ for (let channel = 0; channel < channels; channel += 1) {
275
+ outputs[channel][frame] = samples[frame * channels + channel];
276
+ }
277
+ }
278
+ return outputs;
279
+ }
280
+ function mixToMono(samples, channels) {
281
+ if (channels === 1) {
282
+ return samples;
283
+ }
284
+ const separated = deinterleaveChannels(samples, channels);
285
+ const mono = new Float32Array(separated[0]?.length ?? 0);
286
+ for (const channel of separated) {
287
+ for (let i = 0; i < mono.length; i += 1) {
288
+ mono[i] += channel[i] / channels;
289
+ }
290
+ }
291
+ return mono;
292
+ }
293
+ function toBuffer(input) {
294
+ if (Buffer.isBuffer(input)) {
295
+ return input;
296
+ }
297
+ if (input instanceof ArrayBuffer) {
298
+ return Buffer.from(input);
299
+ }
300
+ if (ArrayBuffer.isView(input)) {
301
+ return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
302
+ }
303
+ return Buffer.from(input);
304
+ }
305
+ function isFrame(value) {
306
+ return Boolean(value && typeof value === "object" && "payload" in value && "sampleType" in value);
307
+ }
308
+ function clampSample(value) {
309
+ if (!Number.isFinite(value)) {
310
+ return 0;
311
+ }
312
+ return Math.max(-1, Math.min(1, value));
313
+ }
314
+ function readInt24(view, offset) {
315
+ const value = view.getUint8(offset) | view.getUint8(offset + 1) << 8 | view.getUint8(offset + 2) << 16;
316
+ return value & 8388608 ? value | 4278190080 : value;
317
+ }
318
+ function writeInt24(view, offset, value) {
319
+ const clamped = Math.max(-8388608, Math.min(8388607, value));
320
+ view.setUint8(offset, clamped & 255);
321
+ view.setUint8(offset + 1, clamped >> 8 & 255);
322
+ view.setUint8(offset + 2, clamped >> 16 & 255);
323
+ }
324
+ // Annotate the CommonJS export names for ESM import in node:
325
+ 0 && (module.exports = {
326
+ TCI_STREAM_HEADER_BYTES,
327
+ TciSampleType,
328
+ TciStreamType,
329
+ buildStreamFrame,
330
+ buildTxAudioFrame,
331
+ deinterleaveChannels,
332
+ float32ToPcm16,
333
+ mixToMono,
334
+ normalizeSampleType,
335
+ normalizeStreamType,
336
+ parseStreamFrame,
337
+ payloadToFloat32,
338
+ pcm16ToFloat32,
339
+ sampleTypeBytes,
340
+ sampleTypeName,
341
+ samplesToPayload
342
+ });
343
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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 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 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 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 const payloadLength = sampleCount * bytesPerSample * channels;\n const expectedLength = TCI_STREAM_HEADER_BYTES + payloadLength;\n if (buffer.byteLength !== expectedLength) {\n throw new TciError(\n 'invalid-frame',\n `TCI stream frame length mismatch: header says ${sampleCount} samples (${payloadLength} payload bytes), got ${buffer.byteLength - TCI_STREAM_HEADER_BYTES}`,\n );\n }\n if (payloadLength % (bytesPerSample * channels) !== 0) {\n throw new TciError('invalid-frame', 'TCI payload length is not aligned to sample type and channel count');\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: normalizeStreamType(header[6]),\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 sampleCount = payload.byteLength / bytesPerSample / channels;\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"]}
@@ -0,0 +1,60 @@
1
+ declare const TCI_STREAM_HEADER_BYTES: number;
2
+ declare enum TciStreamType {
3
+ IQ_STREAM = 0,
4
+ RX_AUDIO_STREAM = 1,
5
+ TX_AUDIO_STREAM = 2,
6
+ TX_CHRONO = 3,
7
+ LINEOUT_STREAM = 4
8
+ }
9
+ declare enum TciSampleType {
10
+ INT16 = 0,
11
+ INT24 = 1,
12
+ INT32 = 2,
13
+ FLOAT32 = 3
14
+ }
15
+ type TciSampleTypeName = 'int16' | 'int24' | 'int32' | 'float32';
16
+ interface TciStreamFrame {
17
+ receiver: number;
18
+ sampleRate: number;
19
+ sampleType: TciSampleType;
20
+ codec: number;
21
+ crc: number;
22
+ /** Byte length of the payload following the 64-byte TCI stream header. */
23
+ payloadLength: number;
24
+ streamType: TciStreamType;
25
+ channels: number;
26
+ reserved: number[];
27
+ payload: Buffer;
28
+ /** Official Stream.length value: number of samples per channel in the payload. */
29
+ sampleCount: number;
30
+ }
31
+ interface BuildStreamFrameOptions {
32
+ receiver?: number;
33
+ sampleRate: number;
34
+ sampleType: TciSampleType | TciSampleTypeName;
35
+ streamType: TciStreamType;
36
+ channels: number;
37
+ payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;
38
+ samples?: Float32Array | readonly number[];
39
+ codec?: number;
40
+ crc?: number;
41
+ reserved?: readonly number[];
42
+ }
43
+ interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {
44
+ receiver?: number;
45
+ }
46
+ declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView): TciStreamFrame;
47
+ declare function buildStreamFrame(options: BuildStreamFrameOptions): Buffer;
48
+ declare function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer;
49
+ declare function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number;
50
+ declare function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName;
51
+ declare function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType;
52
+ declare function normalizeStreamType(streamType: TciStreamType | number): TciStreamType;
53
+ declare function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array;
54
+ declare function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer;
55
+ declare function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array;
56
+ declare function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer;
57
+ declare function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[];
58
+ declare function mixToMono(samples: Float32Array, channels: number): Float32Array;
59
+
60
+ 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 };
@@ -0,0 +1,60 @@
1
+ declare const TCI_STREAM_HEADER_BYTES: number;
2
+ declare enum TciStreamType {
3
+ IQ_STREAM = 0,
4
+ RX_AUDIO_STREAM = 1,
5
+ TX_AUDIO_STREAM = 2,
6
+ TX_CHRONO = 3,
7
+ LINEOUT_STREAM = 4
8
+ }
9
+ declare enum TciSampleType {
10
+ INT16 = 0,
11
+ INT24 = 1,
12
+ INT32 = 2,
13
+ FLOAT32 = 3
14
+ }
15
+ type TciSampleTypeName = 'int16' | 'int24' | 'int32' | 'float32';
16
+ interface TciStreamFrame {
17
+ receiver: number;
18
+ sampleRate: number;
19
+ sampleType: TciSampleType;
20
+ codec: number;
21
+ crc: number;
22
+ /** Byte length of the payload following the 64-byte TCI stream header. */
23
+ payloadLength: number;
24
+ streamType: TciStreamType;
25
+ channels: number;
26
+ reserved: number[];
27
+ payload: Buffer;
28
+ /** Official Stream.length value: number of samples per channel in the payload. */
29
+ sampleCount: number;
30
+ }
31
+ interface BuildStreamFrameOptions {
32
+ receiver?: number;
33
+ sampleRate: number;
34
+ sampleType: TciSampleType | TciSampleTypeName;
35
+ streamType: TciStreamType;
36
+ channels: number;
37
+ payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;
38
+ samples?: Float32Array | readonly number[];
39
+ codec?: number;
40
+ crc?: number;
41
+ reserved?: readonly number[];
42
+ }
43
+ interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {
44
+ receiver?: number;
45
+ }
46
+ declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView): TciStreamFrame;
47
+ declare function buildStreamFrame(options: BuildStreamFrameOptions): Buffer;
48
+ declare function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer;
49
+ declare function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number;
50
+ declare function sampleTypeName(sampleType: TciSampleType): TciSampleTypeName;
51
+ declare function normalizeSampleType(sampleType: TciSampleType | TciSampleTypeName | number): TciSampleType;
52
+ declare function normalizeStreamType(streamType: TciStreamType | number): TciStreamType;
53
+ declare function payloadToFloat32(frameOrPayload: TciStreamFrame | Buffer | Uint8Array, sampleType?: TciSampleType | TciSampleTypeName): Float32Array;
54
+ declare function samplesToPayload(samples: Float32Array | readonly number[], sampleType: TciSampleType | TciSampleTypeName): Buffer;
55
+ declare function pcm16ToFloat32(input: Buffer | Uint8Array | Int16Array): Float32Array;
56
+ declare function float32ToPcm16(samples: Float32Array | readonly number[]): Buffer;
57
+ declare function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[];
58
+ declare function mixToMono(samples: Float32Array, channels: number): Float32Array;
59
+
60
+ 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 };