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