/* * ***************************************************************************** * 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 org.apache.commons.math3.util.FastMath; 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 { private static final Logger mLog = LoggerFactory.getLogger(PowerSquelch.class); private State mState = State.MUTE; private SinglePoleIirFilter mFilter; private float mPower = 0.0f; private float mSquelchThreshold; private int mRampThreshold; private int mRampCount; private boolean mSquelchChanged = false; private boolean mStateChange = false; private int mPowerLevelBroadcastCount = 0; private int mPowerLevelBroadcastThreshold; 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 * - 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(float alpha, float 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 float getSquelchThreshold() { return 10.0f * (float)FastMath.log10(mSquelchThreshold); } /** * Sets the squelch threshold * @param squelchThreshold in decibels */ public void setSquelchThreshold(double squelchThreshold) { mSquelchThreshold = (float) FastMath.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(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); 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 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().floatValue()); } 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; } }