Skip to content

Commit

Permalink
Merge pull request #71 from cprepos/cprepos-virtual-rekordbox
Browse files Browse the repository at this point in the history
Start integrating virtual rekordbox
  • Loading branch information
brunchboy authored May 1, 2024
2 parents 5b9e44f + 9af44fb commit 5cdef76
Show file tree
Hide file tree
Showing 8 changed files with 2,911 additions and 65 deletions.
26 changes: 24 additions & 2 deletions src/main/java/org/deepsymmetry/beatlink/CdjStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,22 @@ private TrackSourceSlot findTrackSourceSlot() {
return result;
}

/**
* Determine the enum value corresponding to the track source slot found in the packet from an Opus-Quad.
*
* @return the proper value
*/
private TrackSourceSlot findOpusTrackSourceSlot() {
if (rekordboxId != 0) {
switch (packetBytes[41]){
case 0: return TrackSourceSlot.SD_SLOT;
case 3: return TrackSourceSlot.USB_SLOT;
}
}

return TrackSourceSlot.UNKNOWN;
}

/**
* Determine the enum value corresponding to the track type found in the packet.
*
Expand Down Expand Up @@ -613,8 +629,6 @@ public CdjStatus(DatagramPacket packet) {
if (expectedStatusPacketSizes.add(packetBytes.length)) {
logger.warn("Processing CDJ Status packets with unexpected lengths " + packetBytes.length + ".");
}
trackSourcePlayer = packetBytes[40];
trackSourceSlot = findTrackSourceSlot();
trackType = findTrackType();
rekordboxId = (int)Util.bytesToNumber(packetBytes, 44, 4);
pitch = (int)Util.bytesToNumber(packetBytes, 141, 3);
Expand All @@ -624,6 +638,14 @@ public CdjStatus(DatagramPacket packet) {
playState3 = findPlayState3();
firmwareVersion = new String(packetBytes, 124, 4).trim();
handingMasterToDevice = Util.unsign(packetBytes[MASTER_HAND_OFF]);

if (Util.isOpusQuad(deviceName)) {
trackSourcePlayer = translateOpusPlayerNumbers(packetBytes[40]);
trackSourceSlot = findOpusTrackSourceSlot();
} else {
trackSourcePlayer = packetBytes[40];
trackSourceSlot = findTrackSourceSlot();
}
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/deepsymmetry/beatlink/DeviceAnnouncement.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ public DeviceAnnouncement(DatagramPacket packet) {
number = Util.unsign(packetBytes[36]);
}

/**
* Constructor sets all the immutable interpreted fields based on the packet content.
* This Constructor allows you to inject Device Number.
*
* @param packet the device announcement packet that was received
* @param deviceNumber the device number you want to emulate
*/
public DeviceAnnouncement(DatagramPacket packet, int deviceNumber) {
if (packet.getLength() != 54) {
throw new IllegalArgumentException("Device announcement packet must be 54 bytes long");
}
address = packet.getAddress();
packetBytes = new byte[packet.getLength()];
System.arraycopy(packet.getData(), 0, packetBytes, 0, packet.getLength());
timestamp = System.currentTimeMillis();
name = new String(packetBytes, 12, 20).trim();
number = deviceNumber;
}

/**
* Get the address on which this device was seen.
*
Expand Down
94 changes: 38 additions & 56 deletions src/main/java/org/deepsymmetry/beatlink/DeviceFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,30 +52,14 @@ public class DeviceFinder extends LifecycleParticipant {
*/
private static final AtomicLong firstDeviceTime = new AtomicLong(0);

/**
* Indicates we were started with VirtualRekordbox running so we are just acting as a proxy for it, to
* work with the Opus Quad.
*/
private final AtomicBoolean proxyingForVirtualRekordbox = new AtomicBoolean(false);

/**
* Check whether we are simply proxying information from VirtualRekordbox so that we can work with the Opus
* Quad rather than real Pro DJ Link hardware.
*
* @return an indication that we are in a limited mode to support the Opus Quad.
*/
public boolean inOpusQuadCompatibilityMode() {
return proxyingForVirtualRekordbox.get();
}

/**
* Check whether we are presently listening for device announcements.
*
* @return {@code true} if our socket is open and monitoring for DJ Link device announcements on the network,
* or if we were started in a mode where we delegate most of our responsibility to VirtualRekordbox
*/
public boolean isRunning() {
return inOpusQuadCompatibilityMode() || socket.get() != null;
return socket.get() != null;
}

/**
Expand Down Expand Up @@ -111,10 +95,9 @@ public long getFirstDeviceTime() {
private final Map<DeviceReference, DeviceAnnouncement> devices = new ConcurrentHashMap<DeviceReference, DeviceAnnouncement>();

/**
* Remove any device announcements that are so old that the device seems to have gone away. Will be called by
* VirtualRekordbox when in Opus Quad compatibility mode.
* Remove any device announcements that are so old that the device seems to have gone away.
*/
void expireDevices() {
private void expireDevices() {
long now = System.currentTimeMillis();
// Make a copy so we don't have to worry about concurrent modification.
Map<DeviceReference, DeviceAnnouncement> copy = new HashMap<DeviceReference, DeviceAnnouncement>(devices);
Expand Down Expand Up @@ -191,41 +174,38 @@ public boolean isAddressIgnored(InetAddress address) {
}

/**
* Makes sure we get shut down if the VirtualRekordbox does when in proxy mode.
*/
private final LifecycleListener virtualRekordboxLifecycleListener = new LifecycleListener() {
@Override
public void started(LifecycleParticipant sender) {
logger.debug("Nothing to do when VirtualRekordbox starts.");
}

@Override
public void stopped(LifecycleParticipant sender) {
if (inOpusQuadCompatibilityMode()) {
logger.info("Shutting down because VirtualRekordbox is and we were proxying for it.");
stop();
}
}
};

/**
* Handle a device announcement packet we have received, or one that VirtualRekordbox has received if we
* are in Opus Quad compatibility mode.
* Handle a device announcement packet we have received.
*
* @param announcement the device announcement that has been received.
*/
void processAnnouncement(DeviceAnnouncement announcement) {
private void processAnnouncement(DeviceAnnouncement announcement) {
final boolean foundNewDevice = isDeviceNew(announcement);
updateDevices(announcement);
if (foundNewDevice) {
deliverFoundAnnouncement(announcement);
}
}

/**
* Handle a device announcement packet we have received from the Opus Quad.
*
* @param packet the packet from Opus Quad to infer the 4 players
*/
private void createAndProcessOpusAnnouncements(DatagramPacket packet) {
for (int i = 1; i <= 4; i++) {
DeviceAnnouncement opusAnnouncement = new DeviceAnnouncement(packet, i);
updateDevices(opusAnnouncement);

if (isDeviceNew(opusAnnouncement)) {
deliverFoundAnnouncement(opusAnnouncement);
}
}
}

/**
* <p>In normal operation (with Pro DJ Link devices), start listening for device announcements and keeping
* track of the DJ Link devices visible on the network. If VirtualRekordbox is running, then we are actually
* in Opus Quad compatibility mode, and will do far less,acting as a proxy for packets that it is responsible
* in Opus Quad compatibility mode, and will do far less, acting as a proxy for packets that it is responsible
* for receiving.</p>
*
* <p>If already active, has no effect.</p>
Expand All @@ -238,14 +218,6 @@ public synchronized void start() throws SocketException {
startTime.set(System.currentTimeMillis());
deliverLifecycleAnnouncement(logger, true);

// See if we are just going to proxy information for VirtualRekordbox.
// TODO uncomment once this exists.
// VirtualRekordbox.getInstance().addLifecycleListener(virtualRekordboxLifecycleListener);
// if (VirtualRekordbox.getInstance().isRunning()) {
// proxyingForVirtualRekordbox.set(true);
// return true;
// }

socket.set(new DatagramSocket(ANNOUNCEMENT_PORT));

final byte[] buffer = new byte[512];
Expand Down Expand Up @@ -287,12 +259,22 @@ public void run() {
logger.warn("Processing too-long " + kind.name + " packet; expected 54 bytes, but got " +
packet.getLength() + ".");
}

DeviceAnnouncement announcement = new DeviceAnnouncement(packet);
processAnnouncement(announcement);
if (VirtualCdj.getInstance().isRunning() &&
announcement.getDeviceNumber() == VirtualCdj.getInstance().getDeviceNumber()) {
// Someone is using the same device number as we are! Try to defend it.
VirtualCdj.getInstance().defendDeviceNumber(announcement.getAddress());

if (Util.isOpusQuad(announcement.getDeviceName())) {

createAndProcessOpusAnnouncements(packet);
} else {

processAnnouncement(announcement);


if (VirtualCdj.getInstance().isRunning() &&
announcement.getDeviceNumber() == VirtualCdj.getInstance().getDeviceNumber()) {
// Someone is using the same device number as we are! Try to defend it.
VirtualCdj.getInstance().defendDeviceNumber(announcement.getAddress());
}
}
}
} else if (kind == Util.PacketType.DEVICE_HELLO) {
Expand Down Expand Up @@ -496,7 +478,7 @@ private DeviceFinder() {

@Override
public String toString() {
StringBuilder sb = new StringBuilder() ;
StringBuilder sb = new StringBuilder();
sb.append("DeviceFinder[active:").append(isRunning());
if (isRunning()) {
sb.append(", startTime:").append(getStartTime()).append(", firstDeviceTime:").append(getFirstDeviceTime());
Expand Down
23 changes: 22 additions & 1 deletion src/main/java/org/deepsymmetry/beatlink/DeviceUpdate.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ public DeviceUpdate(DatagramPacket packet, String name, int length) {
System.arraycopy(packet.getData(), 0, packetBytes, 0, packet.getLength());
deviceName = new String(packetBytes, 11, 20).trim();
preNexusCdj = deviceName.startsWith("CDJ") && (deviceName.endsWith("900") || deviceName.endsWith("2000"));
deviceNumber = Util.unsign(packetBytes[33]);

if (Util.isOpusQuad(deviceName)){
deviceNumber = translateOpusPlayerNumbers(packetBytes[40]);
} else {
deviceNumber = Util.unsign(packetBytes[33]);
}
}

/**
Expand Down Expand Up @@ -196,6 +201,22 @@ public byte[] getPacketBytes() {
@SuppressWarnings("WeakerAccess")
public abstract boolean isBeatWithinBarMeaningful();

/**
* Adjust the player numbers from the Opus-Quad so that they are 1-4 as expected.
*
* @return the proper value
*/
int translateOpusPlayerNumbers(int reportedPlayerNumber) {
switch (reportedPlayerNumber){
case 9: return 1;
case 10: return 2;
case 11: return 3;
case 12: return 4;
}

return reportedPlayerNumber;
}

@Override
public String toString() {
return "DeviceUpdate[deviceNumber:" + deviceNumber +
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/org/deepsymmetry/beatlink/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ public enum PacketType {
*/
DEVICE_HELLO(0x0a, "Device Hello", DeviceFinder.ANNOUNCEMENT_PORT),

/**
* These are the bytes that all in one players like the Opus-Quad send to announce to Rekordbox Lighting that
* it is available on the network. Once VirtualRekordbox sends its hello message to the player, it stops sending
* these and starts sending CdjStatus updates.
*/
DEVICE_REKORDBOX_LIGHTING_HELLO_BYTES(0x10, "Rekordbox Lighting Hello Bytes", VirtualCdj.UPDATE_PORT),


/**
* A series of three of these is sent at 300ms intervals when a device is starting to establish its
* device number.
Expand Down Expand Up @@ -132,6 +140,13 @@ public enum PacketType {
*/
CDJ_STATUS(0x0a, "CDJ Status", VirtualCdj.UPDATE_PORT),

/**
* Metadata that includes track album art, possibly waveforms and more. We do not use this information at the moment
* because it is not complete enough to support all of the Beat Link Trigger functionality. Instead, we download
* the track data from a Rekordbox USB.
*/
OPUS_METADATA(0x56, "OPUS Metadata", VirtualCdj.UPDATE_PORT),

/**
* A command to load a particular track; usually sent by rekordbox.
*/
Expand Down Expand Up @@ -954,6 +969,9 @@ public static String highResolutionPath(String artPath) {
return artPath.replaceFirst("(\\.\\w+$)", "_m$1");
}

public static boolean isOpusQuad(String deviceName){
return deviceName.equals("OPUS-QUAD");
}

/**
* Prevent instantiation.
Expand Down
62 changes: 56 additions & 6 deletions src/main/java/org/deepsymmetry/beatlink/VirtualCdj.java
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,45 @@ public synchronized void setDeviceName(String name) {
System.arraycopy(name.getBytes(), 0, keepAliveBytes, DEVICE_NAME_OFFSET, name.getBytes().length);
}

/**
* Reacts to player status updates to reflect the current playback state.
*/
private final DeviceUpdateListener updateListener = new DeviceUpdateListener() {
@Override
public void received(DeviceUpdate update) {
processUpdate(update);
}
};

public DeviceUpdateListener getUpdateListener() {
return updateListener;
}


/**
* Reacts to player status updates to reflect the current playback state.
*/
private final MasterListener masterListener = new MasterListener() {
@Override
public void masterChanged(DeviceUpdate update) {
deliverMasterChangedAnnouncement(update);
}

@Override
public void tempoChanged(double tempo) {
deliverTempoChangedAnnouncement(tempo);
}

@Override
public void newBeat(Beat beat) {
deliverBeatAnnouncement(beat);
}
};

public MasterListener getMasterListener() {
return masterListener;
}

/**
* The initial packet sent three times when coming online.
*/
Expand Down Expand Up @@ -1077,12 +1116,11 @@ public void stopped(LifecycleParticipant sender) {
public synchronized boolean start() throws SocketException {
if (!isRunning()) {
// See if we are just going to proxy information for VirtualRekordbox.
// TODO uncomment once this exists.
// VirtualRekordbox.getInstance().addLifecycleListener(virtualRekordboxLifecycleListener);
// if (VirtualRekordbox.getInstance().isRunning()) {
// proxyingForVirtualRekordbox.set(true);
// return true;
// }
VirtualRekordbox.getInstance().addLifecycleListener(virtualRekordboxLifecycleListener);
if (VirtualRekordbox.getInstance().isRunning()) {
proxyingForVirtualRekordbox.set(true);
return true;
}

// Set up so we know we have to shut down if the DeviceFinder shuts down.
DeviceFinder.getInstance().addLifecycleListener(deviceFinderLifecycleListener);
Expand Down Expand Up @@ -1418,6 +1456,18 @@ private void deliverDeviceUpdate(final DeviceUpdate update) {
}
}

// This is a way to get mediaDetails from Rekordbox and pass to all MediaDetailsListeners
private final MediaDetailsListener mediaDetailsListener = new MediaDetailsListener(){
@Override
public void detailsAvailable(MediaDetails details) {
deliverMediaDetailsUpdate(details);
}
};

public MediaDetailsListener getMediaDetailsListener() {
return mediaDetailsListener;
}

/**
* Keeps track of the registered media details listeners.
*/
Expand Down
Loading

0 comments on commit 5cdef76

Please sign in to comment.