Skip to content

Commit

Permalink
Refactoring towards client-friendly Opus Quad support
Browse files Browse the repository at this point in the history
  • Loading branch information
brunchboy committed May 10, 2024
1 parent 434b686 commit 8dcc6ec
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 227 deletions.
31 changes: 20 additions & 11 deletions src/main/java/org/deepsymmetry/beatlink/CdjStatus.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.deepsymmetry.beatlink;

import org.deepsymmetry.beatlink.data.OpusProvider;
import org.deepsymmetry.beatlink.data.SlotReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -132,7 +134,7 @@ public enum TrackSourceSlot {
public static final Map<Byte,TrackSourceSlot> TRACK_SOURCE_SLOT_MAP;

static {
Map<Byte,TrackSourceSlot> scratch = new HashMap<Byte, TrackSourceSlot>();
Map<Byte,TrackSourceSlot> scratch = new HashMap<>();
for (TrackSourceSlot slot : TrackSourceSlot.values()) {
scratch.put(slot.protocolValue, slot);
}
Expand Down Expand Up @@ -198,7 +200,7 @@ public enum TrackType {
public static final Map<Byte,TrackType> TRACK_TYPE_MAP;

static {
Map<Byte,TrackType> scratch = new HashMap<Byte, TrackType>();
Map<Byte,TrackType> scratch = new HashMap<>();
for (TrackType type : TrackType.values()) {
scratch.put(type.protocolValue, type);
}
Expand Down Expand Up @@ -308,7 +310,7 @@ public enum PlayState1 {
public static final Map<Byte,PlayState1> PLAY_STATE_1_MAP;

static {
Map<Byte,PlayState1> scratch = new HashMap<Byte, PlayState1>();
Map<Byte,PlayState1> scratch = new HashMap<>();
for (PlayState1 state : PlayState1.values()) {
scratch.put(state.protocolValue, state);
}
Expand Down Expand Up @@ -373,7 +375,7 @@ public enum PlayState2 {
public static final Map<Byte,PlayState2> PLAY_STATE_2_MAP;

static {
Map<Byte,PlayState2> scratch = new HashMap<Byte, PlayState2>();
Map<Byte,PlayState2> scratch = new HashMap<>();
for (PlayState2 state : PlayState2.values()) {
scratch.put(state.protocolValue, state);
}
Expand Down Expand Up @@ -445,7 +447,7 @@ public enum PlayState3 {
public static final Map<Byte,PlayState3> PLAY_STATE_3_MAP;

static {
Map<Byte,PlayState3> scratch = new HashMap<Byte, PlayState3>();
Map<Byte,PlayState3> scratch = new HashMap<>();
for (PlayState3 state : PlayState3.values()) {
scratch.put(state.protocolValue, state);
}
Expand Down Expand Up @@ -567,7 +569,7 @@ private PlayState3 findPlayState3() {
* Contains the sizes we expect CDJ status 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> expectedStatusPacketSizes = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
private static final Set<Integer> expectedStatusPacketSizes = Collections.newSetFromMap(new ConcurrentHashMap<>());
static {
expectedStatusPacketSizes.addAll(Arrays.asList(0xd0, 0xd4, 0x11c, 0x124));
}
Expand All @@ -578,7 +580,7 @@ private PlayState3 findPlayState3() {
* size, a comma, and the actual size), so we can report it just once and don’t fill up the log file with endless
* copies or the same warning.
*/
private static final Set<String> misreportedPacketSizes = Collections.newSetFromMap(new HashMap<String, Boolean>());
private static final Set<String> misreportedPacketSizes = Collections.newSetFromMap(new HashMap<>());

/**
* The smallest packet size from which we can be constructed. Anything less than this and we are missing
Expand Down Expand Up @@ -611,7 +613,7 @@ public CdjStatus(DatagramPacket packet) {
}

if (expectedStatusPacketSizes.add(packetBytes.length)) {
logger.warn("Processing CDJ Status packets with unexpected lengths " + packetBytes.length + ".");
logger.warn("Processing CDJ Status packets with unexpected lengths {}.", packetBytes.length);
}
trackType = findTrackType();
rekordboxId = (int)Util.bytesToNumber(packetBytes, 44, 4);
Expand All @@ -624,10 +626,17 @@ public CdjStatus(DatagramPacket packet) {
handingMasterToDevice = Util.unsign(packetBytes[MASTER_HAND_OFF]);

if (Util.isOpusQuad(deviceName)) {
trackSourcePlayer = Util.translateOpusPlayerNumbers(packetBytes[40]);
int sourcePlayer = Util.translateOpusPlayerNumbers(packetBytes[40]);
if (sourcePlayer != 0) {
final SlotReference matchedSourceSlot = VirtualRekordbox.getInstance().findMatchedTrackSourceSlotForPlayer(deviceNumber);
if (matchedSourceSlot != null) {
sourcePlayer = matchedSourceSlot.player;
}
}
trackSourcePlayer = sourcePlayer;
trackSourceSlot = TrackSourceSlot.USB_SLOT;
// Indicate that we have a metadata archive available for the USB slot:
packetBytes[111] = 0;
// Indicate whether we have a metadata archive available for the USB slot:
packetBytes[111] = (byte) (OpusProvider.getInstance().findArchive(deviceNumber) == null? 4 : 0);
} else {
trackSourcePlayer = packetBytes[40];
trackSourceSlot = findTrackSourceSlot();
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/deepsymmetry/beatlink/VirtualCdj.java
Original file line number Diff line number Diff line change
Expand Up @@ -1481,11 +1481,11 @@ public Set<MediaDetailsListener> getMediaDetailsListeners() {
}

/**
* Send a media details response to all registered listeners. Is also called from VirtualRekordbox in proxy mode.
* Send a media details response to all registered listeners, only public so that it can be called from {@link org.deepsymmetry.beatlink.data.OpusProvider} when archives are attached.
*
* @param details the response that has just arrived
*/
void deliverMediaDetailsUpdate(final MediaDetails details) {
public void deliverMediaDetailsUpdate(final MediaDetails details) {
for (MediaDetailsListener listener : getMediaDetailsListeners()) {
try {
listener.detailsAvailable(details);
Expand Down
88 changes: 39 additions & 49 deletions src/main/java/org/deepsymmetry/beatlink/VirtualRekordbox.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
package org.deepsymmetry.beatlink;

import org.deepsymmetry.beatlink.data.MetadataFinder;
import org.deepsymmetry.beatlink.data.OpusProvider;
import org.deepsymmetry.beatlink.data.SlotReference;
import org.deepsymmetry.cratedigger.Database;
import org.deepsymmetry.cratedigger.pdb.RekordboxAnlz;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.*;

import static org.deepsymmetry.beatlink.CdjStatus.TrackSourceSlot.SD_SLOT;
import static org.deepsymmetry.beatlink.CdjStatus.TrackSourceSlot.USB_SLOT;

/**
Expand Down Expand Up @@ -299,6 +294,7 @@ public static String getDeviceName() {

private static final byte[] deviceName = "rekordbox".getBytes();

// TODO this (and the arrays above) need JavaDoc.
public void requestPSSI() throws IOException{
if (DeviceFinder.getInstance().isRunning() && !DeviceFinder.getInstance().getCurrentDevices().isEmpty()) {
InetAddress address = DeviceFinder.getInstance().getCurrentDevices().iterator().next().getAddress();
Expand All @@ -308,6 +304,28 @@ public void requestPSSI() throws IOException{
}
}

/**
* Keeps track of the PSSI bytes we have received for each player.
*/
private final Map<Integer, ByteBuffer> playerSongStructures = new ConcurrentHashMap<>();

/**
* Keeps track of the source slots we've matched to metadata archives for each player.
*/
private final Map<Integer, SlotReference> playerTrackSourceSlots = new ConcurrentHashMap<>();

/**
* Given a player number (normalized to the range 1-4), returns the track source slot associated with the
* metadata archive that we have matched that player's track to, if any, so we can report it in a meaningful
* way in {@link CdjStatus} packets.
*
* @param player the player whose track we are interested in
* @return the Opus Quad USB slot that has a metadata archive mounted that matched that player's current track
*/
SlotReference findMatchedTrackSourceSlotForPlayer(int player) {
return playerTrackSourceSlots.get(player);
}

/**
* Given an update packet sent to us, create the appropriate object to describe it.
*
Expand Down Expand Up @@ -338,15 +356,14 @@ private DeviceUpdate buildUpdate(DatagramPacket packet) {
if (length >= CdjStatus.MINIMUM_PACKET_SIZE) {
CdjStatus status = new CdjStatus(packet);

// If player number is zero the deck does not have a song loaded, just return.
// 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) {
return null;
playerSongStructures.remove(status.getDeviceNumber());
playerTrackSourceSlots.remove(status.getDeviceNumber());
}

return status;
} else {
logger.warn("Ignoring too-short CDJ Status packet with length " + length + " (we need " + CdjStatus.MINIMUM_PACKET_SIZE +
" bytes).");
logger.warn("Ignoring too-short CDJ Status packet with length {} (we need " + CdjStatus.MINIMUM_PACKET_SIZE + " bytes).", length);
return null;
}

Expand All @@ -361,47 +378,20 @@ private DeviceUpdate buildUpdate(DatagramPacket packet) {
case OPUS_METADATA:
byte[] data = packet.getData();
// PSSI Data
if (data[0x25] == 10) {
if (data[0x25] == 10) { // TODO should this be a named constant?

int rekordboxId = (int) Util.bytesToNumber(data, 0x28, 4);
// If track is loaded and OpusProvider has attached media
if (rekordboxId != 0 && OpusProvider.getInstance().hasAttachedArchive()) {
final byte[] pssiFromOpus = Arrays.copyOfRange(data, 0x35, data.length);
final int rekordboxId = (int) Util.bytesToNumber(data, 0x28, 4);
// Record this song structure so that we can use it for matching tracks in CdjStatus packets.
if (rekordboxId != 0) {
final ByteBuffer pssiFromOpus = ByteBuffer.wrap(Arrays.copyOfRange(data, 0x35, data.length));
final int player = Util.translateOpusPlayerNumbers(data[0x21]);

OpusProvider.getInstance().handlePSSIMatching(rekordboxId, pssiFromOpus, player);

SlotReference slotRef = SlotReference.getSlotReference(player, USB_SLOT);

// If missing, fill in MediaDetails otherwise MetadataFinder won't forward us to OpusProvider.
// This will happen once per player on startup (or reconnect).
if (MetadataFinder.getInstance().getMediaDetailsFor(slotRef) == null) {
int trackCount = 0;
int playlistCount = 0;
long lastModified = 0;

OpusProvider.RekordboxUsbArchive archive = OpusProvider.getInstance().findArchive(slotRef);
if (archive != null) {
Database database = archive.getDatabase();
trackCount = database.trackIndex.size();
playlistCount = database.playlistIndex.size();
lastModified = database.sourceFile.lastModified();

}

MediaDetails details = new MediaDetails(slotRef,
CdjStatus.TrackType.REKORDBOX,
OpusProvider.opusName,
trackCount,
playlistCount,
lastModified
);

// Forward this to VirtualCdj where it will be sent to clients. This should only happen once
// per SlotReference
VirtualCdj.getInstance().deliverMediaDetailsUpdate(details);
playerSongStructures.put(player, pssiFromOpus);
// Also record the conceptual source slot that represents the USB slot from which this track seems to have been loaded
// TODO we need to check that the track was loaded from a player, and not rekordbox, as well, before trying to do this!
final int sourceSlot = OpusProvider.getInstance().findMatchingUsbSlotForTrack(rekordboxId, player, pssiFromOpus);
if (sourceSlot != 0) { // We found match, record it.
playerTrackSourceSlots.put(player, SlotReference.getSlotReference(sourceSlot, USB_SLOT));
}

}
}
return null;
Expand Down Expand Up @@ -912,7 +902,7 @@ private void sendAnnouncements() {

Thread.sleep(getAnnounceInterval());

requestPSSI();
requestPSSI(); // TODO Shouldn't we only do this when we detect a new track has been loaded?

} catch (Throwable t) {
logger.warn("Unable to send announcement packets, flushing DeviceFinder due to likely network change and shutting down.", t);
Expand Down
Loading

0 comments on commit 8dcc6ec

Please sign in to comment.