mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
#36 NBFM squelch control. Updates NBFM decoder to incorporate squelch control. Squelch is a configuration item and also available to adjust at runtime with new 'Channel' tab in Now Playing window. All decoders were updated to produce continuous channel power (dB) measurements that are displayed in the Channel tab's new Power Meter. (#1100)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
@@ -69,6 +69,7 @@ import io.github.dsheirer.module.decode.mpt1327.MPT1327TrafficChannelManager;
|
||||
import io.github.dsheirer.module.decode.mpt1327.Sync;
|
||||
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
|
||||
import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
|
||||
import io.github.dsheirer.module.decode.nbfm.NBFMDecoderState;
|
||||
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
|
||||
@@ -303,21 +304,19 @@ public class DecoderFactory
|
||||
}
|
||||
}
|
||||
|
||||
private static void processNBFM(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfiguration decodeConfig) {
|
||||
modules.add(new NBFMDecoder(decodeConfig));
|
||||
modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.NBFM, channel.getName()));
|
||||
AudioModule audioModuleFM = new AudioModule(aliasList);
|
||||
private static void processNBFM(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfiguration decodeConfig)
|
||||
{
|
||||
if(!(decodeConfig instanceof DecodeConfigNBFM))
|
||||
{
|
||||
throw new IllegalArgumentException("Can't create NBFM decoder - unrecognized decode config type: " +
|
||||
(decodeConfig != null ? decodeConfig.getClass() : "null/empty"));
|
||||
}
|
||||
|
||||
//Check if the user wants all audio recorded ..
|
||||
if(((DecodeConfigNBFM)decodeConfig).getRecordAudio())
|
||||
{
|
||||
audioModuleFM.setRecordAudio(true);
|
||||
}
|
||||
modules.add(audioModuleFM);
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
|
||||
}
|
||||
DecodeConfigNBFM decodeConfigNBFM = (DecodeConfigNBFM)decodeConfig;
|
||||
|
||||
modules.add(new NBFMDecoder(decodeConfigNBFM));
|
||||
modules.add(new NBFMDecoderState(channel.getName(), decodeConfigNBFM));
|
||||
modules.add(new AudioModule(aliasList));
|
||||
}
|
||||
|
||||
private static void processAM(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfiguration decodeConfig) {
|
||||
@@ -571,7 +570,6 @@ public class DecoderFactory
|
||||
case NBFM:
|
||||
DecodeConfigNBFM origNBFM = (DecodeConfigNBFM)config;
|
||||
DecodeConfigNBFM copyNBFM = new DecodeConfigNBFM();
|
||||
copyNBFM.setRecordAudio(origNBFM.getRecordAudio());
|
||||
copyNBFM.setBandwidth(origNBFM.getBandwidth());
|
||||
return copyNBFM;
|
||||
case P25_PHASE1:
|
||||
|
||||
@@ -36,7 +36,7 @@ public enum DecoderType
|
||||
LTR("LTR", "LTR", Protocol.LTR),
|
||||
LTR_NET("LTR-Net", "LTR-Net", Protocol.LTR_NET),
|
||||
MPT1327("MPT1327", "MPT1327", Protocol.MPT1327),
|
||||
NBFM("NBFM", "NBFM", Protocol.UNKNOWN),
|
||||
NBFM("NBFM", "NBFM", Protocol.NBFM),
|
||||
PASSPORT("Passport", "Passport", Protocol.PASSPORT),
|
||||
P25_PHASE1("P25 Phase 1", "P25-1", Protocol.APCO25),
|
||||
P25_PHASE2("P25 Phase 2", "P25-2", Protocol.APCO25_PHASE2),
|
||||
|
||||
@@ -9,6 +9,7 @@ import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.FrequencyCorrectionSyncMonitor;
|
||||
import io.github.dsheirer.dsp.psk.pll.PLLBandwidth;
|
||||
import io.github.dsheirer.dsp.squelch.PowerMonitor;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
@@ -50,6 +51,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
protected CostasLoop mCostasLoop;
|
||||
protected FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor;
|
||||
protected DMRMessageFramer mMessageFramer;
|
||||
private PowerMonitor mPowerMonitor = new PowerMonitor();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -91,6 +93,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
getSymbolRate() + " symbol rate)");
|
||||
}
|
||||
|
||||
mPowerMonitor.setSampleRate((int)sampleRate);
|
||||
mSampleRate = sampleRate;
|
||||
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
|
||||
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
|
||||
@@ -125,6 +128,9 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
//User accounting of the incoming buffer is handled by the filter
|
||||
ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer);
|
||||
|
||||
//Process buffer for power measurements
|
||||
mPowerMonitor.process(basebandFiltered);
|
||||
|
||||
//User accounting of the incoming buffer is handled by the gain filter
|
||||
ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered);
|
||||
|
||||
@@ -278,6 +284,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener)
|
||||
{
|
||||
mSourceEventListener = listener;
|
||||
mPowerMonitor.setSourceEventListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,6 +294,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
mSourceEventListener = null;
|
||||
mPowerMonitor.setSourceEventListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,14 +21,20 @@ 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.source.tuner.channel.ChannelSpecification;
|
||||
|
||||
public class DecodeConfigNBFM extends DecodeConfiguration
|
||||
/**
|
||||
* Decoder configuration for an NBFM channel
|
||||
*/
|
||||
public class DecodeConfigNBFM extends DecodeConfiguration implements ISquelchConfiguration
|
||||
{
|
||||
private Bandwidth mBandwidth = Bandwidth.BW_12_5;
|
||||
private boolean mRecordAudio = false;
|
||||
private Boolean mRecordAudio;
|
||||
private int mTalkgroup = 1;
|
||||
private int mSquelchThreshold = -60;
|
||||
|
||||
public DecodeConfigNBFM()
|
||||
{
|
||||
@@ -71,27 +77,74 @@ public class DecodeConfigNBFM extends DecodeConfiguration
|
||||
mBandwidth = bandwidth;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "recordAudio")
|
||||
public boolean getRecordAudio()
|
||||
// @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 mRecordAudio;
|
||||
return mTalkgroup;
|
||||
}
|
||||
|
||||
public void setRecordAudio(boolean recordAudio)
|
||||
/**
|
||||
* Sets the talkgroup identifier to attach to any demodulated audio streams
|
||||
* @param talkgroup (1-65,535)
|
||||
*/
|
||||
public void setTalkgroup(int talkgroup)
|
||||
{
|
||||
mRecordAudio = recordAudio;
|
||||
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"),
|
||||
BW_25_0("25.0 kHz");
|
||||
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)
|
||||
Bandwidth(String label, double value)
|
||||
{
|
||||
mLabel = label;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,5 +152,10 @@ public class DecodeConfigNBFM extends DecodeConfiguration
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public double getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,62 @@
|
||||
******************************************************************************/
|
||||
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.Window;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
|
||||
import io.github.dsheirer.dsp.filter.resample.RealResampler;
|
||||
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.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.IReusableBufferProvider;
|
||||
import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class NBFMDecoder extends PrimaryDecoder
|
||||
/**
|
||||
* Decoder module with integrated narrowband FM (12.5 or 25.0 kHz channel) demodulator
|
||||
*/
|
||||
public class NBFMDecoder extends PrimaryDecoder implements ISourceEventListener, ISourceEventProvider,
|
||||
IReusableComplexBufferListener, Listener<ReusableComplexBuffer>, IReusableBufferProvider,
|
||||
IDecoderStateEventProvider
|
||||
{
|
||||
public NBFMDecoder( DecodeConfiguration config )
|
||||
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoder.class);
|
||||
private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0;
|
||||
private static final double POWER_SQUELCH_ALPHA_DECAY = 0.0002;
|
||||
private static final double POWER_SQUELCH_THRESHOLD_DB = -78.0;
|
||||
private static final int POWER_SQUELCH_RAMP = 4;
|
||||
|
||||
private ComplexFIRFilter2 mIQFilter;
|
||||
private SquelchingFMDemodulator mDemodulator = new SquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY,
|
||||
POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP);
|
||||
private RealResampler mResampler;
|
||||
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
|
||||
private Listener<ReusableFloatBuffer> mResampledReusableBufferListener;
|
||||
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.setSquelchThreshold(config.getSquelchThreshold());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -35,9 +81,22 @@ public class NBFMDecoder extends PrimaryDecoder
|
||||
return DecoderType.NBFM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<ReusableComplexBuffer> getReusableComplexBufferListener()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
return mSourceEventProcessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mDemodulator.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,4 +108,232 @@ public class NBFMDecoder extends PrimaryDecoder
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBufferListener(Listener<ReusableFloatBuffer> listener)
|
||||
{
|
||||
mResampledReusableBufferListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeBufferListener()
|
||||
{
|
||||
mResampledReusableBufferListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ReusableComplexBuffer reusableComplexBuffer)
|
||||
{
|
||||
if(mIQFilter == null)
|
||||
{
|
||||
reusableComplexBuffer.decrementUserCount();
|
||||
throw new IllegalStateException("NBFM demodulator module must receive a sample rate change source " +
|
||||
"event before it can process complex sample buffers");
|
||||
}
|
||||
|
||||
ReusableComplexBuffer basebandFilteredBuffer = mIQFilter.filter(reusableComplexBuffer);
|
||||
ReusableFloatBuffer demodulatedBuffer = mDemodulator.demodulate(basebandFilteredBuffer);
|
||||
|
||||
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)
|
||||
{
|
||||
demodulatedBuffer.incrementUserCount();
|
||||
notifyIdle();
|
||||
}
|
||||
else
|
||||
{
|
||||
mResampler.resample(demodulatedBuffer);
|
||||
notifyCallContinuation();
|
||||
}
|
||||
|
||||
//Set to squelch if necessary to close out the audio buffers
|
||||
if(!mSquelch && mDemodulator.isMuted())
|
||||
{
|
||||
mSquelch = true;
|
||||
notifyCallEnd();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
demodulatedBuffer.decrementUserCount();
|
||||
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(mIQFilter != null)
|
||||
{
|
||||
mIQFilter.dispose();
|
||||
mIQFilter = null;
|
||||
}
|
||||
|
||||
double sampleRate = sourceEvent.getValue().doubleValue();
|
||||
|
||||
if((sampleRate < (2.0 * mChannelBandwidth)))
|
||||
{
|
||||
throw new IllegalStateException("FM Demodulator with channel bandwidth [" + mChannelBandwidth +
|
||||
"] requires a channel sample rate of [" + (2.0 * mChannelBandwidth + "] - sample rate of [" +
|
||||
sampleRate + "] is not supported"));
|
||||
}
|
||||
|
||||
int passBandStop = (int)(mChannelBandwidth * .8);
|
||||
int stopBandStart = (int)mChannelBandwidth;
|
||||
|
||||
float[] filterTaps = null;
|
||||
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(sampleRate)
|
||||
.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
|
||||
{
|
||||
filterTaps = 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 +
|
||||
"] - using sinc filter");
|
||||
}
|
||||
|
||||
if(filterTaps == null)
|
||||
{
|
||||
filterTaps = FilterFactory.getLowPass(sampleRate, passBandStop, stopBandStart, 60,
|
||||
Window.WindowType.HAMMING, true);
|
||||
}
|
||||
|
||||
mIQFilter = new ComplexFIRFilter2(filterTaps);
|
||||
|
||||
mResampler = new RealResampler(sampleRate, mOutputSampleRate, 2000, 1000);
|
||||
|
||||
mResampler.setListener(reusableFloatBuffer ->
|
||||
{
|
||||
if(mResampledReusableBufferListener != null)
|
||||
{
|
||||
mResampledReusableBufferListener.receive(reusableFloatBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
reusableFloatBuffer.decrementUserCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Narrow Band FM decoder channel state - provides the minimum channel state functionality
|
||||
*/
|
||||
public class NBFMDecoderState extends DecoderState implements ISourceEventListener
|
||||
{
|
||||
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;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@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()
|
||||
{
|
||||
return DecoderType.NBFM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SourceEvent> getSourceEventListener()
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.github.dsheirer.module.decode.nbfm;
|
||||
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Narrowband FM talkgroup. Note: this is a logical identifier that is assignable from a user-specified
|
||||
* configuration so that NBFM audio events are compatible with other features of the sdrtrunk system.
|
||||
*/
|
||||
public class NBFMTalkgroup extends TalkgroupIdentifier
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param value 1-65,535
|
||||
*/
|
||||
public NBFMTalkgroup(int value)
|
||||
{
|
||||
super(value, Role.TO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.NBFM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package io.github.dsheirer.module.decode.p25.identifier.channel;
|
||||
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
/**
|
||||
* Standard AM/FM channel with discrete frequency
|
||||
*/
|
||||
public class StandardChannel implements IChannelDescriptor
|
||||
{
|
||||
private static final DecimalFormat FREQUENCY_FORMATTER = new DecimalFormat("0.000");
|
||||
private long mFrequency;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param frequency of the channel (Hz)
|
||||
*/
|
||||
public StandardChannel(long frequency)
|
||||
{
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDownlinkFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUplinkFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getFrequencyBandIdentifiers()
|
||||
{
|
||||
return new int[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrequencyBand(IFrequencyBand bandIdentifier)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTDMAChannel()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTimeslotCount()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return FREQUENCY_FORMATTER.format(mFrequency / 1E6d) + " MHz";
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.p25.phase1;
|
||||
|
||||
import io.github.dsheirer.dsp.squelch.PowerMonitor;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
@@ -32,9 +33,10 @@ import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableByteBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
|
||||
public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEventListener, ISourceEventProvider,
|
||||
IReusableComplexBufferListener, Listener<ReusableComplexBuffer>, IReusableByteBufferProvider
|
||||
{
|
||||
private double mSampleRate;
|
||||
@@ -43,6 +45,7 @@ public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEve
|
||||
private P25P1MessageProcessor mMessageProcessor;
|
||||
private Listener<SourceEvent> mSourceEventListener;
|
||||
private double mSymbolRate;
|
||||
protected PowerMonitor mPowerMonitor = new PowerMonitor();
|
||||
|
||||
public P25P1Decoder(double symbolRate)
|
||||
{
|
||||
@@ -52,6 +55,18 @@ public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEve
|
||||
getDibitBroadcaster().addListener(mByteBufferAssembler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener )
|
||||
{
|
||||
mPowerMonitor.setSourceEventListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
mPowerMonitor.setSourceEventListener(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembler for packaging Dibit stream into reusable byte buffers.
|
||||
*/
|
||||
@@ -111,6 +126,7 @@ public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEve
|
||||
getSymbolRate() + " symbol rate)");
|
||||
}
|
||||
|
||||
mPowerMonitor.setSampleRate((int)sampleRate);
|
||||
mSampleRate = sampleRate;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.FrequencyCorrectionSyncMonitor;
|
||||
import io.github.dsheirer.dsp.psk.pll.PLLBandwidth;
|
||||
import io.github.dsheirer.dsp.squelch.PowerMonitor;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
@@ -67,7 +68,6 @@ public class P25P1DecoderC4FM extends P25P1Decoder
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
super.setSampleRate(sampleRate);
|
||||
|
||||
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
|
||||
|
||||
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
|
||||
@@ -103,6 +103,9 @@ public class P25P1DecoderC4FM extends P25P1Decoder
|
||||
//User accounting of the incoming buffer is handled by the filter
|
||||
ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer);
|
||||
|
||||
//Process the buffer for power meter measurements (before gain is applied)
|
||||
mPowerMonitor.process(basebandFiltered);
|
||||
|
||||
//User accounting of the incoming buffer is handled by the gain filter
|
||||
ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered);
|
||||
|
||||
|
||||
@@ -113,6 +113,9 @@ public class P25P1DecoderLSM extends P25P1Decoder
|
||||
//The filter will decrement the user count when finished
|
||||
ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer);
|
||||
|
||||
//Process the buffer for power measurements
|
||||
mPowerMonitor.process(basebandFiltered);
|
||||
|
||||
//AGC will decrement the user count when finished
|
||||
ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.p25.phase2;
|
||||
|
||||
import io.github.dsheirer.dsp.squelch.PowerMonitor;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
@@ -32,12 +33,13 @@ import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableByteBuffer;
|
||||
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
|
||||
/**
|
||||
* Base P25 Phase 2 Decoder
|
||||
*/
|
||||
public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEventListener, ISourceEventProvider,
|
||||
IReusableComplexBufferListener, Listener<ReusableComplexBuffer>, IReusableByteBufferProvider
|
||||
{
|
||||
private double mSampleRate;
|
||||
@@ -45,6 +47,7 @@ public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEve
|
||||
private DibitToByteBufferAssembler mByteBufferAssembler = new DibitToByteBufferAssembler(300);
|
||||
private P25P2MessageProcessor mMessageProcessor;
|
||||
private double mSymbolRate;
|
||||
protected PowerMonitor mPowerMonitor = new PowerMonitor();
|
||||
|
||||
public P25P2Decoder(double symbolRate)
|
||||
{
|
||||
@@ -54,6 +57,18 @@ public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEve
|
||||
getDibitBroadcaster().addListener(mByteBufferAssembler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSourceEventListener(Listener<SourceEvent> listener )
|
||||
{
|
||||
mPowerMonitor.setSourceEventListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSourceEventListener()
|
||||
{
|
||||
mPowerMonitor.setSourceEventListener(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembler for packaging Dibit stream into reusable byte buffers.
|
||||
*/
|
||||
@@ -113,6 +128,7 @@ public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEve
|
||||
getSymbolRate() + " symbol rate)");
|
||||
}
|
||||
|
||||
mPowerMonitor.setSampleRate((int)sampleRate);
|
||||
mSampleRate = sampleRate;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.FrequencyCorrectionSyncMonitor;
|
||||
import io.github.dsheirer.dsp.psk.pll.PLLBandwidth;
|
||||
import io.github.dsheirer.dsp.squelch.PowerMonitor;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateListener;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
@@ -120,6 +121,9 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate
|
||||
//User accounting of the incoming buffer is handled by the filter
|
||||
ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer);
|
||||
|
||||
//Process the buffer for power measurements
|
||||
mPowerMonitor.process(basebandFiltered);
|
||||
|
||||
//User accounting of the incoming buffer is handled by the gain filter
|
||||
ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user