Skip to content

Commit

Permalink
Merge pull request #73 from cprepos/cprepos-virtual-rekordbox
Browse files Browse the repository at this point in the history
Cprepos virtual rekordbox
  • Loading branch information
brunchboy authored May 5, 2024
2 parents 50177fa + 4f53026 commit c5b46c8
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 163 deletions.
7 changes: 6 additions & 1 deletion src/main/java/org/deepsymmetry/beatlink/CdjStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,8 @@ private TrackSourceSlot findOpusTrackSourceSlot() {
if (rekordboxId != 0) {
switch (packetBytes[41]){
case 0: return TrackSourceSlot.SD_SLOT;
case 3: return TrackSourceSlot.USB_SLOT;
// TODO, figure out a way to identify more than one USB at a time
case 3: return TrackSourceSlot.SD_SLOT;
}
}

Expand Down Expand Up @@ -642,6 +643,10 @@ public CdjStatus(DatagramPacket packet) {
if (Util.isOpusQuad(deviceName)) {
trackSourcePlayer = translateOpusPlayerNumbers(packetBytes[40]);
trackSourceSlot = findOpusTrackSourceSlot();
// isLocalSdLoaded() == true
packetBytes[115] = 0;
// isLocalUsbLoaded() == true
packetBytes[111] = 0;
} else {
trackSourcePlayer = packetBytes[40];
trackSourceSlot = findTrackSourceSlot();
Expand Down
61 changes: 43 additions & 18 deletions src/main/java/org/deepsymmetry/beatlink/MediaDetails.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package org.deepsymmetry.beatlink;

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.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.nio.file.FileSystem;
import java.util.*;

/**
* Represents information about the media mounted in a player's slot; returned in response to a media query packet.
Expand Down Expand Up @@ -102,26 +104,51 @@ public ByteBuffer getRawBytes() {
/**
* Given a source byte array, return the UTF-16 string length in bytes by finding a UTF-16 NUL sequence.
* If a UTF-16 NUL sequence is not found, maxLength or source.length-offset is returned, which ever is smaller.
*
* @param source the source byte array representing a UTF-16 string
* @param offset the byte offset to start at in the source byte array
*
* @param source the source byte array representing a UTF-16 string
* @param offset the byte offset to start at in the source byte array
* @param maxLength the maximum length of the UTF-16 string (in bytes)
*
* @return the length in number of bytes excluding the UTF-16 NUL sequence, else maxLength or source.length-offset, which ever is smaller.
*/
private int getUTF16StringLength(byte[] source, int offset, int maxLength) {
int numBytes = maxLength;
int clampedMaxLength = Math.min(maxLength, source.length-offset);
for (int i=offset; i<offset+clampedMaxLength-1; i+=2) {
if (source[i] == 0x00 && source[i+1] == 0x00) {
numBytes = i-offset;
int clampedMaxLength = Math.min(maxLength, source.length - offset);

for (int i = offset; i < offset + clampedMaxLength - 1; i += 2) {
if (source[i] == 0x00 && source[i + 1] == 0x00) {
numBytes = i - offset;
break;
}
}
return numBytes;
}

/**
* 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.
*
* @param slotReference Slot Reference to Emulate
* @param mediaType Media Type to Emulate
* @param name Name of device
*/
MediaDetails(SlotReference slotReference, CdjStatus.TrackType mediaType, String name) {
if (!OpusProvider.getInstance().isRunning()) {
throw new IllegalStateException("OpusProvider must be started up to use this Constructor");
}
Database db = OpusProvider.getInstance().findDatabase(slotReference);
this.slotReference = slotReference;
this.mediaType = mediaType;
this.name = name;
this.creationDate = Long.toString(db.sourceFile.lastModified());
this.trackCount = db.trackIndex.size();
this.totalSize = 0;
this.playlistCount = db.playlistIndex.size();
this.rawBytes = ByteBuffer.wrap(new byte[]{});
this.color = new Color(0);
this.freeSpace = 0;
this.hasMySettings = false;
}

/**
* Constructor sets all the immutable interpreted fields based on the packet content.
*
Expand All @@ -134,7 +161,7 @@ private int getUTF16StringLength(byte[] source, int offset, int maxLength) {
/**
* Constructor sets all the immutable interpreted fields based on the packet content.
*
* @param packet the media response packet that was received or cached
* @param packet the media response packet that was received or cached
* @param packetLength the number of bytes within the packet which were actually received
*/
public MediaDetails(byte[] packet, int packetLength) {
Expand All @@ -147,7 +174,7 @@ public MediaDetails(byte[] packet, int packetLength) {
" bytes and were given only " + packetCopy.length);
}

final int payloadLength = (int)Util.bytesToNumber(packetCopy, 0x22, 2);
final int payloadLength = (int) Util.bytesToNumber(packetCopy, 0x22, 2);
if (packetCopy.length != payloadLength + 0x24) {
logger.warn("Received Media response packet with reported payload length of " + payloadLength + " and actual payload length of " +
(packetCopy.length - 0x24));
Expand Down Expand Up @@ -186,9 +213,9 @@ public MediaDetails(byte[] packet, int packetLength) {
}
}

trackCount = (int)Util.bytesToNumber(packetCopy, 0xa6, 2);
trackCount = (int) Util.bytesToNumber(packetCopy, 0xa6, 2);
color = ColorItem.colorForId(packetCopy[0xa8]);
playlistCount = (int)Util.bytesToNumber(packetCopy, 0xae, 2);
playlistCount = (int) Util.bytesToNumber(packetCopy, 0xae, 2);
hasMySettings = packetCopy[0xab] != 0;
totalSize = Util.bytesToNumber(packetCopy, 0xb0, 8);
freeSpace = Util.bytesToNumber(packetCopy, 0xb8, 8);
Expand Down Expand Up @@ -216,9 +243,7 @@ public String hashKey() {
* free space because those probably just reflect history entries being added.
*
* @param originalMedia the media details when information about it was saved
*
* @return true if there have been detectable significant changes to the media since it was saved
*
* @throws IllegalArgumentException if the {@link #hashKey()} values of the media detail objects differ
*/
public boolean hasChanged(MediaDetails originalMedia) {
Expand Down
26 changes: 24 additions & 2 deletions src/main/java/org/deepsymmetry/beatlink/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import java.awt.*;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.Collections;
Expand Down Expand Up @@ -959,6 +958,29 @@ public static String phraseLabel(final SongStructureEntry phrase) {
}
}

/**
* Scan a network interface to find if it has an address space which matches the device we are trying to reach.
* If so, return the address specification.
*
* @param announcement the DJ Link device we are trying to communicate with
* @param networkInterface the network interface we are testing
* @return the address which can be used to communicate with the device on the interface, or null
*/
public static InterfaceAddress findMatchingAddress(DeviceAnnouncement announcement, NetworkInterface networkInterface) {
for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
if (address == null) {
// This should never happen, but we are protecting against a Windows Java bug, see
// https://bugs.java.com/bugdatabase/view_bug?bug_id=8023649
logger.warn("Received a null InterfaceAddress from networkInterface.getInterfaceAddresses(), is this Windows? " +
"Do you have a VPN installed? Trying to recover by ignoring it.");
} else if ((address.getBroadcast() != null) &&
Util.sameNetwork(address.getNetworkPrefixLength(), announcement.getAddress(), address.getAddress())) {
return address;
}
}
return null;
}

/**
* Transforms an album art path to the version that will contain high resolution art, if that is available.
*
Expand Down
29 changes: 3 additions & 26 deletions src/main/java/org/deepsymmetry/beatlink/VirtualCdj.java
Original file line number Diff line number Diff line change
Expand Up @@ -563,29 +563,6 @@ void processBeat(Beat beat) {
}
}

/**
* Scan a network interface to find if it has an address space which matches the device we are trying to reach.
* If so, return the address specification.
*
* @param aDevice the DJ Link device we are trying to communicate with
* @param networkInterface the network interface we are testing
* @return the address which can be used to communicate with the device on the interface, or null
*/
private InterfaceAddress findMatchingAddress(DeviceAnnouncement aDevice, NetworkInterface networkInterface) {
for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
if (address == null) {
// This should never happen, but we are protecting against a Windows Java bug, see
// https://bugs.java.com/bugdatabase/view_bug?bug_id=8023649
logger.warn("Received a null InterfaceAddress from networkInterface.getInterfaceAddresses(), is this Windows? " +
"Do you have a VPN installed? Trying to recover by ignoring it.");
} else if ((address.getBroadcast() != null) &&
Util.sameNetwork(address.getNetworkPrefixLength(), aDevice.getAddress(), address.getAddress())) {
return address;
}
}
return null;
}

/**
* The number of milliseconds for which the {@link DeviceFinder} needs to have been watching the network in order
* for us to be confident we can choose a device number that will not conflict.
Expand Down Expand Up @@ -913,7 +890,7 @@ private boolean createVirtualCdj() throws SocketException {
matchedAddress = null;
DeviceAnnouncement aDevice = DeviceFinder.getInstance().getCurrentDevices().iterator().next();
for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
InterfaceAddress candidate = findMatchingAddress(aDevice, networkInterface);
InterfaceAddress candidate = Util.findMatchingAddress(aDevice, networkInterface);
if (candidate != null) {
if (matchedAddress == null) {
matchedAddress = candidate;
Expand Down Expand Up @@ -1503,11 +1480,11 @@ public Set<MediaDetailsListener> getMediaDetailsListeners() {
}

/**
* Send a media details response to all registered listeners.
* Send a media details response to all registered listeners. Is also called from VirtualRekordbox in proxy mode.
*
* @param details the response that has just arrived
*/
private void deliverMediaDetailsUpdate(final MediaDetails details) {
void deliverMediaDetailsUpdate(final MediaDetails details) {
for (MediaDetailsListener listener : getMediaDetailsListeners()) {
try {
listener.detailsAvailable(details);
Expand Down
Loading

0 comments on commit c5b46c8

Please sign in to comment.