diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml index 39329f5c..d0051597 100644 --- a/.idea/dictionaries/denny.xml +++ b/.idea/dictionaries/denny.xml @@ -10,6 +10,7 @@ talkgroup tgid ultravox + xtea \ No newline at end of file diff --git a/src/audio/broadcast/BroadcastState.java b/src/audio/broadcast/BroadcastState.java index 818b8553..7c04ae6e 100644 --- a/src/audio/broadcast/BroadcastState.java +++ b/src/audio/broadcast/BroadcastState.java @@ -65,6 +65,11 @@ public enum BroadcastState */ UNKNOWN_HOST("Unknown Host or Invalid Port", true), + /** + * Unsupported audio format + */ + UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true), + /** * Unspecified error */ diff --git a/src/audio/broadcast/BroadcasterFactory.java b/src/audio/broadcast/BroadcasterFactory.java index 91f1f260..cc416e1a 100644 --- a/src/audio/broadcast/BroadcasterFactory.java +++ b/src/audio/broadcast/BroadcasterFactory.java @@ -20,6 +20,8 @@ package audio.broadcast; import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration; import audio.broadcast.shoutcast.v1.ShoutcastV1Handler; +import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration; +import audio.broadcast.shoutcast.v2.ShoutcastV2Handler; import audio.convert.IAudioConverter; import audio.convert.MP3AudioConverter; import controller.ThreadPoolManager; @@ -51,6 +53,9 @@ public class BroadcasterFactory case SHOUTCAST_V1: return new Broadcaster(threadPoolManager, new ShoutcastV1Handler((ShoutcastV1Configuration)configuration, converter)); + case SHOUTCAST_V2: + return new Broadcaster(threadPoolManager, + new ShoutcastV2Handler((ShoutcastV2Configuration)configuration, converter)); case UNKNOWN: break; } @@ -70,7 +75,6 @@ public class BroadcasterFactory { case MP3: return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_NO_VARIABLE_BITRATE); - } return null; diff --git a/src/audio/broadcast/shoutcast/v1/ShoutcastV1Handler.java b/src/audio/broadcast/shoutcast/v1/ShoutcastV1Handler.java index 7e4d0dcc..b15d35ba 100644 --- a/src/audio/broadcast/shoutcast/v1/ShoutcastV1Handler.java +++ b/src/audio/broadcast/shoutcast/v1/ShoutcastV1Handler.java @@ -133,12 +133,12 @@ public class ShoutcastV1Handler extends BroadcastHandler { if(!connected() && System.currentTimeMillis() - mLastConnectionAttempt >= RECONNECT_INTERVAL_MILLISECONDS) { + mLastConnectionAttempt = System.currentTimeMillis(); + createSocket(); if(mSocket.isConnected()) { - mLog.debug("Sending credentials"); - StringBuilder sb = new StringBuilder(); //Password @@ -186,7 +186,6 @@ public class ShoutcastV1Handler extends BroadcastHandler */ private void send(String data) throws IOException { - mLog.debug("Sending [" + data + "]"); if(data != null && !data.isEmpty() && mOutputStream != null) { mOutputStream.writeBytes(data); @@ -200,7 +199,6 @@ public class ShoutcastV1Handler extends BroadcastHandler */ private void send(byte[] data) throws IOException { - mLog.debug("Sending bytes:" + data.length); if(connected()) { int sent = 0; @@ -344,7 +342,7 @@ public class ShoutcastV1Handler extends BroadcastHandler config.setAlias("Test Configuration"); config.setHost("localhost"); config.setPort(8000); - config.setPassword("denny3"); + config.setPassword("denny3:#1"); config.setStreamName("Denny's Audio Broadcast Test"); config.setGenre("Scanner Audio"); config.setPublic(true); diff --git a/src/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java b/src/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java index f42fd780..eb121759 100644 --- a/src/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java +++ b/src/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java @@ -24,14 +24,13 @@ import audio.broadcast.BroadcastServerType; public class ShoutcastV2Configuration extends BroadcastConfiguration { - private String mMountPoint; - private String mStreamName; - private String mSongTitle; - private String mGenre; - private boolean mPublic; - private int mChannels; + private int mStreamID; + private String mUserID; private int mBitRate; + private String mStreamName; + private String mStreamGenre; private String mURL; + private boolean mPublic = true; public ShoutcastV2Configuration(BroadcastFormat format) @@ -42,26 +41,60 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration @Override public BroadcastServerType getBroadcastServerType() { - return BroadcastServerType.SHOUTCAST_V1; - } - - public String getMountPoint() - { - return mMountPoint; + return BroadcastServerType.SHOUTCAST_V2; } /** - * Sets the optional mount point. Shoutcast v1.x servers don't require a mount point, but v2.x servers do require - * the mount point. - * - * Note: if the supplied mount point contains a leading slash (/) it will be removed. - * - * @param mountPoint + * Stream ID */ - public void setMountPoint(String mountPoint) + public int getStreamID() { - //Strip the leading slash from the mount point if the user supplies it - mMountPoint = mountPoint.replace("/", ""); + return mStreamID; + } + + /** + * Sets the stream ID + * + * @param streamID numeric stream identifier between 1 - 2147483647 + */ + public void setStreamID(int streamID) + { + assert(1 <= streamID && streamID <= Integer.MAX_VALUE); + mStreamID = streamID; + } + + /** + * User ID + */ + public String getUserID() + { + return mUserID; + } + + /** + * Sets the user ID + * @param userID + */ + public void setUserID(String userID) + { + mUserID = userID; + } + + /** + * Bit rate in kilobits per second + */ + public int getBitRate() + { + return mBitRate; + } + + /** + * Bit rate + * @param bitRate in kilobits per second + */ + public void setBitRate(int bitRate) + { + mBitRate = bitRate; } /** @@ -81,37 +114,20 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration mStreamName = name; } - /** - * Song title - */ - public String getSongTitle() - { - return mSongTitle; - } - - /** - * Set the song title - */ - public void setSongTitle(String title) - { - mSongTitle = title; - } - /** * Stream genre */ - public String getGenre() + public String getStreamGenre() { - return mGenre; + return mStreamGenre; } /** - * Stream genre - * @param genre + * Sets the genre tag for this stream */ - public void setGenre(String genre) + public void setStreamGenre(String genre) { - mGenre = genre; + mStreamGenre = genre; } /** @@ -131,39 +147,6 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration mPublic = isPublic; } - /** - * Number of audio channels in the broadcast - */ - public int getChannels() - { - return mChannels; - } - - /** - * Sets the number of audio channels in the broadcast - */ - public void setChannels(int channels) - { - mChannels = channels; - } - - /** - * Bit rate in bits per second - */ - public int getBitRate() - { - return mBitRate; - } - - /** - * Bit rate - * @param bitRate in samples per second - */ - public void setBitRate(int bitRate) - { - mBitRate = bitRate; - } - /** * URL associated with the broadcast where users can find additional details. */ diff --git a/src/audio/broadcast/shoutcast/v2/ShoutcastV2Handler.java b/src/audio/broadcast/shoutcast/v2/ShoutcastV2Handler.java new file mode 100644 index 00000000..3144870c --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/ShoutcastV2Handler.java @@ -0,0 +1,570 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2; + +import audio.AudioPacket; +import audio.broadcast.BroadcastFormat; +import audio.broadcast.BroadcastHandler; +import audio.broadcast.BroadcastState; +import audio.broadcast.Broadcaster; +import audio.broadcast.BroadcasterFactory; +import audio.broadcast.shoutcast.v2.message.AuthenticateBroadcast; +import audio.broadcast.shoutcast.v2.message.ConfigureIcyGenre; +import audio.broadcast.shoutcast.v2.message.ConfigureIcyName; +import audio.broadcast.shoutcast.v2.message.ConfigureIcyPublic; +import audio.broadcast.shoutcast.v2.message.ConfigureIcyURL; +import audio.broadcast.shoutcast.v2.message.MP3Audio; +import audio.broadcast.shoutcast.v2.message.NegotiateBufferSize; +import audio.broadcast.shoutcast.v2.message.NegotiateMaxPayloadSize; +import audio.broadcast.shoutcast.v2.message.RequestCipher; +import audio.broadcast.shoutcast.v2.message.SetupBroadcast; +import audio.broadcast.shoutcast.v2.message.Standby; +import audio.broadcast.shoutcast.v2.message.UltravoxMessage; +import audio.broadcast.shoutcast.v2.message.UltravoxMessageFactory; +import audio.broadcast.shoutcast.v2.message.UltravoxMessageType; +import audio.broadcast.shoutcast.v2.message.UltravoxMetadata; +import audio.broadcast.shoutcast.v2.message.XMLMetadata; +import audio.convert.IAudioConverter; +import controller.ThreadPoolManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import record.wave.AudioPacketMonoWaveReader; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ShoutcastV2Handler extends BroadcastHandler +{ + private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2Handler.class ); + + private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds + private long mLastConnectionAttempt = 0; + + private Socket mSocket; + private DataOutputStream mOutputStream; + private DataInputStream mInputStream; + private int mMaxPayloadSize = 16377; + + /** + * Creates a Shoutcast Version 1 broadcast handler. + * @param configuration + * @param audioConverter + */ + public ShoutcastV2Handler(ShoutcastV2Configuration configuration, IAudioConverter audioConverter) + { + super(configuration, audioConverter); + } + + /** + * Shoutcast V2 Configuration information + */ + private ShoutcastV2Configuration getShoutcastConfiguration() + { + return (ShoutcastV2Configuration)getBroadcastConfiguration(); + } + + /** + * Broadcast audio packets + * + * @param audioPackets + */ + @Override + public void broadcast(List audioPackets) + { + //If we're connected, send the audio, otherwise discard it + if(connect()) + { + byte[] convertedAudio = mAudioConverter.convert(audioPackets); + + if(convertedAudio != null) + { + List audioMessages = getAudioMessages(convertedAudio); + + for(UltravoxMessage audioMessage: audioMessages) + { + try + { + send(audioMessage, false); + } + catch(IOException e) + { + mLog.error("Error while dispatching audio", e); + setBroadcastState(BroadcastState.BROADCAST_ERROR); + return; + } + } + } + } + else + { + mLog.debug("Not connected"); + } + } + + /** + * Indicates if the audio handler is currently connected to the remote server and capable of streaming audio. + * + * This method will attempt to establish a connection or reconnect to the streaming server. + * @return true if the audio handler can stream audio + */ + private boolean connect() + { + if(!connected()) + { + createConnection(); + } + + return connected(); + } + + + /** + * Disconnect from the remote server + */ + @Override + public void disconnect() + { +// TerminateBroadcast terminate = (TerminateBroadcast)UltravoxMessageFactory +// .getMessage(UltravoxMessageType.TERMINATE_BROADCAST); +// response = send(terminate, out, in); +// mLog.debug("Terminate Response:" + (response.getPayload() != null ? response.getPayload() : "")); + + } + + /** + * Sends the message. When get response is true, reads from the input stream and converts the response to a + * message that is returned. + * + * @param message to send to the shoutcast/ultravox server + * @param getResponse true if the sent message will generate a response + * @return response message when getResponse is set to true + * @throws IOException if there is an issue while communicating with the server + */ + private UltravoxMessage send(UltravoxMessage message, boolean getResponse) throws IOException + { + byte[] bytes = message.getMessage(); + + mOutputStream.write(bytes); + + if(getResponse) + { + try + { + Thread.sleep(50); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + + int available = mInputStream.available(); + + if(available > 0) + { + byte[] buffer = new byte[available]; + int read = mInputStream.read(buffer); + + return UltravoxMessageFactory.getMessage(buffer); + } + } + + return null; + } + + /** + * Loads the encoded audio byte array into a list of ultravox messages ready for transmission + * @param audio to embed as message payload + * @return list of messages containing the audio payload + */ + private List getAudioMessages(byte[] audio) + { + List messages = new ArrayList<>(); + + int pointer = 0; + + while(pointer < audio.length) + { + int payloadSize = Math.min(mMaxPayloadSize, audio.length - pointer); + + byte[] payload = new byte[payloadSize]; + + System.arraycopy(audio, pointer, payload, 0, payloadSize); + + MP3Audio mp3Audio = (MP3Audio)UltravoxMessageFactory.getMessage(UltravoxMessageType.MP3_DATA); + mp3Audio.setPayload(payload); + messages.add(mp3Audio); + + pointer += payloadSize; + } + + return messages; + } + + /** + * Encodes the list of metadata in one or more Ultravox XMLMetadata messages according to the maximum negotiated + * payload size for the current connection. Each entry in the list of metadata strings should be an xml encoded + * value: value + * + * See UltravoxMetadata.TAG.asXML(String value) + * + * @param xmlMetadata list of xml encoded values + * @return a sequence of XMLMetadata messages sufficient to carry the complete set of metadata + */ + private List getMetadataMessages(List xmlMetadata) + { + List messages = new ArrayList<>(); + + StringBuilder sb = new StringBuilder(); + + sb.append("\n\n"); + + for(String metadata: xmlMetadata) + { + sb.append(metadata); + } + + sb.append("\n"); + + mLog.debug("Metadata:" + sb.toString()); + byte[] xml = sb.toString().getBytes(); + + int pointer = 0; + int messageCounter = 1; + int messageCount = (int)Math.ceil((double)xml.length / (double)(mMaxPayloadSize - 6)); + + if(messageCount > 32) + { + messageCount = 32; //Max number of metadata messages in a sequence + } + + + while(pointer < xml.length && messageCounter <= messageCount) + { + int payloadSize = Math.min(mMaxPayloadSize - 6, xml.length - pointer); + + byte[] payload = new byte[payloadSize + 6]; + + payload[1] = (byte)(0x01); + payload[3] = (byte)(messageCount & 0xFF); + payload[5] = (byte)(messageCounter & 0xFF); + + System.arraycopy(xml, pointer, payload, 6, payloadSize); + + XMLMetadata message = (XMLMetadata)UltravoxMessageFactory.getMessage(UltravoxMessageType.XML_METADATA); + message.setPayload(payload); + messages.add(message); + + pointer += payloadSize; + } + + return messages; + } + + + /** + * Creates a connnection to the remote server using the shoutcast configuration information. Once disconnected + * following a successful connection, attempts to reestablish a connection on a set interval + */ + private void createConnection() + { + if(!connected() && System.currentTimeMillis() - mLastConnectionAttempt >= RECONNECT_INTERVAL_MILLISECONDS) + { + mLastConnectionAttempt = System.currentTimeMillis(); + + createSocket(); + + if (mSocket.isConnected()) + { + RequestCipher requestCipher = (RequestCipher) UltravoxMessageFactory + .getMessage(UltravoxMessageType.REQUEST_CIPHER); + + try + { + UltravoxMessage response = send(requestCipher, true); + + if (response instanceof RequestCipher) + { + RequestCipher cipherResponse = (RequestCipher) response; + + AuthenticateBroadcast authenticateBroadcast = (AuthenticateBroadcast) UltravoxMessageFactory + .getMessage(UltravoxMessageType.AUTHENTICATE_BROADCAST); + authenticateBroadcast.setCredentials(cipherResponse.getCipher(), + getShoutcastConfiguration().getStreamID(), getShoutcastConfiguration().getUserID(), + getShoutcastConfiguration().getPassword()); + response = send(authenticateBroadcast, true); + + if (response.isErrorResponse()) + { + setBroadcastState(BroadcastState.INVALID_PASSWORD); + return; + } + + UltravoxMessage streamMimeType = UltravoxMessageFactory + .getMessage(UltravoxMessageType.STREAM_MIME_TYPE); + streamMimeType.setPayload(getShoutcastConfiguration().getBroadcastFormat().getValue()); + response = send(streamMimeType, true); + + if (response.isErrorResponse()) + { + setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT); + mLog.error("Audio format [" + getShoutcastConfiguration().getBroadcastFormat() + "] not supported"); + return; + } + + SetupBroadcast setupBroadcast = (SetupBroadcast) UltravoxMessageFactory + .getMessage(UltravoxMessageType.SETUP_BROADCAST); + setupBroadcast.setBitRate(getShoutcastConfiguration().getBitRate(), + getShoutcastConfiguration().getBitRate()); + response = send(setupBroadcast, true); + + if (response.isErrorResponse()) + { + setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT); + mLog.error("Audio bit rate [" + getShoutcastConfiguration().getBitRate() + "] not supported"); + return; + } + + NegotiateMaxPayloadSize negotiateMaxPayloadSize = (NegotiateMaxPayloadSize) UltravoxMessageFactory + .getMessage(UltravoxMessageType.NEGOTIATE_MAX_PAYLOAD_SIZE); + negotiateMaxPayloadSize.setMaximumPayloadSize(mMaxPayloadSize, 14000); + response = send(negotiateMaxPayloadSize, true); + mMaxPayloadSize = ((NegotiateMaxPayloadSize) response).getMaximumPayloadSize(); + + List metadata = new ArrayList<>(); + + if (getShoutcastConfiguration().getStreamName() != null) + { + metadata.add(UltravoxMetadata.TITLE_1.asXML(getShoutcastConfiguration().getStreamName())); + + ConfigureIcyName icyName = (ConfigureIcyName) UltravoxMessageFactory + .getMessage(UltravoxMessageType.CONFIGURE_ICY_NAME); + icyName.setName(getShoutcastConfiguration().getStreamName()); + response = send(icyName, false); + } + + if (getShoutcastConfiguration().getStreamGenre() != null) + { + metadata.add(UltravoxMetadata.GENRE.asXML(getShoutcastConfiguration().getStreamGenre())); + + ConfigureIcyGenre icyGenre = (ConfigureIcyGenre) UltravoxMessageFactory + .getMessage(UltravoxMessageType.CONFIGURE_ICY_GENRE); + icyGenre.setGenre(getShoutcastConfiguration().getStreamGenre()); + response = send(icyGenre, false); + } + + if (getShoutcastConfiguration().getURL() != null) + { + ConfigureIcyURL icyURL = (ConfigureIcyURL) UltravoxMessageFactory + .getMessage(UltravoxMessageType.CONFIGURE_ICY_URL); + icyURL.setURL(getShoutcastConfiguration().getURL()); + response = send(icyURL, false); + } + + ConfigureIcyPublic icyPublic = (ConfigureIcyPublic) UltravoxMessageFactory + .getMessage(UltravoxMessageType.CONFIGURE_ICY_PUBLIC); + icyPublic.setPublic(getShoutcastConfiguration().isPublic()); + response = send(icyPublic, false); + + Standby standby = (Standby) UltravoxMessageFactory.getMessage(UltravoxMessageType.STANDBY); + response = send(standby, true); + + if (response.isValidResponse()) + { + setBroadcastState(BroadcastState.CONNECTED); + } + else + { + setBroadcastState(BroadcastState.DISCONNECTED); + mLog.error("Error message after configuration and sending standby:" + + (response.getError() != null ? response.getError() : "no error message")); + } + + metadata.add(UltravoxMetadata.BROADCAST_CLIENT_APPLICATION.asXML("sdrtrunk")); + + mLog.debug("Fetching metadata messages from metadata..."); + List messages = getMetadataMessages(metadata); + + for (UltravoxMessage message : messages) + { + mLog.debug("Sending metadata message"); + send(message, true); + } + + mLog.debug("We're connected!!!"); + } + } + catch (IOException e) + { + setBroadcastState(BroadcastState.ERROR); + mLog.error("Error while creating connection to server", e); + } + } + } + } + + /** + * Creates a socket connection to the remote server and sets the state to CONNECTING. + */ + private void createSocket() + { + if(mSocket == null) + { + try + { + mSocket = new Socket(getShoutcastConfiguration().getHost(), + getShoutcastConfiguration().getPort()); + mOutputStream = new DataOutputStream(mSocket.getOutputStream()); + mInputStream = new DataInputStream(mSocket.getInputStream()); + } + catch(UnknownHostException uhe) + { + setBroadcastState(BroadcastState.UNKNOWN_HOST); + mLog.error("Unknown host or port. Unable to create connection to streaming server host[" + + getShoutcastConfiguration().getHost() + "] and port[" + + getShoutcastConfiguration().getPort() + "] - will reattempt connection periodically"); + return; + } + catch(IOException ioe) + { + setBroadcastState(BroadcastState.ERROR); + + mLog.error("Error connecting to streaming server host[" + + getShoutcastConfiguration().getHost() + "] and port[" + + getShoutcastConfiguration().getPort() + "]", ioe); + return; + } + } + + if(mSocket.isConnected()) + { + setBroadcastState(BroadcastState.CONNECTING); + } + else + { + try + { + SocketAddress address = getShoutcastConfiguration().getAddress(); + mSocket.connect(address); + setBroadcastState(BroadcastState.CONNECTING); + } + catch(UnknownHostException uhe) + { + setBroadcastState(BroadcastState.UNKNOWN_HOST); + } + catch(IOException e) + { + setBroadcastState(BroadcastState.ERROR); + + mLog.error("Error connecting to streaming server host[" + + getShoutcastConfiguration().getHost() + "] and port[" + + getShoutcastConfiguration().getPort() + "]", e); + } + } + } + + public static UltravoxMessage send(UltravoxMessage message, + DataOutputStream out, + DataInputStream in, + int maxPayloadSize) throws IOException + { + byte[] bytes = message.getMessage(); + + out.write(bytes); + + try + { + Thread.sleep(50); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + + int available = in.available(); + + if(available > 0) + { + byte[] buffer = new byte[available]; + int read = in.read(buffer); + + return UltravoxMessageFactory.getMessage(buffer); + } + + return null; + } + + + public static void main(String[] args) + { + boolean test = false; + + ShoutcastV2Configuration config = new ShoutcastV2Configuration(BroadcastFormat.MP3); + config.setStreamID(1); + config.setAlias("Test Configuration"); + config.setHost("localhost"); + config.setPort(8000); + config.setPassword("denny3"); + config.setStreamName("Denny's Audio Broadcast Test"); + config.setStreamGenre("Public Safety"); + config.setPublic(true); + config.setURL("http://localhost:8000"); + config.setBitRate(16); + + if(test) + { + } + else + { + ThreadPoolManager threadPoolManager = new ThreadPoolManager(); + + final Broadcaster broadcaster = BroadcasterFactory.getBroadcaster(threadPoolManager,config); + + Path path = Paths.get("/home/denny/Music/PCM.wav"); + mLog.debug("Opening: " + path.toString()); + + mLog.debug("Registering and starting audio playback"); + + while(true) + { + mLog.debug("Playback started [" + path.toString() + "]"); + + try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true)) + { + reader.setListener(broadcaster); + reader.read(); + } + catch (IOException e) + { + mLog.error("Error", e); + } + + mLog.debug("Playback ended [" + path.toString() + "]"); + } + } + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/AuthenticateBroadcast.java b/src/audio/broadcast/shoutcast/v2/message/AuthenticateBroadcast.java new file mode 100644 index 00000000..a35de1b3 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/AuthenticateBroadcast.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +import util.XTEA; + +import java.util.Arrays; + +public class AuthenticateBroadcast extends UltravoxMessage +{ + /** + * Client request to server to send authentication credentials + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + AuthenticateBroadcast() + { + super(UltravoxMessageType.AUTHENTICATE_BROADCAST); + } + + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + AuthenticateBroadcast(byte[] data) + { + super(data); + } + + /** + * Set credential to authenticate the stream with the server + * + * @param encryptionKey received from the server in the RequestCipher message + * @param streamID that we're going to stream to + * @param userID for the stream. Note: this is optional and can be null. The server currently ignores this value. + * @param password for the stream. + */ + public void setCredentials(String encryptionKey, int streamID, String userID, String password) + { + XTEA xtea = new XTEA(encryptionKey); + + StringBuilder sb = new StringBuilder(); + + sb.append(SHOUTCAST_VERSION); + sb.append(":"); + sb.append(streamID); + sb.append(":"); + sb.append(userID != null ? encrypt(xtea, userID) : ""); + sb.append(":"); + sb.append(password != null ? encrypt(xtea, password) : ""); + + setPayload(sb.toString()); + } + + /** + * Encrypts the value using the XTEA block cipher algorithm and returns the encrypted value as a 16 hex value + * + * @param xtea instance of the XTEA algorithm with a loaded encryption key + * @param value to encode + * @return encrypted password as 64-bit, 16 character hexadecimal value + */ + private static String encrypt(XTEA xtea, String value) + { + byte[] valueBytes = value.getBytes(); + + if(valueBytes.length % 8 != 0 || valueBytes.length == 0) + { + int newLength = ((valueBytes.length / 8) + 1) * 8; + valueBytes = Arrays.copyOf(valueBytes, newLength); + } + + byte[] encrypted = xtea.encrypt(valueBytes); + + StringBuilder sb = new StringBuilder(); + + for(byte b: encrypted) + { + sb.append(String.format("%02x", b)); + } + + return sb.toString(); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyGenre.java b/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyGenre.java new file mode 100644 index 00000000..2c720844 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyGenre.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class ConfigureIcyGenre extends UltravoxMessage +{ + /** + * Client request to server to configure the ICY-GENRE metadata + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + ConfigureIcyGenre() + { + super(UltravoxMessageType.CONFIGURE_ICY_GENRE); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + ConfigureIcyGenre(byte[] data) + { + super(data); + } + + public void setGenre(String genre) + { + setPayload(genre); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyName.java b/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyName.java new file mode 100644 index 00000000..4a4490da --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyName.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +import util.XTEA; + +import java.util.Arrays; + +public class ConfigureIcyName extends UltravoxMessage +{ + /** + * Client request to server to configure the ICY-NAME metadata + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + ConfigureIcyName() + { + super(UltravoxMessageType.CONFIGURE_ICY_NAME); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + ConfigureIcyName(byte[] data) + { + super(data); + } + + public void setName(String name) + { + setPayload(name); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyPublic.java b/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyPublic.java new file mode 100644 index 00000000..0b53d977 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyPublic.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class ConfigureIcyPublic extends UltravoxMessage +{ + /** + * Client request to server to configure the ICY-PUBLIC metadata + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + ConfigureIcyPublic() + { + super(UltravoxMessageType.CONFIGURE_ICY_PUBLIC); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + ConfigureIcyPublic(byte[] data) + { + super(data); + } + + public void setPublic(boolean publicStream) + { + setPayload(publicStream ? "1" : "0"); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyURL.java b/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyURL.java new file mode 100644 index 00000000..87c5f9cb --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/ConfigureIcyURL.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class ConfigureIcyURL extends UltravoxMessage +{ + /** + * Client request to server to configure the ICY-URL metadata + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + ConfigureIcyURL() + { + super(UltravoxMessageType.CONFIGURE_ICY_URL); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + ConfigureIcyURL(byte[] data) + { + super(data); + } + + public void setURL(String url) + { + setPayload(url); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/FlushCachedMetadata.java b/src/audio/broadcast/shoutcast/v2/message/FlushCachedMetadata.java new file mode 100644 index 00000000..3defeaf3 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/FlushCachedMetadata.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class FlushCachedMetadata extends UltravoxMessage +{ + /** + * Client request to server to flush any currently cached stream metadata + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + FlushCachedMetadata() + { + super(UltravoxMessageType.FLUSH_CACHED_METADATA); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + FlushCachedMetadata(byte[] data) + { + super(data); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/UltravoxMessageType.java b/src/audio/broadcast/shoutcast/v2/message/MP3Audio.java similarity index 60% rename from src/audio/broadcast/shoutcast/v2/UltravoxMessageType.java rename to src/audio/broadcast/shoutcast/v2/message/MP3Audio.java index 62f9151c..27247282 100644 --- a/src/audio/broadcast/shoutcast/v2/UltravoxMessageType.java +++ b/src/audio/broadcast/shoutcast/v2/message/MP3Audio.java @@ -16,34 +16,29 @@ * along with this program. If not, see * ******************************************************************************/ -package audio.broadcast.shoutcast.v2; +package audio.broadcast.shoutcast.v2.message; -public enum UltravoxMessageType +public class MP3Audio extends UltravoxMessage { - AUTHENTICATE_BROADCAST(0x1001), - - UNKNOWN(0x0000); - - private int mValue; - - private UltravoxMessageType(int value) + /** + * Client request to server to send authentication credentials + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + MP3Audio() { - mValue = value; + super(UltravoxMessageType.MP3_DATA); } - public int getValue() - { - return mValue; - } - public static UltravoxMessageType fromValue(int value) + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + MP3Audio(byte[] data) { - switch(value) - { - case 0x1001: - return AUTHENTICATE_BROADCAST; - default: - return UNKNOWN; - } + super(data); } } diff --git a/src/audio/broadcast/shoutcast/v2/message/NegotiateBufferSize.java b/src/audio/broadcast/shoutcast/v2/message/NegotiateBufferSize.java new file mode 100644 index 00000000..9e7523da --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/NegotiateBufferSize.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class NegotiateBufferSize extends UltravoxMessage +{ + /** + * Client request to server to negotiate the server's buffer size + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + NegotiateBufferSize() + { + super(UltravoxMessageType.NEGOTIATE_BUFFER_SIZE); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + NegotiateBufferSize(byte[] data) + { + super(data); + } + + /** + * Sets the desired and minimum requested buffer sizes to send to the server + * @param desired buffer size in kilobytes + * @param minimum buffer size in kilobytes + */ + public void setBufferSize(int desired, int minimum) + { + StringBuilder sb = new StringBuilder(); + sb.append(desired).append(":").append(minimum); + + setPayload(sb.toString()); + } + + /** + * Negotiated buffer size from a server response message. + * + * @return server negotiated buffer size, or zero if this is not a server response message, or this is an error + * server response message, or the server response message buffer size cannot be determined from the message payload. + */ + public int getBufferSize() + { + String payload = getPayload(); + + if(payload != null && payload.startsWith("ACK:")) + { + try + { + int bufferSize = Integer.parseInt(payload.substring(4)); + return bufferSize; + } + catch(Exception e) + { + //Do nothing, we couldn't parse the value + } + } + + return 0; + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/NegotiateMaxPayloadSize.java b/src/audio/broadcast/shoutcast/v2/message/NegotiateMaxPayloadSize.java new file mode 100644 index 00000000..d06ab0c2 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/NegotiateMaxPayloadSize.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class NegotiateMaxPayloadSize extends UltravoxMessage +{ + /** + * Client request to server to negotiate maximum payload size + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + NegotiateMaxPayloadSize() + { + super(UltravoxMessageType.NEGOTIATE_MAX_PAYLOAD_SIZE); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + NegotiateMaxPayloadSize(byte[] data) + { + super(data); + } + + /** + * Sets the maximum payload size. + * + * @param desiredMaximum payload size (no larger than 16377) + * @param minimum acceptable payload size (no larger than 16377) + */ + public void setMaximumPayloadSize(int desiredMaximum, int minimum) + { + assert(desiredMaximum <= 16377); + assert(minimum <= 16377); + + StringBuilder sb = new StringBuilder(); + sb.append(desiredMaximum).append(":").append(minimum); + + setPayload(sb.toString()); + } + + /** + * Negotiated maximum payload size from a server response message. + * + * @return server negotiated maximum payload size, or zero if this is not a server response message, or this is an + * error server response message, or the server response message maximum payload size cannot be determined from the + * message payload. + */ + public int getMaximumPayloadSize() + { + String payload = getPayload(); + + if(payload != null && payload.startsWith("ACK:")) + { + try + { + int maximumPayloadSize = Integer.parseInt(payload.substring(4)); + return maximumPayloadSize; + } + catch(Exception e) + { + //Do nothing, we couldn't parse the value + } + } + + return 0; + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/RequestCipher.java b/src/audio/broadcast/shoutcast/v2/message/RequestCipher.java new file mode 100644 index 00000000..3c241034 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/RequestCipher.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class RequestCipher extends UltravoxMessage +{ + /** + * Client request to server for encryption key + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + RequestCipher() + { + super(UltravoxMessageType.REQUEST_CIPHER); + setPayload(SHOUTCAST_VERSION); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + RequestCipher(byte[] data) + { + super(data); + } + + /** + * Cipher key returned from the server to use for encrypting user id and password credentials + * + * @return cipher key or null + */ + public String getCipher() + { + if(isValidResponse()) + { + return getPayload().replace(VALID_RESPONSE_PAYLOAD_PREFIX, ""); + } + + return null; + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/SetupBroadcast.java b/src/audio/broadcast/shoutcast/v2/message/SetupBroadcast.java new file mode 100644 index 00000000..e2cec80c --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/SetupBroadcast.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class SetupBroadcast extends UltravoxMessage +{ + /** + * Client request to server to specify the stream bit rate + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + SetupBroadcast() + { + super(UltravoxMessageType.SETUP_BROADCAST); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + SetupBroadcast(byte[] data) + { + super(data); + } + + /** + * Sets the average and maximum bit rates. Set both to the same value for a Constant Bit Rate (CBR) stream. + * + * @param average bit rate of the stream in kbps + * @param maximum bit rate of the stream in kbps + */ + public void setBitRate(int average, int maximum) + { + StringBuilder sb = new StringBuilder(); + sb.append(average).append(":").append(maximum); + + setPayload(sb.toString()); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/Standby.java b/src/audio/broadcast/shoutcast/v2/message/Standby.java new file mode 100644 index 00000000..0da92698 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/Standby.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class Standby extends UltravoxMessage +{ + /** + * Client request to server to notify that configuration is complete, prepare for streaming + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + Standby() + { + super(UltravoxMessageType.STANDBY); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + Standby(byte[] data) + { + super(data); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/StreamMimeType.java b/src/audio/broadcast/shoutcast/v2/message/StreamMimeType.java new file mode 100644 index 00000000..8ce0a6c8 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/StreamMimeType.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +import audio.broadcast.BroadcastFormat; + +public class StreamMimeType extends UltravoxMessage +{ + /** + * Client request to server to set stream MIME type + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + StreamMimeType() + { + super(UltravoxMessageType.STREAM_MIME_TYPE); + } + + /** + * Server response to client request + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + StreamMimeType(byte[] data) + { + super(data); + } + + /** + * Sets the broadcast format or MIME type + * @param format of the stream + */ + public void setFormat(BroadcastFormat format) + { + setPayload(format.getValue()); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/TerminateBroadcast.java b/src/audio/broadcast/shoutcast/v2/message/TerminateBroadcast.java new file mode 100644 index 00000000..125e8b13 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/TerminateBroadcast.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class TerminateBroadcast extends UltravoxMessage +{ + /** + * Client request to server to terminate the broadcast + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + TerminateBroadcast() + { + super(UltravoxMessageType.TERMINATE_BROADCAST); + } + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + TerminateBroadcast(byte[] data) + { + super(data); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/UltravoxMessage.java b/src/audio/broadcast/shoutcast/v2/message/UltravoxMessage.java similarity index 63% rename from src/audio/broadcast/shoutcast/v2/UltravoxMessage.java rename to src/audio/broadcast/shoutcast/v2/message/UltravoxMessage.java index f9c94f68..5c5b0282 100644 --- a/src/audio/broadcast/shoutcast/v2/UltravoxMessage.java +++ b/src/audio/broadcast/shoutcast/v2/message/UltravoxMessage.java @@ -16,13 +16,19 @@ * along with this program. If not, see * ******************************************************************************/ -package audio.broadcast.shoutcast.v2; +package audio.broadcast.shoutcast.v2.message; import bits.BinaryMessage; -import com.sun.org.apache.regexp.internal.RE; + +import java.util.Arrays; public abstract class UltravoxMessage { + public static final String SHOUTCAST_VERSION = "2.1"; + public static final String VALID_RESPONSE_PREFIX = "ACK"; + public static final String VALID_RESPONSE_PAYLOAD_PREFIX = "ACK:"; + public static final String ERROR_RESPONSE_PREFIX = "NAK:"; + public static final int[] SYNC = {0,1,2,3,4,5,6,7}; public static final int[] RESERVED = {8,9,10,11}; public static final int REQUIRED_DELIVERY = 12; @@ -38,25 +44,45 @@ public abstract class UltravoxMessage */ public UltravoxMessage(byte[] data) { - mMessage = new BinaryMessage(data); + mMessage = new BinaryMessage(data.length * 8); + + int pointer = 0; + + for(byte b: data) + { + mMessage.setByte(pointer, b); + pointer += 8; + } } /** * Constructs an empty message of the specified length * * @param ultravoxMessageType type of message - * @param length of message in bits (64 minimum) */ - public UltravoxMessage(UltravoxMessageType ultravoxMessageType, int length) + public UltravoxMessage(UltravoxMessageType ultravoxMessageType) { - assert(length >= 64); - - mMessage = new BinaryMessage(length); + mMessage = new BinaryMessage(56); setSync(); setMessageType(ultravoxMessageType); } + /** + * Message byte array + * + * @return message bytes + */ + public byte[] getMessage() + { + return mMessage.toByteArray(); + } + + public String toString() + { + return mMessage.toString(); + } + /** * Sets the message sync bits to the predefined sync pattern: 01011010 (0x5A) */ @@ -138,8 +164,25 @@ public abstract class UltravoxMessage */ public String getPayload() { - //TODO: parse payload field - return null; + int payloadLength = mMessage.getInt(PAYLOAD_LENGTH); + + byte[] payload = new byte[payloadLength]; + + int payloadPointer = 48; + + for(int x = 0; x < payloadLength; x++) + { + payload[x] = mMessage.getByte(payloadPointer); + payloadPointer += 8; + } + + //Strip the trailing 0x00 null value from a server response + if(payload[payload.length - 1] == 0) + { + return new String(Arrays.copyOf(payload, payload.length - 1)); + } + + return new String(payload); } /** @@ -147,11 +190,50 @@ public abstract class UltravoxMessage */ public void setPayload(String payload) { - //TODO: implement payload, along with trailing 0x00 byte and update overall payload size field. + setPayload(payload.getBytes()); } - private void setPayloadLength(int length) + public void setPayload(byte[] payload) { - //TODO: correctly size the message to payload + 1 byte and set the payload length field + int messageBitLength = (7 + payload.length) * 8; + + mMessage.setSize(messageBitLength); + mMessage.setInt(payload.length, PAYLOAD_LENGTH); + + int payloadPointer = 48; + + for(byte payloadByte: payload) + { + mMessage.setByte(payloadPointer, payloadByte); + payloadPointer += 8; + } + + mMessage.setByte(payloadPointer, (byte)0x00); + } + + public boolean isValidResponse() + { + String payload = getPayload(); + + return payload != null && payload.startsWith(VALID_RESPONSE_PREFIX); + } + + public boolean isErrorResponse() + { + String payload = getPayload(); + + return payload != null && payload.startsWith(ERROR_RESPONSE_PREFIX); + } + + public String getError() + { + String payload = getPayload(); + + if(payload != null) + { + return payload.replace(ERROR_RESPONSE_PREFIX, ""); + } + + return null; } } diff --git a/src/audio/broadcast/shoutcast/v2/UltravoxMessageClass.java b/src/audio/broadcast/shoutcast/v2/message/UltravoxMessageClass.java similarity index 97% rename from src/audio/broadcast/shoutcast/v2/UltravoxMessageClass.java rename to src/audio/broadcast/shoutcast/v2/message/UltravoxMessageClass.java index dd7d2572..d92c9f86 100644 --- a/src/audio/broadcast/shoutcast/v2/UltravoxMessageClass.java +++ b/src/audio/broadcast/shoutcast/v2/message/UltravoxMessageClass.java @@ -16,7 +16,7 @@ * along with this program. If not, see * ******************************************************************************/ -package audio.broadcast.shoutcast.v2; +package audio.broadcast.shoutcast.v2.message; public enum UltravoxMessageClass { diff --git a/src/audio/broadcast/shoutcast/v2/message/UltravoxMessageFactory.java b/src/audio/broadcast/shoutcast/v2/message/UltravoxMessageFactory.java new file mode 100644 index 00000000..cd3be9ab --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/UltravoxMessageFactory.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class UltravoxMessageFactory +{ + /** + * Creates an empty message of the specified message type for sending to the server + */ + public static UltravoxMessage getMessage(UltravoxMessageType type) + { + switch(type) + { + case AUTHENTICATE_BROADCAST: + return new AuthenticateBroadcast(); + case CONFIGURE_ICY_GENRE: + return new ConfigureIcyGenre(); + case CONFIGURE_ICY_NAME: + return new ConfigureIcyName(); + case CONFIGURE_ICY_PUBLIC: + return new ConfigureIcyPublic(); + case CONFIGURE_ICY_URL: + return new ConfigureIcyURL(); + case FLUSH_CACHED_METADATA: + return new FlushCachedMetadata(); + case MP3_DATA: + return new MP3Audio(); + case NEGOTIATE_BUFFER_SIZE: + return new NegotiateBufferSize(); + case NEGOTIATE_MAX_PAYLOAD_SIZE: + return new NegotiateMaxPayloadSize(); + case REQUEST_CIPHER: + return new RequestCipher(); + case SETUP_BROADCAST: + return new SetupBroadcast(); + case STANDBY: + return new Standby(); + case STREAM_MIME_TYPE: + return new StreamMimeType(); + case TERMINATE_BROADCAST: + return new TerminateBroadcast(); + case XML_METADATA: + return new XMLMetadata(); + default: + return null; + } + } + + /** + * Creates an ultravox message from the server response bytes. + * + * @param bytes in network order (big-endian). + * + * @return constructed message or null if the message type cannot be determined or is unrecognized + */ + public static UltravoxMessage getMessage(byte[] bytes) + { + UltravoxMessageType type = getMessageType(bytes); + + switch(type) + { + case AUTHENTICATE_BROADCAST: + return new AuthenticateBroadcast(bytes); + case CONFIGURE_ICY_GENRE: + return new ConfigureIcyGenre(bytes); + case CONFIGURE_ICY_NAME: + return new ConfigureIcyName(bytes); + case CONFIGURE_ICY_PUBLIC: + return new ConfigureIcyPublic(bytes); + case CONFIGURE_ICY_URL: + return new ConfigureIcyURL(bytes); + case FLUSH_CACHED_METADATA: + return new FlushCachedMetadata(bytes); + case MP3_DATA: + return new MP3Audio(bytes); + case NEGOTIATE_BUFFER_SIZE: + return new NegotiateBufferSize(bytes); + case NEGOTIATE_MAX_PAYLOAD_SIZE: + return new NegotiateMaxPayloadSize(bytes); + case REQUEST_CIPHER: + return new RequestCipher(bytes); + case SETUP_BROADCAST: + return new SetupBroadcast(bytes); + case STANDBY: + return new Standby(bytes); + case STREAM_MIME_TYPE: + return new StreamMimeType(bytes); + case TERMINATE_BROADCAST: + return new TerminateBroadcast(bytes); + case XML_METADATA: + return new XMLMetadata(bytes); + case UNKNOWN: + default: + return null; + } + } + + /** + * Identifies the message type from the server response byte array + * + * @param data in little-endian format + * @return message type or UNKNOWN if the message type cannot be determined. + */ + private static UltravoxMessageType getMessageType(byte[] data) + { + if(data != null && data.length > 4) + { + int value = (data[2] << 8) + data[3]; + + return UltravoxMessageType.fromValue(value); + } + + return UltravoxMessageType.UNKNOWN; + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/UltravoxMessageType.java b/src/audio/broadcast/shoutcast/v2/message/UltravoxMessageType.java new file mode 100644 index 00000000..656e83bd --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/UltravoxMessageType.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public enum UltravoxMessageType +{ + AUTHENTICATE_BROADCAST(0x1001), + SETUP_BROADCAST(0x1002), + NEGOTIATE_BUFFER_SIZE(0x1003), + STANDBY(0x1004), + TERMINATE_BROADCAST(0x1005), + FLUSH_CACHED_METADATA(0x1006), + NEGOTIATE_MAX_PAYLOAD_SIZE(0x1008), + REQUEST_CIPHER(0x1009), + STREAM_MIME_TYPE(0x1040), + CONFIGURE_ICY_NAME(0x1100), + CONFIGURE_ICY_GENRE(0x1101), + CONFIGURE_ICY_URL(0x1102), + CONFIGURE_ICY_PUBLIC(0x1103), + XML_METADATA(0x3902), + MP3_DATA(0x7000), + UNKNOWN(0x0000); + + private int mValue; + + private UltravoxMessageType(int value) + { + mValue = value; + } + + public int getValue() + { + return mValue; + } + + public static UltravoxMessageType fromValue(int value) + { + switch(value) + { + case 0x1001: + return AUTHENTICATE_BROADCAST; + case 0x1002: + return SETUP_BROADCAST; + case 0x1003: + return NEGOTIATE_BUFFER_SIZE; + case 0x1004: + return STANDBY; + case 0x1005: + return TERMINATE_BROADCAST; + case 0x1006: + return FLUSH_CACHED_METADATA; + case 0x1008: + return NEGOTIATE_MAX_PAYLOAD_SIZE; + case 0x1009: + return REQUEST_CIPHER; + case 0x1040: + return STREAM_MIME_TYPE; + case 0x1100: + return CONFIGURE_ICY_NAME; + case 0x1101: + return CONFIGURE_ICY_GENRE; + case 0x1102: + return CONFIGURE_ICY_URL; + case 0x1103: + return CONFIGURE_ICY_PUBLIC; + case 0x3902: + return XML_METADATA; + case 0x7000: + return MP3_DATA; + default: + return UNKNOWN; + } + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/UltravoxMetadata.java b/src/audio/broadcast/shoutcast/v2/message/UltravoxMetadata.java new file mode 100644 index 00000000..d32fd13e --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/UltravoxMetadata.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public enum UltravoxMetadata +{ + ALBUM_TITLE("TALB"), + ARTIST("TPE1"), + AUDIO_SOURCE_WEBPAGE("WOAS"), + BROADCAST_CLIENT_APPLICATION("TENC"), + COMMENT("COMM"), + CUSTOM_TEXT_FIELD("TXXX"), + ENCODE_SETTINGS("TSSE"), + GENERAL_BINARY_OBJECT("GEOB"), + GENRE("TCON"), + OWNER("TOWN"), + RADIO_STATION("TRSN"), + RADIO_STATION_OWNER("TRSO"), + RADIO_STATION_WEBPAGE("WORS"), + RECORDING_TIME("TDRC"), + SOURCE_MEDIA_TYPE("TMED"), + TITLE_1("TIT1"), + TITLE_2("TIT2"), + TITLE_3("TIT3"), + URL("URL"), + UNKNOWN("UNKNOWN"); + + private String mXMLTag; + + private UltravoxMetadata(String xmlTag) + { + mXMLTag = xmlTag; + } + + public String getXMLTag() + { + return mXMLTag; + } + + /** + * Encodes the value as an XML tag stream + * @param value to enclose in XML tags + * @return xml tag wrapped value + */ + public String asXML(String value) + { + StringBuilder sb = new StringBuilder(); + sb.append("<").append(getXMLTag()).append(">"); + sb.append(value); + sb.append(""); + + return sb.toString(); + } +} diff --git a/src/audio/broadcast/shoutcast/v2/message/XMLMetadata.java b/src/audio/broadcast/shoutcast/v2/message/XMLMetadata.java new file mode 100644 index 00000000..e981d7b5 --- /dev/null +++ b/src/audio/broadcast/shoutcast/v2/message/XMLMetadata.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package audio.broadcast.shoutcast.v2.message; + +public class XMLMetadata extends UltravoxMessage +{ + /** + * Client request to server to send cacheable XML metadata + * + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + */ + XMLMetadata() + { + super(UltravoxMessageType.XML_METADATA); + } + + + /** + * Server response to client request + * Package private constructor. Use the UltravoxMessageFactory for this constructor. + * + * @param data bytes received from the server + */ + XMLMetadata(byte[] data) + { + super(data); + } + +// /** +// * Adds custom metadata (TXXX) tagged value to the metadata set. The metadata value is identified by the +// * id parameter. +// * +// * @param id tag to identify the type of metadata +// * @param value for the id tag +// */ +// public void addCustomMetadata(String id, String value) +// { +// StringBuilder sb = new StringBuilder(); +// sb.append("<").append(UltravoxMetadata.CUSTOM_TEXT_FIELD.getXMLTag()); +// sb.append(" id=\"").append(id).append("\">"); +// sb.append(value); +// sb.append(""); +// +// mMetadata.add(sb.toString()); +// } +// +// /** +// * Adds the binary object data to the metadata set. +// * @param id tag to identify the binary object +// * @param mime type +// * @param filename associated with the data +// * @param base64Data base-64 encoded binary data to embed in the metadata tag +// */ +// public void addBinaryObject(String id, String mime, String filename, String base64Data) +// { +// StringBuilder sb = new StringBuilder(); +// sb.append("<").append(UltravoxMetadata.GENERAL_BINARY_OBJECT.getXMLTag()); +// sb.append(" mime=\"").append(mime).append("\""); +// sb.append(" filename=\"").append(filename).append("\""); +// sb.append(" id=\"").append(id).append("\""); +// sb.append(">"); +// sb.append(base64Data); +// sb.append(""); +// +// mMetadata.add(sb.toString()); +// } +// +// /** +// * Adds a recording time (TDRC) tagged metadata timestamp +// * +// * @param timestamp for the recording time +// */ +// public void addRecordingTime(long timestamp) +// { +// Calendar calendar = new GregorianCalendar(); +// calendar.setTimeInMillis(timestamp); +// +// StringBuilder sb = new StringBuilder(); +// sb.append("\n"); +// sb.append("").append(calendar.get(Calendar.YEAR)).append("\n"); +// sb.append("").append(calendar.get(Calendar.MONTH)).append("\n"); +// sb.append("").append(calendar.get(Calendar.DAY_OF_MONTH)).append("\n"); +// sb.append("").append(calendar.get(Calendar.HOUR_OF_DAY)).append("\n"); +// sb.append("").append(calendar.get(Calendar.MINUTE)).append("\n"); +// sb.append("").append(calendar.get(Calendar.ZONE_OFFSET)).append("\n"); +// sb.append("\n"); +// +// mMetadata.add(sb.toString()); +// } +} diff --git a/src/bits/BinaryMessage.java b/src/bits/BinaryMessage.java index b9ae1570..56e21a12 100644 --- a/src/bits/BinaryMessage.java +++ b/src/bits/BinaryMessage.java @@ -17,6 +17,7 @@ ******************************************************************************/ package bits; +import java.util.Arrays; import java.util.BitSet; import java.util.logging.LogManager; @@ -283,6 +284,33 @@ public class BinaryMessage extends BitSet return values; } + + /** + * Returns this message as a little endian byte array. Extra 0 bits will be padded to the end to make the overall + * length a multiple of 8. + * @return little endian message byte array + */ + public byte[] toByteArray() + { + int length = size() / 8; + + if(length * 8 < size()) + { + length ++; + } + + byte[] bytes = new byte[length]; + + int pointer = 0; + + for(int x = 0; x < length; x++) + { + bytes[x] = getByte(pointer); + pointer += 8; + } + + return bytes; + } /** * Returns this bitset as a reversed bit order array of integer ones and zeros @@ -421,6 +449,57 @@ public class BinaryMessage extends BitSet return (byte)( value & 0xFF ); } + /** + * Returns the byte value contained between index and index + 7 bit positions + * @param index specifying the start of the byte value + * @return byte value contained at index <> index + 7 bit positions + */ + public byte getByte(int index) + { + assert((index + 7) <= size()); + + int value = 0; + + for(int x = 0; x < 8; x++) + { + value = value << 1; + + if(get(index + x)) + { + value++; + } + } + + return (byte)value; + } + + /** + * Sets the byte value at index position through index + 7 position. + * + * @param index bit position where to write the MSB of the byte value + * @param value to write at the index through index + 7 bit positions + */ + public void setByte(int index, byte value) + { + assert((index + 8) <= size()); + + int mask = 0x80; + + for(int x = 0; x < 8; x++) + { + if((mask & value) == mask) + { + set(index + x); + } + else + { + clear(index + x); + } + + mask = mask >> 1; + } + } + /** * Returns the long value represented by the bit array * @param bits - an array of bit positions that will be treated as if they diff --git a/src/util/XTEA.java b/src/util/XTEA.java new file mode 100644 index 00000000..575c92c8 --- /dev/null +++ b/src/util/XTEA.java @@ -0,0 +1,184 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package util; + +import java.util.Arrays; + +public class XTEA +{ + //Key Schedule = (sqrt(5) ­ 1) * 2^31 + private static final int K = 0x9E3779B9; + + //Byte mask values + private static final int MASK0 = 0xFF; + private static final int MASK1 = 0xFF00; + private static final int MASK2 = 0xFF0000; + private static final int MASK3 = 0xFF000000; + + //Pre-calculated sub keys derived from encryption key + private int mSubKeys1[] = new int[32]; + private int mSubKeys2[] = new int[32]; + + /** + * XTEA block cipher encryption using 128 bit encryption key and 64 bit plain text block size with 32 Feistel rounds. + * Shorter keys and/or plaintext values can be zero padded to the specified lengths. + * + * Algorithm source code adapted from: https://people.rit.edu/rab3106/CryptoReport.pdf (as accessed Sep. 2016) + * + * The encryption key can be changed at any point by invoking the setKey() method. + * + * @param key for encryption + */ + public XTEA(String key) + { + setKey(Arrays.copyOf(key.getBytes(), 16)); + } + + /** + * Set the key for this block cipher. Key must be 128 bits or 16 bytes. The key value can be zero padded to + * achieve the specified key length. + * + * @param key encryption key to use + */ + public void setKey(byte[] key) + { + assert(key.length == 16); + + int keyInts[] = new int[8]; + + //Convert the key to an array of integers + for (int i = 0; i < 16; i += 4) + { + keyInts[i / 4] = ((key[i + 3] << 0)) & MASK0 | ((key[i + 2]) << 8) & MASK1 | + ((key[i + 1]) << 16) & MASK2 | ((key[i + 0]) << 24) & MASK3; + } + + //Calculate subkeys for each Feistel round + int sum, cycle; + + for (sum = 0, cycle = 0; cycle < 32; cycle++) + { + //Calculate round 1 subkey + mSubKeys1[cycle] = keyInts[sum & 3]; + + //Add K to sum + sum += K; + + //Calculate round 2 subkey + mSubKeys2[cycle] = keyInts[(sum >>> 11) & 3]; + } + } + + /** + * Encrypts the plaintext argument. Textual byte array must be 64 bits or 8 bytes in length and can be zero padded + * to achieve the specified length. Encrypted text byte array replaces the original plaintext byte array. + * + * @param plainText to encrypt + * @return cipher text + */ + public byte[] encrypt(byte[] plainText) + { + assert(plainText.length == 8); + + //Convert the 8 bytes of plaintext to 2 integers + int result0 = (plainText[3] << 0) & MASK0 | (plainText[2] << 8) & MASK1 | + (plainText[1] << 16) & MASK2 | (plainText[0] << 24) & MASK3; + int result1 = (plainText[7] << 0) & MASK0 | (plainText[6] << 8) & MASK1 | + (plainText[5] << 16) & MASK2 | (plainText[4] << 24) & MASK3; + + //Unrolled loop for all Feistel rounds + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 0) + mSubKeys1[0]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 1) + mSubKeys2[0]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 1) + mSubKeys1[1]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 2) + mSubKeys2[1]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 2) + mSubKeys1[2]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 3) + mSubKeys2[2]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 3) + mSubKeys1[3]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 4) + mSubKeys2[3]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 4) + mSubKeys1[4]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 5) + mSubKeys2[4]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 5) + mSubKeys1[5]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 6) + mSubKeys2[5]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 6) + mSubKeys1[6]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 7) + mSubKeys2[6]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 7) + mSubKeys1[7]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 8) + mSubKeys2[7]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 8) + mSubKeys1[8]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 9) + mSubKeys2[8]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 9) + mSubKeys1[9]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 10) + mSubKeys2[9]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 10) + mSubKeys1[10]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 11) + mSubKeys2[10]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 11) + mSubKeys1[11]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 12) + mSubKeys2[11]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 12) + mSubKeys1[12]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 13) + mSubKeys2[12]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 13) + mSubKeys1[13]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 14) + mSubKeys2[13]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 14) + mSubKeys1[14]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 15) + mSubKeys2[14]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 15) + mSubKeys1[15]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 16) + mSubKeys2[15]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 16) + mSubKeys1[16]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 17) + mSubKeys2[16]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 17) + mSubKeys1[17]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 18) + mSubKeys2[17]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 18) + mSubKeys1[18]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 19) + mSubKeys2[18]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 19) + mSubKeys1[19]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 20) + mSubKeys2[19]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 20) + mSubKeys1[20]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 21) + mSubKeys2[20]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 21) + mSubKeys1[21]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 22) + mSubKeys2[21]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 22) + mSubKeys1[22]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 23) + mSubKeys2[22]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 23) + mSubKeys1[23]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 24) + mSubKeys2[23]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 24) + mSubKeys1[24]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 25) + mSubKeys2[24]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 25) + mSubKeys1[25]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 26) + mSubKeys2[25]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 26) + mSubKeys1[26]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 27) + mSubKeys2[26]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 27) + mSubKeys1[27]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 28) + mSubKeys2[27]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 28) + mSubKeys1[28]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 29) + mSubKeys2[28]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 29) + mSubKeys1[29]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 30) + mSubKeys2[29]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 30) + mSubKeys1[30]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 31) + mSubKeys2[30]; + result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 31) + mSubKeys1[31]; + result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 32) + mSubKeys2[31]; + + byte[] cipherText = new byte[8]; + + cipherText[0] = (byte)(result0 >>> 24); + cipherText[1] = (byte)(result0 >>> 16); + cipherText[2] = (byte)(result0 >>> 8); + cipherText[3] = (byte)(result0 >>> 0); + cipherText[4] = (byte)(result1 >>> 24); + cipherText[5] = (byte)(result1 >>> 16); + cipherText[6] = (byte)(result1 >>> 8); + cipherText[7] = (byte)(result1 >>> 0); + + return cipherText; + } +}