mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
NBFM Channel level CTCSS/DCS and audio features (#2425)
NBFM decoder enhancements. CTCSS and DCS squelch control. Adds deemphasis, high pass filter control, and auto-level control
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2024 Dennis Sheirer
|
||||
# Copyright (C) 2014-2026 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
|
||||
|
||||
@@ -47,7 +47,7 @@ import io.github.dsheirer.identifier.talkgroup.FullyQualifiedTalkgroupIdentifier
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
@@ -22,7 +22,7 @@ package io.github.dsheirer.alias.id.dcs;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
|
||||
/**
|
||||
* Digital Coded Squelch (DCS) tone identifier
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
/**
|
||||
* Used for defining an array list of filter methods to be implemented in AudioModule
|
||||
* The filters will be applied in the order given in the list.
|
||||
* Each audio filter mush implement the filter method.
|
||||
*/
|
||||
|
||||
public abstract class AbstractAudioFilter
|
||||
{
|
||||
/**
|
||||
* To be overridden by each audio filter
|
||||
* @param audio a buffer of 8 KHz audio samples
|
||||
* @return float[] filtered buffer of 8 KHz audio samples
|
||||
*/
|
||||
public abstract float[] filter(float[] audio);
|
||||
}
|
||||
44
src/main/java/io/github/dsheirer/audio/AudioGainFilter.java
Normal file
44
src/main/java/io/github/dsheirer/audio/AudioGainFilter.java
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.audio;
|
||||
|
||||
import io.github.dsheirer.dsp.gain.AudioGainAndDcFilter;
|
||||
|
||||
/**
|
||||
* This is a wrapper for the AudioGainAndDcFilter class so it can be used with the AudioModule filter list.
|
||||
* Usually instantiated in the DecoderFactory
|
||||
*/
|
||||
public class AudioGainFilter extends AbstractAudioFilter
|
||||
{
|
||||
AudioGainAndDcFilter mAudioGain;
|
||||
|
||||
public AudioGainFilter(float minGain, float maxGain, float objectiveAmplitude)
|
||||
{
|
||||
mAudioGain = new AudioGainAndDcFilter(minGain, maxGain, objectiveAmplitude);
|
||||
mAudioGain.setDecayRate(2); // to remove pumping effect
|
||||
mAudioGain.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] filter(float[] audio)
|
||||
{
|
||||
return mAudioGain.process(audio);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ import io.github.dsheirer.sample.real.IRealBufferListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides packaging of demodulated audio sample buffers into audio segments for broadcast to registered listeners.
|
||||
* Includes audio packet metadata in constructed audio segments.
|
||||
@@ -42,36 +44,7 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
Listener<float[]>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
|
||||
private static float[] sHighPassFilterCoefficients;
|
||||
private final boolean mAudioFilterEnable;
|
||||
|
||||
static
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.stopBandCutoff(200)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.025)
|
||||
.passBandStart(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.build();
|
||||
try
|
||||
{
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
sHighPassFilterCoefficients = designer.getImpulseResponse();
|
||||
}
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
}
|
||||
|
||||
private final IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
|
||||
private List<AbstractAudioFilter>mAudioFilters;
|
||||
private final SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private SquelchState mSquelchState = SquelchState.SQUELCH;
|
||||
|
||||
@@ -81,23 +54,23 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
* @param aliasList for aliasing identifiers
|
||||
* @param timeslot for this audio module
|
||||
* @param maxAudioSegmentLength in milliseconds
|
||||
* @param audioFilterEnable to enable or disable high-pass audio filter
|
||||
* @param audioFilters a list of audio filters to apply to the incoming audio samples
|
||||
*/
|
||||
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength, boolean audioFilterEnable)
|
||||
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength, List<AbstractAudioFilter> audioFilters)
|
||||
{
|
||||
super(aliasList, timeslot, maxAudioSegmentLength);
|
||||
mAudioFilterEnable = audioFilterEnable;
|
||||
mAudioFilters = audioFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Audio Module.
|
||||
* @param aliasList for aliasing identifiers
|
||||
* @param audioFilterEnable to enable or disable high-pass audio filter
|
||||
* @param audioFilters a list of audio filters to apply to the incoming audio samples
|
||||
*/
|
||||
public AudioModule(AliasList aliasList, boolean audioFilterEnable)
|
||||
public AudioModule(AliasList aliasList, List<AbstractAudioFilter> audioFilters)
|
||||
{
|
||||
super(aliasList);
|
||||
mAudioFilterEnable = audioFilterEnable;
|
||||
mAudioFilters = audioFilters;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -128,9 +101,13 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
{
|
||||
if(mSquelchState == SquelchState.UNSQUELCH)
|
||||
{
|
||||
if(mAudioFilterEnable)
|
||||
// apply audio filters if any (i.e. deemphasis, high pass, etc.)
|
||||
if(!mAudioFilters.isEmpty())
|
||||
{
|
||||
audioBuffer = mHighPassFilter.filter(audioBuffer);
|
||||
for(AbstractAudioFilter filter : mAudioFilters)
|
||||
{
|
||||
audioBuffer = filter.filter(audioBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
addAudio(audioBuffer);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class HighPassAudioFilter extends AbstractAudioFilter
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(HighPassAudioFilter.class);
|
||||
private static float[] sHighPassFilterCoefficients;
|
||||
|
||||
static
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.stopBandCutoff(200)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.025)
|
||||
.passBandStart(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.build();
|
||||
try
|
||||
{
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
sHighPassFilterCoefficients = designer.getImpulseResponse();
|
||||
}
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
}
|
||||
|
||||
private final IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
|
||||
|
||||
@Override
|
||||
public float[] filter(float[] audio)
|
||||
{
|
||||
audio = mHighPassFilter.filter(audio);
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import org.apache.commons.math3.util.FastMath;
|
||||
/**
|
||||
* Implements the Goertzel Filter as described in
|
||||
* http://en.wikipedia.org/wiki/Goertzel_algorithm
|
||||
* <p>
|
||||
*
|
||||
* Calculates the magnitude of a specific frequency component within a window of
|
||||
* samples, using a Goertzel Filter (ie an optimized DFT).
|
||||
*/
|
||||
@@ -101,9 +101,10 @@ public class GoertzelFilter
|
||||
//power = s_prev2 * s_prev2 + s_prev * s_prev - coeff * s_prev * s_prev2 ;
|
||||
|
||||
double magnitude = (s_prev2 * s_prev2) + (s_prev * s_prev) - (mCoefficient * s_prev * s_prev2);
|
||||
int binZero = getBinZeroPower(samples);
|
||||
|
||||
int power = (int)(20 * FastMath.log10(magnitude / binZero));
|
||||
// Removed because bin(0) is likely to be (int) zero on audio samples
|
||||
//int binZero = getBinZeroPower(samples);
|
||||
//int power = (int)(20 * FastMath.log10(magnitude / binZero));
|
||||
int power = (int)(20 * FastMath.log10(magnitude));
|
||||
|
||||
return power;
|
||||
}
|
||||
@@ -119,7 +120,6 @@ public class GoertzelFilter
|
||||
{
|
||||
retVal += sample;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,22 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.filter.iir;
|
||||
|
||||
import io.github.dsheirer.audio.AbstractAudioFilter;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public class DeemphasisFilter
|
||||
public class DeemphasisFilter extends AbstractAudioFilter
|
||||
{
|
||||
private static final float MAX_SAMPLE_VALUE = 0.95f;
|
||||
private float mAlpha;
|
||||
private float mGain;
|
||||
private float mPrevious = 0.0f;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance
|
||||
* @param sampleRate - usually 8000 Hz
|
||||
* @param cutoff - cutoff frequency
|
||||
* @param gain - gain (linear)
|
||||
*/
|
||||
public DeemphasisFilter( float sampleRate, float cutoff, float gain )
|
||||
{
|
||||
mAlpha = (float)FastMath.exp( -2.0 * FastMath.PI * cutoff * ( 1.0 / sampleRate ) );
|
||||
@@ -45,7 +52,7 @@ public class DeemphasisFilter
|
||||
}
|
||||
else return FastMath.max(value, -MAX_SAMPLE_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] filter( float[] samples )
|
||||
{
|
||||
for( int x = 0; x < samples.length; x++ )
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -47,6 +47,7 @@ public class AudioGainAndDcFilter
|
||||
private float mObjectiveGain;
|
||||
private float mObjectiveAmplitude;
|
||||
private float mMaxObservedAmplitude;
|
||||
private float mPercentDecayRate = 10f;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -82,8 +83,9 @@ public class AudioGainAndDcFilter
|
||||
float currentAmplitude;
|
||||
float dcAccumulator = 0.0f;
|
||||
|
||||
//Decay the max observed value by 10% each buffer so that an initial spike doesn't carry across all buffers
|
||||
mMaxObservedAmplitude *= 0.9f;
|
||||
//Decay the max observed value by a specified percentage each buffer so that an initial
|
||||
// spike doesn't carry across all buffers
|
||||
mMaxObservedAmplitude *= ((100 - mPercentDecayRate )/ 100);
|
||||
|
||||
for(float sample: samples)
|
||||
{
|
||||
@@ -133,7 +135,17 @@ public class AudioGainAndDcFilter
|
||||
}
|
||||
|
||||
mCurrentGain = gain;
|
||||
//mLog.debug("Gain: {}", gain);
|
||||
mObjectiveGain = objective;
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the default decay rate of 10 %.
|
||||
* @param newRate specified as a percentage (i.e. 2 equals 2%)
|
||||
*/
|
||||
public void setDecayRate(float newRate)
|
||||
{
|
||||
mPercentDecayRate = newRate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
package io.github.dsheirer.gui.playlist.alias.identifier;
|
||||
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -28,7 +28,11 @@ import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.squelch.SquelchDecoderConfig;
|
||||
import io.github.dsheirer.module.decode.squelch.ctcss.CTCSSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
|
||||
import io.github.dsheirer.module.decode.nbfm.DeemphasisMode;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
@@ -46,13 +50,8 @@ import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.TitledPane;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.skin.ComboBoxListViewSkin;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
@@ -70,6 +69,7 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
private TitledPane mSourcePane;
|
||||
private TextField mTalkgroupField;
|
||||
private ToggleSwitch mAudioFilterEnable;
|
||||
private ToggleSwitch mALCEnable;
|
||||
private TextFormatter<Integer> mTalkgroupTextFormatter;
|
||||
private ToggleSwitch mBasebandRecordSwitch;
|
||||
private SegmentedButton mBandwidthButton;
|
||||
@@ -81,6 +81,11 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
private final IntegerFormatter mDecimalFormatter = new IntegerFormatter(1, 65535);
|
||||
private final HexFormatter mHexFormatter = new HexFormatter(1, 65535);
|
||||
|
||||
private ComboBox<DeemphasisMode> mDeemphasisCombo;
|
||||
private ComboBox<SquelchDecoderConfig.SquelchType> mSquelchTypeCombo;
|
||||
private ComboBox<CTCSSCode> mCtcssCodeCombo;
|
||||
private ComboBox<DCSCode> mDcsCodeCombo;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for playlists
|
||||
@@ -128,6 +133,7 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
gridPane.setHgap(10);
|
||||
gridPane.setVgap(10);
|
||||
|
||||
// channel bandwidth
|
||||
Label bandwidthLabel = new Label("Channel Bandwidth");
|
||||
GridPane.setHalignment(bandwidthLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(bandwidthLabel, 0, 0);
|
||||
@@ -136,16 +142,86 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
GridPane.setConstraints(getBandwidthButton(), 1, 0);
|
||||
gridPane.getChildren().add(getBandwidthButton());
|
||||
|
||||
//De-emphasis
|
||||
Label deemphasisLabel = new Label("De-emphasis");
|
||||
GridPane.setHalignment(deemphasisLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(deemphasisLabel, 0, 1);
|
||||
gridPane.getChildren().add(deemphasisLabel);
|
||||
GridPane.setConstraints(getDeemphasisCombo(), 1, 1, 2, 1);
|
||||
gridPane.getChildren().add(getDeemphasisCombo());
|
||||
|
||||
// High pass audio filter
|
||||
// Note: normally the label and its toggle switch are one cell. They were split to make the next row
|
||||
// look better. The label is manually created and placed here. Might want to create a new grid pane instead.
|
||||
Label HPFLabel = new Label("High-pass Audio Filter (300 Hz)");
|
||||
GridPane.setHalignment(HPFLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(HPFLabel, 2, 1);
|
||||
gridPane.getChildren().add(HPFLabel);
|
||||
GridPane.setConstraints(getAudioFilterEnable(), 3, 1);
|
||||
gridPane.getChildren().add(getAudioFilterEnable());
|
||||
|
||||
// ALC (Automatic Level Control)
|
||||
GridPane.setConstraints(getALCEnable(), 4, 1);
|
||||
gridPane.getChildren().add(getALCEnable());
|
||||
|
||||
/*
|
||||
* The intention here is to have talkgroup and squelch decoder settings on a single line at the bottom
|
||||
* of the pane. In the future a button can be added to add additional talkgroups and filter lines to
|
||||
* allow for multiple decoders on a single NBFM channel. For the time being, a user can "clone" the
|
||||
* channel and setup another decoder at the expense of another channel processing thread.
|
||||
*/
|
||||
// talkgroup
|
||||
Label talkgroupLabel = new Label("Talkgroup To Assign");
|
||||
GridPane.setHalignment(talkgroupLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(talkgroupLabel, 0, 1);
|
||||
GridPane.setConstraints(talkgroupLabel, 0, 2);
|
||||
gridPane.getChildren().add(talkgroupLabel);
|
||||
|
||||
GridPane.setConstraints(getTalkgroupField(), 1, 1);
|
||||
GridPane.setConstraints(getTalkgroupField(), 1, 2);
|
||||
gridPane.getChildren().add(getTalkgroupField());
|
||||
|
||||
GridPane.setConstraints(getAudioFilterEnable(), 2, 1);
|
||||
gridPane.getChildren().add(getAudioFilterEnable());
|
||||
// Squelch Decoder type
|
||||
Label typeLabel = new Label("Squelch Decoder Type");
|
||||
GridPane.setHalignment(typeLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(typeLabel, 2, 2);
|
||||
gridPane.getChildren().add(typeLabel);
|
||||
|
||||
mSquelchTypeCombo = new ComboBox<>();
|
||||
mSquelchTypeCombo.getItems().addAll(SquelchDecoderConfig.SquelchType.SQUELCH_TYPE);
|
||||
mSquelchTypeCombo.setValue(SquelchDecoderConfig.SquelchType.NONE);
|
||||
mSquelchTypeCombo.valueProperty().addListener((obs, ov, nv) -> {
|
||||
updateSquelchCodeVisibility();
|
||||
mCtcssCodeCombo.setValue(null);
|
||||
mDcsCodeCombo.setValue(null);
|
||||
// this hack solves the problem of the promptText going away after using the comboBox first time (Java 25):
|
||||
mCtcssCodeCombo.setSkin(new ComboBoxListViewSkin<>(mCtcssCodeCombo));
|
||||
mDcsCodeCombo.setSkin(new ComboBoxListViewSkin<>(mDcsCodeCombo));
|
||||
modifiedProperty().set(true);
|
||||
});
|
||||
GridPane.setConstraints(mSquelchTypeCombo, 3, 2);
|
||||
gridPane.getChildren().add(mSquelchTypeCombo);
|
||||
|
||||
// CTCSS code selector
|
||||
mCtcssCodeCombo = new ComboBox<>();
|
||||
mCtcssCodeCombo.getItems().addAll(CTCSSCode.STANDARD_CODES); // excludes unknown/dummy codes
|
||||
mCtcssCodeCombo.setValue(null);
|
||||
mCtcssCodeCombo.setPromptText("Select CTCSS tone");
|
||||
mCtcssCodeCombo.setPrefWidth(200);
|
||||
mCtcssCodeCombo.setVisible(false);
|
||||
mCtcssCodeCombo.valueProperty().addListener((obs, ov, nv) -> modifiedProperty().set(true));
|
||||
GridPane.setConstraints(mCtcssCodeCombo, 4, 2);
|
||||
gridPane.getChildren().add(mCtcssCodeCombo);
|
||||
|
||||
// DCS code selector
|
||||
mDcsCodeCombo = new ComboBox<>();
|
||||
mDcsCodeCombo.getItems().addAll(DCSCode.STANDARD_CODES);
|
||||
mDcsCodeCombo.getItems().addAll(DCSCode.INVERTED_CODES);
|
||||
mDcsCodeCombo.setValue(null);
|
||||
mDcsCodeCombo.setPromptText("Select DCS code");
|
||||
mDcsCodeCombo.setPrefWidth(200);
|
||||
mDcsCodeCombo.setVisible(false);
|
||||
mDcsCodeCombo.valueProperty().addListener((obs, ov, nv) -> modifiedProperty().set(true));
|
||||
GridPane.setConstraints(mDcsCodeCombo, 4, 2);
|
||||
gridPane.getChildren().add(mDcsCodeCombo);
|
||||
|
||||
mDecoderPane.setContent(gridPane);
|
||||
|
||||
@@ -163,6 +239,46 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
return mDecoderPane;
|
||||
}
|
||||
|
||||
private ComboBox<DeemphasisMode> getDeemphasisCombo()
|
||||
{
|
||||
if(mDeemphasisCombo == null)
|
||||
{
|
||||
mDeemphasisCombo = new ComboBox<>();
|
||||
mDeemphasisCombo.getItems().addAll(DeemphasisMode.values());
|
||||
mDeemphasisCombo.setValue(DeemphasisMode.NBFM_300);
|
||||
mDeemphasisCombo.setTooltip(new Tooltip("FM de-emphasis restores flat audio from pre-emphasized FM signal"));
|
||||
mDeemphasisCombo.valueProperty().addListener((obs, ov, nv) -> modifiedProperty().set(true));
|
||||
}
|
||||
return mDeemphasisCombo;
|
||||
}
|
||||
|
||||
private void updateSquelchCodeVisibility()
|
||||
{
|
||||
switch(mSquelchTypeCombo.getValue())
|
||||
{
|
||||
case SquelchDecoderConfig.SquelchType.NONE:
|
||||
mCtcssCodeCombo.setVisible(false);
|
||||
mDcsCodeCombo.setVisible(false);
|
||||
break;
|
||||
case SquelchDecoderConfig.SquelchType.CTCSS:
|
||||
mCtcssCodeCombo.setVisible(true);
|
||||
mDcsCodeCombo.setVisible(false);
|
||||
break;
|
||||
case SquelchDecoderConfig.SquelchType.DCS:
|
||||
mCtcssCodeCombo.setVisible(false);
|
||||
mDcsCodeCombo.setVisible(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetSquelchCodes()
|
||||
{
|
||||
mSquelchTypeCombo.setValue(SquelchDecoderConfig.SquelchType.NONE);
|
||||
mCtcssCodeCombo.setValue(null);
|
||||
mDcsCodeCombo.setValue(null);
|
||||
updateSquelchCodeVisibility();
|
||||
}
|
||||
|
||||
private TitledPane getEventLogPane()
|
||||
{
|
||||
if(mEventLogPane == null)
|
||||
@@ -262,14 +378,31 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
{
|
||||
if(mAudioFilterEnable == null)
|
||||
{
|
||||
mAudioFilterEnable = new ToggleSwitch("High-Pass Audio Filter");
|
||||
// removed label from toggle switch so that the label and the switch can be in different columns
|
||||
// to make things look better on the next line. Perhaps make a new grid pane instead.
|
||||
//mAudioFilterEnable = new ToggleSwitch("High-Pass Audio Filter (300 Hz)");
|
||||
mAudioFilterEnable = new ToggleSwitch("");
|
||||
mAudioFilterEnable.setTooltip(new Tooltip("High-pass filter to remove DC offset and sub-audible signalling"));
|
||||
mAudioFilterEnable.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mAudioFilterEnable;
|
||||
}
|
||||
/**
|
||||
* Toggle switch for enable/disable the Automatic Level Control (ALC) in the audio module.
|
||||
* @return toggle switch.
|
||||
*/
|
||||
private ToggleSwitch getALCEnable()
|
||||
{
|
||||
if(mALCEnable == null)
|
||||
{
|
||||
mALCEnable = new ToggleSwitch("Automatic Level Control");
|
||||
mALCEnable.setTooltip(new Tooltip("Automatic Level Control (ALC). Tries to make the volume of each received transmission the same."));
|
||||
mALCEnable.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mALCEnable;
|
||||
}
|
||||
private SegmentedButton getBandwidthButton()
|
||||
{
|
||||
if(mBandwidthButton == null)
|
||||
@@ -327,6 +460,7 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
{
|
||||
mTalkgroupField = new TextField();
|
||||
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
|
||||
mTalkgroupField.setPrefWidth(100);
|
||||
}
|
||||
|
||||
return mTalkgroupField;
|
||||
@@ -412,6 +546,46 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
updateTextFormatter(decodeConfigNBFM.getTalkgroup());
|
||||
getAudioFilterEnable().setDisable(false);
|
||||
getAudioFilterEnable().setSelected(decodeConfigNBFM.isAudioFilter());
|
||||
|
||||
getALCEnable().setDisable(false);
|
||||
getALCEnable().setSelected(decodeConfigNBFM.isAudioALC());
|
||||
|
||||
getDeemphasisCombo().setValue(decodeConfigNBFM.getDeemphasis());
|
||||
|
||||
List<SquelchDecoderConfig> savedSquelchDecoders = decodeConfigNBFM.getSquelchDecoders();
|
||||
if(savedSquelchDecoders != null && !savedSquelchDecoders.isEmpty())
|
||||
{
|
||||
|
||||
// At present time, only one decoder per channel is used
|
||||
SquelchDecoderConfig filter = savedSquelchDecoders.get(0);
|
||||
mSquelchTypeCombo.setValue(filter.getSquelchType());
|
||||
updateSquelchCodeVisibility();
|
||||
if(filter.getSquelchType() == SquelchDecoderConfig.SquelchType.NONE)
|
||||
{
|
||||
mSquelchTypeCombo.setValue(SquelchDecoderConfig.SquelchType.NONE);
|
||||
resetSquelchCodes();
|
||||
}
|
||||
if(filter.getSquelchType() == SquelchDecoderConfig.SquelchType.CTCSS)
|
||||
{
|
||||
CTCSSCode code = filter.getCTCSSCode();
|
||||
if(code != null && code != CTCSSCode.UNKNOWNH && code != CTCSSCode.UNKNOWNL)
|
||||
{
|
||||
mCtcssCodeCombo.setValue(code);
|
||||
}
|
||||
}
|
||||
if(filter.getSquelchType() == SquelchDecoderConfig.SquelchType.DCS)
|
||||
{
|
||||
DCSCode code = filter.getDCSCode();
|
||||
if(code != null)
|
||||
{
|
||||
mDcsCodeCombo.setValue(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resetSquelchCodes();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -426,6 +600,11 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
getTalkgroupField().setDisable(true);
|
||||
getAudioFilterEnable().setDisable(true);
|
||||
getAudioFilterEnable().setSelected(false);
|
||||
getALCEnable().setDisable(true);
|
||||
getALCEnable().setSelected(false);
|
||||
|
||||
getDeemphasisCombo().setValue(DeemphasisMode.NONE);
|
||||
resetSquelchCodes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,6 +640,33 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
|
||||
config.setTalkgroup(talkgroup);
|
||||
config.setAudioFilter(getAudioFilterEnable().isSelected());
|
||||
config.setAudioALC(getALCEnable().isSelected());
|
||||
|
||||
config.setDeemphasis(getDeemphasisCombo().getValue());
|
||||
|
||||
List<SquelchDecoderConfig> squelchDecoders = new ArrayList<>();
|
||||
SquelchDecoderConfig.SquelchType selectedType = mSquelchTypeCombo.getValue();
|
||||
if(selectedType == SquelchDecoderConfig.SquelchType.CTCSS)
|
||||
{
|
||||
CTCSSCode code = mCtcssCodeCombo.getValue();
|
||||
if(code != null)
|
||||
{
|
||||
squelchDecoders.add(new SquelchDecoderConfig(selectedType, code.name()));
|
||||
}
|
||||
}
|
||||
if(selectedType == SquelchDecoderConfig.SquelchType.DCS)
|
||||
{
|
||||
DCSCode code = mDcsCodeCombo.getValue();
|
||||
if(code != null)
|
||||
{
|
||||
squelchDecoders.add(new SquelchDecoderConfig(selectedType, code.name()));
|
||||
}
|
||||
}
|
||||
if(selectedType == SquelchDecoderConfig.SquelchType.NONE)
|
||||
{
|
||||
squelchDecoders.add((new SquelchDecoderConfig(selectedType, null)));
|
||||
}
|
||||
config.setSquelchDecoders(squelchDecoders);
|
||||
getItem().setDecodeConfiguration(config);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.identifier.ctcss;
|
||||
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.module.decode.squelch.ctcss.CTCSSCode;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Continuous Tone Coded Squelch code identifier. This is decoded from the transmitted signal and represents the FROM role.
|
||||
*/
|
||||
public class CTCSSIdentifier extends Identifier<CTCSSCode>
|
||||
{
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param code detected.
|
||||
*/
|
||||
public CTCSSIdentifier(CTCSSCode code)
|
||||
{
|
||||
super(code, IdentifierClass.USER, Form.TONE, Role.FROM);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.CTCSS;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,14 +21,14 @@ package io.github.dsheirer.module.decode;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.alias.action.AliasActionManager;
|
||||
import io.github.dsheirer.audio.AbstractAudioModule;
|
||||
import io.github.dsheirer.audio.AudioModule;
|
||||
import io.github.dsheirer.audio.*;
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMap;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
|
||||
import io.github.dsheirer.dsp.filter.iir.DeemphasisFilter;
|
||||
import io.github.dsheirer.filter.AllPassFilter;
|
||||
import io.github.dsheirer.filter.FilterSet;
|
||||
import io.github.dsheirer.filter.IFilter;
|
||||
@@ -41,9 +41,10 @@ import io.github.dsheirer.module.decode.am.AMDecoderState;
|
||||
import io.github.dsheirer.module.decode.am.DecodeConfigAM;
|
||||
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSDecoder;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSDecoderState;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSMessageFilter;
|
||||
import io.github.dsheirer.module.decode.nbfm.DeemphasisMode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSDecoder;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSDecoderState;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSMessageFilter;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRDecoder;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRDecoderState;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRTrafficChannelManager;
|
||||
@@ -333,7 +334,9 @@ public class DecoderFactory
|
||||
private static void processPassport(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfiguration decodeConfig) {
|
||||
modules.add(new PassportDecoder(decodeConfig));
|
||||
modules.add(new PassportDecoderState());
|
||||
modules.add(new AudioModule(aliasList, AUDIO_FILTER_ENABLE));
|
||||
List<AbstractAudioFilter> filterList = new ArrayList<>();
|
||||
filterList.add(new HighPassAudioFilter());
|
||||
modules.add(new AudioModule(aliasList, filterList));
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH));
|
||||
@@ -357,14 +360,15 @@ public class DecoderFactory
|
||||
ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName());
|
||||
Sync sync = mptConfig.getSync();
|
||||
modules.add(new MPT1327Decoder(sync));
|
||||
|
||||
List<AbstractAudioFilter> filterList = new ArrayList<>();
|
||||
filterList.add(new HighPassAudioFilter());
|
||||
final int callTimeoutMilliseconds = mptConfig.getCallTimeoutSeconds() * 1000;
|
||||
|
||||
// Set max segment audio sample length slightly above call timeout to
|
||||
// not create a new segment if the processing chain finishes a bit after
|
||||
// actual call timeout.
|
||||
long maxAudioSegmentLengthMillis = (callTimeoutMilliseconds + 5000);
|
||||
modules.add(new AudioModule(aliasList, AbstractAudioModule.DEFAULT_TIMESLOT, maxAudioSegmentLengthMillis, AUDIO_FILTER_ENABLE));
|
||||
modules.add(new AudioModule(aliasList, AbstractAudioModule.DEFAULT_TIMESLOT, maxAudioSegmentLengthMillis, filterList));
|
||||
|
||||
SourceType sourceType = channel.getSourceConfiguration().getSourceType();
|
||||
if(sourceType == SourceType.TUNER || sourceType == SourceType.TUNER_MULTIPLE_FREQUENCIES)
|
||||
@@ -403,7 +407,9 @@ public class DecoderFactory
|
||||
private static void processLTRNet(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfigLTRNet decodeConfig) {
|
||||
modules.add(new LTRNetDecoder(decodeConfig));
|
||||
modules.add(new LTRNetDecoderState());
|
||||
modules.add(new AudioModule(aliasList, AUDIO_FILTER_ENABLE));
|
||||
List<AbstractAudioFilter> filterList = new ArrayList<>();
|
||||
filterList.add(new HighPassAudioFilter());
|
||||
modules.add(new AudioModule(aliasList, filterList));
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH));
|
||||
@@ -421,7 +427,9 @@ public class DecoderFactory
|
||||
MessageDirection direction = decodeConfig.getMessageDirection();
|
||||
modules.add(new LTRStandardDecoder(direction));
|
||||
modules.add(new LTRStandardDecoderState());
|
||||
modules.add(new AudioModule(aliasList, AUDIO_FILTER_ENABLE));
|
||||
List<AbstractAudioFilter> filterList = new ArrayList<>();
|
||||
filterList.add(new HighPassAudioFilter());
|
||||
modules.add(new AudioModule(aliasList, filterList));
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH));
|
||||
@@ -444,9 +452,26 @@ public class DecoderFactory
|
||||
}
|
||||
|
||||
DecodeConfigNBFM decodeConfigNBFM = (DecodeConfigNBFM)decodeConfig;
|
||||
modules.add(new NBFMDecoder(decodeConfigNBFM));
|
||||
modules.add(new NBFMDecoderState(channel.getName(), decodeConfigNBFM));
|
||||
modules.add(new AudioModule(aliasList, 0, 60000, decodeConfigNBFM.isAudioFilter()));
|
||||
NBFMDecoderState decoderState = new NBFMDecoderState(channel.getName(), decodeConfigNBFM);
|
||||
NBFMDecoder decoder = new NBFMDecoder(decodeConfigNBFM);
|
||||
modules.add(decoder);
|
||||
modules.add(decoderState);
|
||||
List<AbstractAudioFilter> filterList = new ArrayList<>();
|
||||
if(decodeConfigNBFM.isAudioFilter())
|
||||
{
|
||||
filterList.add(new HighPassAudioFilter());
|
||||
}
|
||||
if(decodeConfigNBFM.getDeemphasis() != null && decodeConfigNBFM.getDeemphasis() != DeemphasisMode.NONE)
|
||||
{
|
||||
float cutoff = (float) decodeConfigNBFM.getDeemphasis().getCutoff();
|
||||
filterList.add(new DeemphasisFilter(8000, cutoff, 1.0f));
|
||||
}
|
||||
if(decodeConfigNBFM.isAudioALC())
|
||||
{
|
||||
filterList.add(new AudioGainFilter(1, 10, 0.90f));
|
||||
}
|
||||
|
||||
modules.add(new AudioModule(aliasList, 0, 60000, filterList));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,7 +487,8 @@ public class DecoderFactory
|
||||
{
|
||||
modules.add(new AMDecoder(configAM));
|
||||
modules.add(new AMDecoderState(channel.getName(), configAM));
|
||||
modules.add(new AudioModule(aliasList, 0, 60000, AUDIO_FILTER_ENABLE));
|
||||
List<AbstractAudioFilter> filterList = new ArrayList<>(); // no default filtering for AM
|
||||
modules.add(new AudioModule(aliasList, 0, 60000, filterList));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -40,6 +40,7 @@ public enum DecoderType
|
||||
|
||||
//Auxiliary Decoders
|
||||
DCS("Digital Coded Squelch (DCS)", "DCS", Protocol.DCS),
|
||||
CTCSS("Continuous Tone Squelch", "CTCSS", Protocol.CTCSS),
|
||||
FLEETSYNC2("Fleetsync II", "Fleetsync2", Protocol.FLEETSYNC),
|
||||
LJ_1200("LJ1200 173.075", "LJ1200", Protocol.LOJACK),
|
||||
MDC1200("MDC1200", "MDC1200", Protocol.MDC1200),
|
||||
|
||||
@@ -93,7 +93,7 @@ public abstract class AnalogDecoderState extends DecoderState implements ISource
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates/starts a decode call evnet.
|
||||
* Creates/starts a decode call event.
|
||||
*/
|
||||
private void startCallEvent()
|
||||
{
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dcs;
|
||||
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.dcs.DCSIdentifier;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Digital Coded Squelch (DCS) tone detected message
|
||||
*/
|
||||
public class DCSMessage extends Message
|
||||
{
|
||||
private final DCSCode mDCSCode;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param code that was detected
|
||||
* @param timestamp when the code was detected
|
||||
*/
|
||||
public DCSMessage(DCSCode code, long timestamp)
|
||||
{
|
||||
super(timestamp);
|
||||
mDCSCode = code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "Digital Coded Squelch (DCS) Detected: " + mDCSCode.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* The DCS code that was detected.
|
||||
* @return code
|
||||
*/
|
||||
public DCSCode getDCSCode()
|
||||
{
|
||||
return mDCSCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return true; //We only send a message when the tone was valid
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.DCS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
return Collections.singletonList(new DCSIdentifier(mDCSCode));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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,23 +19,38 @@
|
||||
package io.github.dsheirer.module.decode.nbfm;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.dsp.squelch.NoiseSquelch;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog;
|
||||
import io.github.dsheirer.module.decode.squelch.SquelchDecoderConfig;
|
||||
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Decoder configuration for an NBFM channel
|
||||
* Decoder configuration for an NBFM channel.
|
||||
*
|
||||
* Supports channel-level CTCSS/DCS tone filtering and FM de-emphasis.
|
||||
*/
|
||||
public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
{
|
||||
private boolean mAudioFilter = true;
|
||||
private boolean mAudioALC = false;
|
||||
private float mSquelchNoiseOpenThreshold = NoiseSquelch.DEFAULT_NOISE_OPEN_THRESHOLD;
|
||||
private float mSquelchNoiseCloseThreshold = NoiseSquelch.DEFAULT_NOISE_CLOSE_THRESHOLD;
|
||||
private int mSquelchHysteresisOpenThreshold = NoiseSquelch.DEFAULT_HYSTERESIS_OPEN_THRESHOLD;
|
||||
private int mSquelchHysteresisCloseThreshold = NoiseSquelch.DEFAULT_HYSTERESIS_CLOSE_THRESHOLD;
|
||||
|
||||
// Channel-level squelch filtering
|
||||
private List<SquelchDecoderConfig> mSquelchDecoders = new ArrayList<>();
|
||||
|
||||
// FM de-emphasis
|
||||
private DeemphasisMode mDeemphasis = DeemphasisMode.NONE;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
@@ -94,6 +109,25 @@ public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
mAudioFilter = audioFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the user wants automatic level control
|
||||
* @return enable status, defaults to false;
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "audioALC")
|
||||
public boolean isAudioALC()
|
||||
{
|
||||
return mAudioALC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the enabled state of high-pass filtering of the demodulated audio.
|
||||
* @param audioALC set to enabled automatic level control (ALC)
|
||||
*/
|
||||
public void setAudioALC(boolean audioALC)
|
||||
{
|
||||
mAudioALC = audioALC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Squelch noise open threshold in the range 0.0 to 1.0 with a default of 0.1
|
||||
* @return noise open threshold
|
||||
@@ -104,6 +138,19 @@ public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
return mSquelchNoiseOpenThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the squelch noise threshold.
|
||||
* @param open in range 0.0 to 1.0 with a default of 0.1
|
||||
*/
|
||||
public void setSquelchNoiseOpenThreshold(float open)
|
||||
{
|
||||
if(open < NoiseSquelch.MINIMUM_NOISE_THRESHOLD || open > NoiseSquelch.MAXIMUM_NOISE_THRESHOLD)
|
||||
{
|
||||
throw new IllegalArgumentException("Squelch noise open threshold is out of range: " + open);
|
||||
}
|
||||
mSquelchNoiseOpenThreshold = open;
|
||||
}
|
||||
|
||||
/**
|
||||
* Squelch noise close threshold in the range 0.0 to 1.0, greater than or equal to open threshold, with a default of 0.2
|
||||
* @return noise close threshold
|
||||
@@ -114,20 +161,6 @@ public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
return mSquelchNoiseCloseThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the squelch noise threshold.
|
||||
* @param open in range 0.0 to 1.0 with a default of 0.1
|
||||
*/
|
||||
public void setSquelchNoiseOpenThreshold(float open)
|
||||
{
|
||||
if(open < NoiseSquelch.MINIMUM_NOISE_THRESHOLD || open > NoiseSquelch.MAXIMUM_NOISE_THRESHOLD)
|
||||
{
|
||||
throw new IllegalArgumentException("Squelch noise open threshold is out of range: " + open);
|
||||
}
|
||||
|
||||
mSquelchNoiseOpenThreshold = open;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the squelch noise close threshold.
|
||||
* @param close in range 0.0 to 1.0 and greater than or equal to open, with a default of 0.1
|
||||
@@ -138,7 +171,6 @@ public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
{
|
||||
throw new IllegalArgumentException("Squelch noise close threshold is out of range: " + close);
|
||||
}
|
||||
|
||||
mSquelchNoiseCloseThreshold = close;
|
||||
}
|
||||
|
||||
@@ -162,7 +194,6 @@ public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
{
|
||||
throw new IllegalArgumentException("Squelch hysteresis open threshold is out of range: " + open);
|
||||
}
|
||||
|
||||
mSquelchHysteresisOpenThreshold = open;
|
||||
}
|
||||
|
||||
@@ -186,7 +217,68 @@ public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
{
|
||||
throw new IllegalArgumentException("Squelch hysteresis close threshold is out of range: " + close);
|
||||
}
|
||||
|
||||
mSquelchHysteresisCloseThreshold = close;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of CTCSS/DCS squelch decoders for this channel.
|
||||
*/
|
||||
@JacksonXmlElementWrapper(localName = "squelchDecoders")
|
||||
@JacksonXmlProperty(localName = "squelchDecoder")
|
||||
public List<SquelchDecoderConfig> getSquelchDecoders()
|
||||
{
|
||||
return mSquelchDecoders;
|
||||
}
|
||||
|
||||
public void setSquelchDecoders(List<SquelchDecoderConfig> squelchDecoders)
|
||||
{
|
||||
mSquelchDecoders = squelchDecoders != null ? squelchDecoders : new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a squelch decoder to the channel configuration
|
||||
*/
|
||||
public void addSquelchDecoder(SquelchDecoderConfig decoder)
|
||||
{
|
||||
if(decoder != null)
|
||||
{
|
||||
mSquelchDecoders.add(decoder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a squelch filter from the channel configuration
|
||||
*/
|
||||
public void removeSquelchDecoder(SquelchDecoderConfig decoder)
|
||||
{
|
||||
mSquelchDecoders.remove(decoder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if squelch filtering is enabled for this channel
|
||||
*/
|
||||
@JsonIgnore
|
||||
public boolean isSquelchDecoderEnabled()
|
||||
{
|
||||
List<SquelchDecoderConfig> decoders = getSquelchDecoders();
|
||||
// TODO right now only looking at first and only decoder, need to fix when multiple decoders are possible
|
||||
return !decoders.isEmpty() && decoders.getFirst().getSquelchType() != SquelchDecoderConfig.SquelchType.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* FM de-emphasis mode. Standard FM broadcasting uses pre-emphasis to boost high
|
||||
* frequencies during transmission. De-emphasis restores flat frequency response
|
||||
* during receive, improving audio clarity.
|
||||
* TIA-603-E is the US standard for NBFM de-emphasis.
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "deemphasis")
|
||||
public DeemphasisMode getDeemphasis()
|
||||
{
|
||||
return mDeemphasis;
|
||||
}
|
||||
|
||||
public void setDeemphasis(DeemphasisMode deemphasis)
|
||||
{
|
||||
mDeemphasis = deemphasis != null ? deemphasis : DeemphasisMode.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.module.decode.nbfm;
|
||||
|
||||
/**
|
||||
* FM de-emphasis
|
||||
*
|
||||
* Per TIA-603-E, all NBFM use a -6 dB per octave roll off from 300 Hz to 3000 Hz.
|
||||
* It also specifies an additional -12 dB above 2500 (not implemented to save on filter passes, the resampler
|
||||
* takes care of a lot of that), and an additional -6 dB below 500 (not implemented to save on filter passes, the
|
||||
* existing high pass filter takes care of most of that). European standard has same specifications (unlike
|
||||
* commercial FM, which the search engines struggle with).
|
||||
*/
|
||||
public enum DeemphasisMode
|
||||
{
|
||||
NONE("None", 0),
|
||||
//OTHER_166US("166 µs (Other)", 6024),
|
||||
NBFM_300("-6dB/octave @ 300-3KHz", 300);
|
||||
|
||||
private final String mLabel;
|
||||
private final int mCutoff;
|
||||
|
||||
DeemphasisMode(String label, int cutoffFreq)
|
||||
{
|
||||
mLabel = label;
|
||||
mCutoff = cutoffFreq;
|
||||
}
|
||||
|
||||
public int getCutoff()
|
||||
{
|
||||
return mCutoff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -37,6 +37,14 @@ import io.github.dsheirer.dsp.squelch.NoiseSquelchState;
|
||||
import io.github.dsheirer.dsp.window.WindowType;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.SquelchControlDecoder;
|
||||
import io.github.dsheirer.module.decode.squelch.SquelchCodeState;
|
||||
import io.github.dsheirer.module.decode.squelch.ctcss.CTCSSMessage;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSDecoder;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSMessage;
|
||||
import io.github.dsheirer.module.decode.squelch.SquelchDecoderConfig;
|
||||
import io.github.dsheirer.module.decode.squelch.ctcss.CTCSSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.ctcss.CTCSSDetector;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
||||
@@ -46,10 +54,11 @@ import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* NBFM decoder with integrated noise squelch. Demodulates complex sample buffers and feeds unfiltered, demodulated
|
||||
* audio to Noise Squelch. Squelch operates on the noise level with open and close thresholds to pass low-noise audio
|
||||
* and block high-noise audio. Audio is filtered and resampled to 8 kHz for downstream consumers.
|
||||
* NBFM decoder with integrated noise squelch, channel-level squelch tone decoders.
|
||||
*
|
||||
*/
|
||||
public class NBFMDecoder extends SquelchControlDecoder implements ISourceEventListener, IComplexSamplesListener,
|
||||
Listener<ComplexSamples>, IRealBufferProvider, IDecoderStateEventProvider, INoiseSquelchController
|
||||
@@ -67,6 +76,12 @@ public class NBFMDecoder extends SquelchControlDecoder implements ISourceEventLi
|
||||
private Listener<DecoderStateEvent> mDecoderStateEventListener;
|
||||
private RealResampler mResampler;
|
||||
private final double mChannelBandwidth;
|
||||
private boolean mSquelchDecoderEnabled = false;
|
||||
private List<CTCSSCode> mConfiguredCTCSSCodes = new ArrayList<>();
|
||||
private Set<DCSCode> mConfiguredDCSCodes = new HashSet<>();
|
||||
private CTCSSDetector mCTCSSDetector = null;
|
||||
private DCSDecoder mDCSDetector = null;
|
||||
private boolean mMute = true;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -82,7 +97,11 @@ public class NBFMDecoder extends SquelchControlDecoder implements ISourceEventLi
|
||||
mNoiseSquelch = new NoiseSquelch(config.getSquelchNoiseOpenThreshold(), config.getSquelchNoiseCloseThreshold(),
|
||||
config.getSquelchHysteresisOpenThreshold(), config.getSquelchHysteresisCloseThreshold());
|
||||
|
||||
//Send squelch controlled audio to the resampler and notify the decoder state that the call continues.
|
||||
// Configure squelch code decoders
|
||||
configureSquelchDecoders(config);
|
||||
|
||||
// Audio pipeline: NoiseSquelch -> Resampler -> CTCSS or DCS -> 1200 baud decoders -> HPF -> De-emphasis -> gain -> Output
|
||||
|
||||
mNoiseSquelch.setAudioListener(audio -> {
|
||||
// if squelch is closing (it hasn't propagated yet to mute the audio)
|
||||
// call the resampler with lastBatch set to true. This will zero pad the input buffer and ensure
|
||||
@@ -94,27 +113,91 @@ public class NBFMDecoder extends SquelchControlDecoder implements ISourceEventLi
|
||||
else
|
||||
{
|
||||
mResampler.resample(audio); // this method will set lastBatch to false
|
||||
notifyCallContinuation();
|
||||
if (!mSquelchDecoderEnabled)
|
||||
{
|
||||
notifyCallContinuation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Notify the decoder state of call starts and ends
|
||||
mNoiseSquelch.setSquelchStateListener(squelchState -> {
|
||||
if(squelchState == SquelchState.SQUELCH)
|
||||
if(squelchState == SquelchState.SQUELCH && !mSquelchDecoderEnabled)
|
||||
{
|
||||
notifyCallEnd();
|
||||
}
|
||||
else
|
||||
{
|
||||
notifyCallStart();
|
||||
// When tone filtering is enabled, delay call start until tone is matched.
|
||||
// The call start will be triggered from ctcssDetected/dcsDetected instead.
|
||||
if(!mSquelchDecoderEnabled)
|
||||
{
|
||||
notifyCallStart();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoder type.
|
||||
* @return type
|
||||
* Configures the set of allowed codes from the channel decode configuration
|
||||
*/
|
||||
private void configureSquelchDecoders(DecodeConfigNBFM config)
|
||||
{
|
||||
mSquelchDecoderEnabled = config.isSquelchDecoderEnabled();
|
||||
|
||||
if(mSquelchDecoderEnabled)
|
||||
{
|
||||
// at the present time, only a single decoder per channel is configured, however the playlist and other
|
||||
// storage allows for multiple decoders per channel
|
||||
List<SquelchDecoderConfig> decoders = config.getSquelchDecoders();
|
||||
for(SquelchDecoderConfig decoder : decoders)
|
||||
{
|
||||
if(!decoder.isValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
switch(decoder.getSquelchType())
|
||||
{
|
||||
case CTCSS:
|
||||
CTCSSCode ctcss = decoder.getCTCSSCode();
|
||||
if(ctcss != null && ctcss != CTCSSCode.UNKNOWNH)
|
||||
{
|
||||
mConfiguredCTCSSCodes.add(ctcss);
|
||||
}
|
||||
break;
|
||||
case DCS:
|
||||
DCSCode dcs = decoder.getDCSCode();
|
||||
if(dcs != null)
|
||||
{
|
||||
mConfiguredDCSCodes.add(dcs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we configured tone filtering but have no valid tones, disable it
|
||||
if(mConfiguredCTCSSCodes.isEmpty() && mConfiguredDCSCodes.isEmpty())
|
||||
{
|
||||
mLog.warn("Tone filtering enabled but no valid CTCSS/DCS codes configured — disabling tone filter");
|
||||
mSquelchDecoderEnabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create CTCSS detector if we have CTCSS codes to detect
|
||||
if(!mConfiguredCTCSSCodes.isEmpty())
|
||||
{
|
||||
mCTCSSDetector = new CTCSSDetector(mConfiguredCTCSSCodes);
|
||||
}
|
||||
|
||||
// Create DCS detector if we have DCS codes to detect
|
||||
if(!mConfiguredDCSCodes.isEmpty())
|
||||
{
|
||||
mDCSDetector = new DCSDecoder(mConfiguredDCSCodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
@@ -198,7 +281,7 @@ public class NBFMDecoder extends SquelchControlDecoder implements ISourceEventLi
|
||||
public void start() {}
|
||||
|
||||
@Override
|
||||
public void stop() {}
|
||||
public void stop() {}
|
||||
|
||||
/**
|
||||
* Broadcasts the demodulated, resampled to 8 kHz audio samples to the registered listener.
|
||||
@@ -261,7 +344,13 @@ public class NBFMDecoder extends SquelchControlDecoder implements ISourceEventLi
|
||||
float[] filteredQ = mQBasebandFilter.filter(decimatedQ);
|
||||
|
||||
float[] demodulated = mDemodulator.demodulate(filteredI, filteredQ);
|
||||
|
||||
/**
|
||||
* The following call completes a series of tasks:
|
||||
* - determines noise squelch states
|
||||
* - resamples audio via the squelch listener
|
||||
* - processes resample audio for squelch decoders via the resampler listener
|
||||
* - broadcasts audio to downstream registered listeners, such as audioModule as determined by DecoderFactory
|
||||
*/
|
||||
mNoiseSquelch.process(demodulated);
|
||||
|
||||
//Once we process the sample buffer, if the ending state is squelch closed, update the decoder state that we
|
||||
@@ -372,7 +461,16 @@ public class NBFMDecoder extends SquelchControlDecoder implements ISourceEventLi
|
||||
|
||||
float[] coefficients = null;
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder().sampleRate(decimatedSampleRate * 2).gridDensity(16).oddLength(true).passBandCutoff(passBandStop).passBandAmplitude(1.0).passBandRipple(0.01).stopBandStart(stopBandStart).stopBandAmplitude(0.0).stopBandRipple(0.005) //Approximately 90 dB attenuation
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(decimatedSampleRate * 2)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(passBandStop)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(stopBandStart)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.005)
|
||||
.build();
|
||||
|
||||
try
|
||||
@@ -394,7 +492,85 @@ public class NBFMDecoder extends SquelchControlDecoder implements ISourceEventLi
|
||||
mQBasebandFilter = FilterFactory.getRealFilter(coefficients);
|
||||
|
||||
mResampler = new RealResampler(decimatedSampleRate, DEMODULATED_AUDIO_SAMPLE_RATE, 4192, 512);
|
||||
mResampler.setListener(NBFMDecoder.this::broadcast);
|
||||
|
||||
mResampler.setListener(NBFMDecoder.this::processResampledAudio);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the Resampled audio for squelch decoding. This also where audio is muted if no tone code match.
|
||||
* @param resampled audio buffer
|
||||
*/
|
||||
private void processResampledAudio(float [] resampled)
|
||||
{
|
||||
if(!mNoiseSquelch.isSquelched())
|
||||
{
|
||||
if(mCTCSSDetector != null)
|
||||
{
|
||||
CTCSSMessage ctcssMessage = mCTCSSDetector.process(resampled);
|
||||
getMessageListener().receive(ctcssMessage); // sending: one of the listeners is NBFMDecoderState
|
||||
if(ctcssMessage.getCallEvent() != null)
|
||||
{
|
||||
// handles START, CONTINUATION, END.
|
||||
broadcast(new DecoderStateEvent(this, ctcssMessage.getCallEvent(), State.CALL, 0));
|
||||
}
|
||||
mMute = ctcssMessage.getMutedStatus();
|
||||
}
|
||||
/*
|
||||
* While the CTCSS decoder can determine a tone in a single buffer of resampled audio, the DCS decoder
|
||||
* requires 2.6 buffers to determine a code. DCSMessage will be null for audio buffers where there is
|
||||
* no state change.
|
||||
*/
|
||||
if(mDCSDetector != null)
|
||||
{
|
||||
DCSMessage dcsMessage = mDCSDetector.process(resampled);
|
||||
if (dcsMessage != null)
|
||||
{
|
||||
getMessageListener().receive(dcsMessage); // sending: one of the listeners is NBFMDecoderState
|
||||
if (dcsMessage.getCallEvent() != null)
|
||||
{
|
||||
// handles START, CONTINUATION, END.
|
||||
broadcast(new DecoderStateEvent(this, dcsMessage.getCallEvent(), State.CALL, 0));
|
||||
}
|
||||
mMute = dcsMessage.getMutedStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// there can still be one buffer's worth of resampled audio after .isSquelched()
|
||||
// which may result in two consecutive paths here. While the squelch decoders will mute here,
|
||||
// the noise squelch will mute audio downstream in AudioModule (via broadcast(resampled) below).
|
||||
if(mCTCSSDetector != null)
|
||||
{
|
||||
mCTCSSDetector.reset();
|
||||
CTCSSMessage message = new CTCSSMessage(mConfiguredCTCSSCodes.getFirst(),
|
||||
"Noise squelch closed.",
|
||||
DecoderStateEvent.Event.END,
|
||||
SquelchCodeState.LOST);
|
||||
getMessageListener().receive(message); // sending: one of the listeners is NBFMDecoderState
|
||||
notifyCallEnd();
|
||||
mMute = true;
|
||||
}
|
||||
if(mDCSDetector != null)
|
||||
{
|
||||
mDCSDetector.inlineReset();
|
||||
DCSMessage message = new DCSMessage(mDCSDetector.getmConfiguredCode(),
|
||||
"Noise squelch closed.",
|
||||
DecoderStateEvent.Event.END,
|
||||
SquelchCodeState.LOST);
|
||||
getMessageListener().receive(message); // sending: one of the listeners is NBFMDecoderState
|
||||
notifyCallEnd();
|
||||
mMute = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(mSquelchDecoderEnabled && mMute)
|
||||
{
|
||||
return; // drop audio buffer here for muting
|
||||
}
|
||||
|
||||
broadcast(resampled);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -24,11 +24,22 @@ import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.analog.AnalogDecoderState;
|
||||
import io.github.dsheirer.module.decode.squelch.ctcss.CTCSSMessage;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSMessage;
|
||||
import io.github.dsheirer.module.decode.squelch.SquelchDecoderConfig;
|
||||
import io.github.dsheirer.module.decode.squelch.ctcss.CTCSSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* NBFM decoder state
|
||||
* NBFM decoder state - tracks channel tone filtering configuration and detected tones
|
||||
*/
|
||||
public class NBFMDecoderState extends AnalogDecoderState
|
||||
{
|
||||
@@ -36,16 +47,32 @@ public class NBFMDecoderState extends AnalogDecoderState
|
||||
private Identifier mChannelNameIdentifier;
|
||||
private Identifier mTalkgroupIdentifier;
|
||||
|
||||
// Tone filter configuration (from DecodeConfigNBFM)
|
||||
private boolean mSquelchDecoderEnabled = false;
|
||||
private List<SquelchDecoderConfig> mConfiguredSquelchDecoders = new ArrayList<>();
|
||||
|
||||
// Current status
|
||||
private volatile String mToneStatus = "No tone detected";
|
||||
|
||||
// Per-tone detection counts: key = display string, value = [acceptCount, rejectCount]
|
||||
private final Map<String, int[]> mToneCounts = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param channelName to use for this channel
|
||||
* @param decodeConfig with talkgroup identifier
|
||||
* @param decodeConfig with talkgroup identifier and tone filter settings
|
||||
*/
|
||||
public NBFMDecoderState(String channelName, DecodeConfigNBFM decodeConfig)
|
||||
{
|
||||
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : "NBFM CHANNEL";
|
||||
mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.CONFIGURATION, Form.CHANNEL_NAME, Role.ANY);
|
||||
mTalkgroupIdentifier = new NBFMTalkgroup(decodeConfig.getTalkgroup());
|
||||
|
||||
mSquelchDecoderEnabled = decodeConfig.isSquelchDecoderEnabled();
|
||||
if(mSquelchDecoderEnabled && decodeConfig.getSquelchDecoders() != null)
|
||||
{
|
||||
mConfiguredSquelchDecoders.addAll(decodeConfig.getSquelchDecoders());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,13 +93,171 @@ public class NBFMDecoderState extends AnalogDecoderState
|
||||
return mTalkgroupIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by IMessage receive when a matching CTCSS tone is detected (allowed)
|
||||
*/
|
||||
public void setAcceptedCTCSS(CTCSSCode code)
|
||||
{
|
||||
if(code != null)
|
||||
{
|
||||
mToneStatus = "CTCSS: " + code.getDisplayString() + " [ALLOWED]";
|
||||
incrementCount(code.getDisplayString(), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by IMessage receive when a matching DCS code is detected (allowed)
|
||||
*/
|
||||
public void setAcceptedDCS(DCSCode code)
|
||||
{
|
||||
if(code != null)
|
||||
{
|
||||
mToneStatus = "DCS: " + code.toString() + " [ALLOWED]";
|
||||
incrementCount("DCS " + code.toString(), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by IMessage receive when a non-matching CTCSS tone is detected (rejected)
|
||||
*/
|
||||
public void setRejectedCTCSS(CTCSSCode code)
|
||||
{
|
||||
if(code != null)
|
||||
{
|
||||
mToneStatus = "CTCSS: " + code.getDisplayString() + " [REJECTED]";
|
||||
incrementCount(code.getDisplayString(), false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by IMessage receive when a non-matching DCS code is detected (rejected)
|
||||
*/
|
||||
public void setRejectedDCS(DCSCode code)
|
||||
{
|
||||
if(code != null)
|
||||
{
|
||||
mToneStatus = "DCS: " + code.toString() + " [REJECTED]";
|
||||
incrementCount("DCS " + code.toString(), false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by IMessage receive when tone is lost
|
||||
*/
|
||||
public void setToneLost()
|
||||
{
|
||||
mToneStatus = "No tone detected";
|
||||
getIdentifierCollection().remove(Form.TONE);
|
||||
}
|
||||
|
||||
private void incrementCount(String toneLabel, boolean accepted)
|
||||
{
|
||||
synchronized(mToneCounts)
|
||||
{
|
||||
int[] counts = mToneCounts.computeIfAbsent(toneLabel, k -> new int[]{0, 0});
|
||||
if(accepted)
|
||||
{
|
||||
if (counts[0] < Integer.MAX_VALUE)
|
||||
{
|
||||
counts[0]++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(counts[1] < Integer.MAX_VALUE)
|
||||
{
|
||||
counts[1]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides information for UI display in Now Playing panel.
|
||||
*/
|
||||
@Override
|
||||
public String getActivitySummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Activity Summary\n");
|
||||
sb.append("\tDecoder: NBFM");
|
||||
sb.append("\n\n");
|
||||
sb.append("Activity Summary - Decoder:NBFM\n");
|
||||
|
||||
sb.append("\n\nSquelch Decoder: ");
|
||||
if(mSquelchDecoderEnabled)
|
||||
{
|
||||
sb.append("ENABLED\n");
|
||||
sb.append("Configured decoder: ");
|
||||
if(!mConfiguredSquelchDecoders.isEmpty())
|
||||
{
|
||||
sb.append(mConfiguredSquelchDecoders.get(0).getDisplayString());
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("Disabled\n");
|
||||
}
|
||||
|
||||
synchronized(mToneCounts)
|
||||
{
|
||||
if(!mToneCounts.isEmpty())
|
||||
{
|
||||
sb.append("\nDetected codes:\n");
|
||||
for(Map.Entry<String, int[]> entry : mToneCounts.entrySet())
|
||||
{
|
||||
String status;
|
||||
int accepted = entry.getValue()[0];
|
||||
int rejected = entry.getValue()[1];
|
||||
if(accepted > 0)
|
||||
{
|
||||
status = accepted < Integer.MAX_VALUE ? "ALLOWED (" + accepted + ")" : "ALLOWED (>Max value)";
|
||||
}
|
||||
else
|
||||
{
|
||||
status = rejected < Integer.MAX_VALUE ? "REJECTED (" + rejected + ")" : "REJECTED (>Max value)";
|
||||
}
|
||||
sb.append("\t").append(entry.getKey()).append(" - ").append(status).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for IMessages originating in NBFM decoder. If the squelch decoders are enabled,
|
||||
* the message contains information on decoded squelch code (CTCSS/DCS) which is added to the
|
||||
* identifier collection. A tally is collected for codes accepted and rejected for use
|
||||
* in getActivitySummary.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void receive(IMessage message)
|
||||
{
|
||||
if(message instanceof CTCSSMessage ctcssMessage)
|
||||
{
|
||||
getIdentifierCollection().update(ctcssMessage.getIdentifiers());
|
||||
if (ctcssMessage.getCodeState() != null)
|
||||
{
|
||||
switch (ctcssMessage.getCodeState())
|
||||
{
|
||||
case ACCEPTED -> setAcceptedCTCSS(ctcssMessage.getCTCSSCode());
|
||||
case REJECTED -> setRejectedCTCSS(ctcssMessage.getCTCSSCode());
|
||||
case LOST -> setToneLost();
|
||||
}
|
||||
}
|
||||
}
|
||||
if(message instanceof DCSMessage dcsMessage)
|
||||
{
|
||||
getIdentifierCollection().update(dcsMessage.getIdentifiers());
|
||||
if (dcsMessage.getCodeState() != null)
|
||||
{
|
||||
switch (dcsMessage.getCodeState())
|
||||
{
|
||||
case ACCEPTED -> setAcceptedDCS(dcsMessage.getDCSCode());
|
||||
case REJECTED -> setRejectedDCS(dcsMessage.getDCSCode());
|
||||
case LOST -> setToneLost();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.module.decode.squelch;
|
||||
|
||||
/**
|
||||
* SquelchCodeState is used in iMessage subclasses such as CTCSSMessage and DCSMessage, and the
|
||||
* NBFMDecoderState to indicate the state of the current squelch code being received.
|
||||
*/
|
||||
|
||||
public enum SquelchCodeState
|
||||
{
|
||||
ACCEPTED,
|
||||
REJECTED,
|
||||
LOST
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.module.decode.squelch;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.module.decode.squelch.ctcss.CTCSSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
* Channel-level tone/code filter configuration. Supports CTCSS & DCS.
|
||||
* Audio is only passed when at least one
|
||||
* configured filter matches the received signal.
|
||||
*/
|
||||
public class SquelchDecoderConfig
|
||||
{
|
||||
|
||||
public enum SquelchType
|
||||
{
|
||||
NONE("None"),
|
||||
CTCSS("CTCSS"),
|
||||
DCS("DCS");
|
||||
|
||||
public static final Set<SquelchType> SQUELCH_TYPE ;
|
||||
static
|
||||
{
|
||||
EnumSet<SquelchType> types = EnumSet.allOf(SquelchType.class);
|
||||
SQUELCH_TYPE = java.util.Collections.unmodifiableSet(types);
|
||||
}
|
||||
private final String mLabel;
|
||||
|
||||
SquelchType(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
|
||||
private SquelchType mSquelchType = SquelchType.NONE;
|
||||
private String mValue = "";
|
||||
|
||||
/**
|
||||
* Default constructor for Jackson XML deserialization
|
||||
*/
|
||||
public SquelchDecoderConfig()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param squelchType of this filter (CTCSS, DCS)
|
||||
* @param value the tone/code value as a string
|
||||
*/
|
||||
public SquelchDecoderConfig(SquelchType squelchType, String value)
|
||||
{
|
||||
mSquelchType = squelchType;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Squelch filter type (None, CTCSS, DCS)
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "squelchType")
|
||||
public SquelchType getSquelchType()
|
||||
{
|
||||
return mSquelchType;
|
||||
}
|
||||
|
||||
public void setSquelchType(SquelchType squelchType)
|
||||
{
|
||||
mSquelchType = squelchType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value for the tone/code filter.
|
||||
* For CTCSS: the CTCSSCode enum name is the frequency (e.g. "179.9")
|
||||
* For DCS: the DCSCode enum name (e.g. "D023N")
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "value")
|
||||
public String getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setValue(String value)
|
||||
{
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CTCSS code for this filter, or null if not a CTCSS filter or value is invalid.
|
||||
*/
|
||||
@JsonIgnore
|
||||
public CTCSSCode getCTCSSCode()
|
||||
{
|
||||
if(mSquelchType == SquelchType.CTCSS && mValue != null && !mValue.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return CTCSSCode.valueOf(mValue);
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
// Try to parse as frequency
|
||||
try
|
||||
{
|
||||
float freq = Float.parseFloat(mValue);
|
||||
return CTCSSCode.fromFrequency(freq);
|
||||
}
|
||||
catch(NumberFormatException nfe)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the DCS code for this filter, or null if not a DCS filter or value is invalid.
|
||||
*/
|
||||
@JsonIgnore
|
||||
public DCSCode getDCSCode()
|
||||
{
|
||||
if(mSquelchType == SquelchType.DCS && mValue != null && !mValue.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return DCSCode.valueOf(mValue);
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this filter configuration is valid
|
||||
*/
|
||||
@JsonIgnore
|
||||
public boolean isValid()
|
||||
{
|
||||
return switch(mSquelchType)
|
||||
{
|
||||
case CTCSS -> getCTCSSCode() != null && getCTCSSCode() != CTCSSCode.UNKNOWNH;
|
||||
case DCS -> getDCSCode() != null;
|
||||
case NONE -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display string for this filter
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getDisplayString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(mSquelchType).append(": ");
|
||||
|
||||
switch(mSquelchType)
|
||||
{
|
||||
case CTCSS:
|
||||
CTCSSCode ctcss = getCTCSSCode();
|
||||
sb.append(ctcss != null ? ctcss.getDisplayString() : mValue);
|
||||
break;
|
||||
case DCS:
|
||||
DCSCode dcs = getDCSCode();
|
||||
sb.append(dcs != null ? dcs.toString() : mValue);
|
||||
break;
|
||||
case NONE:
|
||||
sb.append("No squelch filter configured.");
|
||||
break;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getDisplayString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.module.decode.squelch.ctcss;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Standard CTCSS (Continuous Tone-Coded Squelch System) tones.
|
||||
* Also known as PL (Private Line) tones (Motorola trademark).
|
||||
*
|
||||
* Frequencies and EIA/TIA standard designators from TIA-603.
|
||||
*
|
||||
* Two dummy tones are at each end of the spectrum. It was noted during testing that when a tone was not
|
||||
* present, the algorithm would drift to the lowest one, and we don't want a false detection when in fact there
|
||||
* is no tone at all. Testing never revealed the highest tone falsely detected, but want completeness.
|
||||
*/
|
||||
public enum CTCSSCode
|
||||
{
|
||||
UNKNOWNL(65.0f, "UKNL", "65.0"),
|
||||
TONE_670(67.0f, "XZ", "67.0"),
|
||||
TONE_693(69.3f, "WZ", "69.3"),
|
||||
TONE_719(71.9f, "XA", "71.9"),
|
||||
TONE_744(74.4f, "WA", "74.4"),
|
||||
TONE_770(77.0f, "XB", "77.0"),
|
||||
TONE_797(79.7f, "WB", "79.7"),
|
||||
TONE_825(82.5f, "YZ", "82.5"),
|
||||
TONE_854(85.4f, "YA", "85.4"),
|
||||
TONE_885(88.5f, "YB", "88.5"),
|
||||
TONE_915(91.5f, "ZZ", "91.5"),
|
||||
TONE_948(94.8f, "ZA", "94.8"),
|
||||
TONE_974(97.4f, "ZB", "97.4"),
|
||||
TONE_1000(100.0f, "1Z", "100.0"),
|
||||
TONE_1035(103.5f, "1A", "103.5"),
|
||||
TONE_1072(107.2f, "1B", "107.2"),
|
||||
TONE_1109(110.9f, "2Z", "110.9"),
|
||||
TONE_1148(114.8f, "2A", "114.8"),
|
||||
TONE_1188(118.8f, "2B", "118.8"),
|
||||
TONE_1230(123.0f, "3Z", "123.0"),
|
||||
TONE_1273(127.3f, "3A", "127.3"),
|
||||
TONE_1318(131.8f, "3B", "131.8"),
|
||||
TONE_1365(136.5f, "4Z", "136.5"),
|
||||
TONE_1413(141.3f, "4A", "141.3"),
|
||||
TONE_1462(146.2f, "4B", "146.2"),
|
||||
TONE_1514(151.4f, "5Z", "151.4"),
|
||||
TONE_1567(156.7f, "5A", "156.7"),
|
||||
TONE_1622(162.2f, "5B", "162.2"),
|
||||
TONE_1679(167.9f, "6Z", "167.9"),
|
||||
TONE_1738(173.8f, "6A", "173.8"),
|
||||
TONE_1799(179.9f, "6B", "179.9"),
|
||||
TONE_1862(186.2f, "7Z", "186.2"),
|
||||
TONE_1928(192.8f, "7A", "192.8"),
|
||||
TONE_2035(203.5f, "M1", "203.5"),
|
||||
TONE_2065(206.5f, "M2", "206.5"),
|
||||
TONE_2107(210.7f, "M3", "210.7"),
|
||||
TONE_2181(218.1f, "M4", "218.1"),
|
||||
TONE_2257(225.7f, "M5", "225.7"),
|
||||
TONE_2291(229.1f, "M6", "229.1"),
|
||||
TONE_2336(233.6f, "M7", "233.6"),
|
||||
TONE_2418(241.8f, "8Z", "241.8"),
|
||||
TONE_2503(250.3f, "9Z", "250.3"),
|
||||
TONE_2541(254.1f, "0Z", "254.1"),
|
||||
UNKNOWNH(260.0f, "UKNH", "260.0");
|
||||
/**
|
||||
* Set of all standard (non-UNKNOWN) CTCSS codes
|
||||
*/
|
||||
public static final Set<CTCSSCode> STANDARD_CODES;
|
||||
public static final Set<CTCSSCode> DETECTING_CODES;
|
||||
|
||||
static
|
||||
{
|
||||
EnumSet<CTCSSCode> codes = EnumSet.allOf(CTCSSCode.class);
|
||||
DETECTING_CODES = java.util.Collections.unmodifiableSet(codes);
|
||||
codes.remove(UNKNOWNL);
|
||||
codes.remove(UNKNOWNH);
|
||||
STANDARD_CODES = java.util.Collections.unmodifiableSet(codes);
|
||||
}
|
||||
|
||||
private final float mFrequency;
|
||||
private final String mDesignator;
|
||||
private final String mFrequencyLabel;
|
||||
|
||||
CTCSSCode(float frequency, String designator, String frequencyLabel)
|
||||
{
|
||||
mFrequency = frequency;
|
||||
mDesignator = designator;
|
||||
mFrequencyLabel = frequencyLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tone frequency in Hz
|
||||
*/
|
||||
public float getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* EIA/TIA standard designator (e.g. "XZ", "1A", "M7")
|
||||
*/
|
||||
public String getDesignator()
|
||||
{
|
||||
return mDesignator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display string showing frequency and designator
|
||||
*/
|
||||
public String getDisplayString()
|
||||
{
|
||||
return mFrequencyLabel + " Hz (" + mDesignator + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getDisplayString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the CTCSSCode closest to the given frequency within ±1 Hz tolerance.
|
||||
* @param frequency in Hz
|
||||
* @return matching code or UNKNOWN if no match
|
||||
*/
|
||||
public static CTCSSCode fromFrequency(float frequency)
|
||||
{
|
||||
CTCSSCode best = UNKNOWNH;
|
||||
float bestDelta = Float.MAX_VALUE;
|
||||
|
||||
for(CTCSSCode code : STANDARD_CODES)
|
||||
{
|
||||
float delta = Math.abs(code.mFrequency - frequency);
|
||||
if(delta < bestDelta)
|
||||
{
|
||||
bestDelta = delta;
|
||||
best = code;
|
||||
}
|
||||
}
|
||||
|
||||
return bestDelta <= 1.0f ? best : UNKNOWNH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.module.decode.squelch.ctcss;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.module.decode.squelch.SquelchCodeState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Real-time CTCSS tone detector using the Goertzel algorithm.
|
||||
*
|
||||
* Processing approach:
|
||||
* - Analyze an 8 KHz audio block of 512 samples
|
||||
* - Runs Goertzel on each standard frequency
|
||||
* - Run a standard deviation on the array of detected tones to determine a threshold
|
||||
* - Reports the strongest matching tone if it exceeds the threshold
|
||||
* Processing can be logged when the decoded messages log is turned on in the channel configuration of the playlist
|
||||
* or by viewing the messages tab in the Now Playing window.
|
||||
*/
|
||||
public class CTCSSDetector
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(CTCSSDetector.class);
|
||||
private static final double RESAMPLED_AUDIO_SAMPLE_RATE = 8000.0;
|
||||
private static final int MIN_BLOCK_SIZE = 512;
|
||||
|
||||
/**
|
||||
* K factor is the multiplier * std deviation which determines threshold
|
||||
*/
|
||||
private static final float K_FACTOR = 3.0f;
|
||||
/**
|
||||
* Number of consecutive detections required before reporting a match.
|
||||
* The first and last detected tones in a transmission tend to be incorrect,
|
||||
* so we require at least 2 detections up front and at the end.
|
||||
*/
|
||||
private static final int OPEN_THRESHOLD_COUNT = 2;
|
||||
private static final int CLOSE_THRESHOLD_COUNT = 2;
|
||||
// Detection states
|
||||
private int mOpenCounter = 0;
|
||||
private int mCloseCounter = 0;
|
||||
private boolean mMuted = true;
|
||||
private int mRejectedQualification = 0;
|
||||
private CTCSSCode mPreviousRejectedCode = null;
|
||||
|
||||
private final List<CTCSSCode> mTargetCodes; // codes we are looking to match in this channel
|
||||
private final float[] mDetectingFrequencies;
|
||||
private final CTCSSCode[] mDetectingCodeArray; // codes we are detecting
|
||||
// Goertzel coefficients for each target frequency
|
||||
private final double[] mCoefficients;
|
||||
|
||||
/**
|
||||
* Constructs a CTCSS detector for a specific set of target tones.
|
||||
*
|
||||
* @param targetCodes the set of CTCSS codes to accept as matches. If null or empty, accepts all standard codes.
|
||||
*/
|
||||
public CTCSSDetector(List<CTCSSCode> targetCodes)
|
||||
{
|
||||
mTargetCodes = targetCodes;
|
||||
|
||||
// Detect all codes including one on each end of the spectrum (DETECTING_CODES)
|
||||
// We find the strongest tone across all frequencies, then check if it's in our allowed set.
|
||||
Set<CTCSSCode> allCodes = CTCSSCode.DETECTING_CODES;
|
||||
mDetectingCodeArray = allCodes.toArray(new CTCSSCode[0]);
|
||||
mDetectingFrequencies = new float[mDetectingCodeArray.length];
|
||||
mCoefficients = new double[mDetectingCodeArray.length];
|
||||
|
||||
for(int i = 0; i < mDetectingCodeArray.length; i++)
|
||||
{
|
||||
mDetectingFrequencies[i] = mDetectingCodeArray[i].getFrequency();
|
||||
}
|
||||
|
||||
// Pre-compute Goertzel coefficients: 2 * cos(2π * freq / sampleRate)
|
||||
for(int i = 0; i < mDetectingFrequencies.length; i++)
|
||||
{
|
||||
double normalizedFreq = mDetectingFrequencies[i] / RESAMPLED_AUDIO_SAMPLE_RATE;
|
||||
mCoefficients[i] = (2.0 * Math.cos(2.0 * Math.PI * normalizedFreq));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a buffer of demodulated FM audio samples.
|
||||
* Goertzel analysis performed on a block of 512 audio samples and the relative power is calculated
|
||||
* for each CTCSS tone and placed into a frequency distribution array. The standard deviation of the array
|
||||
* is calculated to determine threshold based on the noise. The highest powered tone must exceed this threshold
|
||||
* to be considered for match detection. Testing revealed that there can be multiple peaks present in
|
||||
* the distribution that were considered to be interference rather than desired signals.
|
||||
*
|
||||
* @param samples demodulated audio samples
|
||||
* @return CTCSSMessage for broadcasting state to listeners
|
||||
*/
|
||||
public CTCSSMessage process(float[] samples)
|
||||
{
|
||||
CTCSSMessage ctcssMessage = new CTCSSMessage(mTargetCodes.getFirst());
|
||||
double[] distribution = new double[mDetectingFrequencies.length];
|
||||
double maxPower = 0;
|
||||
int maxIndex = -1;
|
||||
if(samples == null || samples.length < MIN_BLOCK_SIZE)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for(int i = 0; i < mDetectingFrequencies.length; i++)
|
||||
{
|
||||
double power = goertzel(samples, samples.length, mCoefficients[i]);
|
||||
distribution[i] = power;
|
||||
|
||||
if(power > maxPower)
|
||||
{
|
||||
maxPower = power;
|
||||
maxIndex = i;
|
||||
}
|
||||
}
|
||||
// Testing shows that if the power is below approx. 100, the tone can't be detected reliably
|
||||
// The lowest-most and upper-most tones are not valid CTCSS tones.
|
||||
if(maxPower < 100 || maxIndex == 0 || maxIndex == mDetectingFrequencies.length - 1)
|
||||
{
|
||||
// skip further detection
|
||||
ctcssMessage.setFirstThreshold(false);
|
||||
ctcssMessage.setMessage("Signal too weak or detected CTCSS tone is outside of range of valid tones");
|
||||
updateDetectionState(null, ctcssMessage);
|
||||
return ctcssMessage;
|
||||
}
|
||||
|
||||
ctcssMessage.setFirstThreshold(true);
|
||||
// Determine threshold based on the noise level in the distribution using standard deviation.
|
||||
// adjacent channel interference from strong signals will cause multiple spikes in the distribution
|
||||
double sum = 0.0;
|
||||
for (double num : distribution)
|
||||
{
|
||||
sum += num;
|
||||
}
|
||||
double mean = sum / distribution.length;
|
||||
double varianceSum = 0.0;
|
||||
for (double num : distribution)
|
||||
{
|
||||
varianceSum += Math.pow(num - mean, 2);
|
||||
}
|
||||
|
||||
double variance = varianceSum / (distribution.length - 1); // sample type of std dev
|
||||
double stdDev = Math.sqrt(variance);
|
||||
double threshold = (stdDev * K_FACTOR) + mean;
|
||||
ctcssMessage.setPower(maxPower);
|
||||
ctcssMessage.setPowerThreshold(threshold);
|
||||
if(maxPower > threshold)
|
||||
{
|
||||
CTCSSCode detected = mDetectingCodeArray[maxIndex];
|
||||
updateDetectionState(detected, ctcssMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
updateDetectionState(null, ctcssMessage); // set the code to null
|
||||
}
|
||||
|
||||
return ctcssMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goertzel algorithm - computes the energy at a specific frequency.
|
||||
*
|
||||
* @param samples the input samples
|
||||
* @param numSamples number of samples to process
|
||||
* @param coefficient pre-computed 2*cos(2π*f/fs)
|
||||
* @return the magnitude squared at the target frequency
|
||||
*/
|
||||
private double goertzel(float[] samples, int numSamples, double coefficient)
|
||||
{
|
||||
double s0 = 0;
|
||||
double s1 = 0;
|
||||
double s2 = 0;
|
||||
|
||||
for(int i = 0; i < numSamples; i++)
|
||||
{
|
||||
s0 = samples[i] + (coefficient * s1) - s2;
|
||||
s2 = s1;
|
||||
s1 = s0;
|
||||
}
|
||||
|
||||
// Magnitude squared = s1^2 + s2^2 - coefficient * s1 * s2
|
||||
return (s1 * s1) + (s2 * s2) - (coefficient * s1 * s2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles detection logic of a CTCSS tone in the current block and updates
|
||||
* the CTCSSMessage states.
|
||||
* @param newCode currently detected CTCSS code, or null if no code detected
|
||||
* @param ctcssMessage current ctcssMessage for updating
|
||||
*/
|
||||
private void updateDetectionState(CTCSSCode newCode, CTCSSMessage ctcssMessage)
|
||||
{
|
||||
if (mMuted)
|
||||
{
|
||||
if (newCode != null && mTargetCodes.contains(newCode))
|
||||
{
|
||||
if (mOpenCounter >= OPEN_THRESHOLD_COUNT)
|
||||
{
|
||||
// unmute and report detected code
|
||||
mMuted = false;
|
||||
ctcssMessage.setMutedStatus(false);
|
||||
ctcssMessage.setCTCSSCode(newCode);
|
||||
ctcssMessage.setMessage("Correct tone detected, thresholds passed, now unmuting.");
|
||||
ctcssMessage.setCallEvent(DecoderStateEvent.Event.START);
|
||||
ctcssMessage.setCodeState(SquelchCodeState.ACCEPTED);
|
||||
mCloseCounter = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// increment counter and wait for next audio buffer
|
||||
mOpenCounter++;
|
||||
ctcssMessage.setCTCSSCode(newCode);
|
||||
ctcssMessage.setMessage("Waiting for consecutive correct detections.");
|
||||
}
|
||||
}
|
||||
else // wrong tone or no tone, still muted
|
||||
{
|
||||
mOpenCounter = 0;
|
||||
ctcssMessage.setMutedStatus(true);
|
||||
ctcssMessage.setCTCSSCode(newCode);
|
||||
if(newCode != null)
|
||||
{
|
||||
/*
|
||||
* Qualify the rejected code so that the information in the Now Playing -> Details tab
|
||||
* is accurate in case someone doesn't know the correct squelch code for this channel.
|
||||
* We don't want to display codes that are just single detections in noise.
|
||||
*/
|
||||
if(mRejectedQualification == OPEN_THRESHOLD_COUNT)
|
||||
{
|
||||
if(newCode == mPreviousRejectedCode)
|
||||
{
|
||||
ctcssMessage.setCodeState(SquelchCodeState.REJECTED);
|
||||
// this will increment past the OPEN_THRESHOLD_COUNT so the rejected state is only sent once.
|
||||
mRejectedQualification++;
|
||||
}
|
||||
else
|
||||
{
|
||||
mRejectedQualification = 0;
|
||||
mPreviousRejectedCode = newCode;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(newCode == mPreviousRejectedCode)
|
||||
{
|
||||
mRejectedQualification++;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
mRejectedQualification = 0;
|
||||
mPreviousRejectedCode = newCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ctcssMessage.setCodeState(SquelchCodeState.LOST);
|
||||
}
|
||||
}
|
||||
}
|
||||
else // unmuted and call is ongoing
|
||||
{
|
||||
if (newCode != null && mTargetCodes.contains(newCode))
|
||||
{
|
||||
// all is good, call continues
|
||||
mCloseCounter = 0;
|
||||
ctcssMessage.setMutedStatus(false);
|
||||
ctcssMessage.setCTCSSCode(newCode);
|
||||
ctcssMessage.setCallEvent(DecoderStateEvent.Event.CONTINUATION);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mCloseCounter >= CLOSE_THRESHOLD_COUNT)
|
||||
{
|
||||
// Mute audio and report rejected code or lost tone
|
||||
mMuted = true;
|
||||
ctcssMessage.setMutedStatus(true);
|
||||
ctcssMessage.setCTCSSCode(newCode);
|
||||
ctcssMessage.setMessage("Incorrect tone detected or lost, now muting.");
|
||||
ctcssMessage.setCallEvent(DecoderStateEvent.Event.END);
|
||||
mOpenCounter = 0;
|
||||
if(newCode != null)
|
||||
{
|
||||
ctcssMessage.setCodeState(SquelchCodeState.REJECTED);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctcssMessage.setCodeState(SquelchCodeState.LOST);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// all is still good with the call, but a bad tone was decoded and the closeCounter hasn't
|
||||
// reached a threshold yet.
|
||||
mCloseCounter++;
|
||||
ctcssMessage.setMutedStatus(false);
|
||||
ctcssMessage.setCTCSSCode(newCode);
|
||||
ctcssMessage.setMessage("Waiting for consecutive incorrect or no tone.");
|
||||
ctcssMessage.setCallEvent(DecoderStateEvent.Event.CONTINUATION);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the detector state. Called when the noise squelch closes
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mMuted = true;
|
||||
mCloseCounter = 0;
|
||||
mOpenCounter = 0;
|
||||
mPreviousRejectedCode = null;
|
||||
mRejectedQualification = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.module.decode.squelch.ctcss;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.ctcss.CTCSSIdentifier;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.squelch.SquelchCodeState;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CTCSS Detector message carrying CTCSS code, state and debug info for distribution
|
||||
*/
|
||||
public class CTCSSMessage extends Message
|
||||
{
|
||||
// null is a necessary default for many of these
|
||||
private CTCSSCode mConfiguredCode = null;
|
||||
private CTCSSCode mCTCSSCode = null;
|
||||
private String mDebugMessage = null;
|
||||
private CTCSSIdentifier mIdentifier = null;
|
||||
private boolean mMutedStatus = true;
|
||||
private boolean mFirstThreshold = false;
|
||||
private double mPower = 0.0;
|
||||
private double mPowerThreshold = 0.0;
|
||||
private DecoderStateEvent.Event mCallEvent = null;
|
||||
private SquelchCodeState mCodeState = null;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public CTCSSMessage()
|
||||
{
|
||||
super(); // takes care of timestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* constructor
|
||||
* @param configuredCode Configured code for unmuting audio
|
||||
*/
|
||||
public CTCSSMessage(CTCSSCode configuredCode)
|
||||
{
|
||||
super(); // takes care of timestamp
|
||||
mConfiguredCode = configuredCode;
|
||||
}
|
||||
|
||||
public CTCSSMessage(CTCSSCode configuredCode, String initialMessage, DecoderStateEvent.Event decoderEvent, SquelchCodeState codeState)
|
||||
{
|
||||
super(); // takes care of timestamp
|
||||
mConfiguredCode = configuredCode;
|
||||
mDebugMessage = initialMessage;
|
||||
mCallEvent = decoderEvent;
|
||||
mCodeState = codeState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
String toneString = mCTCSSCode != null ? mCTCSSCode.toString() : "None";
|
||||
String mutedString = mMutedStatus ? "MUTED" : "UNMUTED";
|
||||
String message = mDebugMessage != null ? mDebugMessage : "";
|
||||
|
||||
String s = MessageFormat.format("CTCSS: {0}, Config: {1}, Detected: {2}, 1st Thresh: {3}, Pwr: {4}, Pwr Thresh: {5}, {6}",
|
||||
mutedString,
|
||||
mConfiguredCode,
|
||||
toneString,
|
||||
mFirstThreshold,
|
||||
String.format("%.1f", mPower),
|
||||
String.format("%.1f", mPowerThreshold),
|
||||
message
|
||||
);
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.CTCSS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
if(mIdentifier != null)
|
||||
{
|
||||
return Collections.singletonList(mIdentifier);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public void setCTCSSCode(CTCSSCode code)
|
||||
{
|
||||
mCTCSSCode = code;
|
||||
mIdentifier = code != null ? new CTCSSIdentifier(code) : null;
|
||||
}
|
||||
|
||||
public CTCSSCode getCTCSSCode()
|
||||
{
|
||||
return mCTCSSCode;
|
||||
}
|
||||
|
||||
public void setMessage(String s)
|
||||
{
|
||||
mDebugMessage = s;
|
||||
}
|
||||
|
||||
public void setMutedStatus(boolean b)
|
||||
{
|
||||
mMutedStatus = b;
|
||||
}
|
||||
|
||||
public boolean getMutedStatus()
|
||||
{
|
||||
return mMutedStatus;
|
||||
}
|
||||
|
||||
public void setFirstThreshold(boolean b)
|
||||
{
|
||||
mFirstThreshold = b;
|
||||
}
|
||||
|
||||
public void setPower(double power)
|
||||
{
|
||||
mPower = power;
|
||||
}
|
||||
|
||||
public void setPowerThreshold(double powerThreshold)
|
||||
{
|
||||
mPowerThreshold = powerThreshold;
|
||||
}
|
||||
|
||||
public void setCodeState(SquelchCodeState state)
|
||||
{
|
||||
mCodeState = state;
|
||||
}
|
||||
|
||||
public SquelchCodeState getCodeState()
|
||||
{
|
||||
return mCodeState;
|
||||
}
|
||||
|
||||
public void setCallEvent(DecoderStateEvent.Event state)
|
||||
{
|
||||
mCallEvent = state;
|
||||
}
|
||||
|
||||
public DecoderStateEvent.Event getCallEvent()
|
||||
{
|
||||
return mCallEvent;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dcs;
|
||||
package io.github.dsheirer.module.decode.squelch.dcs;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -17,8 +17,9 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dcs;
|
||||
package io.github.dsheirer.module.decode.squelch.dcs;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
@@ -26,14 +27,17 @@ import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.squelch.SquelchCodeState;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IRealBufferListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Digital Coded Squelch (DCS) demodulator/decoder designed to work with FM demodulated 8 kHz input audio samples and
|
||||
* decode 134.4-baud signalling present in the unfiltered demodulated audio stream.
|
||||
* decode 134.4-baud signaling present in the unfiltered demodulated audio stream.
|
||||
*
|
||||
* This decoder monitors the incoming sample stream looking for either a positive or negative sequence of samples where
|
||||
* the slope of these values exceeds a threshold and causes the symbol state to flip. The symbol state remains across
|
||||
@@ -43,6 +47,10 @@ import org.slf4j.LoggerFactory;
|
||||
* designed to work with 8 kHz audio sample rate. Each baud period is (8,000 / 134.4) = 59.52 samples per symbol and
|
||||
* a full DCS tone sequence is 23 bits * 59.52 samples/bit = 1369 samples. The DCS tone sequence is continuously
|
||||
* transmitted at a rate of 134.4 bits-per-second / 23 = 5.84 repeats per second while the source continues transmitting.
|
||||
*
|
||||
* This decoder was modified to optionally run in inlineMode, when it is directly called from NBFM decoder for
|
||||
* channel level DCS detection and muting. This determination is made during instantiation.
|
||||
*
|
||||
*/
|
||||
public class DCSDecoder extends Decoder implements IRealBufferListener, Listener<float[]>
|
||||
{
|
||||
@@ -58,6 +66,10 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
private static final int IDEAL_SYMBOL_TRANSITION_MIN = 11;
|
||||
private static final int IDEAL_SYMBOL_TRANSITION_MAX = 19;
|
||||
private static final int CODE_MASK = 0x7FFFFF; //23-bit mask
|
||||
private static final int LOSS_CODEWORDS = 3;
|
||||
private static final int CONFIRMATION_COUNT = 1; // one confirming, unmutes on second decode
|
||||
private static final int BIT_THRESHOLD_FOR_LOSS = 24; // one more than actually needed
|
||||
|
||||
private static float[] sLowPassFilterCoefficients;
|
||||
private boolean mSymbol = false;
|
||||
private float mBaudCounter = 0f;
|
||||
@@ -65,6 +77,14 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
private float[] mResidual = new float[OVERLAP];
|
||||
private int mCode = 0;
|
||||
private int mExcessiveOneSequenceCounter = 0;
|
||||
private boolean mInlineMode = true;
|
||||
private Set<DCSCode> mTargetCodes = null;
|
||||
private int mConfirmationCounter = 0;
|
||||
private int mCodewordsSinceMatch = 0;
|
||||
private DCSCode mConfiguredCode = null;
|
||||
private int mSamplesToSkip = 0;
|
||||
private boolean mMuted = true;
|
||||
private int mBitCounter = 0;
|
||||
|
||||
static
|
||||
{
|
||||
@@ -100,8 +120,20 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public DCSDecoder(Set<DCSCode> targetCodes)
|
||||
{
|
||||
if(targetCodes != null && !targetCodes.isEmpty())
|
||||
{
|
||||
DCSCode configuredCode = targetCodes.iterator().next();
|
||||
mConfiguredCode = DCSCode.hasValue(configuredCode.getValue()) ? configuredCode : DCSCode.UNKNOWN;
|
||||
}
|
||||
mTargetCodes = targetCodes;
|
||||
mInlineMode = true;
|
||||
}
|
||||
|
||||
public DCSDecoder()
|
||||
{
|
||||
mInlineMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,6 +145,11 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
return DecoderType.DCS;
|
||||
}
|
||||
|
||||
public DCSCode getmConfiguredCode()
|
||||
{
|
||||
return mConfiguredCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the IRealBufferListener interface
|
||||
*/
|
||||
@@ -121,19 +158,29 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
{
|
||||
return this::receive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the demodulated 8 kHz audio samples to extract the 300-baud LTR signalling and delivers the decoded
|
||||
* Processes the demodulated 8 kHz audio samples to extract the 134.4-baud signaling and delivers the decoded
|
||||
* symbol array to the registered listener.
|
||||
* @param samples to demodulate.
|
||||
*/
|
||||
public void receive(float[] samples)
|
||||
{
|
||||
if(getMessageListener() != null)
|
||||
process(samples);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process this audio buffer looking for a DCS code. It takes 2.6 512 sample-sized buffers to detecte a code.
|
||||
* @param samples audio samples to decode (array size is 512)
|
||||
* @return DCSMessage to notify a caller (usually NBFMDecoder) that a code was received or not for this audio buffer.
|
||||
* Since it requires 2.6 buffers to detect/reject a code, this is usually null. Null indicates no state change.
|
||||
*/
|
||||
public DCSMessage process(float[] samples)
|
||||
{
|
||||
DCSMessage inlineDCSMessage = null;
|
||||
if(getMessageListener() != null || mInlineMode)
|
||||
{
|
||||
float[] filtered = mLowPassFilter.filter(samples);
|
||||
float[] buffer = new float[filtered.length + OVERLAP];
|
||||
int samplesToSkip = 0;
|
||||
int timingAdjust = 0;
|
||||
float slope = 0;
|
||||
|
||||
@@ -146,9 +193,9 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer++)
|
||||
{
|
||||
//Don't calculate slope if we're within the baud period following the last symbol transition
|
||||
if(samplesToSkip > 0)
|
||||
if(mSamplesToSkip > 0)
|
||||
{
|
||||
samplesToSkip--;
|
||||
mSamplesToSkip--;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -176,7 +223,7 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
}
|
||||
|
||||
mBaudCounter += timingAdjust;
|
||||
samplesToSkip = POST_TRANSITION_SAMPLES_TO_SKIP + timingAdjust;
|
||||
mSamplesToSkip = POST_TRANSITION_SAMPLES_TO_SKIP + timingAdjust;
|
||||
}
|
||||
else if(slope < mMaxSlope)
|
||||
{
|
||||
@@ -205,7 +252,7 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
}
|
||||
|
||||
mBaudCounter += timingAdjust;
|
||||
samplesToSkip = POST_TRANSITION_SAMPLES_TO_SKIP + timingAdjust;
|
||||
mSamplesToSkip = POST_TRANSITION_SAMPLES_TO_SKIP + timingAdjust;
|
||||
}
|
||||
else if(slope > mMaxSlope)
|
||||
{
|
||||
@@ -221,9 +268,34 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
{
|
||||
//Update the code value adding in the current symbol
|
||||
mCode = (Integer.rotateLeft(mCode, 1) + (mSymbol ? 1 : 0)) & CODE_MASK;
|
||||
if(DCSCode.hasValue(mCode))
|
||||
if(!mInlineMode)
|
||||
{
|
||||
getMessageListener().receive(new DCSMessage(DCSCode.fromValue(mCode), System.currentTimeMillis()));
|
||||
if(DCSCode.hasValue(mCode))
|
||||
{
|
||||
DCSMessage detectionMessage = new DCSMessage(DCSCode.fromValue(mCode));
|
||||
getMessageListener().receive(detectionMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(DCSCode.hasValue(mCode))
|
||||
{
|
||||
// a valid code has been received. Don't know if it's the one we're looking for
|
||||
inlineDCSMessage = updateDetectionState(DCSCode.fromValue(mCode));
|
||||
mBitCounter = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mBitCounter < BIT_THRESHOLD_FOR_LOSS)
|
||||
{
|
||||
mBitCounter++;
|
||||
}
|
||||
else // No code has been detected in 24 bit periods, signal it's gone OR never received
|
||||
{
|
||||
inlineDCSMessage = updateDetectionState(null);
|
||||
mBitCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mBaudCounter -= BAUD_LENGTH;
|
||||
@@ -253,6 +325,7 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
mLog.warn("Unexpected error while processing DCS samples", e);
|
||||
}
|
||||
}
|
||||
return inlineDCSMessage; // most of the time this is null, ignored if not inline mode
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,4 +363,119 @@ public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
|
||||
|
||||
return (float)(sumXY / SLOPE_CALCULATION_SUM_XX);
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mSymbol = false;
|
||||
mBaudCounter = 0;
|
||||
mMaxSlope = 0;
|
||||
mCode = 0;
|
||||
mExcessiveOneSequenceCounter = 0;
|
||||
mSamplesToSkip = 0;
|
||||
mConfirmationCounter = 0;
|
||||
mCodewordsSinceMatch = 0;
|
||||
mResidual = new float[OVERLAP];
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when noiseSquelch closes in inlineMode
|
||||
* @return DCSMessage
|
||||
*/
|
||||
public void inlineReset()
|
||||
{
|
||||
reset();
|
||||
mMuted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles detection logic of a decoded DCS code in inlineMode. This is called when there is a valid code
|
||||
* or at least 23 bit periods have elapsed since the previous code, if any.
|
||||
* @param newCode currently detected DCS code, or null if no code detected
|
||||
* @return DCSMessage containing detected code, optional string message, call event info, muted status
|
||||
*/
|
||||
private DCSMessage updateDetectionState(DCSCode newCode)
|
||||
{
|
||||
DCSMessage detectionMessage = new DCSMessage(mConfiguredCode);
|
||||
if (mMuted)
|
||||
{
|
||||
if (newCode != null && mTargetCodes.contains(newCode))
|
||||
{
|
||||
if (mConfirmationCounter >= CONFIRMATION_COUNT)
|
||||
{
|
||||
// unmute and report detected code
|
||||
mMuted = false;
|
||||
detectionMessage.setMutedStatus(false);
|
||||
detectionMessage.setDCSCode(newCode);
|
||||
detectionMessage.setMessage("Correct DCS code detected, confirmation passed, now unmuting.");
|
||||
detectionMessage.setCallEvent(DecoderStateEvent.Event.START);
|
||||
detectionMessage.setCodeState(SquelchCodeState.ACCEPTED);
|
||||
mCodewordsSinceMatch = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// increment counter and wait for next detection
|
||||
mConfirmationCounter++;
|
||||
detectionMessage.setDCSCode(newCode);
|
||||
detectionMessage.setMessage("Waiting for consecutive correct detections.");
|
||||
}
|
||||
}
|
||||
else // wrong code or no code
|
||||
{
|
||||
mConfirmationCounter = 0;
|
||||
detectionMessage.setMutedStatus(true);
|
||||
detectionMessage.setDCSCode(newCode);
|
||||
if(newCode != null)
|
||||
{
|
||||
detectionMessage.setCodeState(SquelchCodeState.REJECTED);
|
||||
}
|
||||
else
|
||||
{
|
||||
detectionMessage.setCodeState(SquelchCodeState.LOST);
|
||||
}
|
||||
}
|
||||
}
|
||||
else // unmuted and call is ongoing
|
||||
{
|
||||
if (newCode != null && mTargetCodes.contains(newCode))
|
||||
{
|
||||
// all is good, call continues
|
||||
mCodewordsSinceMatch = 0;
|
||||
detectionMessage.setMutedStatus(false);
|
||||
detectionMessage.setDCSCode(newCode);
|
||||
detectionMessage.setCallEvent(DecoderStateEvent.Event.CONTINUATION);
|
||||
}
|
||||
else // code is null or doesn't match desired code
|
||||
{
|
||||
if (mCodewordsSinceMatch >= LOSS_CODEWORDS)
|
||||
{
|
||||
// Mute audio and report rejected code or lost tone
|
||||
mMuted = true;
|
||||
detectionMessage.setMutedStatus(true);
|
||||
detectionMessage.setDCSCode(newCode);
|
||||
detectionMessage.setMessage("Incorrect DCS code or code lost, now muting.");
|
||||
detectionMessage.setCallEvent(DecoderStateEvent.Event.END);
|
||||
mConfirmationCounter = 0;
|
||||
if(newCode != null)
|
||||
{
|
||||
detectionMessage.setCodeState(SquelchCodeState.REJECTED);
|
||||
}
|
||||
else
|
||||
{
|
||||
detectionMessage.setCodeState(SquelchCodeState.LOST);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// all is still good with the call, but a bad tone was decoded and codewordsSinceMatch hasn't
|
||||
// reached a threshold yet.
|
||||
mCodewordsSinceMatch++;
|
||||
detectionMessage.setMutedStatus(false);
|
||||
detectionMessage.setDCSCode(newCode);
|
||||
detectionMessage.setMessage("Waiting for consecutive incorrect or no code.");
|
||||
detectionMessage.setCallEvent(DecoderStateEvent.Event.CONTINUATION);
|
||||
}
|
||||
}
|
||||
}
|
||||
return detectionMessage;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dcs;
|
||||
package io.github.dsheirer.module.decode.squelch.dcs;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.github.dsheirer.module.decode.squelch.dcs;
|
||||
|
||||
/**
|
||||
* converts a 9 - bit, 3 digit octal DCS code to a 23 bit binary word that can be utilized by the DCS decoder
|
||||
* for non-standard DCS codes.
|
||||
*/
|
||||
|
||||
public class DCSEncode
|
||||
{
|
||||
// Parity check generator polynomial: x^11 + x^9 + x^7 + x^6 + x^5 + x + 1 = 0xC75
|
||||
// In many implementations, it is written as 0xAE3 depending on bit ordering.
|
||||
// Here we use the standard cyclic code generator polynomial 0xC75 for MSB-first CRC layout.
|
||||
private static final int GENERATOR_POLY = 0xC75;
|
||||
|
||||
|
||||
public static int OctStr2Int (String octalString)
|
||||
{
|
||||
return Integer.parseInt(octalString, 8) | 0x800;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a 12-bit message into a 23-bit systematic Golay codeword.
|
||||
*
|
||||
* @param message A 12-bit integer (0 to 4095).
|
||||
* @return A 23-bit encoded integer, that has been bit-reversed for use in DCSDecoder.
|
||||
*/
|
||||
public static int encode(int message)
|
||||
{
|
||||
// Enforce 12-bit bound
|
||||
message &= 0xFFF;
|
||||
|
||||
// Compute the 11 golay bits using modulo-2 division (CRC style)
|
||||
int remainder = message << 11;
|
||||
for (int i = 22; i >= 11; i--) {
|
||||
if ((remainder & (1 << i)) != 0) {
|
||||
remainder ^= (GENERATOR_POLY << (i - 11));
|
||||
}
|
||||
}
|
||||
|
||||
// Combine the message bits and the calculated parity bits
|
||||
int combinedMessage = ((remainder & 0x7ff) << 12) | message;
|
||||
|
||||
// reverse the order of the bits for DCSDecoder input -- LSB to MSB
|
||||
int bitPosition = 0x400000; // 23rd bit position set
|
||||
int reversedMessage = 0;
|
||||
while(bitPosition != 0)
|
||||
{
|
||||
if((combinedMessage & 1) == 1)
|
||||
{
|
||||
reversedMessage = reversedMessage | bitPosition;
|
||||
}
|
||||
bitPosition = bitPosition >>> 1;
|
||||
combinedMessage = combinedMessage >> 1;
|
||||
}
|
||||
|
||||
return reversedMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.module.decode.squelch.dcs;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.dcs.DCSIdentifier;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.squelch.SquelchCodeState;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Digital Coded Squelch (DCS) tone detected message
|
||||
*/
|
||||
public class DCSMessage extends Message
|
||||
{
|
||||
// null is a necessary default for many of these
|
||||
private DCSCode mDCSCode = null;
|
||||
private DCSCode mConfiguredCode = null;
|
||||
private String mDebugMessage = null;
|
||||
private DCSIdentifier mIdentifier = null;
|
||||
private boolean mMutedStatus = true;
|
||||
private DecoderStateEvent.Event mCallEvent = null;
|
||||
private SquelchCodeState mCodeState = null;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param code that was detected
|
||||
* @param timestamp when the code was detected
|
||||
*/
|
||||
public DCSMessage(DCSCode code, long timestamp)
|
||||
{
|
||||
super();
|
||||
mDCSCode = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param configuredCode that was detected
|
||||
*/
|
||||
public DCSMessage(DCSCode configuredCode)
|
||||
{
|
||||
super(); // takes care of timestamp
|
||||
mConfiguredCode = configuredCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance, usually at squelch close
|
||||
* @param configuredCode that was detected
|
||||
* @param initialMessage for debug or log
|
||||
* @param callEvent for the initial DecoderStateEvent (usually END)
|
||||
* @param codeState for the initial code state (usually LOST)
|
||||
*/
|
||||
public DCSMessage(DCSCode configuredCode, String initialMessage, DecoderStateEvent.Event callEvent, SquelchCodeState codeState)
|
||||
{
|
||||
super(); // takes care of timestamp
|
||||
mConfiguredCode = configuredCode;
|
||||
mDebugMessage = initialMessage;
|
||||
mCallEvent = callEvent;
|
||||
mCodeState = codeState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public DCSMessage()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
String DCSString = mDCSCode != null ? mDCSCode.toString() : "None";
|
||||
String mutedString = mMutedStatus ? "MUTED" : "UNMUTED";
|
||||
String message = mDebugMessage != null ? mDebugMessage : "";
|
||||
|
||||
String s = MessageFormat.format("DCS: {0}, Config: {1}, Detected: {2}, {3}",
|
||||
mutedString,
|
||||
mConfiguredCode,
|
||||
DCSString,
|
||||
message
|
||||
);
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* The DCS code that was detected.
|
||||
* @return code
|
||||
*/
|
||||
public DCSCode getDCSCode()
|
||||
{
|
||||
return mDCSCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return true; //We only send a message when the tone was valid
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.DCS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
if(mIdentifier != null)
|
||||
return Collections.singletonList(mIdentifier);
|
||||
else
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public void setDCSCode(DCSCode code)
|
||||
{
|
||||
mDCSCode = code;
|
||||
mIdentifier = code != null ? new DCSIdentifier(code) : null;
|
||||
}
|
||||
|
||||
public void setMessage(String s)
|
||||
{
|
||||
mDebugMessage = s;
|
||||
}
|
||||
|
||||
public void setMutedStatus(boolean b)
|
||||
{
|
||||
mMutedStatus = b;
|
||||
}
|
||||
|
||||
public boolean getMutedStatus()
|
||||
{
|
||||
return mMutedStatus;
|
||||
}
|
||||
|
||||
public void setCodeState(SquelchCodeState state)
|
||||
{
|
||||
mCodeState = state;
|
||||
}
|
||||
|
||||
public SquelchCodeState getCodeState()
|
||||
{
|
||||
return mCodeState;
|
||||
}
|
||||
|
||||
public void setCallEvent(DecoderStateEvent.Event state)
|
||||
{
|
||||
mCallEvent = state;
|
||||
}
|
||||
|
||||
public DecoderStateEvent.Event getCallEvent()
|
||||
{
|
||||
return mCallEvent;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dcs;
|
||||
package io.github.dsheirer.module.decode.squelch.dcs;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
@@ -30,6 +30,7 @@ public enum Protocol
|
||||
APCO25_PHASE2("APCO-25 P2", "APCO25PHASE2", 12000),
|
||||
ARS("ARS", "ARS", 0),
|
||||
CELLOCATOR("CELLOCATOR", "CELLOCATOR", 0),
|
||||
CTCSS("CTCSS", "CTCSS", 0),
|
||||
DCS("DCS", "DCS", 134),
|
||||
DMR("DMR", "DMR", 9600),
|
||||
FLEETSYNC("Fleetsync", "FLEETSYNC", 1200),
|
||||
|
||||
Reference in New Issue
Block a user