Skip to content

Commit

Permalink
Decode and display active loops reported by CDJ-3000s
Browse files Browse the repository at this point in the history
  • Loading branch information
brunchboy committed Oct 23, 2023
1 parent 194294c commit 4575543
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ This change log follows the conventions of
- Precise position packets sent by CDJ-3000s are processed and used to keep
track of the exact playback position of these players with much higher fidelity
than is possible for other players, even when they are not currently playing.
- Recognition of dynamic loop information sent by CDJ-3000s, including display
of such loops in the WaveformDetailComoponent. This means that even loops which
do not exist within the track metadata can be displayed on CDJ-3000s.
- A new utility function to calculate a raw pitch integer value given a
human-oriented pitch percentage value, because the new precise position
packets sent by the CDJ-3000 use the humane approach.
Expand Down
54 changes: 52 additions & 2 deletions src/main/java/org/deepsymmetry/beatlink/CdjStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ public class CdjStatus extends DeviceUpdate {
* master role to anther device, labeled <i>M<sub>h</sub></i> in the
* <a href="https://djl-analysis.deepsymmetry.org/djl-analysis/vcdj.html#cdj-status-packets">Packet Analysis document</a>.
*
* Normally it holds the value 0xff, but during a tempo master hand-off, it holds
* <p>Normally it holds the value 0xff, but during a tempo master hand-off, it holds
* the device number of the incoming tempo master, until that device asserts the master state, after which this
* device will stop doing so.
* device will stop doing so.</p>
*/
public static final int MASTER_HAND_OFF = 0x9f;

Expand Down Expand Up @@ -1085,6 +1085,56 @@ public long getPacketNumber() {
return Util.bytesToNumber(packetBytes, 200, 4);
}

/**
* Checks whether the packet is large enough (from a new enough player, such as the CDJ-3000) that it
* can report the player's currently active loop boundaries.
*
* @return {@code true} if the loop-related methods can ever return nonzero values
*/
public boolean canReportLooping() {
return packetBytes.length >= 0x1ca;
}

/**
* If there is an active loop (and {@link #canReportLooping()} is {@code true}), returns the number of beats
* in the loop.
*
* @return 0 if no loop is active (or can be reported), or the number of beats being looped over
*/
public int getActiveLoopBeats() {
if (canReportLooping()) {
return (int)Util.bytesToNumber(packetBytes, 0x1c8, 2);
}
return 0;
}

/**
* If there is an active loop (and {@link #canReportLooping()} is {@code true}), returns position within
* the track (in milliseconds) at which the loop begins. Because this can conceivably be zero even when a loop
* is active, code should check {@link #getLoopEnd()} to determine that loop information is being reported.
*
* @return 0 if no loop is active (or can be reported), or the millisecond time at which the loop starts
*/
public long getLoopStart() {
if (canReportLooping()) {
return Util.bytesToNumber(packetBytes, 0x1b6, 4) * 65536 / 1000;
}
return 0;
}

/**
* If there is an active loop (and {@link #canReportLooping()} is {@code true}), returns position within
* the track (in milliseconds) at which the loop ends.
*
* @return 0 if no loop is active (or can be reported), or the millisecond time at which the loop ends
*/
public long getLoopEnd() {
if (canReportLooping()) {
return Util.bytesToNumber(packetBytes, 0x1be, 4) * 65536 / 1000;
}
return 0;
}

@Override
public String toString() {
return "CdjStatus[device:" + deviceNumber + ", name:" + deviceName +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,17 @@ protected void paintComponent(Graphics g) {
}
}

// Also draw loop regions for any players that are actively playing loops.
g.setColor(LOOP_BACKGROUND);
for (final PlaybackState state : playbackStateMap.values()) {
final CdjStatus status = (CdjStatus) VirtualCdj.getInstance().getLatestStatusFor(state.player);
if (status.getLoopEnd() > 0) {
final int start = millisecondsToX(status.getLoopStart());
final int end = millisecondsToX(status.getLoopEnd());
g.fillRect(start, axis - maxHeight, end - start, maxHeight * 2);
}
}

int lastBeat = 0;
if (beatGrid.get() != null) { // Find what beat was represented by the column just before the first we draw.
lastBeat = beatGrid.get().findBeatAtTime(Util.halfFrameToTime(getSegmentForX(clipRect.x - 1)));
Expand Down

0 comments on commit 4575543

Please sign in to comment.