tci-client-node 0.1.1 → 0.2.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 +9 -0
- package/README.md +33 -13
- package/dist/audio/index.cjs +63 -20
- package/dist/audio/index.cjs.map +1 -1
- package/dist/audio/index.d.cts +18 -3
- package/dist/audio/index.d.ts +18 -3
- package/dist/audio/index.js +63 -20
- package/dist/audio/index.js.map +1 -1
- package/dist/dialect/index.cjs +331 -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 +292 -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 +890 -224
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -12
- package/dist/index.d.ts +54 -12
- package/dist/index.js +876 -224
- 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 +77 -25
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +2 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +77 -25
- 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-9seY9Th-.d.ts +63 -0
- package/dist/types-xRotf9gW.d.cts +63 -0
- package/package.json +15 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Add an extensible TCI transport, dialect registry, strict handshake, and protocol identity model.
|
|
6
|
+
- Support ExpertSDR TCI 1.4 through 2.0, AetherSDR's 1.5 hybrid protocol, Thetis 2.0, and observed legacy command shapes.
|
|
7
|
+
- Treat constrained drive acknowledgements as successful clamped writes and add active drive readback.
|
|
8
|
+
- Correct scalar versus per-channel `Stream.length` handling for mono, stereo, legacy, and modern streams.
|
|
9
|
+
- Track negotiated audio parameters and expose dialect-aware handshake diagnostics.
|
package/README.md
CHANGED
|
@@ -6,9 +6,11 @@ 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.2.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
|
|
@@ -48,16 +50,11 @@ client.on('rxAudioFrame', (frame) => {
|
|
|
48
50
|
client.on('txChrono', (request) => {
|
|
49
51
|
// The host application decides what to transmit.
|
|
50
52
|
// Send silence if no TX audio is ready.
|
|
51
|
-
client.
|
|
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
|
-
});
|
|
53
|
+
client.sendTxAudioForChrono(request, new Float32Array(request.sampleCount));
|
|
58
54
|
});
|
|
59
55
|
|
|
60
|
-
await client.connect();
|
|
56
|
+
const handshake = await client.connect();
|
|
57
|
+
console.log(handshake.identity, handshake.dialect.dialect.id);
|
|
61
58
|
await client.setFrequency(14_074_000);
|
|
62
59
|
await client.setMode('digu');
|
|
63
60
|
await client.configureAudio({
|
|
@@ -70,20 +67,43 @@ await client.startAudio();
|
|
|
70
67
|
await client.setPtt(true, { source: 'tci' });
|
|
71
68
|
```
|
|
72
69
|
|
|
70
|
+
`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.
|
|
71
|
+
|
|
72
|
+
## Dialects
|
|
73
|
+
|
|
74
|
+
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`.
|
|
75
|
+
|
|
76
|
+
`generic-observed` derives legacy versus TRX-indexed `DRIVE` syntax from startup state. Applications can provide a custom dialect directly or use a custom `TciDialectRegistry`.
|
|
77
|
+
|
|
78
|
+
## Power Writes
|
|
79
|
+
|
|
80
|
+
Servers may apply a local band or PA safety limit and broadcast a different drive value. Use the detailed result when the difference matters:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const result = await client.setDriveWithResult(100);
|
|
84
|
+
// { requested: 100, applied: 50, outcome: 'clamped', acknowledgement: 'state' }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The compatibility `setDrive()` method remains available and treats a clamped value as a successful write rather than a timeout.
|
|
88
|
+
|
|
73
89
|
## Subpath Exports
|
|
74
90
|
|
|
75
91
|
- `tci-client-node`: `TciClient`, `createTciClient`, high-level radio/audio API, errors, and core types.
|
|
76
92
|
- `tci-client-node/protocol`: text command parser/formatter, escaping helpers, and command queue.
|
|
77
93
|
- `tci-client-node/audio`: stream frame parser/builder and sample conversion helpers.
|
|
94
|
+
- `tci-client-node/dialect`: dialect interfaces, built-ins, detection, and registry.
|
|
95
|
+
- `tci-client-node/transport`: transport interface and default WebSocket transport.
|
|
78
96
|
- `tci-client-node/testing`: `MockTciServer` and `FakeWebSocket` helpers for tests.
|
|
79
97
|
|
|
80
98
|
## Audio Frames
|
|
81
99
|
|
|
82
100
|
The official TCI `Stream` header is 16 little-endian `uint32` fields. In this package:
|
|
83
101
|
|
|
84
|
-
- `
|
|
85
|
-
- `
|
|
86
|
-
- `
|
|
102
|
+
- `headerSampleCount` is the raw `Stream.length` field.
|
|
103
|
+
- `sampleCount` is the canonical scalar value count across all channels.
|
|
104
|
+
- `frameCount` is `sampleCount / channels`.
|
|
105
|
+
- `payloadLength` is the actual byte length after the 64-byte header. `TX_CHRONO` frames are valid with no payload.
|
|
106
|
+
- 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.
|
|
87
107
|
|
|
88
108
|
Supported sample types are `int16`, `int24`, `int32`, and `float32`.
|
|
89
109
|
|
package/dist/audio/index.cjs
CHANGED
|
@@ -68,7 +68,7 @@ var TciSampleType = /* @__PURE__ */ ((TciSampleType2) => {
|
|
|
68
68
|
TciSampleType2[TciSampleType2["FLOAT32"] = 3] = "FLOAT32";
|
|
69
69
|
return TciSampleType2;
|
|
70
70
|
})(TciSampleType || {});
|
|
71
|
-
function parseStreamFrame(input) {
|
|
71
|
+
function parseStreamFrame(input, options = {}) {
|
|
72
72
|
const buffer = toBuffer(input);
|
|
73
73
|
if (buffer.byteLength < TCI_STREAM_HEADER_BYTES) {
|
|
74
74
|
throw new TciError("invalid-frame", `TCI stream frame is shorter than ${TCI_STREAM_HEADER_BYTES} bytes`);
|
|
@@ -76,28 +76,49 @@ function parseStreamFrame(input) {
|
|
|
76
76
|
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
77
77
|
const header = Array.from({ length: 16 }, (_, index) => view.getUint32(index * 4, true));
|
|
78
78
|
const sampleType = normalizeSampleType(header[2]);
|
|
79
|
-
|
|
79
|
+
const streamType = normalizeStreamType(header[6]);
|
|
80
|
+
let channels = header[7] || options.negotiatedChannels || 0;
|
|
80
81
|
const bytesPerSample = sampleTypeBytes(sampleType);
|
|
81
|
-
const
|
|
82
|
+
const headerSampleCount = header[5];
|
|
82
83
|
const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
|
|
83
84
|
if (channels <= 0) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
if (streamType === 3 /* TX_CHRONO */ && actualPayloadLength === 0) {
|
|
86
|
+
channels = 1;
|
|
87
|
+
} else {
|
|
88
|
+
const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;
|
|
89
|
+
if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
|
|
90
|
+
throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
|
|
91
|
+
}
|
|
92
|
+
channels = inferredChannels;
|
|
87
93
|
}
|
|
88
|
-
channels = inferredChannels;
|
|
89
94
|
}
|
|
90
|
-
const payloadLength =
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
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) {
|
|
95
|
+
const payloadLength = actualPayloadLength;
|
|
96
|
+
const alignedFrameBytes = bytesPerSample * channels;
|
|
97
|
+
if (payloadLength % alignedFrameBytes !== 0) {
|
|
99
98
|
throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
|
|
100
99
|
}
|
|
100
|
+
const actualScalarCount = payloadLength / bytesPerSample;
|
|
101
|
+
const requestedSemantics = options.lengthSemantics ?? "auto";
|
|
102
|
+
const lengthSemantics = resolveLengthSemantics(
|
|
103
|
+
requestedSemantics,
|
|
104
|
+
streamType,
|
|
105
|
+
headerSampleCount,
|
|
106
|
+
actualScalarCount,
|
|
107
|
+
channels
|
|
108
|
+
);
|
|
109
|
+
const sampleCount = lengthSemantics === "per-channel" ? headerSampleCount * channels : headerSampleCount;
|
|
110
|
+
if (streamType !== 3 /* TX_CHRONO */) {
|
|
111
|
+
const expectedPayloadLength = sampleCount * bytesPerSample;
|
|
112
|
+
if (payloadLength !== expectedPayloadLength) {
|
|
113
|
+
throw new TciError(
|
|
114
|
+
"invalid-frame",
|
|
115
|
+
`TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (sampleCount % channels !== 0) {
|
|
120
|
+
throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
|
|
121
|
+
}
|
|
101
122
|
return {
|
|
102
123
|
receiver: header[0],
|
|
103
124
|
sampleRate: header[1],
|
|
@@ -105,11 +126,14 @@ function parseStreamFrame(input) {
|
|
|
105
126
|
codec: header[3],
|
|
106
127
|
crc: header[4],
|
|
107
128
|
payloadLength,
|
|
108
|
-
streamType
|
|
129
|
+
streamType,
|
|
109
130
|
channels,
|
|
110
131
|
reserved: header.slice(8),
|
|
111
132
|
payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
|
|
112
|
-
|
|
133
|
+
headerSampleCount,
|
|
134
|
+
sampleCount,
|
|
135
|
+
frameCount: sampleCount / channels,
|
|
136
|
+
lengthSemantics
|
|
113
137
|
};
|
|
114
138
|
}
|
|
115
139
|
function buildStreamFrame(options) {
|
|
@@ -123,7 +147,19 @@ function buildStreamFrame(options) {
|
|
|
123
147
|
if (payload.byteLength % (bytesPerSample * channels) !== 0) {
|
|
124
148
|
throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
|
|
125
149
|
}
|
|
126
|
-
const
|
|
150
|
+
const derivedSampleCount = payload.byteLength / bytesPerSample;
|
|
151
|
+
const sampleCount = options.sampleCount ?? derivedSampleCount;
|
|
152
|
+
if (!Number.isInteger(sampleCount) || sampleCount < 0) {
|
|
153
|
+
throw new TciError("invalid-frame", `Invalid TCI sample count: ${sampleCount}`);
|
|
154
|
+
}
|
|
155
|
+
if (payload.byteLength > 0 && sampleCount !== derivedSampleCount) {
|
|
156
|
+
throw new TciError("invalid-frame", `Explicit scalar sample count ${sampleCount} does not match payload count ${derivedSampleCount}`);
|
|
157
|
+
}
|
|
158
|
+
if (sampleCount % channels !== 0) {
|
|
159
|
+
throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
|
|
160
|
+
}
|
|
161
|
+
const lengthSemantics = options.lengthSemantics === "per-channel" ? "per-channel" : "scalar";
|
|
162
|
+
const headerSampleCount = lengthSemantics === "per-channel" ? sampleCount / channels : sampleCount;
|
|
127
163
|
const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
|
|
128
164
|
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
129
165
|
const reserved = options.reserved ?? [];
|
|
@@ -133,7 +169,7 @@ function buildStreamFrame(options) {
|
|
|
133
169
|
sampleType,
|
|
134
170
|
options.codec ?? 0,
|
|
135
171
|
options.crc ?? 0,
|
|
136
|
-
|
|
172
|
+
headerSampleCount,
|
|
137
173
|
options.streamType,
|
|
138
174
|
channels,
|
|
139
175
|
...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0)
|
|
@@ -142,6 +178,13 @@ function buildStreamFrame(options) {
|
|
|
142
178
|
payload.copy(frame, TCI_STREAM_HEADER_BYTES);
|
|
143
179
|
return frame;
|
|
144
180
|
}
|
|
181
|
+
function resolveLengthSemantics(requested, streamType, headerSampleCount, actualScalarCount, channels) {
|
|
182
|
+
if (requested !== "auto") return requested;
|
|
183
|
+
if (streamType === 3 /* TX_CHRONO */ && actualScalarCount === 0) return "scalar";
|
|
184
|
+
if (actualScalarCount === headerSampleCount) return "scalar";
|
|
185
|
+
if (actualScalarCount === headerSampleCount * channels) return "per-channel";
|
|
186
|
+
return "scalar";
|
|
187
|
+
}
|
|
145
188
|
function buildTxAudioFrame(options) {
|
|
146
189
|
return buildStreamFrame({ ...options, streamType: 2 /* TX_AUDIO_STREAM */ });
|
|
147
190
|
}
|
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 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"]}
|
|
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 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-xRotf9gW.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;
|
|
@@ -36,6 +48,9 @@ interface BuildStreamFrameOptions {
|
|
|
36
48
|
channels: number;
|
|
37
49
|
payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;
|
|
38
50
|
samples?: Float32Array | readonly number[];
|
|
51
|
+
/** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */
|
|
52
|
+
sampleCount?: number;
|
|
53
|
+
lengthSemantics?: TciStreamLengthSemantics;
|
|
39
54
|
codec?: number;
|
|
40
55
|
crc?: number;
|
|
41
56
|
reserved?: readonly number[];
|
|
@@ -43,7 +58,7 @@ interface BuildStreamFrameOptions {
|
|
|
43
58
|
interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {
|
|
44
59
|
receiver?: number;
|
|
45
60
|
}
|
|
46
|
-
declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView): TciStreamFrame;
|
|
61
|
+
declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView, options?: ParseStreamFrameOptions): TciStreamFrame;
|
|
47
62
|
declare function buildStreamFrame(options: BuildStreamFrameOptions): Buffer;
|
|
48
63
|
declare function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer;
|
|
49
64
|
declare function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number;
|
|
@@ -57,4 +72,4 @@ declare function float32ToPcm16(samples: Float32Array | readonly number[]): Buff
|
|
|
57
72
|
declare function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[];
|
|
58
73
|
declare function mixToMono(samples: Float32Array, channels: number): Float32Array;
|
|
59
74
|
|
|
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 };
|
|
75
|
+
export { type BuildStreamFrameOptions, type BuildTxAudioFrameOptions, type ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciSampleType, type TciSampleTypeName, type TciStreamFrame, TciStreamType, buildStreamFrame, buildTxAudioFrame, 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-9seY9Th-.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;
|
|
@@ -36,6 +48,9 @@ interface BuildStreamFrameOptions {
|
|
|
36
48
|
channels: number;
|
|
37
49
|
payload?: Buffer | Uint8Array | ArrayBuffer | ArrayBufferView;
|
|
38
50
|
samples?: Float32Array | readonly number[];
|
|
51
|
+
/** Explicit Stream.length value, used for header-only frames such as TX_CHRONO. */
|
|
52
|
+
sampleCount?: number;
|
|
53
|
+
lengthSemantics?: TciStreamLengthSemantics;
|
|
39
54
|
codec?: number;
|
|
40
55
|
crc?: number;
|
|
41
56
|
reserved?: readonly number[];
|
|
@@ -43,7 +58,7 @@ interface BuildStreamFrameOptions {
|
|
|
43
58
|
interface BuildTxAudioFrameOptions extends Omit<BuildStreamFrameOptions, 'streamType'> {
|
|
44
59
|
receiver?: number;
|
|
45
60
|
}
|
|
46
|
-
declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView): TciStreamFrame;
|
|
61
|
+
declare function parseStreamFrame(input: Buffer | ArrayBuffer | ArrayBufferView, options?: ParseStreamFrameOptions): TciStreamFrame;
|
|
47
62
|
declare function buildStreamFrame(options: BuildStreamFrameOptions): Buffer;
|
|
48
63
|
declare function buildTxAudioFrame(options: BuildTxAudioFrameOptions): Buffer;
|
|
49
64
|
declare function sampleTypeBytes(sampleType: TciSampleType | TciSampleTypeName): number;
|
|
@@ -57,4 +72,4 @@ declare function float32ToPcm16(samples: Float32Array | readonly number[]): Buff
|
|
|
57
72
|
declare function deinterleaveChannels(samples: Float32Array, channels: number): Float32Array[];
|
|
58
73
|
declare function mixToMono(samples: Float32Array, channels: number): Float32Array;
|
|
59
74
|
|
|
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 };
|
|
75
|
+
export { type BuildStreamFrameOptions, type BuildTxAudioFrameOptions, type ParseStreamFrameOptions, TCI_STREAM_HEADER_BYTES, TciSampleType, type TciSampleTypeName, type TciStreamFrame, TciStreamType, buildStreamFrame, buildTxAudioFrame, 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`);
|
|
@@ -35,28 +35,49 @@ function parseStreamFrame(input) {
|
|
|
35
35
|
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
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] || options.negotiatedChannels || 0;
|
|
39
40
|
const bytesPerSample = sampleTypeBytes(sampleType);
|
|
40
|
-
const
|
|
41
|
+
const headerSampleCount = header[5];
|
|
41
42
|
const actualPayloadLength = buffer.byteLength - TCI_STREAM_HEADER_BYTES;
|
|
42
43
|
if (channels <= 0) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
if (streamType === 3 /* TX_CHRONO */ && actualPayloadLength === 0) {
|
|
45
|
+
channels = 1;
|
|
46
|
+
} else {
|
|
47
|
+
const inferredChannels = headerSampleCount > 0 ? actualPayloadLength / headerSampleCount / bytesPerSample : 1;
|
|
48
|
+
if (!Number.isInteger(inferredChannels) || inferredChannels <= 0) {
|
|
49
|
+
throw new TciError("invalid-frame", `Invalid TCI channel count: ${channels}`);
|
|
50
|
+
}
|
|
51
|
+
channels = inferredChannels;
|
|
46
52
|
}
|
|
47
|
-
channels = inferredChannels;
|
|
48
53
|
}
|
|
49
|
-
const payloadLength =
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
throw new TciError(
|
|
53
|
-
"invalid-frame",
|
|
54
|
-
`TCI stream frame length mismatch: header says ${sampleCount} samples (${payloadLength} payload bytes), got ${buffer.byteLength - TCI_STREAM_HEADER_BYTES}`
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
if (payloadLength % (bytesPerSample * channels) !== 0) {
|
|
54
|
+
const payloadLength = actualPayloadLength;
|
|
55
|
+
const alignedFrameBytes = bytesPerSample * channels;
|
|
56
|
+
if (payloadLength % alignedFrameBytes !== 0) {
|
|
58
57
|
throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
|
|
59
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;
|
|
69
|
+
if (streamType !== 3 /* TX_CHRONO */) {
|
|
70
|
+
const expectedPayloadLength = sampleCount * bytesPerSample;
|
|
71
|
+
if (payloadLength !== expectedPayloadLength) {
|
|
72
|
+
throw new TciError(
|
|
73
|
+
"invalid-frame",
|
|
74
|
+
`TCI stream frame length mismatch: header says ${headerSampleCount} samples using ${lengthSemantics} semantics (${expectedPayloadLength} payload bytes), got ${payloadLength}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (sampleCount % channels !== 0) {
|
|
79
|
+
throw new TciError("invalid-frame", `TCI scalar sample count ${sampleCount} is not divisible by ${channels} channels`);
|
|
80
|
+
}
|
|
60
81
|
return {
|
|
61
82
|
receiver: header[0],
|
|
62
83
|
sampleRate: header[1],
|
|
@@ -64,11 +85,14 @@ function parseStreamFrame(input) {
|
|
|
64
85
|
codec: header[3],
|
|
65
86
|
crc: header[4],
|
|
66
87
|
payloadLength,
|
|
67
|
-
streamType
|
|
88
|
+
streamType,
|
|
68
89
|
channels,
|
|
69
90
|
reserved: header.slice(8),
|
|
70
91
|
payload: buffer.subarray(TCI_STREAM_HEADER_BYTES),
|
|
71
|
-
|
|
92
|
+
headerSampleCount,
|
|
93
|
+
sampleCount,
|
|
94
|
+
frameCount: sampleCount / channels,
|
|
95
|
+
lengthSemantics
|
|
72
96
|
};
|
|
73
97
|
}
|
|
74
98
|
function buildStreamFrame(options) {
|
|
@@ -82,7 +106,19 @@ function buildStreamFrame(options) {
|
|
|
82
106
|
if (payload.byteLength % (bytesPerSample * channels) !== 0) {
|
|
83
107
|
throw new TciError("invalid-frame", "TCI payload length is not aligned to sample type and channel count");
|
|
84
108
|
}
|
|
85
|
-
const
|
|
109
|
+
const derivedSampleCount = payload.byteLength / bytesPerSample;
|
|
110
|
+
const sampleCount = options.sampleCount ?? derivedSampleCount;
|
|
111
|
+
if (!Number.isInteger(sampleCount) || sampleCount < 0) {
|
|
112
|
+
throw new TciError("invalid-frame", `Invalid TCI sample count: ${sampleCount}`);
|
|
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;
|
|
86
122
|
const frame = Buffer.alloc(TCI_STREAM_HEADER_BYTES + payload.byteLength);
|
|
87
123
|
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
88
124
|
const reserved = options.reserved ?? [];
|
|
@@ -92,7 +128,7 @@ function buildStreamFrame(options) {
|
|
|
92
128
|
sampleType,
|
|
93
129
|
options.codec ?? 0,
|
|
94
130
|
options.crc ?? 0,
|
|
95
|
-
|
|
131
|
+
headerSampleCount,
|
|
96
132
|
options.streamType,
|
|
97
133
|
channels,
|
|
98
134
|
...Array.from({ length: 8 }, (_, index) => reserved[index] ?? 0)
|
|
@@ -101,6 +137,13 @@ function buildStreamFrame(options) {
|
|
|
101
137
|
payload.copy(frame, TCI_STREAM_HEADER_BYTES);
|
|
102
138
|
return frame;
|
|
103
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
|
+
}
|
|
104
147
|
function buildTxAudioFrame(options) {
|
|
105
148
|
return buildStreamFrame({ ...options, streamType: 2 /* TX_AUDIO_STREAM */ });
|
|
106
149
|
}
|