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 +21 -0
- package/README.md +133 -0
- package/dist/audio/index.cjs +343 -0
- package/dist/audio/index.cjs.map +1 -0
- package/dist/audio/index.d.cts +60 -0
- package/dist/audio/index.d.ts +60 -0
- package/dist/audio/index.js +301 -0
- package/dist/audio/index.js.map +1 -0
- package/dist/index-CK3XdXP3.d.cts +42 -0
- package/dist/index-Dfmrk2MR.d.ts +42 -0
- package/dist/index.cjs +1182 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +1117 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/index.cjs +309 -0
- package/dist/protocol/index.cjs.map +1 -0
- package/dist/protocol/index.d.cts +2 -0
- package/dist/protocol/index.d.ts +2 -0
- package/dist/protocol/index.js +274 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/testing/index.cjs +572 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +78 -0
- package/dist/testing/index.d.ts +78 -0
- package/dist/testing/index.js +533 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/text-BwCWY1k1.d.cts +21 -0
- package/dist/text-BwCWY1k1.d.ts +21 -0
- package/package.json +93 -0
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 };
|