#35 Updates AM & NBFM decoders to use Adaptive Squelch, a power squelch that includes an adaptive noise floor monitor. Implements vectorized magnitude operations for AM & FM decoders. Adds talkgroup assignment and squelch controls to AM channel configuration editor. Implements new AM gain control that attempts to normalize audio gain quickly at beginning of call/transmission and maintains that gain across lifecycle of the call and resets the gain for the next call. (#1540)

Notes on usage:
- When using either AM or FM squelch control use either the Heterodyne channelizer or if using the Polyphase channelizer, avoid using trunk tracking decoders like DMR or P25 or set the squelch threshold high enough to avoid changes in noise floor from polyphase channelizer where it can vary between using the one channel or the two-channel combined channel sources.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2023-05-07 07:27:44 -04:00
committed by GitHub
parent 5654254cbb
commit 4f8b8e978b
74 changed files with 4630 additions and 2012 deletions

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* 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
@@ -15,27 +14,21 @@
*
* 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;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.dsp.squelch.ISquelchConfiguration;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
/**
* Decoder configuration for an NBFM channel
*/
public class DecodeConfigNBFM extends DecodeConfiguration implements ISquelchConfiguration
public class DecodeConfigNBFM extends DecodeConfigAnalog
{
private Bandwidth mBandwidth = Bandwidth.BW_12_5;
private Boolean mRecordAudio;
private int mTalkgroup = 1;
private int mSquelchThreshold = -60;
public DecodeConfigNBFM()
{
}
@@ -46,116 +39,27 @@ public class DecodeConfigNBFM extends DecodeConfiguration implements ISquelchCon
return DecoderType.NBFM;
}
/**
* Channel bandwidth
*/
@JacksonXmlProperty(isAttribute = true, localName = "bandwidth")
public Bandwidth getBandwidth()
@Override
protected Bandwidth getDefaultBandwidth()
{
return mBandwidth;
return Bandwidth.BW_12_5;
}
/**
* Channel sample stream specification.
*/
@JsonIgnore
@Override
public ChannelSpecification getChannelSpecification()
{
if(mBandwidth == Bandwidth.BW_12_5)
switch(getBandwidth())
{
return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0);
}
else
{
return new ChannelSpecification(50000.0, 25000, 12500.0, 13500.0);
}
}
/**
* Sets the channel bandwidth
*/
public void setBandwidth(Bandwidth bandwidth)
{
mBandwidth = bandwidth;
}
// @Deprecated //No longer used: 20211107
// @JacksonXmlProperty(isAttribute = true, localName = "recordAudio")
// public boolean getRecordAudio()
// {
// return false;
// }
//
// @Deprecated //No longer used: 20211107
// public void setRecordAudio(boolean recordAudio)
// {
// }
//
/**
* Talkgroup to associate with audio produced by the NBFM decoder
*/
@JacksonXmlProperty(isAttribute = true, localName = "talkgroup")
public int getTalkgroup()
{
return mTalkgroup;
}
/**
* Sets the talkgroup identifier to attach to any demodulated audio streams
* @param talkgroup (1-65,535)
*/
public void setTalkgroup(int talkgroup)
{
if(talkgroup < 1 || talkgroup > 65535)
{
throw new IllegalArgumentException("Valid talkgroup range is 1 - 65,535");
}
mTalkgroup = talkgroup;
}
/**
* Sets squelch threshold
* @param threshold (dB)
*/
@JacksonXmlProperty(isAttribute = true, localName = "squelch")
@Override
public void setSquelchThreshold(int threshold)
{
mSquelchThreshold = threshold;
}
/**
* Squelch threshold
* @return threshold (dB)
*/
@Override
public int getSquelchThreshold()
{
return mSquelchThreshold;
}
public enum Bandwidth
{
BW_12_5("12.5 kHz", 12500.0),
BW_25_0("25.0 kHz", 25000.0);
private String mLabel;
private double mValue;
Bandwidth(String label, double value)
{
mLabel = label;
mValue = value;
}
@Override
public String toString()
{
return mLabel;
}
public double getValue()
{
return mValue;
case BW_12_5:
return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0);
case BW_25_0:
return new ChannelSpecification(50000.0, 25000, 12500.0, 13500.0);
default:
throw new IllegalArgumentException("Unrecognized FM bandwidth value: " + getBandwidth());
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* 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
@@ -18,352 +18,34 @@
*/
package io.github.dsheirer.module.decode.nbfm;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.IDecoderStateEventProvider;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.resample.RealResampler;
import io.github.dsheirer.dsp.fm.FmDemodulatorFactory;
import io.github.dsheirer.dsp.fm.ISquelchingFmDemodulator;
import io.github.dsheirer.dsp.window.WindowType;
import io.github.dsheirer.dsp.fm.SquelchingFMDemodulator;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.PrimaryDecoder;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
import io.github.dsheirer.sample.real.IRealBufferProvider;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.module.decode.analog.SquelchingAnalogDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Decoder module with integrated narrowband FM (12.5 or 25.0 kHz channel) demodulator
* Narrow Band FM Decoder module with integrated squelch control.
*/
public class NBFMDecoder extends PrimaryDecoder implements ISourceEventListener, ISourceEventProvider,
IComplexSamplesListener, Listener<ComplexSamples>, IRealBufferProvider,
IDecoderStateEventProvider
public class NBFMDecoder extends SquelchingAnalogDecoder
{
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoder.class);
private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0;
private static final float POWER_SQUELCH_ALPHA_DECAY = 0.0004f;
private static final float POWER_SQUELCH_THRESHOLD_DB = -78.0f;
private static final int POWER_SQUELCH_RAMP = 4;
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoder.class);
private static final float SQUELCH_ALPHA_DECAY = 0.0004f;
private IRealFilter mIBasebandFilter;
private IRealFilter mQBasebandFilter;
private IRealDecimationFilter mIDecimationFilter;
private IRealDecimationFilter mQDecimationFilter;
private ISquelchingFmDemodulator mDemodulator;
private RealResampler mResampler;
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
private Listener<float[]> mResampledBufferListener;
private Listener<DecoderStateEvent> mDecoderStateEventListener;
private double mChannelBandwidth;
private double mOutputSampleRate = DEMODULATED_AUDIO_SAMPLE_RATE;
private boolean mSquelch = true;
/**
* Constructs an instance
* @param config to setup the decoder
*/
public NBFMDecoder( DecodeConfigNBFM config )
{
super( config );
mChannelBandwidth = config.getBandwidth().getValue();
mDemodulator = FmDemodulatorFactory.getSquelchingFmDemodulator(POWER_SQUELCH_ALPHA_DECAY,
POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP);
mDemodulator.setSquelchThreshold(config.getSquelchThreshold());
}
@Override
public DecoderType getDecoderType()
/**
* Constructs an instance
*
* @param config to setup the decoder
*/
public NBFMDecoder(DecodeConfigNBFM config)
{
return DecoderType.NBFM;
super(config, new SquelchingFMDemodulator(SQUELCH_ALPHA_DECAY, config.getSquelchThreshold(),
config.isSquelchAutoTrack()));
}
@Override
public Listener<ComplexSamples> getComplexSamplesListener()
{
return this;
}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return mSourceEventProcessor;
}
@Override
public void reset()
{
mDemodulator.reset();
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void setBufferListener(Listener<float[]> listener)
{
mResampledBufferListener = listener;
}
@Override
public void removeBufferListener()
{
mResampledBufferListener = null;
}
@Override
public void receive(ComplexSamples samples)
{
if(mIDecimationFilter == null || mQDecimationFilter == null)
{
throw new IllegalStateException("NBFM demodulator module must receive a sample rate change source " +
"event before it can process complex sample buffers");
}
float[] decimatedI = mIDecimationFilter.decimateReal(samples.i());
float[] decimatedQ = mQDecimationFilter.decimateReal(samples.q());
float[] filteredI = mIBasebandFilter.filter(decimatedI);
float[] filteredQ = mQBasebandFilter.filter(decimatedQ);
float[] demodulated = mDemodulator.demodulate(filteredI, filteredQ);
if(mResampler != null)
{
//If we're currently squelched and the squelch state changed while demodulating the baseband samples,
// then un-squelch so we can send this buffer
if(mSquelch && mDemodulator.isSquelchChanged())
{
mSquelch = false;
notifyCallStart();
}
//Either send the demodulated buffer to the resampler for distro, or decrement the user count
if(mSquelch)
{
notifyIdle();
}
else
{
mResampler.resample(demodulated);
notifyCallContinuation();
}
//Set to squelch if necessary to close out the audio buffers
if(!mSquelch && mDemodulator.isMuted())
{
mSquelch = true;
notifyCallEnd();
}
}
else
{
notifyIdle();
}
}
/**
* Broadcasts a call start state event
*/
private void notifyCallStart()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.START, State.CALL, 0));
}
/**
* Broadcasts a call continuation state event
*/
private void notifyCallContinuation()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.CALL, 0));
}
/**
* Broadcasts a call end state event
*/
private void notifyCallEnd()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.END, State.CALL, 0));
}
/**
* Broadcasts an idle notification
*/
private void notifyIdle()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.IDLE, 0));
}
/**
* Broadcasts the decoder state event to an optional registered listener
*/
private void broadcast(DecoderStateEvent event)
{
if(mDecoderStateEventListener != null)
{
mDecoderStateEventListener.receive(event);
}
}
/**
* Sets the decoder state listener
*/
@Override
public void setDecoderStateListener(Listener<DecoderStateEvent> listener)
{
mDecoderStateEventListener = listener;
}
/**
* Removes the decoder state event listener
*/
@Override
public void removeDecoderStateListener()
{
mDecoderStateEventListener = null;
}
/**
* Registers the listener to receive squelch change events from the demodulator/squelch controller.
*/
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mDemodulator.setSourceEventListener(listener);
}
/**
* De-registers the listener
*/
@Override
public void removeSourceEventListener()
{
mDemodulator.setSourceEventListener(null);
}
/**
* Monitors sample rate change source event(s) to setup the initial I/Q filter and passes squelch threshold
* change requests down to the demodulator.
*/
public class SourceEventProcessor implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE)
{
if(mIBasebandFilter != null)
{
mIBasebandFilter = null;
mQBasebandFilter = null;
}
double sampleRate = sourceEvent.getValue().doubleValue();
int decimationRate = 0;
double decimatedSampleRate = sampleRate;
if(sampleRate / 2 >= (mChannelBandwidth * 2))
{
decimationRate = 2;
while(sampleRate / decimationRate / 2 >= (mChannelBandwidth * 2))
{
decimationRate *= 2;
}
}
if(decimationRate > 0)
{
decimatedSampleRate /= decimationRate;
}
mIDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate);
mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate);
if((decimatedSampleRate < (2.0 * mChannelBandwidth)))
{
throw new IllegalStateException("FM Demodulator with channel bandwidth [" + mChannelBandwidth +
"] requires a channel sample rate of [" + (2.0 * mChannelBandwidth + "] - sample rate of [" +
decimatedSampleRate + "] is not supported"));
}
int passBandStop = (int)(mChannelBandwidth * .8);
int stopBandStart = (int)mChannelBandwidth;
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
.build();
try
{
coefficients = FilterFactory.getTaps(specification);
}
catch(FilterDesignException fde)
{
mLog.error("Couldn't design FM demodulator remez filter for sample rate [" + sampleRate +
"] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart +
"] - will proceed using sinc (low-pass) filter");
}
if(coefficients == null)
{
mLog.info("Unable to use remez filter designer for sample rate [" + decimatedSampleRate +
"] pass band stop [" + passBandStop +
"] and stop band start [" + stopBandStart + "] - will proceed using simple low pass filter design");
coefficients = FilterFactory.getLowPass(decimatedSampleRate, passBandStop, stopBandStart, 60,
WindowType.HAMMING, true);
}
mIBasebandFilter = FilterFactory.getRealFilter(coefficients);
mQBasebandFilter = FilterFactory.getRealFilter(coefficients);
mResampler = new RealResampler(decimatedSampleRate, mOutputSampleRate, 4192, 512);
mResampler.setListener(resampled ->
{
if(mResampledBufferListener != null)
{
mResampledBufferListener.receive(resampled);
}
});
}
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
{
//Send request for squelch threshold change to the demodulator/squelch controller.
mDemodulator.receive(sourceEvent);
}
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
{
mDemodulator.receive(sourceEvent);
}
}
}
@Override
public DecoderType getDecoderType()
{
return DecoderType.NBFM;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* 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
@@ -16,42 +16,31 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nbfm;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.p25.identifier.channel.StandardChannel;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.dsheirer.module.decode.analog.AnalogDecoderState;
/**
* Narrow Band FM decoder channel state - provides the minimum channel state functionality
* NBFM decoder state
*/
public class NBFMDecoderState extends DecoderState implements ISourceEventListener
public class NBFMDecoderState extends AnalogDecoderState
{
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoderState.class);
private Listener<SourceEvent> mSourceEventListener = new SourceEventListener();
private String mChannelName;
private DecodeEvent mDecodeEvent;
private Identifier mChannelNameIdentifier;
private Identifier mTalkgroupIdentifier;
private IChannelDescriptor mChannelDescriptor = null;
/**
* Constructs an instance
* @param channelName to use for this channel
* @param decodeConfig with talkgroup identifier
*/
public NBFMDecoderState(String channelName, DecodeConfigNBFM decodeConfig)
{
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : "NBFM CHANNEL";
@@ -59,116 +48,6 @@ public class NBFMDecoderState extends DecoderState implements ISourceEventListen
mTalkgroupIdentifier = new NBFMTalkgroup(decodeConfig.getTalkgroup());
}
@Override
public void init()
{
}
@Override
public String getActivitySummary()
{
StringBuilder sb = new StringBuilder();
sb.append("Activity Summary\n");
sb.append("\tDecoder: NBFM");
sb.append("\n\n");
return sb.toString();
}
@Override
public void receive(IMessage t)
{
/* Not implemented */
}
@Override
public void receiveDecoderStateEvent(DecoderStateEvent event)
{
switch(event.getEvent())
{
case REQUEST_RESET ->
{
getIdentifierCollection().update(mChannelNameIdentifier);
}
case START ->
{
if(event.getState() == State.CALL)
{
startCallEvent();
}
}
case END ->
{
if(event.getState() == State.CALL)
{
endCallEvent();
}
}
case CONTINUATION ->
{
if(event.getState() == State.CALL)
{
continueCallEvent();
}
else
{
endCallEvent();
}
}
}
}
/**
* Creates/starts a decode call evnet.
*/
private void startCallEvent()
{
getIdentifierCollection().update(mTalkgroupIdentifier);
if(mDecodeEvent == null)
{
mDecodeEvent = DecodeEvent.builder(System.currentTimeMillis())
.channel(mChannelDescriptor)
.eventDescription("CALL")
.details("NBFM")
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
.build();
broadcast(mDecodeEvent);
}
}
/**
* Continues (or starts) the call decode event and updates the current timestamp
*/
private void continueCallEvent()
{
if(mDecodeEvent == null)
{
startCallEvent();
}
mDecodeEvent.update(System.currentTimeMillis());
broadcast(mDecodeEvent);
}
/**
* Ends the call decode event
*/
private void endCallEvent()
{
if(mDecodeEvent != null)
{
mDecodeEvent.end(System.currentTimeMillis());
broadcast(mDecodeEvent);
mDecodeEvent = null;
}
//Remote any user identifiers from the identifier collection
resetState();
}
@Override
public DecoderType getDecoderType()
{
@@ -176,34 +55,24 @@ public class NBFMDecoderState extends DecoderState implements ISourceEventListen
}
@Override
public void start()
protected Identifier getChannelNameIdentifier()
{
getIdentifierCollection().update(mChannelNameIdentifier);
return mChannelNameIdentifier;
}
@Override
public void stop()
protected Identifier getTalkgroupIdentifier()
{
return mTalkgroupIdentifier;
}
@Override
public Listener<SourceEvent> getSourceEventListener()
public String getActivitySummary()
{
return mSourceEventListener;
}
/**
* Monitors source events to capture the channel frequency for use in decode events.
*/
private class SourceEventListener implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE)
{
mChannelDescriptor = new StandardChannel(sourceEvent.getValue().longValue());
}
}
StringBuilder sb = new StringBuilder();
sb.append("Activity Summary\n");
sb.append("\tDecoder: NBFM");
sb.append("\n\n");
return sb.toString();
}
}