Skip to content

Commit

Permalink
Try to handle unreliable playing flag from Opus
Browse files Browse the repository at this point in the history
Also fill in some missing JavaDoc to clear up build warnings,
and update some code to take advantage of the newer Java
release we are building with.
  • Loading branch information
brunchboy committed May 26, 2024
1 parent d869113 commit ce66c0f
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 43 deletions.
25 changes: 19 additions & 6 deletions src/main/java/org/deepsymmetry/beatlink/CdjStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ public enum PlayState2 {
* The player is stopped.
*/
STOPPED (0x7e),
/**
* We have seen the Opus Quad report this value when it is playing but the main status flag lies about that fact.
*/
OPUS_MOVING (0xfa),
/**
* We saw an unknown value, so we don’t know what it means.
*/
Expand Down Expand Up @@ -752,19 +756,28 @@ public double getEffectiveTempo() {
}

/**
* Was the CDJ playing a track when this update was sent?
* 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.
*
* @return true if the play flag was set, or, if this seems to be a non-nexus player, if <em>P<sub>1</sub></em>
* has a value corresponding to a playing state.
* and <em>P<sub>2</sub></em> have values corresponding to a playing state.
*/
@SuppressWarnings("WeakerAccess")
public boolean isPlaying() {
if (packetBytes.length >= 212) {
return (packetBytes[STATUS_FLAGS] & PLAYING_FLAG) > 0;
final boolean simpleResult = (packetBytes[STATUS_FLAGS] & PLAYING_FLAG) > 0;
if (!simpleResult && Util.isOpusQuad(deviceName)) {
// 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;
}
} else {
final PlayState1 state = getPlayState1();
return state == PlayState1.PLAYING || state == PlayState1.LOOPING ||
(state == PlayState1.SEARCHING && getPlayState2() == PlayState2.MOVING);
// 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 ||
(playState1 == PlayState1.SEARCHING && playState2 == PlayState2.MOVING);
}
}

Expand Down
30 changes: 14 additions & 16 deletions src/main/java/org/deepsymmetry/beatlink/MediaDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
import org.deepsymmetry.beatlink.data.ColorItem;
import org.deepsymmetry.beatlink.data.OpusProvider;
import org.deepsymmetry.beatlink.data.SlotReference;
import org.deepsymmetry.cratedigger.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
Expand Down Expand Up @@ -91,7 +90,7 @@ public ByteBuffer getRawBytes() {
* Contains the sizes we expect Media response packets to have so we can log a warning if we get an unusual
* one. We will then add the new size to the list so it only gets logged once per run.
*/
private static final Set<Integer> expectedMediaPacketSizes = new HashSet<Integer>(Collections.singletonList(0xc0));
private static final Set<Integer> expectedMediaPacketSizes = new HashSet<>(Collections.singletonList(0xc0));

/**
* The smallest packet size from which we can be constructed. Anything less than this and we are missing
Expand Down Expand Up @@ -122,12 +121,15 @@ private int getUTF16StringLength(byte[] source, int offset, int maxLength) {
}

/**
* Constructor to emulate an actual MediaDetails object from CDJs. This allows our Status packets
* to use OpusProvider to enrich the track data. OpusProvider must be started up to use this method.
* Constructor to emulate an actual MediaDetails object from CDJs. This allows our status packets
* to use {@link OpusProvider} to enrich the track data. {@link OpusProvider} must be started to use this method.
*
* @param slotReference Slot Reference to Emulate
* @param slotReference Slot Reference to which these media details belong
* @param mediaType Media Type to Emulate
* @param name Name of device
* @param name Name of the emulated media volume
* @param trackCount How many tracks are present in the media
* @param playlistCount How many playlists are present in the media
* @param lastModified When the media was last changed
*/
public MediaDetails(SlotReference slotReference, CdjStatus.TrackType mediaType, String name,
int trackCount, int playlistCount, long lastModified) {
Expand Down Expand Up @@ -200,15 +202,11 @@ public MediaDetails(byte[] packet, int packetLength) {
name = "rekordbox mobile";
creationDate = "";
} else {
try {
int mediaNameLength = getUTF16StringLength(packetCopy, 0x2c, 0x40);
int creationDateLength = getUTF16StringLength(packetCopy, 0x6c, 0x18);

name = new String(packetCopy, 0x2c, mediaNameLength, "UTF-16BE").trim();
creationDate = new String(packetCopy, 0x6c, creationDateLength, "UTF-16BE").trim();
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Java no longer supports UTF-16BE encoding?!", e);
}
int mediaNameLength = getUTF16StringLength(packetCopy, 0x2c, 0x40);
int creationDateLength = getUTF16StringLength(packetCopy, 0x6c, 0x18);

name = new String(packetCopy, 0x2c, mediaNameLength, StandardCharsets.UTF_16BE).trim();
creationDate = new String(packetCopy, 0x6c, creationDateLength, StandardCharsets.UTF_16BE).trim();
}

trackCount = (int) Util.bytesToNumber(packetCopy, 0xa6, 2);
Expand Down
35 changes: 14 additions & 21 deletions src/main/java/org/deepsymmetry/beatlink/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,18 +226,12 @@ public enum PacketType {
*/
public static final Map<Integer, Map<Byte, PacketType>> PACKET_TYPE_MAP;
static {
Map<Integer, Map<Byte, PacketType>> scratch = new HashMap<Integer, Map<Byte, PacketType>>();
Map<Integer, Map<Byte, PacketType>> scratch = new HashMap<>();
for (PacketType packetType : PacketType.values()) {
Map<Byte, PacketType> portMap = scratch.get(packetType.port);
if (portMap == null) {
portMap = new HashMap<Byte, PacketType>();
scratch.put(packetType.port, portMap);
}
Map<Byte, PacketType> portMap = scratch.computeIfAbsent(packetType.port, k -> new HashMap<>());
portMap.put(packetType.protocolValue, packetType);
}
for (Map.Entry<Integer, Map<Byte, PacketType>> entry : scratch.entrySet()) {
scratch.put(entry.getKey(), Collections.unmodifiableMap(entry.getValue()));
}
scratch.replaceAll((k, v) -> Collections.unmodifiableMap(v));
PACKET_TYPE_MAP = Collections.unmodifiableMap(scratch);
}

Expand Down Expand Up @@ -275,12 +269,12 @@ public static void setPayloadByte(byte[] payload, int address, byte value) {
/**
* Used to keep track of when we report seeing a packet to an unexpected port, so we only do that once.
*/
private static final Set<Integer> unknownPortsReported = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
private static final Set<Integer> unknownPortsReported = Collections.newSetFromMap(new ConcurrentHashMap<>());

/**
* Used to keep track of when we report seeing a packet of an unknown type reported to a port, so we do that only once.
*/
private static final Map<Integer, Set<Byte>> unknownPortTypesReported = new ConcurrentHashMap<Integer, Set<Byte>>();
private static final Map<Integer, Set<Byte>> unknownPortTypesReported = new ConcurrentHashMap<>();

/**
* Check to see whether a packet starts with the standard header bytes, followed by a known byte identifying it.
Expand Down Expand Up @@ -317,12 +311,9 @@ public static PacketType validateHeader(DatagramPacket packet, int port) {

final PacketType result = portMap.get(data[PACKET_TYPE_OFFSET]);
if (result == null) { // Warn about unrecognized type, once.
Set<Byte> typesReportedForPort = unknownPortTypesReported.get(port);
if (typesReportedForPort == null) { // First problem we have seen for this port, set up set for it.
typesReportedForPort = Collections.newSetFromMap(new ConcurrentHashMap<Byte, Boolean>());
unknownPortTypesReported.put(port, typesReportedForPort);
}
if (!typesReportedForPort.contains(data[PACKET_TYPE_OFFSET])) {
Set<Byte> typesReportedForPort = unknownPortTypesReported.computeIfAbsent(port, k -> Collections.newSetFromMap(new ConcurrentHashMap<>()));
// First problem we have seen for this port, set up set for it.
if (!typesReportedForPort.contains(data[PACKET_TYPE_OFFSET])) {
logger.warn("Do not know any Pro DJ Link packets received on port " + port + " with type " +
String.format("0x%02x", data[PACKET_TYPE_OFFSET]) + " (this will be reported only once).");
typesReportedForPort.add(data[PACKET_TYPE_OFFSET]);
Expand Down Expand Up @@ -541,13 +532,13 @@ public static int timeToHalfFrameRounded(long milliseconds) {
* one thread creates the file and another thinks it has already been downloaded and tries to
* parse the partial file.
*/
private static final Map<String, Object> namedLocks = new HashMap<String, Object>();
private static final Map<String, Object> namedLocks = new HashMap<>();

/**
* Counts the threads that are currently using a named lock, so we can know when it can be
* removed from the maps.
*/
private static final Map<String, Integer> namedLockUseCounts = new HashMap<String, Integer>();
private static final Map<String, Integer> namedLockUseCounts = new HashMap<>();

/**
* Obtain an object that can be synchronized against to provide exclusive access to a named resource,
Expand Down Expand Up @@ -1018,9 +1009,11 @@ public static boolean isOpusQuad(String deviceName){
}

/**
* Adjust the player numbers from the Opus-Quad so that they are 1-4 as expected.
* Adjust the player numbers from the Opus-Quad so that they are 1-4 as expected by users and the rest of the
* ecosystem.
*
* @return the proper value
* @param reportedPlayerNumber the device number actually reported by the Opus Quad in the range 9-12
* @return the logical value in the range 1-4 corresponding to the deck label
*/
public static int translateOpusPlayerNumbers(int reportedPlayerNumber) {
return reportedPlayerNumber & 7;
Expand Down

0 comments on commit ce66c0f

Please sign in to comment.