diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml
index fc474e43..3721dbce 100644
--- a/.idea/dictionaries/denny.xml
+++ b/.idea/dictionaries/denny.xml
@@ -14,6 +14,7 @@
nutall
overflowable
polyphase
+ qpsk
remez
resampler
samplerate
diff --git a/src/main/java/io/github/dsheirer/bits/ISyncProcessor.java b/src/main/java/io/github/dsheirer/bits/ISyncProcessor.java
index 52acb572..655e6d6f 100644
--- a/src/main/java/io/github/dsheirer/bits/ISyncProcessor.java
+++ b/src/main/java/io/github/dsheirer/bits/ISyncProcessor.java
@@ -1,6 +1,26 @@
+/*******************************************************************************
+ * 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
+ *
+ ******************************************************************************/
package io.github.dsheirer.bits;
public interface ISyncProcessor
{
- public void checkSync( long value );
+ /**
+ * Checks if the value matches a known sync pattern
+ * @param value
+ * @return
+ */
+ boolean checkSync(long value);
}
diff --git a/src/main/java/io/github/dsheirer/bits/MultiSyncPatternMatcher.java b/src/main/java/io/github/dsheirer/bits/MultiSyncPatternMatcher.java
index c313b484..4b4e8621 100644
--- a/src/main/java/io/github/dsheirer/bits/MultiSyncPatternMatcher.java
+++ b/src/main/java/io/github/dsheirer/bits/MultiSyncPatternMatcher.java
@@ -1,111 +1,130 @@
/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2014 Dennis Sheirer
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see
+ * 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
+ *
******************************************************************************/
package io.github.dsheirer.bits;
+import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import org.apache.commons.lang3.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
- * Multi-Sync pattern matcher. Accepts multiple sync detector objects for
+ * Multi-Sync pattern matcher. Accepts multiple sync detector objects for
* comparision against the incoming bit stream.
- *
+ *
* Note: works for sync patterns up to 63 bits (integer size - 1 ) long.
* If a 64 bit or larger sync pattern is applied, then the wrapping feature
* of the Long.rotate methods will wrap the MSB around to the LSB and
* corrupt the matching value.
- *
*/
public class MultiSyncPatternMatcher
{
- private List mSyncProcessors = new ArrayList();
-
- private long mBits = 0;
- private long mMask = 0;
-
- public MultiSyncPatternMatcher( int syncSize )
- {
- Validate.isTrue(syncSize < 64);
-
- //Setup a bit mask of all ones the length of the sync pattern
- mMask = (long)( ( Math.pow( 2, syncSize ) ) - 1 );
- }
-
- public void dispose()
- {
- mSyncProcessors.clear();
- }
-
- /**
- * Processes two bits before checking sync processors for a match.
- */
- public void receive( boolean bit1, boolean bit2 )
- {
- mBits = Long.rotateLeft( mBits, 1 );
-
- mBits &= mMask;
-
- if( bit1 )
- {
- mBits += 1;
- }
+ private final static Logger mLog = LoggerFactory.getLogger(MultiSyncPatternMatcher.class);
- mBits = Long.rotateLeft( mBits, 1 );
-
- mBits &= mMask;
-
- if( bit2 )
- {
- mBits += 1;
- }
+ private ISyncDetectListener mSyncDetectListener;
+ private int mSyncLossThreshold;
+ private int mBitCount;
+ private List mSyncProcessors = new ArrayList();
- for( ISyncProcessor processor: mSyncProcessors )
- {
- processor.checkSync( mBits );
- }
- }
-
- /**
- * Processes one bit before checking sync processors for a match.
- */
- public void receive( boolean bit )
- {
- mBits = Long.rotateLeft( mBits, 1 );
-
- mBits &= mMask;
-
- if( bit )
- {
- mBits += 1;
- }
+ private long mBits = 0;
+ private long mMask = 0;
- for( ISyncProcessor processor: mSyncProcessors )
- {
- processor.checkSync( mBits );
- }
- }
+ public MultiSyncPatternMatcher(ISyncDetectListener syncDetectListener, int syncLossThreshold, int syncSize)
+ {
+ mSyncDetectListener = syncDetectListener;
+ mSyncLossThreshold = syncLossThreshold;
+ Validate.isTrue(syncSize < 64);
- /**
- * Adds a sync processor to receive the bit stream.
- */
- public void add( ISyncProcessor processor )
- {
- mSyncProcessors.add( processor );
- }
+ //Setup a bit mask of all ones the length of the sync pattern
+ mMask = (long)((Math.pow(2, syncSize)) - 1);
+ }
+
+ public void dispose()
+ {
+ mSyncProcessors.clear();
+ }
+
+ /**
+ * Processes two bits before checking sync processors for a match.
+ */
+ public void receive(boolean bit1, boolean bit2)
+ {
+ mBits = Long.rotateLeft(mBits, 1);
+
+ mBits &= mMask;
+
+ if(bit1)
+ {
+ mBits += 1;
+ }
+
+ mBits = Long.rotateLeft(mBits, 1);
+
+ mBits &= mMask;
+
+ if(bit2)
+ {
+ mBits += 1;
+ }
+
+ mBitCount += 2;
+
+ for(ISyncProcessor processor : mSyncProcessors)
+ {
+ if(processor.checkSync(mBits))
+ {
+ mSyncDetectListener.syncDetected();
+ mBitCount = 0;
+ }
+ }
+
+ if(mBitCount > mSyncLossThreshold)
+ {
+ mBitCount = 0;
+ mSyncDetectListener.syncLost();
+ }
+ }
+
+ /**
+ * Processes one bit before checking sync processors for a match.
+ */
+ public void receive(boolean bit)
+ {
+ mBits = Long.rotateLeft(mBits, 1);
+
+ mBits &= mMask;
+
+ if(bit)
+ {
+ mBits += 1;
+ }
+
+ for(ISyncProcessor processor : mSyncProcessors)
+ {
+ processor.checkSync(mBits);
+ }
+ }
+
+ /**
+ * Adds a sync processor to receive the bit stream.
+ */
+ public void add(ISyncProcessor processor)
+ {
+ mSyncProcessors.add(processor);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java
index c2748f83..6362e160 100644
--- a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java
+++ b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java
@@ -1,50 +1,68 @@
+/*******************************************************************************
+ * 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
+ *
+ ******************************************************************************/
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
public class SoftSyncDetector implements ISyncProcessor
{
- private ISyncDetectListener mListener;
- private long mPattern;
- private int mThreshold;
-
- public SoftSyncDetector( ISyncDetectListener listener, long pattern, int threshold )
- {
- this( pattern, threshold );
-
- mListener = listener;
- }
-
- public SoftSyncDetector( long pattern, int threshold )
- {
- mPattern = pattern;
- mThreshold = threshold;
- }
-
- public void dispose()
- {
- mListener = null;
- }
+ private ISyncDetectListener mListener;
+ private long mPattern;
+ private int mThreshold;
- @Override
- public void checkSync( long value )
- {
- long difference = value ^ mPattern;
-
- if( ( difference == 0 || Long.bitCount( difference ) <= mThreshold ) &&
- mListener != null )
- {
- mListener.syncDetected();
- }
- }
-
- public void setThreshold( int threshold )
- {
- mThreshold = threshold;
- }
-
- public void setListener( ISyncDetectListener listener )
- {
- mListener = listener;
- }
+ public SoftSyncDetector(ISyncDetectListener listener, long pattern, int threshold)
+ {
+ this(pattern, threshold);
+
+ mListener = listener;
+ }
+
+ public SoftSyncDetector(long pattern, int threshold)
+ {
+ mPattern = pattern;
+ mThreshold = threshold;
+ }
+
+ public void dispose()
+ {
+ mListener = null;
+ }
+
+ @Override
+ public boolean checkSync(long value)
+ {
+ long difference = value ^ mPattern;
+
+ if((difference == 0 || Long.bitCount(difference) <= mThreshold) &&
+ mListener != null)
+ {
+ mListener.syncDetected();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void setThreshold(int threshold)
+ {
+ mThreshold = threshold;
+ }
+
+ public void setListener(ISyncDetectListener listener)
+ {
+ mListener = listener;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/bits/SyncDetector.java b/src/main/java/io/github/dsheirer/bits/SyncDetector.java
index 23aa6be0..aedaceec 100644
--- a/src/main/java/io/github/dsheirer/bits/SyncDetector.java
+++ b/src/main/java/io/github/dsheirer/bits/SyncDetector.java
@@ -1,35 +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
+ *
+ ******************************************************************************/
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
public class SyncDetector implements ISyncProcessor
{
- private long mPattern;
- private ISyncDetectListener mListener;
+ private long mPattern;
+ private ISyncDetectListener mListener;
- public SyncDetector( long pattern )
- {
- mPattern = pattern;
- }
-
- public SyncDetector( long pattern, ISyncDetectListener listener )
- {
- this( pattern );
-
- mListener = listener;
- }
-
- public void setListener( ISyncDetectListener listener )
- {
- mListener = listener;
- }
+ public SyncDetector(long pattern)
+ {
+ mPattern = pattern;
+ }
- @Override
- public void checkSync( long value )
- {
- if( value == mPattern && mListener != null )
- {
- mListener.syncDetected();
- }
- }
+ public SyncDetector(long pattern, ISyncDetectListener listener)
+ {
+ this(pattern);
+
+ mListener = listener;
+ }
+
+ public void setListener(ISyncDetectListener listener)
+ {
+ mListener = listener;
+ }
+
+ @Override
+ public boolean checkSync(long value)
+ {
+ if(value == mPattern)
+ {
+ if(mListener != null)
+ {
+ mListener.syncDetected();
+ }
+
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java
index f2d069d5..bc71e4f1 100644
--- a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java
@@ -35,17 +35,16 @@ import io.github.dsheirer.module.decode.event.ICallEventProvider;
import io.github.dsheirer.sample.IOverflowListener;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.ISourceEventListener;
+import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.heartbeat.Heartbeat;
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static io.github.dsheirer.source.SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
-
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
- IDecoderStateEventProvider, ISourceEventListener, IHeartbeatListener, ISquelchStateProvider,
- IAttributeChangeRequestListener, IOverflowListener
+ IDecoderStateEventProvider, ISourceEventListener, ISourceEventProvider, IHeartbeatListener,
+ ISquelchStateProvider, IAttributeChangeRequestListener, IOverflowListener
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class);
@@ -57,12 +56,13 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
private Listener mCallEventListener;
private Listener mDecoderStateListener;
private Listener mSquelchStateListener;
+ private Listener mExternalSourceEventListener;
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
private ChannelType mChannelType;
private TrafficChannelManager mTrafficChannelEndListener;
private CallEvent mTrafficChannelCallEvent;
- private SourceChangeListener mSourceChangeListener;
+ private SourceEventListener mInternalSourceEventListener;
private boolean mSquelchLocked = false;
private boolean mSelected = false;
@@ -160,12 +160,12 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
@Override
public Listener getSourceEventListener()
{
- if(mSourceChangeListener == null)
+ if(mInternalSourceEventListener == null)
{
- mSourceChangeListener = new SourceChangeListener();
+ mInternalSourceEventListener = new SourceEventListener();
}
- return mSourceChangeListener;
+ return mInternalSourceEventListener;
}
private boolean isStandardChannel()
@@ -255,6 +255,17 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
}
}
+ /**
+ * Broadcasts the source event to a registered external source event listener
+ */
+ protected void broadcast(SourceEvent sourceEvent)
+ {
+ if(mExternalSourceEventListener != null)
+ {
+ mExternalSourceEventListener.receive(sourceEvent);
+ }
+ }
+
/**
* Sets the squelch state listener
*/
@@ -516,18 +527,46 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
}
/**
- * Listener to receive frequency change events and rebroadcast them to the decoder states
+ * Registers the listener to receive source events from the channel state
*/
- public class SourceChangeListener implements Listener
+ @Override
+ public void setSourceEventListener(Listener listener)
+ {
+ mExternalSourceEventListener = listener;
+ }
+
+ /**
+ * De-Registers a listener from receiving source events from the channel state
+ */
+ @Override
+ public void removeSourceEventListener()
+ {
+ mExternalSourceEventListener = null;
+ }
+
+ /**
+ * Listener to receive source events.
+ */
+ public class SourceEventListener implements Listener
{
@Override
public void receive(SourceEvent sourceEvent)
{
- if(sourceEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
+ switch(sourceEvent.getEvent())
{
- long frequency = sourceEvent.getValue().longValue();
-
- broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
+ case NOTIFICATION_FREQUENCY_CHANGE:
+ //Rebroadcast source frequency change events for the decoder(s) to process
+ long frequency = sourceEvent.getValue().longValue();
+ broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
+ break;
+ case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
+ //Rebroadcast frequency error measurements to external listener if we're currently
+ //in an active (ie sync locked) state.
+ if(getState().isActiveState())
+ {
+ broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), mChannelType.name()));
+ }
+ break;
}
}
}
diff --git a/src/main/java/io/github/dsheirer/channel/state/State.java b/src/main/java/io/github/dsheirer/channel/state/State.java
index d6e5b09f..a4047e7f 100644
--- a/src/main/java/io/github/dsheirer/channel/state/State.java
+++ b/src/main/java/io/github/dsheirer/channel/state/State.java
@@ -109,6 +109,17 @@ public enum State
public abstract boolean canChangeTo(State state);
+ /**
+ * Indicates that this state is an active (ie decoding) state and is one of the states
+ * enumerated in the CALL_STATES enumeration set.
+ *
+ * @return true if this state is an active state
+ */
+ public boolean isActiveState()
+ {
+ return CALL_STATES.contains(this);
+ }
+
public String getDisplayValue()
{
return mDisplayValue;
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java
index 8be164e1..a569117e 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java
@@ -556,6 +556,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//ignore
break;
+ case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
+ //Rebroadcast so that the tuner source can process this event
+ mSourceEventBroadcaster.broadcast(sourceEvent);
+ break;
default:
mLog.error("Received unrecognized source event from polyphase channel source [" +
sourceEvent.getEvent() + "]");
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java
deleted file mode 100644
index ce46030c..00000000
--- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelResultsBuffer.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*******************************************************************************
- * 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
- *
- ******************************************************************************/
-package io.github.dsheirer.dsp.filter.channelizer;
-
-import java.util.Arrays;
-
-public class PolyphaseChannelResultsBuffer
-{
- private long mTimestamp;
- private int mPointer = 0;
- private int mMaxSize;
- private float[][] mChannelResults;
-
- /**
- * Buffer containing an array of polyphase channelizer complex channel results.
- * @param timestamp reference relative to the first channel results.
- * @param maxSize of channel results to store in this buffer
- */
- public PolyphaseChannelResultsBuffer(long timestamp, int maxSize)
- {
- mTimestamp = timestamp;
- mMaxSize = maxSize;
- }
-
- /**
- * Reference timestamp relative to the first channel results contained in this buffer.
- * @return reference timestamp in milliseconds since epoch
- */
- public long getTimestamp()
- {
- return mTimestamp;
- }
-
- /**
- * Indicates if this buffer is full
- */
- public boolean isFull()
- {
- return mPointer >= mMaxSize;
- }
-
- /**
- * Indicates if this buffer is empty
- */
- public boolean isEmpty()
- {
- return mPointer == 0;
- }
-
- /**
- * Adds the channel results to this buffer
- * @param channelResults to add
- * @throws IllegalArgumentException if the buffer is full or if the size of the channel results array is a different
- * length than any of the preceeding channel results that were added to this buffer
- */
- public void add(float[] channelResults) throws IllegalArgumentException
- {
- if(mPointer >= mMaxSize)
- {
- throw new IllegalArgumentException("Buffer is full - cannot add new channel results");
- }
-
- if(mPointer == 0)
- {
- mChannelResults = new float[mMaxSize][channelResults.length];
- }
- else
- {
- int currentLength = mChannelResults[0].length;
-
- if(channelResults.length != currentLength)
- {
- throw new IllegalArgumentException("Channel results length [" + channelResults.length + "] cannot be " +
- "a different length than the channel results that are already contained in this buffer [" +
- currentLength + "]");
- }
- }
-
- mChannelResults[mPointer++] = channelResults;
- }
-
- /**
- * Channel results array contained in this buffer
- */
- public float[][] getChannelResults()
- {
- //If this buffer is full, return all of the channel results
- if(mPointer == mMaxSize)
- {
- return mChannelResults;
- }
- //Otherwise, truncate the results to return
- else
- {
- return Arrays.copyOf(mChannelResults, mPointer);
- }
- }
-
- /**
- * Extracts a single I/Q interleaved sample buffer from the channel results
- * @param iChannelIndex to extract for the inphase sample. The q index is assumed to be one greater than i.
- * @return float[] containing interleaved I and Q samples
- * @throws IllegalArgumentException if this buffer doesn't contain any samples or if the requested channel is
- * not within the channel range of the contained channel results.
- */
- public float[] getChannel(int iChannelIndex)
- {
- if(isEmpty())
- {
- throw new IllegalArgumentException("Buffer is empty - cannot extract channel [" + iChannelIndex + "]");
- }
-
- if(!isValidChannelIndex(iChannelIndex))
- {
- throw new IllegalArgumentException("Channel [" + iChannelIndex + "] is not valid for the contained channel " +
- "results -- max channel is " + getMaxChannelIndex());
- }
-
- float[] samples = new float[mChannelResults.length * 2];
- int pointer = 0;
-
- int qChannelIndex = iChannelIndex + 1;
-
- for(int x = 0; x < mPointer; x++)
- {
- float[] channelResults = mChannelResults[x];
-
- samples[pointer++] = channelResults[iChannelIndex];
- samples[pointer++] = channelResults[qChannelIndex];
- }
-
- return samples;
- }
-
- /**
- * Indicates if the channel argument is valid for the contained channel results.
- * @param channelIndex number (0 to N) to check
- * @return false if the buffer is empty or if the channel is greater than the contained channel results
- */
- private boolean isValidChannelIndex(int channelIndex)
- {
- return channelIndex <= getMaxChannelIndex();
- }
-
- /**
- * Maximum channel index represented in the channel results contained in this buffer.
- * @return max channel number, or 0 if the buffer is empty
- */
- private int getMaxChannelIndex()
- {
- if(isEmpty())
- {
- return 0;
- }
-
- return mChannelResults[0].length;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java
index 222e0690..1872fb96 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelSource.java
@@ -237,4 +237,11 @@ public class PolyphaseChannelSource extends TunerChannelSource
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
}
+
+ @Override
+ public String toString()
+ {
+ return "POLYPHASE [" + mPolyphaseChannelOutputProcessor.getInputChannelCount() + "] " +
+ getTunerChannel().getFrequency();
+ }
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java
index 599832fb..ec27da9a 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/ChannelOutputProcessor.java
@@ -62,6 +62,12 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
mChannelResultsQueue = new OverflowableReusableBufferTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5));
}
+ @Override
+ public int getPolyphaseChannelIndexCount()
+ {
+ return mInputChannelCount;
+ }
+
public void dispose()
{
if(mChannelResultsQueue != null)
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java
index 70eb7c4a..3b88b07e 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/output/IPolyphaseChannelOutputProcessor.java
@@ -59,6 +59,11 @@ public interface IPolyphaseChannelOutputProcessor
*/
void setPolyphaseChannelIndices(List indexes);
+ /**
+ * List of current polyphase channel indices for the output processor
+ */
+ int getPolyphaseChannelIndexCount();
+
/**
* Updates the synthesis filter taps for this output processor
* @param filter
diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java
index 2317af18..cde125aa 100644
--- a/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java
+++ b/src/main/java/io/github/dsheirer/dsp/fsk/LTRDecoder.java
@@ -134,6 +134,12 @@ public class LTRDecoder implements Listener, ISyncDetectListener
mSynchronizationMonitor.syncDetected();
}
+ @Override
+ public void syncLost()
+ {
+ //no-op
+ }
+
/**
* Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is
* greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is
diff --git a/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java b/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java
index 1608a3d6..7d9746b7 100644
--- a/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java
+++ b/src/main/java/io/github/dsheirer/dsp/fsk/SynchronizationMonitor.java
@@ -30,13 +30,13 @@ import org.slf4j.LoggerFactory;
*
* Synchronization States:
*
- * -COARSE - default state. demoted from MEDIUM after one message period with no sync detection.
+ * -COARSE - default state. demoted from MEDIUM after one message period with no sync detection.
*
- * -MEDIUM - promoted from COARSE when one sync detection occurs after two message periods, or demoted from FINE
- * after two message periods with no sync detections.
+ * -MEDIUM - promoted from COARSE when one sync detection occurs after two message periods, or demoted from FINE
+ * after two message periods with no sync detections.
*
- * -FINE - promoted from MEDIUM after two or more consecutive sync detections within the preceding two or more
- * message periods.
+ * -FINE - promoted from MEDIUM after two or more consecutive sync detections within the preceding two or more
+ * message periods.
*/
public class SynchronizationMonitor implements ISyncDetectListener
{
@@ -50,6 +50,7 @@ public class SynchronizationMonitor implements ISyncDetectListener
/**
* Constructs the synchronization monitor for the specified message symbol length.
+ *
* @param messageLength in symbols.
*/
public SynchronizationMonitor(int messageLength)
@@ -127,6 +128,12 @@ public class SynchronizationMonitor implements ISyncDetectListener
updateSyncState();
}
+ @Override
+ public void syncLost()
+ {
+ //no-op
+ }
+
/**
* Increments the symbol count for each decoded symbol.
*/
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/AdaptivePLLGainMonitor.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/AdaptivePLLGainMonitor.java
new file mode 100644
index 00000000..eb48da17
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/AdaptivePLLGainMonitor.java
@@ -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
+ *
+ ******************************************************************************/
+package io.github.dsheirer.dsp.psk.pll;
+
+import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
+import io.github.dsheirer.module.decode.p25.P25Decoder;
+import io.github.dsheirer.source.SourceEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Monitors decode sync events to adaptively control PLL loop gain.
+ */
+public class AdaptivePLLGainMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
+{
+ private final static Logger mLog = LoggerFactory.getLogger(AdaptivePLLGainMonitor.class);
+
+ private static final int MAX_SYNC_COUNT = 6;
+ private CostasLoop mCostasLoop;
+ private P25Decoder mP25Decoder;
+ private PLLGain mPLLGain = PLLGain.LEVEL_1;
+ private int mSyncCount;
+
+ /**
+ * Constructs an adaptive monitor to monitor the sync state of a decoder and adjust the gain
+ * level of the costas loop accordingly.
+ *
+ * @param costasLoop to receive adaptive gain updates.
+ * @param p25Decoder to receive frequency error source events
+ */
+ public AdaptivePLLGainMonitor(CostasLoop costasLoop, P25Decoder p25Decoder)
+ {
+ mCostasLoop = costasLoop;
+ mCostasLoop.setPLLGain(mPLLGain);
+ mCostasLoop.setFrequencyErrorProcessor(this);
+ mP25Decoder = p25Decoder;
+ }
+
+ /**
+ * Sync detection event. Updates the running sync count and updates the PLL gain level.
+ */
+ @Override
+ public void syncDetected()
+ {
+ mSyncCount++;
+ update();
+ }
+
+ /**
+ * Sync loss event. Updates the running sync count and updates the PLL gain level.
+ */
+ @Override
+ public void syncLost()
+ {
+ mSyncCount -= 2;
+ update();
+ }
+
+ /**
+ * Updates the gain state for a sync detect or sync loss
+ */
+ private void update()
+ {
+ if(mSyncCount < 0)
+ {
+ mSyncCount = 0;
+ return;
+ }
+
+ if(mSyncCount > MAX_SYNC_COUNT)
+ {
+ mSyncCount = MAX_SYNC_COUNT;
+ return;
+ }
+
+ PLLGain gain = PLLGain.fromSyncCount(mSyncCount);
+
+ //Only update the costas loop if the gain value changes
+ if(gain != mPLLGain)
+ {
+ mPLLGain = gain;
+ mCostasLoop.setPLLGain(mPLLGain);
+ }
+ }
+
+ /**
+ * Resets the monitor and commands the PLL to use the highest gain level.
+ */
+ public void reset()
+ {
+ mSyncCount = 0;
+ mPLLGain = PLLGain.LEVEL_1;
+ mCostasLoop.setPLLGain(mPLLGain);
+ }
+
+ @Override
+ public void processFrequencyError(long frequencyError)
+ {
+ //Only rebroadcast the costas loop frequency error if the gain level has progressed above the
+ //default gain of 1, meaning that we have received at least 2 sync events
+ if(mPLLGain != PLLGain.LEVEL_1)
+ {
+ mP25Decoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
index 9c7968ee..3fe40ea5 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
@@ -41,10 +41,12 @@ public class CostasLoop implements IPhaseLockedLoop
private double mDamping = Math.sqrt(2.0) / 2.0;
private double mAlphaGain;
private double mBetaGain;
- private Tracking mTracking = Tracking.FASTEST;
+ private PLLGain mPLLGain = PLLGain.LEVEL_1;
private double mSymbolRate;
private double mSampleRate;
- private int mCounter;
+ private IFrequencyErrorProcessor mFrequencyErrorProcessor;
+ private long mFrequencyError;
+ private int mBaudCounter;
/**
* Costas Loop for tracking and correcting frequency error in a received carrier signal.
@@ -60,6 +62,17 @@ public class CostasLoop implements IPhaseLockedLoop
updateLoopBandwidth();
}
+ /**
+ * Registers the listener to receive the measured frequency error as tracked by the PLL at one
+ * second intervals.
+ *
+ * @param listener to receive frequency error measurement.
+ */
+ public void setFrequencyErrorProcessor(IFrequencyErrorProcessor listener)
+ {
+ mFrequencyErrorProcessor = listener;
+ }
+
/**
* 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.
@@ -89,21 +102,21 @@ public class CostasLoop implements IPhaseLockedLoop
*/
private void updateLoopBandwidth()
{
- double bandwidth = TWO_PI / mTracking.getLoopBandwidth();
+ double bandwidth = TWO_PI / mPLLGain.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
+ * Sets the PLLGain state for this costas loop. The PLLGain state affects the aggressiveness of the alpha/beta
* gain values in synchronizing with the signal carrier.
*
- * @param tracking
+ * @param PLLGain
*/
- public void setTracking(Tracking tracking)
+ public void setPLLGain(PLLGain PLLGain)
{
- mTracking = tracking;
+ mPLLGain = PLLGain;
updateLoopBandwidth();
}
@@ -159,7 +172,7 @@ public class CostasLoop implements IPhaseLockedLoop
mLoopFrequency += (mBetaGain * phaseError);
mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError);
- /* Normalize phase between +/- 2 * PI */
+ //Normalize phase between +/- 2 * PI
if(mLoopPhase > TWO_PI)
{
mLoopPhase -= TWO_PI;
@@ -170,7 +183,7 @@ public class CostasLoop implements IPhaseLockedLoop
mLoopPhase += TWO_PI;
}
- /* Limit frequency to +/- maximum loop frequency */
+ //Limit frequency to +/- maximum loop frequency
if(mLoopFrequency > mMaximumLoopFrequency)
{
mLoopFrequency = mMaximumLoopFrequency;
@@ -181,17 +194,25 @@ public class CostasLoop implements IPhaseLockedLoop
mLoopFrequency = -mMaximumLoopFrequency;
}
- //TODO: broadcast channel offset to the tuner to use in auto-adjusting tuner PPM
-// mCounter++;
-//
-// if(mCounter > (5 * mSymbolRate))
-// {
-// mCounter = 0;
-// mLog.info("Current Costas Loop Frequency: " + mLoopFrequency + " [" +
-// (int)(mSampleRate / TWO_PI * mLoopFrequency) + " Hz Offset] sample rate [" + mSampleRate + "]");
-// }
+ //Broadcast current frequency error measurement once a second to an external listener
+ mBaudCounter++;
+
+ if(mBaudCounter > mSymbolRate)
+ {
+ mBaudCounter = 0;
+
+ mFrequencyError = (long)(mSampleRate / TWO_PI * mLoopFrequency);
+
+ if(mFrequencyErrorProcessor != null)
+ {
+ mFrequencyErrorProcessor.processFrequencyError(mFrequencyError);
+ }
+ }
}
+ /**
+ * Resets the PLL internal tracking values
+ */
@Override
public void reset()
{
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/IFrequencyErrorProcessor.java
similarity index 68%
rename from src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java
rename to src/main/java/io/github/dsheirer/dsp/psk/pll/IFrequencyErrorProcessor.java
index a4611d20..f891b5b2 100644
--- a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/IFrequencyErrorProcessor.java
@@ -16,27 +16,14 @@
package io.github.dsheirer.dsp.psk.pll;
/**
- * Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
+ * Interface for passing frequency error measurements.
*/
-public enum Tracking
+public interface IFrequencyErrorProcessor
{
- FASTEST(50.0),
- FAST(100.0),
- NORMAL(200.0),
- SLOW(400.0);
-
- private double mLoopBandwidth;
-
- Tracking(double loopBandwidth)
- {
- mLoopBandwidth = loopBandwidth;
- }
-
/**
- * Loop bandwidth setting
+ * Provides a frequency error measurement to the listener.
+ *
+ * @param error value as measured by the provider
*/
- public double getLoopBandwidth()
- {
- return mLoopBandwidth;
- }
+ void processFrequencyError(long error);
}
diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/PLLGain.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/PLLGain.java
new file mode 100644
index 00000000..482a444e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/PLLGain.java
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * 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
+ *
+ ******************************************************************************/
+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 PLLGain
+{
+ LEVEL_1(50.0, 0, 1),
+ LEVEL_2(75.0, 2, 4),
+ LEVEL_3(100.0, 5, 6),
+ LEVEL_4(125.0, 7, 8),
+ LEVEL_5(150.0, 9, 10);
+
+ private double mLoopBandwidth;
+ private int mRangeStart;
+ private int mRangeEnd;
+
+ PLLGain(double loopBandwidth, int start, int end)
+ {
+ mLoopBandwidth = loopBandwidth;
+ mRangeStart = start;
+ mRangeEnd = end;
+ }
+
+ /**
+ * Loop bandwidth setting
+ */
+ public double getLoopBandwidth()
+ {
+ return mLoopBandwidth;
+ }
+
+ public int getRangeStart()
+ {
+ return mRangeStart;
+ }
+
+ public int getRangeEnd()
+ {
+ return mRangeEnd;
+ }
+
+ /**
+ * Look up PLL gain from a running sync detection count.
+ *
+ * @param syncCount 0-10
+ * @return PLL gain appropriate for the sync detection value
+ */
+ public static PLLGain fromSyncCount(int syncCount)
+ {
+ if(syncCount <= LEVEL_1.getRangeEnd())
+ {
+ return LEVEL_1;
+ }
+ else if(syncCount <= LEVEL_2.getRangeEnd())
+ {
+ return LEVEL_2;
+ }
+ else if(syncCount <= LEVEL_3.getRangeEnd())
+ {
+ return LEVEL_3;
+ }
+ else if(syncCount <= LEVEL_4.getRangeEnd())
+ {
+ return LEVEL_4;
+ }
+ else
+ {
+ return LEVEL_5;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java b/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java
index 5a47798a..139deefb 100644
--- a/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java
+++ b/src/main/java/io/github/dsheirer/dsp/symbol/ISyncDetectListener.java
@@ -15,7 +15,13 @@
******************************************************************************/
package io.github.dsheirer.dsp.symbol;
+/**
+ * Listener interface to be notified each time a sync pattern has been detected and/or when the sync
+ * has been lost.
+ */
public interface ISyncDetectListener
{
- void syncDetected();
+ void syncDetected();
+
+ void syncLost();
}
diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java
index b762fab1..41f6341c 100644
--- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java
+++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/LJ1200Pane.java
@@ -44,6 +44,12 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
{
mLog.debug("Tower Sync Detected!");
}
+
+ @Override
+ public void syncLost()
+ {
+ //no-op
+ }
});
mDecoder.getTransponderMessageFramer().setSyncDetectListener(new ISyncDetectListener()
{
@@ -52,6 +58,12 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
{
mLog.debug("Transponder Sync Detected!");
}
+
+ @Override
+ public void syncLost()
+ {
+ //no-op
+ }
});
}
diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java
index 98807bfa..431a86c0 100644
--- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java
+++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MDC1200Pane.java
@@ -44,6 +44,12 @@ public class MDC1200Pane extends AbstractAFSK1200Pane
{
mLog.debug("Sync Detected!");
}
+
+ @Override
+ public void syncLost()
+ {
+ mLog.debug("Sync Lost!");
+ }
});
}
diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java
index 83910f71..5ddbf912 100644
--- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java
+++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/MPT1327Pane.java
@@ -44,6 +44,12 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
{
// mLog.debug("Control Sync Detected!");
}
+
+ @Override
+ public void syncLost()
+ {
+ //no-op
+ }
});
mDecoder.getTrafficMessageFramer().setSyncDetectListener(new ISyncDetectListener()
{
@@ -52,6 +58,12 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
{
// mLog.debug("Traffic Sync Detected!");
}
+
+ @Override
+ public void syncLost()
+ {
+ //no-op
+ }
});
}
diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java
index 1f96e25e..f136836b 100644
--- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java
+++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/P25Phase1LSMPane.java
@@ -176,7 +176,7 @@ public class P25Phase1LSMPane extends ComplexDecoderPane
{
if(mPLLFrequencyLineChart == null)
{
- mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40);
+ mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -2500, 2500, 200, 40);
}
return mPLLFrequencyLineChart;
@@ -186,7 +186,7 @@ public class P25Phase1LSMPane extends ComplexDecoderPane
{
if(mSamplesPerSymbolLineChart == null)
{
- mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40);
+ mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 4.5, 7.0, 0.1, 40);
}
return mSamplesPerSymbolLineChart;
diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java
index 9b8b997e..feee0a58 100644
--- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java
+++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/Tait1200Pane.java
@@ -44,6 +44,12 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
{
mLog.debug("ANI Sync Detected!");
}
+
+ @Override
+ public void syncLost()
+ {
+
+ }
});
mDecoder.getGPSMessageFramer().setSyncDetectListener(new ISyncDetectListener()
{
@@ -52,6 +58,12 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
{
mLog.debug("GPS Sync Detected!");
}
+
+ @Override
+ public void syncLost()
+ {
+
+ }
});
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java b/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java
index 3bf17098..7d5d4462 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/C4FMSymbolFilter.java
@@ -149,7 +149,7 @@ public class C4FMSymbolFilter implements Listener, ISourceEventL
private static final int SAMPLE_RATE = 48000;
private static final int SYMBOL_RATE = 4800;
- /* Tracking loop gain constant */
+ /* PLLGain loop gain constant */
private static final double K_SYMBOL_SPREAD = 0.0100;
/* Constraints on symbol spreading */
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
index f88348cf..0fe9f4a1 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
@@ -132,7 +132,7 @@ public abstract class P25Decoder extends Decoder implements ISourceEventListener
* issue frequency correction requests to the channel source.
* @param sourceEvent to broadcast
*/
- protected void broadcast(SourceEvent sourceEvent)
+ public void broadcast(SourceEvent sourceEvent)
{
if(mSourceEventListener != null)
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java
index f29512f8..a67eeed5 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java
@@ -23,8 +23,8 @@ import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
+import io.github.dsheirer.dsp.psk.pll.AdaptivePLLGainMonitor;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
-import io.github.dsheirer.dsp.psk.pll.Tracking;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
@@ -36,10 +36,12 @@ import java.util.Map;
public class P25DecoderC4FM extends P25Decoder
{
private final static Logger mLog = LoggerFactory.getLogger(P25DecoderC4FM.class);
- protected static final float SAMPLE_COUNTER_GAIN = 0.5f;
+
+ protected static final float SAMPLE_COUNTER_GAIN = 0.3f;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator;
protected CostasLoop mCostasLoop;
+ protected AdaptivePLLGainMonitor mPLLGainMonitor;
protected P25MessageFramer mMessageFramer;
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
private Map mBasebandFilters = new HashMap<>();
@@ -54,7 +56,7 @@ public class P25DecoderC4FM extends P25Decoder
public P25DecoderC4FM(AliasList aliasList)
{
super(4800.0, aliasList);
- setSampleRate(48000.0);
+ setSampleRate(25000.0);
}
public void setSampleRate(double sampleRate)
@@ -64,14 +66,16 @@ public class P25DecoderC4FM extends P25Decoder
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
- mCostasLoop.setTracking(Tracking.FASTEST);
+ mPLLGainMonitor = new AdaptivePLLGainMonitor(mCostasLoop, this);
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
- //Message framer can issue a symbol-inversion correction request to the PLL when detected
- mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
+ //The Costas Loop receives symbol-inversion correction requests when detected.
+ //The PLL gain monitor receives sync detect/loss signals from the message framer
+ mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop, mPLLGainMonitor);
mMessageFramer.setListener(getMessageProcessor());
+ mMessageFramer.setSampleRate(sampleRate);
mQPSKDemodulator.setSymbolListener(mMessageFramer);
}
@@ -165,6 +169,10 @@ public class P25DecoderC4FM extends P25Decoder
mCostasLoop.reset();
setSampleRate(sourceEvent.getValue().doubleValue());
break;
+ case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
+ //Reset the PLL if/when the tuner PPM changes so that we can re-lock
+ mCostasLoop.reset();
+ break;
}
}
@@ -183,5 +191,6 @@ public class P25DecoderC4FM extends P25Decoder
public void reset()
{
mCostasLoop.reset();
+ mPLLGainMonitor.reset();
}
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java
index 835c9769..4f2356dd 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderLSM.java
@@ -22,20 +22,17 @@ import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
+import io.github.dsheirer.dsp.psk.pll.AdaptivePLLGainMonitor;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
-import io.github.dsheirer.dsp.psk.pll.Tracking;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.source.SourceEvent;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class P25DecoderLSM extends P25Decoder
{
- private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class);
- protected static final float SAMPLE_COUNTER_GAIN = 0.5f;
+ protected static final float SAMPLE_COUNTER_GAIN = 0.3f;
private Map mBasebandFilters = new HashMap<>();
private ComplexFIRFilter2 mBasebandFilter;
@@ -43,6 +40,7 @@ public class P25DecoderLSM extends P25Decoder
protected DQPSKGardnerDemodulator mQPSKDemodulator;
protected P25MessageFramer mMessageFramer;
protected CostasLoop mCostasLoop;
+ protected AdaptivePLLGainMonitor mPLLGainMonitor;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
/**
@@ -54,11 +52,12 @@ public class P25DecoderLSM extends P25Decoder
public P25DecoderLSM(AliasList aliasList)
{
super(4800.0, aliasList);
- setSampleRate(48000.0);
+ setSampleRate(25000.0);
}
/**
* Sets or changes the channel sample rate
+ *
* @param sampleRate in hertz
*/
public void setSampleRate(double sampleRate)
@@ -68,14 +67,17 @@ public class P25DecoderLSM extends P25Decoder
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
- mCostasLoop.setTracking(Tracking.NORMAL);
+ mPLLGainMonitor = new AdaptivePLLGainMonitor(mCostasLoop, this);
+
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
- //Message framer can issue a symbol-inversion correction request to the PLL when detected
- mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
+ //The Costas Loop receives symbol-inversion correction requests when detected.
+ //The PLL gain monitor receives sync detect/loss signals from the message framer
+ mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop, mPLLGainMonitor);
mMessageFramer.setListener(getMessageProcessor());
+ mMessageFramer.setSampleRate(sampleRate);
mQPSKDemodulator.setSymbolListener(mMessageFramer);
}
@@ -97,13 +99,18 @@ public class P25DecoderLSM extends P25Decoder
/**
* Filters the complex buffer and returns a new reusable complex buffer with the filtered contents.
+ *
* @param reusableComplexBuffer to filter
* @return filtered complex buffer
*/
protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer)
{
+ //No additional filtering of the channel is currently needed, since the polyphase channelizer
+ //provides the filtering.
+ return reusableComplexBuffer;
+
//User accounting of the incoming buffer is handled by the filter
- return mBasebandFilter.filter(reusableComplexBuffer);
+// return mBasebandFilter.filter(reusableComplexBuffer);
}
/**
@@ -119,6 +126,10 @@ public class P25DecoderLSM extends P25Decoder
setSampleRate(sourceEvent.getValue().doubleValue());
mCostasLoop.reset();
break;
+ case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
+ //Reset the PLL if/when the tuner PPM changes so that we can re-lock
+ mCostasLoop.reset();
+ break;
}
}
@@ -129,6 +140,7 @@ public class P25DecoderLSM extends P25Decoder
public void reset()
{
mCostasLoop.reset();
+ mPLLGainMonitor.reset();
}
public void dispose()
@@ -163,7 +175,7 @@ public class P25DecoderLSM extends P25Decoder
if(filter == null)
{
- filter = FilterFactory.getLowPass(48000, 7250, 8000, 60,
+ filter = FilterFactory.getLowPass(getSampleRate(), 7250, 8000, 60,
WindowType.HANN, true);
mBasebandFilters.put(getSampleRate(), filter);
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java
index 7419861d..4f6579b1 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25MessageFramer.java
@@ -36,11 +36,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.List;
public class P25MessageFramer implements Listener
{
- private final static Logger mLog =
- LoggerFactory.getLogger(P25MessageFramer.class);
+ private final static Logger mLog = LoggerFactory.getLogger(P25MessageFramer.class);
/* Determines the threshold for sync pattern soft matching */
private static final int SYNC_MATCH_THRESHOLD = 2;
@@ -49,9 +49,10 @@ public class P25MessageFramer implements Listener
/* Costas Loop phase lock error correction values. A phase lock error of
* 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An
* error of 180 degrees requires a correction of 1/2 of the symbol rate */
- public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d;
- public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d;
- public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d;
+ public static final double DEFAULT_SAMPLE_RATE = 25000.0;
+ public static final double FREQUENCY_PHASE_CORRECTION_90_DEGREES = 1200.0;
+ public static final double FREQUENCY_PHASE_CORRECTION_180_DEGREES = 2400.0;
+
public static final int TSBK_BEGIN = 64;
public static final int TSBK_CRC_START = 144;
public static final int TSBK_END = 260;
@@ -71,9 +72,12 @@ public class P25MessageFramer implements Listener
private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD);
- private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher(48);
- private ArrayList mAssemblers =
- new ArrayList();
+ private PLLPhaseInversionDetector mInversionDetector90CW;
+ private PLLPhaseInversionDetector mInversionDetector90CCW;
+ private PLLPhaseInversionDetector mInversionDetector180;
+
+ private MultiSyncPatternMatcher mMatcher;
+ private List mAssemblers = new ArrayList();
private Listener mListener;
private AliasList mAliasList;
@@ -88,10 +92,13 @@ public class P25MessageFramer implements Listener
* to the message length, and then broadcast that bit buffer to the registered
* listener.
*/
- public P25MessageFramer(AliasList aliasList)
+ public P25MessageFramer(AliasList aliasList, ISyncDetectListener syncDetectListener)
{
mAliasList = aliasList;
+ //Assign the sync detect listener to the matcher with a sync loss threshold equal to the longest message length
+ mMatcher = new MultiSyncPatternMatcher(syncDetectListener, DataUnitID.LDU1.getMessageLength(), 48);
+
mPrimarySyncDetector.setListener(new ISyncDetectListener()
{
@Override
@@ -106,6 +113,12 @@ public class P25MessageFramer implements Listener
}
}
}
+
+ @Override
+ public void syncLost()
+ {
+ //no-op
+ }
});
mMatcher.add(mPrimarySyncDetector);
@@ -122,25 +135,41 @@ public class P25MessageFramer implements Listener
mAssemblers.add(new P25MessageAssembler());
}
- public P25MessageFramer(AliasList aliasList, IPhaseLockedLoop phaseLockedLoop)
+ public P25MessageFramer(AliasList aliasList, IPhaseLockedLoop phaseLockedLoop, ISyncDetectListener syncDetectListener)
{
- this(aliasList);
+ this(aliasList, syncDetectListener);
if(phaseLockedLoop != null)
{
//Add additional sync pattern detectors to detect when we get 90/180 degree out of phase sync pattern
//detections so that we can apply correction to the phase locked loop
- mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW,
- phaseLockedLoop, PHASE_CORRECTION_90_DEGREES));
+ mInversionDetector90CW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW,
+ phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_90_DEGREES);
+ mMatcher.add(mInversionDetector90CW);
- mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW,
- phaseLockedLoop, -PHASE_CORRECTION_90_DEGREES));
+ mInversionDetector90CCW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW,
+ phaseLockedLoop, DEFAULT_SAMPLE_RATE, -FREQUENCY_PHASE_CORRECTION_90_DEGREES);
+ mMatcher.add(mInversionDetector90CCW);
- mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180,
- phaseLockedLoop, PHASE_CORRECTION_180_DEGREES));
+ mInversionDetector180 = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180,
+ phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_180_DEGREES);
+ mMatcher.add(mInversionDetector180);
}
}
+ /**
+ * Updates the incoming sample stream sample rate to allow the PLL phase inversion detectors to
+ * recalculate their internal phase correction values.
+ *
+ * @param sampleRate of the incoming sample stream
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ mInversionDetector180.setSampleRate(sampleRate);
+ mInversionDetector90CW.setSampleRate(sampleRate);
+ mInversionDetector90CCW.setSampleRate(sampleRate);
+ }
+
public void dispose()
{
for(P25MessageAssembler a : mAssemblers)
@@ -671,34 +700,46 @@ public class P25MessageFramer implements Listener
}
/**
- * Sync pattern detector to listen for costas loop phase lock errors and
- * apply a phase correction to the costas loop so that we don't miss any
- * messages.
+ * Sync pattern detector to listen for costas loop phase lock errors and apply a phase correction to the costas
+ * loop so that we don't miss any messages.
*
- * When the costas loop locks with a +/- 90 degree or 180 degree phase
- * error, the slicer will incorrectly apply the symbol pattern rotated left
- * or right by the phase error. However, we can detect these rotated sync
- * patterns and apply immediate phase correction so that message processing
- * can continue.
+ * When the costas loop locks with a +/- 90 degree or 180 degree phase error, the slicer will incorrectly apply
+ * the symbol pattern rotated left or right by the phase error. However, we can detect these rotated sync patterns
+ * and apply immediate phase correction so that message processing can continue.
*/
public class PLLPhaseInversionDetector extends SyncDetector
{
+ private FrameSync mFrameSync;
private IPhaseLockedLoop mPhaseLockedLoop;
- private double mCorrection;
+ private double mSampleRate;
+ private double mFrequencyCorrection;
+ private double mPllCorrection;
- public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double correction)
+ /**
+ * Constructs the PLL phase inversion detector.
+ *
+ * @param frameSync pattern to monitor for detecting phase inversion errors
+ * @param phaseLockedLoop to receive phase correction values
+ * @param sampleRate of the incoming sample stream
+ * @param frequencyCorrection to apply to the PLL. Examples:
+ * QPSK +/-90 degree correction: +/-SYMBOL RATE / 4.0
+ * QPSK 180 degree correction: SYMBOL RATE / 2.0
+ */
+ public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double sampleRate,
+ double frequencyCorrection)
{
super(frameSync.getSync());
-
+ mFrameSync = frameSync;
mPhaseLockedLoop = phaseLockedLoop;
- mCorrection = correction;
+ mFrequencyCorrection = frequencyCorrection;
+ setSampleRate(sampleRate);
setListener(new ISyncDetectListener()
{
@Override
public void syncDetected()
{
- mPhaseLockedLoop.correctInversion(mCorrection);
+ mPhaseLockedLoop.correctInversion(mPllCorrection);
/* Since we detected a sync pattern, start a message assembler */
for(P25MessageAssembler assembler : mAssemblers)
@@ -710,7 +751,24 @@ public class P25MessageFramer implements Listener
}
}
}
+
+ @Override
+ public void syncLost()
+ {
+ //no-op
+ }
});
+
+ }
+
+ /**
+ * Sets or adjusts the sample rate so that the phase inversion correction value can be recalculated.
+ * @param sampleRate
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ mSampleRate = sampleRate;
+ mPllCorrection = 2.0 * Math.PI * mFrequencyCorrection / mSampleRate;
}
}
}
diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java
index 405fbb85..9c1179d1 100644
--- a/src/main/java/io/github/dsheirer/source/SourceEvent.java
+++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java
@@ -31,6 +31,8 @@ public class SourceEvent
NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED,
NOTIFICATION_FREQUENCY_CHANGE,
NOTIFICATION_FREQUENCY_CORRECTION_CHANGE,
+ NOTIFICATION_MEASURED_FREQUENCY_ERROR,
+ NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED,
NOTIFICATION_SAMPLE_RATE_CHANGE,
NOTIFICATION_STOP_SAMPLE_STREAM,
@@ -124,6 +126,14 @@ public class SourceEvent
return mSource;
}
+ /**
+ * Sets the source associated with this event
+ */
+ public void setSource(Source source)
+ {
+ mSource = source;
+ }
+
/**
* Indicates if this event contains an optional source
*/
@@ -189,6 +199,28 @@ public class SourceEvent
return new SourceEvent(Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, frequencyCorrection);
}
+ /**
+ * Creates a new frequency error measurement notification event.
+ *
+ * @param frequencyError in hertz
+ */
+ public static SourceEvent frequencyErrorMeasurement(long frequencyError)
+ {
+ return new SourceEvent(Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR, frequencyError);
+ }
+
+ /**
+ * Creates a new frequency error measurement notification event. This event is different from
+ * the raw frequency error measurement and indicates that the current state of the PLL is
+ * locked and tracking the signal.
+ *
+ * @param frequencyError in hertz
+ */
+ public static SourceEvent frequencyErrorMeasurementSyncLocked(long frequencyError, String eventDescription)
+ {
+ return new SourceEvent(Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED, frequencyError, eventDescription);
+ }
+
/**
* Creates a new sample rate change event
*
@@ -288,7 +320,7 @@ public class SourceEvent
{
return "SOURCE EVENT:" + mEvent +
" VALUE:" + (mValue != null ? mValue : "(empty)") +
- " SOURCE:" + (mSource != null ? mSource.getClass() : "(null)") +
+ " SOURCE:" + (mSource != null ? mSource.toString() : "(null)") +
" DESCRIPTION:" + (mEventDescription != null ? mEventDescription : "");
}
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/FrequencyErrorCorrectionManager.java b/src/main/java/io/github/dsheirer/source/tuner/FrequencyErrorCorrectionManager.java
new file mode 100644
index 00000000..47a98645
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/FrequencyErrorCorrectionManager.java
@@ -0,0 +1,149 @@
+/*******************************************************************************
+ * 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
+ *
+ ******************************************************************************/
+package io.github.dsheirer.source.tuner;
+
+import io.github.dsheirer.source.SourceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Monitors measured frequency error PPM values from the tuner and (when enabled) applies the current
+ * error measurement to the tuner controller to adjust the tuner's PPM and align the frequency value.
+ */
+public class FrequencyErrorCorrectionManager
+{
+ private final static Logger mLog = LoggerFactory.getLogger(FrequencyErrorCorrectionManager.class);
+
+ private static final double FREQUENCY_CORRECTION_ERROR_THRESHOLD = 0.4;
+ private static final long AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS = 30 * 1000; //30 seconds
+ private long mObservationPeriodStart;
+ private double mPPMRequired;
+ private boolean mEnabled = true;
+ private TunerController mTunerController;
+
+ /**
+ * Constructs a frequency error correction manager for the specified tuner controller.
+ */
+ public FrequencyErrorCorrectionManager(TunerController tunerController)
+ {
+ mTunerController = tunerController;
+ }
+
+ /**
+ * Enables or disables automatic frequency PPM correction.
+ * @param enabled to true to turn on automatic PPM adjustments.
+ */
+ public void setEnabled(boolean enabled)
+ {
+ mEnabled = enabled;
+ }
+
+ /**
+ * Resets monitoring
+ */
+ public void reset()
+ {
+ mObservationPeriodStart = 0;
+ mPPMRequired = 0;
+ }
+
+ /**
+ * Applies a PPM adjustment value to the tuner's current PPM setting, when auto-correction is enabled.
+ */
+ private void applyCorrection()
+ {
+ if(mEnabled && mTunerController.hasMeasuredFrequencyError())
+ {
+ double frequencyCorrection = mTunerController.getFrequencyCorrection();
+ frequencyCorrection -= mTunerController.getPPMFrequencyError();
+
+ try
+ {
+ mLog.info("Auto-Correcting Tuner PPM to [" + frequencyCorrection + "]");
+
+ mTunerController.setFrequencyCorrection(frequencyCorrection);
+ reset();
+ }
+ catch(SourceException se)
+ {
+ mLog.error("Error while applying automatic frequency error PPM correction value: " + frequencyCorrection);
+ }
+ }
+ }
+
+ /**
+ * Updates the monitor with a new PPM frequency error measurement from the tuner.
+ */
+ public void updatePPM(double ppm)
+ {
+ if(ppm > FREQUENCY_CORRECTION_ERROR_THRESHOLD)
+ {
+ if(mObservationPeriodStart == 0)
+ {
+ mObservationPeriodStart = System.currentTimeMillis();
+ mPPMRequired = ppm;
+ }
+ else
+ {
+ if(mPPMRequired > 0)
+ {
+ mPPMRequired = ppm;
+
+ if(mObservationPeriodStart + AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS < System.currentTimeMillis())
+ {
+ applyCorrection();
+ }
+ }
+ else
+ {
+ //Opposite polarity ppm value ... restart the timer
+ mObservationPeriodStart = System.currentTimeMillis();
+ mPPMRequired = ppm;
+ }
+ }
+ }
+ else if(ppm < -FREQUENCY_CORRECTION_ERROR_THRESHOLD)
+ {
+ if(mObservationPeriodStart == 0)
+ {
+ mObservationPeriodStart = System.currentTimeMillis();
+ mPPMRequired = ppm;
+ }
+ else
+ {
+ if(mPPMRequired < 0)
+ {
+ mPPMRequired = ppm;
+
+ if(mObservationPeriodStart + AUTO_CORRECTION_OBSERVATION_PERIOD_MILLISECONDS < System.currentTimeMillis())
+ {
+ applyCorrection();
+ }
+ }
+ else
+ {
+ //Opposite polarity ppm value ... restart the timer
+ mObservationPeriodStart = System.currentTimeMillis();
+ mPPMRequired = ppm;
+ }
+ }
+ }
+ else
+ {
+ reset();
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java
index d74f52a8..857db597 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/Tuner.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/Tuner.java
@@ -22,6 +22,8 @@ import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.TunerEvent.Event;
import io.github.dsheirer.source.tuner.manager.ChannelSourceManager;
import io.github.dsheirer.source.tuner.manager.TunerChannelSourceManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Tuner provides an interface to a software or hardware tuner controller that provides I/Q sample data coupled with a
@@ -29,9 +31,12 @@ import io.github.dsheirer.source.tuner.manager.TunerChannelSourceManager;
*/
public abstract class Tuner implements ISourceEventProcessor
{
+ private final static Logger mLog = LoggerFactory.getLogger(Tuner.class);
+
private Broadcaster mTunerEventBroadcaster = new Broadcaster<>();
private ChannelSourceManager mChannelSourceManager;
private TunerController mTunerController;
+ private TunerFrequencyErrorMonitor mTunerFrequencyErrorMonitor;
private String mName;
/**
@@ -49,6 +54,9 @@ public abstract class Tuner implements ISourceEventProcessor
mChannelSourceManager = new TunerChannelSourceManager(mTunerController);
//Register to receive channel count change notifications
mChannelSourceManager.addSourceEventListener(this::process);
+
+ mTunerFrequencyErrorMonitor = new TunerFrequencyErrorMonitor(this);
+ mTunerFrequencyErrorMonitor.start();
}
@Override
@@ -60,16 +68,23 @@ public abstract class Tuner implements ISourceEventProcessor
broadcast(new TunerEvent(Tuner.this, Event.CHANNEL_COUNT));
break;
case NOTIFICATION_FREQUENCY_CHANGE:
- broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY));
+ broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY_UPDATED));
+ break;
+ case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
+ broadcast(new TunerEvent(Tuner.this, Event.FREQUENCY_ERROR_UPDATED));
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
- broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE));
+ broadcast(new TunerEvent(Tuner.this, Event.SAMPLE_RATE_UPDATED));
break;
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
broadcast(new TunerEvent(Tuner.this, Event.LOCK_STATE_CHANGE));
break;
+ case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
+ mTunerFrequencyErrorMonitor.receive(event);
+ break;
default:
+ mLog.debug("Unrecognized Source Event: " + event.toString());
break;
}
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java
index 88b65f68..4ccfac71 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java
@@ -41,6 +41,7 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
private int mMiddleUnusableHalfBandwidth;
private double mUsableBandwidthPercentage;
private Listener mSourceEventListener;
+ private int mMeasuredFrequencyError;
/**
* Abstract tuner controller class. The tuner controller manages frequency bandwidth and currently tuned channels
@@ -242,6 +243,46 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor,
return (int)(getUsableBandwidth() / 2);
}
+ /**
+ * Current measured frequency error as received/reported from certain downstream decoders.
+ * @return measured frequency error in hertz averaged over 1-second intervals
+ */
+ public int getMeasuredFrequencyError()
+ {
+ return mMeasuredFrequencyError;
+ }
+
+ /**
+ * Indicates if this tuner controller has a non-zero measured frequency error value.
+ */
+ public boolean hasMeasuredFrequencyError()
+ {
+ return mMeasuredFrequencyError != 0;
+ }
+
+ /**
+ * Calculates the current PPM error value as measured by certain downstream decoders.
+ * @return
+ */
+ public double getPPMFrequencyError()
+ {
+ if(hasMeasuredFrequencyError())
+ {
+ return mMeasuredFrequencyError / ((double)getFrequency() / 1E6d);
+ }
+
+ return 0.0d;
+ }
+
+ /**
+ * Sets the measured frequency error average.
+ * @param measuredFrequencyError in hertz averaged over a 5 second interval.
+ */
+ public void setMeasuredFrequencyError(int measuredFrequencyError)
+ {
+ mMeasuredFrequencyError = measuredFrequencyError;
+ }
+
/**
* Sets the listener to be notified any time that the tuner changes frequency
* or bandwidth/sample rate.
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerEvent.java b/src/main/java/io/github/dsheirer/source/tuner/TunerEvent.java
index e4f85cc4..c7e9ca8b 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerEvent.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerEvent.java
@@ -23,12 +23,13 @@ public class TunerEvent
public enum Event
{
- ADD,
- LOCK_STATE_CHANGE,
- REMOVE,
CHANNEL_COUNT,
- FREQUENCY,
- SAMPLE_RATE,
+ FREQUENCY_UPDATED,
+ FREQUENCY_ERROR_UPDATED,
+ LOCK_STATE_CHANGE,
+ MEASURED_FREQUENCY_ERROR_UPDATED,
+ SAMPLE_RATE_UPDATED,
+
CLEAR_MAIN_SPECTRAL_DISPLAY,
REQUEST_MAIN_SPECTRAL_DISPLAY,
REQUEST_NEW_SPECTRAL_DISPLAY;
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerFrequencyErrorMonitor.java b/src/main/java/io/github/dsheirer/source/tuner/TunerFrequencyErrorMonitor.java
new file mode 100644
index 00000000..d27c48cd
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerFrequencyErrorMonitor.java
@@ -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
+ *
+ ******************************************************************************/
+package io.github.dsheirer.source.tuner;
+
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.source.SourceEvent;
+import io.github.dsheirer.util.ThreadPool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Monitors frequency error measurements received from certain decoders (e.g. P25) and averages those
+ * measurements over an interval period. Averaged measurement values are applied to the tuner controller
+ * and broadcast as a TunerEvent for visual display.
+ */
+public class TunerFrequencyErrorMonitor implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(TunerFrequencyErrorMonitor.class);
+ private static final int PROCESSING_INTERVAL_SECONDS = 5;
+ private LinkedTransferQueue mMeasurementsQueue = new LinkedTransferQueue<>();
+ private List mProcessingMeasurements = new ArrayList();
+ private ScheduledFuture> mTimerHandle;
+ private Tuner mTuner;
+
+ /**
+ * Constructs a monitor for the tuner argument.
+ * @param tuner to monitor for frequency error measurements
+ */
+ public TunerFrequencyErrorMonitor(Tuner tuner)
+ {
+ mTuner = tuner;
+ }
+
+ /**
+ * Primary method for receiving frequency error measurements from downstream decoders.
+ */
+ @Override
+ public void receive(SourceEvent sourceEvent)
+ {
+ if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED)
+ {
+ mMeasurementsQueue.add(sourceEvent.getValue().intValue());
+ }
+ }
+
+ /**
+ * Calculates the average frequency error measurement value from discrete measurements received from
+ * the decoders over the interval period.
+ */
+ private void process()
+ {
+ mMeasurementsQueue.drainTo(mProcessingMeasurements);
+
+ if(mProcessingMeasurements.size() > 0)
+ {
+ int sum = 0;
+
+ for(Integer measurement: mProcessingMeasurements)
+ {
+ sum += measurement;
+ }
+
+ //Note: the measurements are the frequency correction being applied to each channel to compensate
+ //for the error int the channel. So, we report the negated value as the current error measurement
+ broadcast(-(sum / mProcessingMeasurements.size()));
+ }
+ else
+ {
+ broadcast(0);
+ }
+
+ mProcessingMeasurements.clear();
+ }
+
+ private void broadcast(int averageError)
+ {
+ mTuner.getTunerController().setMeasuredFrequencyError(averageError);
+ mTuner.broadcast(new TunerEvent(mTuner, TunerEvent.Event.MEASURED_FREQUENCY_ERROR_UPDATED));
+ }
+
+ /**
+ * Starts this monitor processing frequency error measurements received from select channel decoders
+ * and averaging those values over a 1 second interval for transmission as a tuner event.
+ */
+ public void start()
+ {
+ if(mTimerHandle == null)
+ {
+ mTimerHandle = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0,
+ PROCESSING_INTERVAL_SECONDS, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * Stops this monitor
+ */
+ public void stop()
+ {
+ if(mTimerHandle != null)
+ {
+ mTimerHandle.cancel(true);
+ mTimerHandle = null;
+ broadcast(0);
+ }
+ }
+
+ /**
+ * Runnable implementation to invoke the process() method when the timer fires.
+ */
+ public class Processor implements Runnable
+ {
+ @Override
+ public void run()
+ {
+ process();
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
index 54004b71..3350bcae 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
@@ -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
+ *
+ ******************************************************************************/
package io.github.dsheirer.source.tuner;
import io.github.dsheirer.properties.SystemProperties;
@@ -31,17 +46,19 @@ public class TunerModel extends AbstractTableModel implements Listener mTuners = new ArrayList<>();
private List> mTunerEventListeners = new ArrayList<>();
private DecimalFormat mFrequencyFormat = new DecimalFormat("0.00000");
private DecimalFormat mSampleRateFormat = new DecimalFormat("0.000");
+ private DecimalFormat mFrequencyErrorPPMFormat = new DecimalFormat("0.0");
private TunerConfigurationModel mTunerConfigurationModel;
@@ -206,15 +223,21 @@ public class TunerModel extends AbstractTableModel implements Listener 0);
break;
+ case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
+ //Rebroadcast these frequency measurement errors to the tuner and tuner controller
+ //for display and/or correction
+ broadcast(sourceEvent);
+ break;
default:
mLog.info("Unrecognized source event: " + sourceEvent);
break;
diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerConfiguration.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerConfiguration.java
index bb88c8ef..1c1ad8d4 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerConfiguration.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerConfiguration.java
@@ -30,6 +30,7 @@ public class R820TTunerConfiguration extends TunerConfiguration
private R820TTunerController.R820TVGAGain mVGAGain = R820TTunerController.R820TVGAGain.GAIN_210;
private double mFrequencyCorrection = 0.0d;
private RTL2832TunerController.SampleRate mSampleRate = RTL2832TunerController.SampleRate.RATE_2_400MHZ;
+ private boolean mAutoPPMCorrection = true;
/**
* Default constructor for JAXB
@@ -115,4 +116,23 @@ public class R820TTunerConfiguration extends TunerConfiguration
{
mSampleRate = sampleRate;
}
+
+ /**
+ * Indicates if automatic correction of PPM from measured frequency error is enabled/disabled.
+ * @return true if auto-correction is enabled.
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "auto_ppm_correction_enabled")
+ public boolean getAutoPPMCorrectionEnabled()
+ {
+ return mAutoPPMCorrection;
+ }
+
+ /**
+ * Sets the enabled state for auto-correction of PPM from measured frequency error values.
+ * @param enabled
+ */
+ public void setAutoPPMCorrectionEnabled(boolean enabled)
+ {
+ mAutoPPMCorrection = enabled;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java
index b8b3e1e8..d06681f2 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerController.java
@@ -23,6 +23,7 @@
package io.github.dsheirer.source.tuner.rtl.r820t;
import io.github.dsheirer.source.SourceException;
+import io.github.dsheirer.source.tuner.FrequencyErrorCorrectionManager;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController;
@@ -33,7 +34,6 @@ import org.usb4java.DeviceDescriptor;
import org.usb4java.LibUsb;
import org.usb4java.LibUsbException;
-import javax.swing.JPanel;
import javax.usb.UsbException;
import java.nio.ByteBuffer;
@@ -70,12 +70,36 @@ public class R820TTunerController extends RTL2832TunerController
0x6C, 0x83, 0x80, 0x00, 0x0F, 0x00, 0xC0, 0x30,
0x48, 0xCC, 0x60, 0x00, 0x54, 0xAE, 0x4A, 0xC0};
- private JPanel mEditor;
+ public FrequencyErrorCorrectionManager mFrequencyErrorCorrectionManager;
public R820TTunerController(Device device, DeviceDescriptor deviceDescriptor) throws SourceException
{
super(device, deviceDescriptor, MIN_FREQUENCY, MAX_FREQUENCY, DC_SPIKE_AVOID_BUFFER,
USABLE_BANDWIDTH_PERCENT);
+
+ mFrequencyErrorCorrectionManager = new FrequencyErrorCorrectionManager(this);
+ }
+
+ /**
+ * Manager for applying automatic frequency error PPM adjustments to the tuner controller based on
+ * frequency error measurements received from certain downstream decoders (e.g. P25).
+ * @return manager
+ */
+ public FrequencyErrorCorrectionManager getFrequencyErrorCorrectionManager()
+ {
+ return mFrequencyErrorCorrectionManager;
+ }
+
+ /**
+ * Overrides updates for measured frequency error so that the updates can also be applied to the
+ * frequency error correction manager for automatic PPM updating.
+ * @param measuredFrequencyError in hertz averaged over a 5 second interval.
+ */
+ @Override
+ public void setMeasuredFrequencyError(int measuredFrequencyError)
+ {
+ super.setMeasuredFrequencyError(measuredFrequencyError);
+ getFrequencyErrorCorrectionManager().updatePPM(getPPMFrequencyError());
}
@Override
@@ -106,6 +130,8 @@ public class R820TTunerController extends RTL2832TunerController
double correction = config.getFrequencyCorrection();
setFrequencyCorrection(correction);
+ getFrequencyErrorCorrectionManager().setEnabled(config.getAutoPPMCorrectionEnabled());
+
R820TGain masterGain = config.getMasterGain();
setGain(masterGain, true);
diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java
index fd2f009a..47620750 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java
@@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory;
import org.usb4java.LibUsbException;
import javax.swing.JButton;
+import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
@@ -64,6 +65,7 @@ public class R820TTunerEditor extends TunerConfigurationEditor
private JButton mTunerInfo;
private JComboBox mComboSampleRate;
private JSpinner mFrequencyCorrection;
+ private JCheckBox mAutoPPMEnabled;
private JComboBox mComboMasterGain;
private JComboBox mComboMixerGain;
private JComboBox mComboLNAGain;
@@ -202,6 +204,22 @@ public class R820TTunerEditor extends TunerConfigurationEditor
add(new JLabel("PPM:"));
add(mFrequencyCorrection);
+ add(new JLabel("")); //Space filler
+ add(new JLabel("")); //Space filler
+ add(new JLabel("")); //Space filler
+ mAutoPPMEnabled = new JCheckBox("PPM Auto-Correction");
+ mAutoPPMEnabled.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ boolean enabled = mAutoPPMEnabled.isSelected();
+ mController.getFrequencyErrorCorrectionManager().setEnabled(enabled);
+ save();
+ }
+ });
+ add(mAutoPPMEnabled);
+
add(new JSeparator(JSeparator.HORIZONTAL), "span,grow");
/**
@@ -435,6 +453,11 @@ public class R820TTunerEditor extends TunerConfigurationEditor
mFrequencyCorrection.setEnabled(enabled);
}
+ if(mAutoPPMEnabled.isEnabled() != enabled)
+ {
+ mAutoPPMEnabled.setEnabled(enabled);
+ }
+
updateSampleRateToolTip();
if(mController.isLocked())
@@ -524,6 +547,8 @@ public class R820TTunerEditor extends TunerConfigurationEditor
.getModel()).getNumber().doubleValue();
config.setFrequencyCorrection(value);
+ config.setAutoPPMCorrectionEnabled(mAutoPPMEnabled.isSelected());
+
config.setSampleRate((SampleRate)mComboSampleRate.getSelectedItem());
R820TGain gain = (R820TGain)mComboMasterGain.getSelectedItem();
@@ -560,6 +585,7 @@ public class R820TTunerEditor extends TunerConfigurationEditor
setControlsEnabled(true);
mConfigurationName.setText(config.getName());
mFrequencyCorrection.setValue(config.getFrequencyCorrection());
+ mAutoPPMEnabled.setSelected(config.getAutoPPMCorrectionEnabled());
mComboSampleRate.setSelectedItem(config.getSampleRate());
mComboMasterGain.setSelectedItem(config.getMasterGain());
mComboMixerGain.setSelectedItem(config.getMixerGain());
diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
index 8dc9e082..064c6f57 100644
--- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
+++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
@@ -217,7 +217,8 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
ReusableComplexBuffer reusableBuffer = mReusableComplexBufferQueue.getBuffer(samples.length);
- reusableBuffer.reloadFrom(samples, System.currentTimeMillis());
+ System.arraycopy(samples, 0, reusableBuffer.getSamples(), 0, samples.length);
+ reusableBuffer.setTimestamp(System.currentTimeMillis());
mListener.receive(reusableBuffer);
}
}
diff --git a/src/main/java/io/github/dsheirer/spectrum/WaterfallPanel.java b/src/main/java/io/github/dsheirer/spectrum/WaterfallPanel.java
index cdc1f507..7dd15a3d 100644
--- a/src/main/java/io/github/dsheirer/spectrum/WaterfallPanel.java
+++ b/src/main/java/io/github/dsheirer/spectrum/WaterfallPanel.java
@@ -455,7 +455,10 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
@Override
public void run()
{
- mMemoryImageSource.newPixels(mPixels, mColorModel, 0, mDFTSize);
+ if(mMemoryImageSource != null)
+ {
+ mMemoryImageSource.newPixels(mPixels, mColorModel, 0, mDFTSize);
+ }
}
});
}