From 457554394511d0c3d06add21a9fdaaccc29629bb Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sun, 22 Oct 2023 23:16:56 -0500 Subject: [PATCH] Decode and display active loops reported by CDJ-3000s --- CHANGELOG.md | 3 ++ .../org/deepsymmetry/beatlink/CdjStatus.java | 54 ++++++++++++++++++- .../data/WaveformDetailComponent.java | 11 ++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66bb4550..56ed910d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java b/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java index f07541bd..c4cb121f 100644 --- a/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java +++ b/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java @@ -29,9 +29,9 @@ public class CdjStatus extends DeviceUpdate { * master role to anther device, labeled Mh in the * Packet Analysis document. * - * Normally it holds the value 0xff, but during a tempo master hand-off, it holds + *

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.

*/ public static final int MASTER_HAND_OFF = 0x9f; @@ -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 + diff --git a/src/main/java/org/deepsymmetry/beatlink/data/WaveformDetailComponent.java b/src/main/java/org/deepsymmetry/beatlink/data/WaveformDetailComponent.java index 2770156c..b9a47634 100644 --- a/src/main/java/org/deepsymmetry/beatlink/data/WaveformDetailComponent.java +++ b/src/main/java/org/deepsymmetry/beatlink/data/WaveformDetailComponent.java @@ -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)));