mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
#36 NBFM squelch control. Updates NBFM decoder to incorporate squelch control. Squelch is a configuration item and also available to adjust at runtime with new 'Channel' tab in Now Playing window. All decoders were updated to produce continuous channel power (dB) measurements that are displayed in the Channel tab's new Power Meter. (#1100)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package io.github.dsheirer.dsp.filter.iir;
|
||||
|
||||
/**
|
||||
* Single pole IIR filter
|
||||
*/
|
||||
public class SinglePoleIirFilter
|
||||
{
|
||||
/**
|
||||
* Decay value, in range: 0 <-> 1.0
|
||||
*/
|
||||
private double mAlpha;
|
||||
|
||||
/**
|
||||
* 1.0 - alpha
|
||||
*/
|
||||
private double mOneMinusAlpha;
|
||||
|
||||
/**
|
||||
* Current filter output value.
|
||||
*/
|
||||
private double mOutput;
|
||||
|
||||
/**
|
||||
* Constructs a single pole IIR filter
|
||||
* @param alpha decay value in range (0.0 - 1.0)
|
||||
*/
|
||||
public SinglePoleIirFilter(double alpha)
|
||||
{
|
||||
if(alpha < 0.0 || alpha > 1.0)
|
||||
{
|
||||
throw new IllegalArgumentException("alpha decay value must be in range: 0.0 - 1.0");
|
||||
}
|
||||
|
||||
mAlpha = alpha;
|
||||
mOneMinusAlpha = 1.0 - alpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the specified sample and returns the filter output
|
||||
* @param sample to process/filter
|
||||
* @return filtered output
|
||||
*/
|
||||
public double filter(double sample)
|
||||
{
|
||||
mOutput = (mOutput * mOneMinusAlpha) + (mAlpha * sample);
|
||||
return mOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current output value
|
||||
*/
|
||||
public double getValue()
|
||||
{
|
||||
return mOutput;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014,2015 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.dsp.squelch.PowerSquelch;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
|
||||
/**
|
||||
* FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
|
||||
*
|
||||
* Implements listener of source events to process runtime squelch threshold change request events
|
||||
* which are forwarded to the power squelch control.
|
||||
*/
|
||||
public class SquelchingFMDemodulator extends FMDemodulator implements Listener<SourceEvent>
|
||||
{
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("SquelchingFMDemodulator");
|
||||
private PowerSquelch mPowerSquelch;
|
||||
private boolean mSquelchChanged = false;
|
||||
|
||||
/**
|
||||
* Creates an FM demodulator instance with a default gain of 1.0.
|
||||
*/
|
||||
public SquelchingFMDemodulator(double alpha, double threshold, int ramp)
|
||||
{
|
||||
mPowerSquelch = new PowerSquelch(alpha, threshold, ramp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive notifications of squelch change events from the power squelch.
|
||||
*/
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mPowerSquelch.setSourceEventListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Demodulates the complex baseband sample buffer and returns a demodulated reusable buffer with the user count
|
||||
* set to 1. The complex baseband buffer's user count is decremented after demodulation.
|
||||
*
|
||||
* @param basebandSampleBuffer containing samples to demodulate
|
||||
* @return demodulated sample buffer.
|
||||
*/
|
||||
@Override
|
||||
public ReusableFloatBuffer demodulate(ReusableComplexBuffer basebandSampleBuffer)
|
||||
{
|
||||
setSquelchChanged(false);
|
||||
|
||||
ReusableFloatBuffer demodulatedBuffer = mReusableBufferQueue.getBuffer(basebandSampleBuffer.getSampleCount());
|
||||
|
||||
float[] basebandSamples = basebandSampleBuffer.getSamples();
|
||||
float[] demodulatedSamples = demodulatedBuffer.getSamples();
|
||||
float i,q;
|
||||
|
||||
for(int x = 0; x < basebandSamples.length; x += 2)
|
||||
{
|
||||
i = basebandSamples[x];
|
||||
q = basebandSamples[x + 1];
|
||||
|
||||
mPowerSquelch.process(i, q);
|
||||
|
||||
if(mPowerSquelch.isUnmuted() || mPowerSquelch.isDecay())
|
||||
{
|
||||
demodulatedSamples[x / 2] = demodulate(i, q);
|
||||
}
|
||||
else
|
||||
{
|
||||
demodulatedSamples[x / 2] = 0.0f;
|
||||
}
|
||||
|
||||
if(mPowerSquelch.isSquelchChanged())
|
||||
{
|
||||
setSquelchChanged(true);
|
||||
}
|
||||
}
|
||||
|
||||
basebandSampleBuffer.decrementUserCount();
|
||||
|
||||
return demodulatedBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the threshold for squelch control
|
||||
* @param threshold (dB)
|
||||
*/
|
||||
public void setSquelchThreshold(double threshold)
|
||||
{
|
||||
mPowerSquelch.setSquelchThreshold(threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the squelch state has changed during the processing of buffer(s)
|
||||
*/
|
||||
public boolean isSquelchChanged()
|
||||
{
|
||||
return mSquelchChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or resets the squelch changed flag.
|
||||
*/
|
||||
private void setSquelchChanged(boolean changed)
|
||||
{
|
||||
mSquelchChanged = changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the squelch state is currently muted
|
||||
*/
|
||||
public boolean isMuted()
|
||||
{
|
||||
return mPowerSquelch.isMuted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
//Only forward squelch threshold change request events
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
|
||||
{
|
||||
mPowerSquelch.receive(sourceEvent);
|
||||
}
|
||||
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
|
||||
{
|
||||
mPowerSquelch.receive(sourceEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.github.dsheirer.dsp.squelch;
|
||||
|
||||
/**
|
||||
* Interface for configurations that support squelch threshold setting.
|
||||
*/
|
||||
public interface ISquelchConfiguration
|
||||
{
|
||||
/**
|
||||
* Sets the squelch threshold
|
||||
* @param threshold (dB)
|
||||
*/
|
||||
void setSquelchThreshold(int threshold);
|
||||
|
||||
/**
|
||||
* Squelch threshold
|
||||
* @return threshold (dB)
|
||||
*/
|
||||
int getSquelchThreshold();
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package io.github.dsheirer.dsp.squelch;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.iir.SinglePoleIirFilter;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Power monitor. Provides periodic broadcast of current channel power from I/Q sample buffers.
|
||||
*/
|
||||
public class PowerMonitor
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(PowerMonitor.class);
|
||||
private int mPowerLevelBroadcastCount = 0;
|
||||
private int mPowerLevelBroadcastThreshold;
|
||||
private Listener<SourceEvent> mSourceEventListener;
|
||||
private SinglePoleIirFilter mPowerFilter = new SinglePoleIirFilter(0.1);
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public PowerMonitor()
|
||||
{
|
||||
mPowerLevelBroadcastThreshold = 25000; //Based on a default sample rate of 50 kHz
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sample rate to effect the frequency of power level notifications where the notifications are
|
||||
* sent twice a second.
|
||||
* @param sampleRate in hertz
|
||||
*/
|
||||
public void setSampleRate(int sampleRate)
|
||||
{
|
||||
mPowerLevelBroadcastThreshold = sampleRate / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a complex IQ sample and changes squelch state when the signal power is above or below the
|
||||
* threshold value.
|
||||
* @param inphase complex sample component
|
||||
* @param quadrature complex sample component
|
||||
*/
|
||||
public void process(double inphase, double quadrature)
|
||||
{
|
||||
mPowerLevelBroadcastCount++;
|
||||
|
||||
if(mPowerLevelBroadcastCount > mPowerLevelBroadcastThreshold)
|
||||
{
|
||||
mPowerFilter.filter(inphase * inphase + quadrature * quadrature);
|
||||
}
|
||||
|
||||
if(mPowerLevelBroadcastCount > (mPowerLevelBroadcastThreshold + 10))
|
||||
{
|
||||
mPowerLevelBroadcastCount = 0;
|
||||
broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPowerFilter.getValue())));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a complex baseband sample buffer. Note: this method does not decrement the user count
|
||||
* on the buffer.
|
||||
*
|
||||
* @param buffer to process
|
||||
*/
|
||||
public void process(ReusableComplexBuffer buffer)
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
int offset;
|
||||
|
||||
for(int x = 0; x < buffer.getSampleCount(); x++)
|
||||
{
|
||||
offset = x * 2;
|
||||
process(samples[offset], samples[offset + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive power level notifications and squelch threshold requests
|
||||
*/
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mSourceEventListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the source event to an optional register listener
|
||||
*/
|
||||
private void broadcast(SourceEvent event)
|
||||
{
|
||||
if(mSourceEventListener != null)
|
||||
{
|
||||
mSourceEventListener.receive(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
279
src/main/java/io/github/dsheirer/dsp/squelch/PowerSquelch.java
Normal file
279
src/main/java/io/github/dsheirer/dsp/squelch/PowerSquelch.java
Normal file
@@ -0,0 +1,279 @@
|
||||
package io.github.dsheirer.dsp.squelch;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.iir.SinglePoleIirFilter;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Power squelch
|
||||
*
|
||||
* Modeled after gnuradio complex power squelch:
|
||||
* https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/pwr_squelch_cc_impl.cc
|
||||
*/
|
||||
public class PowerSquelch implements Listener<SourceEvent>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(PowerSquelch.class);
|
||||
private State mState = State.MUTE;
|
||||
private SinglePoleIirFilter mFilter;
|
||||
private double mPower = 0.0f;
|
||||
private double mSquelchThreshold;
|
||||
private int mRampThreshold;
|
||||
private int mRampCount;
|
||||
private boolean mSquelchChanged = false;
|
||||
private boolean mStateChange = false;
|
||||
private int mPowerLevelBroadcastCount = 0;
|
||||
private int mPowerLevelBroadcastThreshold;
|
||||
private Listener<SourceEvent> mSourceEventListener;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* Testing against a 12.5 kHz analog FM modulated signal, the following parameters provided a
|
||||
* good responsiveness. A threshold of -80.0 dB seemed to trigger significant flapping during un-squelching.
|
||||
* - alpha: 0.0001
|
||||
* - threshold: 78.0 dB
|
||||
* - ramp: 4 (samples)
|
||||
*
|
||||
* @param alpha decay value of the single pole IIR filter in range: 0.0 - 1.0. The smaller the alpha value,
|
||||
* the slower the squelch response.
|
||||
* @param squelchThreshold in decibels. Signal power must exceed this threshold value for unsquelch.
|
||||
* @param ramp count of samples before transition between mute and unmute. Setting this value to zero
|
||||
* causes immediate mute and unmute. Set to higher count to prevent mute/unmute flapping.
|
||||
*/
|
||||
public PowerSquelch(double alpha, double squelchThreshold, int ramp)
|
||||
{
|
||||
mFilter = new SinglePoleIirFilter(alpha);
|
||||
setSquelchThreshold(squelchThreshold);
|
||||
mRampThreshold = ramp;
|
||||
mPowerLevelBroadcastThreshold = 25000; //Based on a default sample rate of 50 kHz
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sample rate to effect the frequency of power level notifications where the notifications are
|
||||
* sent twice a second.
|
||||
* @param sampleRate in hertz
|
||||
*/
|
||||
public void setSampleRate(int sampleRate)
|
||||
{
|
||||
mPowerLevelBroadcastThreshold = sampleRate / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Squelch threshold value
|
||||
* @return value in decibels
|
||||
*/
|
||||
public double getSquelchThreshold()
|
||||
{
|
||||
return 10.0 * Math.log10(mSquelchThreshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the squelch threshold
|
||||
* @param squelchThreshold in decibels
|
||||
*/
|
||||
public void setSquelchThreshold(double squelchThreshold)
|
||||
{
|
||||
mSquelchThreshold = Math.pow(10.0, squelchThreshold / 10.0);
|
||||
broadcast(SourceEvent.squelchThreshold(null, squelchThreshold));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a complex IQ sample and changes squelch state when the signal power is above or below the
|
||||
* threshold value.
|
||||
* @param inphase complex sample component
|
||||
* @param quadrature complex sample component
|
||||
*/
|
||||
public void process(double inphase, double quadrature)
|
||||
{
|
||||
mPower = mFilter.filter(inphase * inphase + quadrature * quadrature);
|
||||
|
||||
mPowerLevelBroadcastCount++;
|
||||
if(mPowerLevelBroadcastCount % mPowerLevelBroadcastThreshold == 0)
|
||||
{
|
||||
mPowerLevelBroadcastCount = 0;
|
||||
broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPower)));
|
||||
}
|
||||
|
||||
mStateChange = false;
|
||||
|
||||
switch(mState)
|
||||
{
|
||||
case MUTE -> {
|
||||
if(!mute())
|
||||
{
|
||||
if(mRampThreshold > 0)
|
||||
{
|
||||
mState = State.ATTACK;
|
||||
mRampCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
mState = State.UNMUTE;
|
||||
mStateChange = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
case ATTACK -> {
|
||||
if(mRampCount >= mRampThreshold)
|
||||
{
|
||||
mState = State.UNMUTE;
|
||||
mStateChange = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
mRampCount++;
|
||||
}
|
||||
}
|
||||
case DECAY -> {
|
||||
if(mRampCount <= 0)
|
||||
{
|
||||
mState = State.MUTE;
|
||||
mStateChange = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
mRampCount--;
|
||||
}
|
||||
}
|
||||
case UNMUTE -> {
|
||||
if(mute())
|
||||
{
|
||||
if(mRampThreshold > 0)
|
||||
{
|
||||
mState = State.DECAY;
|
||||
mRampCount--;
|
||||
}
|
||||
else
|
||||
{
|
||||
mState = State.MUTE;
|
||||
mStateChange = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Signal that the squelch state has changed if we modified the squelch state above.
|
||||
setSquelchChanged(mStateChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the current state is muted
|
||||
*/
|
||||
public boolean isMuted()
|
||||
{
|
||||
return mState == State.MUTE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the current state is unmuted
|
||||
*/
|
||||
public boolean isUnmuted()
|
||||
{
|
||||
return mState == State.UNMUTE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the current state is fading in (attach) or fading out (decay)
|
||||
*/
|
||||
public boolean isFading()
|
||||
{
|
||||
return isAttack() || isDecay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the squelch is in a ramp up attack state
|
||||
*/
|
||||
public boolean isAttack()
|
||||
{
|
||||
return mState == State.ATTACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the squelch is in a ramp down decay state
|
||||
*/
|
||||
public boolean isDecay()
|
||||
{
|
||||
return mState == State.DECAY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current power level
|
||||
* @return current power level in dB
|
||||
*/
|
||||
public float getPower()
|
||||
{
|
||||
return (float)(10.0 * Math.log10(mPower));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the current power level is below the threshold, indicating that the state should be muted.
|
||||
*/
|
||||
private boolean mute()
|
||||
{
|
||||
return mPower < mSquelchThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the squelch state has changed (muted > unmuted, or vice-versa)
|
||||
*/
|
||||
public boolean isSquelchChanged()
|
||||
{
|
||||
return mSquelchChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or resets the squelch changed flag
|
||||
*/
|
||||
public void setSquelchChanged(boolean changed)
|
||||
{
|
||||
mSquelchChanged = changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive power level notifications and squelch threshold requests
|
||||
*/
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mSourceEventListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the source event to an optional register listener
|
||||
*/
|
||||
private void broadcast(SourceEvent event)
|
||||
{
|
||||
if(mSourceEventListener != null)
|
||||
{
|
||||
mSourceEventListener.receive(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary method to receive requests for squelch threshold change
|
||||
*/
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
|
||||
{
|
||||
setSquelchThreshold(sourceEvent.getValue().doubleValue());
|
||||
}
|
||||
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
|
||||
{
|
||||
broadcast(SourceEvent.squelchThreshold(null, getSquelchThreshold()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State of squelch processing
|
||||
*/
|
||||
private enum State
|
||||
{
|
||||
ATTACK,
|
||||
DECAY,
|
||||
MUTE,
|
||||
UNMUTE;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user