alphatheta-connect 0.26.0 → 0.27.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/lib/cli.js CHANGED
@@ -811,14 +811,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
811
811
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
812
812
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
813
813
  };
814
- var _Control_hostDevice, _Control_beatSocket, _Control_correlationByte;
814
+ var _Control_hostDevice, _Control_beatSocket, _Control_statusSocket, _Control_correlationByte;
815
815
  Object.defineProperty(exports, "__esModule", ({ value: true }));
816
- exports.makePlaystatePacket = void 0;
816
+ exports.makePlaystatePacket = exports.makeLoadTrackPacket = void 0;
817
817
  const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
818
818
  const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
819
819
  const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
820
820
  const udp_1 = __webpack_require__(/*! src/utils/udp */ "./src/utils/udp.ts");
821
+ const load_1 = __webpack_require__(/*! ./load */ "./src/control/load.ts");
821
822
  const stagehand_1 = __webpack_require__(/*! ./stagehand */ "./src/control/stagehand.ts");
823
+ var load_2 = __webpack_require__(/*! ./load */ "./src/control/load.ts");
824
+ Object.defineProperty(exports, "makeLoadTrackPacket", ({ enumerable: true, get: function () { return load_2.makeLoadTrackPacket; } }));
822
825
  const STATE_MAP = {
823
826
  [types_1.CDJStatus.PlayState.Cued]: 0x01,
824
827
  [types_1.CDJStatus.PlayState.Playing]: 0x00,
@@ -839,20 +842,51 @@ const makePlaystatePacket = ({ hostDevice, device, playState }) => Uint8Array.fr
839
842
  ]);
840
843
  exports.makePlaystatePacket = makePlaystatePacket;
841
844
  class Control {
842
- constructor(beatSocket, hostDevice) {
845
+ constructor(beatSocket, hostDevice, statusSocket = null) {
843
846
  _Control_hostDevice.set(this, void 0);
844
847
  /**
845
848
  * The socket used to send control packets
846
849
  */
847
850
  _Control_beatSocket.set(this, void 0);
851
+ /**
852
+ * The socket bound to the status port, which the Load Track command must
853
+ * leave from: a CDJ-3000 ignores it from any other port. Absent when the
854
+ * control service was built without one.
855
+ */
856
+ _Control_statusSocket.set(this, void 0);
848
857
  /**
849
858
  * Randomized correlation byte per session for Stagehand commands
850
859
  */
851
860
  _Control_correlationByte.set(this, void 0);
852
861
  __classPrivateFieldSet(this, _Control_beatSocket, beatSocket, "f");
862
+ __classPrivateFieldSet(this, _Control_statusSocket, statusSocket, "f");
853
863
  __classPrivateFieldSet(this, _Control_hostDevice, hostDevice, "f");
854
864
  __classPrivateFieldSet(this, _Control_correlationByte, Math.floor(Math.random() * 256), "f");
855
865
  }
866
+ /**
867
+ * Send one Stagehand transport packet (0x07) carrying `op` with the press /
868
+ * release flag, for opcodes the named methods do not cover.
869
+ */
870
+ async transport(device, op, press) {
871
+ const p = (0, stagehand_1.makeStagehandTransportPacket)(__classPrivateFieldGet(this, _Control_hostDevice, "f"), op, press, __classPrivateFieldGet(this, _Control_correlationByte, "f"));
872
+ await (0, udp_1.udpSend)(__classPrivateFieldGet(this, _Control_beatSocket, "f"), p, constants_1.BEAT_PORT, device.ip.address);
873
+ }
874
+ /**
875
+ * Tell a player to load a track, as rekordbox does when a row is dragged
876
+ * onto a deck: the Load Track command (0x19) from our status port to the
877
+ * player's. The player answers 0x1a and the load shows in its next status.
878
+ *
879
+ * `source` names where the track lives: a device number and its slot (a
880
+ * player's USB or SD, or the rekordbox slot of a library served over the
881
+ * link).
882
+ */
883
+ async loadTrack(device, trackId, source) {
884
+ if (__classPrivateFieldGet(this, _Control_statusSocket, "f") === null) {
885
+ throw new Error('loadTrack needs the status socket; connect through ProlinkNetwork');
886
+ }
887
+ const p = (0, load_1.makeLoadTrackPacket)(__classPrivateFieldGet(this, _Control_hostDevice, "f"), device, trackId, source);
888
+ await (0, udp_1.udpSend)(__classPrivateFieldGet(this, _Control_statusSocket, "f"), p, constants_1.STATUS_PORT, device.ip.address);
889
+ }
856
890
  /**
857
891
  * Start or stop a CDJ on the network.
858
892
  * Delegates automatically to Stagehand-specific transport control if connected in Stagehand mode.
@@ -918,10 +952,79 @@ class Control {
918
952
  await (0, udp_1.udpSend)(__classPrivateFieldGet(this, _Control_beatSocket, "f"), p, constants_1.STATUS_PORT, device.ip.address);
919
953
  }
920
954
  }
921
- _Control_hostDevice = new WeakMap(), _Control_beatSocket = new WeakMap(), _Control_correlationByte = new WeakMap();
955
+ _Control_hostDevice = new WeakMap(), _Control_beatSocket = new WeakMap(), _Control_statusSocket = new WeakMap(), _Control_correlationByte = new WeakMap();
922
956
  exports["default"] = Control;
923
957
 
924
958
 
959
+ /***/ },
960
+
961
+ /***/ "./src/control/load.ts"
962
+ /*!*****************************!*\
963
+ !*** ./src/control/load.ts ***!
964
+ \*****************************/
965
+ (__unused_webpack_module, exports, __webpack_require__) {
966
+
967
+ "use strict";
968
+
969
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
970
+ exports.LOAD_TRACK_LENGTH = void 0;
971
+ exports.makeLoadTrackPacket = makeLoadTrackPacket;
972
+ const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
973
+ const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
974
+ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
975
+ /**
976
+ * Byte length of a Load Track command.
977
+ */
978
+ exports.LOAD_TRACK_LENGTH = 0x58;
979
+ /**
980
+ * Generates the Load Track command (0x19, 88 bytes), unicast to a player's
981
+ * status port.
982
+ *
983
+ * Byte for byte what rekordbox 7.2 sends a CDJ-3000 (captured 2026-09-13 and
984
+ * verified live on two CDJ-3000s): the status-style header with a 20-byte
985
+ * name, `01 01`, our device number, the length `0034` of what follows, our
986
+ * number again, then the source device, slot and track type, the track ID
987
+ * big-endian, `32` at 0x33, the destination player counted from zero at
988
+ * 0x40, and `32` at 0x4b.
989
+ *
990
+ * A player only honours the command from the address its keep-alives
991
+ * announced and from port 50002.
992
+ */
993
+ function makeLoadTrackPacket(hostDevice, device, trackId, source) {
994
+ var _a;
995
+ const packet = new Uint8Array(exports.LOAD_TRACK_LENGTH);
996
+ // 0-9: magic header
997
+ packet.set(constants_1.PROLINK_HEADER, 0);
998
+ // 10: kind 0x19
999
+ packet[10] = 0x19;
1000
+ // 11-30: device name (20 bytes)
1001
+ packet.set((0, utils_1.buildName)(hostDevice), 11);
1002
+ // 31-35: 01, subtype 01, our number, then the length 0x0034 of what follows
1003
+ packet[31] = 0x01;
1004
+ packet[32] = 0x01;
1005
+ packet[33] = hostDevice.id;
1006
+ packet[34] = 0x00;
1007
+ packet[35] = 0x34;
1008
+ // 36: our number again
1009
+ packet[36] = hostDevice.id;
1010
+ // 40-42: the source device, its slot and the track type
1011
+ packet[40] = source.deviceId;
1012
+ packet[41] = source.slot;
1013
+ packet[42] = (_a = source.trackType) !== null && _a !== void 0 ? _a : types_1.TrackType.RB;
1014
+ // 44-47: the track ID, big-endian
1015
+ packet[44] = (trackId >>> 24) & 0xff;
1016
+ packet[45] = (trackId >>> 16) & 0xff;
1017
+ packet[46] = (trackId >>> 8) & 0xff;
1018
+ packet[47] = trackId & 0xff;
1019
+ packet[0x33] = 0x32;
1020
+ // 0x40: the player to load onto, counted from zero; a player accepts the
1021
+ // command by address regardless
1022
+ packet[0x40] = Math.max(device.id - 1, 0);
1023
+ packet[0x4b] = 0x32;
1024
+ return packet;
1025
+ }
1026
+
1027
+
925
1028
  /***/ },
926
1029
 
927
1030
  /***/ "./src/control/stagehand.ts"
@@ -938,59 +1041,75 @@ exports.makeStagehandPrefWritePacket = makeStagehandPrefWritePacket;
938
1041
  const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
939
1042
  const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
940
1043
  /**
941
- * Generates a Stagehand transport control packet (0x07, 56 bytes)
1044
+ * The name field of a Stagehand unicast frame: 19 bytes, one shorter than the
1045
+ * broadcast frames' 20, so the bytes that follow it sit at offset 30.
1046
+ */
1047
+ function buildUnicastName(device) {
1048
+ return (0, utils_1.buildName)(device).subarray(0, 19);
1049
+ }
1050
+ /**
1051
+ * Generates a Stagehand transport control packet (0x07, 48 bytes).
1052
+ *
1053
+ * Byte for byte what the iPad app sends a CDJ-3000 (captured 2026-05-23,
1054
+ * `stagehand-allproto-20260523T142615.pcap`, pinned in the tests): the
1055
+ * unicast header `03 01 00`, a byte that varies per command, the length
1056
+ * `0030`, then `3a`, `01`, the opcode and the press flag each in their own
1057
+ * 16-bit slot.
942
1058
  *
943
1059
  * @param hostDevice - The Stagehand device posing as sender
944
- * @param op - The command opcode (e.g. 0x0f, 0x14, 0x18, 0x1a, 0x1b)
1060
+ * @param op - The command opcode (e.g. 0x0f, 0x14, 0x18, 0x19, 0x1a, 0x1b)
945
1061
  * @param press - Whether the action is press (true) or release (false)
946
- * @param correlationByte - The randomized per-session correlation byte
1062
+ * @param correlationByte - The per-session correlation byte
947
1063
  */
948
1064
  function makeStagehandTransportPacket(hostDevice, op, press, correlationByte) {
949
- const packet = new Uint8Array(56);
1065
+ const packet = new Uint8Array(48);
950
1066
  // 0-9: magic header
951
1067
  packet.set(constants_1.PROLINK_HEADER, 0);
952
1068
  // 10: opcode 0x07
953
1069
  packet[10] = 0x07;
954
- // 11-30: device name
955
- packet.set((0, utils_1.buildName)(hostDevice), 11);
956
- // 31: 0x01
1070
+ // 11-29: device name
1071
+ packet.set(buildUnicastName(hostDevice), 11);
1072
+ // 30-32: unicast header 03 01 00
1073
+ packet[30] = 0x03;
957
1074
  packet[31] = 0x01;
958
- // 32: 0x03
959
- packet[32] = 0x03;
1075
+ packet[32] = 0x00;
960
1076
  // 33: per-session correlation byte
961
1077
  packet[33] = correlationByte;
962
- // 34-35: remaining length 0x0030 (48 bytes)
1078
+ // 34-35: packet length 0x0030 (48 bytes)
963
1079
  packet[34] = 0x00;
964
1080
  packet[35] = 0x30;
965
- // 40: Stagehand sub-id 0x3a
966
- packet[40] = 0x3a;
967
- // 44: command opcode
968
- packet[44] = op;
969
- // 46: press/release flag
970
- packet[46] = press ? 0x01 : 0x00;
1081
+ // 39: Stagehand sub-id 0x3a
1082
+ packet[39] = 0x3a;
1083
+ // 41: 0x01
1084
+ packet[41] = 0x01;
1085
+ // 43: command opcode
1086
+ packet[43] = op;
1087
+ // 45: press/release flag
1088
+ packet[45] = press ? 0x01 : 0x00;
971
1089
  return packet;
972
1090
  }
973
1091
  /**
974
- * Generates a Stagehand preference write packet (0x6b, 124 bytes)
1092
+ * Generates a Stagehand preference write packet (0x6b, 116 bytes).
1093
+ *
1094
+ * As captured from the iPad app (same capture as the transport packet): the
1095
+ * unicast header `03 01 00 3a`, the length `0050`, the write flag, then the
1096
+ * preference slots.
975
1097
  *
976
1098
  * @param hostDevice - The Stagehand device posing as sender
977
1099
  * @param options - The preferences to write (onAir, quantize)
978
1100
  */
979
1101
  function makeStagehandPrefWritePacket(hostDevice, options) {
980
- const packet = new Uint8Array(124);
1102
+ const packet = new Uint8Array(116);
981
1103
  // 0-9: magic header
982
1104
  packet.set(constants_1.PROLINK_HEADER, 0);
983
1105
  // 10: opcode 0x6b
984
1106
  packet[10] = 0x6b;
985
- // 11-30: device name
986
- const name = (0, utils_1.buildName)(hostDevice);
987
- // Trailing byte 30 (which is offset 30, meaning index 19 of name) is set to 0x03
988
- name[19] = 0x03;
989
- packet.set(name, 11);
990
- // 31: 0x01 (subscription-id-a constant)
1107
+ // 11-29: device name
1108
+ packet.set(buildUnicastName(hostDevice), 11);
1109
+ // 30-32: unicast header 03 01 00
1110
+ packet[30] = 0x03;
991
1111
  packet[31] = 0x01;
992
- // 32: 0x03 (constant)
993
- packet[32] = 0x03;
1112
+ packet[32] = 0x00;
994
1113
  // 33: Stagehand sub-id constant 0x3a
995
1114
  packet[33] = 0x3a;
996
1115
  // 34-35: body length 0x0050 (80 bytes)
@@ -1467,7 +1586,9 @@ async function viaLocal(local, opts) {
1467
1586
  if (!orm) {
1468
1587
  break;
1469
1588
  }
1470
- yield orm.findTrack(entry.id);
1589
+ // Playlist entries reference the track through trackId; entry.id is the
1590
+ // entry's own row id (or its index, for OneLibrary), not a track.
1591
+ yield orm.findTrack(entry.trackId);
1471
1592
  }
1472
1593
  };
1473
1594
  const tracks = { [Symbol.asyncIterator]: iterateTracks };
@@ -3285,7 +3406,9 @@ var RekordboxAnlz = (function() {
3285
3406
  this.time = this._io.readU4be();
3286
3407
  this.loopTime = this._io.readU4be();
3287
3408
  this.colorId = this._io.readU1();
3288
- this._unnamed9 = this._io.readBytes(11);
3409
+ this._unnamed9 = this._io.readBytes(7);
3410
+ this.loopNumerator = this._io.readU2be();
3411
+ this.loopDenominator = this._io.readU2be();
3289
3412
  if (this.lenEntry > 43) {
3290
3413
  this.lenComment = this._io.readU4be();
3291
3414
  }
@@ -3305,7 +3428,7 @@ var RekordboxAnlz = (function() {
3305
3428
  this.colorBlue = this._io.readU1();
3306
3429
  }
3307
3430
  if ((this.lenEntry - this.lenComment) > 48) {
3308
- this._unnamed16 = this._io.readBytes(((this.lenEntry - 48) - this.lenComment));
3431
+ this._unnamed18 = this._io.readBytes(((this.lenEntry - 48) - this.lenComment));
3309
3432
  }
3310
3433
  }
3311
3434
 
@@ -3337,6 +3460,16 @@ var RekordboxAnlz = (function() {
3337
3460
  * and has been assigned a color.
3338
3461
  */
3339
3462
 
3463
+ /**
3464
+ * The numerator of the loop size fraction for a quantized loop,
3465
+ * e.g. 4 for a 4-beat loop. Zero when the loop is not quantized.
3466
+ */
3467
+
3468
+ /**
3469
+ * The denominator of the loop size fraction for a quantized loop,
3470
+ * e.g. 1 for a 4-beat loop.
3471
+ */
3472
+
3340
3473
  /**
3341
3474
  * The comment assigned to this cue by the DJ, if any, with a trailing NUL.
3342
3475
  */
@@ -5767,16 +5900,20 @@ function makeBeatGrid(data) {
5767
5900
  * Fill cue and loop data from the ANLZ section
5768
5901
  */
5769
5902
  function makeCueAndLoop(data) {
5770
- return data.body.cues.map((entry) => {
5903
+ return data.body.cues
5904
+ .map((entry) => {
5771
5905
  // Cues with the status 0 are likely leftovers that were removed
5772
- const button = entry.hotCue === 0 ? false : entry.type;
5906
+ // `hotCue` is the button number (1-8, A-H); zero marks a memory cue.
5907
+ // `type` only says whether the entry is a cue point (1) or a loop (2).
5908
+ const button = entry.hotCue === 0 ? false : entry.hotCue;
5773
5909
  const isCue = entry.type === 0x01;
5774
5910
  const isLoop = entry.type === 0x02;
5775
5911
  // NOTE: Unlike the remotedb, these entries are already in milliseconds.
5776
5912
  const offset = entry.time;
5777
5913
  const length = entry.loopTime - offset;
5778
5914
  return (0, utils_1.makeCueLoopEntry)(isCue, isLoop, offset, length, button);
5779
- });
5915
+ })
5916
+ .filter((c) => c !== null);
5780
5917
  }
5781
5918
  /**
5782
5919
  * Fill waveform HD data from the ANLZ section
@@ -5812,9 +5949,13 @@ function makeExtendedCues(data) {
5812
5949
  b: (_c = entry.colorBlue) !== null && _c !== void 0 ? _c : 0,
5813
5950
  };
5814
5951
  }
5815
- // Add comment if present
5952
+ // Add comment if present. The tag stores it with a trailing NUL, which
5953
+ // is not part of what the DJ typed.
5816
5954
  if (entry.lenComment > 0 && entry.comment) {
5817
- cue.comment = entry.comment;
5955
+ const comment = entry.comment.replace(/\0+$/, '');
5956
+ if (comment.length > 0) {
5957
+ cue.comment = comment;
5958
+ }
5818
5959
  }
5819
5960
  // Add quantized loop information if present
5820
5961
  if (entry.loopNumerator !== undefined && entry.loopNumerator > 0) {
@@ -5880,19 +6021,24 @@ function makeSongStructure(data) {
5880
6021
  },
5881
6022
  };
5882
6023
  const mood = (_a = moodMap[data.body.mood]) !== null && _a !== void 0 ? _a : 'high';
5883
- const bank = (_b = bankMap[data.body.rawBank]) !== null && _b !== void 0 ? _b : 'default';
6024
+ const bank = (_b = bankMap[data.body.bank]) !== null && _b !== void 0 ? _b : 'default';
6025
+ // Field names follow the Kaitai-generated object (rekordbox_anlz.ksy
6026
+ // `song_structure_entry`): `phraseNumber`, `beatNumber`, `fillIn`,
6027
+ // `fillInBeatNumber`, and `kind` is a `phrase_high` / `phrase_mid` /
6028
+ // `phrase_low` object whose `id` carries the raw kind value.
5884
6029
  const phrases = data.body.entries.map((entry) => {
5885
6030
  var _a;
6031
+ const kind = typeof entry.kind === 'object' ? entry.kind.id : entry.kind;
5886
6032
  const phrase = {
5887
- index: entry.index,
5888
- beat: entry.beat,
5889
- kind: entry.kind,
5890
- phraseType: (_a = phraseTypeMap[mood][entry.kind]) !== null && _a !== void 0 ? _a : 'Unknown',
6033
+ index: entry.phraseNumber,
6034
+ beat: entry.beatNumber,
6035
+ kind,
6036
+ phraseType: (_a = phraseTypeMap[mood][kind]) !== null && _a !== void 0 ? _a : 'Unknown',
5891
6037
  };
5892
6038
  // Add fill-in information if present
5893
- if (entry.fill > 0) {
5894
- phrase.fill = entry.fill;
5895
- phrase.fillBeat = entry.beatFill;
6039
+ if (entry.fillIn > 0) {
6040
+ phrase.fill = entry.fillIn;
6041
+ phrase.fillBeat = entry.fillInBeatNumber;
5896
6042
  }
5897
6043
  return phrase;
5898
6044
  });
@@ -7149,9 +7295,9 @@ class ProlinkNetwork {
7149
7295
  // Create unified database
7150
7296
  const database = new db_1.default(localdb, remotedb, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
7151
7297
  // Create controller service
7152
- const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj);
7298
+ const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_statusSocket, "f"));
7153
7299
  __classPrivateFieldSet(this, _ProlinkNetwork_state, types_1.NetworkState.Connected, "f");
7154
- __classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, heartbeat, control, remotedb, localdb, database }, "f");
7300
+ __classPrivateFieldSet(this, _ProlinkNetwork_connection, { vcdj, announcer, heartbeat, control, remotedb, localdb, database }, "f");
7155
7301
  tx.finish();
7156
7302
  }
7157
7303
  /**
@@ -7238,6 +7384,15 @@ class ProlinkNetwork {
7238
7384
  get positionEmitter() {
7239
7385
  return __classPrivateFieldGet(this, _ProlinkNetwork_state, "f") === types_1.NetworkState.Connected ? __classPrivateFieldGet(this, _ProlinkNetwork_positionEmitter, "f") : null;
7240
7386
  }
7387
+ /**
7388
+ * The device this network announces itself as, once connected: the
7389
+ * `hostDevice` the status emitter's media query and the control service
7390
+ * send from.
7391
+ */
7392
+ get virtualDevice() {
7393
+ var _a, _b;
7394
+ return (_b = (_a = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _a === void 0 ? void 0 : _a.vcdj) !== null && _b !== void 0 ? _b : null;
7395
+ }
7241
7396
  /**
7242
7397
  * Get the {@link Control} service. This service can be used to control the
7243
7398
  * Playstate of CDJs on the network.