mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 15:47:25 -05:00
Compare commits
38 Commits
v0.3.0-bet
...
v0.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e321bdaa3 | ||
|
|
f191d28c98 | ||
|
|
afc3d299b6 | ||
|
|
0471d90b54 | ||
|
|
8f68967b6c | ||
|
|
91b950c13d | ||
|
|
581ff9f449 | ||
|
|
67c3cb9639 | ||
|
|
cebd2da89f | ||
|
|
73b6ef164f | ||
|
|
b2c7ac8c66 | ||
|
|
108691dc3c | ||
|
|
455aa97286 | ||
|
|
d608834214 | ||
|
|
41660e1881 | ||
|
|
110ccb158c | ||
|
|
64ccacf04a | ||
|
|
f00b9555fe | ||
|
|
85442a9648 | ||
|
|
c67a0b329d | ||
|
|
8035d64fcd | ||
|
|
df2a061a08 | ||
|
|
e430319937 | ||
|
|
724f769771 | ||
|
|
c196cfebed | ||
|
|
e80b5866d4 | ||
|
|
c8994d5593 | ||
|
|
99ef57c90f | ||
|
|
be62ae0459 | ||
|
|
b9be6f43e0 | ||
|
|
d5ad3f807d | ||
|
|
d85ebd018d | ||
|
|
ab4e31ae00 | ||
|
|
5efd89632e | ||
|
|
1dc3863489 | ||
|
|
441286ba13 | ||
|
|
e4323df347 | ||
|
|
a872e0d664 |
10
CHANGELOG
10
CHANGELOG
@@ -1,5 +1,13 @@
|
||||
0.3.2 20171105
|
||||
- Resolves issue with tuner frequency adjustments that was causing channel processing
|
||||
to lockup and show persistent 'overflow' state in each channel status
|
||||
- Resolves issue with broadcastify 'mountpoint-in-use' error handling and updates
|
||||
memory usage logging
|
||||
|
||||
0.3.0-beta1 20170129
|
||||
0.3.1 20171009
|
||||
- Resolves Java 9 incompatibility issues
|
||||
|
||||
0.3.0 20170922
|
||||
- REQUIRES: Java 8 (or newer) for compatibility and eventual changeover to JavaFX
|
||||
- FEATURE: audio streaming - adds capability to stream to multiple remote broadcast
|
||||
servers (Broadcastify, Icecast v2.3 and v2.4+, and Shoutcast V1 and V2). Each
|
||||
|
||||
@@ -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
|
||||
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.>
|
||||
Copyright (C) <year> <name of author>
|
||||
{one line to give the program's name and a brief idea of what it does.}
|
||||
Copyright (C) {year} {name of author}
|
||||
|
||||
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
|
||||
@@ -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
|
||||
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 is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
@@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -2,10 +2,9 @@
|
||||
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)
|
||||
* [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)
|
||||
* [Download](https://github.com/DSheirer/sdrtrunk/releases)
|
||||
* [Support](https://groups.google.com/forum/#!forum/sdrtrunk)
|
||||
|
||||

|
||||
**Figure 1:** sdrtrunk **Version 0.3.0** Application Screenshot
|
||||
**Figure 1:** sdrtrunk **Version 0.3.0** Application Screenshot
|
||||
|
||||
233
build/build.xml
233
build/build.xml
@@ -1,121 +1,142 @@
|
||||
<project name="SDRTrunk" default="SDRTrunk" basedir=".">
|
||||
<description>
|
||||
Ant build script for SDRTrunk Decoder Application
|
||||
</description>
|
||||
<description>
|
||||
Ant build script for SDRTrunk Decoder Application
|
||||
</description>
|
||||
|
||||
<!-- version properties -->
|
||||
<property name="major" value="0"/>
|
||||
<property name="minor" value="3"/>
|
||||
<property name="patch" value="0"/>
|
||||
<property name="release" value="-beta10"/>
|
||||
<property name="filename" value="sdrtrunk_${major}.${minor}.${patch}${release}"/>
|
||||
|
||||
<!-- Build properties -->
|
||||
<property name="repo" location=".."/>
|
||||
<property name="source.code" location="${repo}/src"/>
|
||||
<property name="source.configs" location="${repo}/config"/>
|
||||
<property name="source.images" location="${repo}/images"/>
|
||||
<property name="source.libs" location="${repo}/imports"/>
|
||||
<property name="source.license" location="${repo}/license"/>
|
||||
<property name="source.scripts" location="${repo}/scripts"/>
|
||||
<!-- version properties -->
|
||||
<property name="major" value="0"/>
|
||||
<property name="minor" value="3"/>
|
||||
<property name="patch" value="2"/>
|
||||
<property name="release" value=""/>
|
||||
<property name="filename" value="sdrtrunk_${major}.${minor}.${patch}${release}"/>
|
||||
|
||||
<property name="output" location="${repo}/product"/>
|
||||
<property name="output.classes" location="${repo}/classes"/>
|
||||
<property name="output.folder" location="${output}/sdrtrunk"/>
|
||||
<property name="output.config" location="${output.folder}/config"/>
|
||||
<property name="output.images" location="${output.folder}/images"/>
|
||||
<property name="output.libs" location="${output.folder}/libs"/>
|
||||
<!-- Build properties -->
|
||||
<property name="repo" location=".."/>
|
||||
<property name="source.code" location="${repo}/src"/>
|
||||
<property name="source.configs" location="${repo}/config"/>
|
||||
<property name="source.images" location="${repo}/images"/>
|
||||
<property name="source.libs" location="${repo}/imports"/>
|
||||
<property name="source.scripts" location="${repo}/scripts"/>
|
||||
|
||||
<path id="classpath">
|
||||
<fileset dir="${source.libs}" includes="*.jar" />
|
||||
</path>
|
||||
|
||||
<target name="init">
|
||||
<!-- Create the classes folder to contain the compiled java classes -->
|
||||
<mkdir dir="${output.classes}"/>
|
||||
|
||||
<!-- Create version file -->
|
||||
<echo message="${major}.${minor}.${patch}-${release}" file="${output.classes}/sdrtrunk-version"/>
|
||||
</target>
|
||||
<property name="output" location="${repo}/product"/>
|
||||
<property name="output.classes" location="${repo}/classes"/>
|
||||
<property name="output.folder" location="${output}/sdrtrunk"/>
|
||||
<property name="output.config" location="${output.folder}/config"/>
|
||||
<property name="output.images" location="${output.folder}/images"/>
|
||||
<property name="output.libs" location="${output.folder}/libs"/>
|
||||
|
||||
<target name="compile" depends="clean,init" description="Compile java classes" >
|
||||
<javac srcdir="${source.code}" destdir="${output.classes}" classpathref="classpath" includeantruntime="false" />
|
||||
</target>
|
||||
<path id="classpath">
|
||||
<fileset dir="${source.libs}" includes="*.jar"/>
|
||||
</path>
|
||||
|
||||
<target name="SDRTrunk" depends="compile" description="Create SDRTrunk application" >
|
||||
<!-- Create the distribution directory -->
|
||||
<mkdir dir="${output.folder}"/>
|
||||
<target name="init">
|
||||
<!-- Create the classes folder to contain the compiled java classes -->
|
||||
<mkdir dir="${output.classes}"/>
|
||||
|
||||
<!-- Create jar file -->
|
||||
<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>
|
||||
|
||||
<fileset dir="${source.license}">
|
||||
<include name="license.txt"/>
|
||||
</fileset>
|
||||
</jar>
|
||||
|
||||
<!-- Copy third-party libraries -->
|
||||
<mkdir dir="${output.libs}"/>
|
||||
<copy todir="${output.libs}">
|
||||
<fileset dir="${source.libs}" includes="*.jar" />
|
||||
</copy>
|
||||
|
||||
<!-- Copy config files -->
|
||||
<mkdir dir="${output.config}"/>
|
||||
<!-- Create version file -->
|
||||
<echo message="${major}.${minor}.${patch}-${release}" file="${output.classes}/sdrtrunk-version"/>
|
||||
</target>
|
||||
|
||||
<copy todir="${output.config}">
|
||||
<fileset dir="${source.configs}">
|
||||
<include name="*.rules"/>
|
||||
</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="${source.license}">
|
||||
<include name="license.txt"/>
|
||||
</fileset>
|
||||
</copy>
|
||||
|
||||
<!-- Copy program scripts -->
|
||||
<copy todir="${output.folder}">
|
||||
<fileset dir="${source.scripts}" />
|
||||
</copy>
|
||||
<target name="compile" depends="clean,init,compile-java-8,compile-java-9"
|
||||
description="Compile java classes" />
|
||||
|
||||
<target name="compile-java-8" depends="check-java-version" if="java.8" description="Compiles code with Java 8 arguments">
|
||||
<echo message="Compiling with Java 8 Options"/>
|
||||
<javac srcdir="${source.code}" destdir="${output.classes}" classpathref="classpath" includeantruntime="false"/>
|
||||
</target>
|
||||
|
||||
<target name="compile-java-9" depends="check-java-version" if="java.9" description="Compiles code with Java 9 arguments">
|
||||
<echo message="Compiling with Java 9 Options"/>
|
||||
<javac srcdir="${source.code}" destdir="${output.classes}" classpathref="classpath" includeantruntime="false">
|
||||
<compilerarg value="--add-modules"/>
|
||||
<compilerarg value="java.xml.bind"/>
|
||||
</javac>
|
||||
</target>
|
||||
|
||||
<target name="check-java-version" description="Determines if the compiler is Java 8 or 9">
|
||||
<condition property="java.8" value="true">
|
||||
<contains string="${java.version}" substring="1.8"/>
|
||||
</condition>
|
||||
<condition property="java.9" value="true">
|
||||
<equals arg1="${java.version}" arg2="9"/>
|
||||
</condition>
|
||||
<echo message="Java Version: ${java.version}"/>
|
||||
</target>
|
||||
|
||||
<target name="SDRTrunk" depends="compile" description="Create SDRTrunk application">
|
||||
<!-- Create the distribution directory -->
|
||||
<mkdir dir="${output.folder}"/>
|
||||
|
||||
<!-- Create jar file -->
|
||||
<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>
|
||||
|
||||
<fileset dir="${repo}">
|
||||
<include name="LICENSE"/>
|
||||
</fileset>
|
||||
</jar>
|
||||
|
||||
<!-- Copy third-party libraries -->
|
||||
<mkdir dir="${output.libs}"/>
|
||||
<copy todir="${output.libs}">
|
||||
<fileset dir="${source.libs}" includes="*.jar"/>
|
||||
</copy>
|
||||
|
||||
<!-- Copy config files -->
|
||||
<mkdir dir="${output.config}"/>
|
||||
|
||||
<copy todir="${output.config}">
|
||||
<fileset dir="${source.configs}">
|
||||
<include name="*.rules"/>
|
||||
</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 -->
|
||||
<zip destfile="${output}/${filename}.zip" basedir="${output}" />
|
||||
<!-- Bundle the files into .zip -->
|
||||
<zip destfile="${output}/${filename}.zip" basedir="${output}"/>
|
||||
|
||||
<!-- Remove linux start script before tar task -->
|
||||
<delete file="${output.folder}/run_sdrtrunk_linux.sh"/>
|
||||
<!-- Remove linux start script before tar task -->
|
||||
<delete file="${output.folder}/run_sdrtrunk_linux.sh"/>
|
||||
|
||||
<!-- Bundle files into .tar.gz and copy/set permissions on linux start script -->
|
||||
<tar destfile="${output}/${filename}.tar.gz" compression="gzip">
|
||||
<tarfileset dir="${output.folder}" prefix="sdrtrunk" />
|
||||
<tarfileset dir="${source.scripts}" prefix="sdrtrunk" filemode="777">
|
||||
<include name="run_sdrtrunk_linux.sh"/>
|
||||
</tarfileset>
|
||||
</tar>
|
||||
|
||||
<delete dir="${output.folder}"/>
|
||||
</target>
|
||||
<!-- Bundle files into .tar.gz and copy/set permissions on linux start script -->
|
||||
<tar destfile="${output}/${filename}.tar.gz" compression="gzip">
|
||||
<tarfileset dir="${output.folder}" prefix="sdrtrunk"/>
|
||||
<tarfileset dir="${source.scripts}" prefix="sdrtrunk" filemode="777">
|
||||
<include name="run_sdrtrunk_linux.sh"/>
|
||||
</tarfileset>
|
||||
</tar>
|
||||
|
||||
<target name="clean" description="Clean previous build artifacts" >
|
||||
<delete dir="${output.classes}"/>
|
||||
<delete dir="${output}"/>
|
||||
</target>
|
||||
<delete dir="${output.folder}"/>
|
||||
</target>
|
||||
|
||||
<target name="clean" description="Clean previous build artifacts">
|
||||
<delete dir="${output.classes}"/>
|
||||
<delete dir="${output}"/>
|
||||
</target>
|
||||
|
||||
</project>
|
||||
@@ -1,4 +1,6 @@
|
||||
<configuration>
|
||||
<conversionRule conversionWord="memory_usage" converterClass="util.MemoryUsageLogger" />
|
||||
|
||||
<timestamp key="appstart" datePattern="HHmmss"/>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
@@ -9,7 +11,7 @@
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%-25(%d{yyyyMMdd HHmmss.SSS} [%thread]) %-5level %logger{30} - %msg%n</pattern>
|
||||
<pattern>%-25(%d{yyyyMMdd HHmmss.SSS} [%thread]) %-5level %logger{30} - %msg %memory_usage%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
@@ -19,7 +21,7 @@
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%-12(%d{HH:mm:ss.SSS}) %-5level %logger{25} - %msg%n</pattern>
|
||||
<pattern>%-12(%d{HH:mm:ss.SSS}) %-5level %logger{25} - %msg %memory_usage%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
#!/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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -68,7 +68,7 @@ public enum BroadcastState
|
||||
/**
|
||||
* Specified mount point is already in use
|
||||
*/
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", true),
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false),
|
||||
|
||||
/**
|
||||
* Server is not known or reachable
|
||||
|
||||
@@ -36,6 +36,7 @@ import util.ThreadPool;
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.channels.UnresolvedAddressException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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 String TERMINATOR = "\r\n";
|
||||
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 IoSession mStreamingSession = null;
|
||||
@@ -99,10 +101,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(10000);
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(CONNECTION_ATTEMPT_TIMEOUT);
|
||||
|
||||
// mSocketConnector.getFilterChain().addLast("logger",
|
||||
// new LoggingFilter(IcecastTCPAudioBroadcaster.class));
|
||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
||||
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
||||
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
|
||||
|
||||
mSocketConnector.getFilterChain().addLast("codec",
|
||||
new ProtocolCodecFilter(new IcecastCodecFactory()));
|
||||
@@ -123,9 +126,20 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
ConnectFuture future = mSocketConnector
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
future.awaitUninterruptibly();
|
||||
|
||||
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
|
||||
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)
|
||||
{
|
||||
Throwable throwableCause = rie.getCause();
|
||||
@@ -137,16 +151,20 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
else if(throwableCause != null)
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("Failed to connect", rie);
|
||||
mLog.debug("[" + getStreamName() + "] failed to connect", rie);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("Failed to connect - no exception is available");
|
||||
mLog.debug("[" + getStreamName() + "] failed to connect - no exception is available");
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
@@ -164,12 +182,13 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
*/
|
||||
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
|
||||
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(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
mSocketConnector.dispose();
|
||||
mStreamingSession = null;
|
||||
mSocketConnector = null;
|
||||
|
||||
mConnecting.set(false);
|
||||
|
||||
super.sessionClosed(session);
|
||||
}
|
||||
|
||||
@@ -253,45 +273,36 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
|
||||
if(connected())
|
||||
{
|
||||
mLog.info("Streaming connection error detected - resetting connection - " + reason);
|
||||
disconnect();
|
||||
connect();
|
||||
}
|
||||
else if(reason.startsWith("Connection reset"))
|
||||
{
|
||||
mLog.info("Streaming connection reset by remote server - reestablishing connection");
|
||||
disconnect();
|
||||
connect();
|
||||
}
|
||||
else if(reason.startsWith("Operation timed out"))
|
||||
{
|
||||
mLog.info("Streaming connection timed out - resetting connection");
|
||||
disconnect();
|
||||
connect();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
mLog.error("Unrecognized IO error: " + reason + ". Streaming halted.", cause);
|
||||
}
|
||||
}
|
||||
else if(ioe instanceof WriteTimeoutException)
|
||||
{
|
||||
mLog.info("Network write timeout error - resetting connection");
|
||||
disconnect();
|
||||
connect();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
mLog.error("Unspecified IO error - streaming halted.", cause);
|
||||
mLog.error("[" + getStreamName() + "] Unspecified IO error - streaming halted.", cause);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Broadcast error", cause);
|
||||
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
}
|
||||
@@ -314,11 +325,19 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
|
||||
}
|
||||
else if(message.contains("Content-Type: text/html"))
|
||||
{
|
||||
//Disregard ... this normally follows the mount point in use error
|
||||
}
|
||||
else if(message.contains("Invalid Password") ||
|
||||
message.contains("Authentication Required"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
||||
}
|
||||
else if(message.contains("HTTP/1.1 501"))
|
||||
{
|
||||
disconnect(); //So that we can reconnect later
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Unrecognized server response:" + message);
|
||||
@@ -328,7 +347,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
|
||||
mLog.error("[" + getStreamName() + "]Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
|
||||
"] received:" + object.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,10 @@ public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
|
||||
|
||||
disconnect();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
@@ -185,12 +189,13 @@ public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
|
||||
*/
|
||||
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
|
||||
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(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
mSocketConnector.dispose();
|
||||
mStreamingSession = null;
|
||||
mSocketConnector = null;
|
||||
|
||||
mConnecting.set(false);
|
||||
super.sessionClosed(session);
|
||||
@@ -255,13 +260,11 @@ public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
|
||||
{
|
||||
mLog.info("Streaming connection reset by remote server - reestablishing connection");
|
||||
disconnect();
|
||||
connect();
|
||||
}
|
||||
else if(reason.startsWith("Operation timed out"))
|
||||
{
|
||||
mLog.info("Streaming connection timed out - resetting connection");
|
||||
disconnect();
|
||||
connect();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -204,6 +204,10 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
|
||||
|
||||
disconnect();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
@@ -221,13 +225,13 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
|
||||
*/
|
||||
public void disconnect()
|
||||
{
|
||||
if(connected())
|
||||
if(connected() && mStreamingSession != null)
|
||||
{
|
||||
if(mStreamingSession != null)
|
||||
{
|
||||
mStreamingSession.write(new TerminateBroadcast());
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,15 +353,15 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
|
||||
@Override
|
||||
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(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
mSocketConnector.dispose();
|
||||
mStreamingSession = null;
|
||||
mSocketConnector = null;
|
||||
|
||||
mConnecting.set(false);
|
||||
super.sessionClosed(session);
|
||||
@@ -378,13 +382,11 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
|
||||
{
|
||||
mLog.info("Streaming connection reset by remote server - reestablishing connection");
|
||||
disconnect();
|
||||
connect();
|
||||
}
|
||||
else if(reason.startsWith("Operation timed out"))
|
||||
{
|
||||
mLog.info("Streaming connection timed out - resetting connection");
|
||||
disconnect();
|
||||
connect();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -71,6 +71,40 @@ public class AuthenticateBroadcast extends UltravoxMessage
|
||||
setPayload(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the value using the XTEA block cipher algorithm. The byte array is sliced into 8-byte chunks and each
|
||||
* chunk is encrypted into a 16 hexadecimal character string. All encrypted chunks are concatenated and returned
|
||||
* as a single string. If the value byte array is not an even multiple of 8 characters, it will be padded with
|
||||
* zero valued bytes to make it an even multiple of eight.
|
||||
*
|
||||
* @param xtea instance of the XTEA algorithm with a loaded encryption key
|
||||
* @param value to encode
|
||||
* @return value encrypted into one or more multiples of 16 hexadecimal characters
|
||||
*/
|
||||
private static String encrypt(XTEA xtea, byte[] value)
|
||||
{
|
||||
if(value.length % 8 != 0 || value.length == 0)
|
||||
{
|
||||
int newLength = ((value.length / 8) + 1) * 8;
|
||||
value = Arrays.copyOf(value, newLength);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(int x = 0; x < value.length; x += 8)
|
||||
{
|
||||
byte[] plaintext = Arrays.copyOfRange(value, x, x + 8);
|
||||
byte[] encrypted = xtea.encrypt(plaintext);
|
||||
|
||||
for(byte b: encrypted)
|
||||
{
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the value using the XTEA block cipher algorithm and returns the encrypted value as a 16 hex value
|
||||
*
|
||||
@@ -80,23 +114,6 @@ public class AuthenticateBroadcast extends UltravoxMessage
|
||||
*/
|
||||
private static String encrypt(XTEA xtea, String value)
|
||||
{
|
||||
byte[] valueBytes = value.getBytes();
|
||||
|
||||
if(valueBytes.length % 8 != 0 || valueBytes.length == 0)
|
||||
{
|
||||
int newLength = ((valueBytes.length / 8) + 1) * 8;
|
||||
valueBytes = Arrays.copyOf(valueBytes, newLength);
|
||||
}
|
||||
|
||||
byte[] encrypted = xtea.encrypt(valueBytes);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(byte b: encrypted)
|
||||
{
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
return encrypt(xtea, value.getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,17 @@ public class BinaryMessage extends BitSet
|
||||
{
|
||||
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
|
||||
|
||||
65
src/bits/CorrectedBinaryMessage.java
Normal file
65
src/bits/CorrectedBinaryMessage.java
Normal 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;
|
||||
}
|
||||
}
|
||||
31
src/channel/heartbeat/Heartbeat.java
Normal file
31
src/channel/heartbeat/Heartbeat.java
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
26
src/channel/heartbeat/IHeartbeatListener.java
Normal file
26
src/channel/heartbeat/IHeartbeatListener.java
Normal 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();
|
||||
}
|
||||
28
src/channel/heartbeat/IHeartbeatProvider.java
Normal file
28
src/channel/heartbeat/IHeartbeatProvider.java
Normal 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();
|
||||
}
|
||||
@@ -20,6 +20,8 @@ package channel.state;
|
||||
|
||||
import audio.squelch.ISquelchStateProvider;
|
||||
import audio.squelch.SquelchState;
|
||||
import channel.heartbeat.Heartbeat;
|
||||
import channel.heartbeat.IHeartbeatListener;
|
||||
import channel.metadata.Attribute;
|
||||
import channel.metadata.AttributeChangeRequest;
|
||||
import channel.metadata.IAttributeChangeRequestListener;
|
||||
@@ -39,16 +41,13 @@ import sample.real.IOverflowListener;
|
||||
import source.tuner.frequency.FrequencyChangeEvent;
|
||||
import source.tuner.frequency.IFrequencyChangeListener;
|
||||
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static source.tuner.frequency.FrequencyChangeEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
|
||||
|
||||
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
|
||||
IDecoderStateEventProvider, IFrequencyChangeListener, ISquelchStateProvider, IAttributeChangeRequestListener,
|
||||
IOverflowListener
|
||||
IDecoderStateEventProvider, IFrequencyChangeListener, IHeartbeatListener, ISquelchStateProvider,
|
||||
IAttributeChangeRequestListener, IOverflowListener
|
||||
{
|
||||
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<SquelchState> mSquelchStateListener;
|
||||
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
|
||||
private StateMonitor mStateMonitor = new StateMonitor();
|
||||
private ScheduledFuture<?> mStateMonitorFuture;
|
||||
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
|
||||
private ChannelType mChannelType;
|
||||
private TrafficChannelManager mTrafficChannelEndListener;
|
||||
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
|
||||
* configured for the channel and provides squelch control and decoder state
|
||||
* reset events.
|
||||
* Channel state tracks the overall state of all processing modules and decoders configured for the channel and
|
||||
* provides squelch control and decoder state reset events.
|
||||
*
|
||||
* Uses a state enumeration that defines allowable channel state transitions in
|
||||
* order to track a call or data decode event from start to finish. Uses a
|
||||
* 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 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.
|
||||
* Uses a state enumeration that defines allowable channel state transitions in order to track a call or data decode
|
||||
* event from start to finish. Uses a 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
|
||||
* reset to IDLE.
|
||||
*
|
||||
* State Descriptions:
|
||||
* IDLE Normal state. No voice or data call activity
|
||||
* CALL/DATA/ENCRYPTED/CONTROL
|
||||
* Decoding states.
|
||||
* FADE The phase after a voice or data call when either an explicit
|
||||
* call end has been received, or when no new signalling updates
|
||||
* have been received, and the fade timer has expired. This phase
|
||||
* 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.
|
||||
* IDLE: Normal state. No voice or data call activity
|
||||
* CALL/DATA/ENCRYPTED/CONTROL: Decoding states.
|
||||
* FADE: The phase after a voice or data call when either an explicit call end has been received, or when no new
|
||||
* signalling updates have been received, and the fade timer has expired. This phase 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)
|
||||
{
|
||||
@@ -137,23 +126,11 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
@Override
|
||||
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));
|
||||
|
||||
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);
|
||||
}
|
||||
setState(State.CALL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,16 +139,6 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
{
|
||||
mMutableMetadata.resetAllAttributes();
|
||||
|
||||
if(mStateMonitorFuture != null)
|
||||
{
|
||||
boolean success = mStateMonitorFuture.cancel(true);
|
||||
|
||||
if(!success)
|
||||
{
|
||||
mLog.error("Couldn't stop monitoring scheduled future");
|
||||
}
|
||||
}
|
||||
|
||||
mTrafficChannelEndListener = null;
|
||||
|
||||
if(mTrafficChannelCallEvent != null)
|
||||
@@ -182,8 +149,6 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
|
||||
mTrafficChannelCallEvent = null;
|
||||
|
||||
mStateMonitorFuture = null;
|
||||
|
||||
mSquelchLocked = false;
|
||||
}
|
||||
|
||||
@@ -192,7 +157,6 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
|
||||
mCallEventListener = null;
|
||||
mDecoderStateListener = null;
|
||||
mSquelchStateListener = null;
|
||||
mStateMonitor = null;
|
||||
}
|
||||
|
||||
@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
|
||||
* values so that the state monitor will not change state. Broadcasts a
|
||||
* squelch event when the state changes and the audio squelch state should
|
||||
* change. Also broadcasts changed attribute and decoder state events so
|
||||
* that external processes can maintain sync with this channel state.
|
||||
* Sets the channel state to the specified state, or updates the timeout values so that the state monitor will not
|
||||
* change state. Broadcasts a squelch event when the state changes and the audio squelch state should change. Also
|
||||
* broadcasts changed attribute and decoder state events so that external processes can maintain sync with this
|
||||
* channel 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 call tear down
|
||||
if(isStandardChannel())
|
||||
{
|
||||
broadcast(SquelchState.SQUELCH);
|
||||
updateFadeTimeout();
|
||||
mState = state;
|
||||
}
|
||||
break;
|
||||
case DATA:
|
||||
case ENCRYPTED:
|
||||
case CONTROL:
|
||||
//Don't allow traffic channels to be control channels, otherwise they can't transition to teardown
|
||||
if(isStandardChannel())
|
||||
{
|
||||
broadcast(SquelchState.SQUELCH);
|
||||
updateFadeTimeout();
|
||||
mState = state;
|
||||
break;
|
||||
case CALL:
|
||||
broadcast(SquelchState.UNSQUELCH);
|
||||
updateFadeTimeout();
|
||||
mState = state;
|
||||
break;
|
||||
case FADE:
|
||||
processFadeState();
|
||||
break;
|
||||
case IDLE:
|
||||
processIdleState();
|
||||
break;
|
||||
case TEARDOWN:
|
||||
processTeardownState();
|
||||
break;
|
||||
case RESET:
|
||||
mState = State.IDLE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
mMutableMetadata.receive(new AttributeChangeRequest<State>(Attribute.CHANNEL_STATE,
|
||||
mSourceOverflow ? State.OVERFLOW : mState));
|
||||
}
|
||||
break;
|
||||
case DATA:
|
||||
case ENCRYPTED:
|
||||
broadcast(SquelchState.SQUELCH);
|
||||
updateFadeTimeout();
|
||||
mState = state;
|
||||
break;
|
||||
case CALL:
|
||||
broadcast(SquelchState.UNSQUELCH);
|
||||
updateFadeTimeout();
|
||||
mState = state;
|
||||
break;
|
||||
case FADE:
|
||||
processFadeState();
|
||||
break;
|
||||
case IDLE:
|
||||
processIdleState();
|
||||
break;
|
||||
case TEARDOWN:
|
||||
processTeardownState();
|
||||
break;
|
||||
case RESET:
|
||||
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,
|
||||
* 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
|
||||
* state to IDLE (standard channel) or to TEARDOWN (traffic channel) when
|
||||
* decoding stops or the monitored channel returns to a no signal state.
|
||||
* Processes periodic heartbeats received from the processing chain to perform state monitoring and cleanup
|
||||
* functions.
|
||||
*
|
||||
* 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.
|
||||
* Monitors decoder state events to automatically transition the channel state to IDLE (standard channel) or to
|
||||
* TEARDOWN (traffic channel) when decoding stops or the monitored channel returns to a no signal state.
|
||||
*
|
||||
* 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
|
||||
public void run()
|
||||
public void receive(Heartbeat heartbeat)
|
||||
{
|
||||
try
|
||||
{
|
||||
synchronized(mState)
|
||||
if(State.CALL_STATES.contains(mState) && mFadeTimeout <= System.currentTimeMillis())
|
||||
{
|
||||
if(State.CALL_STATES.contains(mState) &&
|
||||
mFadeTimeout <= System.currentTimeMillis())
|
||||
processFadeState();
|
||||
}
|
||||
else if(mState == State.FADE && mEndTimeout <= System.currentTimeMillis())
|
||||
{
|
||||
if(isTrafficChannel())
|
||||
{
|
||||
processFadeState();
|
||||
processTeardownState();
|
||||
}
|
||||
else if(mState == State.FADE &&
|
||||
mEndTimeout <= System.currentTimeMillis())
|
||||
else
|
||||
{
|
||||
if(isTrafficChannel())
|
||||
{
|
||||
processTeardownState();
|
||||
}
|
||||
else
|
||||
{
|
||||
processIdleState();
|
||||
}
|
||||
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() +
|
||||
"] current [" + System.currentTimeMillis() +
|
||||
"] mResetTimeout [" + mEndTimeout +
|
||||
|
||||
@@ -163,7 +163,7 @@ public class CRCLJ
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies 1 or 2 bit error positions that match the checksum error value.
|
||||
* Identifies 1 bit error positions that match the checksum error value.
|
||||
*/
|
||||
public static int[] findBitErrors( int checksumError )
|
||||
{
|
||||
@@ -180,24 +180,6 @@ public class CRCLJ
|
||||
}
|
||||
}
|
||||
|
||||
/* Two bit errors */
|
||||
for( int x = 0; x < 48; x++ )
|
||||
{
|
||||
for( int y = 0; y < 48; y++ )
|
||||
{
|
||||
if( x != y &&
|
||||
( CHECKSUMS[ x ] ^ CHECKSUMS[ y ] ) == checksumError )
|
||||
{
|
||||
int[] errors = new int[ 2 ];
|
||||
|
||||
errors[ 0 ] = x;
|
||||
errors[ 1 ] = y;
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
86
src/edac/trellis/DMRNode.java
Normal file
86
src/edac/trellis/DMRNode.java
Normal 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()];
|
||||
}
|
||||
}
|
||||
77
src/edac/trellis/Node.java
Normal file
77
src/edac/trellis/Node.java
Normal 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
131
src/edac/trellis/Path.java
Normal 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);
|
||||
}
|
||||
}
|
||||
226
src/edac/trellis/ViterbiDecoder.java
Normal file
226
src/edac/trellis/ViterbiDecoder.java
Normal 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;
|
||||
}
|
||||
}
|
||||
159
src/edac/trellis/ViterbiDecoder_3_4_DMR.java
Normal file
159
src/edac/trellis/ViterbiDecoder_3_4_DMR.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -151,41 +151,4 @@ public class RadioReferenceService
|
||||
|
||||
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!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import icon.IconManager;
|
||||
import map.MapService;
|
||||
import module.log.EventLogManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import playlist.PlaylistManager;
|
||||
@@ -80,16 +81,16 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
|
||||
public SDRTrunk()
|
||||
{
|
||||
mLog.info("");
|
||||
mLog.info("*******************************************************************");
|
||||
mLog.info("**** sdrtrunk: a trunked radio and digital decoding application ***");
|
||||
mLog.info("**** website: https://github.com/dsheirer/sdrtrunk ***");
|
||||
mLog.info("*******************************************************************");
|
||||
mLog.info("");
|
||||
mLog.info("Host CPU Cores: " + Runtime.getRuntime().availableProcessors());
|
||||
mLog.info("Host Memory Total: " + Runtime.getRuntime().totalMemory());
|
||||
mLog.info("Host Memory Max: " + Runtime.getRuntime().maxMemory());
|
||||
mLog.info("Host Memory Free: " + Runtime.getRuntime().freeMemory());
|
||||
mLog.info("Memory Logging Format: [Used/Allocated PercentUsed%]");
|
||||
mLog.info("Host CPU Cores: " + Runtime.getRuntime().availableProcessors());
|
||||
mLog.info("Host OS Name: " + System.getProperty("os.name"));
|
||||
mLog.info("Host OS Arch: " + System.getProperty("os.arch"));
|
||||
mLog.info("Host OS Version: " + System.getProperty("os.version"));
|
||||
mLog.info("Host Max Java Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().maxMemory()));
|
||||
|
||||
//Setup the application home directory
|
||||
Path home = getHomePath();
|
||||
|
||||
@@ -24,16 +24,17 @@ import audio.IAudioPacketProvider;
|
||||
import audio.squelch.ISquelchStateListener;
|
||||
import audio.squelch.ISquelchStateProvider;
|
||||
import audio.squelch.SquelchState;
|
||||
import channel.heartbeat.Heartbeat;
|
||||
import channel.heartbeat.IHeartbeatListener;
|
||||
import channel.heartbeat.IHeartbeatProvider;
|
||||
import channel.metadata.AttributeChangeRequest;
|
||||
import channel.metadata.IAttributeChangeRequestListener;
|
||||
import channel.metadata.IAttributeChangeRequestProvider;
|
||||
import channel.state.ChannelState;
|
||||
import channel.state.DecoderState;
|
||||
import channel.state.DecoderStateEvent;
|
||||
import channel.state.DecoderStateEvent.Event;
|
||||
import channel.state.IDecoderStateEventListener;
|
||||
import channel.state.IDecoderStateEventProvider;
|
||||
import channel.state.State;
|
||||
import controller.channel.Channel.ChannelType;
|
||||
import controller.channel.ChannelEvent;
|
||||
import controller.channel.IChannelEventListener;
|
||||
@@ -63,8 +64,6 @@ import sample.real.RealBuffer;
|
||||
import source.ComplexSource;
|
||||
import source.RealSource;
|
||||
import source.Source;
|
||||
import source.SourceException;
|
||||
import source.tuner.TunerChannelSource;
|
||||
import source.tuner.frequency.FrequencyChangeEvent;
|
||||
import source.tuner.frequency.IFrequencyChangeListener;
|
||||
import source.tuner.frequency.IFrequencyChangeProvider;
|
||||
@@ -103,6 +102,7 @@ public class ProcessingChain implements IChannelEventListener
|
||||
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<DecoderStateEvent> mDecoderStateEventBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<FrequencyChangeEvent> mFrequencyChangeEventBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<Heartbeat> mHeartbeatBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<Message> mMessageBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<RealBuffer> mFilteredRealBufferBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<RealBuffer> mUnFilteredRealBufferBroadcaster = new Broadcaster<>();
|
||||
@@ -329,6 +329,11 @@ public class ProcessingChain implements IChannelEventListener
|
||||
}
|
||||
}
|
||||
|
||||
if(module instanceof IHeartbeatListener)
|
||||
{
|
||||
mHeartbeatBroadcaster.addListener(((IHeartbeatListener)module).getHeartbeatListener());
|
||||
}
|
||||
|
||||
if(module instanceof IMessageListener)
|
||||
{
|
||||
mMessageBroadcaster.addListener(((IMessageListener) module).getMessageListener());
|
||||
@@ -391,6 +396,11 @@ public class ProcessingChain implements IChannelEventListener
|
||||
mFrequencyChangeEventBroadcaster.removeListener(((IFrequencyChangeListener) module).getFrequencyChangeListener());
|
||||
}
|
||||
|
||||
if(module instanceof IHeartbeatListener)
|
||||
{
|
||||
mHeartbeatBroadcaster.removeListener(((IHeartbeatListener)module).getHeartbeatListener());
|
||||
}
|
||||
|
||||
if(module instanceof IMessageListener)
|
||||
{
|
||||
mMessageBroadcaster.removeListener(((IMessageListener) module).getMessageListener());
|
||||
@@ -448,6 +458,11 @@ public class ProcessingChain implements IChannelEventListener
|
||||
((IFrequencyChangeProvider) module).setFrequencyChangeListener(mFrequencyChangeEventBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof IHeartbeatProvider)
|
||||
{
|
||||
((IHeartbeatProvider)module).setHeartbeatListener(mHeartbeatBroadcaster);
|
||||
}
|
||||
|
||||
if(module instanceof IMessageProvider)
|
||||
{
|
||||
((IMessageProvider) module).setMessageListener(mMessageBroadcaster);
|
||||
@@ -505,6 +520,11 @@ public class ProcessingChain implements IChannelEventListener
|
||||
((IFrequencyChangeProvider) module).setFrequencyChangeListener(null);
|
||||
}
|
||||
|
||||
if(module instanceof IHeartbeatProvider)
|
||||
{
|
||||
((IHeartbeatProvider)module).removeHeartbeatListener();
|
||||
}
|
||||
|
||||
if(module instanceof IMessageProvider)
|
||||
{
|
||||
((IMessageProvider) module).setMessageListener(null);
|
||||
|
||||
@@ -421,7 +421,7 @@ public class P25DecoderState extends DecoderState
|
||||
mCurrentCallEvent = new P25CallEvent.Builder(CallEventType.CALL)
|
||||
.aliasList(getAliasList())
|
||||
.channel(mCurrentChannel)
|
||||
.details((hdu.isEncrypted() ? "ENCRYPTED " : ""))
|
||||
.details((hdu.isEncryptedAudio() ? "ENCRYPTED " : ""))
|
||||
.frequency(mCurrentChannelFrequency)
|
||||
.to(to)
|
||||
.build();
|
||||
@@ -1107,7 +1107,7 @@ public class P25DecoderState extends DecoderState
|
||||
broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT)
|
||||
.aliasList(getAliasList())
|
||||
.channel(gvcu.getChannelA())
|
||||
.details((gvcu.isEncrypted() ? "ENCRYPTED" : ""))
|
||||
.details((gvcu.isEncryptedLinkControlWord() ? "ENCRYPTED" : ""))
|
||||
.frequency(gvcu.getDownlinkFrequencyA())
|
||||
.to(userA)
|
||||
.build());
|
||||
@@ -1122,7 +1122,7 @@ public class P25DecoderState extends DecoderState
|
||||
broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT)
|
||||
.aliasList(getAliasList())
|
||||
.channel(gvcu.getChannelB())
|
||||
.details((gvcu.isEncrypted() ? "ENCRYPTED" : ""))
|
||||
.details((gvcu.isEncryptedLinkControlWord() ? "ENCRYPTED" : ""))
|
||||
.frequency(gvcu.getDownlinkFrequencyB())
|
||||
.to(userB)
|
||||
.build());
|
||||
@@ -1151,7 +1151,7 @@ public class P25DecoderState extends DecoderState
|
||||
broadcast(new P25CallEvent.Builder(CallEventType.CALL_DETECT)
|
||||
.aliasList(getAliasList())
|
||||
.channel(gvcue.getTransmitChannel())
|
||||
.details((gvcue.isEncrypted() ? "ENCRYPTED" : ""))
|
||||
.details((gvcue.isEncryptedLinkControlWord() ? "ENCRYPTED" : ""))
|
||||
.frequency(gvcue.getDownlinkFrequency())
|
||||
.to(group)
|
||||
.build());
|
||||
@@ -1187,7 +1187,7 @@ public class P25DecoderState extends DecoderState
|
||||
if(mCurrentCallEvent.getDetails() == null)
|
||||
{
|
||||
mCurrentCallEvent.setDetails(
|
||||
(gvcuser.isEncrypted() ? "ENCRYPTED " : "") +
|
||||
(gvcuser.isEncryptedLinkControlWord() ? "ENCRYPTED " : "") +
|
||||
(gvcuser.isEmergency() ? "EMERGENCY " : ""));
|
||||
|
||||
broadcast(mCurrentCallEvent);
|
||||
@@ -1399,7 +1399,7 @@ public class P25DecoderState extends DecoderState
|
||||
if(mCurrentCallEvent.getDetails() == null)
|
||||
{
|
||||
mCurrentCallEvent.setDetails(
|
||||
(tivcu.isEncrypted() ? "ENCRYPTED " : "") +
|
||||
(tivcu.isEncryptedLinkControlWord() ? "ENCRYPTED " : "") +
|
||||
(tivcu.isEmergency() ? "EMERGENCY " : ""));
|
||||
|
||||
broadcast(mCurrentCallEvent);
|
||||
@@ -1482,7 +1482,7 @@ public class P25DecoderState extends DecoderState
|
||||
if(mCurrentCallEvent.getDetails() == null)
|
||||
{
|
||||
mCurrentCallEvent.setDetails(
|
||||
(uuvcu.isEncrypted() ? "ENCRYPTED " : "") +
|
||||
(uuvcu.isEncryptedLinkControlWord() ? "ENCRYPTED " : "") +
|
||||
(uuvcu.isEmergency() ? "EMERGENCY " : ""));
|
||||
|
||||
broadcast(mCurrentCallEvent);
|
||||
@@ -1713,33 +1713,41 @@ public class P25DecoderState extends DecoderState
|
||||
.build());
|
||||
break;
|
||||
case SNDCP_RF_CONFIRMED_DATA:
|
||||
SNDCPUserData sud = (SNDCPUserData)pduc;
|
||||
|
||||
StringBuilder sbFrom = new StringBuilder();
|
||||
StringBuilder sbTo = new StringBuilder();
|
||||
|
||||
sbFrom.append(sud.getSourceIPAddress());
|
||||
sbTo.append(sud.getDestinationIPAddress());
|
||||
|
||||
if(sud.getIPProtocol() == IPProtocol.UDP)
|
||||
if(pduc instanceof SNDCPUserData)
|
||||
{
|
||||
sbFrom.append(":");
|
||||
sbFrom.append(sud.getUDPSourcePort());
|
||||
sbTo.append(":");
|
||||
sbTo.append(sud.getUDPDestinationPort());
|
||||
SNDCPUserData sud = (SNDCPUserData)pduc;
|
||||
|
||||
StringBuilder sbFrom = new StringBuilder();
|
||||
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;
|
||||
case SNDCP_RF_UNCONFIRMED_DATA:
|
||||
break;
|
||||
|
||||
@@ -12,6 +12,9 @@ import jmbe.iface.AudioConverter;
|
||||
import message.IMessageListener;
|
||||
import message.Message;
|
||||
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 org.slf4j.Logger;
|
||||
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 static final String IMBE_CODEC = "IMBE";
|
||||
|
||||
private static boolean mLibraryLoadStatusLogged = false;
|
||||
|
||||
private boolean mCanConvertAudio = false;
|
||||
private boolean mEncryptedCall = false;
|
||||
private boolean mEncryptedCallStateEstablished = false;
|
||||
|
||||
private AudioConverter mAudioConverter;
|
||||
private Listener<AudioPacket> mAudioPacketListener;
|
||||
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f);
|
||||
private Metadata mMetadata;
|
||||
private LDU1Message mCachedLDU1Message = null;
|
||||
|
||||
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
|
||||
* audio message contains 9 imbe voice frames. Each frame is converted to
|
||||
* audio when the JMBE library is loaded, processed by auto-gain, and
|
||||
* broadcast to the audio packet listener.
|
||||
* Processes call header (HDU) and voice frame (LDU1/LDU2) messages to decode audio and to determine the
|
||||
* encrypted audio status of a call event. Only the HDU and LDU2 messages convey encrypted call status. If an
|
||||
* LDU1 message is received without a preceding HDU message, then the LDU1 message is cached until the first
|
||||
* 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)
|
||||
{
|
||||
if (mCanConvertAudio && mAudioPacketListener != null)
|
||||
if(mCanConvertAudio && mAudioPacketListener != null)
|
||||
{
|
||||
if (message instanceof LDUMessage)
|
||||
if(mEncryptedCallStateEstablished)
|
||||
{
|
||||
LDUMessage ldu = (LDUMessage) message;
|
||||
|
||||
/* Only process unencrypted audio frames */
|
||||
if (!(ldu.isValid() && ldu.isEncrypted()))
|
||||
if(message instanceof LDUMessage)
|
||||
{
|
||||
for (byte[] frame : ldu.getIMBEFrames())
|
||||
{
|
||||
float[] audio = mAudioConverter.decode(frame);
|
||||
|
||||
audio = mGain.apply(audio);
|
||||
|
||||
mAudioPacketListener.receive(new AudioPacket(audio, mMetadata.copyOf()));
|
||||
}
|
||||
processAudio((LDUMessage)message);
|
||||
}
|
||||
}
|
||||
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")
|
||||
Class temp = Class.forName("jmbe.JMBEAudioLibrary");
|
||||
|
||||
library = (AudioConversionLibrary) temp.newInstance();
|
||||
library = (AudioConversionLibrary)temp.newInstance();
|
||||
|
||||
if ((library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 &&
|
||||
library.getBuildVersion() >= 3) || library.getMajorVersion() >= 1)
|
||||
if((library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 &&
|
||||
library.getBuildVersion() >= 3) || library.getMajorVersion() >= 1)
|
||||
{
|
||||
mAudioConverter = library.getAudioConverter(IMBE_CODEC, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO);
|
||||
|
||||
if (mAudioConverter != null)
|
||||
if(mAudioConverter != null)
|
||||
{
|
||||
mCanConvertAudio = true;
|
||||
|
||||
if (!mLibraryLoadStatusLogged)
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("JMBE audio conversion library [");
|
||||
@@ -144,7 +192,7 @@ public class P25AudioModule extends Module implements Listener<Message>, IAudioP
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!mLibraryLoadStatusLogged)
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
mLog.info("JMBE audio conversion library NOT FOUND");
|
||||
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());
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException e1)
|
||||
catch(ClassNotFoundException e1)
|
||||
{
|
||||
if (!mLibraryLoadStatusLogged)
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
mLog.error("Couldn't find/load JMBE audio conversion library");
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
}
|
||||
catch (InstantiationException e1)
|
||||
catch(InstantiationException e1)
|
||||
{
|
||||
if (!mLibraryLoadStatusLogged)
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
mLog.error("Couldn't instantiate JMBE audio conversion library class");
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
}
|
||||
catch (IllegalAccessException e1)
|
||||
catch(IllegalAccessException e1)
|
||||
{
|
||||
if (!mLibraryLoadStatusLogged)
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library due to "
|
||||
+ "security restrictions");
|
||||
+ "security restrictions");
|
||||
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
|
||||
* doesn't have a squelch state. If there are IMBE audio frames, we have
|
||||
* audio. We use this listener to signal to the recorder manager that the
|
||||
* channel state is resetting and it should end the recording.
|
||||
* Wrapper for squelch state to process end of call actions. At call end the encrypted call state established
|
||||
* flag is reset so that the encrypted audio state for the next call can be properly detected and we send an
|
||||
* END audio packet so that downstream processors like the audio recorder can properly close out a call sequence.
|
||||
*/
|
||||
public class SquelchStateListener implements Listener<SquelchState>
|
||||
{
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ public class HDUMessage extends P25Message
|
||||
return Encryption.fromValue( mMessage.getInt( ALGORITHM_ID ) );
|
||||
}
|
||||
|
||||
public boolean isEncrypted()
|
||||
public boolean isEncryptedAudio()
|
||||
{
|
||||
return getEncryption() != Encryption.UNENCRYPTED;
|
||||
}
|
||||
|
||||
@@ -1,244 +1,246 @@
|
||||
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 bits.BinaryMessage;
|
||||
import edac.CRC;
|
||||
import edac.Hamming10;
|
||||
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
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( LDU1Message.class );
|
||||
|
||||
public static final int ENCRYPTION_FLAG = 352;
|
||||
public static final int IMPLICIT_VENDOR_FLAG = 353;
|
||||
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[] GOLAY_WORD_STARTS = {
|
||||
352,362,372,382,536,546,556,566,720,730,740,750,904,914,924,934,1088,
|
||||
1098,1108,1118,1272,1282,1292,1302 };
|
||||
private final static Logger mLog = LoggerFactory.getLogger(LDU1Message.class);
|
||||
|
||||
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_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_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_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_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_10 = { 740,741,742,743,744,745 };
|
||||
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_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_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_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_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_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_11 = { 1302,1303,1304,1305,1306,1307 };
|
||||
public static final int ENCRYPTED_LINK_CONTROL_WORD_FLAG = 352;
|
||||
public static final int IMPLICIT_VENDOR_FLAG = 353;
|
||||
public static final int[] OPCODE = {354, 355, 356, 357, 362, 363};
|
||||
public static final int[] VENDOR = {364, 365, 366, 367, 372, 373, 374, 375};
|
||||
|
||||
/* Reed-Solomon(24,12,13) code protects the link control word. Maximum
|
||||
* correctable errors are: Hamming Distance(13) / 2 = 6 */
|
||||
public static final ReedSolomon_63_47_17 mReedSolomonDecoder =
|
||||
new ReedSolomon_63_47_17( 6 );
|
||||
public static final int[] GOLAY_WORD_STARTS = {
|
||||
352, 362, 372, 382, 536, 546, 556, 566, 720, 730, 740, 750, 904, 914, 924, 934, 1088,
|
||||
1098, 1108, 1118, 1272, 1282, 1292, 1302};
|
||||
|
||||
public LDU1Message( BinaryMessage message, DataUnitID duid,
|
||||
AliasList aliasList )
|
||||
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_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_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_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_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_10 = {740, 741, 742, 743, 744, 745};
|
||||
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_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_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_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_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_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_11 = {1302, 1303, 1304, 1305, 1306, 1307};
|
||||
|
||||
/* Reed-Solomon(24,12,13) code protects the link control word. Maximum
|
||||
* correctable errors are: Hamming Distance(13) / 2 = 6 */
|
||||
public static final ReedSolomon_63_47_17 mReedSolomonDecoder =
|
||||
new ReedSolomon_63_47_17(6);
|
||||
|
||||
public LDU1Message(BinaryMessage message, DataUnitID duid,
|
||||
AliasList aliasList)
|
||||
{
|
||||
super( message, duid, aliasList );
|
||||
super(message, duid, aliasList);
|
||||
|
||||
|
||||
/* NID CRC is checked in the message framer, thus a constructed message
|
||||
* means it passed the CRC */
|
||||
mCRC = new CRC[ 3 ];
|
||||
mCRC[ 0 ] = CRC.PASSED;
|
||||
|
||||
checkCRC();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclass constructor from an existing LDU1Message
|
||||
*/
|
||||
protected LDU1Message( LDU1Message message )
|
||||
{
|
||||
super( message.getSourceMessage(), DataUnitID.LDU1, message.getAliasList() );
|
||||
|
||||
mCRC = message.mCRC;
|
||||
}
|
||||
* means it passed the CRC */
|
||||
mCRC = new CRC[3];
|
||||
mCRC[0] = CRC.PASSED;
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted()
|
||||
{
|
||||
return mMessage.get( ENCRYPTION_FLAG );
|
||||
}
|
||||
|
||||
private void checkCRC()
|
||||
{
|
||||
mCRC[ 1 ] = CRC.PASSED;
|
||||
checkCRC();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclass constructor from an existing LDU1Message
|
||||
*/
|
||||
protected LDU1Message(LDU1Message message)
|
||||
{
|
||||
super(message.getSourceMessage(), DataUnitID.LDU1, message.getAliasList());
|
||||
|
||||
mCRC = message.mCRC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the contents of the Link Control Word are encrypted.
|
||||
*
|
||||
* @return true if the contents of the LCW are encrypted.
|
||||
*/
|
||||
public boolean isEncryptedLinkControlWord()
|
||||
{
|
||||
return mMessage.get(ENCRYPTED_LINK_CONTROL_WORD_FLAG);
|
||||
}
|
||||
|
||||
private void checkCRC()
|
||||
{
|
||||
mCRC[1] = CRC.PASSED;
|
||||
|
||||
/* Hamming( 10,6,3 ) error detection and correction */
|
||||
for( int index: GOLAY_WORD_STARTS )
|
||||
{
|
||||
int errors = Hamming10.checkAndCorrect( mMessage, index );
|
||||
|
||||
if( errors > 1 )
|
||||
{
|
||||
mCRC[ 1 ] = CRC.FAILED_CRC;
|
||||
}
|
||||
}
|
||||
for(int index : GOLAY_WORD_STARTS)
|
||||
{
|
||||
int errors = Hamming10.checkAndCorrect(mMessage, index);
|
||||
|
||||
if(errors > 1)
|
||||
{
|
||||
mCRC[1] = CRC.FAILED_CRC;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reed-Solomon( 24,16,9 ) error detection and correction
|
||||
* Check the Reed-Solomon parity bits. The RS decoder expects the code
|
||||
* words and reed solomon parity hex codewords in reverse order.
|
||||
*
|
||||
* Since this is a truncated RS(63) codes, we pad the code with zeros */
|
||||
int[] input = new int [63];
|
||||
int[] output = new int [63];
|
||||
|
||||
input[ 0 ] = mMessage.getInt( RS_HEX_11 );
|
||||
input[ 1 ] = mMessage.getInt( RS_HEX_10 );
|
||||
input[ 2 ] = mMessage.getInt( RS_HEX_9 );
|
||||
input[ 3 ] = mMessage.getInt( RS_HEX_8 );
|
||||
input[ 4 ] = mMessage.getInt( RS_HEX_7 );
|
||||
input[ 5 ] = mMessage.getInt( RS_HEX_6 );
|
||||
input[ 6 ] = mMessage.getInt( RS_HEX_5 );
|
||||
input[ 7 ] = mMessage.getInt( RS_HEX_4 );
|
||||
input[ 8 ] = mMessage.getInt( RS_HEX_3 );
|
||||
input[ 9 ] = mMessage.getInt( RS_HEX_2 );
|
||||
input[ 10 ] = mMessage.getInt( RS_HEX_1 );
|
||||
input[ 11 ] = mMessage.getInt( RS_HEX_0 );
|
||||
|
||||
input[ 12 ] = mMessage.getInt( CW_HEX_11 );
|
||||
input[ 13 ] = mMessage.getInt( CW_HEX_10 );
|
||||
input[ 14 ] = mMessage.getInt( CW_HEX_9 );
|
||||
input[ 15 ] = mMessage.getInt( CW_HEX_8 );
|
||||
input[ 16 ] = mMessage.getInt( CW_HEX_7 );
|
||||
input[ 17 ] = mMessage.getInt( CW_HEX_6 );
|
||||
input[ 18 ] = mMessage.getInt( CW_HEX_5 );
|
||||
input[ 19 ] = mMessage.getInt( CW_HEX_4 );
|
||||
input[ 20 ] = mMessage.getInt( CW_HEX_3 );
|
||||
input[ 21 ] = mMessage.getInt( CW_HEX_2 );
|
||||
input[ 22 ] = mMessage.getInt( CW_HEX_1 );
|
||||
input[ 23 ] = mMessage.getInt( CW_HEX_0 );
|
||||
int[] input = new int[63];
|
||||
int[] output = new int[63];
|
||||
|
||||
input[0] = mMessage.getInt(RS_HEX_11);
|
||||
input[1] = mMessage.getInt(RS_HEX_10);
|
||||
input[2] = mMessage.getInt(RS_HEX_9);
|
||||
input[3] = mMessage.getInt(RS_HEX_8);
|
||||
input[4] = mMessage.getInt(RS_HEX_7);
|
||||
input[5] = mMessage.getInt(RS_HEX_6);
|
||||
input[6] = mMessage.getInt(RS_HEX_5);
|
||||
input[7] = mMessage.getInt(RS_HEX_4);
|
||||
input[8] = mMessage.getInt(RS_HEX_3);
|
||||
input[9] = mMessage.getInt(RS_HEX_2);
|
||||
input[10] = mMessage.getInt(RS_HEX_1);
|
||||
input[11] = mMessage.getInt(RS_HEX_0);
|
||||
|
||||
input[12] = mMessage.getInt(CW_HEX_11);
|
||||
input[13] = mMessage.getInt(CW_HEX_10);
|
||||
input[14] = mMessage.getInt(CW_HEX_9);
|
||||
input[15] = mMessage.getInt(CW_HEX_8);
|
||||
input[16] = mMessage.getInt(CW_HEX_7);
|
||||
input[17] = mMessage.getInt(CW_HEX_6);
|
||||
input[18] = mMessage.getInt(CW_HEX_5);
|
||||
input[19] = mMessage.getInt(CW_HEX_4);
|
||||
input[20] = mMessage.getInt(CW_HEX_3);
|
||||
input[21] = mMessage.getInt(CW_HEX_2);
|
||||
input[22] = mMessage.getInt(CW_HEX_1);
|
||||
input[23] = mMessage.getInt(CW_HEX_0);
|
||||
/* 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
|
||||
{
|
||||
/* Since we've detected that we can correct the RS words, mark the
|
||||
* hamming crc as corrected as well, if it is currently failed */
|
||||
if( mCRC[ 1 ] == CRC.FAILED_CRC )
|
||||
{
|
||||
mCRC[ 1 ] = CRC.CORRECTED;
|
||||
}
|
||||
|
||||
mCRC[ 2 ] = CRC.PASSED;
|
||||
if(mCRC[1] == CRC.FAILED_CRC)
|
||||
{
|
||||
mCRC[1] = CRC.CORRECTED;
|
||||
}
|
||||
|
||||
mCRC[2] = CRC.PASSED;
|
||||
|
||||
/* Only fix the codeword data hex codewords */
|
||||
repairHexCodeword( input, output, 12, CW_HEX_11 );
|
||||
repairHexCodeword( input, output, 13, CW_HEX_10 );
|
||||
repairHexCodeword( input, output, 14, CW_HEX_9 );
|
||||
repairHexCodeword( input, output, 15, CW_HEX_8 );
|
||||
repairHexCodeword( input, output, 16, CW_HEX_7 );
|
||||
repairHexCodeword( input, output, 17, CW_HEX_6 );
|
||||
repairHexCodeword( input, output, 18, CW_HEX_5 );
|
||||
repairHexCodeword( input, output, 19, CW_HEX_4 );
|
||||
repairHexCodeword( input, output, 20, CW_HEX_3 );
|
||||
repairHexCodeword( input, output, 21, CW_HEX_2 );
|
||||
repairHexCodeword( input, output, 22, CW_HEX_1 );
|
||||
repairHexCodeword( input, output, 23, CW_HEX_0 );
|
||||
repairHexCodeword(input, output, 12, CW_HEX_11);
|
||||
repairHexCodeword(input, output, 13, CW_HEX_10);
|
||||
repairHexCodeword(input, output, 14, CW_HEX_9);
|
||||
repairHexCodeword(input, output, 15, CW_HEX_8);
|
||||
repairHexCodeword(input, output, 16, CW_HEX_7);
|
||||
repairHexCodeword(input, output, 17, CW_HEX_6);
|
||||
repairHexCodeword(input, output, 18, CW_HEX_5);
|
||||
repairHexCodeword(input, output, 19, CW_HEX_4);
|
||||
repairHexCodeword(input, output, 20, CW_HEX_3);
|
||||
repairHexCodeword(input, output, 21, CW_HEX_2);
|
||||
repairHexCodeword(input, output, 22, CW_HEX_1);
|
||||
repairHexCodeword(input, output, 23, CW_HEX_0);
|
||||
}
|
||||
}
|
||||
|
||||
private void repairHexCodeword( int[] input, int[] output, int index, int[] indexSet )
|
||||
{
|
||||
if( input[ index ] != output[ index ] )
|
||||
{
|
||||
mMessage.load( indexSet[ 0 ], 6, output[ index ] );
|
||||
mCRC[ 2 ] = CRC.CORRECTED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( getMessageStub() );
|
||||
sb.append( " " );
|
||||
sb.append( mMessage.toString() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String getMessageStub()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( super.getMessageStub() );
|
||||
|
||||
if( isEncrypted() )
|
||||
{
|
||||
sb.append( "ENCRYPTED" );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( isImplicitFormat() || getVendor() == Vendor.STANDARD )
|
||||
{
|
||||
sb.append( getOpcode().getLabel() );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "VENDOR:" + getVendor().getLabel() + " " );
|
||||
sb.append( getVendorOpcode().getLabel() );
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public boolean isImplicitFormat()
|
||||
{
|
||||
return mMessage.get( IMPLICIT_VENDOR_FLAG );
|
||||
}
|
||||
|
||||
public LinkControlOpcode getOpcode()
|
||||
{
|
||||
return LinkControlOpcode.fromValue( mMessage.getInt( OPCODE ) );
|
||||
}
|
||||
private void repairHexCodeword(int[] input, int[] output, int index, int[] indexSet)
|
||||
{
|
||||
if(input[index] != output[index])
|
||||
{
|
||||
mMessage.load(indexSet[0], 6, output[index]);
|
||||
mCRC[2] = CRC.CORRECTED;
|
||||
}
|
||||
}
|
||||
|
||||
public VendorLinkControlOpcode getVendorOpcode()
|
||||
{
|
||||
return VendorLinkControlOpcode.fromValue( mMessage.getInt( OPCODE ) );
|
||||
}
|
||||
|
||||
public Vendor getVendor()
|
||||
{
|
||||
if( isImplicitFormat() )
|
||||
{
|
||||
return Vendor.STANDARD;
|
||||
}
|
||||
|
||||
return Vendor.fromValue( mMessage.getInt( VENDOR ) );
|
||||
}
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append(getMessageStub());
|
||||
sb.append(" ");
|
||||
sb.append(mMessage.toString());
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String getMessageStub()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append(super.getMessageStub());
|
||||
|
||||
if(isEncryptedLinkControlWord())
|
||||
{
|
||||
sb.append("ENCRYPTED");
|
||||
}
|
||||
else
|
||||
{
|
||||
if(isImplicitFormat() || getVendor() == Vendor.STANDARD)
|
||||
{
|
||||
sb.append(getOpcode().getLabel());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("VENDOR:" + getVendor().getLabel() + " ");
|
||||
sb.append(getVendorOpcode().getLabel());
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public boolean isImplicitFormat()
|
||||
{
|
||||
return mMessage.get(IMPLICIT_VENDOR_FLAG);
|
||||
}
|
||||
|
||||
public LinkControlOpcode getOpcode()
|
||||
{
|
||||
return LinkControlOpcode.fromValue(mMessage.getInt(OPCODE));
|
||||
}
|
||||
|
||||
public VendorLinkControlOpcode getVendorOpcode()
|
||||
{
|
||||
return VendorLinkControlOpcode.fromValue(mMessage.getInt(OPCODE));
|
||||
}
|
||||
|
||||
public Vendor getVendor()
|
||||
{
|
||||
if(isImplicitFormat())
|
||||
{
|
||||
return Vendor.STANDARD;
|
||||
}
|
||||
|
||||
return Vendor.fromValue(mMessage.getInt(VENDOR));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +72,11 @@ public class LDU2Message extends LDUMessage
|
||||
|
||||
checkCRC();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted()
|
||||
|
||||
/**
|
||||
* Indicates if the audio stream is encrypted
|
||||
*/
|
||||
public boolean isEncryptedAudio()
|
||||
{
|
||||
Encryption encryption = getEncryption();
|
||||
|
||||
|
||||
@@ -35,8 +35,6 @@ public abstract class LDUMessage extends P25Message
|
||||
super( message, duid, aliasList );
|
||||
}
|
||||
|
||||
public abstract boolean isEncrypted();
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
|
||||
@@ -124,10 +124,6 @@ public class OverflowableTransferQueue<E>
|
||||
mQueue.clear();
|
||||
mCounter.set(0);
|
||||
mOverflow.set(false);
|
||||
if(mOverflowListener != null)
|
||||
{
|
||||
mOverflowListener.sourceOverflow(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
******************************************************************************/
|
||||
package source;
|
||||
|
||||
import channel.heartbeat.IHeartbeatProvider;
|
||||
import module.Module;
|
||||
import sample.SampleType;
|
||||
import sample.real.IOverflowListener;
|
||||
@@ -26,7 +27,8 @@ import source.tuner.frequency.IFrequencyChangeProvider;
|
||||
/**
|
||||
* Abstract class to define the minimum functionality of a sample data provider.
|
||||
*/
|
||||
public abstract class Source extends Module implements IFrequencyChangeListener, IFrequencyChangeProvider
|
||||
public abstract class Source extends Module implements IFrequencyChangeListener, IFrequencyChangeProvider,
|
||||
IHeartbeatProvider
|
||||
{
|
||||
protected SampleType mSampleType;
|
||||
protected IOverflowListener mOverflowListener;
|
||||
|
||||
@@ -1,174 +1,191 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 Dennis Sheirer
|
||||
*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package source.mixer;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
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 channel.heartbeat.Heartbeat;
|
||||
import dsp.gain.AutomaticGainControl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.Listener;
|
||||
import sample.adapter.ISampleAdapter;
|
||||
import sample.real.RealBuffer;
|
||||
import source.RealSource;
|
||||
import source.SourceException;
|
||||
import dsp.gain.AutomaticGainControl;
|
||||
import source.tuner.frequency.FrequencyChangeEvent;
|
||||
|
||||
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
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( RealMixerSource.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RealMixerSource.class);
|
||||
|
||||
private long mFrequency = 0;
|
||||
private int mBufferSize = 16384;
|
||||
|
||||
private BufferReader mBufferReader = new BufferReader();
|
||||
private TargetDataLine mTargetDataLine;
|
||||
private AudioFormat mAudioFormat;
|
||||
private int mBytesPerFrame = 0;
|
||||
private ISampleAdapter mSampleAdapter;
|
||||
private AutomaticGainControl mAGC = new AutomaticGainControl();
|
||||
|
||||
CopyOnWriteArrayList<Listener<RealBuffer>> mSampleListeners = new CopyOnWriteArrayList<Listener<RealBuffer>>();
|
||||
|
||||
/**
|
||||
* Real Mixer Source - constructs a reader on the mixer/sound card target
|
||||
* data line using the specified audio format (sample size, sample rate )
|
||||
* and broadcasts real buffers (float arrays) to all registered listeners.
|
||||
* Reads buffers sized to 10% of the sample rate specified in audio format.
|
||||
*
|
||||
* @param targetDataLine - mixer or sound card to be used
|
||||
*
|
||||
* @param format - audio format
|
||||
*
|
||||
* @param name - token name to use for this source
|
||||
*
|
||||
* @param sampleAdapter - adapter to convert byte array data read from the
|
||||
* mixer into float array data. Can optionally invert the channel data if
|
||||
* the left/right stereo channels are inverted.
|
||||
*/
|
||||
public RealMixerSource( TargetDataLine targetDataLine,
|
||||
AudioFormat format,
|
||||
ISampleAdapter sampleAdapter )
|
||||
private long mFrequency = 0;
|
||||
private int mBufferSize = 16384;
|
||||
|
||||
private BufferReader mBufferReader = new BufferReader();
|
||||
private TargetDataLine mTargetDataLine;
|
||||
private AudioFormat mAudioFormat;
|
||||
private int mBytesPerFrame = 0;
|
||||
private ISampleAdapter mSampleAdapter;
|
||||
private AutomaticGainControl mAGC = new AutomaticGainControl();
|
||||
private Listener<Heartbeat> mHeartbeatListener;
|
||||
private static final Heartbeat HEARTBEAT = new Heartbeat();
|
||||
|
||||
private CopyOnWriteArrayList<Listener<RealBuffer>> mSampleListeners = new CopyOnWriteArrayList<Listener<RealBuffer>>();
|
||||
|
||||
/**
|
||||
* Real Mixer Source - constructs a reader on the mixer/sound card target
|
||||
* data line using the specified audio format (sample size, sample rate )
|
||||
* and broadcasts real buffers (float arrays) to all registered listeners.
|
||||
* Reads buffers sized to 10% of the sample rate specified in audio format.
|
||||
*
|
||||
* @param targetDataLine - mixer or sound card to be used
|
||||
* @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
|
||||
* the left/right stereo channels are inverted.
|
||||
*/
|
||||
public RealMixerSource(TargetDataLine targetDataLine,
|
||||
AudioFormat format,
|
||||
ISampleAdapter sampleAdapter)
|
||||
{
|
||||
mTargetDataLine = targetDataLine;
|
||||
mTargetDataLine = targetDataLine;
|
||||
mAudioFormat = format;
|
||||
mSampleAdapter = sampleAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
//Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
@Override
|
||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
{
|
||||
mSampleListeners.add( listener );
|
||||
|
||||
/* If this is the first listener, start the reader thread */
|
||||
if( !mBufferReader.isRunning() )
|
||||
{
|
||||
Thread thread = new Thread( mBufferReader );
|
||||
thread.setDaemon( true );
|
||||
thread.setName( "Sample Reader" );
|
||||
thread.start();
|
||||
}
|
||||
//Not implemented
|
||||
}
|
||||
|
||||
public void removeListener( Listener<RealBuffer> listener )
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
mSampleListeners.remove( listener );
|
||||
//Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
//Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@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(!mBufferReader.isRunning())
|
||||
{
|
||||
Thread thread = new Thread(mBufferReader);
|
||||
thread.setDaemon(true);
|
||||
thread.setName("Sample Reader");
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeListener(Listener<RealBuffer> listener)
|
||||
{
|
||||
mSampleListeners.remove(listener);
|
||||
|
||||
/* If this is the laster listener, stop the reader thread */
|
||||
if( mSampleListeners.isEmpty() )
|
||||
{
|
||||
mBufferReader.stop();
|
||||
}
|
||||
if(mSampleListeners.isEmpty())
|
||||
{
|
||||
mBufferReader.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcast( RealBuffer samples )
|
||||
public void broadcast(RealBuffer samples)
|
||||
{
|
||||
Iterator<Listener<RealBuffer>> it = mSampleListeners.iterator();
|
||||
|
||||
while( it.hasNext() )
|
||||
{
|
||||
Listener<RealBuffer> next = it.next();
|
||||
Iterator<Listener<RealBuffer>> it = mSampleListeners.iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
Listener<RealBuffer> next = it.next();
|
||||
|
||||
/* if this is the last (or only) listener, send him the original
|
||||
* buffer, otherwise send him a copy of the buffer */
|
||||
if( it.hasNext() )
|
||||
{
|
||||
next.receive( samples.copyOf() );
|
||||
}
|
||||
else
|
||||
{
|
||||
next.receive( samples );
|
||||
}
|
||||
}
|
||||
if(it.hasNext())
|
||||
{
|
||||
next.receive(samples.copyOf());
|
||||
}
|
||||
else
|
||||
{
|
||||
next.receive(samples);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int getSampleRate()
|
||||
{
|
||||
if( mTargetDataLine != null )
|
||||
{
|
||||
return (int)mTargetDataLine.getFormat().getSampleRate();
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if(mTargetDataLine != null)
|
||||
{
|
||||
return (int)mTargetDataLine.getFormat().getSampleRate();
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +193,7 @@ public class RealMixerSource extends RealSource
|
||||
*/
|
||||
public long getFrequency() throws SourceException
|
||||
{
|
||||
return mFrequency;
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,122 +201,128 @@ public class RealMixerSource extends RealSource
|
||||
* 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
|
||||
*/
|
||||
public void setFrequency( long frequency )
|
||||
public void setFrequency(long frequency)
|
||||
{
|
||||
mFrequency = frequency;
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reader thread. Performs blocking read against the mixer target data
|
||||
* line, converts the samples to an array of floats using the specified
|
||||
* adapter. Dispatches float arrays to all registered listeners.
|
||||
*/
|
||||
public class BufferReader implements Runnable
|
||||
{
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
|
||||
@Override
|
||||
/**
|
||||
* Reader thread. Performs blocking read against the mixer target data
|
||||
* line, converts the samples to an array of floats using the specified
|
||||
* adapter. Dispatches float arrays to all registered listeners.
|
||||
*/
|
||||
public class BufferReader implements Runnable
|
||||
{
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if( mRunning.compareAndSet( false, true ) )
|
||||
{
|
||||
if( mTargetDataLine == null )
|
||||
{
|
||||
mRunning.set( false );
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
//Send a heartbeat every time we run
|
||||
if(mHeartbeatListener != null)
|
||||
{
|
||||
mHeartbeatListener.receive(HEARTBEAT);
|
||||
}
|
||||
|
||||
mLog.error( "ComplexMixerSource - mixer target data line"
|
||||
+ " is null" );
|
||||
}
|
||||
else
|
||||
{
|
||||
mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8;
|
||||
if(mTargetDataLine == null)
|
||||
{
|
||||
mRunning.set(false);
|
||||
|
||||
if( mBytesPerFrame == AudioSystem.NOT_SPECIFIED )
|
||||
{
|
||||
mBytesPerFrame = 2;
|
||||
}
|
||||
mLog.error("ComplexMixerSource - mixer target data line"
|
||||
+ " is null");
|
||||
}
|
||||
else
|
||||
{
|
||||
mBytesPerFrame = mAudioFormat.getSampleSizeInBits() / 8;
|
||||
|
||||
if(mBytesPerFrame == AudioSystem.NOT_SPECIFIED)
|
||||
{
|
||||
mBytesPerFrame = 2;
|
||||
}
|
||||
|
||||
/* Set buffer size to 1/10 second of samples */
|
||||
mBufferSize = (int)( mAudioFormat.getSampleRate()
|
||||
* 0.05 ) * mBytesPerFrame;
|
||||
mBufferSize = (int)(mAudioFormat.getSampleRate()
|
||||
* 0.05) * mBytesPerFrame;
|
||||
|
||||
/* We'll reuse the same buffer for each read */
|
||||
byte[] buffer = new byte[ mBufferSize ];
|
||||
|
||||
try
|
||||
{
|
||||
mTargetDataLine.open( mAudioFormat );
|
||||
byte[] buffer = new byte[mBufferSize];
|
||||
|
||||
mTargetDataLine.start();
|
||||
}
|
||||
catch ( LineUnavailableException e )
|
||||
try
|
||||
{
|
||||
mLog.error( "ComplexMixerSource - mixer target data line"
|
||||
+ "not available to read data from", e );
|
||||
|
||||
mRunning.set( false );
|
||||
mTargetDataLine.open(mAudioFormat);
|
||||
|
||||
mTargetDataLine.start();
|
||||
}
|
||||
|
||||
while( mRunning.get() )
|
||||
{
|
||||
try
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
mLog.error("ComplexMixerSource - mixer target data line"
|
||||
+ "not available to read data from", e);
|
||||
|
||||
mRunning.set(false);
|
||||
}
|
||||
|
||||
while(mRunning.get())
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Blocking read - waits until the buffer fills */
|
||||
mTargetDataLine.read( buffer, 0, buffer.length );
|
||||
mTargetDataLine.read(buffer, 0, buffer.length);
|
||||
|
||||
/* Convert samples to float array */
|
||||
float[] samples =
|
||||
mSampleAdapter.convert( buffer );
|
||||
float[] samples =
|
||||
mSampleAdapter.convert(buffer);
|
||||
|
||||
/* 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"
|
||||
+ "from the mixer target data line", e );
|
||||
|
||||
mRunning.set( false );
|
||||
mLog.error("ComplexMixerSource - error while reading"
|
||||
+ "from the mixer target data line", e);
|
||||
|
||||
mRunning.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Close the data line if it is still open */
|
||||
if( mTargetDataLine != null && mTargetDataLine.isOpen() )
|
||||
{
|
||||
mTargetDataLine.close();
|
||||
}
|
||||
}
|
||||
if(mTargetDataLine != null && mTargetDataLine.isOpen())
|
||||
{
|
||||
mTargetDataLine.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the reader thread
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if( mRunning.compareAndSet( true, false ) )
|
||||
{
|
||||
mTargetDataLine.stop();
|
||||
mTargetDataLine.close();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Stops the reader thread
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if(mRunning.compareAndSet(true, false))
|
||||
{
|
||||
mTargetDataLine.stop();
|
||||
mTargetDataLine.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the reader thread is running
|
||||
*/
|
||||
public boolean isRunning()
|
||||
{
|
||||
return mRunning.get();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Indicates if the reader thread is running
|
||||
*/
|
||||
public boolean isRunning()
|
||||
{
|
||||
return mRunning.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
if( mBufferReader != null )
|
||||
{
|
||||
mBufferReader.stop();
|
||||
mSampleListeners.clear();
|
||||
}
|
||||
if(mBufferReader != null)
|
||||
{
|
||||
mBufferReader.stop();
|
||||
mSampleListeners.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
******************************************************************************/
|
||||
package source.tuner;
|
||||
|
||||
import channel.heartbeat.Heartbeat;
|
||||
import channel.heartbeat.IHeartbeatProvider;
|
||||
import dsp.filter.FilterFactory;
|
||||
import dsp.filter.Window.WindowType;
|
||||
import dsp.filter.cic.ComplexPrimeCICDecimate;
|
||||
@@ -58,6 +60,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
|
||||
|
||||
private static int CHANNEL_RATE = 48000;
|
||||
private static int CHANNEL_PASS_FREQUENCY = 12000;
|
||||
private static final Heartbeat HEARTBEAT = new Heartbeat();
|
||||
|
||||
private OverflowableTransferQueue<ComplexBuffer> mBuffer;
|
||||
|
||||
@@ -66,6 +69,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
|
||||
private Oscillator mMixer;
|
||||
private ComplexPrimeCICDecimate mDecimationFilter;
|
||||
private Listener<ComplexBuffer> mListener;
|
||||
private Listener<Heartbeat> mHeartbeatListener;
|
||||
private IFrequencyChangeProcessor mFrequencyChangeProcessor;
|
||||
private DownstreamProcessor mDownstreamFrequencyEventProcessor = new DownstreamProcessor();
|
||||
private ScheduledFuture<?> mTaskHandle;
|
||||
@@ -352,6 +356,25 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
|
||||
return mDownstreamFrequencyEventProcessor.getFrequencyChangeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* components are those that receive samples from this tuner channel source. These downstream components will be
|
||||
@@ -477,6 +500,13 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
|
||||
{
|
||||
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);
|
||||
|
||||
for(Buffer buffer : mSampleBuffers)
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* 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 source.tuner;
|
||||
|
||||
import java.util.SortedSet;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import source.SourceException;
|
||||
import source.tuner.configuration.TunerConfiguration;
|
||||
import source.tuner.frequency.FrequencyChangeEvent;
|
||||
@@ -32,365 +27,344 @@ import source.tuner.frequency.FrequencyController;
|
||||
import source.tuner.frequency.FrequencyController.Tunable;
|
||||
import source.tuner.frequency.IFrequencyChangeProcessor;
|
||||
|
||||
import java.util.SortedSet;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
public abstract class TunerController implements Tunable, IFrequencyChangeProcessor
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( TunerController.class );
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TunerController.class);
|
||||
|
||||
/* List of currently tuned channels being served to demod channels */
|
||||
private SortedSet<TunerChannel> mTunedChannels = new ConcurrentSkipListSet<>();
|
||||
protected FrequencyController mFrequencyController;
|
||||
private int mMiddleUnusable;
|
||||
private double mUsableBandwidthPercentage;
|
||||
|
||||
/**
|
||||
* Abstract tuner controller class. The tuner controller manages frequency
|
||||
* bandwidth and currently tuned channels that are being fed samples from
|
||||
* the tuner.
|
||||
*
|
||||
* @param minimumFrequency minimum uncorrected tunable frequency
|
||||
* @param maximumFrequency maximum uncorrected tunable frequency
|
||||
* @param middleUnusable is the +/- value center DC spike to avoid for channels
|
||||
* @param usableBandwidthPercentage is the unusable space at the extreme ends of the spectrum
|
||||
*
|
||||
* @throws SourceException - for any issues related to constructing the
|
||||
* class, tuning a frequency, or setting the bandwidth
|
||||
*/
|
||||
public TunerController( long minimumFrequency,
|
||||
long maximumFrequency,
|
||||
int middleUnusable,
|
||||
double usableBandwidthPercentage )
|
||||
{
|
||||
mFrequencyController = new FrequencyController( this,
|
||||
minimumFrequency,
|
||||
maximumFrequency,
|
||||
0.0d );
|
||||
mMiddleUnusable = middleUnusable;
|
||||
mUsableBandwidthPercentage = usableBandwidthPercentage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the settings in the tuner configuration
|
||||
*/
|
||||
public abstract void apply( TunerConfiguration config )
|
||||
throws SourceException;
|
||||
/* List of currently tuned channels being served to demod channels */
|
||||
private SortedSet<TunerChannel> mTunedChannels = new ConcurrentSkipListSet<>();
|
||||
protected FrequencyController mFrequencyController;
|
||||
private int mMiddleUnusable;
|
||||
private double mUsableBandwidthPercentage;
|
||||
|
||||
/**
|
||||
* Responds to requests to set the frequency
|
||||
*/
|
||||
@Override
|
||||
public void frequencyChanged( FrequencyChangeEvent event ) throws SourceException
|
||||
{
|
||||
if( event.getEvent() == Event.REQUEST_FREQUENCY_CHANGE )
|
||||
{
|
||||
setFrequency( event.getValue().longValue() );
|
||||
}
|
||||
}
|
||||
|
||||
public int getBandwidth()
|
||||
/**
|
||||
* Abstract tuner controller class. The tuner controller manages frequency bandwidth and currently tuned channels
|
||||
* that are being fed samples from the tuner.
|
||||
*
|
||||
* @param minimumFrequency minimum uncorrected tunable frequency
|
||||
* @param maximumFrequency maximum uncorrected tunable frequency
|
||||
* @param middleUnusable is the +/- value center DC spike to avoid for channels
|
||||
* @param usableBandwidth percentage of usable bandwidth relative to space at the extreme ends of the spectrum
|
||||
* @throws SourceException - for any issues related to constructing the
|
||||
* class, tuning a frequency, or setting the bandwidth
|
||||
*/
|
||||
public TunerController(long minimumFrequency, long maximumFrequency, int middleUnusable, double usableBandwidth)
|
||||
{
|
||||
return mFrequencyController.getBandwidth();
|
||||
mFrequencyController = new FrequencyController(this, minimumFrequency, maximumFrequency, 0.0d);
|
||||
mMiddleUnusable = middleUnusable;
|
||||
mUsableBandwidthPercentage = usableBandwidth;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies the settings in the tuner configuration
|
||||
*/
|
||||
public abstract void apply(TunerConfiguration config) throws SourceException;
|
||||
|
||||
/**
|
||||
* Responds to requests to set the frequency
|
||||
*/
|
||||
@Override
|
||||
public void frequencyChanged(FrequencyChangeEvent event) throws SourceException
|
||||
{
|
||||
if(event.getEvent() == Event.REQUEST_FREQUENCY_CHANGE)
|
||||
{
|
||||
setFrequency(event.getValue().longValue());
|
||||
}
|
||||
}
|
||||
|
||||
public int getBandwidth()
|
||||
{
|
||||
return mFrequencyController.getBandwidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the center frequency of the local oscillator.
|
||||
*
|
||||
*
|
||||
* @param frequency in hertz
|
||||
* @throws SourceException - if the tuner has any issues
|
||||
*/
|
||||
public void setFrequency( long frequency ) throws SourceException
|
||||
{
|
||||
mFrequencyController.setFrequency( frequency );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the center frequency of the local oscillator
|
||||
*
|
||||
* @return frequency in hertz
|
||||
*/
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequencyController.getFrequency();
|
||||
}
|
||||
|
||||
public int getSampleRate()
|
||||
{
|
||||
return mFrequencyController.getBandwidth();
|
||||
}
|
||||
|
||||
public double getFrequencyCorrection()
|
||||
{
|
||||
return mFrequencyController.getFrequencyCorrection();
|
||||
}
|
||||
|
||||
public void setFrequencyCorrection( double correction ) throws SourceException
|
||||
{
|
||||
mFrequencyController.setFrequencyCorrection( correction );
|
||||
}
|
||||
|
||||
public long getMinFrequency()
|
||||
{
|
||||
return mFrequencyController.getMinimumFrequency();
|
||||
}
|
||||
|
||||
public long getMaxFrequency()
|
||||
{
|
||||
return mFrequencyController.getMaximumFrequency();
|
||||
}
|
||||
|
||||
private long getMinTunedFrequency() throws SourceException
|
||||
{
|
||||
return mFrequencyController.getFrequency() - ( getUsableBandwidth() / 2 );
|
||||
}
|
||||
|
||||
private long getMaxTunedFrequency() throws SourceException
|
||||
{
|
||||
return mFrequencyController.getFrequency() + ( getUsableBandwidth() / 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if channel along with all of the other currently sourced
|
||||
* channels can fit within the tunable bandwidth.
|
||||
*/
|
||||
private boolean canTune( TunerChannel channel )
|
||||
{
|
||||
//Make sure we're within the tunable frequency range of this tuner
|
||||
if( getMinFrequency() < channel.getMinFrequency() &&
|
||||
getMaxFrequency() > channel.getMaxFrequency() )
|
||||
{
|
||||
//If this is the first lock, then we're good
|
||||
if( mTunedChannels.isEmpty() )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
int usableBandwidth = getUsableBandwidth();
|
||||
long minLockedFrequency = mTunedChannels.first().getMinFrequency();
|
||||
long maxLockedFrequency = mTunedChannels.last().getMaxFrequency();
|
||||
|
||||
//Requested channel is within current locked channel frequency range
|
||||
if( minLockedFrequency <= channel.getMinFrequency() &&
|
||||
channel.getMaxFrequency() <= maxLockedFrequency )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Requested channel is higher than min locked frequency
|
||||
if( channel.getMaxFrequency() > minLockedFrequency &&
|
||||
channel.getMaxFrequency() - minLockedFrequency <= usableBandwidth )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
//Requested channel is lower than the max locked frequency
|
||||
if( channel.getMinFrequency() <= maxLockedFrequency &&
|
||||
maxLockedFrequency - channel.getMinFrequency() <= usableBandwidth )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a digital drop channel (DDC) as a tuner channel from from
|
||||
* the tuner, or returns null if the channel cannot be sourced from the
|
||||
* tuner.
|
||||
*
|
||||
* This controller can't provide a channel if the channel, along with the
|
||||
* current set of sourced channels, can't be accomodated within the current
|
||||
* tuner bandwidth, or if a center frequency cannot be calculated that will
|
||||
* ensure all of the channels can be accomodated and any defined central DC
|
||||
* spike avoided.
|
||||
*
|
||||
* @param tuner to source the channel from
|
||||
* @param channel with defined center frequency and bandwidth
|
||||
*
|
||||
* @return fully constructed tuner channel or null
|
||||
*
|
||||
* @throws RejectedExecutionException if the decimation processor has an error
|
||||
*/
|
||||
public TunerChannelSource getChannel( Tuner tuner, TunerChannel channel )
|
||||
throws RejectedExecutionException
|
||||
{
|
||||
TunerChannelSource source = null;
|
||||
|
||||
if( canTune( channel ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
mTunedChannels.add( channel );
|
||||
|
||||
if( requiresLOUpdate( channel ) )
|
||||
{
|
||||
updateLOFrequency();
|
||||
}
|
||||
|
||||
source = new TunerChannelSource( tuner, channel );
|
||||
}
|
||||
catch( SourceException se )
|
||||
{
|
||||
mTunedChannels.remove( channel );
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
public int getChannelCount()
|
||||
{
|
||||
return mTunedChannels.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the tuner's LO frequency must be updated in order to
|
||||
* accomodate the tuner channel
|
||||
*/
|
||||
private boolean requiresLOUpdate( TunerChannel channel ) throws SourceException
|
||||
{
|
||||
return !( getMinTunedFrequency() <= channel.getMinFrequency() &&
|
||||
channel.getMaxFrequency() <= getMaxTunedFrequency() &&
|
||||
( ( mMiddleUnusable == 0 ) ||
|
||||
( !channel.overlaps( getTunedFrequency() - mMiddleUnusable,
|
||||
getTunedFrequency() + mMiddleUnusable ) ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the currently sourced tuner channel from this tuner and shuts
|
||||
* down the tuner if no other sources exist.
|
||||
*/
|
||||
public void releaseChannel( TunerChannelSource tunerChannelSource )
|
||||
{
|
||||
if( tunerChannelSource != null )
|
||||
{
|
||||
mTunedChannels.remove( tunerChannelSource.getTunerChannel() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Local Oscillator frequency accomodate the current set of tuned
|
||||
* channels.
|
||||
*
|
||||
* If there is only a single tuned channel, it is placed immediately to the
|
||||
* right of the usable bandwidth right of the central DC spike.
|
||||
*
|
||||
* Otherwise, it places the highest channel frequency at the upper end of
|
||||
* the tuner bandwidth, and then iteratively moves the center frequency
|
||||
* higher until all channels fit within the bandwidth and none of the
|
||||
* channels overlap any defined central DC spike unusable region. If a
|
||||
* center tune frequency cannot be calculated, throw an exception so that
|
||||
* the most recently added channel can be removed.
|
||||
*
|
||||
* Note: the tuned frequency is not changed until a legitimate new frequency
|
||||
* can be calculated. If an exception is thrown, the current frequency is
|
||||
* retained, so that the recently added channel can be removed and all other
|
||||
* channels can continue as previously arranged.
|
||||
*
|
||||
* @throws SourceException if the set of tuner channels, including a recently
|
||||
* added channel, cannot be tuned within the current tuner bandwidth and any
|
||||
* central DC spike unusable region.
|
||||
*/
|
||||
private void updateLOFrequency() throws SourceException
|
||||
{
|
||||
long frequency = getFrequency();
|
||||
|
||||
boolean frequencyValid = true;
|
||||
|
||||
//If there is only 1 channel, position it to the right of center
|
||||
if( mTunedChannels.size() == 1 )
|
||||
{
|
||||
frequency = mTunedChannels.first().getMinFrequency() - mMiddleUnusable;
|
||||
}
|
||||
else
|
||||
{
|
||||
long minLockedFrequency = mTunedChannels.first().getMinFrequency();
|
||||
long maxLockedFrequency = mTunedChannels.last().getMaxFrequency();
|
||||
|
||||
//Start by placing the highest frequency channel at the high end of
|
||||
//the spectrum
|
||||
frequency = maxLockedFrequency - ( getUsableBandwidth() / 2 );
|
||||
|
||||
//Iterate the channels and make sure that none of them overlap the
|
||||
//center DC spike buffer, if one exists
|
||||
if( mMiddleUnusable > 0 )
|
||||
{
|
||||
boolean processingRequired = true;
|
||||
|
||||
while( frequencyValid && processingRequired )
|
||||
{
|
||||
processingRequired = false;
|
||||
|
||||
long minAvoid = frequency - mMiddleUnusable;
|
||||
long maxAvoid = frequency + mMiddleUnusable;
|
||||
|
||||
for( TunerChannel channel: mTunedChannels )
|
||||
{
|
||||
if( channel.overlaps( minAvoid, maxAvoid ) )
|
||||
{
|
||||
//Can we move the frequency lower so that the
|
||||
//channel sits to the right of center?
|
||||
long adjustment = channel.getMaxFrequency() - minAvoid;
|
||||
|
||||
if( frequency + adjustment -
|
||||
( getUsableBandwidth() / 2 ) <= minLockedFrequency )
|
||||
{
|
||||
frequency += adjustment;
|
||||
processingRequired = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
frequencyValid = false;
|
||||
}
|
||||
|
||||
//break out of the for/each loop, so that we can
|
||||
//start over again with all of the channels
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( frequencyValid )
|
||||
{
|
||||
mFrequencyController.setFrequency( frequency );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new SourceException( "Couldn't calculate viable center "
|
||||
+ "frequency from set of tuner channels" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Usable bandwidth - total bandwidth minus the unusable space at either end
|
||||
* of the spectrum.
|
||||
*/
|
||||
private int getUsableBandwidth()
|
||||
{
|
||||
return (int)( getBandwidth() * mUsableBandwidthPercentage );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to be notified any time that the tuner changes frequency
|
||||
* or bandwidth/sample rate.
|
||||
*
|
||||
* Note: this is normally used by the Tuner. Any additional listeners can
|
||||
* be registered on the tuner.
|
||||
*/
|
||||
public void addListener( IFrequencyChangeProcessor processor )
|
||||
public void setFrequency(long frequency) throws SourceException
|
||||
{
|
||||
mFrequencyController.addListener( processor );
|
||||
mFrequencyController.setFrequency(frequency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the center frequency of the local oscillator
|
||||
*
|
||||
* @return frequency in hertz
|
||||
*/
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequencyController.getFrequency();
|
||||
}
|
||||
|
||||
public int getSampleRate()
|
||||
{
|
||||
return mFrequencyController.getBandwidth();
|
||||
}
|
||||
|
||||
public double getFrequencyCorrection()
|
||||
{
|
||||
return mFrequencyController.getFrequencyCorrection();
|
||||
}
|
||||
|
||||
public void setFrequencyCorrection(double correction) throws SourceException
|
||||
{
|
||||
mFrequencyController.setFrequencyCorrection(correction);
|
||||
}
|
||||
|
||||
public long getMinFrequency()
|
||||
{
|
||||
return mFrequencyController.getMinimumFrequency();
|
||||
}
|
||||
|
||||
public long getMaxFrequency()
|
||||
{
|
||||
return mFrequencyController.getMaximumFrequency();
|
||||
}
|
||||
|
||||
private long getMinTunedFrequency() throws SourceException
|
||||
{
|
||||
return mFrequencyController.getFrequency() - (getUsableBandwidth() / 2);
|
||||
}
|
||||
|
||||
private long getMaxTunedFrequency() throws SourceException
|
||||
{
|
||||
return mFrequencyController.getFrequency() + (getUsableBandwidth() / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if channel along with all of the other currently sourced
|
||||
* channels can fit within the tunable bandwidth.
|
||||
*/
|
||||
private boolean canTune(TunerChannel channel)
|
||||
{
|
||||
//Make sure we're within the tunable frequency range of this tuner
|
||||
if(getMinFrequency() < channel.getMinFrequency() && getMaxFrequency() > channel.getMaxFrequency())
|
||||
{
|
||||
//If this is the first lock, then we're good
|
||||
if(mTunedChannels.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
int usableBandwidth = getUsableBandwidth();
|
||||
long minLockedFrequency = mTunedChannels.first().getMinFrequency();
|
||||
long maxLockedFrequency = mTunedChannels.last().getMaxFrequency();
|
||||
|
||||
//Requested channel is within current locked channel frequency range
|
||||
if(minLockedFrequency <= channel.getMinFrequency() && channel.getMaxFrequency() <= maxLockedFrequency)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Requested channel is higher than min locked frequency
|
||||
if(channel.getMaxFrequency() > minLockedFrequency &&
|
||||
channel.getMaxFrequency() - minLockedFrequency <= usableBandwidth)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Requested channel is lower than the max locked frequency
|
||||
if(channel.getMinFrequency() <= maxLockedFrequency &&
|
||||
maxLockedFrequency - channel.getMinFrequency() <= usableBandwidth)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a digital drop channel (DDC) as a tuner channel from from the tuner, or returns null if the channel
|
||||
* cannot be sourced from the tuner.
|
||||
*
|
||||
* This controller can't provide a channel if the channel, along with the current set of sourced channels, can't be
|
||||
* accomodated within the current tuner bandwidth, or if a center frequency cannot be calculated that will ensure
|
||||
* all of the channels can be accomodated and any defined central DC spike avoided.
|
||||
*
|
||||
* @param tuner to source the channel from
|
||||
* @param channel with defined center frequency and bandwidth
|
||||
* @return fully constructed tuner channel or null
|
||||
* @throws RejectedExecutionException if the decimation processor has an error
|
||||
*/
|
||||
public TunerChannelSource getChannel(Tuner tuner, TunerChannel channel) throws RejectedExecutionException
|
||||
{
|
||||
TunerChannelSource source = null;
|
||||
|
||||
if(canTune(channel))
|
||||
{
|
||||
try
|
||||
{
|
||||
mTunedChannels.add(channel);
|
||||
|
||||
if(requiresLOUpdate(channel))
|
||||
{
|
||||
updateLOFrequency();
|
||||
}
|
||||
|
||||
source = new TunerChannelSource(tuner, channel);
|
||||
}
|
||||
catch(SourceException se)
|
||||
{
|
||||
mTunedChannels.remove(channel);
|
||||
source = null;
|
||||
}
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
public int getChannelCount()
|
||||
{
|
||||
return mTunedChannels.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the tuner's LO frequency must be updated in order to accommodate the tuner channel
|
||||
*/
|
||||
private boolean requiresLOUpdate(TunerChannel channel) throws SourceException
|
||||
{
|
||||
boolean isBelow = channel.getMinFrequency() < getMinTunedFrequency();
|
||||
boolean isAbove = channel.getMaxFrequency() > getMaxTunedFrequency();
|
||||
boolean overlapsCenter = mMiddleUnusable > 0 &&
|
||||
channel.overlaps(getFrequency() - mMiddleUnusable, getFrequency() + mMiddleUnusable);
|
||||
|
||||
return isAbove || isBelow || overlapsCenter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the currently sourced tuner channel from this tuner and shuts down the tuner if no other sources exist.
|
||||
*/
|
||||
public void releaseChannel(TunerChannelSource tunerChannelSource)
|
||||
{
|
||||
if(tunerChannelSource != null)
|
||||
{
|
||||
mTunedChannels.remove(tunerChannelSource.getTunerChannel());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Local Oscillator frequency to accomodate the current set of tuned channels.
|
||||
*
|
||||
* If there is only a single tuned channel, it is placed immediately to the right of the usable bandwidth right
|
||||
* of the central DC spike.
|
||||
*
|
||||
* Otherwise, it places the highest channel frequency at the upper end of the tuner bandwidth, and then iteratively
|
||||
* moves the center frequency higher until all channels fit within the bandwidth and none of the channels overlap
|
||||
* any defined central DC spike unusable region. If a center tune frequency cannot be calculated, throw an
|
||||
* exception so that the most recently added channel can be removed.
|
||||
*
|
||||
* Note: the tuned frequency is not changed until a legitimate new frequency can be calculated. If an exception
|
||||
* is thrown, the current frequency is retained, so that the recently added channel can be removed and all other
|
||||
* channels can continue as previously arranged.
|
||||
*
|
||||
* @throws SourceException if the set of tuner channels, including a recently added channel, cannot be tuned
|
||||
* within the current tuner bandwidth and any central DC spike unusable region.
|
||||
*/
|
||||
private void updateLOFrequency() throws SourceException
|
||||
{
|
||||
long candidateFrequency;
|
||||
|
||||
boolean isValidCandidateFrequency = true;
|
||||
|
||||
//If there is only 1 channel set the center frequency so that the channel is positioned to the right of center
|
||||
if(mTunedChannels.size() == 1)
|
||||
{
|
||||
candidateFrequency = mTunedChannels.first().getMinFrequency() - mMiddleUnusable + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
long minLockedFrequency = mTunedChannels.first().getMinFrequency();
|
||||
long maxLockedFrequency = mTunedChannels.last().getMaxFrequency();
|
||||
|
||||
//Start by placing the highest frequency channel at the high end of the spectrum
|
||||
candidateFrequency = maxLockedFrequency - (getUsableBandwidth() / 2);
|
||||
|
||||
//Iterate the channels and make sure that none of them overlap the center DC spike buffer, if one exists
|
||||
if(mMiddleUnusable > 0)
|
||||
{
|
||||
boolean processingRequired = true;
|
||||
|
||||
while(isValidCandidateFrequency && processingRequired)
|
||||
{
|
||||
processingRequired = false;
|
||||
|
||||
long minAvoid = candidateFrequency - mMiddleUnusable;
|
||||
long maxAvoid = candidateFrequency + mMiddleUnusable;
|
||||
|
||||
//If any of the center channel(s) overlap the central DC spike avoid area, we'll iteratively
|
||||
//increase the tuned frequency causing the set of channels to move left in the tuned bandwidth until
|
||||
//we either find a good center tune frequency, or we walk the lowest frequency channel out of the
|
||||
//minimum tuned range, in which case we'll throw an exception indicating we don't have a solution.
|
||||
for(TunerChannel channel : mTunedChannels)
|
||||
{
|
||||
if(channel.overlaps(minAvoid, maxAvoid))
|
||||
{
|
||||
//Calculate a tuned frequency adjustment that places this overlapping channel just to the
|
||||
//left of the central DC spike avoid zone
|
||||
long adjustment = channel.getMaxFrequency() - minAvoid + 1;
|
||||
|
||||
//If the candidate frequency doesn't push the lowest channel out of bounds, make adjustment
|
||||
if(candidateFrequency + adjustment - (getUsableBandwidth() / 2) <= minLockedFrequency)
|
||||
{
|
||||
candidateFrequency += adjustment;
|
||||
processingRequired = true;
|
||||
}
|
||||
//Otherwise, punt and indicate that we can't find a center frequency or add the channel
|
||||
else
|
||||
{
|
||||
isValidCandidateFrequency = false;
|
||||
}
|
||||
|
||||
//break out of the for/each loop, so that we can start over again with all of the channels
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isValidCandidateFrequency)
|
||||
{
|
||||
mFrequencyController.setFrequency(candidateFrequency);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new SourceException("Couldn't calculate viable center frequency from set of tuner channels");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Usable bandwidth - total bandwidth minus the unusable space at either end of the spectrum.
|
||||
*/
|
||||
private int getUsableBandwidth()
|
||||
{
|
||||
return (int)(getBandwidth() * mUsableBandwidthPercentage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to be notified any time that the tuner changes frequency or bandwidth/sample rate.
|
||||
*
|
||||
* Note: this is normally used by the Tuner. Any additional listeners can be registered on the tuner.
|
||||
*/
|
||||
public void addListener(IFrequencyChangeProcessor processor)
|
||||
{
|
||||
mFrequencyController.addListener(processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the frequency change listener
|
||||
*/
|
||||
public void removeListener( IFrequencyChangeProcessor processor )
|
||||
public void removeListener(IFrequencyChangeProcessor processor)
|
||||
{
|
||||
mFrequencyController.removeFrequencyChangeProcessor( processor );
|
||||
mFrequencyController.removeFrequencyChangeProcessor(processor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class USBTransferProcessor implements TransferCallback
|
||||
private static final int TRANSFER_BUFFER_POOL_SIZE = 16;
|
||||
|
||||
//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
|
||||
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
|
||||
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.
|
||||
int result = LibUsb.handleEventsTimeoutCompleted(null, USB_TIMEOUT_MS,
|
||||
@@ -415,23 +415,27 @@ public class USBTransferProcessor implements TransferCallback
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
mFilledBuffers.drainTo(mBuffersToDispatch, 15);
|
||||
mFilledBuffers.drainTo(mBuffersToDispatch, 50);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,19 +18,9 @@
|
||||
******************************************************************************/
|
||||
package source.wave;
|
||||
|
||||
import java.io.File;
|
||||
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 channel.heartbeat.Heartbeat;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.ConversionUtils;
|
||||
import sample.Listener;
|
||||
import sample.complex.ComplexBuffer;
|
||||
@@ -39,22 +29,31 @@ import source.IControllableFileSource;
|
||||
import source.IFrameLocationListener;
|
||||
import source.tuner.frequency.FrequencyChangeEvent;
|
||||
|
||||
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
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( ComplexWaveSource.class );
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger(ComplexWaveSource.class);
|
||||
|
||||
private IFrameLocationListener mFrameLocationListener;
|
||||
private int mBytesPerFrame;
|
||||
private int mFrameCounter = 0;
|
||||
private IFrameLocationListener mFrameLocationListener;
|
||||
private int mBytesPerFrame;
|
||||
private int mFrameCounter = 0;
|
||||
private long mFrequency = 0;
|
||||
private Listener<ComplexBuffer> mListener;
|
||||
private AudioInputStream mInputStream;
|
||||
private File mFile;
|
||||
|
||||
public ComplexWaveSource( File file ) throws IOException
|
||||
|
||||
public ComplexWaveSource(File file) throws IOException
|
||||
{
|
||||
mFile = file;
|
||||
mFile = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,89 +75,105 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented
|
||||
*/
|
||||
@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()
|
||||
public void setHeartbeatListener(Listener<Heartbeat> listener)
|
||||
{
|
||||
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.
|
||||
*/
|
||||
/**
|
||||
* Not implemented
|
||||
*/
|
||||
@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()
|
||||
{
|
||||
return mFrequency;
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the value returned from getFrequency() for this source.
|
||||
*/
|
||||
public void setFrequency( long frequency )
|
||||
public void setFrequency(long frequency)
|
||||
{
|
||||
mFrequency = frequency;
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Closes the source file
|
||||
*/
|
||||
public void close() throws IOException
|
||||
{
|
||||
if( mInputStream != null )
|
||||
{
|
||||
mInputStream.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException( "Can't close wave source - was not opened" );
|
||||
}
|
||||
|
||||
mInputStream = null;
|
||||
if(mInputStream != null)
|
||||
{
|
||||
mInputStream.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException("Can't close wave source - was not opened");
|
||||
}
|
||||
|
||||
mInputStream = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,121 +181,121 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
|
||||
*/
|
||||
public void open() throws IOException, UnsupportedAudioFileException
|
||||
{
|
||||
if( mInputStream == null )
|
||||
{
|
||||
mInputStream = AudioSystem.getAudioInputStream( mFile );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException( "Can't open wave source - is already opened" );
|
||||
}
|
||||
if(mInputStream == null)
|
||||
{
|
||||
mInputStream = AudioSystem.getAudioInputStream(mFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException("Can't open wave source - is already opened");
|
||||
}
|
||||
|
||||
|
||||
AudioFormat format = mInputStream.getFormat();
|
||||
|
||||
|
||||
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 " +
|
||||
"channels 16-bit samples FOUND: " +
|
||||
mInputStream.getFormat().getChannels() + " channels " +
|
||||
mInputStream.getFormat().getSampleSizeInBits() + "-bit samples" );
|
||||
throw new IOException("Unsupported Wave Format - EXPECTED: 2 " +
|
||||
"channels 16-bit samples FOUND: " +
|
||||
mInputStream.getFormat().getChannels() + " channels " +
|
||||
mInputStream.getFormat().getSampleSizeInBits() + "-bit samples");
|
||||
}
|
||||
|
||||
/* Broadcast that we're at frame location 0 */
|
||||
broadcast( 0 );
|
||||
broadcast(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the number of frames and sends a buffer to the listener
|
||||
*/
|
||||
@Override
|
||||
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
|
||||
@Override
|
||||
public void next(int frames) 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 */
|
||||
int samplesRead = mInputStream.read( buffer );
|
||||
int samplesRead = mInputStream.read(buffer);
|
||||
|
||||
mFrameCounter += samplesRead;
|
||||
|
||||
broadcast( mFrameCounter );
|
||||
|
||||
if( broadcast && mListener != null )
|
||||
{
|
||||
if( samplesRead < buffer.length )
|
||||
{
|
||||
buffer = Arrays.copyOf( buffer, samplesRead );
|
||||
}
|
||||
mFrameCounter += samplesRead;
|
||||
|
||||
float[] samples = ConversionUtils
|
||||
.convertFromSigned16BitSamples( buffer );
|
||||
|
||||
mListener.receive( new ComplexBuffer( samples ) );
|
||||
}
|
||||
broadcast(mFrameCounter);
|
||||
|
||||
if(broadcast && mListener != null)
|
||||
{
|
||||
if(samplesRead < buffer.length)
|
||||
{
|
||||
buffer = Arrays.copyOf(buffer, samplesRead);
|
||||
}
|
||||
|
||||
float[] samples = ConversionUtils
|
||||
.convertFromSigned16BitSamples(buffer);
|
||||
|
||||
mListener.receive(new ComplexBuffer(samples));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Override
|
||||
public void setListener( Listener<ComplexBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the listener from receiving sample buffers
|
||||
*/
|
||||
public void removeListener( Listener<ComplexBuffer> listener )
|
||||
@Override
|
||||
public void setListener(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()
|
||||
{
|
||||
mListener = null;
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile()
|
||||
{
|
||||
return mFile;
|
||||
}
|
||||
@Override
|
||||
public File getFile()
|
||||
{
|
||||
return mFile;
|
||||
}
|
||||
|
||||
private void broadcast( int byteLocation )
|
||||
{
|
||||
int frameLocation = (int)( byteLocation / mBytesPerFrame );
|
||||
|
||||
if( mFrameLocationListener != null )
|
||||
{
|
||||
mFrameLocationListener.frameLocationUpdated( frameLocation );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener( IFrameLocationListener listener )
|
||||
{
|
||||
mFrameLocationListener = listener;
|
||||
}
|
||||
private void broadcast(int byteLocation)
|
||||
{
|
||||
int frameLocation = (int)(byteLocation / mBytesPerFrame);
|
||||
|
||||
@Override
|
||||
public void removeListener( IFrameLocationListener listener )
|
||||
{
|
||||
mFrameLocationListener = null;
|
||||
}
|
||||
if(mFrameLocationListener != null)
|
||||
{
|
||||
mFrameLocationListener.frameLocationUpdated(frameLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener(IFrameLocationListener listener)
|
||||
{
|
||||
mFrameLocationListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(IFrameLocationListener listener)
|
||||
{
|
||||
mFrameLocationListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,25 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014,2015 Dennis Sheirer
|
||||
*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package source.wave;
|
||||
|
||||
import java.io.File;
|
||||
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 channel.heartbeat.Heartbeat;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.ConversionUtils;
|
||||
import sample.Listener;
|
||||
import sample.real.RealBuffer;
|
||||
@@ -38,131 +28,154 @@ import source.IFrameLocationListener;
|
||||
import source.RealSource;
|
||||
import source.tuner.frequency.FrequencyChangeEvent;
|
||||
|
||||
public class RealWaveSource extends RealSource
|
||||
implements IControllableFileSource, AutoCloseable
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( RealWaveSource.class );
|
||||
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;
|
||||
|
||||
private IFrameLocationListener mFrameLocationListener;
|
||||
private int mBytesPerFrame;
|
||||
private int mFrameCounter = 0;
|
||||
public class RealWaveSource extends RealSource implements IControllableFileSource, AutoCloseable
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RealWaveSource.class);
|
||||
|
||||
private IFrameLocationListener mFrameLocationListener;
|
||||
private int mBytesPerFrame;
|
||||
private int mFrameCounter = 0;
|
||||
private long mFrequency = 0;
|
||||
private Listener<RealBuffer> mListener;
|
||||
private AudioInputStream mInputStream;
|
||||
private File mFile;
|
||||
|
||||
public RealWaveSource( File file ) throws IOException
|
||||
|
||||
public RealWaveSource(File file) throws IOException
|
||||
{
|
||||
mFile = file;
|
||||
mFile = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
@Override
|
||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
//Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
//Not implemented
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
//Not implemented
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented
|
||||
*/
|
||||
@Override
|
||||
public void setHeartbeatListener(Listener<Heartbeat> listener)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented
|
||||
*/
|
||||
@Override
|
||||
public void removeHeartbeatListener()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
stop();
|
||||
start( null );
|
||||
}
|
||||
@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 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 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 long getFrameCount() throws IOException
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public int getSampleRate()
|
||||
{
|
||||
if( mInputStream != null )
|
||||
{
|
||||
return (int)mInputStream.getFormat().getSampleRate();
|
||||
}
|
||||
|
||||
return 0;
|
||||
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.
|
||||
*/
|
||||
/**
|
||||
* Returns the frequency set for this file. Normally returns zero, but
|
||||
* the value can be set with setFrequency() method.
|
||||
*/
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the value returned from getFrequency() for this source.
|
||||
*/
|
||||
public void setFrequency( long frequency )
|
||||
public void setFrequency(long frequency)
|
||||
{
|
||||
mFrequency = frequency;
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Closes the source file
|
||||
*/
|
||||
public void close() throws IOException
|
||||
{
|
||||
if( mInputStream != null )
|
||||
{
|
||||
mInputStream.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException( "Can't close wave source - was not opened" );
|
||||
}
|
||||
|
||||
mInputStream = null;
|
||||
if(mInputStream != null)
|
||||
{
|
||||
mInputStream.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException("Can't close wave source - was not opened");
|
||||
}
|
||||
|
||||
mInputStream = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,126 +183,126 @@ public class RealWaveSource extends RealSource
|
||||
*/
|
||||
public void open() throws IOException, UnsupportedAudioFileException
|
||||
{
|
||||
if( mInputStream == null )
|
||||
{
|
||||
mInputStream = AudioSystem.getAudioInputStream( mFile );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException( "Can't open wave source - is already opened" );
|
||||
}
|
||||
if(mInputStream == null)
|
||||
{
|
||||
mInputStream = AudioSystem.getAudioInputStream(mFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException("Can't open wave source - is already opened");
|
||||
}
|
||||
|
||||
|
||||
AudioFormat format = mInputStream.getFormat();
|
||||
|
||||
|
||||
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 " +
|
||||
"channel 16-bit samples FOUND: " +
|
||||
mInputStream.getFormat().getChannels() + " channels " +
|
||||
mInputStream.getFormat().getSampleSizeInBits() + "-bit samples" );
|
||||
throw new IOException("Unsupported Wave Format - EXPECTED: 1 " +
|
||||
"channel 16-bit samples FOUND: " +
|
||||
mInputStream.getFormat().getChannels() + " channels " +
|
||||
mInputStream.getFormat().getSampleSizeInBits() + "-bit samples");
|
||||
}
|
||||
|
||||
/* Broadcast that we're at frame location 0 */
|
||||
broadcast( 0 );
|
||||
broadcast(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the number of frames and sends a buffer to the listener
|
||||
*/
|
||||
@Override
|
||||
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
|
||||
@Override
|
||||
public void next(int frames) throws IOException
|
||||
{
|
||||
if( mInputStream != null )
|
||||
{
|
||||
byte[] buffer = new byte[ mBytesPerFrame * frames ];
|
||||
|
||||
/* Fill the buffer with samples from the file */
|
||||
int samplesRead = mInputStream.read( buffer );
|
||||
|
||||
mFrameCounter += samplesRead;
|
||||
|
||||
broadcast( mFrameCounter );
|
||||
|
||||
if( broadcast && mListener != null )
|
||||
{
|
||||
if( samplesRead < buffer.length )
|
||||
{
|
||||
if( samplesRead == -1 )
|
||||
{
|
||||
throw new IOException( "End of recording" );
|
||||
}
|
||||
|
||||
buffer = Arrays.copyOf( buffer, samplesRead );
|
||||
}
|
||||
next(frames, true);
|
||||
}
|
||||
|
||||
float[] samples = ConversionUtils
|
||||
.convertFromSigned16BitSamples( buffer );
|
||||
|
||||
mListener.receive( new RealBuffer( samples ) );
|
||||
}
|
||||
/**
|
||||
* 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];
|
||||
|
||||
/* Fill the buffer with samples from the file */
|
||||
int samplesRead = mInputStream.read(buffer);
|
||||
|
||||
mFrameCounter += samplesRead;
|
||||
|
||||
broadcast(mFrameCounter);
|
||||
|
||||
if(broadcast && mListener != null)
|
||||
{
|
||||
if(samplesRead < buffer.length)
|
||||
{
|
||||
if(samplesRead == -1)
|
||||
{
|
||||
throw new IOException("End of recording");
|
||||
}
|
||||
|
||||
buffer = Arrays.copyOf(buffer, samplesRead);
|
||||
}
|
||||
|
||||
float[] samples = ConversionUtils
|
||||
.convertFromSigned16BitSamples(buffer);
|
||||
|
||||
mListener.receive(new RealBuffer(samples));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Override
|
||||
public void setListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the listener from receiving sample buffers
|
||||
*/
|
||||
public void removeListener( Listener<RealBuffer> listener )
|
||||
@Override
|
||||
public void setListener(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()
|
||||
{
|
||||
mListener = null;
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile()
|
||||
{
|
||||
return mFile;
|
||||
}
|
||||
|
||||
private void broadcast( int byteLocation )
|
||||
{
|
||||
int frameLocation = (int)( byteLocation / mBytesPerFrame );
|
||||
|
||||
if( mFrameLocationListener != null )
|
||||
{
|
||||
mFrameLocationListener.frameLocationUpdated( frameLocation );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener( IFrameLocationListener listener )
|
||||
{
|
||||
mFrameLocationListener = listener;
|
||||
}
|
||||
@Override
|
||||
public File getFile()
|
||||
{
|
||||
return mFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener( IFrameLocationListener listener )
|
||||
{
|
||||
mFrameLocationListener = null;
|
||||
}
|
||||
private void broadcast(int byteLocation)
|
||||
{
|
||||
int frameLocation = (int)(byteLocation / mBytesPerFrame);
|
||||
|
||||
if(mFrameLocationListener != null)
|
||||
{
|
||||
mFrameLocationListener.frameLocationUpdated(frameLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener(IFrameLocationListener listener)
|
||||
{
|
||||
mFrameLocationListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(IFrameLocationListener listener)
|
||||
{
|
||||
mFrameLocationListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
54
src/util/MemoryUsageLogger.java
Normal file
54
src/util/MemoryUsageLogger.java
Normal file
@@ -0,0 +1,54 @@
|
||||
/*******************************************************************************
|
||||
* 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 util;
|
||||
|
||||
import ch.qos.logback.classic.pattern.ClassicConverter;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
/**
|
||||
* Custom logging plugin for logging memory usage with logback
|
||||
*
|
||||
* Add the following to the logback.xml configuration file to use this custom converter:
|
||||
*
|
||||
* <conversionRule conversionWord="memory_usage" converterClass="util.MemoryUsageLogger" />
|
||||
*
|
||||
* Update the encoder pattern to include %memory_usage within the pattern
|
||||
*/
|
||||
public class MemoryUsageLogger extends ClassicConverter
|
||||
{
|
||||
@Override
|
||||
public String convert(ILoggingEvent iLoggingEvent)
|
||||
{
|
||||
//Method argument is ignored - we simply return memory usage statistics
|
||||
long allocated = Runtime.getRuntime().totalMemory();
|
||||
long free = Runtime.getRuntime().freeMemory();
|
||||
long used = allocated - free;
|
||||
|
||||
int usedPercentage = (int)((double)(used) / (double)allocated * 100.0);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("[").append(FileUtils.byteCountToDisplaySize(used).replace(" ", ""));
|
||||
sb.append("/").append(FileUtils.byteCountToDisplaySize(allocated).replace(" ", ""));
|
||||
sb.append(" ").append(usedPercentage).append("%]");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user