Compare commits

...

55 Commits

Author SHA1 Message Date
Denny
0e321bdaa3 Updates build to 0.3.2 (final) 2017-12-02 03:25:41 -05:00
Denny Sheirer
f191d28c98 Merge pull request #295 from DSheirer/241-lj1200-error-check-to-0-3-2-branch
241 LJ1200 Error Correction (v0.3.2 branch)
2017-12-02 03:19:12 -05:00
Denny
afc3d299b6 #241 Resolves issue with error correction routine introducing invalid messages
(cherry picked from commit 910a08f)
2017-12-02 03:18:12 -05:00
Denny
0471d90b54 Updates build to 0.3.2 Beta #3 2017-11-23 07:32:24 -05:00
Denny
8f68967b6c #289 resolves issue with encrypting username and password with XTEA
algorithm using credential values longer than 8 characters/bytes.

(cherry picked from commit 1ddc0a3)
2017-11-23 07:31:52 -05:00
Denny
91b950c13d Updates build version to 0.3.2-beta2 2017-11-04 09:42:27 -04:00
Denny
581ff9f449 Updates CHANGELOG
(cherry picked from commit 64688a3)
2017-11-04 09:41:36 -04:00
Denny
67c3cb9639 Issue #283
Updates Icecast TCP broadcaster to correctly handle content type:text/html
message that normally follows mount point in use error received from
broadcastify.

Changes mount point in use broadcast state to be a temporary error so that
the stream will attempt to reconnect after receiving this error.

Updates logging to include memory allocation/usage metrics with each
logging entry in order to support debugging issues related to high
memory usage situations.  Updates initial application logging to better
identify the host system operating environment.

(cherry picked from commit 8be3399)
2017-11-04 09:41:23 -04:00
Denny
cebd2da89f Updates build to v0.3.2-beta1 2017-10-21 09:47:58 -04:00
Denny Sheirer
73b6ef164f Merge pull request #280 from DSheirer/276-tuner-controller-issue-cherry-pick
Resolves #276 Traffic Channel threads locking in overflow state.  Issue
2017-10-21 09:46:30 -04:00
Denny
b2c7ac8c66 Resolves #276 Traffic Channel threads locking in overflow state. Issue
occured when the ChannelProcessionManager was allocating a new channel
causing the tuner to invoke the updateLOFrequency() method that was
trying to find a center tune frequency that would accommodate all of the
channels, specifically in the instance where the distance between the
highest channel and the middle channel was exactly one-half of the
tuner's bandwidth.  This situation would cause the calculated adjustment
to be zero and would cause an infinite loop, locking all channels from
accessing the any channel startup/teardown events and causing running
channels to block with buffer overflows. Added 1 to the adjustment to
ensure that the initial adjustment placed the center overlapping channel
fully to the left of the center DC spike so that on the next iteration
that channel would no longer be overlapping.

(cherry picked from commit f3d3a81)
2017-10-21 09:45:51 -04:00
Denny Sheirer
108691dc3c Merge pull request #274 from DSheirer/272-java-9-compatibility
272 java 9 compatibility
2017-10-09 12:40:04 -04:00
Denny
455aa97286 Updates build number to 'final' 2017-10-09 12:35:28 -04:00
Denny
d608834214 Resolves #272. Updates the windows launch script to perform conditional
launching based on the JRE version 8 or 9.
2017-10-09 16:33:58 +00:00
Denny
41660e1881 Updates the build number to 0.3.1 for the next patch release
Updates the build script for conditional compilation using either Java
8 or Java 9 options.  Java 9 deprecated the java.xml.bind package and
removed it as a default Java SE package, so we have to reference it via
the --add-modules argument.

Updates the linux/mac launch script to check the java runtime version
and use either a Java 8 or Java 9 launch command.
2017-10-08 09:00:10 -04:00
Denny Sheirer
110ccb158c Update README.md
Removed Version 0.2.0 user manual link
2017-09-22 06:16:34 -04:00
Denny
64ccacf04a Releasing Version 0.3.0 (final). 2017-09-22 05:56:27 -04:00
Denny Sheirer
f00b9555fe Merge pull request #271 from DSheirer/269-viterbi-3-4-rate
269 Viterbi decoder for DMR 3/4-rate packet messages
2017-09-15 11:50:19 -04:00
Denny
85442a9648 Resolves #269 - creates a generic Viterbi decoder and a Digital Mobile Radio (DMR) 3/4 Rate Viterbi decoder implementation for decoding Trellis Coded Modulation (TCM) encoded DMR Data packets. 2017-09-15 11:40:23 -04:00
Denny Sheirer
c67a0b329d Merge pull request #267 from DSheirer/266-licence-directory
266 Update build script to use gitlab license file
2017-09-10 04:55:57 -04:00
Denny
8035d64fcd Resolves #266 - updates build script to use new gitlab license file and
removes previous license file.
2017-09-10 04:54:26 -04:00
Denny Sheirer
df2a061a08 Create LICENSE 2017-08-28 19:51:13 -04:00
Denny
e430319937 Updates build to 0.3.0 Beta 14 2017-08-19 07:15:57 -04:00
Denny Sheirer
724f769771 Merge pull request #262 from DSheirer/260-icecast-stream-doesn't-reconnect
260 - Icecast and Shoutcast streaming doesn't auto-reconnect
2017-08-19 07:14:55 -04:00
Denny
c196cfebed Resolves issue #260 - applies changes to Shoutcast V1 and v2 that were
applied to the Icecast streamer for issue releated to reconnecting.
2017-08-19 07:13:08 -04:00
Denny
e80b5866d4 Resolves issue #260 where audio streaming was not re-connecting after a
disconnect.
2017-08-18 20:07:53 -04:00
Denny
c8994d5593 Updates build to version 0.3.0 Beta 13 2017-07-22 05:45:36 -04:00
Denny Sheirer
99ef57c90f Merge pull request #255 from DSheirer/249-overflow-teardown-lockup
249 Channel TEARDOWN/OVERFLOW state lockup
2017-07-22 05:44:12 -04:00
Denny
be62ae0459 Resolves #249 - removes the state monitor runnable from the channel
state and replaces it with a processing chain heartbeat that is generated
by the sample source.

Previously, the state monitor was running on a separate thread and this
was causing multi-threading issues.  With this update, all channel state
operations (and all module operations) occur on the source's buffer
processing thread.
2017-07-22 05:42:13 -04:00
Denny Sheirer
b9be6f43e0 Merge pull request #253 from DSheirer/252-usb-transfer-processor-bad-logging
252 USB transfer processor erroneous logging and performance enhancements
2017-07-09 12:29:21 +00:00
Denny
d5ad3f807d Resolves issue where the tuner's buffer processor is logging that an overflow state has been cleared, caused by a clearing of the buffer when the tuner is being shutdown because there are no more channel consumers registered on the tuner.
Tweaked buffer size limits and buffer handling to attempt to avoid overflow situations in the first place.

Resolved an issue where the tuner processor was not deregistering correctly from the LIBUSB tuner processor during tuner shutdown.
2017-07-09 08:21:07 -04:00
Denny Sheirer
d85ebd018d Merge pull request #251 from DSheirer/250-state-monitor-throwable
StateMonitor - try/catch for Throwables
2017-07-01 11:39:21 +00:00
Denny
ab4e31ae00 Ensures that any leftover cached LDU1 message is reset when the call ends. 2017-07-01 07:36:55 -04:00
Denny
5efd89632e Ensures that any leftover cached LDU1 message is reset when the call ends. 2017-05-28 07:30:05 -04:00
Denny
1dc3863489 Updates build to 0.3.0 Beta 12 2017-05-28 07:22:43 -04:00
Denny Sheirer
441286ba13 Merge pull request #247 from DSheirer/244-ldu1-encryption-flag
244 Prevent P25 Encrypted Audio Decoding/Playback
2017-05-28 11:20:20 +00:00
Denny
e4323df347 Resolves issue #244 - updates the P25 IMBE audio module to correctly
detect encrypted audio state so that encrypted audio is not (incorrectly)
decoded and played back to the user.

Processes HDU and LDU2 messages to identify encrypted call state.  Once
established, all subsequent LDU1/2 messages are decoded only if the call
is unencrypted.  If an LDU1 message is received out-of-sequence, meaning
that the initial HDU message was missed, then the LDU1 is cached until
we receive an LDU2 message and correctly determine the call encryption
state.
2017-05-28 07:17:09 -04:00
Denny
a872e0d664 Updates version to Beta 0.3.0 Beta 11 2017-05-20 07:24:27 -04:00
Denny Sheirer
fbc92cfe31 Merge pull request #246 from DSheirer/245-sound-card-source-issue
Using sound card or mixer as a source throws an error
2017-05-20 10:55:23 +00:00
Denny
1a432197ac Resolves #245 issue with NPE for frequency change event listener when using the sound card/mixer as a source. 2017-05-20 06:51:53 -04:00
Denny
fbc6e508bd Increments build to 0.3.0-beta10 2017-04-14 07:46:18 -04:00
Denny Sheirer
f9b6e67b00 Merge pull request #240 from DSheirer/234-long-alias-names-cause-audio-label-to-wrap
Long alias names cause audio label to wrap
2017-03-31 01:13:17 +00:00
Denny
a628800a71 Resolves #234 - updates the miglayout layout manager to allow jlabel text strings to be automatically truncated (with ellipses) when the alias name is too long. 2017-03-30 21:12:06 -04:00
Denny
25f71a76c4 Resolves #234 - hard codes alias name truncation when name exceeds 33 characters in length. 2017-03-30 20:03:48 -04:00
Denny
5fe8ccd478 Resolves #234 - change the order of assigning an icon and alias text to the audio panel in an attempt to prevent the audio channel panel from wrapping to the next line when the text is too long to display. 2017-03-30 19:52:35 -04:00
Denny Sheirer
069f951a86 Merge pull request #239 from DSheirer/228-traffic-channel-allocation-for-do-not-monitor
Traffic channel manager now allocates traffic channels for do not monitor aliases
2017-03-30 23:27:38 +00:00
Denny
dab6df7836 Resolves #228 - traffic channel manager now allocates traffic channels even when the alias is marked as do not monitor 2017-03-30 19:26:35 -04:00
Denny Sheirer
89da46b63c Merge pull request #238 from DSheirer/235-lojack-decoder-error-on-construction
Resolves error on constructing a LoJack decoder state.
2017-03-30 23:19:29 +00:00
Denny
5f2ce2a9f2 Resolves #235 - fixes error on constructing a LoJack decoder state. 2017-03-30 19:18:39 -04:00
Denny Sheirer
7a0b377971 Merge pull request #232 from DSheirer/229-p25-phase2-event-duplicates
Resolves #229 - duplicate P25 TDMA call events.
2017-03-23 21:52:47 +00:00
Denny
5dbe0ecce3 Resolves #229 - duplicate P25 TDMA call events. 2017-03-23 21:48:52 +00:00
Denny Sheirer
6647c581a6 Merge pull request #231 from DSheirer/230-p25-tdma-frequency-bands
P25 Phase II TDMA frequency bands are now displayed in details summary
2017-03-23 21:01:58 +00:00
Denny
a2ab5bfa95 Resolves #230 - P25 Phase II TDMA frequency bands are now displayed in the channel details summary. 2017-03-23 20:55:42 +00:00
Denny
ed486440b8 Merge remote-tracking branch 'origin/master' 2017-03-12 11:33:17 -04:00
Denny
13464e01fc Updates build to 0.3.0 Beta 9 2017-03-12 11:33:04 -04:00
49 changed files with 2723 additions and 1682 deletions

View File

@@ -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

View File

@@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
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>.

View File

@@ -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)
![sdrtrunk Application Overview - Version 0.3.0](https://github.com/DSheirer/sdrtrunk/wiki/images/ApplicationOverview_V0.3.0.png)
**Figure 1:** sdrtrunk **Version 0.3.0** Application Screenshot
**Figure 1:** sdrtrunk **Version 0.3.0** Application Screenshot

View File

@@ -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="-beta8"/>
<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>

View File

@@ -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>

View File

@@ -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

View File

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

View File

@@ -90,7 +90,8 @@ public class AudioChannelPanel extends JPanel
private void init()
{
setLayout(new MigLayout("align center center, insets 0 0 0 0", "[][][align right]0[grow,fill]", ""));
setLayout(new MigLayout("align center center, insets 0 0 0 0",
"[][][align right]0[grow,fill]", ""));
setBackground(mBackgroundColor);
mMutedLabel.setFont(mFont);
@@ -109,7 +110,7 @@ public class AudioChannelPanel extends JPanel
mTo.setFont(mFont);
mTo.setForeground(mValueColor);
add(mTo);
add(mTo,"wmin 10lp");
}
@Override
@@ -159,30 +160,40 @@ public class AudioChannelPanel extends JPanel
* does not occur on the Swing event thread -- wrap any calls to this
* method with an event thread call.
*/
private void updateLabel(JLabel valueLabel, String value, JLabel aliasLabel, Alias alias)
private void updateLabel(JLabel textLabel, String value, JLabel iconLabel, Alias alias)
{
if(value != null)
{
if(alias != null)
{
valueLabel.setText(alias.getName());
aliasLabel.setIcon(mIconManager.getIcon(alias.getIconName(), 18));
iconLabel.setIcon(mIconManager.getIcon(alias.getIconName(), 18));
String aliasName = alias.getName();
//Truncate label if length exceeds 33 characters - hack!
if(aliasName.length() > 33)
{
textLabel.setText(aliasName.substring(0,30) + "...");
}
else
{
textLabel.setText(aliasName);
}
}
else
{
valueLabel.setText(value);
aliasLabel.setIcon(null);
iconLabel.setIcon(null);
textLabel.setText(value);
}
}
else
{
valueLabel.setText("-----");
aliasLabel.setIcon(null);
iconLabel.setIcon(null);
textLabel.setText("-----");
}
}
/**
* Processes audio metadata to update this panel's display values
*/

View File

@@ -33,8 +33,7 @@ public class AudioChannelsPanel extends JPanel
public AudioChannelsPanel(IconManager iconManager, SettingsManager settingsManager, IAudioController controller)
{
// setLayout(new MigLayout("insets 0 0 0 0", "[][grow,fill][][grow,fill]", "[grow,fill]"));
setLayout(new MigLayout("insets 0 0 0 0", "[][sg,grow,fill][][sg,grow,fill]", "[grow,fill]"));
setLayout(new MigLayout("insets 0 0 0 0", "[][sg abc,grow,fill][][sg abc,grow,fill]", "[grow,fill]"));
setBackground(Color.BLACK);
@@ -44,7 +43,7 @@ public class AudioChannelsPanel extends JPanel
for(int x = 0; x < outputs.size(); x++)
{
add(new AudioChannelPanel(iconManager, settingsManager, outputs.get(x)), "growx");
add(new AudioChannelPanel(iconManager, settingsManager, outputs.get(x)));
if(x < outputs.size() - 1)
{

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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());
}
}

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,8 @@ package channel.state;
import audio.squelch.ISquelchStateProvider;
import audio.squelch.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 +

View File

@@ -63,13 +63,11 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
private Listener<CallEvent> mCallEventListener;
private ChannelModel mChannelModel;
private ChannelProcessingManager mChannelProcessingManager;
private DecodeConfiguration mDecodeConfiguration;
private RecordConfiguration mRecordConfiguration;
private String mSystem;
private String mSite;
private String mAliasListName;
private CallEvent mPreviousDoNotMonitorCallEvent;
/**
* Monitors call events and allocates traffic decoder channels in response
@@ -77,7 +75,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
* traffic channel allocations.
*
* @param channelModel containing channels currently in use
* @param channelProcessingManager
* @param decodeConfiguration - decoder configuration to use for each
* traffic channel allocation.
* @param recordConfiguration - recording options for each traffic channel
@@ -88,7 +85,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
* in the pool
*/
public TrafficChannelManager(ChannelModel channelModel,
ChannelProcessingManager channelProcessingManager,
DecodeConfiguration decodeConfiguration,
RecordConfiguration recordConfiguration,
String system,
@@ -97,7 +93,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
int trafficChannelPoolSize)
{
mChannelModel = channelModel;
mChannelProcessingManager = channelProcessingManager;
mDecodeConfiguration = decodeConfiguration;
mRecordConfiguration = recordConfiguration;
mSystem = system;
@@ -120,7 +115,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
mCallEventListener = null;
mDecodeConfiguration = null;
mPreviousDoNotMonitorCallEvent = null;
}
/**
@@ -197,21 +191,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
long frequency = callEvent.getFrequency();
/* Check the from/to aliases for do not monitor priority */
if(isDoNotMonitor(callEvent))
{
callEvent.setCallEventType(CallEventType.CALL_DO_NOT_MONITOR);
if(isSameCallEvent(mPreviousDoNotMonitorCallEvent, callEvent))
{
return;
}
else
{
mPreviousDoNotMonitorCallEvent = callEvent;
}
}
else if(frequency > 0)
if(frequency > 0)
{
Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(Type.TRAFFIC,
frequency, mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
@@ -330,31 +310,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
return false;
}
/**
* Checks for aliases that contain a do not follow/process priority.
*
* @param event
* @return - true if there is an alias that has a do not process alias id
*/
private boolean isDoNotMonitor(CallEvent event)
{
Alias to = event.getToIDAlias();
if(to != null && to.hasCallPriority() && to.getCallPriority() == Priority.DO_NOT_MONITOR)
{
return true;
}
Alias from = event.getFromIDAlias();
if(from != null && from.hasCallPriority() && from.getCallPriority() == Priority.DO_NOT_MONITOR)
{
return true;
}
return false;
}
@Override
public Listener<DecoderStateEvent> getDecoderStateListener()
{

View File

@@ -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;
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -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();

View File

@@ -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<>();
@@ -320,7 +320,18 @@ public class ProcessingChain implements IChannelEventListener
if(module instanceof IFrequencyChangeListener)
{
mFrequencyChangeEventBroadcaster.addListener(((IFrequencyChangeListener) module).getFrequencyChangeListener());
Listener<FrequencyChangeEvent> frequencyChangeEventListener =
((IFrequencyChangeListener)module).getFrequencyChangeListener();
if(frequencyChangeEventListener != null)
{
mFrequencyChangeEventBroadcaster.addListener(frequencyChangeEventListener);
}
}
if(module instanceof IHeartbeatListener)
{
mHeartbeatBroadcaster.addListener(((IHeartbeatListener)module).getHeartbeatListener());
}
if(module instanceof IMessageListener)
@@ -385,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());
@@ -442,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);
@@ -499,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);

View File

@@ -222,7 +222,7 @@ public class DecoderFactory
if(channelType == ChannelType.STANDARD)
{
modules.add(new TrafficChannelManager(channelModel, channelProcessingManager, decodeConfig,
modules.add(new TrafficChannelManager(channelModel, decodeConfig,
channel.getRecordConfiguration(), channel.getSystem(), channel.getSite(),
(aliasList != null ? aliasList.getName() : null), mptConfig.getTrafficChannelPoolSize()));
}
@@ -263,7 +263,7 @@ public class DecoderFactory
if(channelType == ChannelType.STANDARD)
{
modules.add(new TrafficChannelManager(channelModel, channelProcessingManager, decodeConfig,
modules.add(new TrafficChannelManager(channelModel, decodeConfig,
channel.getRecordConfiguration(), channel.getSystem(), channel.getSite(),
(aliasList != null ? aliasList.getName() : null), p25Config.getTrafficChannelPoolSize()));
}

View File

@@ -54,7 +54,7 @@ public class LJ1200DecoderState extends DecoderState
super(aliasList);
mAddressAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_TO,
getAttributeChangeRequestListener(), getAliasList(), AliasIDType.LOJACK);
getAttributeChangeRequestListener(), getAliasList(), AliasIDType.SITE);
}
@Override

View File

@@ -315,6 +315,8 @@ public class P25DecoderState extends DecoderState
*/
public void registerCallEvent(P25CallEvent event)
{
String channel = event.getChannel();
if(mChannelCallMap.containsKey(event.getChannel()))
{
P25CallEvent previousEvent = mChannelCallMap.remove(event.getChannel());
@@ -419,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();
@@ -1105,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());
@@ -1120,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());
@@ -1149,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());
@@ -1185,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);
@@ -1397,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);
@@ -1480,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);
@@ -1557,6 +1559,7 @@ public class P25DecoderState extends DecoderState
break;
case IDENTIFIER_UPDATE_NON_VUHF:
case IDENTIFIER_UPDATE_VHF_UHF_BANDS:
case IDENTIFIER_UPDATE_TDMA:
IdentifierUpdate iu = (IdentifierUpdate)tsbk;
if(!mBands.containsKey(iu.getIdentifier()))
@@ -1710,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;
@@ -2735,6 +2746,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(pgvcg.isTDMAChannel() ? "TDMA " : "");
details.append(pgvcg.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(pgvcg.getPriority());
details.append(pgvcg.isEncryptedChannel() ? " ENCRYPTED" : "");
@@ -2742,7 +2754,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(CallEventType.PATCH_GROUP_CALL)
.aliasList(getAliasList())
.channel(channel + (pgvcg.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(pgvcg.getDownlinkFrequency())
.from(from)
@@ -2775,8 +2787,9 @@ public class P25DecoderState extends DecoderState
{
P25CallEvent event = new P25CallEvent.Builder(CallEventType.PATCH_GROUP_CALL)
.aliasList(getAliasList())
.channel(channel + (gvcgu.isTDMAChannel1() ? " TDMA" : ""))
.details((gvcgu.isEncrypted() ? "ENCRYPTED " : ""))
.channel(channel)
.details((gvcgu.isTDMAChannel1() ? "TDMA " : "") +
(gvcgu.isEncrypted() ? "ENCRYPTED " : ""))
.frequency(gvcgu.getDownlinkFrequency1())
.to(to)
.build();
@@ -2801,8 +2814,9 @@ public class P25DecoderState extends DecoderState
{
P25CallEvent event = new P25CallEvent.Builder(CallEventType.PATCH_GROUP_CALL)
.aliasList(getAliasList())
.channel(channel2 + (gvcgu.isTDMAChannel2() ? " TDMA" : ""))
.details((gvcgu.isEncrypted() ? "ENCRYPTED " : ""))
.channel(channel2)
.details((gvcgu.isTDMAChannel2() ? "TDMA " : "") +
(gvcgu.isEncrypted() ? "ENCRYPTED " : ""))
.frequency(gvcgu.getDownlinkFrequency2())
.to(to2)
.build();
@@ -2866,6 +2880,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(gdcg.isTDMAChannel() ? "TDMA " : "");
details.append(gdcg.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(gdcg.getPriority()).append(" ");
details.append(gdcg.isEncryptedChannel() ? "ENCRYPTED " : "");
@@ -2873,7 +2888,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(CallEventType.DATA_CALL)
.aliasList(getAliasList())
.channel(channel + (gdcg.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(gdcg.getDownlinkFrequency())
.from(from)
@@ -2904,6 +2919,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(gvcg.isTDMAChannel() ? "TDMA " : "");
details.append(gvcg.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(gvcg.getPriority()).append(" ");
details.append(gvcg.isEncryptedChannel() ? "ENCRYPTED " : "");
@@ -2911,7 +2927,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(CallEventType.GROUP_CALL)
.aliasList(getAliasList())
.channel(channel + (gvcg.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(gvcg.getDownlinkFrequency())
.from(from)
@@ -2928,8 +2944,7 @@ public class P25DecoderState extends DecoderState
}
break;
case GROUP_VOICE_CHANNEL_GRANT_UPDATE:
GroupVoiceChannelGrantUpdate gvcgu =
(GroupVoiceChannelGrantUpdate)message;
GroupVoiceChannelGrantUpdate gvcgu = (GroupVoiceChannelGrantUpdate)message;
channel = gvcgu.getChannel1();
from = null;
@@ -2942,6 +2957,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(gvcgu.isTDMAChannel() ? "TDMA " : "");
details.append(gvcgu.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(gvcgu.getPriority()).append(" ");
details.append(gvcgu.isEncryptedChannel() ? "ENCRYPTED " : "");
@@ -2949,7 +2965,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(CallEventType.GROUP_CALL)
.aliasList(getAliasList())
.channel(gvcgu.getChannel1() + (gvcgu.isTDMAChannel1() ? " TDMA" : ""))
.channel(gvcgu.getChannel1())
.details(details.toString())
.frequency(gvcgu.getDownlinkFrequency1())
.from(from)
@@ -2977,6 +2993,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(gvcgu.isTDMAChannel() ? "TDMA " : "");
details.append(gvcgu.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(gvcgu.getPriority()).append(" ");
details.append(gvcgu.isEncryptedChannel() ? "ENCRYPTED " : "");
@@ -2984,7 +3001,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event2 = new P25CallEvent.Builder(CallEventType.GROUP_CALL)
.aliasList(getAliasList())
.channel(gvcgu.getChannel2() + (gvcgu.isTDMAChannel2() ? " TDMA" : ""))
.channel(gvcgu.getChannel2())
.details(details.toString())
.frequency(gvcgu.getDownlinkFrequency2())
.to(gvcgu.getGroupAddress2())
@@ -3016,6 +3033,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(gvcgue.isTDMAChannel() ? "TDMA " : "");
details.append(gvcgue.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(gvcgue.getPriority()).append(" ");
details.append(gvcgue.isEncryptedChannel() ? "ENCRYPTED " : "");
@@ -3023,7 +3041,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(CallEventType.GROUP_CALL)
.aliasList(getAliasList())
.channel(channel + (gvcgue.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(gvcgue.getDownlinkFrequency())
.from(from)
@@ -3053,6 +3071,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(idcg.isTDMAChannel() ? "TDMA " : "");
details.append(idcg.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(idcg.getPriority()).append(" ");
details.append(idcg.isEncryptedChannel() ? "ENCRYPTED " : "");
@@ -3060,7 +3079,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(CallEventType.DATA_CALL)
.aliasList(getAliasList())
.channel(channel + (idcg.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(idcg.getDownlinkFrequency())
.from(from)
@@ -3090,13 +3109,14 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(sdcg.isTDMAChannel() ? "TDMA " : "");
details.append(sdcg.isEmergency() ? "EMERGENCY " : "");
details.append(sdcg.isEncryptedChannel() ? "ENCRYPTED " : "");
details.append(sdcg.getSessionMode().name());
P25CallEvent event = new P25CallEvent.Builder(CallEventType.DATA_CALL)
.aliasList(getAliasList())
.channel(channel + (sdcg.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(sdcg.getDownlinkFrequency())
.from(from)
@@ -3128,6 +3148,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(tivcg.isTDMAChannel() ? "TDMA " : "");
details.append(tivcg.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(tivcg.getPriority()).append(" ");
details.append(tivcg.isEncryptedChannel() ? "ENCRYPTED " : "");
@@ -3137,7 +3158,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(
CallEventType.TELEPHONE_INTERCONNECT)
.aliasList(getAliasList())
.channel(channel + (tivcg.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(tivcg.getDownlinkFrequency())
.from(from)
@@ -3170,6 +3191,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(tivcgu.isTDMAChannel() ? "TDMA " : "");
details.append(tivcgu.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(tivcgu.getPriority()).append(" ");
details.append(tivcgu.isEncryptedChannel() ? "ENCRYPTED " : "");
@@ -3179,7 +3201,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(
CallEventType.TELEPHONE_INTERCONNECT)
.aliasList(getAliasList())
.channel(channel + (tivcgu.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(tivcgu.getDownlinkFrequency())
.from(from)
@@ -3209,6 +3231,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(uuvcg.isTDMAChannel() ? "TDMA " : "");
details.append(uuvcg.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(uuvcg.getPriority()).append(" ");
details.append(uuvcg.isEncryptedChannel() ? " ENCRYPTED " : "");
@@ -3217,7 +3240,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(
CallEventType.UNIT_TO_UNIT_CALL)
.aliasList(getAliasList())
.channel(channel + (uuvcg.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(uuvcg.getDownlinkFrequency())
.from(from)
@@ -3247,6 +3270,7 @@ public class P25DecoderState extends DecoderState
else
{
StringBuilder details = new StringBuilder();
details.append(uuvcgu.isTDMAChannel() ? "TDMA " : "");
details.append(uuvcgu.isEmergency() ? "EMERGENCY " : "");
details.append("PRI:").append(uuvcgu.getPriority()).append(" ");
details.append(uuvcgu.isEncryptedChannel() ? "ENCRYPTED " : "");
@@ -3255,7 +3279,7 @@ public class P25DecoderState extends DecoderState
P25CallEvent event = new P25CallEvent.Builder(
CallEventType.UNIT_TO_UNIT_CALL)
.aliasList(getAliasList())
.channel(channel + (uuvcgu.isTDMAChannel() ? " TDMA" : ""))
.channel(channel)
.details(details.toString())
.frequency(uuvcgu.getDownlinkFrequency())
.from(from)

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -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));
}
}

View File

@@ -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();

View File

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

View File

@@ -41,7 +41,7 @@ public class IdentifierUpdateTDMA extends IdentifierUpdate
sb.append( " ID:" + getIdentifier() );
sb.append( " BASE:" + getBaseFrequency() );
sb.append( " BW:" + getBandwidth() );
sb.append( " BANDWIDTH:" + getBandwidth() );
sb.append( " SPACING:" + getChannelSpacing() );
sb.append( " OFFSET:" + getTransmitOffset() );
sb.append( " TYPE:" + getChannelType().toString() );
@@ -137,7 +137,7 @@ public class IdentifierUpdateTDMA extends IdentifierUpdate
sb.append( mAccessType.name() );
sb.append( " BANDWIDTH:" + mBandwidth );
sb.append( " SLOT/CARRIER:" + mSlotsPerCarrier );
sb.append( " TIMESLOTS PER CARRIER:" + mSlotsPerCarrier );
sb.append( " VOCODER:" + mVocoder.name() );
return sb.toString();

View File

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

View File

@@ -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;

View File

@@ -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();
}
}
}

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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();
}
}