diff --git a/images/no_record.png b/images/no_record.png new file mode 100644 index 00000000..392e09a5 Binary files /dev/null and b/images/no_record.png differ diff --git a/src/alias/Alias.java b/src/alias/Alias.java index acccd6b5..9d462c80 100644 --- a/src/alias/Alias.java +++ b/src/alias/Alias.java @@ -24,6 +24,7 @@ import java.util.List; import javax.xml.bind.annotation.XmlAttribute; import alias.action.AliasAction; +import alias.priority.Priority; import audio.inverted.AudioType; public class Alias @@ -40,7 +41,7 @@ public class Alias public String toString() { - return "Alias: " + mName; + return getName(); } @XmlAttribute @@ -151,4 +152,36 @@ public class Alias return AudioType.NORMAL; } + + /** + * Returns the priority level of this alias, if defined, or the default priority + */ + public int getCallPriority() + { + for( AliasID id: mAliasIDs ) + { + if( id.getType() == AliasIDType.Priority ) + { + return ((Priority)id).getPriority(); + } + } + + return Priority.DEFAULT_PRIORITY; + } + + /** + * Inspects the alias for a non-recordable alias id. Default is true; + */ + public boolean isRecordable() + { + for( AliasID id: getId() ) + { + if( id.getType() == AliasIDType.NonRecordable ) + { + return false; + } + } + + return true; + } } diff --git a/src/alias/AliasIDType.java b/src/alias/AliasIDType.java index dea58b9c..a03ca358 100644 --- a/src/alias/AliasIDType.java +++ b/src/alias/AliasIDType.java @@ -25,6 +25,8 @@ public enum AliasIDType MDC1200, MIN, MPT1327, + NonRecordable, + Priority, Site, Status, Talkgroup; diff --git a/src/alias/AliasList.java b/src/alias/AliasList.java index 0f182e40..7b0a6ced 100644 --- a/src/alias/AliasList.java +++ b/src/alias/AliasList.java @@ -31,17 +31,22 @@ import alias.action.AliasAction; import alias.action.beep.BeepAction; import alias.action.clip.ClipAction; import alias.action.script.ScriptAction; -import alias.esn.Esn; -import alias.fleetsync.FleetsyncID; -import alias.fleetsync.StatusID; -import alias.mdc.MDC1200ID; -import alias.mobileID.Min; -import alias.mpt1327.MPT1327ID; -import alias.siteID.SiteID; -import alias.talkgroup.TalkgroupID; -import alias.uniqueID.UniqueID; +import alias.id.esn.Esn; +import alias.id.fleetsync.FleetsyncID; +import alias.id.fleetsync.StatusID; +import alias.id.mdc.MDC1200ID; +import alias.id.mobileID.Min; +import alias.id.mpt1327.MPT1327ID; +import alias.id.siteID.SiteID; +import alias.id.talkgroup.TalkgroupID; +import alias.id.uniqueID.UniqueID; +import alias.priority.Priority; +import alias.record.NonRecordable; +import audio.metadata.MetadataType; @XmlSeeAlso( { Alias.class, + + /* Alias ID's */ AliasID.class, FleetsyncID.class, Esn.class, @@ -54,6 +59,10 @@ import alias.uniqueID.UniqueID; StatusID.class, TalkgroupID.class, + /* Alias Attributes */ + NonRecordable.class, + Priority.class, + /* Alias Actions */ AliasAction.class, BeepAction.class, @@ -518,7 +527,7 @@ public class AliasList implements Comparable } } break; - case FROM_TALKGROUP: + case FROM: case TO: return getTalkgroupAlias( value ); case UNIQUE_ID: diff --git a/src/alias/AliasNode.java b/src/alias/AliasNode.java index ceb5e6fb..f9dd555e 100644 --- a/src/alias/AliasNode.java +++ b/src/alias/AliasNode.java @@ -34,24 +34,26 @@ import alias.action.clip.ClipAction; import alias.action.clip.ClipActionNode; import alias.action.script.ScriptAction; import alias.action.script.ScriptActionNode; -import alias.esn.ESNNode; -import alias.esn.Esn; -import alias.fleetsync.FleetsyncID; -import alias.fleetsync.FleetsyncIDNode; -import alias.fleetsync.StatusID; -import alias.fleetsync.StatusIDNode; -import alias.mdc.MDC1200ID; -import alias.mdc.MDC1200IDNode; -import alias.mobileID.MINNode; -import alias.mobileID.Min; -import alias.mpt1327.MPT1327ID; -import alias.mpt1327.MPT1327IDNode; -import alias.siteID.SiteID; -import alias.siteID.SiteIDNode; -import alias.talkgroup.TalkgroupID; -import alias.talkgroup.TalkgroupIDNode; -import alias.uniqueID.UniqueID; -import alias.uniqueID.UniqueIDNode; +import alias.id.esn.ESNNode; +import alias.id.esn.Esn; +import alias.id.fleetsync.FleetsyncID; +import alias.id.fleetsync.FleetsyncIDNode; +import alias.id.fleetsync.StatusID; +import alias.id.fleetsync.StatusIDNode; +import alias.id.mdc.MDC1200ID; +import alias.id.mdc.MDC1200IDNode; +import alias.id.mobileID.MINNode; +import alias.id.mobileID.Min; +import alias.id.mpt1327.MPT1327ID; +import alias.id.mpt1327.MPT1327IDNode; +import alias.id.siteID.SiteID; +import alias.id.siteID.SiteIDNode; +import alias.id.talkgroup.TalkgroupID; +import alias.id.talkgroup.TalkgroupIDNode; +import alias.id.uniqueID.UniqueID; +import alias.id.uniqueID.UniqueIDNode; +import alias.record.NonRecordable; +import alias.record.NonRecordableNode; import controller.ConfigurableNode; public class AliasNode extends ConfigurableNode @@ -95,50 +97,51 @@ public class AliasNode extends ConfigurableNode { for( AliasID aliasID: getAlias().getId() ) { - if( aliasID instanceof Esn ) + switch( aliasID.getType() ) { - getModel().addNode( new ESNNode( (Esn)aliasID ), - AliasNode.this, getChildCount() ); - } - else if( aliasID instanceof FleetsyncID ) - { - getModel().addNode( new FleetsyncIDNode( (FleetsyncID)aliasID ), - AliasNode.this, getChildCount() ); - } - else if( aliasID instanceof MDC1200ID ) - { - getModel().addNode( new MDC1200IDNode( (MDC1200ID)aliasID ), - AliasNode.this, getChildCount() ); - } - else if( aliasID instanceof Min ) - { - getModel().addNode( new MINNode( (Min)aliasID ), - AliasNode.this, getChildCount() ); - } - else if( aliasID instanceof MPT1327ID ) - { - getModel().addNode( new MPT1327IDNode( (MPT1327ID)aliasID ), - AliasNode.this, getChildCount() ); - } - else if( aliasID instanceof SiteID ) - { - getModel().addNode( new SiteIDNode( (SiteID)aliasID ), - AliasNode.this, getChildCount() ); - } - else if( aliasID instanceof StatusID ) - { - getModel().addNode( new StatusIDNode( (StatusID)aliasID ), - AliasNode.this, getChildCount() ); - } - else if( aliasID instanceof TalkgroupID ) - { - getModel().addNode( new TalkgroupIDNode( (TalkgroupID)aliasID ), - AliasNode.this, getChildCount() ); - } - else if( aliasID instanceof UniqueID ) - { - getModel().addNode( new UniqueIDNode( (UniqueID)aliasID ), - AliasNode.this, getChildCount() ); + case ESN: + getModel().addNode( new ESNNode( (Esn)aliasID ), + AliasNode.this, getChildCount() ); + break; + case Fleetsync: + getModel().addNode( new FleetsyncIDNode( (FleetsyncID)aliasID ), + AliasNode.this, getChildCount() ); + break; + case LTRNetUID: + getModel().addNode( new UniqueIDNode( (UniqueID)aliasID ), + AliasNode.this, getChildCount() ); + break; + case MDC1200: + getModel().addNode( new MDC1200IDNode( (MDC1200ID)aliasID ), + AliasNode.this, getChildCount() ); + break; + case MIN: + getModel().addNode( new MINNode( (Min)aliasID ), + AliasNode.this, getChildCount() ); + break; + case MPT1327: + getModel().addNode( new MPT1327IDNode( (MPT1327ID)aliasID ), + AliasNode.this, getChildCount() ); + break; + case NonRecordable: + getModel().addNode( new NonRecordableNode( (NonRecordable)aliasID ), + AliasNode.this, getChildCount() ); + break; + case Site: + getModel().addNode( new SiteIDNode( (SiteID)aliasID ), + AliasNode.this, getChildCount() ); + break; + case Status: + getModel().addNode( new StatusIDNode( (StatusID)aliasID ), + AliasNode.this, getChildCount() ); + break; + case Talkgroup: + getModel().addNode( new TalkgroupIDNode( (TalkgroupID)aliasID ), + AliasNode.this, getChildCount() ); + break; + default: + throw new IllegalArgumentException( "Unrecognized alias ID [" + + aliasID.getType() + "]"); } } @@ -280,6 +283,27 @@ public class AliasNode extends ConfigurableNode } ); addIDMenu.add( addMPTItem ); + JMenuItem nonRecordableItem = new JMenuItem( "Non-Recordable" ); + nonRecordableItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + NonRecordable non = new NonRecordable(); + + getAlias().addAliasID( non ); + + NonRecordableNode node = new NonRecordableNode( non ); + + getModel().addNode( node, + AliasNode.this, + AliasNode.this.getChildCount() ); + + node.show(); + } + } ); + addIDMenu.add( nonRecordableItem ); + JMenuItem addSiteItem = new JMenuItem( "Site" ); addSiteItem.addActionListener( new ActionListener() { diff --git a/src/alias/esn/ESNEditor.java b/src/alias/id/esn/ESNEditor.java similarity index 99% rename from src/alias/esn/ESNEditor.java rename to src/alias/id/esn/ESNEditor.java index 9f8db499..e2a55f2b 100644 --- a/src/alias/esn/ESNEditor.java +++ b/src/alias/id/esn/ESNEditor.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 alias.esn; +package alias.id.esn; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/esn/ESNNode.java b/src/alias/id/esn/ESNNode.java similarity index 99% rename from src/alias/esn/ESNNode.java rename to src/alias/id/esn/ESNNode.java index 4b6d837f..1b6234d0 100644 --- a/src/alias/esn/ESNNode.java +++ b/src/alias/id/esn/ESNNode.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 alias.esn; +package alias.id.esn; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/esn/Esn.java b/src/alias/id/esn/Esn.java similarity index 98% rename from src/alias/esn/Esn.java rename to src/alias/id/esn/Esn.java index a2cfe786..09198031 100644 --- a/src/alias/esn/Esn.java +++ b/src/alias/id/esn/Esn.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 alias.esn; +package alias.id.esn; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/alias/fleetsync/FleetsyncID.java b/src/alias/id/fleetsync/FleetsyncID.java similarity index 98% rename from src/alias/fleetsync/FleetsyncID.java rename to src/alias/id/fleetsync/FleetsyncID.java index 43d0a979..ca92a75c 100644 --- a/src/alias/fleetsync/FleetsyncID.java +++ b/src/alias/id/fleetsync/FleetsyncID.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 alias.fleetsync; +package alias.id.fleetsync; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/alias/fleetsync/FleetsyncIDEditor.java b/src/alias/id/fleetsync/FleetsyncIDEditor.java similarity index 99% rename from src/alias/fleetsync/FleetsyncIDEditor.java rename to src/alias/id/fleetsync/FleetsyncIDEditor.java index b0be3ddb..83b244f6 100644 --- a/src/alias/fleetsync/FleetsyncIDEditor.java +++ b/src/alias/id/fleetsync/FleetsyncIDEditor.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 alias.fleetsync; +package alias.id.fleetsync; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/fleetsync/FleetsyncIDNode.java b/src/alias/id/fleetsync/FleetsyncIDNode.java similarity index 98% rename from src/alias/fleetsync/FleetsyncIDNode.java rename to src/alias/id/fleetsync/FleetsyncIDNode.java index 1cea3feb..9a16c1d9 100644 --- a/src/alias/fleetsync/FleetsyncIDNode.java +++ b/src/alias/id/fleetsync/FleetsyncIDNode.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 alias.fleetsync; +package alias.id.fleetsync; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/fleetsync/StatusID.java b/src/alias/id/fleetsync/StatusID.java similarity index 98% rename from src/alias/fleetsync/StatusID.java rename to src/alias/id/fleetsync/StatusID.java index 6f19dd2e..bbcd694c 100644 --- a/src/alias/fleetsync/StatusID.java +++ b/src/alias/id/fleetsync/StatusID.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 alias.fleetsync; +package alias.id.fleetsync; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/alias/fleetsync/StatusIDEditor.java b/src/alias/id/fleetsync/StatusIDEditor.java similarity index 99% rename from src/alias/fleetsync/StatusIDEditor.java rename to src/alias/id/fleetsync/StatusIDEditor.java index 1a191de2..fe3a6348 100644 --- a/src/alias/fleetsync/StatusIDEditor.java +++ b/src/alias/id/fleetsync/StatusIDEditor.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 alias.fleetsync; +package alias.id.fleetsync; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/fleetsync/StatusIDNode.java b/src/alias/id/fleetsync/StatusIDNode.java similarity index 98% rename from src/alias/fleetsync/StatusIDNode.java rename to src/alias/id/fleetsync/StatusIDNode.java index 40db6536..621626fb 100644 --- a/src/alias/fleetsync/StatusIDNode.java +++ b/src/alias/id/fleetsync/StatusIDNode.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 alias.fleetsync; +package alias.id.fleetsync; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/mdc/MDC1200ID.java b/src/alias/id/mdc/MDC1200ID.java similarity index 98% rename from src/alias/mdc/MDC1200ID.java rename to src/alias/id/mdc/MDC1200ID.java index bcd29467..cf866e35 100644 --- a/src/alias/mdc/MDC1200ID.java +++ b/src/alias/id/mdc/MDC1200ID.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 alias.mdc; +package alias.id.mdc; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/alias/mdc/MDC1200IDEditor.java b/src/alias/id/mdc/MDC1200IDEditor.java similarity index 99% rename from src/alias/mdc/MDC1200IDEditor.java rename to src/alias/id/mdc/MDC1200IDEditor.java index a0c53b77..6003fd56 100644 --- a/src/alias/mdc/MDC1200IDEditor.java +++ b/src/alias/id/mdc/MDC1200IDEditor.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 alias.mdc; +package alias.id.mdc; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/mdc/MDC1200IDNode.java b/src/alias/id/mdc/MDC1200IDNode.java similarity index 99% rename from src/alias/mdc/MDC1200IDNode.java rename to src/alias/id/mdc/MDC1200IDNode.java index 0af27b15..5a5a837b 100644 --- a/src/alias/mdc/MDC1200IDNode.java +++ b/src/alias/id/mdc/MDC1200IDNode.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 alias.mdc; +package alias.id.mdc; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/mobileID/MINEditor.java b/src/alias/id/mobileID/MINEditor.java similarity index 98% rename from src/alias/mobileID/MINEditor.java rename to src/alias/id/mobileID/MINEditor.java index b24bf3ca..8b0f982a 100644 --- a/src/alias/mobileID/MINEditor.java +++ b/src/alias/id/mobileID/MINEditor.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 alias.mobileID; +package alias.id.mobileID; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/mobileID/MINNode.java b/src/alias/id/mobileID/MINNode.java similarity index 98% rename from src/alias/mobileID/MINNode.java rename to src/alias/id/mobileID/MINNode.java index c8ea62ff..cf832f91 100644 --- a/src/alias/mobileID/MINNode.java +++ b/src/alias/id/mobileID/MINNode.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 alias.mobileID; +package alias.id.mobileID; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/mobileID/Min.java b/src/alias/id/mobileID/Min.java similarity index 98% rename from src/alias/mobileID/Min.java rename to src/alias/id/mobileID/Min.java index 649b9df0..ca6b02dd 100644 --- a/src/alias/mobileID/Min.java +++ b/src/alias/id/mobileID/Min.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 alias.mobileID; +package alias.id.mobileID; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/alias/mpt1327/MPT1327ID.java b/src/alias/id/mpt1327/MPT1327ID.java similarity index 98% rename from src/alias/mpt1327/MPT1327ID.java rename to src/alias/id/mpt1327/MPT1327ID.java index fc7c6a1f..5aa4c463 100644 --- a/src/alias/mpt1327/MPT1327ID.java +++ b/src/alias/id/mpt1327/MPT1327ID.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 alias.mpt1327; +package alias.id.mpt1327; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/alias/mpt1327/MPT1327IDEditor.java b/src/alias/id/mpt1327/MPT1327IDEditor.java similarity index 99% rename from src/alias/mpt1327/MPT1327IDEditor.java rename to src/alias/id/mpt1327/MPT1327IDEditor.java index a6be8bc7..ab6dcdb6 100644 --- a/src/alias/mpt1327/MPT1327IDEditor.java +++ b/src/alias/id/mpt1327/MPT1327IDEditor.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 alias.mpt1327; +package alias.id.mpt1327; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/mpt1327/MPT1327IDNode.java b/src/alias/id/mpt1327/MPT1327IDNode.java similarity index 98% rename from src/alias/mpt1327/MPT1327IDNode.java rename to src/alias/id/mpt1327/MPT1327IDNode.java index e6bb9039..ab591365 100644 --- a/src/alias/mpt1327/MPT1327IDNode.java +++ b/src/alias/id/mpt1327/MPT1327IDNode.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 alias.mpt1327; +package alias.id.mpt1327; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/siteID/SiteID.java b/src/alias/id/siteID/SiteID.java similarity index 98% rename from src/alias/siteID/SiteID.java rename to src/alias/id/siteID/SiteID.java index 7a6a0350..ba5f57e0 100644 --- a/src/alias/siteID/SiteID.java +++ b/src/alias/id/siteID/SiteID.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 alias.siteID; +package alias.id.siteID; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/alias/siteID/SiteIDEditor.java b/src/alias/id/siteID/SiteIDEditor.java similarity index 99% rename from src/alias/siteID/SiteIDEditor.java rename to src/alias/id/siteID/SiteIDEditor.java index b45e33e1..526e89c2 100644 --- a/src/alias/siteID/SiteIDEditor.java +++ b/src/alias/id/siteID/SiteIDEditor.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 alias.siteID; +package alias.id.siteID; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/siteID/SiteIDNode.java b/src/alias/id/siteID/SiteIDNode.java similarity index 98% rename from src/alias/siteID/SiteIDNode.java rename to src/alias/id/siteID/SiteIDNode.java index 09659149..26569699 100644 --- a/src/alias/siteID/SiteIDNode.java +++ b/src/alias/id/siteID/SiteIDNode.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 alias.siteID; +package alias.id.siteID; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/talkgroup/TalkgroupID.java b/src/alias/id/talkgroup/TalkgroupID.java similarity index 98% rename from src/alias/talkgroup/TalkgroupID.java rename to src/alias/id/talkgroup/TalkgroupID.java index 6fca233c..476e9add 100644 --- a/src/alias/talkgroup/TalkgroupID.java +++ b/src/alias/id/talkgroup/TalkgroupID.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 alias.talkgroup; +package alias.id.talkgroup; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/alias/talkgroup/TalkgroupIDEditor.java b/src/alias/id/talkgroup/TalkgroupIDEditor.java similarity index 99% rename from src/alias/talkgroup/TalkgroupIDEditor.java rename to src/alias/id/talkgroup/TalkgroupIDEditor.java index 5c52855e..4275989d 100644 --- a/src/alias/talkgroup/TalkgroupIDEditor.java +++ b/src/alias/id/talkgroup/TalkgroupIDEditor.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 alias.talkgroup; +package alias.id.talkgroup; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/talkgroup/TalkgroupIDNode.java b/src/alias/id/talkgroup/TalkgroupIDNode.java similarity index 98% rename from src/alias/talkgroup/TalkgroupIDNode.java rename to src/alias/id/talkgroup/TalkgroupIDNode.java index 39cc1f64..0a8e7e0a 100644 --- a/src/alias/talkgroup/TalkgroupIDNode.java +++ b/src/alias/id/talkgroup/TalkgroupIDNode.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 alias.talkgroup; +package alias.id.talkgroup; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/uniqueID/UniqueID.java b/src/alias/id/uniqueID/UniqueID.java similarity index 98% rename from src/alias/uniqueID/UniqueID.java rename to src/alias/id/uniqueID/UniqueID.java index 6b2d1487..f6596294 100644 --- a/src/alias/uniqueID/UniqueID.java +++ b/src/alias/id/uniqueID/UniqueID.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 alias.uniqueID; +package alias.id.uniqueID; import javax.xml.bind.annotation.XmlAttribute; diff --git a/src/alias/uniqueID/UniqueIDEditor.java b/src/alias/id/uniqueID/UniqueIDEditor.java similarity index 99% rename from src/alias/uniqueID/UniqueIDEditor.java rename to src/alias/id/uniqueID/UniqueIDEditor.java index e342fff8..7b46f176 100644 --- a/src/alias/uniqueID/UniqueIDEditor.java +++ b/src/alias/id/uniqueID/UniqueIDEditor.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 alias.uniqueID; +package alias.id.uniqueID; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/alias/uniqueID/UniqueIDNode.java b/src/alias/id/uniqueID/UniqueIDNode.java similarity index 98% rename from src/alias/uniqueID/UniqueIDNode.java rename to src/alias/id/uniqueID/UniqueIDNode.java index 30e32667..5c850f15 100644 --- a/src/alias/uniqueID/UniqueIDNode.java +++ b/src/alias/id/uniqueID/UniqueIDNode.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 alias.uniqueID; +package alias.id.uniqueID; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/src/util/waveaudio/WavFileException.java b/src/alias/priority/Priority.java similarity index 55% rename from src/util/waveaudio/WavFileException.java rename to src/alias/priority/Priority.java index f2335f2a..007f7ca4 100644 --- a/src/util/waveaudio/WavFileException.java +++ b/src/alias/priority/Priority.java @@ -15,29 +15,53 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package util.waveaudio; +package alias.priority; -public class WavFileException extends Exception +import javax.xml.bind.annotation.XmlAttribute; + +import alias.AliasID; +import alias.AliasIDType; + +/** + * Specifies an (audio) priority level for this alias. + */ +public class Priority extends AliasID { + public static final int MIN_PRIORITY = 1; + public static final int MAX_PRIORITY = 100; + public static final int DEFAULT_PRIORITY = 100; + + private int mPriority; + + public Priority() + { + } - public WavFileException() + @XmlAttribute + public int getPriority() + { + return mPriority; + } + + public void setPriority( int priority ) + { + mPriority = priority; + } + + public String toString() + { + return "Priority: " + mPriority; + } + + @Override + public boolean matches( AliasID id ) { - super(); + return false; } - public WavFileException(String message) + @Override + public AliasIDType getType() { - super(message); + return AliasIDType.Priority; } - - public WavFileException(String message, Throwable cause) - { - super(message, cause); - } - - public WavFileException(Throwable cause) - { - super(cause); - } - } diff --git a/src/alias/priority/PriorityEditor.java b/src/alias/priority/PriorityEditor.java new file mode 100644 index 00000000..5dab619c --- /dev/null +++ b/src/alias/priority/PriorityEditor.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2015 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.priority; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.JTextArea; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import module.decode.config.DecodeConfiguration; +import net.miginfocom.swing.MigLayout; +import controller.ConfigurableNode; + +public class PriorityEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + private PriorityNode mPriorityNode; + private JSlider mPrioritySlider; + private JLabel mPrioritySliderLabel; + + public PriorityEditor( PriorityNode priorityNode ) + { + mPriorityNode = priorityNode; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "fill,wrap 2", "[right][left]", "[][][][grow]" ) ); + + add( new JLabel( "Priority" ), "span,align center" ); + + mPrioritySlider = new JSlider( JSlider.HORIZONTAL, + Priority.MIN_PRIORITY, + Priority.MAX_PRIORITY, + Priority.DEFAULT_PRIORITY ); + + mPrioritySlider.setMajorTickSpacing( 20 ); + mPrioritySlider.setMinorTickSpacing( 5 ); + mPrioritySlider.setPaintTicks( true ); + + mPrioritySlider.setLabelTable( mPrioritySlider.createStandardLabels( 20, 20 ) ); + mPrioritySlider.setPaintLabels( true ); + + mPrioritySliderLabel = new JLabel( "Priority: " + mPrioritySlider.getValue() + " " ); + + mPrioritySlider.addChangeListener( new ChangeListener() + { + @Override + public void stateChanged( ChangeEvent e ) + { + mPrioritySliderLabel.setText( "Priority: " + mPrioritySlider.getValue() ); + } + } ); + + add( mPrioritySliderLabel ); + add( mPrioritySlider, "wrap,grow" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( PriorityEditor.this ); + add( btnSave, "growx,push" ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( PriorityEditor.this ); + add( btnReset, "growx,push" ); + + StringBuilder sb = new StringBuilder(); + sb.append( "Select a value for audio priority within the range " ); + sb.append( Priority.MIN_PRIORITY ); + sb.append( " and " ); + sb.append( Priority.MAX_PRIORITY ); + sb.append( ". Lower values indicate higher priority levels." ); + + JTextArea helpText = new JTextArea( sb.toString() ); + helpText.setLineWrap( true ); + add( helpText, "span,grow,push" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + mPriorityNode.getPriority().setPriority( mPrioritySlider.getValue() ); + + ((ConfigurableNode)mPriorityNode.getParent()).sort(); + + mPriorityNode.save(); + + mPriorityNode.show(); + } + else if( command.contentEquals( "Reset" ) ) + { + mPrioritySlider.setValue( mPriorityNode.getPriority().getPriority() ); + } + + mPriorityNode.refresh(); + } +} diff --git a/src/alias/priority/PriorityNode.java b/src/alias/priority/PriorityNode.java new file mode 100644 index 00000000..66442610 --- /dev/null +++ b/src/alias/priority/PriorityNode.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2015 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.priority; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasIDNode; +import alias.AliasNode; + +public class PriorityNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public PriorityNode( Priority priority ) + { + super( priority ); + } + + @Override + public JPanel getEditor() + { + return new PriorityEditor( this ); + } + + public Priority getPriority() + { + return (Priority)getUserObject(); + } + + public String toString() + { + return "Priority: " + getPriority().getPriority(); + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Delete" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( getPriority() ); + + save(); + + getModel().removeNodeFromParent( PriorityNode.this ); + } + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/alias/record/NonRecordable.java b/src/alias/record/NonRecordable.java new file mode 100644 index 00000000..00c69482 --- /dev/null +++ b/src/alias/record/NonRecordable.java @@ -0,0 +1,19 @@ +package alias.record; + +import alias.AliasID; +import alias.AliasIDType; + +public class NonRecordable extends AliasID +{ + @Override + public AliasIDType getType() + { + return AliasIDType.NonRecordable; + } + + @Override + public boolean matches( AliasID id ) + { + return false; + } +} diff --git a/src/alias/record/NonRecordableEditor.java b/src/alias/record/NonRecordableEditor.java new file mode 100644 index 00000000..771d7a0f --- /dev/null +++ b/src/alias/record/NonRecordableEditor.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 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.record; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JTextArea; + +import net.miginfocom.swing.MigLayout; + +public class NonRecordableEditor extends JPanel implements ActionListener +{ + private static final long serialVersionUID = 1L; + + private NonRecordableNode mNode; + + public NonRecordableEditor( NonRecordableNode node ) + { + mNode = node; + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "fill", "[left]", "[grow]" ) ); + + JTextArea description = new JTextArea( "Non-Recordable. Any audio where" + + "this alias is a participant in the call will not be recorded." ); + + description.setLineWrap( true ); + description.setBackground( getBackground() ); + + add( description, "growx,span" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( NonRecordableEditor.this ); + add( btnSave, "growx,push" ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( NonRecordableEditor.this ); + add( btnReset, "growx,push" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + mNode.save(); + mNode.show(); + } + + mNode.refresh(); + } +} diff --git a/src/alias/record/NonRecordableNode.java b/src/alias/record/NonRecordableNode.java new file mode 100644 index 00000000..1d4632ea --- /dev/null +++ b/src/alias/record/NonRecordableNode.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 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.record; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; + +import alias.AliasID; +import alias.AliasIDNode; +import alias.AliasNode; + +public class NonRecordableNode extends AliasIDNode +{ + private static final long serialVersionUID = 1L; + + public NonRecordableNode( NonRecordable id ) + { + super( id ); + } + + @Override + public JPanel getEditor() + { + return new NonRecordableEditor( this ); + } + + @Override + public String getIconPath() + { + return "images/non_recordable.png"; + } + + public String toString() + { + return "Non-Recordable"; + } + + public JPopupMenu getContextMenu() + { + JPopupMenu retVal = new JPopupMenu(); + + JMenuItem addSystemItem = new JMenuItem( "Delete" ); + addSystemItem.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + int n = JOptionPane.showConfirmDialog( getModel().getTree(), + "Are you sure you want to permanently delete this node?" ); + + if( n == JOptionPane.YES_OPTION ) + { + AliasNode parent = (AliasNode)getParent(); + + parent.getAlias().removeAliasID( (AliasID)getUserObject() ); + + save(); + + getModel().removeNodeFromParent( NonRecordableNode.this ); + } + } + } ); + + retVal.add( addSystemItem ); + + return retVal; + } +} diff --git a/src/audio/AudioChannelPanel.java b/src/audio/AudioChannelPanel.java index 7922db3f..876bdf0f 100644 --- a/src/audio/AudioChannelPanel.java +++ b/src/audio/AudioChannelPanel.java @@ -27,10 +27,10 @@ import settings.SettingChangeListener; import settings.SettingsManager; import settings.ColorSetting.ColorSettingName; import alias.Alias; -import alias.Metadata; -import alias.MetadataType; import audio.AudioEvent.Type; import audio.metadata.AudioMetadata; +import audio.metadata.Metadata; +import audio.metadata.MetadataType; public class AudioChannelPanel extends JPanel implements Listener, SettingChangeListener @@ -51,14 +51,14 @@ public class AudioChannelPanel extends JPanel private JLabel mChannelName; private JLabel mMutedLabel; - private JLabel mFromLabel = new JLabel( "From:" ); - private JLabel mFrom = new JLabel( "" ); - private JLabel mFromAlias = new JLabel( "" ); - private JLabel mToLabel = new JLabel( "To:" ); private JLabel mTo = new JLabel( "" ); private JLabel mToAlias = new JLabel( "" ); + private JLabel mFromLabel = new JLabel( "From:" ); + private JLabel mFrom = new JLabel( "" ); + private JLabel mFromAlias = new JLabel( "" ); + private boolean mConfigured = false; private BooleanControl mMuteControl; @@ -138,6 +138,18 @@ public class AudioChannelPanel extends JPanel mMutedLabel.setForeground( Color.RED ); add( mMutedLabel, "wrap" ); + + mToLabel.setFont( mFont ); + mToLabel.setForeground( mLabelColor ); + add( mToLabel ); + + mTo.setFont( mFont ); + mTo.setForeground( mLabelColor ); + add( mTo ); + + mToAlias.setFont( mFont ); + mToAlias.setForeground( mLabelColor ); + add( mToAlias,"wrap" ); mFromLabel.setFont( mFont ); mFromLabel.setForeground( mLabelColor ); @@ -150,18 +162,6 @@ public class AudioChannelPanel extends JPanel mFromAlias.setFont( mFont ); mFromAlias.setForeground( mLabelColor ); add( mFromAlias,"wrap" ); - - mToLabel.setFont( mFont ); - mToLabel.setForeground( mLabelColor ); - add( mToLabel ); - - mTo.setFont( mFont ); - mTo.setForeground( mLabelColor ); - add( mTo ); - - mToAlias.setFont( mFont ); - mToAlias.setForeground( mLabelColor ); - add( mToAlias ); } private void updateMuteState() @@ -287,7 +287,7 @@ public class AudioChannelPanel extends JPanel { if( !mConfigured || audioMetadata.isUpdated() ) { - final Metadata from = audioMetadata.getMetadata( MetadataType.FROM_TALKGROUP ); + final Metadata from = audioMetadata.getMetadata( MetadataType.FROM ); final Metadata to = audioMetadata.getMetadata( MetadataType.TO ); @@ -296,11 +296,17 @@ public class AudioChannelPanel extends JPanel @Override public void run() { - mFrom.setText( from.getValue() ); - updateAlias( mFromAlias, from.getAlias() ); + if( from != null ) + { + mFrom.setText( from.getValue() ); + updateAlias( mFromAlias, from.getAlias() ); + } - mTo.setText( to.getValue() ); - updateAlias( mToAlias, to.getAlias() ); + if( to != null ) + { + mTo.setText( to.getValue() ); + updateAlias( mToAlias, to.getAlias() ); + } } } ); diff --git a/src/audio/AudioManager.java b/src/audio/AudioManager.java index 09d7dc2f..d354fef7 100644 --- a/src/audio/AudioManager.java +++ b/src/audio/AudioManager.java @@ -38,7 +38,6 @@ public class AudioManager implements Listener, IAudioController private Map mChannelMap = new HashMap<>(); private List mChannelNames = new ArrayList<>(); private AudioOutput mAudioOutput; - private boolean mMuted = false; private Listener mConfigurationChangeListener; @@ -90,10 +89,6 @@ public class AudioManager implements Listener, IAudioController @Override public void run() { - if( !mMuted ) - { - - } removeCompletedChannelAssignments(); if( mAudioPacketQueue != null ) @@ -104,55 +99,62 @@ public class AudioManager implements Listener, IAudioController for( AudioPacket packet: packets ) { - int source = packet.getMetadata().getSource(); - - /* Existing channel assignment */ - if( mChannelAssignments.containsKey( source ) ) + int source = packet.getAudioMetadata().getSource(); + + if( packet.getType() == AudioPacket.Type.END ) { - mChannelAssignments.get( source ).getAudioOutput() - .receive( packet ); + } - /* Start a new channel assignment */ - else if( !mAvailableAudioOutputs.isEmpty() ) - { - AudioOutput channel = mAvailableAudioOutputs.remove( 0 ); - - mChannelAssignments.put( packet.getMetadata().getSource(), - new AudioChannelAssignment( channel, packet.getMetadata().getPriority() ) ); - - updateLowestPriorityAssignment(); - - channel.receive( packet ); - } - /* Override the lowest priority assignment */ else { - if( mLowestPrioritySource > -1 ) + /* Existing channel assignment */ + if( mChannelAssignments.containsKey( source ) ) { - int priority = packet.getMetadata().getPriority(); - - if( packet.getMetadata().isSelected() || - priority < mChannelAssignments - .get( mLowestPrioritySource ).getPriority() ) + mChannelAssignments.get( source ).getAudioOutput() + .receive( packet ); + } + /* Start a new channel assignment */ + else if( !mAvailableAudioOutputs.isEmpty() ) + { + AudioOutput channel = mAvailableAudioOutputs.remove( 0 ); + + mChannelAssignments.put( packet.getAudioMetadata().getSource(), + new AudioChannelAssignment( channel, packet.getAudioMetadata().getPriority() ) ); + + updateLowestPriorityAssignment(); + + channel.receive( packet ); + } + /* Override the lowest priority assignment */ + else + { + if( mLowestPrioritySource > -1 ) { - AudioChannelAssignment assignment = - mChannelAssignments.remove( mLowestPrioritySource ); - - if( assignment != null ) - { - assignment.setPriority( priority ); + int priority = packet.getAudioMetadata().getPriority(); - int overrideSource = packet.getMetadata().getSource(); - - mChannelAssignments.put( overrideSource, assignment ); - - mLowestPrioritySource = overrideSource; - - assignment.getAudioOutput().receive( packet ); - } - else + if( packet.getAudioMetadata().isSelected() || + priority < mChannelAssignments + .get( mLowestPrioritySource ).getPriority() ) { - mLowestPrioritySource = -1; + AudioChannelAssignment assignment = + mChannelAssignments.remove( mLowestPrioritySource ); + + if( assignment != null ) + { + assignment.setPriority( priority ); + + int overrideSource = packet.getAudioMetadata().getSource(); + + mChannelAssignments.put( overrideSource, assignment ); + + mLowestPrioritySource = overrideSource; + + assignment.getAudioOutput().receive( packet ); + } + else + { + mLowestPrioritySource = -1; + } } } } diff --git a/src/audio/AudioModule.java b/src/audio/AudioModule.java index b0e1a1d3..25c3e1f5 100644 --- a/src/audio/AudioModule.java +++ b/src/audio/AudioModule.java @@ -8,9 +8,9 @@ import org.slf4j.LoggerFactory; import sample.Listener; import sample.real.IRealBufferListener; import sample.real.RealBuffer; -import alias.Metadata; import audio.metadata.AudioMetadata; import audio.metadata.IMetadataListener; +import audio.metadata.Metadata; import audio.squelch.ISquelchStateListener; import audio.squelch.SquelchState; import controller.channel.ChannelEvent; @@ -61,16 +61,11 @@ public class AudioModule extends Module implements IAudioPacketProvider, private boolean mRemoveDC = false; - public AudioModule() - { - this( false ); - } - - public AudioModule( boolean removeDC ) + public AudioModule( boolean record, boolean removeDC ) { mSourceID = ++UNIQUE_ID; - mAudioMetadata = new AudioMetadata( mSourceID ); + mAudioMetadata = new AudioMetadata( mSourceID, record ); mRemoveDC = removeDC; @@ -89,10 +84,28 @@ public class AudioModule extends Module implements IAudioPacketProvider, } @Override - public void init() + public void reset() { mAudioMetadata.reset(); } + + @Override + public void start() + { + /* No start operations provided */ + } + + @Override + public void stop() + { +// /* Issue an end audio packet in case a recorder is still rolling */ +// if( mAudioPacketListener != null ) +// { +// mLog.debug( "Sending end audio packet - stop() invoked" ); +// mAudioPacketListener.receive( new AudioPacket( AudioPacket.Type.END, +// mAudioMetadata.copyOf() ) ); +// } + } /** * Processes demodulated audio samples into audio packets with current audio @@ -108,6 +121,9 @@ public class AudioModule extends Module implements IAudioPacketProvider, mAudioFilter.filter( audio ); AudioPacket packet = new AudioPacket( audio, mAudioMetadata.copyOf() ); + + /* Reset the updated flag on the metadata */ + mAudioMetadata.setUpdated( false ); mAudioPacketListener.receive( packet ); } @@ -185,6 +201,12 @@ public class AudioModule extends Module implements IAudioPacketProvider, @Override public void receive( SquelchState state ) { + if( state == SquelchState.SQUELCH && mAudioPacketListener != null ) + { + mAudioPacketListener.receive( new AudioPacket( AudioPacket.Type.END, + mAudioMetadata.copyOf() ) ); + } + mSquelchState = state; } } diff --git a/src/audio/AudioPacket.java b/src/audio/AudioPacket.java index 2989fa59..c14754e1 100644 --- a/src/audio/AudioPacket.java +++ b/src/audio/AudioPacket.java @@ -1,27 +1,50 @@ package audio; +import sample.real.RealBuffer; import audio.metadata.AudioMetadata; +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014,2015 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 AudioPacket { private Type mType; - private float[] mAudioData; + private RealBuffer mAudioData; private AudioMetadata mAudioMetadata; - public AudioPacket( Type type ) + public AudioPacket( Type type, AudioMetadata metadata ) { mType = type; + mAudioMetadata = metadata; } public AudioPacket( float[] audio, AudioMetadata metadata ) { - this( Type.AUDIO ); + this( Type.AUDIO, metadata ); - mAudioData = audio; - mAudioMetadata = metadata; + mAudioData = new RealBuffer( audio ); } - public AudioMetadata getMetadata() + public boolean hasAudioMetadata() + { + return mAudioMetadata != null; + } + + public AudioMetadata getAudioMetadata() { return mAudioMetadata; } @@ -36,7 +59,7 @@ public class AudioPacket return mType; } - public float[] getAudioData() + public RealBuffer getAudioBuffer() { return mAudioData; } diff --git a/src/audio/AudioPacketRateCounter.java b/src/audio/AudioPacketRateCounter.java index cb888ab5..99835a90 100644 --- a/src/audio/AudioPacketRateCounter.java +++ b/src/audio/AudioPacketRateCounter.java @@ -34,7 +34,7 @@ public class AudioPacketRateCounter implements Listener mStartTime = System.currentTimeMillis(); } - mCount += packet.getAudioData().length; + mCount += packet.getAudioBuffer().getSamples().length; long elapsed = System.currentTimeMillis() - mStartTime; diff --git a/src/audio/IAudioPacketListener.java b/src/audio/IAudioPacketListener.java new file mode 100644 index 00000000..298f5e1c --- /dev/null +++ b/src/audio/IAudioPacketListener.java @@ -0,0 +1,8 @@ +package audio; + +import sample.Listener; + +public interface IAudioPacketListener +{ + public Listener getAudioPacketListener(); +} diff --git a/src/audio/metadata/AudioMetadata.java b/src/audio/metadata/AudioMetadata.java index a75da2fe..6d462b2e 100644 --- a/src/audio/metadata/AudioMetadata.java +++ b/src/audio/metadata/AudioMetadata.java @@ -8,38 +8,92 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; -import alias.Metadata; -import alias.MetadataType; +import alias.Alias; +import alias.priority.Priority; +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014,2015 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 AudioMetadata implements Listener { protected static final Logger mLog = LoggerFactory.getLogger( AudioMetadata.class ); private int mSource; - private int mPriority = 99; - private boolean mSelected = false; private List mMetadata = new ArrayList<>(); private boolean mUpdated = false; + + /* Channel Selectect == highest priority */ + private boolean mSelected = false; + + /* Call Priority */ + private int mPriority = Priority.DEFAULT_PRIORITY; + + /* Indicates if the overall source is recordable */ + private boolean mSourceRecordable = false; - public AudioMetadata( int source ) + /* 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 ); + } + + /** + * 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 ); + AudioMetadata copy = new AudioMetadata( mSource, mSourceRecordable ); copy.mPriority = mPriority; copy.mSelected = mSelected; + copy.mRecordable = mRecordable; copy.mMetadata.addAll( mMetadata ); - - if( mUpdated ) - { - copy.mUpdated = true; - - mUpdated = false; - } + copy.mUpdated = mUpdated; return copy; } @@ -58,10 +112,42 @@ public class AudioMetadata implements Listener it.remove(); } } + + /* Update the recordable state, identifier and audio call priority levels */ + mRecordable = mSourceRecordable; + setIdentifier( mSource ); + mPriority = Priority.DEFAULT_PRIORITY; + + for( Metadata metadata: mMetadata ) + { + if( metadata.hasAlias() ) + { + Alias alias = metadata.getAlias(); + + if( metadata.getMetadataType() == MetadataType.FROM || + metadata.getMetadataType() == MetadataType.TO ) + { + if( !alias.isRecordable() ) + { + mRecordable = false; + } + } + + if( metadata.getMetadataType() == MetadataType.TO ) + { + setIdentifier( mSource, metadata.getValue() ); + } + + if( alias != null && alias.getCallPriority() < mPriority ) + { + mPriority = alias.getCallPriority(); + } + } + } mUpdated = true; } - + /** * 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() @@ -71,6 +157,11 @@ public class AudioMetadata implements Listener { return mUpdated; } + + public void setUpdated( boolean updated ) + { + mUpdated = updated; + } /** * Adds the metadata. Any temporal metadata will be removed when the @@ -89,7 +180,23 @@ public class AudioMetadata implements Listener if( metadata.hasAlias() ) { - //TODO: assign priority from alias here + Alias alias = metadata.getAlias(); + + /* Capture non-recordable aliases */ + if( metadata.getMetadataType() == MetadataType.FROM || + metadata.getMetadataType() == MetadataType.TO ) + { + if( !alias.isRecordable() ) + { + mRecordable = false; + } + } + + /* Capture call priority levels from aliases */ + if( alias.getCallPriority() < mPriority ) + { + mPriority = alias.getCallPriority(); + } } } @@ -137,13 +244,10 @@ public class AudioMetadata implements Listener } /** - * Sets the priority of this audio packet in the range of 1 - Max Integer value - * - * @param priority (1 - Max Int) - */ + * Sets the priority of this audio packet within the defined min/max priority range */ public void setPriority( int priority ) { - assert( 1 <= priority && priority <= Integer.MAX_VALUE ); + assert( Priority.MIN_PRIORITY <= priority && priority <= Priority.MAX_PRIORITY ); mPriority = priority; @@ -177,4 +281,18 @@ public class AudioMetadata implements Listener { 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; + } } diff --git a/src/audio/metadata/IMetadataListener.java b/src/audio/metadata/IMetadataListener.java index a4672515..9fbddc9e 100644 --- a/src/audio/metadata/IMetadataListener.java +++ b/src/audio/metadata/IMetadataListener.java @@ -1,7 +1,6 @@ package audio.metadata; import sample.Listener; -import alias.Metadata; public interface IMetadataListener { diff --git a/src/audio/metadata/IMetadataProvider.java b/src/audio/metadata/IMetadataProvider.java index afb2ebdc..dd80af4a 100644 --- a/src/audio/metadata/IMetadataProvider.java +++ b/src/audio/metadata/IMetadataProvider.java @@ -1,7 +1,6 @@ package audio.metadata; import sample.Listener; -import alias.Metadata; public interface IMetadataProvider { diff --git a/src/alias/Metadata.java b/src/audio/metadata/Metadata.java similarity index 97% rename from src/alias/Metadata.java rename to src/audio/metadata/Metadata.java index e1976183..884be702 100644 --- a/src/alias/Metadata.java +++ b/src/audio/metadata/Metadata.java @@ -1,4 +1,6 @@ -package alias; +package audio.metadata; + +import alias.Alias; public class Metadata { diff --git a/src/alias/MetadataReset.java b/src/audio/metadata/MetadataReset.java similarity index 88% rename from src/alias/MetadataReset.java rename to src/audio/metadata/MetadataReset.java index 8857ec4d..6b6a6fa9 100644 --- a/src/alias/MetadataReset.java +++ b/src/audio/metadata/MetadataReset.java @@ -1,4 +1,4 @@ -package alias; +package audio.metadata; public class MetadataReset extends Metadata { diff --git a/src/alias/MetadataType.java b/src/audio/metadata/MetadataType.java similarity index 80% rename from src/alias/MetadataType.java rename to src/audio/metadata/MetadataType.java index e5426763..349df03a 100644 --- a/src/alias/MetadataType.java +++ b/src/audio/metadata/MetadataType.java @@ -1,19 +1,21 @@ -package alias; +package audio.metadata; public enum MetadataType { - CHANNEL( "Channel" ), + CHANNEL_NUMBER( "Channel Number" ), + CHANNEL_NAME( "Channel Name" ), DETAILS( "Details" ), ESN( "ESN" ), FLEETSYNC( "Fleetsync" ), FREQUENCY( "Frequency" ), - FROM_TALKGROUP( "From" ), + FROM( "From" ), MDC1200( "MDC-1200" ), MOBILE_ID( "Mobile ID" ), MPT1327( "MPT-1327" ), PROTOCOL( "Protocol" ), SITE_ID( "Site ID" ), STATUS( "Status" ), + SYSTEM( "System" ), TO( "To" ), UNIQUE_ID( "Unique ID" ), diff --git a/src/audio/output/MonoAudioOutput.java b/src/audio/output/MonoAudioOutput.java index eb16ddb3..532d4385 100644 --- a/src/audio/output/MonoAudioOutput.java +++ b/src/audio/output/MonoAudioOutput.java @@ -176,7 +176,7 @@ public class MonoAudioOutput extends AudioOutput } else if( packet.getType() == Type.AUDIO ) { - float[] samples = packet.getAudioData(); + float[] samples = packet.getAudioBuffer().getSamples(); /* Little-endian byte buffer */ ByteBuffer buffer = @@ -195,7 +195,7 @@ public class MonoAudioOutput extends AudioOutput checkStart(); - broadcast( packet.getMetadata() ); + broadcast( packet.getAudioMetadata() ); } } } diff --git a/src/controller/ResourceManager.java b/src/controller/ResourceManager.java index 1e186a7c..91256fda 100644 --- a/src/controller/ResourceManager.java +++ b/src/controller/ResourceManager.java @@ -50,27 +50,25 @@ public class ResourceManager { this( new SettingsManager(), new PlaylistManager(), - new EventLogManager(), - new RecorderManager() ); + new EventLogManager() ); } public ResourceManager( SettingsManager settingsManager, PlaylistManager playlistManager, - EventLogManager eventLogManager, - RecorderManager recorderManager ) + EventLogManager eventLogManager ) { mSettingsManager = settingsManager; mPlaylistManager = playlistManager; mEventLogManager = eventLogManager; - mRecorderManager = recorderManager; - + /** * ChannelManager requires a reference to the ResourceManager so that * channel processing chains can access system wide resources */ mThreadPoolManager = new ThreadPoolManager(); mAudioManager = new AudioManager( mThreadPoolManager ); - mChannelManager = new ChannelManager( this ); + mRecorderManager = new RecorderManager( mThreadPoolManager ); + mChannelManager = new ChannelManager( this ); mSourceManager = new SourceManager( this ); mRecordingSourceManager = new RecordingSourceManager( this ); mTunerManager = new TunerManager( this ); diff --git a/src/controller/ThreadPoolManager.java b/src/controller/ThreadPoolManager.java index 8915ffbe..d73bfcf8 100644 --- a/src/controller/ThreadPoolManager.java +++ b/src/controller/ThreadPoolManager.java @@ -117,6 +117,8 @@ public class ThreadPoolManager public enum ThreadType { AUDIO_PROCESSING, + AUDIO_RECORDING, + BASEBAND_RECORDING, SOURCE_SAMPLE_PROCESSING, DECIMATION, DECODER; diff --git a/src/controller/channel/Channel.java b/src/controller/channel/Channel.java index 398b8092..dedd7573 100644 --- a/src/controller/channel/Channel.java +++ b/src/controller/channel/Channel.java @@ -46,6 +46,8 @@ import module.log.config.EventLogConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import record.RecorderManager; +import record.RecorderType; import record.config.RecordConfiguration; import sample.Listener; import source.Source; @@ -58,6 +60,8 @@ import source.config.SourceConfiguration; import source.tuner.TunerChannel; import source.tuner.TunerChannel.Type; import alias.AliasList; +import audio.metadata.Metadata; +import audio.metadata.MetadataType; import controller.ResourceManager; import controller.channel.ChannelEvent.Event; import controller.config.Configuration; @@ -727,7 +731,7 @@ public class Channel extends Configuration } /** - * Sets up the channel and prepares to commense processing. Sets up the + * Sets up the channel and prepares to start processing. Sets up the * processing chain and requests a source. * * @throws SourceException - if no source is available or if there is an @@ -738,9 +742,52 @@ public class Channel extends Configuration setupProcessingChain(); setupSource(); + } + + /** + * Setup recording options. Adds RecorderManager as a audio packet listener + * to receive and record decoded audio. + */ + private void setupRecording() + { + List recorders = mRecordConfiguration.getRecorders(); + + if( !recorders.isEmpty() ) + { + if( recorders.contains( RecorderType.AUDIO ) ) + { + mProcessingChain.addAudioPacketListener( + mResourceManager.getRecorderManager() ); + } + + /* Add baseband recorder */ + if( ( recorders.contains( RecorderType.BASEBAND ) && + mChannelType == ChannelType.STANDARD ) ) + { + mProcessingChain.addModule( RecorderManager.getBasebandRecorder( + mResourceManager.getThreadPoolManager(), getChannelName() ) ); + } + + /* Add traffic channel baseband recorder */ + if( recorders.contains( RecorderType.TRAFFIC_BASEBAND ) && + mChannelType == ChannelType.TRAFFIC ) + { + mProcessingChain.addModule( RecorderManager.getBasebandRecorder( + mResourceManager.getThreadPoolManager(), getChannelName() ) ); + } + } + } + + private String getChannelName() + { + StringBuilder sb = new StringBuilder(); + sb.append( mSystem ); + sb.append( "_" ); + sb.append( mSite ); + sb.append( "_" ); + sb.append( mName ); - setupLogging(); - //TODO: Add recorder(s) + return sb.toString(); } private void setupLogging() @@ -748,17 +795,13 @@ public class Channel extends Configuration mLog.debug( "Setting up logging ..." ); if( mProcessingChain != null ) { - StringBuilder sb = new StringBuilder(); - sb.append( mSystem ); - sb.append( "_" ); - sb.append( mSite ); - sb.append( "_" ); - sb.append( mName ); - - List loggers = mResourceManager.getEventLogManager() - .getLoggers( mEventLogConfiguration, sb.toString() ); + String channelName = getChannelName(); - mLog.debug( "Created [" + loggers.size() + "] loggers for [" + sb.toString() + "]" ); + List loggers = mResourceManager.getEventLogManager() + .getLoggers( mEventLogConfiguration, channelName ); + + mLog.debug( "Created [" + loggers.size() + "] loggers for [" + + channelName + "]" ); if( !loggers.isEmpty() ) { @@ -783,8 +826,10 @@ public class Channel extends Configuration /* Processing Modules */ List modules = DecoderFactory.getModules( mChannelType, - mResourceManager, mDecodeConfiguration, mAuxDecodeConfiguration, - aliasList ); + mResourceManager, mDecodeConfiguration, mRecordConfiguration, + mAuxDecodeConfiguration, aliasList, mSystem, + mSite ); + mProcessingChain.addModules( modules ); /* Get message filters for the set of processing modules */ @@ -808,6 +853,19 @@ public class Channel extends Configuration /* Create the call event model to receive call events */ mCallEventModel = new CallEventModel(); mProcessingChain.addCallEventListener( mCallEventModel ); + + setupLogging(); + + setupRecording(); + + /* Inject channel metadata that will be inserted into audio packets + * for the recorder manager and streaming */ + mProcessingChain.broadcast( + new Metadata( MetadataType.SYSTEM, mSystem.getName() ) ); + mProcessingChain.broadcast( + new Metadata( MetadataType.SITE_ID, mSite.getName() ) ); + mProcessingChain.broadcast( + new Metadata( MetadataType.CHANNEL_NAME, mName ) ); } } diff --git a/src/dsp/gain/DirectGainControl.java b/src/dsp/gain/DirectGainControl.java index 97fec10e..2f9ab934 100644 --- a/src/dsp/gain/DirectGainControl.java +++ b/src/dsp/gain/DirectGainControl.java @@ -62,11 +62,16 @@ public class DirectGainControl implements GainController, RealSampleListener { mListener = null; } + + public float correct( float sample ) + { + return sample * mGain; + } @Override public void receive( float sample ) { - mListener.receive( (float)( sample * mGain ) ); + mListener.receive( correct( sample ) ); } @Override diff --git a/src/module/Module.java b/src/module/Module.java index 27f89420..87295327 100644 --- a/src/module/Module.java +++ b/src/module/Module.java @@ -26,9 +26,12 @@ public abstract class Module * * Modules can optionally implement any of the following interfaces: * + * IAudioPacketListener consumes audio packets * IAudioPacketProvider produces audio packets - * ICallEventProvider call events - * IChannelEventProvider channel events (reset, selection, start, etc) + * ICallEventListener consumes call events + * ICallEventProvider provides call events + * IChannelEventListener consumes channel events + * IChannelEventProvider provides channel events (reset, selection, start, etc) * IComplexBufferListener consumes complex samples and normally * produces demodulated real sample buffers * IFrequencyCorrectionController provides tuned frequency error corrections @@ -42,16 +45,30 @@ public abstract class Module { } + /** + * Initialize or reset all internal states to default - prepare to start + * processing or resume processing, potentially with a different source. + * + * This method is invoked after constructing a module, and following any stop() + * calls and before any start() calls. This allows the module to prior to + * starting a module and starting it again processing a newly tuned channel, + * like a traffic channel. + */ + public abstract void reset(); + + /** + * Start processing. + */ + public abstract void start(); + + /** + * Stop processing + */ + public abstract void stop(); + /** * Dispose of all resources and listeners and prepare for garbage collection */ public abstract void dispose(); - - /** - * Initializes or reset all internal states to default - prepare to process - * again. This method is usually invoked between stopping a module and - * starting it again processing a newly tuned channel, like a traffic - * channel. - */ - public abstract void init(); + } diff --git a/src/module/ProcessingChain.java b/src/module/ProcessingChain.java index a25c6540..f98ccf5b 100644 --- a/src/module/ProcessingChain.java +++ b/src/module/ProcessingChain.java @@ -54,11 +54,12 @@ import source.SourceException; import source.tuner.TunerChannelSource; import source.tuner.frequency.FrequencyCorrectionControl; import source.tuner.frequency.IFrequencyCorrectionController; -import alias.Metadata; import audio.AudioPacket; +import audio.IAudioPacketListener; import audio.IAudioPacketProvider; import audio.metadata.IMetadataListener; import audio.metadata.IMetadataProvider; +import audio.metadata.Metadata; import audio.squelch.ISquelchStateListener; import audio.squelch.ISquelchStateProvider; import audio.squelch.SquelchState; @@ -151,6 +152,17 @@ public class ProcessingChain implements IChannelEventListener mRealBufferBroadcaster.dispose(); mSquelchStateBroadcaster.dispose(); } + + /** + * Broadcasts the metadata to any registered listeners + */ + public void broadcast( Metadata metadata ) + { + if( mMetadataBroadcaster != null ) + { + mMetadataBroadcaster.broadcast( metadata ); + } + } /** * Indicates if this processing chain is currently receiving samples from @@ -274,6 +286,12 @@ public class ProcessingChain implements IChannelEventListener mModules.add( module ); /* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Listeners <<<<<<<<<<<<<<<<<<<<<<<<<<< */ + if( module instanceof IAudioPacketListener ) + { + mAudioPacketBroadcaster.addListener( + ((IAudioPacketListener)module).getAudioPacketListener() ); + } + if( module instanceof ICallEventListener ) { mCallEventBroadcaster.addListener( @@ -396,10 +414,16 @@ public class ProcessingChain implements IChannelEventListener { if( mSource != null ) { - /* Initialize each of the modules */ + /* Reset each of the modules */ for( Module module: mModules ) { - module.init(); + module.reset(); + } + + /* Start each of the modules */ + for( Module module: mModules ) + { + module.start(); } /* Register with the source to receive sample data. Setup a @@ -475,6 +499,12 @@ public class ProcessingChain implements IChannelEventListener mThreadPoolManager.cancel( mBufferProcessingTask ); mBufferProcessingTask = null; } + + /* Stop each of the modules */ + for( Module module: mModules ) + { + module.stop(); + } } } diff --git a/src/module/decode/Decoder.java b/src/module/decode/Decoder.java index ea216ff5..35b8ef27 100644 --- a/src/module/decode/Decoder.java +++ b/src/module/decode/Decoder.java @@ -73,11 +73,27 @@ public abstract class Decoder extends Module * external modules can be correctly configured. This method is invoked * prior to processing start. */ - public void init() + public void reset() { mDecoderState.init(); } - + + /** + * Start the decoder + */ + @Override + public void start() + { + } + + /** + * Stop the decoder + */ + @Override + public void stop() + { + } + /** * Identifies the decoder type (ie protocol) */ diff --git a/src/module/decode/DecoderFactory.java b/src/module/decode/DecoderFactory.java index 28a092f3..a7ea21d6 100644 --- a/src/module/decode/DecoderFactory.java +++ b/src/module/decode/DecoderFactory.java @@ -71,6 +71,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import properties.SystemProperties; +import record.RecorderType; +import record.config.RecordConfiguration; import settings.SettingsManager; import util.TimeStamp; import alias.AliasList; @@ -79,6 +81,8 @@ import controller.ResourceManager; import controller.channel.Channel.ChannelType; import controller.channel.ChannelNode; import controller.channel.map.ChannelMap; +import controller.site.Site; +import controller.system.System; import filter.AllPassFilter; import filter.FilterSet; import filter.IFilter; @@ -87,7 +91,7 @@ public class DecoderFactory { private final static Logger mLog = LoggerFactory.getLogger( DecoderFactory.class ); - public static final boolean NO_DC_REMOVAL = false; + public static final boolean NO_REMOVE_DC = false; public static final boolean REMOVE_DC = true; public static final int NO_FREQUENCY_CORRECTION = 0; @@ -96,80 +100,98 @@ public class DecoderFactory * Returns a list of one primary decoder and any auxiliary decoders, as * specified in the configurations. * - * @param config - primary decoder configuration + * @param channelType - standard or traffic channel type + * @param resourceManager + * @param decodeConfig - primary decoder configuration + * @param recordConfig - recording configuration used by traffic channel manager * @param auxConfig - auxiliary decoder(s) configuration + * @param optional alias list - (null is ok) * * @return list of configured decoders */ public static List getModules( ChannelType channelType, ResourceManager resourceManager, - DecodeConfiguration config, + DecodeConfiguration decodeConfig, + RecordConfiguration recordConfig, AuxDecodeConfiguration auxConfig, - AliasList aliasList ) - { - List modules = getPrimaryModules( channelType, config, - aliasList, resourceManager ); + AliasList aliasList, + System system, + Site site ) + { + List modules = getPrimaryModules( channelType, decodeConfig, + recordConfig, aliasList, system, site, resourceManager ); modules.addAll( getAuxiliaryDecoders( auxConfig, aliasList ) ); return modules; } - public static FMDemodulatorModule getFMDemodulator( DecodeConfiguration config, int pass, int stop, boolean removeDC ) + public static FMDemodulatorModule getFMDemodulator( + DecodeConfiguration config, int pass, int stop, boolean removeDC ) { if( config.isAFCEnabled() ) { - return new FMDemodulatorModule( pass, stop, config.getAFCMaximumCorrection(), removeDC ); + return new FMDemodulatorModule( pass, stop, + config.getAFCMaximumCorrection(), removeDC ); } else { - return new FMDemodulatorModule( pass, stop, NO_FREQUENCY_CORRECTION, removeDC ); + return new FMDemodulatorModule( pass, stop, + NO_FREQUENCY_CORRECTION, removeDC ); } } /** * Constructs a primary decoder as specified in the decode configuration - * @param config - primary decoder configuration + * + * @param channelType - traffic or standard decode channel + * @param decodeConfig - primary decoder configuration + * @param recordConfig - recording options * @param aliasList - optional alias list + * @param resourceManager - shared resource manager * @return configured decoder or null */ public static List getPrimaryModules( ChannelType channelType, - DecodeConfiguration config, - AliasList aliasList, + DecodeConfiguration decodeConfig, + RecordConfiguration recordConfig, + AliasList aliasList, + System system, + Site site, ResourceManager resourceManager ) { List modules = new ArrayList(); /* Baseband low-pass filter pass and stop frequencies */ - int pass = config.getDecoderType().getChannelBandwidth() / 2; + int pass = decodeConfig.getDecoderType().getChannelBandwidth() / 2; int stop = pass + 1250; - switch( config.getDecoderType() ) + boolean recordAudio = recordConfig.getRecorders().contains( RecorderType.AUDIO ); + + switch( decodeConfig.getDecoderType() ) { case AM: // decoder = new AMDecoder(); break; case NBFM: - modules.add( new AudioModule() ); - modules.add( new NBFMDecoder( config ) ); - modules.add( getFMDemodulator( config, pass, stop, REMOVE_DC ) ); + modules.add( new AudioModule( recordAudio, NO_REMOVE_DC ) ); + modules.add( new NBFMDecoder( decodeConfig ) ); + modules.add( getFMDemodulator( decodeConfig, pass, stop, REMOVE_DC ) ); break; case LTR_STANDARD: - modules.add( new AudioModule( REMOVE_DC ) ); - MessageDirection direction = ((DecodeConfigLTRStandard)config).getMessageDirection(); + modules.add( new AudioModule( recordAudio, REMOVE_DC ) ); + MessageDirection direction = ((DecodeConfigLTRStandard)decodeConfig).getMessageDirection(); modules.add( new LTRStandardDecoder( aliasList, direction ) ); - modules.add( getFMDemodulator( config, pass, stop, NO_DC_REMOVAL ) ); + modules.add( getFMDemodulator( decodeConfig, pass, stop, NO_REMOVE_DC ) ); break; case LTR_NET: - modules.add( new AudioModule( REMOVE_DC ) ); - modules.add( new LTRNetDecoder( (DecodeConfigLTRNet)config, aliasList ) ); -// modules.add( new ComplexBufferRecorderModule( getRecordingFilename( "decimated_baseband" ) ) ); - modules.add( getFMDemodulator( config, pass, stop, NO_DC_REMOVAL ) ); + modules.add( new AudioModule( recordAudio, REMOVE_DC ) ); + modules.add( new LTRNetDecoder( (DecodeConfigLTRNet)decodeConfig, aliasList ) ); + modules.add( getFMDemodulator( decodeConfig, pass, stop, NO_REMOVE_DC ) ); break; case MPT1327: - DecodeConfigMPT1327 mptConfig = (DecodeConfigMPT1327)config; + DecodeConfigMPT1327 mptConfig = (DecodeConfigMPT1327)decodeConfig; - modules.add( new AudioModule() ); + modules.add( new AudioModule( recordAudio, NO_REMOVE_DC ) ); ChannelMap channelMap = resourceManager.getPlaylistManager().getPlayist() .getChannelMapList().getChannelMap( mptConfig.getChannelMapName() ); @@ -181,25 +203,25 @@ public class DecoderFactory long timeout = mptConfig.getCallTimeout() * 1000; //convert to milliseconds modules.add( new TrafficChannelManager( resourceManager, - config, timeout, + decodeConfig, recordConfig, system, site, + aliasList.getName(), timeout, mptConfig.getTrafficChannelPoolSize() ) ); } -// modules.add( new RealBufferRecorderModule( getRecordingFilename( "demodulated_mpt1327" ) ) ); - modules.add( getFMDemodulator( config, pass, stop, REMOVE_DC ) ); + modules.add( getFMDemodulator( decodeConfig, pass, stop, REMOVE_DC ) ); break; case PASSPORT: // decoder = new PassportDecoder( config, aliasList ); break; case P25_PHASE1: - DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1)config; + DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1)decodeConfig; Modulation modulation = p25Config.getModulation(); switch( modulation ) { case C4FM: - modules.add( getFMDemodulator( config, 6750, 7500, REMOVE_DC ) ); + modules.add( getFMDemodulator( decodeConfig, 6750, 7500, REMOVE_DC ) ); modules.add( new P25_C4FMDecoder( aliasList, channelType ) ); break; case CQPSK: @@ -215,16 +237,17 @@ public class DecoderFactory long timeout = p25Config.getCallTimeout() * 1000; //convert to milliseconds modules.add( new TrafficChannelManager( resourceManager, - config, timeout, + decodeConfig, recordConfig, system, site, + aliasList.getName(), timeout, p25Config.getTrafficChannelPoolSize() ) ); } - modules.add( new P25AudioModule() ); + modules.add( new P25AudioModule( recordAudio ) ); break; default: throw new IllegalArgumentException( "Unknown decoder type [" + - config.getDecoderType().toString() + "]" ); + decodeConfig.getDecoderType().toString() + "]" ); } return modules; diff --git a/src/module/decode/event/CallEvent.java b/src/module/decode/event/CallEvent.java index 1cfcf9ca..88d8f43e 100644 --- a/src/module/decode/event/CallEvent.java +++ b/src/module/decode/event/CallEvent.java @@ -23,7 +23,7 @@ import java.util.Date; import module.decode.DecoderType; import alias.Alias; import alias.AliasList; -import alias.MetadataType; +import audio.metadata.MetadataType; public abstract class CallEvent implements Comparable { @@ -133,7 +133,7 @@ public abstract class CallEvent implements Comparable public MetadataType getFromIDType() { - return MetadataType.FROM_TALKGROUP; + return MetadataType.FROM; } public void setFromID( String id ) diff --git a/src/module/decode/mpt1327/MPT1327Decoder.java b/src/module/decode/mpt1327/MPT1327Decoder.java index 1ef00a4d..6bd1a3dc 100644 --- a/src/module/decode/mpt1327/MPT1327Decoder.java +++ b/src/module/decode/mpt1327/MPT1327Decoder.java @@ -26,6 +26,9 @@ import instrument.tap.stream.FloatTap; import java.util.ArrayList; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import module.decode.Decoder; import module.decode.DecoderType; import sample.Broadcaster; @@ -35,6 +38,7 @@ import sample.real.RealBuffer; import alias.AliasList; import bits.MessageFramer; import bits.SyncPattern; +import controller.channel.Channel; import controller.channel.Channel.ChannelType; import controller.channel.map.ChannelMap; import dsp.filter.FilterFactory; @@ -54,6 +58,8 @@ import dsp.fsk.FSK2Decoder.Output; public class MPT1327Decoder extends Decoder implements IRealBufferListener, Instrumentable { + private final static Logger mLog = LoggerFactory.getLogger( MPT1327Decoder.class ); + /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ private static final int sDECIMATED_SAMPLE_RATE = 24000; diff --git a/src/module/decode/mpt1327/MPT1327DecoderPanel.java b/src/module/decode/mpt1327/MPT1327DecoderPanel.java index 579aa112..1288d3b4 100644 --- a/src/module/decode/mpt1327/MPT1327DecoderPanel.java +++ b/src/module/decode/mpt1327/MPT1327DecoderPanel.java @@ -35,12 +35,12 @@ public class MPT1327DecoderPanel extends DecoderPanel private static final long serialVersionUID = 1L; private JLabel mProtocol = new JLabel( "" ); - private JLabel mSiteOrFromTalkgroup = new JLabel( "" ); - private JLabel mSiteOrFromTalkgroupAlias = new JLabel( "" ); + private JLabel mSiteOrToTalkgroup = new JLabel( "" ); + private JLabel mSiteOrToTalkgroupAlias = new JLabel( "" ); - private JLabel mToTalkgroupLabel = new JLabel( "" ); - private JLabel mToTalkgroup = new JLabel( "" ); - private JLabel mToTalkgroupAliasLabel = new JLabel( "" ); + private JLabel mFromTalkgroupLabel = new JLabel( "" ); + private JLabel mFromTalkgroup = new JLabel( "" ); + private JLabel mFromTalkgroupAliasLabel = new JLabel( "" ); public MPT1327DecoderPanel( SettingsManager settingsManager, MPT1327Decoder decoder ) { @@ -71,35 +71,35 @@ public class MPT1327DecoderPanel extends DecoderPanel MPT1327DecoderState mptState = getDecoderState(); - mSiteOrFromTalkgroup.setFont( mFontDecoder ); - mSiteOrFromTalkgroup.setForeground( mColorLabelDecoder ); + mSiteOrToTalkgroup.setFont( mFontDecoder ); + mSiteOrToTalkgroup.setForeground( mColorLabelDecoder ); - mSiteOrFromTalkgroupAlias.setFont( mFontDecoder ); - mSiteOrFromTalkgroupAlias.setForeground( mColorLabelDecoder ); + mSiteOrToTalkgroupAlias.setFont( mFontDecoder ); + mSiteOrToTalkgroupAlias.setForeground( mColorLabelDecoder ); - mToTalkgroupLabel.setFont( mFontDetails ); - mToTalkgroupLabel.setForeground( mColorLabelDetails ); + mFromTalkgroupLabel.setFont( mFontDetails ); + mFromTalkgroupLabel.setForeground( mColorLabelDetails ); - mToTalkgroup.setFont( mFontDecoder ); - mToTalkgroup.setForeground( mColorLabelDecoder ); + mFromTalkgroup.setFont( mFontDecoder ); + mFromTalkgroup.setForeground( mColorLabelDecoder ); - mToTalkgroupAliasLabel.setFont( mFontDecoder ); - mToTalkgroupAliasLabel.setForeground( mColorLabelDecoder ); + mFromTalkgroupAliasLabel.setFont( mFontDecoder ); + mFromTalkgroupAliasLabel.setForeground( mColorLabelDecoder ); switch( mptState.getChannelType() ) { case STANDARD: - mSiteOrFromTalkgroup.setText( mptState.getSite() ); - setAliasLabel( mSiteOrFromTalkgroupAlias, mptState.getSiteAlias() ); + mSiteOrToTalkgroup.setText( mptState.getSite() ); + setAliasLabel( mSiteOrToTalkgroupAlias, mptState.getSiteAlias() ); break; case TRAFFIC: - mSiteOrFromTalkgroup.setText( mptState.getFromTalkgroup() ); - setAliasLabel( mSiteOrFromTalkgroupAlias, mptState.getFromTalkgroupAlias() ); + mSiteOrToTalkgroup.setText( mptState.getToTalkgroup() ); + setAliasLabel( mSiteOrToTalkgroupAlias, mptState.getToTalkgroupAlias() ); - mToTalkgroupLabel.setText( "To:" ); - mToTalkgroup.setText( getDecoderState().getToTalkgroup() ); - setAliasLabel( mToTalkgroupAliasLabel, mptState.getToTalkgroupAlias() ); + mFromTalkgroupLabel.setText( "From:" ); + mFromTalkgroup.setText( getDecoderState().getFromTalkgroup() ); + setAliasLabel( mFromTalkgroupAliasLabel, mptState.getFromTalkgroupAlias() ); break; default: throw new IllegalArgumentException( "Unrecognized channel " @@ -108,14 +108,14 @@ public class MPT1327DecoderPanel extends DecoderPanel } add( mProtocol ); - add( mSiteOrFromTalkgroup ); - add( mSiteOrFromTalkgroupAlias, "wrap" ); + add( mSiteOrToTalkgroup ); + add( mSiteOrToTalkgroupAlias, "wrap" ); if( mptState.getChannelType() == ChannelType.TRAFFIC ) { - add( mToTalkgroupLabel ); - add( mToTalkgroup ); - add( mToTalkgroupAliasLabel, "wrap" ); + add( mFromTalkgroupLabel ); + add( mFromTalkgroup ); + add( mFromTalkgroupAliasLabel, "wrap" ); } } @@ -156,18 +156,18 @@ public class MPT1327DecoderPanel extends DecoderPanel switch( changedAttribute ) { case CHANNEL_SITE_NUMBER: - mSiteOrFromTalkgroup.setText( String.valueOf( state.getSite() ) ); - setAliasLabel( mSiteOrFromTalkgroupAlias, state.getSiteAlias() ); - repaint(); - break; - case FROM_TALKGROUP: - mSiteOrFromTalkgroup.setText( state.getFromTalkgroup() ); - setAliasLabel( mSiteOrFromTalkgroupAlias, state.getFromTalkgroupAlias() ); + mSiteOrToTalkgroup.setText( String.valueOf( state.getSite() ) ); + setAliasLabel( mSiteOrToTalkgroupAlias, state.getSiteAlias() ); repaint(); break; case TO_TALKGROUP: - mToTalkgroup.setText( state.getToTalkgroup() ); - setAliasLabel( mToTalkgroupAliasLabel, state.getToTalkgroupAlias() ); + mSiteOrToTalkgroup.setText( state.getToTalkgroup() ); + setAliasLabel( mSiteOrToTalkgroupAlias, state.getToTalkgroupAlias() ); + repaint(); + break; + case FROM_TALKGROUP: + mFromTalkgroup.setText( state.getFromTalkgroup() ); + setAliasLabel( mFromTalkgroupAliasLabel, state.getFromTalkgroupAlias() ); repaint(); break; default: @@ -193,27 +193,27 @@ public class MPT1327DecoderPanel extends DecoderPanel { mProtocol.setForeground( mColorLabelDecoder ); } - if( mSiteOrFromTalkgroup != null ) + if( mSiteOrToTalkgroup != null ) { - mSiteOrFromTalkgroup.setForeground( mColorLabelDecoder ); + mSiteOrToTalkgroup.setForeground( mColorLabelDecoder ); } - if( mSiteOrFromTalkgroupAlias != null ) + if( mSiteOrToTalkgroupAlias != null ) { - mSiteOrFromTalkgroupAlias.setForeground( mColorLabelDecoder ); + mSiteOrToTalkgroupAlias.setForeground( mColorLabelDecoder ); } - if( mToTalkgroup != null ) + if( mFromTalkgroup != null ) { - mToTalkgroup.setForeground( mColorLabelDecoder ); + mFromTalkgroup.setForeground( mColorLabelDecoder ); } - if( mToTalkgroupAliasLabel != null ) + if( mFromTalkgroupAliasLabel != null ) { - mToTalkgroupAliasLabel.setForeground( mColorLabelDecoder ); + mFromTalkgroupAliasLabel.setForeground( mColorLabelDecoder ); } break; case CHANNEL_STATE_LABEL_DETAILS: - if( mToTalkgroupLabel != null ) + if( mFromTalkgroupLabel != null ) { - mToTalkgroupLabel.setForeground( mColorLabelDetails ); + mFromTalkgroupLabel.setForeground( mColorLabelDetails ); } break; default: diff --git a/src/module/decode/mpt1327/MPT1327DecoderState.java b/src/module/decode/mpt1327/MPT1327DecoderState.java index 3e4db86e..6d44a744 100644 --- a/src/module/decode/mpt1327/MPT1327DecoderState.java +++ b/src/module/decode/mpt1327/MPT1327DecoderState.java @@ -245,42 +245,6 @@ public class MPT1327DecoderState extends DecoderState } } -// /** -// * Intercept the fade event so that we can generate a call end event -// */ -// @Override -// public void fade( final CallEventType type ) -// { -// /* -// * We can receive multiple call tear-down messages -- only post a call -// * end event for the one that can change the state to fade -// */ -// if( getState().canChangeTo( State.FADE ) ) -// { -// CallEvent current = getCurrentCallEvent(); -// -// if( current != null ) -// { -// mCallEventModel.setEnd( current ); -// } -// else -// { -// mCallEventModel.add( -// new MPT1327CallEvent.Builder( type ) -// .aliasList( getAliasList() ) -// .channel( String.valueOf( mChannelNumber ) ) -// .frequency( mChannelMap.getFrequency( mChannelNumber ) ) -// .from( mFromTalkgroup ) -// .to( mToTalkgroup ) -// .build() ); -// } -// -// setCurrentCall( null ); -// } -// -// super.fade( type ); -// } - public void reset() { mIdents.clear(); @@ -343,7 +307,7 @@ public class MPT1327DecoderState extends DecoderState { if( hasAliasList() ) { - return getAliasList().getTalkgroupAlias( mFromTalkgroup ); + return getAliasList().getMPT1327Alias( mFromTalkgroup ); } return null; @@ -370,7 +334,7 @@ public class MPT1327DecoderState extends DecoderState { if( hasAliasList() ) { - return getAliasList().getTalkgroupAlias( mToTalkgroup ); + return getAliasList().getMPT1327Alias( mToTalkgroup ); } return null; diff --git a/src/module/decode/p25/C4FMSymbolFilter.java b/src/module/decode/p25/C4FMSymbolFilter.java index 3edb6d7f..15f24b9b 100644 --- a/src/module/decode/p25/C4FMSymbolFilter.java +++ b/src/module/decode/p25/C4FMSymbolFilter.java @@ -11,14 +11,17 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sample.Listener; +import sample.real.RealBuffer; import sample.real.RealSampleListener; import sample.real.RealSampleProvider; import source.tuner.frequency.FrequencyCorrectionControl; import source.tuner.frequency.FrequencyCorrectionControl.FrequencyCorrectionResetListener; +import dsp.gain.DirectGainControl; import dsp.gain.GainController; public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, - RealSampleListener, + Listener, RealSampleProvider, Instrumentable { @@ -202,8 +205,11 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, private RealSampleListener mListener; - private GainController mGainController; + private DirectGainControl mGainController = + new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f ); + private FrequencyCorrectionControl mFrequencyCorrectionControl; + private int mFrequencyAdjustmentRequested = 0; /** * C4FM Symbol Filter @@ -218,11 +224,8 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, * setting for this filter is 15.4 and that yields a symbol spread that * is centered on 2.0, ranging 1.92 to 2.08. */ - public C4FMSymbolFilter( GainController gainController, - FrequencyCorrectionControl frequencyControl ) + public C4FMSymbolFilter( FrequencyCorrectionControl frequencyControl ) { - mGainController = gainController; - mFrequencyCorrectionControl = frequencyControl; /* Register for reset events, so we can reset internal frequency tracking */ @@ -237,9 +240,36 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, } @Override + public void receive( RealBuffer buffer ) + { + for( float sample: buffer.getSamples() ) + { + receive( sample ); + } + + /* If a frequency correction was requested during the processing of this + * buffer, we'll apply the change and it will be reflected in the next + * arriving buffer. Reset the lock on frequency correction and reset + * the internal frequency correction tracker */ + if( mFrequencyAdjustmentRequested != 0 ) + { + mFrequencyCorrectionControl.adjust( mFrequencyAdjustmentRequested ); + + mLog.debug( "Adjusting frequency [" + mFrequencyAdjustmentRequested + + "] correction is now: " + mFrequencyCorrectionControl + .getErrorCorrection() ); + + mFrequencyAdjustmentRequested = 0; + + /* reset internal frequency error tracker */ + mCoarseFrequencyCorrection = 0; + } + } + public void receive( float sample ) { - //TODO: limit frequency corrections to once per buffer !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!11 + sample = mGainController.correct( sample ); + if( mReset ) { mGainController.reset(); @@ -353,15 +383,11 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, mFineFrequencyCorrection += ( symbolError * K_FINE_FREQUENCY ); - /* send frequency correction */ + /* Queue a frequency adjustment (once per buffer) as needed */ if( Math.abs( mCoarseFrequencyCorrection ) > COARSE_FREQUENCY_DEADBAND ) { - int adjustment = 500 * ( mCoarseFrequencyCorrection > 0 ? 1 : -1 ); - - mFrequencyCorrectionControl.adjust( adjustment ); - - /* reset internal frequency error tracker */ - mCoarseFrequencyCorrection = 0; + mFrequencyAdjustmentRequested = + 500 * ( mCoarseFrequencyCorrection > 0 ? 1 : -1 ); } if( mSymbolSpreadTap != null ) diff --git a/src/module/decode/p25/P25DecoderState.java b/src/module/decode/p25/P25DecoderState.java index 55dd03b7..570492f3 100644 --- a/src/module/decode/p25/P25DecoderState.java +++ b/src/module/decode/p25/P25DecoderState.java @@ -1501,6 +1501,7 @@ public class P25DecoderState extends DecoderState } else { + /* These are alternate trunking control blocks in PDU format */ switch( pdu.getOpcode() ) { case ADJACENT_STATUS_BROADCAST: @@ -1583,7 +1584,7 @@ public class P25DecoderState extends DecoderState GroupDataChannelGrantExtended gdcge = (GroupDataChannelGrantExtended)pdu; - broadcast( new P25CallEvent.Builder( CallEventType.DATA_CALL ) + CallEvent callEvent = new P25CallEvent.Builder( CallEventType.DATA_CALL ) .aliasList( getAliasList() ) .channel( gdcge.getTransmitChannel() ) .details( ( gdcge.isEncrypted() ? "ENCRYPTED" : "" ) + @@ -1591,7 +1592,9 @@ public class P25DecoderState extends DecoderState .frequency( gdcge.getDownlinkFrequency() ) .from( gdcge.getSourceAddress() ) .to( gdcge.getGroupAddress() ) - .build() ); + .build(); + + broadcast( new TrafficChannelAllocationEvent( this, callEvent ) ); } else { @@ -1604,16 +1607,17 @@ public class P25DecoderState extends DecoderState GroupVoiceChannelGrantExplicit gvcge = (GroupVoiceChannelGrantExplicit)pdu; - broadcast( new P25CallEvent.Builder( CallEventType.CALL ) + CallEvent callEvent = new P25CallEvent.Builder( CallEventType.CALL ) .aliasList( getAliasList() ) .channel( gvcge.getTransmitChannel() ) - .details( "Trigger: PDU-GRP_V_CHAN_USER" + - ( gvcge.isEncrypted() ? "ENCRYPTED" : "" ) + + .details( ( gvcge.isEncrypted() ? "ENCRYPTED" : "" ) + ( gvcge.isEmergency() ? " EMERGENCY" : "") ) .frequency( gvcge.getDownlinkFrequency() ) .from( gvcge.getSourceAddress() ) .to( gvcge.getGroupAddress() ) - .build() ); + .build(); + + broadcast( new TrafficChannelAllocationEvent( this, callEvent ) ); } else { @@ -1626,18 +1630,19 @@ public class P25DecoderState extends DecoderState IndividualDataChannelGrantExtended idcge = (IndividualDataChannelGrantExtended)pdu; - broadcast( new P25CallEvent.Builder( CallEventType.DATA_CALL ) + CallEvent callEvent = new P25CallEvent.Builder( CallEventType.DATA_CALL ) .aliasList( getAliasList() ) .channel( idcge.getTransmitChannel() ) - .details( "Trigger:PDU-IND_DATA_CHAN_GRNT " + - ( idcge.isEncrypted() ? "ENCRYPTED" : "" ) + + .details( ( idcge.isEncrypted() ? "ENCRYPTED" : "" ) + ( idcge.isEmergency() ? " EMERGENCY" : "") ) .frequency( idcge.getDownlinkFrequency() ) .from( idcge.getSourceWACN() + "-" + idcge.getSourceSystemID() + "-" + idcge.getSourceAddress() ) .to( idcge.getTargetAddress() ) - .build() ); + .build(); + + broadcast( new TrafficChannelAllocationEvent( this, callEvent ) ); } else { @@ -1773,7 +1778,7 @@ public class P25DecoderState extends DecoderState TelephoneInterconnectChannelGrantExplicit ticge = (TelephoneInterconnectChannelGrantExplicit)pdu; - broadcast( new P25CallEvent.Builder( CallEventType.CALL ) + CallEvent callEvent = new P25CallEvent.Builder( CallEventType.CALL ) .aliasList( getAliasList() ) .channel( ticge.getTransmitChannel() ) .details( "Trigger: PDU-TEL_INT_V_CH_GRNT " + @@ -1783,7 +1788,9 @@ public class P25DecoderState extends DecoderState " CALL TIMER:" + ticge.getCallTimer() ) .frequency( ticge.getDownlinkFrequency() ) .from( ticge.getAddress() ) - .build() ); + .build(); + + broadcast( new TrafficChannelAllocationEvent( this, callEvent ) ); break; case UNIT_REGISTRATION_RESPONSE: if( pdu instanceof UnitRegistrationResponseExtended ) @@ -1851,7 +1858,7 @@ public class P25DecoderState extends DecoderState UnitToUnitVoiceChannelGrantExtended uuvcge = (UnitToUnitVoiceChannelGrantExtended)pdu; - broadcast( new P25CallEvent.Builder( CallEventType.CALL ) + CallEvent uuCallEvent = new P25CallEvent.Builder( CallEventType.CALL ) .aliasList( getAliasList() ) .channel( uuvcge.getTransmitChannel() ) .details( "Trigger:PDU-U2U_V-CH_GRNT " + @@ -1862,7 +1869,9 @@ public class P25DecoderState extends DecoderState uuvcge.getSourceSystemID() + "-" + uuvcge.getSourceID() ) .to( uuvcge.getTargetAddress() ) - .build() ); + .build(); + + broadcast( new TrafficChannelAllocationEvent( this, uuCallEvent ) ); } else { diff --git a/src/module/decode/p25/P25_C4FMDecoder.java b/src/module/decode/p25/P25_C4FMDecoder.java index e5b5fe73..5647fa25 100644 --- a/src/module/decode/p25/P25_C4FMDecoder.java +++ b/src/module/decode/p25/P25_C4FMDecoder.java @@ -19,6 +19,7 @@ package module.decode.p25; import instrument.tap.Tap; import instrument.tap.stream.DibitTap; +import instrument.tap.stream.FloatBufferTap; import instrument.tap.stream.FloatTap; import java.util.ArrayList; @@ -30,14 +31,12 @@ import org.slf4j.LoggerFactory; import sample.Listener; import sample.real.IRealBufferListener; import sample.real.RealBuffer; -import sample.real.RealBufferToStreamConverter; import source.tuner.frequency.FrequencyCorrectionControl; import alias.AliasList; import controller.channel.Channel.ChannelType; import dsp.filter.FilterFactory; import dsp.filter.Window.WindowType; import dsp.filter.fir.real.RealFIRFilter_RB_RB; -import dsp.gain.DirectGainControl; public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener { @@ -45,7 +44,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener /* Instrumentation Taps */ private static final String INSTRUMENT_FILTER_OUTPUT = "Tap Point: Pre-Filter Output"; - private static final String INSTRUMENT_DGC_OUTPUT = "Tap Point: DGC Output"; private static final String INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT = "Tap Point: Symbol Filter Output"; private static final String INSTRUMENT_C4FM_SLICER_OUTPUT = "Tap Point: C4FM Slicer Output"; @@ -53,9 +51,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener private List mAvailableTaps; private RealFIRFilter_RB_RB mC4FMPreFilter; - private DirectGainControl mDGC; - private RealBufferToStreamConverter mStreamConverter = - new RealBufferToStreamConverter(); private FrequencyCorrectionControl mFrequencyCorrectionControl; private C4FMSymbolFilter mSymbolFilter; private C4FMSlicer mC4FMSlicer; @@ -82,22 +77,14 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener mC4FMPreFilter = new RealFIRFilter_RB_RB( FilterFactory.getLowPass( 48000, 6750, 7500, 60, WindowType.HANNING, true ), 1.0f ); - /* Convert buffers to a sample stream so that the symbol filter can - * provide fine-grained gain control over individual samples */ - mC4FMPreFilter.setListener( mStreamConverter ); - - /* Apply gain remotely controlled by the downstream symbol filter */ - mDGC = new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f ); - mStreamConverter.setListener( mDGC ); - /* Issue tuned frequency correction commands, remotely controlled by the * downstream symbol filter */ mFrequencyCorrectionControl = new FrequencyCorrectionControl( MAXIMUM_FREQUENCY_CORRECTION ); - + /* Shape gain and frequency offsets to optimize sample stream */ - mSymbolFilter = new C4FMSymbolFilter( mDGC, mFrequencyCorrectionControl ); - mDGC.setListener( mSymbolFilter ); + mSymbolFilter = new C4FMSymbolFilter( mFrequencyCorrectionControl ); + mC4FMPreFilter.setListener( mSymbolFilter ); /* Convert samples to symbols */ mC4FMSlicer = new C4FMSlicer(); @@ -119,12 +106,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener mC4FMPreFilter.dispose(); mC4FMPreFilter = null; - mDGC.dispose(); - mDGC = null; - - mStreamConverter.dispose(); - mStreamConverter = null; - mFrequencyCorrectionControl.dispose(); mFrequencyCorrectionControl = null; @@ -172,7 +153,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener mAvailableTaps = new ArrayList(); mAvailableTaps.add( new FloatTap( INSTRUMENT_FILTER_OUTPUT, 0, 1.0f ) ); - mAvailableTaps.add( new FloatTap( INSTRUMENT_DGC_OUTPUT, 0, 1.0f ) ); if( mSymbolFilter != null ) { @@ -200,14 +180,9 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener switch( tap.getName() ) { case INSTRUMENT_FILTER_OUTPUT: - FloatTap filterOutput = (FloatTap)tap; - mStreamConverter.setListener( filterOutput ); - filterOutput.setListener( mDGC ); - break; - case INSTRUMENT_DGC_OUTPUT: - FloatTap dgcTap = (FloatTap)tap; - mDGC.setListener( dgcTap ); - dgcTap.setListener( mSymbolFilter ); + FloatBufferTap filterOutput = (FloatBufferTap)tap; + mC4FMPreFilter.setListener( filterOutput ); + filterOutput.setListener( mSymbolFilter ); break; case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: FloatTap symbolTap = (FloatTap)tap; @@ -242,10 +217,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener switch( tap.getName() ) { case INSTRUMENT_FILTER_OUTPUT: - mStreamConverter.setListener( mDGC ); - break; - case INSTRUMENT_DGC_OUTPUT: - mDGC.setListener( mSymbolFilter ); + mC4FMPreFilter.setListener( mSymbolFilter ); break; case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: mSymbolFilter.setListener( mC4FMSlicer ); diff --git a/src/module/decode/p25/audio/P25AudioModule.java b/src/module/decode/p25/audio/P25AudioModule.java index 140ee98b..a3389f7e 100644 --- a/src/module/decode/p25/audio/P25AudioModule.java +++ b/src/module/decode/p25/audio/P25AudioModule.java @@ -15,12 +15,12 @@ import org.slf4j.LoggerFactory; import controller.channel.ChannelEvent; import controller.channel.IChannelEventListener; import sample.Listener; -import alias.Metadata; import audio.AudioFormats; import audio.AudioPacket; import audio.IAudioPacketProvider; import audio.metadata.AudioMetadata; import audio.metadata.IMetadataListener; +import audio.metadata.Metadata; public class P25AudioModule extends Module implements Listener, IAudioPacketProvider, IMessageListener, IChannelEventListener, IMetadataListener @@ -41,10 +41,12 @@ public class P25AudioModule extends Module implements Listener, private AudioMetadata mAudioMetadata; private ChannelEventListener mChannelEventListener = new ChannelEventListener(); - public P25AudioModule() + public P25AudioModule( boolean record ) { mSourceID = ++UNIQUE_ID; - mAudioMetadata = new AudioMetadata( mSourceID ); + mAudioMetadata = new AudioMetadata( mSourceID, record ); + + loadConverter(); } @Override @@ -60,9 +62,21 @@ public class P25AudioModule extends Module implements Listener, } @Override - public void init() + public void reset() { - loadConverter(); + mAudioMetadata.reset(); + } + + @Override + public void start() + { + + } + + @Override + public void stop() + { + } /** diff --git a/src/module/decode/state/ChannelState.java b/src/module/decode/state/ChannelState.java index 546d2e60..8f094bc8 100644 --- a/src/module/decode/state/ChannelState.java +++ b/src/module/decode/state/ChannelState.java @@ -31,10 +31,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; -import alias.Metadata; -import alias.MetadataReset; -import alias.MetadataType; import audio.metadata.IMetadataProvider; +import audio.metadata.Metadata; +import audio.metadata.MetadataReset; +import audio.metadata.MetadataType; import audio.squelch.ISquelchStateProvider; import audio.squelch.SquelchState; import controller.ThreadPoolManager; @@ -117,9 +117,18 @@ public class ChannelState extends Module implements ICallEventProvider, } @Override - public void init() + public void reset() + { + } + + @Override + public void start() + { + } + + @Override + public void stop() { - } public void dispose() @@ -500,7 +509,7 @@ public class ChannelState extends Module implements ICallEventProvider, if( channel != null ) { - broadcast( new Metadata( MetadataType.CHANNEL, channel, true ) ); + broadcast( new Metadata( MetadataType.CHANNEL_NUMBER, channel, true ) ); } broadcast( new Metadata( MetadataType.PROTOCOL, diff --git a/src/module/decode/state/ChannelStatePanel.java b/src/module/decode/state/ChannelStatePanel.java index 34240743..15addcb5 100644 --- a/src/module/decode/state/ChannelStatePanel.java +++ b/src/module/decode/state/ChannelStatePanel.java @@ -225,7 +225,7 @@ public class ChannelStatePanel extends JPanel g2.drawLine( 0, getHeight() - 1, getWidth(), getHeight() - 1 ); /* Draw channel selected box */ - if( getChannel().isSelected() ) + if( getChannel() != null && getChannel().isSelected() ) { g2.setColor( mColorChannelSelected ); g2.drawRect( 1, 1, getWidth() - 2, getHeight() - 2 ); diff --git a/src/module/decode/state/TrafficChannelManager.java b/src/module/decode/state/TrafficChannelManager.java index cf3021cf..27294b34 100644 --- a/src/module/decode/state/TrafficChannelManager.java +++ b/src/module/decode/state/TrafficChannelManager.java @@ -14,10 +14,12 @@ import module.decode.event.ICallEventProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import record.config.RecordConfiguration; import sample.Listener; import source.config.SourceConfigTuner; import source.tuner.TunerChannel; import source.tuner.TunerChannel.Type; +import alias.AliasList; import controller.ResourceManager; import controller.channel.Channel; import controller.channel.Channel.ChannelType; @@ -38,7 +40,11 @@ public class TrafficChannelManager extends Module private Listener mCallEventListener; private DecodeConfiguration mDecodeConfiguration; + private RecordConfiguration mRecordConfiguration; private ResourceManager mResourceManager; + private System mSystem; + private Site mSite; + private String mAliasListName; private long mTrafficChannelTimeout; /** @@ -51,6 +57,8 @@ public class TrafficChannelManager extends Module * @param decodeConfiguration - decoder configuration to use for each * traffic channel allocation. * + * @param recordConfiguration - recording options for each traffic channel + * * @param trafficChannelTimeout - millisecond call timer limit used when an * end of call signalling event is not decoded. * @@ -59,11 +67,19 @@ public class TrafficChannelManager extends Module */ public TrafficChannelManager( ResourceManager resourceManager, DecodeConfiguration decodeConfiguration, + RecordConfiguration recordConfiguration, + System system, + Site site, + String aliasListName, long trafficChannelTimeout, int trafficChannelPoolSize ) { mResourceManager = resourceManager; mDecodeConfiguration = decodeConfiguration; + mRecordConfiguration = recordConfiguration; + mSystem = system; + mSite = site; + mAliasListName = aliasListName; mTrafficChannelTimeout = trafficChannelTimeout; mTrafficChannelPoolMaximumSize = trafficChannelPoolSize; } @@ -97,10 +113,14 @@ public class TrafficChannelManager extends Module channel.setDecodeConfiguration( mDecodeConfiguration ); + channel.setRecordConfiguration( mRecordConfiguration ); + channel.setResourceManager( mResourceManager ); channel.setTrafficChannelTimeout( mTrafficChannelTimeout ); + channel.setAliasListName( mAliasListName ); + mTrafficChannelPool.add( channel ); } @@ -109,8 +129,8 @@ public class TrafficChannelManager extends Module { channel.setSourceConfiguration( new SourceConfigTuner( tunerChannel ) ); - channel.setSystem( new System( "Traffic" ), false ); - channel.setSite( new Site( "Channel" ), false ); + channel.setSystem( mSystem, false ); + channel.setSite( mSite, false ); channel.setName( channelNumber ); channel.setEnabled( true ); @@ -124,8 +144,8 @@ public class TrafficChannelManager extends Module { channel.setEnabled( false ); - channel.setSystem( null, false ); - channel.setSite( null, false ); + channel.setSystem( mSystem, false ); + channel.setSite( mSite, false ); channel.setName( "Traffic" ); } } @@ -208,9 +228,30 @@ public class TrafficChannelManager extends Module } @Override - public void init() + public void reset() { - + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + if( !mTrafficChannelsInUse.isEmpty() ) + { + List channels = new ArrayList<>(); + + /* Copy the keyset so we don't get concurrent modification of the map */ + channels.addAll( mTrafficChannelsInUse.keySet() ); + + for( String channel: channels ) + { + callEnd( channel ); + } + } } /** diff --git a/src/module/demodulate/am/AMDemodulatorModule.java b/src/module/demodulate/am/AMDemodulatorModule.java index 5476529c..d9a77292 100644 --- a/src/module/demodulate/am/AMDemodulatorModule.java +++ b/src/module/demodulate/am/AMDemodulatorModule.java @@ -94,7 +94,7 @@ public class AMDemodulatorModule extends Module } @Override - public void init() + public void reset() { } @@ -109,4 +109,14 @@ public class AMDemodulatorModule extends Module { mAGC.removeListener(); } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } } diff --git a/src/module/demodulate/fm/FMDemodulatorModule.java b/src/module/demodulate/fm/FMDemodulatorModule.java index 299b523b..19d428b4 100644 --- a/src/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/module/demodulate/fm/FMDemodulatorModule.java @@ -151,7 +151,7 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene } @Override - public void init() + public void reset() { mDemodulator.reset(); @@ -193,4 +193,14 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene mUnfilteredBufferBroadcaster.removeListener( mListener ); } } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } } diff --git a/src/module/log/CallEventLogger.java b/src/module/log/CallEventLogger.java index 972dc805..3d01e45b 100644 --- a/src/module/log/CallEventLogger.java +++ b/src/module/log/CallEventLogger.java @@ -56,7 +56,7 @@ public class CallEventLogger extends EventLogger implements ICallEventListener, } @Override - public void init() + public void reset() { super.start(); } diff --git a/src/module/log/MessageEventLogger.java b/src/module/log/MessageEventLogger.java index b9a579d0..65c1c918 100644 --- a/src/module/log/MessageEventLogger.java +++ b/src/module/log/MessageEventLogger.java @@ -56,7 +56,7 @@ public class MessageEventLogger extends EventLogger implements } @Override - public void init() + public void reset() { super.start(); } diff --git a/src/module/record/ComplexBufferRecorderModule.java b/src/module/record/ComplexBufferRecorderModule.java deleted file mode 100644 index 2e896095..00000000 --- a/src/module/record/ComplexBufferRecorderModule.java +++ /dev/null @@ -1,64 +0,0 @@ -package module.record; - -import java.io.IOException; - -import module.Module; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import record.wave.ComplexWaveRecorder; -import sample.Listener; -import sample.complex.ComplexBuffer; -import sample.complex.IComplexBufferListener; - -public class ComplexBufferRecorderModule extends Module - implements IComplexBufferListener -{ - private final static Logger mLog = - LoggerFactory.getLogger( ComplexBufferRecorderModule.class ); - - private ComplexWaveRecorder mRecorder; - private String mFilename; - - public ComplexBufferRecorderModule( String filename ) - { - mFilename = filename; - mRecorder = new ComplexWaveRecorder( 48000, mFilename ); - } - - @Override - public Listener getComplexBufferListener() - { - return mRecorder; - } - - @Override - public void dispose() - { - try - { - mRecorder.stop(); - } - catch ( IOException e ) - { - mLog.error( "Error stopping the recorder", e ); - } - } - - @Override - public void init() - { - mLog.debug( "Starting the recorder [" + mFilename + "]" ); - - try - { - mRecorder.start(); - } - catch ( IOException e ) - { - mLog.error( "Error starting the recorder", e ); - } - } - -} diff --git a/src/module/record/RealBufferRecorderModule.java b/src/module/record/RealBufferRecorderModule.java deleted file mode 100644 index a31bb981..00000000 --- a/src/module/record/RealBufferRecorderModule.java +++ /dev/null @@ -1,64 +0,0 @@ -package module.record; - -import java.io.IOException; - -import module.Module; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import record.wave.FloatWaveRecorder; -import sample.Listener; -import sample.real.IRealBufferListener; -import sample.real.RealBuffer; - -public class RealBufferRecorderModule extends Module - implements IRealBufferListener -{ - private final static Logger mLog = - LoggerFactory.getLogger( RealBufferRecorderModule.class ); - - private FloatWaveRecorder mRecorder; - private String mFilename; - - public RealBufferRecorderModule( String filename ) - { - mFilename = filename; - mRecorder = new FloatWaveRecorder( 48000, mFilename ); - } - - @Override - public Listener getRealBufferListener() - { - return mRecorder; - } - - @Override - public void dispose() - { - try - { - mRecorder.stop(); - } - catch ( IOException e ) - { - mLog.error( "Error stopping the recorder", e ); - } - } - - @Override - public void init() - { - mLog.debug( "Starting the recorder [" + mFilename + "]" ); - - try - { - mRecorder.start(); - } - catch ( IOException e ) - { - mLog.error( "Error starting the recorder", e ); - } - } - -} diff --git a/src/record/RecordComponentEditor.java b/src/record/RecordComponentEditor.java index c5a8a7c2..41270a30 100644 --- a/src/record/RecordComponentEditor.java +++ b/src/record/RecordComponentEditor.java @@ -30,6 +30,7 @@ public class RecordComponentEditor extends AbstractChannelEditor private JCheckBox mAudioRecorder = new JCheckBox( "Audio" ); private JCheckBox mBasebandRecorder = new JCheckBox( "Baseband I/Q" ); + private JCheckBox mTrafficBasebandRecorder = new JCheckBox( "Traffic Channel Baseband I/Q" ); public RecordComponentEditor( ChannelNode channelNode ) { @@ -37,6 +38,7 @@ public class RecordComponentEditor extends AbstractChannelEditor add( mAudioRecorder, "span" ); add( mBasebandRecorder, "span" ); + add( mTrafficBasebandRecorder, "span" ); reset(); } @@ -58,6 +60,12 @@ public class RecordComponentEditor extends AbstractChannelEditor { config.addRecorder( RecorderType.BASEBAND ); } + + if( mTrafficBasebandRecorder.isSelected() ) + { + config.addRecorder( RecorderType.BASEBAND ); + } + } @Override @@ -76,6 +84,9 @@ public class RecordComponentEditor extends AbstractChannelEditor mBasebandRecorder.setSelected( config.getRecorders() .contains( RecorderType.BASEBAND ) ); + + mTrafficBasebandRecorder.setSelected( config.getRecorders() + .contains( RecorderType.TRAFFIC_BASEBAND ) ); } }); } diff --git a/src/record/RecorderManager.java b/src/record/RecorderManager.java index 07cf924d..0d77efe1 100644 --- a/src/record/RecorderManager.java +++ b/src/record/RecorderManager.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2014,2015 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 @@ -17,56 +17,174 @@ ******************************************************************************/ package record; + import java.io.File; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import properties.SystemProperties; -import record.config.RecordConfiguration; -import record.wave.ComplexWaveRecorder; -import record.wave.FloatWaveRecorder; -import util.TimeStamp; +import record.wave.ComplexBufferWaveRecorder; +import record.wave.RealBufferWaveRecorder; +import sample.Listener; +import audio.AudioPacket; +import audio.metadata.Metadata; +import audio.metadata.MetadataType; +import controller.ThreadPoolManager; -public class RecorderManager +public class RecorderManager implements Listener { - public static final int sSAMPLE_RATE = 48000; + private static final Logger mLog = LoggerFactory.getLogger( RecorderManager.class ); - public RecorderManager() + public static final int AUDIO_SAMPLE_RATE = 48000; + + private static final long RECORDER_INACTIVITY_THRESHOLD = 10000; //1 minute in millis + +// private static final RealBuffer AUDIO_SEPARATOR = new AudioSeparator(); + + private Map mRecorders = new HashMap<>(); + + private ThreadPoolManager mThreadPoolManager; + + public RecorderManager( ThreadPoolManager threadPoolManager ) { + mThreadPoolManager = threadPoolManager; + } + + @Override + public void receive( AudioPacket audioPacket ) + { + if( audioPacket.hasAudioMetadata() && + audioPacket.getAudioMetadata().isRecordable() ) + { + String identifier = audioPacket.getAudioMetadata().getIdentifier(); + + if( mRecorders.containsKey( identifier ) ) + { + RealBufferWaveRecorder recorder = mRecorders.get( identifier ); + + if( audioPacket.getType() == AudioPacket.Type.AUDIO ) + { + recorder.receive( audioPacket.getAudioBuffer() ); + } + else if( audioPacket.getType() == AudioPacket.Type.END ) + { + recorder.stop(); + mRecorders.remove( identifier ); + } + } + else + { + if( audioPacket.getType() == AudioPacket.Type.AUDIO ) + { + String filePrefix = getFilePrefix( audioPacket ); + + RealBufferWaveRecorder recorder = + new RealBufferWaveRecorder( mThreadPoolManager, + AUDIO_SAMPLE_RATE, filePrefix ); + + recorder.start(); + + recorder.receive( audioPacket.getAudioBuffer() ); + mRecorders.put( identifier, recorder ); + } + } + } + } + + /** + * Constructs a file name and path for an audio recording + */ + private String getFilePrefix( AudioPacket packet ) + { + StringBuilder sb = new StringBuilder(); + + sb.append( SystemProperties.getInstance() + .getApplicationFolder( "recordings" ) ); + + sb.append( File.separator ); + + Metadata systemMetadata = packet.getAudioMetadata() + .getMetadata( MetadataType.SYSTEM ); + + sb.append( systemMetadata != null ? + systemMetadata.getValue() : "UNKNOWN_SYSTEM" ); + + Metadata toMetadata = packet.getAudioMetadata() + .getMetadata( MetadataType.TO ); + + if( toMetadata != null ) + { + sb.append( "_TO_" ); + sb.append( toMetadata.getValue() ); + } + + Metadata fromMetadata = packet.getAudioMetadata() + .getMetadata( MetadataType.FROM ); + + if( fromMetadata != null ) + { + sb.append( "_FROM_" ); + sb.append( fromMetadata.getValue() ); + } + + return sb.toString(); } - //TODO: make this return modules - public List getRecorders( RecordConfiguration config ) + /** + * Returns a list of recorder modules for use in a processing chain. + * Note: currently this only returns a baseband recorder. + * + * Create an instance of this RecorderManager class to listen to and centrally + * record all decoded audio. + */ + public static ComplexBufferWaveRecorder getBasebandRecorder( + ThreadPoolManager threadPoolManager, String channelName ) { - /* Note: the file suffix (ie .wav) gets added by the recorder */ - - //Get the base recording filename - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); sb.append( SystemProperties.getInstance() .getApplicationFolder( "recordings" ) ); sb.append( File.separator ); - sb.append( TimeStamp.getTimeStamp( "_" ) ); - sb.append( "_" ); -// sb.append( channel.getChannel().getName() ); - - ArrayList retVal = new ArrayList(); + sb.append( channelName ); + sb.append( "_baseband" ); - for( RecorderType recorder: config.getRecorders() ) + return new ComplexBufferWaveRecorder( threadPoolManager, + AUDIO_SAMPLE_RATE, sb.toString() ); + } + + /** + * Removes any recorders where the recent activity timestamp has exceeded + * the retention threshold. + */ + public class RecorderProcessor implements Runnable + { + @Override + public void run() { - switch( recorder ) + if( !mRecorders.isEmpty() ) { - case AUDIO: - retVal.add( new FloatWaveRecorder( sSAMPLE_RATE, - sb.toString() + "_audio" ) ); - break; - case BASEBAND: - retVal.add( new ComplexWaveRecorder( sSAMPLE_RATE, - sb.toString() + "_baseband" ) ); - break; + List keysToRemove = new ArrayList<>(); + + for( Entry entry: mRecorders.entrySet() ) + { + if( entry.getValue().getLastBufferReceived() + + RECORDER_INACTIVITY_THRESHOLD < System.currentTimeMillis() ) + { + keysToRemove.add( entry.getKey() ); + } + } + + for( String key: keysToRemove ) + { + RealBufferWaveRecorder recorder = mRecorders.remove( key ); + recorder.dispose(); + } } } - - - return retVal; } } diff --git a/src/record/RecorderType.java b/src/record/RecorderType.java index a055ea63..bb83ba1f 100644 --- a/src/record/RecorderType.java +++ b/src/record/RecorderType.java @@ -20,7 +20,8 @@ package record; public enum RecorderType { AUDIO( "Audio (.wav)"), - BASEBAND( "Baseband I/Q (.wav)"); + BASEBAND( "Baseband I/Q (.wav)"), + TRAFFIC_BASEBAND( "Traffic Channel Baseband I/Q (.wav)" ); private String mDisplayString; diff --git a/src/record/wave/BooleanWaveRecorder.java b/src/record/wave/BooleanWaveRecorder.java deleted file mode 100644 index fab3b04f..00000000 --- a/src/record/wave/BooleanWaveRecorder.java +++ /dev/null @@ -1,181 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.LinkedBlockingQueue; - -import javax.sound.sampled.AudioFormat; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import record.Recorder; -import record.RecorderType; -import sample.Listener; -import util.waveaudio.WaveWriter; -import buffer.FloatSampleBufferAssembler; - -/** - * Threaded WAVE audio recorder for recording mono short samples to a wave file - */ -public class BooleanWaveRecorder extends Recorder implements Listener -{ - private final static Logger mLog = - LoggerFactory.getLogger( BooleanWaveRecorder.class ); - - private AudioFormat mAudioFormat; - private boolean mRunning = false; - private boolean mPaused = false; - private WaveWriter mWriter; - private String mFileName; - private BufferProcessor mBufferProcessor; - - //Sample buffer queue ... constrained to 512 sample buffers (arbitrary size) - private LinkedBlockingQueue mToWriteBufferQueue = - new LinkedBlockingQueue( 512 ); - private FloatSampleBufferAssembler mCurrentBuffer; - - public BooleanWaveRecorder( int sampleRate, String filename ) - { - super( RecorderType.AUDIO ); - - mAudioFormat = new AudioFormat( sampleRate, //SampleRate - 16, //Sample Size - 1, //Channels - true, //Signed - false ); //Little Endian - - mCurrentBuffer = new FloatSampleBufferAssembler( sampleRate ); - - mFileName = filename; - } - - @Override - public String getFileName() - { - return mFileName + ".wav"; - } - - public void start() throws IOException - { - if( !mRunning ) - { - mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat ); - mWriter.start(); - - mBufferProcessor = new BufferProcessor(); - mBufferProcessor.start(); - - mRunning = true; - } - } - - public void stop() throws IOException - { - if( mRunning ) - { - mBufferProcessor = null; - - mWriter.stop(); - mWriter = null; - - mRunning = false; - } - } - - public void pause() - { - mPaused = true; - } - - public void resume() - { - mPaused = false; - } - - /** - * Assumes that the byte[] represents samples corresponding to the - * audio format object defined at construction - */ - public void receive( ByteBuffer buffer ) - { - if( mRunning && !mPaused ) - { - boolean success = mToWriteBufferQueue.offer( buffer ); - - if( !success ) - { - mLog.error( "Wave recorder buffer overflow - throwing away " + - buffer.capacity() + " samples" ); - } - } - } - - @Override - public void receive( Boolean sample ) - { - /** - * If the sample is true, put short value of 5000, otherwise - * put a short value of -5000 - */ - mCurrentBuffer.put( sample ? 5000 : (short)-5000 ); - - if( !mCurrentBuffer.hasRemaining() ) - { - //Dispatch the buffer to be written to disk - if( !mToWriteBufferQueue.offer( mCurrentBuffer.get() ) ) - { - //We had an overflow ... log it - mLog.error( "ShortWaveRecorder: buffer overflow - throwing " - + "away 1 second of sample data" ); - } - - mCurrentBuffer.reset(); - } - } - - public class BufferProcessor extends Thread - { - public void run() - { - while( true ) - { - try - { - if( mWriter != null ) - { - mWriter.write( mToWriteBufferQueue.take() ); - } - } - catch ( IOException ioe ) - { - mLog.error( "IOException while trying to write to the " - + "wave writer", ioe ); - } - catch ( InterruptedException e ) - { - mLog.error( "Oops ... error while processing the buffer " - + "queue for the wave recorder", e ); - } - } - } - } - -} diff --git a/src/record/wave/ComplexBufferWaveRecorder.java b/src/record/wave/ComplexBufferWaveRecorder.java new file mode 100644 index 00000000..4c14eb3c --- /dev/null +++ b/src/record/wave/ComplexBufferWaveRecorder.java @@ -0,0 +1,225 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014,2015 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 java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.sound.sampled.AudioFormat; + +import module.Module; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.Listener; +import sample.complex.ComplexBuffer; +import sample.complex.IComplexBufferListener; +import util.TimeStamp; +import controller.ThreadPoolManager; +import controller.ThreadPoolManager.ThreadType; + +/** + * WAVE audio recorder module for recording complex (I&Q) samples to a wave file + */ +public class ComplexBufferWaveRecorder extends Module + implements IComplexBufferListener, Listener +{ + private final static Logger mLog = + LoggerFactory.getLogger( ComplexBufferWaveRecorder.class ); + + private WaveWriter mWriter; + private String mFilePrefix; + private Path mFile; + private AudioFormat mAudioFormat; + + private ThreadPoolManager mThreadPoolManager; + private BufferProcessor mBufferProcessor; + private ScheduledFuture mProcessorHandle; + private LinkedBlockingQueue mBuffers = new LinkedBlockingQueue<>( 500 ); + + public ComplexBufferWaveRecorder( ThreadPoolManager threadPoolManager, + int sampleRate, String filePrefix ) + { + mThreadPoolManager = threadPoolManager; + + mAudioFormat = new AudioFormat( sampleRate, //SampleRate + 16, //Sample Size + 2, //Channels + true, //Signed + false ); //Little Endian + + mFilePrefix = filePrefix; + } + + /** + * Converts the float samples in a complex buffer to a little endian 16-bit + * buffer + */ + public static ByteBuffer convert( ComplexBuffer buffer ) + { + float[] samples = buffer.getSamples(); + + ByteBuffer converted = ByteBuffer.allocate( samples.length * 2 ); + converted.order( ByteOrder.LITTLE_ENDIAN ); + + for( float sample: samples ) + { + converted.putShort( (short)( sample * Short.MAX_VALUE ) ); + } + + return converted; + } + + public Path getFile() + { + return mFile; + } + + public void start() + { + if( !running() ) + { + if( mBufferProcessor == null ) + { + mBufferProcessor = new BufferProcessor(); + } + + try + { + StringBuilder sb = new StringBuilder(); + sb.append( mFilePrefix ); + sb.append( "_" ); + sb.append( TimeStamp.getTimeStamp( "_" ) ); + sb.append( ".wav" ); + + mFile = Paths.get( sb.toString() ); + + mWriter = new WaveWriter( mAudioFormat, mFile ); + + /* Schedule the processor to run every 500 milliseconds */ + mProcessorHandle = mThreadPoolManager.scheduleFixedRate( + ThreadType.BASEBAND_RECORDING, mBufferProcessor, 500, + TimeUnit.MILLISECONDS ); + } + catch( IOException io ) + { + mLog.error( "Error starting complex baseband recorder", io ); + } + } + } + + public boolean running() + { + return mProcessorHandle != null; + } + + public void stop() + { + if( running() ) + { + mThreadPoolManager.cancel( mProcessorHandle ); + + mProcessorHandle = null; + + if( mWriter != null ) + { + try + { + mWriter.close(); + } + catch( IOException io ) + { + mLog.error( "Error stopping complex wave recorder [" + + getFile() + "]", io ); + } + } + + } + + mBuffers.clear(); + mWriter = null; + mFile = null; + } + + @Override + public void receive( ComplexBuffer buffer ) + { + if( running() ) + { + boolean success = mBuffers.offer( buffer ); + + if( !success ) + { + mLog.error( "ComplexWaveRecorder: buffer overflow or error - throwing " + + "away a buffer of sample data" ); + stop(); + } + } + } + + @Override + public Listener getComplexBufferListener() + { + return this; + } + + @Override + public void dispose() + { + stop(); + + mThreadPoolManager = null; + } + + @Override + public void reset() + { + } + + public class BufferProcessor implements Runnable + { + public void run() + { + try + { + ComplexBuffer buffer = mBuffers.poll(); + + while( buffer != null ) + { + mWriter.write( convert( buffer ) ); + buffer = mBuffers.poll(); + } + } + catch ( IOException ioe ) + { + /* Stop this module if/when we get an IO exception */ + stop(); + + mLog.error( "IOException while trying to write to the wave " + + "writer", ioe ); + } + } + } +} diff --git a/src/record/wave/ComplexWaveRecorder.java b/src/record/wave/ComplexWaveRecorder.java deleted file mode 100644 index efe94c21..00000000 --- a/src/record/wave/ComplexWaveRecorder.java +++ /dev/null @@ -1,199 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.concurrent.LinkedBlockingQueue; - -import javax.sound.sampled.AudioFormat; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import record.Recorder; -import record.RecorderType; -import sample.Listener; -import sample.complex.ComplexBuffer; -import util.waveaudio.WaveWriter; - -/** - * Threaded WAVE audio recorder for recording complex (I&Q) samples to a wave - * file - */ -public class ComplexWaveRecorder extends Recorder implements Listener -{ - private final static Logger mLog = - LoggerFactory.getLogger( ComplexWaveRecorder.class ); - - private AudioFormat mAudioFormat; - private boolean mRunning = false; - private boolean mPaused = false; - private WaveWriter mWriter; - private String mFileName; - private BufferProcessor mBufferProcessor; - - private Listener mListener; - - //Sample buffer queue ... constrained to 512 sample buffers (arbitrary size) - private LinkedBlockingQueue mToWriteBufferQueue = - new LinkedBlockingQueue( 512 ); - - public ComplexWaveRecorder( int sampleRate, String filename ) - { - super( RecorderType.AUDIO ); - - mAudioFormat = new AudioFormat( sampleRate, //SampleRate - 16, //Sample Size - 2, //Channels - true, //Signed - false ); //Little Endian - - mFileName = filename; - } - - /** - * Converts the float samples in a complex buffer to a little endian 16-bit - * buffer - */ - public static ByteBuffer convert( ComplexBuffer buffer ) - { - float[] samples = buffer.getSamples(); - - ByteBuffer converted = ByteBuffer.allocate( samples.length * 2 ); - converted.order( ByteOrder.LITTLE_ENDIAN ); - - for( float sample: samples ) - { - converted.putShort( (short)( sample * Short.MAX_VALUE ) ); - } - - return converted; - } - - @Override - public String getFileName() - { - return mFileName + ".wav"; - } - - public void start() throws IOException - { - if( !mRunning ) - { - mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat ); - mWriter.start(); - - mBufferProcessor = new BufferProcessor(); - mBufferProcessor.start(); - - mRunning = true; - } - } - - public void stop() throws IOException - { - if( mRunning ) - { - mBufferProcessor = null; - - mWriter.stop(); - mWriter = null; - - mRunning = false; - } - } - - public void pause() - { - mPaused = true; - } - - public void resume() - { - mPaused = false; - } - - /** - * Assumes that the byte[] represents samples corresponding to the - * audio format object defined at construction - */ - public void receive( ByteBuffer buffer ) - { - if( mRunning && !mPaused ) - { - boolean success = mToWriteBufferQueue.offer( buffer ); - - if( !success ) - { - mLog.error( "Wave recorder buffer overflow - throwing away " + - buffer.capacity() + " samples" ); - } - } - } - - @Override - public void receive( ComplexBuffer buffer ) - { - //Dispatch the buffer to be written to disk - if( !mToWriteBufferQueue.offer( convert( buffer ) ) ) - { - //We had an overflow ... log it - mLog.error( "ComplexWaveRecorder: buffer overflow - throwing " - + "away a buffer of sample data" ); - } - - if( mListener != null ) - { - mListener.receive( buffer ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public class BufferProcessor extends Thread - { - public void run() - { - while( true ) - { - try - { - if( mWriter != null ) - { - mWriter.write( mToWriteBufferQueue.take() ); - } - } - catch ( IOException ioe ) - { - mLog.error( "IOException while trying to write to the wave " - + "writer", ioe ); - } - catch ( InterruptedException e ) - { - mLog.error( "Oops ... error while processing the buffer " - + "queue for the wave recorder", e ); - } - } - } - } -} diff --git a/src/record/wave/FloatWaveRecorder.java b/src/record/wave/FloatWaveRecorder.java deleted file mode 100644 index 29768399..00000000 --- a/src/record/wave/FloatWaveRecorder.java +++ /dev/null @@ -1,222 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.concurrent.LinkedBlockingQueue; - -import javax.sound.sampled.AudioFormat; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import record.Recorder; -import record.RecorderType; -import sample.Listener; -import sample.real.RealBuffer; -import sample.real.RealSampleListener; -import util.waveaudio.WaveWriter; -import buffer.FloatSampleBufferAssembler; - -/** - * Threaded WAVE audio recorder for recording mono float samples to a wave file - */ -public class FloatWaveRecorder extends Recorder implements RealSampleListener, - Listener -{ - private final static Logger mLog = - LoggerFactory.getLogger( FloatWaveRecorder.class ); - - private AudioFormat mAudioFormat; - private boolean mRunning = false; - private boolean mPaused = false; - private WaveWriter mWriter; - private String mFileName; - private BufferProcessor mBufferProcessor; - - //Sample buffer queue ... constrained to 512 sample buffers (arbitrary size) - private LinkedBlockingQueue mToWriteBufferQueue = - new LinkedBlockingQueue( 512 ); - private FloatSampleBufferAssembler mCurrentBuffer; - - private Listener mListener; - - public FloatWaveRecorder( int sampleRate, String filename ) - { - super( RecorderType.AUDIO ); - - mAudioFormat = new AudioFormat( sampleRate, //SampleRate - 16, //Sample Size - 1, //Channels - true, //Signed - false ); //Little Endian - - mCurrentBuffer = new FloatSampleBufferAssembler( sampleRate ); - - mFileName = filename; - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - @Override - public String getFileName() - { - return mFileName + ".wav"; - } - - public void start() throws IOException - { - if( !mRunning ) - { - mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat ); - mWriter.start(); - - mBufferProcessor = new BufferProcessor(); - mBufferProcessor.start(); - - mRunning = true; - } - } - - public void stop() throws IOException - { - if( mRunning ) - { - mBufferProcessor = null; - - mWriter.stop(); - mWriter = null; - - mRunning = false; - } - } - - public void pause() - { - mPaused = true; - } - - public void resume() - { - mPaused = false; - } - - /** - * Assumes that the byte[] represents samples corresponding to the - * audio format object defined at construction - */ - public void receive( ByteBuffer buffer ) - { - if( mRunning && !mPaused ) - { - boolean success = mToWriteBufferQueue.offer( buffer ); - - if( !success ) - { - mLog.error( "Wave recorder buffer overflow - throwing away " + - buffer.capacity() + " samples" ); - } - } - } - - @Override - public void receive( RealBuffer buffer ) - { - //Dispatch the buffer to be written to disk - if( !mToWriteBufferQueue.offer( convert( buffer ) ) ) - { - //We had an overflow ... log it - mLog.error( "buffer overflow - throwing away a buffer of sample data" ); - } - - if( mListener != null ) - { - mListener.receive( buffer ); - } - } - - /** - * Converts the float samples in a complex buffer to a little endian 16-bit - * buffer - */ - public static ByteBuffer convert( RealBuffer buffer ) - { - float[] samples = buffer.getSamples(); - - ByteBuffer converted = ByteBuffer.allocate( samples.length * 2 ); - converted.order( ByteOrder.LITTLE_ENDIAN ); - - for( float sample: samples ) - { - converted.putShort( (short)( sample * Short.MAX_VALUE ) ); - } - - return converted; - } - - @Override - public void receive( float sample ) - { - mCurrentBuffer.put( sample ); - - if( !mCurrentBuffer.hasRemaining() ) - { - //Dispatch the buffer to be written to disk - if( !mToWriteBufferQueue.offer( mCurrentBuffer.get() ) ) - { - //We had an overflow ... log it - mLog.error( "FloatWaveRecorder: buffer overflow - throwing " - + "away 1 second of sample data" ); - } - - mCurrentBuffer.reset(); - } - } - - public class BufferProcessor extends Thread - { - public void run() - { - while( true ) - { - try - { - if( mWriter != null ) - { - mWriter.write( mToWriteBufferQueue.take() ); - } - } - catch ( IOException ioe ) - { - mLog.error( "IOException while trying to write to the wave " - + "writer", ioe ); - } - catch ( InterruptedException e ) - { - mLog.error( "Oops ... error while processing the buffer " - + "queue for the wave recorder", e ); - } - } - } - } -} diff --git a/src/record/wave/RealBufferWaveRecorder.java b/src/record/wave/RealBufferWaveRecorder.java new file mode 100644 index 00000000..179d2cc1 --- /dev/null +++ b/src/record/wave/RealBufferWaveRecorder.java @@ -0,0 +1,237 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014,2015 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 java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.sound.sampled.AudioFormat; + +import module.Module; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.Listener; +import sample.real.IRealBufferListener; +import sample.real.RealBuffer; +import util.TimeStamp; +import controller.ThreadPoolManager; +import controller.ThreadPoolManager.ThreadType; + +/** + * WAVE audio recorder module for recording real sample buffers to a wave file + */ +public class RealBufferWaveRecorder extends Module + implements IRealBufferListener, Listener +{ + private final static Logger mLog = + LoggerFactory.getLogger( RealBufferWaveRecorder.class ); + + private WaveWriter mWriter; + private String mFilePrefix; + private Path mFile; + private AudioFormat mAudioFormat; + + private ThreadPoolManager mThreadPoolManager; + private BufferProcessor mBufferProcessor; + private ScheduledFuture mProcessorHandle; + private LinkedBlockingQueue mBuffers = + new LinkedBlockingQueue<>( 500 ); + private long mLastBufferReceived; + + public RealBufferWaveRecorder( ThreadPoolManager threadPoolManager, + int sampleRate, String filePrefix ) + { + mThreadPoolManager = threadPoolManager; + + mAudioFormat = new AudioFormat( sampleRate, //SampleRate + 16, //Sample Size + 1, //Channels + true, //Signed + false ); //Little Endian + + mFilePrefix = filePrefix; + } + + /** + * Timestamp of when the latest buffer was received by this recorder + */ + public long getLastBufferReceived() + { + return mLastBufferReceived; + } + + /** + * Converts the float samples in a complex buffer to a little endian 16-bit + * buffer + */ + public static ByteBuffer convert( RealBuffer buffer ) + { + float[] samples = buffer.getSamples(); + + ByteBuffer converted = ByteBuffer.allocate( samples.length * 2 ); + converted.order( ByteOrder.LITTLE_ENDIAN ); + + for( float sample: samples ) + { + converted.putShort( (short)( sample * Short.MAX_VALUE ) ); + } + + return converted; + } + + public Path getFile() + { + return mFile; + } + + public void start() + { + if( !running() ) + { + if( mBufferProcessor == null ) + { + mBufferProcessor = new BufferProcessor(); + } + + try + { + StringBuilder sb = new StringBuilder(); + sb.append( mFilePrefix ); + sb.append( "_" ); + sb.append( TimeStamp.getTimeStamp( "_" ) ); + sb.append( ".wav" ); + + mFile = Paths.get( sb.toString() ); + + mWriter = new WaveWriter( mAudioFormat, mFile ); + + /* Schedule the processor to run every 500 milliseconds */ + mProcessorHandle = mThreadPoolManager.scheduleFixedRate( + ThreadType.AUDIO_RECORDING, mBufferProcessor, 500, + TimeUnit.MILLISECONDS ); + } + catch( IOException io ) + { + mLog.error( "Error starting real buffer recorder", io ); + } + } + } + + public boolean running() + { + return mProcessorHandle != null; + } + + public void stop() + { + if( running() ) + { + mThreadPoolManager.cancel( mProcessorHandle ); + + mProcessorHandle = null; + + if( mWriter != null ) + { + try + { + mWriter.close(); + } + catch( IOException io ) + { + mLog.error( "Error stopping real wave recorder [" + + getFile() + "]", io ); + } + } + + } + + mBuffers.clear(); + mWriter = null; + mFile = null; + } + + @Override + public void receive( RealBuffer buffer ) + { + if( running() ) + { + boolean success = mBuffers.offer( buffer ); + + if( !success ) + { + mLog.error( "recorder buffer overflow or error - throwing " + + "away a buffer of sample data" ); + stop(); + } + + mLastBufferReceived = System.currentTimeMillis(); + } + } + + @Override + public Listener getRealBufferListener() + { + return this; + } + + @Override + public void dispose() + { + stop(); + + mThreadPoolManager = null; + } + + @Override + public void reset() + { + } + + public class BufferProcessor implements Runnable + { + public void run() + { + try + { + RealBuffer buffer = mBuffers.poll(); + + while( buffer != null ) + { + mWriter.write( convert( buffer ) ); + buffer = mBuffers.poll(); + } + } + catch ( IOException ioe ) + { + /* Stop this module if/when we get an IO exception */ + stop(); + + mLog.error( "IOException while trying to write to the wave " + + "writer", ioe ); + } + } + } +} diff --git a/src/record/wave/WaveRecorder.java b/src/record/wave/WaveRecorder.java deleted file mode 100644 index a2a3a6cd..00000000 --- a/src/record/wave/WaveRecorder.java +++ /dev/null @@ -1,141 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.LinkedBlockingQueue; - -import javax.sound.sampled.AudioFormat; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import record.Recorder; -import record.RecorderType; -import util.waveaudio.WaveWriter; - -/** - * Threaded WAVE audio recorder. - */ -public class WaveRecorder extends Recorder -{ - private final static Logger mLog = - LoggerFactory.getLogger( WaveRecorder.class ); - - private AudioFormat mAudioFormat; - private boolean mRunning = false; - private boolean mPaused = false; - private WaveWriter mWriter; - private String mFileName; - private BufferProcessor mBufferProcessor; - - //Sample buffer queue ... constrained to 512 sample buffers (arbitrary size) - LinkedBlockingQueue mBufferQueue = - new LinkedBlockingQueue( 512 ); - - public WaveRecorder( AudioFormat format, String filename ) - { - super( RecorderType.AUDIO ); - - mAudioFormat = format; - mFileName = filename; - } - - public void start() throws IOException - { - if( !mRunning ) - { - mWriter = new WaveWriter( mFileName + ".wav", mAudioFormat ); - mWriter.start(); - - mBufferProcessor = new BufferProcessor(); - mBufferProcessor.start(); - - mRunning = true; - } - } - - public void stop() throws IOException - { - if( mRunning ) - { - mBufferProcessor = null; - - mWriter.stop(); - mWriter = null; - - mRunning = false; - } - } - - public void pause() - { - mPaused = true; - } - - public void resume() - { - mPaused = false; - } - - /** - * Assumes that the byte[] represents samples corresponding to the - * audio format object defined at construction - */ - public void receive( ByteBuffer buffer ) - { - if( mRunning && !mPaused ) - { - boolean success = mBufferQueue.offer( buffer ); - - if( !success ) - { - mLog.error( "Wave recorder buffer overflow - throwing away " + - buffer.capacity() + " samples" ); - } - } - } - - public class BufferProcessor extends Thread - { - public void run() - { - while( true ) - { - try - { - if( mWriter != null ) - { - mWriter.write( mBufferQueue.take() ); - } - } - catch ( IOException ioe ) - { - mLog.error( "IOException while trying to write to the " - + "wave writer", ioe ); - } - catch ( InterruptedException e ) - { - mLog.error( "Oops ... error while processing the buffer" - + " queue for the wave recorder", e ); - } - } - } - } -} diff --git a/src/record/wave/WaveUtils.java b/src/record/wave/WaveUtils.java new file mode 100644 index 00000000..050fa83f --- /dev/null +++ b/src/record/wave/WaveUtils.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 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 java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import javax.sound.sampled.AudioFormat; + + +public class WaveUtils +{ + /** + * Creates a wave file header with the RIFF chunk descriptor and the WAVE + * format chunk 1 and chunk 2 header + */ + public static ByteBuffer getWaveHeader( AudioFormat format ) + { + ByteBuffer descriptor = ByteBuffer.allocate( 44 ); + descriptor.order( ByteOrder.LITTLE_ENDIAN ); + + /* Chunk ID = RIFF */ + descriptor.put( (byte)0x52 ); //R + descriptor.put( (byte)0x49 ); //I + descriptor.put( (byte)0x46 ); //F + descriptor.put( (byte)0x46 ); //F + + /* Chunk Size = 36 bytes (initial empty size) */ + descriptor.put( (byte)36 ); + descriptor.put( (byte)0 ); + descriptor.put( (byte)0 ); + descriptor.put( (byte)0 ); + + /* Format = WAVE */ + descriptor.put( (byte)0x57 ); //W + descriptor.put( (byte)0x41 ); //A + descriptor.put( (byte)0x56 ); //V + descriptor.put( (byte)0x45 ); //E + + /* Sub Chunk 1 ID */ + descriptor.put( (byte)0x66 ); //f + descriptor.put( (byte)0x6D ); //m + descriptor.put( (byte)0x74 ); //t + descriptor.put( (byte)0x20 ); //space + + /* SubChunk1Size = 1 */ + descriptor.put( (byte)0x10 ); + descriptor.put( (byte)0x0 ); + descriptor.put( (byte)0x0 ); + descriptor.put( (byte)0x0 ); + + /* Audio format = 1 (Uncompressed PCM) */ + descriptor.put( (byte)0x1 ); + descriptor.put( (byte)0x0 ); + + /* Number of Channels */ + descriptor.asShortBuffer().put( (short)format.getChannels() ); + descriptor.position( descriptor.position() + 2 ); + + /* Sample Rate - assumes integral sample rate */ + descriptor.asIntBuffer().put( (int)format.getSampleRate() ); + descriptor.position( descriptor.position() + 4 ); + + int frameByteRate = format.getChannels() * format.getSampleSizeInBits() / 8; + + /* Byte Rate = sample rate * channels * bits per sample / 8 */ + int byteRate = (int)( format.getSampleRate() * frameByteRate ); + descriptor.asIntBuffer().put( byteRate ); + descriptor.position( descriptor.position() + 4 ); + + /* Block Align */ + descriptor.asShortBuffer().put( (short)frameByteRate ); + descriptor.position( descriptor.position() + 2 ); + + /* Bits per Sample */ + descriptor.asShortBuffer().put( (short)( format.getSampleSizeInBits() ) ); + descriptor.position( descriptor.position() + 2 ); + + /* Sub Chunk 2 ID */ + descriptor.put( (byte)0x64 ); //d + descriptor.put( (byte)0x61 ); //a + descriptor.put( (byte)0x74 ); //t + descriptor.put( (byte)0x61 ); //a + + /* Sub Chunk 2 Size */ + descriptor.put( (byte)0x0 ); + descriptor.put( (byte)0x0 ); + descriptor.put( (byte)0x0 ); + descriptor.put( (byte)0x0 ); + + return descriptor; + } +} diff --git a/src/record/wave/WaveWriter.java b/src/record/wave/WaveWriter.java new file mode 100644 index 00000000..d1c64a1c --- /dev/null +++ b/src/record/wave/WaveWriter.java @@ -0,0 +1,251 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 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 java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +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.Matcher; +import java.util.regex.Pattern; + +import javax.sound.sampled.AudioFormat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WaveWriter implements AutoCloseable +{ + private final static Logger mLog = LoggerFactory.getLogger( WaveWriter.class ); + + private static final Pattern FILENAME_PATTERN = + Pattern.compile( "(.*_)(\\d+)(\\.wav)" ); + public static final long MAX_WAVE_SIZE = 2l * (long)Integer.MAX_VALUE; + + private AudioFormat mAudioFormat; + private int mFileRolloverCounter = 1; + private long mMaxSize; + private Path mFile; + private FileChannel mFileChannel; + + /** + * Constructs a new wave writer that is open with a complete header, ready + * for writing buffers of PCM sample data. + * + * Each time the maximum file size is reached, a new file is created with a + * series suffix appended to the file name. + * + * @param format - audio format (channels, sample size, sample rate) + * @param file - wave file to write + * @param maxSize - maximum file size ( range: 1 - 4,294,967,294 bytes ) + * @throws IOException - if there are any IO issues + */ + public WaveWriter( AudioFormat format, Path file, long maxSize ) + throws IOException + { + assert( format != null ); + assert( file != null ); + + mAudioFormat = format; + mFile = file; + + if( 0 < maxSize && maxSize <= MAX_WAVE_SIZE ) + { + mMaxSize = maxSize; + } + else + { + mMaxSize = MAX_WAVE_SIZE; + } + + open(); + } + + /** + * Constructs a new wave writer that is open with a complete header, ready + * for writing buffers of PCM sample data. The maximum file size is limited + * to the max size specified in the wave file format: max unsigned integer + * + * @param format - audio format (channels, sample size, sample rate) + * @param file - wave file to write + * @throws IOException - if there are any IO issues + */ + public WaveWriter( AudioFormat format, Path file ) throws IOException + { + this( format, file, Integer.MAX_VALUE * 2 ); + } + + /** + * Opens the file and writes a wave header. + */ + private void open() throws IOException + { + Files.createFile( mFile ); + + mFileChannel = (FileChannel.open( mFile, StandardOpenOption.WRITE ) ); + + ByteBuffer header = WaveUtils.getWaveHeader( mAudioFormat ); + + header.flip(); + + while( header.hasRemaining() ) + { + mFileChannel.write( header ); + } + } + + /** + * Closes the file + */ + public void close() throws IOException + { + mFileChannel.close(); + } + + /** + * Writes the buffer contents to the file. Assumes that the buffer is full + * and the first byte of data is at position 0. + */ + public void write( ByteBuffer buffer ) throws IOException + { + buffer.position( 0 ); + + /* Write the full buffer if there is room, respecting the max file size */ + if( mFileChannel.size() + buffer.capacity() < mMaxSize ) + { + while( buffer.hasRemaining() ) + { + mFileChannel.write( buffer ); + } + + updateWaveFileSize(); + } + else + { + /* Split the buffer to finish filling the current file and then put + * the leftover into a new file */ + int remaining = (int)( mMaxSize - mFileChannel.size() ); + + /* Ensure we write full frames to fill up the remaining size */ + remaining -= (int)( remaining % mAudioFormat.getFrameSize() ); + + byte[] bytes = buffer.array(); + + ByteBuffer current = ByteBuffer.wrap( Arrays.copyOf( bytes, remaining ) ); + + ByteBuffer next = ByteBuffer.wrap( Arrays.copyOfRange( bytes, + remaining, bytes.length ) ); + + while( current.hasRemaining() ) + { + mFileChannel.write( current ); + } + + updateWaveFileSize(); + + rollover(); + + while( next.hasRemaining() ) + { + mFileChannel.write( next ); + } + + updateWaveFileSize(); + } + } + + /** + * Closes out the current file, appends an incremented sequence number to + * the file name and opens up a new file. + */ + private void rollover() throws IOException + { + close(); + + mFileRolloverCounter++; + + updateFileName(); + + open(); + } + + /** + * Updates the overall and the chunk2 sizes + */ + private void updateWaveFileSize() throws IOException + { + /* Update overall wave size (total size - 8 bytes) */ + mFileChannel.write( getUnsignedIntegerBuffer( mFileChannel.size() - 8 ), 4 ); + + /* Update chunk 2 size (total size - 44 bytes) */ + mFileChannel.write( getUnsignedIntegerBuffer( mFileChannel.size() - 44 ), 40 ); + } + + /** + * Creates a little-endian 4-byte buffer containing an unsigned 32-bit + * integer value derived from the 4 least significant bytes of the argument. + * + * The buffer's position is set to 0 to prepare it for writing to a channel. + */ + private static ByteBuffer getUnsignedIntegerBuffer( long size ) + { + ByteBuffer buffer = ByteBuffer.allocate( 4 ); + + buffer.put( (byte)( size & 0xFFl ) ); + buffer.put( (byte)( ( size & 0xFF00l ) >> 8 ) ); + buffer.put( (byte)( ( size & 0xFF0000l ) >> 16 ) ); + buffer.put( (byte)( ( size & 0xFF000000l ) >> 32 ) ); + + buffer.position( 0 ); + + return buffer; + } + + /** + * Updates the current file name with the rollover counter series suffix + */ + private void updateFileName() + { + String filename = mFile.toString(); + + if( mFileRolloverCounter == 2 ) + { + filename = filename.replace( ".wav", "_2.wav" ); + } + else + { + Matcher m = FILENAME_PATTERN.matcher( filename ); + + if( m.find() ) + { + StringBuilder sb = new StringBuilder(); + sb.append( m.group( 1 ) ); + sb.append( mFileRolloverCounter ); + sb.append( m.group( 3 ) ); + + filename = sb.toString(); + } + } + + mFile = Paths.get( filename ); + } +} diff --git a/src/record/wave/WriteExample.java b/src/record/wave/WriteExample.java deleted file mode 100644 index 159a8fe7..00000000 --- a/src/record/wave/WriteExample.java +++ /dev/null @@ -1,72 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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 java.io.File; - -import util.waveaudio.WavFile; - - -public class WriteExample -{ - public static void main(String[] args) - { - try - { - int sampleRate = 44100; // Samples per second - double duration = 5.0; // Seconds - - // Calculate the number of frames required for specified duration - long numFrames = (long)(duration * sampleRate); - - // Create a wav file with the name specified as the first argument - WavFile wavFile = WavFile.newWavFile(new File(args[0]), 2, numFrames, 16, sampleRate); - - // Create a buffer of 100 frames - double[][] buffer = new double[2][100]; - - // Initialise a local frame counter - long frameCounter = 0; - - // Loop until all frames written - while (frameCounter < numFrames) - { - // Determine how many frames to write, up to a maximum of the buffer size - long remaining = wavFile.getFramesRemaining(); - int toWrite = (remaining > 100) ? 100 : (int) remaining; - - // Fill the buffer, one tone per channel - for (int s=0 ; s - ******************************************************************************/ -package util.waveaudio; - - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintStream; - - - -public class WavFile -{ - private enum IOState {READING, WRITING, CLOSED}; - private final static int BUFFER_SIZE = 4096; - - private final static int FMT_CHUNK_ID = 0x20746D66; - private final static int DATA_CHUNK_ID = 0x61746164; - private final static int RIFF_CHUNK_ID = 0x46464952; - private final static int RIFF_TYPE_ID = 0x45564157; - - private File file; // File that will be read from or written to - private IOState ioState; // Specifies the IO State of the Wav File (used for snaity checking) - private int bytesPerSample; // Number of bytes required to store a single sample - private long numFrames; // Number of frames within the data section - private FileOutputStream oStream; // Output stream used for writting data - private FileInputStream iStream; // Input stream used for reading data - private double floatScale; // Scaling factor used for int <-> float conversion - private double floatOffset; // Offset factor used for int <-> float conversion - private boolean wordAlignAdjust; // Specify if an extra byte at the end of the data chunk is required for word alignment - - // Wav Header - private int numChannels; // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535) - private long sampleRate; // 4 bytes unsigned, 0x00000001 (1) to 0xFFFFFFFF (4,294,967,295) - // Although a java int is 4 bytes, it is signed, so need to use a long - private int blockAlign; // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535) - private int validBits; // 2 bytes unsigned, 0x0002 (2) to 0xFFFF (65,535) - - // Buffering - private byte[] buffer; // Local buffer used for IO - private int bufferPointer; // Points to the current position in local buffer - private int bytesRead; // Bytes read after last read into local buffer - private long frameCounter; // Current number of frames read or written - - // Cannot instantiate WavFile directly, must either use newWavFile() or openWavFile() - private WavFile() - { - buffer = new byte[BUFFER_SIZE]; - } - - public int getNumChannels() - { - return numChannels; - } - - public long getNumFrames() - { - return numFrames; - } - - public long getFramesRemaining() - { - return numFrames - frameCounter; - } - - public long getSampleRate() - { - return sampleRate; - } - - public int getValidBits() - { - return validBits; - } - - public static WavFile newWavFile(File file, int numChannels, long numFrames, int validBits, long sampleRate) throws IOException, WavFileException - { - // Instantiate new Wavfile and initialise - WavFile wavFile = new WavFile(); - wavFile.file = file; - wavFile.numChannels = numChannels; - wavFile.numFrames = numFrames; - wavFile.sampleRate = sampleRate; - wavFile.bytesPerSample = (validBits + 7) / 8; - wavFile.blockAlign = wavFile.bytesPerSample * numChannels; - wavFile.validBits = validBits; - - // Sanity check arguments - if (numChannels < 1 || numChannels > 65535) throw new WavFileException("Illegal number of channels, valid range 1 to 65536"); - if (numFrames < 0) throw new WavFileException("Number of frames must be positive"); - if (validBits < 2 || validBits > 65535) throw new WavFileException("Illegal number of valid bits, valid range 2 to 65536"); - if (sampleRate < 0) throw new WavFileException("Sample rate must be positive"); - - // Create output stream for writing data - wavFile.oStream = new FileOutputStream(file); - - // Calculate the chunk sizes - long dataChunkSize = wavFile.blockAlign * numFrames; - long mainChunkSize = 4 + // Riff Type - 8 + // Format ID and size - 16 + // Format data - 8 + // Data ID and size - dataChunkSize; - - // Chunks must be word aligned, so if odd number of audio data bytes - // adjust the main chunk size - if (dataChunkSize % 2 == 1) { - mainChunkSize += 1; - wavFile.wordAlignAdjust = true; - } - else { - wavFile.wordAlignAdjust = false; - } - - // Set the main chunk size - putLE(RIFF_CHUNK_ID, wavFile.buffer, 0, 4); - putLE(mainChunkSize, wavFile.buffer, 4, 4); - putLE(RIFF_TYPE_ID, wavFile.buffer, 8, 4); - - // Write out the header - wavFile.oStream.write(wavFile.buffer, 0, 12); - - // Put format data in buffer - long averageBytesPerSecond = sampleRate * wavFile.blockAlign; - - putLE(FMT_CHUNK_ID, wavFile.buffer, 0, 4); // Chunk ID - putLE(16, wavFile.buffer, 4, 4); // Chunk Data Size - putLE(1, wavFile.buffer, 8, 2); // Compression Code (Uncompressed) - putLE(numChannels, wavFile.buffer, 10, 2); // Number of channels - putLE(sampleRate, wavFile.buffer, 12, 4); // Sample Rate - putLE(averageBytesPerSecond, wavFile.buffer, 16, 4); // Average Bytes Per Second - putLE(wavFile.blockAlign, wavFile.buffer, 20, 2); // Block Align - putLE(validBits, wavFile.buffer, 22, 2); // Valid Bits - - // Write Format Chunk - wavFile.oStream.write(wavFile.buffer, 0, 24); - - // Start Data Chunk - putLE(DATA_CHUNK_ID, wavFile.buffer, 0, 4); // Chunk ID - putLE(dataChunkSize, wavFile.buffer, 4, 4); // Chunk Data Size - - // Write Format Chunk - wavFile.oStream.write(wavFile.buffer, 0, 8); - - // Calculate the scaling factor for converting to a normalised double - if (wavFile.validBits > 8) - { - // If more than 8 validBits, data is signed - // Conversion required multiplying by magnitude of max positive value - wavFile.floatOffset = 0; - wavFile.floatScale = Long.MAX_VALUE >> (64 - wavFile.validBits); - } - else - { - // Else if 8 or less validBits, data is unsigned - // Conversion required dividing by max positive value - wavFile.floatOffset = 1; - wavFile.floatScale = 0.5 * ((1 << wavFile.validBits) - 1); - } - - // Finally, set the IO State - wavFile.bufferPointer = 0; - wavFile.bytesRead = 0; - wavFile.frameCounter = 0; - wavFile.ioState = IOState.WRITING; - - return wavFile; - } - - public static WavFile openWavFile(File file) throws IOException, WavFileException - { - // Instantiate new Wavfile and store the file reference - WavFile wavFile = new WavFile(); - wavFile.file = file; - - // Create a new file input stream for reading file data - wavFile.iStream = new FileInputStream(file); - - // Read the first 12 bytes of the file - int bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 12); - if (bytesRead != 12) throw new WavFileException("Not enough wav file bytes for header"); - - // Extract parts from the header - long riffChunkID = getLE(wavFile.buffer, 0, 4); - long chunkSize = getLE(wavFile.buffer, 4, 4); - long riffTypeID = getLE(wavFile.buffer, 8, 4); - - // Check the header bytes contains the correct signature - if (riffChunkID != RIFF_CHUNK_ID) throw new WavFileException("Invalid Wav Header data, incorrect riff chunk ID"); - if (riffTypeID != RIFF_TYPE_ID) throw new WavFileException("Invalid Wav Header data, incorrect riff type ID"); - - // Check that the file size matches the number of bytes listed in header - if (file.length() != chunkSize+8) { - throw new WavFileException("Header chunk size (" + chunkSize + ") does not match file size (" + file.length() + ")"); - } - - boolean foundFormat = false; - boolean foundData = false; - - // Search for the Format and Data Chunks - while (true) - { - // Read the first 8 bytes of the chunk (ID and chunk size) - bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 8); - if (bytesRead == -1) throw new WavFileException("Reached end of file without finding format chunk"); - if (bytesRead != 8) throw new WavFileException("Could not read chunk header"); - - // Extract the chunk ID and Size - long chunkID = getLE(wavFile.buffer, 0, 4); - chunkSize = getLE(wavFile.buffer, 4, 4); - - // Word align the chunk size - // chunkSize specifies the number of bytes holding data. However, - // the data should be word aligned (2 bytes) so we need to calculate - // the actual number of bytes in the chunk - long numChunkBytes = (chunkSize%2 == 1) ? chunkSize+1 : chunkSize; - - if (chunkID == FMT_CHUNK_ID) - { - // Flag that the format chunk has been found - foundFormat = true; - - // Read in the header info - bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 16); - - // Check this is uncompressed data - int compressionCode = (int) getLE(wavFile.buffer, 0, 2); - if (compressionCode != 1) throw new WavFileException("Compression Code " + compressionCode + " not supported"); - - // Extract the format information - wavFile.numChannels = (int) getLE(wavFile.buffer, 2, 2); - wavFile.sampleRate = getLE(wavFile.buffer, 4, 4); - wavFile.blockAlign = (int) getLE(wavFile.buffer, 12, 2); - wavFile.validBits = (int) getLE(wavFile.buffer, 14, 2); - - if (wavFile.numChannels == 0) throw new WavFileException("Number of channels specified in header is equal to zero"); - if (wavFile.blockAlign == 0) throw new WavFileException("Block Align specified in header is equal to zero"); - if (wavFile.validBits < 2) throw new WavFileException("Valid Bits specified in header is less than 2"); - if (wavFile.validBits > 64) throw new WavFileException("Valid Bits specified in header is greater than 64, this is greater than a long can hold"); - - // Calculate the number of bytes required to hold 1 sample - wavFile.bytesPerSample = (wavFile.validBits + 7) / 8; - if (wavFile.bytesPerSample * wavFile.numChannels != wavFile.blockAlign) - throw new WavFileException("Block Align does not agree with bytes required for validBits and number of channels"); - - // Account for number of format bytes and then skip over - // any extra format bytes - numChunkBytes -= 16; - if (numChunkBytes > 0) wavFile.iStream.skip(numChunkBytes); - } - else if (chunkID == DATA_CHUNK_ID) - { - // Check if we've found the format chunk, - // If not, throw an exception as we need the format information - // before we can read the data chunk - if (foundFormat == false) throw new WavFileException("Data chunk found before Format chunk"); - - // Check that the chunkSize (wav data length) is a multiple of the - // block align (bytes per frame) - if (chunkSize % wavFile.blockAlign != 0) throw new WavFileException("Data Chunk size is not multiple of Block Align"); - - // Calculate the number of frames - wavFile.numFrames = chunkSize / wavFile.blockAlign; - - // Flag that we've found the wave data chunk - foundData = true; - - break; - } - else - { - // If an unknown chunk ID is found, just skip over the chunk data - wavFile.iStream.skip(numChunkBytes); - } - } - - // Throw an exception if no data chunk has been found - if (foundData == false) throw new WavFileException("Did not find a data chunk"); - - // Calculate the scaling factor for converting to a normalised double - if (wavFile.validBits > 8) - { - // If more than 8 validBits, data is signed - // Conversion required dividing by magnitude of max negative value - wavFile.floatOffset = 0; - wavFile.floatScale = 1 << (wavFile.validBits - 1); - } - else - { - // Else if 8 or less validBits, data is unsigned - // Conversion required dividing by max positive value - wavFile.floatOffset = -1; - wavFile.floatScale = 0.5 * ((1 << wavFile.validBits) - 1); - } - - wavFile.bufferPointer = 0; - wavFile.bytesRead = 0; - wavFile.frameCounter = 0; - wavFile.ioState = IOState.READING; - - return wavFile; - } - - // Get and Put little endian data from local buffer - // ------------------------------------------------ - private static long getLE(byte[] buffer, int pos, int numBytes) - { - numBytes --; - pos += numBytes; - - long val = buffer[pos] & 0xFF; - for (int b=0 ; b>= 8; - pos ++; - } - } - - // Sample Writing and Reading - // -------------------------- - private void writeSample(long val) throws IOException - { - for (int b=0 ; b>= 8; - bufferPointer ++; - } - } - - private long readSample() throws IOException, WavFileException - { - long val = 0; - - for (int b=0 ; b 0) oStream.write(buffer, 0, bufferPointer); - - // If an extra byte is required for word alignment, add it to the end - if (wordAlignAdjust) oStream.write(0); - - // Close the stream and set to null - oStream.close(); - oStream = null; - } - - // Flag that the stream is closed - ioState = IOState.CLOSED; - } - - public void display() - { - display(System.out); - } - - public void display(PrintStream out) - { - out.printf("File: %s\n", file); - out.printf("Channels: %d, Frames: %d\n", numChannels, numFrames); - out.printf("IO State: %s\n", ioState); - out.printf("Sample Rate: %d, Block Align: %d\n", sampleRate, blockAlign); - out.printf("Valid Bits: %d, Bytes per sample: %d\n", validBits, bytesPerSample); - } - - public static void main(String[] args) - { - if (args.length < 1) - { - System.err.println("Must supply filename"); - System.exit(1); - } - - try - { - for (String filename : args) - { - WavFile readWavFile = openWavFile(new File(filename)); - readWavFile.display(); - - long numFrames = readWavFile.getNumFrames(); - int numChannels = readWavFile.getNumChannels(); - int validBits = readWavFile.getValidBits(); - long sampleRate = readWavFile.getSampleRate(); - - WavFile writeWavFile = newWavFile(new File("out.wav"), numChannels, numFrames, validBits, sampleRate); - - final int BUF_SIZE = 5001; - -// int[] buffer = new int[BUF_SIZE * numChannels]; -// long[] buffer = new long[BUF_SIZE * numChannels]; - double[] buffer = new double[BUF_SIZE * numChannels]; - - int framesRead = 0; - int framesWritten = 0; - - do - { - framesRead = readWavFile.readFrames(buffer, BUF_SIZE); - framesWritten = writeWavFile.writeFrames(buffer, BUF_SIZE); - System.out.printf("%d %d\n", framesRead, framesWritten); - } - while (framesRead != 0); - - readWavFile.close(); - writeWavFile.close(); - } - - WavFile writeWavFile = newWavFile(new File("out2.wav"), 1, 10, 23, 44100); - double[] buffer = new double[10]; - writeWavFile.writeFrames(buffer, 10); - writeWavFile.close(); - } - catch (Exception e) - { - System.err.println(e); - e.printStackTrace(); - } - } -} diff --git a/src/util/waveaudio/WaveUtils.java b/src/util/waveaudio/WaveUtils.java deleted file mode 100644 index f2659cd4..00000000 --- a/src/util/waveaudio/WaveUtils.java +++ /dev/null @@ -1,149 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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.waveaudio; - - -public class WaveUtils -{ - /** - * Returns a byte array representing a wave file RIFF chunk descriptor, - * for mono channel, signed 16-bit audio sample recording. - * - * @param sampleRate - audio sample rate - * - * @param duration - recording duration in seconds. Note: this is a - * temporary value, since you won't know the exact duration until after the - * recording is complete. Your code should overwrite this initial value - * with the correct value, upon completion of the recording - */ - public static byte[] getRIFFChunkDescriptor( float sampleRate, int duration ) - { - //Byte array large enough to hold the descriptor - byte[] buffer = new byte[ 12 ]; - - //Load "RIFF" into the descriptor (4-bytes) - loadDouble( buffer, 0x46464952, 0, 4 ); - - //Initial total size value ... will be overwritten later (4-bytes) - //Note: assumes that sample rate is an integer value - int totalSize = 36 + ( (int)sampleRate * duration ); - - loadDouble( buffer, totalSize, 4, 4); - - //Load "WAVE" into the descriptor (4-bytes) - loadDouble( buffer, 0x45564157, 8, 4); - - return buffer; - } - - /** - * WAVE format descriptor - * @param sampleRate - */ - public static byte[] getWAVEFormatDescriptor( float sampleRate, - int numberOfChannels, - int bitsPerSample ) - { - int bytesPerSample = bitsPerSample / 8; - - //Byte array large enough to hold the descriptor - byte[] buffer = new byte[ 24 ]; - - //SubChunk1ID = "fmt " - loadDouble( buffer, 0x20746D66, 0, 4); - - //SubChunk1Size = 16 - loadDouble( buffer, 16, 4, 4); - - //AudioFormat = PCM ( 1 = not compressed ) - loadDouble( buffer, 1, 8, 2 ); - - //NumberOfChannels - loadDouble( buffer, numberOfChannels, 10, 2); - - //SampleRate - //Note: assumes sample rate is an integer value - loadDouble( buffer, (int)sampleRate, 12, 4 ); - - //ByteRate ( sampleRate * numberOfChannels * bytesPerSample ) - loadDouble( buffer, - ( (int)sampleRate * numberOfChannels * bytesPerSample ), - 16, - 4 ); - - //BlockAlign ( numberOfChannels * bytesPerSample ) - loadDouble( buffer, ( numberOfChannels * bytesPerSample ), 20, 2 ); - - //BitsPerSample - loadDouble( buffer, bitsPerSample, 22, 2 ); - - return buffer; - } - - /** - * Returns formatted data chunk header, based on the following values: - * - * @param numberSamples - number of samples that will be in the chunk - * @param numberChannels - number of channels - * @param bitsPerSample - bits per sample - * @return - */ - public static byte[] getDataChunkHeader( int numberBytes, - int numberChannels ) - { - //Byte array large enough to hold the descriptor - byte[] buffer = new byte[ 24 ]; - - //SubChunk2ID = "data" - loadDouble( buffer, 0x61746164, 0, 4 ); - - //SubChunk2Size - loadDouble( buffer, ( numberBytes * numberChannels ), 4, 4 ); - - return buffer; - } - - /** - * Loads valueToLoad into the byte buffer at start location - * @param buffer - byte array - * @param valueToLoad - * @param start - * @param numberOfBytes - */ - protected static void loadDouble ( byte[] buffer, - long valueToLoad, - int start, - int numberOfBytes ) - { - if( !( ( start + numberOfBytes ) <= buffer.length ) ) - { - throw new IllegalArgumentException( "Byte array buffer is not " - + "large enough to load value:" + valueToLoad ); - } - - for ( int x = 0; x < numberOfBytes; x++ ) - { - buffer[ start ] = (byte) ( valueToLoad & 0xFF); - - valueToLoad >>= 8; - - start++; - } - } - -} diff --git a/src/util/waveaudio/WaveWriter.java b/src/util/waveaudio/WaveWriter.java deleted file mode 100644 index 2b09bdc4..00000000 --- a/src/util/waveaudio/WaveWriter.java +++ /dev/null @@ -1,172 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 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.waveaudio; - -import java.io.BufferedOutputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; - -import javax.sound.sampled.AudioFormat; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class WaveWriter -{ - private final static Logger mLog = LoggerFactory.getLogger( WaveWriter.class ); - - private static final int sTOTAL_BYTE_LIMIT = Integer.MAX_VALUE; - private BufferedOutputStream mOutputStream; - private boolean mRunning = false; - private AudioFormat mAudioFormat; - private String mFileName; - private int mTotalByteCount = 0; - private int mFileRolloverCounter = 0; - - public WaveWriter( String fileName, AudioFormat format ) - { - mAudioFormat = format; - mFileName = fileName; - } - - public void start() throws IOException - { - if( !mRunning ) - { - openFile(); - - mRunning = true; - } - } - - public void stop() throws IOException - { - if( mRunning ) - { - closeFile(); - - //Increment file rollover count, in case we re-start the recorder - mFileRolloverCounter++; - - mRunning = false; - } - } - - public void write( ByteBuffer buffer ) throws IOException - { - checkFileRollover( buffer.capacity() ); - - if( mRunning && mOutputStream != null ) - { - //Write the data - mOutputStream.write( buffer.array() ); - mTotalByteCount += buffer.capacity(); - } - } - - protected void checkFileRollover( int byteCount ) throws IOException - { - if( mTotalByteCount + byteCount >= sTOTAL_BYTE_LIMIT ) - { - closeFile(); - - mFileRolloverCounter++; - - openFile(); - } - } - - private void closeFile() throws IOException - { - mOutputStream.flush(); - mOutputStream.close(); - - String filename = mFileName; - - //If this is a rollover file, append the rollover count to the filename - if( mFileRolloverCounter > 0 ) - { - filename += mFileRolloverCounter; - } - - //Correct the total count. Subtract 8 bytes from the total, to account - //for the ChunkID and ChunkSize bytes, at the beginning of the file - correctWAVEFileByteCount( filename, ( mTotalByteCount ) ); - } - - private void correctWAVEFileByteCount( String fileName, int newByteCount ) throws IOException - { - RandomAccessFile raf = new RandomAccessFile( fileName, "rw" ); - - //Get byte array for newByteCount minus header (8 bytes) - byte[] newTotalSize = new byte[ 4 ]; - WaveUtils.loadDouble( newTotalSize, ( newByteCount - 8 ), 0, 4 ); - - //Seek to byte position 4 and write the 4 byte length value - raf.seek( 4 ); - raf.write( newTotalSize ); - - //Get byte array for newByteCount minus all headers (44 bytes) - byte[] newDataSize = new byte[ 4 ]; - WaveUtils.loadDouble( newDataSize, ( newByteCount - 44 ), 0, 4 ); - - //Seek to byte position 4 and write the 4 byte length value - raf.seek( 40 ); - raf.write( newDataSize ); - - mLog.info( "Closing recording [" + mFileName + "] amending file byte size to:" + newByteCount ); - - raf.close(); - } - - private void openFile() throws IOException - { - mTotalByteCount = 0; - - String filename = mFileName; - - if( mFileRolloverCounter > 0 ) - { - filename += mFileRolloverCounter; - } - - mOutputStream = new BufferedOutputStream( new FileOutputStream( filename ) ); - - //Write the RIFF descriptor - byte[] riff = WaveUtils.getRIFFChunkDescriptor( - mAudioFormat.getSampleRate(), 10 ); - mOutputStream.write( riff ); - mTotalByteCount += riff.length; - - //Write the WAVE descriptor - byte[] wave = WaveUtils.getWAVEFormatDescriptor( - mAudioFormat.getSampleRate(), - mAudioFormat.getChannels(), - mAudioFormat.getSampleSizeInBits() ); - mOutputStream.write( wave ); - mTotalByteCount += wave.length; - - //Write the data chunk header - for 10 seconds of audio - byte[] header = WaveUtils.getDataChunkHeader( - (int)(10 * mAudioFormat.getSampleRate() ), mAudioFormat.getChannels() ); - mOutputStream.write( header ); - mTotalByteCount += header.length; - } -}