Skip to content

Commit

Permalink
Adding framework for VirtualRekordbox to use.
Browse files Browse the repository at this point in the history
  • Loading branch information
brunchboy committed Apr 30, 2024
1 parent b35ef92 commit 5b9e44f
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 35 deletions.
86 changes: 73 additions & 13 deletions src/main/java/org/deepsymmetry/beatlink/DeviceFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.net.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

Expand Down Expand Up @@ -51,13 +52,30 @@ 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
* @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 socket.get() != null;
return inOpusQuadCompatibilityMode() || socket.get() != null;
}

/**
Expand Down Expand Up @@ -93,9 +111,10 @@ 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.
* 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.
*/
private void expireDevices() {
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 All @@ -111,7 +130,7 @@ private void expireDevices() {
}

/**
* Record a device announcement in the devices map, so we know whe saw it.
* Record a device announcement in the devices map, so we know we saw it.
*
* @param announcement the announcement to be recorded
*/
Expand Down Expand Up @@ -172,18 +191,63 @@ public boolean isAddressIgnored(InetAddress address) {
}

/**
* Start listening for device announcements and keeping track of the DJ Link devices visible on the network.
* If already listening, has no effect.
* 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.
*
* @param announcement the device announcement that has been received.
*/
void processAnnouncement(DeviceAnnouncement announcement) {
final boolean foundNewDevice = isDeviceNew(announcement);
updateDevices(announcement);
if (foundNewDevice) {
deliverFoundAnnouncement(announcement);
}
}

/**
* <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
* for receiving.</p>
*
* <p>If already active, has no effect.</p>
*
* @throws SocketException if the socket to listen on port 50000 cannot be created
*/
public synchronized void start() throws SocketException {

if (!isRunning()) {
socket.set(new DatagramSocket(ANNOUNCEMENT_PORT));
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];
final DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
Thread receiver = new Thread(null, new Runnable() {
Expand Down Expand Up @@ -224,11 +288,7 @@ public void run() {
packet.getLength() + ".");
}
DeviceAnnouncement announcement = new DeviceAnnouncement(packet);
final boolean foundNewDevice = isDeviceNew(announcement);
updateDevices(announcement);
if (foundNewDevice) {
deliverFoundAnnouncement(announcement);
}
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.
Expand Down
112 changes: 90 additions & 22 deletions src/main/java/org/deepsymmetry/beatlink/VirtualCdj.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,30 @@ public class VirtualCdj extends LifecycleParticipant {
*/
private final AtomicReference<DatagramSocket> socket = new AtomicReference<DatagramSocket>();

/**
* 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 posing as a virtual CDJ and receiving device status updates.
*
* @return true if our socket is open, sending presence announcements, and receiving status packets
* @return true if our socket is open, sending presence announcements, and receiving status packets,
* or if we were started in a mode where we delegate most of our responsibility to VirtualRekordbox
*/
public boolean isRunning() {
return socket.get() != null && claimingNumber.get() == 0;
return inOpusQuadCompatibilityMode() || (socket.get() != null && claimingNumber.get() == 0);
}

/**
Expand Down Expand Up @@ -446,12 +463,15 @@ private DeviceUpdate buildUpdate(DatagramPacket packet) {


/**
* Process a device update once it has been received. Track it as the most recent update from its address,
* <p>Process a device update once it has been received. Track it as the most recent update from its address,
* and notify any registered listeners, including master listeners if it results in changes to tracked state,
* such as the current master player and tempo. Also handles the Baroque dance of handing off the tempo master
* role from or to another device.
* role from or to another device.</p>
*
* <p>This used to be a private method, but it was made package accessible as part of the effort to support
* Opus Quad hardware, so the VirtualRekordbox could proxy the status packets it receives through us.</p>
*/
private void processUpdate(DeviceUpdate update) {
void processUpdate(DeviceUpdate update) {
updates.put(DeviceReference.getDeviceReference(update), update);

// Keep track of the largest sync number we see.
Expand Down Expand Up @@ -922,6 +942,30 @@ private boolean createVirtualCdj() throws SocketException {
}

// Set up our buffer and packet to receive incoming messages.
final Thread receiver = createStatusReceiver();
receiver.start();

// Create the thread which announces our participation in the DJ Link network, to request update packets
Thread announcer = new Thread(null, new Runnable() {
@Override
public void run() {
while (isRunning()) {
sendAnnouncement(broadcastAddress.get());
}
}
}, "beat-link VirtualCdj announcement sender");
announcer.setDaemon(true);
announcer.start();
deliverLifecycleAnnouncement(logger, true);
return true;
}

/**
* Create a thread that will wait for and process status update packets sent to our socket.
*
* @return the thread
*/
private Thread createStatusReceiver() {
final byte[] buffer = new byte[512];
final DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

Expand Down Expand Up @@ -959,21 +1003,7 @@ public void run() {
}, "beat-link VirtualCdj status receiver");
receiver.setDaemon(true);
receiver.setPriority(Thread.MAX_PRIORITY);
receiver.start();

// Create the thread which announces our participation in the DJ Link network, to request update packets
Thread announcer = new Thread(null, new Runnable() {
@Override
public void run() {
while (isRunning()) {
sendAnnouncement(broadcastAddress.get());
}
}
}, "beat-link VirtualCdj announcement sender");
announcer.setDaemon(true);
announcer.start();
deliverLifecycleAnnouncement(logger, true);
return true;
return receiver;
}

/**
Expand Down Expand Up @@ -1014,16 +1044,46 @@ public void stopped(LifecycleParticipant sender) {
};

/**
* Start announcing ourselves and listening for status packets. If already active, has no effect. Requires the
* 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();
}
}
};

/**
* <p>In normal operation (with Pro DJ Link devices), start announcing ourselves and listening for status packets.
* 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 for receiving. Normal operation Requires the
* {@link DeviceFinder} to be active in order to find out how to communicate with other devices, so will start
* that if it is not already.
* that if it is not already.</p>
*
* <p>If already active, has no effect.</p>
*
* @return true if we found DJ Link devices and were able to create the {@code VirtualCdj}, or it was already running.
* @throws SocketException if the socket to listen on port 50002 cannot be created
*/
@SuppressWarnings("UnusedReturnValue")
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;
// }

// Set up so we know we have to shut down if the DeviceFinder shuts down.
DeviceFinder.getInstance().addLifecycleListener(deviceFinderLifecycleListener);

Expand Down Expand Up @@ -1077,6 +1137,11 @@ public synchronized boolean start(byte deviceNumber) throws SocketException {
*/
public synchronized void stop() {
if (isRunning()) {
if (inOpusQuadCompatibilityMode()) {
// We were just running in proxy mode, so nothing really needs shutting down.
proxyingForVirtualRekordbox.set(false);
return;
}
try {
setSendingStatus(false);
} catch (Throwable t) {
Expand Down Expand Up @@ -1978,6 +2043,9 @@ public synchronized void setSendingStatus(boolean send) throws IOException {

if (send) { // Start sending status packets.
ensureRunning();
if (proxyingForVirtualRekordbox.get()) {
throw new IllegalStateException("Cannot send status when in Opus Quad compatibility mode.");
}
if ((getDeviceNumber() < 1) || (getDeviceNumber() > 4)) {
throw new IllegalStateException("Can only send status when using a standard player number, 1 through 4.");
}
Expand Down

0 comments on commit 5b9e44f

Please sign in to comment.