/* * ***************************************************************************** * Copyright (C) 2014-2023 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.squelch; import io.github.dsheirer.dsp.filter.iir.SinglePoleIirFilter; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.SourceEvent; import java.time.Duration; import org.apache.commons.math3.util.FastMath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Adaptive squelch control that optionally tracks the noise floor and auto-adjusts squelch threshold. * * Initially modeled on: https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/simple_squelch_cc_impl.cc */ public class AdaptiveSquelch implements Listener { private static final Logger mLog = LoggerFactory.getLogger(AdaptiveSquelch.class); private static final long AUTO_ADJUST_PERIOD_MS = Duration.ofSeconds(5).toMillis(); private SinglePoleIirFilter mFilter; private float mPower = 0.0f; private boolean mSquelch; private float mSquelchThreshold; private Float mNoiseFloor; private float mSquelchBuffer; private boolean mSquelchChanged = false; private int mPowerLevelBroadcastCount = 0; private int mPowerLevelBroadcastThreshold; private boolean mAutoTrackNoiseFloor; private long mLastAutoAdjustTimestamp; private Listener 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 * * @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 autoTrack the noise floor when squelched and periodically readjust the squelch threshold. */ public AdaptiveSquelch(float alpha, float squelchThreshold, boolean autoTrack) { mFilter = new SinglePoleIirFilter(alpha); setSquelchThreshold(squelchThreshold); mAutoTrackNoiseFloor = autoTrack; mLastAutoAdjustTimestamp = System.currentTimeMillis(); mPowerLevelBroadcastThreshold = 25000; //Based on a default sample rate of 50 kHz, so 2x/second } /** * 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 float getSquelchThreshold() { return toDb(mSquelchThreshold); } /** * Formats the value as decibels * @param value to format * @return formatted value. */ private float toDb(float value) { return 10.0f * (float)FastMath.log10(value); } /** * Converts from a decibel value * @param valueDb in decibels * @return not decibels */ private float fromDb(float valueDb) { return (float)FastMath.pow(10.0, valueDb / 10.0); } /** * Enables or disables the squelch auto-tracking feature where the squelch threshold is auto-adjusted based on the * observed channel power when the signal is not present (ie squelch state). * @param autoTrack true to turn on auto-track */ public void setSquelchAutoTrack(boolean autoTrack) { mAutoTrackNoiseFloor = autoTrack; broadcast(SourceEvent.squelchAutoTrack(getSquelchAutoTrack())); } /** * Current squelch auto-track setting. * @return true if auto-track is enabled. */ public boolean getSquelchAutoTrack() { return mAutoTrackNoiseFloor; } /** * Sets the squelch threshold * @param squelchThreshold in decibels */ public void setSquelchThreshold(float squelchThreshold) { float squelchThresholdDb = fromDb(squelchThreshold); setSquelchThreshold(squelchThresholdDb, true); } /** * Sets the squelch threshold. This method is synchronized to coordinate changes requested by the user/UI thread * and auto-track changes made by the stream processing thread. * * @param squelchThreshold value * @param resetNoiseFloor to reset the noise floor and squelch threshold */ private synchronized void setSquelchThreshold(float squelchThreshold, boolean resetNoiseFloor) { mSquelchThreshold = squelchThreshold; if(resetNoiseFloor) { //Set noise floor to null so that both the noise floor and the squelch buffer get updated after first power reading mNoiseFloor = null; } broadcast(SourceEvent.squelchThreshold(null, getSquelchThreshold())); } /** * Sets the squelch state * @param squelch true to squelch and false to unsquelch */ private void setSquelch(boolean squelch) { mSquelch = squelch; mSquelchChanged = true; } /** * 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(float inphase, float quadrature) { process(inphase * inphase + quadrature * quadrature); } /** * Processes a complex IQ sample and changes squelch state when the signal power is above or below the * threshold value. * @param magnitude of a complex sample (inphase * inphase + quadrature * quadrature) */ public void process(float magnitude) { mPower = mFilter.filter(magnitude); if(mSquelch && mPower >= mSquelchThreshold) { setSquelch(false); } else if(!mSquelch && mPower < mSquelchThreshold) { setSquelch(true); } mPowerLevelBroadcastCount++; if(mPowerLevelBroadcastCount % mPowerLevelBroadcastThreshold == 0) { mPowerLevelBroadcastCount = 0; broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPower))); if(mSquelch) { adjustSquelchThreshold(); } } } /** * With the user specified squelch threshold, establishes a squelch buffer (delta) from the actual noise floor and * then automatically updates the squelch threshold to be the current noise floor plus this threshold value. * * Note: this method should only be invoked from a squelch (true) state. */ private void adjustSquelchThreshold() { if(mAutoTrackNoiseFloor) { if(mNoiseFloor == null) { //We set the squelch buffer just once, after the squelch threshold is set and noise floor is set to null mSquelchBuffer = mSquelchThreshold - mPower; } mNoiseFloor = mPower; if((System.currentTimeMillis() - mLastAutoAdjustTimestamp) > AUTO_ADJUST_PERIOD_MS) { setSquelchThreshold(mNoiseFloor + mSquelchBuffer, false); mLastAutoAdjustTimestamp = System.currentTimeMillis(); } } } /** * Indicates if the current state is muted */ public boolean isMuted() { return mSquelch; } /** * Indicates if the current state is unmuted */ public boolean isUnmuted() { return !mSquelch; } /** * Current power level * @return current power level in dB */ public float getPower() { return (float)(10.0 * Math.log10(mPower)); } /** * 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 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) { switch(sourceEvent.getEvent()) { case REQUEST_CHANGE_SQUELCH_THRESHOLD: setSquelchThreshold(sourceEvent.getValue().floatValue()); break; case REQUEST_CURRENT_SQUELCH_THRESHOLD: broadcast(SourceEvent.squelchThreshold(null, getSquelchThreshold())); break; case REQUEST_CHANGE_SQUELCH_AUTO_TRACK: setSquelchAutoTrack(sourceEvent.getValue().intValue() == 1 ? true : false); break; case REQUEST_CURRENT_SQUELCH_AUTO_TRACK: broadcast(SourceEvent.squelchAutoTrack(getSquelchAutoTrack())); break; } } }