Merge branch 'master' into 39-polyphase-channelizer

# Conflicts:
#	src/channel/state/ChannelState.java
#	src/module/ProcessingChain.java
#	src/source/Source.java
#	src/source/mixer/RealMixerSource.java
#	src/source/tuner/TunerChannelSource.java
#	src/source/wave/RealWaveSource.java
This commit is contained in:
Denny
2017-10-15 07:17:57 -04:00
35 changed files with 2196 additions and 1172 deletions

View File

@@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found. the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.> {one line to give the program's name and a brief idea of what it does.}
Copyright (C) <year> <name of author> Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode: notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author> {project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details. under certain conditions; type `show c' for details.

View File

@@ -2,7 +2,6 @@
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR). A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/GettingStarted) * [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/GettingStarted)
* [User's Manual Version 0.2.0](https://github.com/DSheirer/sdrtrunk/wiki/UserManual)
* [User's Manual Version 0.3.0](https://github.com/DSheirer/sdrtrunk/wiki/UserManual_Version0.3.0) * [User's Manual Version 0.3.0](https://github.com/DSheirer/sdrtrunk/wiki/UserManual_Version0.3.0)
* [Download](https://github.com/DSheirer/sdrtrunk/releases) * [Download](https://github.com/DSheirer/sdrtrunk/releases)
* [Support](https://groups.google.com/forum/#!forum/sdrtrunk) * [Support](https://groups.google.com/forum/#!forum/sdrtrunk)

View File

@@ -1,121 +1,142 @@
<project name="SDRTrunk" default="SDRTrunk" basedir="."> <project name="SDRTrunk" default="SDRTrunk" basedir=".">
<description> <description>
Ant build script for SDRTrunk Decoder Application Ant build script for SDRTrunk Decoder Application
</description> </description>
<!-- version properties --> <!-- version properties -->
<property name="major" value="0"/> <property name="major" value="0"/>
<property name="minor" value="3"/> <property name="minor" value="3"/>
<property name="patch" value="0"/> <property name="patch" value="1"/>
<property name="release" value="-beta10"/> <property name="release" value="final"/>
<property name="filename" value="sdrtrunk_${major}.${minor}.${patch}${release}"/> <property name="filename" value="sdrtrunk_${major}.${minor}.${patch}${release}"/>
<!-- Build properties --> <!-- Build properties -->
<property name="repo" location=".."/> <property name="repo" location=".."/>
<property name="source.code" location="${repo}/src"/> <property name="source.code" location="${repo}/src"/>
<property name="source.configs" location="${repo}/config"/> <property name="source.configs" location="${repo}/config"/>
<property name="source.images" location="${repo}/images"/> <property name="source.images" location="${repo}/images"/>
<property name="source.libs" location="${repo}/imports"/> <property name="source.libs" location="${repo}/imports"/>
<property name="source.license" location="${repo}/license"/> <property name="source.scripts" location="${repo}/scripts"/>
<property name="source.scripts" location="${repo}/scripts"/>
<property name="output" location="${repo}/product"/> <property name="output" location="${repo}/product"/>
<property name="output.classes" location="${repo}/classes"/> <property name="output.classes" location="${repo}/classes"/>
<property name="output.folder" location="${output}/sdrtrunk"/> <property name="output.folder" location="${output}/sdrtrunk"/>
<property name="output.config" location="${output.folder}/config"/> <property name="output.config" location="${output.folder}/config"/>
<property name="output.images" location="${output.folder}/images"/> <property name="output.images" location="${output.folder}/images"/>
<property name="output.libs" location="${output.folder}/libs"/> <property name="output.libs" location="${output.folder}/libs"/>
<path id="classpath"> <path id="classpath">
<fileset dir="${source.libs}" includes="*.jar" /> <fileset dir="${source.libs}" includes="*.jar"/>
</path> </path>
<target name="init"> <target name="init">
<!-- Create the classes folder to contain the compiled java classes --> <!-- Create the classes folder to contain the compiled java classes -->
<mkdir dir="${output.classes}"/> <mkdir dir="${output.classes}"/>
<!-- Create version file --> <!-- Create version file -->
<echo message="${major}.${minor}.${patch}-${release}" file="${output.classes}/sdrtrunk-version"/> <echo message="${major}.${minor}.${patch}-${release}" file="${output.classes}/sdrtrunk-version"/>
</target> </target>
<target name="compile" depends="clean,init" description="Compile java classes" > <target name="compile" depends="clean,init,compile-java-8,compile-java-9"
<javac srcdir="${source.code}" destdir="${output.classes}" classpathref="classpath" includeantruntime="false" /> description="Compile java classes" />
</target>
<target name="SDRTrunk" depends="compile" description="Create SDRTrunk application" > <target name="compile-java-8" depends="check-java-version" if="java.8" description="Compiles code with Java 8 arguments">
<!-- Create the distribution directory --> <echo message="Compiling with Java 8 Options"/>
<mkdir dir="${output.folder}"/> <javac srcdir="${source.code}" destdir="${output.classes}" classpathref="classpath" includeantruntime="false"/>
</target>
<!-- Create jar file --> <target name="compile-java-9" depends="check-java-version" if="java.9" description="Compiles code with Java 9 arguments">
<jar jarfile="${output.folder}/sdrtrunk.jar" basedir="${output.classes}"> <echo message="Compiling with Java 9 Options"/>
<manifest> <javac srcdir="${source.code}" destdir="${output.classes}" classpathref="classpath" includeantruntime="false">
<attribute name="Title" value="sdrtrunk"/> <compilerarg value="--add-modules"/>
<attribute name="Main-Class" value="gui.SDRTrunk"/> <compilerarg value="java.xml.bind"/>
<attribute name="Version" value="${major}.${minor}.${patch}-${release}"/> </javac>
</manifest> </target>
<fileset dir="${source.configs}">
<exclude name="*.rules"/>
</fileset>
<fileset dir="${source.license}"> <target name="check-java-version" description="Determines if the compiler is Java 8 or 9">
<include name="license.txt"/> <condition property="java.8" value="true">
</fileset> <contains string="${java.version}" substring="1.8"/>
</jar> </condition>
<condition property="java.9" value="true">
<equals arg1="${java.version}" arg2="9"/>
</condition>
<echo message="Java Version: ${java.version}"/>
</target>
<!-- Copy third-party libraries --> <target name="SDRTrunk" depends="compile" description="Create SDRTrunk application">
<mkdir dir="${output.libs}"/> <!-- Create the distribution directory -->
<copy todir="${output.libs}"> <mkdir dir="${output.folder}"/>
<fileset dir="${source.libs}" includes="*.jar" />
</copy>
<!-- Copy config files --> <!-- Create jar file -->
<mkdir dir="${output.config}"/> <jar jarfile="${output.folder}/sdrtrunk.jar" basedir="${output.classes}">
<manifest>
<attribute name="Title" value="sdrtrunk"/>
<attribute name="Main-Class" value="gui.SDRTrunk"/>
<attribute name="Version" value="${major}.${minor}.${patch}-${release}"/>
</manifest>
<fileset dir="${source.configs}">
<exclude name="*.rules"/>
</fileset>
<copy todir="${output.config}"> <fileset dir="${repo}">
<fileset dir="${source.configs}"> <include name="LICENSE"/>
<include name="*.rules"/> </fileset>
</fileset> </jar>
</copy>
<!-- Copy image files --> <!-- Copy third-party libraries -->
<mkdir dir="${output.images}"/> <mkdir dir="${output.libs}"/>
<copy todir="${output.images}"> <copy todir="${output.libs}">
<fileset dir="${source.images}" /> <fileset dir="${source.libs}" includes="*.jar"/>
</copy> </copy>
<!-- Copy license file --> <!-- Copy config files -->
<copy todir="${output.folder}"> <mkdir dir="${output.config}"/>
<fileset dir="${source.license}">
<include name="license.txt"/>
</fileset>
</copy>
<!-- Copy program scripts --> <copy todir="${output.config}">
<copy todir="${output.folder}"> <fileset dir="${source.configs}">
<fileset dir="${source.scripts}" /> <include name="*.rules"/>
</copy> </fileset>
</copy>
<!-- Copy image files -->
<mkdir dir="${output.images}"/>
<copy todir="${output.images}">
<fileset dir="${source.images}"/>
</copy>
<!-- Copy license file -->
<copy todir="${output.folder}">
<fileset dir="${repo}">
<include name="LICENSE"/>
</fileset>
</copy>
<!-- Copy program scripts -->
<copy todir="${output.folder}">
<fileset dir="${source.scripts}"/>
</copy>
<!-- Bundle the files into .zip --> <!-- Bundle the files into .zip -->
<zip destfile="${output}/${filename}.zip" basedir="${output}" /> <zip destfile="${output}/${filename}.zip" basedir="${output}"/>
<!-- Remove linux start script before tar task --> <!-- Remove linux start script before tar task -->
<delete file="${output.folder}/run_sdrtrunk_linux.sh"/> <delete file="${output.folder}/run_sdrtrunk_linux.sh"/>
<!-- Bundle files into .tar.gz and copy/set permissions on linux start script --> <!-- Bundle files into .tar.gz and copy/set permissions on linux start script -->
<tar destfile="${output}/${filename}.tar.gz" compression="gzip"> <tar destfile="${output}/${filename}.tar.gz" compression="gzip">
<tarfileset dir="${output.folder}" prefix="sdrtrunk" /> <tarfileset dir="${output.folder}" prefix="sdrtrunk"/>
<tarfileset dir="${source.scripts}" prefix="sdrtrunk" filemode="777"> <tarfileset dir="${source.scripts}" prefix="sdrtrunk" filemode="777">
<include name="run_sdrtrunk_linux.sh"/> <include name="run_sdrtrunk_linux.sh"/>
</tarfileset> </tarfileset>
</tar> </tar>
<delete dir="${output.folder}"/> <delete dir="${output.folder}"/>
</target> </target>
<target name="clean" description="Clean previous build artifacts" > <target name="clean" description="Clean previous build artifacts">
<delete dir="${output.classes}"/> <delete dir="${output.classes}"/>
<delete dir="${output}"/> <delete dir="${output}"/>
</target> </target>
</project> </project>

View File

@@ -1,2 +1,16 @@
#!/bin/bash #!/bin/bash
java -XX:+UseG1GC -cp "*:libs/*" gui.SDRTrunk
java -version
# Test for java version 9
JAVA_9=$(java -version 2>&1 | grep -i version | sed 's/.* "\([9]\).*/\1/')
if [ -z ${JAVA_9+x} ]; then
echo Using Java 9 program options
# Java 9 uses the G1 garbage collector by default and requires that we specify the
# java.xml.bind package since it's been removed from the java se core library
java -cp "*:libs/*" --add-modules java.xml.bind gui.SDRTrunk;
else
echo Using Java 8 program options
java -XX:+UseG1GC -cp "*:libs/*" gui.SDRTrunk;
fi

View File

@@ -1 +1,25 @@
java -XX:+UseG1GC -cp "SDRTrunk.jar;*;libs/*;config/*;images/*" gui.SDRTrunk @echo off
java -version
SETLOCAL ENABLEDELAYEDEXPANSION
for /f "usebackq tokens=3" %%a in (`java -version 2^>^&1 ^| find "version"`) do (
rem set b to the unquoted version string parsed from the java version command
set b=%%~a
rem set c to the first character of the unquoted version string
set c=!b:~0,1!
if !c! equ 9 (
echo Using Java 9 options
java -cp "SDRTrunk.jar;*;libs/*;config/*;images/*" --add-modules java.xml.bind gui.SDRTrunk
) else (
echo Using Java 8 options
java -XX:+UseG1GC -cp "SDRTrunk.jar;*;libs/*;config/*;images/*" gui.SDRTrunk
)
)
ENDLOCAL
@echo on

View File

@@ -36,6 +36,7 @@ import util.ThreadPool;
import java.io.IOException; import java.io.IOException;
import java.net.ConnectException; import java.net.ConnectException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.channels.UnresolvedAddressException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -44,7 +45,8 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
private final static Logger mLog = LoggerFactory.getLogger(IcecastTCPAudioBroadcaster.class); private final static Logger mLog = LoggerFactory.getLogger(IcecastTCPAudioBroadcaster.class);
private final static String TERMINATOR = "\r\n"; private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":"; private final static String SEPARATOR = ":";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
private static final long CONNECTION_ATTEMPT_TIMEOUT = 5000; //5 seconds
private NioSocketConnector mSocketConnector; private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null; private IoSession mStreamingSession = null;
@@ -99,10 +101,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
if(mSocketConnector == null) if(mSocketConnector == null)
{ {
mSocketConnector = new NioSocketConnector(); mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000); mSocketConnector.setConnectTimeoutCheckInterval(CONNECTION_ATTEMPT_TIMEOUT);
// mSocketConnector.getFilterChain().addLast("logger", // LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
// new LoggingFilter(IcecastTCPAudioBroadcaster.class)); // loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
mSocketConnector.getFilterChain().addLast("codec", mSocketConnector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new IcecastCodecFactory())); new ProtocolCodecFilter(new IcecastCodecFactory()));
@@ -123,9 +126,20 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
ConnectFuture future = mSocketConnector ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort())); getBroadcastConfiguration().getPort()));
future.awaitUninterruptibly();
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT, TimeUnit.MILLISECONDS);
mStreamingSession = future.getSession(); mStreamingSession = future.getSession();
} }
catch(UnresolvedAddressException uae)
{
//What do you do with this? Nothing.
}
catch(InterruptedException ie)
{
//We couldn't connect within the timeout period -- disconnect so that we can try again later
disconnect();
}
catch(RuntimeIoException rie) catch(RuntimeIoException rie)
{ {
Throwable throwableCause = rie.getCause(); Throwable throwableCause = rie.getCause();
@@ -137,16 +151,20 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
else if(throwableCause != null) else if(throwableCause != null)
{ {
setBroadcastState(BroadcastState.ERROR); setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect", rie); mLog.debug("[" + getStreamName() + "] failed to connect", rie);
} }
else else
{ {
setBroadcastState(BroadcastState.ERROR); setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect - no exception is available"); mLog.debug("[" + getStreamName() + "] failed to connect - no exception is available");
} }
disconnect(); disconnect();
} }
catch(Throwable t)
{
disconnect();
}
mConnecting.set(false); mConnecting.set(false);
} }
@@ -164,12 +182,13 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
*/ */
public void disconnect() public void disconnect()
{ {
if(connected()) if(connected() && mStreamingSession != null)
{ {
if(mStreamingSession != null) mStreamingSession.closeNow();
{ }
mStreamingSession.closeNow(); else
} {
mLastConnectionAttempt = System.currentTimeMillis();
} }
} }
@@ -226,17 +245,18 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
@Override @Override
public void sessionClosed(IoSession session) throws Exception public void sessionClosed(IoSession session) throws Exception
{ {
mLastConnectionAttempt = System.currentTimeMillis();
//If there is already an error state, don't override it. Otherwise, set state to disconnected //If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState()) if(!getBroadcastState().isErrorState())
{ {
setBroadcastState(BroadcastState.DISCONNECTED); setBroadcastState(BroadcastState.DISCONNECTED);
} }
mSocketConnector.dispose();
mStreamingSession = null; mStreamingSession = null;
mSocketConnector = null;
mConnecting.set(false); mConnecting.set(false);
super.sessionClosed(session); super.sessionClosed(session);
} }
@@ -253,45 +273,36 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
if(connected()) if(connected())
{ {
mLog.info("Streaming connection error detected - resetting connection - " + reason);
disconnect(); disconnect();
connect();
} }
else if(reason.startsWith("Connection reset")) else if(reason.startsWith("Connection reset"))
{ {
mLog.info("Streaming connection reset by remote server - reestablishing connection");
disconnect(); disconnect();
connect();
} }
else if(reason.startsWith("Operation timed out")) else if(reason.startsWith("Operation timed out"))
{ {
mLog.info("Streaming connection timed out - resetting connection");
disconnect(); disconnect();
connect();
} }
else else
{ {
setBroadcastState(BroadcastState.ERROR); setBroadcastState(BroadcastState.ERROR);
disconnect(); disconnect();
mLog.error("Unrecognized IO error: " + reason + ". Streaming halted.", cause);
} }
} }
else if(ioe instanceof WriteTimeoutException) else if(ioe instanceof WriteTimeoutException)
{ {
mLog.info("Network write timeout error - resetting connection");
disconnect(); disconnect();
connect();
} }
else else
{ {
setBroadcastState(BroadcastState.ERROR); setBroadcastState(BroadcastState.ERROR);
disconnect(); disconnect();
mLog.error("Unspecified IO error - streaming halted.", cause); mLog.error("[" + getStreamName() + "] Unspecified IO error - streaming halted.", cause);
} }
} }
else else
{ {
mLog.error("Broadcast error", cause); mLog.error("[" + getStreamName() + "] Broadcast error", cause);
setBroadcastState(BroadcastState.ERROR); setBroadcastState(BroadcastState.ERROR);
disconnect(); disconnect();
} }
@@ -319,6 +330,10 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
{ {
setBroadcastState(BroadcastState.INVALID_CREDENTIALS); setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
} }
else if(message.contains("HTTP/1.1 501"))
{
disconnect(); //So that we can reconnect later
}
else else
{ {
mLog.error("Unrecognized server response:" + message); mLog.error("Unrecognized server response:" + message);
@@ -328,7 +343,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
} }
else else
{ {
mLog.error("Icecast TCP broadcaster - unrecognized message [ " + object.getClass() + mLog.error("[" + getStreamName() + "]Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
"] received:" + object.toString()); "] received:" + object.toString());
} }
} }

View File

@@ -168,6 +168,10 @@ public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
disconnect(); disconnect();
} }
catch(Throwable t)
{
disconnect();
}
mConnecting.set(false); mConnecting.set(false);
} }
@@ -185,12 +189,13 @@ public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
*/ */
public void disconnect() public void disconnect()
{ {
if(connected()) if(connected() && mStreamingSession != null)
{ {
if(mStreamingSession != null) mStreamingSession.closeNow();
{ }
mStreamingSession.closeNow(); else
} {
mLastConnectionAttempt = System.currentTimeMillis();
} }
} }
@@ -226,15 +231,15 @@ public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
@Override @Override
public void sessionClosed(IoSession session) throws Exception public void sessionClosed(IoSession session) throws Exception
{ {
mLastConnectionAttempt = System.currentTimeMillis();
//If there is already an error state, don't override it. Otherwise, set state to disconnected //If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState()) if(!getBroadcastState().isErrorState())
{ {
setBroadcastState(BroadcastState.DISCONNECTED); setBroadcastState(BroadcastState.DISCONNECTED);
} }
mSocketConnector.dispose();
mStreamingSession = null; mStreamingSession = null;
mSocketConnector = null;
mConnecting.set(false); mConnecting.set(false);
super.sessionClosed(session); super.sessionClosed(session);
@@ -255,13 +260,11 @@ public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
{ {
mLog.info("Streaming connection reset by remote server - reestablishing connection"); mLog.info("Streaming connection reset by remote server - reestablishing connection");
disconnect(); disconnect();
connect();
} }
else if(reason.startsWith("Operation timed out")) else if(reason.startsWith("Operation timed out"))
{ {
mLog.info("Streaming connection timed out - resetting connection"); mLog.info("Streaming connection timed out - resetting connection");
disconnect(); disconnect();
connect();
} }
else else
{ {

View File

@@ -204,6 +204,10 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
disconnect(); disconnect();
} }
catch(Throwable t)
{
disconnect();
}
mConnecting.set(false); mConnecting.set(false);
} }
@@ -221,13 +225,13 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
*/ */
public void disconnect() public void disconnect()
{ {
if(connected()) if(connected() && mStreamingSession != null)
{ {
if(mStreamingSession != null) mStreamingSession.closeNow();
{ }
mStreamingSession.write(new TerminateBroadcast()); else
mStreamingSession.closeNow(); {
} mLastConnectionAttempt = System.currentTimeMillis();
} }
} }
@@ -349,15 +353,15 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
@Override @Override
public void sessionClosed(IoSession session) throws Exception public void sessionClosed(IoSession session) throws Exception
{ {
mLastConnectionAttempt = System.currentTimeMillis();
//If there is already an error state, don't override it. Otherwise, set state to disconnected //If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState()) if(!getBroadcastState().isErrorState())
{ {
setBroadcastState(BroadcastState.DISCONNECTED); setBroadcastState(BroadcastState.DISCONNECTED);
} }
mSocketConnector.dispose();
mStreamingSession = null; mStreamingSession = null;
mSocketConnector = null;
mConnecting.set(false); mConnecting.set(false);
super.sessionClosed(session); super.sessionClosed(session);
@@ -378,13 +382,11 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
{ {
mLog.info("Streaming connection reset by remote server - reestablishing connection"); mLog.info("Streaming connection reset by remote server - reestablishing connection");
disconnect(); disconnect();
connect();
} }
else if(reason.startsWith("Operation timed out")) else if(reason.startsWith("Operation timed out"))
{ {
mLog.info("Streaming connection timed out - resetting connection"); mLog.info("Streaming connection timed out - resetting connection");
disconnect(); disconnect();
connect();
} }
else else
{ {

View File

@@ -137,6 +137,17 @@ public class BinaryMessage extends BitSet
mCRC = crc; mCRC = crc;
} }
/**
* Indicates the number of bit errors that were corrected in this message.
*
* @see CorrectedBinaryMessage subclass that supports setting this value
* @return number of corrected bits in this message
*/
public int getCorrectedBitCount()
{
return 0;
}
/** /**
* Current pointer index * Current pointer index
*/ */

View File

@@ -0,0 +1,65 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package bits;
import java.util.BitSet;
public class CorrectedBinaryMessage extends BinaryMessage
{
private int mCorrectedBitCount;
/**
* Subclass of binary message class to allow capturing a corrected bits metric.
* @param size of message to create.
*/
public CorrectedBinaryMessage(int size)
{
super(size);
}
public CorrectedBinaryMessage(int size, boolean[] bitsToPreload)
{
super(size, bitsToPreload);
}
public CorrectedBinaryMessage(BitSet bitset, int size)
{
super(bitset, size);
}
public CorrectedBinaryMessage(byte[] data)
{
super(data);
}
@Override
public int getCorrectedBitCount()
{
return mCorrectedBitCount;
}
/**
* Sets the number of bits that were corrected in this message
* @param count of corrected bits
*/
public void setCorrectedBitCount(int count)
{
mCorrectedBitCount = count;
}
}

View File

@@ -0,0 +1,31 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package channel.heartbeat;
public class Heartbeat
{
/**
* Periodic pulse that is broadcast to subscribing modules within the demodulating chain so that
* monitoring of state and other attributes can occur on the primary demodulation and decoding
* thread.
*/
public Heartbeat()
{
}
}

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package channel.heartbeat;
import sample.Listener;
public interface IHeartbeatListener
{
public Listener<Heartbeat> getHeartbeatListener();
}

View File

@@ -0,0 +1,28 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package channel.heartbeat;
import sample.Listener;
public interface IHeartbeatProvider
{
public void setHeartbeatListener(Listener<Heartbeat> listener);
public void removeHeartbeatListener();
}

View File

@@ -20,6 +20,8 @@ package channel.state;
import audio.squelch.ISquelchStateProvider; import audio.squelch.ISquelchStateProvider;
import audio.squelch.SquelchState; import audio.squelch.SquelchState;
import channel.heartbeat.Heartbeat;
import channel.heartbeat.IHeartbeatListener;
import channel.metadata.Attribute; import channel.metadata.Attribute;
import channel.metadata.AttributeChangeRequest; import channel.metadata.AttributeChangeRequest;
import channel.metadata.IAttributeChangeRequestListener; import channel.metadata.IAttributeChangeRequestListener;
@@ -39,16 +41,13 @@ import sample.real.IOverflowListener;
import source.SourceEvent; import source.SourceEvent;
import source.ISourceEventListener; import source.ISourceEventListener;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import static source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE; import static source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener, public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
IDecoderStateEventProvider, ISourceEventListener, ISquelchStateProvider, IAttributeChangeRequestListener, IDecoderStateEventProvider, ISourceEventListener, ISquelchStateProvider, IAttributeChangeRequestListener,
IOverflowListener IOverflowListener, IHeartbeatListener
{ {
private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class); private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class);
@@ -61,8 +60,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
private Listener<DecoderStateEvent> mDecoderStateListener; private Listener<DecoderStateEvent> mDecoderStateListener;
private Listener<SquelchState> mSquelchStateListener; private Listener<SquelchState> mSquelchStateListener;
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver(); private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
private StateMonitor mStateMonitor = new StateMonitor(); private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
private ScheduledFuture<?> mStateMonitorFuture;
private ChannelType mChannelType; private ChannelType mChannelType;
private TrafficChannelManager mTrafficChannelEndListener; private TrafficChannelManager mTrafficChannelEndListener;
private CallEvent mTrafficChannelCallEvent; private CallEvent mTrafficChannelCallEvent;
@@ -79,30 +77,21 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
/** /**
* Channel state tracks the overall state of all processing modules and decoders * Channel state tracks the overall state of all processing modules and decoders configured for the channel and
* configured for the channel and provides squelch control and decoder state * provides squelch control and decoder state reset events.
* reset events.
* *
* Uses a state enumeration that defines allowable channel state transitions in * Uses a state enumeration that defines allowable channel state transitions in order to track a call or data decode
* order to track a call or data decode event from start to finish. Uses a * event from start to finish. Uses a timer to monitor for inactivity and to provide a FADE period that indicates
* timer to monitor for inactivity and to provide a FADE period that indicates * to the user that the activity has stopped while continuing to provide details about the call, before the state is
* to the user that the activity has stopped while continuing to provide details * reset to IDLE.
* about the call, before the state is reset to IDLE.
*
* Since this class is multi-threaded between decoder events and the internal
* timer that monitors for inactivity, this class uses the mState object for
* thread synchronization.
* *
* State Descriptions: * State Descriptions:
* IDLE Normal state. No voice or data call activity * IDLE: Normal state. No voice or data call activity
* CALL/DATA/ENCRYPTED/CONTROL * CALL/DATA/ENCRYPTED/CONTROL: Decoding states.
* Decoding states. * FADE: The phase after a voice or data call when either an explicit call end has been received, or when no new
* FADE The phase after a voice or data call when either an explicit * signalling updates have been received, and the fade timer has expired. This phase allows for gui updates to
* call end has been received, or when no new signalling updates * signal to the user that the call is ended, while continuing to display the call details for the user
* have been received, and the fade timer has expired. This phase * TEARDOWN: Indicates a traffic channel that will be torn down for reuse.
* allows for gui updates to signal to the user that the call is
* ended, while continuing to display the call details for the user
* TEARDOWN Indicates a traffic channel that will be torn down for reuse.
*/ */
public ChannelState(ChannelType channelType) public ChannelState(ChannelType channelType)
{ {
@@ -137,23 +126,11 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
@Override @Override
public void start(ScheduledExecutorService executor) public void start(ScheduledExecutorService executor)
{ {
if(mStateMonitorFuture == null && mStateMonitor != null) mMutableMetadata.receive(new AttributeChangeRequest<State>(Attribute.CHANNEL_STATE, mState));
if(mTrafficChannelEndListener != null)
{ {
mMutableMetadata.receive(new AttributeChangeRequest<State>(Attribute.CHANNEL_STATE, mState)); setState(State.CALL);
if(mTrafficChannelEndListener != null)
{
setState(State.CALL);
}
try
{
mStateMonitorFuture = executor.scheduleAtFixedRate(mStateMonitor, 0, 20, TimeUnit.MILLISECONDS);
}
catch(RejectedExecutionException ree)
{
mLog.error("state monitor scheduled task rejected", ree);
}
} }
} }
@@ -162,16 +139,6 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
{ {
mMutableMetadata.resetAllAttributes(); mMutableMetadata.resetAllAttributes();
if(mStateMonitorFuture != null)
{
boolean success = mStateMonitorFuture.cancel(true);
if(!success)
{
mLog.error("Couldn't stop monitoring scheduled future");
}
}
mTrafficChannelEndListener = null; mTrafficChannelEndListener = null;
if(mTrafficChannelCallEvent != null) if(mTrafficChannelCallEvent != null)
@@ -182,8 +149,6 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
mTrafficChannelCallEvent = null; mTrafficChannelCallEvent = null;
mStateMonitorFuture = null;
mSquelchLocked = false; mSquelchLocked = false;
} }
@@ -192,7 +157,6 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
mCallEventListener = null; mCallEventListener = null;
mDecoderStateListener = null; mDecoderStateListener = null;
mSquelchStateListener = null; mSquelchStateListener = null;
mStateMonitor = null;
} }
@Override @Override
@@ -312,70 +276,74 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
} }
/** /**
* Sets the channel state to the specified state, or updates the timeout * Sets the channel state to the specified state, or updates the timeout values so that the state monitor will not
* values so that the state monitor will not change state. Broadcasts a * change state. Broadcasts a squelch event when the state changes and the audio squelch state should change. Also
* squelch event when the state changes and the audio squelch state should * broadcasts changed attribute and decoder state events so that external processes can maintain sync with this
* change. Also broadcasts changed attribute and decoder state events so * channel state.
* that external processes can maintain sync with this channel state.
*/ */
protected void setState(State state) protected void setState(State state)
{ {
synchronized(mState) if(state == mState)
{ {
if(state == mState) if(State.CALL_STATES.contains(state))
{ {
if(State.CALL_STATES.contains(state)) updateFadeTimeout();
{
updateFadeTimeout();
}
} }
else if(state != mState && mState.canChangeTo(state)) }
else if(mState.canChangeTo(state))
{
switch(state)
{ {
switch(state) case CONTROL:
{ //Don't allow traffic channels to be control channels, otherwise they can't transition to teardown
case CONTROL: if(isStandardChannel())
//Don't allow traffic channels to be control channels, {
//otherwise they can't transition to call tear down
if(isStandardChannel())
{
broadcast(SquelchState.SQUELCH);
updateFadeTimeout();
mState = state;
}
break;
case DATA:
case ENCRYPTED:
broadcast(SquelchState.SQUELCH); broadcast(SquelchState.SQUELCH);
updateFadeTimeout(); updateFadeTimeout();
mState = state; mState = state;
break; }
case CALL: break;
broadcast(SquelchState.UNSQUELCH); case DATA:
updateFadeTimeout(); case ENCRYPTED:
mState = state; broadcast(SquelchState.SQUELCH);
break; updateFadeTimeout();
case FADE: mState = state;
processFadeState(); break;
break; case CALL:
case IDLE: broadcast(SquelchState.UNSQUELCH);
processIdleState(); updateFadeTimeout();
break; mState = state;
case TEARDOWN: break;
processTeardownState(); case FADE:
break; processFadeState();
case RESET: break;
mState = State.IDLE; case IDLE:
break; processIdleState();
default: break;
break; case TEARDOWN:
} processTeardownState();
break;
mMutableMetadata.receive(new AttributeChangeRequest<State>(Attribute.CHANNEL_STATE, case RESET:
mSourceOverflow ? State.OVERFLOW : mState)); mState = State.IDLE;
break;
default:
break;
} }
mMutableMetadata.receive(new AttributeChangeRequest<State>(Attribute.CHANNEL_STATE,
mSourceOverflow ? State.OVERFLOW : mState));
} }
} }
/**
* Receiver inner class that implements the IHeartbeatListener interface to receive heartbeat messages.
*/
@Override
public Listener<Heartbeat> getHeartbeatListener()
{
return mHeartbeatReceiver;
}
/** /**
* This method is invoked if the source buffer provider goes into overflow state. Since this is an external state, * This method is invoked if the source buffer provider goes into overflow state. Since this is an external state,
* we use the mSourceOverflow variable to override the internal state reported to external listeners. * we use the mSourceOverflow variable to override the internal state reported to external listeners.
@@ -625,45 +593,41 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
} }
/** /**
* Monitors decoder state events to automatically transition the channel * Processes periodic heartbeats received from the processing chain to perform state monitoring and cleanup
* state to IDLE (standard channel) or to TEARDOWN (traffic channel) when * functions.
* decoding stops or the monitored channel returns to a no signal state.
* *
* Provides a FADE transition state to allow for momentary decoding dropouts * Monitors decoder state events to automatically transition the channel state to IDLE (standard channel) or to
* and to allow the user access to call details for a fade period upon * TEARDOWN (traffic channel) when decoding stops or the monitored channel returns to a no signal state.
* call end. *
* Provides a FADE transition state to allow for momentary decoding dropouts and to allow the user access to call
* details for a fade period upon call end.
*/ */
public class StateMonitor implements Runnable public class HeartbeatReceiver implements Listener<Heartbeat>
{ {
@Override @Override
public void run() public void receive(Heartbeat heartbeat)
{ {
try try
{ {
synchronized(mState) if(State.CALL_STATES.contains(mState) && mFadeTimeout <= System.currentTimeMillis())
{ {
if(State.CALL_STATES.contains(mState) && processFadeState();
mFadeTimeout <= System.currentTimeMillis()) }
else if(mState == State.FADE && mEndTimeout <= System.currentTimeMillis())
{
if(isTrafficChannel())
{ {
processFadeState(); processTeardownState();
} }
else if(mState == State.FADE && else
mEndTimeout <= System.currentTimeMillis())
{ {
if(isTrafficChannel()) processIdleState();
{
processTeardownState();
}
else
{
processIdleState();
}
} }
} }
} }
catch(Exception e) catch(Throwable e)
{ {
mLog.error("Exception thrown while state monitor is running " + mLog.error("An error occurred while state monitor was running " +
"- state [" + getState() + "- state [" + getState() +
"] current [" + System.currentTimeMillis() + "] current [" + System.currentTimeMillis() +
"] mResetTimeout [" + mEndTimeout + "] mResetTimeout [" + mEndTimeout +

View File

@@ -0,0 +1,86 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package edac.trellis;
public class DMRNode extends Node
{
//Hamming error values indicating the number of bits set in each indexed value, 0-15
public static final int[] HAMMING_ERROR_COUNT = new int[]{0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
/**
* Transmitted quad-bit value, derived from ETSI TS 102-361-1, Table B.7 Trellis encoder state transition table
*
* Note: the B.7 table contains constellation point ID numbers that have to be converted into actual bit values
* where the bits represent the dibit pair for the constellation point.
*/
public static final int[][] DMR_TRANSITION_MATRIX = new int[][]
{
{2,13,14,1,7,8,11,4},
{14,1,7,8,11,4,2,13},
{10,5,6,9,15,0,3,12},
{6,9,15,0,3,12,10,5},
{15,0,3,12,10,5,6,9},
{3,12,10,5,6,9,15,0},
{7,8,11,4,2,13,14,1},
{11,4,2,13,14,1,7,8}
};
/**
* Trellis Coded Modulation (TCM) 3/4-rate trellis node for decoding DMR
*
* @param inputValue for the trellis time instant
* @param transmittedOutputValue the actual four-bit output value that was transmitted.
*/
public DMRNode(int inputValue, int transmittedOutputValue)
{
super(inputValue, transmittedOutputValue);
}
/**
* Calculates the error between the preceeding node and this node as the Hamming distance between the expected
* four-bit output value versus the actual transmitted output value.
*
* @param precedingNode that precedes this node
* @return error value for this node relative to the preceeding node argument
*/
@Override
public int getError(Node precedingNode)
{
if(precedingNode != null)
{
int expectedOutputValue = getOutputValue(precedingNode);
int errorMask = expectedOutputValue ^ getTransmittedOutputValue();
return HAMMING_ERROR_COUNT[errorMask];
}
return 0;
}
/**
* Expected output four-bit value when this node is preceeded by the argument node.
*
* @param precedingNode that precedes this node
* @return expected output value
*/
@Override
public int getOutputValue(Node precedingNode)
{
return DMR_TRANSITION_MATRIX[precedingNode.getInputValue()][getInputValue()];
}
}

View File

@@ -0,0 +1,77 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package edac.trellis;
public abstract class Node
{
private int mInputValue;
private int mTransmittedOutputValue;
/**
* Trellis Coded Modulation (TCM) node. A node represents a time instant on the trellis and contains an input
* value and the corresponding transmitted output value for the time instant. A node does not maintain the
* state at the time instant -- the state is determined by the input value of a preceeding node.
*
* @param inputValue for this node
* @param transmittedOutputValue for this node
*/
public Node(int inputValue, int transmittedOutputValue)
{
mInputValue = inputValue;
mTransmittedOutputValue = transmittedOutputValue;
}
/**
* Input value for this node.
*
* @return input value
*/
public int getInputValue()
{
return mInputValue;
}
/**
* Transmitted output value for this node
* @return transmitted output value
*/
public int getTransmittedOutputValue()
{
return mTransmittedOutputValue;
}
/**
* Calculates the error value for this node (time instant) by determining the expected output value that would have
* been transmitted using the state from the preceding node argument and this node's input value and comparing that
* expected value to the actual transmitted output value. This error is normally represented as the Hamming
* distance between the two values.
*
* @param precedingNode that precedes this node
* @return error value for this node relative to the preceeding node
*/
public abstract int getError(Node precedingNode);
/**
* Expected output value when this node is preceeded by the node specified in the argument.
*
* @param precedingNode that precedes this node
* @return expected output value
*/
public abstract int getOutputValue(Node precedingNode);
}

131
src/edac/trellis/Path.java Normal file
View File

@@ -0,0 +1,131 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package edac.trellis;
import java.util.ArrayList;
import java.util.List;
public class Path
{
private List<Node> mNodes = new ArrayList<>();
private int mError = 0;
/**
* Trellis Coded Modulation (TCM) path. Represents a sequence of nodes that traverse various time instants and
* states across a trellis.
*
* Note: each node can be shared across multiple paths. This class does not assume that any nodes are unique.
* @param startingNode to start the path. This node normally contains an input value of zero, but is implementation
* specific.
*/
public Path(Node startingNode)
{
add(startingNode);
}
/**
* Trellis Coded Modulation (TCM) path. Represents a sequence of nodes that traverse various time instants and
* states across a trellis.
*
* Note: each node can be shared across multiple paths. This class does not assume that any nodes are unique.
*
* @param original path to create a copy from. Each of the nodes in the original will be added to this path and
* after construction both this path and the original will contain the exact same set of nodes.
*/
private Path(Path original)
{
for(Node node: original.getNodes())
{
add(node);
}
}
/**
* Cumulative error across all nodes contained within this path. Error value is updated as each new node is added
* to this path.
*
* @return cumulative error value
*/
public int getError()
{
return mError;
}
/**
* Length or number of nodes contained within this path. Note: this value includes the starting node and may
* also include a flushing node. The values contained in the starting and flushing nodes are normally not used
* when decoding a path.
*
* @return number of nodes in this path
*/
public int getLength()
{
return mNodes.size() - 1;
}
/**
* The set of nodes contained in this path.
* @return nodes
*/
public List<Node> getNodes()
{
return mNodes;
}
/**
* Adds the node to this path and updates the cumulative error value for this path by adding the node's error
* value to the running error total.
*
* @param node to add to this path
*/
public void add(Node node)
{
if(!mNodes.isEmpty())
{
mError += node.getError(getLastNode());
}
mNodes.add(node);
}
/**
* Last node in this path.
*
* @return last node
*/
public Node getLastNode()
{
if(!mNodes.isEmpty())
{
return mNodes.get(mNodes.size() - 1);
}
return null;
}
/**
* Utility method to create an exact copy of this path.
*
* @return a new copy of this path.
*/
public Path copyOf()
{
return new Path(this);
}
}

View File

@@ -0,0 +1,226 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package edac.trellis;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public abstract class ViterbiDecoder
{
private int mInputBitLength;
private int mInputValueCount;
private int mOutputBitLength;
private int mOutputValueCount;
/**
* Viterbi decoder for trellis coded modulation (TCM) encoded binary sequences.
*
* @param inputBitLength for coding (e.g. 3/4 rate input bit length is 3)
* @param outputBitLength for coding (e.g. 3/4 rate output bit length is 4)
*/
public ViterbiDecoder(int inputBitLength, int outputBitLength)
{
mInputBitLength = inputBitLength;
mOutputBitLength = outputBitLength;
mInputValueCount = (int)Math.pow(2.0, mInputBitLength);
mOutputValueCount = (int)Math.pow(2.0, mOutputBitLength);
}
/**
* Creates a node that represents an input value and the actual/transmitted output value.
*
* Note: this method must be implemented by a sub-class since the node requires a specific state transition
* table/matrix implementation.
*
* @param inputValue for the specific trellis node
* @param transmittedOutputValue the actual transmitted output value
* @return node for decoding
*/
protected abstract Node createNode(int inputValue, int transmittedOutputValue);
/**
* Creates a flushing or final node that represents an actual/transmitted output value and a flushing input value.
*
* The final flushing input value is implementation-specific, therefore this will be implemented in any subclass
* of this decoder. A normal flushing input value is zero.
*
* @param transmittedOutputValue the actual transmitted output value
* @return flushing node
*/
protected abstract Node createFlushingNode(int transmittedOutputValue);
/**
* Creates a starting node that represents a starting state and notional transmitted output value.
*
* The starting input value is implementation-specific, therefore this will be implemented in any subclass
* of this decoder. A normal starting input value is zero.
*
* @return starting node
*/
protected abstract Node createStartingNode();
/**
* Size in bits of the input and state values (e.g. 3/4 rate input size is 3)
*
* @return input bit size
*/
public int getInputBitLength()
{
return mInputBitLength;
}
/**
* Count of input or state values where the set of values is in the range of 0 <> count - 1
*
* @return input or state values count
*/
public int getInputValueCount()
{
return mInputValueCount;
}
/**
* Size in bits of the output constellation or transmitted symbol value (e.g. 3/4 rate output size is 4)
*
* @return output size in bits
*/
public int getOutputBitLength()
{
return mOutputBitLength;
}
/**
* Count of input or state values where the set of values is in the range of 0 <> count - 1
*
* @return input or state values count
*/
public int getOutputValueCount()
{
return mOutputValueCount;
}
/**
* Decodes the TCM encoded transmitted output values and returns a path the represents the most likely transmitted
* sequence of nodes.
*
* @param transmittedOutputValues from the encoded message
* @return most likely path representing the transmitted values
*/
public Path decode(int[] transmittedOutputValues)
{
Collection<Path> survivingPaths = new ArrayList<>();
survivingPaths.add(new Path(createStartingNode()));
//Add all but the last transmitted value to the starting path, creating a set of survivors at each iteration.
for(int x = 0; x < transmittedOutputValues.length - 1; x++)
{
survivingPaths = add(survivingPaths, transmittedOutputValues[x]);
}
//Flush the survivors with the final transmitted value and return the lone surviving path
return flush(survivingPaths, transmittedOutputValues[transmittedOutputValues.length - 1]);
}
/**
* Creates a set of new nodes representing all possible input values and the transmitted output value argument
* and iteratively adds the set of nodes to a copy of each of the surviving paths. A set of surviving paths is
* returned that represents each path that terminates at a distinct input/state value that has the least error
* value.
*
* @param survivingPaths from the previous time instant to evaluate for the next time instant in the trellis
* using the transmitted output value.
*
* @param transmittedOutputValue that will be decoded for this time instant.
*
* @return paths that have survived (ie have the lowest error value) after adding nodes for the transmitted
* output value
*
*/
public Collection<Path> add(Collection<Path> survivingPaths, int transmittedOutputValue)
{
Map<Integer,Path> survivingPathMap = new HashMap<>();
for(int x = 0; x < getInputValueCount(); x++)
{
//Create a single/unique node for each possible input value combined with the transmitted output value
Node node = createNode(x, transmittedOutputValue);
//Evaluate each surviving path by creating a copy and adding the node to it and see if it survives
for(Path path: survivingPaths)
{
Path pathToEvaluate = path.copyOf();
pathToEvaluate.add(node);
if(survivingPathMap.containsKey(node.getInputValue()))
{
Path survivingPath = survivingPathMap.get(node.getInputValue());
//Replace the current survivor path if this evaluate path has a lower error value
if(pathToEvaluate.getError() < survivingPath.getError())
{
survivingPathMap.put(node.getInputValue(), pathToEvaluate);
}
}
else
{
//No survivor yet -- set this evaluate path as the first survivor
survivingPathMap.put(node.getInputValue(), pathToEvaluate);
}
}
}
return survivingPathMap.values();
}
/**
* Flushes each of the surviving paths with a final/flushing node using the transmitted symbol. A final flushing
* input value or state is implementation-specific, but normally uses a value of zero. Since all paths will be
* evaluated against each other as terminating at the same state, only the path with the lowest error value will
* be returned from this method.
*
* @param survivingPaths to flush with the final flushing node
* @param transmittedOutputValue to use for the final flushing node
*
* @return the single survivor path that remains after adding the flushing node to each of the candidate argument
* survivor paths.
*/
public Path flush(Collection<Path> survivingPaths, int transmittedOutputValue)
{
Node flushingNode = createFlushingNode(transmittedOutputValue);
Path bestPath = null;
for(Path survivingPath: survivingPaths)
{
survivingPath.add(flushingNode);
if(bestPath == null || (survivingPath.getError() < bestPath.getError()))
{
bestPath = survivingPath;
}
}
return bestPath;
}
}

View File

@@ -0,0 +1,159 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package edac.trellis;
import bits.BinaryMessage;
import bits.CorrectedBinaryMessage;
import java.util.List;
public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder
{
/**
* Viterbi decoder for Digital Mobile Radio (DMR) 3/4 rate Trellis Coded Modulation (TCM) encoded messages.
*/
public ViterbiDecoder_3_4_DMR()
{
super(3,4);
}
/**
* Decodes a 3/4 rate trellis coded modulation (TCM) encoded DMR binary message containing 196 bits that have
* already been deinterleaved.
*
* @param encodedMessage to decode that has already been deinterleaved.
* @return decoded message
*/
public BinaryMessage decode(BinaryMessage encodedMessage)
{
int[] symbols = getSymbols(encodedMessage);
Path mostLikelyPath = decode(symbols);
return getMessage(mostLikelyPath);
}
/**
* Extracts the decoded/corrected message from the surviving path argument.
*
* @param path to extract a message from
* @return corrected binary message.
*/
private static CorrectedBinaryMessage getMessage(Path path)
{
List<Node> mNodes = path.getNodes();
//Each node contains an input value excluding the starting(0) and final flushing(0) nodes.
CorrectedBinaryMessage message = new CorrectedBinaryMessage((mNodes.size() - 2) * 3);
if(mNodes.size() > 2)
{
for(int x = 1; x < mNodes.size() - 1; x++)
{
int inputValue = mNodes.get(x).getInputValue();
int messageOffset = 3 * (x - 1);
if((inputValue & 4) == 4)
{
message.set(messageOffset);
}
if((inputValue & 2) == 2)
{
message.set(messageOffset + 1);
}
if((inputValue & 1) == 1)
{
message.set(messageOffset + 2);
}
}
}
//Transfer the corrected error count to the message
message.setCorrectedBitCount(path.getError());
return message;
}
/**
* Extracts (49) four-bit symbols (196 bits / 4) from the deinterleaved DMR binary message
*
* @param encodedMessage that deinterleaved and contains 196-bits of 3/4 TCM encoding
* @return symbols
*/
public int[] getSymbols(BinaryMessage encodedMessage)
{
//Ensure we have an integral number of transmitted symbols (nibbles) in the message
if(encodedMessage.size() % getOutputBitLength() != 0)
{
throw new IllegalArgumentException("Encoded message must contain an integral number of 4-bit symbols -" +
" message size: " + encodedMessage.size());
}
int[] symbols = new int[encodedMessage.size() / getOutputBitLength()];
int index = 0;
int pointer = 0;
while(index < encodedMessage.size())
{
int symbol = encodedMessage.getInt(index, index + (getOutputBitLength() - 1));
symbols[pointer++] = symbol;
index += getOutputBitLength();
}
return symbols;
}
/**
* Creates a DMR node
* @param inputValue for the specific trellis node
* @param transmittedOutputValue the actual transmitted output value
* @return created node
*/
@Override
protected Node createNode(int inputValue, int transmittedOutputValue)
{
return new DMRNode(inputValue, transmittedOutputValue);
}
/**
* Creates a DMR flushing node. DMR uses an input value of zero to flush the final output value out of the
* encoder.
*
* @param transmittedOutputValue the actual transmitted output value
* @return
*/
@Override
protected Node createFlushingNode(int transmittedOutputValue)
{
return new DMRNode(0, transmittedOutputValue);
}
/**
* Creates a DMR starting node. DMR uses a starting input value of zero for the initial node.
*/
@Override
protected Node createStartingNode()
{
return new DMRNode(0,0);
}
}

View File

@@ -151,41 +151,4 @@ public class RadioReferenceService
return mClient.getUserFeedBroadcasts(mUserAuthorization); return mClient.getUserFeedBroadcasts(mUserAuthorization);
} }
public static void main(String[] args)
{
try
{
RadioReferenceService service = new RadioReferenceService("dsheirer", "1dodgeram");
Long expiration = service.getAccountExpirationDate();
mLog.debug("Expiration Date:" + (expiration != null ? new Date(expiration).toString() : "unknown"));
mLog.debug("Is Active: " + service.isAccountActive());
UserFeedBroadcast[] feeds = service.getUserFeedBroadcasts();
for(UserFeedBroadcast feed: feeds)
{
mLog.debug("Feed ID: " + feed.getFeedId() + " Desc:" + feed.getDescr());
}
}
catch(AxisFault af)
{
mLog.debug("Hey, I caught an axis fault!");
mLog.debug("Reason:" + af.getFaultReason());
}
catch(RemoteException re)
{
re.printStackTrace();
}
catch(Exception e)
{
e.printStackTrace();
}
mLog.debug("Finished!");
}
} }

View File

@@ -24,6 +24,9 @@ import audio.IAudioPacketProvider;
import audio.squelch.ISquelchStateListener; import audio.squelch.ISquelchStateListener;
import audio.squelch.ISquelchStateProvider; import audio.squelch.ISquelchStateProvider;
import audio.squelch.SquelchState; import audio.squelch.SquelchState;
import channel.heartbeat.Heartbeat;
import channel.heartbeat.IHeartbeatListener;
import channel.heartbeat.IHeartbeatProvider;
import channel.metadata.AttributeChangeRequest; import channel.metadata.AttributeChangeRequest;
import channel.metadata.IAttributeChangeRequestListener; import channel.metadata.IAttributeChangeRequestListener;
import channel.metadata.IAttributeChangeRequestProvider; import channel.metadata.IAttributeChangeRequestProvider;
@@ -98,6 +101,7 @@ public class ProcessingChain implements IChannelEventListener
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster<>(); private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster<>();
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>(); private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
private Broadcaster<DecoderStateEvent> mDecoderStateEventBroadcaster = new Broadcaster<>(); private Broadcaster<DecoderStateEvent> mDecoderStateEventBroadcaster = new Broadcaster<>();
private Broadcaster<Heartbeat> mHeartbeatBroadcaster = new Broadcaster<>();
private Broadcaster<SourceEvent> mFrequencyChangeEventBroadcaster = new Broadcaster<>(); private Broadcaster<SourceEvent> mFrequencyChangeEventBroadcaster = new Broadcaster<>();
private Broadcaster<Message> mMessageBroadcaster = new Broadcaster<>(); private Broadcaster<Message> mMessageBroadcaster = new Broadcaster<>();
private Broadcaster<RealBuffer> mFilteredRealBufferBroadcaster = new Broadcaster<>(); private Broadcaster<RealBuffer> mFilteredRealBufferBroadcaster = new Broadcaster<>();
@@ -316,7 +320,12 @@ public class ProcessingChain implements IChannelEventListener
if(module instanceof ISourceEventListener) if(module instanceof ISourceEventListener)
{ {
mFrequencyChangeEventBroadcaster.addListener(((ISourceEventListener) module).getSourceEventListener()); mFrequencyChangeEventBroadcaster.addListener(frequencyChangeEventListener);
}
if(module instanceof IHeartbeatListener)
{
mHeartbeatBroadcaster.addListener(((IHeartbeatListener)module).getHeartbeatListener());
} }
if(module instanceof IMessageListener) if(module instanceof IMessageListener)
@@ -381,6 +390,11 @@ public class ProcessingChain implements IChannelEventListener
mFrequencyChangeEventBroadcaster.removeListener(((ISourceEventListener) module).getSourceEventListener()); mFrequencyChangeEventBroadcaster.removeListener(((ISourceEventListener) module).getSourceEventListener());
} }
if(module instanceof IHeartbeatListener)
{
mHeartbeatBroadcaster.removeListener(((IHeartbeatListener)module).getHeartbeatListener());
}
if(module instanceof IMessageListener) if(module instanceof IMessageListener)
{ {
mMessageBroadcaster.removeListener(((IMessageListener) module).getMessageListener()); mMessageBroadcaster.removeListener(((IMessageListener) module).getMessageListener());
@@ -433,6 +447,16 @@ public class ProcessingChain implements IChannelEventListener
((IDecoderStateEventProvider) module).setDecoderStateListener(mDecoderStateEventBroadcaster); ((IDecoderStateEventProvider) module).setDecoderStateListener(mDecoderStateEventBroadcaster);
} }
if(module instanceof IHeartbeatProvider)
{
((IHeartbeatProvider)module).setHeartbeatListener(mHeartbeatBroadcaster);
}
if(module instanceof IMessageProvider)
{
((IMessageProvider) module).setMessageListener(mMessageBroadcaster);
}
if(module instanceof IFilteredRealBufferProvider) if(module instanceof IFilteredRealBufferProvider)
{ {
((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(mFilteredRealBufferBroadcaster); ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(mFilteredRealBufferBroadcaster);
@@ -490,6 +514,16 @@ public class ProcessingChain implements IChannelEventListener
((IDecoderStateEventProvider) module).setDecoderStateListener(null); ((IDecoderStateEventProvider) module).setDecoderStateListener(null);
} }
if(module instanceof IHeartbeatProvider)
{
((IHeartbeatProvider)module).removeHeartbeatListener();
}
if(module instanceof IMessageProvider)
{
((IMessageProvider) module).setMessageListener(null);
}
if(module instanceof IFilteredRealBufferProvider) if(module instanceof IFilteredRealBufferProvider)
{ {
((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(null); ((IFilteredRealBufferProvider) module).setFilteredRealBufferListener(null);

View File

@@ -421,7 +421,7 @@ public class P25DecoderState extends DecoderState
mCurrentCallEvent = new P25CallEvent.Builder(CallEventType.CALL) mCurrentCallEvent = new P25CallEvent.Builder(CallEventType.CALL)
.aliasList(getAliasList()) .aliasList(getAliasList())
.channel(mCurrentChannel) .channel(mCurrentChannel)
.details((hdu.isEncrypted() ? "ENCRYPTED " : "")) .details((hdu.isEncryptedAudio() ? "ENCRYPTED " : ""))
.frequency(mCurrentChannelFrequency) .frequency(mCurrentChannelFrequency)
.to(to) .to(to)
.build(); .build();
@@ -1107,7 +1107,7 @@ public class P25DecoderState extends DecoderState
broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT) broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT)
.aliasList(getAliasList()) .aliasList(getAliasList())
.channel(gvcu.getChannelA()) .channel(gvcu.getChannelA())
.details((gvcu.isEncrypted() ? "ENCRYPTED" : "")) .details((gvcu.isEncryptedLinkControlWord() ? "ENCRYPTED" : ""))
.frequency(gvcu.getDownlinkFrequencyA()) .frequency(gvcu.getDownlinkFrequencyA())
.to(userA) .to(userA)
.build()); .build());
@@ -1122,7 +1122,7 @@ public class P25DecoderState extends DecoderState
broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT) broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT)
.aliasList(getAliasList()) .aliasList(getAliasList())
.channel(gvcu.getChannelB()) .channel(gvcu.getChannelB())
.details((gvcu.isEncrypted() ? "ENCRYPTED" : "")) .details((gvcu.isEncryptedLinkControlWord() ? "ENCRYPTED" : ""))
.frequency(gvcu.getDownlinkFrequencyB()) .frequency(gvcu.getDownlinkFrequencyB())
.to(userB) .to(userB)
.build()); .build());
@@ -1151,7 +1151,7 @@ public class P25DecoderState extends DecoderState
broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT) broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT)
.aliasList(getAliasList()) .aliasList(getAliasList())
.channel(gvcue.getTransmitChannel()) .channel(gvcue.getTransmitChannel())
.details((gvcue.isEncrypted() ? "ENCRYPTED" : "")) .details((gvcue.isEncryptedLinkControlWord() ? "ENCRYPTED" : ""))
.frequency(gvcue.getDownlinkFrequency()) .frequency(gvcue.getDownlinkFrequency())
.to(group) .to(group)
.build()); .build());
@@ -1187,7 +1187,7 @@ public class P25DecoderState extends DecoderState
if(mCurrentCallEvent.getDetails() == null) if(mCurrentCallEvent.getDetails() == null)
{ {
mCurrentCallEvent.setDetails( mCurrentCallEvent.setDetails(
(gvcuser.isEncrypted() ? "ENCRYPTED " : "") + (gvcuser.isEncryptedLinkControlWord() ? "ENCRYPTED " : "") +
(gvcuser.isEmergency() ? "EMERGENCY " : "")); (gvcuser.isEmergency() ? "EMERGENCY " : ""));
broadcast(mCurrentCallEvent); broadcast(mCurrentCallEvent);
@@ -1399,7 +1399,7 @@ public class P25DecoderState extends DecoderState
if(mCurrentCallEvent.getDetails() == null) if(mCurrentCallEvent.getDetails() == null)
{ {
mCurrentCallEvent.setDetails( mCurrentCallEvent.setDetails(
(tivcu.isEncrypted() ? "ENCRYPTED " : "") + (tivcu.isEncryptedLinkControlWord() ? "ENCRYPTED " : "") +
(tivcu.isEmergency() ? "EMERGENCY " : "")); (tivcu.isEmergency() ? "EMERGENCY " : ""));
broadcast(mCurrentCallEvent); broadcast(mCurrentCallEvent);
@@ -1482,7 +1482,7 @@ public class P25DecoderState extends DecoderState
if(mCurrentCallEvent.getDetails() == null) if(mCurrentCallEvent.getDetails() == null)
{ {
mCurrentCallEvent.setDetails( mCurrentCallEvent.setDetails(
(uuvcu.isEncrypted() ? "ENCRYPTED " : "") + (uuvcu.isEncryptedLinkControlWord() ? "ENCRYPTED " : "") +
(uuvcu.isEmergency() ? "EMERGENCY " : "")); (uuvcu.isEmergency() ? "EMERGENCY " : ""));
broadcast(mCurrentCallEvent); broadcast(mCurrentCallEvent);
@@ -1713,33 +1713,41 @@ public class P25DecoderState extends DecoderState
.build()); .build());
break; break;
case SNDCP_RF_CONFIRMED_DATA: case SNDCP_RF_CONFIRMED_DATA:
SNDCPUserData sud = (SNDCPUserData)pduc; if(pduc instanceof SNDCPUserData)
StringBuilder sbFrom = new StringBuilder();
StringBuilder sbTo = new StringBuilder();
sbFrom.append(sud.getSourceIPAddress());
sbTo.append(sud.getDestinationIPAddress());
if(sud.getIPProtocol() == IPProtocol.UDP)
{ {
sbFrom.append(":"); SNDCPUserData sud = (SNDCPUserData)pduc;
sbFrom.append(sud.getUDPSourcePort());
sbTo.append(":"); StringBuilder sbFrom = new StringBuilder();
sbTo.append(sud.getUDPDestinationPort()); StringBuilder sbTo = new StringBuilder();
sbFrom.append(sud.getSourceIPAddress());
sbTo.append(sud.getDestinationIPAddress());
if(sud.getIPProtocol() == IPProtocol.UDP)
{
sbFrom.append(":");
sbFrom.append(sud.getUDPSourcePort());
sbTo.append(":");
sbTo.append(sud.getUDPDestinationPort());
}
broadcast(new DecoderStateEvent(this, Event.START, State.DATA));
broadcast(new P25CallEvent.Builder(CallEventType.DATA_CALL)
.aliasList(getAliasList())
.channel(mCurrentChannel)
.details("DATA: " + sud.getPayload() +
" RADIO IP:" + sbTo.toString())
.frequency(mCurrentChannelFrequency)
.from(sbFrom.toString())
.to(pduc.getLogicalLinkID())
.build());
}
else
{
mLog.error("Error - expected SNDCPUserData instance but class was: " + pduc.getClass() +
" and PDU Type:" + pduc.getPDUType().name());
} }
broadcast(new DecoderStateEvent(this, Event.START, State.DATA));
broadcast(new P25CallEvent.Builder(CallEventType.DATA_CALL)
.aliasList(getAliasList())
.channel(mCurrentChannel)
.details("DATA: " + sud.getPayload() +
" RADIO IP:" + sbTo.toString())
.frequency(mCurrentChannelFrequency)
.from(sbFrom.toString())
.to(pduc.getLogicalLinkID())
.build());
break; break;
case SNDCP_RF_UNCONFIRMED_DATA: case SNDCP_RF_UNCONFIRMED_DATA:
break; break;

View File

@@ -12,6 +12,9 @@ import jmbe.iface.AudioConverter;
import message.IMessageListener; import message.IMessageListener;
import message.Message; import message.Message;
import module.Module; import module.Module;
import module.decode.p25.message.hdu.HDUMessage;
import module.decode.p25.message.ldu.LDU1Message;
import module.decode.p25.message.ldu.LDU2Message;
import module.decode.p25.message.ldu.LDUMessage; import module.decode.p25.message.ldu.LDUMessage;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -25,14 +28,18 @@ public class P25AudioModule extends Module implements Listener<Message>, IAudioP
private final static Logger mLog = LoggerFactory.getLogger(P25AudioModule.class); private final static Logger mLog = LoggerFactory.getLogger(P25AudioModule.class);
private static final String IMBE_CODEC = "IMBE"; private static final String IMBE_CODEC = "IMBE";
private static boolean mLibraryLoadStatusLogged = false; private static boolean mLibraryLoadStatusLogged = false;
private boolean mCanConvertAudio = false; private boolean mCanConvertAudio = false;
private boolean mEncryptedCall = false;
private boolean mEncryptedCallStateEstablished = false;
private AudioConverter mAudioConverter; private AudioConverter mAudioConverter;
private Listener<AudioPacket> mAudioPacketListener; private Listener<AudioPacket> mAudioPacketListener;
private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f); private NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f);
private Metadata mMetadata; private Metadata mMetadata;
private LDU1Message mCachedLDU1Message = null;
public P25AudioModule(Metadata metadata) public P25AudioModule(Metadata metadata)
{ {
@@ -76,32 +83,73 @@ public class P25AudioModule extends Module implements Listener<Message>, IAudioP
} }
/** /**
* Primary processing method for p25 imbe audio frame messages. Each LDU * Processes call header (HDU) and voice frame (LDU1/LDU2) messages to decode audio and to determine the
* audio message contains 9 imbe voice frames. Each frame is converted to * encrypted audio status of a call event. Only the HDU and LDU2 messages convey encrypted call status. If an
* audio when the JMBE library is loaded, processed by auto-gain, and * LDU1 message is received without a preceding HDU message, then the LDU1 message is cached until the first
* broadcast to the audio packet listener. * LDU2 message is received and the encryption state can be determined. Both the LDU1 and the LDU2 message are
* then processed for audio if the call is unencrypted.
*/ */
public void receive(Message message) public void receive(Message message)
{ {
if (mCanConvertAudio && mAudioPacketListener != null) if(mCanConvertAudio && mAudioPacketListener != null)
{ {
if (message instanceof LDUMessage) if(mEncryptedCallStateEstablished)
{ {
LDUMessage ldu = (LDUMessage) message; if(message instanceof LDUMessage)
/* Only process unencrypted audio frames */
if (!(ldu.isValid() && ldu.isEncrypted()))
{ {
for (byte[] frame : ldu.getIMBEFrames()) processAudio((LDUMessage)message);
{
float[] audio = mAudioConverter.decode(frame);
audio = mGain.apply(audio);
mAudioPacketListener.receive(new AudioPacket(audio, mMetadata.copyOf()));
}
} }
} }
else
{
if(message instanceof HDUMessage)
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = ((HDUMessage)message).isEncryptedAudio();
}
else if(message instanceof LDU1Message)
{
//When we receive an LDU1 message with first receiving the HDU message, cache the LDU1 Message
//until we can determine the encrypted call state from the next LDU2 message
mCachedLDU1Message = (LDU1Message)message;
}
else if(message instanceof LDU2Message)
{
mEncryptedCallStateEstablished = true;
LDU2Message ldu2 = (LDU2Message)message;
mEncryptedCall = ldu2.isEncryptedAudio();
if(mCachedLDU1Message != null)
{
processAudio(mCachedLDU1Message);
mCachedLDU1Message = null;
}
processAudio(ldu2);
}
}
}
}
/**
* Processes an audio packet by decoding the IMBE audio frames and rebroadcasting them as PCM audio packets.
*/
private void processAudio(LDUMessage ldu)
{
if(!mEncryptedCall)
{
for(byte[] frame : ldu.getIMBEFrames())
{
float[] audio = mAudioConverter.decode(frame);
audio = mGain.apply(audio);
mAudioPacketListener.receive(new AudioPacket(audio, mMetadata.copyOf()));
}
}
else
{
//Encrypted audio processing not implemented
} }
} }
@@ -119,18 +167,18 @@ public class P25AudioModule extends Module implements Listener<Message>, IAudioP
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
Class temp = Class.forName("jmbe.JMBEAudioLibrary"); Class temp = Class.forName("jmbe.JMBEAudioLibrary");
library = (AudioConversionLibrary) temp.newInstance(); library = (AudioConversionLibrary)temp.newInstance();
if ((library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 && if((library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 &&
library.getBuildVersion() >= 3) || library.getMajorVersion() >= 1) library.getBuildVersion() >= 3) || library.getMajorVersion() >= 1)
{ {
mAudioConverter = library.getAudioConverter(IMBE_CODEC, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO); mAudioConverter = library.getAudioConverter(IMBE_CODEC, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO);
if (mAudioConverter != null) if(mAudioConverter != null)
{ {
mCanConvertAudio = true; mCanConvertAudio = true;
if (!mLibraryLoadStatusLogged) if(!mLibraryLoadStatusLogged)
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("JMBE audio conversion library ["); sb.append("JMBE audio conversion library [");
@@ -144,7 +192,7 @@ public class P25AudioModule extends Module implements Listener<Message>, IAudioP
} }
else else
{ {
if (!mLibraryLoadStatusLogged) if(!mLibraryLoadStatusLogged)
{ {
mLog.info("JMBE audio conversion library NOT FOUND"); mLog.info("JMBE audio conversion library NOT FOUND");
mLibraryLoadStatusLogged = true; mLibraryLoadStatusLogged = true;
@@ -156,28 +204,28 @@ public class P25AudioModule extends Module implements Listener<Message>, IAudioP
mLog.warn("JMBE library version 0.3.3 or higher is required - found: " + library.getVersion()); mLog.warn("JMBE library version 0.3.3 or higher is required - found: " + library.getVersion());
} }
} }
catch (ClassNotFoundException e1) catch(ClassNotFoundException e1)
{ {
if (!mLibraryLoadStatusLogged) if(!mLibraryLoadStatusLogged)
{ {
mLog.error("Couldn't find/load JMBE audio conversion library"); mLog.error("Couldn't find/load JMBE audio conversion library");
mLibraryLoadStatusLogged = true; mLibraryLoadStatusLogged = true;
} }
} }
catch (InstantiationException e1) catch(InstantiationException e1)
{ {
if (!mLibraryLoadStatusLogged) if(!mLibraryLoadStatusLogged)
{ {
mLog.error("Couldn't instantiate JMBE audio conversion library class"); mLog.error("Couldn't instantiate JMBE audio conversion library class");
mLibraryLoadStatusLogged = true; mLibraryLoadStatusLogged = true;
} }
} }
catch (IllegalAccessException e1) catch(IllegalAccessException e1)
{ {
if (!mLibraryLoadStatusLogged) if(!mLibraryLoadStatusLogged)
{ {
mLog.error("Couldn't load JMBE audio conversion library due to " mLog.error("Couldn't load JMBE audio conversion library due to "
+ "security restrictions"); + "security restrictions");
mLibraryLoadStatusLogged = true; mLibraryLoadStatusLogged = true;
} }
} }
@@ -196,19 +244,25 @@ public class P25AudioModule extends Module implements Listener<Message>, IAudioP
} }
/** /**
* Wrapper for squelch state listener. Internally, the P25 audio module * Wrapper for squelch state to process end of call actions. At call end the encrypted call state established
* doesn't have a squelch state. If there are IMBE audio frames, we have * flag is reset so that the encrypted audio state for the next call can be properly detected and we send an
* audio. We use this listener to signal to the recorder manager that the * END audio packet so that downstream processors like the audio recorder can properly close out a call sequence.
* channel state is resetting and it should end the recording.
*/ */
public class SquelchStateListener implements Listener<SquelchState> public class SquelchStateListener implements Listener<SquelchState>
{ {
@Override @Override
public void receive(SquelchState state) public void receive(SquelchState state)
{ {
if (state == SquelchState.SQUELCH && mAudioPacketListener != null) if(state == SquelchState.SQUELCH)
{ {
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf())); if(mAudioPacketListener != null)
{
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
}
mEncryptedCallStateEstablished = false;
mEncryptedCall = false;
mCachedLDU1Message = null;
} }
} }
} }

View File

@@ -245,7 +245,7 @@ public class HDUMessage extends P25Message
return Encryption.fromValue( mMessage.getInt( ALGORITHM_ID ) ); return Encryption.fromValue( mMessage.getInt( ALGORITHM_ID ) );
} }
public boolean isEncrypted() public boolean isEncryptedAudio()
{ {
return getEncryption() != Encryption.UNENCRYPTED; return getEncryption() != Encryption.UNENCRYPTED;
} }

View File

@@ -1,244 +1,246 @@
package module.decode.p25.message.ldu; package module.decode.p25.message.ldu;
import module.decode.p25.reference.DataUnitID;
import module.decode.p25.reference.LinkControlOpcode;
import module.decode.p25.reference.Vendor;
import module.decode.p25.reference.VendorLinkControlOpcode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import alias.AliasList; import alias.AliasList;
import bits.BinaryMessage; import bits.BinaryMessage;
import edac.CRC; import edac.CRC;
import edac.Hamming10; import edac.Hamming10;
import edac.ReedSolomon_63_47_17; import edac.ReedSolomon_63_47_17;
import module.decode.p25.reference.DataUnitID;
import module.decode.p25.reference.LinkControlOpcode;
import module.decode.p25.reference.Vendor;
import module.decode.p25.reference.VendorLinkControlOpcode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LDU1Message extends LDUMessage public class LDU1Message extends LDUMessage
{ {
private final static Logger mLog = LoggerFactory.getLogger( LDU1Message.class ); private final static Logger mLog = LoggerFactory.getLogger(LDU1Message.class);
public static final int ENCRYPTION_FLAG = 352; public static final int ENCRYPTED_LINK_CONTROL_WORD_FLAG = 352;
public static final int IMPLICIT_VENDOR_FLAG = 353; public static final int IMPLICIT_VENDOR_FLAG = 353;
public static final int[] OPCODE = { 354,355,356,357,362,363 }; public static final int[] OPCODE = {354, 355, 356, 357, 362, 363};
public static final int[] VENDOR = { 364,365,366,367,372,373,374,375 }; public static final int[] VENDOR = {364, 365, 366, 367, 372, 373, 374, 375};
public static final int[] GOLAY_WORD_STARTS = { public static final int[] GOLAY_WORD_STARTS = {
352,362,372,382,536,546,556,566,720,730,740,750,904,914,924,934,1088, 352, 362, 372, 382, 536, 546, 556, 566, 720, 730, 740, 750, 904, 914, 924, 934, 1088,
1098,1108,1118,1272,1282,1292,1302 }; 1098, 1108, 1118, 1272, 1282, 1292, 1302};
public static final int[] CW_HEX_0 = { 352,353,354,355,356,357 }; public static final int[] CW_HEX_0 = {352, 353, 354, 355, 356, 357};
public static final int[] CW_HEX_1 = { 362,363,364,365,366,367 }; public static final int[] CW_HEX_1 = {362, 363, 364, 365, 366, 367};
public static final int[] CW_HEX_2 = { 372,373,374,375,376,377 }; public static final int[] CW_HEX_2 = {372, 373, 374, 375, 376, 377};
public static final int[] CW_HEX_3 = { 382,383,384,385,386,387 }; public static final int[] CW_HEX_3 = {382, 383, 384, 385, 386, 387};
public static final int[] CW_HEX_4 = { 536,537,538,539,540,541 }; public static final int[] CW_HEX_4 = {536, 537, 538, 539, 540, 541};
public static final int[] CW_HEX_5 = { 546,547,548,549,550,551 }; public static final int[] CW_HEX_5 = {546, 547, 548, 549, 550, 551};
public static final int[] CW_HEX_6 = { 556,557,558,559,560,561 }; public static final int[] CW_HEX_6 = {556, 557, 558, 559, 560, 561};
public static final int[] CW_HEX_7 = { 566,567,568,569,570,571 }; public static final int[] CW_HEX_7 = {566, 567, 568, 569, 570, 571};
public static final int[] CW_HEX_8 = { 720,721,722,723,724,725 }; public static final int[] CW_HEX_8 = {720, 721, 722, 723, 724, 725};
public static final int[] CW_HEX_9 = { 730,731,732,733,734,735 }; public static final int[] CW_HEX_9 = {730, 731, 732, 733, 734, 735};
public static final int[] CW_HEX_10 = { 740,741,742,743,744,745 }; public static final int[] CW_HEX_10 = {740, 741, 742, 743, 744, 745};
public static final int[] CW_HEX_11 = { 750,751,752,753,754,755 }; public static final int[] CW_HEX_11 = {750, 751, 752, 753, 754, 755};
public static final int[] RS_HEX_0 = { 904,905,906,907,908,909 }; public static final int[] RS_HEX_0 = {904, 905, 906, 907, 908, 909};
public static final int[] RS_HEX_1 = { 914,915,916,917,918,919 }; public static final int[] RS_HEX_1 = {914, 915, 916, 917, 918, 919};
public static final int[] RS_HEX_2 = { 924,925,926,927,928,929 }; public static final int[] RS_HEX_2 = {924, 925, 926, 927, 928, 929};
public static final int[] RS_HEX_3 = { 934,935,936,937,938,939 }; public static final int[] RS_HEX_3 = {934, 935, 936, 937, 938, 939};
public static final int[] RS_HEX_4 = { 1088,1089,1090,1091,1092,1093 }; public static final int[] RS_HEX_4 = {1088, 1089, 1090, 1091, 1092, 1093};
public static final int[] RS_HEX_5 = { 1098,1099,1100,1101,1102,1103 }; public static final int[] RS_HEX_5 = {1098, 1099, 1100, 1101, 1102, 1103};
public static final int[] RS_HEX_6 = { 1108,1109,1110,1111,1112,1113 }; public static final int[] RS_HEX_6 = {1108, 1109, 1110, 1111, 1112, 1113};
public static final int[] RS_HEX_7 = { 1118,1119,1120,1121,1122,1123 }; public static final int[] RS_HEX_7 = {1118, 1119, 1120, 1121, 1122, 1123};
public static final int[] RS_HEX_8 = { 1272,1273,1274,1275,1276,1277 }; public static final int[] RS_HEX_8 = {1272, 1273, 1274, 1275, 1276, 1277};
public static final int[] RS_HEX_9 = { 1282,1283,1284,1285,1286,1287 }; public static final int[] RS_HEX_9 = {1282, 1283, 1284, 1285, 1286, 1287};
public static final int[] RS_HEX_10 = { 1292,1293,1294,1295,1296,1297 }; public static final int[] RS_HEX_10 = {1292, 1293, 1294, 1295, 1296, 1297};
public static final int[] RS_HEX_11 = { 1302,1303,1304,1305,1306,1307 }; public static final int[] RS_HEX_11 = {1302, 1303, 1304, 1305, 1306, 1307};
/* Reed-Solomon(24,12,13) code protects the link control word. Maximum /* Reed-Solomon(24,12,13) code protects the link control word. Maximum
* correctable errors are: Hamming Distance(13) / 2 = 6 */ * correctable errors are: Hamming Distance(13) / 2 = 6 */
public static final ReedSolomon_63_47_17 mReedSolomonDecoder = public static final ReedSolomon_63_47_17 mReedSolomonDecoder =
new ReedSolomon_63_47_17( 6 ); new ReedSolomon_63_47_17(6);
public LDU1Message( BinaryMessage message, DataUnitID duid, public LDU1Message(BinaryMessage message, DataUnitID duid,
AliasList aliasList ) AliasList aliasList)
{ {
super( message, duid, aliasList ); super(message, duid, aliasList);
/* NID CRC is checked in the message framer, thus a constructed message /* NID CRC is checked in the message framer, thus a constructed message
* means it passed the CRC */ * means it passed the CRC */
mCRC = new CRC[ 3 ]; mCRC = new CRC[3];
mCRC[ 0 ] = CRC.PASSED; mCRC[0] = CRC.PASSED;
checkCRC(); checkCRC();
} }
/** /**
* Subclass constructor from an existing LDU1Message * Subclass constructor from an existing LDU1Message
*/ */
protected LDU1Message( LDU1Message message ) protected LDU1Message(LDU1Message message)
{ {
super( message.getSourceMessage(), DataUnitID.LDU1, message.getAliasList() ); super(message.getSourceMessage(), DataUnitID.LDU1, message.getAliasList());
mCRC = message.mCRC; mCRC = message.mCRC;
} }
@Override /**
public boolean isEncrypted() * Indicates if the contents of the Link Control Word are encrypted.
{ *
return mMessage.get( ENCRYPTION_FLAG ); * @return true if the contents of the LCW are encrypted.
} */
public boolean isEncryptedLinkControlWord()
{
return mMessage.get(ENCRYPTED_LINK_CONTROL_WORD_FLAG);
}
private void checkCRC() private void checkCRC()
{ {
mCRC[ 1 ] = CRC.PASSED; mCRC[1] = CRC.PASSED;
/* Hamming( 10,6,3 ) error detection and correction */ /* Hamming( 10,6,3 ) error detection and correction */
for( int index: GOLAY_WORD_STARTS ) for(int index : GOLAY_WORD_STARTS)
{ {
int errors = Hamming10.checkAndCorrect( mMessage, index ); int errors = Hamming10.checkAndCorrect(mMessage, index);
if( errors > 1 ) if(errors > 1)
{ {
mCRC[ 1 ] = CRC.FAILED_CRC; mCRC[1] = CRC.FAILED_CRC;
} }
} }
/* Reed-Solomon( 24,16,9 ) error detection and correction /* Reed-Solomon( 24,16,9 ) error detection and correction
* Check the Reed-Solomon parity bits. The RS decoder expects the code * Check the Reed-Solomon parity bits. The RS decoder expects the code
* words and reed solomon parity hex codewords in reverse order. * words and reed solomon parity hex codewords in reverse order.
* *
* Since this is a truncated RS(63) codes, we pad the code with zeros */ * Since this is a truncated RS(63) codes, we pad the code with zeros */
int[] input = new int [63]; int[] input = new int[63];
int[] output = new int [63]; int[] output = new int[63];
input[ 0 ] = mMessage.getInt( RS_HEX_11 ); input[0] = mMessage.getInt(RS_HEX_11);
input[ 1 ] = mMessage.getInt( RS_HEX_10 ); input[1] = mMessage.getInt(RS_HEX_10);
input[ 2 ] = mMessage.getInt( RS_HEX_9 ); input[2] = mMessage.getInt(RS_HEX_9);
input[ 3 ] = mMessage.getInt( RS_HEX_8 ); input[3] = mMessage.getInt(RS_HEX_8);
input[ 4 ] = mMessage.getInt( RS_HEX_7 ); input[4] = mMessage.getInt(RS_HEX_7);
input[ 5 ] = mMessage.getInt( RS_HEX_6 ); input[5] = mMessage.getInt(RS_HEX_6);
input[ 6 ] = mMessage.getInt( RS_HEX_5 ); input[6] = mMessage.getInt(RS_HEX_5);
input[ 7 ] = mMessage.getInt( RS_HEX_4 ); input[7] = mMessage.getInt(RS_HEX_4);
input[ 8 ] = mMessage.getInt( RS_HEX_3 ); input[8] = mMessage.getInt(RS_HEX_3);
input[ 9 ] = mMessage.getInt( RS_HEX_2 ); input[9] = mMessage.getInt(RS_HEX_2);
input[ 10 ] = mMessage.getInt( RS_HEX_1 ); input[10] = mMessage.getInt(RS_HEX_1);
input[ 11 ] = mMessage.getInt( RS_HEX_0 ); input[11] = mMessage.getInt(RS_HEX_0);
input[ 12 ] = mMessage.getInt( CW_HEX_11 ); input[12] = mMessage.getInt(CW_HEX_11);
input[ 13 ] = mMessage.getInt( CW_HEX_10 ); input[13] = mMessage.getInt(CW_HEX_10);
input[ 14 ] = mMessage.getInt( CW_HEX_9 ); input[14] = mMessage.getInt(CW_HEX_9);
input[ 15 ] = mMessage.getInt( CW_HEX_8 ); input[15] = mMessage.getInt(CW_HEX_8);
input[ 16 ] = mMessage.getInt( CW_HEX_7 ); input[16] = mMessage.getInt(CW_HEX_7);
input[ 17 ] = mMessage.getInt( CW_HEX_6 ); input[17] = mMessage.getInt(CW_HEX_6);
input[ 18 ] = mMessage.getInt( CW_HEX_5 ); input[18] = mMessage.getInt(CW_HEX_5);
input[ 19 ] = mMessage.getInt( CW_HEX_4 ); input[19] = mMessage.getInt(CW_HEX_4);
input[ 20 ] = mMessage.getInt( CW_HEX_3 ); input[20] = mMessage.getInt(CW_HEX_3);
input[ 21 ] = mMessage.getInt( CW_HEX_2 ); input[21] = mMessage.getInt(CW_HEX_2);
input[ 22 ] = mMessage.getInt( CW_HEX_1 ); input[22] = mMessage.getInt(CW_HEX_1);
input[ 23 ] = mMessage.getInt( CW_HEX_0 ); input[23] = mMessage.getInt(CW_HEX_0);
/* indexes 24 - 62 are defaulted to zero */ /* indexes 24 - 62 are defaulted to zero */
boolean irrecoverableErrors = mReedSolomonDecoder.decode( input, output ); boolean irrecoverableErrors = mReedSolomonDecoder.decode(input, output);
if( irrecoverableErrors ) if(irrecoverableErrors)
{ {
mCRC[ 2 ] = CRC.FAILED_CRC; mCRC[2] = CRC.FAILED_CRC;
} }
else else
{ {
/* Since we've detected that we can correct the RS words, mark the /* Since we've detected that we can correct the RS words, mark the
* hamming crc as corrected as well, if it is currently failed */ * hamming crc as corrected as well, if it is currently failed */
if( mCRC[ 1 ] == CRC.FAILED_CRC ) if(mCRC[1] == CRC.FAILED_CRC)
{ {
mCRC[ 1 ] = CRC.CORRECTED; mCRC[1] = CRC.CORRECTED;
} }
mCRC[ 2 ] = CRC.PASSED; mCRC[2] = CRC.PASSED;
/* Only fix the codeword data hex codewords */ /* Only fix the codeword data hex codewords */
repairHexCodeword( input, output, 12, CW_HEX_11 ); repairHexCodeword(input, output, 12, CW_HEX_11);
repairHexCodeword( input, output, 13, CW_HEX_10 ); repairHexCodeword(input, output, 13, CW_HEX_10);
repairHexCodeword( input, output, 14, CW_HEX_9 ); repairHexCodeword(input, output, 14, CW_HEX_9);
repairHexCodeword( input, output, 15, CW_HEX_8 ); repairHexCodeword(input, output, 15, CW_HEX_8);
repairHexCodeword( input, output, 16, CW_HEX_7 ); repairHexCodeword(input, output, 16, CW_HEX_7);
repairHexCodeword( input, output, 17, CW_HEX_6 ); repairHexCodeword(input, output, 17, CW_HEX_6);
repairHexCodeword( input, output, 18, CW_HEX_5 ); repairHexCodeword(input, output, 18, CW_HEX_5);
repairHexCodeword( input, output, 19, CW_HEX_4 ); repairHexCodeword(input, output, 19, CW_HEX_4);
repairHexCodeword( input, output, 20, CW_HEX_3 ); repairHexCodeword(input, output, 20, CW_HEX_3);
repairHexCodeword( input, output, 21, CW_HEX_2 ); repairHexCodeword(input, output, 21, CW_HEX_2);
repairHexCodeword( input, output, 22, CW_HEX_1 ); repairHexCodeword(input, output, 22, CW_HEX_1);
repairHexCodeword( input, output, 23, CW_HEX_0 ); repairHexCodeword(input, output, 23, CW_HEX_0);
} }
} }
private void repairHexCodeword( int[] input, int[] output, int index, int[] indexSet ) private void repairHexCodeword(int[] input, int[] output, int index, int[] indexSet)
{ {
if( input[ index ] != output[ index ] ) if(input[index] != output[index])
{ {
mMessage.load( indexSet[ 0 ], 6, output[ index ] ); mMessage.load(indexSet[0], 6, output[index]);
mCRC[ 2 ] = CRC.CORRECTED; mCRC[2] = CRC.CORRECTED;
} }
} }
@Override @Override
public String getMessage() public String getMessage()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append( getMessageStub() ); sb.append(getMessageStub());
sb.append( " " ); sb.append(" ");
sb.append( mMessage.toString() ); sb.append(mMessage.toString());
return sb.toString(); return sb.toString();
} }
public String getMessageStub() public String getMessageStub()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append( super.getMessageStub() ); sb.append(super.getMessageStub());
if( isEncrypted() ) if(isEncryptedLinkControlWord())
{ {
sb.append( "ENCRYPTED" ); sb.append("ENCRYPTED");
} }
else else
{ {
if( isImplicitFormat() || getVendor() == Vendor.STANDARD ) if(isImplicitFormat() || getVendor() == Vendor.STANDARD)
{ {
sb.append( getOpcode().getLabel() ); sb.append(getOpcode().getLabel());
} }
else else
{ {
sb.append( "VENDOR:" + getVendor().getLabel() + " " ); sb.append("VENDOR:" + getVendor().getLabel() + " ");
sb.append( getVendorOpcode().getLabel() ); sb.append(getVendorOpcode().getLabel());
} }
} }
return sb.toString(); return sb.toString();
} }
public boolean isImplicitFormat() public boolean isImplicitFormat()
{ {
return mMessage.get( IMPLICIT_VENDOR_FLAG ); return mMessage.get(IMPLICIT_VENDOR_FLAG);
} }
public LinkControlOpcode getOpcode() public LinkControlOpcode getOpcode()
{ {
return LinkControlOpcode.fromValue( mMessage.getInt( OPCODE ) ); return LinkControlOpcode.fromValue(mMessage.getInt(OPCODE));
} }
public VendorLinkControlOpcode getVendorOpcode() public VendorLinkControlOpcode getVendorOpcode()
{ {
return VendorLinkControlOpcode.fromValue( mMessage.getInt( OPCODE ) ); return VendorLinkControlOpcode.fromValue(mMessage.getInt(OPCODE));
} }
public Vendor getVendor() public Vendor getVendor()
{ {
if( isImplicitFormat() ) if(isImplicitFormat())
{ {
return Vendor.STANDARD; return Vendor.STANDARD;
} }
return Vendor.fromValue( mMessage.getInt( VENDOR ) ); return Vendor.fromValue(mMessage.getInt(VENDOR));
} }
} }

View File

@@ -73,8 +73,10 @@ public class LDU2Message extends LDUMessage
checkCRC(); checkCRC();
} }
@Override /**
public boolean isEncrypted() * Indicates if the audio stream is encrypted
*/
public boolean isEncryptedAudio()
{ {
Encryption encryption = getEncryption(); Encryption encryption = getEncryption();

View File

@@ -35,8 +35,6 @@ public abstract class LDUMessage extends P25Message
super( message, duid, aliasList ); super( message, duid, aliasList );
} }
public abstract boolean isEncrypted();
@Override @Override
public String getMessage() public String getMessage()
{ {

View File

@@ -124,10 +124,6 @@ public class OverflowableTransferQueue<E>
mQueue.clear(); mQueue.clear();
mCounter.set(0); mCounter.set(0);
mOverflow.set(false); mOverflow.set(false);
if(mOverflowListener != null)
{
mOverflowListener.sourceOverflow(false);
}
} }
} }
} }

View File

@@ -18,6 +18,7 @@
******************************************************************************/ ******************************************************************************/
package source; package source;
import channel.heartbeat.IHeartbeatProvider;
import module.Module; import module.Module;
import sample.SampleType; import sample.SampleType;
import sample.real.IOverflowListener; import sample.real.IOverflowListener;
@@ -25,7 +26,7 @@ import sample.real.IOverflowListener;
/** /**
* Abstract class to define the minimum functionality of a sample data provider. * Abstract class to define the minimum functionality of a sample data provider.
*/ */
public abstract class Source extends Module implements ISourceEventListener, ISourceEventProvider public abstract class Source extends Module implements ISourceEventListener, ISourceEventProvider, IHeartbeatProvider
{ {
protected IOverflowListener mOverflowListener; protected IOverflowListener mOverflowListener;

View File

@@ -17,64 +17,60 @@
******************************************************************************/ ******************************************************************************/
package source.mixer; package source.mixer;
import java.util.Iterator; import channel.heartbeat.Heartbeat;
import java.util.concurrent.CopyOnWriteArrayList; import dsp.gain.AutomaticGainControl;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.TargetDataLine;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sample.Listener; import sample.Listener;
import sample.adapter.ISampleAdapter; import sample.adapter.ISampleAdapter;
import sample.real.RealBuffer; import sample.real.RealBuffer;
import source.RealSource; import source.RealSource;
import source.SourceException; import source.SourceException;
import dsp.gain.AutomaticGainControl;
import source.SourceEvent; import source.SourceEvent;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.TargetDataLine;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
public class RealMixerSource extends RealSource public class RealMixerSource extends RealSource
{ {
private final static Logger mLog = LoggerFactory.getLogger( RealMixerSource.class ); private final static Logger mLog = LoggerFactory.getLogger(RealMixerSource.class);
private long mFrequency = 0; private long mFrequency = 0;
private int mBufferSize = 16384; private int mBufferSize = 16384;
private BufferReader mBufferReader = new BufferReader(); private BufferReader mBufferReader = new BufferReader();
private TargetDataLine mTargetDataLine; private TargetDataLine mTargetDataLine;
private AudioFormat mAudioFormat; private AudioFormat mAudioFormat;
private int mBytesPerFrame = 0; private int mBytesPerFrame = 0;
private ISampleAdapter mSampleAdapter; private ISampleAdapter mSampleAdapter;
private AutomaticGainControl mAGC = new AutomaticGainControl(); private Listener<Heartbeat> mHeartbeatListener;
private static final Heartbeat HEARTBEAT = new Heartbeat();
CopyOnWriteArrayList<Listener<RealBuffer>> mSampleListeners = new CopyOnWriteArrayList<Listener<RealBuffer>>(); private CopyOnWriteArrayList<Listener<RealBuffer>> mSampleListeners = new CopyOnWriteArrayList<Listener<RealBuffer>>();
/** /**
* Real Mixer Source - constructs a reader on the mixer/sound card target * Real Mixer Source - constructs a reader on the mixer/sound card target
* data line using the specified audio format (sample size, sample rate ) * data line using the specified audio format (sample size, sample rate )
* and broadcasts real buffers (float arrays) to all registered listeners. * and broadcasts real buffers (float arrays) to all registered listeners.
* Reads buffers sized to 10% of the sample rate specified in audio format. * Reads buffers sized to 10% of the sample rate specified in audio format.
* *
* @param targetDataLine - mixer or sound card to be used * @param targetDataLine - mixer or sound card to be used
* * @param format - audio format
* @param format - audio format * @param sampleAdapter - adapter to convert byte array data read from the
* * mixer into float array data. Can optionally invert the channel data if
* @param name - token name to use for this source * the left/right stereo channels are inverted.
* */
* @param sampleAdapter - adapter to convert byte array data read from the public RealMixerSource(TargetDataLine targetDataLine,
* mixer into float array data. Can optionally invert the channel data if AudioFormat format,
* the left/right stereo channels are inverted. ISampleAdapter sampleAdapter)
*/
public RealMixerSource( TargetDataLine targetDataLine,
AudioFormat format,
ISampleAdapter sampleAdapter )
{ {
mTargetDataLine = targetDataLine; mTargetDataLine = targetDataLine;
mAudioFormat = format; mAudioFormat = format;
mSampleAdapter = sampleAdapter; mSampleAdapter = sampleAdapter;
} }
@@ -98,77 +94,97 @@ public class RealMixerSource extends RealSource
return null; return null;
} }
@Override /**
public void reset() * Registers the listener to receive heartbeats from this source.
{ *
} * @param listener to receive heartbeats
*/
@Override @Override
public void start( ScheduledExecutorService executor ) public void setHeartbeatListener(Listener<Heartbeat> listener)
{
}
@Override
public void stop()
{
}
public void setListener( Listener<RealBuffer> listener )
{ {
mSampleListeners.add( listener ); mHeartbeatListener = listener;
}
/**
* Removes the currently registered heartbeat listener
*/
@Override
public void removeHeartbeatListener()
{
mHeartbeatListener = null;
}
@Override
public void reset()
{
}
@Override
public void start(ScheduledExecutorService executor)
{
}
@Override
public void stop()
{
}
public void setListener(Listener<RealBuffer> listener)
{
mSampleListeners.add(listener);
/* If this is the first listener, start the reader thread */ /* If this is the first listener, start the reader thread */
if( !mBufferReader.isRunning() ) if(!mBufferReader.isRunning())
{ {
Thread thread = new Thread( mBufferReader ); Thread thread = new Thread(mBufferReader);
thread.setDaemon( true ); thread.setDaemon(true);
thread.setName( "Sample Reader" ); thread.setName("Sample Reader");
thread.start(); thread.start();
} }
} }
public void removeListener( Listener<RealBuffer> listener ) public void removeListener(Listener<RealBuffer> listener)
{ {
mSampleListeners.remove( listener ); mSampleListeners.remove(listener);
/* If this is the laster listener, stop the reader thread */ /* If this is the laster listener, stop the reader thread */
if( mSampleListeners.isEmpty() ) if(mSampleListeners.isEmpty())
{ {
mBufferReader.stop(); mBufferReader.stop();
} }
} }
public void broadcast( RealBuffer samples ) public void broadcast(RealBuffer samples)
{ {
Iterator<Listener<RealBuffer>> it = mSampleListeners.iterator(); Iterator<Listener<RealBuffer>> it = mSampleListeners.iterator();
while( it.hasNext() ) while(it.hasNext())
{ {
Listener<RealBuffer> next = it.next(); Listener<RealBuffer> next = it.next();
/* if this is the last (or only) listener, send him the original /* if this is the last (or only) listener, send him the original
* buffer, otherwise send him a copy of the buffer */ * buffer, otherwise send him a copy of the buffer */
if( it.hasNext() ) if(it.hasNext())
{ {
next.receive( samples.copyOf() ); next.receive(samples.copyOf());
} }
else else
{ {
next.receive( samples ); next.receive(samples);
} }
} }
} }
public int getSampleRate() public int getSampleRate()
{ {
if( mTargetDataLine != null ) if(mTargetDataLine != null)
{ {
return (int)mTargetDataLine.getFormat().getSampleRate(); return (int)mTargetDataLine.getFormat().getSampleRate();
} }
else else
{ {
return 0; return 0;
} }
} }
/** /**
@@ -176,7 +192,7 @@ public class RealMixerSource extends RealSource
*/ */
public long getFrequency() throws SourceException public long getFrequency() throws SourceException
{ {
return mFrequency; return mFrequency;
} }
/** /**
@@ -184,122 +200,128 @@ public class RealMixerSource extends RealSource
* be useful if you are streaming an external audio source in through the * be useful if you are streaming an external audio source in through the
* sound card and you want to specify a frequency for that source * sound card and you want to specify a frequency for that source
*/ */
public void setFrequency( long frequency ) public void setFrequency(long frequency)
{ {
mFrequency = frequency; mFrequency = frequency;
} }
/** /**
* Reader thread. Performs blocking read against the mixer target data * Reader thread. Performs blocking read against the mixer target data
* line, converts the samples to an array of floats using the specified * line, converts the samples to an array of floats using the specified
* adapter. Dispatches float arrays to all registered listeners. * adapter. Dispatches float arrays to all registered listeners.
*/ */
public class BufferReader implements Runnable public class BufferReader implements Runnable
{ {
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
@Override @Override
public void run() public void run()
{ {
if( mRunning.compareAndSet( false, true ) ) if(mRunning.compareAndSet(false, true))
{ {
if( mTargetDataLine == null ) //Send a heartbeat every time we run
{ if(mHeartbeatListener != null)
mRunning.set( false ); {
mHeartbeatListener.receive(HEARTBEAT);
}
mLog.error( "ComplexMixerSource - mixer target data line" if(mTargetDataLine == null)
+ " is null" ); {
} mRunning.set(false);
else
{
mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8;
if( mBytesPerFrame == AudioSystem.NOT_SPECIFIED ) mLog.error("ComplexMixerSource - mixer target data line"
{ + " is null");
mBytesPerFrame = 2; }
} else
{
mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8;
if(mBytesPerFrame == AudioSystem.NOT_SPECIFIED)
{
mBytesPerFrame = 2;
}
/* Set buffer size to 1/10 second of samples */ /* Set buffer size to 1/10 second of samples */
mBufferSize = (int)( mAudioFormat.getSampleRate() mBufferSize = (int)(mAudioFormat.getSampleRate()
* 0.05 ) * mBytesPerFrame; * 0.05) * mBytesPerFrame;
/* We'll reuse the same buffer for each read */ /* We'll reuse the same buffer for each read */
byte[] buffer = new byte[ mBufferSize ]; byte[] buffer = new byte[mBufferSize];
try try
{ {
mTargetDataLine.open( mAudioFormat ); mTargetDataLine.open(mAudioFormat);
mTargetDataLine.start(); mTargetDataLine.start();
} }
catch ( LineUnavailableException e ) catch(LineUnavailableException e)
{ {
mLog.error( "ComplexMixerSource - mixer target data line" mLog.error("ComplexMixerSource - mixer target data line"
+ "not available to read data from", e ); + "not available to read data from", e);
mRunning.set( false ); mRunning.set(false);
} }
while( mRunning.get() ) while(mRunning.get())
{ {
try try
{ {
/* Blocking read - waits until the buffer fills */ /* Blocking read - waits until the buffer fills */
mTargetDataLine.read( buffer, 0, buffer.length ); mTargetDataLine.read(buffer, 0, buffer.length);
/* Convert samples to float array */ /* Convert samples to float array */
float[] samples = float[] samples =
mSampleAdapter.convert( buffer ); mSampleAdapter.convert(buffer);
/* Dispatch samples to registered listeners */ /* Dispatch samples to registered listeners */
broadcast( new RealBuffer( samples ) ); broadcast(new RealBuffer(samples));
} }
catch ( Exception e ) catch(Exception e)
{ {
mLog.error( "ComplexMixerSource - error while reading" mLog.error("ComplexMixerSource - error while reading"
+ "from the mixer target data line", e ); + "from the mixer target data line", e);
mRunning.set( false ); mRunning.set(false);
} }
} }
} }
/* Close the data line if it is still open */ /* Close the data line if it is still open */
if( mTargetDataLine != null && mTargetDataLine.isOpen() ) if(mTargetDataLine != null && mTargetDataLine.isOpen())
{ {
mTargetDataLine.close(); mTargetDataLine.close();
} }
} }
} }
/** /**
* Stops the reader thread * Stops the reader thread
*/ */
public void stop() public void stop()
{ {
if( mRunning.compareAndSet( true, false ) ) if(mRunning.compareAndSet(true, false))
{ {
mTargetDataLine.stop(); mTargetDataLine.stop();
mTargetDataLine.close(); mTargetDataLine.close();
} }
} }
/** /**
* Indicates if the reader thread is running * Indicates if the reader thread is running
*/ */
public boolean isRunning() public boolean isRunning()
{ {
return mRunning.get(); return mRunning.get();
} }
} }
@Override @Override
public void dispose() public void dispose()
{ {
if( mBufferReader != null ) if(mBufferReader != null)
{ {
mBufferReader.stop(); mBufferReader.stop();
mSampleListeners.clear(); mSampleListeners.clear();
} }
} }
} }

View File

@@ -18,6 +18,8 @@
******************************************************************************/ ******************************************************************************/
package source.tuner; package source.tuner;
import channel.heartbeat.Heartbeat;
import channel.heartbeat.IHeartbeatProvider;
import dsp.filter.FilterFactory; import dsp.filter.FilterFactory;
import dsp.filter.Window.WindowType; import dsp.filter.Window.WindowType;
import dsp.filter.cic.ComplexPrimeCICDecimate; import dsp.filter.cic.ComplexPrimeCICDecimate;
@@ -57,6 +59,7 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro
private static int CHANNEL_RATE = 48000; private static int CHANNEL_RATE = 48000;
private static int CHANNEL_PASS_FREQUENCY = 12000; private static int CHANNEL_PASS_FREQUENCY = 12000;
private static final Heartbeat HEARTBEAT = new Heartbeat();
private OverflowableTransferQueue<ComplexBuffer> mBuffer; private OverflowableTransferQueue<ComplexBuffer> mBuffer;
@@ -66,6 +69,7 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro
private ComplexPrimeCICDecimate mDecimationFilter; private ComplexPrimeCICDecimate mDecimationFilter;
private Listener<ComplexBuffer> mListener; private Listener<ComplexBuffer> mListener;
private ISourceEventProcessor mFrequencyChangeProcessor; private ISourceEventProcessor mFrequencyChangeProcessor;
private Listener<Heartbeat> mHeartbeatListener;
private DownstreamProcessor mDownstreamFrequencyEventProcessor = new DownstreamProcessor(); private DownstreamProcessor mDownstreamFrequencyEventProcessor = new DownstreamProcessor();
private ScheduledFuture<?> mTaskHandle; private ScheduledFuture<?> mTaskHandle;
@@ -350,6 +354,25 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro
return mDownstreamFrequencyEventProcessor.getSourceEventListener(); return mDownstreamFrequencyEventProcessor.getSourceEventListener();
} }
/**
* Registers the listener to receive heartbeats from this source.
* @param listener to receive heartbeats
*/
@Override
public void setHeartbeatListener(Listener<Heartbeat> listener)
{
mHeartbeatListener = listener;
}
/**
* Removes the currently registered heartbeat listener
*/
@Override
public void removeHeartbeatListener()
{
mHeartbeatListener = null;
}
/** /**
* Managers frequency change requests and notifications from/to any downstream component. Downstream * Managers frequency change requests and notifications from/to any downstream component. Downstream
* components are those that receive samples from this tuner channel source. These downstream components will be * components are those that receive samples from this tuner channel source. These downstream components will be
@@ -475,6 +498,13 @@ public class TunerChannelSource extends ComplexSource implements ISourceEventPro
{ {
if(mProcessing) if(mProcessing)
{ {
//Send a heartbeat every time this runs to allow downstream components to perform periodic
//state monitoring functions on this thread
if(mHeartbeatListener != null)
{
mHeartbeatListener.receive(HEARTBEAT);
}
mBuffer.drainTo(mSampleBuffers, 20); mBuffer.drainTo(mSampleBuffers, 20);
for(Buffer buffer : mSampleBuffers) for(Buffer buffer : mSampleBuffers)

View File

@@ -54,7 +54,7 @@ public class USBTransferProcessor implements TransferCallback
private static final int TRANSFER_BUFFER_POOL_SIZE = 16; private static final int TRANSFER_BUFFER_POOL_SIZE = 16;
//Maximum number of filled buffers for the blocking queue //Maximum number of filled buffers for the blocking queue
private static final int FILLED_BUFFER_MAX_CAPACITY = 300; private static final int FILLED_BUFFER_MAX_CAPACITY = 500;
//Threshold for resetting buffer overflow condition //Threshold for resetting buffer overflow condition
private static final int FILLED_BUFFER_OVERFLOW_RESET_THRESHOLD = 100; private static final int FILLED_BUFFER_OVERFLOW_RESET_THRESHOLD = 100;
@@ -200,7 +200,7 @@ public class USBTransferProcessor implements TransferCallback
//Unregister from LibUSB processor so that it auto-stops LibUSB processing //Unregister from LibUSB processor so that it auto-stops LibUSB processing
TunerManager.LIBUSB_TRANSFER_PROCESSOR.registerTransferProcessor(this); TunerManager.LIBUSB_TRANSFER_PROCESSOR.unregisterTransferProcessor(this);
//Directly invoke the timeout handler to ensure that our cancelled transfer buffers are flushed. //Directly invoke the timeout handler to ensure that our cancelled transfer buffers are flushed.
int result = LibUsb.handleEventsTimeoutCompleted(null, USB_TIMEOUT_MS, int result = LibUsb.handleEventsTimeoutCompleted(null, USB_TIMEOUT_MS,
@@ -415,23 +415,27 @@ public class USBTransferProcessor implements TransferCallback
@Override @Override
public void run() public void run()
{ {
try mFilledBuffers.drainTo(mBuffersToDispatch, 50);
{
mFilledBuffers.drainTo(mBuffersToDispatch, 15);
for(byte[] buffer : mBuffersToDispatch) while(!mBuffersToDispatch.isEmpty())
{
try
{ {
float[] complexSamples = mSampleAdapter.convert(buffer); for(byte[] buffer : mBuffersToDispatch)
{
float[] complexSamples = mSampleAdapter.convert(buffer);
mComplexBufferBroadcaster.broadcast(new ComplexBuffer(complexSamples)); mComplexBufferBroadcaster.broadcast(new ComplexBuffer(complexSamples));
}
}
catch(Exception e)
{
mLog.error(mDeviceName + " - error while dispatching complex IQ buffer samples", e);
} }
}
catch(Exception e)
{
mLog.error(mDeviceName + " - error while dispatching complex IQ buffer samples", e);
}
mBuffersToDispatch.clear(); mBuffersToDispatch.clear();
mFilledBuffers.drainTo(mBuffersToDispatch, 50);
}
} }
} }
} }

View File

@@ -18,19 +18,9 @@
******************************************************************************/ ******************************************************************************/
package source.wave; package source.wave;
import java.io.File; import channel.heartbeat.Heartbeat;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ScheduledExecutorService;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sample.ConversionUtils; import sample.ConversionUtils;
import sample.Listener; import sample.Listener;
import sample.complex.ComplexBuffer; import sample.complex.ComplexBuffer;
@@ -39,22 +29,31 @@ import source.IControllableFileSource;
import source.IFrameLocationListener; import source.IFrameLocationListener;
import source.SourceEvent; import source.SourceEvent;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ScheduledExecutorService;
public class ComplexWaveSource extends ComplexSource implements IControllableFileSource public class ComplexWaveSource extends ComplexSource implements IControllableFileSource
{ {
private final static Logger mLog = private final static Logger mLog =
LoggerFactory.getLogger( ComplexWaveSource.class ); LoggerFactory.getLogger(ComplexWaveSource.class);
private IFrameLocationListener mFrameLocationListener; private IFrameLocationListener mFrameLocationListener;
private int mBytesPerFrame; private int mBytesPerFrame;
private int mFrameCounter = 0; private int mFrameCounter = 0;
private long mFrequency = 0; private long mFrequency = 0;
private Listener<ComplexBuffer> mListener; private Listener<ComplexBuffer> mListener;
private AudioInputStream mInputStream; private AudioInputStream mInputStream;
private File mFile; private File mFile;
public ComplexWaveSource( File file ) throws IOException public ComplexWaveSource(File file) throws IOException
{ {
mFile = file; mFile = file;
} }
@Override @Override
@@ -76,72 +75,88 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
return null; return null;
} }
/**
* Not implemented
*/
@Override @Override
public void reset() public void setHeartbeatListener(Listener<Heartbeat> listener)
{
stop();
start( null );
}
@Override
public void start( ScheduledExecutorService executor )
{
try
{
open();
}
catch ( IOException | UnsupportedAudioFileException e )
{
mLog.error( "Error starting complex wave source" );
}
}
@Override
public void stop()
{
try
{
close();
}
catch ( IOException e )
{
mLog.error( "Error stopping complex wave source" );
}
}
@Override
public long getFrameCount() throws IOException
{
// TODO Auto-generated method stub
return 0;
}
@Override
public int getSampleRate()
{ {
if( mInputStream != null )
{
return (int)mInputStream.getFormat().getSampleRate();
}
return 0;
} }
/** /**
* Returns the frequency set for this file. Normally returns zero, but * Not implemented
* the value can be set with setFrequency() method. */
*/ @Override
public void removeHeartbeatListener()
{
}
@Override
public void reset()
{
stop();
start(null);
}
@Override
public void start(ScheduledExecutorService executor)
{
try
{
open();
}
catch(IOException | UnsupportedAudioFileException e)
{
mLog.error("Error starting complex wave source");
}
}
@Override
public void stop()
{
try
{
close();
}
catch(IOException e)
{
mLog.error("Error stopping complex wave source");
}
}
@Override
public long getFrameCount() throws IOException
{
// TODO Auto-generated method stub
return 0;
}
@Override
public int getSampleRate()
{
if(mInputStream != null)
{
return (int)mInputStream.getFormat().getSampleRate();
}
return 0;
}
/**
* Returns the frequency set for this file. Normally returns zero, but
* the value can be set with setFrequency() method.
*/
public long getFrequency() public long getFrequency()
{ {
return mFrequency; return mFrequency;
} }
/** /**
* Changes the value returned from getFrequency() for this source. * Changes the value returned from getFrequency() for this source.
*/ */
public void setFrequency( long frequency ) public void setFrequency(long frequency)
{ {
mFrequency = frequency; mFrequency = frequency;
} }
/** /**
@@ -149,16 +164,16 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
*/ */
public void close() throws IOException public void close() throws IOException
{ {
if( mInputStream != null ) if(mInputStream != null)
{ {
mInputStream.close(); mInputStream.close();
} }
else else
{ {
throw new IOException( "Can't close wave source - was not opened" ); throw new IOException("Can't close wave source - was not opened");
} }
mInputStream = null; mInputStream = null;
} }
/** /**
@@ -166,69 +181,69 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
*/ */
public void open() throws IOException, UnsupportedAudioFileException public void open() throws IOException, UnsupportedAudioFileException
{ {
if( mInputStream == null ) if(mInputStream == null)
{ {
mInputStream = AudioSystem.getAudioInputStream( mFile ); mInputStream = AudioSystem.getAudioInputStream(mFile);
} }
else else
{ {
throw new IOException( "Can't open wave source - is already opened" ); throw new IOException("Can't open wave source - is already opened");
} }
AudioFormat format = mInputStream.getFormat(); AudioFormat format = mInputStream.getFormat();
mBytesPerFrame = format.getFrameSize(); mBytesPerFrame = format.getFrameSize();
if( format.getChannels() != 2 || format.getSampleSizeInBits() != 16 ) if(format.getChannels() != 2 || format.getSampleSizeInBits() != 16)
{ {
throw new IOException( "Unsupported Wave Format - EXPECTED: 2 " + throw new IOException("Unsupported Wave Format - EXPECTED: 2 " +
"channels 16-bit samples FOUND: " + "channels 16-bit samples FOUND: " +
mInputStream.getFormat().getChannels() + " channels " + mInputStream.getFormat().getChannels() + " channels " +
mInputStream.getFormat().getSampleSizeInBits() + "-bit samples" ); mInputStream.getFormat().getSampleSizeInBits() + "-bit samples");
} }
/* Broadcast that we're at frame location 0 */ /* Broadcast that we're at frame location 0 */
broadcast( 0 ); broadcast(0);
} }
/** /**
* Reads the number of frames and sends a buffer to the listener * Reads the number of frames and sends a buffer to the listener
*/ */
@Override @Override
public void next( int frames ) throws IOException public void next(int frames) throws IOException
{
next( frames, true );
}
/**
* Reads the number of frames and optionally sends the buffer to the listener
*/
public void next( int frames, boolean broadcast ) throws IOException
{ {
if( mInputStream != null ) next(frames, true);
}
/**
* Reads the number of frames and optionally sends the buffer to the listener
*/
public void next(int frames, boolean broadcast) throws IOException
{
if(mInputStream != null)
{ {
byte[] buffer = new byte[ mBytesPerFrame * frames ]; byte[] buffer = new byte[mBytesPerFrame * frames];
/* Fill the buffer with samples from the file */ /* Fill the buffer with samples from the file */
int samplesRead = mInputStream.read( buffer ); int samplesRead = mInputStream.read(buffer);
mFrameCounter += samplesRead; mFrameCounter += samplesRead;
broadcast( mFrameCounter ); broadcast(mFrameCounter);
if( broadcast && mListener != null ) if(broadcast && mListener != null)
{ {
if( samplesRead < buffer.length ) if(samplesRead < buffer.length)
{ {
buffer = Arrays.copyOf( buffer, samplesRead ); buffer = Arrays.copyOf(buffer, samplesRead);
} }
float[] samples = ConversionUtils float[] samples = ConversionUtils
.convertFromSigned16BitSamples( buffer ); .convertFromSigned16BitSamples(buffer);
mListener.receive( new ComplexBuffer( samples ) ); mListener.receive(new ComplexBuffer(samples));
} }
} }
} }
@@ -236,51 +251,51 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
* Registers the listener to receive sample buffers as they are read from * Registers the listener to receive sample buffers as they are read from
* the wave file * the wave file
*/ */
@Override @Override
public void setListener( Listener<ComplexBuffer> listener ) public void setListener(Listener<ComplexBuffer> listener)
{
mListener = listener;
}
/**
* Unregisters the listener from receiving sample buffers
*/
public void removeListener( Listener<ComplexBuffer> listener )
{ {
mListener = null; mListener = listener;
} }
@Override /**
* Unregisters the listener from receiving sample buffers
*/
public void removeListener(Listener<ComplexBuffer> listener)
{
mListener = null;
}
@Override
public void dispose() public void dispose()
{ {
mListener = null; mListener = null;
} }
@Override @Override
public File getFile() public File getFile()
{ {
return mFile; return mFile;
} }
private void broadcast( int byteLocation ) private void broadcast(int byteLocation)
{ {
int frameLocation = (int)( byteLocation / mBytesPerFrame ); int frameLocation = (int)(byteLocation / mBytesPerFrame);
if( mFrameLocationListener != null ) if(mFrameLocationListener != null)
{ {
mFrameLocationListener.frameLocationUpdated( frameLocation ); mFrameLocationListener.frameLocationUpdated(frameLocation);
} }
} }
@Override @Override
public void setListener( IFrameLocationListener listener ) public void setListener(IFrameLocationListener listener)
{ {
mFrameLocationListener = listener; mFrameLocationListener = listener;
} }
@Override @Override
public void removeListener( IFrameLocationListener listener ) public void removeListener(IFrameLocationListener listener)
{ {
mFrameLocationListener = null; mFrameLocationListener = null;
} }
} }

View File

@@ -17,44 +17,41 @@
******************************************************************************/ ******************************************************************************/
package source.wave; package source.wave;
import java.io.File; import channel.heartbeat.Heartbeat;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ScheduledExecutorService;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sample.ConversionUtils; import sample.ConversionUtils;
import sample.Listener; import sample.Listener;
import sample.real.RealBuffer; import sample.real.RealBuffer;
import source.IControllableFileSource; import source.IControllableFileSource;
import source.IFrameLocationListener; import source.IFrameLocationListener;
import source.RealSource; import source.RealSource;
import source.SourceEvent; import source.SourceEvent
public class RealWaveSource extends RealSource import javax.sound.sampled.AudioFormat;
implements IControllableFileSource, AutoCloseable import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ScheduledExecutorService;
public class RealWaveSource extends RealSource implements IControllableFileSource, AutoCloseable
{ {
private final static Logger mLog = private final static Logger mLog = LoggerFactory.getLogger(RealWaveSource.class);
LoggerFactory.getLogger( RealWaveSource.class );
private IFrameLocationListener mFrameLocationListener; private IFrameLocationListener mFrameLocationListener;
private int mBytesPerFrame; private int mBytesPerFrame;
private int mFrameCounter = 0; private int mFrameCounter = 0;
private long mFrequency = 0; private long mFrequency = 0;
private Listener<RealBuffer> mListener; private Listener<RealBuffer> mListener;
private AudioInputStream mInputStream; private AudioInputStream mInputStream;
private File mFile; private File mFile;
public RealWaveSource( File file ) throws IOException public RealWaveSource(File file) throws IOException
{ {
mFile = file; mFile = file;
} }
@Override @Override
@@ -76,76 +73,92 @@ public class RealWaveSource extends RealSource
return null; return null;
} }
/**
@Override * Not implemented
public void reset() */
{ @Override
stop(); public void setHeartbeatListener(Listener<Heartbeat> listener)
start( null );
}
@Override
public void start( ScheduledExecutorService executor )
{
try
{
open();
}
catch ( IOException | UnsupportedAudioFileException e )
{
mLog.error( "Error starting real wave source", e );
}
}
@Override
public void stop()
{
try
{
close();
}
catch ( IOException e )
{
mLog.error( "Error stopping real wave source", e );
}
}
@Override
public long getFrameCount() throws IOException
{
// TODO Auto-generated method stub
return 0;
}
@Override
public int getSampleRate()
{ {
if( mInputStream != null )
{
return (int)mInputStream.getFormat().getSampleRate();
}
return 0;
} }
/** /**
* Returns the frequency set for this file. Normally returns zero, but * Not implemented
* the value can be set with setFrequency() method. */
*/ @Override
public void removeHeartbeatListener()
{
}
@Override
public void reset()
{
stop();
start(null);
}
@Override
public void start(ScheduledExecutorService executor)
{
try
{
open();
}
catch(IOException | UnsupportedAudioFileException e)
{
mLog.error("Error starting real wave source", e);
}
}
@Override
public void stop()
{
try
{
close();
}
catch(IOException e)
{
mLog.error("Error stopping real wave source", e);
}
}
@Override
public long getFrameCount() throws IOException
{
// TODO Auto-generated method stub
return 0;
}
@Override
public int getSampleRate()
{
if(mInputStream != null)
{
return (int)mInputStream.getFormat().getSampleRate();
}
return 0;
}
/**
* Returns the frequency set for this file. Normally returns zero, but
* the value can be set with setFrequency() method.
*/
public long getFrequency() public long getFrequency()
{ {
return mFrequency; return mFrequency;
} }
/** /**
* Changes the value returned from getFrequency() for this source. * Changes the value returned from getFrequency() for this source.
*/ */
public void setFrequency( long frequency ) public void setFrequency(long frequency)
{ {
mFrequency = frequency; mFrequency = frequency;
} }
/** /**
@@ -153,16 +166,16 @@ public class RealWaveSource extends RealSource
*/ */
public void close() throws IOException public void close() throws IOException
{ {
if( mInputStream != null ) if(mInputStream != null)
{ {
mInputStream.close(); mInputStream.close();
} }
else else
{ {
throw new IOException( "Can't close wave source - was not opened" ); throw new IOException("Can't close wave source - was not opened");
} }
mInputStream = null; mInputStream = null;
} }
/** /**
@@ -170,74 +183,74 @@ public class RealWaveSource extends RealSource
*/ */
public void open() throws IOException, UnsupportedAudioFileException public void open() throws IOException, UnsupportedAudioFileException
{ {
if( mInputStream == null ) if(mInputStream == null)
{ {
mInputStream = AudioSystem.getAudioInputStream( mFile ); mInputStream = AudioSystem.getAudioInputStream(mFile);
} }
else else
{ {
throw new IOException( "Can't open wave source - is already opened" ); throw new IOException("Can't open wave source - is already opened");
} }
AudioFormat format = mInputStream.getFormat(); AudioFormat format = mInputStream.getFormat();
mBytesPerFrame = format.getFrameSize(); mBytesPerFrame = format.getFrameSize();
if( format.getChannels() != 1 || format.getSampleSizeInBits() != 16 ) if(format.getChannels() != 1 || format.getSampleSizeInBits() != 16)
{ {
throw new IOException( "Unsupported Wave Format - EXPECTED: 1 " + throw new IOException("Unsupported Wave Format - EXPECTED: 1 " +
"channel 16-bit samples FOUND: " + "channel 16-bit samples FOUND: " +
mInputStream.getFormat().getChannels() + " channels " + mInputStream.getFormat().getChannels() + " channels " +
mInputStream.getFormat().getSampleSizeInBits() + "-bit samples" ); mInputStream.getFormat().getSampleSizeInBits() + "-bit samples");
} }
/* Broadcast that we're at frame location 0 */ /* Broadcast that we're at frame location 0 */
broadcast( 0 ); broadcast(0);
} }
/** /**
* Reads the number of frames and sends a buffer to the listener * Reads the number of frames and sends a buffer to the listener
*/ */
@Override @Override
public void next( int frames ) throws IOException public void next(int frames) throws IOException
{
next( frames, true );
}
/**
* Reads the number of frames and optionally sends the buffer to the listener
*/
public void next( int frames, boolean broadcast ) throws IOException
{ {
if( mInputStream != null ) next(frames, true);
}
/**
* Reads the number of frames and optionally sends the buffer to the listener
*/
public void next(int frames, boolean broadcast) throws IOException
{
if(mInputStream != null)
{ {
byte[] buffer = new byte[ mBytesPerFrame * frames ]; byte[] buffer = new byte[mBytesPerFrame * frames];
/* Fill the buffer with samples from the file */ /* Fill the buffer with samples from the file */
int samplesRead = mInputStream.read( buffer ); int samplesRead = mInputStream.read(buffer);
mFrameCounter += samplesRead; mFrameCounter += samplesRead;
broadcast( mFrameCounter ); broadcast(mFrameCounter);
if( broadcast && mListener != null ) if(broadcast && mListener != null)
{ {
if( samplesRead < buffer.length ) if(samplesRead < buffer.length)
{ {
if( samplesRead == -1 ) if(samplesRead == -1)
{ {
throw new IOException( "End of recording" ); throw new IOException("End of recording");
} }
buffer = Arrays.copyOf( buffer, samplesRead ); buffer = Arrays.copyOf(buffer, samplesRead);
} }
float[] samples = ConversionUtils float[] samples = ConversionUtils
.convertFromSigned16BitSamples( buffer ); .convertFromSigned16BitSamples(buffer);
mListener.receive( new RealBuffer( samples ) ); mListener.receive(new RealBuffer(samples));
} }
} }
} }
@@ -245,51 +258,51 @@ public class RealWaveSource extends RealSource
* Registers the listener to receive sample buffers as they are read from * Registers the listener to receive sample buffers as they are read from
* the wave file * the wave file
*/ */
@Override @Override
public void setListener( Listener<RealBuffer> listener ) public void setListener(Listener<RealBuffer> listener)
{
mListener = listener;
}
/**
* Unregisters the listener from receiving sample buffers
*/
public void removeListener( Listener<RealBuffer> listener )
{ {
mListener = null; mListener = listener;
} }
@Override /**
* Unregisters the listener from receiving sample buffers
*/
public void removeListener(Listener<RealBuffer> listener)
{
mListener = null;
}
@Override
public void dispose() public void dispose()
{ {
mListener = null; mListener = null;
} }
@Override @Override
public File getFile() public File getFile()
{ {
return mFile; return mFile;
} }
private void broadcast( int byteLocation ) private void broadcast(int byteLocation)
{ {
int frameLocation = (int)( byteLocation / mBytesPerFrame ); int frameLocation = (int)(byteLocation / mBytesPerFrame);
if( mFrameLocationListener != null ) if(mFrameLocationListener != null)
{ {
mFrameLocationListener.frameLocationUpdated( frameLocation ); mFrameLocationListener.frameLocationUpdated(frameLocation);
} }
} }
@Override @Override
public void setListener( IFrameLocationListener listener ) public void setListener(IFrameLocationListener listener)
{ {
mFrameLocationListener = listener; mFrameLocationListener = listener;
} }
@Override @Override
public void removeListener( IFrameLocationListener listener ) public void removeListener(IFrameLocationListener listener)
{ {
mFrameLocationListener = null; mFrameLocationListener = null;
} }
} }