mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
Interim checkin for call recording.
This commit is contained in:
BIN
images/no_record.png
Normal file
BIN
images/no_record.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ public enum AliasIDType
|
||||
MDC1200,
|
||||
MIN,
|
||||
MPT1327,
|
||||
NonRecordable,
|
||||
Priority,
|
||||
Site,
|
||||
Status,
|
||||
Talkgroup;
|
||||
|
||||
@@ -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<AliasList>
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FROM_TALKGROUP:
|
||||
case FROM:
|
||||
case TO:
|
||||
return getTalkgroupAlias( value );
|
||||
case UNIQUE_ID:
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.esn;
|
||||
package alias.id.esn;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.esn;
|
||||
package alias.id.esn;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.esn;
|
||||
package alias.id.esn;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.fleetsync;
|
||||
package alias.id.fleetsync;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.fleetsync;
|
||||
package alias.id.fleetsync;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.fleetsync;
|
||||
package alias.id.fleetsync;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.fleetsync;
|
||||
package alias.id.fleetsync;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.fleetsync;
|
||||
package alias.id.fleetsync;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.fleetsync;
|
||||
package alias.id.fleetsync;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.mdc;
|
||||
package alias.id.mdc;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.mdc;
|
||||
package alias.id.mdc;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.mdc;
|
||||
package alias.id.mdc;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.mobileID;
|
||||
package alias.id.mobileID;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.mobileID;
|
||||
package alias.id.mobileID;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.mobileID;
|
||||
package alias.id.mobileID;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.mpt1327;
|
||||
package alias.id.mpt1327;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.mpt1327;
|
||||
package alias.id.mpt1327;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.mpt1327;
|
||||
package alias.id.mpt1327;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.siteID;
|
||||
package alias.id.siteID;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.siteID;
|
||||
package alias.id.siteID;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.siteID;
|
||||
package alias.id.siteID;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.talkgroup;
|
||||
package alias.id.talkgroup;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.talkgroup;
|
||||
package alias.id.talkgroup;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.talkgroup;
|
||||
package alias.id.talkgroup;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.uniqueID;
|
||||
package alias.id.uniqueID;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.uniqueID;
|
||||
package alias.id.uniqueID;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package alias.uniqueID;
|
||||
package alias.id.uniqueID;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@@ -15,29 +15,53 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
123
src/alias/priority/PriorityEditor.java
Normal file
123
src/alias/priority/PriorityEditor.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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();
|
||||
}
|
||||
}
|
||||
86
src/alias/priority/PriorityNode.java
Normal file
86
src/alias/priority/PriorityNode.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
19
src/alias/record/NonRecordable.java
Normal file
19
src/alias/record/NonRecordable.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
75
src/alias/record/NonRecordableEditor.java
Normal file
75
src/alias/record/NonRecordableEditor.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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();
|
||||
}
|
||||
}
|
||||
88
src/alias/record/NonRecordableNode.java
Normal file
88
src/alias/record/NonRecordableNode.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<AudioEvent>, 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() );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
private Map<String,AudioOutput> mChannelMap = new HashMap<>();
|
||||
private List<String> mChannelNames = new ArrayList<>();
|
||||
private AudioOutput mAudioOutput;
|
||||
private boolean mMuted = false;
|
||||
|
||||
private Listener<AudioEvent> mConfigurationChangeListener;
|
||||
|
||||
@@ -90,10 +89,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if( !mMuted )
|
||||
{
|
||||
|
||||
}
|
||||
removeCompletedChannelAssignments();
|
||||
|
||||
if( mAudioPacketQueue != null )
|
||||
@@ -104,55 +99,62 @@ public class AudioManager implements Listener<AudioPacket>, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class AudioPacketRateCounter implements Listener<AudioPacket>
|
||||
mStartTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
mCount += packet.getAudioData().length;
|
||||
mCount += packet.getAudioBuffer().getSamples().length;
|
||||
|
||||
long elapsed = System.currentTimeMillis() - mStartTime;
|
||||
|
||||
|
||||
8
src/audio/IAudioPacketListener.java
Normal file
8
src/audio/IAudioPacketListener.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package audio;
|
||||
|
||||
import sample.Listener;
|
||||
|
||||
public interface IAudioPacketListener
|
||||
{
|
||||
public Listener<AudioPacket> getAudioPacketListener();
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
public class AudioMetadata implements Listener<Metadata>
|
||||
{
|
||||
protected static final Logger mLog = LoggerFactory.getLogger( AudioMetadata.class );
|
||||
|
||||
private int mSource;
|
||||
private int mPriority = 99;
|
||||
private boolean mSelected = false;
|
||||
private List<Metadata> 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<Metadata>
|
||||
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<Metadata>
|
||||
{
|
||||
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<Metadata>
|
||||
|
||||
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<Metadata>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Metadata>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package audio.metadata;
|
||||
|
||||
import sample.Listener;
|
||||
import alias.Metadata;
|
||||
|
||||
public interface IMetadataListener
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package audio.metadata;
|
||||
|
||||
import sample.Listener;
|
||||
import alias.Metadata;
|
||||
|
||||
public interface IMetadataProvider
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
package alias;
|
||||
package audio.metadata;
|
||||
|
||||
import alias.Alias;
|
||||
|
||||
public class Metadata
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
package alias;
|
||||
package audio.metadata;
|
||||
|
||||
public class MetadataReset extends Metadata
|
||||
{
|
||||
@@ -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" ),
|
||||
|
||||
@@ -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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -117,6 +117,8 @@ public class ThreadPoolManager
|
||||
public enum ThreadType
|
||||
{
|
||||
AUDIO_PROCESSING,
|
||||
AUDIO_RECORDING,
|
||||
BASEBAND_RECORDING,
|
||||
SOURCE_SAMPLE_PROCESSING,
|
||||
DECIMATION,
|
||||
DECODER;
|
||||
|
||||
@@ -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<RecorderType> 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<Module> loggers = mResourceManager.getEventLogManager()
|
||||
.getLoggers( mEventLogConfiguration, sb.toString() );
|
||||
String channelName = getChannelName();
|
||||
|
||||
mLog.debug( "Created [" + loggers.size() + "] loggers for [" + sb.toString() + "]" );
|
||||
List<Module> 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<Module> 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 ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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<Module> getModules( ChannelType channelType,
|
||||
ResourceManager resourceManager,
|
||||
DecodeConfiguration config,
|
||||
DecodeConfiguration decodeConfig,
|
||||
RecordConfiguration recordConfig,
|
||||
AuxDecodeConfiguration auxConfig,
|
||||
AliasList aliasList )
|
||||
{
|
||||
List<Module> modules = getPrimaryModules( channelType, config,
|
||||
aliasList, resourceManager );
|
||||
AliasList aliasList,
|
||||
System system,
|
||||
Site site )
|
||||
{
|
||||
List<Module> 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<Module> getPrimaryModules( ChannelType channelType,
|
||||
DecodeConfiguration config,
|
||||
AliasList aliasList,
|
||||
DecodeConfiguration decodeConfig,
|
||||
RecordConfiguration recordConfig,
|
||||
AliasList aliasList,
|
||||
System system,
|
||||
Site site,
|
||||
ResourceManager resourceManager )
|
||||
{
|
||||
List<Module> modules = new ArrayList<Module>();
|
||||
|
||||
/* 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;
|
||||
|
||||
@@ -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<CallEvent>
|
||||
{
|
||||
@@ -133,7 +133,7 @@ public abstract class CallEvent implements Comparable<CallEvent>
|
||||
|
||||
public MetadataType getFromIDType()
|
||||
{
|
||||
return MetadataType.FROM_TALKGROUP;
|
||||
return MetadataType.FROM;
|
||||
}
|
||||
|
||||
public void setFromID( String id )
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<RealBuffer>,
|
||||
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 )
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<Tap> 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<Tap>();
|
||||
|
||||
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 );
|
||||
|
||||
@@ -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<Message>,
|
||||
IAudioPacketProvider, IMessageListener, IChannelEventListener, IMetadataListener
|
||||
@@ -41,10 +41,12 @@ public class P25AudioModule extends Module implements Listener<Message>,
|
||||
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<Message>,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
public void reset()
|
||||
{
|
||||
loadConverter();
|
||||
mAudioMetadata.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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<CallEvent> 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<String> 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public class CallEventLogger extends EventLogger implements ICallEventListener,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
public void reset()
|
||||
{
|
||||
super.start();
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public class MessageEventLogger extends EventLogger implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
public void reset()
|
||||
{
|
||||
super.start();
|
||||
}
|
||||
|
||||
@@ -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<ComplexBuffer> 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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<RealBuffer> 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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ) );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<AudioPacket>
|
||||
{
|
||||
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<String,RealBufferWaveRecorder> 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<Recorder> 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<Recorder> retVal = new ArrayList<Recorder>();
|
||||
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<String> keysToRemove = new ArrayList<>();
|
||||
|
||||
for( Entry<String,RealBufferWaveRecorder> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<Boolean>
|
||||
{
|
||||
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<ByteBuffer> mToWriteBufferQueue =
|
||||
new LinkedBlockingQueue<ByteBuffer>( 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
225
src/record/wave/ComplexBufferWaveRecorder.java
Normal file
225
src/record/wave/ComplexBufferWaveRecorder.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<ComplexBuffer>
|
||||
{
|
||||
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<ComplexBuffer> 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<ComplexBuffer> 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<ComplexBuffer>
|
||||
{
|
||||
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<ComplexBuffer> mListener;
|
||||
|
||||
//Sample buffer queue ... constrained to 512 sample buffers (arbitrary size)
|
||||
private LinkedBlockingQueue<ByteBuffer> mToWriteBufferQueue =
|
||||
new LinkedBlockingQueue<ByteBuffer>( 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<ComplexBuffer> 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<RealBuffer>
|
||||
{
|
||||
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<ByteBuffer> mToWriteBufferQueue =
|
||||
new LinkedBlockingQueue<ByteBuffer>( 512 );
|
||||
private FloatSampleBufferAssembler mCurrentBuffer;
|
||||
|
||||
private Listener<RealBuffer> 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<RealBuffer> 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
237
src/record/wave/RealBufferWaveRecorder.java
Normal file
237
src/record/wave/RealBufferWaveRecorder.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<RealBuffer>
|
||||
{
|
||||
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<RealBuffer> 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<RealBuffer> 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<ByteBuffer> mBufferQueue =
|
||||
new LinkedBlockingQueue<ByteBuffer>( 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/record/wave/WaveUtils.java
Normal file
108
src/record/wave/WaveUtils.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
251
src/record/wave/WaveWriter.java
Normal file
251
src/record/wave/WaveWriter.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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 );
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<toWrite ; s++, frameCounter++)
|
||||
{
|
||||
buffer[0][s] = Math.sin(2.0 * Math.PI * 400 * frameCounter / sampleRate);
|
||||
buffer[1][s] = Math.sin(2.0 * Math.PI * 500 * frameCounter / sampleRate);
|
||||
}
|
||||
|
||||
// Write the buffer
|
||||
wavFile.writeFrames(buffer, toWrite);
|
||||
}
|
||||
|
||||
// Close the wavFile
|
||||
wavFile.close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.err.println(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package sample.complex;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -13,6 +13,19 @@ public class RealBuffer
|
||||
{
|
||||
mSamples = samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Package private to allow subclass construction and subsequent definition
|
||||
* of the sample data
|
||||
*/
|
||||
RealBuffer()
|
||||
{
|
||||
}
|
||||
|
||||
protected void setSamples( float[] samples )
|
||||
{
|
||||
mSamples = samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a deep copy of the buffer
|
||||
|
||||
@@ -1,864 +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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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<numBytes ; b++) val = (val << 8) + (buffer[--pos] & 0xFF);
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
private static void putLE(long val, byte[] buffer, int pos, int numBytes)
|
||||
{
|
||||
for (int b=0 ; b<numBytes ; b++)
|
||||
{
|
||||
buffer[pos] = (byte) (val & 0xFF);
|
||||
val >>= 8;
|
||||
pos ++;
|
||||
}
|
||||
}
|
||||
|
||||
// Sample Writing and Reading
|
||||
// --------------------------
|
||||
private void writeSample(long val) throws IOException
|
||||
{
|
||||
for (int b=0 ; b<bytesPerSample ; b++)
|
||||
{
|
||||
if (bufferPointer == BUFFER_SIZE)
|
||||
{
|
||||
oStream.write(buffer, 0, BUFFER_SIZE);
|
||||
bufferPointer = 0;
|
||||
}
|
||||
|
||||
buffer[bufferPointer] = (byte) (val & 0xFF);
|
||||
val >>= 8;
|
||||
bufferPointer ++;
|
||||
}
|
||||
}
|
||||
|
||||
private long readSample() throws IOException, WavFileException
|
||||
{
|
||||
long val = 0;
|
||||
|
||||
for (int b=0 ; b<bytesPerSample ; b++)
|
||||
{
|
||||
if (bufferPointer == bytesRead)
|
||||
{
|
||||
int read = iStream.read(buffer, 0, BUFFER_SIZE);
|
||||
if (read == -1) throw new WavFileException("Not enough data available");
|
||||
bytesRead = read;
|
||||
bufferPointer = 0;
|
||||
}
|
||||
|
||||
int v = buffer[bufferPointer];
|
||||
if (b < bytesPerSample-1 || bytesPerSample == 1) v &= 0xFF;
|
||||
val += v << (b * 8);
|
||||
|
||||
bufferPointer ++;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
// Short
|
||||
public int readFrames(short[] sampleBuffer, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
return readFrames(sampleBuffer, 0, numFramesToRead);
|
||||
}
|
||||
|
||||
public int readFrames(short[] sampleBuffer, int offset, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToRead ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++)
|
||||
{
|
||||
sampleBuffer[offset] = (short) readSample();
|
||||
offset ++;
|
||||
}
|
||||
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToRead;
|
||||
}
|
||||
|
||||
public int readFrames(short[][] sampleBuffer, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
return readFrames(sampleBuffer, 0, numFramesToRead);
|
||||
}
|
||||
|
||||
public int readFrames(short[][] sampleBuffer, int offset, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToRead ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++) sampleBuffer[c][offset] = (short) readSample();
|
||||
|
||||
offset ++;
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToRead;
|
||||
}
|
||||
|
||||
public int writeFrames(short[] sampleBuffer, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
return writeFrames(sampleBuffer, 0, numFramesToWrite);
|
||||
}
|
||||
|
||||
public int writeFrames(short[] sampleBuffer, int offset, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToWrite ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++)
|
||||
{
|
||||
writeSample(sampleBuffer[offset]);
|
||||
offset ++;
|
||||
}
|
||||
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToWrite;
|
||||
}
|
||||
|
||||
public int writeFrames(short[][] sampleBuffer, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
return writeFrames(sampleBuffer, 0, numFramesToWrite);
|
||||
}
|
||||
|
||||
public int writeFrames(short[][] sampleBuffer, int offset, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToWrite ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++) writeSample(sampleBuffer[c][offset]);
|
||||
|
||||
offset ++;
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToWrite;
|
||||
}
|
||||
|
||||
// Integer
|
||||
// -------
|
||||
public int readFrames(int[] sampleBuffer, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
return readFrames(sampleBuffer, 0, numFramesToRead);
|
||||
}
|
||||
|
||||
public int readFrames(int[] sampleBuffer, int offset, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToRead ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++)
|
||||
{
|
||||
sampleBuffer[offset] = (int) readSample();
|
||||
offset ++;
|
||||
}
|
||||
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToRead;
|
||||
}
|
||||
|
||||
public int readFrames(int[][] sampleBuffer, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
return readFrames(sampleBuffer, 0, numFramesToRead);
|
||||
}
|
||||
|
||||
public int readFrames(int[][] sampleBuffer, int offset, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToRead ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++) sampleBuffer[c][offset] = (int) readSample();
|
||||
|
||||
offset ++;
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToRead;
|
||||
}
|
||||
|
||||
public int writeFrames(int[] sampleBuffer, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
return writeFrames(sampleBuffer, 0, numFramesToWrite);
|
||||
}
|
||||
|
||||
public int writeFrames(int[] sampleBuffer, int offset, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToWrite ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++)
|
||||
{
|
||||
writeSample(sampleBuffer[offset]);
|
||||
offset ++;
|
||||
}
|
||||
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToWrite;
|
||||
}
|
||||
|
||||
public int writeFrames(int[][] sampleBuffer, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
return writeFrames(sampleBuffer, 0, numFramesToWrite);
|
||||
}
|
||||
|
||||
public int writeFrames(int[][] sampleBuffer, int offset, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToWrite ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++) writeSample(sampleBuffer[c][offset]);
|
||||
|
||||
offset ++;
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToWrite;
|
||||
}
|
||||
|
||||
// Long
|
||||
// ----
|
||||
public int readFrames(long[] sampleBuffer, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
return readFrames(sampleBuffer, 0, numFramesToRead);
|
||||
}
|
||||
|
||||
public int readFrames(long[] sampleBuffer, int offset, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToRead ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++)
|
||||
{
|
||||
sampleBuffer[offset] = readSample();
|
||||
offset ++;
|
||||
}
|
||||
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToRead;
|
||||
}
|
||||
|
||||
public int readFrames(long[][] sampleBuffer, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
return readFrames(sampleBuffer, 0, numFramesToRead);
|
||||
}
|
||||
|
||||
public int readFrames(long[][] sampleBuffer, int offset, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToRead ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++) sampleBuffer[c][offset] = readSample();
|
||||
|
||||
offset ++;
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToRead;
|
||||
}
|
||||
|
||||
public int writeFrames(long[] sampleBuffer, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
return writeFrames(sampleBuffer, 0, numFramesToWrite);
|
||||
}
|
||||
|
||||
public int writeFrames(long[] sampleBuffer, int offset, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToWrite ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++)
|
||||
{
|
||||
writeSample(sampleBuffer[offset]);
|
||||
offset ++;
|
||||
}
|
||||
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToWrite;
|
||||
}
|
||||
|
||||
public int writeFrames(long[][] sampleBuffer, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
return writeFrames(sampleBuffer, 0, numFramesToWrite);
|
||||
}
|
||||
|
||||
public int writeFrames(long[][] sampleBuffer, int offset, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToWrite ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++) writeSample(sampleBuffer[c][offset]);
|
||||
|
||||
offset ++;
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToWrite;
|
||||
}
|
||||
|
||||
// Double
|
||||
// ------
|
||||
public int readFrames(double[] sampleBuffer, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
return readFrames(sampleBuffer, 0, numFramesToRead);
|
||||
}
|
||||
|
||||
public int readFrames(double[] sampleBuffer, int offset, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToRead ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++)
|
||||
{
|
||||
sampleBuffer[offset] = floatOffset + (double) readSample() / floatScale;
|
||||
offset ++;
|
||||
}
|
||||
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToRead;
|
||||
}
|
||||
|
||||
public int readFrames(double[][] sampleBuffer, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
return readFrames(sampleBuffer, 0, numFramesToRead);
|
||||
}
|
||||
|
||||
public int readFrames(double[][] sampleBuffer, int offset, int numFramesToRead) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.READING) throw new IOException("Cannot read from WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToRead ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++) sampleBuffer[c][offset] = floatOffset + (double) readSample() / floatScale;
|
||||
|
||||
offset ++;
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToRead;
|
||||
}
|
||||
|
||||
public int writeFrames(double[] sampleBuffer, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
return writeFrames(sampleBuffer, 0, numFramesToWrite);
|
||||
}
|
||||
|
||||
public int writeFrames(double[] sampleBuffer, int offset, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToWrite ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++)
|
||||
{
|
||||
writeSample((long) (floatScale * (floatOffset + sampleBuffer[offset])));
|
||||
offset ++;
|
||||
}
|
||||
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToWrite;
|
||||
}
|
||||
|
||||
public int writeFrames(double[][] sampleBuffer, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
return writeFrames(sampleBuffer, 0, numFramesToWrite);
|
||||
}
|
||||
|
||||
public int writeFrames(double[][] sampleBuffer, int offset, int numFramesToWrite) throws IOException, WavFileException
|
||||
{
|
||||
if (ioState != IOState.WRITING) throw new IOException("Cannot write to WavFile instance");
|
||||
|
||||
for (int f=0 ; f<numFramesToWrite ; f++)
|
||||
{
|
||||
if (frameCounter == numFrames) return f;
|
||||
|
||||
for (int c=0 ; c<numChannels ; c++) writeSample((long) (floatScale * (floatOffset + sampleBuffer[c][offset])));
|
||||
|
||||
offset ++;
|
||||
frameCounter ++;
|
||||
}
|
||||
|
||||
return numFramesToWrite;
|
||||
}
|
||||
|
||||
|
||||
public void close() throws IOException
|
||||
{
|
||||
// Close the input stream and set to null
|
||||
if (iStream != null)
|
||||
{
|
||||
iStream.close();
|
||||
iStream = null;
|
||||
}
|
||||
|
||||
if (oStream != null)
|
||||
{
|
||||
// Write out anything still in the local buffer
|
||||
if (bufferPointer > 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user