mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -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:
@@ -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;
|
||||
Reference in New Issue
Block a user