diff --git a/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java b/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java index 87dcade..e55fe5e 100644 --- a/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java +++ b/src/main/java/org/deepsymmetry/beatlink/CdjStatus.java @@ -1,5 +1,6 @@ package org.deepsymmetry.beatlink; +import org.apiguardian.api.API; import org.deepsymmetry.beatlink.data.OpusProvider; import org.deepsymmetry.beatlink.data.SlotReference; import org.slf4j.Logger; @@ -14,7 +15,7 @@ * * @author James Elliott */ -@SuppressWarnings("WeakerAccess") +@API(status = API.Status.STABLE) public class CdjStatus extends DeviceUpdate { private static final Logger logger = LoggerFactory.getLogger(CdjStatus.class); @@ -23,7 +24,7 @@ public class CdjStatus extends DeviceUpdate { * The byte within the status packet which contains useful status information, labeled F in the * Packet Analysis document. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final int STATUS_FLAGS = 0x89; /** @@ -35,13 +36,14 @@ public class CdjStatus extends DeviceUpdate { * the device number of the incoming tempo master, until that device asserts the master state, after which this * device will stop doing so.

*/ + @API(status = API.Status.STABLE) public static final int MASTER_HAND_OFF = 0x9f; /** * The bit within the status flag that indicates the player has degraded to BPM Sync mode, as described in the * Packet Analysis document. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final int BPM_SYNC_FLAG = 0x02; /** @@ -50,27 +52,28 @@ public class CdjStatus extends DeviceUpdate { * A player is considered to be on the air when it is connected to a mixer channel that is not faded out. * Only Nexus mixers seem to support this capability. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final int ON_AIR_FLAG = 0x08; /** * The bit within the status flag that indicates the player is synced, as illustrated in the * Packet Analysis document. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final int SYNCED_FLAG = 0x10; /** * The bit within the status flag that indicates the player is the tempo master, as illustrated in * the Packet Analysis document. */ + @API(status = API.Status.STABLE) public static final int MASTER_FLAG = 0x20; /** * The bit within the status flag that indicates the player is playing, as illustrated in the * Packet Analysis document. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final int PLAYING_FLAG = 0x40; /** @@ -85,12 +88,14 @@ public class CdjStatus extends DeviceUpdate { * * @return the device number from which the current track was loaded */ + @API(status = API.Status.STABLE) public int getTrackSourcePlayer() { return trackSourcePlayer; } /** * The possible values describing from where the track was loaded, labeled Sr in * the Packet Analysis document. */ + @API(status = API.Status.STABLE) public enum TrackSourceSlot { /** * Nothing has been loaded. @@ -130,7 +135,7 @@ public enum TrackSourceSlot { /** * Allows a known track source slot value to be looked up based on the byte that was seen in a status update. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final Map TRACK_SOURCE_SLOT_MAP; static { @@ -159,6 +164,7 @@ public enum TrackSourceSlot { * The possible values describing the track type, labeled tr in * the Packet Analysis document. */ + @API(status = API.Status.STABLE) public enum TrackType { /** * No track has been loaded. @@ -185,7 +191,7 @@ public enum TrackType { /** * The value that represents this track type in a status update. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public final byte protocolValue; TrackType(int value) { @@ -196,7 +202,7 @@ public enum TrackType { /** * Allows a known track source type value to be looked up based on the byte that was seen in a status update. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final Map TRACK_TYPE_MAP; static { @@ -219,6 +225,7 @@ public enum TrackType { * * @return the type of track that is currently loaded */ + @API(status = API.Status.STABLE) public TrackType getTrackType() { return trackType; } /** @@ -235,13 +242,14 @@ public enum TrackType { * * @return the rekordbox database ID of the current track */ + @API(status = API.Status.STABLE) public int getRekordboxId() { return rekordboxId; } /** * The possible values of the first play state found in the packet, labeled P1 in * the Packet Analysis document. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public enum PlayState1 { /** * No track has been loaded. @@ -291,6 +299,7 @@ public enum PlayState1 { /** * The value that represents this play state in a status update. */ + @API(status = API.Status.STABLE) public final byte protocolValue; /** @@ -306,7 +315,7 @@ public enum PlayState1 { /** * Allows a known P1 value to be looked up based on the byte that was seen in a status update. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final Map PLAY_STATE_1_MAP; static { @@ -329,7 +338,7 @@ public enum PlayState1 { * * @return the first play state element */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public PlayState1 getPlayState1() { return playState1; } @@ -338,6 +347,7 @@ public PlayState1 getPlayState1() { * The possible values of the second play state found in the packet, labeled P2 in * the Packet Analysis document. */ + @API(status = API.Status.STABLE) public enum PlayState2 { /** * The player is moving through a track. @@ -359,7 +369,7 @@ public enum PlayState2 { /** * The value that represents this play state in a status update. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public final byte protocolValue; /** @@ -375,7 +385,7 @@ public enum PlayState2 { /** * Allows a known P2 value to be looked up based on the byte that was seen in a status update. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final Map PLAY_STATE_2_MAP; static { @@ -398,7 +408,7 @@ public enum PlayState2 { * * @return the second play state element */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public PlayState2 getPlayState2() { return playState2; } @@ -407,6 +417,7 @@ public PlayState2 getPlayState2() { * The possible values of the third play state found in the packet, labeled P3 in * the Packet Analysis document. */ + @API(status = API.Status.STABLE) public enum PlayState3 { /** * No track has been loaded. @@ -447,7 +458,7 @@ public enum PlayState3 { /** * Allows a known P3 value to be looked up based on the byte that was seen in a status update. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final Map PLAY_STATE_3_MAP; static { @@ -470,6 +481,7 @@ public enum PlayState3 { * * @return the third play state element */ + @API(status = API.Status.STABLE) public PlayState3 getPlayState3() { return playState3; } @@ -590,6 +602,7 @@ private PlayState3 findPlayState3() { * The smallest packet size from which we can be constructed. Anything less than this and we are missing * crucial information. */ + @API(status = API.Status.STABLE) public static final int MINIMUM_PACKET_SIZE = 0xcc; /** @@ -597,6 +610,7 @@ private PlayState3 findPlayState3() { * * @param packet the CDJ status packet that was received */ + @API(status = API.Status.STABLE) public CdjStatus(DatagramPacket packet) { super(packet, "CDJ status", packet.getLength()); @@ -629,7 +643,7 @@ public CdjStatus(DatagramPacket packet) { handingMasterToDevice = Util.unsign(packetBytes[MASTER_HAND_OFF]); final byte trackSourceByte = packetBytes[40]; - if (isFromOpusQuad() && (trackSourceByte < 16)) { + if (isFromOpusQuad && (trackSourceByte < 16)) { int sourcePlayer = Util.translateOpusPlayerNumbers(trackSourceByte); if (sourcePlayer != 0) { final SlotReference matchedSourceSlot = VirtualRekordbox.getInstance().findMatchedTrackSourceSlotForPlayer(deviceNumber); @@ -682,6 +696,7 @@ public int getPitch() { * @param number the subscript identifying the copy of the pitch information you are interested in * @return the specified raw device pitch information copy found in the update */ + @API(status = API.Status.STABLE) public int getPitch(int number) { switch (number) { case 1: return pitch; @@ -700,6 +715,7 @@ public int getPitch(int number) { * * @return the track BPM to two decimal places multiplied by 100 */ + @API(status = API.Status.STABLE) public int getBpm() { return bpm; } @@ -714,6 +730,7 @@ public int getBpm() { * * @return the beat number within the current measure of music */ + @API(status = API.Status.STABLE) public int getBeatWithinBar() { return packetBytes[166]; } @@ -761,11 +778,11 @@ public double getEffectiveTempo() { * @return true if the play flag was set, or, if this seems to be a non-nexus player, if P1 * and P2 have values corresponding to a playing state. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public boolean isPlaying() { if (packetBytes.length >= 212) { final boolean simpleResult = (packetBytes[STATUS_FLAGS] & PLAYING_FLAG) > 0; - if (!simpleResult && isFromOpusQuad()) { + if (!simpleResult && isFromOpusQuad) { // Sometimes the Opus Quad lies and reports that it is not playing in this flag, even though it actually is. // Try to recover from that. return playState1 == PlayState1.PLAYING || playState1 == PlayState1.LOOPING || @@ -787,7 +804,7 @@ public boolean isPlaying() { * * @return true if the bpm-sync flag was set */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public boolean isBpmOnlySynced() { return (packetBytes[STATUS_FLAGS] & BPM_SYNC_FLAG) > 0; } @@ -797,7 +814,6 @@ public boolean isBpmOnlySynced() { * * @return true if the sync flag was set */ - @SuppressWarnings("WeakerAccess") @Override public boolean isSynced() { return (packetBytes[STATUS_FLAGS] & SYNCED_FLAG) > 0; @@ -810,7 +826,7 @@ public boolean isSynced() { * * @return true if the on-air flag was set */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public boolean isOnAir() { return (packetBytes[STATUS_FLAGS] & ON_AIR_FLAG) > 0; } @@ -820,6 +836,7 @@ public boolean isOnAir() { * * @return true if there is a USB drive mounted locally */ + @API(status = API.Status.STABLE) public boolean isLocalUsbLoaded() { return (packetBytes[111] == 0); } @@ -829,6 +846,7 @@ public boolean isLocalUsbLoaded() { * * @return true if there is a local USB drive currently being unmounted */ + @API(status = API.Status.STABLE) public boolean isLocalUsbUnloading() { return (packetBytes[111] == 2); } @@ -838,6 +856,7 @@ public boolean isLocalUsbUnloading() { * * @return true if there is no local USB drive mounted */ + @API(status = API.Status.STABLE) public boolean isLocalUsbEmpty() { return (packetBytes[111] == 4); } @@ -847,6 +866,7 @@ public boolean isLocalUsbEmpty() { * * @return true if there is a Secure Digital card mounted locally */ + @API(status = API.Status.STABLE) public boolean isLocalSdLoaded() { return (packetBytes[115] == 0); } @@ -856,6 +876,7 @@ public boolean isLocalSdLoaded() { * * @return true if there is a local Secure Digital card currently being unmounted */ + @API(status = API.Status.STABLE) public boolean isLocalSdUnloading() { return (packetBytes[115] == 2); } @@ -865,6 +886,7 @@ public boolean isLocalSdUnloading() { * * @return true if there is no local Secure Digital card mounted */ + @API(status = API.Status.STABLE) public boolean isLocalSdEmpty() { return (packetBytes[115] == 4); } @@ -877,6 +899,7 @@ public boolean isLocalSdEmpty() { * * @return true if there is no disc mounted or the disc drive has powered off */ + @API(status = API.Status.STABLE) public boolean isDiscSlotEmpty() { return (packetBytes[0x37] != 0x1e) && (packetBytes[0x37] != 0x11); } @@ -887,6 +910,7 @@ public boolean isDiscSlotEmpty() { * * @return true if the disc drive has powered off */ + @API(status = API.Status.STABLE) public boolean isDiscSlotAsleep() { return (packetBytes[0x37] == 1); } @@ -900,6 +924,7 @@ public boolean isDiscSlotAsleep() { * * @return the number of tracks found on the mounted disc or loaded playlist/player menu, or zero if no disc is mounted nor is a playlist/menu in use */ + @API(status = API.Status.STABLE) public int getDiscTrackCount() { return (int)Util.bytesToNumber(packetBytes, 0x46, 2); } @@ -909,6 +934,7 @@ public int getDiscTrackCount() { * * @return true if a track has been loaded */ + @API(status = API.Status.STABLE) public boolean isTrackLoaded() { return playState1 != PlayState1.NO_TRACK; } @@ -918,6 +944,7 @@ public boolean isTrackLoaded() { * * @return true if a loop is being played */ + @API(status = API.Status.STABLE) public boolean isLooping() { return playState1 == PlayState1.LOOPING; } @@ -925,8 +952,9 @@ public boolean isLooping() { /** * Is the player currently paused? * - * @return true if the player is paused, whether or not at the cue point + * @return true if the player is paused, regardless of whether at the cue point */ + @API(status = API.Status.STABLE) public boolean isPaused() { return (playState1 == PlayState1.PAUSED) || (playState1 == PlayState1.CUED); } @@ -936,6 +964,7 @@ public boolean isPaused() { * * @return true if the player is paused at the cue point */ + @API(status = API.Status.STABLE) public boolean isCued() { return playState1 == PlayState1.CUED; } @@ -945,6 +974,7 @@ public boolean isCued() { * * @return true if the player is searching forwards or backwards */ + @API(status = API.Status.STABLE) public boolean isSearching() { return playState1 == PlayState1.SEARCHING; } @@ -954,6 +984,7 @@ public boolean isSearching() { * * @return true if playback stopped because a track ended */ + @API(status = API.Status.STABLE) public boolean isAtEnd() { return playState1 == PlayState1.ENDED; } @@ -963,6 +994,7 @@ public boolean isAtEnd() { * * @return true if forward playback is underway */ + @API(status = API.Status.STABLE) public boolean isPlayingForwards() { return (playState1 == PlayState1.PLAYING) && (playState3 != PlayState3.PAUSED_OR_REVERSE); } @@ -972,6 +1004,7 @@ public boolean isPlayingForwards() { * * @return true if reverse playback is underway */ + @API(status = API.Status.STABLE) public boolean isPlayingBackwards() { return (playState1 == PlayState1.PLAYING) && (playState3 == PlayState3.PAUSED_OR_REVERSE); } @@ -981,6 +1014,7 @@ public boolean isPlayingBackwards() { * * @return true if forward playback in vinyl mode is underway */ + @API(status = API.Status.STABLE) public boolean isPlayingVinylMode() { return playState3 == PlayState3.FORWARD_VINYL; } @@ -990,6 +1024,7 @@ public boolean isPlayingVinylMode() { * * @return true if forward playback in CDJ mode is underway */ + @API(status = API.Status.STABLE) public boolean isPlayingCdjMode() { return playState3 == PlayState3.FORWARD_CDJ; } @@ -999,6 +1034,7 @@ public boolean isPlayingCdjMode() { * * @return true if some player has USB, SD, or other media that can be linked to */ + @API(status = API.Status.STABLE) public boolean isLinkMediaAvailable() { return (packetBytes[117] != 0); } @@ -1008,7 +1044,7 @@ public boolean isLinkMediaAvailable() { * * @return true if the player is playing, searching, or loading a track */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public boolean isBusy() { return packetBytes[39] != 0; } @@ -1019,7 +1055,7 @@ public boolean isBusy() { * * @return the index of the current track */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public int getTrackNumber() { return (int)Util.bytesToNumber(packetBytes, 50, 2); } @@ -1030,6 +1066,7 @@ public int getTrackNumber() { * @return a number that becomes one greater than the value reported by any other player when a player gives up * its role as the tempo master. */ + @API(status = API.Status.STABLE) public int getSyncNumber() { return (int)Util.bytesToNumber(packetBytes, 0x84, 4); } @@ -1044,7 +1081,7 @@ public int getSyncNumber() { * * @return the number of the beat within the track that is currently being played, or -1 if unknown */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public int getBeatNumber() { long result = Util.bytesToNumber(packetBytes, 160, 4); if (result != 0xffffffffL) { @@ -1065,7 +1102,7 @@ public int getBeatNumber() { * @return the cue beat countdown, or 511 if no countdown is in effect * @see #formatCueCountdown() */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public int getCueCountdown() { return (int)Util.bytesToNumber(packetBytes, 164, 2); } @@ -1076,7 +1113,7 @@ public int getCueCountdown() { * @return the value that the CDJ would display to indicate the distance to the next cue * @see #getCueCountdown() */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public String formatCueCountdown() { int count = getCueCountdown(); @@ -1102,6 +1139,7 @@ public String formatCueCountdown() { * * @return the version of the firmware in the player */ + @API(status = API.Status.STABLE) public String getFirmwareVersion() { return firmwareVersion; } @@ -1111,6 +1149,7 @@ public String getFirmwareVersion() { * * @return the number of this packet */ + @API(status = API.Status.STABLE) public long getPacketNumber() { return Util.bytesToNumber(packetBytes, 200, 4); } @@ -1121,6 +1160,7 @@ public long getPacketNumber() { * * @return {@code true} if the loop-related methods can ever return nonzero values */ + @API(status = API.Status.STABLE) public boolean canReportLooping() { return packetBytes.length >= 0x1ca; } @@ -1131,6 +1171,7 @@ public boolean canReportLooping() { * * @return 0 if no loop is active (or can be reported), or the number of beats being looped over */ + @API(status = API.Status.STABLE) public int getActiveLoopBeats() { if (canReportLooping()) { return (int)Util.bytesToNumber(packetBytes, 0x1c8, 2); @@ -1145,6 +1186,7 @@ public int getActiveLoopBeats() { * * @return 0 if no loop is active (or can be reported), or the millisecond time at which the loop starts */ + @API(status = API.Status.STABLE) public long getLoopStart() { if (canReportLooping()) { return Util.bytesToNumber(packetBytes, 0x1b6, 4) * 65536 / 1000; @@ -1158,6 +1200,7 @@ public long getLoopStart() { * * @return 0 if no loop is active (or can be reported), or the millisecond time at which the loop ends */ + @API(status = API.Status.STABLE) public long getLoopEnd() { if (canReportLooping()) { return Util.bytesToNumber(packetBytes, 0x1be, 4) * 65536 / 1000; diff --git a/src/main/java/org/deepsymmetry/beatlink/DeviceAnnouncement.java b/src/main/java/org/deepsymmetry/beatlink/DeviceAnnouncement.java index 73d6296..abfc981 100644 --- a/src/main/java/org/deepsymmetry/beatlink/DeviceAnnouncement.java +++ b/src/main/java/org/deepsymmetry/beatlink/DeviceAnnouncement.java @@ -1,5 +1,6 @@ package org.deepsymmetry.beatlink; +import org.apiguardian.api.API; import org.deepsymmetry.beatlink.data.OpusProvider; import java.net.DatagramPacket; @@ -12,6 +13,7 @@ * * @author James Elliott */ +@API(status = API.Status.STABLE) public class DeviceAnnouncement { /** @@ -44,6 +46,7 @@ public class DeviceAnnouncement { * * @param packet the device announcement packet that was received */ + @API(status = API.Status.STABLE) public DeviceAnnouncement(DatagramPacket packet) { if (packet.getLength() != 54) { throw new IllegalArgumentException("Device announcement packet must be 54 bytes long"); @@ -52,7 +55,8 @@ public DeviceAnnouncement(DatagramPacket packet) { packetBytes = new byte[packet.getLength()]; System.arraycopy(packet.getData(), 0, packetBytes, 0, packet.getLength()); timestamp = System.currentTimeMillis(); - name = new String(packetBytes, 12, 20).trim().intern(); + name = new String(packetBytes, 12, 20).trim(); + isOpusQuad = name.equals(OpusProvider.OPUS_NAME); number = Util.unsign(packetBytes[36]); } @@ -63,6 +67,7 @@ public DeviceAnnouncement(DatagramPacket packet) { * @param packet the device announcement packet that was received * @param deviceNumber the device number you want to emulate */ + @API(status = API.Status.EXPERIMENTAL) public DeviceAnnouncement(DatagramPacket packet, int deviceNumber) { if (packet.getLength() != 54) { throw new IllegalArgumentException("Device announcement packet must be 54 bytes long"); @@ -71,7 +76,8 @@ public DeviceAnnouncement(DatagramPacket packet, int deviceNumber) { packetBytes = new byte[packet.getLength()]; System.arraycopy(packet.getData(), 0, packetBytes, 0, packet.getLength()); timestamp = System.currentTimeMillis(); - name = new String(packetBytes, 12, 20).trim().intern(); + name = new String(packetBytes, 12, 20).trim(); + isOpusQuad = name.equals(OpusProvider.OPUS_NAME); number = deviceNumber; } @@ -80,6 +86,7 @@ public DeviceAnnouncement(DatagramPacket packet, int deviceNumber) { * * @return the network address from which the device is communicating */ + @API(status = API.Status.STABLE) public InetAddress getAddress() { return address; } @@ -89,6 +96,7 @@ public InetAddress getAddress() { * * @return the millisecond timestamp at which we last received an announcement from this device */ + @API(status = API.Status.STABLE) public long getTimestamp() { return timestamp; } @@ -98,6 +106,7 @@ public long getTimestamp() { * * @return the device name */ + @API(status = API.Status.STABLE) public String getDeviceName() { return name; } @@ -107,6 +116,7 @@ public String getDeviceName() { * * @return the player number found in the device announcement packet */ + @API(status = API.Status.STABLE) public int getDeviceNumber() { return number; } @@ -118,6 +128,7 @@ public int getDeviceNumber() { * @deprecated use {@link #getDeviceName()} instead for consistency with the device update classes */ @Deprecated + @API(status = API.Status.DEPRECATED) public String getName() { return name; } @@ -129,6 +140,7 @@ public String getName() { * @deprecated use {@link #getDeviceNumber()} instead for consistency with the device update classes */ @Deprecated + @API(status = API.Status.DEPRECATED) public int getNumber() { return number; } @@ -138,6 +150,7 @@ public int getNumber() { * * @return the device's Ethernet address */ + @API(status = API.Status.STABLE) public byte[] getHardwareAddress() { byte[] result = new byte[6]; System.arraycopy(packetBytes, 38, result, 0, 6); @@ -149,6 +162,7 @@ public byte[] getHardwareAddress() { * * @return the data sent by the device to announce its presence on the network */ + @API(status = API.Status.STABLE) public byte[] getPacketBytes() { byte[] result = new byte[packetBytes.length]; System.arraycopy(packetBytes, 0, result, 0, packetBytes.length); @@ -157,15 +171,9 @@ public byte[] getPacketBytes() { /** * Check whether a device update came from an Opus Quad, which behaves very differently from true Pro DJ Link hardware. - * - * @return {@code true} when the device name reported in this update matches the one reported by the Opus Quad */ - public boolean isOpusQuad() { - //noinspection StringEquality - return name == OpusProvider.opusName; // Since strings are interned, can be compared this way. - } - - + @API(status = API.Status.EXPERIMENTAL) + public final boolean isOpusQuad; @Override public String toString() { diff --git a/src/main/java/org/deepsymmetry/beatlink/DeviceFinder.java b/src/main/java/org/deepsymmetry/beatlink/DeviceFinder.java index 1d4d154..7fdcd8a 100644 --- a/src/main/java/org/deepsymmetry/beatlink/DeviceFinder.java +++ b/src/main/java/org/deepsymmetry/beatlink/DeviceFinder.java @@ -257,7 +257,7 @@ public synchronized void start() throws SocketException { DeviceAnnouncement announcement = new DeviceAnnouncement(packet); - if (announcement.isOpusQuad()) { + if (announcement.isOpusQuad) { createAndProcessOpusAnnouncements(packet); } else { processAnnouncement(announcement); diff --git a/src/main/java/org/deepsymmetry/beatlink/DeviceUpdate.java b/src/main/java/org/deepsymmetry/beatlink/DeviceUpdate.java index deb6d98..6e19832 100644 --- a/src/main/java/org/deepsymmetry/beatlink/DeviceUpdate.java +++ b/src/main/java/org/deepsymmetry/beatlink/DeviceUpdate.java @@ -1,5 +1,6 @@ package org.deepsymmetry.beatlink; +import org.apiguardian.api.API; import org.deepsymmetry.beatlink.data.OpusProvider; import java.net.DatagramPacket; @@ -10,6 +11,7 @@ * * @author James Elliott */ +@API(status = API.Status.STABLE) public abstract class DeviceUpdate { /** @@ -20,7 +22,6 @@ public abstract class DeviceUpdate { /** * When this update was received. */ - @SuppressWarnings("WeakerAccess") final long timestamp; /** @@ -50,7 +51,7 @@ public abstract class DeviceUpdate { * @param name the type of packet that is being processed, in case a problem needs to be reported * @param length the expected length of the packet */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public DeviceUpdate(DatagramPacket packet, String name, int length) { timestamp = System.nanoTime(); if (packet.getLength() != length) { @@ -59,10 +60,11 @@ public DeviceUpdate(DatagramPacket packet, String name, int length) { address = packet.getAddress(); packetBytes = new byte[packet.getLength()]; System.arraycopy(packet.getData(), 0, packetBytes, 0, packet.getLength()); - deviceName = new String(packetBytes, 11, 20).trim().intern(); + deviceName = new String(packetBytes, 11, 20).trim(); + isFromOpusQuad = deviceName.equals(OpusProvider.OPUS_NAME); preNexusCdj = deviceName.startsWith("CDJ") && (deviceName.endsWith("900") || deviceName.endsWith("2000")); - if (isFromOpusQuad()) { + if (isFromOpusQuad) { deviceNumber = Util.translateOpusPlayerNumbers(packetBytes[40]); } else { deviceNumber = Util.unsign(packetBytes[33]); @@ -74,6 +76,7 @@ public DeviceUpdate(DatagramPacket packet, String name, int length) { * * @return the network address from which the update was sent */ + @API(status = API.Status.STABLE) public InetAddress getAddress() { return address; } @@ -83,6 +86,7 @@ public InetAddress getAddress() { * * @return the nanosecond timestamp at which we received this update */ + @API(status = API.Status.STABLE) public long getTimestamp() { return timestamp; } @@ -92,6 +96,7 @@ public long getTimestamp() { * * @return the device name */ + @API(status = API.Status.STABLE) public String getDeviceName() { return deviceName; } @@ -104,6 +109,7 @@ public String getDeviceName() { * * @return {@code true} if the device name starts with "CDJ" and ends with "0". */ + @API(status = API.Status.STABLE) public boolean isPreNexusCdj() { return preNexusCdj; } @@ -113,25 +119,23 @@ public boolean isPreNexusCdj() { * * @return the player number found in the update packet */ + @API(status = API.Status.STABLE) public int getDeviceNumber() { return deviceNumber; } /** - * Check whether a device update came from an Opus Quad, which behaves very differently from true Pro DJ Link hardware. - * - * @return {@code true} when the device name reported in this update matches the one reported by the Opus Quad + * Indicates whether this device update came from an Opus Quad, which behaves very differently from true Pro DJ Link hardware. */ - public boolean isFromOpusQuad() { - //noinspection StringEquality - return deviceName == OpusProvider.opusName; // Since strings are interned, can be compared this way. - } + @API(status = API.Status.EXPERIMENTAL) + public final boolean isFromOpusQuad; /** * Get the raw data bytes of the device update packet. * * @return the data sent by the device to update its status */ + @API(status = API.Status.STABLE) public byte[] getPacketBytes() { byte[] result = new byte[packetBytes.length]; System.arraycopy(packetBytes, 0, result, 0, packetBytes.length); @@ -210,7 +214,6 @@ public byte[] getPacketBytes() { * * @return true for status packets from players, false for status packets from mixers */ - @SuppressWarnings("WeakerAccess") public abstract boolean isBeatWithinBarMeaningful(); @Override diff --git a/src/main/java/org/deepsymmetry/beatlink/VirtualCdj.java b/src/main/java/org/deepsymmetry/beatlink/VirtualCdj.java index 34bbea8..58b3a85 100644 --- a/src/main/java/org/deepsymmetry/beatlink/VirtualCdj.java +++ b/src/main/java/org/deepsymmetry/beatlink/VirtualCdj.java @@ -10,6 +10,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import org.apiguardian.api.API; import org.deepsymmetry.beatlink.data.MetadataFinder; import org.deepsymmetry.beatlink.data.SlotReference; import org.deepsymmetry.electro.Metronome; @@ -27,7 +28,7 @@ * * @author James Elliott */ -@SuppressWarnings("WeakerAccess") +@API(status = API.Status.STABLE) public class VirtualCdj extends LifecycleParticipant { private static final Logger logger = LoggerFactory.getLogger(VirtualCdj.class); @@ -35,8 +36,13 @@ public class VirtualCdj extends LifecycleParticipant { /** * The port to which other devices will send status update messages. */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public static final int UPDATE_PORT = 50002; + + /** + * The position within a keep-alive packet at which the MAC address is stored. + */ + @API(status = API.Status.STABLE) public static final int MAC_ADDRESS_OFFSET = 38; /** @@ -56,6 +62,7 @@ public class VirtualCdj extends LifecycleParticipant { * * @return an indication that we are in a limited mode to support the Opus Quad. */ + @API(status = API.Status.EXPERIMENTAL) public boolean inOpusQuadCompatibilityMode() { return proxyingForVirtualRekordbox.get(); } @@ -66,6 +73,7 @@ public boolean inOpusQuadCompatibilityMode() { * @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 {@link VirtualRekordbox} */ + @API(status = API.Status.STABLE) public boolean isRunning() { return inOpusQuadCompatibilityMode() || (socket.get() != null && claimingNumber.get() == 0); } @@ -76,6 +84,7 @@ public boolean isRunning() { * @return the local address we present to the DJ Link network * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public InetAddress getLocalAddress() { ensureRunning(); return socket.get().getLocalAddress(); @@ -94,6 +103,7 @@ public InetAddress getLocalAddress() { * @return the address on which packets can be broadcast to the other DJ Link devices * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public InetAddress getBroadcastAddress() { ensureRunning(); return broadcastAddress.get(); @@ -119,6 +129,7 @@ public InetAddress getBroadcastAddress() { * * @param attempt true if self-assignment should try to use device numbers below 5 when available */ + @API(status = API.Status.STABLE) public void setUseStandardPlayerNumber(boolean attempt) { useStandardPlayerNumber.set(attempt); } @@ -133,6 +144,7 @@ public void setUseStandardPlayerNumber(boolean attempt) { * * @return true if self-assignment should try to use device numbers below 5 when available */ + @API(status = API.Status.STABLE) public boolean getUseStandardPlayerNumber() { return useStandardPlayerNumber.get(); } @@ -149,6 +161,7 @@ public boolean getUseStandardPlayerNumber() { * * @return the virtual player number */ + @API(status = API.Status.STABLE) public synchronized byte getDeviceNumber() { return keepAliveBytes[DEVICE_NUMBER_OFFSET]; } @@ -170,7 +183,7 @@ public synchronized byte getDeviceNumber() { * @param number the virtual player number * @throws IllegalStateException if we are currently running */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public synchronized void setDeviceNumber(byte number) { if (isRunning()) { throw new IllegalStateException("Can't change device number once started."); @@ -189,6 +202,7 @@ public synchronized void setDeviceNumber(byte number) { * * @return the announcement interval */ + @API(status = API.Status.STABLE) public int getAnnounceInterval() { return announceInterval.get(); } @@ -200,6 +214,7 @@ public int getAnnounceInterval() { * @param interval the announcement interval * @throws IllegalArgumentException if interval is not between 200 and 2000 */ + @API(status = API.Status.STABLE) public void setAnnounceInterval(int interval) { if (interval < 200 || interval > 2000) { throw new IllegalArgumentException("Interval must be between 200 and 2000"); @@ -223,16 +238,19 @@ public void setAnnounceInterval(int interval) { /** * The location of the device name in the announcement packet. */ + @API(status = API.Status.STABLE) public static final int DEVICE_NAME_OFFSET = 0x0c; /** * The length of the device name in the announcement packet. */ + @API(status = API.Status.STABLE) public static final int DEVICE_NAME_LENGTH = 0x14; /** * The location of the device number in the announcement packet. */ + @API(status = API.Status.STABLE) public static final int DEVICE_NUMBER_OFFSET = 0x24; /** @@ -240,6 +258,7 @@ public void setAnnounceInterval(int interval) { * * @return the device name reported in our presence announcement packets */ + @API(status = API.Status.STABLE) public static String getDeviceName() { return new String(keepAliveBytes, DEVICE_NAME_OFFSET, DEVICE_NAME_LENGTH).trim(); } @@ -250,6 +269,7 @@ public static String getDeviceName() { * * @param name the device name to report in our presence announcement packets. */ + @API(status = API.Status.STABLE) public synchronized void setDeviceName(String name) { if (name.getBytes().length > DEVICE_NAME_LENGTH) { throw new IllegalArgumentException("name cannot be more than " + DEVICE_NAME_LENGTH + " bytes long"); @@ -263,6 +283,7 @@ public synchronized void setDeviceName(String name) { */ private final DeviceUpdateListener updateListener = this::processUpdate; + @API(status = API.Status.STABLE) public DeviceUpdateListener getUpdateListener() { return updateListener; } @@ -338,6 +359,7 @@ public DeviceUpdateListener getUpdateListener() { * @return the most recent update from a device which reported itself as the master * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public DeviceUpdate getTempoMaster() { ensureRunning(); return tempoMaster.get(); @@ -368,6 +390,7 @@ private void setTempoMaster(DeviceUpdate newMaster) { * * @return the BPM fraction that will trigger a tempo change update */ + @API(status = API.Status.STABLE) public double getTempoEpsilon() { return Double.longBitsToDouble(tempoEpsilon.get()); } @@ -377,6 +400,7 @@ public double getTempoEpsilon() { * * @param epsilon the BPM fraction that will trigger a tempo change update */ + @API(status = API.Status.STABLE) public void setTempoEpsilon(double epsilon) { tempoEpsilon.set(Double.doubleToLongBits(epsilon)); } @@ -392,6 +416,7 @@ public void setTempoEpsilon(double epsilon) { * @return the most recently reported master tempo * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public double getMasterTempo() { ensureRunning(); return Double.longBitsToDouble(masterTempo.get()); @@ -629,6 +654,7 @@ private boolean selfAssignDeviceNumber() { * @return the list of network interfaces on which we might receive player packets * @throws IllegalStateException if we are not running */ + @API(status = API.Status.STABLE) public List getMatchingInterfaces() { if (proxyingForVirtualRekordbox.get()) { return VirtualRekordbox.getInstance().getMatchingInterfaces(); @@ -988,6 +1014,7 @@ private Thread createStatusReceiver() { * @return the device announcements of any players which are on unreachable networks, or hopefully an empty list * @throws IllegalStateException if we are not running */ + @API(status = API.Status.STABLE) public Set findUnreachablePlayers() { ensureRunning(); Set result = new HashSet<>(); @@ -1047,7 +1074,7 @@ public void stopped(LifecycleParticipant sender) { * @return true if we found DJ Link devices and were able to create the {@code VirtualCdj}, or it was already running. * @throws Exception if the socket to listen on port 50002 cannot be created or there is some other problem starting. */ - @SuppressWarnings("UnusedReturnValue") + @API(status = API.Status.STABLE) public synchronized boolean start() throws Exception { if (!isRunning()) { @@ -1073,7 +1100,7 @@ public synchronized boolean start() throws Exception { // See if there is an Opus Quad on the network, which means we need to be in the limited compatibility mode. for (DeviceAnnouncement device : DeviceFinder.getInstance().getCurrentDevices()) { - if (device.isOpusQuad()) { + if (device.isOpusQuad) { proxyingForVirtualRekordbox.set(true); VirtualRekordbox.getInstance().addLifecycleListener(virtualRekordboxLifecycleListener); final boolean success = VirtualRekordbox.getInstance().start(); @@ -1107,6 +1134,7 @@ public synchronized boolean start() throws Exception { * @return true if we found DJ Link devices and were able to create the {@code VirtualCdj}, or it was already running. * @throws Exception if the socket to listen on port 50002 cannot be created or there is some other problem starting. */ + @API(status = API.Status.STABLE) public synchronized boolean start(byte deviceNumber) throws Exception { if (!isRunning()) { setDeviceNumber(deviceNumber); @@ -1118,6 +1146,7 @@ public synchronized boolean start(byte deviceNumber) throws Exception { /** * Stop announcing ourselves and listening for status updates. */ + @API(status = API.Status.STABLE) public synchronized void stop() { if (isRunning()) { broadcastAddress.set(null); @@ -1172,6 +1201,7 @@ private void sendAnnouncement(InetAddress broadcastAddress) { * @return the most recent detailed status update received for all active devices * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public Set getLatestStatus() { ensureRunning(); Set result = new HashSet<>(); @@ -1198,6 +1228,7 @@ public Set getLatestStatus() { * @return the most recent detailed status update received for that device * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public DeviceUpdate getLatestStatusFor(DeviceUpdate device) { ensureRunning(); return updates.get(DeviceReference.getDeviceReference(device)); @@ -1216,6 +1247,7 @@ public DeviceUpdate getLatestStatusFor(DeviceUpdate device) { * @return the most recent detailed status update received for that device * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public DeviceUpdate getLatestStatusFor(DeviceAnnouncement device) { ensureRunning(); return updates.get(DeviceReference.getDeviceReference(device)); @@ -1234,6 +1266,7 @@ public DeviceUpdate getLatestStatusFor(DeviceAnnouncement device) { * @return the matching detailed status update or null if none have been received * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public DeviceUpdate getLatestStatusFor(int deviceNumber) { ensureRunning(); for (DeviceUpdate update : updates.values()) { @@ -1266,6 +1299,7 @@ public DeviceUpdate getLatestStatusFor(int deviceNumber) { * * @param listener the master listener to add */ + @API(status = API.Status.STABLE) public void addMasterListener(MasterListener listener) { if (listener != null) { masterListeners.add(listener); @@ -1279,6 +1313,7 @@ public void addMasterListener(MasterListener listener) { * * @param listener the master listener to remove */ + @API(status = API.Status.STABLE) public void removeMasterListener(MasterListener listener) { if (listener != null) { masterListeners.remove(listener); @@ -1290,10 +1325,10 @@ public void removeMasterListener(MasterListener listener) { * * @return the currently registered tempo master listeners */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public Set getMasterListeners() { // Make a copy so callers get an immutable snapshot of the current state. - return Collections.unmodifiableSet(new HashSet<>(masterListeners)); + return Set.copyOf(masterListeners); } /** @@ -1363,7 +1398,7 @@ private void deliverBeatAnnouncement(final Beat beat) { * * @param listener the device update listener to add */ - @SuppressWarnings("SameParameterValue") + @API(status = API.Status.STABLE) public void addUpdateListener(DeviceUpdateListener listener) { if (listener != null) { updateListeners.add(listener); @@ -1377,6 +1412,7 @@ public void addUpdateListener(DeviceUpdateListener listener) { * * @param listener the device update listener to remove */ + @API(status = API.Status.STABLE) public void removeUpdateListener(DeviceUpdateListener listener) { if (listener != null) { updateListeners.remove(listener); @@ -1388,9 +1424,10 @@ public void removeUpdateListener(DeviceUpdateListener listener) { * * @return the currently registered update listeners */ + @API(status = API.Status.STABLE) public Set getUpdateListeners() { // Make a copy so callers get an immutable snapshot of the current state. - return Collections.unmodifiableSet(new HashSet<>(updateListeners)); + return Set.copyOf(updateListeners); } /** @@ -1430,6 +1467,7 @@ private void deliverDeviceUpdate(final DeviceUpdate update) { * * @param listener the media details listener to add */ + @API(status = API.Status.STABLE) public void addMediaDetailsListener(MediaDetailsListener listener) { if (listener != null) { detailsListeners.add(listener); @@ -1443,6 +1481,7 @@ public void addMediaDetailsListener(MediaDetailsListener listener) { * * @param listener the media details listener to remove */ + @API(status = API.Status.STABLE) public void removeMediaDetailsListener(MediaDetailsListener listener) { if (listener != null) { detailsListeners.remove(listener); @@ -1454,9 +1493,10 @@ public void removeMediaDetailsListener(MediaDetailsListener listener) { * * @return the currently registered details listeners */ + @API(status = API.Status.STABLE) public Set getMediaDetailsListeners() { // Make a copy so callers get an immutable snapshot of the current state. - return Collections.unmodifiableSet(new HashSet<>(detailsListeners)); + return Set.copyOf(detailsListeners); } /** @@ -1464,6 +1504,7 @@ public Set getMediaDetailsListeners() { * * @param details the response that has just arrived */ + @API(status = API.Status.STABLE) public void deliverMediaDetailsUpdate(final MediaDetails details) { for (MediaDetailsListener listener : getMediaDetailsListeners()) { try { @@ -1484,7 +1525,6 @@ public void deliverMediaDetailsUpdate(final MediaDetails details) { * * @throws IOException if there is a problem sending the packet */ - @SuppressWarnings("SameParameterValue") private void assembleAndSendPacket(Util.PacketType kind, byte[] payload, InetAddress destination, int port) throws IOException { DatagramPacket packet = Util.buildPacket(kind, ByteBuffer.wrap(keepAliveBytes, DEVICE_NAME_OFFSET, DEVICE_NAME_LENGTH).asReadOnlyBuffer(), @@ -1508,6 +1548,7 @@ private void assembleAndSendPacket(Util.PacketType kind, byte[] payload, InetAdd * * @throws IOException if there is a problem sending the request. */ + @API(status = API.Status.STABLE) public void sendMediaQuery(SlotReference slot) throws IOException { final DeviceAnnouncement announcement = DeviceFinder.getInstance().getLatestAnnouncementFrom(slot.player); if (announcement == null) { @@ -1558,6 +1599,7 @@ private void sendSyncControlCommand(DeviceUpdate target, byte command) throws IO * @throws IllegalStateException if the {@code VirtualCdj} is not active * @throws IllegalArgumentException if {@code deviceNumber} is not found on the network */ + @API(status = API.Status.STABLE) public void sendSyncModeCommand(int deviceNumber, boolean synced) throws IOException { final DeviceUpdate update = getLatestStatusFor(deviceNumber); if (update == null) { @@ -1576,6 +1618,7 @@ public void sendSyncModeCommand(int deviceNumber, boolean synced) throws IOExcep * @throws IllegalStateException if the {@code VirtualCdj} is not active * @throws NullPointerException if {@code update} is {@code null} */ + @API(status = API.Status.STABLE) public void sendSyncModeCommand(DeviceUpdate target, boolean synced) throws IOException { sendSyncControlCommand(target, synced? (byte)0x10 : (byte)0x20); } @@ -1589,6 +1632,7 @@ public void sendSyncModeCommand(DeviceUpdate target, boolean synced) throws IOEx * @throws IllegalStateException if the {@code VirtualCdj} is not active * @throws IllegalArgumentException if {@code deviceNumber} is not found on the network */ + @API(status = API.Status.STABLE) public void appointTempoMaster(int deviceNumber) throws IOException { final DeviceUpdate update = getLatestStatusFor(deviceNumber); if (update == null) { @@ -1606,6 +1650,7 @@ public void appointTempoMaster(int deviceNumber) throws IOException { * @throws IllegalStateException if the {@code VirtualCdj} is not active * @throws NullPointerException if {@code update} is {@code null} */ + @API(status = API.Status.STABLE) public void appointTempoMaster(DeviceUpdate target) throws IOException { sendSyncControlCommand(target, (byte)0x01); } @@ -1626,6 +1671,7 @@ public void appointTempoMaster(DeviceUpdate target) throws IOException { * @throws IOException if there is a problem broadcasting the command to the players * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public void sendFaderStartCommand(Set deviceNumbersToStart, Set deviceNumbersToStop) throws IOException { ensureRunning(); byte[] payload = new byte[FADER_START_PAYLOAD.length]; @@ -1662,6 +1708,7 @@ public void sendFaderStartCommand(Set deviceNumbersToStart, Set deviceNumbersOnAir) throws IOException { ensureRunning(); byte[] payload = new byte[CHANNELS_ON_AIR_PAYLOAD.length]; @@ -1696,6 +1743,7 @@ public void sendOnAirCommand(Set deviceNumbersOnAir) throws IOException * @throws IOException if there is a problem broadcasting the command to the players * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public void sendOnAirExtendedCommand(Set deviceNumbersOnAir) throws IOException { ensureRunning(); byte[] payload = new byte[CHANNELS_ON_AIR_EXTENDED_PAYLOAD.length]; @@ -1739,6 +1787,7 @@ public void sendOnAirExtendedCommand(Set deviceNumbersOnAir) throws IOE * @throws IOException if there is a problem sending the command * @throws IllegalStateException if the {@code VirtualCdj} is not active or the target device cannot be found */ + @API(status = API.Status.STABLE) public void sendLoadTrackCommand(int targetPlayer, int rekordboxId, int sourcePlayer, CdjStatus.TrackSourceSlot sourceSlot, CdjStatus.TrackType sourceType) throws IOException { @@ -1761,6 +1810,7 @@ public void sendLoadTrackCommand(int targetPlayer, int rekordboxId, * @throws IOException if there is a problem sending the command * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public void sendLoadTrackCommand(DeviceUpdate target, int rekordboxId, int sourcePlayer, CdjStatus.TrackSourceSlot sourceSlot, CdjStatus.TrackType sourceType) throws IOException { @@ -1829,6 +1879,7 @@ public void sendLoadTrackCommand(DeviceUpdate target, int rekordboxId, * @throws IOException if there is a problem sending the command * @throws IllegalStateException if the {@code VirtualCdj} is not active or the target device cannot be found */ + @API(status = API.Status.STABLE) public void sendLoadSettingsCommand(int targetPlayer, PlayerSettings settings) throws IOException { final DeviceUpdate update = getLatestStatusFor(targetPlayer); @@ -1847,6 +1898,7 @@ public void sendLoadSettingsCommand(int targetPlayer, PlayerSettings settings) * @throws IOException if there is a problem sending the command * @throws IllegalStateException if the {@code VirtualCdj} is not active */ + @API(status = API.Status.STABLE) public void sendLoadSettingsCommand(DeviceUpdate target, PlayerSettings settings) throws IOException { ensureRunning(); byte[] payload = new byte[LOAD_SETTINGS_PAYLOAD.length]; @@ -1892,6 +1944,7 @@ public void sendLoadSettingsCommand(DeviceUpdate target, PlayerSettings settings * * @return the millisecond interval that will pass between status packets we send */ + @API(status = API.Status.STABLE) public synchronized int getStatusInterval() { return statusInterval; } @@ -1905,6 +1958,7 @@ public synchronized int getStatusInterval() { * * @throws IllegalArgumentException if {@code interval} is less than 20 or more than 2000 */ + @API(status = API.Status.STABLE) public synchronized void setStatusInterval(int interval) { if (interval < 20 || interval > 2000) { throw new IllegalArgumentException("interval must be between 20 and 2000"); @@ -1966,6 +2020,7 @@ private void notifyBeatSenderOfChange() { * * @return the beat number that was sent, computed from the current (or stopped) playback position */ + @API(status = API.Status.STABLE) public long sendBeat() { return sendBeat(getPlaybackPosition()); } @@ -1978,6 +2033,7 @@ public long sendBeat() { * * @return the beat number that was sent */ + @API(status = API.Status.STABLE) public long sendBeat(Snapshot snapshot) { byte[] payload = new byte[BEAT_PAYLOAD.length]; System.arraycopy(BEAT_PAYLOAD, 0, payload, 0, BEAT_PAYLOAD.length); @@ -2026,6 +2082,7 @@ public long sendBeat(Snapshot snapshot) { * range 1 through 4 * @throws IOException if there is a problem starting the {@link BeatFinder} */ + @API(status = API.Status.STABLE) public synchronized void setSendingStatus(boolean send) throws IOException { if (isSendingStatus() == send) { return; @@ -2045,20 +2102,7 @@ public synchronized void setSendingStatus(boolean send) throws IOException { final AtomicBoolean stillRunning = new AtomicBoolean(true); sendingStatus = stillRunning; // Allow other threads to stop us when necessary. - - Thread sender = new Thread(null, () -> { - while (stillRunning.get()) { - sendStatus(); - try { - //noinspection BusyWait - Thread.sleep(getStatusInterval()); - } catch (InterruptedException e) { - logger.warn("beat-link VirtualCDJ status sender thread was interrupted; continuing"); - } - } - }, "beat-link VirtualCdj status sender"); - sender.setDaemon(true); - sender.start(); + startSenderThread(stillRunning); if (isSynced()) { // If we are supposed to be synced, we need to respond to master beats and tempo changes. addMasterListener(ourSyncMasterListener); @@ -2081,11 +2125,33 @@ public synchronized void setSendingStatus(boolean send) throws IOException { } } + /** + * Creates and starts the thread which sends our status packets as long as {@code stillRunning} remains {@code true}. + * + * @param stillRunning used to determine when to shut down + */ + private void startSenderThread(AtomicBoolean stillRunning) { + Thread sender = new Thread(null, () -> { + while (stillRunning.get()) { + sendStatus(); + try { + //noinspection BusyWait + Thread.sleep(getStatusInterval()); + } catch (InterruptedException e) { + logger.warn("beat-link VirtualCDJ status sender thread was interrupted; continuing"); + } + } + }, "beat-link VirtualCdj status sender"); + sender.setDaemon(true); + sender.start(); + } + /** * Check whether we are currently sending status packets. * * @return {@code true} if we are sending status packets, and can participate in (and control) tempo and beat sync */ + @API(status = API.Status.STABLE) public synchronized boolean isSendingStatus() { return (sendingStatus != null); } @@ -2115,6 +2181,7 @@ public synchronized boolean isSendingStatus() { * * @param playing {@code true} if we should seem to be playing */ + @API(status = API.Status.STABLE) public void setPlaying(boolean playing) { if (this.playing.get() == playing) { @@ -2144,6 +2211,7 @@ public void setPlaying(boolean playing) { * * @return {@code true} if we are reporting active playback */ + @API(status = API.Status.STABLE) public boolean isPlaying() { return playing.get(); } @@ -2153,6 +2221,7 @@ public boolean isPlaying() { * * @return the current (or last, if we are stopped) playback state */ + @API(status = API.Status.STABLE) public Snapshot getPlaybackPosition() { if (playing.get()) { return metronome.getSnapshot(); @@ -2171,6 +2240,7 @@ public Snapshot getPlaybackPosition() { * * @param ms the number of millisecond to shift the simulated playback position */ + @API(status = API.Status.STABLE) public void adjustPlaybackPosition(int ms) { if (ms != 0) { metronome.adjustStart(-ms); @@ -2203,6 +2273,7 @@ public void adjustPlaybackPosition(int ms) { * @throws IllegalStateException if we are not sending status updates * @throws IOException if there is a problem sending the master yield request */ + @API(status = API.Status.STABLE) public synchronized void becomeTempoMaster() throws IOException { logger.debug("Trying to become master."); if (!isSendingStatus()) { @@ -2235,6 +2306,7 @@ public synchronized void becomeTempoMaster() throws IOException { * * @return {@code true} if we hold the tempo master role */ + @API(status = API.Status.STABLE) public boolean isTempoMaster() { return master.get(); } @@ -2276,6 +2348,7 @@ public void newBeat(Beat beat) { * * @param sync if {@code true}, our status packets will be tempo and beat aligned with the tempo master */ + @API(status = API.Status.STABLE) public synchronized void setSynced(boolean sync) { if (synced.get() != sync) { // We are changing sync state, so add or remove our master listener as appropriate. @@ -2299,6 +2372,7 @@ public synchronized void setSynced(boolean sync) { * * @return {@code true} if our status packets will be tempo and beat aligned with the tempo master */ + @API(status = API.Status.STABLE) public boolean isSynced() { return synced.get(); } @@ -2316,6 +2390,7 @@ public boolean isSynced() { * * @param audible {@code true} if we should report ourselves as being on the air in our status packets */ + @API(status = API.Status.STABLE) public void setOnAir(boolean audible) { onAir.set(audible); } @@ -2327,6 +2402,7 @@ public void setOnAir(boolean audible) { * * @return audible {@code true} if we should report ourselves as being on the air in our status packets */ + @API(status = API.Status.STABLE) public boolean isOnAir() { return onAir.get(); } @@ -2338,6 +2414,7 @@ public boolean isOnAir() { * * @param bpm the tempo, in beats per minute, that we should report in our status and beat packets */ + @API(status = API.Status.STABLE) public void setTempo(double bpm) { if (bpm == 0.0) { throw new IllegalArgumentException("Tempo cannot be zero."); @@ -2357,6 +2434,7 @@ public void setTempo(double bpm) { * * @return the tempo, in beats per minute, that we are reporting in our status and beat packets */ + @API(status = API.Status.STABLE) public double getTempo() { return metronome.getTempo(); } @@ -2366,6 +2444,7 @@ public double getTempo() { * one. If we are told to jump to a larger beat than this, we map it back into the range we will play. This would * be a little over nine hours at 120 bpm, which seems long enough for any track. */ + @API(status = API.Status.STABLE) public final int MAX_BEAT = 65536; /** @@ -2386,6 +2465,7 @@ private int wrapBeat(int beat) { * * @param beat the beat that we should pretend to be playing */ + @API(status = API.Status.STABLE) public synchronized void jumpToBeat(int beat) { if (beat < 1) { @@ -2528,6 +2608,7 @@ private void sendStatus() { * * @return the only instance of this class which exists */ + @API(status = API.Status.STABLE) public static VirtualCdj getInstance() { return ourInstance; } diff --git a/src/main/java/org/deepsymmetry/beatlink/data/AlbumArt.java b/src/main/java/org/deepsymmetry/beatlink/data/AlbumArt.java index 700c3ef..e3994d8 100644 --- a/src/main/java/org/deepsymmetry/beatlink/data/AlbumArt.java +++ b/src/main/java/org/deepsymmetry/beatlink/data/AlbumArt.java @@ -1,5 +1,6 @@ package org.deepsymmetry.beatlink.data; +import org.apiguardian.api.API; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,6 +18,7 @@ * * @author James Elliott */ +@API(status = API.Status.STABLE) public class AlbumArt { private static final Logger logger = LoggerFactory.getLogger(AlbumArt.class); @@ -25,7 +27,6 @@ public class AlbumArt { * The unique artwork identifier that was used to request this album art. Even though it is not a track, the * same pieces of information are used. */ - @SuppressWarnings("WeakerAccess") public final DataReference artReference; /** @@ -38,7 +39,7 @@ public class AlbumArt { * * @return the bytes that make up the album art */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public ByteBuffer getRawBytes() { rawBytes.rewind(); return rawBytes.slice(); @@ -49,6 +50,7 @@ public ByteBuffer getRawBytes() { * * @return the newly-created image, ready to be drawn */ + @API(status = API.Status.STABLE) public BufferedImage getImage() { ByteBuffer artwork = getRawBytes(); artwork.rewind(); @@ -70,16 +72,13 @@ public BufferedImage getImage() { * * @throws IOException if there is a problem reading the file */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public AlbumArt(DataReference artReference, File file) throws IOException { this.artReference = artReference; - RandomAccessFile raf = new RandomAccessFile(file, "r"); - try { - byte[] bytes = new byte[(int)raf.length()]; + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + byte[] bytes = new byte[(int) raf.length()]; raf.readFully(bytes); rawBytes = ByteBuffer.wrap(bytes); - } finally { - raf.close(); } } @@ -89,7 +88,7 @@ public AlbumArt(DataReference artReference, File file) throws IOException { * @param artReference the unique database reference that was used to request this artwork * @param rawBytes the bytes of image data as loaded from the player or media export */ - @SuppressWarnings("WeakerAccess") + @API(status = API.Status.STABLE) public AlbumArt(DataReference artReference, ByteBuffer rawBytes) { this.artReference = artReference; byte[] bytes = new byte[rawBytes.remaining()]; diff --git a/src/main/java/org/deepsymmetry/beatlink/data/OpusProvider.java b/src/main/java/org/deepsymmetry/beatlink/data/OpusProvider.java index c866bec..7177ceb 100644 --- a/src/main/java/org/deepsymmetry/beatlink/data/OpusProvider.java +++ b/src/main/java/org/deepsymmetry/beatlink/data/OpusProvider.java @@ -38,7 +38,7 @@ public class OpusProvider { /** * The device name reported by Opus Quad, so we can recognize when we are dealing with one of these devices. */ - public static final String opusName = "OPUS-QUAD"; + public static final String OPUS_NAME = "OPUS-QUAD"; /** * Keep track of whether we are running.