diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java
index 2dcebb7c..8f0efdc4 100644
--- a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java
+++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java
@@ -46,6 +46,8 @@ public enum TalkgroupFormat
"MDC-1200 valid value range is 1-65,535"),
MPT1327("###-####", 1, 0x7FFFFF, "000-0001 to 127-8192",
"MPT-1327 valid ranges are 0-127(prefix)
and 1-8192(ident) (ie. 000-0001 to 127-8192)"),
+ NBFM("*****", 1, 0xFFFF, "1 to 65,535",
+ "NBFM valid value range is 1-65,535"),
PASSPORT("*****", 1, 0xFFFF, "1 to 65,535",
"Passport valid value range is 1-65,535"),
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
@@ -131,6 +133,8 @@ public enum TalkgroupFormat
return MDC1200;
case MPT1327:
return MPT1327;
+ case NBFM:
+ return NBFM;
case PASSPORT:
return PASSPORT;
default:
diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java
index fe0bc823..919239b4 100644
--- a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java
+++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java
@@ -28,6 +28,7 @@ import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupForm
import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter;
+import io.github.dsheirer.preference.identifier.talkgroup.NBFMTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.PassportTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
@@ -56,6 +57,7 @@ public class TalkgroupFormatter
mFormatterMap.put(Protocol.LTR_NET, ltr);
mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter());
mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter());
+ mFormatterMap.put(Protocol.NBFM, new NBFMTalkgroupFormatter());
mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter());
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
}
diff --git a/src/main/java/io/github/dsheirer/channel/metadata/NowPlayingPanel.java b/src/main/java/io/github/dsheirer/channel/metadata/NowPlayingPanel.java
index 6b882bbf..3c7886ea 100644
--- a/src/main/java/io/github/dsheirer/channel/metadata/NowPlayingPanel.java
+++ b/src/main/java/io/github/dsheirer/channel/metadata/NowPlayingPanel.java
@@ -22,6 +22,7 @@ package io.github.dsheirer.channel.metadata;
import com.jidesoft.swing.JideSplitPane;
import com.jidesoft.swing.JideTabbedPane;
import io.github.dsheirer.channel.details.ChannelDetailPanel;
+import io.github.dsheirer.gui.power.ChannelPowerPanel;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.module.decode.event.DecodeEventPanel;
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
@@ -38,6 +39,7 @@ public class NowPlayingPanel extends JPanel
private ChannelDetailPanel mChannelDetailPanel;
private DecodeEventPanel mDecodeEventPanel;
private MessageActivityPanel mMessageActivityPanel;
+ private ChannelPowerPanel mChannelPowerPanel;
/**
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
@@ -49,6 +51,7 @@ public class NowPlayingPanel extends JPanel
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
+ mChannelPowerPanel = new ChannelPowerPanel(playlistManager);
init();
}
@@ -61,6 +64,7 @@ public class NowPlayingPanel extends JPanel
tabbedPane.addTab("Details", mChannelDetailPanel);
tabbedPane.addTab("Events", mDecodeEventPanel);
tabbedPane.addTab("Messages", mMessageActivityPanel);
+ tabbedPane.addTab("Channel", mChannelPowerPanel);
tabbedPane.setFont(this.getFont());
tabbedPane.setForeground(Color.BLACK);
@@ -73,5 +77,6 @@ public class NowPlayingPanel extends JPanel
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
+ mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelPowerPanel);
}
}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/iir/SinglePoleIirFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/iir/SinglePoleIirFilter.java
new file mode 100644
index 00000000..c6d1ad97
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/iir/SinglePoleIirFilter.java
@@ -0,0 +1,56 @@
+package io.github.dsheirer.dsp.filter.iir;
+
+/**
+ * Single pole IIR filter
+ */
+public class SinglePoleIirFilter
+{
+ /**
+ * Decay value, in range: 0 <-> 1.0
+ */
+ private double mAlpha;
+
+ /**
+ * 1.0 - alpha
+ */
+ private double mOneMinusAlpha;
+
+ /**
+ * Current filter output value.
+ */
+ private double mOutput;
+
+ /**
+ * Constructs a single pole IIR filter
+ * @param alpha decay value in range (0.0 - 1.0)
+ */
+ public SinglePoleIirFilter(double alpha)
+ {
+ if(alpha < 0.0 || alpha > 1.0)
+ {
+ throw new IllegalArgumentException("alpha decay value must be in range: 0.0 - 1.0");
+ }
+
+ mAlpha = alpha;
+ mOneMinusAlpha = 1.0 - alpha;
+ }
+
+ /**
+ * Processes the specified sample and returns the filter output
+ * @param sample to process/filter
+ * @return filtered output
+ */
+ public double filter(double sample)
+ {
+ mOutput = (mOutput * mOneMinusAlpha) + (mAlpha * sample);
+ return mOutput;
+ }
+
+ /**
+ * Current output value
+ */
+ public double getValue()
+ {
+ return mOutput;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/fm/SquelchingFMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/SquelchingFMDemodulator.java
new file mode 100644
index 00000000..fe586a44
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/fm/SquelchingFMDemodulator.java
@@ -0,0 +1,146 @@
+/*******************************************************************************
+ * SDR Trunk
+ * Copyright (C) 2014,2015 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
+ ******************************************************************************/
+package io.github.dsheirer.dsp.fm;
+
+import io.github.dsheirer.dsp.squelch.PowerSquelch;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
+import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
+import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
+import io.github.dsheirer.source.SourceEvent;
+
+/**
+ * FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
+ *
+ * Implements listener of source events to process runtime squelch threshold change request events
+ * which are forwarded to the power squelch control.
+ */
+public class SquelchingFMDemodulator extends FMDemodulator implements Listener
+{
+ private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("SquelchingFMDemodulator");
+ private PowerSquelch mPowerSquelch;
+ private boolean mSquelchChanged = false;
+
+ /**
+ * Creates an FM demodulator instance with a default gain of 1.0.
+ */
+ public SquelchingFMDemodulator(double alpha, double threshold, int ramp)
+ {
+ mPowerSquelch = new PowerSquelch(alpha, threshold, ramp);
+ }
+
+ /**
+ * Registers the listener to receive notifications of squelch change events from the power squelch.
+ */
+ public void setSourceEventListener(Listener listener)
+ {
+ mPowerSquelch.setSourceEventListener(listener);
+ }
+
+ /**
+ * Demodulates the complex baseband sample buffer and returns a demodulated reusable buffer with the user count
+ * set to 1. The complex baseband buffer's user count is decremented after demodulation.
+ *
+ * @param basebandSampleBuffer containing samples to demodulate
+ * @return demodulated sample buffer.
+ */
+ @Override
+ public ReusableFloatBuffer demodulate(ReusableComplexBuffer basebandSampleBuffer)
+ {
+ setSquelchChanged(false);
+
+ ReusableFloatBuffer demodulatedBuffer = mReusableBufferQueue.getBuffer(basebandSampleBuffer.getSampleCount());
+
+ float[] basebandSamples = basebandSampleBuffer.getSamples();
+ float[] demodulatedSamples = demodulatedBuffer.getSamples();
+ float i,q;
+
+ for(int x = 0; x < basebandSamples.length; x += 2)
+ {
+ i = basebandSamples[x];
+ q = basebandSamples[x + 1];
+
+ mPowerSquelch.process(i, q);
+
+ if(mPowerSquelch.isUnmuted() || mPowerSquelch.isDecay())
+ {
+ demodulatedSamples[x / 2] = demodulate(i, q);
+ }
+ else
+ {
+ demodulatedSamples[x / 2] = 0.0f;
+ }
+
+ if(mPowerSquelch.isSquelchChanged())
+ {
+ setSquelchChanged(true);
+ }
+ }
+
+ basebandSampleBuffer.decrementUserCount();
+
+ return demodulatedBuffer;
+ }
+
+ /**
+ * Sets the threshold for squelch control
+ * @param threshold (dB)
+ */
+ public void setSquelchThreshold(double threshold)
+ {
+ mPowerSquelch.setSquelchThreshold(threshold);
+ }
+
+ /**
+ * Indicates if the squelch state has changed during the processing of buffer(s)
+ */
+ public boolean isSquelchChanged()
+ {
+ return mSquelchChanged;
+ }
+
+ /**
+ * Sets or resets the squelch changed flag.
+ */
+ private void setSquelchChanged(boolean changed)
+ {
+ mSquelchChanged = changed;
+ }
+
+ /**
+ * Indicates if the squelch state is currently muted
+ */
+ public boolean isMuted()
+ {
+ return mPowerSquelch.isMuted();
+ }
+
+ @Override
+ public void receive(SourceEvent sourceEvent)
+ {
+ //Only forward squelch threshold change request events
+ if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
+ {
+ mPowerSquelch.receive(sourceEvent);
+ }
+ else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
+ {
+ mPowerSquelch.receive(sourceEvent);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/dsp/squelch/ISquelchConfiguration.java b/src/main/java/io/github/dsheirer/dsp/squelch/ISquelchConfiguration.java
new file mode 100644
index 00000000..4bb72aa0
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/squelch/ISquelchConfiguration.java
@@ -0,0 +1,19 @@
+package io.github.dsheirer.dsp.squelch;
+
+/**
+ * Interface for configurations that support squelch threshold setting.
+ */
+public interface ISquelchConfiguration
+{
+ /**
+ * Sets the squelch threshold
+ * @param threshold (dB)
+ */
+ void setSquelchThreshold(int threshold);
+
+ /**
+ * Squelch threshold
+ * @return threshold (dB)
+ */
+ int getSquelchThreshold();
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/squelch/PowerMonitor.java b/src/main/java/io/github/dsheirer/dsp/squelch/PowerMonitor.java
new file mode 100644
index 00000000..3311d031
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/squelch/PowerMonitor.java
@@ -0,0 +1,97 @@
+package io.github.dsheirer.dsp.squelch;
+
+import io.github.dsheirer.dsp.filter.iir.SinglePoleIirFilter;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
+import io.github.dsheirer.source.SourceEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Power monitor. Provides periodic broadcast of current channel power from I/Q sample buffers.
+ */
+public class PowerMonitor
+{
+ private static final Logger mLog = LoggerFactory.getLogger(PowerMonitor.class);
+ private int mPowerLevelBroadcastCount = 0;
+ private int mPowerLevelBroadcastThreshold;
+ private Listener mSourceEventListener;
+ private SinglePoleIirFilter mPowerFilter = new SinglePoleIirFilter(0.1);
+
+ /**
+ * Constructs an instance
+ */
+ public PowerMonitor()
+ {
+ mPowerLevelBroadcastThreshold = 25000; //Based on a default sample rate of 50 kHz
+ }
+
+ /**
+ * Sets the sample rate to effect the frequency of power level notifications where the notifications are
+ * sent twice a second.
+ * @param sampleRate in hertz
+ */
+ public void setSampleRate(int sampleRate)
+ {
+ mPowerLevelBroadcastThreshold = sampleRate / 2;
+ }
+
+ /**
+ * Processes a complex IQ sample and changes squelch state when the signal power is above or below the
+ * threshold value.
+ * @param inphase complex sample component
+ * @param quadrature complex sample component
+ */
+ public void process(double inphase, double quadrature)
+ {
+ mPowerLevelBroadcastCount++;
+
+ if(mPowerLevelBroadcastCount > mPowerLevelBroadcastThreshold)
+ {
+ mPowerFilter.filter(inphase * inphase + quadrature * quadrature);
+ }
+
+ if(mPowerLevelBroadcastCount > (mPowerLevelBroadcastThreshold + 10))
+ {
+ mPowerLevelBroadcastCount = 0;
+ broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPowerFilter.getValue())));
+ }
+ }
+
+ /**
+ * Processes a complex baseband sample buffer. Note: this method does not decrement the user count
+ * on the buffer.
+ *
+ * @param buffer to process
+ */
+ public void process(ReusableComplexBuffer buffer)
+ {
+ float[] samples = buffer.getSamples();
+ int offset;
+
+ for(int x = 0; x < buffer.getSampleCount(); x++)
+ {
+ offset = x * 2;
+ process(samples[offset], samples[offset + 1]);
+ }
+ }
+
+ /**
+ * Registers the listener to receive power level notifications and squelch threshold requests
+ */
+ public void setSourceEventListener(Listener listener)
+ {
+ mSourceEventListener = listener;
+ }
+
+ /**
+ * Broadcasts the source event to an optional register listener
+ */
+ private void broadcast(SourceEvent event)
+ {
+ if(mSourceEventListener != null)
+ {
+ mSourceEventListener.receive(event);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/squelch/PowerSquelch.java b/src/main/java/io/github/dsheirer/dsp/squelch/PowerSquelch.java
new file mode 100644
index 00000000..3991a3ff
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/squelch/PowerSquelch.java
@@ -0,0 +1,279 @@
+package io.github.dsheirer.dsp.squelch;
+
+import io.github.dsheirer.dsp.filter.iir.SinglePoleIirFilter;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.source.SourceEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Power squelch
+ *
+ * Modeled after gnuradio complex power squelch:
+ * https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/pwr_squelch_cc_impl.cc
+ */
+public class PowerSquelch implements Listener
+{
+ private static final Logger mLog = LoggerFactory.getLogger(PowerSquelch.class);
+ private State mState = State.MUTE;
+ private SinglePoleIirFilter mFilter;
+ private double mPower = 0.0f;
+ private double mSquelchThreshold;
+ private int mRampThreshold;
+ private int mRampCount;
+ private boolean mSquelchChanged = false;
+ private boolean mStateChange = false;
+ private int mPowerLevelBroadcastCount = 0;
+ private int mPowerLevelBroadcastThreshold;
+ private Listener mSourceEventListener;
+
+ /**
+ * Constructs an instance
+ *
+ * Testing against a 12.5 kHz analog FM modulated signal, the following parameters provided a
+ * good responsiveness. A threshold of -80.0 dB seemed to trigger significant flapping during un-squelching.
+ * - alpha: 0.0001
+ * - threshold: 78.0 dB
+ * - ramp: 4 (samples)
+ *
+ * @param alpha decay value of the single pole IIR filter in range: 0.0 - 1.0. The smaller the alpha value,
+ * the slower the squelch response.
+ * @param squelchThreshold in decibels. Signal power must exceed this threshold value for unsquelch.
+ * @param ramp count of samples before transition between mute and unmute. Setting this value to zero
+ * causes immediate mute and unmute. Set to higher count to prevent mute/unmute flapping.
+ */
+ public PowerSquelch(double alpha, double squelchThreshold, int ramp)
+ {
+ mFilter = new SinglePoleIirFilter(alpha);
+ setSquelchThreshold(squelchThreshold);
+ mRampThreshold = ramp;
+ mPowerLevelBroadcastThreshold = 25000; //Based on a default sample rate of 50 kHz
+ }
+
+ /**
+ * Sets the sample rate to effect the frequency of power level notifications where the notifications are
+ * sent twice a second.
+ * @param sampleRate in hertz
+ */
+ public void setSampleRate(int sampleRate)
+ {
+ mPowerLevelBroadcastThreshold = sampleRate / 2;
+ }
+
+ /**
+ * Squelch threshold value
+ * @return value in decibels
+ */
+ public double getSquelchThreshold()
+ {
+ return 10.0 * Math.log10(mSquelchThreshold);
+ }
+
+ /**
+ * Sets the squelch threshold
+ * @param squelchThreshold in decibels
+ */
+ public void setSquelchThreshold(double squelchThreshold)
+ {
+ mSquelchThreshold = Math.pow(10.0, squelchThreshold / 10.0);
+ broadcast(SourceEvent.squelchThreshold(null, squelchThreshold));
+ }
+
+ /**
+ * Processes a complex IQ sample and changes squelch state when the signal power is above or below the
+ * threshold value.
+ * @param inphase complex sample component
+ * @param quadrature complex sample component
+ */
+ public void process(double inphase, double quadrature)
+ {
+ mPower = mFilter.filter(inphase * inphase + quadrature * quadrature);
+
+ mPowerLevelBroadcastCount++;
+ if(mPowerLevelBroadcastCount % mPowerLevelBroadcastThreshold == 0)
+ {
+ mPowerLevelBroadcastCount = 0;
+ broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPower)));
+ }
+
+ mStateChange = false;
+
+ switch(mState)
+ {
+ case MUTE -> {
+ if(!mute())
+ {
+ if(mRampThreshold > 0)
+ {
+ mState = State.ATTACK;
+ mRampCount++;
+ }
+ else
+ {
+ mState = State.UNMUTE;
+ mStateChange = true;
+ }
+ }
+ }
+ case ATTACK -> {
+ if(mRampCount >= mRampThreshold)
+ {
+ mState = State.UNMUTE;
+ mStateChange = true;
+ }
+ else
+ {
+ mRampCount++;
+ }
+ }
+ case DECAY -> {
+ if(mRampCount <= 0)
+ {
+ mState = State.MUTE;
+ mStateChange = true;
+ }
+ else
+ {
+ mRampCount--;
+ }
+ }
+ case UNMUTE -> {
+ if(mute())
+ {
+ if(mRampThreshold > 0)
+ {
+ mState = State.DECAY;
+ mRampCount--;
+ }
+ else
+ {
+ mState = State.MUTE;
+ mStateChange = true;
+ }
+ }
+ }
+ }
+
+ //Signal that the squelch state has changed if we modified the squelch state above.
+ setSquelchChanged(mStateChange);
+ }
+
+ /**
+ * Indicates if the current state is muted
+ */
+ public boolean isMuted()
+ {
+ return mState == State.MUTE;
+ }
+
+ /**
+ * Indicates if the current state is unmuted
+ */
+ public boolean isUnmuted()
+ {
+ return mState == State.UNMUTE;
+ }
+
+ /**
+ * Indicates if the current state is fading in (attach) or fading out (decay)
+ */
+ public boolean isFading()
+ {
+ return isAttack() || isDecay();
+ }
+
+ /**
+ * Indicates the squelch is in a ramp up attack state
+ */
+ public boolean isAttack()
+ {
+ return mState == State.ATTACK;
+ }
+
+ /**
+ * Indicates the squelch is in a ramp down decay state
+ */
+ public boolean isDecay()
+ {
+ return mState == State.DECAY;
+ }
+
+ /**
+ * Current power level
+ * @return current power level in dB
+ */
+ public float getPower()
+ {
+ return (float)(10.0 * Math.log10(mPower));
+ }
+
+ /**
+ * Indicates if the current power level is below the threshold, indicating that the state should be muted.
+ */
+ private boolean mute()
+ {
+ return mPower < mSquelchThreshold;
+ }
+
+ /**
+ * Indicates if the squelch state has changed (muted > unmuted, or vice-versa)
+ */
+ public boolean isSquelchChanged()
+ {
+ return mSquelchChanged;
+ }
+
+ /**
+ * Sets or resets the squelch changed flag
+ */
+ public void setSquelchChanged(boolean changed)
+ {
+ mSquelchChanged = changed;
+ }
+
+ /**
+ * Registers the listener to receive power level notifications and squelch threshold requests
+ */
+ public void setSourceEventListener(Listener listener)
+ {
+ mSourceEventListener = listener;
+ }
+
+ /**
+ * Broadcasts the source event to an optional register listener
+ */
+ private void broadcast(SourceEvent event)
+ {
+ if(mSourceEventListener != null)
+ {
+ mSourceEventListener.receive(event);
+ }
+ }
+
+ /**
+ * Primary method to receive requests for squelch threshold change
+ */
+ @Override
+ public void receive(SourceEvent sourceEvent)
+ {
+ if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
+ {
+ setSquelchThreshold(sourceEvent.getValue().doubleValue());
+ }
+ else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
+ {
+ broadcast(SourceEvent.squelchThreshold(null, getSquelchThreshold()));
+ }
+ }
+
+ /**
+ * State of squelch processing
+ */
+ private enum State
+ {
+ ATTACK,
+ DECAY,
+ MUTE,
+ UNMUTE;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/control/DbPowerMeter.java b/src/main/java/io/github/dsheirer/gui/control/DbPowerMeter.java
new file mode 100644
index 00000000..611ba1c8
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/control/DbPowerMeter.java
@@ -0,0 +1,349 @@
+package io.github.dsheirer.gui.control;
+
+import javax.swing.BorderFactory;
+import javax.swing.JComponent;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Power meter for displaying signal power levels in dB scale with optional peak value and squelch threshold lines.
+ */
+public class DbPowerMeter extends JComponent
+{
+ public static final double DEFAULT_MINIMUM_POWER = -110.0d;
+ public static final double DEFAULT_MAXIMUM_POWER = 0.0d;
+ private static final int BAR_WIDTH = 30;
+ private static final int PADDING = 3;
+ private static final int DOUBLE_PADDING = PADDING * 2;
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0");
+ private static final Color COLOR_BAR = Color.LIGHT_GRAY;
+ private static final Color COLOR_THRESHOLD = Color.BLUE;
+ private static final Color COLOR_PEAK = Color.PINK;
+
+ private double mMinimumValue = DEFAULT_MINIMUM_POWER;
+ private double mMaximumValue = DEFAULT_MAXIMUM_POWER;
+ private double mExtent = mMaximumValue - mMinimumValue;
+ private double mPeak = mMinimumValue;
+ private double mPower = mMinimumValue;
+ private double mSquelchThreshold = mMinimumValue;
+
+ private boolean mPeakVisible = false;
+ private boolean mSquelchThresholdVisible = false;
+
+ /**
+ * Constructs an instance.
+ */
+ public DbPowerMeter()
+ {
+ setPreferredSize(new Dimension(80, 100));
+ setBorder(BorderFactory.createTitledBorder("Power (dB)"));
+ }
+
+ /**
+ * Resets peak, power and squelch to minimums
+ */
+ public void reset()
+ {
+ mPeak = mMinimumValue;
+ mPower = mMinimumValue;
+ mSquelchThreshold = mMinimumValue;
+ repaint();
+ }
+
+ /**
+ * Primary method for rendering the control
+ */
+ @Override
+ protected void paintComponent(Graphics g)
+ {
+ super.paintComponent(g);
+
+ //Draw meter border
+ g.setColor(getForeground());
+ g.drawRect(getInsets().left, getInsets().top, BAR_WIDTH, getHeight() - getInsets().top - getInsets().bottom);
+
+ //Draw filled bar
+ g.setColor(COLOR_BAR);
+ int fillHeight = getHeight() - getInsets().top - getInsets().bottom - DOUBLE_PADDING;
+ int height = (int) (fillHeight * getPowerPercent() - PADDING);
+ int top = (fillHeight - height) + getInsets().top + PADDING;
+ g.fillRect(getInsets().left + PADDING, top, BAR_WIDTH - DOUBLE_PADDING, height);
+
+ //Draw threshold
+ if(isSquelchThresholdVisible())
+ {
+ g.setColor(COLOR_THRESHOLD);
+ drawPercentLine(g, getSquelchThresholdPercent());
+ }
+
+ //Draw peak line
+ if(isPeakVisible())
+ {
+ g.setColor(COLOR_PEAK);
+ drawPercentLine(g, getPeakPercent());
+ }
+
+ //Draw legend/scale
+ g.setColor(getForeground());
+ for(double legendValue: getScaleValues())
+ {
+ drawScaleText(g, legendValue);
+ }
+ }
+
+ /**
+ * Draws a line as a percentage of the value range (extent) using the current graphics color.
+ */
+ private void drawPercentLine(Graphics g, double percentValue)
+ {
+ int totalHeight = getHeight() - getInsets().top - getInsets().bottom - DOUBLE_PADDING;
+ int height = (int) (totalHeight * percentValue - PADDING);
+ int top = (totalHeight - height) + getInsets().top + PADDING;
+ int left = getInsets().left;
+ g.drawLine(left, top, left + BAR_WIDTH, top);
+ }
+
+ /**
+ * Draws a tick and text for the value
+ * @param g graphics object
+ * @param value to draw
+ */
+ private void drawScaleText(Graphics g, double value)
+ {
+ double percent = getPercent(value);
+ int totalHeight = getHeight() - getInsets().top - getInsets().bottom - DOUBLE_PADDING;
+ int height = (int) (totalHeight * percent - PADDING);
+ int top = (totalHeight - height) + getInsets().top + PADDING;
+ int left = getInsets().left + BAR_WIDTH - PADDING;
+ int textOffset = getFontMetrics(getFont()).getMaxAscent() / 2 - 2;
+ String label = DECIMAL_FORMAT.format(value);
+ g.drawString(label, left + DOUBLE_PADDING, top + textOffset);
+ }
+
+ /**
+ * Scale values to display, dynamically adapted to the height of the control
+ */
+ private List getScaleValues()
+ {
+ double ascent = getFontMetrics(getFont()).getMaxAscent() * 2;
+ double labelCount = getHeight() / ascent;
+ double interval = getExtent() / labelCount;
+ List values = new ArrayList<>();
+ for(double x = 0; x > getMinimumValue(); x -= interval)
+ {
+ values.add(x);
+ }
+
+ return values;
+ }
+
+ /**
+ * Current power level
+ * @return power level (dB)
+ */
+ public double getPower()
+ {
+ return mPower;
+ }
+
+ /**
+ * Sets the current power level constrained to the minimum and maximum values for this control.
+ * @param power (dB)
+ */
+ public void setPower(double power)
+ {
+ if(power > getMaximumValue())
+ {
+ mPower = getMaximumValue();
+ }
+ else if(power < getMinimumValue())
+ {
+ mPower = getMinimumValue();
+ }
+ else
+ {
+ mPower = power;
+ }
+
+ repaint();
+ }
+
+ /**
+ * Squelch threshold value.
+ * @return threshold (dB)
+ */
+ public double getSquelchThreshold()
+ {
+ return mSquelchThreshold;
+ }
+
+ /**
+ * Sets the squelch thresold value
+ * @param squelchThreshold (dB)
+ */
+ public void setSquelchThreshold(double squelchThreshold)
+ {
+ mSquelchThreshold = squelchThreshold;
+ repaint();
+ }
+
+ /**
+ * Minimum displayable power level
+ * @return minimum (dB) - defaults to -110 dB
+ */
+ public double getMinimumValue()
+ {
+ return mMinimumValue;
+ }
+
+ /**
+ * Sets the minimum displayable power level
+ * @param minimumValue (dB)
+ */
+ public void setMinimumValue(double minimumValue)
+ {
+ mMinimumValue = minimumValue;
+ updateExtent();
+ repaint();
+ }
+
+ /**
+ * Maximum displayable power level
+ * @return maximum (dB) - defaults to 0 dB
+ */
+ public double getMaximumValue()
+ {
+ return mMaximumValue;
+ }
+
+ /**
+ * Sets the maximum displayable power level
+ * @param maximumValue (dB)
+ */
+ public void setMaximumValue(double maximumValue)
+ {
+ mMaximumValue = maximumValue;
+ updateExtent();
+ repaint();
+ }
+
+ /**
+ * Updates the min/max extent
+ */
+ private void updateExtent()
+ {
+ mExtent = getMaximumValue() - getMinimumValue();
+ }
+
+ /**
+ * Extent or range of displayable values.
+ * @return extent (dB)
+ */
+ private double getExtent()
+ {
+ return mExtent;
+ }
+
+ /**
+ * Peak value line
+ * @return peak (dB)
+ */
+ public double getPeak()
+ {
+ return mPeak;
+ }
+
+ /**
+ * Sets the peak value line
+ * @param peak (db)
+ */
+ public void setPeak(double peak)
+ {
+ mPeak = peak;
+ repaint();
+ }
+
+ /**
+ * Indicates if the peak line is set to visible and the current peak value is within the min/max range.
+ */
+ public boolean isPeakVisible()
+ {
+ return mPeakVisible & isValidValue(getPeak());
+ }
+
+ /**
+ * Sets the peak line visibility
+ * @param peakVisible value
+ */
+ public void setPeakVisible(boolean peakVisible)
+ {
+ mPeakVisible = peakVisible;
+ repaint();
+ }
+
+ /**
+ * Indicates if the squelch threshold line is set to visible and the current squelch threshold value is
+ * within the min/max displayable value range.
+ * @return true if visible.
+ */
+ public boolean isSquelchThresholdVisible()
+ {
+ return mSquelchThresholdVisible & isValidValue(getSquelchThreshold());
+ }
+
+ /**
+ * Sets the visibility of the squelch threshold line
+ * @param squelchThresholdVisible visibility
+ */
+ public void setSquelchThresholdVisible(boolean squelchThresholdVisible)
+ {
+ mSquelchThresholdVisible = squelchThresholdVisible;
+ repaint();
+ }
+
+ /**
+ * Calculates the percentage of the min/max extent for the value
+ * @param value to calculate
+ * @return percentage of displayable value range
+ */
+ private double getPercent(double value)
+ {
+ return (value - getMinimumValue()) / getExtent();
+ }
+
+ /**
+ * Power as percentage of displayable range
+ */
+ private double getPowerPercent()
+ {
+ return getPercent(mPower);
+ }
+
+ /**
+ * Squelch threshold as percentage of displayable range
+ */
+ private double getSquelchThresholdPercent()
+ {
+ return getPercent(mSquelchThreshold);
+ }
+
+ /**
+ * Peak as percentage of displayable range
+ */
+ private double getPeakPercent()
+ {
+ return getPercent(mPeak);
+ }
+
+ /**
+ * Indicates if the value is valid, meaning it falls within the min/max range of displayable values.
+ */
+ private boolean isValidValue(double value)
+ {
+ return getMinimumValue() <= value && value <= getMaximumValue();
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/control/IntegerFormatter.java b/src/main/java/io/github/dsheirer/gui/control/IntegerFormatter.java
index cc8bfda6..9d522e1d 100644
--- a/src/main/java/io/github/dsheirer/gui/control/IntegerFormatter.java
+++ b/src/main/java/io/github/dsheirer/gui/control/IntegerFormatter.java
@@ -50,7 +50,7 @@ public class IntegerFormatter extends TextFormatter
*/
public static class IntegerFilter implements UnaryOperator
{
- private String DECIMAL_REGEX = "[0-9].*";
+ private String DECIMAL_REGEX = "\\-?[0-9].*";
private int mMinimum;
private int mMaximum;
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java
index 82b9cc74..00933b14 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java
@@ -710,6 +710,9 @@ public class AliasItemEditor extends Editor
mptMenu.getItems().add(new AddTalkgroupItem(Protocol.MPT1327));
mptMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.MPT1327));
+ Menu nbfmMenu = new ProtocolMenu(Protocol.NBFM);
+ nbfmMenu.getItems().add(new AddTalkgroupItem(Protocol.NBFM));
+
Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT);
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
passportMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.PASSPORT));
@@ -723,7 +726,7 @@ public class AliasItemEditor extends Editor
lojackMenu.getItems().add(new AddLojackItem());
mAddIdentifierButton.getItems().addAll(p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu,
- passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu);
+ nbfmMenu, passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu);
}
return mAddIdentifierButton;
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java
index a1045631..db8aabb3 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java
@@ -253,10 +253,14 @@ public class TalkgroupEditor extends IdentifierEditor
"Format: 0 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.MPT1327, IntegerFormat.FORMATTED,
new PrefixIdentFormatter(0,0xFFFFF), "Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
+ mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF),
+ "Format: 1 - 65535"));
+ mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
+ "Format: 1 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
- "Format: 0 - 65535"));
+ "Format: 0 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
- "Format: 0 - FFFF"));
+ "Format: 0 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.DECIMAL, new IntegerFormatter(0,16777215),
"Format: 0 - FFFFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.FORMATTED, new IntegerFormatter(0,16777215),
@@ -265,7 +269,7 @@ public class TalkgroupEditor extends IdentifierEditor
"Format: 0 - FFFFFF"));
}
- public class TalkgroupDetail
+ public static class TalkgroupDetail
{
private Protocol mProtocol;
private IntegerFormat mIntegerFormat;
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java
index 2744cf88..e1494896 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java
@@ -83,7 +83,7 @@ public abstract class ChannelConfigurationEditor extends Editor
private final static Logger mLog = LoggerFactory.getLogger(ChannelConfigurationEditor.class);
private PlaylistManager mPlaylistManager;
- private UserPreferences mUserPreferences;
+ protected UserPreferences mUserPreferences;
protected EditorModificationListener mEditorModificationListener = new EditorModificationListener();
private Button mPlayButton;
private TextField mSystemField;
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/NBFMConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/NBFMConfigurationEditor.java
index 49b11bbd..08ec4145 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/channel/NBFMConfigurationEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/NBFMConfigurationEditor.java
@@ -22,6 +22,9 @@
package io.github.dsheirer.gui.playlist.channel;
+import io.github.dsheirer.gui.control.DbPowerMeter;
+import io.github.dsheirer.gui.control.HexFormatter;
+import io.github.dsheirer.gui.control.IntegerFormatter;
import io.github.dsheirer.gui.playlist.decoder.AuxDecoderConfigurationEditor;
import io.github.dsheirer.gui.playlist.eventlog.EventLogConfigurationEditor;
import io.github.dsheirer.gui.playlist.source.FrequencyEditor;
@@ -34,15 +37,23 @@ import io.github.dsheirer.module.log.EventLogType;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
+import io.github.dsheirer.preference.identifier.IntegerFormat;
+import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TextFormatter;
import javafx.scene.control.TitledPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleButton;
+import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.text.TextAlignment;
import org.controlsfx.control.SegmentedButton;
@@ -64,12 +75,20 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
private TitledPane mEventLogPane;
private TitledPane mRecordPane;
private TitledPane mSourcePane;
- private ToggleSwitch mAudioRecordSwitch;
+ private TextField mTalkgroupField;
+ private TextField mSquelchThresholdField;
+ private TextFormatter mTalkgroupTextFormatter;
+ private IntegerFormatter mSquelchTextFormatter = new IntegerFormatter((int)DbPowerMeter.DEFAULT_MINIMUM_POWER,
+ (int)DbPowerMeter.DEFAULT_MAXIMUM_POWER);
private ToggleSwitch mBasebandRecordSwitch;
private SegmentedButton mBandwidthButton;
+
private SourceConfigurationEditor mSourceConfigurationEditor;
private AuxDecoderConfigurationEditor mAuxDecoderConfigurationEditor;
private EventLogConfigurationEditor mEventLogConfigurationEditor;
+ private TalkgroupValueChangeListener mTalkgroupValueChangeListener = new TalkgroupValueChangeListener();
+ private IntegerFormatter mDecimalFormatter = new IntegerFormatter(1, 65535);
+ private HexFormatter mHexFormatter = new HexFormatter(1, 65535);
/**
* Constructs an instance
@@ -109,20 +128,37 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
{
mDecoderPane = new TitledPane();
mDecoderPane.setText("Decoder: NBFM");
- mDecoderPane.setExpanded(false);
+ mDecoderPane.setExpanded(true);
GridPane gridPane = new GridPane();
gridPane.setPadding(new Insets(10,10,10,10));
gridPane.setHgap(10);
+ gridPane.setVgap(10);
Label bandwidthLabel = new Label("Channel Bandwidth");
- GridPane.setHalignment(bandwidthLabel, HPos.LEFT);
+ GridPane.setHalignment(bandwidthLabel, HPos.RIGHT);
GridPane.setConstraints(bandwidthLabel, 0, 0);
gridPane.getChildren().add(bandwidthLabel);
GridPane.setConstraints(getBandwidthButton(), 1, 0);
gridPane.getChildren().add(getBandwidthButton());
+ Label squelchLabel = new Label("Squelch Threshold");
+ GridPane.setHalignment(squelchLabel, HPos.RIGHT);
+ GridPane.setConstraints(squelchLabel, 2, 0);
+ gridPane.getChildren().add(squelchLabel);
+
+ GridPane.setConstraints(getSquelchThresholdField(), 3, 0);
+ gridPane.getChildren().add(getSquelchThresholdField());
+
+ Label talkgroupLabel = new Label("Talkgroup To Assign");
+ GridPane.setHalignment(talkgroupLabel, HPos.RIGHT);
+ GridPane.setConstraints(talkgroupLabel, 4, 0);
+ gridPane.getChildren().add(talkgroupLabel);
+
+ GridPane.setConstraints(getTalkgroupField(), 5, 0);
+ gridPane.getChildren().add(getTalkgroupField());
+
mDecoderPane.setContent(gridPane);
//Special handling - the pill button doesn't like to set a selected state if the pane is not expanded,
@@ -174,20 +210,12 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
gridPane.setHgap(10);
gridPane.setVgap(10);
- GridPane.setConstraints(getAudioRecordSwitch(), 0, 0);
- gridPane.getChildren().add(getAudioRecordSwitch());
-
- Label recordAudioLabel = new Label("Audio");
- GridPane.setHalignment(recordAudioLabel, HPos.LEFT);
- GridPane.setConstraints(recordAudioLabel, 1, 0);
- gridPane.getChildren().add(recordAudioLabel);
-
- GridPane.setConstraints(getBasebandRecordSwitch(), 0, 1);
+ GridPane.setConstraints(getBasebandRecordSwitch(), 0, 0);
gridPane.getChildren().add(getBasebandRecordSwitch());
Label recordBasebandLabel = new Label("Channel (Baseband I&Q)");
GridPane.setHalignment(recordBasebandLabel, HPos.LEFT);
- GridPane.setConstraints(recordBasebandLabel, 1, 1);
+ GridPane.setConstraints(recordBasebandLabel, 1, 0);
gridPane.getChildren().add(recordBasebandLabel);
mRecordPane.setContent(gridPane);
@@ -255,24 +283,114 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
mBandwidthButton.getToggleGroup().selectedToggleProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+
+ //Note: there is a weird timing bug with the segmented button where the toggles are not added to
+ //the toggle group until well after the control is rendered. We attempt to setItem() on the
+ //decode configuration and we're unable to correctly set the bandwidth setting. As a work
+ //around, we'll listen for the toggles to be added and update them here. This normally only
+ //happens when we first instantiate the editor and load an item for editing the first time.
+ mBandwidthButton.getToggleGroup().getToggles().addListener(new ListChangeListener()
+ {
+ @Override
+ public void onChanged(Change extends Toggle> c)
+ {
+ //This change event happens when the toggles are added -- we don't need to inspect the change event
+ if(getItem() != null && getItem().getDecodeConfiguration() instanceof DecodeConfigNBFM)
+ {
+ //Capture current modified state so that we can reapply after adjusting control states
+ boolean modified = modifiedProperty().get();
+
+ DecodeConfigNBFM config = (DecodeConfigNBFM)getItem().getDecodeConfiguration();
+ DecodeConfigNBFM.Bandwidth bandwidth = config.getBandwidth();
+ if(bandwidth == null)
+ {
+ bandwidth = DecodeConfigNBFM.Bandwidth.BW_12_5;
+ }
+
+ for(Toggle toggle: getBandwidthButton().getToggleGroup().getToggles())
+ {
+ toggle.setSelected(toggle.getUserData() == bandwidth);
+ }
+
+ modifiedProperty().set(modified);
+ }
+ }
+ });
}
return mBandwidthButton;
}
- private ToggleSwitch getAudioRecordSwitch()
+ private TextField getTalkgroupField()
{
- if(mAudioRecordSwitch == null)
+ if(mTalkgroupField == null)
{
- mAudioRecordSwitch = new ToggleSwitch();
- mAudioRecordSwitch.setDisable(true);
- mAudioRecordSwitch.setTextAlignment(TextAlignment.RIGHT);
- mAudioRecordSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ mTalkgroupField = new TextField();
+ mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
}
- return mAudioRecordSwitch;
+ return mTalkgroupField;
}
+ private TextField getSquelchThresholdField()
+ {
+ if(mSquelchThresholdField == null)
+ {
+ mSquelchThresholdField = new TextField();
+ mSquelchThresholdField.setTextFormatter(mSquelchTextFormatter);
+ }
+
+ return mSquelchThresholdField;
+ }
+
+ /**
+ * Updates the talkgroup editor's text formatter.
+ * @param value to set in the control.
+ */
+ private void updateTextFormatter(int value)
+ {
+ if(mTalkgroupTextFormatter != null)
+ {
+ mTalkgroupTextFormatter.valueProperty().removeListener(mTalkgroupValueChangeListener);
+ }
+
+ IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(Protocol.NBFM);
+
+ if(format == null)
+ {
+ format = IntegerFormat.DECIMAL;
+ }
+
+ if(format == IntegerFormat.DECIMAL)
+ {
+ mTalkgroupTextFormatter = mDecimalFormatter;
+ getTalkgroupField().setTooltip(new Tooltip("1 - 65,535"));
+ }
+ else
+ {
+ mTalkgroupTextFormatter = mDecimalFormatter;
+ getTalkgroupField().setTooltip(new Tooltip("1 - FFFF"));
+ }
+
+ mTalkgroupTextFormatter.setValue(value);
+
+ getTalkgroupField().setTextFormatter(mTalkgroupTextFormatter);
+ mTalkgroupTextFormatter.valueProperty().addListener(mTalkgroupValueChangeListener);
+ }
+
+ /**
+ * Change listener to detect when talkgroup value has changed and set modified property to true.
+ */
+ public class TalkgroupValueChangeListener implements ChangeListener
+ {
+ @Override
+ public void changed(ObservableValue extends Integer> observable, Integer oldValue, Integer newValue)
+ {
+ modifiedProperty().set(true);
+ }
+ }
+
+
private ToggleSwitch getBasebandRecordSwitch()
{
if(mBasebandRecordSwitch == null)
@@ -294,20 +412,17 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
{
getBandwidthButton().setDisable(false);
DecodeConfigNBFM decodeConfigNBFM = (DecodeConfigNBFM)config;
- DecodeConfigNBFM.Bandwidth bandwidth = decodeConfigNBFM.getBandwidth();
-
- if(bandwidth == null)
- {
- bandwidth = DecodeConfigNBFM.Bandwidth.BW_12_5;
- }
+ final DecodeConfigNBFM.Bandwidth bandwidth = (decodeConfigNBFM.getBandwidth() != null ?
+ decodeConfigNBFM.getBandwidth() : DecodeConfigNBFM.Bandwidth.BW_12_5);
for(Toggle toggle: getBandwidthButton().getToggleGroup().getToggles())
{
toggle.setSelected(toggle.getUserData() == bandwidth);
}
- getAudioRecordSwitch().setDisable(false);
- getAudioRecordSwitch().selectedProperty().set(decodeConfigNBFM.getRecordAudio());
+ updateTextFormatter(decodeConfigNBFM.getTalkgroup());
+
+ mSquelchTextFormatter.setValue(decodeConfigNBFM.getSquelchThreshold());
}
else
{
@@ -318,8 +433,8 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
toggle.setSelected(false);
}
- getAudioRecordSwitch().setDisable(true);
- getAudioRecordSwitch().selectedProperty().set(false);
+ updateTextFormatter(0);
+ getTalkgroupField().setDisable(true);
}
}
@@ -346,8 +461,15 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
config.setBandwidth(bandwidth);
- config.setRecordAudio(getAudioRecordSwitch().isSelected());
+ Integer talkgroup = mTalkgroupTextFormatter.getValue();
+ if(talkgroup == null)
+ {
+ talkgroup = 1;
+ }
+
+ config.setTalkgroup(talkgroup);
+ config.setSquelchThreshold(mSquelchTextFormatter.getValue());
getItem().setDecodeConfiguration(config);
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java
index 6477ea1c..23f89afe 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java
@@ -413,15 +413,18 @@ public class RadioReferenceDecoder
case "DMR":
return Protocol.DMR;
case "LTR":
- if(flavor.getName().contentEquals("Standard") || flavor.getName().contentEquals("Net"))
+ if(flavor != null)
{
- return Protocol.LTR;
+ if(flavor.getName().contentEquals("Standard") || flavor.getName().contentEquals("Net"))
+ {
+ return Protocol.LTR;
+ }
+ else if(flavor.getName().contentEquals("Passport"))
+ {
+ return Protocol.PASSPORT;
+ }
}
- else if(flavor.getName().contentEquals("Passport"))
- {
- return Protocol.PASSPORT;
- }
- break;
+ return Protocol.LTR;
case "MPT-1327":
return Protocol.MPT1327;
case "Project 25":
diff --git a/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java b/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java
new file mode 100644
index 00000000..9e350898
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java
@@ -0,0 +1,211 @@
+package io.github.dsheirer.gui.power;
+
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.dsp.squelch.ISquelchConfiguration;
+import io.github.dsheirer.gui.control.DbPowerMeter;
+import io.github.dsheirer.module.ProcessingChain;
+import io.github.dsheirer.playlist.PlaylistManager;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.source.SourceEvent;
+import jiconfont.icons.font_awesome.FontAwesome;
+import jiconfont.swing.IconFontSwing;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import java.awt.EventQueue;
+import java.text.DecimalFormat;
+
+/**
+ * Display for channel power and squelch details
+ */
+public class ChannelPowerPanel extends JPanel implements Listener
+{
+ private static final Logger mLog = LoggerFactory.getLogger(ChannelPowerPanel.class);
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0");
+ private static final String NOT_AVAILABLE = "Not Available";
+ private PlaylistManager mPlaylistManager;
+ private ProcessingChain mProcessingChain;
+ private DbPowerMeter mPowerMeter = new DbPowerMeter();
+ private PeakMonitor mPeakMonitor = new PeakMonitor(DbPowerMeter.DEFAULT_MINIMUM_POWER);
+ private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
+ private JLabel mPowerLabel;
+ private JLabel mPeakLabel;
+ private JLabel mSquelchLabel;
+ private JLabel mSquelchValueLabel;
+ private JButton mSquelchUpButton;
+ private JButton mSquelchDownButton;
+ private double mSquelchThreshold;
+
+ /**
+ * Constructs an instance.
+ */
+ public ChannelPowerPanel(PlaylistManager playlistManager)
+ {
+ mPlaylistManager = playlistManager;
+
+ setLayout(new MigLayout("", "[][grow,fill]", "[grow,fill]"));
+ mPowerMeter.setPeakVisible(true);
+ mPowerMeter.setSquelchThresholdVisible(true);
+ add(mPowerMeter);
+
+ JPanel valuePanel = new JPanel();
+ valuePanel.setLayout(new MigLayout("", "[right][left][][]", ""));
+
+ mPeakLabel = new JLabel("0");
+ valuePanel.add(new JLabel("Peak:"));
+ valuePanel.add(mPeakLabel, "wrap");
+
+ mPowerLabel = new JLabel("0");
+ valuePanel.add(new JLabel("Power:"));
+ valuePanel.add(mPowerLabel, "wrap");
+
+ mSquelchLabel = new JLabel("Squelch:");
+ mSquelchLabel.setEnabled(false);
+ valuePanel.add(mSquelchLabel);
+ mSquelchValueLabel = new JLabel(NOT_AVAILABLE);
+ mSquelchValueLabel.setEnabled(false);
+ valuePanel.add(mSquelchValueLabel, "wrap");
+
+ IconFontSwing.register(FontAwesome.getIconFont());
+ Icon iconUp = IconFontSwing.buildIcon(FontAwesome.ANGLE_UP, 12);
+ mSquelchUpButton = new JButton(iconUp);
+ mSquelchUpButton.setEnabled(false);
+ mSquelchUpButton.addActionListener(e -> broadcast(SourceEvent.requestSquelchThreshold(null, mSquelchThreshold + 1)));
+ valuePanel.add(mSquelchUpButton);
+
+ Icon iconDown = IconFontSwing.buildIcon(FontAwesome.ANGLE_DOWN, 12);
+ mSquelchDownButton = new JButton(iconDown);
+ mSquelchDownButton.setEnabled(false);
+ mSquelchDownButton.addActionListener(e -> broadcast(SourceEvent.requestSquelchThreshold(null, mSquelchThreshold - 1)));
+ valuePanel.add(mSquelchDownButton);
+
+ add(valuePanel);
+ }
+
+ /**
+ * Updates the channel's decode configuration with a new squelch threshold value
+ */
+ private void setConfigSquelchThreshold(int threshold)
+ {
+ if(mProcessingChain != null)
+ {
+ Channel channel = mPlaylistManager.getChannelProcessingManager().getChannel(mProcessingChain);
+
+ if(channel != null && channel.getDecodeConfiguration() instanceof ISquelchConfiguration)
+ {
+ ISquelchConfiguration configuration = (ISquelchConfiguration)channel.getDecodeConfiguration();
+ configuration.setSquelchThreshold(threshold);
+ mPlaylistManager.schedulePlaylistSave();
+ }
+ }
+ }
+
+ private void broadcast(SourceEvent sourceEvent)
+ {
+ if(mProcessingChain != null)
+ {
+ mProcessingChain.broadcast(sourceEvent);
+ }
+ }
+
+ /**
+ * Resets controls when changing processing chain source. Note: this must be called on the Swing
+ * dispatch thread because it directly invokes swing components.
+ */
+ private void reset()
+ {
+ mPeakMonitor.reset();
+ mPowerMeter.reset();
+
+ mPeakLabel.setText("0");
+ mPowerLabel.setText("0");
+
+ mSquelchLabel.setEnabled(false);
+ mSquelchValueLabel.setText("Not Available");
+ mSquelchValueLabel.setEnabled(false);
+ mSquelchUpButton.setEnabled(false);
+ mSquelchDownButton.setEnabled(false);
+ }
+
+ /**
+ * Receive notifications of request to provide display of processing chain details.
+ */
+ @Override
+ public void receive(ProcessingChain processingChain)
+ {
+ if(mProcessingChain != null)
+ {
+ mProcessingChain.removeSourceEventListener(mSourceEventProcessor);
+ }
+
+ //Invoking reset - we're on the Swing dispatch thread here
+ reset();
+
+ mProcessingChain = processingChain;
+
+ if(mProcessingChain != null)
+ {
+ mProcessingChain.addSourceEventListener(mSourceEventProcessor);
+ }
+
+ broadcast(SourceEvent.requestCurrentSquelchThreshold(null));
+ }
+
+ /**
+ * Processor for source event stream to capture power level and squelch related source events.
+ */
+ private class SourceEventProcessor implements Listener
+ {
+ @Override
+ public void receive(SourceEvent sourceEvent)
+ {
+ switch(sourceEvent.getEvent())
+ {
+ case NOTIFICATION_CHANNEL_POWER ->
+ {
+ final double power = sourceEvent.getValue().doubleValue();
+ final double peak = mPeakMonitor.process(power);
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mPowerMeter.setPower(power);
+ mPowerLabel.setText(DECIMAL_FORMAT.format(power));
+
+ mPowerMeter.setPeak(peak);
+ mPeakLabel.setText(DECIMAL_FORMAT.format(peak));
+ }
+ });
+
+ }
+ case NOTIFICATION_SQUELCH_THRESHOLD ->
+ {
+ final double threshold = sourceEvent.getValue().doubleValue();
+ mSquelchThreshold = threshold;
+ setConfigSquelchThreshold((int)threshold);
+
+ EventQueue.invokeLater(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ mPowerMeter.setSquelchThreshold(threshold);
+ mSquelchLabel.setEnabled(true);
+ mSquelchValueLabel.setEnabled(true);
+ mSquelchValueLabel.setText(DECIMAL_FORMAT.format(threshold));
+ mSquelchDownButton.setEnabled(true);
+ mSquelchUpButton.setEnabled(true);
+ }
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/power/PeakMonitor.java b/src/main/java/io/github/dsheirer/gui/power/PeakMonitor.java
new file mode 100644
index 00000000..5fffcac0
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/power/PeakMonitor.java
@@ -0,0 +1,51 @@
+package io.github.dsheirer.gui.power;
+
+/**
+ * Peak value monitor
+ */
+public class PeakMonitor
+{
+ private double mInitialValue;
+ private double mPeak;
+
+ /**
+ * Constructs an instance
+ * @param initialValue for the peak value
+ */
+ public PeakMonitor(double initialValue)
+ {
+ mInitialValue = initialValue;
+ reset();
+ }
+
+ /**
+ * Current peak value
+ */
+ public double getPeak()
+ {
+ return mPeak;
+ }
+
+ /**
+ * Processes the value and returns the current peak value
+ * @param value to process
+ * @return current peak value
+ */
+ public double process(double value)
+ {
+ if(value > mPeak)
+ {
+ mPeak = value;
+ }
+
+ return getPeak();
+ }
+
+ /**
+ * Resets the peak value to the initial value
+ */
+ public void reset()
+ {
+ mPeak = mInitialValue;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java
index 25b825a1..3ad78310 100644
--- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java
+++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java
@@ -342,6 +342,14 @@ public class ProcessingChain implements Listener
mModules.remove(module);
}
+ /**
+ * Broadcasts the source event to the processing chain modules.
+ */
+ public void broadcast(SourceEvent sourceEvent)
+ {
+ mSourceEventBroadcaster.broadcast(sourceEvent);
+ }
+
/**
* Registers the module as a listener to each of the broadcasters that
* provide the data interface(s) supported by the module.
@@ -830,6 +838,14 @@ public class ProcessingChain implements Listener
mSourceEventBroadcaster.addListener(listener);
}
+ /**
+ * Removes the listener from receiving source events from this processing chain.
+ */
+ public void removeSourceEventListener(Listener listener)
+ {
+ mSourceEventBroadcaster.removeListener(listener);
+ }
+
/**
* Adds the listener to receive frequency change events from the processing chain
*
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
index 9a71bdf0..aca18a9c 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
@@ -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 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 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 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:
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
index fb2dde3b..31690872 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
@@ -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),
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java
index 0f1de917..d5893275 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java
@@ -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 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
diff --git a/src/main/java/io/github/dsheirer/module/decode/nbfm/DecodeConfigNBFM.java b/src/main/java/io/github/dsheirer/module/decode/nbfm/DecodeConfigNBFM.java
index 588b4e79..43a6a7b2 100644
--- a/src/main/java/io/github/dsheirer/module/decode/nbfm/DecodeConfigNBFM.java
+++ b/src/main/java/io/github/dsheirer/module/decode/nbfm/DecodeConfigNBFM.java
@@ -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;
+ }
}
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoder.java
index 31752072..4676744c 100644
--- a/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoder.java
@@ -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, 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 mResampledReusableBufferListener;
+ private Listener 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 getReusableComplexBufferListener()
+ {
+ return this;
+ }
+
+ @Override
+ public Listener 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 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 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 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
+ {
+ @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);
+ }
+ }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoderState.java
new file mode 100644
index 00000000..fac97418
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoderState.java
@@ -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
+ * ****************************************************************************
+ */
+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 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 getSourceEventListener()
+ {
+ return mSourceEventListener;
+ }
+
+ /**
+ * Monitors source events to capture the channel frequency for use in decode events.
+ */
+ private class SourceEventListener implements Listener
+ {
+ @Override
+ public void receive(SourceEvent sourceEvent)
+ {
+ if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE)
+ {
+ mChannelDescriptor = new StandardChannel(sourceEvent.getValue().longValue());
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMTalkgroup.java b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMTalkgroup.java
new file mode 100644
index 00000000..5d212cc5
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMTalkgroup.java
@@ -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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/StandardChannel.java b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/StandardChannel.java
new file mode 100644
index 00000000..2848eeb7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/channel/StandardChannel.java
@@ -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";
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Decoder.java
index 6c2d6ef5..9e83e35a 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Decoder.java
@@ -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, IReusableByteBufferProvider
{
private double mSampleRate;
@@ -43,6 +45,7 @@ public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEve
private P25P1MessageProcessor mMessageProcessor;
private Listener 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 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;
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java
index 6f1a317c..d38ad8e9 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java
@@ -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);
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java
index 171f579e..e37b1746 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSM.java
@@ -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);
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java
index ed74cb3f..0d606d99 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2Decoder.java
@@ -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, 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 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;
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java
index dcb4555e..3dfc5f33 100644
--- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java
+++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderHDQPSK.java
@@ -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);
diff --git a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java
index 4c779007..a51e44ee 100644
--- a/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java
+++ b/src/main/java/io/github/dsheirer/module/demodulate/fm/FMDemodulatorModule.java
@@ -22,6 +22,7 @@ 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.FMDemodulator;
+import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IReusableBufferProvider;
@@ -29,6 +30,7 @@ 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;
@@ -38,13 +40,14 @@ import org.slf4j.LoggerFactory;
*
* Note: no filtering is applied to the demodulated audio.
*/
-public class FMDemodulatorModule extends Module implements ISourceEventListener, IReusableComplexBufferListener,
- Listener, IReusableBufferProvider
+public class FMDemodulatorModule extends Module implements ISourceEventListener, ISourceEventProvider,
+ IReusableComplexBufferListener, Listener, IReusableBufferProvider
{
private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatorModule.class);
private ComplexFIRFilter2 mIQFilter;
private FMDemodulator mDemodulator = new FMDemodulator();
+ private PowerMonitor mPowerMonitor = new PowerMonitor();
private RealResampler mResampler;
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
private Listener mResampledReusableBufferListener;
@@ -76,6 +79,19 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener,
return mSourceEventProcessor;
}
+ @Override
+ public void dispose()
+ {
+ if(mIQFilter != null)
+ {
+ mIQFilter.dispose();
+ mIQFilter = null;
+ }
+
+ mDemodulator.dispose();
+ mDemodulator = null;
+ }
+
@Override
public void reset()
{
@@ -111,10 +127,11 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener,
{
reusableComplexBuffer.decrementUserCount();
throw new IllegalStateException("FM demodulator module must receive a sample rate change source " +
- "event before it can process complex sample buffers");
+ "event before it can process complex sample buffers");
}
ReusableComplexBuffer basebandFilteredBuffer = mIQFilter.filter(reusableComplexBuffer);
+ mPowerMonitor.process(basebandFilteredBuffer);
ReusableFloatBuffer demodulatedBuffer = mDemodulator.demodulate(basebandFilteredBuffer);
if(mResampler != null)
@@ -127,6 +144,18 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener,
}
}
+ @Override
+ public void setSourceEventListener(Listener listener)
+ {
+ mPowerMonitor.setSourceEventListener(listener);
+ }
+
+ @Override
+ public void removeSourceEventListener()
+ {
+ mPowerMonitor.setSourceEventListener(null);
+ }
+
/**
* Monitors sample rate change source event(s) to setup the initial I/Q filter
*/
@@ -148,10 +177,12 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener,
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"));
+ "] requires a channel sample rate of [" + (2.0 * mChannelBandwidth + "] - sample rate of [" +
+ sampleRate + "] is not supported"));
}
+ mPowerMonitor.setSampleRate((int)sampleRate);
+
double cutoff = sampleRate / 4.0;
int passBandStop = (int)cutoff - 500;
int stopBandStart = (int)cutoff + 500;
@@ -159,16 +190,16 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener,
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.028) //Approximately 60 dB attenuation
- .build();
+ .sampleRate(sampleRate)
+ .gridDensity(16)
+ .oddLength(true)
+ .passBandCutoff(passBandStop)
+ .passBandAmplitude(1.0)
+ .passBandRipple(0.01)
+ .stopBandStart(stopBandStart)
+ .stopBandAmplitude(0.0)
+ .stopBandRipple(0.028) //Approximately 60 dB attenuation
+ .build();
try
{
@@ -177,14 +208,14 @@ public class FMDemodulatorModule extends Module implements ISourceEventListener,
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");
+ "] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart +
+ "] - using sinc filter");
}
if(filterTaps == null)
{
filterTaps = FilterFactory.getLowPass(sampleRate, passBandStop, stopBandStart, 60,
- Window.WindowType.HAMMING, true);
+ Window.WindowType.HAMMING, true);
}
mIQFilter = new ComplexFIRFilter2(filterTaps);
diff --git a/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java b/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java
index 169a0d76..080c38b3 100644
--- a/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java
+++ b/src/main/java/io/github/dsheirer/playlist/PlaylistManager.java
@@ -545,7 +545,7 @@ public class PlaylistManager implements Listener
* Schedules a playlist save task. Subsequent calls to this method will be ignored until the save event occurs,
* thus limiting repetitive playlist saving to a minimum.
*/
- private void schedulePlaylistSave()
+ public void schedulePlaylistSave()
{
if(!mPlaylistLoading)
{
diff --git a/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java b/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java
index 26bfc457..362232d4 100644
--- a/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java
+++ b/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java
@@ -152,6 +152,7 @@ public class TalkgroupFormatPreference extends Preference
case APCO25:
case DMR:
case MDC1200:
+ case NBFM:
case PASSPORT:
case UNKNOWN:
default:
@@ -171,6 +172,7 @@ public class TalkgroupFormatPreference extends Preference
case APCO25:
case DMR:
case MDC1200:
+ case NBFM:
case PASSPORT:
case UNKNOWN:
default:
@@ -191,6 +193,7 @@ public class TalkgroupFormatPreference extends Preference
case LTR:
case LTR_NET:
case MPT1327:
+ case NBFM:
case UNKNOWN:
default:
return true;
diff --git a/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NBFMTalkgroupFormatter.java b/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NBFMTalkgroupFormatter.java
new file mode 100644
index 00000000..2535c156
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NBFMTalkgroupFormatter.java
@@ -0,0 +1,78 @@
+/*
+ * ******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2019 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
+ * *****************************************************************************
+ */
+
+package io.github.dsheirer.preference.identifier.talkgroup;
+
+import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
+import io.github.dsheirer.preference.identifier.IntegerFormat;
+
+public class NBFMTalkgroupFormatter extends AbstractIntegerFormatter
+{
+ public static final int DECIMAL_WIDTH = 5;
+ public static final int HEXADECIMAL_WIDTH = 4;
+
+ /**
+ * Formats the individual or group identifier to the specified format and width.
+ */
+ public static String format(TalkgroupIdentifier identifier, IntegerFormat format, boolean fixedWidth)
+ {
+ if(fixedWidth)
+ {
+ switch(format)
+ {
+ case DECIMAL:
+ case FORMATTED:
+ return toDecimal(identifier.getValue(), DECIMAL_WIDTH);
+ case HEXADECIMAL:
+ return toHex(identifier.getValue(), HEXADECIMAL_WIDTH);
+ default:
+ throw new IllegalArgumentException("Unrecognized integer format: " + format);
+ }
+ }
+ else
+ {
+ switch(format)
+ {
+ case DECIMAL:
+ case FORMATTED:
+ return identifier.getValue().toString();
+ case HEXADECIMAL:
+ return toHex(identifier.getValue());
+ default:
+ throw new IllegalArgumentException("Unrecognized integer format: " + format);
+ }
+ }
+ }
+
+ @Override
+ public String format(int value, IntegerFormat integerFormat)
+ {
+ switch(integerFormat)
+ {
+ case DECIMAL:
+ case FORMATTED:
+ return format(value);
+ case HEXADECIMAL:
+ return toHex(value);
+ default:
+ throw new IllegalArgumentException("Unrecognized integer format: " + integerFormat);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/protocol/Protocol.java b/src/main/java/io/github/dsheirer/protocol/Protocol.java
index 9f2f066b..99612938 100644
--- a/src/main/java/io/github/dsheirer/protocol/Protocol.java
+++ b/src/main/java/io/github/dsheirer/protocol/Protocol.java
@@ -39,6 +39,7 @@ public enum Protocol
LRRP("LRRP", "LRRP", 0),
LTR("LTR", "LTR", 300),
LTR_NET("LTR-Net", "LTRNET", 300),
+ NBFM("NBFM", "NBFM", 0),
MDC1200("MDC-1200", "MDC1200", 1200),
MPT1327("MPT-1327", "MPT1327", 1200),
PASSPORT("Passport", "PASSPORT", 300),
@@ -59,7 +60,7 @@ public enum Protocol
}
public static EnumSet TALKGROUP_PROTOCOLS = EnumSet.of(APCO25, DMR, FLEETSYNC, LTR, LTR_NET, MDC1200,
- MPT1327, PASSPORT);
+ MPT1327, NBFM, PASSPORT);
public static EnumSet RADIO_ID_PROTOCOLS = EnumSet.of(APCO25, DMR, PASSPORT);
diff --git a/src/main/java/io/github/dsheirer/source/SourceEvent.java b/src/main/java/io/github/dsheirer/source/SourceEvent.java
index 5ed294ae..df09ce47 100644
--- a/src/main/java/io/github/dsheirer/source/SourceEvent.java
+++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java
@@ -34,6 +34,7 @@ public class SourceEvent
{
NOTIFICATION_CHANNEL_COUNT_CHANGE,
NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE,
+ NOTIFICATION_CHANNEL_POWER,
NOTIFICATION_CHANNEL_SAMPLE_RATE_CHANGE,
NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED,
NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED,
@@ -45,6 +46,7 @@ public class SourceEvent
NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED,
NOTIFICATION_RECORDING_FILE_LOADED,
NOTIFICATION_SAMPLE_RATE_CHANGE,
+ NOTIFICATION_SQUELCH_THRESHOLD,
NOTIFICATION_STOP_SAMPLE_STREAM,
NOTIFICATION_ERROR_STATE,
@@ -53,6 +55,8 @@ public class SourceEvent
REQUEST_FREQUENCY_ROTATION,
REQUEST_FREQUENCY_SELECTION,
REQUEST_SOURCE_DISPOSE,
+ REQUEST_CHANGE_SQUELCH_THRESHOLD,
+ REQUEST_CURRENT_SQUELCH_THRESHOLD,
REQUEST_START_SAMPLE_STREAM,
REQUEST_STOP_SAMPLE_STREAM;
@@ -421,6 +425,50 @@ public class SourceEvent
return new SourceEvent(Event.NOTIFICATION_FREQUENCY_ROTATION_FAILURE, source, frequency);
}
+
+ /**
+ * Creates a notification of current channel power level
+ * @param source producing the notification
+ * @param powerDb of the channel (avg)
+ * @return new source event
+ */
+ public static SourceEvent channelPowerLevel(Source source, double powerDb)
+ {
+ return new SourceEvent(Event.NOTIFICATION_CHANNEL_POWER, source, powerDb);
+ }
+
+ /**
+ * Creates a squelch threshold change notification
+ * @param source for the event
+ * @param thresholdDb setting for the power squelch
+ * @return new source event
+ */
+ public static SourceEvent squelchThreshold(Source source, double thresholdDb)
+ {
+ return new SourceEvent(Event.NOTIFICATION_SQUELCH_THRESHOLD, source, thresholdDb);
+ }
+
+ /**
+ * Requests the current squelch threshold value
+ * @param source optional
+ * @return source event
+ */
+ public static SourceEvent requestCurrentSquelchThreshold(Source source)
+ {
+ return new SourceEvent(Event.REQUEST_CURRENT_SQUELCH_THRESHOLD, source, 0);
+ }
+
+ /**
+ * Creates a new squelch threshold level be applied to the channel's power squelch control
+ * @param source of the request
+ * @param thresholdDb setting to apply
+ * @return new source event
+ */
+ public static SourceEvent requestSquelchThreshold(Source source, double thresholdDb)
+ {
+ return new SourceEvent(Event.REQUEST_CHANGE_SQUELCH_THRESHOLD, source, thresholdDb);
+ }
+
@Override
public String toString()
{
diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/ChannelSpecification.java b/src/main/java/io/github/dsheirer/source/tuner/channel/ChannelSpecification.java
index 776d6eb3..941f0c6c 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/channel/ChannelSpecification.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/channel/ChannelSpecification.java
@@ -71,4 +71,15 @@ public class ChannelSpecification
{
return mStopFrequency;
}
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Channel Specification - min sample rate: ").append(mMinimumSampleRate);
+ sb.append(" bandwidth: ").append(mBandwidth);
+ sb.append(" pass: ").append(mPassFrequency);
+ sb.append(" stop: ").append(mStopFrequency);
+ return sb.toString();
+ }
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java
index 011bedf3..ad16d3ed 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java
@@ -194,6 +194,10 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
break;
case NOTIFICATION_FREQUENCY_ROTATION_FAILURE:
case NOTIFICATION_FREQUENCY_ROTATION_SUCCESS:
+ case NOTIFICATION_CHANNEL_POWER:
+ case NOTIFICATION_SQUELCH_THRESHOLD:
+ case REQUEST_CHANGE_SQUELCH_THRESHOLD:
+ case REQUEST_CURRENT_SQUELCH_THRESHOLD:
//Ignore
break;
default:
diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
index f17b7035..0cefd028 100644
--- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
+++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java
@@ -68,7 +68,12 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
{
if(file == null || !file.exists() || !supports(file))
{
- throw new IOException("Empty or Unsupported file format");
+ throw new IOException("Empty or null file");
+ }
+
+ if(!supports(file))
+ {
+ throw new IOException("Unsupported file format");
}
mFile = file;