#36 NBFM squelch control. Updates NBFM decoder to incorporate squelch control. Squelch is a configuration item and also available to adjust at runtime with new 'Channel' tab in Now Playing window. All decoders were updated to produce continuous channel power (dB) measurements that are displayed in the Channel tab's new Power Meter. (#1100)

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2021-11-14 06:59:58 -05:00
committed by GitHub
parent 7303e06943
commit 4b806ecfde
40 changed files with 2348 additions and 98 deletions

View File

@@ -46,6 +46,8 @@ public enum TalkgroupFormat
"MDC-1200 valid value range is 1-65,535"),
MPT1327("###-####", 1, 0x7FFFFF, "000-0001 to 127-8192",
"<html>MPT-1327 valid ranges are 0-127(prefix)<br>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:

View File

@@ -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());
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
******************************************************************************/
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<SourceEvent>
{
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<SourceEvent> 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);
}
}
}

View File

@@ -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();
}

View File

@@ -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<SourceEvent> 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<SourceEvent> listener)
{
mSourceEventListener = listener;
}
/**
* Broadcasts the source event to an optional register listener
*/
private void broadcast(SourceEvent event)
{
if(mSourceEventListener != null)
{
mSourceEventListener.receive(event);
}
}
}

View File

@@ -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<SourceEvent>
{
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<SourceEvent> 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<SourceEvent> 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;
}
}

View File

@@ -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<Double> getScaleValues()
{
double ascent = getFontMetrics(getFont()).getMaxAscent() * 2;
double labelCount = getHeight() / ascent;
double interval = getExtent() / labelCount;
List<Double> 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();
}
}

View File

@@ -50,7 +50,7 @@ public class IntegerFormatter extends TextFormatter<Integer>
*/
public static class IntegerFilter implements UnaryOperator<Change>
{
private String DECIMAL_REGEX = "[0-9].*";
private String DECIMAL_REGEX = "\\-?[0-9].*";
private int mMinimum;
private int mMaximum;

View File

@@ -710,6 +710,9 @@ public class AliasItemEditor extends Editor<Alias>
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<Alias>
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;

View File

@@ -253,10 +253,14 @@ public class TalkgroupEditor extends IdentifierEditor<Talkgroup>
"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<Talkgroup>
"Format: 0 - FFFFFF"));
}
public class TalkgroupDetail
public static class TalkgroupDetail
{
private Protocol mProtocol;
private IntegerFormat mIntegerFormat;

View File

@@ -83,7 +83,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
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;

View File

@@ -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<Integer> 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<Toggle>()
{
@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<Integer>
{
@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);
}

View File

@@ -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":

View File

@@ -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<ProcessingChain>
{
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<SourceEvent>
{
@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);
}
});
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -342,6 +342,14 @@ public class ProcessingChain implements Listener<ChannelEvent>
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<ChannelEvent>
mSourceEventBroadcaster.addListener(listener);
}
/**
* Removes the listener from receiving source events from this processing chain.
*/
public void removeSourceEventListener(Listener<SourceEvent> listener)
{
mSourceEventBroadcaster.removeListener(listener);
}
/**
* Adds the listener to receive frequency change events from the processing chain
*

View File

@@ -69,6 +69,7 @@ import io.github.dsheirer.module.decode.mpt1327.MPT1327TrafficChannelManager;
import io.github.dsheirer.module.decode.mpt1327.Sync;
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
import io.github.dsheirer.module.decode.nbfm.NBFMDecoderState;
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
@@ -303,21 +304,19 @@ public class DecoderFactory
}
}
private static void processNBFM(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfiguration decodeConfig) {
modules.add(new NBFMDecoder(decodeConfig));
modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.NBFM, channel.getName()));
AudioModule audioModuleFM = new AudioModule(aliasList);
private static void processNBFM(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfiguration decodeConfig)
{
if(!(decodeConfig instanceof DecodeConfigNBFM))
{
throw new IllegalArgumentException("Can't create NBFM decoder - unrecognized decode config type: " +
(decodeConfig != null ? decodeConfig.getClass() : "null/empty"));
}
//Check if the user wants all audio recorded ..
if(((DecodeConfigNBFM)decodeConfig).getRecordAudio())
{
audioModuleFM.setRecordAudio(true);
}
modules.add(audioModuleFM);
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
{
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
}
DecodeConfigNBFM decodeConfigNBFM = (DecodeConfigNBFM)decodeConfig;
modules.add(new NBFMDecoder(decodeConfigNBFM));
modules.add(new NBFMDecoderState(channel.getName(), decodeConfigNBFM));
modules.add(new AudioModule(aliasList));
}
private static void processAM(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfiguration decodeConfig) {
@@ -571,7 +570,6 @@ public class DecoderFactory
case NBFM:
DecodeConfigNBFM origNBFM = (DecodeConfigNBFM)config;
DecodeConfigNBFM copyNBFM = new DecodeConfigNBFM();
copyNBFM.setRecordAudio(origNBFM.getRecordAudio());
copyNBFM.setBandwidth(origNBFM.getBandwidth());
return copyNBFM;
case P25_PHASE1:

View File

@@ -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),

View File

@@ -9,6 +9,7 @@ import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.dsp.psk.pll.FrequencyCorrectionSyncMonitor;
import io.github.dsheirer.dsp.psk.pll.PLLBandwidth;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.module.decode.DecoderType;
@@ -50,6 +51,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
protected CostasLoop mCostasLoop;
protected FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor;
protected DMRMessageFramer mMessageFramer;
private PowerMonitor mPowerMonitor = new PowerMonitor();
/**
* Constructs an instance
@@ -91,6 +93,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
getSymbolRate() + " symbol rate)");
}
mPowerMonitor.setSampleRate((int)sampleRate);
mSampleRate = sampleRate;
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
@@ -125,6 +128,9 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
//User accounting of the incoming buffer is handled by the filter
ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer);
//Process buffer for power measurements
mPowerMonitor.process(basebandFiltered);
//User accounting of the incoming buffer is handled by the gain filter
ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered);
@@ -278,6 +284,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mSourceEventListener = listener;
mPowerMonitor.setSourceEventListener(listener);
}
/**
@@ -287,6 +294,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
public void removeSourceEventListener()
{
mSourceEventListener = null;
mPowerMonitor.setSourceEventListener(null);
}
@Override

View File

@@ -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;
}
}
}

View File

@@ -17,16 +17,62 @@
******************************************************************************/
package io.github.dsheirer.module.decode.nbfm;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.IDecoderStateEventProvider;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.Window;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
import io.github.dsheirer.dsp.filter.resample.RealResampler;
import io.github.dsheirer.dsp.fm.SquelchingFMDemodulator;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.PrimaryDecoder;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IReusableBufferProvider;
import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class NBFMDecoder extends PrimaryDecoder
/**
* Decoder module with integrated narrowband FM (12.5 or 25.0 kHz channel) demodulator
*/
public class NBFMDecoder extends PrimaryDecoder implements ISourceEventListener, ISourceEventProvider,
IReusableComplexBufferListener, Listener<ReusableComplexBuffer>, IReusableBufferProvider,
IDecoderStateEventProvider
{
public NBFMDecoder( DecodeConfiguration config )
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoder.class);
private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0;
private static final double POWER_SQUELCH_ALPHA_DECAY = 0.0002;
private static final double POWER_SQUELCH_THRESHOLD_DB = -78.0;
private static final int POWER_SQUELCH_RAMP = 4;
private ComplexFIRFilter2 mIQFilter;
private SquelchingFMDemodulator mDemodulator = new SquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY,
POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP);
private RealResampler mResampler;
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
private Listener<ReusableFloatBuffer> mResampledReusableBufferListener;
private Listener<DecoderStateEvent> mDecoderStateEventListener;
private double mChannelBandwidth;
private double mOutputSampleRate = DEMODULATED_AUDIO_SAMPLE_RATE;
private boolean mSquelch = true;
/**
* Constructs an instance
* @param config to setup the decoder
*/
public NBFMDecoder( DecodeConfigNBFM config )
{
super( config );
mChannelBandwidth = config.getBandwidth().getValue();
mDemodulator.setSquelchThreshold(config.getSquelchThreshold());
}
@Override
@@ -35,9 +81,22 @@ public class NBFMDecoder extends PrimaryDecoder
return DecoderType.NBFM;
}
@Override
public Listener<ReusableComplexBuffer> getReusableComplexBufferListener()
{
return this;
}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return mSourceEventProcessor;
}
@Override
public void reset()
{
mDemodulator.reset();
}
@Override
@@ -49,4 +108,232 @@ public class NBFMDecoder extends PrimaryDecoder
public void stop()
{
}
@Override
public void setBufferListener(Listener<ReusableFloatBuffer> listener)
{
mResampledReusableBufferListener = listener;
}
@Override
public void removeBufferListener()
{
mResampledReusableBufferListener = null;
}
@Override
public void receive(ReusableComplexBuffer reusableComplexBuffer)
{
if(mIQFilter == null)
{
reusableComplexBuffer.decrementUserCount();
throw new IllegalStateException("NBFM demodulator module must receive a sample rate change source " +
"event before it can process complex sample buffers");
}
ReusableComplexBuffer basebandFilteredBuffer = mIQFilter.filter(reusableComplexBuffer);
ReusableFloatBuffer demodulatedBuffer = mDemodulator.demodulate(basebandFilteredBuffer);
if(mResampler != null)
{
//If we're currently squelched and the squelch state changed while demodulating the baseband samples,
// then un-squelch so we can send this buffer
if(mSquelch && mDemodulator.isSquelchChanged())
{
mSquelch = false;
notifyCallStart();
}
//Either send the demodulated buffer to the resampler for distro, or decrement the user count
if(mSquelch)
{
demodulatedBuffer.incrementUserCount();
notifyIdle();
}
else
{
mResampler.resample(demodulatedBuffer);
notifyCallContinuation();
}
//Set to squelch if necessary to close out the audio buffers
if(!mSquelch && mDemodulator.isMuted())
{
mSquelch = true;
notifyCallEnd();
}
}
else
{
demodulatedBuffer.decrementUserCount();
notifyIdle();
}
}
/**
* Broadcasts a call start state event
*/
private void notifyCallStart()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.START, State.CALL, 0));
}
/**
* Broadcasts a call continuation state event
*/
private void notifyCallContinuation()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.CALL, 0));
}
/**
* Broadcasts a call end state event
*/
private void notifyCallEnd()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.END, State.CALL, 0));
}
/**
* Broadcasts an idle notification
*/
private void notifyIdle()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.IDLE, 0));
}
/**
* Broadcasts the decoder state event to an optional registered listener
*/
private void broadcast(DecoderStateEvent event)
{
if(mDecoderStateEventListener != null)
{
mDecoderStateEventListener.receive(event);
}
}
/**
* Sets the decoder state listener
*/
@Override
public void setDecoderStateListener(Listener<DecoderStateEvent> listener)
{
mDecoderStateEventListener = listener;
}
/**
* Removes the decoder state event listener
*/
@Override
public void removeDecoderStateListener()
{
mDecoderStateEventListener = null;
}
/**
* Registers the listener to receive squelch change events from the demodulator/squelch controller.
*/
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mDemodulator.setSourceEventListener(listener);
}
/**
* De-registers the listener
*/
@Override
public void removeSourceEventListener()
{
mDemodulator.setSourceEventListener(null);
}
/**
* Monitors sample rate change source event(s) to setup the initial I/Q filter and passes squelch threshold
* change requests down to the demodulator.
*/
public class SourceEventProcessor implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE)
{
if(mIQFilter != null)
{
mIQFilter.dispose();
mIQFilter = null;
}
double sampleRate = sourceEvent.getValue().doubleValue();
if((sampleRate < (2.0 * mChannelBandwidth)))
{
throw new IllegalStateException("FM Demodulator with channel bandwidth [" + mChannelBandwidth +
"] requires a channel sample rate of [" + (2.0 * mChannelBandwidth + "] - sample rate of [" +
sampleRate + "] is not supported"));
}
int passBandStop = (int)(mChannelBandwidth * .8);
int stopBandStart = (int)mChannelBandwidth;
float[] filterTaps = null;
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(sampleRate)
.gridDensity(16)
.oddLength(true)
.passBandCutoff(passBandStop)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandStart(stopBandStart)
.stopBandAmplitude(0.0)
.stopBandRipple(0.005) //Approximately 90 dB attenuation
.build();
try
{
filterTaps = FilterFactory.getTaps(specification);
}
catch(FilterDesignException fde)
{
mLog.error("Couldn't design FM demodulator remez filter for sample rate [" + sampleRate +
"] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart +
"] - using sinc filter");
}
if(filterTaps == null)
{
filterTaps = FilterFactory.getLowPass(sampleRate, passBandStop, stopBandStart, 60,
Window.WindowType.HAMMING, true);
}
mIQFilter = new ComplexFIRFilter2(filterTaps);
mResampler = new RealResampler(sampleRate, mOutputSampleRate, 2000, 1000);
mResampler.setListener(reusableFloatBuffer ->
{
if(mResampledReusableBufferListener != null)
{
mResampledReusableBufferListener.receive(reusableFloatBuffer);
}
else
{
reusableFloatBuffer.decrementUserCount();
}
});
}
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
{
//Send request for squelch threshold change to the demodulator/squelch controller.
mDemodulator.receive(sourceEvent);
}
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
{
mDemodulator.receive(sourceEvent);
}
}
}
}

View File

@@ -0,0 +1,209 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nbfm;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.p25.identifier.channel.StandardChannel;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Narrow Band FM decoder channel state - provides the minimum channel state functionality
*/
public class NBFMDecoderState extends DecoderState implements ISourceEventListener
{
private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoderState.class);
private Listener<SourceEvent> mSourceEventListener = new SourceEventListener();
private String mChannelName;
private DecodeEvent mDecodeEvent;
private Identifier mChannelNameIdentifier;
private Identifier mTalkgroupIdentifier;
private IChannelDescriptor mChannelDescriptor = null;
public NBFMDecoderState(String channelName, DecodeConfigNBFM decodeConfig)
{
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : "NBFM CHANNEL";
mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.CONFIGURATION, Form.CHANNEL_NAME, Role.ANY);
mTalkgroupIdentifier = new NBFMTalkgroup(decodeConfig.getTalkgroup());
}
@Override
public void init()
{
}
@Override
public String getActivitySummary()
{
StringBuilder sb = new StringBuilder();
sb.append("Activity Summary\n");
sb.append("\tDecoder: NBFM");
sb.append("\n\n");
return sb.toString();
}
@Override
public void receive(IMessage t)
{
/* Not implemented */
}
@Override
public void receiveDecoderStateEvent(DecoderStateEvent event)
{
switch(event.getEvent())
{
case REQUEST_RESET ->
{
getIdentifierCollection().update(mChannelNameIdentifier);
}
case START ->
{
if(event.getState() == State.CALL)
{
startCallEvent();
}
}
case END ->
{
if(event.getState() == State.CALL)
{
endCallEvent();
}
}
case CONTINUATION ->
{
if(event.getState() == State.CALL)
{
continueCallEvent();
}
else
{
endCallEvent();
}
}
}
}
/**
* Creates/starts a decode call evnet.
*/
private void startCallEvent()
{
getIdentifierCollection().update(mTalkgroupIdentifier);
if(mDecodeEvent == null)
{
mDecodeEvent = DecodeEvent.builder(System.currentTimeMillis())
.channel(mChannelDescriptor)
.eventDescription("CALL")
.details("NBFM")
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
.build();
broadcast(mDecodeEvent);
}
}
/**
* Continues (or starts) the call decode event and updates the current timestamp
*/
private void continueCallEvent()
{
if(mDecodeEvent == null)
{
startCallEvent();
}
mDecodeEvent.update(System.currentTimeMillis());
broadcast(mDecodeEvent);
}
/**
* Ends the call decode event
*/
private void endCallEvent()
{
if(mDecodeEvent != null)
{
mDecodeEvent.end(System.currentTimeMillis());
broadcast(mDecodeEvent);
mDecodeEvent = null;
}
//Remote any user identifiers from the identifier collection
resetState();
}
@Override
public DecoderType getDecoderType()
{
return DecoderType.NBFM;
}
@Override
public void start()
{
getIdentifierCollection().update(mChannelNameIdentifier);
}
@Override
public void stop()
{
}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return mSourceEventListener;
}
/**
* Monitors source events to capture the channel frequency for use in decode events.
*/
private class SourceEventListener implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE)
{
mChannelDescriptor = new StandardChannel(sourceEvent.getValue().longValue());
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -21,6 +21,7 @@
*/
package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.module.decode.DecoderType;
@@ -32,9 +33,10 @@ import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener;
import io.github.dsheirer.sample.buffer.ReusableByteBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEventListener,
public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEventListener, ISourceEventProvider,
IReusableComplexBufferListener, Listener<ReusableComplexBuffer>, IReusableByteBufferProvider
{
private double mSampleRate;
@@ -43,6 +45,7 @@ public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEve
private P25P1MessageProcessor mMessageProcessor;
private Listener<SourceEvent> mSourceEventListener;
private double mSymbolRate;
protected PowerMonitor mPowerMonitor = new PowerMonitor();
public P25P1Decoder(double symbolRate)
{
@@ -52,6 +55,18 @@ public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEve
getDibitBroadcaster().addListener(mByteBufferAssembler);
}
@Override
public void setSourceEventListener(Listener<SourceEvent> listener )
{
mPowerMonitor.setSourceEventListener(listener);
}
@Override
public void removeSourceEventListener()
{
mPowerMonitor.setSourceEventListener(null);
}
/**
* Assembler for packaging Dibit stream into reusable byte buffers.
*/
@@ -111,6 +126,7 @@ public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEve
getSymbolRate() + " symbol rate)");
}
mPowerMonitor.setSampleRate((int)sampleRate);
mSampleRate = sampleRate;
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -21,6 +21,7 @@
*/
package io.github.dsheirer.module.decode.p25.phase2;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.module.decode.DecoderType;
@@ -32,12 +33,13 @@ import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener;
import io.github.dsheirer.sample.buffer.ReusableByteBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
/**
* Base P25 Phase 2 Decoder
*/
public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEventListener,
public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEventListener, ISourceEventProvider,
IReusableComplexBufferListener, Listener<ReusableComplexBuffer>, IReusableByteBufferProvider
{
private double mSampleRate;
@@ -45,6 +47,7 @@ public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEve
private DibitToByteBufferAssembler mByteBufferAssembler = new DibitToByteBufferAssembler(300);
private P25P2MessageProcessor mMessageProcessor;
private double mSymbolRate;
protected PowerMonitor mPowerMonitor = new PowerMonitor();
public P25P2Decoder(double symbolRate)
{
@@ -54,6 +57,18 @@ public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEve
getDibitBroadcaster().addListener(mByteBufferAssembler);
}
@Override
public void setSourceEventListener(Listener<SourceEvent> listener )
{
mPowerMonitor.setSourceEventListener(listener);
}
@Override
public void removeSourceEventListener()
{
mPowerMonitor.setSourceEventListener(null);
}
/**
* Assembler for packaging Dibit stream into reusable byte buffers.
*/
@@ -113,6 +128,7 @@ public abstract class P25P2Decoder extends FeedbackDecoder implements ISourceEve
getSymbolRate() + " symbol rate)");
}
mPowerMonitor.setSampleRate((int)sampleRate);
mSampleRate = sampleRate;
}

View File

@@ -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);

View File

@@ -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<ReusableComplexBuffer>, IReusableBufferProvider
public class FMDemodulatorModule extends Module implements ISourceEventListener, ISourceEventProvider,
IReusableComplexBufferListener, Listener<ReusableComplexBuffer>, 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<ReusableFloatBuffer> 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<SourceEvent> 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);

View File

@@ -545,7 +545,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
* 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)
{

View File

@@ -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;

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
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);
}
}
}

View File

@@ -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<Protocol> TALKGROUP_PROTOCOLS = EnumSet.of(APCO25, DMR, FLEETSYNC, LTR, LTR_NET, MDC1200,
MPT1327, PASSPORT);
MPT1327, NBFM, PASSPORT);
public static EnumSet<Protocol> RADIO_ID_PROTOCOLS = EnumSet.of(APCO25, DMR, PASSPORT);

View File

@@ -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()
{

View File

@@ -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();
}
}

View File

@@ -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:

View File

@@ -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;