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.
- package/CHANGELOG.md +17 -0
- package/README.md +57 -11
- package/dist/audio/index.cjs +52 -11
- package/dist/audio/index.cjs.map +1 -1
- package/dist/audio/index.d.cts +17 -3
- package/dist/audio/index.d.ts +17 -3
- package/dist/audio/index.js +51 -11
- package/dist/audio/index.js.map +1 -1
- package/dist/dialect/index.cjs +349 -0
- package/dist/dialect/index.cjs.map +1 -0
- package/dist/dialect/index.d.cts +29 -0
- package/dist/dialect/index.d.ts +29 -0
- package/dist/dialect/index.js +310 -0
- package/dist/dialect/index.js.map +1 -0
- package/dist/{index-CK3XdXP3.d.cts → index-lbK6NGY4.d.cts} +1 -1
- package/dist/{index-Dfmrk2MR.d.ts → index-paj1AOJY.d.ts} +1 -1
- package/dist/index.cjs +1057 -217
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -12
- package/dist/index.d.ts +106 -12
- package/dist/index.js +1041 -217
- package/dist/index.js.map +1 -1
- package/dist/protocol/index.cjs.map +1 -1
- package/dist/protocol/index.d.cts +1 -1
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/index.js.map +1 -1
- package/dist/testing/index.cjs +79 -14
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +6 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +79 -14
- package/dist/testing/index.js.map +1 -1
- package/dist/transport/index.cjs +175 -0
- package/dist/transport/index.cjs.map +1 -0
- package/dist/transport/index.d.cts +37 -0
- package/dist/transport/index.d.ts +37 -0
- package/dist/transport/index.js +138 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/types-1YXGwrgI.d.cts +65 -0
- package/dist/types-BtPh4Dbd.d.ts +65 -0
- 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.
|
|
9
|
+
`0.3.x` provides a dialect-aware client for application integrations:
|
|
10
10
|
|
|
11
|
-
- WebSocket
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
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
|
-
-
|
|
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
|
|
115
|
-
|
|
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
|
|
package/dist/audio/index.cjs
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
102
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/audio/index.cjs.map
CHANGED
|
@@ -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"]}
|
package/dist/audio/index.d.cts
CHANGED
|
@@ -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
|
-
/**
|
|
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 };
|
package/dist/audio/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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 };
|
package/dist/audio/index.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
61
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|