diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml
index dc5ec847..408050c7 100644
--- a/.idea/dictionaries/denny.xml
+++ b/.idea/dictionaries/denny.xml
@@ -10,6 +10,7 @@
polyphase
samplerate
sdrtrunk
+ sheirer
shoutcast
streamable
talkgroup
diff --git a/src/audio/broadcast/AudioBroadcaster.java b/src/audio/broadcast/AudioBroadcaster.java
index cbec6e17..59c903c7 100644
--- a/src/audio/broadcast/AudioBroadcaster.java
+++ b/src/audio/broadcast/AudioBroadcaster.java
@@ -103,10 +103,23 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
protected abstract void broadcastAudio(byte[] audio);
/**
- * Broadcasts the next song's audio metadata prior to streaming the next song.
- * @param metadata
+ * Protocol-specific metadata updater
*/
- protected abstract void broadcastMetadata(AudioMetadata metadata);
+ protected abstract IBroadcastMetadataUpdater getMetadataUpdater();
+
+ /**
+ * Broadcasts the next song's audio metadata prior to streaming the next song.
+ * @param metadata for the next recording that will be streamed
+ */
+ protected void broadcastMetadata(AudioMetadata metadata)
+ {
+ IBroadcastMetadataUpdater metadataUpdater = getMetadataUpdater();
+
+ if(metadataUpdater != null)
+ {
+ metadataUpdater.update(metadata);
+ }
+ }
/**
* Disconnects the broadcaster from the remote server for a reset or final stop.
diff --git a/src/audio/broadcast/BroadcastFactory.java b/src/audio/broadcast/BroadcastFactory.java
index 5f8b47fa..3fc95768 100644
--- a/src/audio/broadcast/BroadcastFactory.java
+++ b/src/audio/broadcast/BroadcastFactory.java
@@ -32,6 +32,7 @@ import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import audio.broadcast.shoutcast.v1.ShoutcastV1ConfigurationEditor;
import audio.broadcast.shoutcast.v2.ShoutcastV2AudioBroadcaster;
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
+import audio.broadcast.shoutcast.v2.ShoutcastV2ConfigurationEditor;
import audio.convert.IAudioConverter;
import audio.convert.ISilenceGenerator;
import audio.convert.MP3AudioConverter;
@@ -172,6 +173,9 @@ public class BroadcastFactory
case SHOUTCAST_V1:
editor = new ShoutcastV1ConfigurationEditor(broadcastModel, aliasModel, settingsManager);
break;
+ case SHOUTCAST_V2:
+ editor = new ShoutcastV2ConfigurationEditor(broadcastModel, aliasModel, settingsManager);
+ break;
default:
editor = new EmptyEditor();
break;
diff --git a/src/audio/broadcast/BroadcastModel.java b/src/audio/broadcast/BroadcastModel.java
index 05b59cfe..fa0f10d2 100644
--- a/src/audio/broadcast/BroadcastModel.java
+++ b/src/audio/broadcast/BroadcastModel.java
@@ -488,60 +488,67 @@ public class BroadcastModel extends AbstractTableModel implements Listener
+ *
+ ******************************************************************************/
+package audio.broadcast;
+
+import audio.metadata.AudioMetadata;
+
+public interface IBroadcastMetadataUpdater
+{
+ /**
+ * Updates broadcast audio metadata
+ *
+ * @param metadata containing metadata attributes
+ */
+ void update(AudioMetadata metadata);
+}
diff --git a/src/audio/broadcast/icecast/IcecastAudioBroadcaster.java b/src/audio/broadcast/icecast/IcecastAudioBroadcaster.java
index 85eafb27..e7d7ed85 100644
--- a/src/audio/broadcast/icecast/IcecastAudioBroadcaster.java
+++ b/src/audio/broadcast/icecast/IcecastAudioBroadcaster.java
@@ -20,12 +20,12 @@ package audio.broadcast.icecast;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastConfiguration;
-import audio.metadata.AudioMetadata;
+import audio.broadcast.IBroadcastMetadataUpdater;
import controller.ThreadPoolManager;
public abstract class IcecastAudioBroadcaster extends AudioBroadcaster
{
- private IcecastMetadataUpdater mIcecastMetadataUpdater;
+ private IBroadcastMetadataUpdater mMetadataUpdater;
public IcecastAudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration broadcastConfiguration)
{
@@ -40,17 +40,14 @@ public abstract class IcecastAudioBroadcaster extends AudioBroadcaster
return (IcecastConfiguration)getBroadcastConfiguration();
}
- /**
- * Broadcasts an audio metadata update
- */
@Override
- protected void broadcastMetadata(AudioMetadata metadata)
+ protected IBroadcastMetadataUpdater getMetadataUpdater()
{
- if(mIcecastMetadataUpdater == null)
+ if(mMetadataUpdater == null)
{
- mIcecastMetadataUpdater = new IcecastMetadataUpdater(getThreadPoolManager(), getConfiguration());
+ mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getThreadPoolManager(), getConfiguration());
}
- mIcecastMetadataUpdater.update(metadata);
+ return mMetadataUpdater;
}
}
diff --git a/src/audio/broadcast/icecast/IcecastMetadataUpdater.java b/src/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java
similarity index 96%
rename from src/audio/broadcast/icecast/IcecastMetadataUpdater.java
rename to src/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java
index 002a2790..14b9867f 100644
--- a/src/audio/broadcast/icecast/IcecastMetadataUpdater.java
+++ b/src/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java
@@ -18,6 +18,7 @@
******************************************************************************/
package audio.broadcast.icecast;
+import audio.broadcast.IBroadcastMetadataUpdater;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
@@ -25,7 +26,6 @@ import controller.ThreadPoolManager;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
-import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.http.HttpClientCodec;
import org.apache.mina.http.HttpRequestImpl;
import org.apache.mina.http.api.HttpMethod;
@@ -40,7 +40,6 @@ import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
@@ -48,9 +47,9 @@ import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
-public class IcecastMetadataUpdater
+public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater
{
- private final static Logger mLog = LoggerFactory.getLogger(IcecastMetadataUpdater.class);
+ private final static Logger mLog = LoggerFactory.getLogger(IcecastBroadcastMetadataUpdater.class);
private final static String UTF8 = "UTF-8";
private ThreadPoolManager mThreadPoolManager;
@@ -68,7 +67,7 @@ public class IcecastMetadataUpdater
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
* sequence, those updates will be queued and processed in the order received.
*/
- public IcecastMetadataUpdater(ThreadPoolManager threadPoolManager, IcecastConfiguration icecastConfiguration)
+ public IcecastBroadcastMetadataUpdater(ThreadPoolManager threadPoolManager, IcecastConfiguration icecastConfiguration)
{
mThreadPoolManager = threadPoolManager;
mIcecastConfiguration = icecastConfiguration;
@@ -82,7 +81,7 @@ public class IcecastMetadataUpdater
if (mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
-// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter(IcecastMetadataUpdater.class));
+// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter(IcecastBroadcastMetadataUpdater.class));
mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec());
//Each metadata update session is single-use, so we'll shut it down upon success or failure
diff --git a/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java b/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java
index c42cc11d..69d49f1c 100644
--- a/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java
+++ b/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java
@@ -58,7 +58,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
* Note: use @see IcecastHTTPAudioBroadcaster for Icecast version 2.4.x and newer.
*
* This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
- * IcecastTCPIOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
+ * ShoutcastV2IOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
*
* @param configuration for the Icecast stream
*/
diff --git a/src/audio/convert/MP3FrameInspector.java b/src/audio/convert/MP3FrameInspector.java
new file mode 100644
index 00000000..56c91a4a
--- /dev/null
+++ b/src/audio/convert/MP3FrameInspector.java
@@ -0,0 +1,89 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2017 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MP3FrameInspector
+{
+ private final static Logger mLog = LoggerFactory.getLogger(MP3FrameInspector.class);
+
+ public static void inspect(byte[] frames)
+ {
+ if(frames == null || frames.length == 0)
+ {
+ return;
+ }
+
+ if(frames.length % 144 != 0)
+ {
+ log("Frame byte array not multiple of 144 byte frame size - length:" + frames.length, frames);
+ return;
+ }
+
+ //Check for erroneous frame sync
+ for(int x = 0; x < frames.length - 1; x++)
+ {
+ if(frames[x] == (byte)0xFF && (frames[x + 1] & 0xE0) == 0xE0 && (x % 144) != 0)
+ {
+ log("Bad frame sync detected at byte " + x + " in frame " + (x / 144), frames);
+ return;
+ }
+ }
+
+ //Check MP3 headers
+
+// log("Frames ...", frames);
+ }
+
+ private static void log(String message, byte[] frames)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(message).append("\n");
+
+ int frameCounter = 0;
+ sb.append(frameCounter++).append(" ");
+
+ for(int x = 0; x < frames.length; x++)
+ {
+ if(x > 0 && x % 144 == 0)
+ {
+ sb.append("\n");
+ sb.append(frameCounter++).append(" ");
+ }
+
+ sb.append(String.format("%02X ", frames[x]));
+ }
+
+ mLog.info(sb.toString());
+ }
+
+ public static void main(String[] args)
+ {
+ mLog.info("Starting ...");
+ MP3SilenceGenerator gen = new MP3SilenceGenerator();
+
+ byte[] audio = gen.generate(173);
+
+ inspect(audio);
+
+ mLog.info("Finished");
+ }
+}
diff --git a/src/audio/convert/MP3SilenceGenerator.java b/src/audio/convert/MP3SilenceGenerator.java
index 2dbe8264..8e68e6f3 100644
--- a/src/audio/convert/MP3SilenceGenerator.java
+++ b/src/audio/convert/MP3SilenceGenerator.java
@@ -19,13 +19,12 @@
package audio.convert;
import audio.AudioPacket;
-import audio.broadcast.AudioBroadcaster;
-import audio.convert.MP3AudioConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.mp3.MP3Recorder;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
public class MP3SilenceGenerator implements ISilenceGenerator
@@ -33,14 +32,13 @@ public class MP3SilenceGenerator implements ISilenceGenerator
private final static Logger mLog = LoggerFactory.getLogger(MP3SilenceGenerator.class);
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE);
+ private byte[] mPreviousPartialFrameData;
/**
* Generates MP3 audio silence frames
*/
public MP3SilenceGenerator()
{
- //Prime the conversion buffer with 172 ms of audio to produce (and throw away) the first 20 byte buffer
- generate(172);
}
public byte[] generate(long duration)
@@ -50,34 +48,71 @@ public class MP3SilenceGenerator implements ISilenceGenerator
AudioPacket silencePacket = new AudioPacket(silence, null);
List silencePackets = new ArrayList<>();
silencePackets.add(silencePacket);
- return mGenerator.convert(silencePackets);
+
+ byte[] frameData = mGenerator.convert(silencePackets);
+
+ frameData = merge(mPreviousPartialFrameData, frameData);
+
+ if(frameData != null && frameData.length > 0)
+ {
+ if(frameData.length < 144)
+ {
+ mPreviousPartialFrameData = frameData;
+ return null;
+ }
+ else if((frameData.length % 144) == 0)
+ {
+ mPreviousPartialFrameData = null;
+ return frameData;
+ }
+ else
+ {
+ int integralFrameLength = (int)(frameData.length / 144) * 144;
+ mPreviousPartialFrameData = Arrays.copyOfRange(frameData, integralFrameLength, frameData.length);
+ return Arrays.copyOf(frameData, integralFrameLength);
+ }
+ }
+
+ return null;
+ }
+
+ private static byte[] merge(byte[] a, byte[] b)
+ {
+ if(a == null && b == null)
+ {
+ return null;
+ }
+ else if(a == null)
+ {
+ return b;
+ }
+ else if(b == null)
+ {
+ return a;
+ }
+
+ byte[] c = new byte[a.length + b.length];
+ System.arraycopy(a,0,c,0,a.length);
+ System.arraycopy(b,0,c,a.length,b.length);
+
+ return c;
}
public static void main(String[] args)
{
mLog.debug("Starting ...");
-// MP3SilenceGenerator generator = new MP3SilenceGenerator();
-//
-// int sampleCount = 0;
-//
-// int block = 1;
-//
-// for(long x = 0; x < 5000; x ++)
-// {
-// byte[] silence = generator.generate(block);
-//
-// sampleCount += block;
-//
-// if(silence.length > 0)
-// {
-// mLog.debug("Silence:" + x + " Count:" + sampleCount + " Length:" + silence.length);
-// }
-// }
-
- for(int x = 1; x <= 288; x++)
+ for(long x = 243; x < 500; x ++)
{
- mLog.debug(x + ": " + (x / 144));
+ MP3SilenceGenerator generator = new MP3SilenceGenerator();
+
+ byte[] silence = generator.generate(x);
+
+ if(silence != null && silence.length > 0)
+ {
+ mLog.debug("Silence:" + x + " Length:" + silence.length);
+ MP3FrameInspector.inspect(silence);
+ }
}
mLog.debug("Finished");
diff --git a/src/record/mp3/MP3FileInspector.java b/src/record/mp3/MP3FileInspector.java
deleted file mode 100644
index f0ed8b8b..00000000
--- a/src/record/mp3/MP3FileInspector.java
+++ /dev/null
@@ -1,119 +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 record.mp3;
-
-import audio.broadcast.BroadcastModel;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import properties.SystemProperties;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.function.Consumer;
-
-public class MP3FileInspector
-{
- private final static Logger mLog = LoggerFactory.getLogger(MP3FileInspector.class);
-
- private Path mPath;
-
- public MP3FileInspector(Path path)
- {
- mPath = path;
- }
-
- public void inspect()
- {
- try
- {
- byte[] audio = Files.readAllBytes(mPath);
-
- mLog.debug("File:" + mPath.toAbsolutePath().toString() + " Length:" + audio.length + " Residual:" + audio.length % 144);
-
-// mLog.debug(format(Arrays.copyOfRange(audio, 0, 20)));
-// for(int x = 0; x < audio.length - 3; x++)
-// {
-// if(audio[x] == (byte)0xFF && ((audio[x + 1] & 0xE0) == 0xE0))
-// {
-// mLog.debug("\tFrame: " + x + "\t" + format(Arrays.copyOfRange(audio, x, x + 4)));
-//
-// x += 143;
-// }
-// }
-
- }
- catch(IOException ioe)
- {
- mLog.error("Error reading temporary audio stream recording [" + mPath.toString() +
- "] - skipping recording");
- }
- }
-
- public static void main(String[] args)
- {
- mLog.debug("Starting ...");
-
- Path path = SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY);
-
- if(path != null && Files.isDirectory(path))
- {
- StringBuilder sb = new StringBuilder();
- sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
- sb.append("*.*");
-
- try(DirectoryStream directoryStream = Files.newDirectoryStream(path, sb.toString()))
- {
- directoryStream.forEach(new Consumer()
- {
- @Override
- public void accept(Path path)
- {
- MP3FileInspector inspector = new MP3FileInspector(path);
- inspector.inspect();
- }
- });
- }
- catch(IOException ioe)
- {
- mLog.error("Error discovering orphaned temporary stream recording files", ioe);
- }
- }
-
-
-
- mLog.debug("Finished");
- }
-
- private String format(byte[] bytes)
- {
- StringBuilder sb = new StringBuilder();
-
- for(byte b: bytes)
- {
- sb.append(String.format("%02X", b));
- }
-
- return sb.toString();
- }
-}