diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml
index 31ec5bc1..408050c7 100644
--- a/.idea/dictionaries/denny.xml
+++ b/.idea/dictionaries/denny.xml
@@ -1,12 +1,22 @@
+ bahroo
+ broadcastify
esnid
fleetsync
+ icecast
ident
polyphase
+ samplerate
+ sdrtrunk
+ sheirer
+ shoutcast
+ streamable
talkgroup
tgid
+ ultravox
+ xtea
\ No newline at end of file
diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml
index 8ac29dbb..c2d94bbd 100644
--- a/.idea/libraries/imports.xml
+++ b/.idea/libraries/imports.xml
@@ -8,7 +8,6 @@
-
@@ -21,11 +20,46 @@
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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/org_asynchttpclient_async_http_client_2_0_18.xml b/.idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml
new file mode 100644
index 00000000..884ea3f9
--- /dev/null
+++ b/.idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 18d93cc0..2c55e21a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -38,20 +38,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 00000000..e96534fb
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG b/CHANGELOG
index 198a6063..83a91896 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,10 @@
-0.3.0 2016xxxx (work in progress)
- - Requires Java 8 for compatibility with eventual changeover to JavaFX
+0.3.0 2017xxxx (work in progress)
+ - FEATURE: audio streaming - adds capability to stream to multiple remote broadcast
+ servers (Broadcastify, Icecast v2.3 and v2.4+, and Shoutcast V1 and V2). Each
+ stream can be independently configured with a delay. Audio calls are directed
+ to one or more streams by attaching one or more stream identifiers to each alias.
+ - Requires Java 8 (or newer) for compatibility with eventual changeover to JavaFX
- Extensive GUI updates for tuner, channel and alias management.
- Updated alias list management of alias id values and wildcards. Weighting is now
applied to each wildcard so that they can be ordered from most specific to least
@@ -14,10 +18,10 @@
decoders and filters
- New audio playback interface with global mute and support for one or two
audio channels
- - Audio playback is prioritized using a new Call Priority alias action
+ - Audio playback is prioritized using a new Call Priority name action
with selected channels having the highest priority and any audio without
- an alias identifier having the lowest priority.
- - Updated Audio recording per call with new 'Non-Recordable' alias action
+ an name identifier having the lowest priority.
+ - Updated Audio recording per call with new 'Non-Recordable' name action
- Updated Baseband (I/Q) recording at 48 kHz
- Added Airspy tuner support
- New traffic channel pool manager for trunked call following
diff --git a/config/logback.xml b/config/logback.xml
index 631e8aa5..9fe5f368 100644
--- a/config/logback.xml
+++ b/config/logback.xml
@@ -27,4 +27,7 @@
+
+
+
\ No newline at end of file
diff --git a/images/broadcastify.png b/images/broadcastify.png
new file mode 100644
index 00000000..6671ff74
Binary files /dev/null and b/images/broadcastify.png differ
diff --git a/images/icecast.png b/images/icecast.png
new file mode 100644
index 00000000..e57643f2
Binary files /dev/null and b/images/icecast.png differ
diff --git a/images/shoutcast.png b/images/shoutcast.png
new file mode 100644
index 00000000..083f2caf
Binary files /dev/null and b/images/shoutcast.png differ
diff --git a/imports/activation-1.1.jar b/imports/activation-1.1.jar
new file mode 100644
index 00000000..53f82a1c
Binary files /dev/null and b/imports/activation-1.1.jar differ
diff --git a/imports/axis.jar b/imports/axis.jar
new file mode 100644
index 00000000..20b09a59
Binary files /dev/null and b/imports/axis.jar differ
diff --git a/imports/commons-codec-1.9.jar b/imports/commons-codec-1.9.jar
new file mode 100644
index 00000000..ef35f1c5
Binary files /dev/null and b/imports/commons-codec-1.9.jar differ
diff --git a/imports/commons-discovery-0.2.jar b/imports/commons-discovery-0.2.jar
new file mode 100644
index 00000000..b8855484
Binary files /dev/null and b/imports/commons-discovery-0.2.jar differ
diff --git a/imports/commons-logging-1.2.jar b/imports/commons-logging-1.2.jar
new file mode 100644
index 00000000..93a3b9f6
Binary files /dev/null and b/imports/commons-logging-1.2.jar differ
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/java-mail-1.4.jar b/imports/java-mail-1.4.jar
new file mode 100644
index 00000000..e6f70832
Binary files /dev/null and b/imports/java-mail-1.4.jar differ
diff --git a/imports/javassist-3.20.0-GA.jar b/imports/javassist-3.20.0-GA.jar
new file mode 100644
index 00000000..0f1ff725
Binary files /dev/null and b/imports/javassist-3.20.0-GA.jar differ
diff --git a/imports/jaxrpc.jar b/imports/jaxrpc.jar
new file mode 100644
index 00000000..a2c13d9a
Binary files /dev/null and b/imports/jaxrpc.jar differ
diff --git a/imports/mina-core-2.0.16.jar b/imports/mina-core-2.0.16.jar
new file mode 100644
index 00000000..0e6d9883
Binary files /dev/null and b/imports/mina-core-2.0.16.jar differ
diff --git a/imports/mina-http-2.0.16.jar b/imports/mina-http-2.0.16.jar
new file mode 100644
index 00000000..62a5efcc
Binary files /dev/null and b/imports/mina-http-2.0.16.jar differ
diff --git a/imports/radioreference-14.jar b/imports/radioreference-14.jar
new file mode 100644
index 00000000..2c7a5d90
Binary files /dev/null and b/imports/radioreference-14.jar differ
diff --git a/imports/slf4j-api-1.7.21.jar b/imports/slf4j-api-1.7.21.jar
new file mode 100644
index 00000000..2a5c33ec
Binary files /dev/null and b/imports/slf4j-api-1.7.21.jar differ
diff --git a/imports/slf4j-api-1.7.7.jar b/imports/slf4j-api-1.7.7.jar
deleted file mode 100644
index b28e220b..00000000
Binary files a/imports/slf4j-api-1.7.7.jar and /dev/null differ
diff --git a/imports/wsdl4j-1.5.1.jar b/imports/wsdl4j-1.5.1.jar
new file mode 100644
index 00000000..c6254ee6
Binary files /dev/null and b/imports/wsdl4j-1.5.1.jar differ
diff --git a/sdrtrunk.iml b/sdrtrunk.iml
index 4ceeed69..04cc0636 100644
--- a/sdrtrunk.iml
+++ b/sdrtrunk.iml
@@ -20,6 +20,6 @@
-
+
\ No newline at end of file
diff --git a/src/alias/Alias.java b/src/alias/Alias.java
index b17a92b9..3770b014 100644
--- a/src/alias/Alias.java
+++ b/src/alias/Alias.java
@@ -19,7 +19,10 @@ package alias;
import java.awt.Color;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
@@ -30,6 +33,7 @@ import javax.xml.bind.annotation.XmlTransient;
import alias.action.AliasAction;
import alias.id.AliasID;
import alias.id.AliasIDType;
+import alias.id.broadcast.BroadcastChannel;
import alias.id.nonrecordable.NonRecordable;
import alias.id.priority.Priority;
@@ -175,7 +179,34 @@ public class Alias
{
return !mAliasActions.isEmpty();
}
-
+
+ /**
+ * Perform any validation/cleanup actions on this alias.
+ */
+ public void validate()
+ {
+ //Check for duplicate broadcast channels
+ List broadcastChannels = getBroadcastChannels();
+
+ if(broadcastChannels.size() > 0)
+ {
+ Set channelNames = new TreeSet<>();
+
+ for(BroadcastChannel broadcastChannel: broadcastChannels)
+ {
+ String channelName = broadcastChannel.getChannelName();
+
+ if(channelName == null || channelName.isEmpty() || channelNames.contains(channelName))
+ {
+ removeAliasID(broadcastChannel);
+ }
+ else
+ {
+ channelNames.add(channelName);
+ }
+ }
+ }
+ }
/**
* Returns the priority level of this alias, if defined, or the default priority
*/
@@ -184,7 +215,7 @@ public class Alias
{
for( AliasID id: mAliasIDs )
{
- if( id.getType() == AliasIDType.Priority )
+ if( id.getType() == AliasIDType.PRIORITY)
{
return ((Priority)id).getPriority();
}
@@ -197,7 +228,7 @@ public class Alias
{
for( AliasID id: mAliasIDs )
{
- if( id.getType() == AliasIDType.Priority )
+ if( id.getType() == AliasIDType.PRIORITY)
{
return true;
}
@@ -216,7 +247,7 @@ public class Alias
{
for( AliasID id: mAliasIDs )
{
- if( id.getType() == AliasIDType.Priority )
+ if( id.getType() == AliasIDType.PRIORITY)
{
((Priority)id).setPriority( priority );
return;
@@ -229,7 +260,7 @@ public class Alias
addAliasID( p );
}
}
-
+
/**
* Inspects the alias for a non-recordable alias id. Default is true;
*/
@@ -238,7 +269,7 @@ public class Alias
{
for( AliasID id: getId() )
{
- if( id.getType() == AliasIDType.NonRecordable )
+ if( id.getType() == AliasIDType.NON_RECORDABLE)
{
return false;
}
@@ -258,7 +289,7 @@ public class Alias
for( AliasID id: getId() )
{
- if( id.getType() == AliasIDType.NonRecordable )
+ if( id.getType() == AliasIDType.NON_RECORDABLE)
{
toRemove = id;
break;
@@ -274,7 +305,7 @@ public class Alias
{
for( AliasID id: getId() )
{
- if( id.getType() == AliasIDType.NonRecordable )
+ if( id.getType() == AliasIDType.NON_RECORDABLE)
{
return;
}
@@ -283,4 +314,62 @@ public class Alias
addAliasID( new NonRecordable() );
}
}
+
+ /**
+ * Inspects the alias for broadcast channel/streamable alias ids. Default is false;
+ */
+ @XmlTransient
+ public boolean isStreamable()
+ {
+ for( AliasID id: getId() )
+ {
+ if( id.getType() == AliasIDType.BROADCAST_CHANNEL)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * List of broadcast channels specified for this alias.
+ */
+ @XmlTransient
+ public List getBroadcastChannels()
+ {
+ List broadcastChannels = new ArrayList<>();
+
+ for(AliasID id: getId())
+ {
+ if(id.getType() == AliasIDType.BROADCAST_CHANNEL)
+ {
+ broadcastChannels.add((BroadcastChannel)id);
+ }
+ }
+
+ return broadcastChannels;
+ }
+
+ /**
+ * Indicates if this alias contains a broadcast channel alias id with the channel name.
+ */
+ public boolean hasBroadcastChannel(String channel)
+ {
+ if(channel == null || channel.isEmpty())
+ {
+ return false;
+ }
+
+ for(AliasID id: getId())
+ {
+ if(id.getType() == AliasIDType.BROADCAST_CHANNEL &&
+ ((BroadcastChannel)id).getChannelName().contentEquals(channel))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/src/alias/AliasController.java b/src/alias/AliasController.java
index 60696bc6..0619672c 100644
--- a/src/alias/AliasController.java
+++ b/src/alias/AliasController.java
@@ -1,5 +1,6 @@
package alias;
+import audio.broadcast.BroadcastModel;
import gui.editor.Editor;
import java.awt.Color;
@@ -52,12 +53,12 @@ public class AliasController extends JPanel
private IconCellRenderer mIconCellRenderer;
- public AliasController( AliasModel aliasModel, SettingsManager settingsManager )
+ public AliasController(AliasModel aliasModel, BroadcastModel broadcastModel, SettingsManager settingsManager )
{
mAliasModel = aliasModel;
- mAliasEditor = new AliasEditor( mAliasModel, settingsManager );
- mMultipleAliasEditor = new MultipleAliasEditor( mAliasModel, settingsManager );
+ mAliasEditor = new AliasEditor( mAliasModel, broadcastModel, settingsManager );
+ mMultipleAliasEditor = new MultipleAliasEditor( mAliasModel, broadcastModel, settingsManager );
mIconCellRenderer = new IconCellRenderer( settingsManager );
diff --git a/src/alias/AliasEditor.java b/src/alias/AliasEditor.java
index b642df01..9cdd4632 100644
--- a/src/alias/AliasEditor.java
+++ b/src/alias/AliasEditor.java
@@ -17,6 +17,7 @@
******************************************************************************/
package alias;
+import audio.broadcast.BroadcastModel;
import gui.editor.Editor;
import java.awt.Color;
@@ -49,11 +50,11 @@ public class AliasEditor extends Editor
private AliasIdentifierEditor mAliasIdentifierEditor;
private AliasActionEditor mAliasActionEditor;
- public AliasEditor( AliasModel aliasModel, SettingsManager settingsManager )
+ public AliasEditor(AliasModel aliasModel, BroadcastModel broadcastModel, SettingsManager settingsManager )
{
mAliasModel = aliasModel;
mAliasNameEditor = new AliasNameEditor( mAliasModel, settingsManager );
- mAliasIdentifierEditor = new AliasIdentifierEditor( aliasModel );
+ mAliasIdentifierEditor = new AliasIdentifierEditor( aliasModel, broadcastModel );
mAliasActionEditor = new AliasActionEditor( aliasModel );
mAliasModel.addListener( this );
diff --git a/src/alias/AliasFactory.java b/src/alias/AliasFactory.java
index 3f91ccbe..7acb920e 100644
--- a/src/alias/AliasFactory.java
+++ b/src/alias/AliasFactory.java
@@ -17,8 +17,6 @@
******************************************************************************/
package alias;
-import gui.editor.Editor;
-import gui.editor.EmptyEditor;
import alias.action.AliasAction;
import alias.action.AliasActionType;
import alias.action.beep.BeepAction;
@@ -29,6 +27,8 @@ import alias.action.script.ScriptAction;
import alias.action.script.ScriptActionEditor;
import alias.id.AliasID;
import alias.id.AliasIDType;
+import alias.id.broadcast.BroadcastChannel;
+import alias.id.broadcast.BroadcastChannelEditor;
import alias.id.esn.ESNEditor;
import alias.id.esn.Esn;
import alias.id.fleetsync.FleetsyncID;
@@ -53,6 +53,9 @@ import alias.id.talkgroup.TalkgroupID;
import alias.id.talkgroup.TalkgroupIDEditor;
import alias.id.uniqueID.UniqueID;
import alias.id.uniqueID.UniqueIDEditor;
+import audio.broadcast.BroadcastModel;
+import gui.editor.Editor;
+import gui.editor.EmptyEditor;
public class AliasFactory
{
@@ -60,22 +63,27 @@ public class AliasFactory
{
switch( id.getType() )
{
+ case BROADCAST_CHANNEL:
+ BroadcastChannel originalBroadcast = (BroadcastChannel)id;
+ BroadcastChannel copyBroadcast = new BroadcastChannel();
+ copyBroadcast.setChannelName(originalBroadcast.getChannelName());
+ return copyBroadcast;
case ESN:
Esn originalESN = (Esn)id;
Esn copyESN = new Esn();
copyESN.setEsn( originalESN.getEsn() );
return copyESN;
- case Fleetsync:
+ case FLEETSYNC:
FleetsyncID originalFleetsyncID = (FleetsyncID)id;
FleetsyncID copyFleetsyncID = new FleetsyncID();
copyFleetsyncID.setIdent( originalFleetsyncID.getIdent() );
return copyFleetsyncID;
- case LTRNetUID:
+ case LTR_NET_UID:
UniqueID originalUniqueID = (UniqueID)id;
UniqueID copyUniqueID = new UniqueID();
copyUniqueID.setUid( originalUniqueID.getUid() );
return copyUniqueID;
- case LoJack:
+ case LOJACK:
LoJackFunctionAndID originalLoJackFunctionAndID = (LoJackFunctionAndID)id;
LoJackFunctionAndID copyLoJackFunctionAndID = new LoJackFunctionAndID();
copyLoJackFunctionAndID.setFunction( originalLoJackFunctionAndID.getFunction() );
@@ -96,24 +104,24 @@ public class AliasFactory
MPT1327ID copyMPT1327ID = new MPT1327ID();
copyMPT1327ID.setIdent( originalMPT1327ID.getIdent() );
return copyMPT1327ID;
- case NonRecordable:
+ case NON_RECORDABLE:
return new NonRecordable();
- case Priority:
+ case PRIORITY:
Priority originalPriority = (Priority)id;
Priority copyPriority = new Priority();
copyPriority.setPriority( originalPriority.getPriority() );
return copyPriority;
- case Site:
+ case SITE:
SiteID originalSiteID = (SiteID)id;
SiteID copySiteID = new SiteID();
copySiteID.setSite( originalSiteID.getSite() );
return copySiteID;
- case Talkgroup:
+ case TALKGROUP:
TalkgroupID originalTalkgroupID = (TalkgroupID)id;
TalkgroupID copyTalkgroupID = new TalkgroupID();
copyTalkgroupID.setTalkgroup( originalTalkgroupID.getTalkgroup() );
return copyTalkgroupID;
- case Status:
+ case STATUS:
StatusID originalStatusID = (StatusID)id;
StatusID copyStatusID = new StatusID();
copyStatusID.setStatus( originalStatusID.getStatus() );
@@ -183,19 +191,21 @@ public class AliasFactory
return copy;
}
- public static Editor getEditor( AliasID aliasID )
+ public static Editor getEditor( AliasID aliasID, BroadcastModel broadcastModel )
{
if( aliasID != null )
{
switch( aliasID.getType() )
{
+ case BROADCAST_CHANNEL:
+ return new BroadcastChannelEditor(aliasID, broadcastModel);
case ESN:
return new ESNEditor( aliasID );
- case Fleetsync:
+ case FLEETSYNC:
return new FleetsyncIDEditor( aliasID );
- case LTRNetUID:
+ case LTR_NET_UID:
return new UniqueIDEditor( aliasID );
- case LoJack:
+ case LOJACK:
return new LoJackIDEditor( aliasID );
case MDC1200:
return new MDC1200IDEditor( aliasID );
@@ -203,15 +213,15 @@ public class AliasFactory
return new MINEditor( aliasID );
case MPT1327:
return new MPT1327IDEditor( aliasID );
- case NonRecordable:
+ case NON_RECORDABLE:
return new NonRecordableEditor( aliasID );
- case Priority:
+ case PRIORITY:
return new PriorityEditor( aliasID );
- case Site:
+ case SITE:
return new SiteIDEditor( aliasID );
- case Status:
+ case STATUS:
return new StatusIDEditor( aliasID );
- case Talkgroup:
+ case TALKGROUP:
return new TalkgroupIDEditor( aliasID );
default:
break;
@@ -245,13 +255,15 @@ public class AliasFactory
{
switch( type )
{
+ case BROADCAST_CHANNEL:
+ return new BroadcastChannel();
case ESN:
return new Esn();
- case Fleetsync:
+ case FLEETSYNC:
return new FleetsyncID();
- case LTRNetUID:
+ case LTR_NET_UID:
return new UniqueID();
- case LoJack:
+ case LOJACK:
return new LoJackFunctionAndID();
case MDC1200:
return new MDC1200ID();
@@ -259,15 +271,15 @@ public class AliasFactory
return new Min();
case MPT1327:
return new MPT1327ID();
- case NonRecordable:
+ case NON_RECORDABLE:
return new NonRecordable();
- case Priority:
+ case PRIORITY:
return new Priority();
- case Site:
+ case SITE:
return new SiteID();
- case Status:
+ case STATUS:
return new StatusID();
- case Talkgroup:
+ case TALKGROUP:
return new TalkgroupID();
default:
throw new IllegalArgumentException( "Unrecognized Alias ID type: " + type );
diff --git a/src/alias/AliasList.java b/src/alias/AliasList.java
index 0d5e64e1..8b5a6ff8 100644
--- a/src/alias/AliasList.java
+++ b/src/alias/AliasList.java
@@ -41,7 +41,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.regex.Pattern;
public class AliasList implements Listener
{
@@ -120,7 +119,7 @@ public class AliasList implements Listener
mESN.put(esn, alias);
}
break;
- case Fleetsync:
+ case FLEETSYNC:
String fleetsync = ((FleetsyncID)id).getIdent();
if(fleetsync != null)
@@ -134,7 +133,7 @@ public class AliasList implements Listener
mFleetsync.put(fleetsync, alias);
}
break;
- case LoJack:
+ case LOJACK:
mLoJack.put((LoJackFunctionAndID) id, alias);
break;
case MDC1200:
@@ -179,10 +178,10 @@ public class AliasList implements Listener
mMobileID.put(min, alias);
}
break;
- case LTRNetUID:
+ case LTR_NET_UID:
mUniqueID.put(((UniqueID)id).getUid(), alias);
break;
- case Site:
+ case SITE:
String siteID = ((SiteID)id).getSite();
if(siteID != null)
@@ -196,10 +195,10 @@ public class AliasList implements Listener
mSiteID.put(siteID, alias);
}
break;
- case Status:
+ case STATUS:
mStatus.put(((StatusID) id).getStatus(), alias);
break;
- case Talkgroup:
+ case TALKGROUP:
String tgid = ((TalkgroupID)id).getTalkgroup();
if(tgid != null)
@@ -213,8 +212,9 @@ public class AliasList implements Listener
mTalkgroup.put(tgid, alias);
}
break;
- case NonRecordable:
- case Priority:
+ case BROADCAST_CHANNEL:
+ case NON_RECORDABLE:
+ case PRIORITY:
//We don't maintain lookups for these items
break;
default:
@@ -279,7 +279,7 @@ public class AliasList implements Listener
mESN.remove(esn);
break;
- case Fleetsync:
+ case FLEETSYNC:
String fleetsync = ((FleetsyncID)id).getIdent();
if(fleetsync != null)
@@ -292,7 +292,7 @@ public class AliasList implements Listener
mFleetsync.remove(fleetsync);
}
break;
- case LoJack:
+ case LOJACK:
mLoJack.remove((LoJackFunctionAndID) id);
break;
case MDC1200:
@@ -334,16 +334,16 @@ public class AliasList implements Listener
mMobileID.remove(min);
}
break;
- case LTRNetUID:
+ case LTR_NET_UID:
mUniqueID.remove(((UniqueID) id).getUid());
break;
- case Site:
+ case SITE:
mSiteID.remove(((SiteID) id).getSite());
break;
- case Status:
+ case STATUS:
mStatus.remove(((StatusID) id).getStatus());
break;
- case Talkgroup:
+ case TALKGROUP:
String tgid = ((TalkgroupID)id).getTalkgroup();
if(tgid != null)
@@ -356,8 +356,8 @@ public class AliasList implements Listener
mTalkgroup.remove(tgid);
}
break;
- case NonRecordable:
- case Priority:
+ case NON_RECORDABLE:
+ case PRIORITY:
//We don't maintain lookups for these items
break;
default:
diff --git a/src/alias/AliasModel.java b/src/alias/AliasModel.java
index a051a0d0..a46bd532 100644
--- a/src/alias/AliasModel.java
+++ b/src/alias/AliasModel.java
@@ -26,6 +26,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.table.AbstractTableModel;
+import alias.id.broadcast.BroadcastChannel;
import sample.Broadcaster;
import sample.Listener;
import alias.AliasEvent.Event;
@@ -210,6 +211,33 @@ public class AliasModel extends AbstractTableModel
}
}
+ /**
+ * Renames any broadcast channels that have the previous name.
+ * @param previousName to rename
+ * @param newName to assign to the broadcast channel
+ */
+ public void renameBroadcastChannel(String previousName, String newName)
+ {
+ if(previousName == null || previousName.isEmpty() || newName == null || newName.isEmpty())
+ {
+ return;
+ }
+
+ for(Alias alias: mAliases)
+ {
+ if(alias.hasBroadcastChannel(previousName))
+ {
+ for(BroadcastChannel broadcastChannel: alias.getBroadcastChannels())
+ {
+ if(broadcastChannel.getChannelName().contentEquals(previousName))
+ {
+ broadcastChannel.setChannelName(newName);
+ }
+ }
+ }
+ }
+ }
+
@Override
public int getRowCount()
{
@@ -289,6 +317,14 @@ public class AliasModel extends AbstractTableModel
public void broadcast( AliasEvent event )
{
+ Alias alias = event.getAlias();
+
+ //Validate the alias following a user action that changed the alias or any alias IDs
+ if(alias != null)
+ {
+ alias.validate();
+ }
+
if( event.getEvent() == Event.CHANGE )
{
int index = mAliases.indexOf( event.getAlias() );
diff --git a/src/alias/MultipleAliasEditor.java b/src/alias/MultipleAliasEditor.java
index e22e0b1b..ba6861d1 100644
--- a/src/alias/MultipleAliasEditor.java
+++ b/src/alias/MultipleAliasEditor.java
@@ -1,5 +1,8 @@
package alias;
+import alias.id.AliasIDType;
+import alias.id.broadcast.BroadcastChannel;
+import audio.broadcast.BroadcastModel;
import gui.editor.Editor;
import java.awt.Color;
@@ -7,6 +10,7 @@ import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.util.Arrays;
import java.util.List;
import javax.swing.DefaultComboBoxModel;
@@ -33,385 +37,429 @@ import settings.SettingsManager;
import alias.AliasEvent.Event;
import alias.id.priority.Priority;
-public class MultipleAliasEditor extends Editor>
- implements Listener
+public class MultipleAliasEditor extends Editor>
+ implements Listener
{
- private static final long serialVersionUID = 1L;
- private final static Logger mLog = LoggerFactory.getLogger( MultipleAliasEditor.class );
-
- private static String HELP_TEXT =
- "Select attributes below to change for all selected aliases";
+ private static final long serialVersionUID = 1L;
+ private final static Logger mLog = LoggerFactory.getLogger(MultipleAliasEditor.class);
- private JLabel mAliasCountLabel;
- private JLabel mAliasCount;
+ private static String HELP_TEXT =
+ "Select attributes below to change for all selected aliases";
- private JCheckBox mListCheckBox = new JCheckBox( "List" );
- private JComboBox mListCombo = new JComboBox<>();
- private JCheckBox mGroupCheckBox = new JCheckBox( "Group" );
- private JComboBox mGroupCombo = new JComboBox<>();
- private JCheckBox mIconCheckBox = new JCheckBox( "Icon" );
+ private JLabel mAliasCountLabel;
+ private JLabel mAliasCount;
+
+ private JCheckBox mListCheckBox = new JCheckBox("List");
+ private JComboBox mListCombo = new JComboBox<>();
+ private JCheckBox mGroupCheckBox = new JCheckBox("Group");
+ private JComboBox mGroupCombo = new JComboBox<>();
+ private JCheckBox mIconCheckBox = new JCheckBox("Icon");
private JComboBox mMapIconCombo;
- private JCheckBox mColorCheckBox = new JCheckBox( "Color" );
+ private JCheckBox mColorCheckBox = new JCheckBox("Color");
private JButton mButtonColor;
private JButton mBtnIconManager;
- private JCheckBox mRecordCheckBox = new JCheckBox( "Record" );
- private JComboBox mRecordActionCombo = new JComboBox<>( Record.values() );
- private JCheckBox mPriorityCheckBox = new JCheckBox( "Priority" );
+ private JCheckBox mRecordCheckBox = new JCheckBox("Record");
+ private JComboBox mRecordActionCombo = new JComboBox<>(Record.values());
+ private JCheckBox mPriorityCheckBox = new JCheckBox("Priority");
private JSlider mPrioritySlider;
private JLabel mPrioritySliderLabel;
-
- private AliasModel mAliasModel;
- private SettingsManager mSettingsManager;
-
- public MultipleAliasEditor( AliasModel aliasModel, SettingsManager settingsManager )
- {
- mAliasModel = aliasModel;
- mAliasModel.addListener( this );
-
- mSettingsManager = settingsManager;
-
- init();
- }
-
- public List getAliases()
- {
- if( hasItem() )
- {
- return (List)getItem();
- }
-
- return null;
- }
-
- public void init()
- {
- setLayout( new MigLayout( "fill,wrap 2", "[grow,fill][grow,fill]",
- "[][][][][][][][][][][][][grow,fill]" ) );
-
- mAliasCountLabel = new JLabel( "Alias:" );
- add( mAliasCountLabel );
-
- mAliasCount = new JLabel( "Multiple" );
- add( mAliasCount, "span" );
-
- add( new JSeparator(), "span" );
-
- add( new JLabel( HELP_TEXT ), "span" );
-
- add( mListCheckBox );
+ private JCheckBox mStreamCheckBox = new JCheckBox("Stream");
+ private JComboBox mStreamCombo;
- mListCombo.setEditable( true );
- add( mListCombo, "wrap" );
+ private AliasModel mAliasModel;
+ private BroadcastModel mBroadcastModel;
+ private SettingsManager mSettingsManager;
- add( mGroupCheckBox );
+ public MultipleAliasEditor(AliasModel aliasModel, BroadcastModel broadcastModel, SettingsManager settingsManager)
+ {
+ mAliasModel = aliasModel;
+ mBroadcastModel = broadcastModel;
+ mSettingsManager = settingsManager;
- mGroupCombo.setEditable( true );
- add( mGroupCombo, "wrap" );
+ mAliasModel.addListener(this);
- add( mColorCheckBox );
+ init();
+ }
- mButtonColor = new JButton( "Select ..." );
- mButtonColor.addActionListener( new ActionListener()
- {
- @Override
- public void actionPerformed( ActionEvent e )
+ public List getAliases()
+ {
+ if (hasItem())
+ {
+ return (List) getItem();
+ }
+
+ return null;
+ }
+
+ public void init()
+ {
+ setLayout(new MigLayout("fill,wrap 2", "[grow,fill][grow,fill]",
+ "[][][][][][][][][][][][][][grow,fill]"));
+
+ mAliasCountLabel = new JLabel("Alias:");
+ add(mAliasCountLabel);
+
+ mAliasCount = new JLabel("Multiple");
+ add(mAliasCount, "span");
+
+ add(new JSeparator(), "span");
+
+ add(new JLabel(HELP_TEXT), "span");
+
+ add(mListCheckBox);
+
+ mListCombo.setEditable(true);
+ add(mListCombo, "wrap");
+
+ add(mGroupCheckBox);
+
+ mGroupCombo.setEditable(true);
+ add(mGroupCombo, "wrap");
+
+ add(mColorCheckBox);
+
+ mButtonColor = new JButton("Select ...");
+ mButtonColor.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
{
- Color newColor = JColorChooser.showDialog(
- MultipleAliasEditor.this,
- "Choose color for this alias", null );
+ Color newColor = JColorChooser.showDialog(
+ MultipleAliasEditor.this,
+ "Choose color for this alias", null);
- if( newColor != null )
- {
- mButtonColor.setForeground( newColor );
- mButtonColor.setBackground( newColor );
- }
+ if (newColor != null)
+ {
+ mButtonColor.setForeground(newColor);
+ mButtonColor.setBackground(newColor);
+ }
}
- } );
- add( mButtonColor, "wrap" );
-
- add( mIconCheckBox );
-
- mMapIconCombo = new JComboBox( mSettingsManager.getMapIcons() );
+ });
+ add(mButtonColor, "wrap");
- MapIconListCellRenderer renderer = new MapIconListCellRenderer();
- renderer.setPreferredSize( new Dimension( 200, 30 ) );
- mMapIconCombo.setRenderer( renderer );
- add( mMapIconCombo, "wrap" );
+ add(mIconCheckBox);
- //Dummy place holder
- add( new JLabel() );
-
- mBtnIconManager = new JButton( "Icon Manager" );
- mBtnIconManager.addActionListener( new ActionListener()
- {
- @Override
- public void actionPerformed( ActionEvent arg0 )
+ mMapIconCombo = new JComboBox(mSettingsManager.getMapIcons());
+
+ MapIconListCellRenderer renderer = new MapIconListCellRenderer();
+ renderer.setPreferredSize(new Dimension(200, 30));
+ mMapIconCombo.setRenderer(renderer);
+ add(mMapIconCombo, "wrap");
+
+ //Dummy place holder
+ add(new JLabel());
+
+ mBtnIconManager = new JButton("Icon Manager");
+ mBtnIconManager.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent arg0)
{
- final IconManager iconManager =
- new IconManager( mSettingsManager, MultipleAliasEditor.this );
-
- EventQueue.invokeLater( new Runnable()
- {
- @Override
- public void run()
- {
- iconManager.setVisible( true );
- }
- } );
+ final IconManager iconManager =
+ new IconManager(mSettingsManager, MultipleAliasEditor.this);
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ iconManager.setVisible(true);
+ }
+ });
}
- } );
-
- add( mBtnIconManager, "span,wrap" );
-
- add( mRecordCheckBox );
- add( mRecordActionCombo );
+ });
- //Placeholder
- add( new JLabel( " " ) );
- mPrioritySliderLabel = new JLabel( "Priority: " + Priority.MIN_PRIORITY );
- add( mPrioritySliderLabel );
-
- add( mPriorityCheckBox );
-
- mPrioritySlider = new JSlider( JSlider.HORIZONTAL,
- Priority.MIN_PRIORITY,
- Priority.MAX_PRIORITY + 1,
- Priority.MIN_PRIORITY );
+ add(mBtnIconManager, "span,wrap");
- mPrioritySlider.setMajorTickSpacing( 20 );
- mPrioritySlider.setMinorTickSpacing( 5 );
- mPrioritySlider.setPaintTicks( true );
- mPrioritySlider.setLabelTable( mPrioritySlider.createStandardLabels( 25, 25 ) );
- mPrioritySlider.setPaintLabels( true );
- mPrioritySlider.addChangeListener( new ChangeListener()
- {
- @Override
- public void stateChanged( ChangeEvent e )
- {
- int priority = mPrioritySlider.getValue();
+ add(mRecordCheckBox);
+ add(mRecordActionCombo);
- if( priority > Priority.MAX_PRIORITY )
- {
- mPrioritySliderLabel.setText( "Priority: Do Not Monitor" );
- }
- else
- {
- mPrioritySliderLabel.setText( "Priority: " + mPrioritySlider.getValue() );
- }
- }
- } );
- mPrioritySlider.setToolTipText( HELP_TEXT );
-
- add( mPrioritySlider );
-
- JButton saveButton = new JButton( "Save" );
- saveButton.addActionListener( new ActionListener()
- {
- @Override
- public void actionPerformed( ActionEvent e )
- {
- save();
- }
- } );
- add( saveButton );
+ //Placeholder
+ add(new JLabel(" "));
+ mPrioritySliderLabel = new JLabel("Audio Priority: " + Priority.MIN_PRIORITY);
+ add(mPrioritySliderLabel);
- JButton resetButton = new JButton( "Reset" );
- resetButton.addActionListener( new ActionListener()
- {
- @Override
- public void actionPerformed( ActionEvent e )
- {
- reset();
- }
- } );
- add( resetButton );
+ add(mPriorityCheckBox);
- setModified( false );
- }
-
- @Override
- public void setItem( List item )
- {
- super.setItem( item );
+ mPrioritySlider = new JSlider(JSlider.HORIZONTAL,
+ Priority.MIN_PRIORITY,
+ Priority.MAX_PRIORITY + 1,
+ Priority.MIN_PRIORITY);
- StringBuilder sb = new StringBuilder();
- sb.append( "Multiple Selected [ " );
-
- if( hasItem() )
- {
- sb.append( String.valueOf( getAliases().size() ) );
- }
- else
- {
- sb.append( "0" );
- }
+ mPrioritySlider.setMajorTickSpacing(20);
+ mPrioritySlider.setMinorTickSpacing(5);
+ mPrioritySlider.setPaintTicks(true);
+ mPrioritySlider.setLabelTable(mPrioritySlider.createStandardLabels(25, 25));
+ mPrioritySlider.setPaintLabels(true);
+ mPrioritySlider.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ int priority = mPrioritySlider.getValue();
- sb.append( " ]" );
-
- mAliasCount.setText( sb.toString() );
+ if (priority > Priority.MAX_PRIORITY)
+ {
+ mPrioritySliderLabel.setText("Audio Priority: Do Not Monitor");
+ }
+ else
+ {
+ mPrioritySliderLabel.setText("Audio Priority: " + mPrioritySlider.getValue());
+ }
+ }
+ });
+ mPrioritySlider.setToolTipText(HELP_TEXT);
- List listNames = mAliasModel.getListNames();
- listNames.add( 0, "" );
- mListCombo.setModel( new DefaultComboBoxModel(
- listNames.toArray( new String[ listNames.size() ] ) ) );
-
- List groupNames = mAliasModel.getGroupNames();
- groupNames.add( 0, "" );
- mGroupCombo.setModel( new DefaultComboBoxModel(
- groupNames.toArray( new String[ groupNames.size() ] ) ) );
+ add(mPrioritySlider);
- setCheckBoxesUnselected();
-
- setModified( false );
- }
-
- private void setCheckBoxesUnselected()
- {
- mListCheckBox.setSelected( false );
- mGroupCheckBox.setSelected( false );
- mColorCheckBox.setSelected( false );
- mIconCheckBox.setSelected( false );
- mRecordCheckBox.setSelected( false );
- mPriorityCheckBox.setSelected( false );
- }
+ add(mStreamCheckBox);
- @Override
- public void reset()
- {
- List listNames = mAliasModel.getListNames();
- listNames.add( 0, "" );
- mListCombo.setModel( new DefaultComboBoxModel(
- listNames.toArray( new String[ listNames.size() ] ) ) );
-
- List groupNames = mAliasModel.getGroupNames();
- groupNames.add( 0, "" );
- mGroupCombo.setModel( new DefaultComboBoxModel(
- groupNames.toArray( new String[ groupNames.size() ] ) ) );
-
- mButtonColor.setForeground( getForeground() );
- mButtonColor.setBackground( getBackground() );
-
- super.reset();
- }
-
- private boolean hasRequestedChanges()
- {
- return mListCheckBox.isSelected() ||
- mGroupCheckBox.isSelected() ||
- mColorCheckBox.isSelected() ||
- mIconCheckBox.isSelected() ||
- mRecordCheckBox.isSelected() ||
- mPriorityCheckBox.isSelected();
- }
+ mStreamCombo = new JComboBox();
+ mStreamCombo.setEditable(true);
+ add(mStreamCombo, "wrap");
- @Override
- public void save()
- {
- if( hasRequestedChanges() && hasItem() )
- {
- List aliases = getAliases();
-
- for( Alias alias: aliases )
- {
- if( mListCheckBox.isSelected() )
- {
- String list = null;
-
- if( mListCombo.getSelectedItem() != null )
- {
- list = (String)mListCombo.getSelectedItem();
- }
-
- alias.setList( list );
- }
-
- if( mGroupCheckBox.isSelected() )
- {
- String group = null;
-
- if( mGroupCombo.getSelectedItem() != null )
- {
- group = (String)mGroupCombo.getSelectedItem();
- }
-
- alias.setGroup( group );
- }
-
- if( mColorCheckBox.isSelected() )
- {
- alias.setColor( mButtonColor.getForeground().getRGB() );
- }
-
- if( mIconCheckBox.isSelected() )
- {
- if( mMapIconCombo.getSelectedItem() != null )
- {
- alias.setIconName( ((MapIcon)mMapIconCombo.getSelectedItem()).getName() );
- }
- else
- {
- alias.setIconName( null );
- }
- }
-
- if( mRecordCheckBox.isSelected() )
- {
- Record action =
- (Record)mRecordActionCombo.getSelectedItem();
+ JButton saveButton = new JButton("Save");
+ saveButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ save();
+ }
+ });
+ add(saveButton);
- alias.setRecordable( action == Record.RECORDABLE );
- }
-
- if( mPriorityCheckBox.isSelected() )
- {
- int priority = mPrioritySlider.getValue();
+ JButton resetButton = new JButton("Reset");
+ resetButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ reset();
+ }
+ });
+ add(resetButton);
- //This is a work-around -- we use max priority + 1 in the
- //gui to indicate do not monitor, and change the value here
- if( priority == Priority.MAX_PRIORITY + 1 )
- {
- priority = Priority.DO_NOT_MONITOR;
- }
-
- alias.setCallPriority( priority );
- }
- }
+ setModified(false);
+ }
- for( Alias alias: aliases )
- {
- //Broadcast an alias change event to save the updates
- mAliasModel.broadcast( new AliasEvent( alias, Event.CHANGE ) );
- }
- }
- }
+ @Override
+ public void setItem(List item)
+ {
+ super.setItem(item);
- @Override
- public void receive( AliasEvent event )
- {
- if( event.getEvent() == Event.DELETE &&
- hasItem() &&
- getAliases().contains( event.getAlias() ) )
- {
- getAliases().remove( event.getAlias() );
- setItem( getAliases() );
- }
- }
-
- public enum Record
- {
- NON_RECORDABLE( "Non-Recordable" ),
- RECORDABLE( "Recordable" );
-
- private String mLabel;
-
- private Record( String label )
- {
- mLabel = label;
- }
-
- public String toString()
- {
- return mLabel;
- }
- }
+ StringBuilder sb = new StringBuilder();
+ sb.append("Multiple Selected [ ");
+
+ if (hasItem())
+ {
+ sb.append(String.valueOf(getAliases().size()));
+ }
+ else
+ {
+ sb.append("0");
+ }
+
+ sb.append(" ]");
+
+ mAliasCount.setText(sb.toString());
+
+ List listNames = mAliasModel.getListNames();
+ listNames.add(0, "");
+ mListCombo.setModel(new DefaultComboBoxModel(
+ listNames.toArray(new String[listNames.size()])));
+
+ List groupNames = mAliasModel.getGroupNames();
+ groupNames.add(0, "");
+ mGroupCombo.setModel(new DefaultComboBoxModel(
+ groupNames.toArray(new String[groupNames.size()])));
+
+ List streamNames = mBroadcastModel.getBroadcastConfigurationNames();
+ mStreamCombo.setModel(new DefaultComboBoxModel(streamNames.toArray(new String[streamNames.size()])));
+
+ setCheckBoxesUnselected();
+
+ setModified(false);
+ }
+
+ private void setCheckBoxesUnselected()
+ {
+ mListCheckBox.setSelected(false);
+ mGroupCheckBox.setSelected(false);
+ mColorCheckBox.setSelected(false);
+ mIconCheckBox.setSelected(false);
+ mRecordCheckBox.setSelected(false);
+ mPriorityCheckBox.setSelected(false);
+ mStreamCheckBox.setSelected(false);
+ }
+
+ @Override
+ public void reset()
+ {
+ List listNames = mAliasModel.getListNames();
+ listNames.add(0, "");
+ mListCombo.setModel(new DefaultComboBoxModel(
+ listNames.toArray(new String[listNames.size()])));
+
+ List groupNames = mAliasModel.getGroupNames();
+ groupNames.add(0, "");
+ mGroupCombo.setModel(new DefaultComboBoxModel(
+ groupNames.toArray(new String[groupNames.size()])));
+
+ mButtonColor.setForeground(getForeground());
+ mButtonColor.setBackground(getBackground());
+
+ super.reset();
+ }
+
+ private boolean hasRequestedChanges()
+ {
+ return mListCheckBox.isSelected() ||
+ mGroupCheckBox.isSelected() ||
+ mColorCheckBox.isSelected() ||
+ mIconCheckBox.isSelected() ||
+ mRecordCheckBox.isSelected() ||
+ mPriorityCheckBox.isSelected() ||
+ mStreamCheckBox.isSelected();
+ }
+
+ @Override
+ public void save()
+ {
+ if (hasRequestedChanges() && hasItem())
+ {
+ List aliases = getAliases();
+
+ for (Alias alias : aliases)
+ {
+ if (mListCheckBox.isSelected())
+ {
+ String list = null;
+
+ if (mListCombo.getSelectedItem() != null)
+ {
+ list = (String) mListCombo.getSelectedItem();
+ }
+
+ alias.setList(list);
+ }
+
+ if (mGroupCheckBox.isSelected())
+ {
+ String group = null;
+
+ if (mGroupCombo.getSelectedItem() != null)
+ {
+ group = (String) mGroupCombo.getSelectedItem();
+ }
+
+ alias.setGroup(group);
+ }
+
+ if (mColorCheckBox.isSelected())
+ {
+ alias.setColor(mButtonColor.getForeground().getRGB());
+ }
+
+ if (mIconCheckBox.isSelected())
+ {
+ if (mMapIconCombo.getSelectedItem() != null)
+ {
+ alias.setIconName(((MapIcon) mMapIconCombo.getSelectedItem()).getName());
+ }
+ else
+ {
+ alias.setIconName(null);
+ }
+ }
+
+ if (mRecordCheckBox.isSelected())
+ {
+ Record action =
+ (Record) mRecordActionCombo.getSelectedItem();
+
+ alias.setRecordable(action == Record.RECORDABLE);
+ }
+
+ if (mPriorityCheckBox.isSelected())
+ {
+ int priority = mPrioritySlider.getValue();
+
+ //This is a work-around -- we use max priority + 1 in the
+ //gui to indicate do not monitor, and change the value here
+ if (priority == Priority.MAX_PRIORITY + 1)
+ {
+ priority = Priority.DO_NOT_MONITOR;
+ }
+
+ alias.setCallPriority(priority);
+ }
+
+ if(mStreamCheckBox.isSelected())
+ {
+ if(mStreamCombo.getSelectedItem() != null)
+ {
+ String streamName = (String)mStreamCombo.getSelectedItem();
+
+ boolean hasChannel = false;
+
+ //Ensure the alias doesn't already have the channel
+ for(BroadcastChannel channel: alias.getBroadcastChannels())
+ {
+ if(channel.getChannelName().equals(streamName))
+ {
+ hasChannel = true;
+ continue;
+ }
+ }
+
+ if(!hasChannel)
+ {
+ BroadcastChannel channel = (BroadcastChannel)AliasFactory
+ .getAliasID(AliasIDType.BROADCAST_CHANNEL);
+
+ channel.setChannelName(streamName);
+ alias.addAliasID(channel);
+ }
+ }
+ }
+ }
+
+ for (Alias alias : aliases)
+ {
+ //Broadcast an alias change event to save the updates
+ mAliasModel.broadcast(new AliasEvent(alias, Event.CHANGE));
+ }
+ }
+ }
+
+ @Override
+ public void receive(AliasEvent event)
+ {
+ if (event.getEvent() == Event.DELETE &&
+ hasItem() &&
+ getAliases().contains(event.getAlias()))
+ {
+ getAliases().remove(event.getAlias());
+ setItem(getAliases());
+ }
+ }
+
+ public enum Record
+ {
+ NON_RECORDABLE("Non-Recordable"),
+ RECORDABLE("Recordable");
+
+ private String mLabel;
+
+ private Record(String label)
+ {
+ mLabel = label;
+ }
+
+ public String toString()
+ {
+ return mLabel;
+ }
+ }
}
diff --git a/src/alias/id/AliasID.java b/src/alias/id/AliasID.java
index b310330c..25eff7d5 100644
--- a/src/alias/id/AliasID.java
+++ b/src/alias/id/AliasID.java
@@ -21,6 +21,7 @@ import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
+import alias.id.broadcast.BroadcastChannel;
import playlist.version1.Group;
import alias.id.esn.Esn;
import alias.id.fleetsync.FleetsyncID;
@@ -36,7 +37,7 @@ import alias.id.talkgroup.TalkgroupID;
import alias.id.uniqueID.UniqueID;
@SuppressWarnings( "deprecation" )
-@XmlSeeAlso( { Esn.class, FleetsyncID.class, Group.class, LoJackFunctionAndID.class,
+@XmlSeeAlso( {BroadcastChannel.class, Esn.class, FleetsyncID.class, Group.class, LoJackFunctionAndID.class,
MDC1200ID.class, Min.class, MPT1327ID.class, NonRecordable.class,
Priority.class, SiteID.class, StatusID.class, TalkgroupID.class,
UniqueID.class } )
diff --git a/src/alias/id/AliasIDType.java b/src/alias/id/AliasIDType.java
index 7417b66a..6a71ff4f 100644
--- a/src/alias/id/AliasIDType.java
+++ b/src/alias/id/AliasIDType.java
@@ -20,20 +20,20 @@ package alias.id;
public enum AliasIDType
{
ESN( "ESN" ),
- Fleetsync( "Fleetsync" ),
- LoJack( "LoJack" ),
- LTRNetUID( "LTR-Net UID" ),
+ FLEETSYNC( "Fleetsync" ),
+ LOJACK( "LoJack" ),
+ LTR_NET_UID( "LTR-Net UID" ),
MDC1200( "MDC-1200" ),
MIN( "Passport MIN" ),
MPT1327( "MPT-1327" ),
- Site( "Site" ),
- Status( "Status" ),
- Talkgroup( "Talkgroup" ),
+ SITE( "Site" ),
+ STATUS( "Status" ),
+ TALKGROUP( "Talkgroup" ),
- Invert( "Audio Inversion" ),
- Priority( "Audio Priority" ),
- NonRecordable( "Audio Non-Recordable" ),
- Streaming( "Audio Streaming" );
+ INVERT( "Audio Inversion" ),
+ PRIORITY( "Audio Priority" ),
+ NON_RECORDABLE( "Audio Non-Recordable" ),
+ BROADCAST_CHANNEL( "Audio Broadcast Channel" );
private String mLabel;
diff --git a/src/alias/id/AliasIdentifierEditor.java b/src/alias/id/AliasIdentifierEditor.java
index 935d59be..0c20517d 100644
--- a/src/alias/id/AliasIdentifierEditor.java
+++ b/src/alias/id/AliasIdentifierEditor.java
@@ -17,6 +17,7 @@
******************************************************************************/
package alias.id;
+import audio.broadcast.BroadcastModel;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
@@ -61,10 +62,13 @@ public class AliasIdentifierEditor extends Editor
private EditorContainer mEditorContainer = new EditorContainer();
private AliasModel mAliasModel;
+ private BroadcastModel mBroadcastModel;
- public AliasIdentifierEditor( AliasModel model )
+ public AliasIdentifierEditor( AliasModel aliasModel, BroadcastModel broadcastModel )
{
- mAliasModel = model;
+ mAliasModel = aliasModel;
+ mBroadcastModel = broadcastModel;
+
init();
}
@@ -120,45 +124,35 @@ public class AliasIdentifierEditor extends Editor
JPopupMenu menu = new JPopupMenu();
menu.add( new AddAliasIdentifierItem( AliasIDType.ESN ) );
- menu.add( new AddAliasIdentifierItem( AliasIDType.Fleetsync ) );
- menu.add( new AddAliasIdentifierItem( AliasIDType.LoJack ) );
- menu.add( new AddAliasIdentifierItem( AliasIDType.LTRNetUID ) );
+ menu.add( new AddAliasIdentifierItem( AliasIDType.FLEETSYNC) );
+ menu.add( new AddAliasIdentifierItem( AliasIDType.LOJACK) );
+ menu.add( new AddAliasIdentifierItem( AliasIDType.LTR_NET_UID) );
menu.add( new AddAliasIdentifierItem( AliasIDType.MDC1200 ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.MPT1327 ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.MIN ) );
- menu.add( new AddAliasIdentifierItem( AliasIDType.Site ) );
- menu.add( new AddAliasIdentifierItem( AliasIDType.Status ) );
- menu.add( new AddAliasIdentifierItem( AliasIDType.Talkgroup ) );
+ menu.add( new AddAliasIdentifierItem( AliasIDType.SITE) );
+ menu.add( new AddAliasIdentifierItem( AliasIDType.STATUS) );
+ menu.add( new AddAliasIdentifierItem( AliasIDType.TALKGROUP) );
+
+ menu.addSeparator();
+
+ menu.add( new AddAliasIdentifierItem( AliasIDType.BROADCAST_CHANNEL) );
- boolean separatorAdded = false;
-
if( hasItem() )
{
Alias alias = getItem();
-
- if( !alias.hasCallPriority() )
- {
- if( !separatorAdded )
- {
- menu.addSeparator();
- separatorAdded = true;
- }
-
- menu.add( new AddAliasIdentifierItem( AliasIDType.Priority ) );
- }
-
+
if( alias.isRecordable() )
{
- if( !separatorAdded )
- {
- menu.addSeparator();
- separatorAdded = true;
- }
-
- menu.add( new AddAliasIdentifierItem( AliasIDType.NonRecordable ) );
+ menu.add( new AddAliasIdentifierItem( AliasIDType.NON_RECORDABLE) );
+ }
+
+ if( !alias.hasCallPriority() )
+ {
+ menu.add( new AddAliasIdentifierItem( AliasIDType.PRIORITY) );
}
}
-
+
menu.show( e.getComponent(), e.getX(), e.getY() );
}
} );
@@ -327,7 +321,7 @@ public class AliasIdentifierEditor extends Editor
removeAll();
//This will always give us an editor
- mEditor = AliasFactory.getEditor( aliasID );
+ mEditor = AliasFactory.getEditor( aliasID, mBroadcastModel );
add( mEditor );
diff --git a/src/alias/id/broadcast/BroadcastChannel.java b/src/alias/id/broadcast/BroadcastChannel.java
new file mode 100644
index 00000000..f0baf630
--- /dev/null
+++ b/src/alias/id/broadcast/BroadcastChannel.java
@@ -0,0 +1,107 @@
+/*******************************************************************************
+ * 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 alias.id.broadcast;
+
+import alias.id.AliasID;
+import alias.id.AliasIDType;
+
+import javax.xml.bind.annotation.XmlAttribute;
+
+public class BroadcastChannel extends AliasID implements Comparable
+{
+ private String mChannelName;
+
+ public BroadcastChannel()
+ {
+ //JAXB Constructor
+ }
+
+ @Override
+ public int compareTo(BroadcastChannel other)
+ {
+ if(mChannelName != null && other.getChannelName() != null)
+ {
+ return mChannelName.compareTo(other.getChannelName());
+ }
+ else if(mChannelName != null)
+ {
+ return -1;
+ }
+ else
+ {
+ return 1;
+ }
+ }
+
+ /**
+ * Creates a named broadcast channel
+ */
+ public BroadcastChannel(String channelName)
+ {
+ mChannelName = channelName;
+ }
+
+ /**
+ * Name of the broadcastAudio channel configuration
+ */
+ @XmlAttribute(name="channel")
+ public String getChannelName()
+ {
+ return mChannelName;
+ }
+
+ /**
+ * Sets the name of the broadcastAudio channel configuration
+ */
+ public void setChannelName(String channel)
+ {
+ mChannelName = channel;
+ }
+
+ @Override
+ public AliasIDType getType()
+ {
+ return AliasIDType.BROADCAST_CHANNEL;
+ }
+
+ @Override
+ public boolean isValid()
+ {
+ return mChannelName != null;
+ }
+
+ @Override
+ public boolean matches( AliasID id )
+ {
+ return false;
+ }
+
+ @Override
+ public String toString()
+ {
+ if(isValid())
+ {
+ return "Broadcast Channel: " + mChannelName;
+ }
+ else
+ {
+ return "Broadcast Channel: None Selected";
+ }
+ }
+}
diff --git a/src/alias/id/broadcast/BroadcastChannelEditor.java b/src/alias/id/broadcast/BroadcastChannelEditor.java
new file mode 100644
index 00000000..f8d07810
--- /dev/null
+++ b/src/alias/id/broadcast/BroadcastChannelEditor.java
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * 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 alias.id.broadcast;
+
+import alias.id.AliasID;
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastModel;
+import gui.editor.DocumentListenerEditor;
+import net.miginfocom.swing.MigLayout;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.List;
+
+public class BroadcastChannelEditor extends DocumentListenerEditor
+{
+ private static final long serialVersionUID = 1L;
+
+ private static final String HELP_TEXT = ""
+ + "Audio Broadcast Channel
"
+ + "Name of a audio streaming broadcast configuration
"
+ + "to be used for streaming audio to an external
"
+ + "streaming server";
+
+ private JComboBox mBroadcastConfigurations;
+ private BroadcastModel mBroadcastModel;
+
+ public BroadcastChannelEditor(AliasID aliasID, BroadcastModel broadcastModel)
+ {
+ mBroadcastModel = broadcastModel;
+
+ initGUI();
+ setItem( aliasID );
+ }
+
+ private void initGUI()
+ {
+ setLayout( new MigLayout( "fill,wrap 1", "[]", "[][]" ) );
+
+ add( new JLabel( "Broadcast Channel" ), "grow" );
+
+ List channelNames = mBroadcastModel.getBroadcastConfigurationNames();
+
+ mBroadcastConfigurations = new JComboBox(channelNames.toArray(new String[channelNames.size()]));
+ mBroadcastConfigurations.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+
+ add(mBroadcastConfigurations, "grow");
+
+ JLabel help = new JLabel( "Help ..." );
+ help.setForeground( Color.BLUE.brighter() );
+ help.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
+ help.addMouseListener( new MouseAdapter()
+ {
+ @Override
+ public void mouseClicked( MouseEvent e )
+ {
+ JOptionPane.showMessageDialog( BroadcastChannelEditor.this,
+ HELP_TEXT, "Help", JOptionPane.INFORMATION_MESSAGE );
+ }
+ } );
+
+ add( help, "align left" );
+ }
+
+ @Override
+ public void setItem(AliasID item)
+ {
+ super.setItem(item);
+
+ if(item instanceof BroadcastChannel)
+ {
+ BroadcastChannel channel = (BroadcastChannel)item;
+
+ if(channel.isValid())
+ {
+ mBroadcastConfigurations.setSelectedItem(channel.getChannelName());
+ }
+ }
+
+ setModified(false);
+ }
+
+ @Override
+ public void save()
+ {
+ String channel = (String)mBroadcastConfigurations.getSelectedItem();
+
+ BroadcastConfiguration selectedConfiguration = mBroadcastModel.getBroadcastConfiguration(channel);
+
+
+ if(selectedConfiguration != null)
+ {
+ ((BroadcastChannel)getItem()).setChannelName(selectedConfiguration.getName());
+ }
+ else
+ {
+ ((BroadcastChannel)getItem()).setChannelName(null);
+ }
+ }
+
+
+}
diff --git a/src/alias/id/fleetsync/FleetsyncID.java b/src/alias/id/fleetsync/FleetsyncID.java
index bf402f1b..22b00872 100644
--- a/src/alias/id/fleetsync/FleetsyncID.java
+++ b/src/alias/id/fleetsync/FleetsyncID.java
@@ -74,6 +74,6 @@ public class FleetsyncID extends AliasID
@Override
public AliasIDType getType()
{
- return AliasIDType.Fleetsync;
+ return AliasIDType.FLEETSYNC;
}
}
diff --git a/src/alias/id/lojack/LoJackFunctionAndID.java b/src/alias/id/lojack/LoJackFunctionAndID.java
index 038c1444..f2cc8c80 100644
--- a/src/alias/id/lojack/LoJackFunctionAndID.java
+++ b/src/alias/id/lojack/LoJackFunctionAndID.java
@@ -101,7 +101,7 @@ public class LoJackFunctionAndID extends AliasID
@Override
public AliasIDType getType()
{
- return AliasIDType.LoJack;
+ return AliasIDType.LOJACK;
}
@Override
diff --git a/src/alias/id/nonrecordable/NonRecordable.java b/src/alias/id/nonrecordable/NonRecordable.java
index a72d8e12..c881c360 100644
--- a/src/alias/id/nonrecordable/NonRecordable.java
+++ b/src/alias/id/nonrecordable/NonRecordable.java
@@ -8,7 +8,7 @@ public class NonRecordable extends AliasID
@Override
public AliasIDType getType()
{
- return AliasIDType.NonRecordable;
+ return AliasIDType.NON_RECORDABLE;
}
@Override
diff --git a/src/alias/id/priority/Priority.java b/src/alias/id/priority/Priority.java
index caef53b5..9243db45 100644
--- a/src/alias/id/priority/Priority.java
+++ b/src/alias/id/priority/Priority.java
@@ -78,6 +78,6 @@ public class Priority extends AliasID
@Override
public AliasIDType getType()
{
- return AliasIDType.Priority;
+ return AliasIDType.PRIORITY;
}
}
diff --git a/src/alias/id/siteID/SiteID.java b/src/alias/id/siteID/SiteID.java
index 04d6d20c..a16008e1 100644
--- a/src/alias/id/siteID/SiteID.java
+++ b/src/alias/id/siteID/SiteID.java
@@ -63,6 +63,6 @@ public class SiteID extends AliasID
@Override
public AliasIDType getType()
{
- return AliasIDType.Site;
+ return AliasIDType.SITE;
}
}
diff --git a/src/alias/id/status/StatusID.java b/src/alias/id/status/StatusID.java
index ebc8f133..71ccb5d8 100644
--- a/src/alias/id/status/StatusID.java
+++ b/src/alias/id/status/StatusID.java
@@ -70,6 +70,6 @@ public class StatusID extends AliasID
@Override
public AliasIDType getType()
{
- return AliasIDType.Status;
+ return AliasIDType.STATUS;
}
}
diff --git a/src/alias/id/talkgroup/TalkgroupID.java b/src/alias/id/talkgroup/TalkgroupID.java
index 78cae9fd..fe7b3ec3 100644
--- a/src/alias/id/talkgroup/TalkgroupID.java
+++ b/src/alias/id/talkgroup/TalkgroupID.java
@@ -78,6 +78,6 @@ public class TalkgroupID extends AliasID
@Override
public AliasIDType getType()
{
- return AliasIDType.Talkgroup;
+ return AliasIDType.TALKGROUP;
}
}
diff --git a/src/alias/id/uniqueID/UniqueID.java b/src/alias/id/uniqueID/UniqueID.java
index 29e074c6..69302b38 100644
--- a/src/alias/id/uniqueID/UniqueID.java
+++ b/src/alias/id/uniqueID/UniqueID.java
@@ -70,6 +70,6 @@ public class UniqueID extends AliasID
@Override
public AliasIDType getType()
{
- return AliasIDType.LTRNetUID;
+ return AliasIDType.LTR_NET_UID;
}
}
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/broadcast/AudioBroadcaster.java b/src/audio/broadcast/AudioBroadcaster.java
new file mode 100644
index 00000000..7467a2b6
--- /dev/null
+++ b/src/audio/broadcast/AudioBroadcaster.java
@@ -0,0 +1,462 @@
+/*******************************************************************************
+ * 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.convert.ISilenceGenerator;
+import audio.metadata.AudioMetadata;
+import controller.ThreadPoolManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import properties.SystemProperties;
+import sample.Listener;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public abstract class AudioBroadcaster implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger( AudioBroadcaster.class );
+
+ public static final int PROCESSOR_RUN_INTERVAL_MS = 1000;
+
+ private ThreadPoolManager mThreadPoolManager;
+ private ScheduledFuture mScheduledTask;
+
+ private RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor();
+ private List mAudioRecordingQueue = new CopyOnWriteArrayList<>();
+
+ private ISilenceGenerator mSilenceGenerator;
+
+ private Listener mBroadcastEventListener;
+ private BroadcastState mBroadcastState = BroadcastState.READY;
+
+ private int mStreamedAudioCount = 0;
+ private BroadcastConfiguration mBroadcastConfiguration;
+ private long mDelay;
+ private AtomicBoolean mStreaming = new AtomicBoolean();
+
+ /**
+ * AudioBroadcaster for streaming audio recordings to a remote streaming audio server. Audio recordings are
+ * generated by an internal StreamManager that converts an inbound stream of AudioPackets into a recording of the
+ * desired audio format (e.g. MP3) and nominates the recording to an internal recording queue for streaming. The
+ * broadcaster supports receiving audio packets from multiple audio sources. Each audio packet's internal audio
+ * metadata source string is used to reassemble each packet stream. Recordings are capped at 30 seconds length.
+ * If a source audio packet stream exceeds 30 seconds in length, it will be chunked into 30 second recordings.
+ *
+ * This broadcaster supports a time delay setting for delaying broadcast of audio recordings. The delay setting is
+ * defined in the broadcast configuration. When this delay is greater than zero, the recording will remain in the
+ * audio broadcaster queue until the recording start time + delay elapses. Audio recordings are processed in a FIFO
+ * manner.
+ *
+ * Use the start() and stop() methods to connect to/disconnect from the remote server. Audio recordings will be
+ * streamed to the remote server when available. One second silence frames will be broadcast to the server when
+ * there are no recordings available, in order to maintain a connection with the remote server. Any audio packet
+ * streams received while the broadcaster is stopped will be ignored.
+ *
+ * The last audio packet's metadata is automatically attached to the closed audio recording when it is enqueued for
+ * broadcast. That metadata will be updated on the remote server once the audio recording is opened for streaming.
+ */
+ public AudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration broadcastConfiguration)
+ {
+ mThreadPoolManager = threadPoolManager;
+ mBroadcastConfiguration = broadcastConfiguration;
+ mDelay = mBroadcastConfiguration.getDelay();
+ mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat());
+ }
+
+ protected ThreadPoolManager getThreadPoolManager()
+ {
+ return mThreadPoolManager;
+ }
+
+ /**
+ * Broadcast binary audio data frames or sequences.
+ */
+ protected abstract void broadcastAudio(byte[] audio);
+
+ /**
+ * Protocol-specific metadata updater
+ */
+ 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.
+ */
+ protected abstract void disconnect();
+
+ /**
+ * Connects to the remote server specified by the broadcast configuration and starts audio streaming.
+ */
+ public void start()
+ {
+ if(mStreaming.compareAndSet(false, true))
+ {
+ if(mScheduledTask == null)
+ {
+ if(mThreadPoolManager != null)
+ {
+ mScheduledTask = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
+ mRecordingQueueProcessor, PROCESSOR_RUN_INTERVAL_MS, TimeUnit.MILLISECONDS );
+ }
+ }
+ }
+ }
+
+ /**
+ * Disconnects from the remote server.
+ */
+ public void stop()
+ {
+ if(mStreaming.compareAndSet(true, false))
+ {
+ if(mThreadPoolManager != null && mScheduledTask != null)
+ {
+ mThreadPoolManager.cancel(mScheduledTask);
+ }
+
+ disconnect();
+ }
+ }
+
+ /**
+ * Stream name for the broadcast configuration for this broadcaster
+ * @return stream name or null
+ */
+ public String getStreamName()
+ {
+ BroadcastConfiguration config = getBroadcastConfiguration();
+
+ if(config != null)
+ {
+ return config.getName();
+ }
+
+ return null;
+ }
+
+ /**
+ * Size of recording queue for recordings awaiting streaming
+ */
+ public int getQueueSize()
+ {
+ return mAudioRecordingQueue.size();
+ }
+
+ /**
+ * Number of audio recordings streamed to remote server
+ */
+ public int getStreamedAudioCount()
+ {
+ return mStreamedAudioCount;
+ }
+
+ /**
+ * Primary insert method for the stream manager to nominate completed audio recordings for broadcast.
+ *
+ * @param recording to queue for broadcasting
+ */
+ public void receive(AudioRecording recording)
+ {
+ if(connected())
+ {
+ mAudioRecordingQueue.add(recording);
+ broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
+ }
+ }
+
+ /**
+ * Broadcast configuration used by this broadcaster
+ */
+ public BroadcastConfiguration getBroadcastConfiguration()
+ {
+ return mBroadcastConfiguration;
+ }
+
+ /**
+ * Registers the listener to receive broadcast events/state changes
+ */
+ public void setListener(Listener listener)
+ {
+ mBroadcastEventListener = listener;
+ }
+
+ /**
+ * Removes the listener from receiving broadcast events/state changes
+ */
+ public void removeListener()
+ {
+ mBroadcastEventListener = null;
+ }
+
+ /**
+ * Broadcasts the event to any registered listener
+ */
+ public void broadcast(BroadcastEvent event)
+ {
+ if(mBroadcastEventListener != null)
+ {
+ mBroadcastEventListener.receive(event);
+ }
+ }
+
+ /**
+ * Sets the state of the broadcastAudio connection
+ */
+ protected void setBroadcastState(BroadcastState state)
+ {
+ if(mBroadcastState != state)
+ {
+ mLog.info("[" + getStreamName() + "] status: " + state);
+ mBroadcastState = state;
+
+ broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
+
+ if(mBroadcastState.isErrorState())
+ {
+ stop();
+ }
+
+ if(!connected())
+ {
+ //Remove all pending audio recordings
+ while(!mAudioRecordingQueue.isEmpty())
+ {
+ AudioRecording recording = mAudioRecordingQueue.get(0);
+ recording.removePendingReplay();
+ }
+ }
+ }
+ }
+
+ /**
+ * Current state of the broadcastAudio connection
+ */
+ public BroadcastState getBroadcastState()
+ {
+ return mBroadcastState;
+ }
+
+ /**
+ * Indicates if the broadcaster is currently connected to the remote server
+ */
+ protected boolean connected()
+ {
+ return getBroadcastState() == BroadcastState.CONNECTED;
+ }
+
+ /**
+ * Indicates if this broadcaster can connect and is not currently in an error state or a connected state.
+ */
+ public boolean canConnect()
+ {
+ BroadcastState state = getBroadcastState();
+
+ return state != BroadcastState.CONNECTED && !state.isErrorState();
+ }
+
+ /**
+ * Indicates if the current broadcast state is an error state, meaning that it cannot recover or connect using the
+ * current configuration.
+ */
+ protected boolean isErrorState()
+ {
+ return getBroadcastState().isErrorState();
+ }
+
+
+ /**
+ * Audio recording queue processor. Fetches recordings from the queue and chunks the recording byte content
+ * to subclass implementations for broadcast in the appropriate manner.
+ */
+ public class RecordingQueueProcessor implements Runnable
+ {
+ private AtomicBoolean mProcessing = new AtomicBoolean();
+ private ByteArrayInputStream mInputStream;
+ private long mFinalSilencePadding = 0;
+ private int mBytesStreamedActual = 0;
+ private int mBytesStreamedRequired = 0;
+
+ @Override
+ public void run()
+ {
+ if(mProcessing.compareAndSet(false, true))
+ {
+ try
+ {
+ if(mInputStream == null || mInputStream.available() <= 0)
+ {
+ if(mFinalSilencePadding > 0)
+ {
+ broadcastAudio(mSilenceGenerator.generate(mFinalSilencePadding));
+ mFinalSilencePadding = 0;
+ }
+
+ nextRecording();
+ }
+
+ if(mInputStream != null)
+ {
+ //We need to stream at 13.888 fps (144 byte frame) to achieve 2000 Bps or 16 kbps
+ mBytesStreamedRequired += 2000; //2000 bytes per second for 16 kbps data rate
+ int bytesToStream = mBytesStreamedRequired - mBytesStreamedActual;
+
+ //Trim length to whole-frame intervals (144 byte frame)
+ bytesToStream -= (bytesToStream % 144);
+
+ int length = Math.min(bytesToStream, mInputStream.available());
+
+ byte[] audio = new byte[length];
+
+ try
+ {
+ mBytesStreamedActual += mInputStream.read(audio);
+
+ broadcastAudio(audio);
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error reading from in-memory audio recording input stream", ioe);
+ }
+ }
+ else
+ {
+ broadcastAudio(mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS));
+ }
+ }
+ catch(Exception e)
+ {
+ mLog.error("Error while processing audio streaming queue", e);
+ }
+
+ mProcessing.set(false);
+ }
+ }
+
+ /**
+ * Loads the next recording for broadcast
+ */
+ private void nextRecording()
+ {
+ mBytesStreamedActual = 0;
+ mBytesStreamedRequired = 0;
+
+ boolean metadataUpdateRequired = false;
+
+ if(mInputStream != null)
+ {
+ mStreamedAudioCount++;
+ broadcast(new BroadcastEvent(AudioBroadcaster.this,
+ BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
+ metadataUpdateRequired = true;
+ }
+
+ mInputStream = null;
+
+ AudioRecording nextRecording = null;
+
+ //Find the recording with the earliest start time
+ Iterator it = mAudioRecordingQueue.iterator();
+
+ while(it.hasNext())
+ {
+ AudioRecording recording = it.next();
+
+ if(nextRecording == null || (recording.getStartTime() < nextRecording.getStartTime()))
+ {
+ nextRecording = recording;
+ }
+ }
+
+ if(nextRecording != null && nextRecording.getStartTime() + mDelay <= System.currentTimeMillis())
+ {
+ mAudioRecordingQueue.remove(nextRecording);
+
+ try
+ {
+ if(Files.exists(nextRecording.getPath()))
+ {
+ byte[] audio = Files.readAllBytes(nextRecording.getPath());
+
+ if(audio != null && audio.length > 0)
+ {
+ mInputStream = new ByteArrayInputStream(audio);
+
+ mFinalSilencePadding = PROCESSOR_RUN_INTERVAL_MS -
+ (nextRecording.getRecordingLength() % PROCESSOR_RUN_INTERVAL_MS);
+
+ while(mFinalSilencePadding >= PROCESSOR_RUN_INTERVAL_MS)
+ {
+ mFinalSilencePadding -= PROCESSOR_RUN_INTERVAL_MS;
+ }
+
+ if(connected())
+ {
+ broadcastMetadata(nextRecording.getAudioMetadata());
+ }
+
+ metadataUpdateRequired = false;
+ }
+ }
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Stream [" + getBroadcastConfiguration().getName() + "] error reading temporary audio " +
+ "stream recording [" + nextRecording.getPath().toString() + "] - skipping recording - ", ioe);
+
+ mInputStream = null;
+ metadataUpdateRequired = false;
+ }
+
+ nextRecording.removePendingReplay();
+
+ broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
+ }
+
+ //If we closed out a recording and don't have a new/next recording, send an empty metadata update
+ if(metadataUpdateRequired && connected())
+ {
+ broadcastMetadata(null);
+ }
+ }
+ }
+}
diff --git a/src/audio/broadcast/AudioRecording.java b/src/audio/broadcast/AudioRecording.java
new file mode 100644
index 00000000..1d4c3f16
--- /dev/null
+++ b/src/audio/broadcast/AudioRecording.java
@@ -0,0 +1,119 @@
+/*******************************************************************************
+ * 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.metadata.AudioMetadata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class AudioRecording implements Comparable
+{
+ private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
+
+ private Path mPath;
+ private AudioMetadata mAudioMetadata;
+ private long mStartTime;
+ private long mRecordingLength;
+ private AtomicInteger mPendingReplayCount = new AtomicInteger();
+
+ /**
+ * Audio recording that is ready to be streamed
+ * @param path to the audio recording file
+ * @param audioMetadata associated with the recording
+ * @param start time of recording in milliseconds since epoch
+ * @param recordingLength in milliseconds
+ */
+ public AudioRecording(Path path, AudioMetadata audioMetadata, long start, long recordingLength)
+ {
+ mPath = path;
+ mAudioMetadata = audioMetadata;
+ mStartTime = start;
+ mRecordingLength = recordingLength;
+ }
+
+ /**
+ * Path to the completed audio recording
+ */
+ public Path getPath()
+ {
+ return mPath;
+ }
+
+ /**
+ * Optional audio metadata for the recording.
+ */
+ public AudioMetadata getAudioMetadata()
+ {
+ return mAudioMetadata;
+ }
+
+ /**
+ * Recording start time in milliseconds since epoch
+ */
+ public long getStartTime()
+ {
+ return mStartTime;
+ }
+
+ /**
+ * Recording length in milliseconds
+ */
+ public long getRecordingLength()
+ {
+ return mRecordingLength;
+ }
+
+
+ /**
+ * Implements comparable for sorting recordings based on start time in ascending order
+ */
+ @Override
+ public int compareTo(AudioRecording otherRecording)
+ {
+ return Long.compare(getStartTime(), otherRecording.getStartTime());
+ }
+
+ /**
+ * Increments the count of pending replays.
+ */
+ public void addPendingReplay()
+ {
+ mPendingReplayCount.incrementAndGet();
+ }
+
+ /**
+ * Decrements the count of pending replays.
+ */
+ public void removePendingReplay()
+ {
+ mPendingReplayCount.decrementAndGet();
+ }
+
+ /**
+ * Indicates if there are any remaining pending replays. Once the pending replay count is less than or equal to
+ * zero, the recording can be deleted.
+ */
+ public boolean hasPendingReplays()
+ {
+ return mPendingReplayCount.get() > 0;
+ }
+}
diff --git a/src/audio/broadcast/BroadcastConfiguration.java b/src/audio/broadcast/BroadcastConfiguration.java
new file mode 100644
index 00000000..312dca96
--- /dev/null
+++ b/src/audio/broadcast/BroadcastConfiguration.java
@@ -0,0 +1,279 @@
+/*******************************************************************************
+ * 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.broadcastify.BroadcastifyConfiguration;
+import audio.broadcast.icecast.IcecastHTTPConfiguration;
+import audio.broadcast.icecast.IcecastTCPConfiguration;
+import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
+import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlTransient;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+@XmlSeeAlso({BroadcastifyConfiguration.class,
+ IcecastTCPConfiguration.class,
+ IcecastHTTPConfiguration.class,
+ ShoutcastV1Configuration.class,
+ ShoutcastV2Configuration.class})
+@XmlRootElement(name = "stream")
+public abstract class BroadcastConfiguration
+{
+ private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
+ private String mName;
+ private String mHost;
+ private int mPort;
+ private String mPassword;
+ private long mDelay;
+ private boolean mEnabled = true;
+
+ public BroadcastConfiguration()
+ {
+ //No-arg constructor required for JAXB
+ }
+
+ /**
+ * Creates a copy/clone of the configuration
+ */
+ public abstract BroadcastConfiguration copyOf();
+
+ /**
+ * Broadcast audio streaming configuration. Describes the streaming server and configuration details.
+ */
+ public BroadcastConfiguration(BroadcastFormat format)
+ {
+ mBroadcastFormat = format;
+ }
+
+ /**
+ * Broadcast server type
+ */
+ @XmlAttribute(name = "type")
+ public abstract BroadcastServerType getBroadcastServerType();
+
+ /**
+ * Name identifying this broadcastAudio configuration.
+ */
+ @XmlAttribute(name = "name")
+ public String getName()
+ {
+ return mName;
+ }
+
+ /**
+ * Sets the name name that identifies/describes this configuration
+ *
+ * @param name
+ */
+ public void setName(String name)
+ {
+ mName = name;
+ }
+
+ /**
+ * Indicates if this configuration has a name value
+ */
+ public boolean hasName()
+ {
+ return mName != null;
+ }
+
+ /**
+ * BROADCAST server host name.
+ */
+ @XmlAttribute(name = "host")
+ public String getHost()
+ {
+ return mHost;
+ }
+
+ /**
+ * Host name or IP address of the streaming server
+ *
+ * @param host
+ */
+ public void setHost(String host)
+ {
+ mHost = host;
+ }
+
+ /**
+ * Indicates if this configuration has a host value
+ */
+ public boolean hasHost()
+ {
+ return mHost != null;
+ }
+
+ /**
+ * BROADCAST server port number;
+ */
+ @XmlAttribute(name = "port")
+ public int getPort()
+ {
+ return mPort;
+ }
+
+ /**
+ * Port number of the streaming server to where the broadcastAudio will be directed.
+ *
+ * @param port
+ */
+ public void setPort(int port)
+ {
+ mPort = port;
+ }
+
+ /**
+ * Indicates if this configuration contains a port value
+ */
+ public boolean hasPort()
+ {
+ return mPort > 0;
+ }
+
+ public SocketAddress getAddress()
+ {
+ return new InetSocketAddress(getHost(), getPort());
+ }
+
+ /**
+ * Password to authenticate with the streaming server.
+ */
+ @XmlAttribute(name = "password")
+ public String getPassword()
+ {
+ return mPassword;
+ }
+
+ /**
+ * Sets the password used to authenticate the broadcastAudio broadcastAudio user to the streaming server
+ *
+ * @param password
+ */
+ public void setPassword(String password)
+ {
+ mPassword = password;
+ }
+
+ /**
+ * Indicates if this configuration has a password value
+ */
+ public boolean hasPassword()
+ {
+ return mPassword != null;
+ }
+
+ /**
+ * Audio broadcastAudio content type (e.g. mp3)
+ */
+ @XmlAttribute(name = "format")
+ public BroadcastFormat getBroadcastFormat()
+ {
+ return mBroadcastFormat;
+ }
+
+ public void setBroadcastFormat(BroadcastFormat format)
+ {
+ mBroadcastFormat = format;
+ }
+
+ /**
+ * Audio broadcastAudio delay from recording start until the audio file is broadcastAudio to the server.
+ */
+ @XmlAttribute(name = "delay")
+ public long getDelay()
+ {
+ return mDelay;
+ }
+
+ /**
+ * User specified delay in milli-seconds from audio call start until when the audio is broadcastAudio to the server.
+ * @param delay in milliseconds
+ */
+ public void setDelay(long delay)
+ {
+ mDelay = delay;
+ }
+
+ /**
+ * Indicates if this broadcaster is enable, meaning that it will automatically connect on startup.
+ */
+ @XmlAttribute(name = "enabled")
+ public boolean isEnabled()
+ {
+ return mEnabled;
+ }
+
+ public void setEnabled(boolean enabled)
+ {
+ mEnabled = enabled;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ {
+ return true;
+ }
+
+ if (!(o instanceof BroadcastConfiguration))
+ {
+ return false;
+ }
+
+ BroadcastConfiguration that = (BroadcastConfiguration) o;
+
+ if (getBroadcastFormat() != that.getBroadcastFormat())
+ {
+ return false;
+ }
+
+ if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null)
+ {
+ return false;
+ }
+
+ return getBroadcastServerType() == that.getBroadcastServerType();
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = getBroadcastFormat().hashCode();
+ result = 31 * result + (getName() != null ? getName().hashCode() : 0);
+ result = 31 * result + getBroadcastServerType().hashCode();
+ return result;
+ }
+
+ /**
+ * Indicates if this configuration is valid. A minimal check will ensure that it contains at least a hostname and
+ * port number.
+ */
+ @XmlTransient
+ public boolean isValid()
+ {
+ return mHost != null && mPort > 0;
+ }
+}
diff --git a/src/audio/broadcast/BroadcastConfigurationEditor.java b/src/audio/broadcast/BroadcastConfigurationEditor.java
new file mode 100644
index 00000000..678ba383
--- /dev/null
+++ b/src/audio/broadcast/BroadcastConfigurationEditor.java
@@ -0,0 +1,128 @@
+/*******************************************************************************
+ * 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 alias.AliasModel;
+import audio.broadcast.broadcastify.BroadcastifyConfiguration;
+import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
+import gui.editor.DocumentListenerEditor;
+import settings.SettingsManager;
+
+import javax.swing.*;
+
+public abstract class BroadcastConfigurationEditor extends DocumentListenerEditor
+{
+ protected SettingsManager mSettingsManager;
+ protected BroadcastModel mBroadcastModel;
+ protected AliasModel mAliasModel;
+
+ public BroadcastConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, SettingsManager settingsManager)
+ {
+ mBroadcastModel = broadcastModel;
+ mAliasModel = aliasModel;
+ mSettingsManager = settingsManager;
+ }
+
+ /**
+ * Updates the configuration with the new name and prompts the user to update any aliases that have a broadcast
+ * channel alias id with the old name to update to the new name.
+ *
+ * @param broadcastConfiguration to assign a new channel name
+ * @param newName to assign to the broadcast configuration. Note: assumes that validateConfiguration() has been
+ * invoked to verify that newName is non-null and non-empty
+ */
+ protected void updateConfigurationName(BroadcastConfiguration broadcastConfiguration, String newName)
+ {
+ String previousName = broadcastConfiguration.getName();
+
+ if(previousName == null || previousName.isEmpty())
+ {
+ broadcastConfiguration.setName(newName);
+ return;
+ }
+
+ if(!previousName.equals(newName))
+ {
+ mAliasModel.renameBroadcastChannel(previousName, newName);
+ broadcastConfiguration.setName(newName);
+ }
+ }
+
+ /**
+ * Validates a text field control for a non-null, non-empty value
+ * @param field to validate
+ * @param title to use for error dialog
+ * @param message to use for error dialog
+ * @return true if field contains a non-null, non-empty value
+ */
+ protected boolean validateTextField(JTextField field, String title, String message)
+ {
+ String text = field.getText();
+
+ if(text == null || text.isEmpty())
+ {
+ JOptionPane.showMessageDialog(BroadcastConfigurationEditor.this, message, title,
+ JOptionPane.ERROR_MESSAGE);
+
+ field.requestFocus();
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Validates the text field control that contains an integer value for non-null, non-empty and within the
+ * specified min/max valid range.
+ * @param field to validate
+ * @param title to use for error dialog
+ * @param message to use for error dialog
+ * @param minValid value
+ * @param maxValid value
+ * @return true if field contains a non-null, non-empty value within the valid min/max range
+ */
+ protected boolean validateIntegerTextField(JTextField field, String title, String message, int minValid, int maxValid)
+ {
+ if(validateTextField(field, title, message))
+ {
+ String text = field.getText();
+
+ try
+ {
+ int value = Integer.parseInt(text);
+
+ if(minValid <= value && value <= maxValid)
+ {
+ return true;
+ }
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the number value
+ }
+
+ JOptionPane.showMessageDialog(BroadcastConfigurationEditor.this, message, title,
+ JOptionPane.ERROR_MESSAGE);
+
+ field.requestFocus();
+ }
+
+ return false;
+ }
+}
diff --git a/src/audio/broadcast/BroadcastEvent.java b/src/audio/broadcast/BroadcastEvent.java
new file mode 100644
index 00000000..6a48bd5e
--- /dev/null
+++ b/src/audio/broadcast/BroadcastEvent.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast;
+
+public class BroadcastEvent
+{
+ private BroadcastConfiguration mBroadcastConfiguration;
+ private AudioBroadcaster mAudioBroadcaster;
+ private Event mEvent;
+
+ /**
+ * AliasEvent - event describing any changes to an alias
+ * @param configuration - alias that changed
+ * @param event - change event
+ */
+ public BroadcastEvent(BroadcastConfiguration configuration, Event event )
+ {
+ mBroadcastConfiguration = configuration;
+ mEvent = event;
+ }
+
+ public BroadcastEvent(AudioBroadcaster audioBroadcaster, Event event )
+ {
+ mAudioBroadcaster = audioBroadcaster;
+ mEvent = event;
+ }
+
+ public BroadcastConfiguration getBroadcastConfiguration()
+ {
+ return mBroadcastConfiguration;
+ }
+
+ public AudioBroadcaster getAudioBroadcaster()
+ {
+ return mAudioBroadcaster;
+ }
+
+ /**
+ * Indicates if this event pertains to an audio broadcaster
+ */
+ public boolean isAudioBroadcasterEvent()
+ {
+ return mAudioBroadcaster != null;
+ }
+
+ /**
+ * Indicates if this event pertains to a broadcast configuration
+ */
+ public boolean isBroadcastConfigurationEvent()
+ {
+ return mBroadcastConfiguration != null;
+ }
+
+ public Event getEvent()
+ {
+ return mEvent;
+ }
+
+ /**
+ * Channel events to describe the specific event
+ */
+ public enum Event
+ {
+ BROADCASTER_ADD,
+ BROADCASTER_QUEUE_CHANGE,
+ BROADCASTER_STATE_CHANGE,
+ BROADCASTER_STREAMED_COUNT_CHANGE,
+ BROADCASTER_DELETE,
+
+ CONFIGURATION_ADD,
+ CONFIGURATION_CHANGE,
+ CONFIGURATION_DELETE;
+ }
+}
diff --git a/src/audio/broadcast/BroadcastFactory.java b/src/audio/broadcast/BroadcastFactory.java
new file mode 100644
index 00000000..3fc95768
--- /dev/null
+++ b/src/audio/broadcast/BroadcastFactory.java
@@ -0,0 +1,215 @@
+/*******************************************************************************
+ * 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 alias.AliasModel;
+import audio.broadcast.broadcastify.BroadcastifyConfiguration;
+import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
+import audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
+import audio.broadcast.icecast.IcecastHTTPConfiguration;
+import audio.broadcast.icecast.IcecastHTTPConfigurationEditor;
+import audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
+import audio.broadcast.icecast.IcecastTCPConfiguration;
+import audio.broadcast.icecast.IcecastTCPConfigurationEditor;
+import audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
+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;
+import audio.convert.MP3SilenceGenerator;
+import controller.ThreadPoolManager;
+import gui.editor.Editor;
+import gui.editor.EmptyEditor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import record.AudioRecorder;
+import record.mp3.MP3Recorder;
+import settings.SettingsManager;
+
+import java.nio.file.Path;
+
+public class BroadcastFactory
+{
+ private final static Logger mLog = LoggerFactory.getLogger( BroadcastFactory.class );
+
+ public static final int MP3_MONO_16_KHZ_BITRATE = 16;
+ public static final boolean MP3_CONSTANT_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 AudioBroadcaster getBroadcaster(ThreadPoolManager threadPoolManager,
+ BroadcastConfiguration configuration)
+ {
+ if(configuration != null)
+ {
+ IAudioConverter converter = getAudioConverter(configuration);
+
+ if(converter != null)
+ {
+ switch(configuration.getBroadcastServerType())
+ {
+ case BROADCASTIFY:
+ return new IcecastTCPAudioBroadcaster(threadPoolManager,
+ (BroadcastifyConfiguration)configuration);
+ case ICECAST_TCP:
+ return new IcecastTCPAudioBroadcaster(threadPoolManager,
+ (IcecastTCPConfiguration)configuration);
+ case ICECAST_HTTP:
+ return new IcecastHTTPAudioBroadcaster(threadPoolManager,
+ (IcecastHTTPConfiguration)configuration);
+ case SHOUTCAST_V1:
+ return new ShoutcastV1AudioBroadcaster(threadPoolManager,
+ (ShoutcastV1Configuration)configuration);
+ case SHOUTCAST_V2:
+ return new ShoutcastV2AudioBroadcaster(threadPoolManager,
+ (ShoutcastV2Configuration)configuration);
+ case UNKNOWN:
+ default:
+ mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());
+ break;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an audio convert to convert from 8 kHz PCM audio to the specified format
+ * @param configuration containing the requested output audio format
+ * @return audio convert or null
+ */
+ public static IAudioConverter getAudioConverter(BroadcastConfiguration configuration)
+ {
+ switch(configuration.getBroadcastFormat())
+ {
+ case MP3:
+ return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE);
+ default:
+ mLog.info("Unrecognized broadcastAudio format: " + configuration.getBroadcastFormat().name());
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a broadcastAudio configuration for the specified server type and format
+ *
+ * @param serverType for the configuration
+ * @param format for the output audio format
+ * @return constructed (empty) configuration
+ */
+ public static BroadcastConfiguration getConfiguration(BroadcastServerType serverType, BroadcastFormat format)
+ {
+ switch(serverType)
+ {
+ case BROADCASTIFY:
+ return new BroadcastifyConfiguration(format);
+ case ICECAST_HTTP:
+ return new IcecastHTTPConfiguration(format);
+ case ICECAST_TCP:
+ return new IcecastTCPConfiguration(format);
+ case SHOUTCAST_V1:
+ return new ShoutcastV1Configuration(format);
+ case SHOUTCAST_V2:
+ return new ShoutcastV2Configuration(format);
+ case UNKNOWN:
+ default:
+ mLog.info("Unrecognized broadcastAudio server type: " + serverType.name());
+ break;
+ }
+
+ return null;
+ }
+
+ /**
+ * Constructs an editor for the specified broadcastAudio configuration
+ * @param configuration to modify or view
+ * @param broadcastModel model for broadcastAudio configurations
+ * @return an editor for the specified broadcastAudio configuration
+ */
+ public static Editor getEditor(BroadcastConfiguration configuration,
+ BroadcastModel broadcastModel,
+ AliasModel aliasModel,
+ SettingsManager settingsManager)
+ {
+ Editor editor;
+
+ switch (configuration.getBroadcastServerType())
+ {
+ case BROADCASTIFY:
+ editor = new BroadcastifyConfigurationEditor(broadcastModel, aliasModel, settingsManager);
+ break;
+ case ICECAST_TCP:
+ editor = new IcecastTCPConfigurationEditor(broadcastModel, aliasModel, settingsManager);
+ break;
+ case ICECAST_HTTP:
+ editor = new IcecastHTTPConfigurationEditor(broadcastModel, aliasModel, settingsManager);
+ break;
+ 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;
+ }
+
+ editor.setItem(configuration);
+
+ return editor;
+ }
+
+ /**
+ * Creates an audio recorder for the specified broadcastAudio format using the specified path output file name
+ */
+ public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat)
+ {
+ switch (broadcastFormat)
+ {
+ case MP3:
+ return new MP3Recorder(path);
+ default:
+ mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder");
+ return null;
+ }
+ }
+
+ public static ISilenceGenerator getSilenceGenerator(BroadcastFormat format)
+ {
+ switch (format)
+ {
+ case MP3:
+ return new MP3SilenceGenerator();
+ default:
+ throw new IllegalArgumentException("Unrecognized broadcast format [" + format +
+ "] can't create silence generator");
+ }
+ }
+}
diff --git a/src/audio/broadcast/BroadcastFormat.java b/src/audio/broadcast/BroadcastFormat.java
new file mode 100644
index 00000000..134423bd
--- /dev/null
+++ b/src/audio/broadcast/BroadcastFormat.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast;
+
+public enum BroadcastFormat
+{
+ MP3("audio/mpeg", ".mp3");
+
+ private String mValue;
+ private String mFileExtension;
+
+ private BroadcastFormat(String value, String fileExtension)
+ {
+ mValue = value;
+ mFileExtension = fileExtension;
+ }
+
+ public String getValue()
+ {
+ return mValue;
+ }
+
+ public String getFileExtension()
+ {
+ return mFileExtension;
+ }
+}
diff --git a/src/audio/broadcast/BroadcastModel.java b/src/audio/broadcast/BroadcastModel.java
new file mode 100644
index 00000000..703c350c
--- /dev/null
+++ b/src/audio/broadcast/BroadcastModel.java
@@ -0,0 +1,755 @@
+/*******************************************************************************
+ * 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 alias.id.broadcast.BroadcastChannel;
+import audio.AudioPacket;
+import audio.metadata.AudioMetadata;
+import controller.ThreadPoolManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import properties.SystemProperties;
+import sample.Broadcaster;
+import sample.Listener;
+import settings.SettingsManager;
+
+import javax.swing.*;
+import javax.swing.table.AbstractTableModel;
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class BroadcastModel extends AbstractTableModel implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger( BroadcastModel.class );
+
+ public static final String TEMPORARY_STREAM_DIRECTORY = "streaming";
+ public static final String TEMPORARY_STREAM_FILE_SUFFIX = "temporary_streaming_file_";
+
+ private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";
+
+ public static final int COLUMN_SERVER_ICON = 0;
+ public static final int COLUMN_STREAM_NAME = 1;
+ public static final int COLUMN_BROADCASTER_STATUS = 2;
+ public static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
+ public static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
+
+ private List mBroadcastConfigurations = new CopyOnWriteArrayList<>();
+ private List mRecordingQueue = new CopyOnWriteArrayList<>();
+
+ private Map mBroadcastConfigurationMap = new HashMap<>();
+ private Map mBroadcasterMap = new HashMap<>();
+ private ThreadPoolManager mThreadPoolManager;
+ private SettingsManager mSettingsManager;
+ private StreamManager mStreamManager;
+ private Broadcaster mBroadcastEventBroadcaster = new Broadcaster<>();
+
+ /**
+ * Model for managing Broadcast configurations and any associated broadcaster instances.
+ */
+ public BroadcastModel(ThreadPoolManager threadPoolManager, SettingsManager settingsManager)
+ {
+ mThreadPoolManager = threadPoolManager;
+ mSettingsManager = settingsManager;
+ mStreamManager = new StreamManager(threadPoolManager, new CompletedRecordingListener(), BroadcastFormat.MP3,
+ SystemProperties.getInstance().getApplicationFolder(TEMPORARY_STREAM_DIRECTORY));
+ mStreamManager.start();
+
+ //Monitor to remove temporary recording files that have been streamed by all audio broadcasters
+ mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_RECORDING,
+ new RecordingDeletionMonitor(), 15l, TimeUnit.SECONDS );
+
+ removeOrphanedTemporaryRecordings();
+ }
+
+ /**
+ * List of broadcastAudio configuration names
+ */
+ public List getBroadcastConfigurationNames()
+ {
+ List names = new ArrayList<>();
+
+ for(BroadcastConfiguration configuration: mBroadcastConfigurations)
+ {
+ names.add(configuration.getName());
+ }
+
+ return names;
+ }
+
+ /**
+ * Current list of broadcastAudio configurations
+ */
+ public List getBroadcastConfigurations()
+ {
+ return mBroadcastConfigurations;
+ }
+
+ /**
+ * Adds the list of broadcastAudio configurations to this model
+ */
+ public void addBroadcastConfigurations(List configurations)
+ {
+ for(BroadcastConfiguration configuration: configurations)
+ {
+ addBroadcastConfiguration(configuration);
+ }
+ }
+
+ /**
+ * Adds the broadcastAudio configuration to this model
+ */
+ public void addBroadcastConfiguration(BroadcastConfiguration configuration)
+ {
+ if(configuration != null)
+ {
+ ensureUniqueName(configuration);
+
+ if(!mBroadcastConfigurations.contains(configuration))
+ {
+ mBroadcastConfigurations.add(configuration);
+
+ int index = mBroadcastConfigurations.size() - 1;
+
+ fireTableRowsInserted( index, index );
+
+ mBroadcastConfigurationMap.put(configuration.getName(), configuration);
+
+ process( new BroadcastEvent( configuration, BroadcastEvent.Event.CONFIGURATION_ADD) );
+ }
+ }
+ }
+
+ /**
+ * Clones the configuration and adds it this model with a unique configuration name
+ */
+ public BroadcastConfiguration cloneBroadcastConfiguration(BroadcastConfiguration configuration)
+ {
+ if(configuration != null)
+ {
+ BroadcastConfiguration clone = configuration.copyOf();
+ addBroadcastConfiguration(clone);
+ return clone;
+ }
+
+ return null;
+ }
+
+ /**
+ * Updates the configuration's name so that it is unique among all other broadcast configurations
+ * @param configuration
+ */
+ private void ensureUniqueName(BroadcastConfiguration configuration)
+ {
+ if(configuration.getName() == null || configuration.getName().isEmpty())
+ {
+ configuration.setName("New Configuration");
+ }
+
+ while(!isUniqueName(configuration.getName(), configuration))
+ {
+ String currentName = configuration.getName();
+
+ if(currentName.matches(UNIQUE_NAME_REGEX))
+ {
+ int currentVersion = 1;
+
+ StringBuilder sb = new StringBuilder();
+ Matcher m = Pattern.compile(UNIQUE_NAME_REGEX).matcher(currentName);
+
+ if(m.find())
+ {
+ String version = m.group(2);
+
+ try
+ {
+ currentVersion = Integer.parseInt(version);
+ }
+ catch(Exception e)
+ {
+ //Couldn't parse the version number -- keep incrementing until we find a winner
+ }
+
+ currentVersion++;
+
+ sb.append(m.group(1)).append("(").append(currentVersion).append(")");
+ }
+ else
+ {
+ sb.append(configuration.getName()).append("(").append(currentVersion).append(")");
+ }
+
+ configuration.setName(sb.toString());
+ }
+ else
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(configuration.getName()).append("(2)");
+ configuration.setName(sb.toString());
+ }
+ }
+
+ }
+
+ /**
+ * Indicates if the name is unique among all of the broadcast configurations. Checks each of the configurations
+ * in this model for a name collision. Assumes that the name argument will be assigned to the configuration
+ * argument if the name is unique, therefore, the name will not be checked against the configuration argument for
+ * a collision.
+ *
+ * @param name to check for uniqueness
+ * @param configuration to ignore when checking the name for uniqueness
+ * @return true if the name is not null and unique among all configurations managed by this model
+ */
+ public boolean isUniqueName(String name, BroadcastConfiguration configuration)
+ {
+ if(name == null || name.isEmpty())
+ {
+ return false;
+ }
+
+ for(BroadcastConfiguration configurationToCompare: mBroadcastConfigurations)
+ {
+ if(configurationToCompare != configuration &&
+ configurationToCompare.getName() != null &&
+ configurationToCompare.getName().equals(name))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public void removeBroadcastConfiguration(BroadcastConfiguration broadcastConfiguration)
+ {
+ if(broadcastConfiguration != null && mBroadcastConfigurations.contains(broadcastConfiguration))
+ {
+ int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
+
+ mBroadcastConfigurations.remove(broadcastConfiguration);
+
+ mBroadcastConfigurationMap.remove(broadcastConfiguration.getName());
+
+ process(new BroadcastEvent( broadcastConfiguration, BroadcastEvent.Event.CONFIGURATION_DELETE));
+
+ fireTableRowsDeleted( index, index );
+ }
+ }
+
+ /**
+ * Returns the broadcaster associated with the stream name or null if there is no broadcaster setup for the name.
+ */
+ public AudioBroadcaster getBroadcaster(String streamName)
+ {
+ return mBroadcasterMap.get(streamName);
+ }
+
+ @Override
+ public void receive(AudioPacket audioPacket)
+ {
+ if (audioPacket.hasAudioMetadata() &&
+ audioPacket.getAudioMetadata().isStreamable())
+ {
+ mStreamManager.receive(audioPacket);
+ }
+ }
+
+ /**
+ * Creates a new broadcaster for the broadcast configuration and adds it to the model
+ */
+ private void createBroadcaster(BroadcastConfiguration broadcastConfiguration)
+ {
+ if (broadcastConfiguration != null &&
+ broadcastConfiguration.isEnabled() &&
+ broadcastConfiguration.isValid() &&
+ !mBroadcasterMap.containsKey(broadcastConfiguration.getName()))
+ {
+ AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(mThreadPoolManager,
+ broadcastConfiguration);
+
+ if (audioBroadcaster != null)
+ {
+ audioBroadcaster.setListener(new Listener()
+ {
+ @Override
+ public void receive(BroadcastEvent broadcastEvent)
+ {
+ process(broadcastEvent);
+ }
+ });
+
+ audioBroadcaster.start();
+
+ mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
+
+ int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
+
+ if(index >= 0)
+ {
+ fireTableRowsUpdated(index, index);
+ }
+
+ broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
+ }
+ }
+ }
+
+ /**
+ * Shut down a broadcaster created from the configuration and remove it from this model
+ */
+ private void deleteBroadcaster(String name)
+ {
+ if(name != null && mBroadcasterMap.containsKey(name))
+ {
+ AudioBroadcaster audioBroadcaster = mBroadcasterMap.remove(name);
+
+ if(audioBroadcaster != null)
+ {
+ audioBroadcaster.stop();
+ audioBroadcaster.removeListener();
+
+ int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
+
+ if(index >= 0)
+ {
+ fireTableRowsUpdated(index, index);
+ }
+
+ broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_DELETE));
+ }
+ }
+ }
+
+ /**
+ * Returns the broadcast configuration identified by the stream name
+ */
+ public BroadcastConfiguration getBroadcastConfiguration(String streamName)
+ {
+ return mBroadcastConfigurationMap.get(streamName);
+ }
+
+ /**
+ * Registers the listener to receive broadcastAudio configuration events
+ */
+ public void addListener(Listener listener)
+ {
+ mBroadcastEventBroadcaster.addListener(listener);
+ }
+
+ /**
+ * Removes the listener from receiving broadcastAudio configuration events
+ */
+ public void removeListener(Listener listener)
+ {
+ mBroadcastEventBroadcaster.removeListener(listener);
+ }
+
+ /**
+ * Broadcasts the broadcastAudio configuration change event
+ */
+ private void broadcast(BroadcastEvent event)
+ {
+ mBroadcastEventBroadcaster.broadcast( event );
+ }
+
+ /**
+ * Process a broadcast event from one of the broadcasters managed by this model
+ */
+ public void process(BroadcastEvent broadcastEvent)
+ {
+ if(broadcastEvent.isBroadcastConfigurationEvent())
+ {
+ switch(broadcastEvent.getEvent())
+ {
+ case CONFIGURATION_ADD:
+ createBroadcaster(broadcastEvent.getBroadcastConfiguration());
+ break;
+ case CONFIGURATION_CHANGE:
+ BroadcastConfiguration broadcastConfiguration = broadcastEvent.getBroadcastConfiguration();
+ int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
+ fireTableRowsUpdated( index, index );
+
+ //Delete and recreate the broadcaster for any broadcast configuration changes
+ String previousChannelName = cleanupMapAssociations(broadcastConfiguration);
+ deleteBroadcaster(previousChannelName);
+
+ //Delay restarting the broadcaster to allow remote server time to cleanup
+ mThreadPoolManager.scheduleOnce(new DelayedBroadcasterStartup(broadcastConfiguration), 1, TimeUnit.SECONDS);
+ break;
+ case CONFIGURATION_DELETE:
+ deleteBroadcaster(broadcastEvent.getBroadcastConfiguration().getName());
+ break;
+ }
+ }
+ else if(broadcastEvent.isAudioBroadcasterEvent())
+ {
+ int row = mBroadcastConfigurations.indexOf(broadcastEvent.getAudioBroadcaster().getBroadcastConfiguration());
+
+ switch(broadcastEvent.getEvent())
+ {
+ case BROADCASTER_QUEUE_CHANGE:
+ if(row >= 0)
+ {
+ fireTableCellUpdated(row, COLUMN_BROADCASTER_QUEUE_SIZE);
+ }
+ break;
+ case BROADCASTER_STATE_CHANGE:
+ if(row >= 0)
+ {
+ fireTableCellUpdated(row, COLUMN_BROADCASTER_STATUS);
+ }
+ break;
+ case BROADCASTER_STREAMED_COUNT_CHANGE:
+ if(row >= 0)
+ {
+ fireTableCellUpdated(row, COLUMN_BROADCASTER_STREAMED_COUNT);
+ }
+ break;
+ }
+ }
+
+ //Rebroadcast the event to any listeners of this model
+ broadcast(broadcastEvent);
+ }
+
+ /**
+ * When the name of a broadcast configuration changes, remove the old map associations so that they can be replaced
+ * with the new map associations. Remove any broadcaster that is associated with the old configuration name.
+ *
+ * @param broadcastConfiguration
+ * @return previous or current channel name that has been replaced with the current configuration channel name so
+ * that anything else associated with the previous name can be cleaned up.
+ */
+ private String cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
+ {
+ String oldName = null;
+
+ for(Map.Entry entry: mBroadcastConfigurationMap.entrySet())
+ {
+ if(entry.getValue() == broadcastConfiguration)
+ {
+ oldName = entry.getKey();
+ continue;
+ }
+ }
+
+ if(oldName != null)
+ {
+ mBroadcastConfigurationMap.remove(oldName);
+ }
+
+ mBroadcastConfigurationMap.put(broadcastConfiguration.getName(), broadcastConfiguration);
+
+ return oldName;
+ }
+
+ @Override
+ public int getRowCount()
+ {
+ return mBroadcastConfigurations.size();
+ }
+
+ @Override
+ public int getColumnCount()
+ {
+ return 5;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex)
+ {
+ try
+ {
+ if( rowIndex <= mBroadcastConfigurations.size())
+ {
+ BroadcastConfiguration configuration = mBroadcastConfigurations.get(rowIndex);
+
+ if(configuration != null)
+ {
+ switch(columnIndex)
+ {
+ case COLUMN_SERVER_ICON:
+ String iconName = configuration.getBroadcastServerType().getIconName();
+
+ if(iconName != null && mSettingsManager != null)
+ {
+ return mSettingsManager.getImageIcon(iconName, 14);
+ }
+ break;
+ case COLUMN_STREAM_NAME:
+ return configuration.getName();
+ case COLUMN_BROADCASTER_STATUS:
+ AudioBroadcaster audioBroadcasterA = mBroadcasterMap.get(configuration.getName());
+
+ if(audioBroadcasterA != null)
+ {
+ return audioBroadcasterA.getBroadcastState();
+ }
+ else if(!configuration.isEnabled())
+ {
+ return BroadcastState.DISABLED;
+ }
+ else if(!configuration.isValid())
+ {
+ return BroadcastState.INVALID_SETTINGS;
+ }
+ case COLUMN_BROADCASTER_QUEUE_SIZE:
+ AudioBroadcaster audioBroadcasterB = mBroadcasterMap.get(configuration.getName());
+
+ if(audioBroadcasterB != null)
+ {
+ return audioBroadcasterB.getQueueSize();
+ }
+ break;
+ case COLUMN_BROADCASTER_STREAMED_COUNT:
+ AudioBroadcaster audioBroadcasterC = mBroadcasterMap.get(configuration.getName());
+
+ if(audioBroadcasterC != null)
+ {
+ return audioBroadcasterC.getStreamedAudioCount();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ mLog.error("Error accessing data in broadcast model", e);
+ }
+
+ return null;
+ }
+
+ @Override
+ public Class> getColumnClass(int columnIndex)
+ {
+ if(columnIndex == COLUMN_SERVER_ICON)
+ {
+ return ImageIcon.class;
+ }
+
+ return String.class;
+ }
+
+ /**
+ * Broadcast configuration at the specified model row
+ */
+ public BroadcastConfiguration getConfigurationAt(int rowIndex)
+ {
+ return mBroadcastConfigurations.get(rowIndex);
+ }
+
+ /**
+ * Model row number for the specified configuration
+ */
+ public int getRowForConfiguration(BroadcastConfiguration configuration)
+ {
+ return mBroadcastConfigurations.indexOf(configuration);
+ }
+
+ @Override
+ public String getColumnName(int column)
+ {
+ switch(column)
+ {
+ case COLUMN_SERVER_ICON:
+ return null;
+ case COLUMN_STREAM_NAME:
+ return "Name";
+ case COLUMN_BROADCASTER_STATUS:
+ return "Status";
+ case COLUMN_BROADCASTER_QUEUE_SIZE:
+ return "Queued";
+ case COLUMN_BROADCASTER_STREAMED_COUNT:
+ return "Streamed";
+ }
+
+ return null;
+ }
+
+ /**
+ * Cleanup method to remove a temporary recording file from disk.
+ *
+ * @param recording to remove
+ */
+ private void removeRecording(AudioRecording recording)
+ {
+ try
+ {
+ Files.delete(recording.getPath());
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error deleting temporary internet recording file: " + recording.getPath().toString() + " - " +
+ ioe.getMessage());
+ }
+ }
+
+
+ /**
+ * Removes any temporary stream recordings left-over from the previous application run.
+ *
+ * This should only be invoked on startup.
+ */
+ private void removeOrphanedTemporaryRecordings()
+ {
+ Path path = SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY);
+
+ if(path != null && Files.isDirectory(path))
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(TEMPORARY_STREAM_FILE_SUFFIX);
+ sb.append("*.*");
+
+ try(DirectoryStream directoryStream = Files.newDirectoryStream(path, sb.toString()))
+ {
+ directoryStream.forEach(new Consumer()
+ {
+ @Override
+ public void accept(Path path)
+ {
+ try
+ {
+ Files.delete(path);
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Couldn't delete orphaned temporary recording: " + path.toString(), ioe);
+ }
+ }
+ });
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error discovering orphaned temporary stream recording files", ioe);
+ }
+ }
+ }
+
+ /**
+ * Provides delayed startup of a broadcaster to allow for the remote server to complete a disconnection.
+ */
+ public class DelayedBroadcasterStartup implements Runnable
+ {
+ private BroadcastConfiguration mBroadcastConfiguration;
+
+ public DelayedBroadcasterStartup(BroadcastConfiguration configuration)
+ {
+ mBroadcastConfiguration = configuration;
+ }
+
+
+ @Override
+ public void run()
+ {
+ createBroadcaster(mBroadcastConfiguration);
+ }
+ }
+
+ /**
+ * Processes completed audio recordings and distributes them to the audio broadcasters. Adds the recording to
+ * the audio recording queue to be monitored for deletion.
+ */
+ public class CompletedRecordingListener implements Listener
+ {
+ @Override
+ public void receive(AudioRecording audioRecording)
+ {
+ AudioMetadata metadata = audioRecording.getAudioMetadata();
+
+ if(metadata != null && metadata.isStreamable())
+ {
+ for (BroadcastChannel broadcastChannel : metadata.getBroadcastChannels())
+ {
+ String channelName = broadcastChannel.getChannelName();
+
+ if (channelName != null)
+ {
+ AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
+
+ if (audioBroadcaster != null)
+ {
+ audioRecording.addPendingReplay();
+ audioBroadcaster.receive(audioRecording);
+ }
+ }
+ }
+ }
+
+ mRecordingQueue.add(audioRecording);
+ }
+ }
+
+ /**
+ * Monitors the recording queue and removes any recordings that have no pending replays by audio broadcasters
+ */
+ public class RecordingDeletionMonitor implements Runnable
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ List recordingsToDelete = new ArrayList<>();
+
+ Iterator it = mRecordingQueue.iterator();
+
+ AudioRecording recording;
+
+ while(it.hasNext())
+ {
+ recording = it.next();
+
+ if(!recording.hasPendingReplays())
+ {
+ recordingsToDelete.add(recording);
+ }
+ }
+
+ if(!recordingsToDelete.isEmpty())
+ {
+ for(AudioRecording recordingToDelete: recordingsToDelete)
+ {
+ mRecordingQueue.remove(recordingToDelete);
+ removeRecording(recordingToDelete);
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ mLog.error("Error while checking audio recording queue for recordings to delete", e);
+ }
+ }
+ }
+}
diff --git a/src/audio/broadcast/BroadcastPanel.java b/src/audio/broadcast/BroadcastPanel.java
new file mode 100644
index 00000000..7136e83b
--- /dev/null
+++ b/src/audio/broadcast/BroadcastPanel.java
@@ -0,0 +1,332 @@
+/*******************************************************************************
+ * 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 alias.AliasModel;
+import com.jidesoft.swing.JideSplitPane;
+import gui.editor.Editor;
+import gui.editor.EmptyEditor;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import settings.SettingsManager;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.event.TableColumnModelEvent;
+import javax.swing.event.TableColumnModelListener;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableCellRenderer;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+public class BroadcastPanel extends JPanel implements ActionListener, ListSelectionListener
+{
+ private final static Logger mLog = LoggerFactory.getLogger( BroadcastPanel.class );
+
+ private static final String NEW_BROADCAST_CONFIGURATION = "New";
+ private static final String COPY_BROADCAST_CONFIGURATION = "Copy";
+ private static final String DELETE_BROADCAST_CONFIGURATION = "Delete";
+
+ private BroadcastModel mBroadcastModel;
+ private AliasModel mAliasModel;
+ private SettingsManager mSettingsManager;
+
+ private JTable mBroadcastConfigurationTable;
+ private JideSplitPane mSplitPane;
+ private Editor mEmptyEditor = new EmptyEditor<>("Configuration:");
+ private Editor mEditor;
+ private JButton mNewButton = new JButton(NEW_BROADCAST_CONFIGURATION);
+ private JButton mCopyButton = new JButton(COPY_BROADCAST_CONFIGURATION);
+ private JButton mDeleteButton = new JButton(DELETE_BROADCAST_CONFIGURATION);
+
+ public BroadcastPanel(BroadcastModel broadcastModel, AliasModel aliasModel, SettingsManager settingManager)
+ {
+ mBroadcastModel = broadcastModel;
+ mAliasModel = aliasModel;
+ mSettingsManager = settingManager;
+ mEditor = mEmptyEditor;
+
+ init();
+ }
+
+ private void init()
+ {
+ setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
+
+ mBroadcastConfigurationTable = new JTable(mBroadcastModel);
+ mBroadcastConfigurationTable.setAutoCreateRowSorter(true);
+ mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
+
+ mBroadcastConfigurationTable.getColumnModel().getColumn(BroadcastModel.COLUMN_SERVER_ICON).setPreferredWidth(1);
+ mBroadcastConfigurationTable.getColumnModel().getColumn(BroadcastModel.COLUMN_STREAM_NAME).setPreferredWidth(25);
+ mBroadcastConfigurationTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCASTER_STATUS)
+ .setCellRenderer(new StatusCellRenderer());
+
+ JScrollPane scroller = new JScrollPane(mBroadcastConfigurationTable);
+
+ JPanel buttonsPanel = new JPanel();
+ buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]"));
+
+ mNewButton = new JButton("New ...");
+ mNewButton.setToolTipText("Create a new broadcast audio configuration");
+ mNewButton.addMouseListener(new MouseAdapter()
+ {
+ @Override
+ public void mouseClicked(MouseEvent e)
+ {
+ JPopupMenu menu = new JPopupMenu();
+
+ menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.BROADCASTIFY));
+ menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.ICECAST_TCP));
+ menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.ICECAST_HTTP));
+ menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.SHOUTCAST_V1));
+ menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.SHOUTCAST_V2));
+
+ menu.show(e.getComponent(), e.getX(), e.getY());
+ }
+ });
+
+ buttonsPanel.add(mNewButton);
+
+ mCopyButton.addActionListener(this);
+ mCopyButton.setEnabled(false);
+ mCopyButton.setToolTipText("Creates a copy of the currently selected streaming configuration and adds it");
+ buttonsPanel.add(mCopyButton);
+
+ mDeleteButton.addActionListener(this);
+ mDeleteButton.setEnabled(false);
+ mDeleteButton.setToolTipText("Deletes the currently selected streaming configuration");
+ buttonsPanel.add(mDeleteButton);
+
+ JPanel listAndButtonsPanel = new JPanel();
+
+ listAndButtonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill][]"));
+
+ listAndButtonsPanel.add(scroller, "wrap");
+ listAndButtonsPanel.add(buttonsPanel);
+
+ mSplitPane = new JideSplitPane(JideSplitPane.HORIZONTAL_SPLIT);
+ mSplitPane.setDividerSize(5);
+ mSplitPane.setShowGripper(true);
+ mSplitPane.add(listAndButtonsPanel);
+
+ mSplitPane.add(mEditor);
+
+ add(mSplitPane);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ switch(e.getActionCommand())
+ {
+ case COPY_BROADCAST_CONFIGURATION:
+ BroadcastConfiguration copySelected = getSelectedBroadcastConfiguration();
+ mBroadcastModel.cloneBroadcastConfiguration(copySelected);
+ break;
+ case DELETE_BROADCAST_CONFIGURATION:
+ BroadcastConfiguration deleteSelected = getSelectedBroadcastConfiguration();
+
+ if(deleteSelected != null)
+ {
+ int choice = JOptionPane.showConfirmDialog(BroadcastPanel.this, "Do you want to delete broadcast\n" +
+ " configuration " + deleteSelected.getName() + "?", "Delete Broadcast Configuration?",
+ JOptionPane.YES_NO_OPTION);
+
+ if(choice == JOptionPane.YES_OPTION)
+ {
+ mBroadcastModel.removeBroadcastConfiguration(deleteSelected);
+ }
+ }
+ break;
+ }
+
+ }
+
+ /**
+ * Returns the selected configuration
+ */
+ private BroadcastConfiguration getSelectedBroadcastConfiguration()
+ {
+ int viewRow = mBroadcastConfigurationTable.getSelectedRow();
+
+ if(viewRow >= 0)
+ {
+ int modelRow = mBroadcastConfigurationTable.convertRowIndexToModel(viewRow);
+
+ if(modelRow >= 0)
+ {
+ return mBroadcastModel.getConfigurationAt(modelRow);
+ }
+ }
+
+ return null;
+ }
+
+
+ @Override
+ public void valueChanged(ListSelectionEvent e)
+ {
+ if (!e.getValueIsAdjusting())
+ {
+ BroadcastConfiguration selectedConfiguration = getSelectedBroadcastConfiguration();
+
+ //Enable buttons accordingly
+ mCopyButton.setEnabled(selectedConfiguration != null);
+ mDeleteButton.setEnabled(selectedConfiguration != null);
+
+ if (mEditor.isModified())
+ {
+ int option = JOptionPane.showConfirmDialog(
+ BroadcastPanel.this,
+ "Configuration settings have changed. Do you want to save these changes?",
+ "Save Changes?",
+ JOptionPane.YES_NO_OPTION,
+ JOptionPane.QUESTION_MESSAGE);
+
+ if (option == JOptionPane.YES_OPTION)
+ {
+ mEditor.save();
+ }
+ }
+
+ mSplitPane.removePane(mEditor);
+
+ if (selectedConfiguration != null)
+ {
+ mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mAliasModel,
+ mSettingsManager);
+ }
+ else
+ {
+ mEditor = mEmptyEditor;
+ }
+
+ mSplitPane.add(mEditor);
+
+ revalidate();
+ }
+ }
+
+ public class AddBroadcastConfigurationItem extends JMenuItem
+ {
+ private static final long serialVersionUID = 1L;
+
+ private BroadcastServerType mBroadcastServerType;
+
+ public AddBroadcastConfigurationItem(BroadcastServerType type)
+ {
+ super(type.toString(), mSettingsManager.getImageIcon(type.getIconName(), 14));
+
+ mBroadcastServerType = type;
+
+ setToolTipText("Add a new " + type.toString() + " audio stream configuration");
+
+ addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ final BroadcastConfiguration configuration =
+ BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
+
+ if(configuration != null)
+ {
+ mBroadcastModel.addBroadcastConfiguration(configuration);
+
+ int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
+ int tableRow = mBroadcastConfigurationTable.convertRowIndexToView(modelRow);
+ mBroadcastConfigurationTable.changeSelection(tableRow, 0, false, false);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Custom cell renderer for the broadcast state column.
+ */
+ public class StatusCellRenderer extends DefaultTableCellRenderer
+ {
+ public StatusCellRenderer()
+ {
+ setOpaque(true);
+ }
+
+ @Override
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column)
+ {
+ JLabel component = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+
+ if(isSelected)
+ {
+ setBackground(table.getSelectionBackground());
+ setForeground(table.getSelectionForeground());
+ }
+ else
+ {
+ if(value instanceof BroadcastState)
+ {
+ BroadcastState state = (BroadcastState)value;
+
+ if(state == BroadcastState.CONNECTED)
+ {
+ setBackground(Color.GREEN);
+ setForeground(table.getForeground());
+ }
+ else if(state == BroadcastState.DISABLED)
+ {
+ setBackground(table.getBackground());
+ setForeground(Color.LIGHT_GRAY);
+ }
+ else if(state == BroadcastState.INVALID_SETTINGS)
+ {
+ setBackground(Color.YELLOW);
+ setForeground(table.getForeground());
+ }
+ else if(state.isErrorState())
+ {
+ setBackground(Color.RED);
+ setForeground(table.getForeground());
+ }
+ else
+ {
+ setBackground(table.getBackground());
+ setForeground(table.getForeground());
+ }
+ }
+ else
+ {
+ setForeground(table.getForeground());
+ setBackground(table.getBackground());
+ }
+ }
+
+ return this;
+ }
+ }
+}
diff --git a/src/audio/broadcast/BroadcastServerType.java b/src/audio/broadcast/BroadcastServerType.java
new file mode 100644
index 00000000..25d2ca7c
--- /dev/null
+++ b/src/audio/broadcast/BroadcastServerType.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast;
+
+public enum BroadcastServerType
+{
+ BROADCASTIFY("Broadcastify", "images/broadcastify.png"), //Icecast Server 2.3.2
+ ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
+ ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
+ SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
+ SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
+ UNKNOWN("Unknown", null);
+
+ private String mLabel;
+ private String mIconName;
+
+ BroadcastServerType(String label, String iconName)
+ {
+ mLabel = label;
+ mIconName = iconName;
+ }
+
+ public String toString()
+ {
+ return mLabel;
+ }
+
+ public String getIconName()
+ {
+ return mIconName;
+ }
+}
diff --git a/src/audio/broadcast/BroadcastState.java b/src/audio/broadcast/BroadcastState.java
new file mode 100644
index 00000000..5e6d95ec
--- /dev/null
+++ b/src/audio/broadcast/BroadcastState.java
@@ -0,0 +1,116 @@
+/*******************************************************************************
+ * 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;
+
+public enum BroadcastState
+{
+ /**
+ * General error in configuration that causes the remote server to reject the connection
+ */
+ CONFIGURATION_ERROR("Configuration Error", true),
+
+ /**
+ * Connected to the broadcastAudio server and capable of streaming
+ */
+ CONNECTED("Connected", false),
+
+ /**
+ * Connection interrupted, attempting to reconnect.
+ */
+ CONNECTING("Connecting", false),
+
+ /**
+ * Indicates the configuration is disabled
+ */
+ DISABLED("Disabled", true),
+
+ /**
+ * Disconnected from the broadcastAudio server
+ */
+ DISCONNECTED("Disconnected", false),
+
+ /**
+ * Invalid credentials - user name or password
+ */
+ INVALID_CREDENTIALS("Invalid User Name/Password", true),
+
+ /**
+ * Invalid mount point
+ */
+ INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true),
+
+ /**
+ * Invalid configuration settings
+ */
+ INVALID_SETTINGS("Invalid Settings", true),
+
+ /**
+ * Remote server max sources has been exceeded
+ */
+ MAX_SOURCES_EXCEEDED("Max Sources Exceeded", true),
+
+ /**
+ * Specified mount point is already in use
+ */
+ MOUNT_POINT_IN_USE("Mount Point In Use", true),
+
+ /**
+ * Server is not known or reachable
+ */
+ NO_SERVER("No Server", false),
+
+ /**
+ * Initial state with no connection attempted.
+ */
+ READY("Ready", false),
+
+ /**
+ * Error while broadcasting stream data. Temporary error state to allow connection to be reset.
+ */
+ TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false),
+
+ /**
+ * Unsupported audio format
+ */
+ UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", 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/IBroadcastMetadataUpdater.java b/src/audio/broadcast/IBroadcastMetadataUpdater.java
new file mode 100644
index 00000000..04abb236
--- /dev/null
+++ b/src/audio/broadcast/IBroadcastMetadataUpdater.java
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * 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.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/StreamManager.java b/src/audio/broadcast/StreamManager.java
new file mode 100644
index 00000000..b658df7f
--- /dev/null
+++ b/src/audio/broadcast/StreamManager.java
@@ -0,0 +1,238 @@
+/*******************************************************************************
+ * 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 controller.ThreadPoolManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import record.AudioRecorder;
+import sample.Listener;
+import util.TimeStamp;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class StreamManager implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
+ private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
+
+ private static AtomicInteger sNextRecordingNumber = new AtomicInteger();
+
+ private ThreadPoolManager mThreadPoolManager;
+ private Listener mAudioRecordingListener;
+ private BroadcastFormat mBroadcastFormat;
+ private Path mTempDirectory;
+ private Map mStreamRecorders = new HashMap<>();
+ private Runnable mRecorderMonitor;
+ private ScheduledFuture> mRecorderMonitorFuture;
+ private AtomicBoolean mRunning = new AtomicBoolean();
+
+ /**
+ * Stream manager processes all incoming audio packets and reassembles individual audio streams, converts audio
+ * to desired output format and persists each stream to disc. Each recording is capped at a maximum length to
+ * ensure that recordings don't run too long before they are streamed out and to ensure that inactive recordings
+ * are closed in a timely fashion.
+ *
+ * Completed streamable audio recordings are nominated to the output listener (for broadcast) upon completion
+ *
+ * @param threadPoolManager for scheduling runnables
+ * @param listener to receive completed audio recordings
+ * @param tempDirectory where to store temporary audio recordings
+ */
+ public StreamManager(ThreadPoolManager threadPoolManager, Listener listener,
+ BroadcastFormat broadcastFormat, Path tempDirectory)
+ {
+ assert(tempDirectory != null && Files.isDirectory(tempDirectory));
+ assert(mThreadPoolManager != null);
+
+ mThreadPoolManager = threadPoolManager;
+ mAudioRecordingListener = listener;
+ mBroadcastFormat = broadcastFormat;
+ mTempDirectory = tempDirectory;
+ }
+
+ /**
+ * Starts the stream manager. Schedules a processor to monitor for inactive temporary stream recorders
+ * every 2 seconds.
+ */
+ public void start()
+ {
+ if(mRunning.compareAndSet(false, true))
+ {
+ if(mRecorderMonitor == null)
+ {
+ mRecorderMonitor = new RecorderMonitor();
+ }
+
+ mRecorderMonitorFuture = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
+ mRecorderMonitor, 2, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Stops the stream manager. Stops the inactive temporary stream recorder monitoring thread.
+ */
+ public void stop()
+ {
+ if(mRunning.compareAndSet(true, false))
+ {
+ if(mRecorderMonitorFuture != null)
+ {
+ mRecorderMonitorFuture.cancel(true);
+ }
+
+ synchronized (mStreamRecorders)
+ {
+ List streamKeys = new ArrayList<>(mStreamRecorders.keySet());
+
+ for(Integer streamKey: streamKeys)
+ {
+ removeRecorder(streamKey);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void receive(AudioPacket audioPacket)
+ {
+ if(mRunning.get() && audioPacket.hasAudioMetadata())
+ {
+ synchronized (mStreamRecorders)
+ {
+ int sourceChannelID = audioPacket.getAudioMetadata().getSource();
+
+ AudioPacket.Type type = audioPacket.getType();
+
+ if(type == AudioPacket.Type.AUDIO)
+ {
+ if(mStreamRecorders.containsKey(sourceChannelID))
+ {
+ AudioRecorder recorder = mStreamRecorders.get(sourceChannelID);
+
+ if(recorder != null)
+ {
+ recorder.receive(audioPacket);
+ }
+ }
+ else
+ {
+ AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
+ mBroadcastFormat);
+ recorder.start(mThreadPoolManager.getScheduledExecutorService());
+ recorder.receive(audioPacket);
+ mStreamRecorders.put(sourceChannelID, recorder);
+ }
+ }
+ else if(type == AudioPacket.Type.END)
+ {
+ removeRecorder(sourceChannelID);
+ }
+ else
+ {
+ mLog.info("Unrecognized Audio Packet Type: " + type);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes the recorder associated with the source channel ID.
+ *
+ * Note: this method invocation is not thread safe and must be invoked by a thread safe mechanism that protects the
+ * mStreamRecorders map.
+ *
+ * @param sourceChannelID identifying the recorder
+ */
+ private void removeRecorder(Integer sourceChannelID)
+ {
+ if(mStreamRecorders.containsKey(sourceChannelID))
+ {
+ AudioRecorder recorder = mStreamRecorders.remove(sourceChannelID);
+
+ recorder.close(new Listener()
+ {
+ @Override
+ public void receive(AudioRecorder audioRecorder)
+ {
+ AudioRecording audioRecording =
+ new AudioRecording(audioRecorder.getPath(), audioRecorder.getMetadata(),
+ audioRecorder.getTimeRecordingStart(), audioRecorder.getRecordingLength());
+
+ if(mAudioRecordingListener != null)
+ {
+ mAudioRecordingListener.receive(audioRecording);
+ }
+ }
+ });
+
+ }
+ }
+
+ /**
+ * Creates a temporary streaming recording file path
+ */
+ private Path getTemporaryRecordingPath()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
+ sb.append(sNextRecordingNumber.incrementAndGet()).append("_");
+ sb.append( TimeStamp.getLongTimeStamp( "_" ) );
+ sb.append( mBroadcastFormat.getFileExtension() );
+
+ Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString());
+
+ return temporaryRecordingPath;
+ }
+
+ /**
+ * Monitors recorders to ensure they don't exceed the maximum life-span allowed for a recording.
+ */
+ public class RecorderMonitor implements Runnable
+ {
+ @Override
+ public void run()
+ {
+ synchronized (mStreamRecorders)
+ {
+ long now = System.currentTimeMillis();
+
+ mStreamRecorders.entrySet().stream()
+ .filter(entry -> entry.getValue().getTimeRecordingStart() + MAXIMUM_RECORDER_LIFESPAN_MILLIS < now)
+ .forEach(entry ->
+ {
+ mLog.debug("Maximum recording time limit reached - removing recorder: " +
+ entry.getValue().getPath().toString());
+
+ removeRecorder(entry.getKey());
+ });
+ }
+ }
+ }
+}
diff --git a/src/audio/broadcast/broadcastify/BroadcastifyConfiguration.java b/src/audio/broadcast/broadcastify/BroadcastifyConfiguration.java
new file mode 100644
index 00000000..bc10af3e
--- /dev/null
+++ b/src/audio/broadcast/broadcastify/BroadcastifyConfiguration.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * 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.broadcastify;
+
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastFormat;
+import audio.broadcast.BroadcastServerType;
+import audio.broadcast.icecast.IcecastTCPConfiguration;
+
+import javax.xml.bind.annotation.XmlAttribute;
+
+public class BroadcastifyConfiguration extends IcecastTCPConfiguration
+{
+ private int mFeedID;
+
+ public BroadcastifyConfiguration()
+ {
+ //No-arg constructor for JAXB
+ this(BroadcastFormat.MP3);
+ }
+
+ /**
+ * Broadcastify configuration for Icecast 2.3.2 compatible servers
+ *
+ * @param format of output audio (MP3)
+ */
+ public BroadcastifyConfiguration(BroadcastFormat format)
+ {
+ super(format);
+
+ setBitRate(16);
+ setChannels(1);
+ setSampleRate(8000);
+ }
+
+ @Override
+ public BroadcastConfiguration copyOf()
+ {
+ BroadcastifyConfiguration copy = new BroadcastifyConfiguration(getBroadcastFormat());
+
+ //Broadcast Configuration Parameters
+ copy.setName(getName());
+ copy.setHost(getHost());
+ copy.setPort(getPort());
+ copy.setPassword(getPassword());
+ copy.setDelay(getDelay());
+
+ copy.setEnabled(false);
+
+ //Icecast Configuration Parameters
+ copy.setUserName(getUserName());
+ copy.setMountPoint(getMountPoint());
+ copy.setDescription(getDescription());
+ copy.setGenre(getGenre());
+ copy.setPublic(isPublic());
+ copy.setURL(getURL());
+
+ //Broadcastify Configuration Parameters
+ copy.setFeedID(getFeedID());
+
+ return copy;
+ }
+
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.BROADCASTIFY;
+ }
+
+ @XmlAttribute(name="feed_id")
+ public int getFeedID()
+ {
+ return mFeedID;
+ }
+
+ public void setFeedID(int feedID)
+ {
+ mFeedID = feedID;
+ }
+}
diff --git a/src/audio/broadcast/broadcastify/BroadcastifyConfigurationEditor.java b/src/audio/broadcast/broadcastify/BroadcastifyConfigurationEditor.java
new file mode 100644
index 00000000..28603438
--- /dev/null
+++ b/src/audio/broadcast/broadcastify/BroadcastifyConfigurationEditor.java
@@ -0,0 +1,369 @@
+/*******************************************************************************
+ * 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.broadcastify;
+
+import alias.AliasModel;
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastConfigurationEditor;
+import audio.broadcast.BroadcastEvent;
+import audio.broadcast.BroadcastModel;
+import audio.broadcast.BroadcastServerType;
+import com.radioreference.api.soap2.UserFeedBroadcast;
+import gui.editor.DocumentListenerEditor;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+import settings.SettingsManager;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+public class BroadcastifyConfigurationEditor extends BroadcastConfigurationEditor implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger( BroadcastifyConfigurationEditor.class );
+
+ private static final int ONE_MINUTE_MS = 60000;
+
+ private JTextField mName;
+ private JTextField mServer;
+ private JTextField mPort;
+ private JTextField mMountPoint;
+ private JTextField mPassword;
+ private JTextField mFeedID;
+ private JSlider mDelay;
+ private JLabel mDelayValue;
+ private JCheckBox mEnabled;
+ private JButton mSaveButton;
+ private JButton mResetButton;
+
+ public BroadcastifyConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
+ SettingsManager settingsManager)
+ {
+ super(broadcastModel, aliasModel, settingsManager);
+
+ init();
+ }
+
+ private void init()
+ {
+ setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]",
+ "[][][][][][][][][][][][][grow,fill]" ) );
+ setPreferredSize(new Dimension(150,400));
+
+
+ JLabel channelLabel = new JLabel("Broadcastify Stream");
+
+ ImageIcon broadcastifyIcon = mSettingsManager.getImageIcon(BroadcastServerType.BROADCASTIFY.getIconName(), 25);
+ channelLabel.setIcon(broadcastifyIcon);
+ add(channelLabel, "span, align center");
+
+ JButton lookupButton = new JButton("Lookup ...");
+ lookupButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ final UserFeedSelectionDialog dialog = new UserFeedSelectionDialog(BroadcastifyConfigurationEditor.this);
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ dialog.setLocationRelativeTo(BroadcastifyConfigurationEditor.this);
+ dialog.setVisible(true);
+ }
+ });
+ }
+ });
+
+ add(new JLabel(""));
+ add(lookupButton, "grow,wrap");
+
+ add(new JLabel("Name:"));
+ mName = new JTextField();
+ mName.getDocument().addDocumentListener(this);
+ mName.setToolTipText("Unique name for this stream");
+ add(mName);
+
+ add(new JLabel("Server:"));
+ mServer = new JTextField();
+ mServer.getDocument().addDocumentListener(this);
+ mServer.setToolTipText("Server address (e.g. audio3.broadcastify.com). See MyBCFY feed details, Technicals tab");
+ add(mServer);
+
+ add(new JLabel("Port:"));
+ mPort = new JTextField();
+ mPort.getDocument().addDocumentListener(this);
+ mPort.setToolTipText("Server port number (e.g. 80). See MyBCFY feed details, Technicals tab");
+ add(mPort);
+
+ add(new JLabel("Mount:"));
+ mMountPoint = new JTextField();
+ mMountPoint.getDocument().addDocumentListener(this);
+ mMountPoint.setToolTipText("Mount point (e.g. \\awlkejrlkjsd). See MyBCFY feed details, Technicals tab");
+ add(mMountPoint);
+
+ add(new JLabel("Password:"));
+ mPassword = new JTextField();
+ mPassword.getDocument().addDocumentListener(this);
+ mPassword.setToolTipText("Password. See MyBCFY feed details, Technicals tab");
+ add(mPassword);
+
+ add(new JLabel("Feed ID:"));
+ mFeedID = new JTextField();
+ mFeedID.getDocument().addDocumentListener(this);
+ mFeedID.setToolTipText("Feed ID (optional). See MyBCFY feed details");
+ add(mFeedID);
+
+
+ add(new JLabel());
+ mDelayValue = new JLabel("0 Minutes");
+ add(mDelayValue);
+
+ add(new JLabel("Delay:"));
+ mDelay = new JSlider(0,60,0);
+ mDelay.setMajorTickSpacing(10);
+ mDelay.setMinorTickSpacing(5);
+ mDelay.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ mDelayValue.setText(mDelay.getValue() + " Minutes");
+ setModified(true);
+ }
+ });
+ mDelay.setToolTipText("Audio broadcast delay in minutes");
+ add(mDelay);
+
+ mEnabled = new JCheckBox("Enabled");
+ mEnabled.setToolTipText("Enable (checked) or disable this stream");
+ mEnabled.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ add(new JLabel());
+ add(mEnabled);
+
+ mSaveButton = new JButton("Save");
+ mSaveButton.setEnabled(false);
+ mSaveButton.setToolTipText("Click to save configuration information");
+ mSaveButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ save();
+ }
+ });
+ add(mSaveButton);
+
+ mResetButton = new JButton("Reset");
+ mResetButton.setEnabled(false);
+ mResetButton.setToolTipText("Click to reset any changes you've made\n since the last save");
+ mResetButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(false);
+ setItem(getItem());
+ }
+ });
+ add(mResetButton);
+ }
+
+ @Override
+ public void setModified(boolean modified)
+ {
+ super.setModified(modified);
+
+ mSaveButton.setEnabled(modified);
+ mResetButton.setEnabled(modified);
+ }
+
+ @Override
+ public void setItem(BroadcastConfiguration item)
+ {
+ super.setItem(item);
+
+ if(hasItem())
+ {
+ BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem();
+
+ mName.setText(config.getName());
+ mServer.setText(config.getHost());
+ mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
+ mMountPoint.setText(config.getMountPoint());
+ mPassword.setText(config.getPassword());
+ mFeedID.setText(String.valueOf(config.getFeedID()));
+ mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
+ mEnabled.setSelected(config.isEnabled());
+ }
+ else
+ {
+ mName.setText(null);
+ mServer.setText(null);
+ mPort.setText(null);
+ mMountPoint.setText(null);
+ mPassword.setText(null);
+ mFeedID.setText(null);
+ mDelay.setValue(0);
+ mEnabled.setSelected(false);
+ }
+
+ setModified(false);
+ }
+
+ @Override
+ public void save()
+ {
+ BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem();
+
+ if(validateConfiguration())
+ {
+ updateConfigurationName(config, mName.getText());
+ config.setHost(mServer.getText());
+ config.setPort(getPort());
+ config.setMountPoint(mMountPoint.getText());
+ config.setPassword(mPassword.getText());
+ config.setFeedID(getFeedID());
+ config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
+ config.setEnabled(mEnabled.isSelected());
+
+ setModified(false);
+
+ mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
+ }
+ }
+
+ /**
+ * Validates the user specified values in the controls prior to saving them to the configuration
+ */
+ private boolean validateConfiguration()
+ {
+ String name = mName.getText();
+
+ //A unique stream name is required
+ if(!mBroadcastModel.isUniqueName(name, getItem()))
+ {
+ String message = (name == null || name.isEmpty()) ?
+ "Please specify a stream name." : "Stream name " + name +
+ "is already in use.\nPlease choose another name.";
+
+ JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this,
+ message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
+
+ mName.requestFocus();
+
+ return false;
+ }
+
+ //Server address is optional, but required
+ if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
+ {
+ return true;
+ }
+
+ //Port is optional, but required
+ if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
+ {
+ return true;
+ }
+
+ //Mount Point is optional, but required
+ if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
+ {
+ return true;
+ }
+
+ //Password is optional, but required
+ if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
+ {
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
+ */
+ private int getPort()
+ {
+ String port = mPort.getText();
+
+ if(port != null && !port.isEmpty())
+ {
+ try
+ {
+ return Integer.parseInt(port);
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the number value
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
+ */
+ private int getFeedID()
+ {
+ String feedID = mFeedID.getText();
+
+ if(feedID != null && !feedID.isEmpty())
+ {
+ try
+ {
+ return Integer.parseInt(feedID);
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the number value
+ }
+ }
+
+ return 0;
+ }
+
+ @Override
+ public void receive(UserFeedBroadcast userFeedBroadcast)
+ {
+ mName.setText(userFeedBroadcast.getDescr());
+ mServer.setText(userFeedBroadcast.getHostname());
+ mPort.setText(userFeedBroadcast.getPort());
+ mMountPoint.setText(userFeedBroadcast.getMount());
+ mPassword.setText(userFeedBroadcast.getPassword());
+ mFeedID.setText(String.valueOf(userFeedBroadcast.getFeedId()));
+ }
+}
diff --git a/src/audio/broadcast/broadcastify/UserFeedSelectionDialog.java b/src/audio/broadcast/broadcastify/UserFeedSelectionDialog.java
new file mode 100644
index 00000000..d9fb63e3
--- /dev/null
+++ b/src/audio/broadcast/broadcastify/UserFeedSelectionDialog.java
@@ -0,0 +1,247 @@
+/*******************************************************************************
+ * 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.broadcastify;
+
+import com.radioreference.api.soap2.UserFeedBroadcast;
+import external.radioreference.RadioReferenceService;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.xml.rpc.ServiceException;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.net.UnknownHostException;
+import java.rmi.RemoteException;
+
+public class UserFeedSelectionDialog extends JDialog implements DocumentListener
+{
+ private final static Logger mLog = LoggerFactory.getLogger( UserFeedSelectionDialog.class );
+
+ private Listener mListener;
+ private JTextField mUserNameText;
+ private JTextField mPasswordText;
+ private JButton mLookupButton;
+ private JList mUserFeedBroadcastList;
+ private JButton mOKButton;
+
+ public UserFeedSelectionDialog(Listener listener)
+ {
+ mListener = listener;
+ init();
+ }
+
+ private void init()
+ {
+ JPanel panel = new JPanel();
+ panel.setLayout(new MigLayout("","[align right,grow,fill][align left,grow,fill][]","[][][][][grow][]"));
+
+ panel.add(new JLabel("Please enter your credentials"), "align left,span");
+
+ panel.add(new JLabel("User Name:"));
+ mUserNameText = new JTextField();
+ mUserNameText.getDocument().addDocumentListener(this);
+ panel.add(mUserNameText, "wrap");
+
+ panel.add(new JLabel("Password:"));
+ mPasswordText = new JTextField();
+ mPasswordText.getDocument().addDocumentListener(this);
+ panel.add(mPasswordText, "wrap");
+
+ mLookupButton = new JButton("Get Feeds");
+ mLookupButton.setEnabled(false);
+ mLookupButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ mLookupButton.setEnabled(false);
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ RadioReferenceService service =
+ new RadioReferenceService(mUserNameText.getText(), mPasswordText.getText());
+
+ UserFeedBroadcast[] feeds = service.getUserFeedBroadcasts();
+
+ mUserFeedBroadcastList.setListData(feeds);
+ }
+ catch(ServiceException se)
+ {
+ JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
+ "Unable to create radio reference web service client. See log file.",
+ "Error", JOptionPane.ERROR_MESSAGE);
+
+ mLog.error("Error creating the radio reference web service client", se);
+ }
+ catch(RemoteException re)
+ {
+ Throwable cause = re.getCause();
+
+ if(cause == null)
+ {
+ JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
+ re.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
+ }
+ else if(cause instanceof UnknownHostException)
+ {
+ JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
+ "Broadcastify server is unavailable", "No Server",
+ JOptionPane.ERROR_MESSAGE);
+ }
+ else
+ {
+ JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
+ "An unknown error has occurred. Please see log file.", "" +
+ "Unknown Error", JOptionPane.ERROR_MESSAGE);
+
+ mLog.error("Error retrieving user broadcast feed configurations from radio reference " +
+ "service: ", re);
+ }
+ }
+
+ mLookupButton.setEnabled(true);
+ }
+ });
+ }
+ });
+ panel.add(mLookupButton,"grow,span");
+
+ mUserFeedBroadcastList = new JList();
+ mUserFeedBroadcastList.setVisibleRowCount(3);
+ mUserFeedBroadcastList.setLayoutOrientation(JList.VERTICAL);
+ mUserFeedBroadcastList.setCellRenderer(new CellRenderer());
+ mUserFeedBroadcastList.addListSelectionListener(new ListSelectionListener()
+ {
+ @Override
+ public void valueChanged(ListSelectionEvent e)
+ {
+ mOKButton.setEnabled(mUserFeedBroadcastList.getSelectedValue() != null);
+ }
+ });
+
+ JScrollPane listScrollPane = new JScrollPane(mUserFeedBroadcastList);
+ panel.add(listScrollPane, "grow,span");
+
+ mOKButton = new JButton("Ok");
+ mOKButton.setEnabled(false);
+ mOKButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ if(mListener != null)
+ {
+ mListener.receive((UserFeedBroadcast)mUserFeedBroadcastList.getSelectedValue());
+ }
+
+ dispose();
+ }
+ });
+ panel.add(mOKButton,"grow");
+
+ JButton cancelButton = new JButton("Cancel");
+ cancelButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ dispose();
+ }
+ });
+ panel.add(cancelButton, "wrap");
+
+ setTitle("Broadcastify Feed Lookup");
+ setLayout(new BorderLayout());
+ add(panel, BorderLayout.CENTER);
+ setSize(300,250);
+ setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ setModalityType(ModalityType.APPLICATION_MODAL);
+ }
+
+ private void updateButtons()
+ {
+ String username = mUserNameText.getText();
+ String password = mPasswordText.getText();
+
+ mLookupButton.setEnabled(username != null && !username.isEmpty() && password != null && !password.isEmpty());
+ }
+
+ @Override
+ public void insertUpdate(DocumentEvent e)
+ {
+ updateButtons();
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e)
+ {
+ updateButtons();
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e)
+ {
+ updateButtons();
+ }
+
+ private class CellRenderer extends JLabel implements ListCellRenderer
+ {
+ public CellRenderer()
+ {
+ setOpaque(true);
+ setHorizontalAlignment(LEFT);
+ setVerticalAlignment(CENTER);
+ }
+
+ @Override
+ public Component getListCellRendererComponent(JList extends UserFeedBroadcast> list,
+ UserFeedBroadcast value,
+ int index,
+ boolean isSelected,
+ boolean cellHasFocus)
+ {
+ if (isSelected)
+ {
+ setBackground(list.getSelectionBackground());
+ setForeground(list.getSelectionForeground());
+ }
+ else
+ {
+ setBackground(list.getBackground());
+ setForeground(list.getForeground());
+ }
+
+ setText(value.getDescr());
+
+ return this; }
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastAudioBroadcaster.java b/src/audio/broadcast/icecast/IcecastAudioBroadcaster.java
new file mode 100644
index 00000000..e7d7ed85
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastAudioBroadcaster.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * 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.icecast;
+
+import audio.broadcast.AudioBroadcaster;
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.IBroadcastMetadataUpdater;
+import controller.ThreadPoolManager;
+
+public abstract class IcecastAudioBroadcaster extends AudioBroadcaster
+{
+ private IBroadcastMetadataUpdater mMetadataUpdater;
+
+ public IcecastAudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration broadcastConfiguration)
+ {
+ super(threadPoolManager, broadcastConfiguration);
+ }
+
+ /**
+ * Icecast broadcast configuration
+ */
+ protected IcecastConfiguration getConfiguration()
+ {
+ return (IcecastConfiguration)getBroadcastConfiguration();
+ }
+
+ @Override
+ protected IBroadcastMetadataUpdater getMetadataUpdater()
+ {
+ if(mMetadataUpdater == null)
+ {
+ mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getThreadPoolManager(), getConfiguration());
+ }
+
+ return mMetadataUpdater;
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java b/src/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java
new file mode 100644
index 00000000..14b9867f
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastBroadcastMetadataUpdater.java
@@ -0,0 +1,281 @@
+/*******************************************************************************
+ * 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.icecast;
+
+import audio.broadcast.IBroadcastMetadataUpdater;
+import audio.metadata.AudioMetadata;
+import audio.metadata.Metadata;
+import audio.metadata.MetadataType;
+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.http.HttpClientCodec;
+import org.apache.mina.http.HttpRequestImpl;
+import org.apache.mina.http.api.HttpMethod;
+import org.apache.mina.http.api.HttpRequest;
+import org.apache.mina.http.api.HttpVersion;
+import org.apache.mina.transport.socket.nio.NioSocketConnector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import properties.SystemProperties;
+
+import java.io.UnsupportedEncodingException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater
+{
+ private final static Logger mLog = LoggerFactory.getLogger(IcecastBroadcastMetadataUpdater.class);
+ private final static String UTF8 = "UTF-8";
+
+ private ThreadPoolManager mThreadPoolManager;
+ private IcecastConfiguration mIcecastConfiguration;
+
+ private NioSocketConnector mSocketConnector;
+ private AtomicBoolean mUpdating = new AtomicBoolean();
+ private Queue mMetadataQueue = new LinkedTransferQueue<>();
+ private Map mHTTPHeaders;
+ private boolean mStackTraceLoggingSuppressed;
+
+ /**
+ * Icecast song metadata updater. Each metadata update is processed in the order received and the dispatch of
+ * the HTTP update request is processed in a separate thread (runnable) to avoid delaying streaming of this
+ * 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 IcecastBroadcastMetadataUpdater(ThreadPoolManager threadPoolManager, IcecastConfiguration icecastConfiguration)
+ {
+ mThreadPoolManager = threadPoolManager;
+ mIcecastConfiguration = icecastConfiguration;
+ }
+
+ /**
+ * Socket connector - lazy constructor.
+ */
+ private NioSocketConnector getSocketConnector()
+ {
+ if (mSocketConnector == null)
+ {
+ mSocketConnector = new NioSocketConnector();
+// 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
+ mSocketConnector.setHandler(new IoHandlerAdapter()
+ {
+ @Override
+ public void exceptionCaught(IoSession session, Throwable cause) throws Exception
+ {
+ //Single-use session - close it after we receive an error
+ session.closeNow();
+
+ mLog.error("Metadata update failed");
+ }
+
+ @Override
+ public void messageReceived(IoSession session, Object message) throws Exception
+ {
+ //Single-use session - close it after we receive a response
+ session.closeNow();
+ }
+ });
+ }
+
+ return mSocketConnector;
+ }
+
+ /**
+ * Sends a song metadata update to the remote server. Additional metadata updates received while the updater
+ * is in the process of sending an existing metadata update will be queued and processed in received order.
+ *
+ * Note: due to thread timing, there is a slight chance that a concurrent update request will not be processed
+ * and will remain in the update queue until the next metadata update is requested. However, this is a design
+ * trade-off to avoid having a scheduled runnable repeatedly processing the update queue.
+ */
+ public void update(AudioMetadata audioMetadata)
+ {
+ mMetadataQueue.offer(getSong(audioMetadata));
+
+ if (mUpdating.compareAndSet(false, true))
+ {
+ String song = mMetadataQueue.poll();
+
+ while (song != null)
+ {
+ HttpRequest updateRequest = createUpdateRequest(song);
+
+ if (updateRequest != null)
+ {
+ mThreadPoolManager.scheduleOnce(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ ConnectFuture connectFuture = getSocketConnector()
+ .connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
+ mIcecastConfiguration.getPort()));
+ connectFuture.awaitUninterruptibly();
+ IoSession session = connectFuture.getSession();
+
+ if (session != null)
+ {
+ session.write(updateRequest);
+ }
+ }
+ catch (Exception e)
+ {
+ Throwable throwableCause = e.getCause();
+
+ if (throwableCause instanceof ConnectException)
+ {
+ //Do nothing, the server is unavailable
+ }
+ else
+ {
+ if (!mStackTraceLoggingSuppressed)
+ {
+ mLog.error("Error sending metadata update. Future errors will " +
+ "be suppressed", e);
+
+ mStackTraceLoggingSuppressed = true;
+ }
+ }
+ }
+ }
+ }, 0l, TimeUnit.SECONDS);
+ }
+
+ //Fetch next metadata update to send
+ song = mMetadataQueue.poll();
+ }
+
+ mUpdating.set(false);
+ }
+ }
+
+ /**
+ * Creates the song information for a metadata update
+ */
+ private static String getSong(AudioMetadata audioMetadata)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ if(audioMetadata != null)
+ {
+ Metadata to = audioMetadata.getMetadata(MetadataType.TO);
+
+ sb.append("TO:");
+
+ if(to != null)
+ {
+ if(to.hasAlias())
+ {
+ sb.append(to.getAlias().getName());
+ }
+ else
+ {
+ sb.append(to.getValue());
+ }
+ }
+ else
+ {
+ sb.append("UNKNOWN");
+ }
+
+ Metadata from = audioMetadata.getMetadata(MetadataType.FROM);
+
+ sb.append(" FROM:");
+
+ if(from != null)
+ {
+
+ if(from.hasAlias())
+ {
+ sb.append(from.getAlias().getName());
+ }
+ else
+ {
+ sb.append(from.getValue());
+ }
+ }
+ else
+ {
+ sb.append("UNKNOWN");
+ }
+ }
+ else
+ {
+ sb.append("Scanning ....");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Creates an HTTP GET request to update the metadata on the remote server
+ */
+ private HttpRequest createUpdateRequest(String song)
+ {
+ try
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("mode=updinfo");
+ sb.append("&mount=").append(mIcecastConfiguration.getMountPoint());
+ sb.append("&charset=UTF%2d8");
+ sb.append("&song=").append(URLEncoder.encode(song, UTF8));
+
+ HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.GET, "/admin/metadata",
+ sb.toString(), getHTTPHeaders());
+
+ return request;
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ //This should never happen
+ mLog.error("UTF-8 encoding is not supported - can't update song metadata");
+ }
+
+ return null;
+ }
+
+ /**
+ * HTTP headers to use for a metadata update request.
+ */
+ private Map getHTTPHeaders()
+ {
+ if(mHTTPHeaders == null)
+ {
+ mHTTPHeaders = new HashMap<>();
+ mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
+ mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials());
+ }
+
+ return mHTTPHeaders;
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastConfiguration.java b/src/audio/broadcast/icecast/IcecastConfiguration.java
new file mode 100644
index 00000000..e70f6075
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastConfiguration.java
@@ -0,0 +1,292 @@
+/*******************************************************************************
+ * 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.icecast;
+
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastFormat;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlTransient;
+import java.util.Base64;
+
+public abstract class IcecastConfiguration extends BroadcastConfiguration
+{
+ protected String mUserName = "source";
+ private String mMountPoint = "/stream";
+ private String mDescription;
+ private String mGenre;
+ private boolean mPublic;
+ private int mBitRate = 16;
+ private int mChannels = 1;
+ private int mSampleRate = 8000;
+ private String mURL;
+
+ public IcecastConfiguration(BroadcastFormat format)
+ {
+ super(format);
+ }
+
+ public IcecastConfiguration()
+ {
+ }
+
+ /**
+ * Base64 encoded version of the username and password with prepended 'Basic ' tag.
+ */
+ @XmlTransient
+ public String getBase64EncodedCredentials()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ if(hasUserName())
+ {
+ sb.append(getUserName()).append(":").append(getPassword());
+ }
+ else
+ {
+ sb.append(getPassword());
+ }
+
+ String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
+
+ return "Basic " + base64;
+ }
+
+ @XmlAttribute( name="user_name" )
+ public String getUserName()
+ {
+ return mUserName;
+ }
+
+ public void setUserName(String userName)
+ {
+ mUserName = userName;
+ }
+
+ public boolean hasUserName()
+ {
+ return mUserName != null;
+ }
+
+ /**
+ * Mount point or path to the stream
+ * @return mount point
+ */
+ @XmlAttribute( name="mount_point" )
+ public String getMountPoint()
+ {
+ return mMountPoint;
+ }
+
+ /**
+ * Sets the mount point (path) for the stream
+ * @param mountPoint
+ */
+ public void setMountPoint(String mountPoint)
+ {
+ if(mountPoint != null && !mountPoint.isEmpty())
+ {
+ if(mountPoint.startsWith("/"))
+ {
+ mMountPoint = mountPoint;
+ }
+ else
+ {
+ mMountPoint = "/" + mountPoint;
+ }
+ }
+ }
+
+ public boolean hasMountPoint()
+ {
+ return mMountPoint != null;
+ }
+
+ @XmlAttribute( name="description" )
+ public String getDescription()
+ {
+ return mDescription;
+ }
+
+ public void setDescription(String description)
+ {
+ mDescription = description;
+ }
+
+ public boolean hasDescription()
+ {
+ return mDescription != null;
+ }
+
+ /**
+ * Stream genre
+ */
+ @XmlAttribute( name="genre" )
+ public String getGenre()
+ {
+ return mGenre;
+ }
+
+ /**
+ * Stream genre
+ * @param genre
+ */
+ public void setGenre(String genre)
+ {
+ mGenre = genre;
+ }
+
+ public boolean hasGenre()
+ {
+ return mGenre != null;
+ }
+
+ /**
+ * Public visibility of the broadcastAudio
+ */
+ @XmlAttribute( name="public" )
+ public boolean isPublic()
+ {
+ return mPublic;
+ }
+
+ /**
+ * Sets public visibility of the broadcastAudio
+ * @param isPublic indicates if the broadcastAudio should be visible to the public
+ */
+ public void setPublic(boolean isPublic)
+ {
+ mPublic = isPublic;
+ }
+
+ /**
+ * Number of audio channels in the broadcastAudio
+ */
+ @XmlAttribute( name="channels" )
+ public int getChannels()
+ {
+ return mChannels;
+ }
+
+ /**
+ * Sets the number of audio channels in the broadcastAudio
+ */
+ public void setChannels(int channels)
+ {
+ mChannels = channels;
+ }
+
+ public boolean hasChannels()
+ {
+ return mChannels > 0;
+ }
+
+ /**
+ * Bit rate in bits per second
+ */
+ @XmlAttribute( name="bitrate" )
+ public int getBitRate()
+ {
+ return mBitRate;
+ }
+
+ /**
+ * Bit rate
+ * @param bitRate in samples per second
+ */
+ public void setBitRate(int bitRate)
+ {
+ mBitRate = bitRate;
+ }
+
+ public boolean hasBitRate()
+ {
+ return mBitRate > 0;
+ }
+
+ @XmlAttribute( name="sample_rate" )
+ public int getSampleRate()
+ {
+ return mSampleRate;
+ }
+
+ public void setSampleRate(int sampleRate)
+ {
+ mSampleRate = sampleRate;
+ }
+
+ public boolean hasSampleRate()
+ {
+ return mSampleRate > 0;
+ }
+
+ /**
+ * URL associated with the broadcastAudio where users can find additional details.
+ */
+ @XmlAttribute( name="url" )
+ public String getURL()
+ {
+ return mURL;
+ }
+
+ /**
+ * URL associated with the broadcastAudio where users can find additional details.
+ * @param url
+ */
+ public void setURL(String url)
+ {
+ mURL = url;
+ }
+
+ public boolean hasURL()
+ {
+ return mURL != null;
+ }
+
+ @Override
+ public boolean isValid()
+ {
+ if(!super.isValid())
+ {
+ return false;
+ }
+ else if(getUserName() == null)
+ {
+ return false;
+ }
+ else if(getMountPoint() == null)
+ {
+ return false;
+ }
+ else if(getChannels() != 1)
+ {
+ return false;
+ }
+ else if(getSampleRate() <= 0)
+ {
+ return false;
+ }
+ else if(getBitRate() <= 0)
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java b/src/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java
new file mode 100644
index 00000000..d40f9e32
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastHTTPAudioBroadcaster.java
@@ -0,0 +1,329 @@
+/*******************************************************************************
+ * 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.icecast;
+
+import audio.broadcast.BroadcastState;
+import audio.convert.MP3AudioConverter;
+import controller.ThreadPoolManager;
+import org.apache.mina.core.RuntimeIoException;
+import org.apache.mina.core.buffer.IoBuffer;
+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.codec.ProtocolDecoderException;
+import org.apache.mina.http.HttpClientCodec;
+import org.apache.mina.http.HttpException;
+import org.apache.mina.http.HttpRequestImpl;
+import org.apache.mina.http.api.DefaultHttpResponse;
+import org.apache.mina.http.api.HttpMethod;
+import org.apache.mina.http.api.HttpStatus;
+import org.apache.mina.http.api.HttpVersion;
+import org.apache.mina.transport.socket.nio.NioSocketConnector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import properties.SystemProperties;
+
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
+{
+ private static final Logger mLog = LoggerFactory.getLogger( IcecastHTTPAudioBroadcaster.class );
+ private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
+
+ private NioSocketConnector mSocketConnector;
+ private IoSession mStreamingSession = null;
+ private Map mHTTPHeaders;
+
+ private long mLastConnectionAttempt = 0;
+ private AtomicBoolean mConnecting = new AtomicBoolean();
+
+ /**
+ * Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
+ * compatible with Icecast version 2.3.2 and older versions of the server software.
+ *
+ * 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
+ * IcecastHTTPIOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
+ *
+ * @param configuration for the Icecast stream
+ */
+ public IcecastHTTPAudioBroadcaster(ThreadPoolManager threadPoolManager, IcecastHTTPConfiguration configuration)
+ {
+ super(threadPoolManager, configuration);
+ }
+
+ /**
+ * Broadcasts the audio frame or sequence
+ */
+ @Override
+ protected void broadcastAudio(byte[] audio)
+ {
+ if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
+ {
+ IoBuffer buffer = IoBuffer.allocate(audio.length);
+ buffer.put(audio);
+ buffer.flip();
+
+ mStreamingSession.write(buffer);
+ }
+ }
+
+ /**
+ * (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
+ * is currently connected to the remote server following any connection attempts.
+ *
+ * Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
+ *
+ * @return true if the audio handler can stream audio
+ */
+ private boolean connect()
+ {
+ if(!connected() && canConnect() &&
+ (mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
+ mConnecting.compareAndSet(false, true))
+ {
+ mLastConnectionAttempt = System.currentTimeMillis();
+
+ if(mSocketConnector == null)
+ {
+ mSocketConnector = new NioSocketConnector();
+ mSocketConnector.setConnectTimeoutCheckInterval(10000);
+
+// mSocketConnector.getFilterChain().addLast("logger",
+// new LoggingFilter(IcecastTCPAudioBroadcaster.class));
+
+ mSocketConnector.getFilterChain().addLast("codec", new HttpClientCodec());
+ mSocketConnector.setHandler(new IcecastHTTPIOHandler());
+ }
+
+ mStreamingSession = null;
+
+ Runnable runnable = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ setBroadcastState(BroadcastState.CONNECTING);
+
+ try
+ {
+ ConnectFuture future = mSocketConnector
+ .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
+ getBroadcastConfiguration().getPort()));
+ future.awaitUninterruptibly();
+ mStreamingSession = future.getSession();
+ }
+ catch(RuntimeIoException rie)
+ {
+ Throwable throwableCause = rie.getCause();
+
+ if(throwableCause instanceof ConnectException)
+ {
+ setBroadcastState(BroadcastState.NO_SERVER);
+ }
+ else if(throwableCause != null)
+ {
+ setBroadcastState(BroadcastState.ERROR);
+ mLog.debug("Failed to connect", rie);
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.ERROR);
+ mLog.debug("Failed to connect - no exception is available");
+ }
+
+ disconnect();
+ }
+ }
+ };
+
+ getThreadPoolManager().scheduleOnce(runnable, 0l, TimeUnit.SECONDS);
+
+ }
+
+ return connected();
+ }
+
+
+ /**
+ * Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
+ */
+ public void disconnect()
+ {
+ if(mStreamingSession != null)
+ {
+ mStreamingSession.closeNow();
+ }
+
+ mConnecting.set(false);
+ }
+
+ /**
+ * IO Handler for managing Icecast HTTP connection and credentials
+ */
+ public class IcecastHTTPIOHandler extends IoHandlerAdapter
+ {
+ @Override
+ public void sessionOpened(IoSession session) throws Exception
+ {
+ //Send stream configuration and user credentials upon connecting to remote server
+ HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.PUT,
+ getConfiguration().getMountPoint(), "", getHTTPHeaders());
+
+ session.write(request);
+ }
+
+ private Map getHTTPHeaders()
+ {
+ if(mHTTPHeaders == null)
+ {
+ mHTTPHeaders = new HashMap<>();
+ mHTTPHeaders.put(IcecastHeader.ACCEPT.getValue(), "*/*");
+ mHTTPHeaders.put(IcecastHeader.CONTENT_TYPE.getValue(), getConfiguration().getBroadcastFormat().getValue());
+ mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
+ mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getBase64EncodedCredentials());
+ mHTTPHeaders.put(IcecastHeader.PUBLIC.getValue(), getConfiguration().isPublic() ? "1" : "0");
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("samplerate=").append(getConfiguration().getSampleRate()).append(";");
+ sb.append("quality=").append(MP3AudioConverter.AUDIO_QUALITY).append(";");
+ sb.append("channels=").append(getConfiguration().getChannels());
+ mHTTPHeaders.put(IcecastHeader.AUDIO_INFO.getValue(), sb.toString());
+
+ if(getConfiguration().hasName())
+ {
+ mHTTPHeaders.put(IcecastHeader.NAME.getValue(), getConfiguration().getName());
+ }
+
+ if(getConfiguration().hasDescription())
+ {
+ mHTTPHeaders.put(IcecastHeader.DESCRIPTION.getValue(), getConfiguration().getDescription());
+ }
+
+ if(getConfiguration().hasURL())
+ {
+ mHTTPHeaders.put(IcecastHeader.URL.getValue(), getConfiguration().getURL());
+ }
+
+ if(getConfiguration().hasGenre())
+ {
+ mHTTPHeaders.put(IcecastHeader.GENRE.getValue(), getConfiguration().getGenre());
+ }
+
+
+ if(getConfiguration().hasBitRate())
+ {
+ mHTTPHeaders.put(IcecastHeader.BITRATE.getValue(), String.valueOf(getConfiguration().getBitRate()));
+ }
+ }
+
+ return mHTTPHeaders;
+ }
+
+ @Override
+ public void sessionClosed(IoSession session) throws Exception
+ {
+ //If there is already an error state, don't override it. Otherwise, set state to disconnected
+ if(!getBroadcastState().isErrorState())
+ {
+ setBroadcastState(BroadcastState.DISCONNECTED);
+ }
+
+ mSocketConnector.dispose();
+ mStreamingSession = null;
+ mSocketConnector = null;
+
+ mConnecting.set(false);
+ super.sessionClosed(session);
+ }
+
+ @Override
+ public void exceptionCaught(IoSession session, Throwable throwable) throws Exception
+ {
+ if(throwable instanceof ProtocolDecoderException)
+ {
+ Throwable cause = ((ProtocolDecoderException)throwable).getCause();
+
+ if(cause instanceof HttpException &&
+ ((HttpException)cause).getStatusCode() == HttpStatus.CLIENT_ERROR_LENGTH_REQUIRED.code())
+ {
+ //Ignore - Icecast 2.4 sometimes doesn't send any headers with their HTTP responses. Mina expects a
+ //content-length for HTTP responses.
+ }
+ else
+ {
+ mLog.error("HTTP protocol decoder error", throwable);
+ setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
+ disconnect();
+ }
+ }
+ else
+ {
+ mLog.error("Broadcast error", throwable);
+ setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
+ disconnect();
+ }
+
+ mConnecting.set(false);
+ }
+
+ @Override
+ public void messageReceived(IoSession session, Object object) throws Exception
+ {
+ if(object instanceof DefaultHttpResponse)
+ {
+ DefaultHttpResponse response = (DefaultHttpResponse)object;
+
+ switch(response.getStatus())
+ {
+ case INFORMATIONAL_CONTINUE:
+ break;
+ case SUCCESS_OK:
+ setBroadcastState(BroadcastState.CONNECTED);
+ mConnecting.set(false);
+ break;
+ case CLIENT_ERROR_UNAUTHORIZED:
+ setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
+ disconnect();
+ break;
+ case CLIENT_ERROR_FORBIDDEN:
+ setBroadcastState(BroadcastState.CONFIGURATION_ERROR);
+ disconnect();
+ break;
+ default:
+ setBroadcastState(BroadcastState.ERROR);
+ disconnect();
+ mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass());
+ break;
+ }
+ }
+ else
+ {
+ mLog.error("Icecast HTTP broadcaster - unrecognized message [ " + object.getClass() +
+ "] received:" + object.toString());
+ }
+ }
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastHTTPConfiguration.java b/src/audio/broadcast/icecast/IcecastHTTPConfiguration.java
new file mode 100644
index 00000000..ee0be59b
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastHTTPConfiguration.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * 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.icecast;
+
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastFormat;
+import audio.broadcast.BroadcastServerType;
+
+import java.util.Base64;
+
+public class IcecastHTTPConfiguration extends IcecastConfiguration
+{
+ public IcecastHTTPConfiguration()
+ {
+ //no-arg JAXB constructor
+ this(BroadcastFormat.MP3);
+ }
+
+ /**
+ * Icecast 2.4.x+ compatible configuration
+ */
+ public IcecastHTTPConfiguration(BroadcastFormat format)
+ {
+ super(format);
+ setPassword("change me!");
+ }
+
+ @Override
+ public BroadcastConfiguration copyOf()
+ {
+ IcecastHTTPConfiguration copy = new IcecastHTTPConfiguration(getBroadcastFormat());
+
+ //Broadcast Configuration Parameters
+ copy.setName(getName());
+ copy.setHost(getHost());
+ copy.setPort(getPort());
+ copy.setPassword(getPassword());
+ copy.setDelay(getDelay());
+ copy.setEnabled(false);
+
+ //Icecast Configuration Parameters
+ copy.setUserName(getUserName());
+ copy.setMountPoint(getMountPoint());
+ copy.setDescription(getDescription());
+ copy.setGenre(getGenre());
+ copy.setPublic(isPublic());
+ copy.setBitRate(getBitRate());
+ copy.setChannels(getChannels());
+ copy.setSampleRate(getSampleRate());
+ copy.setURL(getURL());
+
+ return copy;
+ }
+
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.ICECAST_HTTP;
+ }
+
+
+}
diff --git a/src/audio/broadcast/icecast/IcecastHTTPConfigurationEditor.java b/src/audio/broadcast/icecast/IcecastHTTPConfigurationEditor.java
new file mode 100644
index 00000000..2352e46b
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastHTTPConfigurationEditor.java
@@ -0,0 +1,351 @@
+/*******************************************************************************
+ * 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.icecast;
+
+import alias.AliasModel;
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastConfigurationEditor;
+import audio.broadcast.BroadcastEvent;
+import audio.broadcast.BroadcastModel;
+import audio.broadcast.BroadcastServerType;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import settings.SettingsManager;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+public class IcecastHTTPConfigurationEditor extends BroadcastConfigurationEditor
+{
+ private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPConfigurationEditor.class );
+
+ private static final int ONE_MINUTE_MS = 60000;
+
+ private JTextField mName;
+ private JTextField mServer;
+ private JTextField mPort;
+ private JTextField mMountPoint;
+ private JTextField mUsername;
+ private JTextField mPassword;
+ private JTextField mDescription;
+ private JTextField mGenre;
+ private JCheckBox mPublic;
+ private JSlider mDelay;
+ private JLabel mDelayValue;
+ private JCheckBox mEnabled;
+ private JButton mSaveButton;
+ private JButton mResetButton;
+
+ public IcecastHTTPConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
+ SettingsManager settingsManager)
+ {
+ super(broadcastModel, aliasModel, settingsManager);
+ init();
+ }
+
+ private void init()
+ {
+ setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]",
+ "[][][][][][][][][][][][][][][grow,fill]" ) );
+ setPreferredSize(new Dimension(150,400));
+
+ JLabel channelLabel = new JLabel("Icecast 2 (v2.4+) Stream");
+
+ ImageIcon icon = mSettingsManager.getImageIcon(BroadcastServerType.ICECAST_HTTP.getIconName(), 25);
+ channelLabel.setIcon(icon);
+
+ add(channelLabel, "span, align center");
+
+ add(new JLabel("Name:"));
+ mName = new JTextField();
+ mName.getDocument().addDocumentListener(this);
+ mName.setToolTipText("Unique name for this stream");
+ add(mName);
+
+ add(new JLabel("Server:"));
+ mServer = new JTextField();
+ mServer.getDocument().addDocumentListener(this);
+ mServer.setToolTipText("Server address (e.g. audio1.icecast.com or localhost)");
+ add(mServer);
+
+ add(new JLabel("Port:"));
+ mPort = new JTextField();
+ mPort.getDocument().addDocumentListener(this);
+ mPort.setToolTipText("Server port number (e.g. 80)");
+ add(mPort);
+
+ add(new JLabel("Mount:"));
+ mMountPoint = new JTextField();
+ mMountPoint.getDocument().addDocumentListener(this);
+ mMountPoint.setToolTipText("Mount point (e.g. /12345)");
+ add(mMountPoint);
+
+ add(new JLabel("User Name:"));
+ mUsername = new JTextField();
+ mUsername.getDocument().addDocumentListener(this);
+ mUsername.setToolTipText("User name for the stream. Default: source");
+ add(mUsername);
+
+ add(new JLabel("Password:"));
+ mPassword = new JTextField();
+ mPassword.getDocument().addDocumentListener(this);
+ mPassword.setToolTipText("Password for the stream");
+ add(mPassword);
+
+ add(new JLabel("Description:"));
+ mDescription = new JTextField();
+ mDescription.getDocument().addDocumentListener(this);
+ mDescription.setToolTipText("Description of the stream contents");
+ add(mDescription);
+
+ add(new JLabel("Genre:"));
+ mGenre = new JTextField();
+ mGenre.getDocument().addDocumentListener(this);
+ mGenre.setToolTipText("Genre (e.g. public safety)");
+ add(mGenre);
+
+ add(new JLabel("Public:"));
+ mPublic = new JCheckBox();
+ mPublic.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ mPublic.setToolTipText("Designate stream as public (checked) or private");
+ add(mPublic);
+
+ add(new JLabel());
+ mDelayValue = new JLabel("0 Minutes");
+ add(mDelayValue);
+
+ add(new JLabel("Delay:"));
+ mDelay = new JSlider(0,60,0);
+ mDelay.setMajorTickSpacing(10);
+ mDelay.setMinorTickSpacing(5);
+ mDelay.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ mDelayValue.setText(mDelay.getValue() + " Minutes");
+ setModified(true);
+ }
+ });
+ mDelay.setToolTipText("Audio broadcast delay in minutes");
+ add(mDelay);
+
+ add(new JLabel("Enabled:"));
+ mEnabled = new JCheckBox();
+ mEnabled.setToolTipText("Enable (checked) or disable this stream");
+ mEnabled.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ add(mEnabled);
+
+ mSaveButton = new JButton("Save");
+ mSaveButton.setEnabled(false);
+ mSaveButton.setToolTipText("Click to save configuration information");
+ mSaveButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ save();
+ }
+ });
+ add(mSaveButton);
+
+ mResetButton = new JButton("Reset");
+ mResetButton.setEnabled(false);
+ mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
+ mResetButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(false);
+ setItem(getItem());
+ }
+ });
+ add(mResetButton);
+ }
+
+ @Override
+ public void setModified(boolean modified)
+ {
+ super.setModified(modified);
+
+ mSaveButton.setEnabled(modified);
+ mResetButton.setEnabled(modified);
+ }
+
+ @Override
+ public void setItem(BroadcastConfiguration item)
+ {
+ super.setItem(item);
+
+ if(hasItem())
+ {
+ IcecastHTTPConfiguration config = (IcecastHTTPConfiguration)getItem();
+
+ mName.setText(config.getName());
+ mServer.setText(config.getHost());
+ mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
+ mMountPoint.setText(config.getMountPoint());
+ mUsername.setText(config.getUserName());
+ mPassword.setText(config.getPassword());
+ mDescription.setText(config.getDescription());
+ mGenre.setText(config.getGenre());
+ mPublic.setSelected(config.isPublic());
+ mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
+ mEnabled.setSelected(config.isEnabled());
+ }
+ else
+ {
+ mName.setText(null);
+ mServer.setText(null);
+ mPort.setText(null);
+ mMountPoint.setText(null);
+ mUsername.setText(null);
+ mPassword.setText(null);
+ mDescription.setText(null);
+ mGenre.setText(null);
+ mPublic.setSelected(true);
+ mDelay.setValue(0);
+ mEnabled.setSelected(false);
+ }
+
+ setModified(false);
+ }
+
+ @Override
+ public void save()
+ {
+ IcecastHTTPConfiguration config = (IcecastHTTPConfiguration)getItem();
+
+ if(validateConfiguration())
+ {
+ updateConfigurationName(config, mName.getText());
+ config.setHost(mServer.getText());
+ config.setPort(getPort());
+ config.setMountPoint(mMountPoint.getText());
+ config.setUserName(mUsername.getText());
+ config.setPassword(mPassword.getText());
+ config.setDescription(mDescription.getText());
+ config.setGenre(mGenre.getText());
+ config.setPublic(mPublic.isSelected());
+ config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
+ config.setEnabled(mEnabled.isSelected());
+
+ setModified(false);
+
+ mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
+ }
+ }
+
+ /**
+ * Validates the user specified values in the controls prior to saving them to the configuration
+ */
+ private boolean validateConfiguration()
+ {
+ String name = mName.getText();
+
+ //A unique stream name is required
+ if(!mBroadcastModel.isUniqueName(name, getItem()))
+ {
+ String message = (name == null || name.isEmpty()) ?
+ "Please specify a stream name." : "Stream name " + name +
+ "is already in use.\nPlease choose another name.";
+
+ JOptionPane.showMessageDialog(IcecastHTTPConfigurationEditor.this,
+ message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
+
+ mName.requestFocus();
+
+ return false;
+ }
+
+ //Server address is optional, but required
+ if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
+ {
+ return true;
+ }
+
+ //Port is optional, but required
+ if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
+ {
+ return true;
+ }
+
+ //Mount Point is required
+ if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
+ {
+ return false;
+ }
+
+ //User name is required
+ if(!validateTextField(mUsername, "Invalid User Name", "Please specify a user name."))
+ {
+ return false;
+ }
+
+ //Password is required
+ if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
+ */
+ private int getPort()
+ {
+ String port = mPort.getText();
+
+ if(port != null && !port.isEmpty())
+ {
+ try
+ {
+ return Integer.parseInt(port);
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the number value
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastHeader.java b/src/audio/broadcast/icecast/IcecastHeader.java
new file mode 100644
index 00000000..b9cd07c7
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastHeader.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * 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.icecast;
+
+public enum IcecastHeader
+{
+ ACCEPT("Accept"),
+ AUDIO_INFO("ice-audio-info"),
+ AUTHORIZATION("Authorization"),
+ BITRATE("ice-bitrate"),
+ CONTENT_TYPE("Content-Type"),
+ DESCRIPTION("ice-description"),
+ EXPECT("Expect"),
+ GENRE("ice-genre"),
+ HOST("Host"),
+ NAME("ice-name"),
+ PUBLIC("ice-public"),
+ URL("ice-url"),
+ USER_AGENT("User-Agent"),
+
+ UNKNOWN("unknown");
+
+ private String mValue;
+
+ private IcecastHeader(String value)
+ {
+ mValue = value;
+ }
+
+ public String getValue()
+ {
+ return mValue;
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java b/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java
new file mode 100644
index 00000000..69d49f1c
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java
@@ -0,0 +1,296 @@
+/*******************************************************************************
+ * 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.icecast;
+
+import audio.broadcast.BroadcastState;
+import audio.broadcast.icecast.codec.IcecastCodecFactory;
+import audio.convert.MP3AudioConverter;
+import controller.ThreadPoolManager;
+import org.apache.mina.core.RuntimeIoException;
+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.codec.ProtocolCodecFilter;
+import org.apache.mina.transport.socket.nio.NioSocketConnector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import properties.SystemProperties;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
+{
+ private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPAudioBroadcaster.class );
+ private final static String TERMINATOR = "\r\n";
+ private final static String SEPARATOR = ":";
+ private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
+
+ private NioSocketConnector mSocketConnector;
+ private IoSession mStreamingSession = null;
+
+ private long mLastConnectionAttempt = 0;
+ private AtomicBoolean mConnecting = new AtomicBoolean();
+
+ /**
+ * Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
+ * compatible with Icecast version 2.3.2 and older versions of the server software.
+ *
+ * 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
+ * ShoutcastV2IOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
+ *
+ * @param configuration for the Icecast stream
+ */
+ public IcecastTCPAudioBroadcaster(ThreadPoolManager threadPoolManager, IcecastTCPConfiguration configuration)
+ {
+ super(threadPoolManager, configuration);
+ }
+
+ /**
+ * Broadcasts the audio frame or sequence
+ */
+ @Override
+ protected void broadcastAudio(byte[] audio)
+ {
+ if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
+ {
+ mStreamingSession.write(audio);
+ }
+ }
+
+ /**
+ * (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
+ * is currently connected to the remote server following any connection attempts.
+ *
+ * Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
+ *
+ * @return true if the audio handler can stream audio
+ */
+ private boolean connect()
+ {
+ if(!connected() && canConnect() &&
+ (mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
+ mConnecting.compareAndSet(false, true))
+ {
+ mLastConnectionAttempt = System.currentTimeMillis();
+
+ if(mSocketConnector == null)
+ {
+ mSocketConnector = new NioSocketConnector();
+ mSocketConnector.setConnectTimeoutCheckInterval(10000);
+
+// mSocketConnector.getFilterChain().addLast("logger",
+// new LoggingFilter(IcecastTCPAudioBroadcaster.class));
+
+ mSocketConnector.getFilterChain().addLast("codec",
+ new ProtocolCodecFilter(new IcecastCodecFactory()));
+ mSocketConnector.setHandler(new IcecastTCPIOHandler());
+ }
+
+ mStreamingSession = null;
+
+ Runnable runnable = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ setBroadcastState(BroadcastState.CONNECTING);
+
+ try
+ {
+ ConnectFuture future = mSocketConnector
+ .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
+ getBroadcastConfiguration().getPort()));
+ future.awaitUninterruptibly();
+ mStreamingSession = future.getSession();
+ }
+ catch(RuntimeIoException rie)
+ {
+ Throwable throwableCause = rie.getCause();
+
+ if(throwableCause instanceof ConnectException)
+ {
+ setBroadcastState(BroadcastState.NO_SERVER);
+ }
+ else if(throwableCause != null)
+ {
+ setBroadcastState(BroadcastState.ERROR);
+ mLog.debug("Failed to connect", rie);
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.ERROR);
+ mLog.debug("Failed to connect - no exception is available");
+ }
+
+ disconnect();
+ }
+ }
+ };
+
+ getThreadPoolManager().scheduleOnce(runnable, 0l, TimeUnit.SECONDS);
+
+ }
+
+ return connected();
+ }
+
+
+ /**
+ * Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
+ */
+ public void disconnect()
+ {
+ if(connected())
+ {
+ if(mStreamingSession != null)
+ {
+ mStreamingSession.closeNow();
+ }
+ }
+ }
+
+ /**
+ * IO Handler for managing Icecast TCP connection and credentials
+ */
+ public class IcecastTCPIOHandler extends IoHandlerAdapter
+ {
+ /**
+ * Sends stream configuration and user credentials upon connecting to remote server
+ */
+ @Override
+ public void sessionOpened(IoSession session) throws Exception
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("SOURCE ").append(getConfiguration().getMountPoint());
+ sb.append(" HTTP/1.0").append(TERMINATOR);
+
+ sb.append("Authorization: ").append(getConfiguration().getBase64EncodedCredentials()).append(TERMINATOR);
+ sb.append(IcecastHeader.USER_AGENT.getValue()).append(SEPARATOR)
+ .append(SystemProperties.getInstance().getApplicationName()).append(TERMINATOR);
+ sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
+ .append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
+ sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
+ .append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
+
+ sb.append(IcecastHeader.AUDIO_INFO.getValue()).append(SEPARATOR);
+ sb.append("samplerate=").append(getConfiguration().getSampleRate()).append(";");
+ sb.append("quality=").append(MP3AudioConverter.AUDIO_QUALITY).append(";");
+ sb.append("channels=").append(getConfiguration().getChannels()).append(TERMINATOR);
+
+ if(getConfiguration().hasName())
+ {
+ sb.append(IcecastHeader.NAME.getValue()).append(SEPARATOR).append(getConfiguration().getName()).append(TERMINATOR);
+ }
+
+ if(getConfiguration().hasGenre())
+ {
+ sb.append(IcecastHeader.GENRE.getValue()).append(SEPARATOR)
+ .append(getConfiguration().getGenre()).append(TERMINATOR);
+ }
+
+ if(getConfiguration().hasDescription())
+ {
+ sb.append(IcecastHeader.DESCRIPTION.getValue()).append(SEPARATOR)
+ .append(getConfiguration().getDescription()).append(TERMINATOR);
+ }
+
+ sb.append(TERMINATOR).append(TERMINATOR);
+
+ session.write(sb.toString());
+ }
+
+ @Override
+ public void sessionClosed(IoSession session) throws Exception
+ {
+ //If there is already an error state, don't override it. Otherwise, set state to disconnected
+ if(!getBroadcastState().isErrorState())
+ {
+ setBroadcastState(BroadcastState.DISCONNECTED);
+ }
+
+ mSocketConnector.dispose();
+ mStreamingSession = null;
+ mSocketConnector = null;
+
+ mConnecting.set(false);
+ super.sessionClosed(session);
+ }
+
+ @Override
+ public void exceptionCaught(IoSession session, Throwable cause) throws Exception
+ {
+ if(cause instanceof IOException && ((IOException)cause).getMessage().startsWith("Connection reset"))
+ {
+ mLog.info("Streaming connection reset by remote server - reestablishing connection");
+ disconnect();
+ connect();
+ }
+ else
+ {
+ mLog.error("Broadcast error", cause);
+ setBroadcastState(BroadcastState.ERROR);
+ disconnect();
+ }
+
+ mConnecting.set(false);
+ }
+
+ @Override
+ public void messageReceived(IoSession session, Object object) throws Exception
+ {
+ if(object instanceof String)
+ {
+ String message = (String)object;
+
+ if(message != null && !message.trim().isEmpty())
+ {
+ if(message.startsWith("HTTP/1.0 200 OK"))
+ {
+ setBroadcastState(BroadcastState.CONNECTED);
+ }
+ else if(message.startsWith("HTTP/1.0 403 Mountpoint in use"))
+ {
+ setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
+ }
+ else if(message.contains("Invalid Password") ||
+ message.contains("Authentication Required"))
+ {
+ setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
+ }
+ else
+ {
+ mLog.error("Unrecognized server response:" + message);
+ setBroadcastState(BroadcastState.ERROR);
+ }
+ }
+ }
+ else
+ {
+ mLog.error("Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
+ "] received:" + object.toString());
+ }
+ }
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastTCPConfiguration.java b/src/audio/broadcast/icecast/IcecastTCPConfiguration.java
new file mode 100644
index 00000000..413f0a32
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastTCPConfiguration.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * 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.icecast;
+
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastFormat;
+import audio.broadcast.BroadcastServerType;
+import audio.broadcast.broadcastify.BroadcastifyConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.xml.bind.annotation.XmlSeeAlso;
+
+@XmlSeeAlso( {BroadcastifyConfiguration.class } )
+public class IcecastTCPConfiguration extends IcecastConfiguration
+{
+ private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
+
+ public IcecastTCPConfiguration()
+ {
+ this(BroadcastFormat.MP3);
+ }
+
+ /**
+ * Icecast 2.3.x and 2.4.x compatible configuration
+ * @param format of audio
+ */
+ public IcecastTCPConfiguration(BroadcastFormat format)
+ {
+ super(format);
+ }
+
+ @Override
+ public BroadcastConfiguration copyOf()
+ {
+ IcecastTCPConfiguration copy = new IcecastTCPConfiguration(getBroadcastFormat());
+
+ //Broadcast Configuration Parameters
+ copy.setName(getName());
+ copy.setHost(getHost());
+ copy.setPort(getPort());
+ copy.setPassword(getPassword());
+ copy.setDelay(getDelay());
+ copy.setEnabled(false);
+
+ //Icecast Configuration Parameters
+ copy.setUserName(getUserName());
+ copy.setMountPoint(getMountPoint());
+ copy.setDescription(getDescription());
+ copy.setGenre(getGenre());
+ copy.setPublic(isPublic());
+ copy.setURL(getURL());
+
+ return copy;
+ }
+
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.ICECAST_TCP;
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastTCPConfigurationEditor.java b/src/audio/broadcast/icecast/IcecastTCPConfigurationEditor.java
new file mode 100644
index 00000000..34dc16f7
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastTCPConfigurationEditor.java
@@ -0,0 +1,337 @@
+/*******************************************************************************
+ * 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.icecast;
+
+import alias.AliasModel;
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastConfigurationEditor;
+import audio.broadcast.BroadcastEvent;
+import audio.broadcast.BroadcastModel;
+import audio.broadcast.BroadcastServerType;
+import audio.broadcast.broadcastify.BroadcastifyConfiguration;
+import gui.editor.DocumentListenerEditor;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import settings.SettingsManager;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+public class IcecastTCPConfigurationEditor extends BroadcastConfigurationEditor
+{
+ private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfigurationEditor.class );
+
+ private static final int ONE_MINUTE_MS = 60000;
+
+ private JTextField mName;
+ private JTextField mServer;
+ private JTextField mPort;
+ private JTextField mMountPoint;
+ private JTextField mPassword;
+ private JTextField mDescription;
+ private JTextField mGenre;
+ private JCheckBox mPublic;
+ private JSlider mDelay;
+ private JLabel mDelayValue;
+ private JCheckBox mEnabled;
+ private JButton mSaveButton;
+ private JButton mResetButton;
+
+ public IcecastTCPConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
+ SettingsManager settingsManager)
+ {
+ super(broadcastModel, aliasModel, settingsManager);
+ init();
+ }
+
+ private void init()
+ {
+ setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]",
+ "[][][][][][][][][][][][][][grow,fill]" ) );
+ setPreferredSize(new Dimension(150,400));
+
+ JLabel channelLabel = new JLabel("Icecast (v2.3) Stream");
+
+ ImageIcon icon = mSettingsManager.getImageIcon(BroadcastServerType.ICECAST_TCP.getIconName(), 25);
+ channelLabel.setIcon(icon);
+
+ add(channelLabel, "span, align center");
+
+ add(new JLabel("Name:"));
+ mName = new JTextField();
+ mName.getDocument().addDocumentListener(this);
+ mName.setToolTipText("Unique name for this stream");
+ add(mName);
+
+ add(new JLabel("Server:"));
+ mServer = new JTextField();
+ mServer.getDocument().addDocumentListener(this);
+ mServer.setToolTipText("Server address (e.g. audio1.icecast.com or localhost)");
+ add(mServer);
+
+ add(new JLabel("Port:"));
+ mPort = new JTextField();
+ mPort.getDocument().addDocumentListener(this);
+ mPort.setToolTipText("Server port number (e.g. 80)");
+ add(mPort);
+
+ add(new JLabel("Mount:"));
+ mMountPoint = new JTextField();
+ mMountPoint.getDocument().addDocumentListener(this);
+ mMountPoint.setToolTipText("Mount point (e.g. /12345)");
+ add(mMountPoint);
+
+ add(new JLabel("Password:"));
+ mPassword = new JTextField();
+ mPassword.getDocument().addDocumentListener(this);
+ mPassword.setToolTipText("Password for the stream");
+ add(mPassword);
+
+ add(new JLabel("Description:"));
+ mDescription = new JTextField();
+ mDescription.getDocument().addDocumentListener(this);
+ mDescription.setToolTipText("Description of the stream contents");
+ add(mDescription);
+
+ add(new JLabel("Genre:"));
+ mGenre = new JTextField();
+ mGenre.getDocument().addDocumentListener(this);
+ mGenre.setToolTipText("Genre (e.g. public safety)");
+ add(mGenre);
+
+ add(new JLabel("Public:"));
+ mPublic = new JCheckBox();
+ mPublic.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ mPublic.setToolTipText("Designate stream as public (checked) or private");
+ add(mPublic);
+
+ add(new JLabel());
+ mDelayValue = new JLabel("0 Minutes");
+ add(mDelayValue);
+
+ add(new JLabel("Delay:"));
+ mDelay = new JSlider(0,60,0);
+ mDelay.setMajorTickSpacing(10);
+ mDelay.setMinorTickSpacing(5);
+ mDelay.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ mDelayValue.setText(mDelay.getValue() + " Minutes");
+ setModified(true);
+ }
+ });
+ mDelay.setToolTipText("Audio broadcast delay in minutes");
+ add(mDelay);
+
+ add(new JLabel("Enabled:")); //Empty
+ mEnabled = new JCheckBox();
+ mEnabled.setToolTipText("Enable (checked) or disable this stream");
+ mEnabled.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ add(mEnabled);
+
+ mSaveButton = new JButton("Save");
+ mSaveButton.setEnabled(false);
+ mSaveButton.setToolTipText("Click to save configuration information");
+ mSaveButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ save();
+ }
+ });
+ add(mSaveButton);
+
+ mResetButton = new JButton("Reset");
+ mResetButton.setEnabled(false);
+ mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
+ mResetButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(false);
+ setItem(getItem());
+ }
+ });
+ add(mResetButton);
+ }
+
+ @Override
+ public void setModified(boolean modified)
+ {
+ super.setModified(modified);
+
+ mSaveButton.setEnabled(modified);
+ mResetButton.setEnabled(modified);
+ }
+
+ @Override
+ public void setItem(BroadcastConfiguration item)
+ {
+ super.setItem(item);
+
+ if(hasItem())
+ {
+ IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
+
+ mName.setText(config.getName());
+ mServer.setText(config.getHost());
+ mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
+ mMountPoint.setText(config.getMountPoint());
+ mPassword.setText(config.getPassword());
+ mDescription.setText(config.getDescription());
+ mGenre.setText(config.getGenre());
+ mPublic.setSelected(config.isPublic());
+ mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
+ mEnabled.setSelected(config.isEnabled());
+ }
+ else
+ {
+ mName.setText(null);
+ mServer.setText(null);
+ mPort.setText(null);
+ mMountPoint.setText(null);
+ mPassword.setText(null);
+ mDescription.setText(null);
+ mGenre.setText(null);
+ mPublic.setSelected(true);
+ mDelay.setValue(0);
+ mEnabled.setSelected(false);
+ }
+
+ setModified(false);
+ }
+
+ @Override
+ public void save()
+ {
+ IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
+
+ if(validateConfiguration())
+ {
+ updateConfigurationName(config, mName.getText());
+ config.setHost(mServer.getText());
+ config.setPort(getPort());
+ config.setMountPoint(mMountPoint.getText());
+ config.setPassword(mPassword.getText());
+ config.setDescription(mDescription.getText());
+ config.setGenre(mGenre.getText());
+ config.setPublic(mPublic.isSelected());
+ config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
+ config.setEnabled(mEnabled.isSelected());
+
+ setModified(false);
+
+ mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
+ }
+ }
+
+ /**
+ * Validates the user specified values in the controls prior to saving them to the configuration
+ */
+ private boolean validateConfiguration()
+ {
+ String name = mName.getText();
+
+ //A unique stream name is required
+ if(!mBroadcastModel.isUniqueName(name, getItem()))
+ {
+ String message = (name == null || name.isEmpty()) ?
+ "Please specify a stream name." : "Stream name " + name +
+ "is already in use.\nPlease choose another name.";
+
+ JOptionPane.showMessageDialog(IcecastTCPConfigurationEditor.this,
+ message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
+
+ mName.requestFocus();
+
+ return false;
+ }
+
+ //Server address is optional, but required
+ if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
+ {
+ return true;
+ }
+
+ //Port is optional, but required
+ if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
+ {
+ return true;
+ }
+
+ //Mount Point is optional, but required
+ if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
+ {
+ return true;
+ }
+
+ //Password is optional, but required
+ if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
+ {
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
+ */
+ private int getPort()
+ {
+ String port = mPort.getText();
+
+ if(port != null && !port.isEmpty())
+ {
+ try
+ {
+ return Integer.parseInt(port);
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the number value
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/audio/broadcast/icecast/codec/IcecastCodecFactory.java b/src/audio/broadcast/icecast/codec/IcecastCodecFactory.java
new file mode 100644
index 00000000..fef72ba8
--- /dev/null
+++ b/src/audio/broadcast/icecast/codec/IcecastCodecFactory.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * 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.icecast.codec;
+
+import org.apache.mina.core.session.IoSession;
+import org.apache.mina.filter.codec.ProtocolCodecFactory;
+import org.apache.mina.filter.codec.ProtocolDecoder;
+import org.apache.mina.filter.codec.ProtocolEncoder;
+import org.apache.mina.filter.codec.textline.LineDelimiter;
+import org.apache.mina.filter.codec.textline.TextLineDecoder;
+
+import java.nio.charset.StandardCharsets;
+
+public class IcecastCodecFactory implements ProtocolCodecFactory
+{
+ private ProtocolEncoder mProtocolEncoder;
+ private ProtocolDecoder mProtocolDecoder;
+
+ /**
+ * Factory for codecs to process Icecast string protocol and byte array audio frames for use with Apache Mina
+ */
+ public IcecastCodecFactory()
+ {
+ }
+
+ @Override
+ public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception
+ {
+ if(mProtocolEncoder == null)
+ {
+ mProtocolEncoder = new IcecastEncoder();
+ }
+
+ return mProtocolEncoder;
+ }
+
+ @Override
+ public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception
+ {
+ if(mProtocolDecoder == null)
+ {
+ mProtocolDecoder = new TextLineDecoder(StandardCharsets.UTF_8, LineDelimiter.AUTO);
+ }
+
+ return mProtocolDecoder;
+ }
+}
diff --git a/src/audio/broadcast/icecast/codec/IcecastEncoder.java b/src/audio/broadcast/icecast/codec/IcecastEncoder.java
new file mode 100644
index 00000000..565b478d
--- /dev/null
+++ b/src/audio/broadcast/icecast/codec/IcecastEncoder.java
@@ -0,0 +1,94 @@
+/*******************************************************************************
+ * 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.icecast.codec;
+
+import org.apache.mina.core.buffer.IoBuffer;
+import org.apache.mina.core.session.AttributeKey;
+import org.apache.mina.core.session.IoSession;
+import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
+import org.apache.mina.filter.codec.ProtocolEncoderOutput;
+import org.apache.mina.filter.codec.textline.LineDelimiter;
+import org.apache.mina.filter.codec.textline.TextLineEncoder;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.StandardCharsets;
+
+public class IcecastEncoder extends ProtocolEncoderAdapter
+{
+ private static final AttributeKey ENCODER = new AttributeKey(TextLineEncoder.class, "encoder");
+ private static final Charset mCharset = StandardCharsets.UTF_8;
+ private static final int MAX_LINE_LENGTH = 2147483647;
+
+ /**
+ * Icecast Protocol Encoder is an Apache Mina compliant protocol encoder that supports Icecast string-based
+ * connection negotiation as well as byte array audio frame data.
+ */
+ public IcecastEncoder()
+ {
+ }
+
+ @Override
+ public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception
+ {
+ //Audio frame data
+ if(message instanceof byte[])
+ {
+ byte[] data = (byte[])message;
+
+ IoBuffer buf = IoBuffer.allocate(data.length).setAutoExpand(false);
+ buf.put(data);
+ buf.flip();
+ out.write(buf);
+ }
+ //Icecast connection messages
+ else if(message instanceof String)
+ {
+ CharsetEncoder encoder = (CharsetEncoder)session.getAttribute(ENCODER);
+
+ if(encoder == null)
+ {
+ encoder = mCharset.newEncoder();
+ session.setAttribute(ENCODER, encoder);
+ }
+
+ String value = (String)message;
+
+ IoBuffer buf = IoBuffer.allocate(value.length()).setAutoExpand(true);
+
+ buf.putString(value, encoder);
+
+
+ if(buf.position() > MAX_LINE_LENGTH)
+ {
+ throw new IllegalArgumentException("Line length: " + buf.position());
+ }
+ else
+ {
+ buf.putString(LineDelimiter.UNIX.getValue(), encoder);
+ buf.flip();
+ out.write(buf);
+ }
+ }
+ else
+ {
+ throw new IllegalArgumentException("Can't encode - unrecognized object type: " + message.getClass());
+ }
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v1/ShoutcastMetadata.java b/src/audio/broadcast/shoutcast/v1/ShoutcastMetadata.java
new file mode 100644
index 00000000..b62eb032
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v1/ShoutcastMetadata.java
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v1;
+
+import audio.broadcast.BroadcastFormat;
+
+public enum ShoutcastMetadata
+{
+ 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 ShoutcastMetadata(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/broadcast/shoutcast/v1/ShoutcastV1AudioBroadcaster.java b/src/audio/broadcast/shoutcast/v1/ShoutcastV1AudioBroadcaster.java
new file mode 100644
index 00000000..9d376174
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v1/ShoutcastV1AudioBroadcaster.java
@@ -0,0 +1,292 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v1;
+
+import audio.broadcast.AudioBroadcaster;
+import audio.broadcast.BroadcastState;
+import audio.broadcast.IBroadcastMetadataUpdater;
+import controller.ThreadPoolManager;
+import org.apache.mina.core.RuntimeIoException;
+import org.apache.mina.core.buffer.IoBuffer;
+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.codec.ProtocolCodecFilter;
+import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
+import org.apache.mina.transport.socket.nio.NioSocketConnector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
+{
+ private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1AudioBroadcaster.class );
+ private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
+
+ private NioSocketConnector mSocketConnector;
+ private IoSession mStreamingSession = null;
+ private IBroadcastMetadataUpdater mMetadataUpdater;
+
+ private long mLastConnectionAttempt = 0;
+ private AtomicBoolean mConnecting = new AtomicBoolean();
+
+ /**
+ * Creates a Shoutcast v1 compatible broadcaster using TCP protocol.
+ *
+ * Note: use @see ShoutcastV2AudioBroadcaster for Shoutcast version 2.x and newer.
+ *
+ * This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
+ * ShoutcastV1IOHandler manages all interaction with the Shoutcast server and manages the overall broadcast state.
+ *
+ * @param configuration for the Shoutcast stream
+ */
+ public ShoutcastV1AudioBroadcaster(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration configuration)
+ {
+ super(threadPoolManager, configuration);
+ }
+
+ /**
+ * Shoutcast V1 broadcast configuration
+ */
+ private ShoutcastV1Configuration getConfiguration()
+ {
+ return (ShoutcastV1Configuration)getBroadcastConfiguration();
+ }
+
+ @Override
+ protected IBroadcastMetadataUpdater getMetadataUpdater()
+ {
+ if(mMetadataUpdater == null)
+ {
+ mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getThreadPoolManager(), getConfiguration());
+ }
+
+ return mMetadataUpdater;
+ }
+
+ /**
+ * Broadcasts the audio frame or sequence
+ */
+ @Override
+ protected void broadcastAudio(byte[] audio)
+ {
+ if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
+ {
+ IoBuffer buf = IoBuffer.allocate(audio.length).setAutoExpand(false);
+ buf.put(audio);
+ buf.flip();
+ mStreamingSession.write(buf);
+ }
+ }
+
+ /**
+ * (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
+ * is currently connected to the remote server following any connection attempts.
+ *
+ * Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
+ *
+ * @return true if the audio handler can stream audio
+ */
+ private boolean connect()
+ {
+ if(!connected() && canConnect() &&
+ (mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
+ mConnecting.compareAndSet(false, true))
+ {
+ mLastConnectionAttempt = System.currentTimeMillis();
+
+ if(mSocketConnector == null)
+ {
+ mSocketConnector = new NioSocketConnector();
+ mSocketConnector.setConnectTimeoutCheckInterval(10000);
+
+// mSocketConnector.getFilterChain().addLast("logger",
+// new LoggingFilter(ShoutcastV1AudioBroadcaster.class));
+
+ mSocketConnector.getFilterChain().addLast("codec",
+ new ProtocolCodecFilter(new TextLineCodecFactory()));
+
+ mSocketConnector.setHandler(new ShoutcastIOHandler());
+ }
+
+ mStreamingSession = null;
+
+ Runnable runnable = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ setBroadcastState(BroadcastState.CONNECTING);
+
+ try
+ {
+ ConnectFuture future = mSocketConnector
+ .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
+ getBroadcastConfiguration().getPort()));
+ future.awaitUninterruptibly();
+ mStreamingSession = future.getSession();
+ }
+ catch(RuntimeIoException rie)
+ {
+ Throwable throwableCause = rie.getCause();
+
+ if(throwableCause instanceof ConnectException)
+ {
+ setBroadcastState(BroadcastState.NO_SERVER);
+ }
+ else if(throwableCause != null)
+ {
+ setBroadcastState(BroadcastState.ERROR);
+ mLog.debug("Failed to connect", rie);
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.ERROR);
+ mLog.debug("Failed to connect - no exception is available");
+ }
+
+ disconnect();
+ }
+ }
+ };
+
+ getThreadPoolManager().scheduleOnce(runnable, 0l, TimeUnit.SECONDS);
+
+ }
+
+ return connected();
+ }
+
+
+ /**
+ * Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
+ */
+ public void disconnect()
+ {
+ if(connected())
+ {
+ if(mStreamingSession != null)
+ {
+ mStreamingSession.closeNow();
+ }
+ }
+ }
+
+ /**
+ * IO Handler for managing Icecast TCP connection and credentials
+ */
+ public class ShoutcastIOHandler extends IoHandlerAdapter
+ {
+ /**
+ * Sends stream configuration and user credentials upon connecting to remote server
+ */
+ @Override
+ public void sessionOpened(IoSession session) throws Exception
+ {
+ StringBuilder sb = new StringBuilder();
+
+ //Password
+ sb.append(getConfiguration().getPassword()).append(ShoutcastMetadata.COMMAND_TERMINATOR);
+
+ //Metadata
+ sb.append(ShoutcastMetadata.STREAM_NAME.encode(getConfiguration().getName()));
+ sb.append(ShoutcastMetadata.PUBLIC.encode(getConfiguration().isPublic()));
+ sb.append(ShoutcastMetadata.GENRE.encode(getConfiguration().getGenre()));
+ sb.append(ShoutcastMetadata.DESCRIPTION.encode(getConfiguration().getDescription()));
+ sb.append(ShoutcastMetadata.AUDIO_BIT_RATE.encode(getConfiguration().getBitRate()));
+
+ //End of connection string
+ sb.append(ShoutcastMetadata.COMMAND_TERMINATOR);
+
+ session.write(sb.toString());
+ }
+
+ @Override
+ public void sessionClosed(IoSession session) throws Exception
+ {
+ //If there is already an error state, don't override it. Otherwise, set state to disconnected
+ if(!getBroadcastState().isErrorState())
+ {
+ setBroadcastState(BroadcastState.DISCONNECTED);
+ }
+
+ mSocketConnector.dispose();
+ mStreamingSession = null;
+ mSocketConnector = null;
+
+ mConnecting.set(false);
+ super.sessionClosed(session);
+ }
+
+ @Override
+ public void exceptionCaught(IoSession session, Throwable cause) throws Exception
+ {
+ if(cause instanceof IOException && ((IOException)cause).getMessage().startsWith("Connection reset"))
+ {
+ mLog.info("Streaming connection reset by remote server - reestablishing connection");
+ disconnect();
+ connect();
+ }
+ else
+ {
+ mLog.error("Broadcast error", cause);
+ setBroadcastState(BroadcastState.ERROR);
+ disconnect();
+ }
+
+ mConnecting.set(false);
+ }
+
+ @Override
+ public void messageReceived(IoSession session, Object object) throws Exception
+ {
+ if(object instanceof String)
+ {
+ String message = (String)object;
+
+ if(message != null && !message.trim().isEmpty())
+ {
+ if(message.startsWith("OK"))
+ {
+ setBroadcastState(BroadcastState.CONNECTED);
+ }
+ else if(message.startsWith("icy-caps:"))
+ {
+ //TODO: what does icy-caps:11 tell us?
+ }
+ else
+ {
+ mLog.error("Unrecognized server response:" + message);
+ setBroadcastState(BroadcastState.ERROR);
+ }
+ }
+ }
+ else
+ {
+ mLog.error("Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
+ "] received:" + object.toString());
+ }
+ }
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java b/src/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java
new file mode 100644
index 00000000..9d80b6a4
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java
@@ -0,0 +1,267 @@
+/*******************************************************************************
+ * 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.broadcast.shoutcast.v1;
+
+import audio.broadcast.IBroadcastMetadataUpdater;
+import audio.metadata.AudioMetadata;
+import audio.metadata.Metadata;
+import audio.metadata.MetadataType;
+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.http.HttpClientCodec;
+import org.apache.mina.http.HttpRequestImpl;
+import org.apache.mina.http.api.HttpMethod;
+import org.apache.mina.http.api.HttpRequest;
+import org.apache.mina.http.api.HttpVersion;
+import org.apache.mina.transport.socket.nio.NioSocketConnector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.UnsupportedEncodingException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
+{
+ private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
+ private final static String UTF8 = "UTF-8";
+
+ private ThreadPoolManager mThreadPoolManager;
+ private ShoutcastV1Configuration mShoutcastV1Configuration;
+
+ private NioSocketConnector mSocketConnector;
+ private AtomicBoolean mUpdating = new AtomicBoolean();
+ private Queue mMetadataQueue = new LinkedTransferQueue<>();
+ private Map mHTTPHeaders;
+ private boolean mStackTraceLoggingSuppressed;
+
+ /**
+ * Shoutcast song metadata updater. Each metadata update is processed in the order received and the dispatch of
+ * the HTTP update request is processed in a separate thread (runnable) to avoid delaying streaming of this
+ * 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 ShoutcastV1BroadcastMetadataUpdater(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration shoutcastV1Configuration)
+ {
+ mThreadPoolManager = threadPoolManager;
+ mShoutcastV1Configuration = shoutcastV1Configuration;
+ }
+
+ /**
+ * Socket connector - lazy constructor.
+ */
+ private NioSocketConnector getSocketConnector()
+ {
+ if (mSocketConnector == null)
+ {
+ mSocketConnector = new NioSocketConnector();
+
+// mSocketConnector.getFilterChain().addLast("logger",
+// new LoggingFilter(ShoutcastV1BroadcastMetadataUpdater.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
+ mSocketConnector.setHandler(new IoHandlerAdapter()
+ {
+ @Override
+ public void exceptionCaught(IoSession session, Throwable cause) throws Exception
+ {
+ //Single-use session - close it after we receive an error
+ session.closeNow();
+ }
+
+ @Override
+ public void messageReceived(IoSession session, Object message) throws Exception
+ {
+ //Single-use session - close it after we receive a response
+ session.closeNow();
+ }
+ });
+ }
+
+ return mSocketConnector;
+ }
+
+ /**
+ * Sends a song metadata update to the remote server. Additional metadata updates received while the updater
+ * is in the process of sending an existing metadata update will be queued and processed in received order.
+ *
+ * Note: due to thread timing, there is a slight chance that a concurrent update request will not be processed
+ * and will remain in the update queue until the next metadata update is requested. However, this is a design
+ * trade-off to avoid having a scheduled runnable repeatedly processing the update queue.
+ */
+ public void update(AudioMetadata audioMetadata)
+ {
+ mMetadataQueue.offer(getSong(audioMetadata));
+
+ if (mUpdating.compareAndSet(false, true))
+ {
+ String song = mMetadataQueue.poll();
+
+ while (song != null)
+ {
+ HttpRequest updateRequest = createUpdateRequest(song);
+
+ if (updateRequest != null)
+ {
+ mThreadPoolManager.scheduleOnce(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ ConnectFuture connectFuture = getSocketConnector()
+ .connect(new InetSocketAddress(mShoutcastV1Configuration.getHost(),
+ mShoutcastV1Configuration.getPort()));
+ connectFuture.awaitUninterruptibly();
+ IoSession session = connectFuture.getSession();
+
+ if (session != null)
+ {
+ session.write(updateRequest);
+ }
+ }
+ catch (Exception e)
+ {
+ Throwable throwableCause = e.getCause();
+
+ if (throwableCause instanceof ConnectException)
+ {
+ //Do nothing, the server is unavailable
+ }
+ else
+ {
+ if (!mStackTraceLoggingSuppressed)
+ {
+ mLog.error("Error sending metadata update. Future errors will " +
+ "be suppressed", e);
+
+ mStackTraceLoggingSuppressed = true;
+ }
+ }
+ }
+ }
+ }, 0l, TimeUnit.SECONDS);
+ }
+
+ //Fetch next metadata update to send
+ song = mMetadataQueue.poll();
+ }
+
+ mUpdating.set(false);
+ }
+ }
+
+ /**
+ * Creates the song information for a metadata update
+ */
+ private static String getSong(AudioMetadata audioMetadata)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ if(audioMetadata != null)
+ {
+ Metadata to = audioMetadata.getMetadata(MetadataType.TO);
+
+ sb.append("TO:");
+
+ if(to != null)
+ {
+ if(to.hasAlias())
+ {
+ sb.append(to.getAlias().getName());
+ }
+ else
+ {
+ sb.append(to.getValue());
+ }
+ }
+ else
+ {
+ sb.append("UNKNOWN");
+ }
+
+ Metadata from = audioMetadata.getMetadata(MetadataType.FROM);
+
+ sb.append(" FROM:");
+
+ if(from != null)
+ {
+
+ if(from.hasAlias())
+ {
+ sb.append(from.getAlias().getName());
+ }
+ else
+ {
+ sb.append(from.getValue());
+ }
+ }
+ else
+ {
+ sb.append("UNKNOWN");
+ }
+ }
+ else
+ {
+ sb.append("Scanning ....");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Creates an HTTP GET request to update the metadata on the remote server
+ */
+ private HttpRequest createUpdateRequest(String song)
+ {
+ try
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("pass=").append(mShoutcastV1Configuration.getPassword());
+ sb.append("&mode=updinfo");
+ sb.append("&song=").append(URLEncoder.encode(song, UTF8));
+
+ Map headers = new HashMap<>();
+
+ HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_0, HttpMethod.GET, "/admin.cgi",
+ sb.toString(), headers);
+
+ return request;
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ //This should never happen
+ mLog.error("UTF-8 encoding is not supported - can't update song metadata");
+ }
+
+ return null;
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v1/ShoutcastV1Configuration.java b/src/audio/broadcast/shoutcast/v1/ShoutcastV1Configuration.java
new file mode 100644
index 00000000..ee1133fa
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v1/ShoutcastV1Configuration.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v1;
+
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastFormat;
+import audio.broadcast.BroadcastServerType;
+
+import javax.xml.bind.annotation.XmlAttribute;
+
+public class ShoutcastV1Configuration extends BroadcastConfiguration
+{
+ private String mGenre;
+ private String mDescription;
+ private boolean mPublic;
+ private int mChannels = 1;
+ private int mBitRate = 16;
+
+ public ShoutcastV1Configuration()
+ {
+ //No-arg JAXB constructor
+ }
+
+ public ShoutcastV1Configuration(BroadcastFormat format)
+ {
+ super(format);
+ }
+
+ @Override
+ public BroadcastConfiguration copyOf()
+ {
+ ShoutcastV1Configuration copy = new ShoutcastV1Configuration(getBroadcastFormat());
+
+ //Broadcast Configuration Parameters
+ copy.setName(getName());
+ copy.setHost(getHost());
+ copy.setPort(getPort());
+ copy.setPassword(getPassword());
+ copy.setDelay(getDelay());
+ copy.setEnabled(false);
+
+ //Icecast Configuration Parameters
+ copy.setGenre(getGenre());
+ copy.setPublic(isPublic());
+ copy.setChannels(getChannels());
+ copy.setBitRate(getBitRate());
+
+ return copy;
+ }
+
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.SHOUTCAST_V1;
+ }
+
+ /**
+ * Stream genre
+ */
+ @XmlAttribute(name="genre")
+ public String getGenre()
+ {
+ return mGenre;
+ }
+
+ /**
+ * Stream genre
+ * @param genre
+ */
+ public void setGenre(String genre)
+ {
+ mGenre = genre;
+ }
+
+ /**
+ * Stream description
+ */
+ @XmlAttribute(name="description")
+ public String getDescription()
+ {
+ return mDescription;
+ }
+
+ /**
+ * Stream description
+ * @param description
+ */
+ public void setDescription(String description)
+ {
+ mDescription = description;
+ }
+
+ /**
+ * Public visibility of the broadcastAudio
+ */
+ @XmlAttribute(name="public")
+ public boolean isPublic()
+ {
+ return mPublic;
+ }
+
+ /**
+ * Sets public visibility of the broadcastAudio
+ * @param isPublic indicates if the broadcastAudio should be visible to the public
+ */
+ public void setPublic(boolean isPublic)
+ {
+ mPublic = isPublic;
+ }
+
+ /**
+ * Number of audio channels in the broadcastAudio
+ */
+ @XmlAttribute(name="channels")
+ public int getChannels()
+ {
+ return mChannels;
+ }
+
+ /**
+ * Sets the number of audio channels in the broadcastAudio
+ */
+ public void setChannels(int channels)
+ {
+ mChannels = channels;
+ }
+
+ /**
+ * Bit rate in bits per second
+ */
+ @XmlAttribute(name="bitrate")
+ public int getBitRate()
+ {
+ return mBitRate;
+ }
+
+ /**
+ * Bit rate
+ * @param bitRate in samples per second
+ */
+ public void setBitRate(int bitRate)
+ {
+ mBitRate = bitRate;
+ }
+
+ @Override
+ public boolean isValid()
+ {
+ return getName() != null && !getName().isEmpty() &&
+ getHost() != null && !getHost().isEmpty() &&
+ getPort() > 0 && getPort() < 65535 &&
+ getPassword() != null && !getPassword().isEmpty();
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v1/ShoutcastV1ConfigurationEditor.java b/src/audio/broadcast/shoutcast/v1/ShoutcastV1ConfigurationEditor.java
new file mode 100644
index 00000000..a6f5565a
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v1/ShoutcastV1ConfigurationEditor.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 audio.broadcast.shoutcast.v1;
+
+import alias.AliasModel;
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastConfigurationEditor;
+import audio.broadcast.BroadcastEvent;
+import audio.broadcast.BroadcastModel;
+import audio.broadcast.BroadcastServerType;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import settings.SettingsManager;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+public class ShoutcastV1ConfigurationEditor extends BroadcastConfigurationEditor
+{
+ private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1ConfigurationEditor.class );
+
+ private static final int ONE_MINUTE_MS = 60000;
+
+ private JTextField mName;
+ private JTextField mServer;
+ private JTextField mPort;
+ private JTextField mPassword;
+ private JTextField mGenre;
+ private JTextField mDescription;
+ private JCheckBox mPublic;
+ private JSlider mDelay;
+ private JLabel mDelayValue;
+ private JCheckBox mEnabled;
+ private JButton mSaveButton;
+ private JButton mResetButton;
+
+ public ShoutcastV1ConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
+ SettingsManager settingsManager)
+ {
+ super(broadcastModel, aliasModel, settingsManager);
+ init();
+ }
+
+ private void init()
+ {
+ setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]",
+ "[][][][][][][][][][][][][][grow,fill]" ) );
+ setPreferredSize(new Dimension(150,400));
+
+ JLabel channelLabel = new JLabel("Shoutcast (v1.x) Stream");
+
+ ImageIcon icon = mSettingsManager.getImageIcon(BroadcastServerType.SHOUTCAST_V1.getIconName(), 25);
+ channelLabel.setIcon(icon);
+
+ add(channelLabel, "span, align center");
+
+ add(new JLabel("Name:"));
+ mName = new JTextField();
+ mName.getDocument().addDocumentListener(this);
+ mName.setToolTipText("Unique name for this stream");
+ add(mName);
+
+ add(new JLabel("Server:"));
+ mServer = new JTextField();
+ mServer.getDocument().addDocumentListener(this);
+ mServer.setToolTipText("Server address (e.g. audio1.shoutcast.com or localhost)");
+ add(mServer);
+
+ add(new JLabel("Port:"));
+ mPort = new JTextField();
+ mPort.getDocument().addDocumentListener(this);
+ mPort.setToolTipText("Server port number (e.g. 80)");
+ add(mPort);
+
+ add(new JLabel("Password:"));
+ mPassword = new JTextField();
+ mPassword.getDocument().addDocumentListener(this);
+ mPassword.setToolTipText("Password for the stream");
+ add(mPassword);
+
+ add(new JLabel("Genre:"));
+ mGenre = new JTextField();
+ mGenre.getDocument().addDocumentListener(this);
+ mGenre.setToolTipText("Genre (e.g. public safety)");
+ add(mGenre);
+
+ add(new JLabel("Description:"));
+ mDescription = new JTextField();
+ mDescription.getDocument().addDocumentListener(this);
+ mDescription.setToolTipText("Description of the stream");
+ add(mDescription);
+
+ add(new JLabel("Public:"));
+ mPublic = new JCheckBox();
+ mPublic.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ mPublic.setToolTipText("Designate stream as public (checked) or private");
+ add(mPublic);
+
+ add(new JLabel());
+ mDelayValue = new JLabel("0 Minutes");
+ add(mDelayValue);
+
+ add(new JLabel("Delay:"));
+ mDelay = new JSlider(0,60,0);
+ mDelay.setMajorTickSpacing(10);
+ mDelay.setMinorTickSpacing(5);
+ mDelay.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ mDelayValue.setText(mDelay.getValue() + " Minutes");
+ setModified(true);
+ }
+ });
+ mDelay.setToolTipText("Audio broadcast delay in minutes");
+ add(mDelay);
+
+ add(new JLabel("Enabled:")); //Empty
+ mEnabled = new JCheckBox();
+ mEnabled.setToolTipText("Enable (checked) or disable this stream");
+ mEnabled.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ add(mEnabled);
+
+ mSaveButton = new JButton("Save");
+ mSaveButton.setEnabled(false);
+ mSaveButton.setToolTipText("Click to save configuration information");
+ mSaveButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ save();
+ }
+ });
+ add(mSaveButton);
+
+ mResetButton = new JButton("Reset");
+ mResetButton.setEnabled(false);
+ mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
+ mResetButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(false);
+ setItem(getItem());
+ }
+ });
+ add(mResetButton);
+ }
+
+ @Override
+ public void setModified(boolean modified)
+ {
+ super.setModified(modified);
+
+ mSaveButton.setEnabled(modified);
+ mResetButton.setEnabled(modified);
+ }
+
+ @Override
+ public void setItem(BroadcastConfiguration item)
+ {
+ super.setItem(item);
+
+ if(hasItem())
+ {
+ ShoutcastV1Configuration config = (ShoutcastV1Configuration)getItem();
+
+ mName.setText(config.getName());
+ mServer.setText(config.getHost());
+ mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
+ mPassword.setText(config.getPassword());
+ mGenre.setText(config.getGenre());
+ mDescription.setText(config.getDescription());
+ mPublic.setSelected(config.isPublic());
+ mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
+ mEnabled.setSelected(config.isEnabled());
+ }
+ else
+ {
+ mName.setText(null);
+ mServer.setText(null);
+ mPort.setText(null);
+ mPassword.setText(null);
+ mGenre.setText(null);
+ mDescription.setText(null);
+ mPublic.setSelected(true);
+ mDelay.setValue(0);
+ mEnabled.setSelected(false);
+ }
+
+ setModified(false);
+ }
+
+ @Override
+ public void save()
+ {
+ ShoutcastV1Configuration config = (ShoutcastV1Configuration)getItem();
+
+ if(validateConfiguration())
+ {
+ updateConfigurationName(config, mName.getText());
+ config.setHost(mServer.getText());
+ config.setPort(getPort());
+ config.setPassword(mPassword.getText());
+ config.setDescription(mDescription.getText());
+ config.setGenre(mGenre.getText());
+ config.setPublic(mPublic.isSelected());
+ config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
+ config.setEnabled(mEnabled.isSelected());
+
+ setModified(false);
+
+ mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
+ }
+ }
+
+ /**
+ * Validates the user specified values in the controls prior to saving them to the configuration
+ */
+ private boolean validateConfiguration()
+ {
+ String name = mName.getText();
+
+ //A unique stream name is required
+ if(!mBroadcastModel.isUniqueName(name, getItem()))
+ {
+ String message = (name == null || name.isEmpty()) ?
+ "Please specify a stream name." : "Stream name " + name +
+ "is already in use.\nPlease choose another name.";
+
+ JOptionPane.showMessageDialog(ShoutcastV1ConfigurationEditor.this,
+ message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
+
+ mName.requestFocus();
+
+ return false;
+ }
+
+ //Server address is optional, but required
+ if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
+ {
+ return true;
+ }
+
+ //Port is optional, but required
+ if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
+ {
+ return true;
+ }
+
+ //Password is optional, but required
+ if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
+ {
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
+ */
+ private int getPort()
+ {
+ String port = mPort.getText();
+
+ if(port != null && !port.isEmpty())
+ {
+ try
+ {
+ return Integer.parseInt(port);
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the number value
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ShoutcastV2AudioBroadcaster.java b/src/audio/broadcast/shoutcast/v2/ShoutcastV2AudioBroadcaster.java
new file mode 100644
index 00000000..57a80596
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ShoutcastV2AudioBroadcaster.java
@@ -0,0 +1,528 @@
+/*******************************************************************************
+ * 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.broadcast.shoutcast.v2;
+
+import audio.broadcast.AudioBroadcaster;
+import audio.broadcast.BroadcastState;
+import audio.broadcast.IBroadcastMetadataUpdater;
+import audio.broadcast.shoutcast.v2.ultravox.AuthenticateBroadcast;
+import audio.broadcast.shoutcast.v2.ultravox.CacheableXMLMetadata;
+import audio.broadcast.shoutcast.v2.ultravox.ConfigureIcyName;
+import audio.broadcast.shoutcast.v2.ultravox.ConfigureIcyPublic;
+import audio.broadcast.shoutcast.v2.ultravox.MP3Audio;
+import audio.broadcast.shoutcast.v2.ultravox.NegotiateMaxPayloadSize;
+import audio.broadcast.shoutcast.v2.ultravox.RequestCipher;
+import audio.broadcast.shoutcast.v2.ultravox.SetupBroadcast;
+import audio.broadcast.shoutcast.v2.ultravox.Standby;
+import audio.broadcast.shoutcast.v2.ultravox.StreamMimeType;
+import audio.broadcast.shoutcast.v2.ultravox.TerminateBroadcast;
+import audio.broadcast.shoutcast.v2.ultravox.UltravoxMessage;
+import audio.broadcast.shoutcast.v2.ultravox.UltravoxMessageFactory;
+import audio.broadcast.shoutcast.v2.ultravox.UltravoxMessageType;
+import audio.broadcast.shoutcast.v2.ultravox.UltravoxMetadata;
+import audio.broadcast.shoutcast.v2.ultravox.UltravoxProtocolFactory;
+import audio.metadata.AudioMetadata;
+import audio.metadata.Metadata;
+import audio.metadata.MetadataType;
+import controller.ThreadPoolManager;
+import org.apache.mina.core.RuntimeIoException;
+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.codec.ProtocolCodecFilter;
+import org.apache.mina.transport.socket.nio.NioSocketConnector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import properties.SystemProperties;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBroadcastMetadataUpdater
+{
+ private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2AudioBroadcaster.class );
+ private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
+ private int mMaxPayloadSize = 16377;
+
+ private NioSocketConnector mSocketConnector;
+ private IoSession mStreamingSession = null;
+
+ private long mLastConnectionAttempt = 0;
+ private AtomicBoolean mConnecting = new AtomicBoolean();
+ private LinkedTransferQueue mMetadataMessageQueue = new LinkedTransferQueue<>();
+
+
+ /**
+ * Shoutcast 2.x (Ultravox 2.1) broadcaster. This broadcaster is compatible with Shoutcast 2.x and newer versions
+ * of the server software.
+ *
+ * Note: use @see ShoutcastV1AudioBroadcaster for Shoutcast version 1.x and older.
+ *
+ * This broadcaster uses the Apache Mina library for the streaming socket connection. The
+ * ShoutcastV2IOHandler manages all interaction with the Shoutcast server and manages the overall broadcast state.
+ *
+ * @param threadPoolManager for thread pool access
+ * @param configuration for the Shoutcast V2 stream
+ */
+ public ShoutcastV2AudioBroadcaster(ThreadPoolManager threadPoolManager, ShoutcastV2Configuration configuration)
+ {
+ super(threadPoolManager, configuration);
+ }
+
+ public ShoutcastV2Configuration getConfiguration()
+ {
+ return (ShoutcastV2Configuration)getBroadcastConfiguration();
+ }
+
+ /**
+ * Broadcasts the audio frame or sequence
+ */
+ @Override
+ protected void broadcastAudio(byte[] audio)
+ {
+ //Dispatch any queued metadata messages
+ UltravoxMessage metadataMessage = mMetadataMessageQueue.poll();
+
+ while(metadataMessage != null)
+ {
+ mStreamingSession.write(metadataMessage);
+ metadataMessage = mMetadataMessageQueue.poll();
+ }
+
+ //Dispatch audio message
+ if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
+ {
+ MP3Audio mp3Audio = new MP3Audio();
+ mp3Audio.setPayload(audio);
+
+ mStreamingSession.write(mp3Audio);
+ }
+ }
+
+ @Override
+ public void update(AudioMetadata metadata)
+ {
+ List metadataMessages = getMetadataMessages(metadata);
+
+ if(!metadataMessages.isEmpty())
+ {
+ mMetadataMessageQueue.addAll(metadataMessages);
+ }
+ }
+
+ @Override
+ protected IBroadcastMetadataUpdater getMetadataUpdater()
+ {
+ return this;
+ }
+
+ /**
+ * (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
+ * is currently connected to the remote server following any connection attempts.
+ *
+ * Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
+ *
+ * @return true if the audio handler can stream audio
+ */
+ private boolean connect()
+ {
+ if(!connected() && canConnect() &&
+ (mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
+ mConnecting.compareAndSet(false, true))
+ {
+ mLastConnectionAttempt = System.currentTimeMillis();
+
+ if(mSocketConnector == null)
+ {
+ mSocketConnector = new NioSocketConnector();
+ mSocketConnector.setConnectTimeoutCheckInterval(10000);
+
+// mSocketConnector.getFilterChain().addLast("logger",
+// new LoggingFilter(ShoutcastV2AudioBroadcaster.class));
+
+ mSocketConnector.getFilterChain().addLast("codec",
+ new ProtocolCodecFilter(new UltravoxProtocolFactory()));
+
+ mSocketConnector.setHandler(new ShoutcastV2IOHandler());
+ }
+
+ mStreamingSession = null;
+
+ Runnable runnable = new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ setBroadcastState(BroadcastState.CONNECTING);
+
+ try
+ {
+ ConnectFuture future = mSocketConnector
+ .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
+ getBroadcastConfiguration().getPort()));
+ future.awaitUninterruptibly();
+ mStreamingSession = future.getSession();
+ }
+ catch(RuntimeIoException rie)
+ {
+ Throwable throwableCause = rie.getCause();
+
+ if(throwableCause instanceof ConnectException)
+ {
+ setBroadcastState(BroadcastState.NO_SERVER);
+ }
+ else if(throwableCause != null)
+ {
+ setBroadcastState(BroadcastState.ERROR);
+ mLog.error("Failed to connect", rie);
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.ERROR);
+ mLog.error("Failed to connect - no exception is available");
+ }
+
+ disconnect();
+ }
+ }
+ };
+
+ getThreadPoolManager().scheduleOnce(runnable, 0l, TimeUnit.SECONDS);
+
+ }
+
+ return connected();
+ }
+
+
+ /**
+ * Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
+ */
+ public void disconnect()
+ {
+ if(connected())
+ {
+ if(mStreamingSession != null)
+ {
+ mStreamingSession.write(new TerminateBroadcast());
+ mStreamingSession.closeNow();
+ }
+ }
+ }
+
+ /**
+ * Encodes the list of metadata in one or more Ultravox CacheableXMLMetadata messages according to the maximum negotiated
+ * payload size for the current connection. Each entry in the list of metadata strings should be an xml encoded
+ * value: value
+ *
+ * See UltravoxMetadata.TAG.asXML(String value)
+ *
+ * @param metadata containing audio metadata tags and attributes
+ * @return a sequence of CacheableXMLMetadata messages sufficient to carry the complete set of metadata
+ */
+ private List getMetadataMessages(AudioMetadata metadata)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("");
+
+ sb.append(UltravoxMetadata.ALBUM_TITLE.asXML(getConfiguration().getName()));
+
+ sb.append(UltravoxMetadata.BROADCAST_CLIENT_APPLICATION.asXML(SystemProperties.getInstance().getApplicationName()));
+
+ if(getConfiguration().hasGenre())
+ {
+ sb.append(UltravoxMetadata.GENRE.asXML(getConfiguration().getGenre()));
+ }
+
+ if(getConfiguration().hasURL())
+ {
+ sb.append(UltravoxMetadata.URL.asXML(getConfiguration().getURL()));
+ }
+
+ if(metadata != null)
+ {
+ Metadata to = metadata.getMetadata(MetadataType.TO);
+ if(to != null)
+ {
+ if(to.hasAlias())
+ {
+ sb.append(UltravoxMetadata.TITLE_2.asXML(to.getAlias().toString()));
+ }
+ else
+ {
+ sb.append(UltravoxMetadata.TITLE_2.asXML(to.getValue()));
+ }
+ }
+
+ Metadata from = metadata.getMetadata(MetadataType.FROM);
+ if(from != null)
+ {
+ if(from.hasAlias())
+ {
+ sb.append(UltravoxMetadata.TITLE_3.asXML("From: " + from.getAlias().toString()));
+ }
+ else
+ {
+ sb.append(UltravoxMetadata.TITLE_3.asXML("From: " + from.getValue()));
+ }
+ }
+ }
+ else
+ {
+ sb.append(UltravoxMetadata.TITLE_2.asXML("Scanning ..."));
+ }
+
+ sb.append("");
+
+ byte[] xml = sb.toString().getBytes();
+
+ int pointer = 0;
+ int messageCounter = 1;
+ int messageCount = (int)Math.ceil((double)xml.length / (double)(mMaxPayloadSize - 6));
+
+ if(messageCount > 32)
+ {
+ messageCount = 32; //Max number of metadata messages in a sequence
+ }
+
+
+ List messages = new ArrayList<>();
+
+ while(pointer < xml.length && messageCounter <= messageCount)
+ {
+ int payloadSize = Math.min(mMaxPayloadSize - 6, xml.length - pointer);
+
+ byte[] payload = new byte[payloadSize + 6];
+
+ payload[1] = (byte)(0x01);
+ payload[3] = (byte)(messageCount & 0xFF);
+ payload[5] = (byte)(messageCounter & 0xFF);
+
+ System.arraycopy(xml, pointer, payload, 6, payloadSize);
+
+ CacheableXMLMetadata message = new CacheableXMLMetadata();
+ message.setPayload(payload);
+ messages.add(message);
+
+ pointer += payloadSize;
+ }
+
+ return messages;
+ }
+
+
+ /**
+ * IO Handler for managing Shoutcast V2 connection and credentials
+ */
+ public class ShoutcastV2IOHandler extends IoHandlerAdapter
+ {
+ /**
+ * Sends stream configuration and user credentials upon connecting to remote server
+ */
+ @Override
+ public void sessionOpened(IoSession session) throws Exception
+ {
+ session.write(UltravoxMessageFactory.getMessage(UltravoxMessageType.REQUEST_CIPHER));
+ }
+
+ @Override
+ public void sessionClosed(IoSession session) throws Exception
+ {
+ //If there is already an error state, don't override it. Otherwise, set state to disconnected
+ if(!getBroadcastState().isErrorState())
+ {
+ setBroadcastState(BroadcastState.DISCONNECTED);
+ }
+
+ mSocketConnector.dispose();
+ mStreamingSession = null;
+ mSocketConnector = null;
+
+ mConnecting.set(false);
+ super.sessionClosed(session);
+ }
+
+ @Override
+ public void exceptionCaught(IoSession session, Throwable cause) throws Exception
+ {
+ if(cause instanceof IOException && ((IOException)cause).getMessage().startsWith("Connection reset"))
+ {
+ mLog.info("Streaming connection reset by remote server - reestablishing connection");
+ disconnect();
+ connect();
+ }
+ else
+ {
+ mLog.error("Broadcast error", cause);
+ setBroadcastState(BroadcastState.ERROR);
+ disconnect();
+ }
+
+ mConnecting.set(false);
+ }
+
+ @Override
+ public void messageReceived(IoSession session, Object object) throws Exception
+ {
+ if(object instanceof UltravoxMessage)
+ {
+ UltravoxMessage ultravoxMessage = (UltravoxMessage)object;
+
+ switch (ultravoxMessage.getMessageType())
+ {
+ case REQUEST_CIPHER:
+ String cipherKey = ((RequestCipher)ultravoxMessage).getCipher();
+
+ AuthenticateBroadcast authenticateBroadcast = new AuthenticateBroadcast();
+
+ authenticateBroadcast.setCredentials(cipherKey, getConfiguration().getStreamID(),
+ getConfiguration().getUserID(), getConfiguration().getPassword());
+
+ session.write(authenticateBroadcast);
+ break;
+ case AUTHENTICATE_BROADCAST:
+ if(ultravoxMessage.isErrorResponse())
+ {
+ String errorMessage = ultravoxMessage.getErrorMessage();
+
+ if(errorMessage.startsWith(AuthenticateBroadcast.STREAM_ID_ERROR))
+ {
+ setBroadcastState(BroadcastState.INVALID_MOUNT_POINT);
+ }
+ else
+ {
+ mLog.error("Invalid Credentials - response: " + ultravoxMessage.getPayload());
+ setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
+ }
+ }
+ else
+ {
+ StreamMimeType streamMimeType = new StreamMimeType();
+ streamMimeType.setFormat(getConfiguration().getBroadcastFormat());
+ session.write(streamMimeType);
+ }
+ break;
+ case STREAM_MIME_TYPE:
+ if(ultravoxMessage.isErrorResponse())
+ {
+ mLog.error("Unsupported Audio Format:" + getConfiguration().getBroadcastFormat().toString() +
+ " - " + ultravoxMessage.getErrorMessage());
+ setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
+ }
+ else
+ {
+ SetupBroadcast setupBroadcast = new SetupBroadcast();
+
+ //Use the same value for average and minimum bit rates
+ setupBroadcast.setBitRate(getConfiguration().getBitRate(), getConfiguration().getBitRate());
+
+ session.write(setupBroadcast);
+ }
+ break;
+ case SETUP_BROADCAST:
+ if(ultravoxMessage.isErrorResponse())
+ {
+ mLog.error("Unsupported Audio Bit Rate:" + getConfiguration().getBitRate() +
+ " - " + ultravoxMessage.getErrorMessage());
+ setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
+ }
+ else
+ {
+ NegotiateMaxPayloadSize negotiateMaxPayloadSize = new NegotiateMaxPayloadSize();
+
+ negotiateMaxPayloadSize.setMaximumPayloadSize(16377, 4192);
+
+ session.write(negotiateMaxPayloadSize);
+ }
+ break;
+ case NEGOTIATE_MAX_PAYLOAD_SIZE:
+ if(ultravoxMessage.isErrorResponse())
+ {
+ mLog.error("Unsupported maximum payload size (18 min - 36 kbps max) - " +
+ ultravoxMessage.getErrorMessage());
+ setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
+ }
+ else
+ {
+ mMaxPayloadSize = ((NegotiateMaxPayloadSize)ultravoxMessage).getMaximumPayloadSize();
+
+ ConfigureIcyPublic configureIcyPublic = new ConfigureIcyPublic();
+ configureIcyPublic.setPublic(getConfiguration().isPublic());
+ session.write(configureIcyPublic);
+ }
+ break;
+ case CONFIGURE_ICY_PUBLIC:
+ if(ultravoxMessage.isErrorResponse())
+ {
+ mLog.error("Error setting shoutcast stream as public - " + ultravoxMessage.getErrorMessage());
+ setBroadcastState(BroadcastState.ERROR);
+ }
+ else
+ {
+ ConfigureIcyName configureIcyName = new ConfigureIcyName();
+ configureIcyName.setName(getConfiguration().getName());
+ session.write(configureIcyName);
+ }
+ break;
+ case CONFIGURE_ICY_NAME:
+ if(ultravoxMessage.isErrorResponse())
+ {
+ mLog.error("Error setting shoutcast stream name - " + ultravoxMessage.getErrorMessage());
+ setBroadcastState(BroadcastState.CONFIGURATION_ERROR);
+ }
+ else
+ {
+ session.write(new Standby());
+ }
+ break;
+ case STANDBY:
+ if(ultravoxMessage.isErrorResponse())
+ {
+ mLog.error("Error following stream configuration and standby - " +
+ ultravoxMessage.getErrorMessage());
+ setBroadcastState(BroadcastState.ERROR);
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.CONNECTED);
+ }
+ break;
+ default:
+ mLog.error("Unrecognized ultravox message:" + ultravoxMessage.getMessageType() +
+ " payload:" + ultravoxMessage.getPayload());
+
+ setBroadcastState(BroadcastState.ERROR);
+ }
+ }
+ else
+ {
+ mLog.debug("Unrecognized message received from shoutcast v2 server:" + object.toString());
+
+ setBroadcastState(BroadcastState.ERROR);
+ }
+ }
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java b/src/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.java
new file mode 100644
index 00000000..c7fafe6a
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ShoutcastV2Configuration.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.shoutcast.v2;
+
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastFormat;
+import audio.broadcast.BroadcastServerType;
+
+import javax.xml.bind.annotation.XmlAttribute;
+
+public class ShoutcastV2Configuration extends BroadcastConfiguration
+{
+ private int mStreamID;
+ private String mUserID;
+ private int mBitRate = 16;
+ private String mGenre;
+ private String mURL;
+ private boolean mPublic = true;
+
+ public ShoutcastV2Configuration()
+ {
+ //No-arg JAXB constructor
+ }
+
+ public ShoutcastV2Configuration(BroadcastFormat format)
+ {
+ super(format);
+ }
+
+ @Override
+ public BroadcastConfiguration copyOf()
+ {
+ ShoutcastV2Configuration copy = new ShoutcastV2Configuration(getBroadcastFormat());
+
+ //Broadcast Configuration Parameters
+ copy.setName(getName());
+ copy.setHost(getHost());
+ copy.setPort(getPort());
+ copy.setPassword(getPassword());
+ copy.setDelay(getDelay());
+ copy.setEnabled(false);
+
+ //Shoutcast V2 Configuration Parameters
+ copy.setStreamID(getStreamID());
+ copy.setUserID(getUserID());
+ copy.setBitRate(getBitRate());
+ copy.setGenre(getGenre());
+ copy.setURL(getURL());
+ copy.setPublic(isPublic());
+
+ return copy;
+ }
+
+
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.SHOUTCAST_V2;
+ }
+
+ /**
+ * Stream ID
+ */
+ @XmlAttribute(name = "stream_id")
+ public int getStreamID()
+ {
+ return mStreamID;
+ }
+
+ /**
+ * Sets the stream ID
+ *
+ * @param streamID numeric stream identifier between 1 - 2147483647
+ */
+ public void setStreamID(int streamID)
+ {
+ assert(1 <= streamID && streamID <= Integer.MAX_VALUE);
+ mStreamID = streamID;
+ }
+
+ /**
+ * User ID
+ */
+ @XmlAttribute(name = "user_id")
+ public String getUserID()
+ {
+ return mUserID;
+ }
+
+ /**
+ * Sets the user ID
+ * @param userID
+ */
+ public void setUserID(String userID)
+ {
+ mUserID = userID;
+ }
+
+ /**
+ * Bit rate in kilobits per second
+ */
+ @XmlAttribute(name = "bitrate")
+ public int getBitRate()
+ {
+ return mBitRate;
+ }
+
+ /**
+ * Bit rate
+ * @param bitRate in kilobits per second
+ */
+ public void setBitRate(int bitRate)
+ {
+ mBitRate = bitRate;
+ }
+
+ /**
+ * Stream genre
+ */
+ @XmlAttribute(name = "genre")
+ public String getGenre()
+ {
+ return mGenre;
+ }
+
+ /**
+ * Sets the genre tag for this stream
+ */
+ public void setGenre(String genre)
+ {
+ mGenre = genre;
+ }
+
+
+ public boolean hasGenre()
+ {
+ return mGenre != null && !mGenre.isEmpty();
+ }
+
+ /**
+ * Public visibility of the broadcastAudio
+ */
+ @XmlAttribute(name = "public")
+ public boolean isPublic()
+ {
+ return mPublic;
+ }
+
+ /**
+ * Sets public visibility of the broadcastAudio
+ * @param isPublic indicates if the broadcastAudio should be visible to the public
+ */
+ public void setPublic(boolean isPublic)
+ {
+ mPublic = isPublic;
+ }
+
+ /**
+ * URL associated with the broadcastAudio where users can find additional details.
+ */
+ @XmlAttribute(name = "url")
+ public String getURL()
+ {
+ return mURL;
+ }
+
+ /**
+ * URL associated with the broadcastAudio where users can find additional details.
+ * @param url
+ */
+ public void setURL(String url)
+ {
+ mURL = url;
+ }
+
+ public boolean hasURL()
+ {
+ return mURL != null && !mURL.isEmpty();
+ }
+
+ @Override
+ public boolean isValid()
+ {
+ return super.isValid() && hasName() &&
+ getUserID() != null && !getUserID().isEmpty() &&
+ 1 <= getStreamID() &&
+ getPassword() != null && !getPassword().isEmpty();
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ShoutcastV2ConfigurationEditor.java b/src/audio/broadcast/shoutcast/v2/ShoutcastV2ConfigurationEditor.java
new file mode 100644
index 00000000..5371d17d
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ShoutcastV2ConfigurationEditor.java
@@ -0,0 +1,363 @@
+/*******************************************************************************
+ * 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.broadcast.shoutcast.v2;
+
+import alias.AliasModel;
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastConfigurationEditor;
+import audio.broadcast.BroadcastEvent;
+import audio.broadcast.BroadcastModel;
+import audio.broadcast.BroadcastServerType;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import settings.SettingsManager;
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+public class ShoutcastV2ConfigurationEditor extends BroadcastConfigurationEditor
+{
+ private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2ConfigurationEditor.class );
+
+ private static final int ONE_MINUTE_MS = 60000;
+
+ private JTextField mName;
+ private JTextField mServer;
+ private JTextField mPort;
+ private JTextField mStreamID;
+ private JTextField mUserID;
+ private JTextField mPassword;
+ private JTextField mGenre;
+ private JCheckBox mPublic;
+ private JSlider mDelay;
+ private JLabel mDelayValue;
+ private JCheckBox mEnabled;
+ private JButton mSaveButton;
+ private JButton mResetButton;
+
+ public ShoutcastV2ConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
+ SettingsManager settingsManager)
+ {
+ super(broadcastModel, aliasModel, settingsManager);
+ init();
+ }
+
+ private void init()
+ {
+ setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]",
+ "[][][][][][][][][][][][][][grow,fill]" ) );
+ setPreferredSize(new Dimension(150,400));
+
+ JLabel channelLabel = new JLabel("Shoutcast (v2) Stream");
+
+ ImageIcon icon = mSettingsManager.getImageIcon(BroadcastServerType.SHOUTCAST_V2.getIconName(), 25);
+ channelLabel.setIcon(icon);
+
+ add(channelLabel, "span, align center");
+
+ add(new JLabel("Name:"));
+ mName = new JTextField();
+ mName.getDocument().addDocumentListener(this);
+ mName.setToolTipText("Unique name for this stream");
+ add(mName);
+
+ add(new JLabel("Server:"));
+ mServer = new JTextField();
+ mServer.getDocument().addDocumentListener(this);
+ mServer.setToolTipText("Server address (e.g. audio1.shoutcast.com or localhost)");
+ add(mServer);
+
+ add(new JLabel("Port:"));
+ mPort = new JTextField();
+ mPort.getDocument().addDocumentListener(this);
+ mPort.setToolTipText("Server port number (e.g. 80)");
+ add(mPort);
+
+ add(new JLabel("Stream ID:"));
+ mStreamID = new JTextField();
+ mStreamID.getDocument().addDocumentListener(this);
+ mStreamID.setToolTipText("Stream Identifier)");
+ add(mStreamID);
+
+ add(new JLabel("User ID:"));
+ mUserID = new JTextField();
+ mUserID.getDocument().addDocumentListener(this);
+ mUserID.setToolTipText("User ID/Name");
+ add(mUserID);
+
+ add(new JLabel("Password:"));
+ mPassword = new JTextField();
+ mPassword.getDocument().addDocumentListener(this);
+ mPassword.setToolTipText("Password for the stream");
+ add(mPassword);
+
+ add(new JLabel("Genre:"));
+ mGenre = new JTextField();
+ mGenre.getDocument().addDocumentListener(this);
+ mGenre.setToolTipText("Genre (e.g. public safety)");
+ add(mGenre);
+
+ add(new JLabel("Public:"));
+ mPublic = new JCheckBox();
+ mPublic.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ mPublic.setToolTipText("Designate stream as public (checked) or private");
+ add(mPublic);
+
+ add(new JLabel());
+ mDelayValue = new JLabel("0 Minutes");
+ add(mDelayValue);
+
+ add(new JLabel("Delay:"));
+ mDelay = new JSlider(0,60,0);
+ mDelay.setMajorTickSpacing(10);
+ mDelay.setMinorTickSpacing(5);
+ mDelay.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent e)
+ {
+ mDelayValue.setText(mDelay.getValue() + " Minutes");
+ setModified(true);
+ }
+ });
+ mDelay.setToolTipText("Audio broadcast delay in minutes");
+ add(mDelay);
+
+ add(new JLabel("Enabled:")); //Empty
+ mEnabled = new JCheckBox();
+ mEnabled.setToolTipText("Enable (checked) or disable this stream");
+ mEnabled.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(true);
+ }
+ });
+ add(mEnabled);
+
+ mSaveButton = new JButton("Save");
+ mSaveButton.setEnabled(false);
+ mSaveButton.setToolTipText("Click to save configuration information");
+ mSaveButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ save();
+ }
+ });
+ add(mSaveButton);
+
+ mResetButton = new JButton("Reset");
+ mResetButton.setEnabled(false);
+ mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
+ mResetButton.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ setModified(false);
+ setItem(getItem());
+ }
+ });
+ add(mResetButton);
+ }
+
+ @Override
+ public void setModified(boolean modified)
+ {
+ super.setModified(modified);
+
+ mSaveButton.setEnabled(modified);
+ mResetButton.setEnabled(modified);
+ }
+
+ @Override
+ public void setItem(BroadcastConfiguration item)
+ {
+ super.setItem(item);
+
+ if(hasItem())
+ {
+ ShoutcastV2Configuration config = (ShoutcastV2Configuration)getItem();
+
+ mName.setText(config.getName());
+ mServer.setText(config.getHost());
+ mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
+ mStreamID.setText(String.valueOf(config.getStreamID()));
+ mUserID.setText(config.getUserID());
+ mPassword.setText(config.getPassword());
+ mGenre.setText(config.getGenre());
+ mPublic.setSelected(config.isPublic());
+ mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
+ mEnabled.setSelected(config.isEnabled());
+ }
+ else
+ {
+ mName.setText(null);
+ mServer.setText(null);
+ mPort.setText(null);
+ mStreamID.setText(null);
+ mUserID.setText(null);
+ mPassword.setText(null);
+ mGenre.setText(null);
+ mPublic.setSelected(true);
+ mDelay.setValue(0);
+ mEnabled.setSelected(false);
+ }
+
+ setModified(false);
+ }
+
+ @Override
+ public void save()
+ {
+ ShoutcastV2Configuration config = (ShoutcastV2Configuration)getItem();
+
+ if(validateConfiguration())
+ {
+ updateConfigurationName(config, mName.getText());
+ config.setHost(mServer.getText());
+ config.setPort(getPort());
+ config.setUserID(mUserID.getText());
+ config.setPassword(mPassword.getText());
+ config.setStreamID(getStreamID());
+ config.setGenre(mGenre.getText());
+ config.setPublic(mPublic.isSelected());
+ config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
+ config.setEnabled(mEnabled.isSelected());
+
+ setModified(false);
+
+ mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
+ }
+ }
+
+ /**
+ * Validates the user specified values in the controls prior to saving them to the configuration
+ */
+ private boolean validateConfiguration()
+ {
+ String name = mName.getText();
+
+ //A unique stream name is required
+ if(!mBroadcastModel.isUniqueName(name, getItem()))
+ {
+ String message = (name == null || name.isEmpty()) ?
+ "Please specify a stream name." : "Stream name " + name +
+ "is already in use.\nPlease choose another name.";
+
+ JOptionPane.showMessageDialog(ShoutcastV2ConfigurationEditor.this,
+ message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
+
+ mName.requestFocus();
+
+ return false;
+ }
+
+ //Server address is required
+ if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
+ {
+ return true;
+ }
+
+ //Port is required
+ if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
+ {
+ return true;
+ }
+
+ //Stream ID is required
+ if(!validateIntegerTextField(mStreamID, "Invalid Stream ID", "Please specify a stream ID of 1 or higher", 1, Integer.MAX_VALUE))
+ {
+ return true;
+ }
+
+ //User ID is required
+ if(!validateTextField(mUserID, "Invalid User ID", "Please specify a user ID/name."))
+ {
+ return true;
+ }
+
+ //Password is required
+ if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
+ {
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
+ */
+ private int getPort()
+ {
+ String port = mPort.getText();
+
+ if(port != null && !port.isEmpty())
+ {
+ try
+ {
+ return Integer.parseInt(port);
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the number value
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
+ */
+ private int getStreamID()
+ {
+ String streamID = mStreamID.getText();
+
+ if(streamID != null && !streamID.isEmpty())
+ {
+ try
+ {
+ return Integer.parseInt(streamID);
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the number value
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/AuthenticateBroadcast.java b/src/audio/broadcast/shoutcast/v2/ultravox/AuthenticateBroadcast.java
new file mode 100644
index 00000000..46463992
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/AuthenticateBroadcast.java
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+import util.XTEA;
+
+import java.util.Arrays;
+
+public class AuthenticateBroadcast extends UltravoxMessage
+{
+ public static final String STREAM_ID_ERROR = "Stream ID Error";
+
+ /**
+ * Client request to server to send authentication credentials
+ */
+ public AuthenticateBroadcast()
+ {
+ super(UltravoxMessageType.AUTHENTICATE_BROADCAST);
+ }
+
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ AuthenticateBroadcast(byte[] data)
+ {
+ super(data);
+ }
+
+ /**
+ * Set credential to authenticate the stream with the server
+ *
+ * @param encryptionKey received from the server in the RequestCipher message
+ * @param streamID that we're going to stream to
+ * @param userID for the stream. Note: this is optional and can be null. The server currently ignores this value.
+ * @param password for the stream.
+ */
+ public void setCredentials(String encryptionKey, int streamID, String userID, String password)
+ {
+ XTEA xtea = new XTEA(encryptionKey);
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(ULTRAVOX_VERSION);
+ sb.append(":");
+ sb.append(streamID);
+ sb.append(":");
+ sb.append(userID != null ? encrypt(xtea, userID) : "");
+ sb.append(":");
+ sb.append(password != null ? encrypt(xtea, password) : "");
+
+ setPayload(sb.toString());
+ }
+
+ /**
+ * Encrypts the value using the XTEA block cipher algorithm and returns the encrypted value as a 16 hex value
+ *
+ * @param xtea instance of the XTEA algorithm with a loaded encryption key
+ * @param value to encode
+ * @return encrypted password as 64-bit, 16 character hexadecimal value
+ */
+ private static String encrypt(XTEA xtea, String value)
+ {
+ byte[] valueBytes = value.getBytes();
+
+ if(valueBytes.length % 8 != 0 || valueBytes.length == 0)
+ {
+ int newLength = ((valueBytes.length / 8) + 1) * 8;
+ valueBytes = Arrays.copyOf(valueBytes, newLength);
+ }
+
+ byte[] encrypted = xtea.encrypt(valueBytes);
+
+ StringBuilder sb = new StringBuilder();
+
+ for(byte b: encrypted)
+ {
+ sb.append(String.format("%02x", b));
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/CacheableXMLMetadata.java b/src/audio/broadcast/shoutcast/v2/ultravox/CacheableXMLMetadata.java
new file mode 100644
index 00000000..06c35560
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/CacheableXMLMetadata.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class CacheableXMLMetadata extends UltravoxMessage
+{
+ /**
+ * Client request to server to send cacheable XML metadata
+ */
+ public CacheableXMLMetadata()
+ {
+ super(UltravoxMessageType.CACHEABLE_XML_METADATA);
+ }
+
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ CacheableXMLMetadata(byte[] data)
+ {
+ super(data);
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyGenre.java b/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyGenre.java
new file mode 100644
index 00000000..917b0a67
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyGenre.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class ConfigureIcyGenre extends UltravoxMessage
+{
+ /**
+ * Client request to server to configure the ICY-GENRE metadata
+ */
+ public ConfigureIcyGenre()
+ {
+ super(UltravoxMessageType.CONFIGURE_ICY_GENRE);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ ConfigureIcyGenre(byte[] data)
+ {
+ super(data);
+ }
+
+ public void setGenre(String genre)
+ {
+ setPayload(genre);
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyName.java b/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyName.java
new file mode 100644
index 00000000..8ee981af
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyName.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class ConfigureIcyName extends UltravoxMessage
+{
+ /**
+ * Client request to server to configure the ICY-NAME metadata
+ */
+ public ConfigureIcyName()
+ {
+ super(UltravoxMessageType.CONFIGURE_ICY_NAME);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ ConfigureIcyName(byte[] data)
+ {
+ super(data);
+ }
+
+ public void setName(String name)
+ {
+ setPayload(name);
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyPublic.java b/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyPublic.java
new file mode 100644
index 00000000..d86f0e65
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyPublic.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class ConfigureIcyPublic extends UltravoxMessage
+{
+ /**
+ * Client request to server to configure the ICY-PUBLIC metadata
+ */
+ public ConfigureIcyPublic()
+ {
+ super(UltravoxMessageType.CONFIGURE_ICY_PUBLIC);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ ConfigureIcyPublic(byte[] data)
+ {
+ super(data);
+ }
+
+ public void setPublic(boolean publicStream)
+ {
+ setPayload(publicStream ? "1" : "0");
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyURL.java b/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyURL.java
new file mode 100644
index 00000000..b05d44f5
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/ConfigureIcyURL.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class ConfigureIcyURL extends UltravoxMessage
+{
+ /**
+ * Client request to server to configure the ICY-URL metadata
+ */
+ public ConfigureIcyURL()
+ {
+ super(UltravoxMessageType.CONFIGURE_ICY_URL);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ ConfigureIcyURL(byte[] data)
+ {
+ super(data);
+ }
+
+ public void setURL(String url)
+ {
+ setPayload(url);
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/FlushCachedMetadata.java b/src/audio/broadcast/shoutcast/v2/ultravox/FlushCachedMetadata.java
new file mode 100644
index 00000000..245279e5
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/FlushCachedMetadata.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class FlushCachedMetadata extends UltravoxMessage
+{
+ /**
+ * Client request to server to flush any currently cached stream metadata
+ */
+ public FlushCachedMetadata()
+ {
+ super(UltravoxMessageType.FLUSH_CACHED_METADATA);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ FlushCachedMetadata(byte[] data)
+ {
+ super(data);
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/MP3Audio.java b/src/audio/broadcast/shoutcast/v2/ultravox/MP3Audio.java
new file mode 100644
index 00000000..f59d5ce9
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/MP3Audio.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class MP3Audio extends UltravoxMessage
+{
+ /**
+ * Client request to server to send authentication credentials
+ *
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ */
+ public MP3Audio()
+ {
+ super(UltravoxMessageType.MP3_DATA);
+ }
+
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ MP3Audio(byte[] data)
+ {
+ super(data);
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/NegotiateBufferSize.java b/src/audio/broadcast/shoutcast/v2/ultravox/NegotiateBufferSize.java
new file mode 100644
index 00000000..05e53544
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/NegotiateBufferSize.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.shoutcast.v2.ultravox;
+
+public class NegotiateBufferSize extends UltravoxMessage
+{
+ /**
+ * Client request to server to negotiate the server's buffer size
+ */
+ public NegotiateBufferSize()
+ {
+ super(UltravoxMessageType.NEGOTIATE_BUFFER_SIZE);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ NegotiateBufferSize(byte[] data)
+ {
+ super(data);
+ }
+
+ /**
+ * Sets the desired and minimum requested buffer sizes to send to the server
+ * @param desired buffer size in kilobytes
+ * @param minimum buffer size in kilobytes
+ */
+ public void setBufferSize(int desired, int minimum)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(desired).append(":").append(minimum);
+
+ setPayload(sb.toString());
+ }
+
+ /**
+ * Negotiated buffer size from a server response ultravox.
+ *
+ * @return server negotiated buffer size, or zero if this is not a server response ultravox, or this is an error
+ * server response ultravox, or the server response ultravox buffer size cannot be determined from the ultravox payload.
+ */
+ public int getBufferSize()
+ {
+ String payload = getPayload();
+
+ if(payload != null && payload.startsWith("ACK:"))
+ {
+ try
+ {
+ int bufferSize = Integer.parseInt(payload.substring(4));
+ return bufferSize;
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the value
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/NegotiateMaxPayloadSize.java b/src/audio/broadcast/shoutcast/v2/ultravox/NegotiateMaxPayloadSize.java
new file mode 100644
index 00000000..11321319
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/NegotiateMaxPayloadSize.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class NegotiateMaxPayloadSize extends UltravoxMessage
+{
+ /**
+ * Client request to server to negotiate maximum payload size
+ */
+ public NegotiateMaxPayloadSize()
+ {
+ super(UltravoxMessageType.NEGOTIATE_MAX_PAYLOAD_SIZE);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ NegotiateMaxPayloadSize(byte[] data)
+ {
+ super(data);
+ }
+
+ /**
+ * Sets the maximum payload size in bytes
+ *
+ * @param desiredMaximum payload size (no larger than 16377)
+ * @param minimum acceptable payload size (no larger than 16377)
+ */
+ public void setMaximumPayloadSize(int desiredMaximum, int minimum)
+ {
+ assert(desiredMaximum <= 16377);
+ assert(minimum <= 16377);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(desiredMaximum).append(":").append(minimum);
+
+ setPayload(sb.toString());
+ }
+
+ /**
+ * Negotiated maximum payload size from a server response ultravox.
+ *
+ * @return server negotiated maximum payload size, or zero if this is not a server response ultravox, or this is an
+ * error server response ultravox, or the server response ultravox maximum payload size cannot be determined from the
+ * ultravox payload.
+ */
+ public int getMaximumPayloadSize()
+ {
+ String payload = getPayload();
+
+ if(payload != null && payload.startsWith("ACK:"))
+ {
+ try
+ {
+ int maximumPayloadSize = Integer.parseInt(payload.substring(4));
+ return maximumPayloadSize;
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the value
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/PassThroughXMLMetadata.java b/src/audio/broadcast/shoutcast/v2/ultravox/PassThroughXMLMetadata.java
new file mode 100644
index 00000000..9f33f82f
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/PassThroughXMLMetadata.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * 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.broadcast.shoutcast.v2.ultravox;
+
+public class PassThroughXMLMetadata extends UltravoxMessage
+{
+ /**
+ * Client request to server to send pass-through XML metadata
+ */
+ public PassThroughXMLMetadata()
+ {
+ super(UltravoxMessageType.PASS_THROUGH_XML_METADATA);
+ }
+
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ PassThroughXMLMetadata(byte[] data)
+ {
+ super(data);
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/RequestCipher.java b/src/audio/broadcast/shoutcast/v2/ultravox/RequestCipher.java
new file mode 100644
index 00000000..4bd2aa11
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/RequestCipher.java
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class RequestCipher extends UltravoxMessage
+{
+ /**
+ * Client request to server for encryption key
+ */
+ public RequestCipher()
+ {
+ super(UltravoxMessageType.REQUEST_CIPHER);
+ setPayload(ULTRAVOX_VERSION);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ RequestCipher(byte[] data)
+ {
+ super(data);
+ }
+
+ /**
+ * Cipher key returned from the server to use for encrypting user id and password credentials
+ *
+ * @return cipher key or null
+ */
+ public String getCipher()
+ {
+ if(isValidResponse())
+ {
+ return getPayload().replace(VALID_RESPONSE_PAYLOAD_PREFIX, "");
+ }
+
+ return null;
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/SetupBroadcast.java b/src/audio/broadcast/shoutcast/v2/ultravox/SetupBroadcast.java
new file mode 100644
index 00000000..59a5fe0c
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/SetupBroadcast.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class SetupBroadcast extends UltravoxMessage
+{
+ /**
+ * Client request to server to specify the stream bit rate
+ */
+ public SetupBroadcast()
+ {
+ super(UltravoxMessageType.SETUP_BROADCAST);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ SetupBroadcast(byte[] data)
+ {
+ super(data);
+ }
+
+ /**
+ * Sets the average and maximum bit rates. Set both to the same value for a Constant Bit Rate (CBR) stream.
+ *
+ * @param average bit rate of the stream in kbps
+ * @param maximum bit rate of the stream in kbps
+ */
+ public void setBitRate(int average, int maximum)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(average).append(":").append(maximum);
+
+ setPayload(sb.toString());
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/Standby.java b/src/audio/broadcast/shoutcast/v2/ultravox/Standby.java
new file mode 100644
index 00000000..b571f976
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/Standby.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class Standby extends UltravoxMessage
+{
+ /**
+ * Client request to server to notify that configuration is complete, prepare for streaming
+ */
+ public Standby()
+ {
+ super(UltravoxMessageType.STANDBY);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ Standby(byte[] data)
+ {
+ super(data);
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/StreamMimeType.java b/src/audio/broadcast/shoutcast/v2/ultravox/StreamMimeType.java
new file mode 100644
index 00000000..86a65aaf
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/StreamMimeType.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+import audio.broadcast.BroadcastFormat;
+
+public class StreamMimeType extends UltravoxMessage
+{
+ /**
+ * Client request to server to set stream MIME type
+ */
+ public StreamMimeType()
+ {
+ super(UltravoxMessageType.STREAM_MIME_TYPE);
+ }
+
+ /**
+ * Server response to client request
+ *
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ StreamMimeType(byte[] data)
+ {
+ super(data);
+ }
+
+ /**
+ * Sets the broadcastAudio format or MIME type
+ * @param format of the stream
+ */
+ public void setFormat(BroadcastFormat format)
+ {
+ setPayload(format.getValue());
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/TerminateBroadcast.java b/src/audio/broadcast/shoutcast/v2/ultravox/TerminateBroadcast.java
new file mode 100644
index 00000000..9a5adccd
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/TerminateBroadcast.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class TerminateBroadcast extends UltravoxMessage
+{
+ /**
+ * Client request to server to terminate the broadcastAudio
+ */
+ public TerminateBroadcast()
+ {
+ super(UltravoxMessageType.TERMINATE_BROADCAST);
+ }
+
+ /**
+ * Server response to client request
+ * Package private constructor. Use the UltravoxMessageFactory for this constructor.
+ *
+ * @param data bytes received from the server
+ */
+ TerminateBroadcast(byte[] data)
+ {
+ super(data);
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxDecoder.java b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxDecoder.java
new file mode 100644
index 00000000..b46efe84
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxDecoder.java
@@ -0,0 +1,50 @@
+/*******************************************************************************
+ * 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.broadcast.shoutcast.v2.ultravox;
+
+import org.apache.mina.core.buffer.IoBuffer;
+import org.apache.mina.core.session.IoSession;
+import org.apache.mina.filter.codec.ProtocolDecoderAdapter;
+import org.apache.mina.filter.codec.ProtocolDecoderOutput;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class UltravoxDecoder extends ProtocolDecoderAdapter
+{
+ private final static Logger mLog = LoggerFactory.getLogger(UltravoxDecoder.class);
+
+ @Override
+ public void decode(IoSession ioSession, IoBuffer ioBuffer, ProtocolDecoderOutput out) throws Exception
+ {
+ byte[] message = new byte[ioBuffer.limit()];
+
+ ioBuffer.get(message);
+
+ UltravoxMessage ultravoxMessage = UltravoxMessageFactory.getMessage(message);
+
+ if(ultravoxMessage != null)
+ {
+ out.write(ultravoxMessage);
+ }
+ else
+ {
+ out.write(message);
+ }
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxEncoder.java b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxEncoder.java
new file mode 100644
index 00000000..bd43e8d2
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxEncoder.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * 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.broadcast.shoutcast.v2.ultravox;
+
+import org.apache.mina.core.buffer.IoBuffer;
+import org.apache.mina.core.session.IoSession;
+import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
+import org.apache.mina.filter.codec.ProtocolEncoderOutput;
+
+public class UltravoxEncoder extends ProtocolEncoderAdapter
+{
+ @Override
+ public void encode(IoSession ioSession, Object message, ProtocolEncoderOutput out) throws Exception
+ {
+ if(message instanceof UltravoxMessage)
+ {
+ byte[] data = ((UltravoxMessage)message).getMessage();
+
+ IoBuffer buf = IoBuffer.allocate(data.length).setAutoExpand(false);
+ buf.put(data);
+ buf.flip();
+ out.write(buf);
+ }
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessage.java b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessage.java
new file mode 100644
index 00000000..eb099b20
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessage.java
@@ -0,0 +1,248 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+import bits.BinaryMessage;
+
+import java.util.Arrays;
+
+public abstract class UltravoxMessage
+{
+ public static final String ULTRAVOX_VERSION = "2.1";
+ public static final String VALID_RESPONSE_PREFIX = "ACK";
+ public static final String VALID_RESPONSE_PAYLOAD_PREFIX = "ACK:";
+ public static final String ERROR_RESPONSE_PREFIX = "NAK:";
+
+ public static final int[] SYNC = {0,1,2,3,4,5,6,7};
+ public static final int[] RESERVED = {8,9,10,11};
+ public static final int REQUIRED_DELIVERY = 12;
+ public static final int[] SEND_QUEUE_PRIORITY = {13,14,15};
+ public static final int[] MESSAGE_CLASS = {16,17,18,19};
+ public static final int[] MESSAGE_CLASS_AND_TYPE = {16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31};
+ public static final int[] PAYLOAD_LENGTH = {32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47};
+
+ private BinaryMessage mMessage;
+
+ /**
+ * Constructs a ultravox from the byte array
+ */
+ public UltravoxMessage(byte[] data)
+ {
+ mMessage = new BinaryMessage(data.length * 8);
+
+ int pointer = 0;
+
+ for(byte b: data)
+ {
+ mMessage.setByte(pointer, b);
+ pointer += 8;
+ }
+ }
+
+ /**
+ * Constructs an empty ultravox of the specified length
+ *
+ * @param ultravoxMessageType type of ultravox
+ */
+ public UltravoxMessage(UltravoxMessageType ultravoxMessageType)
+ {
+ mMessage = new BinaryMessage(56);
+
+ setSync();
+ setMessageType(ultravoxMessageType);
+ }
+
+ /**
+ * Message byte array
+ *
+ * @return ultravox bytes
+ */
+ public byte[] getMessage()
+ {
+ return mMessage.toByteArray();
+ }
+
+ public String toString()
+ {
+ return getMessageType().name();
+ }
+
+ /**
+ * Sets the ultravox sync bits to the predefined sync pattern: 01011010 (0x5A)
+ */
+ private void setSync()
+ {
+ mMessage.setInt(0x5A, SYNC);
+ }
+
+ /**
+ * Ultravox ultravox class
+ */
+ public UltravoxMessageClass getMessageClass()
+ {
+ return UltravoxMessageClass.fromValue(mMessage.getInt(MESSAGE_CLASS));
+ }
+
+ /**
+ * Ultravox ultravox type
+ * @return
+ */
+ public UltravoxMessageType getMessageType()
+ {
+ return UltravoxMessageType.fromValue(mMessage.getInt(MESSAGE_CLASS_AND_TYPE));
+ }
+
+ /**
+ * Sets the ultravox bits according to the ultravox class and type
+ * @param messageType of ultravox ultravox
+ */
+ private void setMessageType(UltravoxMessageType messageType)
+ {
+ mMessage.setInt(messageType.getValue(), MESSAGE_CLASS_AND_TYPE);
+ }
+
+ /**
+ * Indicates if this ultravox has the required delivery flag set
+ */
+ public boolean isRequiredDelivery()
+ {
+ return mMessage.get(REQUIRED_DELIVERY);
+ }
+
+ /**
+ * Sets the required delivery flag according to the argument
+ */
+ public void setRequiredDelivery(boolean required)
+ {
+ if(required)
+ {
+ mMessage.set(REQUIRED_DELIVERY);
+ }
+ else
+ {
+ mMessage.clear(REQUIRED_DELIVERY);
+ }
+ }
+
+ /**
+ * Send queue priority (0 - 7) for this ultravox
+ */
+ public int getSendQueuePriority()
+ {
+ return mMessage.getInt(SEND_QUEUE_PRIORITY);
+ }
+
+ /**
+ * Sets send queue priority for this ultravox
+ * @param priority 0 - 7
+ */
+ public void setSendQueuePriority(int priority)
+ {
+ assert(0 <= priority && priority <= 7);
+
+ mMessage.setInt(priority, SEND_QUEUE_PRIORITY);
+ }
+
+ /**
+ * Message payload
+ */
+ public String getPayload()
+ {
+ int payloadLength = mMessage.getInt(PAYLOAD_LENGTH);
+
+ byte[] payload = new byte[payloadLength];
+
+ int payloadPointer = 48;
+
+ for(int x = 0; x < payloadLength; x++)
+ {
+ payload[x] = mMessage.getByte(payloadPointer);
+ payloadPointer += 8;
+ }
+
+ //Strip the trailing 0x00 null value from a server response
+ if(payload[payload.length - 1] == 0)
+ {
+ return new String(Arrays.copyOf(payload, payload.length - 1));
+ }
+
+ return new String(payload);
+ }
+
+ /**
+ * Sets the ultravox payload, updates the ultravox length field, and appends a trailing 0x00 byte after the payload.
+ */
+ public void setPayload(String payload)
+ {
+ setPayload(payload.getBytes());
+ }
+
+ public void setPayload(byte[] payload)
+ {
+ int messageBitLength = (7 + payload.length) * 8;
+
+ mMessage.setSize(messageBitLength);
+ mMessage.setInt(payload.length, PAYLOAD_LENGTH);
+
+ int payloadPointer = 48;
+
+ for(byte payloadByte: payload)
+ {
+ mMessage.setByte(payloadPointer, payloadByte);
+ payloadPointer += 8;
+ }
+
+ mMessage.setByte(payloadPointer, (byte)0x00);
+ }
+
+ /**
+ * Indicates if the response is a valid (ie non-error) response
+ */
+ public boolean isValidResponse()
+ {
+ String payload = getPayload();
+
+ return payload != null && payload.startsWith(VALID_RESPONSE_PREFIX);
+ }
+
+ /**
+ * Indicates if the response message contains an error message.
+ */
+ public boolean isErrorResponse()
+ {
+ String payload = getPayload();
+
+ return payload != null && payload.startsWith(ERROR_RESPONSE_PREFIX);
+ }
+
+ /**
+ * Textual message available when the error response flag is set.
+ */
+ public String getErrorMessage()
+ {
+ String payload = getPayload();
+
+ if(payload != null)
+ {
+ return payload.replace(ERROR_RESPONSE_PREFIX, "");
+ }
+
+ return null;
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessageClass.java b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessageClass.java
new file mode 100644
index 00000000..08d3b35f
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessageClass.java
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public enum UltravoxMessageClass
+{
+ OPERATIONS(0x0),
+ BROADCASTER(0x1),
+ LISTENER(0x2),
+ CACHEABLE_METADATA_1(0x3),
+ CACHEABLE_METADATA_2(0x4),
+ PASS_THROUGH_METADATA_1(0x5),
+ PASS_THROUGH_METADATA_2(0x6),
+ ENCODED_DATA(0x7),
+ ADVANCED_ENCODED_DATA(0x8),
+ FRAMED_DATA(0x9),
+ CACHEABLE_BINARY_METADATA(0xA),
+ UNKNOWN(0xF);
+
+ private int mValue;
+
+ private UltravoxMessageClass(int value)
+ {
+ mValue = value;
+ }
+
+ public int getValue()
+ {
+ return mValue;
+ }
+
+ public static UltravoxMessageClass fromValue(int value)
+ {
+ if(0x0 <= value && value <= 0xA)
+ {
+ return values()[value];
+ }
+
+ return UNKNOWN;
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessageFactory.java b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessageFactory.java
new file mode 100644
index 00000000..9423f912
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessageFactory.java
@@ -0,0 +1,135 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public class UltravoxMessageFactory
+{
+ /**
+ * Creates an empty ultravox of the specified ultravox type for sending to the server
+ */
+ public static UltravoxMessage getMessage(UltravoxMessageType type)
+ {
+ switch(type)
+ {
+ case AUTHENTICATE_BROADCAST:
+ return new AuthenticateBroadcast();
+ case CACHEABLE_XML_METADATA:
+ return new CacheableXMLMetadata();
+ case CONFIGURE_ICY_GENRE:
+ return new ConfigureIcyGenre();
+ case CONFIGURE_ICY_NAME:
+ return new ConfigureIcyName();
+ case CONFIGURE_ICY_PUBLIC:
+ return new ConfigureIcyPublic();
+ case CONFIGURE_ICY_URL:
+ return new ConfigureIcyURL();
+ case FLUSH_CACHED_METADATA:
+ return new FlushCachedMetadata();
+ case MP3_DATA:
+ return new MP3Audio();
+ case NEGOTIATE_BUFFER_SIZE:
+ return new NegotiateBufferSize();
+ case NEGOTIATE_MAX_PAYLOAD_SIZE:
+ return new NegotiateMaxPayloadSize();
+ case PASS_THROUGH_XML_METADATA:
+ return new PassThroughXMLMetadata();
+ case REQUEST_CIPHER:
+ return new RequestCipher();
+ case SETUP_BROADCAST:
+ return new SetupBroadcast();
+ case STANDBY:
+ return new Standby();
+ case STREAM_MIME_TYPE:
+ return new StreamMimeType();
+ case TERMINATE_BROADCAST:
+ return new TerminateBroadcast();
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Creates an ultravox ultravox from the server response bytes.
+ *
+ * @param bytes in network order (big-endian).
+ *
+ * @return constructed ultravox or null if the ultravox type cannot be determined or is unrecognized
+ */
+ public static UltravoxMessage getMessage(byte[] bytes)
+ {
+ UltravoxMessageType type = getMessageType(bytes);
+
+ switch(type)
+ {
+ case AUTHENTICATE_BROADCAST:
+ return new AuthenticateBroadcast(bytes);
+ case CACHEABLE_XML_METADATA:
+ return new CacheableXMLMetadata(bytes);
+ case CONFIGURE_ICY_GENRE:
+ return new ConfigureIcyGenre(bytes);
+ case CONFIGURE_ICY_NAME:
+ return new ConfigureIcyName(bytes);
+ case CONFIGURE_ICY_PUBLIC:
+ return new ConfigureIcyPublic(bytes);
+ case CONFIGURE_ICY_URL:
+ return new ConfigureIcyURL(bytes);
+ case FLUSH_CACHED_METADATA:
+ return new FlushCachedMetadata(bytes);
+ case MP3_DATA:
+ return new MP3Audio(bytes);
+ case NEGOTIATE_BUFFER_SIZE:
+ return new NegotiateBufferSize(bytes);
+ case NEGOTIATE_MAX_PAYLOAD_SIZE:
+ return new NegotiateMaxPayloadSize(bytes);
+ case PASS_THROUGH_XML_METADATA:
+ return new PassThroughXMLMetadata(bytes);
+ case REQUEST_CIPHER:
+ return new RequestCipher(bytes);
+ case SETUP_BROADCAST:
+ return new SetupBroadcast(bytes);
+ case STANDBY:
+ return new Standby(bytes);
+ case STREAM_MIME_TYPE:
+ return new StreamMimeType(bytes);
+ case TERMINATE_BROADCAST:
+ return new TerminateBroadcast(bytes);
+ case UNKNOWN:
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Identifies the ultravox type from the server response byte array
+ *
+ * @param data in little-endian format
+ * @return ultravox type or UNKNOWN if the ultravox type cannot be determined.
+ */
+ private static UltravoxMessageType getMessageType(byte[] data)
+ {
+ if(data != null && data.length > 4)
+ {
+ int value = (data[2] << 8) + data[3];
+
+ return UltravoxMessageType.fromValue(value);
+ }
+
+ return UltravoxMessageType.UNKNOWN;
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessageType.java b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessageType.java
new file mode 100644
index 00000000..a5836cc3
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMessageType.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.shoutcast.v2.ultravox;
+
+public enum UltravoxMessageType
+{
+ AUTHENTICATE_BROADCAST(0x1001),
+ SETUP_BROADCAST(0x1002),
+ NEGOTIATE_BUFFER_SIZE(0x1003),
+ STANDBY(0x1004),
+ TERMINATE_BROADCAST(0x1005),
+ FLUSH_CACHED_METADATA(0x1006),
+ NEGOTIATE_MAX_PAYLOAD_SIZE(0x1008),
+ REQUEST_CIPHER(0x1009),
+ STREAM_MIME_TYPE(0x1040),
+ CONFIGURE_ICY_NAME(0x1100),
+ CONFIGURE_ICY_GENRE(0x1101),
+ CONFIGURE_ICY_URL(0x1102),
+ CONFIGURE_ICY_PUBLIC(0x1103),
+ CACHEABLE_XML_METADATA(0x3902),
+ PASS_THROUGH_XML_METADATA(0x5902),
+ MP3_DATA(0x7000),
+ UNKNOWN(0x0000);
+
+ private int mValue;
+
+ private UltravoxMessageType(int value)
+ {
+ mValue = value;
+ }
+
+ public int getValue()
+ {
+ return mValue;
+ }
+
+ public static UltravoxMessageType fromValue(int value)
+ {
+ switch(value)
+ {
+ case 0x1001:
+ return AUTHENTICATE_BROADCAST;
+ case 0x1002:
+ return SETUP_BROADCAST;
+ case 0x1003:
+ return NEGOTIATE_BUFFER_SIZE;
+ case 0x1004:
+ return STANDBY;
+ case 0x1005:
+ return TERMINATE_BROADCAST;
+ case 0x1006:
+ return FLUSH_CACHED_METADATA;
+ case 0x1008:
+ return NEGOTIATE_MAX_PAYLOAD_SIZE;
+ case 0x1009:
+ return REQUEST_CIPHER;
+ case 0x1040:
+ return STREAM_MIME_TYPE;
+ case 0x1100:
+ return CONFIGURE_ICY_NAME;
+ case 0x1101:
+ return CONFIGURE_ICY_GENRE;
+ case 0x1102:
+ return CONFIGURE_ICY_URL;
+ case 0x1103:
+ return CONFIGURE_ICY_PUBLIC;
+ case 0x3902:
+ return CACHEABLE_XML_METADATA;
+ case 0x5902:
+ return PASS_THROUGH_XML_METADATA;
+ case 0x7000:
+ return MP3_DATA;
+ default:
+ return UNKNOWN;
+ }
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMetadata.java b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMetadata.java
new file mode 100644
index 00000000..73d21cb3
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxMetadata.java
@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.shoutcast.v2.ultravox;
+
+public enum UltravoxMetadata
+{
+ ALBUM_TITLE("TALB"),
+ ARTIST("TPE1"),
+ AUDIO_SOURCE_WEBPAGE("WOAS"),
+ BROADCAST_CLIENT_APPLICATION("TENC"),
+ COMMENT("COMM"),
+ CUSTOM_TEXT_FIELD("TXXX"),
+ ENCODE_SETTINGS("TSSE"),
+ GENERAL_BINARY_OBJECT("GEOB"),
+ GENRE("TCON"),
+ OWNER("TOWN"),
+ RADIO_STATION("TRSN"),
+ RADIO_STATION_OWNER("TRSO"),
+ RADIO_STATION_WEBPAGE("WORS"),
+ RECORDING_TIME("TDRC"),
+ SOURCE_MEDIA_TYPE("TMED"),
+ TITLE_1("TIT1"),
+ TITLE_2("TIT2"),
+ TITLE_3("TIT3"),
+ URL("URL"),
+ UNKNOWN("UNKNOWN");
+
+ private String mXMLTag;
+
+ private UltravoxMetadata(String xmlTag)
+ {
+ mXMLTag = xmlTag;
+ }
+
+ public String getXMLTag()
+ {
+ return mXMLTag;
+ }
+
+ /**
+ * Encodes the value as an XML tag stream
+ * @param value to enclose in XML tags
+ * @return xml tag wrapped value
+ */
+ public String asXML(String value)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<").append(getXMLTag()).append(">");
+ sb.append(value);
+ sb.append("").append(getXMLTag()).append(">");
+
+ return sb.toString();
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxProtocolFactory.java b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxProtocolFactory.java
new file mode 100644
index 00000000..8aff45b9
--- /dev/null
+++ b/src/audio/broadcast/shoutcast/v2/ultravox/UltravoxProtocolFactory.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * 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.broadcast.shoutcast.v2.ultravox;
+
+import org.apache.mina.core.session.IoSession;
+import org.apache.mina.filter.codec.ProtocolCodecFactory;
+import org.apache.mina.filter.codec.ProtocolDecoder;
+import org.apache.mina.filter.codec.ProtocolEncoder;
+
+public class UltravoxProtocolFactory implements ProtocolCodecFactory
+{
+ private ProtocolEncoder mEncoder;
+ private ProtocolDecoder mDecoder;
+
+ @Override
+ public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception
+ {
+ if(mEncoder == null)
+ {
+ mEncoder = new UltravoxEncoder();
+ }
+
+ return mEncoder;
+ }
+
+ @Override
+ public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception
+ {
+ if(mDecoder == null)
+ {
+ mDecoder = new UltravoxDecoder();
+ }
+
+ return mDecoder;
+ }
+}
diff --git a/src/audio/convert/IAudioConverter.java b/src/audio/convert/IAudioConverter.java
new file mode 100644
index 00000000..92ccb5dc
--- /dev/null
+++ b/src/audio/convert/IAudioConverter.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * 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.AudioPacket;
+
+import java.util.List;
+
+public interface IAudioConverter
+{
+ /**
+ * Converts the PCM audio packets to converted audio format. May produce partial audio frame data.
+ */
+ public byte[] convert(List audioPackets);
+
+ /**
+ * Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the
+ * remaining bytes to produce a full frame.
+ */
+ public byte[] flush();
+}
diff --git a/src/audio/convert/ISilenceGenerator.java b/src/audio/convert/ISilenceGenerator.java
new file mode 100644
index 00000000..ac2e1c5c
--- /dev/null
+++ b/src/audio/convert/ISilenceGenerator.java
@@ -0,0 +1,24 @@
+/*******************************************************************************
+ * 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;
+
+public interface ISilenceGenerator
+{
+ byte[] generate(long duration);
+}
diff --git a/src/audio/convert/MP3AudioConverter.java b/src/audio/convert/MP3AudioConverter.java
new file mode 100644
index 00000000..ea813f1c
--- /dev/null
+++ b/src/audio/convert/MP3AudioConverter.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * 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.Arrays;
+import java.util.List;
+
+public class MP3AudioConverter implements IAudioConverter
+{
+ private final static Logger mLog = LoggerFactory.getLogger( MP3AudioConverter.class );
+ public static final int AUDIO_QUALITY = Lame.QUALITY_LOW;
+ 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, AUDIO_QUALITY, 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;
+
+ try
+ {
+ 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();
+ }
+ catch(Exception e)
+ {
+ mLog.error("There was an error converting audio to MP3: " + e.getMessage());
+ return new byte[0];
+ }
+ }
+
+ @Override
+ public byte[] flush()
+ {
+ byte[] lastPartialFrame = new byte[mEncoder.getMP3BufferSize()];
+
+ int length = mEncoder.encodeFinish(lastPartialFrame);
+
+ return Arrays.copyOf(lastPartialFrame, length);
+ }
+}
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
new file mode 100644
index 00000000..8e68e6f3
--- /dev/null
+++ b/src/audio/convert/MP3SilenceGenerator.java
@@ -0,0 +1,120 @@
+/*******************************************************************************
+ * 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.AudioPacket;
+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
+{
+ 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()
+ {
+ }
+
+ public byte[] generate(long duration)
+ {
+ int length = (int)(duration * 8); //8000 Hz sample rate
+ float[] silence = new float[length];
+ AudioPacket silencePacket = new AudioPacket(silence, null);
+ List silencePackets = new ArrayList<>();
+ silencePackets.add(silencePacket);
+
+ 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 ...");
+
+ for(long x = 243; x < 500; x ++)
+ {
+ 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/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/metadata/AudioMetadata.java b/src/audio/metadata/AudioMetadata.java
index 521ffa79..9bc19cd0 100644
--- a/src/audio/metadata/AudioMetadata.java
+++ b/src/audio/metadata/AudioMetadata.java
@@ -1,28 +1,32 @@
/*******************************************************************************
* SDR Trunk
* 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.metadata;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeSet;
+import alias.id.broadcast.BroadcastChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,288 +36,322 @@ import alias.id.priority.Priority;
public class AudioMetadata implements Listener
{
- protected static final Logger mLog = LoggerFactory.getLogger( AudioMetadata.class );
+ protected static final Logger mLog = LoggerFactory.getLogger(AudioMetadata.class);
- private int mSource;
- private Map mMetadata = new HashMap<>();
- private boolean mUpdated = false;
+ private int mSource;
+ private Map mMetadata = new HashMap<>();
+ private boolean mUpdated = false;
- /* Channel Selected == highest priority */
- private boolean mSelected = false;
+ /* Channel Selected == highest priority */
+ private boolean mSelected = false;
- /* Call Priority */
- private int mPriority = Priority.DEFAULT_PRIORITY;
+ /* Call Priority */
+ private int mPriority = Priority.DEFAULT_PRIORITY;
- /* Indicates if the overall source is recordable */
- private boolean mSourceRecordable = false;
-
- /* Indicates the current recordable state */
- private boolean mRecordable = false;
-
- /* A unique identifier for this metadata */
- private String mIdentifier;
-
- public AudioMetadata( int source, boolean recordable )
- {
- mSource = source;
-
- setIdentifier( mSource );
-
- mSourceRecordable = recordable;
-
- setRecordable( mSourceRecordable );
- }
+ /* Indicates if the overall source is recordable */
+ private boolean mSourceRecordable = false;
- /**
- * Returns a unique identifier for this audio metadata that contains the
- * source id and optionally the TO metadata string.
- */
- public String getIdentifier()
- {
- return mIdentifier;
- }
-
- public void setIdentifier( int source )
- {
- setIdentifier( source, null );
- }
-
- public void setIdentifier( int source, String id )
- {
- mIdentifier = "SRC:" + source + " ID:" + ( id == null ? "UNKNOWN" : id );
- mUpdated = true;
- }
-
- /**
- * Returns a copy of the current audio metadata.
- */
- public AudioMetadata copyOf()
- {
- AudioMetadata copy = new AudioMetadata( mSource, mSourceRecordable );
-
- copy.mPriority = mPriority;
- copy.mSelected = mSelected;
- copy.mRecordable = mRecordable;
+ /* Indicates the current recordable state */
+ private boolean mRecordable = false;
- copy.mMetadata.putAll( mMetadata );
- copy.mUpdated = mUpdated;
- copy.mIdentifier = new String( mIdentifier );
-
- mUpdated = false;
-
- return copy;
- }
+ /* Indicates the current streamable state */
+ private boolean mStreamable = false;
- /**
- * Removes any accumulated temporal metadata and returns to a default set.
- */
- public void reset()
- {
- /* Reset recordable state, identifier and audio call priority levels
- * and reprocess the metadata */
- mRecordable = mSourceRecordable;
- setIdentifier( mSource );
- mPriority = Priority.DEFAULT_PRIORITY;
-
- List toRemove = new ArrayList<>();
+ /* A unique identifier for this metadata */
+ private String mIdentifier;
- for( MetadataType type: mMetadata.keySet() )
- {
- Metadata metadata = mMetadata.get( type );
+ public AudioMetadata(int source, boolean recordable)
+ {
+ mSource = source;
- if( metadata != null )
- {
- if( metadata.isTemporal() )
- {
- toRemove.add( type );
- }
- else
- {
- processMetadata( metadata );
- }
- }
- }
-
- for( MetadataType type: toRemove )
- {
- mMetadata.remove( type );
- }
+ setIdentifier(mSource);
- mUpdated = true;
- }
-
- private void processMetadata( Metadata metadata )
- {
- if( metadata.hasAlias() )
- {
- Alias alias = metadata.getAlias();
-
- if( metadata.getMetadataType() == MetadataType.FROM ||
- metadata.getMetadataType() == MetadataType.TO )
- {
- if( !alias.isRecordable() )
- {
- mRecordable = false;
- }
- }
+ mSourceRecordable = recordable;
- if( metadata.getMetadataType() == MetadataType.TO )
- {
- setIdentifier( mSource, metadata.getValue() );
- }
+ setRecordable(mSourceRecordable);
+ }
- if( alias != null && alias.getCallPriority() < mPriority )
- {
- mPriority = alias.getCallPriority();
- }
- }
- }
-
- /**
- * Indicates if this audio metadata contains updated information. The flag
- * will only be set when new data is added and will be reset by a copyOf()
- * method invocation.
- */
- public boolean isUpdated()
- {
- return mUpdated;
- }
-
- public void setUpdated( boolean updated )
- {
- mUpdated = updated;
- }
+ /**
+ * Returns a unique identifier for this audio metadata that contains the
+ * source id and optionally the TO metadata string.
+ */
+ public String getIdentifier()
+ {
+ return mIdentifier;
+ }
- /**
- * Adds the metadata. Any temporal metadata will be removed when the
- * dispose() method is invoked.
- */
- @Override
- public void receive( Metadata metadata )
- {
- if( metadata.isReset() )
- {
- reset();
- }
- else
- {
- Metadata existing = mMetadata.get( metadata.getMetadataType() );
+ public void setIdentifier(int source)
+ {
+ setIdentifier(source, null);
+ }
- if( existing == null || !existing.equals( metadata ) )
- {
- mMetadata.put( metadata.getMetadataType(), metadata );
- processMetadata( metadata );
- setUpdated( true );
- }
- }
-
- mUpdated = true;
- }
-
- /**
- * Returns the complete set of metadata
- */
- public Map getMetadata()
- {
- return mMetadata;
- }
+ public void setIdentifier(int source, String id)
+ {
+ mIdentifier = "SRC:" + source + " ID:" + (id == null ? "UNKNOWN" : id);
+ mUpdated = true;
+ }
- /**
- * Returns the metadata object matching the type or null
- */
- public Metadata getMetadata( MetadataType type )
- {
- return mMetadata.get( type );
- }
+ /**
+ * Returns a copy of the current audio metadata.
+ */
+ public AudioMetadata copyOf()
+ {
+ AudioMetadata copy = new AudioMetadata(mSource, mSourceRecordable);
- /**
- * Source channel ID
- */
- public void setSource( int source )
- {
- mSource = source;
-
- mUpdated = true;
- }
-
- public int getSource()
- {
- return mSource;
- }
+ copy.mPriority = mPriority;
+ copy.mSelected = mSelected;
+ copy.mRecordable = mRecordable;
+ copy.mStreamable = mStreamable;
- /**
- * Sets the priority of this audio packet within the defined min/max priority range */
- public void setPriority( int priority )
- {
- mPriority = priority;
-
- mUpdated = true;
- }
+ copy.mMetadata.putAll(mMetadata);
+ copy.mUpdated = mUpdated;
+ copy.mIdentifier = new String(mIdentifier);
- /**
- * Returns the priority of this audio metadata
- *
- * @return - priority of audio packet where lower numbers are higher priority
- */
- public int getPriority()
- {
- return mPriority;
- }
-
- public boolean isDoNotMonitor()
- {
- return mPriority == Priority.DO_NOT_MONITOR;
- }
+ mUpdated = false;
- /**
- * Selected audio channel. A selected channel will override the priority
- * level so that the audio can be listened to immediately.
- */
- public void setSelected( boolean selected )
- {
- mSelected = selected;
-
- mUpdated = true;
- }
+ return copy;
+ }
- public boolean isSelected()
- {
- return mSelected;
- }
-
- /**
- * Indicates if audio associated with this metadata is recordable.
- */
- public boolean isRecordable()
- {
- return mRecordable;
- }
-
- public void setRecordable( boolean recordable )
- {
- mRecordable = recordable;
- mUpdated = true;
- }
-
- public String toString()
- {
- StringBuilder sb = new StringBuilder();
-
- sb.append( "Audio Metadata - source:" );
- sb.append( getSource() );
- sb.append( " priority:" );
- sb.append( getPriority() );
- sb.append( " updated:" + isUpdated() );
- sb.append( " " );
-
- for( Entry entry: getMetadata().entrySet() )
- {
- sb.append( entry.getValue().getKey() );
- sb.append( ":" );
- sb.append( entry.getValue().getValue() );
- sb.append( " " );
- }
+ /**
+ * Removes any accumulated temporal metadata and returns to a default set.
+ */
+ public void reset()
+ {
+ //Reset recordable state, identifier and audio call priority levels and reprocess the metadata
+ mRecordable = mSourceRecordable;
+ mStreamable = false;
+ setIdentifier(mSource);
+ mPriority = Priority.DEFAULT_PRIORITY;
- return sb.toString();
- }
+ List toRemove = new ArrayList<>();
+
+ for (MetadataType type : mMetadata.keySet())
+ {
+ Metadata metadata = mMetadata.get(type);
+
+ if (metadata != null)
+ {
+ if (metadata.isTemporal())
+ {
+ toRemove.add(type);
+ }
+ else
+ {
+ processMetadata(metadata);
+ }
+ }
+ }
+
+ for (MetadataType type : toRemove)
+ {
+ mMetadata.remove(type);
+ }
+
+ mUpdated = true;
+ }
+
+ private void processMetadata(Metadata metadata)
+ {
+ if (metadata.hasAlias())
+ {
+ Alias alias = metadata.getAlias();
+
+ if (metadata.getMetadataType() == MetadataType.FROM || metadata.getMetadataType() == MetadataType.TO)
+ {
+ if (!alias.isRecordable())
+ {
+ mRecordable = false;
+ }
+
+ if (alias.isStreamable())
+ {
+ mStreamable = true;
+ }
+ }
+
+ if (metadata.getMetadataType() == MetadataType.TO)
+ {
+ setIdentifier(mSource, metadata.getValue());
+ }
+
+ if (alias != null && alias.getCallPriority() < mPriority)
+ {
+ mPriority = alias.getCallPriority();
+ }
+ }
+ }
+
+ /**
+ * Indicates if this audio metadata contains updated information. The flag
+ * will only be set when new data is added and will be reset by a copyOf()
+ * method invocation.
+ */
+ public boolean isUpdated()
+ {
+ return mUpdated;
+ }
+
+ public void setUpdated(boolean updated)
+ {
+ mUpdated = updated;
+ }
+
+ /**
+ * Adds the metadata. Any temporal metadata will be removed when the
+ * dispose() method is invoked.
+ */
+ @Override
+ public void receive(Metadata metadata)
+ {
+ if (metadata.isReset())
+ {
+ reset();
+ }
+ else
+ {
+ Metadata existing = mMetadata.get(metadata.getMetadataType());
+
+ if (existing == null || !existing.equals(metadata))
+ {
+ mMetadata.put(metadata.getMetadataType(), metadata);
+ processMetadata(metadata);
+ setUpdated(true);
+ }
+ }
+
+ mUpdated = true;
+ }
+
+ /**
+ * Returns the complete set of metadata
+ */
+ public Map getMetadata()
+ {
+ return mMetadata;
+ }
+
+ /**
+ * Returns the metadata object matching the type or null
+ */
+ public Metadata getMetadata(MetadataType type)
+ {
+ return mMetadata.get(type);
+ }
+
+ /**
+ * Source channel ID
+ */
+ public void setSource(int source)
+ {
+ mSource = source;
+
+ mUpdated = true;
+ }
+
+ public int getSource()
+ {
+ return mSource;
+ }
+
+ /**
+ * Sets the priority of this audio packet within the defined min/max priority range
+ */
+ public void setPriority(int priority)
+ {
+ mPriority = priority;
+
+ mUpdated = true;
+ }
+
+ /**
+ * Returns the priority of this audio metadata
+ *
+ * @return - priority of audio packet where lower numbers are higher priority
+ */
+ public int getPriority()
+ {
+ return mPriority;
+ }
+
+ public boolean isDoNotMonitor()
+ {
+ return mPriority == Priority.DO_NOT_MONITOR;
+ }
+
+ /**
+ * Selected audio channel. A selected channel will override the priority
+ * level so that the audio can be listened to immediately.
+ */
+ public void setSelected(boolean selected)
+ {
+ mSelected = selected;
+
+ mUpdated = true;
+ }
+
+ public boolean isSelected()
+ {
+ return mSelected;
+ }
+
+ /**
+ * Indicates if audio associated with this metadata is recordable.
+ */
+ public boolean isRecordable()
+ {
+ return mRecordable;
+ }
+
+ public void setRecordable(boolean recordable)
+ {
+ mRecordable = recordable;
+ mUpdated = true;
+ }
+
+ /**
+ * Indicates if the audio associated with this metadata is streamable
+ */
+ public boolean isStreamable()
+ {
+ return mStreamable;
+ }
+
+ public Collection getBroadcastChannels()
+ {
+ Set broadcastChannels = new TreeSet<>();
+
+ for(Metadata metadata: mMetadata.values())
+ {
+ if(metadata.hasAlias() && metadata.getAlias().isStreamable())
+ {
+ Alias alias = metadata.getAlias();
+
+ broadcastChannels.addAll(alias.getBroadcastChannels());
+ }
+ }
+
+ return broadcastChannels;
+ }
+
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("Audio Metadata - source:");
+ sb.append(getSource());
+ sb.append(" priority:");
+ sb.append(getPriority());
+ sb.append(" updated:" + isUpdated());
+ sb.append(" ");
+
+ for (Entry entry : getMetadata().entrySet())
+ {
+ sb.append(entry.getValue().getKey());
+ sb.append(":");
+ sb.append(entry.getValue().getValue());
+ sb.append(" ");
+ }
+
+ return sb.toString();
+ }
}
diff --git a/src/bits/BinaryMessage.java b/src/bits/BinaryMessage.java
index 66b9463e..56e21a12 100644
--- a/src/bits/BinaryMessage.java
+++ b/src/bits/BinaryMessage.java
@@ -17,7 +17,9 @@
******************************************************************************/
package bits;
+import java.util.Arrays;
import java.util.BitSet;
+import java.util.logging.LogManager;
import edac.CRC;
@@ -103,6 +105,11 @@ public class BinaryMessage extends BitSet
this.or( bitset );
this.mPointer = size - 1;
}
+
+ public BinaryMessage(byte[] data)
+ {
+ this(BitSet.valueOf(data), data.length * 8);
+ }
/**
* Returns a mew binary message containing the bits from (inclusive) to
@@ -277,6 +284,33 @@ public class BinaryMessage extends BitSet
return values;
}
+
+ /**
+ * Returns this message as a little endian byte array. Extra 0 bits will be padded to the end to make the overall
+ * length a multiple of 8.
+ * @return little endian message byte array
+ */
+ public byte[] toByteArray()
+ {
+ int length = size() / 8;
+
+ if(length * 8 < size())
+ {
+ length ++;
+ }
+
+ byte[] bytes = new byte[length];
+
+ int pointer = 0;
+
+ for(int x = 0; x < length; x++)
+ {
+ bytes[x] = getByte(pointer);
+ pointer += 8;
+ }
+
+ return bytes;
+ }
/**
* Returns this bitset as a reversed bit order array of integer ones and zeros
@@ -367,7 +401,24 @@ public class BinaryMessage extends BitSet
return value;
}
-
+
+ public void setInt(int value, int[] indices)
+ {
+ for(int x = 0; x < indices.length; x++)
+ {
+ int mask = 1<<(indices.length - x - 1);
+
+ if((value & mask) == mask)
+ {
+ set(indices[x]);
+ }
+ else
+ {
+ clear(indices[x]);
+ }
+ }
+ }
+
/**
* Returns the byte value represented by the bit array
* @param bits - an array of bit positions that will be treated as if they
@@ -398,6 +449,57 @@ public class BinaryMessage extends BitSet
return (byte)( value & 0xFF );
}
+ /**
+ * Returns the byte value contained between index and index + 7 bit positions
+ * @param index specifying the start of the byte value
+ * @return byte value contained at index <> index + 7 bit positions
+ */
+ public byte getByte(int index)
+ {
+ assert((index + 7) <= size());
+
+ int value = 0;
+
+ for(int x = 0; x < 8; x++)
+ {
+ value = value << 1;
+
+ if(get(index + x))
+ {
+ value++;
+ }
+ }
+
+ return (byte)value;
+ }
+
+ /**
+ * Sets the byte value at index position through index + 7 position.
+ *
+ * @param index bit position where to write the MSB of the byte value
+ * @param value to write at the index through index + 7 bit positions
+ */
+ public void setByte(int index, byte value)
+ {
+ assert((index + 8) <= size());
+
+ int mask = 0x80;
+
+ for(int x = 0; x < 8; x++)
+ {
+ if((mask & value) == mask)
+ {
+ set(index + x);
+ }
+ else
+ {
+ clear(index + x);
+ }
+
+ mask = mask >> 1;
+ }
+ }
+
/**
* Returns the long value represented by the bit array
* @param bits - an array of bit positions that will be treated as if they
@@ -601,11 +703,11 @@ public class BinaryMessage extends BitSet
}
/**
- * Loads the value into the buffer starting at the offset index, and
+ * Loads the value into the buffer starting at the offset index, and
* assuming that the value represents (width) number of bits. The MSB of
- * the value will be located at the offset and the LSB of the value will
+ * the value will be located at the offset and the LSB of the value will
* be located at ( offset + width ).
- *
+ *
* @param offset - starting bit index for the MSB of the value
* @param width - representative bit width of the value
* @param value - value to be loaded into the buffer
@@ -626,7 +728,7 @@ public class BinaryMessage extends BitSet
}
}
}
-
+
/**
* Generates an array of message bit position indexes to support accessing
* a contiguous field value
@@ -775,4 +877,15 @@ public class BinaryMessage extends BitSet
this.xor( mask );
}
+
+ public static void main(String[] args)
+ {
+ BinaryMessage b = new BinaryMessage(32);
+
+ int[] indices = {2,3,7,8};
+
+ b.setInt(0xF, indices);
+
+ System.out.println(b.toString());
+ }
}
diff --git a/src/controller/ControllerPanel.java b/src/controller/ControllerPanel.java
index 1243b164..a66a994c 100644
--- a/src/controller/ControllerPanel.java
+++ b/src/controller/ControllerPanel.java
@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* 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
******************************************************************************/
@@ -27,6 +27,8 @@ import javax.swing.JTable;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
+import audio.broadcast.BroadcastModel;
+import audio.broadcast.BroadcastPanel;
import map.MapPanel;
import map.MapService;
import module.decode.event.CallEventPanel;
@@ -56,122 +58,121 @@ public class ControllerPanel extends JPanel
private static final long serialVersionUID = 1L;
private AliasController mAliasController;
- private AudioPanel mAudioPanel;
+ private AudioPanel mAudioPanel;
+ private BroadcastPanel mBroadcastPanel;
private CallEventPanel mCallEventPanel;
- private ChannelController mChannelController;
+ private ChannelController mChannelController;
private ChannelList mChannelStateList;
- private ChannelModel mChannelModel;
+ private ChannelModel mChannelModel;
private ChannelSpectrumPanel mChannelSpectrumPanel;
- private MapPanel mMapPanel;
+ private MapPanel mMapPanel;
private MessageActivityPanel mMessageActivityPanel;
private TunerViewPanel mTunerManagerPanel;
private JideTabbedPane mTabbedPane;
- protected JTable mChannelActivityTable = new JTable();
+ protected JTable mChannelActivityTable = new JTable();
- public ControllerPanel( AudioManager audioManager,
- AliasModel aliasModel,
- ChannelModel channelModel,
- ChannelMapModel channelMapModel,
- ChannelProcessingManager channelProcessingManager,
- MapService mapService,
- SettingsManager settingsManager,
- SourceManager sourceManager,
- TunerModel tunerModel )
- {
- mChannelModel = channelModel;
+ public ControllerPanel(AudioManager audioManager,
+ AliasModel aliasModel,
+ BroadcastModel broadcastModel,
+ ChannelModel channelModel,
+ ChannelMapModel channelMapModel,
+ ChannelProcessingManager channelProcessingManager,
+ MapService mapService,
+ SettingsManager settingsManager,
+ SourceManager sourceManager,
+ TunerModel tunerModel)
+ {
+ mChannelModel = channelModel;
- mAudioPanel = new AudioPanel( settingsManager, sourceManager, audioManager );
+ mAudioPanel = new AudioPanel(settingsManager, sourceManager, audioManager);
- mMapPanel = new MapPanel( mapService, settingsManager );
-
- mMessageActivityPanel = new MessageActivityPanel( channelProcessingManager );
+ mMapPanel = new MapPanel(mapService, settingsManager);
- mCallEventPanel = new CallEventPanel( settingsManager, channelProcessingManager );
-
- mChannelSpectrumPanel = new ChannelSpectrumPanel( settingsManager,
- channelProcessingManager );
+ mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
- mChannelStateList = new ChannelList( channelModel, channelProcessingManager,
- settingsManager, mAudioPanel );
+ mBroadcastPanel = new BroadcastPanel(broadcastModel, aliasModel, settingsManager);
- mChannelController = new ChannelController( channelModel, channelMapModel,
- sourceManager, aliasModel );
-
- mAliasController = new AliasController( aliasModel, settingsManager );
-
- mTunerManagerPanel = new TunerViewPanel( tunerModel );
+ mCallEventPanel = new CallEventPanel(settingsManager, channelProcessingManager);
- init();
- }
-
- private void init()
- {
- setLayout( new MigLayout( "insets 0 0 0 0 ",
- "[grow,fill]",
- "[grow,fill]") );
-
- //Tabbed View - configuration, calls, messages, map
- mTabbedPane = new JideTabbedPane();
- mTabbedPane.setFont( this.getFont() );
- mTabbedPane.setForeground( Color.BLACK );
- mTabbedPane.addTab( "Tuners", mTunerManagerPanel );
- mTabbedPane.addTab( "Channels", mChannelController );
- mTabbedPane.addTab( "Aliases", mAliasController );
- mTabbedPane.addTab( "Channel Spectrum", mChannelSpectrumPanel );
- mTabbedPane.addTab( "Events", mCallEventPanel );
- mTabbedPane.addTab( "Messages", mMessageActivityPanel );
- mTabbedPane.addTab( "Map", mMapPanel );
+ mChannelSpectrumPanel = new ChannelSpectrumPanel(settingsManager,
+ channelProcessingManager);
+
+ mChannelStateList = new ChannelList(channelModel, channelProcessingManager,
+ settingsManager, mAudioPanel);
+
+ mChannelController = new ChannelController(channelModel, channelMapModel,
+ sourceManager, aliasModel);
+
+ mAliasController = new AliasController(aliasModel, broadcastModel, settingsManager);
+
+ mTunerManagerPanel = new TunerViewPanel(tunerModel);
+
+ init();
+ }
+
+ private void init()
+ {
+ setLayout(new MigLayout("insets 0 0 0 0 ",
+ "[grow,fill]",
+ "[grow,fill]"));
+
+ //Tabbed View - configuration, calls, messages, map
+ mTabbedPane = new JideTabbedPane();
+ mTabbedPane.setFont(this.getFont());
+ mTabbedPane.setForeground(Color.BLACK);
+ mTabbedPane.addTab("Tuners", mTunerManagerPanel);
+ mTabbedPane.addTab("Channels", mChannelController);
+ mTabbedPane.addTab("Aliases", mAliasController);
+ mTabbedPane.addTab("Channel Spectrum", mChannelSpectrumPanel);
+ mTabbedPane.addTab("Events", mCallEventPanel);
+ mTabbedPane.addTab("Map", mMapPanel);
+ mTabbedPane.addTab("Messages", mMessageActivityPanel);
+ mTabbedPane.addTab("Streaming", mBroadcastPanel);
+
+ /**
+ * Change listener to enable/disable the channel spectrum display
+ * only when the tab is visible, and a channel has been selected
+ */
+ mTabbedPane.addChangeListener(new ChangeListener()
+ {
+ @Override
+ public void stateChanged(ChangeEvent event)
+ {
+ int index = mTabbedPane.getSelectedIndex();
+
+ Component component = mTabbedPane.getComponentAt(index);
+
+ if (component instanceof ChannelSpectrumPanel)
+ {
+ mChannelSpectrumPanel.setEnabled(true);
+ }
+ else
+ {
+ mChannelSpectrumPanel.setEnabled(false);
+ }
+ }
+ });
- /**
- * Change listener to enable/disable the channel spectrum display
- * only when the tab is visible, and a channel has been selected
- */
- mTabbedPane.addChangeListener( new ChangeListener()
- {
- @Override
- public void stateChanged( ChangeEvent event )
- {
- int index = mTabbedPane.getSelectedIndex();
-
- Component component = mTabbedPane.getComponentAt( index );
-
- if( component instanceof ChannelSpectrumPanel )
- {
- mChannelSpectrumPanel.setEnabled( true );
- }
- else
- {
- mChannelSpectrumPanel.setEnabled( false );
- }
- }
- } );
-
/* Register each of the components to receive channel events when the
* channels are selected or change */
- mChannelModel.addListener( mCallEventPanel );
- mChannelModel.addListener( mChannelStateList );
- mChannelModel.addListener( mChannelSpectrumPanel );
- mChannelModel.addListener( mMessageActivityPanel );
-
- JScrollPane channelStateListScroll = new JScrollPane();
- channelStateListScroll.getViewport().setView( mChannelStateList );
- channelStateListScroll.setPreferredSize( new Dimension( 400, 500 ) );
+ mChannelModel.addListener(mCallEventPanel);
+ mChannelModel.addListener(mChannelStateList);
+ mChannelModel.addListener(mChannelSpectrumPanel);
+ mChannelModel.addListener(mMessageActivityPanel);
-// JideSplitPane audioChannelListSplit = new JideSplitPane( JideSplitPane.VERTICAL_SPLIT );
-// audioChannelListSplit.setDividerSize( 5 );
-// audioChannelListSplit.add( mAudioPanel );
-// audioChannelListSplit.add( channelStateListScroll );
+ JScrollPane channelStateListScroll = new JScrollPane();
+ channelStateListScroll.getViewport().setView(mChannelStateList);
+ channelStateListScroll.setPreferredSize(new Dimension(400, 500));
- //Set preferred size to influence the split between these panels
-// audioChannelListSplit.setPreferredSize( new Dimension( 400, 500 ) );
- mTabbedPane.setPreferredSize( new Dimension( 880, 500 ) );
-
- JideSplitPane channelSplit = new JideSplitPane( JideSplitPane.HORIZONTAL_SPLIT );
- channelSplit.setDividerSize( 5 );
- channelSplit.add( channelStateListScroll );
- channelSplit.add( mTabbedPane );
-
- add( channelSplit );
- }
+ //Set preferred size to influence the split between these panels
+ mTabbedPane.setPreferredSize(new Dimension(880, 500));
+
+ JideSplitPane channelSplit = new JideSplitPane(JideSplitPane.HORIZONTAL_SPLIT);
+ channelSplit.setDividerSize(5);
+ channelSplit.add(channelStateListScroll);
+ channelSplit.add(mTabbedPane);
+
+ add(channelSplit);
+ }
}
diff --git a/src/controller/ThreadPoolManager.java b/src/controller/ThreadPoolManager.java
index ca993295..d96cf9c6 100644
--- a/src/controller/ThreadPoolManager.java
+++ b/src/controller/ThreadPoolManager.java
@@ -44,6 +44,12 @@ public class ThreadPoolManager
public ScheduledExecutorService getScheduledExecutorService()
{
+ if( mExecutor == null )
+ {
+ mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
+ new NamingThreadFactory( "sdrtrunk" ) );
+ }
+
return mExecutor;
}
@@ -53,14 +59,10 @@ public class ThreadPoolManager
TimeUnit unit )
throws RejectedExecutionException
{
- if( mExecutor == null )
- {
- mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
- new NamingThreadFactory( "sdrtrunk" ) );
- }
+ ScheduledExecutorService executorService = getScheduledExecutorService();
ScheduledFuture> task =
- mExecutor.scheduleAtFixedRate( command, 0, period, unit );
+ executorService.scheduleAtFixedRate( command, 0, period, unit );
mTasks.put( task, type );
@@ -70,13 +72,9 @@ public class ThreadPoolManager
public void scheduleOnce( Runnable command, long delay, TimeUnit unit )
throws RejectedExecutionException
{
- if( mExecutor == null )
- {
- mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
- new NamingThreadFactory( "sdrtrunk" ) );
- }
+ ScheduledExecutorService executorService = getScheduledExecutorService();
- mExecutor.schedule( command, delay, unit );
+ executorService.schedule( command, delay, unit );
}
public boolean cancel( ScheduledFuture> task )
diff --git a/src/external/radioreference/RadioReferenceService.java b/src/external/radioreference/RadioReferenceService.java
new file mode 100644
index 00000000..df46e9f0
--- /dev/null
+++ b/src/external/radioreference/RadioReferenceService.java
@@ -0,0 +1,191 @@
+/*******************************************************************************
+ * 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 external.radioreference;
+
+import com.radioreference.api.soap2.AuthInfo;
+import com.radioreference.api.soap2.RRWsdlLocator;
+import com.radioreference.api.soap2.RRWsdlPortType;
+import com.radioreference.api.soap2.UserFeedBroadcast;
+import com.radioreference.api.soap2.UserInfo;
+import org.apache.axis.AxisFault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.xml.rpc.ServiceException;
+import java.rmi.RemoteException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class RadioReferenceService
+{
+ private final static Logger mLog = LoggerFactory.getLogger( RadioReferenceService.class );
+ private final SimpleDateFormat SDF = new SimpleDateFormat("MM-dd-yyyy");
+
+ public static final String SDRTRUNK_APP_KEY = "88969092";
+ public static final String RADIO_REFERENCE_API_VERSION = "14";
+ public static final String SOAP_RPC_RESPONSE_STYLE = "rpc";
+
+ private RRWsdlLocator mService = new RRWsdlLocator();
+ private RRWsdlPortType mClient;
+ private Long mAccountExpiration;
+
+ private AuthInfo mUserAuthorization;
+
+ /**
+ * Creates a SOAP web service client for interaction with the radioreference.com server.
+ *
+ * @param username for a premium subscriber
+ * @param password for a premium subscriber
+ * @throws ServiceException if the client can't be created
+ * @throws IllegalArgumentException if either the username or password is null or empty
+ */
+ public RadioReferenceService(String username, String password) throws ServiceException
+ {
+ mClient = mService.getRRWsdlPort();
+
+ setAuthorizationInformation(username, password);
+ }
+
+ /**
+ * Sets new premium subscriber user credentials to authenticate to the server.
+ * @param username for a premium subscriber on radioreference.com
+ * @param password for a premium subscriber
+ */
+ public void setAuthorizationInformation(String username, String password) throws IllegalArgumentException
+ {
+ if(username == null || username.isEmpty() || password == null || password.isEmpty())
+ {
+ throw new IllegalArgumentException("Username [" + username + "] and/or password [" + password +
+ "] cannot be null or empty");
+ }
+
+ mUserAuthorization = new AuthInfo(username, password, SDRTRUNK_APP_KEY, RADIO_REFERENCE_API_VERSION,
+ SOAP_RPC_RESPONSE_STYLE);
+ }
+
+ /**
+ * Indicates if the service has a non-null user authorization
+ */
+ public void checkCredentials() throws IllegalArgumentException
+ {
+ if(mUserAuthorization == null)
+ {
+ throw new IllegalStateException("Authorization information (username/password) required");
+ }
+ }
+
+ /**
+ * Account expiration date
+ *
+ * @return expiration date in millis since epoch
+ * @throws AxisFault for remote server errors
+ * @throws IllegalStateException for null or empty login credentials
+ * @throws RemoteException for web service client errors
+ */
+ public long getAccountExpirationDate() throws IllegalStateException, RemoteException
+ {
+ checkCredentials();
+
+ if(mAccountExpiration == null)
+ {
+ UserInfo userInfo = mClient.getUserData(mUserAuthorization);
+
+ if(userInfo != null)
+ {
+ String expiration = userInfo.getSubExpireDate();
+
+ if(expiration != null)
+ {
+ try
+ {
+ mAccountExpiration = SDF.parse(expiration).getTime();
+ }
+ catch(Exception e)
+ {
+ mLog.error("Couldn't parse account expiration date from web service response: " + expiration, e);
+ }
+ }
+ }
+ }
+
+ return mAccountExpiration;
+ }
+
+ /**
+ * Indicates if the radio reference user account is active (ie not expired)
+ *
+ * @throws IllegalStateException for null or empty login credentials
+ * @throws RemoteException for web service client errors
+ */
+ public boolean isAccountActive() throws IllegalStateException, RemoteException
+ {
+ Long expiration = getAccountExpirationDate();
+
+ if(expiration != null)
+ {
+ return System.currentTimeMillis() < expiration;
+ }
+
+ return false;
+ }
+
+ public UserFeedBroadcast[] getUserFeedBroadcasts() throws IllegalArgumentException, RemoteException
+ {
+ checkCredentials();
+
+ return mClient.getUserFeedBroadcasts(mUserAuthorization);
+ }
+
+
+ public static void main(String[] args)
+ {
+ try
+ {
+ RadioReferenceService service = new RadioReferenceService("dsheirer", "1dodgeram");
+
+ Long expiration = service.getAccountExpirationDate();
+
+ mLog.debug("Expiration Date:" + (expiration != null ? new Date(expiration).toString() : "unknown"));
+
+ mLog.debug("Is Active: " + service.isAccountActive());
+
+ UserFeedBroadcast[] feeds = service.getUserFeedBroadcasts();
+
+ for(UserFeedBroadcast feed: feeds)
+ {
+ mLog.debug("Feed ID: " + feed.getFeedId() + " Desc:" + feed.getDescr());
+ }
+ }
+ catch(AxisFault af)
+ {
+ mLog.debug("Hey, I caught an axis fault!");
+ mLog.debug("Reason:" + af.getFaultReason());
+ }
+ catch(RemoteException re)
+ {
+ re.printStackTrace();
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ mLog.debug("Finished!");
+ }
+}
diff --git a/src/gui/SDRTrunk.java b/src/gui/SDRTrunk.java
index 40bfb51a..db8cd501 100644
--- a/src/gui/SDRTrunk.java
+++ b/src/gui/SDRTrunk.java
@@ -1,56 +1,41 @@
/*******************************************************************************
* SDR Trunk
* 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 gui;
-import java.awt.AWTException;
-import java.awt.Desktop;
-import java.awt.Dimension;
-import java.awt.EventQueue;
-import java.awt.Robot;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.KeyEvent;
-import java.awt.image.BufferedImage;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.file.FileSystems;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.concurrent.TimeUnit;
-
-import javax.imageio.ImageIO;
-import javax.swing.JFrame;
-import javax.swing.JMenu;
-import javax.swing.JMenuBar;
-import javax.swing.JMenuItem;
-import javax.swing.JOptionPane;
-import javax.swing.JSeparator;
-import javax.swing.KeyStroke;
-
+import alias.AliasModel;
+import alias.action.AliasActionManager;
+import audio.AudioManager;
+import audio.broadcast.BroadcastModel;
+import com.jidesoft.swing.JideSplitPane;
+import controller.ControllerPanel;
+import controller.ThreadPoolManager;
+import controller.ThreadPoolManager.ThreadType;
+import controller.channel.ChannelEventListener;
+import controller.channel.ChannelModel;
+import controller.channel.ChannelProcessingManager;
+import controller.channel.ChannelSelectionManager;
+import controller.channel.map.ChannelMapModel;
import map.IconManager;
import map.MapService;
import module.log.EventLogManager;
import net.miginfocom.swing.MigLayout;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
import playlist.PlaylistManager;
import properties.SystemProperties;
import record.RecorderManager;
@@ -63,152 +48,155 @@ import source.tuner.TunerSpectralDisplayManager;
import source.tuner.configuration.TunerConfigurationModel;
import spectrum.SpectralDisplayPanel;
import util.TimeStamp;
-import alias.AliasModel;
-import alias.action.AliasActionManager;
-import audio.AudioManager;
-import com.jidesoft.swing.JideSplitPane;
-
-import controller.ControllerPanel;
-import controller.ThreadPoolManager;
-import controller.ThreadPoolManager.ThreadType;
-import controller.channel.ChannelEventListener;
-import controller.channel.ChannelModel;
-import controller.channel.ChannelProcessingManager;
-import controller.channel.ChannelSelectionManager;
-import controller.channel.map.ChannelMapModel;
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
public class SDRTrunk implements Listener
{
- private final static Logger mLog = LoggerFactory.getLogger( SDRTrunk.class );
+ private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
- private ControllerPanel mControllerPanel;
- private SettingsManager mSettingsManager;
- private SpectralDisplayPanel mSpectralPanel;
- private JFrame mMainGui = new JFrame();
-
- private String mTitle;
-
- private boolean mLogChannelAndMemoryUsage = false;
-
- public SDRTrunk()
+ private ControllerPanel mControllerPanel;
+ private SettingsManager mSettingsManager;
+ private SpectralDisplayPanel mSpectralPanel;
+ private JFrame mMainGui = new JFrame();
+
+ private String mTitle;
+
+ private boolean mLogChannelAndMemoryUsage = false;
+
+ public SDRTrunk()
{
- mLog.info( "" );
- mLog.info( "" );
- mLog.info( "*******************************************************************" );
- mLog.info( "**** sdrtrunk: a trunked radio and digital decoding application ***" );
- mLog.info( "**** website: https://github.com/dsheirer/sdrtrunk ***" );
- mLog.info( "*******************************************************************" );
- mLog.info( "" );
- mLog.info( "" );
-
- //Setup the application home directory
- Path home = getHomePath();
-
- mLog.info( "Home path: " + home.toString() );
+ mLog.info("");
+ mLog.info("");
+ mLog.info("*******************************************************************");
+ mLog.info("**** sdrtrunk: a trunked radio and digital decoding application ***");
+ mLog.info("**** website: https://github.com/dsheirer/sdrtrunk ***");
+ mLog.info("*******************************************************************");
+ mLog.info("");
+ mLog.info("");
- //Load properties file
- if( home != null )
- {
- loadProperties( home );
- }
-
- //Log current properties setting
- SystemProperties.getInstance().logCurrentSettings();
-
- TunerConfigurationModel tunerConfigurationModel = new TunerConfigurationModel();
- TunerModel tunerModel = new TunerModel( tunerConfigurationModel );
-
- ThreadPoolManager threadPoolManager = new ThreadPoolManager();
-
- mSettingsManager = new SettingsManager( threadPoolManager, tunerConfigurationModel );
+ //Setup the application home directory
+ Path home = getHomePath();
- AliasModel aliasModel = new AliasModel();
+ mLog.info("Home path: " + home.toString());
- ChannelModel channelModel = new ChannelModel();
-
- ChannelMapModel channelMapModel = new ChannelMapModel();
+ //Load properties file
+ if (home != null)
+ {
+ loadProperties(home);
+ }
- EventLogManager eventLogManager = new EventLogManager();
+ //Log current properties setting
+ SystemProperties.getInstance().logCurrentSettings();
- RecorderManager recorderManager = new RecorderManager( threadPoolManager );
-
- SourceManager sourceManager = new SourceManager( tunerModel,
- mSettingsManager, threadPoolManager );
-
- ChannelProcessingManager channelProcessingManager = new ChannelProcessingManager(
- channelModel, channelMapModel, aliasModel, eventLogManager, recorderManager, sourceManager );
- channelProcessingManager.addAudioPacketListener( recorderManager );
-
- channelModel.addListener( channelProcessingManager );
-
- ChannelSelectionManager channelSelectionManager =
- new ChannelSelectionManager( channelModel );
- channelModel.addListener( channelSelectionManager );
-
- AliasActionManager aliasActionManager = new AliasActionManager( threadPoolManager );
- channelProcessingManager.addMessageListener( aliasActionManager );
-
- AudioManager audioManager = new AudioManager( threadPoolManager, sourceManager.getMixerManager() );
- channelProcessingManager.addAudioPacketListener( audioManager );
+ TunerConfigurationModel tunerConfigurationModel = new TunerConfigurationModel();
+ TunerModel tunerModel = new TunerModel(tunerConfigurationModel);
- MapService mapService = new MapService( mSettingsManager );
- channelProcessingManager.addMessageListener( mapService );
+ ThreadPoolManager threadPoolManager = new ThreadPoolManager();
- mControllerPanel = new ControllerPanel( audioManager, aliasModel,
- channelModel, channelMapModel, channelProcessingManager,
- mapService, mSettingsManager, sourceManager, tunerModel );
+ mSettingsManager = new SettingsManager(threadPoolManager, tunerConfigurationModel);
- mSpectralPanel = new SpectralDisplayPanel( channelModel,
- channelProcessingManager, mSettingsManager );
+ AliasModel aliasModel = new AliasModel();
- TunerSpectralDisplayManager tunerSpectralDisplayManager =
- new TunerSpectralDisplayManager( mSpectralPanel,
- channelModel, channelProcessingManager, mSettingsManager );
- tunerModel.addListener( tunerSpectralDisplayManager );
- tunerModel.addListener( this );
-
- PlaylistManager playlistManager = new PlaylistManager( threadPoolManager,
- aliasModel, channelModel, channelMapModel );
+ ChannelModel channelModel = new ChannelModel();
- playlistManager.init();
+ ChannelMapModel channelMapModel = new ChannelMapModel();
- if( mLogChannelAndMemoryUsage )
- {
- Runnable cml = new ChannelMemoryLogger();
- channelModel.addListener( (ChannelEventListener)cml );
- threadPoolManager.scheduleFixedRate(
- ThreadType.DECODER, cml, 5, TimeUnit.SECONDS );
- }
+ EventLogManager eventLogManager = new EventLogManager();
- mLog.info( "starting main application gui" );
-
- //Initialize the GUI
+ RecorderManager recorderManager = new RecorderManager(threadPoolManager);
+
+ SourceManager sourceManager = new SourceManager(tunerModel,
+ mSettingsManager, threadPoolManager);
+
+ ChannelProcessingManager channelProcessingManager = new ChannelProcessingManager(
+ channelModel, channelMapModel, aliasModel, eventLogManager, recorderManager, sourceManager);
+ channelProcessingManager.addAudioPacketListener(recorderManager);
+
+ channelModel.addListener(channelProcessingManager);
+
+ ChannelSelectionManager channelSelectionManager =
+ new ChannelSelectionManager(channelModel);
+ channelModel.addListener(channelSelectionManager);
+
+ AliasActionManager aliasActionManager = new AliasActionManager(threadPoolManager);
+ channelProcessingManager.addMessageListener(aliasActionManager);
+
+ AudioManager audioManager = new AudioManager(threadPoolManager, sourceManager.getMixerManager());
+ channelProcessingManager.addAudioPacketListener(audioManager);
+
+ BroadcastModel broadcastModel = new BroadcastModel(threadPoolManager, mSettingsManager);
+
+ channelProcessingManager.addAudioPacketListener(broadcastModel);
+
+ MapService mapService = new MapService(mSettingsManager);
+ channelProcessingManager.addMessageListener(mapService);
+
+ mControllerPanel = new ControllerPanel(audioManager, aliasModel, broadcastModel,
+ channelModel, channelMapModel, channelProcessingManager,
+ mapService, mSettingsManager, sourceManager, tunerModel);
+
+ mSpectralPanel = new SpectralDisplayPanel(channelModel,
+ channelProcessingManager, mSettingsManager);
+
+ TunerSpectralDisplayManager tunerSpectralDisplayManager =
+ new TunerSpectralDisplayManager(mSpectralPanel,
+ channelModel, channelProcessingManager, mSettingsManager);
+ tunerModel.addListener(tunerSpectralDisplayManager);
+ tunerModel.addListener(this);
+
+ PlaylistManager playlistManager = new PlaylistManager(threadPoolManager,
+ aliasModel, broadcastModel, channelModel, channelMapModel);
+
+ playlistManager.init();
+
+ if (mLogChannelAndMemoryUsage)
+ {
+ Runnable cml = new ChannelMemoryLogger();
+ channelModel.addListener((ChannelEventListener) cml);
+ threadPoolManager.scheduleFixedRate(
+ ThreadType.DECODER, cml, 5, TimeUnit.SECONDS);
+ }
+
+ mLog.info("starting main application gui");
+
+ //Initialize the GUI
initGUI();
-
- tunerModel.requestFirstTunerDisplay();
+
+ tunerModel.requestFirstTunerDisplay();
//Start the gui
- EventQueue.invokeLater( new Runnable()
+ EventQueue.invokeLater(new Runnable()
{
public void run()
{
try
{
- mMainGui.setVisible( true );
+ mMainGui.setVisible(true);
}
- catch( Exception e )
+ catch (Exception e)
{
e.printStackTrace();
}
}
- } );
+ });
}
/**
* Launch the application.
*/
- public static void main( String[] args )
+ public static void main(String[] args)
{
new SDRTrunk();
}
@@ -218,264 +206,239 @@ public class SDRTrunk implements Listener
*/
private void initGUI()
{
- mMainGui.setLayout( new MigLayout( "insets 0 0 0 0 ",
- "[grow,fill]",
- "[grow,fill]") );
-
- /**
- * Setup main JFrame window
- */
- mTitle = getTitle();
- mMainGui.setTitle( mTitle );
- mMainGui.setBounds( 100, 100, 1280, 800 );
- mMainGui.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
+ mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ",
+ "[grow,fill]",
+ "[grow,fill]"));
- //Set preferred sizes to influence the split
- mSpectralPanel.setPreferredSize( new Dimension( 1280, 300 ) );
- mControllerPanel.setPreferredSize( new Dimension( 1280, 500 ) );
-
- JideSplitPane splitPane = new JideSplitPane( JideSplitPane.VERTICAL_SPLIT );
- splitPane.setDividerSize( 5 );
- splitPane.add( mSpectralPanel );
- splitPane.add( mControllerPanel );
+ /**
+ * Setup main JFrame window
+ */
+ mTitle = SystemProperties.getInstance().getApplicationName();
+ mMainGui.setTitle(mTitle);
+ mMainGui.setBounds(100, 100, 1280, 800);
+ mMainGui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ //Set preferred sizes to influence the split
+ mSpectralPanel.setPreferredSize(new Dimension(1280, 300));
+ mControllerPanel.setPreferredSize(new Dimension(1280, 500));
+
+ JideSplitPane splitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
+ splitPane.setDividerSize(5);
+ splitPane.add(mSpectralPanel);
+ splitPane.add(mControllerPanel);
+
+ mMainGui.add(splitPane, "cell 0 0,span,grow");
- mMainGui.add( splitPane, "cell 0 0,span,grow");
-
/**
* Menu items
*/
JMenuBar menuBar = new JMenuBar();
- mMainGui.setJMenuBar( menuBar );
-
- JMenu fileMenu = new JMenu( "File" );
- menuBar.add( fileMenu );
+ mMainGui.setJMenuBar(menuBar);
- JMenuItem logFilesMenu = new JMenuItem( "Logs & Recordings" );
- logFilesMenu.addActionListener( new ActionListener()
- {
- @Override
- public void actionPerformed( ActionEvent arg0 )
- {
- try
- {
- Desktop.getDesktop().open( getHomePath().toFile() );
- }
- catch( Exception e )
- {
- mLog.error( "Couldn't open file explorer" );
-
- JOptionPane.showMessageDialog( mMainGui,
- "Can't launch file explorer - files are located at: " +
- getHomePath().toString(),
- "Can't launch file explorer",
- JOptionPane.ERROR_MESSAGE );
- }
- }
- } );
- fileMenu.add( logFilesMenu );
-
- JMenuItem settingsMenu = new JMenuItem( "Icon Manager" );
- settingsMenu.addActionListener( new ActionListener()
+ JMenu fileMenu = new JMenu("File");
+ menuBar.add(fileMenu);
+
+ JMenuItem logFilesMenu = new JMenuItem("Logs & Recordings");
+ logFilesMenu.addActionListener(new ActionListener()
{
- @Override
- public void actionPerformed( ActionEvent arg0 )
+ @Override
+ public void actionPerformed(ActionEvent arg0)
{
- final IconManager iconManager = new IconManager( mSettingsManager, mMainGui );
-
- EventQueue.invokeLater( new Runnable()
- {
- @Override
- public void run()
- {
- iconManager.setVisible( true );
- }
- } );
- }
- } );
- fileMenu.add( settingsMenu );
-
- fileMenu.add( new JSeparator() );
-
- JMenuItem exitMenu = new JMenuItem( "Exit" );
- exitMenu.addActionListener(
- new ActionListener()
- {
- public void actionPerformed( ActionEvent event )
- {
- System.exit( 0 );
- }
- }
- );
-
- fileMenu.add( exitMenu );
-
- JMenuItem screenCaptureItem = new JMenuItem( "Screen Capture" );
-
- screenCaptureItem.setMnemonic( KeyEvent.VK_C );
- screenCaptureItem.setAccelerator(
- KeyStroke.getKeyStroke( KeyEvent.VK_C, ActionEvent.ALT_MASK ) );
-
- screenCaptureItem.addActionListener( new ActionListener()
- {
- @Override
- public void actionPerformed( ActionEvent arg0 )
- {
- try
+ try
{
- Robot robot = new Robot();
-
- final BufferedImage image =
- robot.createScreenCapture( mMainGui.getBounds() );
-
- SystemProperties props = SystemProperties.getInstance();
-
- Path capturePath = props.getApplicationFolder( "screen_captures" );
-
- if( !Files.exists( capturePath ) )
- {
- try
- {
- Files.createDirectory( capturePath );
- }
- catch ( IOException e )
- {
- mLog.error( "Couldn't create 'screen_captures' "
- + "subdirectory in the " +
- "SDRTrunk application directory", e );
- }
- }
-
- String filename = TimeStamp.getTimeStamp( "_" ) +
- "_screen_capture.png";
+ Desktop.getDesktop().open(getHomePath().toFile());
+ }
+ catch (Exception e)
+ {
+ mLog.error("Couldn't open file explorer");
- final Path captureFile = capturePath.resolve( filename );
+ JOptionPane.showMessageDialog(mMainGui,
+ "Can't launch file explorer - files are located at: " +
+ getHomePath().toString(),
+ "Can't launch file explorer",
+ JOptionPane.ERROR_MESSAGE);
+ }
+ }
+ });
+ fileMenu.add(logFilesMenu);
- EventQueue.invokeLater( new Runnable()
- {
- @Override
+ JMenuItem settingsMenu = new JMenuItem("Icon Manager");
+ settingsMenu.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent arg0)
+ {
+ final IconManager iconManager = new IconManager(mSettingsManager, mMainGui);
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ iconManager.setVisible(true);
+ }
+ });
+ }
+ });
+ fileMenu.add(settingsMenu);
+
+ fileMenu.add(new JSeparator());
+
+ JMenuItem exitMenu = new JMenuItem("Exit");
+ exitMenu.addActionListener(
+ new ActionListener()
+ {
+ public void actionPerformed(ActionEvent event)
+ {
+ System.exit(0);
+ }
+ }
+ );
+
+ fileMenu.add(exitMenu);
+
+ JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
+
+ screenCaptureItem.setMnemonic(KeyEvent.VK_C);
+ screenCaptureItem.setAccelerator(
+ KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
+
+ screenCaptureItem.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent arg0)
+ {
+ try
+ {
+ Robot robot = new Robot();
+
+ final BufferedImage image =
+ robot.createScreenCapture(mMainGui.getBounds());
+
+ SystemProperties props = SystemProperties.getInstance();
+
+ Path capturePath = props.getApplicationFolder("screen_captures");
+
+ if (!Files.exists(capturePath))
+ {
+ try
+ {
+ Files.createDirectory(capturePath);
+ }
+ catch (IOException e)
+ {
+ mLog.error("Couldn't create 'screen_captures' "
+ + "subdirectory in the " +
+ "SDRTrunk application directory", e);
+ }
+ }
+
+ String filename = TimeStamp.getTimeStamp("_") +
+ "_screen_capture.png";
+
+ final Path captureFile = capturePath.resolve(filename);
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
public void run()
{
- try
+ try
{
- ImageIO.write( image, "png",
- captureFile.toFile() );
+ ImageIO.write(image, "png",
+ captureFile.toFile());
}
- catch ( IOException e )
+ catch (IOException e)
{
- mLog.error( "Couldn't write screen capture to "
- + "file [" + captureFile.toString() + "]", e );
+ mLog.error("Couldn't write screen capture to "
+ + "file [" + captureFile.toString() + "]", e);
}
- }} );
+ }
+ });
}
- catch ( AWTException e )
+ catch (AWTException e)
{
- mLog.error( "Exception while taking screen capture", e );
+ mLog.error("Exception while taking screen capture", e);
}
}
- } );
-
- menuBar.add( screenCaptureItem );
+ });
+
+ menuBar.add(screenCaptureItem);
}
-
+
/**
* Loads the application properties file from the user's home directory,
* creating the properties file for the first-time, if necessary
*/
- private void loadProperties( Path homePath )
+ private void loadProperties(Path homePath)
{
- Path propsPath = homePath.resolve( "SDRTrunk.properties" );
+ Path propsPath = homePath.resolve("SDRTrunk.properties");
- if( !Files.exists( propsPath ) )
- {
- try
+ if (!Files.exists(propsPath))
+ {
+ try
{
- mLog.info( "SDRTrunk - creating application properties file [" +
- propsPath.toAbsolutePath() + "]" );
+ mLog.info("SDRTrunk - creating application properties file [" +
+ propsPath.toAbsolutePath() + "]");
- Files.createFile( propsPath );
+ Files.createFile(propsPath);
}
- catch ( IOException e )
+ catch (IOException e)
{
- mLog.error( "SDRTrunk - couldn't create application properties "
- + "file [" + propsPath.toAbsolutePath(), e );
+ mLog.error("SDRTrunk - couldn't create application properties "
+ + "file [" + propsPath.toAbsolutePath(), e);
}
- }
-
- if( Files.exists( propsPath ) )
- {
- SystemProperties.getInstance().load( propsPath );
- }
- else
- {
- mLog.error( "SDRTrunk - couldn't find or recreate the SDRTrunk " +
- "application properties file" );
- }
+ }
+
+ if (Files.exists(propsPath))
+ {
+ SystemProperties.getInstance().load(propsPath);
+ }
+ else
+ {
+ mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk " +
+ "application properties file");
+ }
}
/**
- * Gets (or creates) the SDRTRunk application home directory.
- *
- * Note: the user can change this setting to allow log files and other
+ * Gets (or creates) the SDRTRunk application home directory.
+ *
+ * Note: the user can change this setting to allow log files and other
* files to reside elsewhere on the file system.
*/
private Path getHomePath()
{
- Path homePath = FileSystems.getDefault()
- .getPath( System.getProperty( "user.home" ), "SDRTrunk" );
+ Path homePath = FileSystems.getDefault()
+ .getPath(System.getProperty("user.home"), "SDRTrunk");
- if( !Files.exists( homePath ) )
- {
- try
- {
- Files.createDirectory( homePath );
-
- mLog.info( "SDRTrunk - created application home directory [" +
- homePath.toString() + "]" );
- }
- catch ( Exception e )
- {
- homePath = null;
-
- mLog.error( "SDRTrunk: exception while creating SDRTrunk home " +
- "directory in the user's home directory", e );
- }
- }
+ if (!Files.exists(homePath))
+ {
+ try
+ {
+ Files.createDirectory(homePath);
- return homePath;
+ mLog.info("SDRTrunk - created application home directory [" +
+ homePath.toString() + "]");
+ }
+ catch (Exception e)
+ {
+ homePath = null;
+
+ mLog.error("SDRTrunk: exception while creating SDRTrunk home " +
+ "directory in the user's home directory", e);
+ }
+ }
+
+ return homePath;
}
-
- private String getTitle()
+
+ @Override
+ public void receive(TunerEvent event)
{
- StringBuilder sb = new StringBuilder();
-
- sb.append( "sdrtrunk" );
-
- try( BufferedReader reader = new BufferedReader(
- new InputStreamReader( this.getClass()
- .getResourceAsStream( "/sdrtrunk-version" ) ) ) )
- {
- String version = reader.readLine();
-
- if( version != null )
- {
- sb.append( " V" );
- sb.append( version );
- }
- }
- catch( Exception e )
- {
- mLog.error( "Couldn't read sdrtrunk version from application jar file" );
- }
-
- return sb.toString();
+ if (event.getEvent() == TunerEvent.Event.REQUEST_MAIN_SPECTRAL_DISPLAY)
+ {
+ mMainGui.setTitle(mTitle + " - " + event.getTuner().getName());
+ }
}
-
- @Override
- public void receive( TunerEvent event )
- {
- if( event.getEvent() == TunerEvent.Event.REQUEST_MAIN_SPECTRAL_DISPLAY )
- {
- mMainGui.setTitle( mTitle + " - " + event.getTuner().getName() );
- }
- }
}
diff --git a/src/instrument/gui/DecoderSelectionFrame.java b/src/instrument/gui/DecoderSelectionFrame.java
index ccd310c8..23239335 100644
--- a/src/instrument/gui/DecoderSelectionFrame.java
+++ b/src/instrument/gui/DecoderSelectionFrame.java
@@ -8,6 +8,7 @@ import javax.swing.JButton;
import javax.swing.JDesktopPane;
import javax.swing.JInternalFrame;
+import audio.broadcast.BroadcastModel;
import module.decode.DecodeConfigurationEditor;
import net.miginfocom.swing.MigLayout;
import playlist.PlaylistManager;
@@ -30,13 +31,14 @@ public class DecoderSelectionFrame extends JInternalFrame
public DecoderSelectionFrame( JDesktopPane desktop,
IControllableFileSource source )
{
+ ThreadPoolManager tpm = new ThreadPoolManager();
+
AliasModel aliasModel = new AliasModel();
+ BroadcastModel broadcastModel = new BroadcastModel(tpm, null);
ChannelModel channelModel = new ChannelModel();
ChannelMapModel channelMapModel = new ChannelMapModel();
-
- ThreadPoolManager tpm = new ThreadPoolManager();
-
- mPlaylistManager = new PlaylistManager( tpm, aliasModel, channelModel, channelMapModel );
+
+ mPlaylistManager = new PlaylistManager( tpm, aliasModel, broadcastModel, channelModel, channelMapModel );
mDesktop = desktop;
mSource = source;
diff --git a/src/module/decode/p25/P25DecoderEditor.java b/src/module/decode/p25/P25DecoderEditor.java
index e647e407..3d54ace1 100644
--- a/src/module/decode/p25/P25DecoderEditor.java
+++ b/src/module/decode/p25/P25DecoderEditor.java
@@ -137,7 +137,6 @@ public class P25DecoderEditor extends ValidatingEditor
@Override
public void validate( Editor editor ) throws EditorValidationException
{
- mLog.debug( "Validating editor:" + editor.getClass() );
/**
* Enforce CQPSK modulation uses a Tuner Source for I/Q sample data
*/
diff --git a/src/module/decode/p25/message/pdu/osp/control/AdjacentStatusBroadcastExtended.java b/src/module/decode/p25/message/pdu/osp/control/AdjacentStatusBroadcastExtended.java
index 1334c1ab..de1183cd 100644
--- a/src/module/decode/p25/message/pdu/osp/control/AdjacentStatusBroadcastExtended.java
+++ b/src/module/decode/p25/message/pdu/osp/control/AdjacentStatusBroadcastExtended.java
@@ -65,14 +65,14 @@ public class AdjacentStatusBroadcastExtended extends PDUMessage
sb.append( " RFSS:" + getRFSS() );
sb.append( " SITE:" + getSiteID() );
-
- sb.append( " CTRL CHAN:" + getReceiveIdentifier() + "-" + getReceiveChannel() );
-
+
+ sb.append( " CTRL CHAN:" + getTransmitIdentifier() + "-" + getTransmitChannel() );
+
sb.append( " DN:" + getDownlinkFrequency() );
-
- sb.append( " " + getTransmitIdentifier() + "-" + getTransmitChannel() );
-
- sb.append( " UP:" + getUplinkFrequency() );
+
+ sb.append( " " + getReceiveIdentifier() + "-" + getReceiveChannel() );
+
+ sb.append( " UP:" + getUplinkFrequency() );
sb.append( " SYS SVC CLASS:" + getSystemServiceClass() );
diff --git a/src/network.java b/src/network.java
new file mode 100644
index 00000000..b7a93210
--- /dev/null
+++ b/src/network.java
@@ -0,0 +1,21 @@
+/*******************************************************************************
+ * 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
+ *
+ ******************************************************************************/
+public class network
+{
+}
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/playlist/PlaylistManager.java b/src/playlist/PlaylistManager.java
index df075a70..d50f7e03 100644
--- a/src/playlist/PlaylistManager.java
+++ b/src/playlist/PlaylistManager.java
@@ -33,6 +33,8 @@ import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
+import audio.broadcast.BroadcastEvent;
+import audio.broadcast.BroadcastModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,6 +58,7 @@ public class PlaylistManager implements ChannelEventListener
private ThreadPoolManager mThreadPoolManager;
private AliasModel mAliasModel;
+ private BroadcastModel mBroadcastModel;
private ChannelModel mChannelModel;
private ChannelMapModel mChannelMapModel;
private Path mCurrentPlaylistPath;
@@ -74,11 +77,13 @@ public class PlaylistManager implements ChannelEventListener
*/
public PlaylistManager( ThreadPoolManager threadPoolManager,
AliasModel aliasModel,
+ BroadcastModel broadcastModel,
ChannelModel channelModel,
- ChannelMapModel channelMapModel )
+ ChannelMapModel channelMapModel)
{
mThreadPoolManager = threadPoolManager;
mAliasModel = aliasModel;
+ mBroadcastModel = broadcastModel;
mChannelModel = channelModel;
mChannelMapModel = channelMapModel;
@@ -103,6 +108,15 @@ public class PlaylistManager implements ChannelEventListener
schedulePlaylistSave();
}
} );
+
+ mBroadcastModel.addListener(new Listener()
+ {
+ @Override
+ public void receive(BroadcastEvent broadcastEvent)
+ {
+ schedulePlaylistSave();
+ }
+ });
}
/**
@@ -166,6 +180,7 @@ public class PlaylistManager implements ChannelEventListener
mPlaylistLoading = true;
mAliasModel.addAliases( playlist.getAliases() );
+ mBroadcastModel.addBroadcastConfigurations(playlist.getBroadcastConfigurations());
mChannelModel.addChannels( playlist.getChannels() );
mChannelMapModel.addChannelMaps( playlist.getChannelMaps() );
@@ -219,6 +234,7 @@ public class PlaylistManager implements ChannelEventListener
PlaylistV2 playlist = new PlaylistV2();
playlist.setAliases( mAliasModel.getAliases() );
+ playlist.setBroadcastConfigurations(mBroadcastModel.getBroadcastConfigurations());
playlist.setChannels( mChannelModel.getChannels() );
playlist.setChannelMaps( mChannelMapModel.getChannelMaps() );
@@ -277,7 +293,7 @@ public class PlaylistManager implements ChannelEventListener
}
catch ( Exception e )
{
- mLog.error( "coulcn't open outputstream to save playlist [" +
+ mLog.error( "couldn't open outputstream to save playlist [" +
mCurrentPlaylistPath.toString() + "]", e );
}
finally
@@ -330,7 +346,7 @@ public class PlaylistManager implements ChannelEventListener
}
catch ( Exception e )
{
- mLog.error( "coulcn't open inputstream to load playlist [" + playlistPath.toString() + "]", e );
+ mLog.error( "couldn't open input stream to load playlist [" + playlistPath.toString() + "]", e );
}
finally
{
diff --git a/src/playlist/PlaylistV2.java b/src/playlist/PlaylistV2.java
index aa3fa2fe..584ba6f6 100644
--- a/src/playlist/PlaylistV2.java
+++ b/src/playlist/PlaylistV2.java
@@ -17,24 +17,23 @@
******************************************************************************/
package playlist;
-import java.util.ArrayList;
-import java.util.List;
+import alias.Alias;
+import audio.broadcast.BroadcastConfiguration;
+import controller.channel.Channel;
+import controller.channel.map.ChannelMap;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
+import java.util.ArrayList;
+import java.util.List;
-import alias.Alias;
-import controller.channel.Channel;
-import controller.channel.map.ChannelMap;
-
-@XmlSeeAlso( { Alias.class,Channel.class,ChannelMap.class } )
-
+@XmlSeeAlso( { Alias.class,Channel.class,ChannelMap.class,BroadcastConfiguration.class} )
@XmlRootElement( name = "playlist" )
public class PlaylistV2
{
- //Version 2 member variables
private List mAliases = new ArrayList<>();
+ private List mBroadcastConfigurations = new ArrayList<>();
private List mChannels = new ArrayList<>();
private List mChannelMaps = new ArrayList<>();
@@ -74,4 +73,15 @@ public class PlaylistV2
{
mChannelMaps = channelMaps;
}
+
+ @XmlElement(name = "stream")
+ public List getBroadcastConfigurations()
+ {
+ return mBroadcastConfigurations;
+ }
+
+ public void setBroadcastConfigurations(List configurations)
+ {
+ mBroadcastConfigurations = configurations;
+ }
}
diff --git a/src/playlist/version1/AliasListOld.java b/src/playlist/version1/AliasListOld.java
index 7df3c6be..199c8db9 100644
--- a/src/playlist/version1/AliasListOld.java
+++ b/src/playlist/version1/AliasListOld.java
@@ -153,7 +153,7 @@ public class AliasListOld implements Comparable
mESN.put( esn.getEsn(), alias );
}
break;
- case Fleetsync:
+ case FLEETSYNC:
FleetsyncID fs = (FleetsyncID)id;
if( fs.getIdent().contains( "*" ) )
@@ -167,7 +167,7 @@ public class AliasListOld implements Comparable
mFleetsync.put( fs.getIdent(), alias );
}
break;
- case LoJack:
+ case LOJACK:
mLoJack.put( (LoJackFunctionAndID)id, alias );
break;
case MDC1200:
@@ -218,12 +218,12 @@ public class AliasListOld implements Comparable
mMobileID.put( min.getMin(), alias );
}
break;
- case LTRNetUID:
+ case LTR_NET_UID:
UniqueID uid = (UniqueID)id;
mUniqueID.put( uid.getUid(), alias );
break;
- case Site:
+ case SITE:
SiteID siteID = (SiteID)id;
if( siteID.getSite().contains( "*" ) )
@@ -237,10 +237,10 @@ public class AliasListOld implements Comparable
}
break;
- case Status:
+ case STATUS:
mStatus.put( ((StatusID)id).getStatus(), alias );
break;
- case Talkgroup:
+ case TALKGROUP:
TalkgroupID tgid = (TalkgroupID)id;
if( tgid.getTalkgroup().contains( "*" ) )
@@ -255,8 +255,8 @@ public class AliasListOld implements Comparable
mTalkgroup.put( tgid.getTalkgroup(), alias );
}
break;
- case NonRecordable:
- case Priority:
+ case NON_RECORDABLE:
+ case PRIORITY:
//We don't maintain lookups for these items
break;
default:
diff --git a/src/properties/SystemProperties.java b/src/properties/SystemProperties.java
index 38837ed5..de25de05 100644
--- a/src/properties/SystemProperties.java
+++ b/src/properties/SystemProperties.java
@@ -17,11 +17,13 @@
******************************************************************************/
package properties;
+import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -47,7 +49,7 @@ public class SystemProperties
private static SystemProperties mInstance;
private static Properties mProperties;
private Path mPropertiesPath;
-
+ private String mApplicationName;
private SystemProperties()
{
@@ -67,7 +69,7 @@ public class SystemProperties
return mInstance;
}
-
+
/**
* Saves any currently changed settings to the application properties file
*/
@@ -86,9 +88,6 @@ public class SystemProperties
"SDRTrunk - SDR Trunking Decoder Application Settings";
mProperties.store( out, comments );
-
-// mLog.info( "SystemProperties - saved current properties [" +
-// propsPath.toString() + "]" );
}
catch( Exception e )
{
@@ -216,6 +215,37 @@ public class SystemProperties
propertiesPath.toString() + "]" );
}
+ public String getApplicationName()
+ {
+ if(mApplicationName == null)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append( "sdrtrunk" );
+
+ try(BufferedReader reader = new BufferedReader(
+ new InputStreamReader( this.getClass()
+ .getResourceAsStream( "/sdrtrunk-version" ) ) ) )
+ {
+ String version = reader.readLine();
+
+ if( version != null )
+ {
+ sb.append( " V" );
+ sb.append( version );
+ }
+ }
+ catch( Exception e )
+ {
+ mLog.error( "Couldn't read sdrtrunk version from application jar file" );
+ }
+
+ mApplicationName = sb.toString();
+ }
+
+ return mApplicationName;
+ }
+
/**
* Returns the value of the property, or null if the property doesn't exist
*/
diff --git a/src/record/AudioRecorder.java b/src/record/AudioRecorder.java
new file mode 100644
index 00000000..61f48158
--- /dev/null
+++ b/src/record/AudioRecorder.java
@@ -0,0 +1,322 @@
+/*******************************************************************************
+ * 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;
+
+import audio.AudioPacket;
+import audio.IAudioPacketListener;
+import audio.metadata.AudioMetadata;
+import module.Module;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+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;
+
+public abstract class AudioRecorder extends Module implements Listener, IAudioPacketListener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(AudioRecorder.class);
+
+ private LinkedBlockingQueue mAudioPacketQueue = new LinkedBlockingQueue<>(500);
+ private List mPacketsToProcess = new ArrayList<>();
+
+ private FileOutputStream mFileOutputStream;
+ private AtomicBoolean mRunning = new AtomicBoolean();
+
+ protected Path mPath;
+ protected AudioMetadata mAudioMetadata;
+ protected long mTimeRecordingStart;
+ protected long mTimeLastPacketReceived;
+ private BufferProcessor mBufferProcessor;
+ private ScheduledFuture> mProcessorHandle;
+ private Listener mRecordingClosedListener;
+
+ private long mSampleCount;
+
+ /**
+ * Abstract audio recorder that implements audio packet queueing and threaded audio conversion/writing to a file
+ *
+ * @param path for the output recording
+ */
+ public AudioRecorder(Path path)
+ {
+ mPath = path;
+ }
+
+ /**
+ * Path for the audio recording file
+ */
+ public Path getPath()
+ {
+ return mPath;
+ }
+
+ /**
+ * Latest audio metadata received for this recording
+ */
+ public AudioMetadata getMetadata()
+ {
+ return mAudioMetadata;
+ }
+
+ /**
+ * Timestamp of the last buffer received by this recorder - allows this recorder to be monitored for automatic
+ * closure after a time period has elapsed.
+ */
+ public long getTimeLastPacketReceived()
+ {
+ return mTimeLastPacketReceived;
+ }
+
+ public long getTimeRecordingStart()
+ {
+ return mTimeRecordingStart;
+ }
+
+ /**
+ * Recording length in milliseconds
+ */
+ public long getRecordingLength()
+ {
+ //Assumes audio sample rate of 8000 samples/second or 8 samples/milli-second
+ return mSampleCount / 8;
+ }
+
+ /**
+ * Implements the IAudioPacketListener interface and simply redirects to the Listener interface.
+ * This is necessary since you can't have multiple methods with the same erasure (ie Listener) in the
+ * parent module class.
+ */
+ @Override
+ public Listener getAudioPacketListener()
+ {
+ return this;
+ }
+
+ /**
+ * Processes the audio packet and captures the latest AudioMetadata for the recording for easy access.
+ */
+ @Override
+ public void receive(AudioPacket audioPacket)
+ {
+ if(mRunning.get())
+ {
+ mTimeRecordingStart = System.currentTimeMillis();
+ mTimeLastPacketReceived = mTimeRecordingStart;
+
+ if(audioPacket.hasAudioMetadata())
+ {
+ mAudioMetadata = audioPacket.getAudioMetadata();
+ }
+
+ boolean success = mAudioPacketQueue.offer(audioPacket);
+
+ if (!success)
+ {
+ mLog.error("recorder buffer overflow - stopping recorder [" + getPath().toString() + "]");
+ stop();
+ }
+ }
+ }
+
+ /**
+ * File output stream for the current recording. Intended to allow sub-classes to write binary data to the file.
+ */
+ protected OutputStream getOutputStream()
+ {
+ return mFileOutputStream;
+ }
+
+ /**
+ * Stops the recorder and flags the recording to be closed. Use this method if you do not need any details about
+ * the final recording. Otherwise, use the close() method and register a closing listener.
+ */
+ public void stop()
+ {
+ close(null);
+ }
+
+ /**
+ * Closes the recording file. Upon successful closing of the recording file, the listener is notified that the
+ * audio recorder is closed.
+ */
+ public void close(Listener listener)
+ {
+ mRecordingClosedListener = listener;
+
+ if(!mRunning.compareAndSet(true, false))
+ {
+ //If this recorder has already been stopped/closed, immediately inform the listener
+ mRecordingClosedListener.receive(this);
+ }
+ }
+
+
+ /**
+ * Records the list of audio packets in the sub-class specific audio format.
+ */
+ protected abstract void record(List audioPackets) throws IOException;
+
+ /**
+ * Starts this recorder as a scheduled thread running under the executor argument
+ *
+ * @param executor to use in scheduling audio conversion and file writes.
+ */
+ public void start(ScheduledExecutorService executor)
+ {
+ if (mRunning.compareAndSet(false, true))
+ {
+ mTimeLastPacketReceived = System.currentTimeMillis();
+
+ if (mBufferProcessor == null)
+ {
+ mBufferProcessor = new BufferProcessor();
+ }
+
+ try
+ {
+ mFileOutputStream = new FileOutputStream(mPath.toFile());
+
+ /* Schedule the handler to run every half second */
+ mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
+ }
+ catch (IOException io)
+ {
+ mLog.error("Error starting audio recorder [" + getPath().toString() + "]", io);
+
+ mRunning.set(false);
+ }
+ }
+ }
+
+ /**
+ * Processes the audio packet queue.
+ */
+ private void processAudioPacketQueue()
+ {
+ mAudioPacketQueue.drainTo(mPacketsToProcess);
+
+ if (!mPacketsToProcess.isEmpty())
+ {
+ try
+ {
+ record(mPacketsToProcess);
+
+ for(AudioPacket packet: mPacketsToProcess)
+ {
+ if(packet.getType() == AudioPacket.Type.AUDIO)
+ {
+ mSampleCount += packet.getAudioBuffer().getSamples().length;
+ }
+ }
+ }
+ catch(IOException ioe)
+ {
+ mLog.debug("Error while recording audio to [" + getPath().toString() + "] - stopping recorder");
+ stop();
+ }
+
+ mPacketsToProcess.clear();
+ }
+ }
+
+ /**
+ * 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()
+ {
+ }
+
+ /**
+ * Flushes any remaining audio to the output file. This method should be implemented by subclasses where an audio
+ * converter may contain residual frame data that should be flushed to disk before closing the audio file.
+ */
+ protected void flush()
+ {
+ }
+
+ /**
+ * Drains the audio packet queue and records the audio packets to file
+ */
+ public class BufferProcessor implements Runnable
+ {
+ private AtomicBoolean mProcessing = new AtomicBoolean();
+
+ public void run()
+ {
+ if (mProcessing.compareAndSet(false, true))
+ {
+ processAudioPacketQueue();
+
+ //If we've been stopped or closed, finish the queue, close the recording, notify the listener, and
+ // cancel the future
+ if(!mRunning.get())
+ {
+ //Allow sub-classes to flush remaining audio frame data to disk.
+ flush();
+
+ if (mFileOutputStream != null)
+ {
+ try
+ {
+ mFileOutputStream.flush();
+ mFileOutputStream.close();
+ }
+ catch (IOException e)
+ {
+ mLog.error("Error closing output stream", e);
+ }
+ }
+
+ if(mRecordingClosedListener != null)
+ {
+ mRecordingClosedListener.receive(AudioRecorder.this);
+ }
+
+ if (mProcessorHandle != null)
+ {
+ mProcessorHandle.cancel(false);
+ mProcessorHandle = null;
+ }
+ }
+
+ mProcessing.set(false);
+ }
+ }
+ }
+}
diff --git a/src/record/mp3/MP3Recorder.java b/src/record/mp3/MP3Recorder.java
new file mode 100644
index 00000000..747496ce
--- /dev/null
+++ b/src/record/mp3/MP3Recorder.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * 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.broadcast.BroadcastModel;
+import audio.convert.MP3AudioConverter;
+import module.Module;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import properties.SystemProperties;
+import record.AudioRecorder;
+import record.wave.AudioPacketMonoWaveReader;
+import sample.Listener;
+import util.TimeStamp;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+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;
+import java.util.function.Consumer;
+
+/**
+ * MP3 recorder for converting 8 kHz PCM audio packets to MP3 and writing to .mp3 file.
+ */
+public class MP3Recorder extends AudioRecorder
+{
+ private final static Logger mLog = LoggerFactory.getLogger(MP3Recorder.class);
+
+ public static final int MP3_BIT_RATE = 16;
+ public static final boolean CONSTANT_BIT_RATE = false;
+
+ private MP3AudioConverter mMP3Converter;
+
+ /**
+ * MP3 audio recorder module for converting audio packets to 16 kHz constant bit rate MP3 format and
+ * recording to a file.
+ *
+ * @param path to the output file. File name should include the .mp3 file extension.
+ */
+ public MP3Recorder(Path path)
+ {
+ super(path);
+
+ mMP3Converter = new MP3AudioConverter(MP3_BIT_RATE, CONSTANT_BIT_RATE);
+ }
+
+ @Override
+ protected void record(List audioPackets) throws IOException
+ {
+ OutputStream outputStream = getOutputStream();
+
+ if(outputStream != null)
+ {
+ processMetadata(audioPackets);
+
+ byte[] mp3Audio = mMP3Converter.convert(audioPackets);
+
+ outputStream.write(mp3Audio);
+ }
+ }
+
+ @Override
+ protected void flush()
+ {
+ byte[] partialFrame = mMP3Converter.flush();
+
+ if(partialFrame != null && partialFrame.length > 0)
+ {
+ try
+ {
+ getOutputStream().write(partialFrame);
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error writing final audio frame data to file", ioe);
+ }
+ }
+ }
+
+ /**
+ * Processes audio metadata contained in the audio packets and converts the metadata to MP3 ID3 metadata tags and
+ * writes the ID3 tags to the output stream.
+ * @param audioPackets
+ */
+ private void processMetadata(List audioPackets)
+ {
+ //TODO: detect metadata changes and write out ID3 tags to the MP3 stream
+ }
+}
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..25b076e9
--- /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];
+
+ /**
+ * 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
diff --git a/src/sample/Broadcaster.java b/src/sample/Broadcaster.java
index 65fa765b..4fc1f819 100644
--- a/src/sample/Broadcaster.java
+++ b/src/sample/Broadcaster.java
@@ -21,7 +21,7 @@ import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
- * Broadcasts a received float sample to multiple listeners
+ * Broadcasts an item to multiple listeners
*/
public class Broadcaster implements Listener
{
diff --git a/src/util/XTEA.java b/src/util/XTEA.java
new file mode 100644
index 00000000..575c92c8
--- /dev/null
+++ b/src/util/XTEA.java
@@ -0,0 +1,184 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package util;
+
+import java.util.Arrays;
+
+public class XTEA
+{
+ //Key Schedule = (sqrt(5) 1) * 2^31
+ private static final int K = 0x9E3779B9;
+
+ //Byte mask values
+ private static final int MASK0 = 0xFF;
+ private static final int MASK1 = 0xFF00;
+ private static final int MASK2 = 0xFF0000;
+ private static final int MASK3 = 0xFF000000;
+
+ //Pre-calculated sub keys derived from encryption key
+ private int mSubKeys1[] = new int[32];
+ private int mSubKeys2[] = new int[32];
+
+ /**
+ * XTEA block cipher encryption using 128 bit encryption key and 64 bit plain text block size with 32 Feistel rounds.
+ * Shorter keys and/or plaintext values can be zero padded to the specified lengths.
+ *
+ * Algorithm source code adapted from: https://people.rit.edu/rab3106/CryptoReport.pdf (as accessed Sep. 2016)
+ *
+ * The encryption key can be changed at any point by invoking the setKey() method.
+ *
+ * @param key for encryption
+ */
+ public XTEA(String key)
+ {
+ setKey(Arrays.copyOf(key.getBytes(), 16));
+ }
+
+ /**
+ * Set the key for this block cipher. Key must be 128 bits or 16 bytes. The key value can be zero padded to
+ * achieve the specified key length.
+ *
+ * @param key encryption key to use
+ */
+ public void setKey(byte[] key)
+ {
+ assert(key.length == 16);
+
+ int keyInts[] = new int[8];
+
+ //Convert the key to an array of integers
+ for (int i = 0; i < 16; i += 4)
+ {
+ keyInts[i / 4] = ((key[i + 3] << 0)) & MASK0 | ((key[i + 2]) << 8) & MASK1 |
+ ((key[i + 1]) << 16) & MASK2 | ((key[i + 0]) << 24) & MASK3;
+ }
+
+ //Calculate subkeys for each Feistel round
+ int sum, cycle;
+
+ for (sum = 0, cycle = 0; cycle < 32; cycle++)
+ {
+ //Calculate round 1 subkey
+ mSubKeys1[cycle] = keyInts[sum & 3];
+
+ //Add K to sum
+ sum += K;
+
+ //Calculate round 2 subkey
+ mSubKeys2[cycle] = keyInts[(sum >>> 11) & 3];
+ }
+ }
+
+ /**
+ * Encrypts the plaintext argument. Textual byte array must be 64 bits or 8 bytes in length and can be zero padded
+ * to achieve the specified length. Encrypted text byte array replaces the original plaintext byte array.
+ *
+ * @param plainText to encrypt
+ * @return cipher text
+ */
+ public byte[] encrypt(byte[] plainText)
+ {
+ assert(plainText.length == 8);
+
+ //Convert the 8 bytes of plaintext to 2 integers
+ int result0 = (plainText[3] << 0) & MASK0 | (plainText[2] << 8) & MASK1 |
+ (plainText[1] << 16) & MASK2 | (plainText[0] << 24) & MASK3;
+ int result1 = (plainText[7] << 0) & MASK0 | (plainText[6] << 8) & MASK1 |
+ (plainText[5] << 16) & MASK2 | (plainText[4] << 24) & MASK3;
+
+ //Unrolled loop for all Feistel rounds
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 0) + mSubKeys1[0];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 1) + mSubKeys2[0];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 1) + mSubKeys1[1];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 2) + mSubKeys2[1];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 2) + mSubKeys1[2];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 3) + mSubKeys2[2];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 3) + mSubKeys1[3];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 4) + mSubKeys2[3];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 4) + mSubKeys1[4];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 5) + mSubKeys2[4];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 5) + mSubKeys1[5];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 6) + mSubKeys2[5];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 6) + mSubKeys1[6];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 7) + mSubKeys2[6];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 7) + mSubKeys1[7];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 8) + mSubKeys2[7];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 8) + mSubKeys1[8];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 9) + mSubKeys2[8];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 9) + mSubKeys1[9];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 10) + mSubKeys2[9];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 10) + mSubKeys1[10];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 11) + mSubKeys2[10];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 11) + mSubKeys1[11];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 12) + mSubKeys2[11];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 12) + mSubKeys1[12];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 13) + mSubKeys2[12];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 13) + mSubKeys1[13];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 14) + mSubKeys2[13];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 14) + mSubKeys1[14];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 15) + mSubKeys2[14];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 15) + mSubKeys1[15];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 16) + mSubKeys2[15];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 16) + mSubKeys1[16];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 17) + mSubKeys2[16];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 17) + mSubKeys1[17];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 18) + mSubKeys2[17];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 18) + mSubKeys1[18];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 19) + mSubKeys2[18];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 19) + mSubKeys1[19];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 20) + mSubKeys2[19];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 20) + mSubKeys1[20];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 21) + mSubKeys2[20];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 21) + mSubKeys1[21];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 22) + mSubKeys2[21];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 22) + mSubKeys1[22];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 23) + mSubKeys2[22];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 23) + mSubKeys1[23];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 24) + mSubKeys2[23];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 24) + mSubKeys1[24];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 25) + mSubKeys2[24];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 25) + mSubKeys1[25];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 26) + mSubKeys2[25];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 26) + mSubKeys1[26];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 27) + mSubKeys2[26];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 27) + mSubKeys1[27];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 28) + mSubKeys2[27];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 28) + mSubKeys1[28];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 29) + mSubKeys2[28];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 29) + mSubKeys1[29];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 30) + mSubKeys2[29];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 30) + mSubKeys1[30];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 31) + mSubKeys2[30];
+ result0 += (result1 << 4 ^ result1 >>> 5) + result1 ^ (K * 31) + mSubKeys1[31];
+ result1 += (result0 << 4 ^ result0 >>> 5) + result0 ^ (K * 32) + mSubKeys2[31];
+
+ byte[] cipherText = new byte[8];
+
+ cipherText[0] = (byte)(result0 >>> 24);
+ cipherText[1] = (byte)(result0 >>> 16);
+ cipherText[2] = (byte)(result0 >>> 8);
+ cipherText[3] = (byte)(result0 >>> 0);
+ cipherText[4] = (byte)(result1 >>> 24);
+ cipherText[5] = (byte)(result1 >>> 16);
+ cipherText[6] = (byte)(result1 >>> 8);
+ cipherText[7] = (byte)(result1 >>> 0);
+
+ return cipherText;
+ }
+}