diff --git a/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java b/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java
index e55fe5e..3ea1bc1 100644
--- a/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java
+++ b/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java
@@ -76,6 +76,25 @@ public class CdjStatus extends DeviceUpdate {
@API(status = API.Status.STABLE)
public static final int PLAYING_FLAG = 0x40;
+ /**
+ * The byte which reports the state of the player’s CD slot, labeled dl in the
+ * Packet Analysis document.
+ */
+ public static final int LOCAL_CD_STATE = 0x37;
+
+ /**
+ * The byte which reports the state of the player’s USB slot, labeled Ul in the
+ * Packet Analysis document.
+ */
+ @API(status = API.Status.STABLE)
+ public static final int LOCAL_USB_STATE = 0x6F;
+
+ /**
+ * The byte which reports the state of the player’s SD slot, labeled Sl in the
+ * Packet Analysis document.
+ */
+ public static final int LOCAL_SD_STATE = 0x73;
+
/**
* The device number of the player from which the track was loaded, if any; labeled Dr in
* the Packet Analysis document.
@@ -513,7 +532,7 @@ public PlayState3 getPlayState3() {
* @return the proper value
*/
private TrackSourceSlot findTrackSourceSlot() {
- TrackSourceSlot result = TRACK_SOURCE_SLOT_MAP.get(packetBytes[41]);
+ TrackSourceSlot result = TRACK_SOURCE_SLOT_MAP.get(packetBytes[0x29]);
if (result == null) {
return TrackSourceSlot.UNKNOWN;
}
@@ -526,7 +545,7 @@ private TrackSourceSlot findTrackSourceSlot() {
* @return the proper value
*/
private TrackType findTrackType() {
- TrackType result = TRACK_TYPE_MAP.get(packetBytes[42]);
+ TrackType result = TRACK_TYPE_MAP.get(packetBytes[0x2a]);
if (result == null) {
return TrackType.UNKNOWN;
}
@@ -539,7 +558,7 @@ private TrackType findTrackType() {
* @return the proper value
*/
private PlayState1 findPlayState1() {
- PlayState1 result = PLAY_STATE_1_MAP.get(packetBytes[123]);
+ PlayState1 result = PLAY_STATE_1_MAP.get(packetBytes[0x7b]);
if (result == null) {
return PlayState1.UNKNOWN;
}
@@ -552,7 +571,7 @@ private PlayState1 findPlayState1() {
* @return the proper value
*/
private PlayState2 findPlayState2() {
- switch (packetBytes[139]) {
+ switch (packetBytes[0x8b]) {
case 0x6a:
case 0x7a:
case (byte)0xfa:
@@ -574,7 +593,7 @@ private PlayState2 findPlayState2() {
* @return the proper value
*/
private PlayState3 findPlayState3() {
- PlayState3 result = PLAY_STATE_3_MAP.get(packetBytes[157]);
+ PlayState3 result = PLAY_STATE_3_MAP.get(packetBytes[0x9d]);
if (result == null) {
return PlayState3.UNKNOWN;
}
@@ -605,6 +624,13 @@ private PlayState3 findPlayState3() {
@API(status = API.Status.STABLE)
public static final int MINIMUM_PACKET_SIZE = 0xcc;
+ /**
+ * Indicates whether the status flags in this instance had to be reused from the previous valid packet received
+ * from this device, because it is an Opus Quad which sent a zero value in the current packet.
+ */
+ @API(status = API.Status.EXPERIMENTAL)
+ public final boolean statusFlagsWereReplayed;
+
/**
* Constructor sets all the immutable interpreted fields based on the packet content.
*
@@ -612,8 +638,22 @@ private PlayState3 findPlayState3() {
*/
@API(status = API.Status.STABLE)
public CdjStatus(DatagramPacket packet) {
+ this(packet, false);
+ }
+
+ /**
+ * Constructor that records whether the status flag is a replay of a previous, valid status flag, because
+ * we are receiving corrupt data from an Opus Quad.
+ *
+ * @param packet the CDJ status packet that was received, except that a zero {@link #STATUS_FLAGS} byte
+ * may have been replaced by a valid one from the previous packet received for this device
+ * @param statusFlagsReplayed indicates whether the status flag replacement has occurred.
+ */
+ CdjStatus(DatagramPacket packet, boolean statusFlagsReplayed) {
super(packet, "CDJ status", packet.getLength());
+ statusFlagsWereReplayed = statusFlagsReplayed;
+
if (packetBytes.length < MINIMUM_PACKET_SIZE) {
throw new IllegalArgumentException("Unable to create a CdjStatus object, packet too short: we need " + MINIMUM_PACKET_SIZE +
" bytes and were given only " + packetBytes.length);
@@ -633,16 +673,16 @@ public CdjStatus(DatagramPacket packet) {
logger.warn("Processing CDJ Status packets with unexpected lengths {}.", packetBytes.length);
}
trackType = findTrackType();
- rekordboxId = (int)Util.bytesToNumber(packetBytes, 44, 4);
- pitch = (int)Util.bytesToNumber(packetBytes, 141, 3);
- bpm = (int)Util.bytesToNumber(packetBytes, 146, 2);
+ rekordboxId = (int)Util.bytesToNumber(packetBytes, 0x2c, 4);
+ pitch = (int)Util.bytesToNumber(packetBytes, 0x8d, 3);
+ bpm = (int)Util.bytesToNumber(packetBytes, 0x92, 2);
playState1 = findPlayState1();
playState2 = findPlayState2();
playState3 = findPlayState3();
- firmwareVersion = new String(packetBytes, 124, 4).trim();
+ firmwareVersion = new String(packetBytes, 0x7c, 4).trim();
handingMasterToDevice = Util.unsign(packetBytes[MASTER_HAND_OFF]);
- final byte trackSourceByte = packetBytes[40];
+ final byte trackSourceByte = packetBytes[0x28];
if (isFromOpusQuad && (trackSourceByte < 16)) {
int sourcePlayer = Util.translateOpusPlayerNumbers(trackSourceByte);
if (sourcePlayer != 0) {
@@ -654,7 +694,7 @@ public CdjStatus(DatagramPacket packet) {
trackSourcePlayer = sourcePlayer;
trackSourceSlot = TrackSourceSlot.USB_SLOT;
// Indicate whether we have a metadata archive available for the USB slot:
- packetBytes[111] = (byte) (OpusProvider.getInstance().findArchive(deviceNumber) == null? 4 : 0);
+ packetBytes[LOCAL_USB_STATE] = (byte) (OpusProvider.getInstance().findArchive(deviceNumber) == null? 4 : 0);
} else {
trackSourcePlayer = trackSourceByte;
trackSourceSlot = findTrackSourceSlot();
@@ -732,7 +772,7 @@ public int getBpm() {
*/
@API(status = API.Status.STABLE)
public int getBeatWithinBar() {
- return packetBytes[166];
+ return packetBytes[0xa6];
}
/**
@@ -773,23 +813,15 @@ public double getEffectiveTempo() {
/**
* Was the CDJ playing a track when this update was sent? Has special logic to try to accommodate the quirks of
- * both pre-nexus players and the Opus Quad.
+ * pre-nexus players.
*
* @return true if the play flag was set, or, if this seems to be a non-nexus player, if P1
* and P2 have values corresponding to a playing state.
*/
@API(status = API.Status.STABLE)
public boolean isPlaying() {
- if (packetBytes.length >= 212) {
- final boolean simpleResult = (packetBytes[STATUS_FLAGS] & PLAYING_FLAG) > 0;
- if (!simpleResult && isFromOpusQuad) {
- // Sometimes the Opus Quad lies and reports that it is not playing in this flag, even though it actually is.
- // Try to recover from that.
- return playState1 == PlayState1.PLAYING || playState1 == PlayState1.LOOPING ||
- (playState1 == PlayState1.SEARCHING && (playState2.protocolValue & 0x0f) == 0x0a);
- } else {
- return simpleResult;
- }
+ if (packetBytes.length >= 0xd4) {
+ return (packetBytes[STATUS_FLAGS] & PLAYING_FLAG) > 0;
} else {
// Pre-nexus players don’t send this critical flag byte at all, so we always have to infer play state.
return playState1 == PlayState1.PLAYING || playState1 == PlayState1.LOOPING ||
@@ -838,7 +870,7 @@ public boolean isOnAir() {
*/
@API(status = API.Status.STABLE)
public boolean isLocalUsbLoaded() {
- return (packetBytes[111] == 0);
+ return (packetBytes[LOCAL_USB_STATE] == 0);
}
/**
@@ -848,7 +880,7 @@ public boolean isLocalUsbLoaded() {
*/
@API(status = API.Status.STABLE)
public boolean isLocalUsbUnloading() {
- return (packetBytes[111] == 2);
+ return (packetBytes[LOCAL_USB_STATE] == 2);
}
/**
@@ -858,7 +890,7 @@ public boolean isLocalUsbUnloading() {
*/
@API(status = API.Status.STABLE)
public boolean isLocalUsbEmpty() {
- return (packetBytes[111] == 4);
+ return (packetBytes[LOCAL_USB_STATE] == 4);
}
/**
@@ -868,7 +900,7 @@ public boolean isLocalUsbEmpty() {
*/
@API(status = API.Status.STABLE)
public boolean isLocalSdLoaded() {
- return (packetBytes[115] == 0);
+ return (packetBytes[LOCAL_SD_STATE] == 0);
}
/**
@@ -878,7 +910,7 @@ public boolean isLocalSdLoaded() {
*/
@API(status = API.Status.STABLE)
public boolean isLocalSdUnloading() {
- return (packetBytes[115] == 2);
+ return (packetBytes[LOCAL_SD_STATE] == 2);
}
/**
@@ -888,7 +920,7 @@ public boolean isLocalSdUnloading() {
*/
@API(status = API.Status.STABLE)
public boolean isLocalSdEmpty() {
- return (packetBytes[115] == 4);
+ return (packetBytes[LOCAL_SD_STATE] == 4);
}
/**
@@ -901,7 +933,7 @@ public boolean isLocalSdEmpty() {
*/
@API(status = API.Status.STABLE)
public boolean isDiscSlotEmpty() {
- return (packetBytes[0x37] != 0x1e) && (packetBytes[0x37] != 0x11);
+ return (packetBytes[LOCAL_CD_STATE] != 0x1e) && (packetBytes[LOCAL_CD_STATE] != 0x11);
}
/**
@@ -912,7 +944,7 @@ public boolean isDiscSlotEmpty() {
*/
@API(status = API.Status.STABLE)
public boolean isDiscSlotAsleep() {
- return (packetBytes[0x37] == 1);
+ return (packetBytes[LOCAL_CD_STATE] == 1);
}
/**
@@ -1036,7 +1068,7 @@ public boolean isPlayingCdjMode() {
*/
@API(status = API.Status.STABLE)
public boolean isLinkMediaAvailable() {
- return (packetBytes[117] != 0);
+ return (packetBytes[0x75] != 0);
}
/**
@@ -1046,7 +1078,7 @@ public boolean isLinkMediaAvailable() {
*/
@API(status = API.Status.STABLE)
public boolean isBusy() {
- return packetBytes[39] != 0;
+ return packetBytes[0x27] != 0;
}
/**
@@ -1057,7 +1089,7 @@ public boolean isBusy() {
*/
@API(status = API.Status.STABLE)
public int getTrackNumber() {
- return (int)Util.bytesToNumber(packetBytes, 50, 2);
+ return (int)Util.bytesToNumber(packetBytes, 0x32, 2);
}
/**
@@ -1083,7 +1115,7 @@ public int getSyncNumber() {
*/
@API(status = API.Status.STABLE)
public int getBeatNumber() {
- long result = Util.bytesToNumber(packetBytes, 160, 4);
+ long result = Util.bytesToNumber(packetBytes, 0xa0, 4);
if (result != 0xffffffffL) {
return (int) result;
} return -1;
@@ -1104,7 +1136,7 @@ public int getBeatNumber() {
*/
@API(status = API.Status.STABLE)
public int getCueCountdown() {
- return (int)Util.bytesToNumber(packetBytes, 164, 2);
+ return (int)Util.bytesToNumber(packetBytes, 0xa4, 2);
}
/**
@@ -1151,7 +1183,7 @@ public String getFirmwareVersion() {
*/
@API(status = API.Status.STABLE)
public long getPacketNumber() {
- return Util.bytesToNumber(packetBytes, 200, 4);
+ return Util.bytesToNumber(packetBytes, 0xc8, 4);
}
/**
@@ -1221,7 +1253,8 @@ public String toString() {
", isBeatWithinBarMeaningful? " + isBeatWithinBarMeaningful() + ", cue: " + formatCueCountdown() +
", Playing? " + isPlaying() + ", Master? " + isTempoMaster() +
", Synced? " + isSynced() + ", On-Air? " + isOnAir() +
- ", handingMasterToDevice:" + handingMasterToDevice + "]";
+ ", handingMasterToDevice: " + handingMasterToDevice +
+ ", statusFlagsWereReplayed: " + statusFlagsWereReplayed + "]";
}
}
diff --git a/src/main/java/org/deepsymmetry/beatlink/DeviceUpdate.java b/src/main/java/org/deepsymmetry/beatlink/DeviceUpdate.java
index 6e19832..3bc3c11 100644
--- a/src/main/java/org/deepsymmetry/beatlink/DeviceUpdate.java
+++ b/src/main/java/org/deepsymmetry/beatlink/DeviceUpdate.java
@@ -14,6 +14,11 @@
@API(status = API.Status.STABLE)
public abstract class DeviceUpdate {
+ /**
+ * The offset at which the device number that sent this packet can be found.
+ */
+ public static final int DEVICE_NUMBER_OFFSET = 0x21;
+
/**
* The address from which this device update was received.
*/
@@ -60,14 +65,14 @@ public DeviceUpdate(DatagramPacket packet, String name, int length) {
address = packet.getAddress();
packetBytes = new byte[packet.getLength()];
System.arraycopy(packet.getData(), 0, packetBytes, 0, packet.getLength());
- deviceName = new String(packetBytes, 11, 20).trim();
+ deviceName = new String(packetBytes, 0x0b, 20).trim();
isFromOpusQuad = deviceName.equals(OpusProvider.OPUS_NAME);
preNexusCdj = deviceName.startsWith("CDJ") && (deviceName.endsWith("900") || deviceName.endsWith("2000"));
if (isFromOpusQuad) {
- deviceNumber = Util.translateOpusPlayerNumbers(packetBytes[40]);
+ deviceNumber = Util.translateOpusPlayerNumbers(packetBytes[DEVICE_NUMBER_OFFSET]);
} else {
- deviceNumber = Util.unsign(packetBytes[33]);
+ deviceNumber = Util.unsign(packetBytes[DEVICE_NUMBER_OFFSET]);
}
}
diff --git a/src/main/java/org/deepsymmetry/beatlink/VirtualRekordbox.java b/src/main/java/org/deepsymmetry/beatlink/VirtualRekordbox.java
index 6bbcf5c..a387fda 100644
--- a/src/main/java/org/deepsymmetry/beatlink/VirtualRekordbox.java
+++ b/src/main/java/org/deepsymmetry/beatlink/VirtualRekordbox.java
@@ -1,7 +1,6 @@
package org.deepsymmetry.beatlink;
import org.apiguardian.api.API;
-import org.deepsymmetry.beatlink.data.MetadataFinder;
import org.deepsymmetry.beatlink.data.OpusProvider;
import org.deepsymmetry.beatlink.data.SlotReference;
import org.slf4j.Logger;
@@ -209,6 +208,10 @@ public void setAnnounceInterval(int interval) {
0x02, 0x0b, 0x04, 0x01, 0x00, 0x00, 0x04, 0x08
};
+ /**
+ * Used to construct the packet we send to request lighting information from the Opus Quad, which is how we
+ * can figure out what tracks it is playing.
+ */
private static final byte[] rekordboxLightingRequestStatusBytes = {
0x51, 0x73, 0x70, 0x74, 0x31, 0x57, 0x6d, 0x4a, 0x4f, 0x4c, 0x11, 0x72, 0x65, 0x6b, 0x6f, 0x72,
0x64, 0x62, 0x6f, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
@@ -328,7 +331,7 @@ public static String getDeviceName() {
* This imitates the request RekordboxLighting sends to get PSSI data from Opus Quad device
* (and maybe more devices in the future).
*
- * @throws IOException
+ * @throws IOException if there is a problem sending the request
*/
@API(status = API.Status.EXPERIMENTAL)
public void requestPSSI() throws IOException{
@@ -356,11 +359,7 @@ public void requestPSSI() throws IOException{
*
* @param usbSlotNumber the slot we have the archive loaded in
*/
-
- /**
- *
- * @param usbSlotNumber
- */
+ @API(status = API.Status.EXPERIMENTAL)
public void clearPlayerCaches(int usbSlotNumber){
playerSongStructures.remove(usbSlotNumber);
playerTrackSourceSlots.remove(usbSlotNumber);
@@ -378,6 +377,13 @@ SlotReference findMatchedTrackSourceSlotForPlayer(int player) {
return playerTrackSourceSlots.get(player);
}
+
+ /**
+ * Keeps track of the most recent valid (non-zero) status flag byte we have received from each device number,
+ * so we can reuse it in cases where a corrupt (zero) value has been sent to us.
+ */
+ private final Map lastValidStatusFlagBytes = new ConcurrentHashMap<>();
+
/**
* Given an update packet sent to us, create the appropriate object to describe it.
*
@@ -406,7 +412,24 @@ private DeviceUpdate buildUpdate(DatagramPacket packet) {
case CDJ_STATUS:
if (length >= CdjStatus.MINIMUM_PACKET_SIZE) {
- CdjStatus status = new CdjStatus(packet);
+ // Try to recover from a malformed Opus Quad packet with a zero value in its status flags by reusing the last valid one we saw from the same device.
+ final byte reportedStatusFlags = packet.getData()[CdjStatus.STATUS_FLAGS];
+ final boolean hadToRecoverStatusFlags = (reportedStatusFlags == 0);
+ final int rawDeviceNumber = packet.getData()[CdjStatus.DEVICE_NUMBER_OFFSET];
+ if (hadToRecoverStatusFlags) {
+ final Byte recoveredStatusFlags = lastValidStatusFlagBytes.get(rawDeviceNumber);
+ if (recoveredStatusFlags != null) {
+ packet.getData()[CdjStatus.STATUS_FLAGS] = recoveredStatusFlags;
+ } else {
+ logger.warn("Unable to recover from malformed Opus Quad status packet because we have not yet received a valid packet from device {}.",
+ rawDeviceNumber);
+ return null; // Discard this packet.
+ }
+ } else {
+ lastValidStatusFlagBytes.put(rawDeviceNumber, reportedStatusFlags); // Record in case next packet for this device is malformed.
+ }
+
+ CdjStatus status = new CdjStatus(packet, hadToRecoverStatusFlags);
// If source player number is zero the deck does not have a song loaded, clear the PSSI and source slot we had for that player.
if (status.getTrackSourcePlayer() == 0) {
@@ -825,7 +848,7 @@ public void sendRekordboxAnnouncement() {
}
/**
- * TODO top-level description needed.
+ * TODO: top-level description needed.
*
* @return true if we found DJ Link devices and were able to create the {@code VirtualRekordbox}.
* @throws Exception if there is a problem opening a socket on the right network
@@ -1036,6 +1059,7 @@ synchronized boolean start() throws Exception {
* @return true if we found DJ Link devices and were able to create the {@code VirtualRekordbox}, or it was already running.
* @throws SocketException if the socket to listen on port 50002 cannot be created
*/
+ @API(status = API.Status.EXPERIMENTAL)
synchronized boolean start(byte deviceNumber) throws Exception {
// TODO I am not sure we actually need this method. If we do want to control the device number that is used,
// we will need to add a mechanism do that from VirtualCdj.
@@ -1049,6 +1073,7 @@ synchronized boolean start(byte deviceNumber) throws Exception {
/**
* Stop announcing ourselves and listening for status updates.
*/
+ @API(status = API.Status.EXPERIMENTAL)
synchronized void stop() {
if (isRunning()) {
DeviceFinder.getInstance().removeIgnoredAddress(socket.get().getLocalAddress());
@@ -1056,6 +1081,7 @@ synchronized void stop() {
socket.set(null);
broadcastAddress.set(null);
updates.clear();
+ lastValidStatusFlagBytes.clear();
setDeviceNumber((byte) 0); // Set up for self-assignment if restarted.
deliverLifecycleAnnouncement(logger, false);
}