alphatheta-connect 0.26.1 → 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)
@@ -7176,9 +7295,9 @@ class ProlinkNetwork {
7176
7295
  // Create unified database
7177
7296
  const database = new db_1.default(localdb, remotedb, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
7178
7297
  // Create controller service
7179
- 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"));
7180
7299
  __classPrivateFieldSet(this, _ProlinkNetwork_state, types_1.NetworkState.Connected, "f");
7181
- __classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, heartbeat, control, remotedb, localdb, database }, "f");
7300
+ __classPrivateFieldSet(this, _ProlinkNetwork_connection, { vcdj, announcer, heartbeat, control, remotedb, localdb, database }, "f");
7182
7301
  tx.finish();
7183
7302
  }
7184
7303
  /**
@@ -7265,6 +7384,15 @@ class ProlinkNetwork {
7265
7384
  get positionEmitter() {
7266
7385
  return __classPrivateFieldGet(this, _ProlinkNetwork_state, "f") === types_1.NetworkState.Connected ? __classPrivateFieldGet(this, _ProlinkNetwork_positionEmitter, "f") : null;
7267
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
+ }
7268
7396
  /**
7269
7397
  * Get the {@link Control} service. This service can be used to control the
7270
7398
  * Playstate of CDJs on the network.