mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -58,11 +58,11 @@ public class FilterViewer extends Application
|
||||
*/
|
||||
private float[] getFilter()
|
||||
{
|
||||
int filterLength = 47;
|
||||
WindowType windowType = WindowType.HAMMING;
|
||||
int filterLength = 31;
|
||||
WindowType windowType = WindowType.BLACKMAN_HARRIS_7;
|
||||
|
||||
// float[] taps = FilterFactory.getHalfBand(filterLength, windowType);
|
||||
float[] taps = FilterFactory.getRootRaisedCosine(50000.0 / 4800.0, 20, 0.2f);
|
||||
float[] taps = FilterFactory.getHighPass(8000, 3000, filterLength, windowType);
|
||||
// float[] taps = FilterFactory.getRootRaisedCosine(50000.0 / 4800.0, 20, 0.2f);
|
||||
|
||||
// taps = HilbertTransform.HALF_BAND_FILTER_47_TAP;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -19,10 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -32,63 +28,22 @@ import org.slf4j.LoggerFactory;
|
||||
public abstract class VectorFMDemodulator implements IDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
private static final int BUFFER_OVERLAP = 1;
|
||||
|
||||
//Initialize buffers to be non-null and resize once the first buffer arrives
|
||||
private float[] mIBuffer = new float[1];
|
||||
private float[] mQBuffer = new float[1];
|
||||
private ScalarFMDemodulator mScalar;
|
||||
|
||||
public VectorFMDemodulator()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector species as specified by the implementing sub-class.
|
||||
* Access the scalar implementation when the sample buffer size is not a multiple of the vector size.
|
||||
*/
|
||||
protected abstract VectorSpecies<Float> getVectorSpecies();
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
protected ScalarFMDemodulator getScalarImplementation()
|
||||
{
|
||||
// VectorSpecies<Float> species= getVectorSpecies();
|
||||
|
||||
VectorUtilities.checkComplexArrayLength(i, q, getVectorSpecies());
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
if(mScalar == null)
|
||||
{
|
||||
mIBuffer = new float[i.length + BUFFER_OVERLAP];
|
||||
mQBuffer = new float[q.length + BUFFER_OVERLAP];
|
||||
mScalar = new ScalarFMDemodulator();
|
||||
}
|
||||
|
||||
//Copy last sample to beginning of buffer arrays
|
||||
mIBuffer[0] = mIBuffer[mIBuffer.length - 1];
|
||||
mQBuffer[0] = mQBuffer[mQBuffer.length - 1];
|
||||
|
||||
//Copy new samples to end of buffer arrays
|
||||
System.arraycopy(i, 0, mIBuffer, 1, i.length);
|
||||
System.arraycopy(q, 0, mQBuffer, 1, q.length);
|
||||
|
||||
float[] demodulated = new float[i.length];
|
||||
|
||||
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
|
||||
|
||||
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += getVectorSpecies().length())
|
||||
{
|
||||
previousI = FloatVector.fromArray(getVectorSpecies(), mIBuffer, bufferPointer);
|
||||
previousQ = FloatVector.fromArray(getVectorSpecies(), mQBuffer, bufferPointer);
|
||||
currentI = FloatVector.fromArray(getVectorSpecies(), mIBuffer, bufferPointer + 1);
|
||||
currentQ = FloatVector.fromArray(getVectorSpecies(), mQBuffer, bufferPointer + 1);
|
||||
|
||||
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
|
||||
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
|
||||
|
||||
//Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors
|
||||
demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO));
|
||||
|
||||
demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN);
|
||||
demod.intoArray(demodulated, bufferPointer);
|
||||
}
|
||||
|
||||
return demodulated;
|
||||
return mScalar;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
|
||||
*/
|
||||
public class VectorFMDemodulator128 implements IDemodulator
|
||||
public class VectorFMDemodulator128 extends VectorFMDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator128.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
@@ -44,9 +43,14 @@ public class VectorFMDemodulator128 implements IDemodulator
|
||||
{
|
||||
}
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i, q);
|
||||
}
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
|
||||
*/
|
||||
public class VectorFMDemodulator256 implements IDemodulator
|
||||
public class VectorFMDemodulator256 extends VectorFMDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator256.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
@@ -44,9 +43,14 @@ public class VectorFMDemodulator256 implements IDemodulator
|
||||
{
|
||||
}
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
|
||||
*/
|
||||
public class VectorFMDemodulator512 implements IDemodulator
|
||||
public class VectorFMDemodulator512 extends VectorFMDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator512.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
@@ -44,9 +43,14 @@ public class VectorFMDemodulator512 implements IDemodulator
|
||||
{
|
||||
}
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
|
||||
*/
|
||||
public class VectorFMDemodulator64 implements IDemodulator
|
||||
public class VectorFMDemodulator64 extends VectorFMDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator64.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
@@ -44,9 +43,14 @@ public class VectorFMDemodulator64 implements IDemodulator
|
||||
{
|
||||
}
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
{
|
||||
|
||||
@@ -37,6 +37,7 @@ public abstract class DifferentialDemodulator implements IDemodulator
|
||||
protected int mBufferOverlap;
|
||||
protected int mInterpolationOffset;
|
||||
protected final Interpolator mInterpolator = InterpolatorFactory.getInterpolator();
|
||||
private DifferentialDemodulatorScalar mScalar;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -59,6 +60,20 @@ public abstract class DifferentialDemodulator implements IDemodulator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scalar implementation for fallback when the sample buffer size is not compatible with the vector implementation.
|
||||
* @return scalar implementation lazy instantiated when needed.
|
||||
*/
|
||||
protected DifferentialDemodulatorScalar getScalarImplementation()
|
||||
{
|
||||
if(mScalar == null)
|
||||
{
|
||||
mScalar = new DifferentialDemodulatorScalar(mSampleRate, mSymbolRate);
|
||||
}
|
||||
|
||||
return mScalar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculated samples per symbol.
|
||||
* @return samples per symbol.
|
||||
|
||||
@@ -48,6 +48,11 @@ public class DifferentialDemodulatorVector128 extends DifferentialDemodulator
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
@@ -60,10 +65,6 @@ public class DifferentialDemodulatorVector128 extends DifferentialDemodulator
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Buffer length must be a multiple of " + VECTOR_SPECIES.length());
|
||||
}
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@ public class DifferentialDemodulatorVector256 extends DifferentialDemodulator
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
@@ -57,10 +62,6 @@ public class DifferentialDemodulatorVector256 extends DifferentialDemodulator
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Buffer length must be a multiple of " + VECTOR_SPECIES.length());
|
||||
}
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ public class DifferentialDemodulatorVector512 extends DifferentialDemodulator
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
@@ -59,10 +64,6 @@ public class DifferentialDemodulatorVector512 extends DifferentialDemodulator
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Buffer length must be a multiple of " + VECTOR_SPECIES.length());
|
||||
}
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.squelch;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
* Interface for a decoder to expose access to noise squelch controls and status registration endpoint(s).
|
||||
*/
|
||||
public interface INoiseSquelchController
|
||||
{
|
||||
/**
|
||||
* Registers the listener to receive noise squelch states every 10 milliseconds for UI display.
|
||||
* @param listener to receive states or pass null to de-register a listener.
|
||||
*/
|
||||
void setNoiseSquelchStateListener(Listener<NoiseSquelchState> listener);
|
||||
|
||||
/**
|
||||
* Sets the open and close noise thresholds in range 0.0 to 1.0, recommend: 0.1 open and 0.2 close
|
||||
* @param open for the open noise variance calculation.
|
||||
* @param close for the close noise variance calculation.
|
||||
*/
|
||||
void setNoiseThreshold(float open, float close);
|
||||
|
||||
/**
|
||||
* Sets the hysteresis threshold in units of 10 milliseconds
|
||||
* @param open in range 1-10, recommend: 4
|
||||
* @param close in range 1-10, recommend: 6
|
||||
*/
|
||||
void setHysteresisThreshold(int open, int close);
|
||||
|
||||
/**
|
||||
* Manual override of the squelch control to set an always un-squelch state (true) or automatic control (false).
|
||||
* @param override (true) or (false) to turn off squelch override.
|
||||
*/
|
||||
void setSquelchOverride(boolean override);
|
||||
}
|
||||
529
src/main/java/io/github/dsheirer/dsp/squelch/NoiseSquelch.java
Normal file
529
src/main/java/io/github/dsheirer/dsp/squelch/NoiseSquelch.java
Normal file
@@ -0,0 +1,529 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.squelch;
|
||||
|
||||
import io.github.dsheirer.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
|
||||
import io.github.dsheirer.dsp.window.WindowType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Squelch processor for unfiltered, demodulated audio that operators by high-pass filtering the audio to remove below
|
||||
* 3,000 Hertz and processes 10-millisecond audio buffers to calculate the noise variance across the samples. This
|
||||
* variance is compared to a noise threshold and uses time hysteresis in the form of consecutive buffers to toggle the
|
||||
* squelch open or closed. Uses a delay buffer equal in length to the hysteresis plus two which enables locating the
|
||||
* precise transition point of squelch/un-squelch and passing the audio of the detected signal while blocking the audio
|
||||
* when there is no signal detected. Register an audio listener to receive squelch controlled audio and a squelch state
|
||||
* listener to receive squelch toggle indications.
|
||||
*/
|
||||
public class NoiseSquelch implements INoiseSquelchController
|
||||
{
|
||||
public static final int DEFAULT_HYSTERESIS_OPEN_THRESHOLD = 4;
|
||||
public static final int DEFAULT_HYSTERESIS_CLOSE_THRESHOLD = 6;
|
||||
public static final int MINIMUM_HYSTERESIS_THRESHOLD = 1;
|
||||
public static final int MAXIMUM_HYSTERESIS_THRESHOLD = 10;
|
||||
public static final int VARIANCE_CALCULATION_WINDOW_MILLISECONDS = 10;
|
||||
public static final float DEFAULT_NOISE_OPEN_THRESHOLD = 0.1f;
|
||||
public static final float DEFAULT_NOISE_CLOSE_THRESHOLD = 0.19f;
|
||||
public static final float MINIMUM_NOISE_THRESHOLD = 0.1f;
|
||||
public static final float MAXIMUM_NOISE_THRESHOLD = 0.5f;
|
||||
private float[] mFilteredBuffer = new float[0];
|
||||
private float[] mAudioBuffer = new float[0];
|
||||
private float mMeanAccumulator;
|
||||
private float mNoiseOpenThreshold = DEFAULT_NOISE_OPEN_THRESHOLD;
|
||||
private float mNoiseCloseThreshold = DEFAULT_NOISE_CLOSE_THRESHOLD;
|
||||
private boolean mSquelch = true;
|
||||
private boolean mSquelchOverride = false;
|
||||
private int mMeanAccumulatorPointer;
|
||||
private int mVarianceWindowSize;
|
||||
private int mHysteresisOpenThreshold = DEFAULT_HYSTERESIS_OPEN_THRESHOLD;
|
||||
private int mHysteresisCloseThreshold = DEFAULT_HYSTERESIS_CLOSE_THRESHOLD;
|
||||
private int mHysteresisCount = 0;
|
||||
private int mSquelchStateBroadcastCounter = 0;
|
||||
private int mSquelchOpenIndex = 0;
|
||||
private int mAudioBufferFilterDelay;
|
||||
private IRealFilter mHighPassFilter;
|
||||
private Listener<float[]> mAudioListener;
|
||||
private Listener<SquelchState> mSquelchStateListener;
|
||||
private Listener<NoiseSquelchState> mNoiseSquelchStateListener;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param noiseOpen in range 0.0 to 1.0 - recommend 0.1 to 0.2 (units of demodulated, high-pass filtered audio
|
||||
* sample variance where the high-pass filtered audio samples range -1.0 to 1.0 and maximum variance is half of that
|
||||
* range: 1.0)
|
||||
* @param hysteresisOpen in range 1 to 10 controls the squelch toggle hysteresis - recommend 4 (units of 10 ms)
|
||||
*/
|
||||
public NoiseSquelch(float noiseOpen, float noiseClose, int hysteresisOpen, int hysteresisClose)
|
||||
{
|
||||
setNoiseThreshold(noiseOpen, noiseClose);
|
||||
setHysteresisThreshold(hysteresisOpen, hysteresisClose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the current state is squelched.
|
||||
* @return true if squelched and false if not squelched.
|
||||
*/
|
||||
public boolean isSquelched()
|
||||
{
|
||||
return mSquelch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the manual squelch override.
|
||||
* @param override (true) or (false) to turn off manual squelch override.
|
||||
*/
|
||||
@Override
|
||||
public void setSquelchOverride(boolean override)
|
||||
{
|
||||
mSquelchOverride = override;
|
||||
|
||||
if(mSquelchOverride)
|
||||
{
|
||||
broadcast(SquelchState.UNSQUELCH);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the noise thresholds
|
||||
* @param open in range 0.0-1.0 and less than or equal to close, recommend 0.1
|
||||
* @param close in range 0.0-1.0 and greater than or equal to open, recommend 0.2
|
||||
*/
|
||||
public void setNoiseThreshold(float open, float close)
|
||||
{
|
||||
if(open < MINIMUM_NOISE_THRESHOLD || close > MAXIMUM_NOISE_THRESHOLD || open > close)
|
||||
{
|
||||
throw new IllegalArgumentException("Noise thresholds open/close [" + open + "/" + close + "] - open must be less than or equal to close and both in range " + MINIMUM_NOISE_THRESHOLD + "-" + MAXIMUM_NOISE_THRESHOLD);
|
||||
}
|
||||
|
||||
mNoiseOpenThreshold = open;
|
||||
mNoiseCloseThreshold = close;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the hysteresis thresholds
|
||||
* @param open in range 1 to 10 and less than or equal to close, recommend 4.
|
||||
* @param close in range 1 to 10 and greater than or equal to open, recommend 6.
|
||||
*/
|
||||
public void setHysteresisThreshold(int open, int close)
|
||||
{
|
||||
if(open < MINIMUM_HYSTERESIS_THRESHOLD || close > MAXIMUM_HYSTERESIS_THRESHOLD || open > close)
|
||||
{
|
||||
throw new IllegalArgumentException("Hysteresis threshold open [" + open + "] must be less than or equal to close [" +
|
||||
close + "] and both in range " + MINIMUM_HYSTERESIS_THRESHOLD + "-" + MAXIMUM_HYSTERESIS_THRESHOLD);
|
||||
}
|
||||
|
||||
mHysteresisOpenThreshold = open;
|
||||
mHysteresisCloseThreshold = close;
|
||||
mHysteresisCount = Math.min(mHysteresisCloseThreshold, mHysteresisCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive noise squelch state updates every 100 milliseconds.
|
||||
* @param listener to register, or de-register by passing in a null.
|
||||
*/
|
||||
public void setNoiseSquelchStateListener(Listener<NoiseSquelchState> listener)
|
||||
{
|
||||
mNoiseSquelchStateListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive squelch state indications. Squelch state events will be sent to the listener
|
||||
* in concert with processed audio buffers. An un-squelch event is followed by one or more audio buffers and then a
|
||||
* squelch event.
|
||||
* @param listener to register or de-register by passing in a null.
|
||||
*/
|
||||
public void setSquelchStateListener(Listener<SquelchState> listener)
|
||||
{
|
||||
mSquelchStateListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive processed audio buffers in between un-squelch/squelch events.
|
||||
* @param listener to register or de-register by passing in a null.
|
||||
*/
|
||||
public void setAudioListener(Listener<float[]> listener)
|
||||
{
|
||||
mAudioListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the current noise squelch state to an optionally registered listener. This can be use used in the
|
||||
* user interface to display the state of the channel and noise squelch activity.
|
||||
* @param noise variance from the current 10-millisecond audio buffer.
|
||||
*/
|
||||
private void broadcastNoiseSquelchState(float noise)
|
||||
{
|
||||
if(mNoiseSquelchStateListener != null)
|
||||
{
|
||||
mNoiseSquelchStateListener.receive(new NoiseSquelchState(mSquelch, mSquelchOverride, noise,
|
||||
mNoiseOpenThreshold, mNoiseCloseThreshold, mHysteresisCount, mHysteresisOpenThreshold,
|
||||
mHysteresisCloseThreshold));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts from the unfiltered audio buffer to an optionally registered audio listener. Note: the start and end
|
||||
* indices are pointers to the filtered audio buffer, and we dispatch from the unfiltered audio buffer which is offset
|
||||
* by half the length of the high-pass audio filter coefficients. This method adjusts the index arguments by this
|
||||
* offset value to align the unfiltered audio with the high-pass filtered audio detection boundaries.
|
||||
*
|
||||
* @param start index of the filtered audio buffer
|
||||
* @param end index of the filtered audio buffer
|
||||
*/
|
||||
private void broadcast(int start, int end)
|
||||
{
|
||||
if(mAudioListener != null)
|
||||
{
|
||||
mAudioListener.receive(Arrays.copyOfRange(mAudioBuffer, start, end));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the squelch state change to an optionally registered squelch state listener.
|
||||
* @param squelchState to dispatch.
|
||||
*/
|
||||
private void broadcast(SquelchState squelchState)
|
||||
{
|
||||
//Override squelch state when we're in manual override
|
||||
if(squelchState == SquelchState.SQUELCH && mSquelchOverride)
|
||||
{
|
||||
squelchState = SquelchState.UNSQUELCH;
|
||||
}
|
||||
|
||||
if(mSquelchStateListener != null)
|
||||
{
|
||||
mSquelchStateListener.receive(squelchState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sample rate for the incoming demodulated audio sample stream.
|
||||
* @param sampleRate of the sample stream.
|
||||
*/
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
float[] coefficients = FilterFactory.getHighPass((int)sampleRate, 3000, 31, WindowType.BLACKMAN_HARRIS_7);
|
||||
mHighPassFilter = new RealFIRFilter(coefficients);
|
||||
mVarianceWindowSize = (int)(sampleRate * (VARIANCE_CALCULATION_WINDOW_MILLISECONDS / 1000.0));
|
||||
|
||||
//Offset for the filter delay between the audio buffer and the filtered audio noise detection buffer.
|
||||
mAudioBufferFilterDelay = (int)Math.ceil(coefficients.length / 2.0);
|
||||
}
|
||||
|
||||
public void process(float[] samples)
|
||||
{
|
||||
float[] filtered = mHighPassFilter.filter(samples);
|
||||
|
||||
int requiredLength = filtered.length + (mVarianceWindowSize * (mHysteresisCloseThreshold + 2));
|
||||
|
||||
//Resize the delay buffers if the incoming sample buffer size changes.
|
||||
if(mFilteredBuffer.length != requiredLength)
|
||||
{
|
||||
mFilteredBuffer = Arrays.copyOf(mFilteredBuffer, requiredLength);
|
||||
|
||||
//The audio buffer is longer to account for filter delay of the high pass filter and to align the indices
|
||||
//across the filtered and audio buffers.
|
||||
mAudioBuffer = Arrays.copyOf(mAudioBuffer, requiredLength + mAudioBufferFilterDelay);
|
||||
}
|
||||
|
||||
//Shuffle the samples to the left in the delay buffers and copy in the new samples.
|
||||
int length = mFilteredBuffer.length - filtered.length;
|
||||
System.arraycopy(mFilteredBuffer, filtered.length, mFilteredBuffer, 0, length);
|
||||
System.arraycopy(filtered, 0, mFilteredBuffer, length, filtered.length);
|
||||
|
||||
length = mAudioBuffer.length - samples.length;
|
||||
System.arraycopy(mAudioBuffer, samples.length, mAudioBuffer, 0, length);
|
||||
System.arraycopy(samples, 0, mAudioBuffer, length, samples.length);
|
||||
|
||||
int squelchCloseIndex;
|
||||
//Adjust the squelch open index in sync with the left-shuffle of the audio samples.
|
||||
|
||||
mSquelchOpenIndex = Math.max(mSquelchOpenIndex - samples.length, 0);
|
||||
|
||||
//Process the newly added (filtered) samples in the delay buffer.
|
||||
for(int x = mFilteredBuffer.length - filtered.length; x < mFilteredBuffer.length; x++)
|
||||
{
|
||||
mMeanAccumulator += mFilteredBuffer[x];
|
||||
mMeanAccumulatorPointer++;
|
||||
|
||||
if(mMeanAccumulatorPointer == mVarianceWindowSize)
|
||||
{
|
||||
float mean = mMeanAccumulator / mVarianceWindowSize;
|
||||
|
||||
//Reset averaging variables
|
||||
mMeanAccumulator = 0.0f;
|
||||
mMeanAccumulatorPointer = 0;
|
||||
|
||||
//Calculate the variance
|
||||
float varianceAccumulator1 = 0.0f, deviation, varianceAccumulator2 = 0.0f;
|
||||
for(int i = x - mVarianceWindowSize + 1; i <= x; i++)
|
||||
{
|
||||
deviation = mFilteredBuffer[i] - mean;
|
||||
varianceAccumulator1 += deviation * deviation;
|
||||
varianceAccumulator2 += deviation;
|
||||
}
|
||||
|
||||
//Formula from Apache Commons Math - Variance class.
|
||||
float noiseVariance = (varianceAccumulator1 - (varianceAccumulator2 * varianceAccumulator2 / mVarianceWindowSize)) / mVarianceWindowSize;
|
||||
boolean below = mSquelch ? (noiseVariance < mNoiseOpenThreshold) : (noiseVariance < mNoiseCloseThreshold);
|
||||
mHysteresisCount += (below ? 1 : -1);
|
||||
|
||||
if(mSquelch && mHysteresisCount >= mHysteresisOpenThreshold)
|
||||
{
|
||||
mSquelch = false;
|
||||
mHysteresisCount = mHysteresisOpenThreshold;
|
||||
mSquelchOpenIndex = findTransition(x);
|
||||
|
||||
if(!mSquelchOverride)
|
||||
{
|
||||
broadcast(SquelchState.UNSQUELCH);
|
||||
}
|
||||
}
|
||||
else if(!mSquelch && mHysteresisCount <= 0)
|
||||
{
|
||||
mSquelch = true;
|
||||
mHysteresisCount = 0;
|
||||
squelchCloseIndex = findTransition(x);
|
||||
|
||||
//Broadcast the partial audio segment ending at the transition point.
|
||||
if(mSquelchOpenIndex < squelchCloseIndex)
|
||||
{
|
||||
//Only broadcast the truncated audio samples when we're not in squelch override
|
||||
if(!mSquelchOverride)
|
||||
{
|
||||
broadcast(mSquelchOpenIndex, squelchCloseIndex);
|
||||
}
|
||||
|
||||
broadcast(SquelchState.SQUELCH);
|
||||
}
|
||||
|
||||
mSquelchOpenIndex = 0;
|
||||
}
|
||||
else if(mSquelch && !mSquelchOverride)
|
||||
{
|
||||
int start = x - (mVarianceWindowSize * mHysteresisOpenThreshold) + 1;
|
||||
int end = start + mVarianceWindowSize;
|
||||
//Make sure start doesn't go negative
|
||||
start = Math.max(0, start);
|
||||
Arrays.fill(mAudioBuffer, start, end, 0.0f);
|
||||
}
|
||||
|
||||
mHysteresisCount = Math.min(mHysteresisCount, mHysteresisCloseThreshold);
|
||||
mHysteresisCount = Math.max(mHysteresisCount, 0);
|
||||
|
||||
//Broadcast squelch state to an optionally registered listener (the channel tab in user interface)
|
||||
mSquelchStateBroadcastCounter++;
|
||||
if(mSquelchStateBroadcastCounter >= 5)
|
||||
{
|
||||
broadcastNoiseSquelchState(noiseVariance);
|
||||
mSquelchStateBroadcastCounter -= 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mSquelchOverride)
|
||||
{
|
||||
broadcast(0, samples.length);
|
||||
}
|
||||
else if(!mSquelch && mSquelchOpenIndex < (samples.length))
|
||||
{
|
||||
mSquelchOpenIndex = Math.max(mSquelchOpenIndex, 0);
|
||||
//Dispatch audio after processing the audio buffer if we end in an un-squelch state.
|
||||
broadcast(mSquelchOpenIndex, samples.length);
|
||||
mSquelchOpenIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the squelch transition point across a hysteresis window delayed by 2x buffer periods from the current
|
||||
* processing index. This enables fine-grained squelch control without losing audio during the un-squelch period or
|
||||
* allowing noise to leak into the squelch period at the end of the transmission. Uses a reduced 20-sample variance
|
||||
* calculation window extended to the hysteresis time period to detect the transition point. Identifies the
|
||||
* transition point within +/- 20 audio samples.
|
||||
*
|
||||
* @param current buffer processing index.
|
||||
* @return buffer index of detected transition.
|
||||
*/
|
||||
private int findTransition(int current)
|
||||
{
|
||||
//Search window is 10-ms buffer size extended to hysteresis period, delayed by 2x buffer lengths.
|
||||
int window = mSquelch ? mHysteresisCloseThreshold : mHysteresisOpenThreshold;
|
||||
|
||||
//Thresholds here may seem reversed but keep in mind that the mSquelch state has already been flipped and thus
|
||||
// represents the state moving forward, and we need to detect the transition from the previous mSquelch state.
|
||||
float varianceThreshold = mSquelch ? mNoiseCloseThreshold : mNoiseOpenThreshold;
|
||||
int hysteresisThreshold = mSquelch ? mHysteresisCloseThreshold : mHysteresisOpenThreshold;
|
||||
|
||||
//Look back window start and end indices are sized according the previous mSquelch state
|
||||
int start = current - (mVarianceWindowSize * (window + 2));
|
||||
int end = start + (mVarianceWindowSize * window);
|
||||
|
||||
int hysteresisCount = 0;
|
||||
int windowSize = mVarianceWindowSize / 5; //Decrease transition variance window to 1/5th of the normal variance window
|
||||
int transitionPointer = 0;
|
||||
|
||||
for(int x = start; x < end; x += windowSize)
|
||||
{
|
||||
float mean = getMean(x, x + windowSize);
|
||||
float variance = getVariance(x, x + windowSize, mean);
|
||||
|
||||
//Detect transition from from un-squelch to squelch
|
||||
boolean exceedsThreshold = (mSquelch ? (variance > varianceThreshold) : (variance < varianceThreshold));
|
||||
|
||||
if(exceedsThreshold)
|
||||
{
|
||||
if(hysteresisCount == 0)
|
||||
{
|
||||
hysteresisCount++;
|
||||
transitionPointer = x;
|
||||
}
|
||||
else
|
||||
{
|
||||
hysteresisCount++;
|
||||
|
||||
if(hysteresisCount >= hysteresisThreshold)
|
||||
{
|
||||
return transitionPointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hysteresisCount--;
|
||||
hysteresisCount = Math.max(0, hysteresisCount);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the mean/average of values between the start and end indices.
|
||||
* @param start index
|
||||
* @param end index
|
||||
* @return mean value.
|
||||
*/
|
||||
private float getMean(int start, int end)
|
||||
{
|
||||
if(start < 0)
|
||||
{
|
||||
start = 0;
|
||||
}
|
||||
|
||||
if(end > mFilteredBuffer.length)
|
||||
{
|
||||
end = mFilteredBuffer.length;
|
||||
}
|
||||
|
||||
float accumulator = 0.0f;
|
||||
|
||||
for(int x = start; x < end; x++)
|
||||
{
|
||||
accumulator += mFilteredBuffer[x];
|
||||
}
|
||||
|
||||
return accumulator / (end - start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the variance of the values between start and end indices using the supplied mean value.
|
||||
* @param start index
|
||||
* @param end index
|
||||
* @param mean of values between start and end, calculated previously
|
||||
* @return variance.
|
||||
*/
|
||||
private float getVariance(int start, int end, float mean)
|
||||
{
|
||||
if(start < 0)
|
||||
{
|
||||
start = 0;
|
||||
}
|
||||
|
||||
if(end > mFilteredBuffer.length)
|
||||
{
|
||||
end = mFilteredBuffer.length;
|
||||
}
|
||||
|
||||
float varianceAccumulator1 = 0.0f, deviation, varianceAccumulator2 = 0.0f;
|
||||
|
||||
for(int i = start; i < end; i++)
|
||||
{
|
||||
deviation = mFilteredBuffer[i] - mean;
|
||||
varianceAccumulator1 += deviation * deviation;
|
||||
varianceAccumulator2 += deviation;
|
||||
}
|
||||
|
||||
int sampleSize = end - start;
|
||||
|
||||
//Formula from Apache Commons Math - Variance class.
|
||||
return (varianceAccumulator1 - (varianceAccumulator2 * varianceAccumulator2 / sampleSize)) / sampleSize;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
// Path directory = Paths.get("/media/denny/T9/NBFM Squelch Research"); //Linux
|
||||
// Path file = directory.resolve("20250629_072704_453850000_Onondaga-County_Fire-EMS_OC-Fire-Disp_79_baseband.wav");
|
||||
// Path file = directory.resolve("DMR_1_CAPPLUS.wav");
|
||||
// Path file = directory.resolve("20250702_034109_145510000_R828D_V4_POLY_No_Antenna_Normal_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034301_145510000_R828D_V4_POLY_No_Antenna_Max_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034403_145510000_R828D_V4_POLY_No_Antenna_Min_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034541_145510000_R828D_V4_HET_No_Antenna_Min_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034635_145510000_R828D_V4_HET_No_Antenna_Max_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034742_145510000_R828D_V4_HET_No_Antenna_Normal_Gain.wav");
|
||||
// Path file = directory.resolve("20250703_043912_162500000_NOAA_25_khz_no_center_signal.wav");
|
||||
// Path file = directory.resolve("20250703_043958_162550000_NOAA_25_khz_yes_center_signal.wav");
|
||||
//
|
||||
// DecodeConfigNBFM config = new DecodeConfigNBFM();
|
||||
// config.setBandwidth(DecodeConfigAnalog.Bandwidth.BW_12_5);
|
||||
// NBFMDecoder decoder = new NBFMDecoder(config);
|
||||
//
|
||||
// try(ComplexWaveSource source = new ComplexWaveSource(file.toFile(), false))
|
||||
// {
|
||||
// source.setListener(iNativeBuffer -> {
|
||||
// Iterator<ComplexSamples> it = iNativeBuffer.iterator();
|
||||
//
|
||||
// while(it.hasNext())
|
||||
// {
|
||||
// ComplexSamples samples = it.next();
|
||||
// decoder.receive(samples);
|
||||
// }
|
||||
// });
|
||||
// source.start();
|
||||
//
|
||||
// decoder.getSourceEventListener().receive(SourceEvent.sampleRateChange(source.getSampleRate()));
|
||||
//
|
||||
// while(true)
|
||||
// {
|
||||
// source.next(2048, true);
|
||||
// }
|
||||
// }
|
||||
// catch(IOException ioe)
|
||||
// {
|
||||
//
|
||||
// ioe.printStackTrace();
|
||||
// }
|
||||
//
|
||||
// System.out.println("Finished");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.squelch;
|
||||
|
||||
/**
|
||||
* Noise Squelch operating status record. Captures a snapshot in time of the squelch status.
|
||||
* @param squelch state, on (true) or off (false)
|
||||
* @param squelchOverride on (true) or off (false)
|
||||
* @param noise - current noise variance
|
||||
* @param noiseOpenThreshold for determining un-squelch state
|
||||
* @param noiseCloseThreshold for determining squelch state
|
||||
* @param hysteresis in units of 10-milliseconds
|
||||
* @param hysteresisOpenThreshold for toggling the squelch state.
|
||||
* @param hysteresisCloseThreshold for toggling the squelch state.
|
||||
*/
|
||||
public record NoiseSquelchState(boolean squelch, boolean squelchOverride, float noise, float noiseOpenThreshold,
|
||||
float noiseCloseThreshold, int hysteresis, int hysteresisOpenThreshold,
|
||||
int hysteresisCloseThreshold){}
|
||||
Reference in New Issue
Block a user