Compare commits

...

40 Commits

Author SHA1 Message Date
Denny
dfaf506e23 Updates changelog 2018-04-18 05:52:15 -04:00
Denny
81c9a352a8 Updates changelog 2018-04-18 05:51:55 -04:00
Denny
e6937764c3 Merge remote-tracking branch 'origin/master' 2018-04-18 05:49:16 -04:00
Denny
5bfc5bf456 Increments build to 0.3.4 beta 2 2018-04-18 05:49:04 -04:00
Denny Sheirer
ce83273393 Merge pull request #345 from DSheirer/344-p25-c4fm-agc
344 Add AGC to P25 C4FM decoder
2018-04-18 09:48:19 +00:00
Denny
95aa9f8803 Adds automatic gain control to P25 C4FM decoder to condition the
samples prior to interpolation.

Resolves issue with using incorrect quadrature index when pulling a
sample from the interpolating buffer.

Resolves issue where the instrumented version of the interpolating
buffer was being constructed in the non-interpolated decoder component.
2018-04-18 05:47:17 -04:00
Denny
3619b480f8 Updates build to 0.3.4 beta 1.
Note: build 0.3.3 was never promoted beyond beta 3, although there were
no reported issues that would have prevented a 0.3.3 release.
2018-03-17 10:43:24 -04:00
Denny Sheirer
830b2a2d34 Merge pull request #339 from DSheirer/336-audio-recording-temp-files
336 - Audio recording temporary file name
2018-03-17 14:41:14 +00:00
Denny
78bb5ffe43 Resolves #336. Updates audio recorder to use a *.tmp file extension
while the file is open/in-use and change the file extension to *.wav
when the recording is closed.
2018-03-17 10:39:35 -04:00
Denny Sheirer
5dd1664844 Merge pull request #338 from DSheirer/330-p25-c4fm-decoder-update
330 p25 c4fm decoder update
2018-03-17 13:51:19 +00:00
Denny
8868c1f954 330 - updates P25 C4FM decoder to use the new DQPSK demodulator and
removes the previous FM demodulator based decoder.
2018-03-17 09:45:23 -04:00
Denny
646bd78037 330-WIP - adds the message decoder to see the P25 messages being decoded
as we step through the recording.
2018-03-04 20:41:43 -05:00
Denny
5af4cb8706 330-WIP - adds the message decoder to see the P25 messages being decoded
as we step through the recording.
2018-03-04 12:07:29 -05:00
Denny
45a6c5a0b6 330 WIP - implements the new data-directed symbol timing and phase
error calculator.  After increasing the gain for symbol timing, this
new symbol evaluator class seems to track the signal very accurately
despite lots of noise and ISI.
2018-03-04 11:42:23 -05:00
Denny
b563f2d11b 330 WIP - created new symbol evaluator class that combines phase and
timing error calculation with symbol decision.  In the process of
changing over to this new class and remove the previous implementations
for the 3 values.  This commit includes the partial implementation
so I can access from another dev machine.
2018-02-27 05:57:48 -05:00
Denny
148ddd0bd5 330 WIP - P25 C4FM decoder fully functional using DQPSK decoder and
new symbol timing error detector.  Each component now has a subclass
that supports instrumentation for use with the DemodulatorViewerFX
application.
2018-02-25 08:56:34 -05:00
Denny
a0630102e6 330 WIP - P25 C4FM decoder fully functional using DQPSK decoder and
new symbol timing error detector.
2018-02-25 05:04:20 -05:00
Denny
3d1ac30646 330 WIP - changing to 45 degree rotation before demodulation. 2018-02-20 05:27:33 -05:00
Denny
dda37a1198 330 WIP - updates the demodulator viewer with an eye diagram and symbol
chart.  Testing various configurations to see which yields the best
PLL and symbol timing locks.
2018-02-18 16:20:45 -05:00
Denny
eaf7c7d11e 330 WIP on redesigning the P25 C4FM decoder. Creates a new decoder
based on the P25 LSM decoder using a costas loop and a gardner detector.
Updates the Demodulator Viewer FX application to add in a symbol chart
and a phase error chart.  The decoder won't lock on yet ... still a wip.
2018-02-17 13:41:36 -05:00
Denny
80c5ad1bc8 WIP - updates to the demodulator viewer app and the recently used files
menu.
2018-02-13 12:31:50 +00:00
Denny
956bf2d76a Adds phase viewer and recent files menu item to viewer desktop. 2018-02-12 12:34:42 +00:00
Denny
1406d5576a 330 - Adds new JavaFX implementation of the demodulator viewer applicaiton
with initial support for playback control and I/Q sample axis viewing.
2018-02-11 10:16:50 -05:00
Denny
03e39f3343 Merge branch 'master' into 330-p25-c4fm-decoder-update 2018-02-11 06:14:23 -05:00
Denny
28f351287c 328 - cleanup issue with NPE 2018-02-11 06:13:56 -05:00
Denny
54227703a2 Cleanup from last PR. 2018-02-11 06:12:57 -05:00
Denny
bb078de0a9 Cleanup from last PR. 2018-02-10 10:23:28 -05:00
Denny
ec30cd6305 Updates changelog 2018-02-10 08:28:40 -05:00
Denny Sheirer
5a88ac0200 Merge pull request #329 from DSheirer/328-auto-start-ordering
328 Channel Auto-Start capability
2018-02-10 08:15:10 -05:00
Denny
05eea44f7e Cleanup 2018-02-10 08:14:00 -05:00
Denny
030fc1617c 328 Channel Auto-Start capability. Adds auto-start and auto-start
ordering to each channel configuration to provide the user with greater
control over initial application startup configuration.

Adds new dialog window that displays on startup that allows the user to
invoke/cancel auto-start, or simply allow the timer to expire whereby
the channels will be auto-started.

Updates channel table model to include an auto-start column and relabels
the 'Enabled' column to read 'Playing'.

Updates channel editor to change the 'Enable/Disable' button to read
'Start/Stop'.

Changes the 'enabled' property of the channel configuration to indicate
the current 'processing' status - isProcessing().

Channel's XML configuration re-purposes the 'enabled' property to indicate
auto-start and adds a new (optional) auto-start 'order' attribute.
2018-02-10 08:09:43 -05:00
Denny Sheirer
594a77a9d0 Merge pull request #327 from DSheirer/321-channel-preferred-tuner
321 channel preferred tuner
2018-02-09 07:13:13 -05:00
Denny
c2fcc9e85d 321 - adds a preferred tuner option to the channel's source configuration
so that on startup, the application attempts to source the channel from
the preferred tuner.

If the tuner is currently not available, or if the tuner cannot source
the channel, then the next available tuner is selected.
2018-02-09 07:11:38 -05:00
Denny
1c06c4382f Adds preferred tuner to tuner source configuration. 2018-02-09 05:55:29 -05:00
Denny Sheirer
3d9b38bef5 Merge pull request #319 from DSheirer/318-dft-processor-non-blocking-queue
318 DFT Processor Non-Blocking Queue redesign
2018-01-15 04:54:40 -05:00
Denny
d5b377b1af Resolves #318. Redesigns DFT processor to use a non-blocking queue approach to
access incoming buffer streams coupled with a overflowable queue that
limits the quantity of complex buffers that are cached for the DFT
processor.
2018-01-14 12:05:39 -05:00
Denny Sheirer
69e9bed2af Updated readme 2018-01-12 03:50:57 -05:00
Denny
ddad4b6fb0 Updates readme 2018-01-01 13:16:20 -05:00
Denny
b65dd7e414 Updates readme 2018-01-01 08:20:30 -05:00
Denny
b076587f30 Updates readme 2018-01-01 08:17:42 -05:00
97 changed files with 7725 additions and 3213 deletions

View File

@@ -1,3 +1,9 @@
0.3.4 20180418
- New P25 C4FM decoder that uses Differential QPSK decoder instead of FM demodulation with 4FSK.
- Adds channel auto-start and ordering capability with startup dialog allowing user to invoke or bypass channel
auto-start. Provides user with more control over startup configuration.
- Adds preferred tuner attribute to channel configuration allowing user to select a preferred tuner for the channel
0.3.3 20171225 0.3.3 20171225
- Changes build system from ant to gradle. Thanks to: Roman Tsypuk https://github.com/tsypuk - Changes build system from ant to gradle. Thanks to: Roman Tsypuk https://github.com/tsypuk
- Adds LIST and ID3 metadata to wav audio recordings - Adds LIST and ID3 metadata to wav audio recordings

View File

@@ -1,17 +1,52 @@
# sdrtrunk # sdrtrunk
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR). A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/GettingStarted) * [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/GettingStarted_V0.3.0)
* [User's Manual Version 0.3.0](https://github.com/DSheirer/sdrtrunk/wiki/UserManual_Version0.3.0) * [User's Manual Version 0.3.0](https://github.com/DSheirer/sdrtrunk/wiki/UserManual_V0.3.0)
* [Download](https://github.com/DSheirer/sdrtrunk/releases) * [Download](https://github.com/DSheirer/sdrtrunk/releases)
* [Support](https://groups.google.com/forum/#!forum/sdrtrunk) * [Support](https://groups.google.com/forum/#!forum/sdrtrunk)
![sdrtrunk Application Overview - Version 0.3.0](https://github.com/DSheirer/sdrtrunk/wiki/images/ApplicationOverview_V0.3.0.png) ![sdrtrunk Application Overview - Version 0.3.0](https://github.com/DSheirer/sdrtrunk/wiki/v0.3/images/ApplicationOverview_V0.3.0.png)
**Figure 1:** sdrtrunk **Version 0.3.0** Application Screenshot **Figure 1:** sdrtrunk **Version 0.3.0** Application Screenshot
# Instructions: # End User Instructions:
If you simply want to download and run the program, please follow these instructions.
## Install Java 8 (or newer)
## Download the latest sdrtrunk release
All release versions of sdrtrunk are available from the [releases](https://github.com/DSheirer/sdrtrunk/releases) tab.
**(final)** These versions have been tested and are the current release version.
**(beta)** These versions are currently being tested for bugs and functionality.
## Run the application
Either double-click on the downloaded file (if supported on your operating system) or open a terminal/command window
and change to the directory where you downloaded the release file and type:
```
java -jar downloaded-jar-filename
```
Note: replace _downloaded-jar-filename_ with the actual name of the sdrtrunk release version that you downloaded
## Optional - P25 Audio
If you're using sdrtrunk with a P25 trunked radio system, the [JMBE](https://github.com/DSheirer/sdrtrunk/wiki/JMBE) wiki page contains instructions
for downloading the JMBE audio library source code and compiling the JMBE library. Copy the resulting JMBE audio library
jar file to the same folder containing the sdrtrunk application to use the library with sdrtrunk.
# Developer Instructions:
If you're interested in modifying and/or compiling the source code, please follow these instructions to use gradle to compile the code.
## Build the project ## Build the project
Project uses gradle as build system. You can build it with locally installed gradle or if you do not have/want install gradle you can use preconfigured linux/windows wrapper. sdrtrunk uses gradle as the build system. You can build it with locally installed gradle or if you do not have/want
to install gradle you can use preconfigured linux/windows wrapper.
### Build with locally installed gradle 4.3.1 ### Build with locally installed gradle 4.3.1
``` ```
gradle clean buildSdr gradle clean buildSdr
@@ -35,20 +70,9 @@ version = '0.3.2'
``` ```
## Run SDRTrunk ## Run SDRTrunk
SDRTrunk is packed into single uber-jar file. There is need to have folders with dependecies and SDRTrunk is packed into single uber-jar file. There is need to have folders with dependencies and
adding them with classpath. Just run with java. adding them with classpath. Just run with java.
### Use Java8 ### Use Java8
``` ```
java -jar build/libs/sdr-trunk-all-0.3.2.jar java -jar build/libs/sdr-trunk-all-0.3.2.jar
``` ```
## TODO ITEMS
- [x] Move project to gradle build
- [x] Changed folders structure, based on java project
- [x] Dependencies are moved to gradle and versions are updated
- [x] Create uberJAR with all dependencies
- [ ] Some dependencies left in imports folder. Find them and include into gradle
- [ ] Move images into resources of project
- [ ] Add tests to project. This would guarantee that after refactoring part of functionality is not broken
- [ ] Create modular OOP system (radio-module, gui, web-ui, etc). This would allow to use RADIO modules to call from CLI, GUI or Web UI. And use SDRTrunk as dependency for other projects.

View File

@@ -15,7 +15,7 @@ repositories {
mavenCentral() mavenCentral()
} }
version = '0.3.3-beta.3' version = '0.3.4-beta.2'
sourceCompatibility = 1.8 sourceCompatibility = 1.8
sourceSets { sourceSets {
@@ -54,6 +54,8 @@ dependencies {
compile 'org.apache.axis:axis-jaxrpc:1.4' compile 'org.apache.axis:axis-jaxrpc:1.4'
compile 'org.apache.axis:axis:1.4' compile 'org.apache.axis:axis:1.4'
compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.3' compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.3'
compile 'org.kordamp.ikonli:ikonli-javafx:2.1.1'
compile 'org.kordamp.ikonli:ikonli-fontawesome-pack:2.1.1'
} }
task wrapper(type: Wrapper) { task wrapper(type: Wrapper) {

104
sdr-trunk.ipr Normal file
View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<option name="DEFAULT_COMPILER" value="Javac"/>
<resourceExtensions>
<entry name=".+\.(properties|xml|html|dtd|tld)"/>
<entry name=".+\.(gif|png|jpeg|jpg)"/>
</resourceExtensions>
<wildcardResourcePatterns>
<entry name="!?*.class"/>
<entry name="!?*.scala"/>
<entry name="!?*.java"/>
<entry name="!?*.groovy"/>
</wildcardResourcePatterns>
<annotationProcessing enabled="false" useClasspath="true"/>
<bytecodeTargetLevel target="1.8"/>
</component>
<component name="CopyrightManager" default="">
<module2copyright/>
</component>
<component name="DependencyValidationManager">
<option name="SKIP_IMPORT_STATEMENTS" value="false"/>
</component>
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false"/>
<component name="GradleUISettings">
<setting name="root"/>
</component>
<component name="GradleUISettings2">
<setting name="root"/>
</component>
<component name="IdProvider" IDEtalkID="11DA1DB66DD62DDA1ED602B7079FE97C"/>
<component name="JavadocGenerationManager">
<option name="OUTPUT_DIRECTORY"/>
<option name="OPTION_SCOPE" value="protected"/>
<option name="OPTION_HIERARCHY" value="true"/>
<option name="OPTION_NAVIGATOR" value="true"/>
<option name="OPTION_INDEX" value="true"/>
<option name="OPTION_SEPARATE_INDEX" value="true"/>
<option name="OPTION_DOCUMENT_TAG_USE" value="false"/>
<option name="OPTION_DOCUMENT_TAG_AUTHOR" value="false"/>
<option name="OPTION_DOCUMENT_TAG_VERSION" value="false"/>
<option name="OPTION_DOCUMENT_TAG_DEPRECATED" value="true"/>
<option name="OPTION_DEPRECATED_LIST" value="true"/>
<option name="OTHER_OPTIONS" value=""/>
<option name="HEAP_SIZE"/>
<option name="LOCALE"/>
<option name="OPEN_IN_BROWSER" value="true"/>
</component>
<component name="ProjectModuleManager">
<modules>
<module filepath="$PROJECT_DIR$/sdr-trunk.iml" fileurl="file://$PROJECT_DIR$/sdr-trunk.iml"/>
</modules>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" assert-keyword="true" jdk-15="true" project-jdk-type="JavaSDK" assert-jdk-15="true" project-jdk-name="1.8">
<output url="file://$PROJECT_DIR$/out"/>
</component>
<component name="SvnBranchConfigurationManager">
<option name="mySupportsUserInfoFilter" value="true"/>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="" vcs=""/>
</component>
<component name="masterDetails">
<states>
<state key="ArtifactsStructureConfigurable.UI">
<UIState>
<splitter-proportions>
<SplitterProportionsDataImpl/>
</splitter-proportions>
<settings/>
</UIState>
</state>
<state key="Copyright.UI">
<UIState>
<splitter-proportions>
<SplitterProportionsDataImpl/>
</splitter-proportions>
</UIState>
</state>
<state key="ProjectJDKs.UI">
<UIState>
<splitter-proportions>
<SplitterProportionsDataImpl>
<option name="proportions">
<list>
<option value="0.2"/>
</list>
</option>
</SplitterProportionsDataImpl>
</splitter-proportions>
<last-edited>1.6</last-edited>
</UIState>
</state>
<state key="ScopeChooserConfigurable.UI">
<UIState>
<splitter-proportions>
<SplitterProportionsDataImpl/>
</splitter-proportions>
<settings/>
</UIState>
</state>
</states>
</component>
</project>

View File

@@ -1,6 +1,6 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014 Dennis Sheirer * Copyright (C) 2014-2018 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -30,61 +30,78 @@ import java.util.Arrays;
*/ */
public class ComplexCircularBuffer public class ComplexCircularBuffer
{ {
Complex[] mBuffer; Complex[] mBuffer;
int mBufferPointer = 0; int mBufferPointer = 0;
public ComplexCircularBuffer( int size ) public ComplexCircularBuffer(int size)
{ {
mBuffer = new Complex[ size ]; mBuffer = new Complex[size];
Arrays.fill( mBuffer, new Complex( 1.0f, 1.0f ) ); Arrays.fill(mBuffer, new Complex(1.0f, 1.0f));
} }
/** public int length()
* Returns the sample currently pointed to in the buffer and stores the {
* new sample in its place return mBuffer.length;
* @param sample }
* @return
*/
public Complex get( Complex sample )
{
Complex retVal = mBuffer[ mBufferPointer ];
put( sample ); /**
* Returns the sample currently pointed to in the buffer and stores the
* new sample in its place
*
* @param sample
* @return
*/
public Complex get(Complex sample)
{
Complex retVal = mBuffer[mBufferPointer];
return retVal; put(sample);
}
public void put( Complex sample ) return retVal;
{ }
mBuffer[ mBufferPointer ] = sample;
mBufferPointer++; public void put(Complex sample)
{
mBuffer[mBufferPointer] = sample;
if( mBufferPointer >= mBuffer.length ) mBufferPointer++;
{
mBufferPointer = 0;
}
}
public Complex[] get( int count ) if(mBufferPointer >= mBuffer.length)
{ {
Complex[] samples = new Complex[ count ]; mBufferPointer = 0;
}
}
int tempCounter = mBufferPointer; /**
* Returns the (count) oldest samples in a time ordered array
*/
public Complex[] get(int count)
{
Complex[] samples = new Complex[count];
for( int x = 0; x < count; x++ ) int tempCounter = mBufferPointer;
{
samples[ x ] = mBuffer[ tempCounter ];
tempCounter++; for(int x = 0; x < count; x++)
{
samples[x] = mBuffer[tempCounter];
if( tempCounter > mBuffer.length - 1 ) tempCounter++;
{
tempCounter = 0;
}
}
return samples; if(tempCounter > mBuffer.length - 1)
} {
tempCounter = 0;
}
}
return samples;
}
/**
* Returns the samples in a time-ordered array
*/
public Complex[] getAll()
{
return get(length());
}
} }

View File

@@ -17,64 +17,164 @@
******************************************************************************/ ******************************************************************************/
package io.github.dsheirer.buffer; package io.github.dsheirer.buffer;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
import org.apache.commons.math3.stat.descriptive.moment.Variance;
import org.apache.commons.math3.stat.descriptive.rank.Max;
import org.apache.commons.math3.stat.descriptive.rank.Min;
import java.util.Arrays; import java.util.Arrays;
/** /**
* Circular sample buffer - allocates a buffer and stores samples in a circular * Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with
* fashion, overwriting older samples with newly arrived samples. Initially * newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value.
* fills buffer with 0-valued samples
*
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
*/ */
public class DoubleCircularBuffer public class DoubleCircularBuffer
{ {
double[] mBuffer; private StandardDeviation mStandardDeviation;
int mBufferPointer = 0; private Variance mVariance;
private Max mMax;
private Mean mMean;
private Min mMin;
private double[] mBuffer;
private int mBufferPointer = 0;
public DoubleCircularBuffer( int size ) /**
{ * Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
mBuffer = new double[ size ]; * @param size of the buffer
* @param initialFillValue to fill the buffer entries
*/
public DoubleCircularBuffer(int size, double initialFillValue)
{
mBuffer = new double[size];
Arrays.fill(mBuffer, initialFillValue);
}
Arrays.fill( mBuffer, 0.0 ); /**
} * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
* @param size of the buffer
*/
public DoubleCircularBuffer(int size)
{
this(size, 0.0);
}
/** /**
* Puts the new value into the buffer and returns the oldest buffer value * Puts the new value into the buffer and returns the oldest buffer value that it replaced
* that it replaced *
* @param newValue * @param newValue to add to the buffer
* @return * @return oldest value that was overwritten by the new value
*/ */
public double get( double newValue ) public double get(double newValue)
{ {
double oldestSample = mBuffer[ mBufferPointer ]; double oldestSample = mBuffer[mBufferPointer];
mBuffer[ mBufferPointer ] = newValue; mBuffer[mBufferPointer] = newValue;
mBufferPointer++; mBufferPointer++;
if( mBufferPointer >= mBuffer.length ) if(mBufferPointer >= mBuffer.length)
{ {
mBufferPointer = 0; mBufferPointer = 0;
} }
return oldestSample; return oldestSample;
} }
/** public double[] getAll()
* Returns the maximum from the values currently in the buffer {
*/ int valuePointer = 0;
public double max() double[] values = new double[mBuffer.length];
{ int bufferPointer = mBufferPointer;
double max = 0.0d;
for( int x = 0; x < mBuffer.length; x++ ) while(valuePointer < values.length)
{ {
if( mBuffer[ x ] > max ) values[valuePointer++] = mBuffer[bufferPointer++];
{
max = mBuffer[ x ];
}
}
return max; if(bufferPointer >= mBuffer.length)
} {
bufferPointer = 0;
}
}
return values;
}
/**
* Places the new value into the buffer, overwriting the oldest value
* @param newValue to add to the buffer
*/
public void put(double newValue)
{
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
/**
* Returns the maximum from the values currently in the buffer
*/
public double max()
{
if(mMax == null)
{
mMax = new Max();
}
return mMax.evaluate(mBuffer, 0, mBuffer.length);
}
/**
* Returns the minimum from the values currently in the buffer
*/
public double min()
{
if(mMin == null)
{
mMin = new Min();
}
return mMin.evaluate(mBuffer, 0, mBuffer.length);
}
public double standardDeviation()
{
if(mStandardDeviation == null)
{
mStandardDeviation = new StandardDeviation();
}
return mStandardDeviation.evaluate(mBuffer);
}
/**
* Calculates the variance of the values contained in this buffer
*/
public double variance()
{
if(mVariance == null)
{
mVariance = new Variance();
}
return mVariance.evaluate(mBuffer);
}
/**
* Calculates the average/mean of the values contained in this buffer
*/
public double mean()
{
if(mMean == null)
{
mMean = new Mean();
}
return mMean.evaluate(mBuffer, 0, mBuffer.length);
}
} }

View File

@@ -25,94 +25,96 @@ import java.util.Arrays;
* fills buffer with 0-valued samples * fills buffer with 0-valued samples
* *
* Can be used as a delay-type buffer, to delay samples by the 'size' amount * Can be used as a delay-type buffer, to delay samples by the 'size' amount
* @author denny
* *
* @author denny
*/ */
public class FloatCircularBuffer public class FloatCircularBuffer
{ {
float[] mBuffer; float[] mBuffer;
int mBufferPointer = 0; int mBufferPointer = 0;
public FloatCircularBuffer( int size ) /**
{ * Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
mBuffer = new float[ size ]; *
* @param size of the buffer
* @param initialFillValue to fill the buffer entries
*/
public FloatCircularBuffer(int size, float initialFillValue)
{
mBuffer = new float[size];
Arrays.fill(mBuffer, initialFillValue);
}
Arrays.fill( mBuffer, (float)0.0 ); /**
} * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
*
* @param size of the buffer
*/
public FloatCircularBuffer(int size)
{
this(size, 0.0f);
}
public int getSize() public float[] getBuffer()
{ {
return mBuffer.length; return mBuffer;
} }
public float[] getBuffer() /**
{ * Puts the new value into the buffer and returns the oldest buffer value that it replaced
return mBuffer; *
} * @param newValue to add to the buffer
* @return oldest value that was overwritten by the new value
*/
public float get(float newValue)
{
float oldestSample = mBuffer[mBufferPointer];
/** mBuffer[mBufferPointer] = newValue;
* Puts the new value into the buffer and returns the oldest buffer value
* that it replaced
* @param newValue
* @return
*/
public float get( float newValue )
{
float oldestSample = mBuffer[ mBufferPointer ];
mBuffer[ mBufferPointer ] = newValue; mBufferPointer++;
mBufferPointer++; if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
if( mBufferPointer >= mBuffer.length ) return oldestSample;
{ }
mBufferPointer = 0;
}
return oldestSample; public float[] getAll()
} {
int valuePointer = 0;
float[] values = new float[mBuffer.length];
int bufferPointer = mBufferPointer;
/** while(valuePointer < values.length)
* Convenience method to put a new value in the buffer, while discarding {
* the old value values[valuePointer++] = mBuffer[bufferPointer++];
*/
public void put( float newValue )
{
get( newValue );
}
/** if(bufferPointer >= mBuffer.length)
* Returns an array of arraySize, loaded with sampleCount of the {
* oldest samples, loaded into the array starting at index 0. bufferPointer = 0;
* }
* This is useful for fetching an array filled half way with samples to }
* use in FFT processing.
* @param arraySize - size of the array (must be larger than sampleCount)
* @param sampleCount - number of the oldest samples in the buffer to load
* @return
*/
public float[] get( int arraySize, int sampleCount )
{
float[] samples = new float[ arraySize ];
if( arraySize > sampleCount ) return values;
{ }
int tempCounter = mBufferPointer;
for( int x = 0; x < sampleCount; x++ ) /**
{ * Places the new value into the buffer, overwriting the oldest value
samples[ x ] = mBuffer[ tempCounter ]; *
* @param newValue to add to the buffer
*/
public void put(float newValue)
{
mBuffer[mBufferPointer] = newValue;
tempCounter++; mBufferPointer++;
if( tempCounter > mBuffer.length )
{
tempCounter = 0;
}
}
}
return samples;
}
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
} }

View File

@@ -137,7 +137,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{ {
for(Channel configuredChannel : mTrafficChannelPool) for(Channel configuredChannel : mTrafficChannelPool)
{ {
if(!configuredChannel.getEnabled()) if(!configuredChannel.isProcessing())
{ {
channel = configuredChannel; channel = configuredChannel;
break; break;
@@ -207,7 +207,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
//Request to enable the channel //Request to enable the channel
mChannelModel.broadcast(trafficChannelEvent); mChannelModel.broadcast(trafficChannelEvent);
if(channel.getEnabled()) if(channel.isProcessing())
{ {
mTrafficChannelsInUse.put(callEvent.getChannel(), channel); mTrafficChannelsInUse.put(callEvent.getChannel(), channel);
} }

View File

@@ -0,0 +1,105 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.controller.channel;
import javax.swing.table.AbstractTableModel;
import java.util.List;
/**
* Auto-Start Channel Model
*/
public class AutoStartChannelModel extends AbstractTableModel
{
private static final long serialVersionUID = 1L;
public static final int COLUMN_ORDER = 0;
public static final int COLUMN_SYSTEM = 1;
public static final int COLUMN_SITE = 2;
public static final int COLUMN_NAME = 3;
public static final int COLUMN_DECODER = 4;
private static final String[] COLUMN_NAMES = new String[] {"Order", "System", "Site", "Name", "Decoder"};
private List<Channel> mChannels;
public AutoStartChannelModel(List<Channel> autoStartChannels)
{
mChannels = autoStartChannels;
}
@Override
public int getRowCount()
{
return mChannels.size();
}
@Override
public int getColumnCount()
{
return COLUMN_NAMES.length;
}
@Override
public String getColumnName(int columnIndex)
{
if(columnIndex < COLUMN_NAMES.length)
{
return COLUMN_NAMES[columnIndex];
}
return null;
}
@Override
public Class<?> getColumnClass(int columnIndex)
{
return String.class;
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex)
{
return false;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex)
{
Channel channel = mChannels.get(rowIndex);
switch(columnIndex)
{
case COLUMN_ORDER:
return channel.getAutoStartOrder();
case COLUMN_SYSTEM:
return channel.getSystem();
case COLUMN_SITE:
return channel.getSite();
case COLUMN_NAME:
return channel.getName();
case COLUMN_DECODER:
return channel.getDecodeConfiguration().getDecoderType().getShortDisplayString();
}
return null;
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex)
{
throw new IllegalArgumentException("Not yet implemented");
}
}

View File

@@ -1,6 +1,6 @@
/******************************************************************************* /*******************************************************************************
* sdrtrunk * sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer * Copyright (C) 2014-2018 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -45,18 +45,18 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
public class Channel extends Configuration implements Listener<FrequencyChangeEvent> public class Channel extends Configuration implements Listener<FrequencyChangeEvent>
{ {
// Standard channels are persisted and traffic channels are temporary // Standard channels are persisted and traffic channels are temporary
public enum ChannelType {STANDARD, TRAFFIC} public enum ChannelType
{
STANDARD, TRAFFIC
}
// Static unique channel identifier tracking // Static unique channel identifier tracking
private static int UNIQUE_ID = 0; private static int UNIQUE_ID = 0;
private DecodeConfiguration mDecodeConfiguration = DecoderFactory.getDefaultDecodeConfiguration(); private DecodeConfiguration mDecodeConfiguration = DecoderFactory.getDefaultDecodeConfiguration();
private AuxDecodeConfiguration mAuxDecodeConfiguration = private AuxDecodeConfiguration mAuxDecodeConfiguration = new AuxDecodeConfiguration();
new AuxDecodeConfiguration(); private SourceConfiguration mSourceConfiguration = SourceConfigFactory.getDefaultSourceConfiguration();
private SourceConfiguration mSourceConfiguration = private EventLogConfiguration mEventLogConfiguration = new EventLogConfiguration();
SourceConfigFactory.getDefaultSourceConfiguration();
private EventLogConfiguration mEventLogConfiguration =
new EventLogConfiguration();
private RecordConfiguration mRecordConfiguration = new RecordConfiguration(); private RecordConfiguration mRecordConfiguration = new RecordConfiguration();
private String mAliasListName; private String mAliasListName;
@@ -64,7 +64,9 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
private String mSite = "Site"; private String mSite = "Site";
private String mName = "Channel"; private String mName = "Channel";
private boolean mEnabled; private boolean mProcessing;
private boolean mAutoStart;
private Integer mAutoStartOrder;
private boolean mSelected; private boolean mSelected;
private TunerChannel mTunerChannel = null; private TunerChannel mTunerChannel = null;
@@ -113,6 +115,8 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
channel.setSystem(mSystem); channel.setSystem(mSystem);
channel.setSite(mSite); channel.setSite(mSite);
channel.setAliasListName(mAliasListName); channel.setAliasListName(mAliasListName);
channel.setAutoStart(mAutoStart);
channel.setAutoStartOrder(mAutoStartOrder);
AuxDecodeConfiguration aux = new AuxDecodeConfiguration(); AuxDecodeConfiguration aux = new AuxDecodeConfiguration();
@@ -258,27 +262,90 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
} }
/** /**
* Indicates if this channel is enabled for processing. * Indicates if this channel is currently started and processing.
*/ */
@JacksonXmlProperty(isAttribute = true, localName = "enabled") @JsonIgnore
public boolean getEnabled() public boolean isProcessing()
{ {
return mEnabled; return mProcessing;
} }
/** /**
* Indicates if this channel will automatically start processing on * Indicates if this channel is currently started and processing.
* application startup or if this channel is currently processing after
* application startup.
* *
* Note: this method is package private and is intended to be managed * Note: this method is package private and is intended to be managed by a channel processing manager instance
* by a co-package central processing manager
* *
* @see ChannelProcessingManager * @see ChannelProcessingManager
*/ */
void setEnabled(boolean enabled) void setProcessing(boolean processing)
{ {
mEnabled = enabled; mProcessing = processing;
}
/**
* Indicates if this channel has auto-start enabled.
*
* Note: the Json (de)serialization tag for auto-start is 'enabled'. We've re-purposed the now
* transient enabled property for the auto-start persisted value to maintain backward compatibility.
*
* @return true if this channel should be auto-started on application startup
*/
@JacksonXmlProperty(isAttribute = true, localName = "enabled")
public boolean getAutoStart()
{
return mAutoStart;
}
/**
* Indicates if this channel is set for auto-start
*/
@JsonIgnore
public boolean isAutoStart()
{
return mAutoStart;
}
/**
* Sets the auto-start status for this channel.
*
* @param autoStart
*/
public void setAutoStart(boolean autoStart)
{
mAutoStart = autoStart;
}
/**
* Auto-start order. Specifies an optional ordering for channels so that when the application starts up,
* the channels can be started in a user-specified order.
*
* @return auto-start order or null
*/
@JacksonXmlProperty(isAttribute = true, localName = "order")
public Integer getAutoStartOrder()
{
return mAutoStartOrder;
}
/**
* Sets the auto-start order for this channel
*
* @param order
*/
public void setAutoStartOrder(Integer order)
{
mAutoStartOrder = order;
}
/**
* Indicates if this channel has an auto-start order specified (ie non-null).
*
* @return true if this channel has an auto-start order
*/
@JsonIgnore
public boolean hasAutoStartOrder()
{
return mAutoStartOrder != null;
} }
/** /**

View File

@@ -0,0 +1,246 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.controller.channel;
import io.github.dsheirer.util.ThreadPool;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ChannelAutoStartFrame extends JFrame
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelAutoStartFrame.class);
private static final int COUNTDOWN_SECONDS = 10;
private ChannelEventListener mChannelEventListener;
private List<Channel> mChannels;
private JLabel mCountdownLabel;
private JButton mStartButton;
private JButton mCancelButton;
private JTable mChannelTable;
private AtomicBoolean mChannelsStarted = new AtomicBoolean();
private ScheduledFuture<?> mTimerFuture;
/**
* Creates and displays a channel auto-start gui for presenting the user with a list of channels that
* will be automatically started once the countdown timer reaches zero, or the user chooses to start
* now or cancel.
*
* @param listener to receive channel start/enable request(s)
* @param channels to auto-start
*/
public ChannelAutoStartFrame(ChannelEventListener listener, List<Channel> channels)
{
mChannelEventListener = listener;
mChannels = channels;
init();
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
setVisible(true);
startTimer();
}
});
}
private void init()
{
setTitle("Auto-Start Channels");
setSize(new Dimension(400,300));
setLocationRelativeTo(null);
addWindowListener(new WindowAdapter()
{
@Override
public void windowClosing(WindowEvent event)
{
mLog.info("Channel auto-start canceled by user - window closed");
stopTimer();
super.windowClosing(event);
}
});
JPanel panel = new JPanel();
panel.setLayout(new MigLayout("","[grow,fill][grow,fill]",
"[][][grow,fill][]"));
panel.add(new JLabel("The following channels will be automatically"), "span");
mCountdownLabel = new JLabel(getCountdownText(COUNTDOWN_SECONDS));
panel.add(mCountdownLabel, "span");
panel.add(new JScrollPane(getChannelTable()), "span");
mStartButton = new JButton("Start Now");
mStartButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mLog.info("Starting [" + mChannels.size() + "] channels now - user invoked");
startChannels();
stopTimer();
}
});
panel.add(mStartButton);
mCancelButton = new JButton("Cancel");
mCancelButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mLog.info("Channel auto-start canceled by user");
stopTimer();
}
});
panel.add(mCancelButton);
setContentPane(panel);
}
private String getCountdownText(int value)
{
return "started in: " + value + " seconds.";
}
/**
* Updates the countdown text label on the swing event thread
* @param value for the countdown
*/
private void updateCountdownText(final int value)
{
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
mCountdownLabel.setText(getCountdownText(value));
}
});
}
/**
* Creates (once) a table of auto-start channels
*/
private JTable getChannelTable()
{
if(mChannelTable == null)
{
AutoStartChannelModel model = new AutoStartChannelModel(mChannels);
mChannelTable = new JTable(model);
}
return mChannelTable;
}
/**
* Sends an enable (ie start) request for each of the auto start channels.
*
* This method is thread-safe and will only be executed once.
*
* Although the redundant channel-start request would be ignored, this once-only will prevent the chance
* that both the timer and the user would attempt to start the channels at the same time.
*/
private void startChannels()
{
if(mChannelsStarted.compareAndSet(false, true))
{
if(mChannelEventListener != null)
{
for(Channel channel: mChannels)
{
mChannelEventListener.channelChanged(new ChannelEvent(channel, ChannelEvent.Event.REQUEST_ENABLE));
}
}
}
}
/**
* Starts the countdown timer to fire once a second.
*/
private void startTimer()
{
if(mTimerFuture == null)
{
mTimerFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new CountdownTimer(), 0, 1, TimeUnit.SECONDS);
}
}
/**
* Stops the countdown timer and disposes this frame
*/
private void stopTimer()
{
if(mTimerFuture != null)
{
mTimerFuture.cancel(true);
mTimerFuture = null;
}
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
dispose();
}
});
}
/**
* Updates the countdown text and auto-starts the channels once the timer falls below zero.
*/
public class CountdownTimer implements Runnable
{
private int mCount = COUNTDOWN_SECONDS;
@Override
public void run()
{
if(mCount >= 0)
{
updateCountdownText(mCount);
mCount--;
}
else
{
mLog.info("Starting [" + mChannels.size() + "] now - timer invoked");
startChannels();
stopTimer();
}
}
}
}

View File

@@ -8,11 +8,19 @@ import net.coderazzi.filters.gui.AutoChoices;
import net.coderazzi.filters.gui.TableFilterHeader; import net.coderazzi.filters.gui.TableFilterHeader;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import javax.swing.*; import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener; import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*; import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
@@ -236,7 +244,7 @@ public class ChannelController extends JPanel
Channel channel = model.getChannelAtIndex( index ); Channel channel = model.getChannelAtIndex( index );
boolean enabled = channel.getEnabled(); boolean enabled = channel.isProcessing();
if( isSelected ) if( isSelected )
{ {

View File

@@ -30,221 +30,227 @@ import io.github.dsheirer.source.SourceConfigurationEditor;
import io.github.dsheirer.source.SourceManager; import io.github.dsheirer.source.SourceManager;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import javax.swing.*; import javax.swing.JButton;
import java.awt.*; import javax.swing.JLabel;
import javax.swing.JOptionPane;
import java.awt.Color;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
public class ChannelEditor extends Editor<Channel> implements ActionListener, ChannelEventListener public class ChannelEditor extends Editor<Channel> implements ActionListener, ChannelEventListener
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final String ACTION_START = "Start";
private static final String ACTION_STOP = "Stop";
private static final String ACTION_SAVE = "Save";
private static final String ACTION_RESET = "Reset";
private NameConfigurationEditor mNameConfigurationEditor; private NameConfigurationEditor mNameConfigurationEditor;
private SourceConfigurationEditor mSourceConfigurationEditor; private SourceConfigurationEditor mSourceConfigurationEditor;
private DecodeConfigurationEditor mDecodeConfigurationEditor; private DecodeConfigurationEditor mDecodeConfigurationEditor;
private AuxDecodeConfigurationEditor mAuxDecodeConfigurationEditor; private AuxDecodeConfigurationEditor mAuxDecodeConfigurationEditor;
private EventLogConfigurationEditor mEventLogConfigurationEditor; private EventLogConfigurationEditor mEventLogConfigurationEditor;
private RecordConfigurationEditor mRecordConfigurationEditor; private RecordConfigurationEditor mRecordConfigurationEditor;
private JButton mEnableButton = new JButton( "Enable" ); private JButton mProcessingStateButton = new JButton(ACTION_START);
private JLabel mChannelName = new JLabel( "Channel:" ); private JLabel mChannelName = new JLabel("Channel:");
private ChannelModel mChannelModel; private ChannelModel mChannelModel;
private ChannelMapModel mChannelMapModel; private ChannelMapModel mChannelMapModel;
private SourceManager mSourceManager; private SourceManager mSourceManager;
private boolean mChannelEnableRequested = false; private boolean mChannelStartRequested = false;
public ChannelEditor( ChannelModel channelModel, public ChannelEditor(ChannelModel channelModel,
ChannelMapModel channelMapModel, ChannelMapModel channelMapModel,
SourceManager sourceManager, SourceManager sourceManager,
AliasModel aliasModel ) AliasModel aliasModel)
{
mChannelModel = channelModel;
mChannelMapModel = channelMapModel;
mSourceManager = sourceManager;
mNameConfigurationEditor = new NameConfigurationEditor( aliasModel, mChannelModel );
init();
}
private void init()
{
setLayout( new MigLayout( "fill,wrap 3", "[grow,fill][grow,fill][grow,fill]", "[grow,fill][]" ) );
JideTabbedPane tabs = new JideTabbedPane();
tabs.setFont( this.getFont() );
tabs.setForeground( Color.BLACK );
tabs.setTabTrailingComponent( mChannelName );
mNameConfigurationEditor.setSaveRequestListener( this );
tabs.addTab( "Name/Alias", mNameConfigurationEditor );
mSourceConfigurationEditor = new SourceConfigurationEditor( mSourceManager );
mSourceConfigurationEditor.setSaveRequestListener( this );
tabs.addTab( "Source", mSourceConfigurationEditor );
mDecodeConfigurationEditor = new DecodeConfigurationEditor( mChannelMapModel );
mDecodeConfigurationEditor.setSaveRequestListener( this );
tabs.addTab( "Decoder", mDecodeConfigurationEditor );
mAuxDecodeConfigurationEditor = new AuxDecodeConfigurationEditor();
mAuxDecodeConfigurationEditor.setSaveRequestListener( this );
tabs.addTab( "Aux Decoders", mAuxDecodeConfigurationEditor );
mEventLogConfigurationEditor = new EventLogConfigurationEditor();
mEventLogConfigurationEditor.setSaveRequestListener( this );
tabs.addTab( "Logging", mEventLogConfigurationEditor );
mRecordConfigurationEditor = new RecordConfigurationEditor();
mRecordConfigurationEditor.setSaveRequestListener( this );
tabs.addTab( "Recording", mRecordConfigurationEditor );
add( tabs, "span" );
mEnableButton.addActionListener( this );
mEnableButton.setEnabled( false );
mEnableButton.setToolTipText( "Start the currently selected channel running/decoding" );
add( mEnableButton );
JButton btnSave = new JButton( "Save" );
btnSave.setToolTipText( "Save changes to the currently selected channel" );
btnSave.addActionListener( ChannelEditor.this );
add( btnSave );
JButton btnReset = new JButton( "Reset" );
btnReset.setToolTipText( "Reload the currently selected channel" );
btnReset.addActionListener( ChannelEditor.this );
add( btnReset );
}
@Override
public void channelChanged( ChannelEvent event )
{
if( hasItem() && getItem() == event.getChannel() )
{
switch( event.getEvent() )
{
case NOTIFICATION_CONFIGURATION_CHANGE:
case NOTIFICATION_PROCESSING_START:
case NOTIFICATION_PROCESSING_STOP:
setItem( getItem() );
mChannelEnableRequested = false;
break;
case NOTIFICATION_DELETE:
setItem( null );
break;
case NOTIFICATION_ENABLE_REJECTED:
if( mChannelEnableRequested )
{
JOptionPane.showMessageDialog( ChannelEditor.this, "Channel could not be "
+ "enabled. This is likely because there are no tuner channels "
+ "available", "Couldn't enable channel", JOptionPane.INFORMATION_MESSAGE );
mChannelEnableRequested = false;
}
break;
default:
break;
}
}
}
@Override
public void actionPerformed( ActionEvent e )
{ {
String command = e.getActionCommand(); mChannelModel = channelModel;
mChannelMapModel = channelMapModel;
mSourceManager = sourceManager;
mNameConfigurationEditor = new NameConfigurationEditor(aliasModel, mChannelModel);
if( command.contentEquals( "Enable" ) ) init();
{
if( hasItem() )
{
save();
mChannelEnableRequested = true;
mChannelModel.broadcast( new ChannelEvent( getItem(), ChannelEvent.Event.REQUEST_ENABLE ) );
}
}
else if( command.contentEquals( "Disable" ) )
{
if( hasItem() )
{
mChannelModel.broadcast( new ChannelEvent( getItem(), ChannelEvent.Event.REQUEST_DISABLE ) );
}
}
else if( command.contentEquals( "Save" ) )
{
save();
}
else if( command.contentEquals( "Reset" ) )
{
setItem( getItem() );
}
} }
/** private void init()
* Sets the channel configuration in each of the channel editor components {
* Note: this method must be invoked on the swing event dispatch thread setLayout(new MigLayout("fill,wrap 3", "[grow,fill][grow,fill][grow,fill]", "[grow,fill][]"));
*/
public void setItem( final Channel channel )
{
super.setItem( channel );
mNameConfigurationEditor.setItem( channel ); JideTabbedPane tabs = new JideTabbedPane();
mSourceConfigurationEditor.setItem( channel ); tabs.setFont(this.getFont());
mDecodeConfigurationEditor.setItem( channel ); tabs.setForeground(Color.BLACK);
mAuxDecodeConfigurationEditor.setItem( channel );
mEventLogConfigurationEditor.setItem( channel );
mRecordConfigurationEditor.setItem( channel );
if( channel != null ) tabs.setTabTrailingComponent(mChannelName);
{
mChannelName.setText( "Channel: " + channel.getName() ); mNameConfigurationEditor.setSaveRequestListener(this);
mEnableButton.setText( channel.getEnabled() ? "Disable" : "Enable" ); tabs.addTab("Name/Alias", mNameConfigurationEditor);
mEnableButton.setEnabled( true );
mEnableButton.setBackground( channel.getEnabled() ? Color.GREEN : getBackground() ); mSourceConfigurationEditor = new SourceConfigurationEditor(mSourceManager);
} mSourceConfigurationEditor.setSaveRequestListener(this);
else tabs.addTab("Source", mSourceConfigurationEditor);
{
mChannelName.setText( "Channel: " ); mDecodeConfigurationEditor = new DecodeConfigurationEditor(mChannelMapModel);
mEnableButton.setText( "Enable" ); mDecodeConfigurationEditor.setSaveRequestListener(this);
mEnableButton.setEnabled( false ); tabs.addTab("Decoder", mDecodeConfigurationEditor);
mEnableButton.setBackground( getBackground() );
} mAuxDecodeConfigurationEditor = new AuxDecodeConfigurationEditor();
} mAuxDecodeConfigurationEditor.setSaveRequestListener(this);
tabs.addTab("Aux Decoders", mAuxDecodeConfigurationEditor);
mEventLogConfigurationEditor = new EventLogConfigurationEditor();
mEventLogConfigurationEditor.setSaveRequestListener(this);
tabs.addTab("Logging", mEventLogConfigurationEditor);
mRecordConfigurationEditor = new RecordConfigurationEditor();
mRecordConfigurationEditor.setSaveRequestListener(this);
tabs.addTab("Recording", mRecordConfigurationEditor);
add(tabs, "span");
mProcessingStateButton.addActionListener(this);
mProcessingStateButton.setEnabled(false);
mProcessingStateButton.setToolTipText("Start/Stop the currently selected channel");
add(mProcessingStateButton);
JButton btnSave = new JButton(ACTION_SAVE);
btnSave.setToolTipText("Save changes to the currently selected channel");
btnSave.addActionListener(ChannelEditor.this);
add(btnSave);
JButton btnReset = new JButton(ACTION_RESET);
btnReset.setToolTipText("Reload the currently selected channel");
btnReset.addActionListener(ChannelEditor.this);
add(btnReset);
}
@Override
public void channelChanged(ChannelEvent event)
{
if(hasItem() && getItem() == event.getChannel())
{
switch(event.getEvent())
{
case NOTIFICATION_CONFIGURATION_CHANGE:
case NOTIFICATION_PROCESSING_START:
case NOTIFICATION_PROCESSING_STOP:
setItem(getItem());
mChannelStartRequested = false;
break;
case NOTIFICATION_DELETE:
setItem(null);
break;
case NOTIFICATION_START_PROCESSING_REJECTED:
if(mChannelStartRequested)
{
JOptionPane.showMessageDialog(ChannelEditor.this, "Channel could not be "
+ "started. This is likely because there are no tuner channels "
+ "available", "Couldn't start channel", JOptionPane.INFORMATION_MESSAGE);
mChannelStartRequested = false;
}
break;
default:
break;
}
}
}
@Override
public void actionPerformed(ActionEvent e)
{
String command = e.getActionCommand();
if(command.contentEquals(ACTION_START))
{
if(hasItem())
{
save();
mChannelStartRequested = true;
mChannelModel.broadcast(new ChannelEvent(getItem(), ChannelEvent.Event.REQUEST_ENABLE));
}
}
else if(command.contentEquals(ACTION_STOP))
{
if(hasItem())
{
mChannelModel.broadcast(new ChannelEvent(getItem(), ChannelEvent.Event.REQUEST_DISABLE));
}
}
else if(command.contentEquals(ACTION_SAVE))
{
save();
}
else if(command.contentEquals(ACTION_RESET))
{
setItem(getItem());
}
}
/**
* Sets the channel configuration in each of the channel editor components
* Note: this method must be invoked on the swing event dispatch thread
*/
public void setItem(final Channel channel)
{
super.setItem(channel);
mNameConfigurationEditor.setItem(channel);
mSourceConfigurationEditor.setItem(channel);
mDecodeConfigurationEditor.setItem(channel);
mAuxDecodeConfigurationEditor.setItem(channel);
mEventLogConfigurationEditor.setItem(channel);
mRecordConfigurationEditor.setItem(channel);
if(channel != null)
{
mChannelName.setText("Channel: " + channel.getName());
mProcessingStateButton.setText(channel.isProcessing() ? ACTION_STOP : ACTION_START);
mProcessingStateButton.setEnabled(true);
mProcessingStateButton.setBackground(channel.isProcessing() ? Color.GREEN : getBackground());
}
else
{
mChannelName.setText("Channel: ");
mProcessingStateButton.setText(ACTION_START);
mProcessingStateButton.setEnabled(false);
mProcessingStateButton.setBackground(getBackground());
}
}
public void save() public void save()
{ {
if( hasItem() ) if(hasItem())
{ {
mNameConfigurationEditor.save(); mNameConfigurationEditor.save();
mSourceConfigurationEditor.save(); mSourceConfigurationEditor.save();
mDecodeConfigurationEditor.save(); mDecodeConfigurationEditor.save();
mAuxDecodeConfigurationEditor.save(); mAuxDecodeConfigurationEditor.save();
mEventLogConfigurationEditor.save(); mEventLogConfigurationEditor.save();
mRecordConfigurationEditor.save(); mRecordConfigurationEditor.save();
try try
{ {
mDecodeConfigurationEditor.validate( mSourceConfigurationEditor ); mDecodeConfigurationEditor.validate(mSourceConfigurationEditor);
mDecodeConfigurationEditor.validate( mAuxDecodeConfigurationEditor ); mDecodeConfigurationEditor.validate(mAuxDecodeConfigurationEditor);
} }
catch( EditorValidationException e ) catch(EditorValidationException e)
{ {
e.getEditor().requestFocus(); e.getEditor().requestFocus();
JOptionPane.showMessageDialog( e.getEditor(), e.getReason(), JOptionPane.showMessageDialog(e.getEditor(), e.getReason(),
"Configuration Error", JOptionPane.ERROR_MESSAGE ); "Configuration Error", JOptionPane.ERROR_MESSAGE);
return; return;
} }
mChannelModel.broadcast( new ChannelEvent( getItem(), mChannelModel.broadcast(new ChannelEvent(getItem(),
ChannelEvent.Event.NOTIFICATION_CONFIGURATION_CHANGE ) ); ChannelEvent.Event.NOTIFICATION_CONFIGURATION_CHANGE));
} }
setModified( false ); setModified(false);
} }
} }

View File

@@ -1,6 +1,6 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer * Copyright (C) 2014-2018 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -43,6 +43,11 @@ public class ChannelEvent
return mEvent; return mEvent;
} }
public String toString()
{
return getEvent().name() + " Channel:" + getChannel().getName();
}
/** /**
* Channel events to describe the specific event * Channel events to describe the specific event
*/ */
@@ -55,7 +60,7 @@ public class ChannelEvent
//Channel is deleted/removed //Channel is deleted/removed
NOTIFICATION_DELETE, NOTIFICATION_DELETE,
//Channel enable request was rejected //Channel enable request was rejected
NOTIFICATION_ENABLE_REJECTED, NOTIFICATION_START_PROCESSING_REJECTED,
//Channel has started processing/decoding //Channel has started processing/decoding
NOTIFICATION_PROCESSING_START, NOTIFICATION_PROCESSING_START,
//Channel has stopped processing/decoding //Channel has stopped processing/decoding

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.module.decode.DecoderType;
import javax.swing.table.AbstractTableModel; import javax.swing.table.AbstractTableModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@@ -42,9 +43,11 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
public static final int COLUMN_ALIAS_LIST = 4; public static final int COLUMN_ALIAS_LIST = 4;
public static final int COLUMN_SOURCE = 5; public static final int COLUMN_SOURCE = 5;
public static final int COLUMN_DECODER = 6; public static final int COLUMN_DECODER = 6;
public static final int COLUMN_AUTO_START = 7;
private static final String[] COLUMN_NAMES = new String[] {"Enabled", "System", "Site", "Name", "Alias List", private static final String[] COLUMN_NAMES = new String[] {"Playing", "System", "Site", "Name", "Alias List",
"Source", "Decoder"}; "Source", "Decoder", "Auto-Start"};
private static final String VALUE_YES = "Yes";
private List<Channel> mChannels = new CopyOnWriteArrayList<>(); private List<Channel> mChannels = new CopyOnWriteArrayList<>();
private List<Channel> mTrafficChannels = new CopyOnWriteArrayList<>(); private List<Channel> mTrafficChannels = new CopyOnWriteArrayList<>();
@@ -209,11 +212,6 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
broadcast(new ChannelEvent(channel, Event.NOTIFICATION_ADD)); broadcast(new ChannelEvent(channel, Event.NOTIFICATION_ADD));
if(channel.getEnabled())
{
broadcast(new ChannelEvent(channel, Event.REQUEST_ENABLE));
}
return index; return index;
} }
@@ -247,6 +245,48 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
} }
} }
/**
* List of channels that have the auto-start flag set.
*
* @return auto-start channels sorted by the channel order value.
*/
public List<Channel> getAutoStartChannels()
{
List<Channel> autoStartChannels = new ArrayList<>();
for(Channel channel: getChannels())
{
if(channel.isAutoStart())
{
autoStartChannels.add(channel);
}
}
Collections.sort(autoStartChannels, new Comparator<Channel>()
{
@Override
public int compare(Channel channel1, Channel channel2)
{
if(channel1.hasAutoStartOrder() && channel2.hasAutoStartOrder())
{
return Integer.compare(channel1.getAutoStartOrder(), channel2.getAutoStartOrder());
}
else if(channel1.hasAutoStartOrder())
{
return -1;
}
else if(channel2.hasAutoStartOrder())
{
return 1;
}
return 0;
}
});
return autoStartChannels;
}
/** /**
* Returns a list of channels that fall within the frequency range * Returns a list of channels that fall within the frequency range
* *
@@ -331,7 +371,7 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
switch(columnIndex) switch(columnIndex)
{ {
case COLUMN_ENABLED: case COLUMN_ENABLED:
return channel.getEnabled() ? "Enabled" : null; return channel.isProcessing() ? VALUE_YES : null;
case COLUMN_SYSTEM: case COLUMN_SYSTEM:
return channel.getSystem(); return channel.getSystem();
case COLUMN_SITE: case COLUMN_SITE:
@@ -344,6 +384,19 @@ public class ChannelModel extends AbstractTableModel implements IChannelEventBro
return channel.getSourceConfiguration().getDescription(); return channel.getSourceConfiguration().getDescription();
case COLUMN_DECODER: case COLUMN_DECODER:
return channel.getDecodeConfiguration().getDecoderType().getShortDisplayString(); return channel.getDecodeConfiguration().getDecoderType().getShortDisplayString();
case COLUMN_AUTO_START:
if(channel.isAutoStart())
{
if(channel.hasAutoStartOrder())
{
return channel.getAutoStartOrder();
}
else
{
return VALUE_YES;
}
}
break;
} }
return null; return null;

View File

@@ -159,7 +159,7 @@ public class ChannelProcessingManager implements ChannelEventListener
} }
break; break;
case REQUEST_DISABLE: case REQUEST_DISABLE:
if(channel.getEnabled()) if(channel.isProcessing())
{ {
switch(channel.getChannelType()) switch(channel.getChannelType())
{ {
@@ -176,7 +176,7 @@ public class ChannelProcessingManager implements ChannelEventListener
} }
} }
case NOTIFICATION_DELETE: case NOTIFICATION_DELETE:
if(channel.getEnabled()) if(channel.isProcessing())
{ {
stopProcessing(channel, true); stopProcessing(channel, true);
} }
@@ -220,9 +220,9 @@ public class ChannelProcessingManager implements ChannelEventListener
if(source == null) if(source == null)
{ {
channel.setEnabled(false); channel.setProcessing(false);
mChannelModel.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_ENABLE_REJECTED)); mChannelModel.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_START_PROCESSING_REJECTED));
return; return;
} }
@@ -325,7 +325,7 @@ public class ChannelProcessingManager implements ChannelEventListener
getChannelMetadataModel().add(processingChain.getChannelState().getMutableMetadata(), channel); getChannelMetadataModel().add(processingChain.getChannelState().getMutableMetadata(), channel);
channel.setEnabled(true); channel.setProcessing(true);
mProcessingChains.put(channel.getChannelID(), processingChain); mProcessingChains.put(channel.getChannelID(), processingChain);
@@ -334,7 +334,7 @@ public class ChannelProcessingManager implements ChannelEventListener
private void stopProcessing(Channel channel, boolean remove) private void stopProcessing(Channel channel, boolean remove)
{ {
channel.setEnabled(false); channel.setProcessing(false);
if(mProcessingChains.containsKey(channel.getChannelID())) if(mProcessingChains.containsKey(channel.getChannelID()))
{ {

View File

@@ -4,8 +4,11 @@ import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.module.ProcessingChain; import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.module.decode.event.ActivitySummaryFrame; import io.github.dsheirer.module.decode.event.ActivitySummaryFrame;
import javax.swing.*; import javax.swing.JMenu;
import java.awt.*; import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import java.awt.Component;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
@@ -23,7 +26,7 @@ public class ChannelUtils
{ {
JMenu menu = new JMenu( "Channel: " + channel.getName() ); JMenu menu = new JMenu( "Channel: " + channel.getName() );
if( channel.getEnabled() ) if( channel.isProcessing() )
{ {
JMenuItem disable = new JMenuItem( "Disable" ); JMenuItem disable = new JMenuItem( "Disable" );
disable.addActionListener( new ActionListener() disable.addActionListener( new ActionListener()

View File

@@ -23,9 +23,21 @@ import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.swing.*; import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener; import javax.swing.event.DocumentListener;
import java.awt.Dimension;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.util.Collections; import java.util.Collections;
@@ -48,6 +60,8 @@ public class NameConfigurationEditor extends Editor<Channel> implements Document
private JComboBox<String> mSystemNameCombo; private JComboBox<String> mSystemNameCombo;
private JComboBox<String> mSiteNameCombo; private JComboBox<String> mSiteNameCombo;
private JComboBox<String> mAliasListCombo; private JComboBox<String> mAliasListCombo;
private JCheckBox mAutoStartCheckBox;
private JSpinner mAutoStartOrder;
public NameConfigurationEditor(AliasModel aliasModel, ChannelModel model) public NameConfigurationEditor(AliasModel aliasModel, ChannelModel model)
{ {
@@ -139,6 +153,40 @@ public class NameConfigurationEditor extends Editor<Channel> implements Document
add(new JLabel("Site:")); add(new JLabel("Site:"));
add(mSiteNameCombo); add(mSiteNameCombo);
JPanel autoStartPanel = new JPanel();
mAutoStartCheckBox = new JCheckBox("Auto-Start");
mAutoStartCheckBox.setToolTipText("Automatically start this channel each time the application runs.");
mAutoStartCheckBox.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mAutoStartOrder.setEnabled(mAutoStartCheckBox.isSelected());
setModified(true);
}
});
autoStartPanel.add(mAutoStartCheckBox);
SpinnerModel model = new SpinnerNumberModel(0,0,500,1);
mAutoStartOrder = new JSpinner(model);
mAutoStartOrder.setEnabled(false);
mAutoStartOrder.setPreferredSize(new Dimension(60, mAutoStartOrder.getPreferredSize().height));
mAutoStartOrder.setToolTipText("Auto-start order: 1 (high) <> 500 (low). 0 = no ordering");
mAutoStartOrder.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
setModified(true);
}
});
autoStartPanel.add(new JLabel("Order:"));
autoStartPanel.add(mAutoStartOrder);
add(new JLabel()); //Empty space holder
add(autoStartPanel);
} }
@Override @Override
@@ -156,6 +204,11 @@ public class NameConfigurationEditor extends Editor<Channel> implements Document
Object aliasList = mAliasListCombo.getSelectedItem(); Object aliasList = mAliasListCombo.getSelectedItem();
getItem().setAliasListName(aliasList == null ? null : (String)aliasList); getItem().setAliasListName(aliasList == null ? null : (String)aliasList);
getItem().setAutoStart(mAutoStartCheckBox.isSelected());
int order = ((Number)mAutoStartOrder.getValue()).intValue();
getItem().setAutoStartOrder((order > 0 ? order : null));
} }
setModified(false); setModified(false);
@@ -182,6 +235,16 @@ public class NameConfigurationEditor extends Editor<Channel> implements Document
{ {
mSiteNameCombo.setEnabled(enabled); mSiteNameCombo.setEnabled(enabled);
} }
if(mAutoStartCheckBox.isEnabled() != enabled)
{
mAutoStartCheckBox.setEnabled(enabled);
}
if(mAutoStartOrder.isEnabled() != enabled)
{
mAutoStartOrder.setEnabled(enabled);
}
} }
@Override @Override
@@ -237,6 +300,19 @@ public class NameConfigurationEditor extends Editor<Channel> implements Document
{ {
mAliasListCombo.setSelectedItem(channel.getAliasListName()); mAliasListCombo.setSelectedItem(channel.getAliasListName());
} }
if(channel.isAutoStart())
{
mAutoStartCheckBox.setSelected(channel.isAutoStart());
mAutoStartOrder.setEnabled(true);
}
else
{
mAutoStartCheckBox.setSelected(false);
mAutoStartOrder.setEnabled(false);
}
mAutoStartOrder.setValue(channel.hasAutoStartOrder() ? channel.getAutoStartOrder() : 0);
} }
else else
{ {
@@ -244,6 +320,7 @@ public class NameConfigurationEditor extends Editor<Channel> implements Document
mChannelName.setText(null); mChannelName.setText(null);
} }
setModified(false); setModified(false);
} }

View File

@@ -666,11 +666,11 @@ public class FilterFactory
* @param alpha - roll-off factor * @param alpha - roll-off factor
* @return - filter coefficients * @return - filter coefficients
*/ */
public static float[] getRootRaisedCosine(int samplesPerSymbol, public static float[] getRootRaisedCosine(float samplesPerSymbol,
int symbols, int symbols,
float alpha) float alpha)
{ {
int taps = samplesPerSymbol * symbols + 1; int taps = (int)(samplesPerSymbol * (float)symbols + 1.0f);
float scale = 0; float scale = 0;
@@ -680,8 +680,8 @@ public class FilterFactory
{ {
float index = (float) x - ((float) taps / 2.0f); float index = (float) x - ((float) taps / 2.0f);
float x1 = (float) Math.PI * index / (float) samplesPerSymbol; float x1 = (float) Math.PI * index / samplesPerSymbol;
float x2 = 4.0f * alpha * index / (float) samplesPerSymbol; float x2 = 4.0f * alpha * index / samplesPerSymbol;
float x3 = x2 * x2 - 1.0f; float x3 = x2 * x2 - 1.0f;
float numerator, denominator; float numerator, denominator;
@@ -692,7 +692,7 @@ public class FilterFactory
{ {
numerator = (float) Math.cos((1.0 + alpha) * x1) + numerator = (float) Math.cos((1.0 + alpha) * x1) +
(float) Math.sin((1.0f - alpha) * x1) / (float) Math.sin((1.0f - alpha) * x1) /
(4.0f * alpha * index / (float) samplesPerSymbol); (4.0f * alpha * index / samplesPerSymbol);
} }
else else
{ {
@@ -713,9 +713,9 @@ public class FilterFactory
x3 = (1.0f - alpha) * x1; x3 = (1.0f - alpha) * x1;
x2 = (1.0f + alpha) * x1; x2 = (1.0f + alpha) * x1;
numerator = (float) (Math.sin(x2) * (1.0f + alpha) * Math.PI - numerator = (float) (Math.sin(x2) * (1.0 + alpha) * Math.PI -
Math.cos(x3) * ((1.0 - alpha) * Math.PI * Math.cos(x3) * ((1.0 - alpha) * Math.PI *
(double) samplesPerSymbol) / (4 * alpha * index) + (double) samplesPerSymbol) / (4.0 * alpha * index) +
Math.sin(x3) * (double) samplesPerSymbol * Math.sin(x3) * (double) samplesPerSymbol *
(double) samplesPerSymbol / (4.0 * alpha * index * index)); (double) samplesPerSymbol / (4.0 * alpha * index * index));

View File

@@ -12,155 +12,169 @@ import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.net.URL;
public class FilterView extends BorderPane public class FilterView extends BorderPane
{ {
private final static Logger mLog = LoggerFactory.getLogger( FilterView.class ); private final static Logger mLog = LoggerFactory.getLogger(FilterView.class);
private CurveFittedAreaChart mImpulseChart; private CurveFittedAreaChart mImpulseChart;
private NumberAxis mImpulseXAxis; private NumberAxis mImpulseXAxis;
private NumberAxis mImpulseYAxis; private NumberAxis mImpulseYAxis;
private CurveFittedAreaChart mDFTChart; private CurveFittedAreaChart mDFTChart;
private NumberAxis mDFTXAxis; private NumberAxis mDFTXAxis;
private NumberAxis mDFTYAxis; private NumberAxis mDFTYAxis;
private static final int FFT_SIZE = 4096; private static final int FFT_SIZE = 4096;
private FloatFFT_1D mFFT = new FloatFFT_1D( FFT_SIZE ); private FloatFFT_1D mFFT = new FloatFFT_1D(FFT_SIZE);
public FilterView( float[] taps ) public FilterView(float[] taps)
{ {
init(); init();
if( taps != null ) if(taps != null)
{ {
setImpulseTaps( taps ); setImpulseTaps(taps);
float[] dft = new float[ FFT_SIZE ]; float[] dft = new float[FFT_SIZE];
System.arraycopy( taps, 0, dft, 0, taps.length ); System.arraycopy(taps, 0, dft, 0, taps.length);
mFFT.realForward( dft ); mFFT.realForward(dft);
setDFTTaps( convertDFTToDecibel( dft, taps.length ) ); setDFTTaps(convertDFTToDecibel(dft, taps.length));
} }
} }
private void init() private void init()
{ {
HBox hbox = new HBox(); HBox hbox = new HBox();
mImpulseXAxis = new NumberAxis( 0.0, 0.5, 1.0 ); mImpulseXAxis = new NumberAxis(0.0, 0.5, 1.0);
mImpulseYAxis = new NumberAxis( -0.2, 1.2, .1 ); mImpulseYAxis = new NumberAxis(-0.2, 1.2, .1);
mImpulseChart = new CurveFittedAreaChart( mImpulseXAxis, mImpulseYAxis ); mImpulseChart = new CurveFittedAreaChart(mImpulseXAxis, mImpulseYAxis);
mImpulseChart.setLegendVisible( false ); mImpulseChart.setLegendVisible(false);
mImpulseChart.setHorizontalGridLinesVisible( false ); mImpulseChart.setHorizontalGridLinesVisible(false);
mImpulseChart.setVerticalGridLinesVisible(false); mImpulseChart.setVerticalGridLinesVisible(false);
mImpulseChart.setAlternativeColumnFillVisible(false); mImpulseChart.setAlternativeColumnFillVisible(false);
mImpulseChart.setAlternativeRowFillVisible(false); mImpulseChart.setAlternativeRowFillVisible(false);
String css = FilterView.class.getResource( "FilterView.css" ).toExternalForm(); URL cssURL = FilterView.class.getResource("FilterView.css");
mImpulseChart.getStylesheets().add( css );
hbox.getChildren().add( mImpulseChart ); String css = null;
HBox.setHgrow( mImpulseChart, Priority.ALWAYS );
mDFTXAxis = new NumberAxis( 0.0, FFT_SIZE / 2, FFT_SIZE / 8 ); if(cssURL != null)
mDFTYAxis = new NumberAxis( -200.0, 10.0, 10.0 );
mDFTChart = new CurveFittedAreaChart( mDFTXAxis, mDFTYAxis );
mDFTChart.setLegendVisible( false );
mDFTChart.setHorizontalGridLinesVisible( false );
mDFTChart.setVerticalGridLinesVisible(false);
mDFTChart.setAlternativeColumnFillVisible(false);
mDFTChart.setAlternativeRowFillVisible(false);
mDFTChart.getStylesheets().add( css );
hbox.getChildren().add( mDFTChart );
HBox.setHgrow( mDFTChart, Priority.ALWAYS );
setCenter( hbox );
}
public void setImpulseTaps( double[] taps )
{
float[] convertedTaps = new float[ taps.length ];
for( int x = 0; x < taps.length; x++ )
{
convertedTaps[ x ] = (float)taps[ x ];
}
setImpulseTaps( convertedTaps );
}
public void setImpulseTaps( float[] taps )
{
double smallest = 0.0;
double largest = 0.0;
final XYChart.Series<Number, Number> series = new XYChart.Series<>();
for( int x = 0; x < taps.length; x++ )
{ {
if( taps[ x ] < smallest ) css = cssURL.toExternalForm();
{
smallest = taps[ x ];
}
if( taps[ x ] > largest )
{
largest = taps[ x ];
}
series.getData().add( new XYChart.Data<Number, Number>( x, taps[ x ] ) );
} }
mImpulseXAxis.setLowerBound( 0.0 ); hbox.getChildren().add(mImpulseChart);
mImpulseXAxis.setUpperBound( taps.length - 1 ); HBox.setHgrow(mImpulseChart, Priority.ALWAYS);
mImpulseXAxis.setTickUnit( 1.0 );
double padding = Math.abs( largest - smallest ) * .1; mDFTXAxis = new NumberAxis(0.0, FFT_SIZE / 2, FFT_SIZE / 8);
mDFTYAxis = new NumberAxis(-200.0, 10.0, 10.0);
mImpulseYAxis.setLowerBound( smallest - padding ); mDFTChart = new CurveFittedAreaChart(mDFTXAxis, mDFTYAxis);
mImpulseYAxis.setUpperBound( largest + padding ); mDFTChart.setLegendVisible(false);
mImpulseYAxis.setTickUnit( padding ); mDFTChart.setHorizontalGridLinesVisible(false);
mDFTChart.setVerticalGridLinesVisible(false);
mDFTChart.setAlternativeColumnFillVisible(false);
mDFTChart.setAlternativeRowFillVisible(false);
if(css != null)
{
mImpulseChart.getStylesheets().add(css);
mDFTChart.getStylesheets().add(css);
}
hbox.getChildren().add(mDFTChart);
HBox.setHgrow(mDFTChart, Priority.ALWAYS);
setCenter(hbox);
}
public void setImpulseTaps(double[] taps)
{
float[] convertedTaps = new float[taps.length];
for(int x = 0; x < taps.length; x++)
{
convertedTaps[x] = (float)taps[x];
}
setImpulseTaps(convertedTaps);
}
public void setImpulseTaps(float[] taps)
{
double smallest = 0.0;
double largest = 0.0;
final XYChart.Series<Number,Number> series = new XYChart.Series<>();
for(int x = 0; x < taps.length; x++)
{
if(taps[x] < smallest)
{
smallest = taps[x];
}
if(taps[x] > largest)
{
largest = taps[x];
}
series.getData().add(new XYChart.Data<Number,Number>(x, taps[x]));
}
mImpulseXAxis.setLowerBound(0.0);
mImpulseXAxis.setUpperBound(taps.length - 1);
mImpulseXAxis.setTickUnit(1.0);
double padding = Math.abs(largest - smallest) * .1;
mImpulseYAxis.setLowerBound(smallest - padding);
mImpulseYAxis.setUpperBound(largest + padding);
mImpulseYAxis.setTickUnit(padding);
ObservableList<Series<Number,Number>> list = mImpulseChart.getData(); ObservableList<Series<Number,Number>> list = mImpulseChart.getData();
list.add( series ); list.add(series);
} }
public void setDFTTaps( float[] taps ) public void setDFTTaps(float[] taps)
{ {
double smallest = 0.0; double smallest = 0.0;
final XYChart.Series<Number, Number> series = new XYChart.Series<>(); final XYChart.Series<Number,Number> series = new XYChart.Series<>();
for( int x = 0; x < taps.length; x++ ) for(int x = 0; x < taps.length; x++)
{ {
if( taps[ x ] < smallest ) if(taps[x] < smallest)
{ {
smallest = taps[ x ]; smallest = taps[x];
} }
series.getData().add( new XYChart.Data<Number, Number>( x, taps[ x ] ) ); series.getData().add(new XYChart.Data<Number,Number>(x, taps[x]));
} }
ObservableList<Series<Number,Number>> list = mDFTChart.getData(); ObservableList<Series<Number,Number>> list = mDFTChart.getData();
list.add( series ); list.add(series);
} }
public float[] convertDFTToDecibel( float[] dft, int tapLength ) public float[] convertDFTToDecibel(float[] dft, int tapLength)
{ {
float[] decibels = new float[ dft.length / 2 ]; float[] decibels = new float[dft.length / 2];
int index = 0; int index = 0;
for( int x = 0; x < decibels.length; x ++ ) for(int x = 0; x < decibels.length; x++)
{ {
index = x * 2; index = x * 2;
decibels[ x ] = 20.0f * (float)Math.log10( ( ( dft[ index ] * dft[ index ] ) + decibels[x] = 20.0f * (float)Math.log10(((dft[index] * dft[index]) +
( dft[ index + 1 ] * dft[ index + 1 ] ) ) ); (dft[index + 1] * dft[index + 1])));
} }
return decibels; return decibels;
} }
} }

View File

@@ -39,15 +39,24 @@ public class FilterViewer extends Application
*/ */
private float[] getFilter() private float[] getFilter()
{ {
// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
// .sampleRate(48000)
// .gridDensity(16)
// .passBandCutoff(2500)
// .passBandAmplitude(1.0)
// .passBandRipple(0.01)
// .stopBandStart(4000)
// .stopBandAmplitude(0.0)
// .stopBandRipple(0.008)
// .build();
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(48000) .sampleRate(48000)
.gridDensity(16) .passBandCutoff(5100)
.passBandCutoff(2500)
.passBandAmplitude(1.0) .passBandAmplitude(1.0)
.passBandRipple(0.01) .passBandRipple(0.01)
.stopBandStart(4000)
.stopBandAmplitude(0.0) .stopBandAmplitude(0.0)
.stopBandRipple(0.008) .stopBandStart(6500)
.stopBandRipple(0.01)
.build(); .build();
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder() // FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
@@ -72,6 +81,9 @@ public class FilterViewer extends Application
float[] taps = null; float[] taps = null;
// taps = FilterFactory.getSinc(6000, 1920, 47, Window.WindowType.BLACKMAN);
// taps = FilterFactory.getRootRaisedCosine(10, 10, 0.2f);
// taps = FilterFactory.getLowPass(48000, 6750, 7500, 60, // taps = FilterFactory.getLowPass(48000, 6750, 7500, 60,
// Window.WindowType.HAMMING, true); // Window.WindowType.HAMMING, true);

View File

@@ -1,54 +1,90 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.filter.fir.complex; package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.dsp.filter.fir.FIRFilter; import io.github.dsheirer.dsp.filter.fir.FIRFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class ComplexFIRFilter extends FIRFilter public class ComplexFIRFilter extends FIRFilter
{ {
private RealFIRFilter mIFilter; private RealFIRFilter mIFilter;
private RealFIRFilter mQFilter; private RealFIRFilter mQFilter;
/** /**
* Complex FIR Filter for processing complex sample pairs. Wraps two real * Complex FIR Filter for processing complex sample pairs. Wraps two real
* FIR filters for processing each of the inphase and quadrature samples. * FIR filters for processing each of the inphase and quadrature samples.
* *
* @param coefficients - filter taps * @param coefficients - filter taps
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain * @param gain - gain to apply to filtered outputs - use 1.0f for no gain
*/ */
public ComplexFIRFilter( float[] coefficients, float gain ) public ComplexFIRFilter(float[] coefficients, float gain)
{ {
mIFilter = new RealFIRFilter( coefficients, gain ); mIFilter = new RealFIRFilter(coefficients, gain);
mQFilter = new RealFIRFilter( coefficients, gain ); mQFilter = new RealFIRFilter(coefficients, gain);
} }
public float[] getCoefficients() public float[] getCoefficients()
{ {
return mIFilter.getCoefficients(); return mIFilter.getCoefficients();
} }
public float filterInphase( float sample ) public float filterInphase(float sample)
{ {
return mIFilter.filter( sample ); return mIFilter.filter(sample);
} }
public float filterQuadrature( float sample ) public float filterQuadrature(float sample)
{ {
return mQFilter.filter( sample ); return mQFilter.filter(sample);
} }
public Complex filter( Complex sample ) public Complex filter(Complex sample)
{ {
float i = filterInphase( sample.inphase() ); float i = filterInphase(sample.inphase());
float q = filterQuadrature( sample.quadrature() ); float q = filterQuadrature(sample.quadrature());
return new Complex( i, q ); return new Complex(i, q);
} }
@Override /**
public void dispose() * Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
{ *
mIFilter.dispose(); * @param buffer with complex samples to filter
mQFilter.dispose(); * @return new buffer containing filtered complex samples
} */
public ComplexBuffer filter(ComplexBuffer buffer)
{
float[] samples = buffer.getSamples();
float[] filtered = new float[samples.length];
for(int x = 0; x < samples.length; x += 2)
{
filtered[x] = filterInphase(samples[x]);
filtered[x + 1] = filterQuadrature(samples[x + 1]);
}
return new ComplexBuffer(filtered);
}
@Override
public void dispose()
{
mIFilter.dispose();
mQFilter.dispose();
}
} }

View File

@@ -3,8 +3,7 @@ package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexBuffer;
public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter public class ComplexFIRFilter_CB_CB extends ComplexFIRFilter implements Listener<ComplexBuffer>
implements Listener<ComplexBuffer>
{ {
private Listener<ComplexBuffer> mListener; private Listener<ComplexBuffer> mListener;

View File

@@ -5,54 +5,86 @@ import org.apache.commons.lang3.Validate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
public class RealInterpolator extends Interpolator public class RealInterpolator extends Interpolator
{ {
private final static Logger mLog = LoggerFactory.getLogger( RealInterpolator.class ); private final static Logger mLog = LoggerFactory.getLogger(RealInterpolator.class);
private float mGain; private float mGain;
public RealInterpolator( float gain ) /**
{ * Provides an interpolated sample point along an eight-sample waveform representation using 128 filters to
mGain = gain; * provide approximate interpolation with a resolution of 1/128th of a sample.
} *
/** * @param gain to apply to the interpolated sample
* Calculates an interpolated value from the 8 samples beginning at the */
* offset index. Uses the mu value to determine which of 128 filter kernels public RealInterpolator(float gain)
* to use in order to closely approximate the ideal frequency response for {
* the value of mu, ranging from 0.0 to 1.0, indicating the interpolated mGain = gain;
* position in the sample set ranging from offset to offset + 7. }
*
* @param samples - sample array of length at least offset + 7
*
* @param mu - interpolated sample position between 0 and 1.0
*
* @return - interpolated sample value
*/
public float filter( float[] samples, int offset, float mu )
{
/* Ensure we have enough samples in the array */
Validate.isTrue(samples.length >= offset + 7);
/* Identify the filter bank that corresponds to mu */ /**
int index = (int)( NSTEPS * mu ); * Calculates an interpolated value from eight samples that start at the offset into the sample array. The
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
* offset+4. The mu argument is translated into an index position between 0 and 128, where a mu of 0.0 will be
* converted to index zero and will be equal to the sample at index offset+3 and a mu of 1.0 will be equal to
* the sample at offset+4. All mu values between 0.0 and 1.0 will be converted to a 1 - 127 index and will
* produce an approximated value from among 127 interpolated sample points between indexes offset+3 and offset+4.
*
* @param samples - sample array of length at least offset + 7
* @param mu - interpolated sample position between 0.0 and 1.0
* @return - interpolated sample value
*/
public float filter(float[] samples, int offset, float mu)
{
/* Ensure we have enough samples in the array */
Validate.isTrue(samples.length >= offset + 7);
float accumulator = ( TAPS[ index ][ 7 ] * samples[ offset ] ); /* Identify the filter bank that corresponds to mu */
accumulator += ( TAPS[ index ][ 6 ] * samples[ offset + 1 ] ); int index = (int)(NSTEPS * mu);
accumulator += ( TAPS[ index ][ 5 ] * samples[ offset + 2 ] );
accumulator += ( TAPS[ index ][ 4 ] * samples[ offset + 3 ] );
accumulator += ( TAPS[ index ][ 3 ] * samples[ offset + 4 ] );
accumulator += ( TAPS[ index ][ 2 ] * samples[ offset + 5 ] );
accumulator += ( TAPS[ index ][ 1 ] * samples[ offset + 6 ] );
accumulator += ( TAPS[ index ][ 0 ] * samples[ offset + 7 ] );
return accumulator * mGain; float accumulator = (TAPS[index][7] * samples[offset]);
} accumulator += (TAPS[index][6] * samples[offset + 1]);
accumulator += (TAPS[index][5] * samples[offset + 2]);
accumulator += (TAPS[index][4] * samples[offset + 3]);
accumulator += (TAPS[index][3] * samples[offset + 4]);
accumulator += (TAPS[index][2] * samples[offset + 5]);
accumulator += (TAPS[index][1] * samples[offset + 6]);
accumulator += (TAPS[index][0] * samples[offset + 7]);
public Complex filter( float[] iSamples, float[] qSamples, int offset, float mu ) return accumulator * mGain;
{ }
float i = filter( iSamples, offset, mu );
float q = filter( qSamples, offset, mu );
return new Complex( i, q ); public Complex filter(float[] iSamples, float[] qSamples, int offset, float mu)
} {
float i = filter(iSamples, offset, mu);
float q = filter(qSamples, offset, mu);
return new Complex(i, q);
}
public static void main(String[] args)
{
RealInterpolator interpolator = new RealInterpolator(1.0f);
DecimalFormat decimalFormat = new DecimalFormat("0.0000");
double TWO_PI = Math.PI * 2.0;
float[] samples = new float[16];
for(int x = 0; x < 16; x++)
{
samples[x] = (float)Math.sin(TWO_PI * (double)x / 8.0);
}
mLog.debug("Samples: " + Arrays.toString(samples));
for(float x = 0.0f; x <= 1.01f; x += 0.1f)
{
mLog.debug(decimalFormat.format(x) + ": " + decimalFormat.format(interpolator.filter(samples, 1, x)));
}
}
} }

View File

@@ -17,62 +17,87 @@
******************************************************************************/ ******************************************************************************/
package io.github.dsheirer.dsp.fm; package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.sample.complex.Complex;
public abstract class FMDemodulator public abstract class FMDemodulator
{ {
private float mPreviousI = 0.0f; private float mPreviousI = 0.0f;
private float mPreviousQ = 0.0f; private float mPreviousQ = 0.0f;
protected float mGain; protected float mGain;
public FMDemodulator( float gain ) public FMDemodulator(float gain)
{ {
mGain = gain; mGain = gain;
} }
public float demodulate( float currentI, float currentQ ) public float demodulate(float currentI, float currentQ)
{ {
/** /**
* Multiply the current sample against the complex conjugate of the * Multiply the current sample against the complex conjugate of the
* previous sample to derive the phase delta between the two samples * previous sample to derive the phase delta between the two samples
* *
* Negating the previous sample quadrature produces the conjugate * Negating the previous sample quadrature produces the conjugate
*/ */
double inphase = ( currentI * mPreviousI ) - ( currentQ * -mPreviousQ ); double inphase = (currentI * mPreviousI) - (currentQ * -mPreviousQ);
double quadrature = ( currentQ * mPreviousI ) + ( currentI * -mPreviousQ ); double quadrature = (currentQ * mPreviousI) + (currentI * -mPreviousQ);
double angle = 0.0f; double angle = 0.0f;
//Check for divide by zero //Check for divide by zero
if( inphase != 0 ) if(inphase != 0)
{ {
/** /**
* Use the arc-tangent of quadrature divided by inphase to * Use the arc-tangent of quadrature divided by inphase to
* get the phase angle (+/-) which was directly manipulated by the * get the phase angle (+/-) which was directly manipulated by the
* original message waveform during the modulation. This value now * original message waveform during the modulation. This value now
* serves as the instantaneous amplitude of the demodulated signal * serves as the instantaneous amplitude of the demodulated signal
*/ */
double denominator = 1.0d / inphase; double denominator = 1.0d / inphase;
angle = Math.atan( (double)quadrature * denominator ); angle = Math.atan((double)quadrature * denominator);
} }
/** /**
* Store the current sample to use during the next iteration * Store the current sample to use during the next iteration
*/ */
mPreviousI = currentI; mPreviousI = currentI;
mPreviousQ = currentQ; mPreviousQ = currentQ;
return (float)( angle * mGain ); return (float)(angle * mGain);
} }
public abstract void dispose(); public static double demodulate(Complex previous, Complex current)
{
double inphase = (current.inphase() * previous.inphase()) - (current.quadrature() * -previous.quadrature());
double quadrature = (current.quadrature() * previous.inphase()) + (current.inphase() * -previous.quadrature());
public void reset() double angle = 0.0f;
{
mPreviousI = 0.0f;
mPreviousQ = 0.0f;
}
public void setGain( float gain ) //Check for divide by zero
{ if(inphase != 0)
mGain = gain; {
} /**
* Use the arc-tangent of quadrature divided by inphase to
* get the phase angle (+/-) which was directly manipulated by the
* original message waveform during the modulation. This value now
* serves as the instantaneous amplitude of the demodulated signal
*/
double denominator = 1.0d / inphase;
angle = Math.atan((double)quadrature * denominator);
}
return angle;
}
public abstract void dispose();
public void reset()
{
mPreviousI = 0.0f;
mPreviousQ = 0.0f;
}
public void setGain(float gain)
{
mGain = gain;
}
} }

View File

@@ -26,8 +26,7 @@ import org.slf4j.LoggerFactory;
public class FMDemodulator_CB extends FMDemodulator implements Listener<ComplexBuffer> public class FMDemodulator_CB extends FMDemodulator implements Listener<ComplexBuffer>
{ {
private final static Logger mLog = private final static Logger mLog = LoggerFactory.getLogger( FMDemodulator_CB.class );
LoggerFactory.getLogger( FMDemodulator_CB.class );
private Listener<RealBuffer> mListener; private Listener<RealBuffer> mListener;

View File

@@ -1,112 +1,125 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.gain; package io.github.dsheirer.dsp.gain;
import io.github.dsheirer.buffer.FloatCircularBuffer; import io.github.dsheirer.buffer.FloatCircularBuffer;
import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexSampleListener; import io.github.dsheirer.sample.complex.ComplexSampleListener;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* This class is modeled using concepts detailed in the GNURadio
* implementation of feed forward AGC class located at:
* https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/
* feedforward_agc_cc_impl.cc
*
******************************************************************************/
public class ComplexFeedForwardGainControl implements ComplexSampleListener public class ComplexFeedForwardGainControl implements ComplexSampleListener
{ {
public static final float OBJECTIVE_ENVELOPE = 1.0f; public static final float OBJECTIVE_ENVELOPE = 1.0f;
public static final float MINIMUM_ENVELOPE = 0.0001f; public static final float MINIMUM_ENVELOPE = 0.0001f;
private ComplexSampleListener mListener; private ComplexSampleListener mListener;
private FloatCircularBuffer mEnvelopeHistory; private FloatCircularBuffer mEnvelopeHistory;
private float mMaxEnvelope = 0.0f; private float mMaxEnvelope = 0.0f;
private float mGain = 1.0f; private float mGain = 1.0f;
/** /**
* Dynamic gain control for incoming sample stream to amplify or attenuate * Dynamic gain control for incoming sample stream to amplify or attenuate
* all samples toward an objective unity)gain, using the maximum envelope * all samples toward an objective unity)gain, using the maximum envelope
* value detected in the stream history window. * value detected in the stream history window.
* *
* Uses the specified damping factor to limit gain swings. Damping factor * Uses the specified damping factor to limit gain swings. Damping factor
* is applied against the delta between current gain value and a recalculated * is applied against the delta between current gain value and a recalculated
* gain value to limit how quickly the gain value will increase or decrease. * gain value to limit how quickly the gain value will increase or decrease.
* *
* @param window - history size to use in detecting maximum envelope value * @param window - history size to use in detecting maximum envelope value
* @param damping - a value between 0 < damping <= 1.0; */
*/ public ComplexFeedForwardGainControl(int window)
public ComplexFeedForwardGainControl( int window ) {
{ mEnvelopeHistory = new FloatCircularBuffer(window);
mEnvelopeHistory = new FloatCircularBuffer( window ); }
}
public void dispose() public void dispose()
{ {
mListener = null; mListener = null;
} }
@Override @Override
public void receive( float inphase, float quadrature ) public void receive(float inphase, float quadrature)
{ {
float envelope = Complex.envelope( inphase, quadrature ); process(inphase, quadrature);
if( envelope > mMaxEnvelope ) /* Apply current gain value to the sample and send to the listener */
{ if(mListener != null)
mMaxEnvelope = envelope; {
mListener.receive(inphase *= mGain, quadrature *= mGain);
}
}
adjustGain(); private void process(float inphase, float quadrature)
} {
float envelope = Complex.envelope(inphase, quadrature);
/* Replace oldest envelope value with current envelope value */ if(envelope > mMaxEnvelope)
float oldestEnvelope = mEnvelopeHistory.get( envelope ); {
mMaxEnvelope = envelope;
/* If the oldest envelope value was the max envelope value, then we adjustGain();
* have to rediscover the max value from the envelope history */ }
if( mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope )
{
mMaxEnvelope = MINIMUM_ENVELOPE;
for( float value: mEnvelopeHistory.getBuffer() ) /* Replace oldest envelope value with current envelope value */
{ float oldestEnvelope = mEnvelopeHistory.get(envelope);
if( value > mMaxEnvelope )
{
mMaxEnvelope = value;
}
}
adjustGain(); /* If the oldest envelope value was the max envelope value, then we
} * have to rediscover the max value from the envelope history */
if(mMaxEnvelope == oldestEnvelope && mMaxEnvelope != envelope)
{
mMaxEnvelope = MINIMUM_ENVELOPE;
/* Apply current gain value to the sample and send to the listener */ for(float value : mEnvelopeHistory.getBuffer())
if( mListener != null ) {
{ if(value > mMaxEnvelope)
mListener.receive( inphase *= mGain, quadrature *= mGain ); {
} mMaxEnvelope = value;
} }
}
private void adjustGain() adjustGain();
{ }
mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope; }
}
public void setListener( ComplexSampleListener listener ) private void adjustGain()
{ {
mListener = listener; mGain = OBJECTIVE_ENVELOPE / mMaxEnvelope;
} }
public void setListener(ComplexSampleListener listener)
{
mListener = listener;
}
public ComplexBuffer filter(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
float[] processed = new float[samples.length];
for(int x = 0; x < samples.length; x += 2)
{
process(samples[x], samples[x + 1]);
processed[x] = samples[x] * mGain;
processed[x + 1] = samples[x + 1] * mGain;
}
return new ComplexBuffer(processed);
}
} }

View File

@@ -5,27 +5,34 @@ import io.github.dsheirer.sample.complex.Complex;
public class ComplexGain implements Listener<Complex> public class ComplexGain implements Listener<Complex>
{ {
private float mGain; private float mGain;
private Listener<Complex> mListener; private Listener<Complex> mListener;
public ComplexGain( float gain ) public ComplexGain(float gain)
{ {
mGain = gain; mGain = gain;
} }
@Override public Complex apply(Complex sample)
public void receive( Complex sample ) {
{ sample.multiply(mGain);
if( mListener != null )
{
sample.multiply( mGain );
mListener.receive( sample ); return sample;
} }
}
public void setListener( Listener<Complex> listener ) @Override
{ public void receive(Complex sample)
mListener = listener; {
} if(mListener != null)
{
apply(sample);
mListener.receive(sample);
}
}
public void setListener(Listener<Complex> listener)
{
mListener = listener;
}
} }

View File

@@ -51,8 +51,7 @@ public class Oscillator
*/ */
private void update() private void update()
{ {
float anglePerSample = float anglePerSample = (float)( 2.0d * Math.PI * mFrequency / mSampleRate );
(float)( 2.0d * Math.PI * mFrequency / mSampleRate );
mAnglePerSample = Complex.fromAngle( anglePerSample ); mAnglePerSample = Complex.fromAngle( anglePerSample );
} }

View File

@@ -0,0 +1,162 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexSampleListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DQPSKDemodulator implements ComplexSampleListener
{
private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class);
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected IPhaseLockedLoop mPLL;
protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator();
private Complex mReceivedSample = new Complex(0, 0);
private Complex mPreviousPrecedingSample = new Complex(0, 0);
private Complex mPreviousCurrentSample = new Complex(0, 0);
private Complex mPrecedingSample = new Complex(0, 0);
private Complex mCurrentSample = new Complex(0, 0);
private Complex mPrecedingSymbol = new Complex(0, 0);
protected Complex mCurrentSymbol = new Complex(0, 0);
private Listener<Dibit> mDibitListener;
/**
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
* a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
* adjust for any changes in symbol timing.
*
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
*/
public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
{
mPLL = phaseLockedLoop;
mInterpolatingSampleBuffer = interpolatingSampleBuffer;
}
/**
* Processes a (filtered) buffer containing complex samples for decoding
* @param complexBuffer with complex samples
*/
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
for(int x = 0; x < samples.length; x += 2)
{
receive(samples[x], samples[x + 1]);
}
}
/**
* Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made.
* @param inphase value for the sample
* @param quadrature value for the sample
*/
@Override
public void receive(float inphase, float quadrature)
{
//Update current sample with values
mReceivedSample.setValues(inphase, quadrature);
//Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector());
//Store the sample in the interpolating buffer
mInterpolatingSampleBuffer.receive(mReceivedSample);
//Calculate the symbol once we've stored enough samples
if(mInterpolatingSampleBuffer.hasSymbol())
{
calculateSymbol();
}
}
/**
* Calculates a symbol from the interpolating buffer
*/
protected void calculateSymbol()
{
//Get preceding sample and an interpolated current sample from the interpolating buffer
mPrecedingSample = mInterpolatingSampleBuffer.getPrecedingSample();
mCurrentSample = mInterpolatingSampleBuffer.getCurrentSample();
//Calculate preceding, current and following symbols as the delta rotation compared to the previous samples
mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(),
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(),
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
//Set gain to unity before we calculate the error value
mPrecedingSymbol.normalize();
mCurrentSymbol.normalize();
mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError());
//Store current samples/symbols to use for the next period
mPreviousPrecedingSample.setValues(mPrecedingSample);
mPreviousCurrentSample.setValues(mCurrentSample);
//Calculate the phase error of the current symbol relative to the expected constellation and provide
//feedback to the PLL
mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
if(mDibitListener != null)
{
mDibitListener.receive(mSymbolEvaluator.getSymbolDecision());
}
}
/**
* Registers the listener to receive dibit symbol decisions from this demodulator
*/
public void setDibitListener(Listener<Dibit> listener)
{
mDibitListener = listener;
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
public static float clip(float value, float maximum)
{
if(value > maximum)
{
return maximum;
}
else if(value < -maximum)
{
return -maximum;
}
return value;
}
}

View File

@@ -0,0 +1,131 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
{
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mSymbolListener;
private Listener<Double> mPLLErrorListener;
private Listener<Double> mPLLFrequencyListener;
private double mSampleRate;
/**
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
* a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
* adjust for any changes in symbol timing.
*
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param interpolatingSampleBuffer
* @param sampleRate of the incoming complex sample stream
*/
public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
double sampleRate)
{
super(phaseLockedLoop, interpolatingSampleBuffer);
mSampleRate = sampleRate;
}
/**
* Overrides the parent class symbol calculation to capture eye diagram data
*/
protected void calculateSymbol()
{
//Eye diagram listener
if(mSymbolDecisionDataListener != null)
{
mSymbolDecisionDataListener.receive(
((InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer).getSymbolDecisionData());
}
super.calculateSymbol();
if(mSamplesPerSymbolListener != null)
{
mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint());
}
//Send to an external constellation symbol listener when registered
if(mSymbolListener != null)
{
mSymbolListener.receive(mCurrentSymbol);
}
if(mPLLErrorListener != null)
{
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
}
if(mPLLFrequencyListener != null)
{
double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency();
loopFrequency *= mSampleRate / (2.0 * Math.PI);
mPLLFrequencyListener.receive(loopFrequency);
}
}
/**
* Registers a listener to receive symbol decision data to produce an eye diagram.
*/
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
}
/**
* Registers the listener to receive decoded QPSK symbols
*/
public void setSymbolListener(Listener<Complex> listener)
{
mSymbolListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLErrorListener(Listener<Double> listener)
{
mPLLErrorListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
}
}

View File

@@ -0,0 +1,137 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
{
private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0);
private Complex mSymbolError = new Complex(0, 0);
private float mPhaseError = 0.0f;
private float mTimingError = 0.0f;
private float mTimingErrorPolarity = 1.0f;
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
/**
* Decision-directed symbol phase and timing error detector and symbol decision slicer.
*
* Symbol decision is based on the closest reference quadrant for the sampled symbol.
*
* Phase and timing error are calculated as the angular distance of the sampled symbol from the reference
* symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation
* relative to the immediately preceding symbol.
*/
public DQPSKSymbolEvaluator()
{
}
/**
* Sets the preceding and current symbols to be evaluated for phase and timing errors and to determine the
* transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the
* phase and timing errors and the symbol decision via their respective accessor methods.
*
* Phase and timing error values are calculated by first determining the symbol and then calculating the phase
* and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign
* relative to the angular vector rotation so that the error value indicates the correct error direction.
*
* @param preceding differential-decoded sample/symbol used to detect rotation - does NOT need to be interpolated
* since it's only a reference
* @param current symbol to be evaluated - interpolated
*/
@Override
public void setSymbol(Complex preceding, Complex current)
{
mSymbolError.setValues(current);
if(mSymbolError.quadrature() > 0.0f)
{
if(mSymbolError.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D00_PLUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_45);
}
else
{
mSymbolDecision = Dibit.D01_PLUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_135);
}
}
else
{
if(mSymbolError.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D10_MINUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_45);
}
else
{
mSymbolDecision = Dibit.D11_MINUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_135);
}
}
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
//arctan of the error angle relative to 0 radians and this provides our error value
mPhaseError = -mSymbolError.quadrature();
//Timing error is the same as phase error with the sign corrected according to the vector's angular rotation
mTimingError = mSymbolError.quadrature() * mTimingErrorPolarity;
}
/**
* Phase error of the symbol relative to the nearest reference symbol.
*
* @return phase error in radians of distance from the reference symbol.
*/
@Override
public float getPhaseError()
{
return mPhaseError;
}
/**
* Timing error of the symbol relative to the nearest reference symbol.
*
* @return timing error in radians of angular distance from the reference symbol recognizing that the symbol
* originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates
* if the symbol was sampled early (-) or late (+) relative to the reference symbol.
*/
@Override
public float getTimingError()
{
return mTimingError;
}
/**
* Reference symbol that is closest to the transmitted/sampled symbol.
*/
@Override
public Dibit getSymbolDecision()
{
return mSymbolDecision;
}
}

View File

@@ -0,0 +1,82 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
public class GardnerDetector
{
private float mErrorInphase;
private float mErrorQuadrature;
private float mError;
/**
* Gardner detector for recovering symbol timing error.
*/
public GardnerDetector()
{
}
/**
* Calculates the symbol timing error by evaluating the previous, middle and current symbol instants.
*
* @param previous symbol
* @param middle symbol (ie midway between the previous and current)
* @param current symbol
* @return symbol timing error
*/
public float getError(Complex previous, Complex middle, Complex current)
{
mErrorInphase = (Math.abs(previous.inphase()) - Math.abs(current.inphase())) * Math.abs(middle.inphase());
mErrorQuadrature = (Math.abs(previous.quadrature()) - Math.abs(current.quadrature())) * Math.abs(middle.quadrature());
mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f);
return mError;
}
/**
* Constrains timing error to +/- the maximum value and corrects any floating point invalid numbers
*/
public static float normalize(float error, float maximum)
{
if(Float.isNaN(error))
{
return 0.0f;
}
else
{
return clip(error, maximum);
}
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
public static float clip(float value, float maximum)
{
if(value > maximum)
{
return maximum;
}
else if(value < -maximum)
{
return -maximum;
}
return value;
}
}

View File

@@ -0,0 +1,50 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public interface IDQPSKSymbolEvaluator
{
/**
* Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle.
*
* @param preceding symbol used to detect (counter-)clock-wise rotation
* @param symbol currently being sampled
*/
void setSymbol(Complex preceding, Complex symbol);
/**
* Phase error of the symbol relative to the closest reference symbol
* @return angular phase error in radians
*/
float getPhaseError();
/**
* Timing error of the symbol relative to the closest reference symbol
* @return angular timing error in radians where a positive value indicates early sampling and sample timing needs
* to be increased and a negative value indicates late sampling where sample timing needs to be decreased.
*/
float getTimingError();
/**
* Dibit decision indicating the reference symbol location closest to the symbol.
* @return symbol decision
*/
Dibit getSymbolDecision();
}

View File

@@ -0,0 +1,29 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public interface IQPSKSymbolDecoder
{
/**
* Decodes the symbol and converts it into a Dibit
* @param symbol to decode
* @return decoded dibit
*/
Dibit decode(Complex symbol);
}

View File

@@ -0,0 +1,28 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
public interface ISymbolPhaseErrorCalculator
{
/**
* Calculates the phase error of the symbol relative to an alignment for the constellation
* @param symbol to calculate error
* @return error value
*/
float getPhaseError(Complex symbol);
}

View File

@@ -0,0 +1,167 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
import io.github.dsheirer.sample.complex.Complex;
public class InterpolatingSampleBuffer
{
private static final float MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL = 0.02f; // +/- 2% deviation
private Complex mPrecedingSample = new Complex(0,0);
private Complex mCurrentSample = new Complex(0,0);
private float[] mDelayLineInphase;
private float[] mDelayLineQuadrature;
private int mDelayLinePointer = 0;
private int mTwiceSamplesPerSymbol;
private float mSamplingPoint;
private float mSampleCounterGain = 1.0f;
private float mDetectedSamplesPerSymbol;
private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
private float mMaximumSamplesPerSymbol;
private float mMinimumSamplesPerSymbol;
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
/**
* Buffer to store complex sample data and produce interpolated samples.
* @param samplesPerSymbol
*/
public InterpolatingSampleBuffer(float samplesPerSymbol)
{
mSamplingPoint = samplesPerSymbol;
mDetectedSamplesPerSymbol = samplesPerSymbol;
mMaximumSamplesPerSymbol = samplesPerSymbol * (1.0f + MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
}
/**
* Stores the sample in the buffer and updates pointers.
* @param sample
*/
public void receive(Complex sample)
{
mSamplingPoint--;
//Fill up the delay line to use with the interpolator
mDelayLineInphase[mDelayLinePointer] = sample.inphase();
mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.inphase();
mDelayLineQuadrature[mDelayLinePointer] = sample.quadrature();
mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.quadrature();
//Increment pointer and keep pointer in bounds
mDelayLinePointer++;
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
}
/**
* Updates the internal sample counter with additional samples to add to the symbol sample counter.
* @param samplesToAdd to the sample counter
*/
public void increaseSampleCounter(float samplesToAdd)
{
mSamplingPoint += samplesToAdd;
}
/**
* Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
* symbol.
*
* @param symbolTimingError from a symbol timing error detector
*/
public void resetAndAdjust(float symbolTimingError)
{
//Adjust detected samples per symbol based on timing error
mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + (symbolTimingError * mDetectedSamplesPerSymbolGain);
//Constrain detected samples per symbol to min/max values
if(mDetectedSamplesPerSymbol > mMaximumSamplesPerSymbol)
{
mDetectedSamplesPerSymbol = mMaximumSamplesPerSymbol;
}
if(mDetectedSamplesPerSymbol < mMinimumSamplesPerSymbol)
{
mDetectedSamplesPerSymbol = mMinimumSamplesPerSymbol;
}
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
increaseSampleCounter((mDetectedSamplesPerSymbol + (symbolTimingError * mSampleCounterGain)));
}
/**
* Current value of the detected samples per symbol
*/
public float getSamplingPoint()
{
return mSamplingPoint;
}
/**
* Indicates if this buffer has accumulated a enough samples to represent a full symbol
*/
public boolean hasSymbol()
{
return mSamplingPoint < 1.0f;
}
/**
* Un-interpolated sample that precedes the current interpolated sampling point. The current sampling point falls
* somewhere between sample index 3 and index 4, therefore the preceding sample is located at index 3.
*/
public Complex getPrecedingSample()
{
mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]);
return mPrecedingSample;
}
/**
* Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a
* complete symbol with the hasSymbol() method.
*/
public Complex getCurrentSample()
{
/* Calculate interpolated current sample */
mCurrentSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint));
return mCurrentSample;
}
/**
* Returns the interpolated inphase value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return inphase value of the interpolated sample
*/
public float getInphase(float interpolation)
{
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation);
}
/**
* Returns the interpolated quadrature value for the specified offset
* @param interpolation into the buffer to calculate the interpolated sample
* @return quadrature value of the interpolated sample
*/
public float getQuadrature(float interpolation)
{
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation);
}
}

View File

@@ -0,0 +1,48 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBuffer
{
private SymbolDecisionData mSymbolDecisionData;
public InterpolatingSampleBufferInstrumented(float samplesPerSymbol)
{
super(samplesPerSymbol);
mSymbolDecisionData = new SymbolDecisionData((int)samplesPerSymbol);
}
public void receive(Complex sample)
{
super.receive(sample);
mSymbolDecisionData.receive(sample);
}
/**
* Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can
* be used to support an external eye-diagram chart.
* @return symbol decision data.
*/
public SymbolDecisionData getSymbolDecisionData()
{
mSymbolDecisionData.setSamplingPoint(getSamplingPoint());
return mSymbolDecisionData;
}
}

View File

@@ -0,0 +1,28 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public class LSMConstellationDecoder implements IQPSKSymbolDecoder
{
@Override
public Dibit decode(Complex symbol)
{
return null;
}
}

View File

@@ -2,7 +2,7 @@ package io.github.dsheirer.dsp.psk;
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014, 2015 Dennis Sheirer * Copyright (C) 2014 - 2018 Dennis Sheirer
* *
* Gardner detector, Costas loop and Interpolator derived from: * Gardner detector, Costas loop and Interpolator derived from:
* *
@@ -32,6 +32,7 @@ package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.buffer.FloatAveragingBuffer; import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator; import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.instrument.Instrumentable; import io.github.dsheirer.instrument.Instrumentable;
import io.github.dsheirer.instrument.tap.Tap; import io.github.dsheirer.instrument.tap.Tap;
@@ -57,125 +58,130 @@ import java.util.List;
*/ */
public class LSMDemodulator implements Instrumentable, ComplexSampleListener public class LSMDemodulator implements Instrumentable, ComplexSampleListener
{ {
private List<TapGroup> mAvailableTaps; private List<TapGroup> mAvailableTaps;
/* 45 degree rotation angle */ /* 45 degree rotation angle */
public static final float THETA = (float)( Math.PI / 4.0d ); public static final float THETA = (float)(Math.PI / 4.0d);
/* 45 degree point */ /* 45 degree point */
public static final Complex POINT_45_DEGREES = public static final Complex POINT_45_DEGREES = new Complex((float)Math.sin(THETA), (float)Math.cos(THETA));
new Complex( (float)Math.sin( THETA ), (float)Math.cos( THETA ) );
private Listener<Complex> mSymbolListener; private Listener<Complex> mSymbolListener;
private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming(); private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming();
private CostasLoop mCostasLoop = new CostasLoop(); private CostasLoop mCostasLoop = new CostasLoop();
private QPSKSymbolDecoder mQPSKSymbolDecoder = new QPSKSymbolDecoder();
private IFrequencyChangeListener mFrequencyChangeListener; private IFrequencyChangeListener mFrequencyChangeListener;
private EyeDiagramDataTap mEyeDiagramDataTap; private EyeDiagramDataTap mEyeDiagramDataTap;
public LSMDemodulator() public LSMDemodulator()
{ {
} }
public void dispose() public CostasLoop getCostasLoop()
{ {
mSymbolListener = null; return mCostasLoop;
mFrequencyChangeListener = null; }
}
public void setSymbolListener( Listener<Complex> listener ) public void dispose()
{ {
mSymbolListener = listener; mSymbolListener = null;
} mFrequencyChangeListener = null;
}
public void removeSymbolListener() public void setSymbolListener(Listener<Complex> listener)
{ {
mSymbolListener = null; mSymbolListener = listener;
} }
@Override public void removeSymbolListener()
public void receive( float inphase, float quadrature ) {
{ mSymbolListener = null;
mGardnerDetector.receive( inphase, quadrature ); }
}
/** @Override
* Applies a phase correction value to the costas loop to correct when a public void receive(float inphase, float quadrature)
* phase lock error is detected in the binary output stream. {
* mGardnerDetector.receive(inphase, quadrature);
* @param correction - value in radians }
*/
public void correctPhaseError( double correction )
{
mCostasLoop.correctPhaseError( correction );
}
/** /**
* Constrains value to the range of ( -maximum <> maximum ) * Applies a phase correction value to the costas loop to correct when a
*/ * phase lock error is detected in the binary output stream.
public static float clip( float value, float maximum ) *
{ * @param correction - value in radians
if( value > maximum ) */
{ public void correctPhaseError(double correction)
return maximum; {
} mCostasLoop.correctPhaseError(correction);
else if( value < -maximum ) }
{
return -maximum;
}
return value; /**
} * Constrains value to the range of ( -maximum <> maximum )
*/
public static float clip(float value, float maximum)
{
if(value > maximum)
{
return maximum;
}
else if(value < -maximum)
{
return -maximum;
}
/** return value;
* Constrains timing error to +/- the maximum value and corrects any }
* floating point invalid numbers
*/
private float normalize( float error, float maximum )
{
if( Float.isNaN( error ) )
{
return 0.0f;
}
else
{
return clip( error, maximum );
}
}
public void addListener( IFrequencyChangeListener listener ) /**
{ * Constrains timing error to +/- the maximum value and corrects any
mFrequencyChangeListener = listener; * floating point invalid numbers
} */
private float normalize(float error, float maximum)
{
if(Float.isNaN(error))
{
return 0.0f;
}
else
{
return clip(error, maximum);
}
}
public void removeListener( IFrequencyChangeListener listener ) public void addListener(IFrequencyChangeListener listener)
{ {
mFrequencyChangeListener = null; mFrequencyChangeListener = listener;
} }
/** public void removeListener(IFrequencyChangeListener listener)
* Processes the current loop frequency of the costas loop and broadcasts {
* tuner frequency offset adjustments 10 times a second as needed to keep mFrequencyChangeListener = null;
* the frequency offset below half of the maximum frequency }
*/
public class FrequencyControl
{
/* Set the trigger threshold at half of the maximum costas loop control
* frequency */
private static final float CORRECTION_THRESHOLD_FREQUENCY =
( 2.0f * (float)Math.PI * 600.0f ) / 48000.0f;
private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 ); /**
* Processes the current loop frequency of the costas loop and broadcasts
* tuner frequency offset adjustments 10 times a second as needed to keep
* the frequency offset below half of the maximum frequency
*/
public class FrequencyControl
{
/* Set the trigger threshold at half of the maximum costas loop control
* frequency */
private static final float CORRECTION_THRESHOLD_FREQUENCY =
(2.0f * (float)Math.PI * 600.0f) / 48000.0f;
private long mFrequencyError = 0; private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer(40);
private int mCounter = 0; private long mFrequencyError = 0;
public void receive( float frequency ) private int mCounter = 0;
{
public void receive(float frequency)
{
// mCounter++; // mCounter++;
// //
// if( mCounter >= 480 ) // if( mCounter >= 480 )
@@ -212,377 +218,402 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
// { // {
// mBuffer.get( frequency ); // mBuffer.get( frequency );
// } // }
} }
} }
/** /**
* Gardner symbol timing detector * Gardner symbol timing detector
*/ */
public class GardnerSymbolTiming implements ComplexSampleListener public class GardnerSymbolTiming implements ComplexSampleListener
{ {
public static final int HALF_SAMPLES_PER_SYMBOL = 5; public static final int HALF_SAMPLES_PER_SYMBOL = 5;
public static final int SAMPLES_PER_SYMBOL = 10; public static final int SAMPLES_PER_SYMBOL = 10;
public static final int TWICE_SAMPLES_PER_SYMBOL = 20; public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
private float[] mDelayLineInphase = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ]; private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
private float[] mDelayLineQuadrature = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ]; private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
private int mDelayLinePointer = 0; private int mDelayLinePointer = 0;
/* Sampling point */ /* Sampling point */
private float mMu = 10.0f; private float mMu = 10.0f;
private float mGainMu = 0.05f; private float mGainMu = 0.05f;
/* Samples per symbol */ /* Samples per symbol */
private float mOmega = 10.0f; private float mOmega = 10.0f;
private float mGainOmega = 0.1f * mGainMu * mGainMu; private float mGainOmega = 0.1f * mGainMu * mGainMu;
private float mOmegaRel = 0.005f; private float mOmegaRel = 0.005f;
private float mOmegaMid = 10.0f; private float mOmegaMid = 10.0f;
private RealInterpolator mInterpolator = new RealInterpolator( 1.0f ); private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
private Complex mPreviousSample = new Complex( 0, 0 ); private Complex mPreviousSample = new Complex(0, 0);
private Complex mPreviousMiddleSample = new Complex( 0, 0 ); private Complex mPreviousMiddleSample = new Complex(0, 0);
private Complex mPreviousSymbol = new Complex( 0, 0 ); private Complex mPreviousSymbol = new Complex(0, 0);
/** /**
* Provides symbol sampling timing control * Provides symbol sampling timing control
*/ */
public GardnerSymbolTiming() public GardnerSymbolTiming()
{ {
} }
@Override @Override
public void receive( float inphase, float quadrature ) public void receive(float inphase, float quadrature)
{ {
/* Count down samples per symbol until we calculate the symbol */ /* Count down samples per symbol until we calculate the symbol */
mMu--; mMu--;
mCostasLoop.increment(); mCostasLoop.increment();
/* Mix incoming sample with costas loop to remove any rotation /* Mix incoming sample with costas loop to remove any rotation
* that is present from a mis-tuned carrier frequency */ * that is present from a mis-tuned carrier frequency */
Complex costas = mCostasLoop.getCurrentVector(); Complex costas = mCostasLoop.getCurrentVector();
float derotatedInphase = Complex.multiplyInphase( inphase, quadrature, costas.inphase(), costas.quadrature() ); float derotatedInphase = Complex.multiplyInphase(inphase, quadrature, costas.inphase(), costas.quadrature());
float derotatedQuadrature = Complex.multiplyQuadrature( inphase, quadrature, costas.inphase(), costas.quadrature() ); float derotatedQuadrature = Complex.multiplyQuadrature(inphase, quadrature, costas.inphase(), costas.quadrature());
/* Fill up the delay line to use with the interpolator */ /* Fill up the delay line to use with the interpolator */
mDelayLineInphase[ mDelayLinePointer ] = derotatedInphase; mDelayLineInphase[mDelayLinePointer] = derotatedInphase;
mDelayLineInphase[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedInphase; mDelayLineInphase[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedInphase;
mDelayLineQuadrature[ mDelayLinePointer ] = derotatedQuadrature; mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature;
mDelayLineQuadrature[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedQuadrature; mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature;
/* Increment pointer and keep pointer in bounds */ /* Increment pointer and keep pointer in bounds */
mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL; mDelayLinePointer = (mDelayLinePointer + 1) % TWICE_SAMPLES_PER_SYMBOL;
/* Calculate the symbol once we've stored enough samples */ /* Calculate the symbol once we've stored enough samples */
if( mMu <= 1.0f ) if(mMu <= 1.0f)
{ {
float half_omega = mOmega / 2.0f; float half_omega = mOmega / 2.0f;
int half_sps = (int)Math.floor( half_omega ); int half_sps = (int)Math.floor(half_omega);
float half_mu = mMu + half_omega - (float)half_sps; float half_mu = mMu + half_omega - (float)half_sps;
if( half_mu > 1.0 ) if(half_mu > 1.0)
{ {
half_mu -= 1.0; half_mu -= 1.0;
half_sps += 1; half_sps += 1;
} }
/* Calculate interpolated middle sample and current sample */ /* Calculate interpolated middle sample and current sample */
float middleSampleInphase = mInterpolator.filter( float middleSampleInphase = mInterpolator.filter(
mDelayLineInphase, mDelayLinePointer, mMu ); mDelayLineInphase, mDelayLinePointer, mMu);
float middleSampleQuadrature = mInterpolator.filter( float middleSampleQuadrature = mInterpolator.filter(
mDelayLineQuadrature, mDelayLinePointer, mMu ); mDelayLineQuadrature, mDelayLinePointer, mMu);
int index = mDelayLinePointer + half_sps; int index = mDelayLinePointer + half_sps;
float currentSampleInphase = mInterpolator.filter( float currentSampleInphase = mInterpolator.filter(
mDelayLineInphase, index, half_mu ); mDelayLineInphase, index, half_mu);
float currentSampleQuadrature = mInterpolator.filter( float currentSampleQuadrature = mInterpolator.filter(
mDelayLineQuadrature, index, half_mu ); mDelayLineQuadrature, index, half_mu);
/* Multiply current sample and conjugate (negated quadrature) of /* Multiply current sample and conjugate (negated quadrature) of
* previous sample to get symbols to use for gardner error feedback */ * previous sample to get symbols to use for gardner error feedback */
Complex middleSymbol = Complex.multiply( middleSampleInphase, Complex middleSymbol = Complex.multiply(middleSampleInphase,
middleSampleQuadrature, mPreviousMiddleSample.conjugate() ); middleSampleQuadrature, mPreviousMiddleSample.conjugate());
Complex currentSymbol = Complex.multiply( currentSampleInphase, Complex currentSymbol = Complex.multiply(currentSampleInphase,
currentSampleQuadrature, mPreviousSample.conjugate() ); currentSampleQuadrature, mPreviousSample.conjugate());
/* Set gain to unity */ /* Set gain to unity */
middleSymbol.normalize(); middleSymbol.normalize();
currentSymbol.normalize(); currentSymbol.normalize();
/* Gardner timing error calculations */ /* Gardner timing error calculations */
float errorInphase = ( mPreviousSymbol.inphase() - float errorInphase = (mPreviousSymbol.inphase() -
currentSymbol.inphase() ) * middleSymbol.inphase(); currentSymbol.inphase()) * middleSymbol.inphase();
float errorQuadrature = ( mPreviousSymbol.quadrature() - float errorQuadrature = (mPreviousSymbol.quadrature() -
currentSymbol.quadrature() ) * middleSymbol.quadrature(); currentSymbol.quadrature()) * middleSymbol.quadrature();
float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f ); float gardnerError = normalize(errorInphase + errorQuadrature, 1.0f);
if( mEyeDiagramDataTap != null ) if(mEyeDiagramDataTap != null)
{ {
mEyeDiagramDataTap.receive( mEyeDiagramDataTap.receive(
new EyeDiagramData( new EyeDiagramData(
Arrays.copyOfRange( mDelayLineInphase, 0, 20 ), Arrays.copyOfRange(mDelayLineInphase, 0, 20),
Arrays.copyOfRange( mDelayLineQuadrature, 0, 20 ), Arrays.copyOfRange(mDelayLineQuadrature, 0, 20),
mMu, (float)half_sps + half_mu, gardnerError ) ); mMu, (float)half_sps + half_mu, gardnerError));
} }
/* mOmega is samples per symbol and is constrained to floating /* mOmega is samples per symbol and is constrained to floating
* between +/- .005 of the nominal 10.0 samples per symbol */ * between +/- .005 of the nominal 10.0 samples per symbol */
mOmega = mOmega + mGainOmega * gardnerError; mOmega = mOmega + mGainOmega * gardnerError;
mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel ); mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
/* Adjust sample timing based on error of current sample */ /* Adjust sample timing based on error of current sample */
mMu += mOmega + ( mGainMu * gardnerError ); mMu += mOmega + (mGainMu * gardnerError);
/* Store current samples/symbols to use for the next period */ /* Store current samples/symbols to use for the next period */
mPreviousSample.setValues( currentSampleInphase, currentSampleQuadrature ); mPreviousSample.setValues(currentSampleInphase, currentSampleQuadrature);
mPreviousMiddleSample.setValues( middleSampleInphase, middleSampleQuadrature ); mPreviousMiddleSample.setValues(middleSampleInphase, middleSampleQuadrature);
mPreviousSymbol = currentSymbol; mPreviousSymbol = currentSymbol;
/* Update costas loop using phase error present in current /* Update costas loop using phase error present in current
* symbol. The symbol is rotated from star orientation to polar * symbol. The symbol is rotated from star orientation to polar
* orientation to simplify error calculation */ * orientation to simplify error calculation */
mCostasLoop.receive( currentSymbol ); mCostasLoop.receive(currentSymbol);
// mCostasLoop.receive( // mCostasLoop.receive(
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) ); // ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
/* Dispatch the differentiated symbol to the registered listener */ /* Dispatch the differentiated symbol to the registered listener */
if( mSymbolListener != null ) if(mSymbolListener != null)
{ {
mSymbolListener.receive( currentSymbol ); mSymbolListener.receive(currentSymbol);
} }
} }
} }
} }
/** /**
* Costas Loop - phase locked loop synchronized to the frequency offset of * Costas Loop - phase locked loop synchronized to the frequency offset of
* the incoming signal to enable the signal to be mixed down to zero offset * the incoming signal to enable the signal to be mixed down to zero offset
* for proper synchronization. The mFrequency value indicates the detected * for proper synchronization. The mFrequency value indicates the detected
* frequency offset. We attempt to keep that value close to zero by issuing * frequency offset. We attempt to keep that value close to zero by issuing
* frequency adjustments to the tuner channel source. * frequency adjustments to the tuner channel source.
* *
* Most of the costas loop code was ported from gnuradio/control_loop and * Most of the costas loop code was ported from gnuradio/control_loop and
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's * gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
* OP25/cqpsk.py * OP25/cqpsk.py
*/ */
public class CostasLoop implements Listener<Complex> public class CostasLoop implements Listener<Complex>, IPhaseLockedLoop
{ {
public static final double TWO_PI = 2.0 * Math.PI; public static final double TWO_PI = 2.0 * Math.PI;
private static final float MAXIMUM_FREQUENCY = private static final float MAXIMUM_FREQUENCY =
( 2400.0f * (float)TWO_PI ) / 48000.0f; (2400.0f * (float)TWO_PI) / 48000.0f;
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */ /* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f; private float mDamping = (float)Math.sqrt(2.0) / 2.0f;
/* Use denominator between 100 (most) and 400 (least) to adjust costas /* Use denominator between 100 (most) and 400 (least) to adjust costas
* loop control level */ * loop control level */
private float mLoopBandwidth = (float)( TWO_PI / 400.0d ); private float mLoopBandwidth = (float)(TWO_PI / 400.0d);
private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) / private float mAlphaGain = (4.0f * mDamping * mLoopBandwidth) /
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) + (1.0f + (2.0f * mDamping * mLoopBandwidth) +
( mLoopBandwidth * mLoopBandwidth ) ); (mLoopBandwidth * mLoopBandwidth));
private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) / private float mBetaGain = (4.0f * mLoopBandwidth * mLoopBandwidth) /
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) + (1.0f + (2.0f * mDamping * mLoopBandwidth) +
( mLoopBandwidth * mLoopBandwidth ) ); (mLoopBandwidth * mLoopBandwidth));
private float mLoopPhase = 0.0f; private float mLoopPhase = 0.0f;
private float mLoopFrequency = 0.0f; private float mLoopFrequency = 0.0f;
private FrequencyControl mFrequencyControl = new FrequencyControl(); private FrequencyControl mFrequencyControl = new FrequencyControl();
public CostasLoop() public CostasLoop()
{ {
} }
/** /**
* Applies a phase correction value to the current phasor. Use this * Applies a phase correction value to the current phasor. Use this
* method to apply correction when a +/- 90, or 180 degree phase error * method to apply correction when a +/- 90, or 180 degree phase error
* is detected in the binary output stream. * is detected in the binary output stream.
* *
* If the supplied correction value places the loop frequency outside * If the supplied correction value places the loop frequency outside
* of the max frequency, then the frequency will be corrected 360 * of the max frequency, then the frequency will be corrected 360
* degrees in the opposite direction to maintain within the max * degrees in the opposite direction to maintain within the max
* frequency bounds. * frequency bounds.
* *
* @param correction - correction value in radians * @param correction - correction value in radians
*/ */
public void correctPhaseError( double correction ) public void correctPhaseError(double correction)
{ {
mLoopFrequency += correction; mLoopFrequency += correction;
if( mLoopFrequency > MAXIMUM_FREQUENCY ) if(mLoopFrequency > MAXIMUM_FREQUENCY)
{ {
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY; mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
} }
if( mLoopFrequency < -MAXIMUM_FREQUENCY ) if(mLoopFrequency < -MAXIMUM_FREQUENCY)
{ {
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY; mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
} }
} }
/** /**
* Increments the phase of the loop * Increments the phase of the loop
*/ */
public void increment() public void increment()
{ {
mLoopPhase += mLoopFrequency; mLoopPhase += mLoopFrequency;
/* Keep the loop phase in bounds */ /* Keep the loop phase in bounds */
unwrapPhase(); unwrapPhase();
} }
private void unwrapPhase() @Override
{ public void reset()
while( mLoopPhase > TWO_PI ) {
{ mLoopPhase = 0.0f;
mLoopPhase -= TWO_PI; mLoopFrequency = 0.0f;
} }
while( mLoopPhase < -TWO_PI ) @Override
{ public void adjust(double error)
mLoopPhase += TWO_PI; {
} adjust(error);
} }
/** @Override
* Current vector of the loop rotated by THETA degrees. public void correctInversion(double correction)
*/ {
public Complex getCurrentVector() correctPhaseError(correction);
{ }
private void unwrapPhase()
{
while(mLoopPhase > TWO_PI)
{
mLoopPhase -= TWO_PI;
}
while(mLoopPhase < -TWO_PI)
{
mLoopPhase += TWO_PI;
}
}
/**
* Current vector of the loop rotated by THETA degrees.
*/
public Complex getCurrentVector()
{
// return ComplexSample.fromAngle( mLoopPhase + THETA ); // return ComplexSample.fromAngle( mLoopPhase + THETA );
return Complex.fromAngle( mLoopPhase ); return Complex.fromAngle(mLoopPhase);
} }
@Override @Override
public void receive( Complex complex ) public Complex incrementAndGetCurrentVector()
{ {
/* Calculate phase error */ return null;
float phaseError = getPhaseError( complex ); }
Dibit d = QPSKPolarSlicer.decide( complex ); @Override
public void receive(Complex complex)
{
/* Calculate phase error */
float phaseError = getPhaseError(complex);
Dibit d = mQPSKSymbolDecoder.decode(complex);
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError ); // mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
/* Adjust for phase error */ /* Adjust for phase error */
adjust( phaseError ); adjust(phaseError);
} }
/** /**
* Updates the costas loop frequency and phase to adjust for the phase * Updates the costas loop frequency and phase to adjust for the phase
* error value * error value
* *
* @param phase_error - (-)= late and (+)= early * @param phase_error - (-)= late and (+)= early
*/ */
private void adjust( float phase_error ) private void adjust(float phase_error)
{ {
mLoopFrequency += mBetaGain * phase_error; mLoopFrequency += mBetaGain * phase_error;
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error; mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
/* Maintain phase between +/- 2 * PI */ /* Maintain phase between +/- 2 * PI */
unwrapPhase(); unwrapPhase();
/* Limit frequency to +/- maximum loop frequency */ /* Limit frequency to +/- maximum loop frequency */
limitFrequency(); limitFrequency();
} }
/** /**
* Constrains the frequency within the bounds of +/- loop frequency * Constrains the frequency within the bounds of +/- loop frequency
*/ */
private void limitFrequency() private void limitFrequency()
{ {
/* Check for and issue tuner offset correction */ /* Check for and issue tuner offset correction */
mFrequencyControl.receive( mLoopFrequency ); mFrequencyControl.receive(mLoopFrequency);
if( mLoopFrequency > MAXIMUM_FREQUENCY ) if(mLoopFrequency > MAXIMUM_FREQUENCY)
{ {
mLoopFrequency = MAXIMUM_FREQUENCY; mLoopFrequency = MAXIMUM_FREQUENCY;
} }
if( mLoopFrequency < -MAXIMUM_FREQUENCY ) if(mLoopFrequency < -MAXIMUM_FREQUENCY)
{ {
mLoopFrequency = -MAXIMUM_FREQUENCY; mLoopFrequency = -MAXIMUM_FREQUENCY;
} }
} }
/** /**
* Detects rotational error present when the costas loop is not aligned * Detects rotational error present when the costas loop is not aligned
* with the carrier frequency. Provides error feedback to adjust * with the carrier frequency. Provides error feedback to adjust
* mixer frequency. * mixer frequency.
*/ */
public float getPhaseError( Complex complex ) public float getPhaseError(Complex complex)
{ {
float phase_error = 0; float phase_error = 0;
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) ) if(Math.abs(complex.inphase()) > Math.abs(complex.quadrature()))
{ {
if( complex.inphase() > 0 ) if(complex.inphase() > 0)
{ {
phase_error = -complex.quadrature(); phase_error = -complex.quadrature();
} }
else else
{ {
phase_error = complex.quadrature(); phase_error = complex.quadrature();
} }
} }
else else
{ {
if( complex.quadrature() > 0 ) if(complex.quadrature() > 0)
{ {
phase_error = complex.inphase(); phase_error = complex.inphase();
} }
else else
{ {
phase_error = -complex.inphase(); phase_error = -complex.inphase();
} }
} }
return phase_error; return phase_error;
} }
} }
@Override @Override
public List<TapGroup> getTapGroups() public List<TapGroup> getTapGroups()
{ {
if( mAvailableTaps == null ) if(mAvailableTaps == null)
{ {
mAvailableTaps = new ArrayList<>(); mAvailableTaps = new ArrayList<>();
TapGroup group = new TapGroup( "LSM Demodulator" ); TapGroup group = new TapGroup("LSM Demodulator");
group.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) ); group.add(new EyeDiagramDataTap("Eye Diagram", 0, 4800));
mAvailableTaps.add( group ); mAvailableTaps.add(group);
} }
return mAvailableTaps; return mAvailableTaps;
} }
@Override @Override
public void registerTap( Tap tap ) public void registerTap(Tap tap)
{ {
if( tap instanceof EyeDiagramDataTap ) if(tap instanceof EyeDiagramDataTap)
{ {
mEyeDiagramDataTap = (EyeDiagramDataTap)tap; mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
} }
} }
@Override @Override
public void unregisterTap( Tap tap ) public void unregisterTap(Tap tap)
{ {
if( tap instanceof EyeDiagramDataTap ) if(tap instanceof EyeDiagramDataTap)
{ {
mEyeDiagramDataTap = null; mEyeDiagramDataTap = null;
} }
} }
} }

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk; package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.dsp.symbol.Dibit;
@@ -5,54 +20,54 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.Complex;
public class QPSKStarSlicer implements Listener<Complex> public class QPSKStarSlicer implements IQPSKSymbolDecoder, Listener<Complex>
{ {
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>(); private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
/** /**
* Slices a ComplexSample representing a phase shifted symbol according to * Slices a Complex sample representing a phase shifted symbol according to the following constellation pattern:
* the following constellation pattern: *
* * 00 +3 | 01 +1
* 00 | 01 * ------|------
* ---|--- * 10 -1 | 11 -1
* 10 | 11 */
*/ public QPSKStarSlicer()
public QPSKStarSlicer() {
{ }
}
public void dispose() public void dispose()
{ {
mBroadcaster.dispose(); mBroadcaster.dispose();
mBroadcaster = null; mBroadcaster = null;
} }
public void addListener( Listener<Dibit> listener ) public void addListener(Listener<Dibit> listener)
{ {
mBroadcaster.addListener( listener ); mBroadcaster.addListener(listener);
} }
public void removeListener( Listener<Dibit> listener ) public void removeListener(Listener<Dibit> listener)
{ {
mBroadcaster.removeListener( listener ); mBroadcaster.removeListener(listener);
} }
@Override @Override
public void receive( Complex sample ) public void receive(Complex sample)
{ {
mBroadcaster.broadcast( decide( sample ) ); mBroadcaster.broadcast(decode(sample));
} }
public static Dibit decide( Complex sample ) @Override
{ public Dibit decode(Complex sample)
if( sample.inphase() > 0 ) {
{ if(sample.inphase() > 0)
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1; {
} return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
else }
{ else
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3; {
} return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
} }
}
} }

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk; package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.dsp.symbol.Dibit;
@@ -5,24 +20,7 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.Complex;
/******************************************************************************* public class QPSKSymbolDecoder implements Listener<Complex>, IQPSKSymbolDecoder
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
public class QPSKPolarSlicer implements Listener<Complex>
{ {
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>(); private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
@@ -37,7 +35,7 @@ public class QPSKPolarSlicer implements Listener<Complex>
* / \ * / \
* / 11 \ * / 11 \
*/ */
public QPSKPolarSlicer() public QPSKSymbolDecoder()
{ {
} }
@@ -60,10 +58,10 @@ public class QPSKPolarSlicer implements Listener<Complex>
@Override @Override
public void receive( Complex complex ) public void receive( Complex complex )
{ {
mBroadcaster.broadcast( decide( complex ) ); mBroadcaster.broadcast( decode( complex ) );
} }
public static Dibit decide( Complex complex ) public Dibit decode(Complex complex )
{ {
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) ) if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
{ {

View File

@@ -0,0 +1,57 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
/**
* Calculates the phase error for a QPSK polar-orientation constellation
*/
public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculator
{
private float mPhaseError;
@Override
public float getPhaseError(Complex symbol)
{
mPhaseError = 0.0f;
if(Math.abs(symbol.inphase()) > Math.abs(symbol.quadrature()))
{
if(symbol.inphase() > 0)
{
mPhaseError = -symbol.quadrature();
}
else
{
mPhaseError = symbol.quadrature();
}
}
else
{
if(symbol.quadrature() > 0)
{
mPhaseError = symbol.inphase();
}
else
{
mPhaseError = -symbol.inphase();
}
}
return mPhaseError;
}
}

View File

@@ -0,0 +1,93 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.sample.complex.Complex;
public class SymbolDecisionData
{
//45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d);
private ComplexCircularBuffer mBuffer;
private float mSamplingPoint;
/**
* Circular buffer for capturing two symbols worth of sample data for use in instrumentation.
* @param samplesPerSymbol to size the buffer
*/
public SymbolDecisionData(int samplesPerSymbol)
{
mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol);
}
/**
* Stores the complex sample in the buffer
*/
public void receive(float inphase, float quadrature)
{
mBuffer.put(new Complex(inphase, quadrature));
}
/**
* Stores the complex sample in the buffer
*/
public void receive(Complex complex)
{
mBuffer.put(complex.copy());
}
/**
* Sets the fractional sampling point for the current symbol to use in interpolating the current symbol.
*/
public void setSamplingPoint(float samplingPoint)
{
mSamplingPoint = samplingPoint;
}
/**
* Fractional sampling point
*/
public float getSamplingPoint()
{
return mSamplingPoint;
}
/**
* Array of demodulated samples representing the current symbol where each current sample is demodulated against
* the previous symbol's sample to produce the differential demodulated sample. Samples are in time order.
*
* Note: differential decoding is on an integral samples-per-symbol basis and does not use fractional interpolation.
*/
public Complex[] getDemodulated()
{
Complex[] samples = mBuffer.getAll();
int length = samples.length / 2;
Complex[] demodulated = new Complex[length];
for(int x = 0; x < samples.length / 2; x++)
{
Complex copy = samples[length + x].copy();
copy.multiply(samples[x].conjugate());
demodulated[x] = copy;
}
return demodulated;
}
}

View File

@@ -0,0 +1,35 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk;
public enum SymbolInversion
{
DEGREES_90_PLUS(Math.PI / 2.0),
DEGREES_90_MINUS(Math.PI / -2.0),
DEGREES_180(Math.PI);
private double mAngleRadians;
SymbolInversion(double angleRadians)
{
mAngleRadians = angleRadians;
}
public double getError()
{
return mAngleRadians;
}
}

View File

@@ -0,0 +1,182 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.sample.complex.Complex;
/**
* Costas Loop - phase locked loop designed to automatically synchronize to the incoming carrier frequency in order to
* zeroize any frequency offset inherent in the signal due to either mis-tuning or carrier frequency drift.
*
* Costas loop structure was modeled from KA1RBI's OP25/cqpsk.py
*
* Loop bandwidth and alpha/beta gain formulas derived from:
* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html
*/
public class CostasLoop implements IPhaseLockedLoop
{
public static final double TWO_PI = 2.0 * Math.PI;
private Complex mCurrentVector = new Complex(0, 0);
private double mLoopPhase = 0.0;
private double mLoopFrequency = 0.0;
private double mMaximumLoopFrequency;
private double mDamping = Math.sqrt(2.0) / 2.0;
private double mAlphaGain;
private double mBetaGain;
private Tracking mTracking = Tracking.NORMAL;
/**
* Costas Loop for tracking and correcting frequency error in a received carrier signal.
*
* @param sampleRate of the incoming samples
* @param symbolRate of the digital signal
*/
public CostasLoop(double sampleRate, double symbolRate)
{
mMaximumLoopFrequency = TWO_PI * (symbolRate / 2.0) / sampleRate;
updateLoopBandwidth();
}
/**
* Corrects the current phase tracker when an inverted output is detected. The inversion will take the form of
* +/- 90 degrees or +/- 180 degrees, although the latter correction is equal regardless of the sign.
*
* If the supplied correction value places the loop frequency outside of the max frequency, then the frequency will
* be corrected 360 degrees in the opposite direction to maintain within the max frequency bounds.
*
* @param correction as measured in radians
*/
public void correctInversion(double correction)
{
mLoopFrequency += correction;
while(mLoopFrequency > mMaximumLoopFrequency)
{
mLoopFrequency -= 2.0 * mMaximumLoopFrequency;
}
while(mLoopFrequency < -mMaximumLoopFrequency)
{
mLoopFrequency += 2.0 * mMaximumLoopFrequency;
}
}
/**
* Updates the loop bandwidth and alpha/beta gains according to the current loop synchronization state.
*/
private void updateLoopBandwidth()
{
double bandwidth = TWO_PI / mTracking.getLoopBandwidth();
mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
}
/**
* Sets the tracking state for this costas loop. The tracking state affects the aggressiveness of the alpha/beta
* gain values in synchronizing with the signal carrier.
*
* @param tracking
*/
public void setTracking(Tracking tracking)
{
mTracking = tracking;
updateLoopBandwidth();
}
/**
* Increments the phase of the loop for each sample received at the sample rate.
*/
public void increment()
{
mLoopPhase += mLoopFrequency;
/* Normalize phase between +/- 2 * PI */
if(mLoopPhase > TWO_PI)
{
mLoopPhase -= TWO_PI;
}
if(mLoopPhase < -TWO_PI)
{
mLoopPhase += TWO_PI;
}
}
/**
* Current vector of the loop. Note: this value is updated for the current angle in radians each time this method
* is invoked.
*/
public Complex getCurrentVector()
{
mCurrentVector.setAngle(mLoopPhase);
return mCurrentVector;
}
@Override
public Complex incrementAndGetCurrentVector()
{
increment();
return getCurrentVector();
}
public double getLoopFrequency()
{
return mLoopFrequency;
}
/**
* Updates the costas loop frequency and phase to adjust for the phase
* error value
*
* @param phaseError - (-)= late and (+)= early
*/
public void adjust(double phaseError)
{
mLoopFrequency += (mBetaGain * phaseError);
mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError);
/* Normalize phase between +/- 2 * PI */
if(mLoopPhase > TWO_PI)
{
mLoopPhase -= TWO_PI;
}
if(mLoopPhase < -TWO_PI)
{
mLoopPhase += TWO_PI;
}
/* Limit frequency to +/- maximum loop frequency */
if(mLoopFrequency > mMaximumLoopFrequency)
{
mLoopFrequency = mMaximumLoopFrequency;
}
if(mLoopFrequency < -mMaximumLoopFrequency)
{
mLoopFrequency = -mMaximumLoopFrequency;
}
}
@Override
public void reset()
{
mLoopPhase = 0.0;
mLoopFrequency = 0.0;
}
}

View File

@@ -0,0 +1,59 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.sample.complex.Complex;
/**
* Phase-Locked Loop (PLL) interface
*/
public interface IPhaseLockedLoop
{
/**
* Increments the PLL by one sample period
*/
void increment();
/**
* Applies an error value to the PLL.
*
* @param error value in the range of -PI <> +PI
*/
void adjust(double error);
/**
* Adjusts the current phase of the loop to account for +/-90 or 180 degree inverted phase locks when detected
* @param correction to apply to the phase of the loop
*/
void correctInversion(double correction);
/**
* Current vector of the PLL to use in de-spinning the incoming samples.
* @return complex vector
*/
Complex getCurrentVector();
/**
* Increments the phase rotation and gets the current vector of the PLL to use in de-spinning the incoming samples.
* @return complex vector
*/
Complex incrementAndGetCurrentVector();
/**
* Reset tracking to 0
*/
void reset();
}

View File

@@ -0,0 +1,42 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk.pll;
/**
* Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
*/
public enum Tracking
{
FASTEST(50.0),
FAST(100.0),
NORMAL(200.0),
SLOW(400.0);
private double mLoopBandwidth;
Tracking(double loopBandwidth)
{
mLoopBandwidth = loopBandwidth;
}
/**
* Loop bandwidth setting
*/
public double getLoopBandwidth()
{
return mLoopBandwidth;
}
}

View File

@@ -25,6 +25,8 @@ import io.github.dsheirer.audio.AudioManager;
import io.github.dsheirer.audio.broadcast.BroadcastModel; import io.github.dsheirer.audio.broadcast.BroadcastModel;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel; import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
import io.github.dsheirer.controller.ControllerPanel; import io.github.dsheirer.controller.ControllerPanel;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
import io.github.dsheirer.controller.channel.ChannelModel; import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager; import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.ChannelSelectionManager; import io.github.dsheirer.controller.channel.ChannelSelectionManager;
@@ -51,8 +53,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.swing.*; import javax.swing.JCheckBoxMenuItem;
import java.awt.*; import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;
import java.awt.AWTException;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Robot;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
@@ -61,6 +74,7 @@ import java.io.IOException;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
public class SDRTrunk implements Listener<TunerEvent> public class SDRTrunk implements Listener<TunerEvent>
{ {
@@ -72,6 +86,8 @@ public class SDRTrunk implements Listener<TunerEvent>
private BroadcastStatusPanel mBroadcastStatusPanel; private BroadcastStatusPanel mBroadcastStatusPanel;
private BroadcastModel mBroadcastModel; private BroadcastModel mBroadcastModel;
private ControllerPanel mControllerPanel; private ControllerPanel mControllerPanel;
private ChannelModel mChannelModel;
private ChannelProcessingManager mChannelProcessingManager;
private SettingsManager mSettingsManager; private SettingsManager mSettingsManager;
private SpectralDisplayPanel mSpectralPanel; private SpectralDisplayPanel mSpectralPanel;
private JFrame mMainGui = new JFrame(); private JFrame mMainGui = new JFrame();
@@ -117,7 +133,7 @@ public class SDRTrunk implements Listener<TunerEvent>
AliasModel aliasModel = new AliasModel(); AliasModel aliasModel = new AliasModel();
ChannelModel channelModel = new ChannelModel(); mChannelModel = new ChannelModel();
ChannelMapModel channelMapModel = new ChannelMapModel(); ChannelMapModel channelMapModel = new ChannelMapModel();
@@ -127,43 +143,42 @@ public class SDRTrunk implements Listener<TunerEvent>
SourceManager sourceManager = new SourceManager(tunerModel, mSettingsManager); SourceManager sourceManager = new SourceManager(tunerModel, mSettingsManager);
ChannelProcessingManager channelProcessingManager = new ChannelProcessingManager( mChannelProcessingManager = new ChannelProcessingManager(mChannelModel, channelMapModel, aliasModel,
channelModel, channelMapModel, aliasModel, eventLogManager, recorderManager, sourceManager); eventLogManager, recorderManager, sourceManager);
channelProcessingManager.addAudioPacketListener(recorderManager); mChannelProcessingManager.addAudioPacketListener(recorderManager);
channelModel.addListener(channelProcessingManager); mChannelModel.addListener(mChannelProcessingManager);
ChannelSelectionManager channelSelectionManager = ChannelSelectionManager channelSelectionManager =
new ChannelSelectionManager(channelModel); new ChannelSelectionManager(mChannelModel);
channelModel.addListener(channelSelectionManager); mChannelModel.addListener(channelSelectionManager);
AliasActionManager aliasActionManager = new AliasActionManager(); AliasActionManager aliasActionManager = new AliasActionManager();
channelProcessingManager.addMessageListener(aliasActionManager); mChannelProcessingManager.addMessageListener(aliasActionManager);
AudioManager audioManager = new AudioManager(sourceManager.getMixerManager()); AudioManager audioManager = new AudioManager(sourceManager.getMixerManager());
channelProcessingManager.addAudioPacketListener(audioManager); mChannelProcessingManager.addAudioPacketListener(audioManager);
mBroadcastModel = new BroadcastModel(mIconManager); mBroadcastModel = new BroadcastModel(mIconManager);
channelProcessingManager.addAudioPacketListener(mBroadcastModel); mChannelProcessingManager.addAudioPacketListener(mBroadcastModel);
MapService mapService = new MapService(mIconManager); MapService mapService = new MapService(mIconManager);
channelProcessingManager.addMessageListener(mapService); mChannelProcessingManager.addMessageListener(mapService);
mControllerPanel = new ControllerPanel(audioManager, aliasModel, mBroadcastModel, mControllerPanel = new ControllerPanel(audioManager, aliasModel, mBroadcastModel,
channelModel, channelMapModel, channelProcessingManager, mIconManager, mChannelModel, channelMapModel, mChannelProcessingManager, mIconManager,
mapService, mSettingsManager, sourceManager, tunerModel); mapService, mSettingsManager, sourceManager, tunerModel);
mSpectralPanel = new SpectralDisplayPanel(channelModel, mSpectralPanel = new SpectralDisplayPanel(mChannelModel,
channelProcessingManager, mSettingsManager); mChannelProcessingManager, mSettingsManager);
TunerSpectralDisplayManager tunerSpectralDisplayManager = TunerSpectralDisplayManager tunerSpectralDisplayManager = new TunerSpectralDisplayManager(mSpectralPanel,
new TunerSpectralDisplayManager(mSpectralPanel, mChannelModel, mChannelProcessingManager, mSettingsManager);
channelModel, channelProcessingManager, mSettingsManager);
tunerModel.addListener(tunerSpectralDisplayManager); tunerModel.addListener(tunerSpectralDisplayManager);
tunerModel.addListener(this); tunerModel.addListener(this);
PlaylistManager playlistManager = new PlaylistManager(aliasModel, mBroadcastModel, channelModel, PlaylistManager playlistManager = new PlaylistManager(aliasModel, mBroadcastModel, mChannelModel,
channelMapModel); channelMapModel);
playlistManager.init(); playlistManager.init();
@@ -183,6 +198,7 @@ public class SDRTrunk implements Listener<TunerEvent>
try try
{ {
mMainGui.setVisible(true); mMainGui.setVisible(true);
autoStartChannels();
} }
catch(Exception e) catch(Exception e)
{ {
@@ -192,6 +208,21 @@ public class SDRTrunk implements Listener<TunerEvent>
}); });
} }
/**
* Shows a dialog that lists the channels that have been designated for auto-start, sorted by auto-start order and
* allows the user to start now, cancel, or allow the timer to expire and then start the channels. The dialog will
* only show if there are one ore more channels designated for auto-start.
*/
private void autoStartChannels()
{
List<Channel> channels = mChannelModel.getAutoStartChannels();
if(channels.size() > 0)
{
ChannelAutoStartFrame autoStartFrame = new ChannelAutoStartFrame(mChannelProcessingManager, channels);
}
}
/** /**
* Launch the application. * Launch the application.
*/ */
@@ -212,7 +243,8 @@ public class SDRTrunk implements Listener<TunerEvent>
*/ */
mTitle = SystemProperties.getInstance().getApplicationName(); mTitle = SystemProperties.getInstance().getApplicationName();
mMainGui.setTitle(mTitle); mMainGui.setTitle(mTitle);
mMainGui.setBounds(100, 100, 1280, 800); mMainGui.setSize(new Dimension(1280,800));
mMainGui.setLocationRelativeTo(null);
mMainGui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); mMainGui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Set preferred sizes to influence the split //Set preferred sizes to influence the split

View File

@@ -2,15 +2,19 @@ package io.github.dsheirer.instrument.gui;
import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.BroadcastModel; import io.github.dsheirer.audio.broadcast.BroadcastModel;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelModel; import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.map.ChannelMapModel; import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.module.decode.DecodeConfigurationEditor; import io.github.dsheirer.module.decode.DecodeConfigurationEditor;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.playlist.PlaylistManager; import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.source.IControllableFileSource; import io.github.dsheirer.source.IControllableFileSource;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import javax.swing.*; import javax.swing.JButton;
import java.awt.*; import javax.swing.JDesktopPane;
import javax.swing.JInternalFrame;
import java.awt.Dimension;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
@@ -18,14 +22,14 @@ public class DecoderSelectionFrame extends JInternalFrame
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private Channel mChannel = new Channel();
private DecodeConfigurationEditor mDecodeEditor = new DecodeConfigurationEditor(null); private DecodeConfigurationEditor mDecodeEditor = new DecodeConfigurationEditor(null);
private PlaylistManager mPlaylistManager; private PlaylistManager mPlaylistManager;
private IControllableFileSource mSource; private IControllableFileSource mSource;
private JDesktopPane mDesktop; private JDesktopPane mDesktop;
public DecoderSelectionFrame(JDesktopPane desktop, public DecoderSelectionFrame(JDesktopPane desktop, IControllableFileSource source)
IControllableFileSource source)
{ {
AliasModel aliasModel = new AliasModel(); AliasModel aliasModel = new AliasModel();
BroadcastModel broadcastModel = new BroadcastModel(null); BroadcastModel broadcastModel = new BroadcastModel(null);
@@ -37,6 +41,7 @@ public class DecoderSelectionFrame extends JInternalFrame
mDesktop = desktop; mDesktop = desktop;
mSource = source; mSource = source;
mDecodeEditor.setItem(mChannel);
initGUI(); initGUI();
} }
@@ -72,17 +77,23 @@ public class DecoderSelectionFrame extends JInternalFrame
@Override @Override
public void actionPerformed(ActionEvent arg0) public void actionPerformed(ActionEvent arg0)
{ {
// DecodeConfiguration config = mDecodeEditor.getDecodeConfig(); mDecodeEditor.save();
//
// if( config != null ) Channel channel = mDecodeEditor.getItem();
// {
// DecoderViewFrame decoderFrame = new DecoderViewFrame( if(channel != null)
// mPlaylistManager, null, mSource ); {
// DecodeConfiguration config = channel.getDecodeConfiguration();
// decoderFrame.setVisible( true );
// if( config != null )
// mDesktop.add( decoderFrame ); {
// } DecoderViewFrame decoderFrame = new DecoderViewFrame(mPlaylistManager, channel, mSource );
decoderFrame.setVisible( true );
mDesktop.add( decoderFrame );
}
}
} }
}); });
} }

View File

@@ -0,0 +1,183 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory;
import io.github.dsheirer.module.decode.DecoderType;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.layout.BorderPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
/**
* Application for viewing and working with demodulators and recording files.
*/
public class DemodulatorViewerFX extends Application
{
private final static Logger mLog = LoggerFactory.getLogger(DemodulatorViewerFX.class);
private Stage mStage;
private MenuBar mMenuBar;
private RecentFilesMenu mRecentFilesMenu;
private ViewerDesktop mViewerDesktop;
@Override
public void start(Stage primaryStage) throws Exception
{
mStage = primaryStage;
setTitle(null);
BorderPane borderPane = new BorderPane();
borderPane.setTop(getMenuBar());
borderPane.setCenter(getViewerDesktop());
Scene scene = new Scene(borderPane, 1500, 900);
mStage.setScene(scene);
mStage.show();
}
private void setTitle(DecoderType decoderType)
{
if(decoderType != null)
{
mStage.setTitle("Demodulator Viewer FX - " + decoderType.getDisplayString());
}
else
{
mStage.setTitle("Demodulator Viewer FX");
}
}
private ViewerDesktop getViewerDesktop()
{
if(mViewerDesktop == null)
{
mViewerDesktop = new ViewerDesktop();
}
return mViewerDesktop;
}
private MenuBar getMenuBar()
{
if(mMenuBar == null)
{
mMenuBar = new MenuBar();
Menu fileMenu = new Menu("File");
MenuItem openMenuItem = new MenuItem("Open ...");
openMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open an I/Q recording file");
File file = fileChooser.showOpenDialog(mStage);
getViewerDesktop().load(file);
getRecentFilesMenu().add(file);
}
});
MenuItem closeMenuItem = new MenuItem("Close");
closeMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
getViewerDesktop().close();
}
});
MenuItem exitMenuItem = new MenuItem("Exit");
exitMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
Platform.exit();
}
});
fileMenu.getItems().addAll(openMenuItem, getRecentFilesMenu(), closeMenuItem, new SeparatorMenuItem(),
exitMenuItem);
mMenuBar.getMenus().add(fileMenu);
Menu decoderMenu = new Menu("Decoder");
for(DecoderType decoderType: DecoderType.values())
{
if(DecoderPaneFactory.isSupported(decoderType))
{
MenuItem decoderMenuItem = new MenuItem(decoderType.getShortDisplayString());
decoderMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
getViewerDesktop().setDecoder(decoderType);
setTitle(decoderType);
}
});
decoderMenu.getItems().add(decoderMenuItem);
}
}
mMenuBar.getMenus().add(decoderMenu);
}
return mMenuBar;
}
private RecentFilesMenu getRecentFilesMenu()
{
if(mRecentFilesMenu == null)
{
mRecentFilesMenu = new RecentFilesMenu("Recent","demodulator.viewer", 5);
mRecentFilesMenu.setFileSelectionListener(new RecentFilesMenu.IFileSelectionListener()
{
@Override
public void fileSelected(File file)
{
getViewerDesktop().load(file);
}
});
}
return mRecentFilesMenu;
}
public static void main(String[] args)
{
launch(args);
}
}

View File

@@ -0,0 +1,311 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.source.IControllableFileSource;
import io.github.dsheirer.source.IFrameLocationListener;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import org.kordamp.ikonli.fontawesome.FontAwesome;
import org.kordamp.ikonli.javafx.FontIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
public class PlaybackController extends HBox implements IFrameLocationListener, Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(PlaybackController.class);
private Button mRewindButton;
private TextField mPlaybackPositionText;
private Button mPlay1Button;
private Button mPlay10Button;
private Button mPlay100Button;
private Button mPlay1000Button;
private HBox mControlsBox;
private Label mFileLabel;
private EventHandler<ActionEvent> mPlayEventHandler;
private DecoderPane mSampleRateListener;
private IControllableFileSource mControllableFileSource;
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
public PlaybackController()
{
super(10);
getChildren().addAll(getControlsBox(), getFileLabel());
setControlsEnabled(false);
}
public void setSampleRateListener(DecoderPane decoderPane)
{
mSampleRateListener = decoderPane;
if(mSampleRateListener != null && mControllableFileSource != null)
{
mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
}
}
public void load(File file)
{
if(file != null && file.isFile())
{
try
{
mControllableFileSource = new ComplexWaveSource(file);
mControllableFileSource.setListener(this);
((ComplexWaveSource)mControllableFileSource).setListener((Listener<ComplexBuffer>)this);
mControllableFileSource.open();
if(mSampleRateListener != null)
{
mSampleRateListener.setSampleRate(mControllableFileSource.getSampleRate());
}
}
catch(UnsupportedAudioFileException e)
{
mLog.error("Unsupported Audio File Type [" + (file != null ? file.getAbsolutePath() : "null") + "]");
Alert alert = new Alert(Alert.AlertType.ERROR, "The file type is unsupported.", ButtonType.OK);
alert.show();
return;
}
catch(IOException ioe)
{
mLog.error("Error opening file [" + (file != null ? file.getAbsolutePath() : "null") + "]", ioe);
Alert alert = new Alert(Alert.AlertType.ERROR, "There was an error opening the file",
ButtonType.OK);
alert.show();
return;
}
getFileLabel().setText(file.getName());
setControlsEnabled(true);
}
}
public void close()
{
if(mControllableFileSource != null)
{
try
{
mControllableFileSource.close();
}
catch(IOException ioe)
{
mLog.error("Error closing file", ioe);
}
}
setControlsEnabled(false);
}
/**
* Adds listener to receive complex buffers from this playback
*/
public void addListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.addListener(listener);
}
/**
* Removes the listener from receiving complex buffers from this playback
*/
public void removeListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.removeListener(listener);
}
/**
* Sets all of the playback controls to the specified enabled state.
*/
private void setControlsEnabled(boolean enabled)
{
getRewindButton().setDisable(!enabled);
getPlaybackPositionText().setDisable(!enabled);
getPlay1Button().setDisable(!enabled);
getPlay10Button().setDisable(!enabled);
getPlay100Button().setDisable(!enabled);
getPlay1000Button().setDisable(!enabled);
}
private HBox getControlsBox()
{
if(mControlsBox == null)
{
mControlsBox = new HBox();
mControlsBox.getChildren().addAll(getRewindButton(), getPlaybackPositionText(), getPlay1Button(),
getPlay10Button(), getPlay100Button(), getPlay1000Button());
}
return mControlsBox;
}
private Button getRewindButton()
{
if(mRewindButton == null)
{
mRewindButton = new Button();
mRewindButton.setGraphic(new FontIcon(FontAwesome.FAST_BACKWARD));
}
return mRewindButton;
}
private Button getPlay1Button()
{
if(mPlay1Button == null)
{
mPlay1Button = new Button("1");
mPlay1Button.setUserData(1);
mPlay1Button.setOnAction(getPlaybackEventHandler());
mPlay1Button.setGraphic(new FontIcon(FontAwesome.PLAY));
}
return mPlay1Button;
}
private Button getPlay10Button()
{
if(mPlay10Button == null)
{
mPlay10Button = new Button("10");
mPlay10Button.setUserData(10);
mPlay10Button.setOnAction(getPlaybackEventHandler());
mPlay10Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
}
return mPlay10Button;
}
private Button getPlay100Button()
{
if(mPlay100Button == null)
{
mPlay100Button = new Button("100");
mPlay100Button.setUserData(100);
mPlay100Button.setOnAction(getPlaybackEventHandler());
mPlay100Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
}
return mPlay100Button;
}
private Button getPlay1000Button()
{
if(mPlay1000Button == null)
{
mPlay1000Button = new Button("1000");
mPlay1000Button.setUserData(1000);
mPlay1000Button.setOnAction(getPlaybackEventHandler());
mPlay1000Button.setGraphic(new FontIcon(FontAwesome.FAST_FORWARD));
}
return mPlay1000Button;
}
private EventHandler<ActionEvent> getPlaybackEventHandler()
{
if(mPlayEventHandler == null)
{
mPlayEventHandler = new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
int count = (int)((Button)event.getSource()).getUserData();
if(mControllableFileSource != null)
{
try
{
mControllableFileSource.next(count, true);
}
catch(IOException ioe)
{
mLog.error("Error while playing samples");
}
}
}
};
}
return mPlayEventHandler;
}
private TextField getPlaybackPositionText()
{
if(mPlaybackPositionText == null)
{
mPlaybackPositionText = new TextField("0");
mPlaybackPositionText.setAlignment(Pos.CENTER);
mPlaybackPositionText.setPrefWidth(80);
}
return mPlaybackPositionText;
}
private Label getFileLabel()
{
if(mFileLabel == null)
{
mFileLabel = new Label();
}
return mFileLabel;
}
@Override
public void frameLocationUpdated(int location)
{
getPlaybackPositionText().setText(String.valueOf(location));
}
@Override
public void frameLocationReset()
{
mLog.info("Frame location reset ... ignoring?");
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
mComplexBufferBroadcaster.broadcast(complexBuffer);
}
}

View File

@@ -0,0 +1,192 @@
package io.github.dsheirer.instrument.gui.viewer;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import java.io.File;
import java.util.prefs.Preferences;
public class RecentFilesMenu extends Menu implements EventHandler<ActionEvent>
{
private Preferences mPreferences = Preferences.userNodeForPackage(RecentFilesMenu.class);
private IFileSelectionListener mFileSelectionListener;
private String mIdentifier;
private int mSize;
/**
* Recently used files menu. Uses java Preferences to store recently accessed files and provides access to those
* files via child menu items to this menu.
*
* @param label to use for this menu.
* @param uniqueIdentifier to use in addition to the class path of this class to uniquely identify recently accessed
* files in a java Preferences data store.
* @param size of the maximum number of recent files to present to the user
*/
public RecentFilesMenu(String label, String uniqueIdentifier, int size)
{
super(label);
mIdentifier = uniqueIdentifier;
mSize = size;
load();
}
/**
* Registers the listener to receive file selection events when the user selects a recently used file.
* @param listener to receive file selection events
*/
public void setFileSelectionListener(IFileSelectionListener listener)
{
mFileSelectionListener = listener;
}
/**
* Adds the file to the recently accessed files list. If this file is already included in the recently files list,
* it will be promoted to the first menu item and all other recently used files will be demoted.
*
* @param file to add to the recently used files list.
*/
public void add(File file)
{
if(file != null)
{
String filePath = file.getAbsolutePath();
int index = mSize - 1;
//Check to see if this file is already in the recent files list
for(int x = mSize - 1; x >= 0; x--)
{
String existingItem = mPreferences.get(getKey(x), null);
if(existingItem != null && existingItem.equals(filePath))
{
index = x;
break;
}
}
//Shift all recent files down to make room for the newly added file at the top of the list
while(index > 0)
{
String newerItem = mPreferences.get(getKey(index - 1), null);
if(newerItem != null)
{
mPreferences.put(getKey(index), newerItem);
}
index--;
}
mPreferences.put(getKey(0), file.getAbsolutePath());
}
load();
}
/**
* Creates a unique key value for each numbered recently used file item in the preferences.
*
* @param index of recent file item.
* @return unique key to use with the preferences instance.
*/
private String getKey(int index)
{
return mIdentifier + ".recent.file." + index;
}
/**
* Clears and (re)loads all child recent file menu items.
*/
private void load()
{
getItems().clear();
for(int x = 0; x < mSize; x++)
{
String path = mPreferences.get(getKey(x), null);
if(path != null)
{
File file = new File(path);
if(file.isFile())
{
MenuItem menuItem = new MenuItem(file.getName());
menuItem.setUserData(file);
menuItem.setOnAction(this);
getItems().add(menuItem);
}
}
}
if(getItems().size() > 0)
{
setDisable(false);
getItems().add(new SeparatorMenuItem());
MenuItem clearMenuItem = new MenuItem("Clear History");
clearMenuItem.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
clearHistory();
}
});
getItems().add(clearMenuItem);
}
else
{
setDisable(true);
}
}
/**
* Clears the recently used files history and removes all items from the preferences instance
*/
private void clearHistory()
{
for(int x = 0; x < mSize; x++)
{
mPreferences.remove(getKey(x));
}
load();
}
/**
* Event handler for all recent file child menu items. Extracts the selected file stored as the user data object
* in each of the menu items and notifies the listener that the recent file has been selected.
*
* @param event from a recent file menu item child of this menu.
*/
@Override
public void handle(ActionEvent event)
{
if(mFileSelectionListener != null)
{
if(event.getSource() instanceof MenuItem)
{
Object fileObject = ((MenuItem)event.getSource()).getUserData();
if(fileObject instanceof File)
{
mFileSelectionListener.fileSelected((File)fileObject);
}
}
}
}
/**
* Listener interface for file selection events.
*/
public interface IFileSelectionListener
{
void fileSelected(File file);
}
}

View File

@@ -0,0 +1,81 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPane;
import io.github.dsheirer.instrument.gui.viewer.decoder.DecoderPaneFactory;
import io.github.dsheirer.module.decode.DecoderType;
import javafx.scene.layout.BorderPane;
import java.io.File;
public class ViewerDesktop extends BorderPane
{
private PlaybackController mPlaybackController;
private DecoderPane mDecoderPane = DecoderPaneFactory.getDefaultPane();
public ViewerDesktop()
{
setBottom(getPlaybackController());
setCenter(getDecoderPane());
}
public void load(File file)
{
close();
getPlaybackController().load(file);
}
public void setDecoder(DecoderType decoderType)
{
getPlaybackController().removeListener(getDecoderPane());
getChildren().remove(getDecoderPane());
mDecoderPane = DecoderPaneFactory.getDecoderPane(decoderType);
setCenter(getDecoderPane());
getPlaybackController().addListener(getDecoderPane());
getPlaybackController().setSampleRateListener(getDecoderPane());
}
/**
* Decoder pane
*/
public DecoderPane getDecoderPane()
{
if(mDecoderPane == null)
{
mDecoderPane = DecoderPaneFactory.getDefaultPane();
}
return mDecoderPane;
}
public void close()
{
getPlaybackController().close();
}
private PlaybackController getPlaybackController()
{
if(mPlaybackController == null)
{
mPlaybackController = new PlaybackController();
}
return mPlaybackController;
}
}

View File

@@ -0,0 +1,121 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ComplexPhaseLineChart extends LineChart implements Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexPhaseLineChart.class);
private static final Complex ANGLE_OFFSET_45_DEGREES = Complex.fromAngle(Math.PI / 4.0);
private ComplexCircularBuffer mComplexCircularBuffer;
private ComplexGain mComplexGain = new ComplexGain(600.0f);
private ObservableList<Data<Integer,Float>> mPhaseValues = FXCollections.observableArrayList();
private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40);
public ComplexPhaseLineChart(int length)
{
super(new NumberAxis("Time", 1, length - 10, 2),
new NumberAxis("Phase", -Math.PI, Math.PI, Math.PI / 2.0));
Series<Integer,Float> phaseSeries = new Series<>("Phase", mPhaseValues);
ObservableList<Series<Integer,Float>> observableList = FXCollections.observableArrayList(phaseSeries);
setData(observableList);
init(length);
}
private void init(int length)
{
mLengthProperty.addListener(new ChangeListener<Number>()
{
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
{
mComplexCircularBuffer = new ComplexCircularBuffer(newValue.intValue());
}
});
mComplexCircularBuffer = new ComplexCircularBuffer(length);
length().setValue(length);
for(int x = 10; x <= length; x++)
{
mPhaseValues.add(new Data(x - 10, 0.0f));
}
}
/**
* Length of the x-axis for displaying samples
*/
public IntegerProperty length()
{
return mLengthProperty;
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
Complex sample;
for(int x = 0; x < samples.length; x += 2)
{
sample = new Complex(samples[x], samples[x + 1]);
Complex copy = sample.copy();
mComplexGain.apply(copy);
mComplexCircularBuffer.put(copy);
}
updateChart();
}
private void updateChart()
{
Complex[] samples = mComplexCircularBuffer.getAll();
for(int x = 10; x < samples.length; x++)
{
Complex previous = samples[x - 10].copy();
Complex sample = samples[x].copy();
if(previous != null && sample != null)
{
// previous.normalize();
// sample.normalize();
previous.add(ANGLE_OFFSET_45_DEGREES);
sample.multiply(previous.conjugate());
// sample.normalize();
mPhaseValues.get(x - 9).setYValue(sample.angle());
}
}
}
}

View File

@@ -0,0 +1,131 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ComplexSampleLineChart extends LineChart implements Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexSampleLineChart.class);
private ComplexCircularBuffer mComplexCircularBuffer;
private ComplexCircularBuffer mDemodulationCircularBuffer;
private ComplexGain mComplexGain = new ComplexGain(500.0f);
private ObservableList<XYChart.Data<Integer,Float>> mISamples = FXCollections.observableArrayList();
private ObservableList<XYChart.Data<Integer,Float>> mQSamples = FXCollections.observableArrayList();
private IntegerProperty mLengthProperty = new SimpleIntegerProperty(40);
public ComplexSampleLineChart(int length, int samplesPerSymbol)
{
super(new NumberAxis("Differential-Demodulated Samples", 0, length, 10),
new NumberAxis("Value", -1.0, 1.0, 0.25));
LineChart.Series<Integer,Float> iSampleSeries = new LineChart.Series<>("Inphase", mISamples);
LineChart.Series<Integer,Float> qSampleSeries = new LineChart.Series<>("Quadrature", mQSamples);
ObservableList<XYChart.Series<Integer,Float>> observableList =
FXCollections.observableArrayList(iSampleSeries, qSampleSeries);
setData(observableList);
init(length, samplesPerSymbol);
}
public void setSamplesPerSymbol(int samplesPerSymbol)
{
mDemodulationCircularBuffer = new ComplexCircularBuffer(samplesPerSymbol);
}
private void init(int length, int samplesPerSymbol)
{
mLengthProperty.addListener(new ChangeListener<Number>()
{
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
{
mComplexCircularBuffer = new ComplexCircularBuffer(newValue.intValue());
}
});
mComplexCircularBuffer = new ComplexCircularBuffer(length);
mDemodulationCircularBuffer = new ComplexCircularBuffer(samplesPerSymbol);
length().setValue(length);
for(int x = 1; x <= length; x++)
{
mISamples.add(new Data(x, 0.0f));
mQSamples.add(new Data(x, 0.0f));
}
}
/**
* Length of the x-axis for displaying samples
*/
public IntegerProperty length()
{
return mLengthProperty;
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
Complex sample = new Complex(0,0);
for(int x = 0; x < samples.length; x += 2)
{
sample.setValues(samples[x], samples[x + 1]);
// mComplexGain.apply(sample);
sample.normalize();
Complex previous = mDemodulationCircularBuffer.get(sample.copy());
sample.multiply(previous.conjugate());
mComplexCircularBuffer.put(sample.copy());
}
updateChart();
}
private void updateChart()
{
Complex[] samples = mComplexCircularBuffer.getAll();
for(int x = 0; x < samples.length; x++)
{
Complex sample = samples[x];
if(sample != null)
{
sample.normalize();
mISamples.get(x).setYValue(sample.inphase());
mQSamples.get(x).setYValue(sample.quadrature());
}
}
}
}

View File

@@ -0,0 +1,72 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.DoubleCircularBuffer;
import io.github.dsheirer.sample.Listener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DoubleLineChart extends LineChart implements Listener<Double>
{
private final static Logger mLog = LoggerFactory.getLogger(DoubleLineChart.class);
private DoubleCircularBuffer mDoubleCircularBuffer;
private ObservableList<Data<Integer,Double>> mPhaseValues = FXCollections.observableArrayList();
public DoubleLineChart(String seriesLabel, double min, double max, double tick, int length)
{
super(new NumberAxis(1, length, 2), new NumberAxis(min, max, tick));
Series<Integer,Double> phaseSeries = new Series<>(seriesLabel, mPhaseValues);
ObservableList<Series<Integer,Double>> observableList = FXCollections.observableArrayList(phaseSeries);
setData(observableList);
init(length);
}
private void init(int length)
{
mDoubleCircularBuffer = new DoubleCircularBuffer(length);
for(int x = 1; x <= length; x++)
{
mPhaseValues.add(new Data(x, 0.0));
}
}
@Override
public void receive(Double phase)
{
mDoubleCircularBuffer.put(phase);
updateChart();
}
private void updateChart()
{
double[] values = mDoubleCircularBuffer.getAll();
for(int x = 0; x < values.length; x++)
{
mPhaseValues.get(x).setYValue(values[x]);
}
}
}

View File

@@ -0,0 +1,121 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EyeDiagramChart extends LineChart implements Listener<SymbolDecisionData>
{
private final static Logger mLog = LoggerFactory.getLogger(EyeDiagramChart.class);
private ObservableList<Series<Double,Double>> mData = FXCollections.observableArrayList();
private int mSeriesCount;
private int mSeriesPointer;
private int mSeriesLength;
public EyeDiagramChart(int seriesCount)
{
super(new NumberAxis("Symbol Timing (5 = center)", 1.0, 10.0, 1.0),
new NumberAxis("Value", -1.25, 1.25, 0.25));
mSeriesCount = seriesCount * 2;
init();
}
private void init()
{
for(int x = 0; x < mSeriesCount; x++)
{
ObservableList<Data<Double,Double>> series = FXCollections.observableArrayList();
mData.add(new Series<>(series));
}
setData(mData);
}
private ObservableList<Data<Double,Double>> getSeries(int series, int length)
{
ObservableList<Data<Double,Double>> seriesData = mData.get(series).getData();
while(seriesData.size() > length)
{
seriesData.remove(0);
}
while(seriesData.size() < length)
{
seriesData.add(new Data<Double,Double>(0.0,0.0));
}
return seriesData;
}
private void checkChartLength(int length)
{
if(mSeriesLength != length)
{
((NumberAxis)getXAxis()).setUpperBound(length);
mSeriesLength = length;
}
}
@Override
public void receive(SymbolDecisionData symbolDecisionData)
{
Complex[] demodulated = symbolDecisionData.getDemodulated();
int length = demodulated.length;
checkChartLength(length);
ObservableList<Data<Double,Double>> iSeries = getSeries(mSeriesPointer++, length + 1);
if(mSeriesPointer >= mSeriesCount)
{
mSeriesPointer = 0;
}
ObservableList<Data<Double,Double>> qSeries = getSeries(mSeriesPointer++, length + 1);
if(mSeriesPointer >= mSeriesCount)
{
mSeriesPointer = 0;
}
for(int x = 0; x < length; x++)
{
demodulated[x].normalize();
Data<Double,Double> iDataPoint = iSeries.get(x);
iDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
double iValue = demodulated[x].inphase();
iDataPoint.setYValue(iValue);
Data<Double,Double> qDataPoint = qSeries.get(x);
qDataPoint.setXValue((double)x + 1 - symbolDecisionData.getSamplingPoint());
double qValue = demodulated[x].quadrature();
qDataPoint.setYValue(qValue);
}
}
}

View File

@@ -0,0 +1,119 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.DoubleCircularBuffer;
import io.github.dsheirer.dsp.fm.FMDemodulator;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FMDemodulatedLineChart extends LineChart implements Listener<ComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatedLineChart.class);
private DoubleCircularBuffer mDoubleCircularBuffer;
private int mSamplesPerSymbol;
private int mTwiceSamplesPerSymbol;
private int mSeriesCount;
private ObservableList<Series<Integer,Double>> mData = FXCollections.observableArrayList();
private Complex mPreviousSample = new Complex(0,0);
private int mSeriesPointer;
private int mSamplePointer;
public FMDemodulatedLineChart(int samplesPerSymbol, int seriesCount)
{
super(new NumberAxis("Time", 1, samplesPerSymbol * 2, 2),
new NumberAxis("Demodulated", -1.0, 1.0, 0.25));
mSamplesPerSymbol = samplesPerSymbol;
mTwiceSamplesPerSymbol = mSamplesPerSymbol * 2;
mSeriesCount = seriesCount;
init();
}
private void init()
{
int seriesLength = 2 * mSamplesPerSymbol;
mDoubleCircularBuffer = new DoubleCircularBuffer(seriesLength * mSeriesCount);
for(int x = 0; x < mSeriesCount; x++)
{
ObservableList<Data<Integer,Double>> series = FXCollections.observableArrayList();
for(int y = 0; y < seriesLength; y++)
{
series.add(new Data<Integer,Double>(y + 1, 0.0));
}
mData.add(new Series<>(series));
}
setData(mData);
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
Complex sample;
for(int x = 0; x < samples.length; x += 2)
{
sample = new Complex(samples[x], samples[x + 1]);
double demodulated = FMDemodulator.demodulate(mPreviousSample, sample);
demodulated *= 2.0;
if(demodulated > 1.0)
{
demodulated = 1.0;
}
else if(demodulated < -1.0)
{
demodulated = -1.0;
}
mDoubleCircularBuffer.put(demodulated);
mPreviousSample = sample;
updateChart(demodulated);
}
}
private void updateChart(double value)
{
Series<Integer,Double> series = mData.get(mSeriesPointer);
Data data = series.getData().get(mSamplePointer++);
data.setYValue(value);
if(mSamplePointer >= mTwiceSamplesPerSymbol)
{
mSamplePointer = 0;
mSeriesPointer++;
if(mSeriesPointer >= mSeriesCount)
{
mSeriesPointer = 0;
}
}
}
}

View File

@@ -0,0 +1,73 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.buffer.DoubleCircularBuffer;
import io.github.dsheirer.sample.Listener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PhaseLineChart extends LineChart implements Listener<Double>
{
private final static Logger mLog = LoggerFactory.getLogger(PhaseLineChart.class);
private DoubleCircularBuffer mDoubleCircularBuffer;
private ObservableList<Data<Integer,Double>> mPhaseValues = FXCollections.observableArrayList();
public PhaseLineChart(int length)
{
super(new NumberAxis("Time", 1, length, 2),
new NumberAxis("Phase", -0.8, 0.8, 0.1));
Series<Integer,Double> phaseSeries = new Series<>("PLL Phase Error", mPhaseValues);
ObservableList<Series<Integer,Double>> observableList = FXCollections.observableArrayList(phaseSeries);
setData(observableList);
init(length);
}
private void init(int length)
{
mDoubleCircularBuffer = new DoubleCircularBuffer(length);
for(int x = 1; x <= length; x++)
{
mPhaseValues.add(new Data(x, 0.0));
}
}
@Override
public void receive(Double phase)
{
mDoubleCircularBuffer.put(phase);
updateChart();
}
private void updateChart()
{
double[] values = mDoubleCircularBuffer.getAll();
for(int x = 0; x < values.length; x++)
{
mPhaseValues.get(x).setYValue(values[x]);
}
}
}

View File

@@ -0,0 +1,90 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.chart;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SymbolChart extends ScatterChart implements Listener<Complex>
{
private final static Logger mLog = LoggerFactory.getLogger(SymbolChart.class);
private ObservableList<ScatterChart.Data> mCurrentPoints = FXCollections.observableArrayList();
// private ObservableList<ScatterChart.Data> mPreviousPoints = FXCollections.observableArrayList();
// private ObservableList<ScatterChart.Data> mOldestPoints = FXCollections.observableArrayList();
private int mDataPointer;
private int mHistory;
/**
* Symbol chart for plotting a series of constellation symbols over time. Maintains three history sets for
* current, previous, and oldest symbols.
*
* @param history size for each of the history sets
*/
public SymbolChart(int history)
{
super(new NumberAxis("I", -1.1, 1.1, 0.2),
new NumberAxis("Q", -1.1, 1.1, 0.2));
mHistory = history;
for(int x = 0; x < mHistory; x++)
{
mCurrentPoints.add(new XYChart.Data<>(0.0, 0.0));
// mPreviousPoints.add(new XYChart.Data<>(0.0, 0.0));
// mOldestPoints.add(new XYChart.Data<>(0.0, 0.0));
}
ObservableList<XYChart.Series> data = FXCollections.observableArrayList();
data.add(new ScatterChart.Series("Constellation", mCurrentPoints));
// data.add(new ScatterChart.Series("Previous", mPreviousPoints));
// data.add(new ScatterChart.Series("Oldest", mOldestPoints));
this.setData(data);
}
@Override
public void receive(Complex complex)
{
//Transfer previous to oldest
// XYChart.Data oldest = mOldestPoints.get(mDataPointer);
// XYChart.Data previous = mPreviousPoints.get(mDataPointer);
XYChart.Data current = mCurrentPoints.get(mDataPointer);
// oldest.setXValue(previous.getXValue());
// oldest.setYValue(previous.getYValue());
//
// previous.setXValue(current.getXValue());
// previous.setYValue(current.getYValue());
current.setXValue(complex.inphase());
current.setYValue(complex.quadrature());
mDataPointer++;
if(mDataPointer >= mHistory)
{
mDataPointer = 0;
}
}
}

View File

@@ -0,0 +1,62 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.decoder;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
public class DecoderPane extends VBox implements Listener<ComplexBuffer>
{
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
private DecoderType mDecoderType;
public DecoderPane(DecoderType decoderType)
{
mDecoderType = decoderType;
}
/**
* Package private - used by factory class to create an empty pane
*/
DecoderPane()
{
getChildren().add(new Text("Please select a decoder from the menu"));
}
@Override
public void receive(ComplexBuffer complexBuffer)
{
mComplexBufferBroadcaster.broadcast(complexBuffer);
}
public void addListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.addListener(listener);
}
public void removeListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.removeListener(listener);
}
public void setSampleRate(double sampleRate)
{
}
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.decoder;
import io.github.dsheirer.module.decode.DecoderType;
import java.util.EnumSet;
public class DecoderPaneFactory
{
private static final EnumSet<DecoderType> SUPPORTED_DECODER_TYPES = EnumSet.of(DecoderType.P25_PHASE1);
/**
* Creates a decoder pane for the decoder type
*/
public static DecoderPane getDecoderPane(DecoderType decoderType)
{
switch(decoderType)
{
case P25_PHASE1:
return new P25Phase1Pane();
}
return getDefaultPane();
}
/**
* Creates an empty decoder pane
*/
public static DecoderPane getDefaultPane()
{
return new DecoderPane();
}
/**
* Indicates if the decoder type is supported by this factory
*/
public static boolean isSupported(DecoderType decoderType)
{
return SUPPORTED_DECODER_TYPES.contains(decoderType);
}
}

View File

@@ -0,0 +1,195 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.instrument.gui.viewer.decoder;
import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.DoubleLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart;
import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart;
import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
import io.github.dsheirer.message.Message;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.p25.P25_C4FMDecoderInstrumented;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class P25Phase1Pane extends DecoderPane
{
private final static Logger mLog = LoggerFactory.getLogger(P25Phase1Pane.class);
private HBox mSampleChartBox;
private ComplexSampleLineChart mSampleLineChart;
private EyeDiagramChart mEyeDiagramChart;
private HBox mDecoderChartBox;
private SymbolChart mSymbolChart;
private PhaseLineChart mPLLPhaseErrorLineChart;
private DoubleLineChart mPLLFrequencyLineChart;
private DoubleLineChart mSamplesPerSymbolLineChart;
private Broadcaster<ComplexBuffer> mFilteredBufferBroadcaster = new Broadcaster<>();
private P25_C4FMDecoderInstrumented mDecoder = new P25_C4FMDecoderInstrumented(null);
public P25Phase1Pane()
{
super(DecoderType.P25_PHASE1);
init();
}
private void init()
{
addListener(getDecoder());
getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster);
getDecoder().setSymbolListener(getSymbolChart());
getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart());
getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart());
getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart());
mFilteredBufferBroadcaster.addListener(getSampleLineChart());
HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
mDecoder.setMessageListener(new Listener<Message>()
{
@Override
public void receive(Message message)
{
mLog.debug(message.getMessage());
}
});
}
@Override
public void setSampleRate(double sampleRate)
{
mLog.debug("Configuring for sample rate: " + sampleRate);
mDecoder.setSampleRate(sampleRate);
double samplesPerSymbol = sampleRate / 4800.0;
getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol);
}
private P25_C4FMDecoderInstrumented getDecoder()
{
return mDecoder;
}
private SymbolChart getSymbolChart()
{
if(mSymbolChart == null)
{
mSymbolChart = new SymbolChart(10);
}
return mSymbolChart;
}
private HBox getDecoderChartBox()
{
if(mDecoderChartBox == null)
{
mDecoderChartBox = new HBox();
mDecoderChartBox.setMaxHeight(Double.MAX_VALUE);
getSymbolChart().setMaxWidth(Double.MAX_VALUE);
getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE);
getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(),
getPLLFrequencyLineChart());
}
return mDecoderChartBox;
}
private HBox getSampleChartBox()
{
if(mSampleChartBox == null)
{
mSampleChartBox = new HBox();
mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS);
HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS);
mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(),
getSamplesPerSymbolLineChart());
}
return mSampleChartBox;
}
private ComplexSampleLineChart getSampleLineChart()
{
if(mSampleLineChart == null)
{
mSampleLineChart = new ComplexSampleLineChart(100, 10);
}
return mSampleLineChart;
}
private EyeDiagramChart getEyeDiagramChart()
{
if(mEyeDiagramChart == null)
{
mEyeDiagramChart = new EyeDiagramChart(10);
}
return mEyeDiagramChart;
}
private PhaseLineChart getPLLPhaseErrorLineChart()
{
if(mPLLPhaseErrorLineChart == null)
{
mPLLPhaseErrorLineChart = new PhaseLineChart(40);
}
return mPLLPhaseErrorLineChart;
}
private DoubleLineChart getPLLFrequencyLineChart()
{
if(mPLLFrequencyLineChart == null)
{
mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40);
}
return mPLLFrequencyLineChart;
}
private DoubleLineChart getSamplesPerSymbolLineChart()
{
if(mSamplesPerSymbolLineChart == null)
{
mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40);
}
return mSamplesPerSymbolLineChart;
}
}

View File

@@ -22,39 +22,39 @@ import io.github.dsheirer.message.Message;
import io.github.dsheirer.module.Module; import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
public abstract class Decoder extends Module public abstract class Decoder extends Module implements IMessageProvider
implements IMessageProvider, Listener<Message>
{ {
/* This has to be a broadcaster in order for references to persist */ /* This has to be a broadcaster in order for references to persist */
protected Listener<Message> mMessageListener; private Listener<Message> mMessageDistributor = new MessageDistributor();
protected Listener<Message> mMessageListener;
/** /**
* Decoder - parent class for all decoders, demodulators and components. * Decoder - parent class for all decoders, demodulators and components.
*/ */
public Decoder() public Decoder()
{ {
} }
@Override @Override
public void dispose() public void dispose()
{ {
mMessageListener = null; mMessageListener = null;
} }
/** /**
* Identifies the decoder type (ie protocol) * Identifies the decoder type (ie protocol)
*/ */
public abstract DecoderType getDecoderType(); public abstract DecoderType getDecoderType();
/** /**
* Adds a listener for receiving decoded messages from this decoder * Adds a listener for receiving decoded messages from this decoder
*/ */
@Override @Override
public void setMessageListener( Listener<Message> listener ) public void setMessageListener(Listener<Message> listener)
{ {
mMessageListener = listener; mMessageListener = listener;
} }
/** /**
* Removes the listener from receiving decoded messages from all attached * Removes the listener from receiving decoded messages from all attached
@@ -63,23 +63,29 @@ public abstract class Decoder extends Module
@Override @Override
public void removeMessageListener() public void removeMessageListener()
{ {
mMessageListener = null; mMessageListener = null;
} }
/** /**
* Broadcasts the message to the registered message listener * Message listener for receiving the output from this decoder
*/ */
public void broadcast( Message message ) protected Listener<Message> getMessageListener()
{ {
if( mMessageListener != null ) return mMessageDistributor;
{
mMessageListener.receive( message );
}
} }
@Override /**
public void receive( Message message ) * Distributes/forwards messages from sub-class decoder implementations to the registered message listener.
{ */
broadcast( message ); public class MessageDistributor implements Listener<Message>
} {
@Override
public void receive(Message message)
{
if(mMessageListener != null)
{
mMessageListener.receive(message);
}
}
}
} }

View File

@@ -246,9 +246,7 @@ public class DecoderFactory
switch(modulation) switch(modulation)
{ {
case C4FM: case C4FM:
modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER)); modules.add(new P25_C4FMDecoder(aliasList));
modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f));
modules.add(new P25_C4FMDecoder(aliasList, decodeConfig.getAFCMaximumCorrection()));
modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM, modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM,
p25Config.getIgnoreDataCalls())); p25Config.getIgnoreDataCalls()));
break; break;

View File

@@ -90,7 +90,7 @@ public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferLis
mMessageProcessor = new Fleetsync2MessageProcessor( aliasList ); mMessageProcessor = new Fleetsync2MessageProcessor( aliasList );
mMessageFramer.addMessageListener( mMessageProcessor ); mMessageFramer.addMessageListener( mMessageProcessor );
mMessageProcessor.setMessageListener( this ); mMessageProcessor.setMessageListener( getMessageListener() );
} }
public void dispose() public void dispose()

View File

@@ -100,7 +100,7 @@ public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListene
mMessageProcessor = new LJ1200MessageProcessor( aliasList ); mMessageProcessor = new LJ1200MessageProcessor( aliasList );
mTowerMessageFramer.addMessageListener( mMessageProcessor ); mTowerMessageFramer.addMessageListener( mMessageProcessor );
mTransponderMessageFramer.addMessageListener( mMessageProcessor ); mTransponderMessageFramer.addMessageListener( mMessageProcessor );
mMessageProcessor.setMessageListener( this ); mMessageProcessor.setMessageListener( getMessageListener() );
} }
@Override @Override

View File

@@ -74,7 +74,7 @@ public class LTRNetDecoder extends Decoder
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
mLTRMessageProcessor.setMessageListener( this ); mLTRMessageProcessor.setMessageListener( getMessageListener() );
} }
@Override @Override

View File

@@ -64,7 +64,7 @@ public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBuffer
mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList ); mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList );
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor ); mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
mLTRMessageProcessor.setMessageListener( this ); mLTRMessageProcessor.setMessageListener( getMessageListener() );
} }
@Override @Override

View File

@@ -102,7 +102,7 @@ public class MDCDecoder extends Decoder implements IFilteredRealBufferListener,
/* Message Processor */ /* Message Processor */
mMessageProcessor = new MDCMessageProcessor( aliasList ); mMessageProcessor = new MDCMessageProcessor( aliasList );
mMessageFramer.addMessageListener( mMessageProcessor ); mMessageFramer.addMessageListener( mMessageProcessor );
mMessageProcessor.addMessageListener( this ); mMessageProcessor.addMessageListener(getMessageListener());
} }
public void dispose() public void dispose()

View File

@@ -148,7 +148,7 @@ public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListen
/* Fully decoded and framed messages processor */ /* Fully decoded and framed messages processor */
mMessageProcessor = new MPT1327MessageProcessor(aliasList); mMessageProcessor = new MPT1327MessageProcessor(aliasList);
mMessageProcessor.setMessageListener(this); mMessageProcessor.setMessageListener(getMessageListener());
mControlMessageFramer.addMessageListener(mMessageProcessor); mControlMessageFramer.addMessageListener(mMessageProcessor);
mTrafficMessageFramer.addMessageListener(mMessageProcessor); mTrafficMessageFramer.addMessageListener(mMessageProcessor);
} }

View File

@@ -1,70 +1,91 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.module.decode.p25; package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.instrument.Instrumentable;
import io.github.dsheirer.module.decode.Decoder; import io.github.dsheirer.module.decode.Decoder;
import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener; import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider; import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider;
public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider
{ {
private P25MessageProcessor mMessageProcessor; private P25MessageProcessor mMessageProcessor;
private AliasList mAliasList;
public P25Decoder( AliasList aliasList ) public P25Decoder(AliasList aliasList)
{
mMessageProcessor = new P25MessageProcessor( aliasList );
mMessageProcessor.setMessageListener( this );
}
public void dispose()
{
super.dispose();
mMessageProcessor.dispose();
mMessageProcessor = null;
}
@Override
public DecoderType getDecoderType()
{ {
return DecoderType.P25_PHASE1; mAliasList = aliasList;
mMessageProcessor = new P25MessageProcessor(mAliasList);
mMessageProcessor.setMessageListener(getMessageListener());
} }
protected P25MessageProcessor getMessageProcessor() protected AliasList getAliasList()
{ {
return mMessageProcessor; return mAliasList;
} }
public abstract Modulation getModulation(); public void dispose()
{
super.dispose();
public enum Modulation mMessageProcessor.dispose();
{ mMessageProcessor = null;
CQPSK( "Simulcast (LSM)", "LSM" ), }
C4FM( "Normal (C4FM)", "C4FM" );
private String mLabel; @Override
private String mShortLabel; public DecoderType getDecoderType()
{
return DecoderType.P25_PHASE1;
}
private Modulation( String label, String shortLabel ) protected P25MessageProcessor getMessageProcessor()
{ {
mLabel = label; return mMessageProcessor;
mShortLabel = shortLabel; }
}
public String getLabel() public abstract Modulation getModulation();
{
return mLabel;
}
public String getShortLabel() public enum Modulation
{ {
return mShortLabel; CQPSK("Simulcast (LSM)", "LSM"),
} C4FM("Normal (C4FM)", "C4FM");
public String toString() private String mLabel;
{ private String mShortLabel;
return getLabel();
} private Modulation(String label, String shortLabel)
} {
mLabel = label;
mShortLabel = shortLabel;
}
public String getLabel()
{
return mLabel;
}
public String getShortLabel()
{
return mShortLabel;
}
public String toString()
{
return getLabel();
}
}
} }

View File

@@ -1,244 +1,204 @@
/******************************************************************************* /*******************************************************************************
* sdrtrunk * sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer * Copyright (C) 2014-2018 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* it under the terms of the GNU General Public License as published by * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* the Free Software Foundation, either version 3 of the License, or * later version.
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* but WITHOUT ANY WARRANTY; without even the implied warranty of * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* 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 * You should have received a copy of the GNU General Public License along with this program.
* along with this program. If not, see <http://www.gnu.org/licenses/> * If not, see <http://www.gnu.org/licenses/>
* *
******************************************************************************/ ******************************************************************************/
package io.github.dsheirer.module.decode.p25; package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.instrument.tap.Tap; import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.instrument.tap.TapGroup; import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.instrument.tap.stream.DibitTap; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.instrument.tap.stream.FloatTap; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.DQPSKDemodulator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.IFilteredRealBufferListener; import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.real.RealBuffer; import io.github.dsheirer.sample.complex.IComplexBufferListener;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.HashMap;
import java.util.List; import java.util.Map;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener public class P25_C4FMDecoder extends P25Decoder implements IComplexBufferListener, Listener<ComplexBuffer>
{ {
private final static Logger mLog = LoggerFactory.getLogger( P25_C4FMDecoder.class ); private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder.class);
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
protected static final double SYMBOL_RATE = 4800;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected DQPSKDemodulator mQPSKDemodulator;
protected CostasLoop mCostasLoop;
protected P25MessageFramer mMessageFramer;
protected double mSampleRate;
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
private ComplexFIRFilter mBasebandFilter;
/* Instrumentation Taps */ public P25_C4FMDecoder(AliasList aliasList)
private static final String INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT = "Tap Point: Symbol Filter Output";
private static final String INSTRUMENT_C4FM_SLICER_OUTPUT = "Tap Point: C4FM Slicer Output";
private List<TapGroup> mAvailableTaps;
private C4FMSymbolFilter mSymbolFilter;
private C4FMSlicer mC4FMSlicer;
private P25MessageFramer mMessageFramer;
/**
* P25 Phase 1 C4FM Decoder processes real buffers of un-filtered,
* demodulated audio and produces decoded P25 Phase 1 Messages.
*
* Provides Frequency Control to steer external tuner source and incorporates
* internal gain control.
*
* Note: use the P25AudioModule to convert the decoded messages into audio.
*
* @param aliasList - optional (can be null) list of alias values for network
* infrastructure and subscriber identities that will be included in each
* decoded message
*/
public P25_C4FMDecoder(AliasList aliasList, int frequencyCorrectionMaximum )
{
super( aliasList );
/* Shape gain and frequency offsets to optimize sample stream */
mSymbolFilter = new C4FMSymbolFilter( frequencyCorrectionMaximum );
/* Convert samples to symbols */
mC4FMSlicer = new C4FMSlicer();
mSymbolFilter.setListener( mC4FMSlicer );
/* Sync pattern detection and message construction */
mMessageFramer = new P25MessageFramer( aliasList );
mC4FMSlicer.addListener( mMessageFramer );
/* Process and broadcast messages */
mMessageFramer.setListener( getMessageProcessor() );
}
@Override
public void dispose()
{
super.dispose();
mSymbolFilter.dispose();
mSymbolFilter = null;
mMessageFramer.dispose();
mMessageFramer = null;
mC4FMSlicer.dispose();
mC4FMSlicer = null;
}
public Modulation getModulation()
{
return Modulation.C4FM;
}
@Override
public Listener<RealBuffer> getFilteredRealBufferListener()
{
return mSymbolFilter;
}
/**
* Instrumentation taps for monitoring internal processing
*/
@Override
public List<TapGroup> getTapGroups()
{ {
if( mAvailableTaps == null ) super(aliasList);
{ setSampleRate(48000.0);
mAvailableTaps = new ArrayList<>();
TapGroup group = new TapGroup( "P25 C4FM Decoder" );
group.add( new FloatTap( INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT, 0, 0.1f ) );
group.add( new DibitTap( INSTRUMENT_C4FM_SLICER_OUTPUT, 0, 0.1f ) );
mAvailableTaps.add( group );
if( mSymbolFilter != null )
{
mAvailableTaps.addAll( mSymbolFilter.getTapGroups() );
}
}
return mAvailableTaps;
} }
/** public void setSampleRate(double sampleRate)
* Adds an instrumentation tap to monitor internal processing.
*/
@Override
public void registerTap( Tap tap )
{ {
if( mSymbolFilter != null ) if(sampleRate <= SYMBOL_RATE * 2)
{ {
mSymbolFilter.registerTap( tap ); throw new IllegalArgumentException("Sample rate must be at least twice the symbol rate [4800]");
} }
switch( tap.getName() ) mSampleRate = sampleRate;
{ mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
FloatTap symbolTap = (FloatTap)tap;
mSymbolFilter.setListener( symbolTap );
symbolTap.setListener( mC4FMSlicer );
break;
case INSTRUMENT_C4FM_SLICER_OUTPUT:
DibitTap slicerTap = (DibitTap)tap;
if( mC4FMSlicer != null ) mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
{
mC4FMSlicer.addListener( slicerTap ); mInterpolatingSampleBuffer = new InterpolatingSampleBuffer((float)(sampleRate / SYMBOL_RATE));
} mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
break;
default: //Message framer can trigger a symbol-inversion correction to the PLL when detected
break; mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
} mMessageFramer.setListener(getMessageProcessor());
mQPSKDemodulator.setDibitListener(mMessageFramer);
} }
/** @Override
* Removes the instrumentation tap. public void receive(ComplexBuffer complexBuffer)
*/
@Override
public void unregisterTap( Tap tap )
{ {
if( mSymbolFilter != null ) ComplexBuffer basebandFiltered = filter(complexBuffer);
{
mSymbolFilter.unregisterTap( tap );
}
switch( tap.getName() ) ComplexBuffer gainApplied = mAGC.filter(basebandFiltered);
{
case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
mSymbolFilter.setListener( mC4FMSlicer );
break;
case INSTRUMENT_C4FM_SLICER_OUTPUT:
DibitTap slicerTap = (DibitTap)tap;
if( mC4FMSlicer != null ) mQPSKDemodulator.receive(gainApplied);
{
mC4FMSlicer.removeListener( slicerTap );
}
break;
default:
throw new IllegalArgumentException( "Unrecognized tap: " +
tap.getName() );
}
} }
@Override protected ComplexBuffer filter(ComplexBuffer complexBuffer)
public void reset() {
{ return mBasebandFilter.filter(complexBuffer);
// TODO Auto-generated method stub }
} private double getSampleRate()
{
return mSampleRate;
}
@Override private float[] getBasebandFilter()
public void start( ScheduledExecutorService executor ) {
{ float[] filter = mBasebandFilters.get(getSampleRate());
// TODO Auto-generated method stub
} if(filter == null)
{
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate((int)getSampleRate())
.passBandCutoff(5100)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandAmplitude(0.0)
.stopBandStart(6500)
.stopBandRipple(0.01)
.build();
@Override try
public void stop() {
{ filter = FilterFactory.getTaps(specification);
// TODO Auto-generated method stub
} }
catch(FilterDesignException fde)
{
mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate());
}
if(filter != null)
{
mBasebandFilters.put(getSampleRate(), filter);
}
else
{
throw new IllegalStateException("Couldn't design a C4FM symbol filter for sample rate: " + mSampleRate);
}
}
@Override return filter;
public void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener ) }
{
if( mSymbolFilter != null )
{
mSymbolFilter.setFrequencyChangeListener( listener );
}
}
@Override public void dispose()
public void removeFrequencyChangeListener() {
{ super.dispose();
if( mSymbolFilter != null )
{
mSymbolFilter.setFrequencyChangeListener( null );
}
}
@Override mBasebandFilter.dispose();
public Listener<FrequencyChangeEvent> getFrequencyChangeListener() mBasebandFilter = null;
{
if( mSymbolFilter != null )
{
return mSymbolFilter.getFrequencyChangeListener();
}
return null; mMessageFramer.dispose();
} mMessageFramer = null;
}
@Override
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
{
}
@Override
public void removeFrequencyChangeListener()
{
}
@Override
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
{
return new Listener<FrequencyChangeEvent>()
{
@Override
public void receive(FrequencyChangeEvent frequencyChangeEvent)
{
switch(frequencyChangeEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
case NOTIFICATION_SAMPLE_RATE_CHANGE:
mCostasLoop.reset();
break;
}
}
};
}
@Override
public Listener<ComplexBuffer> getComplexBufferListener()
{
return P25_C4FMDecoder.this;
}
public Modulation getModulation()
{
return Modulation.C4FM;
}
@Override
public void reset()
{
mCostasLoop.reset();
}
@Override
public void start(ScheduledExecutorService executor)
{
}
@Override
public void stop()
{
}
} }

View File

@@ -0,0 +1,109 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.dsp.psk.DQPSKDemodulatorInstrumented;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
import io.github.dsheirer.dsp.psk.SymbolDecisionData;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class P25_C4FMDecoderInstrumented extends P25_C4FMDecoder
{
private Listener<Double> mPLLPhaseErrorListener;
private Listener<Double> mPLLFrequencyListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mSymbolListener;
private Listener<ComplexBuffer> mFilteredSymbolListener;
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
public P25_C4FMDecoderInstrumented(AliasList aliasList)
{
super(aliasList);
}
/**
* Overrides the filter method so that we can capture the filtered samples for instrumentation
*/
protected ComplexBuffer filter(ComplexBuffer complexBuffer)
{
ComplexBuffer filtered = super.filter(complexBuffer);
if(mFilteredSymbolListener != null)
{
mFilteredSymbolListener.receive(filtered);
}
return filtered;
}
/**
* Overrides this method so we can correctly configure for instrumented operations
*/
public void setSampleRate(double sampleRate)
{
super.setSampleRate(sampleRate);
InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
mInterpolatingSampleBuffer = instrumentedBuffer;
DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, mSampleRate);
mQPSKDemodulator = instrumented;
instrumented.setSymbolListener(mSymbolListener);
instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
instrumented.setDibitListener(mMessageFramer);
}
public void setSymbolListener(Listener<Complex> listener)
{
mSymbolListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolListener(listener);
}
public void setPLLPhaseErrorListener(Listener<Double> listener)
{
mPLLPhaseErrorListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
}
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
}
public void setFilteredBufferListener(Listener<ComplexBuffer> listener)
{
mFilteredSymbolListener = listener;
}
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
}
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
}
}

View File

@@ -23,13 +23,8 @@ import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.LSMDemodulator; import io.github.dsheirer.dsp.psk.LSMDemodulator;
import io.github.dsheirer.dsp.psk.QPSKPolarSlicer; import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder;
import io.github.dsheirer.instrument.tap.Tap;
import io.github.dsheirer.instrument.tap.TapGroup; import io.github.dsheirer.instrument.tap.TapGroup;
import io.github.dsheirer.instrument.tap.stream.ComplexSampleTap;
import io.github.dsheirer.instrument.tap.stream.ComplexTap;
import io.github.dsheirer.instrument.tap.stream.DibitTap;
import io.github.dsheirer.instrument.tap.stream.QPSKTap;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter;
@@ -38,73 +33,72 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
{ {
private final static Logger mLog = LoggerFactory.getLogger( P25_LSMDecoder.class ); private final static Logger mLog = LoggerFactory.getLogger(P25_LSMDecoder.class);
/* Instrumentation Taps */ /* Instrumentation Taps */
private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output";
private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output";
private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output"; private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output";
private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output"; private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output";
private List<TapGroup> mAvailableTaps; private List<TapGroup> mAvailableTaps;
private ComplexFIRFilter_CB_CB mBasebandFilter; private ComplexFIRFilter_CB_CB mBasebandFilter;
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
private ComplexFeedForwardGainControl mAGC = private ComplexFeedForwardGainControl mAGC =
new ComplexFeedForwardGainControl( 32 ); new ComplexFeedForwardGainControl(32);
private LSMDemodulator mLSMDemodulator = new LSMDemodulator(); private LSMDemodulator mLSMDemodulator = new LSMDemodulator();
private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer(); private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder();
private P25MessageFramer mMessageFramer; private P25MessageFramer mMessageFramer;
public P25_LSMDecoder( AliasList aliasList ) public P25_LSMDecoder(AliasList aliasList)
{ {
super( aliasList ); super(aliasList);
mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( mBasebandFilter = new ComplexFIRFilter_CB_CB(FilterFactory.getLowPass(
48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f ); 48000, 7250, 8000, 60, WindowType.HANNING, true), 1.0f);
mBasebandFilter.setListener( mStreamConverter ); mBasebandFilter.setListener(mStreamConverter);
mStreamConverter.setListener( mAGC ); mStreamConverter.setListener(mAGC);
mAGC.setListener( mLSMDemodulator ); mAGC.setListener(mLSMDemodulator);
mLSMDemodulator.setSymbolListener( mQPSKSlicer ); mLSMDemodulator.setSymbolListener(mQPSKSlicer);
mMessageFramer = new P25MessageFramer( aliasList, mLSMDemodulator ); mMessageFramer = new P25MessageFramer(aliasList, mLSMDemodulator.getCostasLoop());
mQPSKSlicer.addListener( mMessageFramer ); mQPSKSlicer.addListener(mMessageFramer);
mMessageFramer.setListener( getMessageProcessor() ); mMessageFramer.setListener(getMessageProcessor());
} }
public void dispose() public void dispose()
{ {
super.dispose(); super.dispose();
mBasebandFilter.dispose(); mBasebandFilter.dispose();
mBasebandFilter = null; mBasebandFilter = null;
mStreamConverter.dispose(); mStreamConverter.dispose();
mStreamConverter = null; mStreamConverter = null;
mAGC.dispose(); mAGC.dispose();
mAGC = null; mAGC = null;
mLSMDemodulator.dispose(); mLSMDemodulator.dispose();
mLSMDemodulator = null; mLSMDemodulator = null;
mQPSKSlicer.dispose(); mQPSKSlicer.dispose();
mQPSKSlicer = null; mQPSKSlicer = null;
mMessageFramer.dispose(); mMessageFramer.dispose();
mMessageFramer = null; mMessageFramer = null;
} }
@Override @Override
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener) public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
@@ -130,130 +124,34 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
} }
@Override @Override
public Listener<ComplexBuffer> getComplexBufferListener() public Listener<ComplexBuffer> getComplexBufferListener()
{
return mBasebandFilter;
}
public Modulation getModulation()
{
return Modulation.CQPSK;
}
/**
* Provides a list of instrumentation taps for monitoring internal processing
*/
@Override
public List<TapGroup> getTapGroups()
{ {
if( mAvailableTaps == null ) return mBasebandFilter;
{
mAvailableTaps = new ArrayList<>();
TapGroup group = new TapGroup( "P25 LSM Decoder" );
group.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) );
group.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) );
group.add( new QPSKTap( INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f ) );
group.add( new DibitTap( INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f ) );
mAvailableTaps.add( group );
if( mLSMDemodulator != null )
{
mAvailableTaps.addAll( mLSMDemodulator.getTapGroups() );
}
}
return mAvailableTaps;
} }
/** public Modulation getModulation()
* Adds the instrumentation tap
*/
@Override
public void registerTap( Tap tap )
{ {
if( mLSMDemodulator != null ) return Modulation.CQPSK;
{
mLSMDemodulator.registerTap( tap );
}
switch( tap.getName() )
{
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
ComplexSampleTap baseband = (ComplexSampleTap)tap;
mStreamConverter.setListener( baseband );
baseband.setListener( mAGC );
break;
case INSTRUMENT_AGC_OUTPUT:
ComplexSampleTap agcSymbol = (ComplexSampleTap)tap;
mAGC.setListener( agcSymbol );
agcSymbol.setListener( mLSMDemodulator );
break;
case INSTRUMENT_LSM_DEMODULATOR_OUTPUT:
QPSKTap qpsk = (QPSKTap)tap;
mLSMDemodulator.setSymbolListener( qpsk );
qpsk.setListener( mQPSKSlicer );
break;
case INSTRUMENT_QPSK_SLICER_OUTPUT:
mQPSKSlicer.addListener( (DibitTap)tap );
break;
default:
throw new IllegalArgumentException( "Unrecognized tap: " +
tap.getName() );
}
} }
/** @Override
* Removes the instrumentation tap public void reset()
*/
@Override
public void unregisterTap( Tap tap )
{ {
if( mLSMDemodulator != null ) // TODO Auto-generated method stub
{
mLSMDemodulator.unregisterTap( tap );
}
switch( tap.getName() )
{
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
mStreamConverter.setListener( mAGC );
break;
case INSTRUMENT_AGC_OUTPUT:
mAGC.setListener( mLSMDemodulator );
break;
case INSTRUMENT_LSM_DEMODULATOR_OUTPUT:
mLSMDemodulator.setSymbolListener( mQPSKSlicer );
break;
case INSTRUMENT_QPSK_SLICER_OUTPUT:
mQPSKSlicer.removeListener( (DibitTap)tap );
break;
default:
throw new IllegalArgumentException( "Unrecognized tap: " +
tap.getName() );
}
} }
@Override @Override
public void reset() public void start(ScheduledExecutorService executor)
{ {
// TODO Auto-generated method stub // TODO Auto-generated method stub
} }
@Override @Override
public void start( ScheduledExecutorService executor ) public void stop()
{ {
// TODO Auto-generated method stub // TODO Auto-generated method stub
} }
@Override
public void stop()
{
// TODO Auto-generated method stub
}
} }

View File

@@ -78,7 +78,7 @@ public class SyncBroadcast extends TSBKMessage
if( isMicroslotsLockedToMinuteRollover() ) if( isMicroslotsLockedToMinuteRollover() )
{ {
sb.append( " MICROSLOT-MINUTE ROLLOVER:LOCKED" ); sb.append( " MICROSLOT-MINUTE ROLLOVER:SLOW" );
} }
else else
{ {

View File

@@ -63,7 +63,7 @@ public class PassportDecoder extends Decoder
mPassportMessageProcessor = new PassportMessageProcessor( aliasList ); mPassportMessageProcessor = new PassportMessageProcessor( aliasList );
mPassportMessageFramer.addMessageListener( mPassportMessageProcessor ); mPassportMessageFramer.addMessageListener( mPassportMessageProcessor );
mPassportMessageProcessor.setMessageListener( this ); mPassportMessageProcessor.setMessageListener( getMessageListener() );
} }
@Override @Override

View File

@@ -104,8 +104,8 @@ public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListe
mMessageFramerGPS.addMessageListener( mMessageAProcessor ); mMessageFramerGPS.addMessageListener( mMessageAProcessor );
mMessageFramerANI.addMessageListener( mMessageBProcessor ); mMessageFramerANI.addMessageListener( mMessageBProcessor );
mMessageAProcessor.setMessageListener( this ); mMessageAProcessor.setMessageListener(getMessageListener());
mMessageBProcessor.setMessageListener( this ); mMessageBProcessor.setMessageListener(getMessageListener());
} }
public void dispose() public void dispose()

View File

@@ -42,6 +42,7 @@ public class RecorderManager implements Listener<AudioPacket>
private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class); private static final Logger mLog = LoggerFactory.getLogger(RecorderManager.class);
public static final int AUDIO_SAMPLE_RATE = 8000; public static final int AUDIO_SAMPLE_RATE = 8000;
public static final int BASEBAND_SAMPLE_RATE = 48000;
public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds public static final long IDLE_RECORDER_REMOVAL_THRESHOLD = 6000; //6 seconds
private Map<String,AudioPacketWaveRecorder> mRecorders = new HashMap<>(); private Map<String,AudioPacketWaveRecorder> mRecorders = new HashMap<>();
@@ -92,6 +93,7 @@ public class RecorderManager implements Listener<AudioPacket>
/** /**
* Primary ingest point for audio packets from all decoding channels * Primary ingest point for audio packets from all decoding channels
*
* @param audioPacket to process * @param audioPacket to process
*/ */
@Override @Override
@@ -114,7 +116,7 @@ public class RecorderManager implements Listener<AudioPacket>
while(!audioPackets.isEmpty()) while(!audioPackets.isEmpty())
{ {
for(AudioPacket audioPacket: audioPackets) for(AudioPacket audioPacket : audioPackets)
{ {
String identifier = audioPacket.getMetadata().getUniqueIdentifier(); String identifier = audioPacket.getMetadata().getUniqueIdentifier();
@@ -229,7 +231,7 @@ public class RecorderManager implements Listener<AudioPacket>
sb.append(SystemProperties.getInstance().getApplicationFolder("recordings")); sb.append(SystemProperties.getInstance().getApplicationFolder("recordings"));
sb.append(File.separator).append(channelName).append("_baseband"); sb.append(File.separator).append(channelName).append("_baseband");
return new ComplexBufferWaveRecorder(AUDIO_SAMPLE_RATE, sb.toString()); return new ComplexBufferWaveRecorder(BASEBAND_SAMPLE_RATE, sb.toString());
} }
/** /**

View File

@@ -104,7 +104,7 @@ public class AudioPacketWaveRecorder extends Module implements Listener<AudioPac
sb.append(mFilePrefix); sb.append(mFilePrefix);
sb.append("_"); sb.append("_");
sb.append(TimeStamp.getLongTimeStamp("_")); sb.append(TimeStamp.getLongTimeStamp("_"));
sb.append(".wav"); sb.append(".tmp");
mFile = Paths.get(sb.toString()); mFile = Paths.get(sb.toString());

View File

@@ -45,7 +45,7 @@ public class WaveWriter implements AutoCloseable
public static final String DATA_CHUNK_ID = "data"; public static final String DATA_CHUNK_ID = "data";
private static final Pattern FILENAME_PATTERN = Pattern.compile("(.*_)(\\d+)(\\.wav)"); private static final Pattern FILENAME_PATTERN = Pattern.compile("(.*_)(\\d+)(\\.tmp)");
public static final long MAX_WAVE_SIZE = 2l * (long)Integer.MAX_VALUE; public static final long MAX_WAVE_SIZE = 2l * (long)Integer.MAX_VALUE;
private AudioFormat mAudioFormat; private AudioFormat mAudioFormat;
@@ -112,7 +112,7 @@ public class WaveWriter implements AutoCloseable
while(Files.exists(mFile)) while(Files.exists(mFile))
{ {
mFile = Paths.get(mFile.toFile().getAbsolutePath().replace(".wav", "_" + version + ".wav")); mFile = Paths.get(mFile.toFile().getAbsolutePath().replace(".tmp", "_" + version + ".tmp"));
version++; version++;
} }
@@ -134,6 +134,35 @@ public class WaveWriter implements AutoCloseable
mFileChannel.force(true); mFileChannel.force(true);
mFileChannel.close(); mFileChannel.close();
mFileChannel = null; mFileChannel = null;
rename();
}
/**
* Renames the file from *.tmp to *.wav after file has been closed.
* @throws IOException
*/
private void rename() throws IOException
{
if(mFile != null && Files.exists(mFile))
{
String renamed = mFile.getFileName().toString();
renamed = renamed.replace(".tmp", ".wav");
Path renamedPath = mFile.resolveSibling(renamed);
int suffix = 1;
while(Files.exists(renamedPath))
{
String renamedWithSuffix = mFile.getFileName().toString();
renamedWithSuffix = renamedWithSuffix.replace(".tmp", "_" + suffix + ".wav");
Path renamedWithSuffixPath = mFile.resolveSibling(renamedWithSuffix);
renamedPath = mFile.resolveSibling(renamedWithSuffixPath);
suffix++;
}
Files.move(mFile, renamedPath);
}
} }
@Override @Override
@@ -379,7 +408,7 @@ public class WaveWriter implements AutoCloseable
if(mFileRolloverCounter == 2) if(mFileRolloverCounter == 2)
{ {
filename = filename.replace(".wav", "_2.wav"); filename = filename.replace(".tmp", "_2.tmp");
} }
else else
{ {

View File

@@ -21,14 +21,13 @@ public class ConversionUtils
*/ */
public static float[] convertFromSigned16BitSamples( ByteBuffer buffer ) public static float[] convertFromSigned16BitSamples( ByteBuffer buffer )
{ {
ShortBuffer byteBuffer = ShortBuffer byteBuffer = buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer();
buffer.order( ByteOrder.LITTLE_ENDIAN ).asShortBuffer();
float[] samples = new float[ buffer.limit() / 2 ]; float[] samples = new float[ buffer.limit() / 2 ];
for( int x = 0; x < samples.length; x++ ) for( int x = 0; x < samples.length; x++ )
{ {
samples[ x ] = (float)byteBuffer.get() / 32767.0f; samples[ x ] = (float)byteBuffer.get() / (float)Short.MAX_VALUE;
} }
return samples; return samples;

View File

@@ -27,407 +27,424 @@ import java.io.Serializable;
*/ */
public class Complex implements Serializable public class Complex implements Serializable
{ {
private final static Logger mLog = private final static Logger mLog = LoggerFactory.getLogger(Complex.class);
LoggerFactory.getLogger( Complex.class );
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private float mLeft; private float mLeft;
private float mRight; private float mRight;
public Complex( float left, float right ) public Complex(float left, float right)
{ {
mLeft = left; mLeft = left;
mRight = right; mRight = right;
} }
public Complex() public Complex()
{ {
this( 0.0f, 0.0f ); this(0.0f, 0.0f);
} }
public void setInphase( float inphase ) public void setInphase(float inphase)
{ {
mLeft = inphase; mLeft = inphase;
} }
public void setQuadrature( float quadrature ) public void setQuadrature(float quadrature)
{ {
mRight = quadrature; mRight = quadrature;
} }
public void setValues( float inphase, float quadrature ) public void setValues(float inphase, float quadrature)
{ {
mLeft = inphase; mLeft = inphase;
mRight = quadrature; mRight = quadrature;
} }
public Complex copy() public void setValues(Complex sampleToCopy)
{ {
return new Complex( mLeft, mRight ); mLeft = sampleToCopy.left();
} mRight = sampleToCopy.right();
}
public String toString() public Complex copy()
{ {
StringBuilder sb = new StringBuilder(); return new Complex(mLeft, mRight);
}
sb.append( "I:" ); public String toString()
sb.append( mLeft ); {
sb.append( " Q:" ); StringBuilder sb = new StringBuilder();
sb.append( mRight );
return sb.toString(); sb.append("I:");
} sb.append(mLeft);
sb.append(" Q:");
sb.append(mRight);
/** return sb.toString();
* Returns a new sample representing the conjugate of this one }
*/
public Complex conjugate()
{
return new Complex( mLeft, -mRight );
}
/** /**
* Multiplies this sample by the scalor value * Returns a new sample representing the conjugate of this one
*/ */
public void multiply( float scalor ) public Complex conjugate()
{ {
mLeft *= scalor; return new Complex(mLeft, -mRight);
mRight *= scalor; }
}
public static Complex multiply( Complex sample, float scalor ) /**
{ * Multiplies this sample by the scalor value
return new Complex( sample.left() * scalor, */
sample.right() * scalor ); public void multiply(float scalor)
} {
mLeft *= scalor;
mRight *= scalor;
}
/** public static Complex multiply(Complex sample, float scalor)
* Calculates the inphase component of multiplying two complex numbers, A and B. {
*/ return new Complex(sample.left() * scalor,
public static float multiplyInphase( float inphaseA, float quadratureA, float inphaseB, float quadratureB ) sample.right() * scalor);
{ }
return ( inphaseA * inphaseB ) - ( quadratureA * quadratureB );
}
/** /**
* Calculates the quadrature component of multiplying two complex numbers, A and B. * Calculates the inphase component of multiplying two complex numbers, A and B.
*/ */
public static float multiplyQuadrature( float inphaseA, float quadratureA, float inphaseB, float quadratureB ) public static float multiplyInphase(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
{ {
return ( quadratureA * inphaseB ) + ( inphaseA * quadratureB ); return (inphaseA * inphaseB) - (quadratureA * quadratureB);
} }
/** /**
* Multiplies this sample by the multiplier sample * Calculates the quadrature component of multiplying two complex numbers, A and B.
*/ */
public void multiply( Complex multiplier ) public static float multiplyQuadrature(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
{ {
float inphase = multiplyInphase( inphase(), quadrature(), return (quadratureA * inphaseB) + (inphaseA * quadratureB);
multiplier.inphase(), multiplier.quadrature() ); }
float quadrature = multiplyQuadrature( inphase(), quadrature(),
multiplier.inphase(), multiplier.quadrature() );
mLeft = inphase; /**
mRight = quadrature; * Multiplies this sample by the multiplier sample
} */
public void multiply(Complex multiplier)
{
float inphase = multiplyInphase(inphase(), quadrature(),
multiplier.inphase(), multiplier.quadrature());
float quadrature = multiplyQuadrature(inphase(), quadrature(),
multiplier.inphase(), multiplier.quadrature());
/** mLeft = inphase;
* Multiplies both samples returning a new sample with the results mRight = quadrature;
*/ }
public static Complex multiply( Complex sample1, Complex sample2 )
{
float inphase = multiplyInphase( sample1.inphase(), sample1.quadrature(),
sample2.inphase(), sample2.quadrature() );
float quadrature = multiplyQuadrature( sample1.inphase(), sample1.quadrature(),
sample2.inphase(), sample2.quadrature() );
return new Complex( inphase, quadrature ); /**
} * Multiplies both samples returning a new sample with the results
*/
public static Complex multiply(Complex sample1, Complex sample2)
{
float inphase = multiplyInphase(sample1.inphase(), sample1.quadrature(),
sample2.inphase(), sample2.quadrature());
float quadrature = multiplyQuadrature(sample1.inphase(), sample1.quadrature(),
sample2.inphase(), sample2.quadrature());
public static Complex multiply( Complex sample, return new Complex(inphase, quadrature);
Float inphase, Float quadrature ) }
{
float i = multiplyInphase( sample.inphase(), sample.quadrature(),
inphase, quadrature );
float q = multiplyQuadrature( sample.inphase(), sample.quadrature(),
inphase, quadrature );
return new Complex( i, q ); public static Complex multiply(Complex sample,
} Float inphase, Float quadrature)
{
float i = multiplyInphase(sample.inphase(), sample.quadrature(),
inphase, quadrature);
float q = multiplyQuadrature(sample.inphase(), sample.quadrature(),
inphase, quadrature);
public static Complex multiply( Float inphase, Float quadrature, Complex sample ) return new Complex(i, q);
{ }
float i = multiplyInphase( inphase, quadrature, sample.inphase(), sample.quadrature() );
float q = multiplyQuadrature( inphase, quadrature, sample.inphase(), sample.quadrature() );
return new Complex( i, q ); public static Complex multiply(Float inphase, Float quadrature, Complex sample)
} {
float i = multiplyInphase(inphase, quadrature, sample.inphase(), sample.quadrature());
float q = multiplyQuadrature(inphase, quadrature, sample.inphase(), sample.quadrature());
public static Complex multiply( float inphaseA, float quadratureA, float inphaseB, float quadratureB ) return new Complex(i, q);
{ }
float i = multiplyInphase( inphaseA, quadratureA, inphaseB, quadratureB );
float q = multiplyQuadrature( inphaseA, quadratureA, inphaseB, quadratureB );
return new Complex( i, q ); public static Complex multiply(float inphaseA, float quadratureA, float inphaseB, float quadratureB)
} {
float i = multiplyInphase(inphaseA, quadratureA, inphaseB, quadratureB);
float q = multiplyQuadrature(inphaseA, quadratureA, inphaseB, quadratureB);
/** return new Complex(i, q);
* Adds the adder sample value to this sample }
*/
public void add( Complex adder )
{
mLeft += adder.left();
mRight += adder.right();
}
/** /**
* Adds the two complex samples returning a new complex sample with the result * Adds the adder sample value to this sample
*/ */
public static Complex add( Complex first, Complex second ) public void add(Complex adder)
{ {
return new Complex( first.left() + second.left(), mLeft += adder.left();
first.right() + second.right() ); mRight += adder.right();
} }
/** /**
* Adds the two complex samples returning a new complex sample with the result * Adds the two complex samples returning a new complex sample with the result
*/ */
public static Complex subtract( Complex first, Complex second ) public static Complex add(Complex first, Complex second)
{ {
return new Complex( first.left() - second.left(), return new Complex(first.left() + second.left(),
first.right() - second.right() ); first.right() + second.right());
} }
/** /**
* Magnitude of this sample * Adds the two complex samples returning a new complex sample with the result
*/ */
public float magnitude() public static Complex subtract(Complex first, Complex second)
{ {
return (float)Math.sqrt( magnitudeSquared() ); return new Complex(first.left() - second.left(),
} first.right() - second.right());
}
public static float magnitude( float inphase, float quadrature ) /**
{ * Magnitude of this sample
return (float)Math.sqrt( ( inphase * inphase ) + ( quadrature * quadrature ) ); */
} public float magnitude()
{
return (float)Math.sqrt(magnitudeSquared());
}
/** public static float magnitude(float inphase, float quadrature)
* Magnitude squared of this sample {
*/ return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature));
public float magnitudeSquared() }
{
return norm();
}
/** /**
* Norm of this sample = ( i * i ) + ( q * q ) * Magnitude squared of this sample
*/ */
public float norm() public float magnitudeSquared()
{ {
return (float)( ( inphase() * inphase() ) + return norm();
( quadrature() * quadrature() ) ); }
}
/** /**
* Returns the vector length to 1 (unit circle) * Norm of this sample = ( i * i ) + ( q * q )
*/ */
public void normalize() public float norm()
{ {
float magnitude = magnitude(); return (float)((inphase() * inphase()) +
(quadrature() * quadrature()));
}
if( magnitude != 0 ) /**
{ * Returns the vector length to 1 (unit circle)
multiply( (float)( 1.0f / magnitude() ) ); */
} public void normalize()
} {
float magnitude = magnitude();
/** if(magnitude != 0)
* Returns the vector length to 1 (unit circle), {
* avoiding square root multiplication multiply((float)(1.0f / magnitude()));
*/ }
public void fastNormalize() }
{
multiply( (float)( 1.95f - magnitudeSquared() ) );
}
public float left() /**
{ * Returns the vector length to 1 (unit circle),
return mLeft; * avoiding square root multiplication
} */
public void fastNormalize()
{
multiply((float)(1.95f - magnitudeSquared()));
}
public float right() public float left()
{ {
return mRight; return mLeft;
} }
public float inphase() public float right()
{ {
return mLeft; return mRight;
} }
/** public float inphase()
* Absolute value of in-phase component {
*/ return mLeft;
public float inPhaseAbsolute() }
{
return Math.abs( mLeft );
}
public float quadrature() /**
{ * Absolute value of in-phase component
return mRight; */
} public float inPhaseAbsolute()
{
return Math.abs(mLeft);
}
/** public float quadrature()
* Absolute value of quadrature component {
*/ return mRight;
public float quadratureAbsolute() }
{
return Math.abs( mRight );
}
public float x() /**
{ * Absolute value of quadrature component
return mLeft; */
} public float quadratureAbsolute()
{
return Math.abs(mRight);
}
public float y() public float x()
{ {
return mRight; return mLeft;
} }
public float real() public float y()
{ {
return mLeft; return mRight;
} }
/** public float real()
* Absolute value of real component {
*/ return mLeft;
public float realAbsolute() }
{
return Math.abs( mLeft );
}
public float imaginary() /**
{ * Absolute value of real component
return mRight; */
} public float realAbsolute()
{
return Math.abs(mLeft);
}
/** public float imaginary()
* Absolute value of imaginary component {
*/ return mRight;
public float imaginaryAbsolute() }
{
return Math.abs( mRight );
}
/** /**
* Returns the greater absolute value between left and right values * Absolute value of imaginary component
*/ */
public float maximumAbsolute() public float imaginaryAbsolute()
{ {
if( Math.abs( mLeft ) > Math.abs( mRight ) ) return Math.abs(mRight);
{ }
return Math.abs( mLeft );
}
else
{
return Math.abs( mRight );
}
}
/** /**
* Creates a new complex sample representing the angle with unit circle * Returns the greater absolute value between left and right values
* magnitude */
* @param angle in radians public float maximumAbsolute()
* @return {
*/ if(Math.abs(mLeft) > Math.abs(mRight))
public static Complex fromAngle( float angle ) {
{ return Math.abs(mLeft);
return new Complex( (float)Math.cos( angle ), }
(float)Math.sin( angle ) ); else
} {
return Math.abs(mRight);
}
}
/** /**
* Angle of this sample in radians * Creates a new complex sample representing the angle with unit circle
*/ * magnitude
public float angle() *
{ * @param angle in radians
return (float)Math.atan2( y(), x() ); * @return
} */
public static Complex fromAngle(double angle)
{
return new Complex((float)Math.cos(angle), (float)Math.sin(angle));
}
/** /**
* Constrains the i and q quantities to +/- value. * Sets this complex instance with the specified angle in radians.
* * @param angle in radians
* @param value - maximum absolute value */
*/ public void setAngle(double angle)
public void clip( float value ) {
{ setInphase((float)Math.cos(angle));
if( mLeft > value ) setQuadrature((float)Math.sin(angle));
{ }
mLeft = value;
}
else if( mLeft < -value )
{
mLeft = -value;
}
if( mRight > value ) /**
{ * Angle of this sample in radians
mRight = value; */
} public float angle()
else if( mRight < -value ) {
{ return (float)Math.atan2(y(), x());
mRight = -value; }
}
}
/** public float angleDegrees()
* Angle of this sample in degrees {
*/ return (float)Math.toDegrees(angle());
public float polarAngle() }
{
return (float)Math.toDegrees( angle() );
}
/** /**
* Provides an approximate magnitude value for this sample. * Constrains the i and q quantities to +/- value.
*/ *
public float envelope() * @param value - maximum absolute value
{ */
return envelope( mLeft, mRight ); public void clip(float value)
} {
if(mLeft > value)
{
mLeft = value;
}
else if(mLeft < -value)
{
mLeft = -value;
}
public static float envelope( float inphase, float quadrature ) if(mRight > value)
{ {
float inphaseAbsolute = Math.abs( inphase ); mRight = value;
float quadratureAbsolute = Math.abs( quadrature ); }
else if(mRight < -value)
{
mRight = -value;
}
}
if( inphaseAbsolute > quadratureAbsolute ) /**
{ * Angle of this sample in degrees
return inphaseAbsolute + ( 0.4f * quadratureAbsolute ); */
} public float polarAngle()
else {
{ return (float)Math.toDegrees(angle());
return quadratureAbsolute + ( 0.4f * inphaseAbsolute ); }
}
}
public static void main( String[] args ) /**
{ * Provides an approximate magnitude value for this sample.
double angle = Math.PI; //180 degrees */
public float envelope()
{
return envelope(mLeft, mRight);
}
Complex s1 = new Complex( (float)Math.sin( angle ), (float)Math.cos( angle ) ); public static float envelope(float inphase, float quadrature)
{
float inphaseAbsolute = Math.abs(inphase);
float quadratureAbsolute = Math.abs(quadrature);
Complex tap = new Complex( 1.0f, -0.1f ); if(inphaseAbsolute > quadratureAbsolute)
{
return inphaseAbsolute + (0.4f * quadratureAbsolute);
}
else
{
return quadratureAbsolute + (0.4f * inphaseAbsolute);
}
}
Complex convolved = Complex.multiply( s1, tap ); public static void main(String[] args)
{
Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
mLog.debug("REF: " + ROTATE_FROM_MINUS_45.toString());
mLog.debug( "s: " + s1.toString() + Complex symbol = new Complex(0.7665009f, -0.6422432f);
" t: " + tap.toString() + mLog.debug("SYM: " + symbol.toString());
" convolved: " + convolved.toString() );
} symbol.multiply(ROTATE_FROM_MINUS_45);
mLog.debug("DER: " + symbol.toString());
}
} }

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.source; package io.github.dsheirer.source;
import javax.sound.sampled.UnsupportedAudioFileException; import javax.sound.sampled.UnsupportedAudioFileException;
@@ -10,22 +25,24 @@ import java.io.IOException;
*/ */
public interface IControllableFileSource public interface IControllableFileSource
{ {
/** /**
* Opens the file source * Opens the file source
*/ */
public void open() throws IOException, UnsupportedAudioFileException; public void open() throws IOException, UnsupportedAudioFileException;
public void close() throws IOException; public void close() throws IOException;
public File getFile(); public File getFile();
public void next( int frames ) throws IOException; public void next(int frames) throws IOException;
public void next( int frames, boolean broadcast ) throws IOException; public void next(int frames, boolean broadcast) throws IOException;
public long getFrameCount() throws IOException; public long getFrameCount() throws IOException;
public void setListener( IFrameLocationListener listener ); public int getSampleRate();
public void removeListener( IFrameLocationListener listener ); public void setListener(IFrameLocationListener listener);
public void removeListener(IFrameLocationListener listener);
} }

View File

@@ -25,7 +25,8 @@ import io.github.dsheirer.source.mixer.MixerSourceEditor;
import io.github.dsheirer.source.tuner.TunerSourceEditor; import io.github.dsheirer.source.tuner.TunerSourceEditor;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import javax.swing.*; import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
@@ -43,7 +44,7 @@ public class SourceConfigurationEditor extends Editor<Channel>
mMixerSourceEditor = new MixerSourceEditor( sourceManager ); mMixerSourceEditor = new MixerSourceEditor( sourceManager );
mMixerSourceEditor.setSaveRequestListener( this ); mMixerSourceEditor.setSaveRequestListener( this );
mTunerSourceEditor = new TunerSourceEditor(); mTunerSourceEditor = new TunerSourceEditor(sourceManager.getTunerModel());
mTunerSourceEditor.setSaveRequestListener( this ); mTunerSourceEditor.setSaveRequestListener( this );
init(); init();

View File

@@ -59,6 +59,11 @@ public class SourceManager
return mTunerManager; return mTunerManager;
} }
public TunerModel getTunerModel()
{
return mTunerModel;
}
public Source getSource(SourceConfiguration config, int bandwidth) public Source getSource(SourceConfiguration config, int bandwidth)
throws SourceException throws SourceException
{ {

View File

@@ -1,6 +1,6 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer * Copyright (C) 2014-2018 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -33,6 +33,7 @@ public class SourceConfigTuner extends SourceConfiguration
private long mFrequency = 0; private long mFrequency = 0;
private int mBandwidth = 12500; private int mBandwidth = 12500;
private String mPreferredTuner;
public SourceConfigTuner() public SourceConfigTuner()
{ {
@@ -58,6 +59,34 @@ public class SourceConfigTuner extends SourceConfiguration
mFrequency = frequency; mFrequency = frequency;
} }
/**
* Preferred tuner to use for this configuration.
* @return tuner name or null
*/
@JacksonXmlProperty(isAttribute = true, localName = "preferred_tuner")
public String getPreferredTuner()
{
return mPreferredTuner;
}
/**
* Indicates if this configuration has a specified preferred tuner
*/
@JsonIgnore
public boolean hasPreferredTuner()
{
return mPreferredTuner != null;
}
/**
* Specifies the preferred tuner to use for this configuration
* @param preferredTuner to use, if available
*/
public void setPreferredTuner(String preferredTuner)
{
mPreferredTuner = preferredTuner;
}
@JsonIgnore @JsonIgnore
@Override @Override
public String getDescription() public String getDescription()

View File

@@ -469,6 +469,7 @@ public class TunerChannelSource extends ComplexSource implements IFrequencyChang
{ {
//Frequency correction requests are the only change requests supported from downstream components //Frequency correction requests are the only change requests supported from downstream components
case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
mLog.debug("Got a freq corr change request: " + event.getValue().intValue());
setFrequencyCorrection(event.getValue().intValue()); setFrequencyCorrection(event.getValue().intValue());
break; break;
} }

View File

@@ -70,6 +70,28 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
return null; return null;
} }
/**
* Find a tuner that matches the name argument
*
* @param name of the tuner
* @return named tuner or null
*/
public Tuner getTuner(String name)
{
if(name != null)
{
for(Tuner tuner: mTuners)
{
if(tuner.getName().equalsIgnoreCase(name))
{
return tuner;
}
}
}
return null;
}
public void addTuners( List<Tuner> Tuners ) public void addTuners( List<Tuner> Tuners )
{ {
for( Tuner tuner: Tuners ) for( Tuner tuner: Tuners )
@@ -271,6 +293,31 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
Tuner tuner; Tuner tuner;
if(config.hasPreferredTuner())
{
tuner = getTuner(config.getPreferredTuner());
if(tuner != null)
{
try
{
retVal = tuner.getChannel( tunerChannel );
if(retVal != null)
{
return retVal;
}
}
catch(Exception e)
{
//Fall through to logger below
}
}
mLog.info("Unable to source channel [" + config.getFrequency() + "] from preferred tuner [" +
config.getPreferredTuner() + "] - searching for another tuner");
}
while( it.hasNext() && retVal == null ) while( it.hasNext() && retVal == null )
{ {
tuner = it.next(); tuner = it.next();
@@ -285,12 +332,10 @@ public class TunerModel extends AbstractTableModel implements Listener<TunerEven
} }
catch ( SourceException e ) catch ( SourceException e )
{ {
mLog.error( "error obtaining channel from tuner [" + mLog.error( "error obtaining channel from tuner [" + tuner.getName() + "]", e );
tuner.getName() + "]", e );
} }
} }
return retVal; return retVal;
} }
} }

View File

@@ -1,6 +1,6 @@
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014 Dennis Sheirer * Copyright (C) 2014-2018 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -26,70 +26,164 @@ import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor; import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TunerSourceEditor extends Editor<Channel> public class TunerSourceEditor extends Editor<Channel>
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final String NO_PREFERRED_TUNER = "(none)";
private static final String UNAVAILABLE_TUNER = " (unavailable)";
private JFrequencyControl mFrequencyControl; private JFrequencyControl mFrequencyControl;
private JComboBox<String> mTunerNameComboBox;
private TunerModel mTunerModel;
private List<String> mCurrentTunerNames;
public TunerSourceEditor() public TunerSourceEditor(TunerModel tunerModel)
{ {
init(); mTunerModel = tunerModel;
} init();
}
private void init() private void init()
{ {
setLayout( new MigLayout( "insets 0 0 0 0", "[left]", "" ) ); loadTunerNames();
mFrequencyControl = new JFrequencyControl();
mFrequencyControl.setEnabled( false );
mFrequencyControl.addListener( new IFrequencyChangeProcessor()
{
@Override
public void frequencyChanged( FrequencyChangeEvent event )
{
setModified( true );
}
} );
add( mFrequencyControl );
}
public void save() setLayout(new MigLayout("insets 0 0 0 0", "[left]", ""));
{ mFrequencyControl = new JFrequencyControl();
if( hasItem() && isModified() ) mFrequencyControl.setEnabled(false);
{ mFrequencyControl.addListener(new IFrequencyChangeProcessor()
SourceConfigTuner config = new SourceConfigTuner(); {
config.setFrequency( mFrequencyControl.getFrequency() ); @Override
getItem().setSourceConfiguration( config ); public void frequencyChanged(FrequencyChangeEvent event)
} {
setModified(true);
}
});
add(mFrequencyControl);
setModified( false ); mTunerNameComboBox = new JComboBox<>(mCurrentTunerNames.toArray(new String[mCurrentTunerNames.size()]));
} mTunerNameComboBox.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(new JLabel("Preferred Tuner:"));
add(mTunerNameComboBox);
}
@Override private void loadTunerNames()
public void setItem( Channel item ) {
{ mCurrentTunerNames = new ArrayList<>();
super.setItem( item );
if( hasItem() ) for(Tuner tuner: mTunerModel.getTuners())
{ {
SourceConfiguration config = getItem().getSourceConfiguration(); mCurrentTunerNames.add(tuner.getName());
}
mFrequencyControl.setEnabled( true ); Collections.sort(mCurrentTunerNames);
if( config instanceof SourceConfigTuner ) mCurrentTunerNames.add(0, NO_PREFERRED_TUNER);
{ }
mFrequencyControl.setFrequency( ((SourceConfigTuner)config).getFrequency(), false );
setModified( false ); private void updateTunerNameCombo(String preferredTuner)
} {
else if(preferredTuner != null && !mCurrentTunerNames.contains(preferredTuner))
{ {
mFrequencyControl.setFrequency( 101000000, false ); List<String> updatedTunerNameList = new ArrayList<>(mCurrentTunerNames);
setModified( true ); String unavailableTuner = preferredTuner + UNAVAILABLE_TUNER;
} updatedTunerNameList.add(unavailableTuner);
} String[] tunerNameArray = updatedTunerNameList.toArray(new String[updatedTunerNameList.size()]);
else mTunerNameComboBox.setModel(new DefaultComboBoxModel<>(tunerNameArray));
{ mTunerNameComboBox.setSelectedItem(unavailableTuner);
mFrequencyControl.setEnabled( false ); }
setModified( false ); else
} {
} String[] tunerNameArray = mCurrentTunerNames.toArray(new String[mCurrentTunerNames.size()]);
mTunerNameComboBox.setModel(new DefaultComboBoxModel<>(tunerNameArray));
if(preferredTuner == null)
{
mTunerNameComboBox.setSelectedItem(NO_PREFERRED_TUNER);
}
else
{
mTunerNameComboBox.setSelectedItem(preferredTuner);
}
}
}
public void save()
{
if(hasItem() && isModified())
{
SourceConfigTuner config = new SourceConfigTuner();
config.setFrequency(mFrequencyControl.getFrequency());
String preferredTuner = (String)mTunerNameComboBox.getSelectedItem();
if(preferredTuner != null)
{
if(preferredTuner.equalsIgnoreCase(NO_PREFERRED_TUNER))
{
config.setPreferredTuner(null);
}
else if(preferredTuner.contains(UNAVAILABLE_TUNER))
{
//don't do anything - the config already contains this unavailable tuner name
}
else
{
config.setPreferredTuner(preferredTuner);
}
}
getItem().setSourceConfiguration(config);
}
setModified(false);
}
@Override
public void setItem(Channel item)
{
super.setItem(item);
if(hasItem())
{
SourceConfiguration config = getItem().getSourceConfiguration();
mFrequencyControl.setEnabled(true);
if(config instanceof SourceConfigTuner)
{
SourceConfigTuner tunerConfig = (SourceConfigTuner)config;
mFrequencyControl.setFrequency(tunerConfig.getFrequency(), false);
updateTunerNameCombo(tunerConfig.getPreferredTuner());
setModified(false);
}
else
{
mFrequencyControl.setFrequency(101000000, false);
setModified(true);
}
}
else
{
mFrequencyControl.setEnabled(false);
setModified(false);
}
}
} }

View File

@@ -167,13 +167,8 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
if(mInputStream != null) if(mInputStream != null)
{ {
mInputStream.close(); mInputStream.close();
mInputStream = null;
} }
else
{
throw new IOException("Can't close wave source - was not opened");
}
mInputStream = null;
} }
/** /**
@@ -239,8 +234,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
buffer = Arrays.copyOf(buffer, samplesRead); buffer = Arrays.copyOf(buffer, samplesRead);
} }
float[] samples = ConversionUtils float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
.convertFromSigned16BitSamples(buffer);
mListener.receive(new ComplexBuffer(samples)); mListener.receive(new ComplexBuffer(samples));
} }

View File

@@ -30,8 +30,13 @@ import io.github.dsheirer.spectrum.menu.SmoothingItem;
import io.github.dsheirer.spectrum.menu.SmoothingTypeItem; import io.github.dsheirer.spectrum.menu.SmoothingTypeItem;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import javax.swing.*; import javax.swing.JLayeredPane;
import java.awt.*; import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import java.awt.Component;
import java.awt.event.ComponentEvent; import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener; import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
@@ -193,7 +198,7 @@ public class ChannelSpectrumPanel extends JPanel
private void start() private void start()
{ {
if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.getEnabled() ) if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.isProcessing() )
{ {
ProcessingChain processingChain = mChannelProcessingManager ProcessingChain processingChain = mChannelProcessingManager
.getProcessingChain( mCurrentChannel ); .getProcessingChain( mCurrentChannel );
@@ -209,7 +214,7 @@ public class ChannelSpectrumPanel extends JPanel
private void stop() private void stop()
{ {
if( mCurrentChannel != null && mCurrentChannel.getEnabled() ) if( mCurrentChannel != null && mCurrentChannel.isProcessing() )
{ {
ProcessingChain processingChain = mChannelProcessingManager ProcessingChain processingChain = mChannelProcessingManager
.getProcessingChain( mCurrentChannel ); .getProcessingChain( mCurrentChannel );

View File

@@ -1,19 +1,16 @@
/******************************************************************************* /*******************************************************************************
* sdrtrunk * sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer * Copyright (C) 2014-2018 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* it under the terms of the GNU General Public License as published by * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* the Free Software Foundation, either version 3 of the License, or * later version.
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* but WITHOUT ANY WARRANTY; without even the implied warranty of * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* 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 * You should have received a copy of the GNU General Public License along with this program.
* along with this program. If not, see <http://www.gnu.org/licenses/> * If not, see <http://www.gnu.org/licenses/>
* *
******************************************************************************/ ******************************************************************************/
package io.github.dsheirer.spectrum; package io.github.dsheirer.spectrum;
@@ -21,7 +18,6 @@ package io.github.dsheirer.spectrum;
import io.github.dsheirer.dsp.filter.Window; import io.github.dsheirer.dsp.filter.Window;
import io.github.dsheirer.dsp.filter.Window.WindowType; import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Buffer;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.SampleType; import io.github.dsheirer.sample.SampleType;
import io.github.dsheirer.sample.complex.ComplexBuffer; import io.github.dsheirer.sample.complex.ComplexBuffer;
@@ -33,60 +29,42 @@ import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Arrays; import java.io.IOException;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* Processes both complex samples or float samples and dispatches a float array * Processes both complex samples or float samples and dispatches a float array of DFT results, using configurable fft
* of DFT results, using configurable fft size and output dispatch timelines. * size and output dispatch timelines.
*/ */
public class DFTProcessor implements Listener<ComplexBuffer>, IFrequencyChangeProcessor, IDFTWidthChangeProcessor public class DFTProcessor implements Listener<ComplexBuffer>, IFrequencyChangeProcessor, IDFTWidthChangeProcessor
{ {
private final static Logger mLog = LoggerFactory.getLogger(DFTProcessor.class); private static final Logger mLog = LoggerFactory.getLogger(DFTProcessor.class);
private static final int BUFFER_QUEUE_MAX_SIZE = 12;
private CopyOnWriteArrayList<DFTResultsConverter> mListeners = new CopyOnWriteArrayList<DFTResultsConverter>(); private static final int BUFFER_QUEUE_OVERFLOW_RESET_THRESHOLD = 6;
private static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate";
//Fixed size sample queue. We set the size at 12 based on the highest
//sample rate tuner (10 MHz) producing 153 frames per second and the lowest
//DFT processing frame rate of 12 frames per second. This means that the
//maximum size needed is 153 / 12 = 11 frames per DFT processing iteration
//so we set it at 12.
private BlockingQueue<ComplexBuffer> mQueue = new ArrayBlockingQueue<>(12);
private ScheduledFuture<?> mProcessorTaskHandle;
public static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate";
//The Cosine and Hanning windows seem to offer the best spectral display with minimal bin leakage/smearing
private WindowType mWindowType = Window.WindowType.HANNING;
private double[] mWindow;
private DFTSize mDFTSize = DFTSize.FFT04096; private DFTSize mDFTSize = DFTSize.FFT04096;
private DFTSize mNewDFTSize = DFTSize.FFT04096; private DFTSize mNewDFTSize = DFTSize.FFT04096;
private double[] mWindow;
/* The Cosine and Hanning windows seem to offer the best spectral display
* with minimal bin leakage/smearing */
private WindowType mWindowType = Window.WindowType.HANNING;
private FloatFFT_1D mFFT = new FloatFFT_1D(mDFTSize.getSize()); private FloatFFT_1D mFFT = new FloatFFT_1D(mDFTSize.getSize());
private int mFrameRate; private int mFrameRate;
private int mSampleRate; private int mSampleRate = 2400000; //Initial high value until we receive update from tuner
private int mFFTFloatsPerFrame; private int mFrameSize;
private float mNewFloatsPerFrame; private int mFrameFlushCount;
private float mNewFloatResidual; private int mFrameOverlapCount;
private float[] mPreviousFrame = new float[8192];
private float[] mCurrentBuffer;
private int mCurrentBufferPointer = 0;
private SampleType mSampleType; private SampleType mSampleType;
private AtomicBoolean mRunning = new AtomicBoolean(); private AtomicBoolean mRunning = new AtomicBoolean();
private ScheduledFuture<?> mProcessorTaskHandle;
private CopyOnWriteArrayList<DFTResultsConverter> mListeners = new CopyOnWriteArrayList<DFTResultsConverter>();
private OverflowableBufferStream mOverflowableBufferStream = new OverflowableBufferStream(BUFFER_QUEUE_MAX_SIZE,
BUFFER_QUEUE_OVERFLOW_RESET_THRESHOLD, mDFTSize.getSize());
private float[] mPreviousSamples;
public DFTProcessor(SampleType sampleType) public DFTProcessor(SampleType sampleType)
{ {
@@ -101,9 +79,8 @@ public class DFTProcessor implements Listener<ComplexBuffer>, IFrequencyChangePr
stop(); stop();
mListeners.clear(); mListeners.clear();
mQueue.clear(); mOverflowableBufferStream.clear();
mWindow = null; mWindow = null;
mCurrentBuffer = null;
} }
public WindowType getWindowType() public WindowType getWindowType()
@@ -161,14 +138,10 @@ public class DFTProcessor implements Listener<ComplexBuffer>, IFrequencyChangePr
public void setFrameRate(int framesPerSecond) public void setFrameRate(int framesPerSecond)
{ {
//TODO: make sure frame rate & sample rate sample requirement doesn't
//expect overlap greater than the previous frame length
if(framesPerSecond < 1 || framesPerSecond > 1000) if(framesPerSecond < 1 || framesPerSecond > 1000)
{ {
throw new IllegalArgumentException("DFTProcessor cannot run " throw new IllegalArgumentException("DFTProcessor cannot run more than 1000 times per second -- requested " +
+ "more than 1000 times per second -- requested setting:" "setting:" + framesPerSecond);
+ framesPerSecond);
} }
mFrameRate = framesPerSecond; mFrameRate = framesPerSecond;
@@ -205,168 +178,46 @@ public class DFTProcessor implements Listener<ComplexBuffer>, IFrequencyChangePr
start(); start();
} }
public int getCalculationsPerSecond()
{
return mFrameRate;
}
/** /**
* Places the sample into a transfer queue for future processing. * Places the sample into a transfer queue for future processing.
*/ */
@Override @Override
public void receive(ComplexBuffer sampleBuffer) public void receive(ComplexBuffer sampleBuffer)
{ {
mQueue.offer(sampleBuffer); mOverflowableBufferStream.offer(sampleBuffer);
}
private void getNextBuffer()
{
mCurrentBuffer = null;
try
{
Buffer buffer = mQueue.take();
mCurrentBuffer = buffer.getSamples();
}
catch(InterruptedException e)
{
mCurrentBuffer = null;
}
mCurrentBufferPointer = 0;
}
private float[] getSamples()
{
int remaining = (int) mFFTFloatsPerFrame;
float[] currentFrame = new float[remaining];
int currentFramePointer = 0;
float integralFloatsToConsume = mNewFloatsPerFrame + mNewFloatResidual;
int newFloatsToConsumeThisFrame = (int) integralFloatsToConsume;
mNewFloatResidual = integralFloatsToConsume - newFloatsToConsumeThisFrame;
/* If the number of required floats for the fft is greater than the
* consumption rate per frame, we have to reach into the previous
* frame to makeup the difference. */
if(newFloatsToConsumeThisFrame < remaining)
{
int previousFloatsRequired = remaining - newFloatsToConsumeThisFrame;
System.arraycopy(mPreviousFrame,
mPreviousFrame.length - previousFloatsRequired,
currentFrame,
currentFramePointer,
previousFloatsRequired);
remaining -= previousFloatsRequired;
currentFramePointer += previousFloatsRequired;
}
/* Fill the rest of the buffer with new samples */
while(mRunning.get() && remaining > 0)
{
if(mCurrentBuffer == null ||
mCurrentBufferPointer >= mCurrentBuffer.length)
{
getNextBuffer();
}
/* If we don't have new samples to use, send the current frame with
* the remaining values as zero */
if(mCurrentBuffer == null)
{
remaining = 0;
}
else
{
int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer;
while(remaining > 0 && samplesAvailable > 0)
{
currentFrame[currentFramePointer++] =
(float) mCurrentBuffer[mCurrentBufferPointer++];
samplesAvailable--;
remaining--;
newFloatsToConsumeThisFrame--;
}
}
}
/* If the incoming float rate is greater than the fft consumption rate,
* then we have to purge some floats, otherwise, store the previous
* frame, because we have overlapping frames */
if(newFloatsToConsumeThisFrame > 0)
{
purge(newFloatsToConsumeThisFrame);
}
else
{
mPreviousFrame = Arrays.copyOf(currentFrame, currentFrame.length);
}
return currentFrame;
} }
private void calculate() private void calculate()
{ {
float[] samples = getSamples(); try
Window.apply(mWindow, samples);
if(mSampleType == SampleType.REAL)
{ {
mFFT.realForward(samples); if(mFrameFlushCount > 0)
}
else
{
mFFT.complexForward(samples);
}
dispatch(samples);
}
private void purge(int samplesToPurge)
{
if(samplesToPurge <= 0)
{
throw new IllegalArgumentException("DFTProcessor - cannot purge "
+ "negative sample amount");
}
while(mRunning.get() && samplesToPurge > 0)
{
if(mCurrentBuffer == null ||
mCurrentBufferPointer >= mCurrentBuffer.length)
{ {
getNextBuffer(); mOverflowableBufferStream.flush(mFrameFlushCount);
} }
if(mCurrentBuffer != null) //If this throws an IO exception, the buffer queue is (temporarily) empty and we return from the method
{ float[] samples = mOverflowableBufferStream.get(mFrameSize, mFrameOverlapCount);
int samplesAvailable = mCurrentBuffer.length - mCurrentBufferPointer;
if(samplesAvailable >= samplesToPurge) Window.apply(mWindow, samples);
{
mCurrentBufferPointer += samplesToPurge; if(mSampleType == SampleType.REAL)
samplesToPurge = 0; {
} mFFT.realForward(samples);
else
{
samplesToPurge -= samplesAvailable;
mCurrentBufferPointer = mCurrentBuffer.length;
}
} }
else else
{ {
samplesToPurge = 0; mFFT.complexForward(samples);
} }
dispatch(samples);
mPreviousSamples = samples;
}
catch(IOException ioe)
{
//No new data, dispatch the previous samples again
dispatch(mPreviousSamples);
} }
} }
@@ -389,11 +240,6 @@ public class DFTProcessor implements Listener<ComplexBuffer>, IFrequencyChangePr
mListeners.add(listener); mListeners.add(listener);
} }
public void removeConverter(DFTResultsConverter listener)
{
mListeners.remove(listener);
}
private class DFTCalculationTask implements Runnable private class DFTCalculationTask implements Runnable
{ {
@Override @Override
@@ -433,22 +279,13 @@ public class DFTProcessor implements Listener<ComplexBuffer>, IFrequencyChangePr
setWindowType(mWindowType); setWindowType(mWindowType);
if(mSampleType == SampleType.COMPLEX)
{
mPreviousFrame = new float[mDFTSize.getSize() * 2];
}
else
{
mPreviousFrame = new float[mDFTSize.getSize()];
}
mFFT = new FloatFFT_1D(mDFTSize.getSize()); mFFT = new FloatFFT_1D(mDFTSize.getSize());
} }
} }
public void clearBuffer() public void clearBuffer()
{ {
mQueue.clear(); mOverflowableBufferStream.clear();
} }
@Override @Override
@@ -466,17 +303,69 @@ public class DFTProcessor implements Listener<ComplexBuffer>, IFrequencyChangePr
} }
/** /**
* * Calculates the frame size, flush count and overlap count to use for each calculation cycle.
*/ */
private void calculateConsumptionRate() private void calculateConsumptionRate()
{ {
mNewFloatResidual = 0.0f; int floatsPerSample = mSampleType == SampleType.COMPLEX ? 2 : 1;
mNewFloatsPerFrame = ((float) mSampleRate / (float) mFrameRate) * mFrameSize = mDFTSize.getSize() * floatsPerSample;
(mSampleType == SampleType.COMPLEX ? 2.0f : 1.0f); mPreviousSamples = new float[mFrameSize];
mFFTFloatsPerFrame = (mSampleType == SampleType.COMPLEX ? int productionRate = mSampleRate * floatsPerSample;
mDFTSize.getSize() * 2 : int consumptionRate = mFrameRate * mFrameSize;
mDFTSize.getSize()); int residual = productionRate - consumptionRate;
if(residual < 0)
{
mFrameFlushCount = 0;
mFrameOverlapCount = -residual / mFrameRate;
if(mFrameOverlapCount * mFrameRate < -residual)
{
mFrameOverlapCount ++;
}
}
else if(residual > 0)
{
mFrameOverlapCount = 0;
mFrameFlushCount = residual / mFrameRate;
if(mFrameFlushCount * mFrameRate < residual)
{
mFrameFlushCount++;
}
}
//Ensure we're flushing or overlapping by even multiples of the sample size
if(mFrameOverlapCount % floatsPerSample != 0)
{
mFrameOverlapCount++;
}
if(mFrameFlushCount % floatsPerSample != 0)
{
mFrameFlushCount++;
}
//If the overlap size is greater than the frame size, we can't do that. Automatically decrease the frame rate
//until we reach a legitimate overlap value.
if(mFrameOverlapCount >= mFrameSize)
{
mLog.warn("Unable to provide frame rate [" + mFrameRate + "] for current DFT size [" + mDFTSize.getSize() +
"] - reducing frame rate");
mFrameRate--;
if(mFrameRate <= 0)
{
//Consider reducing the FFT size as a possible alternative corrective measure
throw new IllegalStateException("Unable to determine a viable frame rate based on incoming sample " +
"rate, DFT size and DFT frame rate");
}
//Recursively call this method to test the new frame rate
calculateConsumptionRate();
}
} }
} }

View File

@@ -0,0 +1,202 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 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 io.github.dsheirer.spectrum;
import io.github.dsheirer.sample.Buffer;
import io.github.dsheirer.sample.OverflowableTransferQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.LinkedList;
public class OverflowableBufferStream extends OverflowableTransferQueue<Buffer>
{
private static final Logger mLog = LoggerFactory.getLogger(OverflowableBufferStream.class);
private int mFlushCount = 0;
private Buffer mCurrentBuffer;
private int mCurrentBufferPointer = 0;
private FloatBuffer mFloatBuffer;
private LinkedList<Buffer> mBufferList = new LinkedList<>();
private int mBufferFetchLimit;
/**
* Stream for receiving Buffers from a producer thread and providing access to a consumer thread to read the
* sample data. Extends the overflowable transfer queue to define maximum and reset thresholds to ensure the
* queue doesn't grow too large.
*
* This stream is designed for consistent sample size requests each time the get() samples method is invoked, such
* as might be needed for an FFT processor. It uses an internal float buffer that must be resized whenever the
* get(sample size) argument changes, so consider using another option if you anticipate that your sample size
* argument during get() operations will vary.
*
* @param maxSize of the internal buffer queue - defines the overflow threshold
* @param resetThreshold of the internal buffer queue - defines when the overflow state will be cleared once the
* queue size has reduced below this threshold
* @param arraySize of the anticipated samples to fetch using the get(x,y) method.
*/
public OverflowableBufferStream(int maxSize, int resetThreshold, int arraySize)
{
super(maxSize, resetThreshold);
mFloatBuffer = FloatBuffer.allocate(arraySize);
mBufferFetchLimit = maxSize = resetThreshold;
}
/**
* Retrieves the specified number of float samples from the stream. When a non-zero overlap is specified, a
* quantity of samples from the previous buffer are preloaded in the buffer and new samples are added to make a
* full array count.
*
* @param sampleCount of float samples to pull from the stream
* @param overlap to reuse part of the previous buffer's samples in this fetch - must be less than sample count
* @return an array of float samples from the stream
* @throws IOException if the buffer queue is/becomes empty and the samples cannot be provided
* @throws IllegalArgumentException if the overlap argument is not less than the sample count argument
*/
public float[] get(int sampleCount, int overlap) throws IOException
{
if(overlap >= sampleCount)
{
throw new IllegalArgumentException("Overlap must be less than the requested sample count");
}
if(mCurrentBuffer == null)
{
//This will throw an IO exception if the queue is empty
getNextBuffer();
}
//Resize the float buffer if the request size changes
if(mFloatBuffer.capacity() != sampleCount)
{
mFloatBuffer = FloatBuffer.allocate(sampleCount);
}
while(mFloatBuffer.hasRemaining())
{
int available = mCurrentBuffer.getSamples().length - mCurrentBufferPointer;
if(available <= 0)
{
getNextBuffer();
}
else
{
//Fill the buffer to capacity
int toCopy = mFloatBuffer.remaining();
if(available < toCopy)
{
toCopy = available;
}
if(toCopy > 0)
{
mFloatBuffer.put(mCurrentBuffer.getSamples(), mCurrentBufferPointer, toCopy);
mCurrentBufferPointer += toCopy;
}
}
}
//If we get to here, the float buffer is full. Get the samples and then refill with overlap if non-zero
mFloatBuffer.rewind();
float[] samples = new float[sampleCount];
mFloatBuffer.get(samples);
mFloatBuffer.clear();
if(overlap > 0)
{
mFloatBuffer.put(samples, samples.length - overlap, overlap);
}
try
{
performFlush();
}
catch(IOException ioe)
{
//Do nothing if we get an IO exception ... the buffer is empty
}
//Reset flush count to zero, even if we ran out of buffers
mFlushCount = 0;
return samples;
}
/**
* Throws away incoming sample data until mFlushCount is zeroized or the queue is empty.
*
* @throws IOException if the queue is emptied while performing flush.
*/
private void performFlush() throws IOException
{
//Flush sample data as requested
while(mFlushCount > 0)
{
int available = mCurrentBuffer.getSamples().length - mCurrentBufferPointer;
if(available <= mFlushCount)
{
mFlushCount -= available;
getNextBuffer();
}
else
{
mCurrentBufferPointer += mFlushCount;
mFlushCount = 0;
}
}
}
/**
* Flushes the specified number of samples from the incoming stream.
* @param sampleCount to throw away
*/
public void flush(int sampleCount)
{
mFlushCount += sampleCount;
}
/**
* Fetches the next buffer from the queue.
*
* @throws IOException if the queue is currently empty
*/
private void getNextBuffer() throws IOException
{
mCurrentBuffer = null;
mCurrentBufferPointer = 0;
if(mBufferList.isEmpty())
{
drainTo(mBufferList, mBufferFetchLimit);
}
if(!mBufferList.isEmpty())
{
mCurrentBuffer = mBufferList.poll();
}
if(mCurrentBuffer == null)
{
throw new IOException("Buffer queue is (currently) empty");
}
}
}

View File

@@ -34,8 +34,16 @@ import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.swing.*; import javax.swing.JPanel;
import java.awt.*; import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ComponentEvent; import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener; import java.awt.event.ComponentListener;
import java.awt.geom.Line2D; import java.awt.geom.Line2D;
@@ -517,14 +525,14 @@ public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequ
for(Channel channel : mVisibleChannels) for(Channel channel : mVisibleChannels)
{ {
if(mChannelDisplay == ChannelDisplay.ALL || if(mChannelDisplay == ChannelDisplay.ALL ||
(mChannelDisplay == ChannelDisplay.ENABLED && channel.getEnabled())) (mChannelDisplay == ChannelDisplay.ENABLED && channel.isProcessing()))
{ {
//Choose the correct background color to use //Choose the correct background color to use
if(channel.isSelected()) if(channel.isSelected())
{ {
graphics.setColor(mColorChannelConfigSelected); graphics.setColor(mColorChannelConfigSelected);
} }
else if(channel.getEnabled()) else if(channel.isProcessing())
{ {
graphics.setColor(mColorChannelConfigProcessing); graphics.setColor(mColorChannelConfigProcessing);
} }