All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.deepsymmetry.beatlink.dbserver.Message Maven / Gradle / Ivy

There is a newer version: 7.4.0
Show newest version
package org.deepsymmetry.beatlink.dbserver;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.DataInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.*;

/**
 * 

Encapsulates a full dbserver message, made up of a list of {@link Field} objects, * and having a particular structure, as described in the * Packet Analysis paper.

* *

Known message types are found in {@link Message.KnownType}. Many requests return a series of messages that * represent menu items; the known versions of these are reflected in {@link Message.MenuItemType}.

* * @author James Elliott */ public class Message { private static final Logger logger = LoggerFactory.getLogger(Client.class.getName()); /** * The special field that marks the start of a new message. */ @SuppressWarnings("WeakerAccess") public static final NumberField MESSAGE_START = new NumberField(0x872349ae, 4); /** * Defines all the message types we know about, with any information we know about their arguments. */ public enum KnownType { /** * The special initial request that is sent as the second action after opening a new connection, to enable * further database queries. */ SETUP_REQ (0x0000, "setup request", "requesting player"), /** * A response indicating that a request could not be fulfilled for some reason. */ INVALID_DATA (0x0001, "invalid data"), /** * Asks for the top-level menu of the player. */ ROOT_MENU_REQ (0x1000, "root menu request", "r:m:s:t", "sort order", "magic constant?"), /** * Asks for a list of genres in the specified media slot. */ GENRE_MENU_REQ (0x1001, "genre menu request", "r:m:s:t", "sort order"), /** * Asks for a list of artists in the specified media slot. */ ARTIST_MENU_REQ (0x1002, "artist menu request", "r:m:s:t", "sort order"), /** * Asks for a list of albums in the specified media slot. */ ALBUM_MENU_REQ (0x1003, "album menu request", "r:m:s:t", "sort order"), /** * Asks for a list of all the tracks in the specified media slot. */ TRACK_MENU_REQ (0x1004, "track menu request", "r:m:s:t", "sort order"), /** * Asks for a list of track tempos found in the specified media slot. */ BPM_MENU_REQ (0x1006, "bpm menu request", "r:m:s:t", "sort order"), /** * Asks for a list of ratings in the specified media slot. */ RATING_MENU_REQ (0x1007, "rating menu request", "r:m:s:t", "sort order"), /** * Asks for a list of performance histories found in the specified media slot. */ HISTORY_MENU_REQ (0x1012, "history menu request", "r:m:s:t", "sort order"), /** * Asks for a list of track keys found in the specified media slot. */ KEY_MENU_REQ (0x1014, "key menu request", "r:m:s:t", "sort order"), /** * Asks for an artist menu for a particular genre in the specified media slot. */ ARTIST_MENU_FOR_GENRE_REQ (0x1101, "artist menu for genre request", "r:m:s:t", "sort", "genre ID"), /** * Asks for an album menu for a particular artist in the specified media slot. */ ALBUM_MENU_FOR_ARTIST_REQ (0x1102, "album menu for artist request", "r:m:s:t", "sort", "artist ID"), /** * Asks for a track menu for a particular album in the specified media slot. */ TRACK_MENU_FOR_ALBUM_REQ (0x1103, "track menu for album request", "r:m:s:t", "sort", "album ID"), /** * Asks for a playlist or folder by ID. */ PLAYLIST_REQ (0x1105, "playlist/folder request", "r:m:s:t", "sort order", "playlist/folder ID", "0=playlist, 1=folder"), /** * Asks for a list of tempo ranges around a given tempo. */ BPM_RANGE_REQ (0x1106, "bpm range request", "r:m:s:t", "sort order", "tempo"), /** * Asks for a track menu for a particular rating in the specified media slot. */ TRACK_MENU_FOR_RATING_REQ (0x1107, "track menu for rating request", "r:m:s:t", "sort", "rating ID"), /** * Asks for a track menu for a particular rating in the specified media slot. */ TRACK_MENU_FOR_HISTORY_REQ (0x1112, "track menu for history entry request", "r:m:s:t", "sort", "history ID"), /** * Asks for a key neighbor menu (showing harmonically compatible keys and their distance, * ranging from 0 to 2) available in the specified media slot. */ NEIGHBOR_MENU_FOR_KEY(0x1114, "neighbor menu for key request", "r:m:s:t", "sort", "key ID"), /** * Asks for an album menu by genre and artist, can specify all artists by passing -1 for artist ID. */ ALBUM_MENU_FOR_GENRE_AND_ARTIST (0x1201, "album menu for genre and artist request", "r:m:s:t:", "sort", "genre ID", "artist ID, or -1 for ALL"), /** * Asks for an track menu by BPM and distance, which represents a percentage tolerance by which the BPM * can differ, ranging from 0 to 6. */ TRACK_MENU_FOR_ARTIST_AND_ALBUM (0x1202, "track menu for BPM and distance request", "r:m:s:t:", "sort", "artist ID", "album ID, or -1 for ALL"), /** * Asks for an track menu by BPM and distance, which represents a percentage tolerance by which the BPM * can differ, ranging from 0 to 6. */ TRACK_MENU_FOR_BPM_AND_DISTANCE (0x1206, "track menu for BPM and distance request", "r:m:s:t:", "sort", "bpm ID", "distance (+/- %, can range from 0-6)"), /** * Asks for an track menu by key and distance (which represents harmonic compatibility as allowed movement * around the circle of fifths), ranging from 0 to 2. */ TRACK_MENU_FOR_KEY_AND_DISTANCE (0x1214, "track menu for key and distance request", "r:m:s:t:", "sort", "key ID", "distance (around circle of fifths)"), /** * Asks to search the database for records matching the specified text (artist, album, track names, * possibly other things). Returns a variety of different menu item types. */ SEARCH_MENU (0x1300, "search by substring request", "r:m:s:t", "sort", "search string byte size", "search string (must be uppercase", "unknown (0)"), /** * Asks for an album menu by genre, artist, and album, can specify all artists and/or albums by passing -1 for * the artist and/or album IDs. */ TRACK_MENU_FOR_GENRE_ARTIST_AND_ALBUM (0x1301, "track menu for genre, artist and album request", "r:m:s:t:", "sort", "genre ID", "artist ID, or -1 for ALL", "album ID, or -1 for ALL"), /** * Asks for the metadata associated with a particular track, by rekordbox ID. */ REKORDBOX_METADATA_REQ (0x2002, "rekordbox track metadata request", "r:m:s:t", "rekordbox id"), /** * Asks for an album artwork image, by artwork ID. */ ALBUM_ART_REQ (0x2003, "album art request", "r:m:s:t", "artwork id"), /** * Asks for the preview (summary) waveform data for a track, by rekordbox ID. */ WAVE_PREVIEW_REQ (0x2004, "track waveform preview request", "r:m:s:t", "unknown (4)", "rekordbox id", "unknown (0)"), /** * Asks for a folder menu, of the raw media filesystem in the specified slot. */ FOLDER_MENU_REQ (0x2006, "folder menu request", "r:m:s:t", "sort order?", "folder id (-1 for root)", "unknown (0)"), // 0x2102 seems to ask about track data file information. /** * Asks for the cue points of a track, by rekordbox ID. */ CUE_LIST_REQ (0x2104, "track cue list request", "r:m:s:t", "rekordbox id"), /** * Asks for metadata about a CD track, by track number. */ UNANALYZED_METADATA_REQ (0x2202, "unanalyzed track metadata request", "r:m:s:t", "track number"), /** * Asks for the beat grid of a track, by rekordbox id. */ BEAT_GRID_REQ (0x2204, "beat grid request", "r:m:s:t", "rekordbox id"), /** * Asks for the detailed waveform data for a track, by rekordbox ID. */ // 0x2504 when loading track?! WAVE_DETAIL_REQ (0x2904, "track waveform detail request", "r:m:s:t", "rekordbox id"), /** * Once a specific type of request has been made and acknowledged, this allows the results to be retrieved, * possibly in paginated chunks starting at offset, returning up to limit results. */ RENDER_MENU_REQ (0x3000, "render items from last requested menu", "r:m:s:t", "offset", "limit", "unknown (0)", "len_a (=limit)?", "unknown (0)"), /** * This response indicates that a query has been accepted, and reports how many results are available. They are * now ready to be retrieved using {@link #RENDER_MENU_REQ}. */ MENU_AVAILABLE (0x4000, "requested menu is available", "request type", "# items available"), /** * When {@link #RENDER_MENU_REQ} is used to retrieve a set of results, this message will be sent as the first * response, followed by as many {@link #MENU_ITEM} messages as were requested. */ MENU_HEADER (0x4001, "rendered menu header"), /** * This response contains the binary image data of requested album art. */ ALBUM_ART (0x4002, "album art", "request type", "unknown (0)", "image length", "image bytes"), /** * Indicates that the item that was just requested cannot be found. */ UNAVAILABLE (0x4003, "requested media unavailable", "request type"), /** * A series of messages of this type are the payload returned in response to {@link #RENDER_MENU_REQ}. The * number requested will be delivered, in between a {@link #MENU_HEADER} and a {@link #MENU_FOOTER} message. * Each message will be of a particular subtype, which is identified by the value of the 7th argument; see * {@link MenuItemType} for known values. */ MENU_ITEM (0x4101, "rendered menu item", "numeric 1 (parent id, e.g. artist for track)", "numeric 2 (this id)", "label 1 byte size", "label 1", "label 2 byte size", "label 2", "item type", "flags? byte 3 is 1 when track played", "album art id", "playlist position"), /** * When {@link #RENDER_MENU_REQ} is used to retrieve a set of results, this message will be sent as the final * response, following any {@link #MENU_ITEM} messages that were requested. */ MENU_FOOTER (0x4201, "rendered menu footer"), /** * Returns the bytes of the small waveform preview to be displayed at the bottom of the player display, * or in rekordbox track lists. */ WAVE_PREVIEW (0x4402, "track waveform preview", "request type", "unknown (0)", "waveform length", "waveform bytes"), /** * Returns information about the beat number (within a bar) and millisecond position within the track of each * beat in a track. */ BEAT_GRID (0x4602, "beat grid", "request type", "unknown (0)", "beat grid length", "beat grid bytes", "unknown (0)"), /** * Returns information about any cue points set in the track. */ CUE_LIST (0x4702, "cue points", "request type", "unknown", "blob 1 length", "blob 1", "unknown (0x24)", "unknown", "unknown", "blob 2 length", "blob 2"), /** * Returns the bytes of the detailed waveform which is scrolled through while the track is playing. */ WAVE_DETAIL (0x4a02, "track waveform detail", "request type", "unknown (0)", "waveform length", "waveform bytes"); /** * The numeric value that identifies this message type, by its presence in a 4-byte number field immediately * following the message start indicator. */ public final long protocolValue; /** * The descriptive name of the message type. */ @SuppressWarnings("WeakerAccess") public final String description; /** * Descriptions of any arguments with known purposes. */ private final String[] arguments; KnownType(long value, String description, String... arguments) { protocolValue = value; this.description = description; this.arguments = arguments.clone(); } /** * Get the descriptive name of the specified message argument, if one is known. * * @param index the zero-based index identifying the argument whose description is desired. * * @return either the description found, or "unknown" if none was found. */ @SuppressWarnings("WeakerAccess") public String describeArgument(int index) { if (index < 0 || index >= arguments.length) { return "unknown"; } return arguments[index]; } /** * Returns the descriptions of all known arguments, in order. * * @return a list of the descriptions of the arguments that are expected for this message type. */ @SuppressWarnings("unused") public List arguments() { return Collections.unmodifiableList(Arrays.asList(arguments)); } } /** * Allows a known message type to be looked up by the message type number. */ @SuppressWarnings("WeakerAccess") public static final Map KNOWN_TYPE_MAP; static { Map scratch = new HashMap(); for (KnownType type : KnownType.values()) { scratch.put(type.protocolValue, type); } KNOWN_TYPE_MAP = Collections.unmodifiableMap(scratch); } public enum MenuItemType { /** * A potentially-nested grouping of other objects, such as a group of playlists in the playlists menu. */ FOLDER (0x0001), /** * The string identifying the name of an album, part of the track metadata response. Also contains the * album ID for listing by album. */ ALBUM_TITLE (0x0002), /** * I don’t yet know where this appears or what it means. */ DISC (0x0003), /** * The string identifying the name of a track, part of the track metadata response, as well as track lists * and play lists. */ TRACK_TITLE (0x0004), /** * The string identifying the musical genre of a track, part of the track metadata response. Also contains * the genre ID for listing by genre. */ GENRE (0x0006), /** * The string identifying the name of the artist for a track, part of the track metadata response, as well * as track lists and play lists. Also contains the artist ID for listing by artist. */ ARTIST (0x0007), /** * When listing playlists, reports the name and ID of an actual playlist, as opposed to a sub-folder. */ PLAYLIST (0x0008), /** * The rating assigned a track by the DJ, part of the track metadata response. */ RATING (0x000a), /** * The duration, in seconds, of a track, part of the track metadata response. */ DURATION (0x000b), /** * The tempo of a track, BPM times 100, part of the track metadata response. */ TEMPO (0x000d), /** * A string containing the label that issued a track, part of the track metadata response. Also contains the * label ID for listing by label. */ LABEL (0x000e), /** * A string containing the musical key of a track, part of the track metadata response. Also contains the * key ID for listing by key. */ KEY (0x000f), /** * This value appears in non-rekordbox metadata, and we have not yet figured out what it contains. */ UNANALYZED_UNKNOWN (0x0010), /** * Indicates the DJ has not assigned a color label to a track, part of the track metadata response. */ COLOR_NONE (0x0013), /** * A label assigned a track by the DJ, marked with a pink dot, part of the track metadata response. */ COLOR_PINK (0x0014), /** * A label assigned a track by the DJ, marked with a red dot, part of the track metadata response. */ COLOR_RED (0x0015), /** * A label assigned a track by the DJ, marked with an orange dot, part of the track metadata response. */ COLOR_ORANGE (0x0016), /** * A label assigned a track by the DJ, marked with a yellow dot, part of the track metadata response. */ COLOR_YELLOW (0x0017), /** * A label assigned a track by the DJ, marked with a green dot, part of the track metadata response. */ COLOR_GREEN (0x0018), /** * A label assigned a track by the DJ, marked with an aqua dot, part of the track metadata response. */ COLOR_AQUA (0x0019), /** * A label assigned a track by the DJ, marked with a blue dot, part of the track metadata response. */ COLOR_BLUE (0x001a), /** * A label assigned a track by the DJ, marked with a purple dot, part of the track metadata response. */ COLOR_PURPLE (0x001b), /** * An arbitrary text string assigned to a track by the DJ. Also contains a comment ID, for listing by comment. */ COMMENT (0x0023), /** * A list of tracks that were performed in a past session. */ HISTORY_PLAYLIST (0x0024), /** * The artist who originally recorded a track. Also contains an original artist ID, for listing by original * artist. */ ORIGINAL_ARTIST (0x0028), /** * The producer who remixed a track. Also contains a remixer ID, for listing by remixer. */ REMIXER (0x0029), /** * A string reporting when the track was added to the collection, in the form "YYYY-MM-DD", part of the track * metadata response. This seems to propagate from iTunes. Also contains a date added ID, for listing by * date added. */ DATE_ADDED (0x002e), /** * The root menu item that takes you to the genre list. */ GENRE_MENU (0x0080), /** * The root menu item that takes you to the artist list. */ ARTIST_MENU (0x0081), /** * The root menu item that takes you to the artist list. */ ALBUM_MENU (0x0082), /** * The root menu item that takes you to the track list. */ TRACK_MENU (0x0083), /** * The root menu item that takes you to the playlist hierarchy. */ PLAYLIST_MENU (0x0084), /** * The root menu item that takes you to the tempo list. */ BPM_MENU (0x0085), /** * The root menu item that takes you to the folder list. */ RATING_MENU (0x0086), /** * The root menu item that takes you to the key list. */ KEY_MENU (0x008b), /** * The root menu item that takes you to the folder list. */ FOLDER_MENU (0x0090), /** * The root menu item that takes you to the search interface. */ SEARCH_MENU (0x0091), /** * The root menu item that takes you to the history list. */ HISTORY_MENU (0x0095), /** * The root menu item that takes you to the hot cue bank list. */ HOT_CUE_BANK_MENU (0x0098), /** * The menu item that lets you choose all values in a subcategory. */ ALL (0x00a0), /** * Reports the title and album of a track, returned when listing playlists or all tracks sorted by album, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_ALBUM (0x0204), /** * Reports the title and genre of a track, returned when listing playlists or all tracks sorted by genre, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_GENRE (0x0604), /** * Reports the title and artist of a track, returned when listing playlists or all tracks sorted by artist, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_ARTIST(0x0704), /** * Reports the title and duration of a track, returned when listing playlists or all tracks sorted by time, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_TIME(0x0b04), /** * Reports the title and rating of a track, returned when listing playlists or all tracks sorted by rating, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_RATING (0x0a04), /** * Reports the title and BPM of a track, returned when listing playlists or all tracks sorted by BPM, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_BPM (0x0d04), /** * Reports the title and label of a track, returned when listing playlists or all tracks sorted by label, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_LABEL (0x0e04), /** * Reports the title and bit rate of a track, return when listing playlists or all tracks sorted by bit rate, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_RATE (0x1004), /** * Reports the title and color of a track, return when listing playlists or all tracks sorted by color, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_LIST_ENTRY_BY_COLOR (0x1a04), /** * Reports the title and comment of a track, returned when listing playlists or all tracks sorted by comment, * or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_COMMENT (0x2304), /** * Reports the title and original artist of a track, returned when listing playlists sorted by original artist, * or in their default sort order when listing all tracks and the DJ has set the original artist as the second * column for track lists. */ TRACK_TITLE_AND_ORIGINAL_ARTIST (0x02804), /** * Reports the title and remixer of a track, returned when listing playlists sorted by remixer, or in their * default sort order when listing all tracks and the DJ has set this as the second column for track lists. */ TRACK_TITLE_AND_REMIXER (0x02904), /** * Reports the title and play count of a track, returned when listing playlists or all tracks sorted by how many * times the DJ has played the track, or in their default sort order when the DJ has set this as the default * second column for track lists. */ TRACK_TITLE_AND_DJ_PLAY_COUNT (0x2a04), /** * Reports the title and date added of a track, returned when listing playlists or all tracks sorted by date * added, or in their default sort order when the DJ has set this as the default second column for track lists. */ TRACK_TITLE_AND_DATE_ADDED (0x2e04), /** * We received a value that we don't recognize, so we don't know what it contains. */ UNKNOWN (-1); /** * The value which identifies this type of menu item by appearing in the seventh argument of a * {@link KnownType#MENU_ITEM} response. */ @SuppressWarnings("WeakerAccess") public final long protocolValue; MenuItemType(long value) { protocolValue = value; } } /** * Allows a menu item type to be looked up by the value seen in the seventh argument of a * {@link KnownType#MENU_ITEM} response. */ @SuppressWarnings("WeakerAccess") public static final Map MENU_ITEM_TYPE_MAP; static { Map scratch = new HashMap(); for (MenuItemType type : MenuItemType.values()) { scratch.put(type.protocolValue, type); } MENU_ITEM_TYPE_MAP = scratch; } /** * The 4-byte number field that provides the sequence number tying a query to its response messages, immediately * following the message start field. */ @SuppressWarnings("WeakerAccess") public final NumberField transaction; /** * The 2-byte number field that identifies what type of message this is, immediately following the transaction * sequence number. */ public final NumberField messageType; /** * The recognized type, if any, of this message. */ public final KnownType knownType; /** * The 1-byte number field that specifies how many arguments the message has. */ @SuppressWarnings("WeakerAccess") public final NumberField argumentCount; /** * The arguments being sent as part of this message. */ public final List arguments; /** * The entire list of fields that make up the message. */ public final List fields; /** * Constructor for experimenting with new message types. * * @param transaction the transaction ID (sequence number) that ties a message to its responses * @param messageType identifies the purpose and structure of the message * @param arguments the arguments to send with the message */ @SuppressWarnings("WeakerAccess") public Message(long transaction, long messageType, Field... arguments) { this(new NumberField(transaction, 4), new NumberField(messageType, 2), arguments); } /** * Constructor from code using known message types. * * @param transaction the transaction ID (sequence number) that ties a message to its responses * @param messageType identifies the purpose and structure of the message * @param arguments the arguments to send with the message */ @SuppressWarnings("SameParameterValue") public Message(long transaction, KnownType messageType, Field... arguments) { this(transaction, messageType.protocolValue, arguments); } /** * Constructor when being read from the network, so already have all the fields created. * * @param transaction the transaction ID (sequence number) that ties a message to its responses * @param messageType identifies the purpose and structure of the message * @param arguments the arguments to send with the message */ @SuppressWarnings("WeakerAccess") public Message(NumberField transaction, NumberField messageType, Field... arguments) { if (transaction.getSize() != 4) { throw new IllegalArgumentException("Message transaction sequence number must be 4 bytes long"); } if (messageType.getSize() != 2) { throw new IllegalArgumentException("Message type must be 2 bytes long"); } if (arguments.length > 12) { throw new IllegalArgumentException("Messages cannot have more than 12 arguments"); } this.transaction = transaction; this.messageType = messageType; this.knownType = KNOWN_TYPE_MAP.get(messageType.getValue()); this.argumentCount = new NumberField(arguments.length, 1); this.arguments = Collections.unmodifiableList(Arrays.asList(arguments.clone())); // Build the list of argument type tags byte[] argTags = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; for (int i = 0; i < arguments.length; i++) { argTags[i] = arguments[i].getArgumentTag(); } // And build the full list of fields that will be sent when this message is sent. Field[] allFields = new Field[arguments.length + 5]; allFields[0] = MESSAGE_START; allFields[1] = transaction; allFields[2] = messageType; allFields[3] = argumentCount; allFields[4] = new BinaryField(argTags); System.arraycopy(arguments, 0, allFields, 5, arguments.length); fields = Collections.unmodifiableList(Arrays.asList(allFields)); } /** * Read the next message from the stream. * * @param is a stream connected to a dbserver which is expected to be sending a message. * * @return the next full message found on the stream. * * @throws IOException if there is a problem reading the message. */ public static Message read(DataInputStream is) throws IOException { final Field start = Field.read(is); if (!(start instanceof NumberField)) { throw new IOException("Did not find number field reading start of message; got: " + start); } if (start.getSize() != 4) { throw new IOException("Number field to start message must be of size 4, got: " + start); } if (((NumberField) start).getValue() != MESSAGE_START.getValue()) { throw new IOException("Number field had wrong value to start message. Expected: " + MESSAGE_START + ", got: " + start); } final Field transaction = Field.read(is); if (!(transaction instanceof NumberField)) { throw new IOException("Did not find number field reading transaction ID of message; got: " + transaction); } if (transaction.getSize() != 4) { throw new IOException("Transaction number field of message must be of size 4, got: " + transaction); } final Field type = Field.read(is); if (!(type instanceof NumberField)) { throw new IOException("Did not find number field reading type of message; got: " + type); } if (type.getSize() != 2) { throw new IOException("Type field of message must be of size 2, got: " + type); } final Field argCountField = Field.read(is); if (!(argCountField instanceof NumberField)) { throw new IOException("Did not find number field reading argument count of message; got: " + argCountField); } if (argCountField.getSize() != 1) { throw new IOException("Argument count field of message must be of size 1, got: " + argCountField); } final int argCount = (int)((NumberField)argCountField).getValue(); if (argCount < 0 || argCount > 12) { throw new IOException("Illegal argument count while reading message; must be between 0 and 12, got: " + argCount); } final Field argTypes = Field.read(is); if (!(argTypes instanceof BinaryField)) { throw new IOException("Did not find binary field reading argument types of message, got: " + argTypes); } byte[] argTags = new byte[12]; ((BinaryField)argTypes).getValue().get(argTags); Field[] arguments = new Field[argCount]; Field lastArg = null; for (int i = 0; i < argCount; i++) { if (argTags[i] == 3 && (lastArg instanceof NumberField) && ((NumberField) lastArg).getValue() == 0) { arguments [i] = new BinaryField(new byte[0]); // Do not attempt to read a zero-length binary field } else { arguments[i] = Field.read(is); } lastArg = arguments[i]; if (lastArg.getArgumentTag() != argTags[i]) { throw new IOException("Found argument of wrong type reading message. Expected tag: " + argTags[i] + " and got: " + arguments[i].getArgumentTag()); } } Message result = new Message((NumberField)transaction, (NumberField)type, arguments); logger.debug("Received> {}", result); return result; } @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("Message: [transaction: ").append(transaction.getValue()); result.append(String.format(", type: 0x%04x (", messageType.getValue())); if (knownType != null) { result.append(knownType.description); } else { result.append("unknown"); } result.append("), arg count: ").append(argumentCount.getValue()).append(String.format(", arguments:%n")); for (int i = 0; i < arguments.size(); i++) { final Field arg = arguments.get(i); result.append(String.format("%4d: ", i + 1)); if (arg instanceof NumberField) { final long value = ((NumberField) arg).getValue(); result.append(String.format("number: %10d (0x%08x)", value, value)); } else if (arg instanceof BinaryField) { ByteBuffer bytes = ((BinaryField)arg).getValue(); byte[] array = new byte[bytes.remaining()]; bytes.get(array); result.append(String.format("blob length %d:",arg.getSize())); for (byte b : array) { result.append(String.format(" %02x", b)); } } else if (arg instanceof StringField) { result.append(String.format("string length %d: \"%s\"", arg.getSize(), ((StringField)arg).getValue())); } else { result.append("unknown: ").append(arg); } String argDescription = "unknown"; if (knownType != null) { argDescription = knownType.describeArgument(i); if (knownType == KnownType.MENU_ITEM && i == 6 && (arg instanceof NumberField)) { String itemType = "unknown"; MenuItemType match = MENU_ITEM_TYPE_MAP.get(((NumberField) arg).getValue()); if (match != null) { itemType = match.name(); } argDescription = argDescription + ": " + itemType; } } result.append(String.format(" [%s]%n", argDescription)); } return result.append("]").toString(); } /** * For many types of query messages, the first argument of the message is a 4-byte integer which we currently * refer to as r:m:s:t, because the first byte is the player number of the player making the * request, the second byte identifies the menu or destination for which information is being * loaded, the third byte identifies the media slot (USB or SD) being asked about (as described in * {@link org.deepsymmetry.beatlink.CdjStatus.TrackSourceSlot}), and the fourth byte identifies the type of * track being worked with (for most requests this is 1, meaning rekordbox). This enumeration lists * the known values for the second, menu, byte. */ public enum MenuIdentifier { /** * The primary menu which appears on the left half of the player display. */ MAIN_MENU (1), /** * The secondary menu which sometimes appears down the right half of the player display. */ SUB_MENU (2), /** * The pseudo-menu of track metadata. */ TRACK_INFO (3), /** * Types of sorting available? I am not entirely sure when this is used. */ SORT_MENU (5), /** * Values which do not display in a menu, such as track waveforms, beat grids, album art, etc. are loaded * “into” this menu/destination. */ DATA (8); /** * The value which identifies this menu or destination by appearing in the second byte of the first argument * of many request messages. */ public final byte protocolValue; MenuIdentifier(int value) { protocolValue = (byte)value; } } /** * Allows a menu/destination to be looked up by the value seen in the second byte of the first argument of many * request messages. */ @SuppressWarnings("WeakerAccess") public static final Map MENU_IDENTIFIER_MAP; static { Map scratch = new HashMap(); for (MenuIdentifier identifier : MenuIdentifier.values()) { scratch.put(identifier.protocolValue, identifier); } MENU_IDENTIFIER_MAP = scratch; } /** * The value returned by {@link #getMenuResultsCount()} when there is no data available for the request * that was made. */ public static final long NO_MENU_RESULTS_AVAILABLE = 0xffffffff; /** * Extracts the result count from a {@link KnownType#MENU_AVAILABLE} response. * * @return the reported count of available results * * @throws IllegalArgumentException if this is not a {@link KnownType#MENU_AVAILABLE} response. */ public long getMenuResultsCount() { if (knownType != Message.KnownType.MENU_AVAILABLE) { throw new IllegalArgumentException("getMenuResultsCount() can only be used with MENU_AVAILABLE responses."); } final NumberField count = (NumberField)arguments.get(1); return count.getValue(); } /** * Extracts the menu item type from a {@link KnownType#MENU_ITEM} response. * * @return the reported type of this menu item * * @throws IllegalArgumentException if this is not a {@link KnownType#MENU_ITEM} response. */ public MenuItemType getMenuItemType() { if (knownType != KnownType.MENU_ITEM) { throw new IllegalArgumentException("getMenuItemType() can only be used with MENU_ITEM responses."); } final NumberField type = (NumberField)arguments.get(6); final MenuItemType result = MENU_ITEM_TYPE_MAP.get(type.getValue()); if (result == null) { return MenuItemType.UNKNOWN; } return result; } /** * Writes the message to the specified channel, for example when creating metadata cache files. * * @param channel the channel to which it should be written * * @throws IOException if there is a problem writing to the channel */ public void write(WritableByteChannel channel) throws IOException { logger.debug("Writing> {}", this); for (Field field : fields) { field.write(channel); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy