diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml
index 31ec5bc1..0df9f405 100644
--- a/.idea/dictionaries/denny.xml
+++ b/.idea/dictionaries/denny.xml
@@ -1,10 +1,12 @@
+ bahroo
esnid
fleetsync
ident
polyphase
+ shoutcast
talkgroup
tgid
diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml
index 8ac29dbb..7e05f72d 100644
--- a/.idea/libraries/imports.xml
+++ b/.idea/libraries/imports.xml
@@ -21,9 +21,10 @@
-
+
+
diff --git a/.idea/libraries/jmbe_interface_0_3_3.xml b/.idea/libraries/jmbe_interface_0_3_3.xml
deleted file mode 100644
index 1b73f0a9..00000000
--- a/.idea/libraries/jmbe_interface_0_3_3.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/libraries/jump3r_1_0_3.xml b/.idea/libraries/jump3r_1_0_3.xml
deleted file mode 100644
index 7b55f0d4..00000000
--- a/.idea/libraries/jump3r_1_0_3.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/imports/java-lame-3.98.4.jar b/imports/java-lame-3.98.4.jar
new file mode 100644
index 00000000..026d96eb
Binary files /dev/null and b/imports/java-lame-3.98.4.jar differ
diff --git a/imports/jump3r-1.0.3.jar b/imports/jump3r-1.0.3.jar
deleted file mode 100644
index 12d8234a..00000000
Binary files a/imports/jump3r-1.0.3.jar and /dev/null differ
diff --git a/sdrtrunk.iml b/sdrtrunk.iml
index 1c1d0bed..b65a79a9 100644
--- a/sdrtrunk.iml
+++ b/sdrtrunk.iml
@@ -20,7 +20,5 @@
-
-
\ No newline at end of file
diff --git a/src/audio/AudioUtils.java b/src/audio/AudioUtils.java
new file mode 100644
index 00000000..fac396f9
--- /dev/null
+++ b/src/audio/AudioUtils.java
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * 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;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import record.wave.RealBufferWaveRecorder;
+import sample.ConversionUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+public class AudioUtils
+{
+ private final static Logger mLog = LoggerFactory.getLogger(AudioUtils.class);
+
+ /**
+ * Converts the audio packets into a byte array of 16-bit, little-endian audio samples
+ */
+ public static byte[] convertTo16BitSamples(List audioPackets)
+ {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+ try
+ {
+ for(AudioPacket packet: audioPackets)
+ {
+ //Converting from 32-bit floats to signed 16-bit samples
+ ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples());
+ stream.write(buffer.array());
+ }
+ }
+ catch(IOException e)
+ {
+ mLog.error("Error writing converted PCM bytes to output stream");
+ }
+
+ return stream.toByteArray();
+ }
+}
diff --git a/src/audio/stream/StreamManager.java b/src/audio/broadcast/AudioBroadcastManager.java
similarity index 78%
rename from src/audio/stream/StreamManager.java
rename to src/audio/broadcast/AudioBroadcastManager.java
index a1a87ca4..c0598a13 100644
--- a/src/audio/stream/StreamManager.java
+++ b/src/audio/broadcast/AudioBroadcastManager.java
@@ -16,18 +16,17 @@
* along with this program. If not, see
*
******************************************************************************/
-package audio.stream;
+package audio.broadcast;
import audio.AudioPacket;
-import audio.IAudioPacketListener;
import controller.ThreadPoolManager;
import sample.Listener;
-public class StreamManager implements IAudioPacketListener, Listener
+public class AudioBroadcastManager implements Listener
{
private ThreadPoolManager mThreadPoolManager;
- public StreamManager(ThreadPoolManager threadPoolManager)
+ public AudioBroadcastManager(ThreadPoolManager threadPoolManager)
{
mThreadPoolManager = threadPoolManager;
}
@@ -37,10 +36,4 @@ public class StreamManager implements IAudioPacketListener, Listener getAudioPacketListener()
- {
- return this;
- }
}
diff --git a/src/audio/stream/StreamModel.java b/src/audio/broadcast/AudioBroadcastModel.java
similarity index 75%
rename from src/audio/stream/StreamModel.java
rename to src/audio/broadcast/AudioBroadcastModel.java
index 9e6b99ed..b5819cf7 100644
--- a/src/audio/stream/StreamModel.java
+++ b/src/audio/broadcast/AudioBroadcastModel.java
@@ -16,24 +16,26 @@
* along with this program. If not, see
*
******************************************************************************/
-package audio.stream;
+package audio.broadcast;
+
+import audio.broadcast.configuration.BroadcastConfiguration;
import javax.swing.table.AbstractTableModel;
import java.util.ArrayList;
import java.util.List;
-public class StreamModel extends AbstractTableModel
+public class AudioBroadcastModel extends AbstractTableModel
{
private static final int COLUMN_CHANNEL_NAME = 0;
private static final int COLUMN_CHANNEL_FORMAT = 1;
private static final int COLUMN_SERVER_NAME = 2;
- private List mStreams = new ArrayList<>();
+ private List mBroadcastConfigurations = new ArrayList<>();
@Override
public int getRowCount()
{
- return mStreams.size();
+ return mBroadcastConfigurations.size();
}
@Override
@@ -45,20 +47,20 @@ public class StreamModel extends AbstractTableModel
@Override
public Object getValueAt(int rowIndex, int columnIndex)
{
- if( rowIndex <= mStreams.size())
+ if( rowIndex <= mBroadcastConfigurations.size())
{
- Stream stream = mStreams.get(rowIndex);
+ BroadcastConfiguration configuration = mBroadcastConfigurations.get(rowIndex);
- if(stream != null)
+ if(configuration != null)
{
switch(columnIndex)
{
case COLUMN_CHANNEL_NAME:
- return stream.getChannel().getName();
+ return configuration.getAlias();
case COLUMN_CHANNEL_FORMAT:
- return stream.getChannel().getSourceFormat().name();
+ return configuration.getBroadcastFormat().name();
case COLUMN_SERVER_NAME:
- return stream.getServer().getName();
+ return configuration.getHost();
default:
break;
}
diff --git a/src/audio/broadcast/Broadcaster.java b/src/audio/broadcast/Broadcaster.java
new file mode 100644
index 00000000..7e107969
--- /dev/null
+++ b/src/audio/broadcast/Broadcaster.java
@@ -0,0 +1,205 @@
+/*******************************************************************************
+ * 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;
+
+import audio.AudioPacket;
+import audio.IAudioPacketListener;
+import audio.broadcast.configuration.BroadcastConfiguration;
+import audio.broadcast.handler.BroadcastHandler;
+import audio.broadcast.handler.BroadcastState;
+import controller.ThreadPoolManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class Broadcaster implements IAudioPacketListener, Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger( Broadcaster.class );
+
+ private ThreadPoolManager mThreadPoolManager;
+ private ScheduledFuture mScheduledTask;
+ private AudioQueueProcessor mAudioQueueProcessor;
+ private LinkedTransferQueue mAudioQueue = new LinkedTransferQueue<>();
+ private BroadcastHandler mBroadcastHandler;
+ private boolean mEnabled = true;
+
+ /**
+ * Broadcaster for sending broadcast audio to a remote broadcast server.
+ */
+ public Broadcaster(ThreadPoolManager threadPoolManager, BroadcastHandler broadcastHandler)
+ {
+ mThreadPoolManager = threadPoolManager;
+ mBroadcastHandler = broadcastHandler;
+ mAudioQueueProcessor = new AudioQueueProcessor();
+
+ //Add broadcast state listener to stop processing if we have an error
+ mBroadcastHandler.addListener(new BroadcastStateMonitor());
+ }
+
+ /**
+ * Broadcast configuration used by this broadcaster
+ */
+ public BroadcastConfiguration getBroadcastConfiguration()
+ {
+ return mBroadcastHandler.getBroadcastConfiguration();
+ }
+
+ /**
+ * Current state of the broadcaster's connection with the remote server
+ */
+ public BroadcastState getBroadcastState()
+ {
+ return mBroadcastHandler.getBroadcastState();
+ }
+
+ /**
+ * Registers the listener to receive broadcast state changes
+ */
+ public void addListener(Listener listener)
+ {
+ mBroadcastHandler.addListener(listener);
+ }
+
+ /**
+ * Removes the listener from receiving broadcast state changes
+ */
+ public void removeListener(Listener listener)
+ {
+ mBroadcastHandler.removeListener(listener);
+ }
+
+ /**
+ * IAudioPacketListener interface method
+ */
+ @Override
+ public Listener getAudioPacketListener()
+ {
+ return this;
+ }
+
+ /**
+ * Disconnects the broadcaster and halts all broadcasting.
+ */
+ public void stop()
+ {
+ mEnabled = false;
+ stopProcessor();
+ mBroadcastHandler.disconnect();
+ }
+
+ /**
+ * Pauses the broadcaster.
+ * @param paused set to true to pause and false to continue
+ */
+ public void setPaused(boolean paused)
+ {
+ if(mEnabled)
+ {
+ mBroadcastHandler.setPaused(paused);
+ }
+ }
+
+ /**
+ * Primary audio packet input method. Audio packets are queued and then processed by the underlying broadcast
+ * handler for broadcast to the remote server.
+ */
+ public void receive(AudioPacket packet)
+ {
+ if(mEnabled)
+ {
+ startProcessor();
+ mAudioQueue.add(packet);
+ }
+ }
+
+ /**
+ * Starts the audio queue processor. The audio packet queue is serviced every 100 milliseconds. Subsequent calls
+ * to this method once running are ignored.
+ */
+ private void startProcessor()
+ {
+ if(mEnabled && mScheduledTask == null)
+ {
+ if(mThreadPoolManager != null)
+ {
+ mScheduledTask = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
+ mAudioQueueProcessor, 100, TimeUnit.MILLISECONDS );
+ }
+ }
+ }
+
+ /**
+ * Stops the audio queue processor
+ */
+ private void stopProcessor()
+ {
+ if(mThreadPoolManager != null && mScheduledTask != null)
+ {
+ mThreadPoolManager.cancel(mScheduledTask);
+ }
+ }
+
+ /**
+ * Monitors the broadcast handler for error state and halts audio broadcasting
+ */
+ public class BroadcastStateMonitor implements Listener
+ {
+ @Override
+ public void receive(BroadcastState broadcastState)
+ {
+ if(broadcastState.isErrorState())
+ {
+ stop();
+ }
+ }
+ }
+
+ /**
+ * Processes audio packets from the audio packet queue and broadcasts the audio
+ */
+ public class AudioQueueProcessor implements Runnable
+ {
+ private AtomicBoolean mProcessing = new AtomicBoolean();
+
+ @Override
+ public void run()
+ {
+ if(mProcessing.compareAndSet(false, true))
+ {
+ List audioPackets = new ArrayList<>();
+
+ mAudioQueue.drainTo(audioPackets);
+
+ if(!audioPackets.isEmpty())
+ {
+ mBroadcastHandler.broadcast(audioPackets);
+ }
+
+ mProcessing.set(false);
+ }
+ }
+ }
+}
diff --git a/src/audio/broadcast/BroadcasterFactory.java b/src/audio/broadcast/BroadcasterFactory.java
new file mode 100644
index 00000000..ce13702e
--- /dev/null
+++ b/src/audio/broadcast/BroadcasterFactory.java
@@ -0,0 +1,80 @@
+/*******************************************************************************
+ * 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;
+
+import audio.broadcast.configuration.BroadcastConfiguration;
+import audio.broadcast.configuration.BroadcastFormat;
+import audio.broadcast.configuration.ShoutcastV1Configuration;
+import audio.broadcast.handler.ShoutcastV1Handler;
+import audio.convert.IAudioConverter;
+import audio.convert.MP3AudioConverter;
+import controller.ThreadPoolManager;
+
+public class BroadcasterFactory
+{
+ public static final int MP3_MONO_16_KHZ_BITRATE = 16;
+ public static final boolean MP3_NO_VARIABLE_BITRATE = false;
+
+ /**
+ * Creates an audio streaming broadcaster for the configuration
+ * @param threadPoolManager for creating an audio broadcasting connection thread
+ * @param configuration describing the server and audio types
+ * @return configured broadcaster or null
+ */
+ public static Broadcaster getBroadcaster(ThreadPoolManager threadPoolManager,
+ BroadcastConfiguration configuration)
+ {
+ IAudioConverter converter = getAudioConverter(configuration.getBroadcastFormat());
+
+ if(converter != null)
+ {
+ switch(configuration.getBroadcastServerType())
+ {
+ case BROADCASTIFY:
+ break;
+ case ICECAST:
+ break;
+ case SHOUTCAST_V1:
+ return new Broadcaster(threadPoolManager,
+ new ShoutcastV1Handler((ShoutcastV1Configuration)configuration, converter));
+ case UNKNOWN:
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an audio convert to convert from 8 kHz PCM audio to the specified format
+ * @param format to convert to
+ * @return audio convert or null
+ */
+ public static IAudioConverter getAudioConverter(BroadcastFormat format)
+ {
+ switch(format)
+ {
+ case MP3:
+ return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_NO_VARIABLE_BITRATE);
+
+ }
+
+ return null;
+ }
+}
diff --git a/src/audio/broadcast/configuration/BroadcastConfiguration.java b/src/audio/broadcast/configuration/BroadcastConfiguration.java
new file mode 100644
index 00000000..2044b727
--- /dev/null
+++ b/src/audio/broadcast/configuration/BroadcastConfiguration.java
@@ -0,0 +1,134 @@
+/*******************************************************************************
+ * 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.configuration;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+public abstract class BroadcastConfiguration
+{
+ private BroadcastFormat mBroadcastFormat;
+ private String mAlias;
+ private String mHost;
+ private int mPort;
+ private String mPassword;
+
+
+ /**
+ * Broadcast audio streaming configuration. Describes the streaming server, broadcast user authentication and
+ * audio broadcast details to use when connecting to a streaming server and broadcasting audio.
+ */
+ public BroadcastConfiguration(BroadcastFormat format)
+ {
+ mBroadcastFormat = format;
+ }
+
+ public abstract BroadcastServerType getBroadcastServerType();
+
+ /**
+ * Alias identifying this broadcast configuration.
+ */
+ public String getAlias()
+ {
+ return mAlias;
+ }
+
+ /**
+ * Sets the alias name that identifies/describes this configuration
+ * @param alias
+ */
+ public void setAlias(String alias)
+ {
+ mAlias = alias;
+ }
+
+ /**
+ * Streaming server host name.
+ */
+ public String getHost()
+ {
+ return mHost;
+ }
+
+ /**
+ * Host name or IP address of the streaming server
+ * @param host
+ */
+ public void setHost(String host)
+ {
+ mHost = host;
+ }
+
+ /**
+ * Streaming server port number;
+ */
+ public int getPort()
+ {
+ return mPort;
+ }
+
+ /**
+ * Port number of the streaming server to where the broadcast will be directed.
+ * @param port
+ */
+ public void setPort(int port)
+ {
+ mPort = port;
+ }
+
+ public SocketAddress getAddress()
+ {
+ return new InetSocketAddress(getHost(), getPort());
+ }
+
+ /**
+ * Password to authenticate with the streaming server.
+ */
+ public String getPassword()
+ {
+ return mPassword;
+ }
+
+ /**
+ * Sets the password used to authenticate the broadcast broadcast user to the streaming server
+ * @param password
+ */
+ public void setPassword(String password)
+ {
+ mPassword = password;
+ }
+
+ /**
+ * Audio broadcast content type (e.g. mp3)
+ */
+ public BroadcastFormat getBroadcastFormat()
+ {
+ return mBroadcastFormat;
+ }
+
+ /**
+ * Audio broadcast content type (e.g. mp3)
+ * @param broadcastFormat
+ */
+ public void setBroadcastFormat(BroadcastFormat broadcastFormat)
+ {
+ mBroadcastFormat = broadcastFormat;
+ }
+
+}
diff --git a/src/audio/stream/shout/source/ShoutSource.java b/src/audio/broadcast/configuration/BroadcastConfigurationFactory.java
similarity index 63%
rename from src/audio/stream/shout/source/ShoutSource.java
rename to src/audio/broadcast/configuration/BroadcastConfigurationFactory.java
index 00d8184d..91bec018 100644
--- a/src/audio/stream/shout/source/ShoutSource.java
+++ b/src/audio/broadcast/configuration/BroadcastConfigurationFactory.java
@@ -16,19 +16,24 @@
* along with this program. If not, see
*
******************************************************************************/
-package audio.stream.shout.source;
+package audio.broadcast.configuration;
-public abstract class ShoutSource implements IShoutSource
+public class BroadcastConfigurationFactory
{
- private ShoutSourceFormat mFormat;
+ public static BroadcastConfiguration getConfiguration(BroadcastServerType serverType, BroadcastFormat format)
+ {
+ switch(serverType)
+ {
+ case BROADCASTIFY:
+ break;
+ case ICECAST:
+ break;
+ case SHOUTCAST_V1:
+ return new ShoutcastV1Configuration(format);
+ case UNKNOWN:
+ break;
+ }
- public ShoutSource(ShoutSourceFormat format)
- {
- mFormat = format;
- }
- @Override
- public ShoutSourceFormat getFormat()
- {
- return mFormat;
+ return null;
}
}
diff --git a/src/audio/stream/shout/server/ShoutServer.java b/src/audio/broadcast/configuration/BroadcastFormat.java
similarity index 78%
rename from src/audio/stream/shout/server/ShoutServer.java
rename to src/audio/broadcast/configuration/BroadcastFormat.java
index 1020a5c9..25f97f37 100644
--- a/src/audio/stream/shout/server/ShoutServer.java
+++ b/src/audio/broadcast/configuration/BroadcastFormat.java
@@ -16,20 +16,21 @@
* along with this program. If not, see
*
******************************************************************************/
-package audio.stream.shout.server;
+package audio.broadcast.configuration;
-public abstract class ShoutServer implements IShoutServer
+public enum BroadcastFormat
{
- private String mName;
+ MP3("audio/mpeg");
- public ShoutServer(String name)
+ private String mValue;
+
+ private BroadcastFormat(String value)
{
- mName = name;
+ mValue = value;
}
- @Override
- public String getName()
+ public String getValue()
{
- return mName;
+ return mValue;
}
}
diff --git a/src/audio/broadcast/configuration/BroadcastMetadata.java b/src/audio/broadcast/configuration/BroadcastMetadata.java
new file mode 100644
index 00000000..917e7fb2
--- /dev/null
+++ b/src/audio/broadcast/configuration/BroadcastMetadata.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * 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.configuration;
+
+public enum BroadcastMetadata
+{
+ AIM("icy-aim:"),
+ AUDIO_BIT_RATE("icy-br:"),
+ CHANNELS("icy-channels:"),
+ CONTENT_TYPE("Content-Type:"),
+ DESCRIPTION("icy-description:"),
+ GENRE("icy-genre:"),
+ ICE_AUDIO_INFO("ice-audio-info:"),
+ ICQ("icy-icq:"),
+ INTERNET_RELAY_CHAT("icy-irc:"),
+ METADATA_INTERVAL("icy-metaint:"),
+ METADATA_REQUEST("icy-metadata:"),
+ PRE_BUFFER("icy-prebuffer:"),
+ PUBLIC("icy-pub:"),
+ RESET("icy-reset:"),
+ SERVER("server:"),
+ STREAM_NAME("icy-name:"),
+ SONG_TITLE("icy-title:"),
+ URL("icy-url:"),
+ USER_AGENT("User-Agent:"),
+ UNKNOWN("Unknown");
+
+ private String mTag;
+
+ private BroadcastMetadata(String tag)
+ {
+ mTag = tag;
+ }
+
+ public static final String COMMAND_TERMINATOR = "\n";
+
+ public String toString()
+ {
+ return mTag;
+ }
+
+ /**
+ * Encodes the metadata tag and value with an end-of-line character, appropriate for sending over the network.
+ */
+ public String encode(String value)
+ {
+ if(value != null && !value.isEmpty())
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mTag).append(value).append(COMMAND_TERMINATOR);
+ return sb.toString();
+ }
+
+ return null;
+ }
+
+ public String encode(boolean value)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mTag).append(value ? "1" : "0").append(COMMAND_TERMINATOR);
+ return sb.toString();
+ }
+
+ public String encode(BroadcastFormat broadcastFormat)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mTag).append(broadcastFormat.getValue()).append(COMMAND_TERMINATOR);
+ return sb.toString();
+ }
+
+ public String encode(int value)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mTag).append(String.valueOf(value)).append(COMMAND_TERMINATOR);
+ return sb.toString();
+ }
+}
diff --git a/src/audio/stream/shout/source/IShoutSource.java b/src/audio/broadcast/configuration/BroadcastServerType.java
similarity index 85%
rename from src/audio/stream/shout/source/IShoutSource.java
rename to src/audio/broadcast/configuration/BroadcastServerType.java
index 198adeb4..c5c26d52 100644
--- a/src/audio/stream/shout/source/IShoutSource.java
+++ b/src/audio/broadcast/configuration/BroadcastServerType.java
@@ -16,9 +16,13 @@
* along with this program. If not, see
*
******************************************************************************/
-package audio.stream.shout.source;
+package audio.broadcast.configuration;
-public interface IShoutSource
+public enum BroadcastServerType
{
- public ShoutSourceFormat getFormat();
+ BROADCASTIFY,
+ ICECAST,
+ SHOUTCAST_V1,
+ SHOUTCAST_V2,
+ UNKNOWN;
}
diff --git a/src/audio/broadcast/configuration/ShoutcastV1Configuration.java b/src/audio/broadcast/configuration/ShoutcastV1Configuration.java
new file mode 100644
index 00000000..0a00d641
--- /dev/null
+++ b/src/audio/broadcast/configuration/ShoutcastV1Configuration.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * 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.configuration;
+
+public class ShoutcastV1Configuration extends BroadcastConfiguration
+{
+ private String mStreamName;
+ private String mGenre;
+ private boolean mPublic;
+ private int mChannels;
+ private int mBitRate;
+ private String mURL;
+
+ public ShoutcastV1Configuration(BroadcastFormat format)
+ {
+ super(format);
+ }
+
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.SHOUTCAST_V1;
+ }
+
+ /**
+ * Stream name
+ */
+ public String getStreamName()
+ {
+ return mStreamName;
+ }
+
+ /**
+ * Sets the stream name
+ * @param name
+ */
+ public void setStreamName(String name)
+ {
+ mStreamName = name;
+ }
+
+ /**
+ * Stream genre
+ */
+ public String getGenre()
+ {
+ return mGenre;
+ }
+
+ /**
+ * Stream genre
+ * @param genre
+ */
+ public void setGenre(String genre)
+ {
+ mGenre = genre;
+ }
+
+ /**
+ * Public visibility of the broadcast
+ */
+ public boolean isPublic()
+ {
+ return mPublic;
+ }
+
+ /**
+ * Sets public visibility of the broadcast
+ * @param isPublic indicates if the broadcast should be visible to the public
+ */
+ public void setPublic(boolean isPublic)
+ {
+ 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.
+ */
+ public String getURL()
+ {
+ return mURL;
+ }
+
+ /**
+ * URL associated with the broadcast where users can find additional details.
+ * @param url
+ */
+ public void setURL(String url)
+ {
+ mURL = url;
+ }
+}
diff --git a/src/audio/broadcast/configuration/ShoutcastV2Configuration.java b/src/audio/broadcast/configuration/ShoutcastV2Configuration.java
new file mode 100644
index 00000000..f92f13ca
--- /dev/null
+++ b/src/audio/broadcast/configuration/ShoutcastV2Configuration.java
@@ -0,0 +1,179 @@
+/*******************************************************************************
+ * 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.configuration;
+
+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 mBitRate;
+ private String mURL;
+
+
+ public ShoutcastV2Configuration(BroadcastFormat format)
+ {
+ super(format);
+ }
+
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.SHOUTCAST_V1;
+ }
+
+ public String getMountPoint()
+ {
+ return mMountPoint;
+ }
+
+ /**
+ * 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
+ */
+ public void setMountPoint(String mountPoint)
+ {
+ //Strip the leading slash from the mount point if the user supplies it
+ mMountPoint = mountPoint.replace("/", "");
+ }
+
+ /**
+ * Stream name
+ */
+ public String getStreamName()
+ {
+ return mStreamName;
+ }
+
+ /**
+ * Sets the stream name
+ * @param name
+ */
+ public void setStreamName(String name)
+ {
+ 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()
+ {
+ return mGenre;
+ }
+
+ /**
+ * Stream genre
+ * @param genre
+ */
+ public void setGenre(String genre)
+ {
+ mGenre = genre;
+ }
+
+ /**
+ * Public visibility of the broadcast
+ */
+ public boolean isPublic()
+ {
+ return mPublic;
+ }
+
+ /**
+ * Sets public visibility of the broadcast
+ * @param isPublic indicates if the broadcast should be visible to the public
+ */
+ public void setPublic(boolean isPublic)
+ {
+ 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.
+ */
+ public String getURL()
+ {
+ return mURL;
+ }
+
+ /**
+ * URL associated with the broadcast where users can find additional details.
+ * @param url
+ */
+ public void setURL(String url)
+ {
+ mURL = url;
+ }
+}
diff --git a/src/audio/broadcast/handler/BroadcastHandler.java b/src/audio/broadcast/handler/BroadcastHandler.java
new file mode 100644
index 00000000..8b3e33df
--- /dev/null
+++ b/src/audio/broadcast/handler/BroadcastHandler.java
@@ -0,0 +1,140 @@
+/*******************************************************************************
+ * 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.handler;
+
+import audio.AudioPacket;
+import audio.broadcast.configuration.BroadcastConfiguration;
+import audio.convert.IAudioConverter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public abstract class BroadcastHandler
+{
+ private final static Logger mLog = LoggerFactory.getLogger( BroadcastHandler.class );
+
+ protected BroadcastConfiguration mBroadcastConfiguration;
+ protected IAudioConverter mAudioConverter;
+ private List> mBroadcastStateListeners = new CopyOnWriteArrayList<>();
+ private BroadcastState mBroadcastState = BroadcastState.READY;
+
+ /**
+ * Abstract audio broadcast handler. Protocol-specific implementations will manage a connection with the remote
+ * broadcast audio server, convert audio using the audio converter argument and dispatch the converted audio to
+ * the remote server.
+ *
+ * @param broadcastConfiguration for configuring the handler
+ * @param audioConverter to convert audio packets to the desired output format
+ */
+ public BroadcastHandler(BroadcastConfiguration broadcastConfiguration, IAudioConverter audioConverter)
+ {
+ mBroadcastConfiguration = broadcastConfiguration;
+ mAudioConverter = audioConverter;
+ }
+
+ public BroadcastConfiguration getBroadcastConfiguration()
+ {
+ return mBroadcastConfiguration;
+ }
+
+ /**
+ * Audio converter
+ */
+ protected IAudioConverter getAudioConverter()
+ {
+ return mAudioConverter;
+ }
+
+ /**
+ * Processes and broadcasts the audio packets
+ */
+ public abstract void broadcast(List audioPackets);
+
+ /**
+ * Disconnects from the remote server and disables the broadcaster.
+ */
+ public abstract void disconnect();
+
+ /**
+ * Pauses or unpauses the broadcast
+ */
+ public void setPaused(boolean paused)
+ {
+ if(!getBroadcastState().isErrorState())
+ {
+ if(paused)
+ {
+ setBroadcastState(BroadcastState.PAUSED);
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.READY);
+ }
+ }
+ }
+
+ /**
+ * Registers the listener to receive broadcast state changes
+ */
+ public void addListener(Listener listener)
+ {
+ mBroadcastStateListeners.add(listener);
+ }
+
+ /**
+ * Removes the listener from receiving broadcast state changes
+ */
+ public void removeListener(Listener listener)
+ {
+ mBroadcastStateListeners.remove(listener);
+ }
+
+ /**
+ * Sets the state of the broadcast connection
+ */
+ protected void setBroadcastState(BroadcastState state)
+ {
+ if(mBroadcastState != state)
+ {
+ mLog.debug("Changing State to: " + state);
+ mBroadcastState = state;
+
+ for(Listener listener: mBroadcastStateListeners)
+ {
+ listener.receive(state);
+ }
+ }
+ }
+
+ /**
+ * Current state of the broadcast connection
+ */
+ public BroadcastState getBroadcastState()
+ {
+ return mBroadcastState;
+ }
+
+ public boolean connected()
+ {
+ return getBroadcastState() == BroadcastState.CONNECTED;
+ }
+}
diff --git a/src/audio/broadcast/handler/BroadcastState.java b/src/audio/broadcast/handler/BroadcastState.java
new file mode 100644
index 00000000..ee9bb432
--- /dev/null
+++ b/src/audio/broadcast/handler/BroadcastState.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * 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.handler;
+
+public enum BroadcastState
+{
+ /**
+ * Error while broadcasting stream data. Temporary error state to allow connection to be reset.
+ */
+ BROADCAST_ERROR("Broadcast Error", false),
+
+ /**
+ * Connected to the broadcast server and capable of streaming
+ */
+ CONNECTED("Connected", false),
+
+ /**
+ * Connection interrupted, attempting to reconnect.
+ */
+ CONNECTING("Connecting", false),
+
+ /**
+ * Disconnected from the broadcast server
+ */
+ DISCONNECTED("Disconnected", true),
+
+ /**
+ * Invalid password
+ */
+ INVALID_PASSWORD("Invalid Password", true),
+
+ /**
+ * Server is not known or reachable
+ */
+ NO_SERVER("No Server", false),
+
+ /**
+ * Connected but paused. No audio is streaming.
+ */
+ PAUSED("Paused", false),
+
+ /**
+ * Initial state with no connection attempted.
+ */
+ READY("Ready", false),
+
+ /**
+ * Server host name or port is invalid
+ */
+ UNKNOWN_HOST("Unknown Host or Invalid Port", true),
+
+ /**
+ * Unspecified error
+ */
+ ERROR("Error", true);
+
+ private String mLabel;
+ private boolean mErrorState;
+
+ private BroadcastState(String label, boolean connected)
+ {
+ mLabel = label;
+ mErrorState = connected;
+ }
+
+ public String toString()
+ {
+ return mLabel;
+ }
+
+ public boolean isErrorState()
+ {
+ return mErrorState;
+ }
+}
diff --git a/src/audio/broadcast/handler/ShoutcastV1Handler.java b/src/audio/broadcast/handler/ShoutcastV1Handler.java
new file mode 100644
index 00000000..ab01bd93
--- /dev/null
+++ b/src/audio/broadcast/handler/ShoutcastV1Handler.java
@@ -0,0 +1,379 @@
+/*******************************************************************************
+ * 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.handler;
+
+import audio.AudioPacket;
+import audio.broadcast.Broadcaster;
+import audio.broadcast.BroadcasterFactory;
+import audio.broadcast.configuration.BroadcastFormat;
+import audio.broadcast.configuration.BroadcastMetadata;
+import audio.broadcast.configuration.ShoutcastV1Configuration;
+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.List;
+
+public class ShoutcastV1Handler extends BroadcastHandler
+{
+ private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1Handler.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 static final int mBufferSize = 2000;
+
+ /**
+ * Creates a Shoutcast Version 1 broadcast handler.
+ * @param configuration
+ * @param audioConverter
+ */
+ public ShoutcastV1Handler(ShoutcastV1Configuration configuration, IAudioConverter audioConverter)
+ {
+ super(configuration, audioConverter);
+ }
+
+ /**
+ * Shoutcast V1 Configuration information
+ */
+ private ShoutcastV1Configuration getShoutcastConfiguration()
+ {
+ return (ShoutcastV1Configuration)getBroadcastConfiguration();
+ }
+
+ /**
+ * Process audio packets using Shoutcast V1 Protocol
+ * @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)
+ {
+ try
+ {
+ send(convertedAudio);
+ }
+ catch(IOException e)
+ {
+ mLog.error("Error while dispatching audio", e);
+ setBroadcastState(BroadcastState.BROADCAST_ERROR);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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()
+ {
+
+ }
+
+ /**
+ * 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)
+ {
+ createSocket();
+
+ if(mSocket.isConnected())
+ {
+ mLog.debug("Sending credentials");
+
+ StringBuilder sb = new StringBuilder();
+
+ //Password
+ sb.append(getShoutcastConfiguration().getPassword()).append(BroadcastMetadata.COMMAND_TERMINATOR);
+
+ //Metadata
+ sb.append(BroadcastMetadata.STREAM_NAME.encode(getShoutcastConfiguration().getStreamName()));
+ sb.append(BroadcastMetadata.URL.encode(getShoutcastConfiguration().getURL()));
+ sb.append(BroadcastMetadata.PUBLIC.encode(getShoutcastConfiguration().isPublic()));
+ sb.append(BroadcastMetadata.GENRE.encode(getShoutcastConfiguration().getGenre()));
+ sb.append(BroadcastMetadata.AUDIO_BIT_RATE.encode(getShoutcastConfiguration().getBitRate()));
+
+ //End of connection string
+ sb.append(BroadcastMetadata.COMMAND_TERMINATOR);
+
+ try
+ {
+ send(sb.toString());
+
+ if(isInvalidResponse())
+ {
+ mLog.error("Invalid response after sending metadata");
+ return;
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.CONNECTED);
+ }
+ }
+ catch(IOException ioe)
+ {
+ mLog.debug("Error while connecting to server", ioe);
+ setBroadcastState(BroadcastState.ERROR);
+ }
+
+ mLog.debug("Connection established");
+ }
+ }
+ }
+
+ /**
+ * Sends the string data to the remote server
+ * @param data to send
+ * @throws IOException if there is an error communicating with the remote server
+ */
+ private void send(String data) throws IOException
+ {
+ mLog.debug("Sending [" + data + "]");
+ if(data != null && !data.isEmpty() && mOutputStream != null)
+ {
+ mOutputStream.writeBytes(data);
+ }
+ }
+
+ /**
+ * Sends the byte data to the remote server
+ * @param data to send
+ * @throws IOException if there is an error communicating with the remote server
+ */
+ private void send(byte[] data) throws IOException
+ {
+ mLog.debug("Sending bytes:" + data.length);
+ if(connected())
+ {
+ int sent = 0;
+
+ while(sent < data.length)
+ {
+ int available = data.length - sent;
+
+ int sending = available 0)
+ {
+ byte[] responseBuffer = new byte[bytesAvailable];
+
+ int bytesRead = 0;
+
+ while(bytesRead < bytesAvailable)
+ {
+ bytesRead += mInputStream.read(responseBuffer, bytesRead, bytesAvailable - bytesRead);
+ }
+
+ return new String(responseBuffer);
+ }
+ }
+
+ return null;
+ }
+
+ private boolean isInvalidResponse()
+ {
+ String response;
+
+ try
+ {
+ response = getResponse();
+ }
+ catch(IOException e)
+ {
+ mLog.error("Error while retrieving server response message", e);
+ setBroadcastState(BroadcastState.ERROR);
+ return false;
+ }
+
+ if(response != null && !response.isEmpty())
+ {
+ if(response.startsWith("OK"))
+ {
+ return false;
+ }
+ else if(response.startsWith("Invalid Password"))
+ {
+ setBroadcastState(BroadcastState.INVALID_PASSWORD);
+ return true;
+ }
+
+ mLog.debug("Unrecognized server response:" + response);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates a socket connection to the remote server and sets the state to CONNECTING.
+ */
+ private void createSocket()
+ {
+ if(mSocket == null)
+ {
+ mLog.debug("Creating socket");
+ 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 void main(String[] args)
+ {
+ ShoutcastV1Configuration config = new ShoutcastV1Configuration(BroadcastFormat.MP3);
+ config.setAlias("Test Configuration");
+ config.setHost("localhost");
+ config.setPort(8000);
+ config.setPassword("denny3");
+ config.setStreamName("Denny's Audio Broadcast Test");
+ config.setGenre("Scanner Audio");
+ config.setPublic(true);
+ config.setURL("http://localhost:8000");
+ config.setBitRate(16);
+
+ 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/stream/shout/source/ShoutSourceFormat.java b/src/audio/convert/IAudioConverter.java
similarity index 84%
rename from src/audio/stream/shout/source/ShoutSourceFormat.java
rename to src/audio/convert/IAudioConverter.java
index bc447db8..86cdd98f 100644
--- a/src/audio/stream/shout/source/ShoutSourceFormat.java
+++ b/src/audio/convert/IAudioConverter.java
@@ -16,9 +16,13 @@
* along with this program. If not, see
*
******************************************************************************/
-package audio.stream.shout.source;
+package audio.convert;
-public enum ShoutSourceFormat
+import audio.AudioPacket;
+
+import java.util.List;
+
+public interface IAudioConverter
{
- MP3;
+ public byte[] convert(List audioPackets);
}
diff --git a/src/audio/convert/MP3AudioConverter.java b/src/audio/convert/MP3AudioConverter.java
new file mode 100644
index 00000000..27b87637
--- /dev/null
+++ b/src/audio/convert/MP3AudioConverter.java
@@ -0,0 +1,77 @@
+/*******************************************************************************
+ * 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.convert;
+
+import audio.AudioFormats;
+import audio.AudioPacket;
+import audio.AudioUtils;
+import net.sourceforge.lame.lowlevel.LameEncoder;
+import net.sourceforge.lame.mp3.Lame;
+import net.sourceforge.lame.mp3.MPEGMode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+
+public class MP3AudioConverter implements IAudioConverter
+{
+ private final static Logger mLog = LoggerFactory.getLogger( MP3AudioConverter.class );
+
+ private LameEncoder mEncoder;
+ private ByteArrayOutputStream mMP3Stream = new ByteArrayOutputStream();
+ private byte[] mMP3Buffer;
+
+ /**
+ * Converts PCM 8kHz 16-bit Little Endian audio packets to Mono, Low Quality MP3 compressed audio.
+ *
+ * @param bitRate for converted MP3 audio
+ * @param variableBitRate (VBR) true or false for constant bit rate (CBR)
+ */
+ public MP3AudioConverter(int bitRate, boolean variableBitRate)
+ {
+ mEncoder = new LameEncoder(AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO,
+ bitRate, MPEGMode.MONO, Lame.QUALITY_LOW, variableBitRate);
+
+ mMP3Buffer = new byte[mEncoder.getPCMBufferSize()];
+ }
+
+ @Override
+ public byte[] convert(List audioPackets)
+ {
+ mMP3Stream.reset();
+
+ byte[] pcmBytes = AudioUtils.convertTo16BitSamples(audioPackets);
+
+ int pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length);
+
+ int mp3BufferSize = 0;
+
+ int pcmBytesPosition = 0;
+
+ while (0 < (mp3BufferSize = mEncoder.encodeBuffer(pcmBytes, pcmBytesPosition, pcmBufferSize, mMP3Buffer)))
+ {
+ pcmBytesPosition += pcmBufferSize;
+ pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length - pcmBytesPosition);
+ mMP3Stream.write(mMP3Buffer, 0, mp3BufferSize);
+ }
+
+ return mMP3Stream.toByteArray();
+ }
+}
diff --git a/src/audio/inverted/AudioAdapter.java b/src/audio/invert/AudioAdapter.java
similarity index 98%
rename from src/audio/inverted/AudioAdapter.java
rename to src/audio/invert/AudioAdapter.java
index f529fb06..db45c734 100644
--- a/src/audio/inverted/AudioAdapter.java
+++ b/src/audio/invert/AudioAdapter.java
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
-package audio.inverted;
+package audio.invert;
import dsp.filter.FilterFactory;
import dsp.filter.FloatFIRFilter;
@@ -23,7 +23,7 @@ import dsp.filter.Window.WindowType;
import dsp.mixer.Oscillator;
/**
- * Based on the AudioType applied, produces normal, muted, or audio inverted
+ * Based on the AudioType applied, produces normal, muted, or audio invert
* audio output.
*/
public class AudioAdapter
diff --git a/src/audio/inverted/AudioInverter.java b/src/audio/invert/AudioInverter.java
similarity index 96%
rename from src/audio/inverted/AudioInverter.java
rename to src/audio/invert/AudioInverter.java
index e14dbb15..f9ec3755 100644
--- a/src/audio/inverted/AudioInverter.java
+++ b/src/audio/invert/AudioInverter.java
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
-package audio.inverted;
+package audio.invert;
import audio.InversionFrequency;
import sample.real.RealSampleListener;
@@ -24,7 +24,7 @@ import dsp.filter.FloatFIRFilter;
import dsp.mixer.Oscillator;
/**
- * Applies audio inversion (or un-inversion) to a stream of float audio samples
+ * Applies audio inversion (or un-inversion) to a broadcast of float audio samples
* by multiplying each successive sample by either a 1 or a -1 value.
*/
public class AudioInverter implements RealSampleListener
diff --git a/src/audio/inverted/AudioType.java b/src/audio/invert/AudioType.java
similarity index 99%
rename from src/audio/inverted/AudioType.java
rename to src/audio/invert/AudioType.java
index 706ee683..0659b781 100644
--- a/src/audio/inverted/AudioType.java
+++ b/src/audio/invert/AudioType.java
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
-package audio.inverted;
+package audio.invert;
public enum AudioType
{
diff --git a/src/audio/inverted/IAudioTypeListener.java b/src/audio/invert/IAudioTypeListener.java
similarity index 97%
rename from src/audio/inverted/IAudioTypeListener.java
rename to src/audio/invert/IAudioTypeListener.java
index 52321dcf..a55feec1 100644
--- a/src/audio/inverted/IAudioTypeListener.java
+++ b/src/audio/invert/IAudioTypeListener.java
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
******************************************************************************/
-package audio.inverted;
+package audio.invert;
public interface IAudioTypeListener
diff --git a/src/audio/stream/Stream.java b/src/audio/stream/Stream.java
deleted file mode 100644
index 2ab62edd..00000000
--- a/src/audio/stream/Stream.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*******************************************************************************
- * 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.stream;
-
-import audio.AudioPacket;
-import audio.stream.shout.channel.IShoutChannel;
-import audio.stream.shout.server.IShoutServer;
-import sample.Listener;
-
-/**
- * Streaming channel encapsulates communication with a specific server and channel.
- */
-public class Stream implements Listener
-{
- private IShoutServer mServer;
- private IShoutChannel mChannel;
-
- public Stream(IShoutServer server, IShoutChannel channel)
- {
- mServer = server;
- mChannel = channel;
- }
-
- public IShoutServer getServer()
- {
- return mServer;
- }
-
- public IShoutChannel getChannel()
- {
- return mChannel;
- }
-
- @Override
- public void receive(AudioPacket audioPacket)
- {
-
- }
-}
diff --git a/src/audio/stream/shout/channel/IShoutChannel.java b/src/audio/stream/shout/channel/IShoutChannel.java
deleted file mode 100644
index 08505a87..00000000
--- a/src/audio/stream/shout/channel/IShoutChannel.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*******************************************************************************
- * 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.stream.shout.channel;
-
-import audio.stream.shout.source.ShoutSourceFormat;
-
-public interface IShoutChannel
-{
- public String getName();
- public ShoutSourceFormat getSourceFormat();
-}
diff --git a/src/audio/stream/shout/server/IShoutServer.java b/src/audio/stream/shout/server/IShoutServer.java
deleted file mode 100644
index f53c3697..00000000
--- a/src/audio/stream/shout/server/IShoutServer.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*******************************************************************************
- * 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.stream.shout.server;
-
-public interface IShoutServer
-{
- public String getName();
-}
diff --git a/src/audio/stream/shout/source/MP3ShoutSource.java b/src/audio/stream/shout/source/MP3ShoutSource.java
deleted file mode 100644
index c5749a4f..00000000
--- a/src/audio/stream/shout/source/MP3ShoutSource.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*******************************************************************************
- * 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.stream.shout.source;
-
-public class MP3ShoutSource extends ShoutSource
-{
- public MP3ShoutSource()
- {
- super(ShoutSourceFormat.MP3);
- }
-}
diff --git a/src/gui/SDRTrunk.java b/src/gui/SDRTrunk.java
index 40bfb51a..c3e6fa40 100644
--- a/src/gui/SDRTrunk.java
+++ b/src/gui/SDRTrunk.java
@@ -43,6 +43,7 @@ import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;
+import audio.broadcast.AudioBroadcastManager;
import map.IconManager;
import map.MapService;
import module.log.EventLogManager;
@@ -152,6 +153,9 @@ public class SDRTrunk implements Listener
AudioManager audioManager = new AudioManager( threadPoolManager, sourceManager.getMixerManager() );
channelProcessingManager.addAudioPacketListener( audioManager );
+ AudioBroadcastManager audioBroadcastManager = new AudioBroadcastManager(threadPoolManager);
+ channelProcessingManager.addAudioPacketListener(audioBroadcastManager);
+
MapService mapService = new MapService( mSettingsManager );
channelProcessingManager.addMessageListener( mapService );
diff --git a/src/org/jma/encoder/audio/IAudioEncoder.java b/src/org/jma/encoder/audio/IAudioEncoder.java
new file mode 100644
index 00000000..ab5eeef2
--- /dev/null
+++ b/src/org/jma/encoder/audio/IAudioEncoder.java
@@ -0,0 +1,18 @@
+package org.jma.encoder.audio;
+
+/**
+ * Retrieved from: http://livertmpjavapublisher.blogspot.com/2011/06/orgtritonuslowlevellameapiiaudioencoder.html
+ * on 5 Sep 2016
+ */
+public interface IAudioEncoder
+{
+ public int encodeBuffer(byte[] input, int offset, int encodedCount, byte[] output);
+
+ public int encodeFinish(byte[] input);
+
+ public int getInputBufferSize();
+
+ public int getOutputBufferSize();
+
+ public void close();
+}
diff --git a/src/record/mp3/MP3Recorder.java b/src/record/mp3/MP3Recorder.java
new file mode 100644
index 00000000..bd5dcdc7
--- /dev/null
+++ b/src/record/mp3/MP3Recorder.java
@@ -0,0 +1,319 @@
+/*******************************************************************************
+ * 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 record.mp3;
+
+import audio.AudioPacket;
+import audio.IAudioPacketListener;
+import audio.convert.MP3AudioConverter;
+import module.Module;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import record.wave.AudioPacketMonoWaveReader;
+import sample.Listener;
+import util.TimeStamp;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * MP3 recorder for converting 8 kHz PCM audio packets to MP3 and writing to .mp3 file.
+ */
+public class MP3Recorder extends Module implements Listener, IAudioPacketListener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(MP3Recorder.class);
+
+ private MP3AudioConverter mMP3Converter;
+ private FileOutputStream mFileOutputStream;
+ private String mFilePrefix;
+ private Path mPath;
+
+ private BufferProcessor mBufferProcessor;
+ private ScheduledFuture> mProcessorHandle;
+ private LinkedBlockingQueue mAudioPacketQueue = new LinkedBlockingQueue<>(500);
+ private long mLastBufferReceived;
+
+ private AtomicBoolean mRunning = new AtomicBoolean();
+
+ /**
+ * MP3 audio recorder module for converting audio packets to 16 kHz constant bit rate MP3 format and
+ * recording to a file.
+ */
+ public MP3Recorder(String filePrefix)
+ {
+ mFilePrefix = filePrefix;
+
+ int mp3BitRate = 16; //16 kHz
+ boolean variableBitRate = false; //CBR
+ mMP3Converter = new MP3AudioConverter(mp3BitRate, variableBitRate);
+ }
+
+ /**
+ * Timestamp of when the latest buffer was received by this recorder
+ */
+ public long getLastBufferReceived()
+ {
+ return mLastBufferReceived;
+ }
+
+ public Path getPath()
+ {
+ return mPath;
+ }
+
+ /**
+ * Starts this recorder using as a scheduled thread broadcast running under the executor argument
+ *
+ * @param executor to use in scheduling MP3 conversion and file writes.
+ */
+ public void start(ScheduledExecutorService executor)
+ {
+ if (mRunning.compareAndSet(false, true))
+ {
+ if (mBufferProcessor == null)
+ {
+ mBufferProcessor = new BufferProcessor();
+ }
+
+ try
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mFilePrefix);
+ sb.append("_");
+ sb.append(TimeStamp.getLongTimeStamp("_"));
+ sb.append(".mp3");
+
+ mPath = Paths.get(sb.toString());
+ mFileOutputStream = new FileOutputStream(mPath.toFile());
+
+ /* Schedule the handler to run every second */
+ mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 200, TimeUnit.MILLISECONDS);
+ }
+ catch (IOException io)
+ {
+ mLog.error("Error starting real buffer recorder", io);
+ }
+ }
+ }
+
+ /**
+ * Stops the recorder.
+ */
+ public void stop()
+ {
+ mRunning.set(false);
+
+ if (mProcessorHandle != null)
+ {
+ mProcessorHandle.cancel(true);
+ }
+
+ mProcessorHandle = null;
+
+ processAudioPacketQueue();
+
+ close();
+ }
+
+ /**
+ * Closes the recording file.
+ */
+ private void close()
+ {
+ if (mFileOutputStream != null)
+ {
+ try
+ {
+ mFileOutputStream.close();
+ }
+ catch (IOException e)
+ {
+ mLog.error("Error closing output stream", e);
+ }
+ }
+ }
+
+ /**
+ * Primary data insert method designed to accept a stream of audio packets and a final ending audio packet.
+ *
+ * Audio packets received before the recorder is started or after the recorder is stopped will be ignored.
+ */
+ @Override
+ public void receive(AudioPacket audioPacket)
+ {
+ if (mRunning.get())
+ {
+ boolean success = mAudioPacketQueue.offer(audioPacket);
+
+ if (!success)
+ {
+ mLog.error("recorder buffer overflow - purging [" + mPath.toFile().getAbsolutePath() + "]");
+
+ mAudioPacketQueue.clear();
+ }
+
+ mLastBufferReceived = System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * IAudioPacketListener interface method.
+ */
+ @Override
+ public Listener getAudioPacketListener()
+ {
+ return this;
+ }
+
+ /**
+ * Disposes this audio recorder and prepares it for reclamation
+ */
+ @Override
+ public void dispose()
+ {
+ stop();
+ }
+
+ /**
+ * Not implemented. Recorder modules are not appropriate for reset and reuse.
+ */
+ @Override
+ public void reset()
+ {
+ }
+
+ /**
+ * Processes the audio packet queue.
+ */
+ private void processAudioPacketQueue()
+ {
+ try
+ {
+ List mPacketsToProcess = new ArrayList<>();
+ mAudioPacketQueue.drainTo(mPacketsToProcess);
+
+ if (!mPacketsToProcess.isEmpty())
+ {
+ boolean stopRequested = false;
+ int stopPacketIndex = 0;
+
+ //An END audio packet is a request to stop the recorder
+ for (AudioPacket packet : mPacketsToProcess)
+ {
+ if (packet.getType() == AudioPacket.Type.END)
+ {
+ stopRequested = true;
+ stopPacketIndex = mPacketsToProcess.indexOf(packet);
+ continue;
+ }
+ }
+
+ if (stopRequested)
+ {
+ mRunning.set(false);
+ mAudioPacketQueue.clear();
+
+ if (stopPacketIndex > 0)
+ {
+ List remainingPacketsToProcess = new ArrayList<>();
+
+ for (int x = 0; x < stopPacketIndex; x++)
+ {
+ remainingPacketsToProcess.add(mPacketsToProcess.get(x));
+ }
+
+ write(remainingPacketsToProcess);
+
+ remainingPacketsToProcess.clear();
+ }
+
+ stop();
+ }
+ else
+ {
+ write(mPacketsToProcess);
+ }
+
+ mPacketsToProcess.clear();
+ }
+ }
+ catch (IOException ioe)
+ {
+ mAudioPacketQueue.clear();
+
+ mLog.error("IOException while processing audio packet queue for MP3 conversion and storage", ioe);
+ }
+ }
+
+ /**
+ * Converts the PCM audio packets to MP3 format and writes the converted audio to the output stream.
+ */
+ private void write(List audioPackets) throws IOException
+ {
+ byte[] mp3Audio = mMP3Converter.convert(audioPackets);
+
+ mFileOutputStream.write(mp3Audio);
+ }
+
+ public class BufferProcessor implements Runnable
+ {
+ private AtomicBoolean mProcessing = new AtomicBoolean();
+
+ public void run()
+ {
+ if (mProcessing.compareAndSet(false, true))
+ {
+ processAudioPacketQueue();
+
+ mProcessing.set(false);
+ }
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ Path path = Paths.get("/home/denny/Music/PCM.wav");
+ mLog.debug("Opening: " + path.toString());
+
+ final MP3Recorder recorder = new MP3Recorder("/home/denny/Music/denny_test");
+ recorder.start(Executors.newSingleThreadScheduledExecutor());
+
+ try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
+ {
+ reader.setListener(recorder);
+ reader.read();
+ recorder.stop();
+ }
+ catch (IOException e)
+ {
+ mLog.error("Error", e);
+ }
+
+ mLog.debug("Finished");
+ }
+}
diff --git a/src/record/wave/AudioPacketMonoWaveReader.java b/src/record/wave/AudioPacketMonoWaveReader.java
new file mode 100644
index 00000000..b8a4af71
--- /dev/null
+++ b/src/record/wave/AudioPacketMonoWaveReader.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 record.wave;
+
+import audio.AudioPacket;
+import audio.IAudioPacketListener;
+import audio.metadata.AudioMetadata;
+import sample.Listener;
+import sample.real.RealBuffer;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+public class AudioPacketMonoWaveReader extends MonoWaveReader implements Listener
+{
+ private AudioMetadata mMetadata = new AudioMetadata(0, false);
+
+ private IAudioPacketListener mListener;
+
+ /**
+ * Reads wav files and converts the contents to AudioPackets.
+ *
+ * @param path of file to read containing 8kHz, 16-bit signed litte endian PCM audio
+ * @param realTime causes the read() method to read and dispatch audio packets in real time. Note: when using
+ * real time playback, the thread calling the read() method will sleep as needed to effect real-time playback,
+ * therefore you should invoke read() on a separate thread if this will impact the calling thread.
+ *
+ * @throws IOException on any file IO errors
+ */
+ public AudioPacketMonoWaveReader(Path path, boolean realTime) throws IOException
+ {
+ super(path, realTime);
+ setListener(this);
+ }
+
+ public void setMetadata(AudioMetadata metadata)
+ {
+ mMetadata = metadata;
+ }
+
+ public void setListener(IAudioPacketListener listener)
+ {
+ mListener = listener;
+ }
+
+ @Override
+ public void receive(RealBuffer realBuffer)
+ {
+ if(mListener != null)
+ {
+ mListener.getAudioPacketListener().receive(new AudioPacket(realBuffer.getSamples(), mMetadata));
+ }
+ }
+}
diff --git a/src/record/wave/MonoWaveReader.java b/src/record/wave/MonoWaveReader.java
new file mode 100644
index 00000000..cd717d46
--- /dev/null
+++ b/src/record/wave/MonoWaveReader.java
@@ -0,0 +1,257 @@
+/*******************************************************************************
+ * 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 record.wave;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.ConversionUtils;
+import sample.Listener;
+import sample.real.RealBuffer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
+public class MonoWaveReader implements AutoCloseable
+{
+ private final static Logger mLog = LoggerFactory.getLogger( MonoWaveReader.class);
+
+ private static final Pattern FILENAME_PATTERN = Pattern.compile( "(.*_)(\\d+)(\\.wav)" );
+
+ private Path mPath;
+ private boolean mRealTime;
+ private InputStream mInputStream;
+ private int mDataByteSize = 0;
+ private Listener mListener;
+ private byte[] mDataBuffer = new byte[8000]; //500 milliseconds of audio
+
+ /**
+ * Wave file reader for PCM 8kHz, 16-bit little-endian format.
+ *
+ * @param path of the wave file
+ * @param realTime to force replay to real-time. Note: you should run this reader on a separate thread if you
+ * choose real time since this reader will sleep the calling thread as needed to effect real-time playback.
+ */
+ public MonoWaveReader(Path path, boolean realTime) throws IOException
+ {
+ assert( path != null );
+ mPath = path;
+ mRealTime = realTime;
+
+ open();
+ }
+
+ /**
+ * Opens the file
+ */
+ private void open() throws IOException
+ {
+ if(!Files.exists(mPath))
+ {
+ throw new IOException("File not found");
+ }
+
+ mInputStream = Files.newInputStream(mPath, StandardOpenOption.READ);
+
+ //Check for RIFF header
+ byte[] buffer = new byte[4];
+ mInputStream.read(buffer);
+
+ if(!Arrays.equals(buffer, WaveUtils.RIFF_CHUNK))
+ {
+ throw new IOException("File is not .wav format - missing RIFF chunk");
+ }
+
+ //Get file size
+ mInputStream.read(buffer);
+ int fileSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
+
+ //Check for WAVE format
+ mInputStream.read(buffer);
+
+ if(!Arrays.equals(buffer, WaveUtils.WAV_FORMAT))
+ {
+ throw new IOException("File is not .wav format - missing WAVE format");
+ }
+
+ //Check for format chunk
+ mInputStream.read(buffer);
+
+ if(!Arrays.equals(buffer, WaveUtils.CHUNK_FORMAT))
+ {
+ throw new IOException("File is not .wav format - missing format chunk");
+ }
+
+ //Get chunk size
+ mInputStream.read(buffer);
+ int chunkSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
+
+ //Get format
+ mInputStream.read(buffer);
+
+ ShortBuffer shortBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
+ short format = shortBuffer.get();
+ if(format != WaveUtils.PCM_FORMAT)
+ {
+ throw new IOException("File format not supported - expecting PCM format");
+ }
+
+ //Get number of channels
+ short channels = shortBuffer.get();
+ if(channels != 1)
+ {
+ throw new IOException("Unsupported channel count - mono audio only");
+ }
+
+ //Get samples per second
+ mInputStream.read(buffer);
+ int sampleRate = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
+
+ //Get bytes per second
+ mInputStream.read(buffer);
+ int bytesPerSecond = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
+
+ mInputStream.read(buffer);
+
+ //Get frame size
+ shortBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
+ short frameSize = shortBuffer.get();
+ if(frameSize != 2)
+ {
+ throw new IOException("PCM frame size not supported - expecting 2 bytes per frame");
+ }
+
+ //Get bits per sample
+ short bitsPerSample = shortBuffer.get();
+ if(bitsPerSample != 16)
+ {
+ throw new IOException("PCM sample size not supported - expecting 16 bits per sample");
+ }
+
+ mInputStream.read(buffer);
+
+ if(!Arrays.equals(buffer, WaveUtils.CHUNK_DATA))
+ {
+ throw new IOException("Unexpected chunk - expecting data chunk");
+ }
+
+ //Get data chunk size
+ mInputStream.read(buffer);
+ mDataByteSize = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get();
+ }
+
+ public void read() throws IOException
+ {
+ long start = System.currentTimeMillis();
+ long samplesRead = 0;
+
+ int totalBytesRead = 0;
+
+ while(totalBytesRead < mDataByteSize)
+ {
+ int bytesRead = mInputStream.read(mDataBuffer);
+
+ totalBytesRead += bytesRead;
+
+ float[] samples = ConversionUtils.convertFromSigned16BitSamples(mDataBuffer);
+
+ if(bytesRead != mDataBuffer.length)
+ {
+ samples = Arrays.copyOf(samples, bytesRead / 2);
+ }
+
+ if(mRealTime)
+ {
+ samplesRead += samples.length;
+
+ long actualElapsed = System.currentTimeMillis() - start;
+
+ long audioElapsed = (long)(((double)samplesRead / 8000.0) * 1000.0);
+
+ if(audioElapsed > actualElapsed)
+ {
+ try
+ {
+ Thread.sleep(audioElapsed - actualElapsed);
+ }
+ catch (InterruptedException e)
+ {
+ }
+ }
+ }
+
+ if(mListener != null)
+ {
+ mListener.receive(new RealBuffer(samples));
+ }
+ }
+
+ close();
+ }
+
+ public void setListener(Listener listener)
+ {
+ mListener = listener;
+ }
+
+ /**
+ * Closes the file
+ */
+ public void close() throws IOException
+ {
+ if(mInputStream != null)
+ {
+ mInputStream.close();
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ Path path = Paths.get("/home/denny/Music/PCM.wav");
+ mLog.debug("Opening: " + path.toString());
+
+ try
+ {
+ MonoWaveReader reader = new MonoWaveReader(path, true);
+ reader.setListener(new Listener()
+ {
+ @Override
+ public void receive(RealBuffer realBuffer)
+ {
+ mLog.debug("Received buffer");
+ }
+ });
+ reader.read();
+ }
+ catch(IOException e)
+ {
+ mLog.error("Error", e);
+ }
+
+ mLog.debug("Finished");
+ }
+}
diff --git a/src/record/wave/WaveUtils.java b/src/record/wave/WaveUtils.java
index 050fa83f..68fb8d8e 100644
--- a/src/record/wave/WaveUtils.java
+++ b/src/record/wave/WaveUtils.java
@@ -17,14 +17,19 @@
******************************************************************************/
package record.wave;
+import javax.sound.sampled.AudioFormat;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-import javax.sound.sampled.AudioFormat;
-
public class WaveUtils
{
+ public static final byte[] RIFF_CHUNK = {(byte)0x52, (byte)0x49, (byte)0x46, (byte)0x46}; //RIFF
+ public static final byte[] WAV_FORMAT = {(byte)0x57, (byte)0x41, (byte)0x56, (byte)0x45}; //WAVE
+ public static final byte[] CHUNK_FORMAT = {(byte)0x66, (byte)0x6D, (byte)0x74, (byte)0x20}; //fmt space
+ public static final byte[] CHUNK_DATA = {(byte)0x64, (byte)0x61, (byte)0x74, (byte)0x61}; //data
+ public static final int PCM_FORMAT = 1;
+
/**
* Creates a wave file header with the RIFF chunk descriptor and the WAVE
* format chunk 1 and chunk 2 header