import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; enum PlayStatus { STOPPED, PLAYING, PAUSED, SEEKING } public class SoundpadRemoteControl { public static final String CLIENT_VERSION = "1.1.2"; private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8; private final boolean PRINT_ERRORS = true; private long lastRequestTimestamp = System.currentTimeMillis(); private RandomAccessFile pipe = null; /** * You may optionally call this method to handle the exception. This method * is also internally called whenever a request is sent. * * @throws FileNotFoundException * if communication cannot be established. */ public void init() throws FileNotFoundException { if (pipe == null) { pipe = new RandomAccessFile("\\\\.\\pipe\\sp_remote_control", "rw"); } } public void uninit() { if (pipe != null) { try { pipe.close(); } catch (IOException ignore) { } finally { pipe = null; } } } /** * Sends a command to Soundpad. Depending on the command the result contains * a status code or a message. * * @param request * command to be executed by Soundpad * @return string response or empty string if transmission fails. The * response is based on HTTP status codes, e.g. R-200 for OK or * R-404 for Not found. Certain commands get a custom response, e.g. * {@link #getSoundlist()} returns an xml formatted sound list. * @throws IOException * if communication fails */ private synchronized String sendRequest(final String request) throws IOException { init(); if (System.currentTimeMillis() == lastRequestTimestamp) { // doing too many requests at the same time can break the pipe try { Thread.sleep(1); } catch (InterruptedException ignore) { } } pipe.write(request.getBytes()); // data size in pipe is first acquirable after reading one byte byte firstByte = pipe.readByte(); byte[] responseBytes = new byte[(int) pipe.length() + 1]; responseBytes[0] = firstByte; pipe.readFully(responseBytes, 1, responseBytes.length - 1); lastRequestTimestamp = System.currentTimeMillis(); return new String(responseBytes, CHARSET_UTF8); } /** * @see #sendRequest(String) * @param request * command to be executed by Soundpad * @return response or empty string if transmission fails. */ private synchronized String sendRequestNoException(final String request) { try { return sendRequest(request); } catch (IOException e) { uninit(); } return ""; } /** * Helper method to print consolidated errors. */ private void printOfflineError() { if (PRINT_ERRORS) { System.err.println("Remote control is offline."); } } /** * @see #printOfflineError() */ private void printNumericError(final String response) { if (PRINT_ERRORS) { System.err.println("Expected numeric response, but received: " + response); } } /** * @see #printOfflineError() */ private void printError(final String response) { if (PRINT_ERRORS) { System.err.println("Failed: " + response); } } private boolean isSuccess(final String response) { return response.startsWith("R-200"); } private boolean isSuccessPrintResponse(final String response) { if (!response.equals("R-200")) { printError(response); return false; } return true; } private long handleNumericLongGetRequest(final String request) { String response = sendRequestNoException(request); if (response.startsWith("R")) { printError(response); } else if (response.isEmpty()) { printOfflineError(); } else { try { return Long.parseLong(response); } catch (NumberFormatException e) { printNumericError(response); } } return -1; } private String handleStringGetRequest(final String request) { String response = sendRequestNoException(request); if (response.startsWith("R")) { printError(response); } else if (response.isEmpty()) { printOfflineError(); } return response; } private String handleSimpleGetRequest(final String request) { String response = sendRequestNoException(request); if (response.startsWith("R")) { printError(response); } return response; } private String handleEmptyGetRequest(final String request) { String response = sendRequestNoException(request); if (response.isEmpty()) { printOfflineError(); } return response; } //***********************************************************************// // // Remote Control v1.0.0 // // Methods which were added in the first version of the interface. // //***********************************************************************// /** * Let Soundpad play the sound at the given index. Index is the first column * in the sound list and is tied to the All sounds category. Every * sound has a unique index, which can be changed by moving the sound within * the All sounds category. The All sounds category is hidden by default and * can be accessed in Soundpad from the menu at Window > Categories > All * sounds. * * @param index * Get the index by calling {@link #getSoundlist()} first. * @return true if a sound with the given index exists. */ public boolean playSound(final int index) { return isSuccess(sendRequestNoException("DoPlaySound(" + index + ")")); } /** * Extends {@link #playSound(int)} by the ability to determine on which * lines the sound shall be played. * * @param index * Get the index by calling {@link #getSoundlist()} first. * @param renderLine * set to true to play on speakers so you hear it. * @param captureLine * set to true to play on microphone so others hear it. * @return true on success */ public boolean playSound(final int index, final boolean renderLine, final boolean captureLine) { String response = sendRequestNoException( "DoPlaySound(" + index + ", " + renderLine + ", " + captureLine + ")"); return isSuccess(response); } /** * Play previous sound in the list. The play mode is the same as it was for * the last played file. Means, if a sound was played on speakers only, then * this function will play on speakers only as well. * * @return true on success */ public boolean playPreviousSound() { return isSuccess(sendRequestNoException("DoPlayPreviousSound()")); } /** * @see #playPreviousSound() * @return true on success */ public boolean playNextSound() { return isSuccess(sendRequestNoException("DoPlayNextSound()")); } public boolean stopSound() { return isSuccess(sendRequestNoException("DoStopSound()")); } public boolean togglePause() { return isSuccess(sendRequestNoException("DoTogglePause()")); } /** * Use negative values to jump backwards. * * @param timeMillis * e.g. 5000 to jump 5 seconds forward. */ public boolean jump(final int timeMillis) { return isSuccess(sendRequestNoException("DoJumpMs(" + timeMillis + ")")); } /** * Jump to a particular position in the currently played sound. * * @param timeMillis * e.g. 5000 to jump to the 5th second of the sound. */ public boolean seek(final int timeMillis) { return isSuccess(sendRequestNoException("DoSeekMs(" + timeMillis + ")")); } /** * Start recording. This call is handled the same way as if a recording is * started by hotkeys, which means a notification sound is played. This is * the default behavior, but the notification sound can be turned off in * Soundpad. * * @return true if recording was started or was already running */ public boolean startRecording() { return isSuccess(sendRequestNoException("DoStartRecording()")); } public boolean stopRecording() { return isSuccess(sendRequestNoException("DoStopRecording()")); } /** * Uses Soundpad's instant search to highlight sounds. */ public boolean search(final String searchTerm) { String response = sendRequestNoException("DoSearch(\"" + searchTerm + "\")"); if (!response.equals("R-200")) { printError(response); return false; } return true; } /** * Closes search panel. */ public boolean resetSearch() { return isSuccess(sendRequestNoException("DoResetSearch()")); } /** * Select previous search hit. Search is always wrapped. Means it starts * again at the first hit if the last hit is reached. */ public boolean selectPreviousHit() { return isSuccess(sendRequestNoException("DoSelectPreviousHit()")); } /** * @see #selectPreviousHit() */ public boolean selectNextHit() { return isSuccess(sendRequestNoException("DoSelectNextHit()")); } /** * Select the sound at the given row in the list. This method was created * before categories were introduced, as such the row is not the sound * index, but the position in the currently selected category. */ public boolean selectRow(final int row) { return isSuccess(sendRequestNoException("DoSelectIndex(" + row + ")")); } /** * Scroll down or up by this many rows. Use negative values to scroll * upwards. */ public boolean scrollBy(final int rows) { return isSuccess(sendRequestNoException("DoScrollBy(" + rows + ")")); } /** * Scroll to a particular row. */ public boolean scrollTo(final int row) { return isSuccess(sendRequestNoException("DoScrollTo(" + row + ")")); } /** * Returns the total amount of sounds over all categories independent of * currently selected category. */ public long getSoundFileCount() { return handleNumericLongGetRequest("GetSoundFileCount()"); } /** * Returns playback position of currently played sound file in milliseconds. */ public long getPlaybackPosition() { return handleNumericLongGetRequest("GetPlaybackPositionInMs()"); } /** * Returns duration of currently played sound file in milliseconds. */ public long getPlaybackDuration() { return handleNumericLongGetRequest("GetPlaybackDurationInMs()"); } /** * Returns recording position in milliseconds. */ public long getRecordingPosition() { return handleNumericLongGetRequest("GetRecordingPositionInMs()"); } /** * Returns current recording peak. */ public long getRecordingPeak() { return handleNumericLongGetRequest("GetRecordingPeak()"); } /** * Get entire sound list. Will return the sounds from the All sounds * category. The All sounds category is hidden by default and can be * accessed in Soundpad from the menu at Window > Categories > All sounds. * * @return xml formatted sound list */ public String getSoundlist() { return handleStringGetRequest("GetSoundlist()"); } /** * Get a section of the sound list from the given index to the end. * * @see #getSoundlist() * * @param fromIndex * starts with 1 * @return xml formatted sound list */ public String getSoundlist(final int fromIndex) { return handleStringGetRequest("GetSoundlist(" + fromIndex + ")"); } /** * Get a section of the sound list. * * @see #getSoundlist() * * @param fromIndex * starts with 1 * @param toIndex * the sound file at toIndex is included in the response * @return xml formatted sound list */ public String getSoundlist(final int fromIndex, final int toIndex) { return handleStringGetRequest("GetSoundlist(" + fromIndex + "," + toIndex + ")"); } public String getMainFrameTitleText() { return handleSimpleGetRequest("GetTitleText()"); } public String getStatusBarText() { return handleSimpleGetRequest("GetStatusBarText()"); } public PlayStatus getPlayStatus() { String response = sendRequestNoException("GetPlayStatus()"); if (response.startsWith("R")) { printError(response); } try { return PlayStatus.valueOf(response); } catch (IllegalArgumentException ignore) { return PlayStatus.STOPPED; } } /** * Returns the version of Soundpad. Not the version of the remote control * interface. */ public String getVersion() { return handleEmptyGetRequest("GetVersion()"); } public String getRemoteControlVersion() { return handleEmptyGetRequest("GetRemoteControlVersion()"); } /** * Add sound at the end of the list. * * @param url * full path and file name, e.g. C:\mysounds\sound.mp3 * @return false if sound does not exist or input is malformed. */ public boolean addSound(final String url) { return isSuccessPrintResponse(sendRequestNoException("DoAddSound(\"" + url + "\")")); } /** * Add sound at the given index to the list. * * @param url * full path and file name, e.g. C:\mysounds\sound.mp3 * @return false if sound does not exist or input is malformed. */ public boolean addSound(final String url, final int insertAtIndex) { return isSuccessPrintResponse( sendRequestNoException("DoAddSound(\"" + url + "\", " + insertAtIndex + ")")); } /** * Removes selected sound file entries. * * @return true on success */ public boolean removeSelectedEntries() { return removeSelectedEntries(false); } /** * @see #removeSelectedEntries() * * @param removeOnDiskToo * triggers confirmation dialog if true */ public boolean removeSelectedEntries(final boolean removeOnDiskToo) { return isSuccess(sendRequestNoException("DoRemoveSelectedEntries(" + removeOnDiskToo + ")")); } /** * Undo last action. Same as Edit > Undo in Soundpad. */ public boolean undo() { return isSuccess(sendRequestNoException("DoUndo()")); } /** * Redo last action. Same as Edit > Redo in Soundpad. */ public boolean redo() { return isSuccess(sendRequestNoException("DoRedo()")); } /** * Shows file selection dialog if sound list was never saved before. */ public boolean saveSoundlist() { return isSuccess(sendRequestNoException("DoSaveSoundlist()")); } /** * @return volume between 0 and 100. */ public int getVolume() { String response = sendRequestNoException("GetVolume()"); if (response.isEmpty()) { printOfflineError(); } else { try { return Integer.parseInt(response); } catch (NumberFormatException e) { printNumericError(response); } } return 0; } /** * @return true if the volume of the speakers is 0 or muted. */ public boolean isMuted() { String response = sendRequestNoException("IsMuted()"); if (response.isEmpty()) { printOfflineError(); } else { try { return Integer.parseInt(response) == 1; } catch (NumberFormatException e) { printNumericError(response); } } return false; } /** * Change volume of the speakers. * * @param volume * a value between 0 and 100. */ public boolean setVolume(final int volume) { return isSuccess(sendRequestNoException("SetVolume(" + volume + ")")); } /** * Mutes or unmutes speakers in Soundpad. */ public boolean toggleMute() { return isSuccess(sendRequestNoException("DoToggleMute()")); } /** * @return true if this client class uses the same remote control interface * version as Soundpad. */ public boolean isCompatible() { return CLIENT_VERSION.equals(getRemoteControlVersion()); } /** * @return true if Soundpad is running and the remote control interface is * accessible. */ public boolean isAlive() { return isSuccess(sendRequestNoException("IsAlive()")); } //***********************************************************************// // // Remote Control v1.1.0 // //***********************************************************************// public boolean playSelectedSound() { return isSuccess(sendRequestNoException("DoPlaySelectedSound()")); } public boolean playCurrentSoundAgain() { return isSuccess(sendRequestNoException("DoPlayCurrentSoundAgain()")); } public boolean playPreviouslyPlayedSound() { return isSuccess(sendRequestNoException("DoPlayPreviouslyPlayedSound()")); } /** * Add a category at the bottom of the category list. * * @param name * Name of the category, must not be empty. * @return true on success */ public boolean addCategory(final String name) { return addCategory(name, -1); } /** * Add a category. * * @param name * Name of the category, must not be empty. * @param parentCategoryIndex * Set to -1 to add it at the bottom or set a category index to * add it at the bottom of that category. Use * {@link #getCategories(boolean, boolean)} to find the index of * a category. * @return true on success */ public boolean addCategory(final String name, final int parentCategoryIndex) { return isSuccessPrintResponse( sendRequestNoException("DoAddCategory(\"" + name + "\", " + parentCategoryIndex + ")")); } /** * Add sound to a particular category and position there-in. * * @param url * full path and file name, e.g. C:\mysounds\sound.mp3 * @return true on success */ public boolean addSound(final String url, final int categoryIndex, final int insertAtPosition) { return isSuccessPrintResponse(sendRequestNoException( "DoAddSound(\"" + url + "\", " + categoryIndex + ", " + insertAtPosition + ")")); } /** * Start recording of the speakers. Method might fail if the microphone is * currently being recorded. This call is handled the same way as if a * recording is started by hotkeys, which means a notification sound is * played. This is the default behavior, but the notification sound can be * turned off in Soundpad. * * @return true if recording was started or was already running */ public boolean startRecordingSpeakers() { return isSuccessPrintResponse(sendRequestNoException("DoStartRecordingSpeakers()")); } /** * Start recording of the microphone. Method might fail if the speakers are * currently being recorded. This call is handled the same way as if a * recording is started by hotkeys, which means a notification sound is * played. This is the default behavior, but the notification sound can be * turned off in Soundpad. * * @return true if recording was started or was already running */ public boolean startRecordingMicrophone() { return isSuccessPrintResponse(sendRequestNoException("DoStartRecordingMicrophone()")); } /** * Select the category identified by its index. Use * {@link #getCategories(boolean, boolean)} to get the index. * * @param categoryIndex * The index of the category to be selected. * @return true on success */ public boolean selectCategory(final int categoryIndex) { return isSuccessPrintResponse(sendRequestNoException("DoSelectCategory(" + categoryIndex + ")")); } public boolean selectPreviousCategory() { return isSuccess(sendRequestNoException("DoSelectPreviousCategory()")); } public boolean selectNextCategory() { return isSuccess(sendRequestNoException("DoSelectNextCategory()")); } /** * Remove a category identified by its index. Use * {@link #getCategories(boolean, boolean)} to get the index. * * @param categoryIndex * The index of the category to be removed. * @return true on success */ public boolean removeCategory(final int categoryIndex) { return isSuccessPrintResponse(sendRequestNoException("DoRemoveCategory(" + categoryIndex + ")")); } /** * Get the category tree. * * @param withSounds * includes all sound entries of each category into the response * @param withIcons * base64 encoded PNGs * @return xml formatted category list */ public String getCategories(final boolean withSounds, final boolean withIcons) { String cmd = String.format("GetCategories(%b, %b)", withSounds, withIcons); String response = sendRequestNoException(cmd); if (response.startsWith("R")) { printError(response); } else if (response.isEmpty()) { printOfflineError(); } return response; } /** * Get a category identified by its index. Use * {@link #getCategories(boolean, boolean)} to get the index. * * @param withSounds * includes all sound entries associated to that category * @param withIcons * base64 encoded PNG * @return xml formatted category list */ public String getCategory(final int categoryIndex, final boolean withSounds, final boolean withIcons) { String cmd = String.format("GetCategory(%d, %b, %b)", categoryIndex, withSounds, withIcons); String response = sendRequestNoException(cmd); if (response.startsWith("R")) { printError(response); } else if (response.isEmpty()) { printOfflineError(); } return response; } //***********************************************************************// // // Remote Control v1.1.1 // //***********************************************************************// /** * Let Soundpad play a sound from a particular category. * * @param categoryIndex * set to -1 to play a sound from the currently selected category * or use {@link #getCategories(boolean, boolean)} to find the * index of a category. * @param soundIndex * it's not the index as used in {@link #playSound(int)}, but the * position in the category, e.g. 5 = 5th sound in the category. * @param renderLine * set to true to play on speakers so you hear it. * @param captureLine * set to true to play on microphone so others hear it. * @return true on success */ public boolean playSoundFromCategory(final int categoryIndex, final int soundIndex, final boolean renderLine, final boolean captureLine) { return isSuccessPrintResponse(sendRequestNoException("DoPlaySoundFromCategory(" + categoryIndex + ", " + soundIndex + ", " + renderLine + ", " + captureLine + ")")); } //***********************************************************************// // // Remote Control v1.1.2 // //***********************************************************************// /** * Let Soundpad play a random sound from any category. * * @param renderLine * set to true to play on speakers so you hear it. * @param captureLine * set to true to play on microphone so others hear it. * @return true on success */ public boolean playRandomSound(final boolean renderLine, final boolean captureLine) { return isSuccessPrintResponse( sendRequestNoException("DoPlayRandomSound(" + renderLine + ", " + captureLine + ")")); } /** * Let Soundpad play a random sound from a particular category. * * @param categoryIndex * set to -1 to play a random sound from the currently selected * category or use {@link #getCategories(boolean, boolean)} to * find the index of a category. * @param renderLine * set to true to play on speakers so you hear it. * @param captureLine * set to true to play on microphone so others hear it. * @return true on success */ public boolean playRandomSoundFromCategory(final int categoryIndex, final boolean renderLine, final boolean captureLine) { return isSuccessPrintResponse(sendRequestNoException( "DoPlayRandomSoundFromCategory(" + categoryIndex + ", " + renderLine + ", " + captureLine + ")")); } /** * @return true if the client is using the trial version or false if it is * the full version. */ public boolean isTrial() { String response = sendRequestNoException("IsTrial()"); if (response.isEmpty()) { printOfflineError(); } else { try { return Integer.parseInt(response) == 1; } catch (NumberFormatException e) { printNumericError(response); } } return false; } }