Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4c536b0
fix: 未定義dataTypeのDataパケット受信時のクラッシュを修正
9c5s Apr 12, 2026
579cc3f
fix: MixerパケットのMixer IDがLayer IDとして誤変換される問題を修正
9c5s Apr 12, 2026
73dddf2
feat: MixerData型に仕様上の全フィールドを追加
9c5s Apr 12, 2026
8f7cbca
refactor!: ErrorパケットのerrorDataを構造化フィールドに置換
9c5s Apr 12, 2026
b5f3ef9
feat: マルチパケットヘッダーフィールドを公開
9c5s Apr 12, 2026
fd23a26
feat: TimeパケットのTimecodeセクションを読み取る
9c5s Apr 12, 2026
4cf8721
feat: StatusパケットのAPP SPECIFICセクションを読み取る
9c5s Apr 12, 2026
41f9b3d
fix: parsePacketの未定義messageTypeクラッシュ修正とレビュー指摘対応
9c5s Apr 12, 2026
98bea38
refactor: TCNetTimePacketTimecodeをTCNetTimecodeに統一しデッドガードにコメント追加
9c5s Apr 12, 2026
a399eb0
docs: Mixer データ制限を実機検証結果に基づき更新
9c5s Apr 12, 2026
94b2d92
refactor!: crossfaderAssignをcrossFaderAssignにリネームしWiki用語を統一
9c5s Apr 12, 2026
aaa20ad
test: receiveUnicastのエラーパステストとバッファ長ガードを追加
9c5s Apr 12, 2026
d9d6be4
refactor: Promise raceテストの共通ヘルパー抽出と可読性向上
9c5s Apr 12, 2026
af849b9
fix: レビュー指摘対応 - multiPacketHeaderのアセンブリパス対応と冗長読み取り排除
9c5s Apr 12, 2026
3d83595
fix: file-chunkパスでmultiPacketHeaderが未設定になる不具合を修正
9c5s Apr 12, 2026
ae48cd9
fix: receiveUnicastの最小バッファ長ガードを26バイトに引き上げ
9c5s Apr 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-dots-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@9c5s/node-tcnet": patch
---

未定義dataTypeのDataパケット受信時にクラッシュする問題を修正
5 changes: 5 additions & 0 deletions .changeset/mixer-id-and-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@9c5s/node-tcnet": minor
---

MixerパケットのMixer ID誤変換を修正し、MixerData型に仕様上の全フィールドを追加
7 changes: 7 additions & 0 deletions .changeset/packet-field-extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@9c5s/node-tcnet": minor
---

パケットフィールド拡充: Timecodeセクション, Error構造化, マルチパケットヘッダー公開, APP SPECIFIC

BREAKING CHANGE: TCNetErrorPacket.errorDataを削除し、dataType/layerId/code/messageTypeに置換
16 changes: 8 additions & 8 deletions docs/wiki/Implementation-Status.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ node-tcnetにおけるTCNet V3.5.1B仕様の実装状況をまとめる。
|------|------|--------|------|
| 2 | OptIn | 68 | 送受信対応 |
| 3 | OptOut | 28 | 送受信対応 |
| 5 | Status | 300 | 受信のみ |
| 5 | Status | 300 | 受信のみ。APP SPECIFICセクション(byte 100-171)対応 |
| 20 | Request | 26 | 送信のみ |
| 200 | Data | 可変 | 受信のみ(DataTypeによる) |
| 13 | Error/Notification | 30 | 受信のみ。TCNASDP認証応答の判定に使用 |
| 13 | Error/Notification | 30 | 受信のみ。dataType/layerId/code/messageType構造化済み |
| 30 | Application Specific Data | 可変 | 送受信対応。TCNASDP認証のハンドシェイクに使用 |
| 204 | File | 可変 | 受信のみ。Artwork(JPEG)のファイル転送。マルチパケット対応 |
| 254 | Time | 154/162 | 受信のみ。OnAirセクション(V3.3.3+)の有無を自動判定する |
| 254 | Time | 154/162 | 受信のみ。OnAirセクション(V3.3.3+)の有無を自動判定。Timecodeセクション対応 |

## 未実装メッセージ

Expand All @@ -31,11 +31,11 @@ node-tcnetにおけるTCNet V3.5.1B仕様の実装状況をまとめる。
|----------|------|--------|------|
| 2 | Metrics Data | 122 | BPM, Speed, Position等 |
| 4 | Meta Data | 548 | Artist, Title, TrackID。V3.5.0+のUTF-16LEに対応 |
| 8 | Beat Grid Data | 2442 | マルチパケット。MultiPacketAssemblerで自動組み立て |
| 8 | Beat Grid Data | 2442 | マルチパケット。MultiPacketAssemblerで自動組み立て。ヘッダー公開 |
| 12 | CUE Data | 436 | 最大18キュー、Loop In/Out Time |
| 16 | Small Wave Form | 2442 | 1200バーの波形データ |
| 32 | Big Wave Form | 可変 | マルチパケット。MultiPacketAssemblerで自動組み立て |
| 128 | Artwork Data | 可変 | マルチパケット。JPEG形式。MultiPacketAssemblerで自動組み立て |
| 16 | Small Wave Form | 2442 | 1200バーの波形データ。ヘッダー公開 |
| 32 | Big Wave Form | 可変 | マルチパケット。MultiPacketAssemblerで自動組み立て。ヘッダー公開 |
| 128 | Artwork Data | 可変 | マルチパケット。JPEG形式。MultiPacketAssemblerで自動組み立て。ヘッダー公開 |
| 150 | Mixer Data | 270 | 6チャンネル対応 |

## 未実装DataPacketタイプ
Expand All @@ -47,7 +47,7 @@ node-tcnetにおけるTCNet V3.5.1B仕様の実装状況をまとめる。
| 項目 | 仕様 | 実装 | 影響 |
|------|------|------|------|
| MetaData V3.4以前 | UTF-8(256文字) | 未対応(パケット破棄) | V3.5.0未満のノードからMetaDataを受信できない |
| Timeパケットサイズ | 162バイト(V3.3.3+) | 154または162バイト | buffer.lengthで自動判定しており、OnAir未対応ノードも正常に処理できる |
| Timeパケットサイズ | 162バイト(V3.3.3+) | 154または162バイト | buffer.lengthで自動判定しており、OnAir未対応ノードも正常に処理できる。Timecodeセクションは154バイト以上で読み取り |

## TCNASDP認証

Expand Down
12 changes: 8 additions & 4 deletions docs/wiki/PRO-DJ-LINK-Bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,16 @@ byte 44-45に`0x001e` (=30) が入ることもある。用途不明 (値30はSMP

### Mixer

以下のフィールドが常に0で返される。
TCNetマスターの種類 (StatusパケットのautoMasterModeまたはノードリストで確認可能) によって取得できるフィールドが異なる。

- `masterAudioLevel`
- 全チャンネルの `audioLevel`
`masterAudioLevel`は常に0で返される (TCNetマスターの種類に依らない)。

フェーダー位置 (`faderLevel`, `crossFader`), TRIMノブ (`trimLevel`), EQ等の物理コントロール値はリアルタイムで取得可能。Audio Levelは音量レベルメーター相当の値だが、Bridge経由では配信されない。
| フィールド群 | Bridgeマスター | ShowKontrolマスター |
|-------------|--------------|-------------------|
| masterAudioLevel | 0 | 0 |
| チャンネル audioLevel | 0 | リアルタイム取得可能 |
| EQ, Filter, Trim等のノブ系 | リアルタイム取得可能 | 0 |
| フェーダー位置 (faderLevel, crossFader) | リアルタイム取得可能 | リアルタイム取得可能 |

### CUE

Expand Down
127 changes: 113 additions & 14 deletions src/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ export class TCNetStatusPacket extends TCNetPacket {
name: string;
}> = new Array(8);

/** APP SPECIFICセクション (byte 100-171, 72バイト) */
appSpecific: Buffer | null = null;

/** バッファからパケットデータを読み取る */
read(): void {
this.data = {
Expand All @@ -301,6 +304,11 @@ export class TCNetStatusPacket extends TCNetPacket {
autoMasterMode: this.buffer.readUInt8(84),
};

// parsePacketがlength()=300で事前検証するため常にtrueだが防御的に残す
if (this.buffer.length >= 172) {
this.appSpecific = Buffer.from(this.buffer.slice(100, 172));
}

for (let n = 0; n < 8; n++) {
this.layers[n] = {
source: this.buffer.readUInt8(34 + n),
Expand Down Expand Up @@ -387,7 +395,7 @@ export type TCNetTimecodeState = (typeof TCNetTimecodeState)[keyof typeof TCNetT
* @category Packets
*/
export class TCNetTimecode {
mode!: number;
smpteMode!: number;
state!: TCNetTimecodeState;
hours!: number;
minutes!: number;
Expand All @@ -400,7 +408,7 @@ export class TCNetTimecode {
* @param offset - 読み取り開始位置
*/
read(buffer: Buffer, offset: number): void {
this.mode = buffer.readUInt8(offset + 0);
this.smpteMode = buffer.readUInt8(offset + 0);
this.state = buffer.readUInt8(offset + 1) as TCNetTimecodeState;
this.hours = buffer.readUInt8(offset + 2);
this.minutes = buffer.readUInt8(offset + 3);
Expand All @@ -419,6 +427,8 @@ export type TCNetTimePacketLayer = {
beatMarker: number;
state: TCNetLayerStatus;
onAir: number;
/** レイヤー別タイムコード (byte 106-153, バッファが十分な場合のみ) */
timecode?: TCNetTimecode;
};

/**
Expand All @@ -431,14 +441,23 @@ export class TCNetTimePacket extends TCNetPacket {

/** バッファからパケットデータを読み取る */
read(): void {
// Timecodeセクション (byte 106-153) は8レイヤー x 6バイト = 48バイト
// 154バイトパケットには含まれるが、将来的に短いバッファが来る可能性に備えガードする
const hasTimecode = this.buffer.length >= 154;
for (let n = 0; n < 8; n++) {
this._layers[n] = {
const layer: TCNetTimePacketLayer = {
currentTimeMillis: this.buffer.readUInt32LE(24 + n * 4),
totalTimeMillis: this.buffer.readUInt32LE(56 + n * 4),
beatMarker: this.buffer.readUInt8(88 + n),
state: this.buffer.readUInt8(96 + n) as TCNetLayerStatus,
onAir: this.buffer.length > 154 ? this.buffer.readUInt8(154 + n) : 255,
};
if (hasTimecode) {
const tc = new TCNetTimecode();
tc.read(this.buffer, 106 + n * 6);
layer.timecode = tc;
}
this._layers[n] = layer;
}
this._generalSMPTEMode = this.buffer.readUInt8(105);
}
Expand Down Expand Up @@ -682,6 +701,36 @@ export class TCNetDataPacketCUE extends TCNetDataPacket {
}
}

/**
* マルチパケットヘッダー情報 (byte 26-41)
* @category Types
*/
export type MultiPacketHeader = {
/** データ全体のサイズ (byte 26-29, UInt32LE) */
totalDataSize: number;
/** パケット総数 (byte 30-33, UInt32LE) */
totalPackets: number;
/** パケット番号 (byte 34-37, UInt32LE) */
packetNo: number;
/** データクラスタサイズ (byte 38-41, UInt32LE) */
dataClusterSize: number;
};

/**
* マルチパケットヘッダーをバッファから読み取るヘルパー
* @param buffer - 読み取り元バッファ
* @returns マルチパケットヘッダー情報
*/
export function readMultiPacketHeader(buffer: Buffer): MultiPacketHeader | null {
if (buffer.length < 42) return null;
return {
totalDataSize: buffer.readUInt32LE(26),
totalPackets: buffer.readUInt32LE(30),
packetNo: buffer.readUInt32LE(34),
dataClusterSize: buffer.readUInt32LE(38),
};
}

/**
* 波形バーを共通パースするファイル内ヘルパー関数。
* dataStart から source の末尾 (または dataStart + maxBytes の手前) まで
Expand Down Expand Up @@ -712,9 +761,12 @@ function parseWaveformBars(source: Buffer, dataStart: number, maxBytes?: number)
*/
export class TCNetDataPacketSmallWaveForm extends TCNetDataPacket {
data: WaveformData | null = null;
/** マルチパケットヘッダー */
multiPacketHeader: MultiPacketHeader | null = null;

/** バッファからパケットデータを読み取る */
read(): void {
this.multiPacketHeader = readMultiPacketHeader(this.buffer);
// T5: バッファが 2400 バイトに満たない場合でもクラッシュしない
this.data = { bars: parseWaveformBars(this.buffer, 42, 2400) };
}
Expand All @@ -741,11 +793,14 @@ export class TCNetDataPacketMixer extends TCNetDataPacket {

/** バッファからパケットデータを読み取る */
read(): void {
// T6: 最大オフセット (channels[5] の crossfaderAssign = 245 + 13 = 258) を確認する
// T6: 最大オフセット (channels[5] の crossFaderAssign = 245 + 13 = 258) を確認する
if (this.buffer.length < 259) {
return;
}

// Mixer IDはLayer IDではないため、基底クラスの-1変換を上書きする
this.layer = this.buffer.readUInt8(25);

const parseChannel = (offset: number): MixerChannel => ({
sourceSelect: this.buffer.readUInt8(offset),
audioLevel: this.buffer.readUInt8(offset + 1),
Expand All @@ -760,7 +815,7 @@ export class TCNetDataPacketMixer extends TCNetDataPacket {
send: this.buffer.readUInt8(offset + 10),
cueA: this.buffer.readUInt8(offset + 11),
cueB: this.buffer.readUInt8(offset + 12),
crossfaderAssign: this.buffer.readUInt8(offset + 13),
crossFaderAssign: this.buffer.readUInt8(offset + 13),
});

this.data = {
Expand All @@ -770,23 +825,49 @@ export class TCNetDataPacketMixer extends TCNetDataPacket {
masterAudioLevel: this.buffer.readUInt8(61),
masterFaderLevel: this.buffer.readUInt8(62),
masterFilter: this.buffer.readUInt8(69),
micEqHi: this.buffer.readUInt8(59),
micEqLow: this.buffer.readUInt8(60),
linkCueA: this.buffer.readUInt8(67),
linkCueB: this.buffer.readUInt8(68),
masterCueA: this.buffer.readUInt8(71),
masterCueB: this.buffer.readUInt8(72),
masterIsolatorOn: this.buffer.readUInt8(74) === 1,
masterIsolatorHi: this.buffer.readUInt8(75),
masterIsolatorMid: this.buffer.readUInt8(76),
masterIsolatorLow: this.buffer.readUInt8(77),
filterHpf: this.buffer.readUInt8(79),
filterLpf: this.buffer.readUInt8(80),
filterResonance: this.buffer.readUInt8(81),
crossFader: this.buffer.readUInt8(99),
crossFaderCurve: this.buffer.readUInt8(98),
sendFxEffect: this.buffer.readUInt8(84),
sendFxExt1: this.buffer.readUInt8(85),
sendFxExt2: this.buffer.readUInt8(86),
sendFxMasterMix: this.buffer.readUInt8(87),
sendFxSizeFeedback: this.buffer.readUInt8(88),
sendFxTime: this.buffer.readUInt8(89),
sendFxHpf: this.buffer.readUInt8(90),
sendFxLevel: this.buffer.readUInt8(91),
sendReturn3Source: this.buffer.readUInt8(92),
sendReturn3Type: this.buffer.readUInt8(93),
sendReturn3On: this.buffer.readUInt8(94),
sendReturn3Level: this.buffer.readUInt8(95),
channelFaderCurve: this.buffer.readUInt8(97),
crossFaderCurve: this.buffer.readUInt8(98),
crossFader: this.buffer.readUInt8(99),
beatFxOn: this.buffer.readUInt8(100) === 1,
beatFxSelect: this.buffer.readUInt8(103),
beatFxLevelDepth: this.buffer.readUInt8(101),
beatFxChannelSelect: this.buffer.readUInt8(102),
beatFxSelect: this.buffer.readUInt8(103),
beatFxFreqHi: this.buffer.readUInt8(104),
beatFxFreqMid: this.buffer.readUInt8(105),
beatFxFreqLow: this.buffer.readUInt8(106),
headphonesPreEq: this.buffer.readUInt8(107),
headphonesALevel: this.buffer.readUInt8(108),
headphonesAMix: this.buffer.readUInt8(109),
headphonesBLevel: this.buffer.readUInt8(110),
headphonesBMix: this.buffer.readUInt8(111),
boothLevel: this.buffer.readUInt8(112),
boothEqHi: this.buffer.readUInt8(113),
boothEqLow: this.buffer.readUInt8(114),
channels: [125, 149, 173, 197, 221, 245].map(parseChannel),
};
}
Expand All @@ -810,9 +891,12 @@ export class TCNetDataPacketMixer extends TCNetDataPacket {
*/
export class TCNetDataPacketBeatGrid extends TCNetDataPacket {
data: BeatGridData | null = null;
/** マルチパケットヘッダー */
multiPacketHeader: MultiPacketHeader | null = null;

/** バッファからパケットデータを読み取る */
read(): void {
this.multiPacketHeader = readMultiPacketHeader(this.buffer);
Comment thread
9c5s marked this conversation as resolved.
this.readFromOffset(42);
}

Expand Down Expand Up @@ -862,9 +946,12 @@ export class TCNetDataPacketBeatGrid extends TCNetDataPacket {
*/
export class TCNetDataPacketBigWaveForm extends TCNetDataPacket {
data: WaveformData | null = null;
/** マルチパケットヘッダー */
multiPacketHeader: MultiPacketHeader | null = null;

/** バッファからパケットデータを読み取る */
read(): void {
this.multiPacketHeader = readMultiPacketHeader(this.buffer);
// T7: parseWaveformBars ヘルパーを使用して重複を排除する
this.data = { bars: parseWaveformBars(this.buffer, 42) };
}
Expand Down Expand Up @@ -914,14 +1001,17 @@ export class TCNetFilePacket extends TCNetDataPacket {
*/
export class TCNetDataPacketArtwork extends TCNetDataPacket {
data: ArtworkData | null = null;
/** マルチパケットヘッダー */
multiPacketHeader: MultiPacketHeader | null = null;

/** バッファからパケットデータを読み取る */
read(): void {
const dataStart = 42;
if (this.buffer.length < dataStart) {
return;
}
const clusterSize = this.buffer.readUInt32LE(38);
this.multiPacketHeader = readMultiPacketHeader(this.buffer);
const clusterSize = this.multiPacketHeader?.dataClusterSize ?? 0;
const end = getClusterEnd(this.buffer.length, dataStart, clusterSize);
this.data = { jpeg: Buffer.from(this.buffer.slice(dataStart, end)) };
}
Expand Down Expand Up @@ -958,12 +1048,21 @@ export class TCNetDataPacketArtwork extends TCNetDataPacket {
* @category Packets
*/
export class TCNetErrorPacket extends TCNetPacket {
/** エラーボディの生データ (offset 24以降) */
errorData!: Buffer;
/** データタイプ (byte 24) */
dataType!: number;
/** レイヤーID (byte 25) */
layerId!: number;
/** エラーコード (byte 26-27, UInt16LE) 1=Unknown, 13=Not Possible, 14=Empty, 255=OK */
code!: number;
/** メッセージタイプ (byte 28-29, UInt16LE) */
messageType!: number;

/** バッファからパケットデータを読み取る */
read(): void {
this.errorData = Buffer.from(this.buffer.slice(24));
this.dataType = this.buffer.readUInt8(24);
this.layerId = this.buffer.readUInt8(25);
this.code = this.buffer.readUInt16LE(26);
this.messageType = this.buffer.readUInt16LE(28);
}

/** パケットデータをバッファに書き込む */
Expand All @@ -973,10 +1072,10 @@ export class TCNetErrorPacket extends TCNetPacket {

/**
* パケットのバイト長を返す
* @returns パケット長 (-1: 可変長)
* @returns パケット長
*/
length(): number {
return -1;
return 30;
}

/**
Expand Down
Loading