From 4f8b8e978ba22b948946fd648c9a5032700f28bf Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Sun, 7 May 2023 07:27:44 -0400 Subject: [PATCH] #35 Updates AM & NBFM decoders to use Adaptive Squelch, a power squelch that includes an adaptive noise floor monitor. Implements vectorized magnitude operations for AM & FM decoders. Adds talkgroup assignment and squelch controls to AM channel configuration editor. Implements new AM gain control that attempts to normalize audio gain quickly at beginning of call/transmission and maintains that gain across lifecycle of the call and resets the gain for the next call. (#1540) Notes on usage: - When using either AM or FM squelch control use either the Heterodyne channelizer or if using the Polyphase channelizer, avoid using trunk tracking decoders like DMR or P25 or set the squelch threshold high enough to avoid changes in noise floor from polyphase channelizer where it can vary between using the one channel or the two-channel combined channel sources. Co-authored-by: Dennis Sheirer --- .idea/misc.xml | 2 +- .../alias/id/talkgroup/TalkgroupFormat.java | 4 + .../id/talkgroup/TalkgroupFormatter.java | 19 +- .../io/github/dsheirer/audio/AudioModule.java | 11 +- .../dsheirer/buffer/FloatCircularBuffer.java | 108 +++-- .../dsheirer/buffer/RealCircularBuffer.java | 124 ------ .../channel/state/DecoderStateEvent.java | 2 +- .../dsp/am/AmDemodulatorCalibration.java | 286 ++++++++++++ .../dsheirer/dsp/am/AmDemodulatorFactory.java | 64 +++ .../dsheirer/dsp/am/IAmDemodulator.java | 28 +- ...odulator.java => ScalarAMDemodulator.java} | 16 +- .../dsp/am/SquelchingAMDemodulator.java | 161 +++++++ .../dsp/am/VectorAMDemodulator128.java | 83 ++++ .../dsp/am/VectorAMDemodulator256.java | 83 ++++ .../dsp/am/VectorAMDemodulator512.java | 83 ++++ .../dsp/am/VectorAMDemodulator64.java | 83 ++++ .../dsp/filter/iir/SinglePoleIirFilter.java | 23 +- .../dsheirer/dsp/fm/FmDemodulatorFactory.java | 37 +- .../github/dsheirer/dsp/fm/IDemodulator.java | 34 ++ .../dsheirer/dsp/fm/IFmDemodulator.java | 14 - .../dsp/fm/ISquelchingDemodulator.java | 69 +++ .../dsp/fm/ISquelchingFmDemodulator.java | 42 -- .../dsheirer/dsp/fm/ScalarFMDemodulator.java | 82 +--- .../dsp/fm/ScalarSquelchingFMDemodulator.java | 98 ----- .../dsp/fm/SquelchingFMDemodulator.java | 161 +++++++ .../dsheirer/dsp/fm/VectorFMDemodulator.java | 77 +--- .../dsp/fm/VectorFMDemodulator128.java | 88 ++++ .../dsp/fm/VectorFMDemodulator256.java | 88 ++++ .../dsp/fm/VectorFMDemodulator512.java | 88 ++++ .../dsp/fm/VectorFMDemodulator64.java | 88 ++++ .../dsp/fm/VectorSquelchingFMDemodulator.java | 229 ---------- .../dsp/gain/AudioGainAndDcFilter.java | 139 ++++++ .../dsp/gain/AutomaticGainControl.java | 166 +++---- .../dsp/magnitude/IMagnitudeCalculator.java | 28 ++ .../dsp/magnitude/MagnitudeFactory.java | 59 +++ .../magnitude/ScalarMagnitudeCalculator.java | 39 ++ .../VectorMagnitudeCalculator128.java | 50 +++ .../VectorMagnitudeCalculator256.java | 50 +++ .../VectorMagnitudeCalculator512.java | 50 +++ .../VectorMagnitudeCalculator64.java | 50 +++ .../dsheirer/dsp/squelch/AdaptiveSquelch.java | 324 ++++++++++++++ .../dsp/squelch/ISquelchConfiguration.java | 12 + .../dsheirer/dsp/squelch/PowerSquelch.java | 34 +- .../instrument/chart/RealSampleLineChart.java | 8 +- .../gui/playlist/alias/AliasItemEditor.java | 7 +- .../alias/identifier/TalkgroupEditor.java | 11 +- .../channel/AMConfigurationEditor.java | 328 ++++++++++++-- .../channel/NBFMConfigurationEditor.java | 46 +- .../dsheirer/gui/power/ChannelPowerPanel.java | 51 ++- .../module/decode/DecoderFactory.java | 41 +- .../dsheirer/module/decode/am/AMDecoder.java | 97 +++-- .../module/decode/am/AMDecoderState.java | 79 ++++ .../module/decode/am/AMTalkgroup.java | 46 ++ .../module/decode/am/DecodeConfigAM.java | 50 ++- .../decode/analog/AnalogDecoderState.java | 177 ++++++++ .../decode/analog/DecodeConfigAnalog.java | 207 +++++++++ .../analog/SquelchingAnalogDecoder.java | 410 ++++++++++++++++++ .../module/decode/nbfm/DecodeConfigNBFM.java | 132 +----- .../module/decode/nbfm/NBFMDecoder.java | 358 +-------------- .../module/decode/nbfm/NBFMDecoderState.java | 171 +------- .../demodulate/am/AMDemodulatorModule.java | 214 --------- .../identifier/TalkgroupFormatPreference.java | 44 +- ...ter.java => AnalogTalkgroupFormatter.java} | 9 +- .../io/github/dsheirer/protocol/Protocol.java | 33 +- .../github/dsheirer/source/SourceEvent.java | 37 +- .../tuner/channel/ChannelSpecification.java | 31 +- .../tuner/channel/TunerChannelSource.java | 3 + .../source/wave/ComplexWaveSource.java | 15 +- .../dsheirer/vector/VectorUtilities.java | 20 +- .../vector/calibrate/CalibrationManager.java | 13 +- .../vector/calibrate/CalibrationType.java | 6 +- .../demodulator/FmDemodulatorCalibration.java | 186 +++++++- .../SquelchingFmDemodulatorCalibration.java | 148 ------- .../magnitude/MagnitudeCalibration.java | 288 ++++++++++++ 74 files changed, 4630 insertions(+), 2012 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java create mode 100644 src/main/java/io/github/dsheirer/dsp/am/AmDemodulatorCalibration.java create mode 100644 src/main/java/io/github/dsheirer/dsp/am/AmDemodulatorFactory.java rename src/main/java/io/github/dsheirer/dsp/am/{AMDemodulator.java => ScalarAMDemodulator.java} (82%) create mode 100644 src/main/java/io/github/dsheirer/dsp/am/SquelchingAMDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator128.java create mode 100644 src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator256.java create mode 100644 src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator512.java create mode 100644 src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator64.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fm/IDemodulator.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/fm/IFmDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fm/ISquelchingDemodulator.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/fm/ISquelchingFmDemodulator.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/fm/ScalarSquelchingFMDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fm/SquelchingFMDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator128.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator256.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator512.java create mode 100644 src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator64.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/fm/VectorSquelchingFMDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/gain/AudioGainAndDcFilter.java create mode 100644 src/main/java/io/github/dsheirer/dsp/magnitude/IMagnitudeCalculator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/magnitude/MagnitudeFactory.java create mode 100644 src/main/java/io/github/dsheirer/dsp/magnitude/ScalarMagnitudeCalculator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator128.java create mode 100644 src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator256.java create mode 100644 src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator512.java create mode 100644 src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator64.java create mode 100644 src/main/java/io/github/dsheirer/dsp/squelch/AdaptiveSquelch.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/am/AMDecoderState.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/am/AMTalkgroup.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/analog/DecodeConfigAnalog.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/analog/SquelchingAnalogDecoder.java delete mode 100644 src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java rename src/main/java/io/github/dsheirer/preference/identifier/talkgroup/{NBFMTalkgroupFormatter.java => AnalogTalkgroupFormatter.java} (94%) delete mode 100644 src/main/java/io/github/dsheirer/vector/calibrate/demodulator/SquelchingFmDemodulatorCalibration.java create mode 100644 src/main/java/io/github/dsheirer/vector/calibrate/magnitude/MagnitudeCalibration.java diff --git a/.idea/misc.xml b/.idea/misc.xml index d88165c0..7c3b4f53 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + \ No newline at end of file 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 8f0efdc4..8fb332f9 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 @@ -34,6 +34,8 @@ import io.github.dsheirer.protocol.Protocol; */ public enum TalkgroupFormat { + AM("*****", 1, 0xFFFF, "1 to 65,535", + "AM valid value range is 1-65,535"), APCO25("********", 0, 0xFFFF, "0 to 65,535", "APCO25 talkgroup valid range is 0 to 65,535"), DMR("********", 0, 0xFFFFFF, "0 to 16,777,215", @@ -122,6 +124,8 @@ public enum TalkgroupFormat switch(protocol) { + case AM: + return AM; case APCO25: return APCO25; case FLEETSYNC: 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 919239b4..f1cdd164 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 @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,7 +14,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * ***************************************************************************** + * **************************************************************************** */ package io.github.dsheirer.alias.id.talkgroup; @@ -23,21 +22,20 @@ package io.github.dsheirer.alias.id.talkgroup; import io.github.dsheirer.preference.identifier.IntegerFormat; import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter; +import io.github.dsheirer.preference.identifier.talkgroup.AnalogTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupFormatter; 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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.text.ParseException; import java.util.EnumMap; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Protocol-specific formatting of talkgroup values and parsing of formatted talkgroup values. @@ -49,6 +47,10 @@ public class TalkgroupFormatter static { + AnalogTalkgroupFormatter analogTalkgroupFormatter = new AnalogTalkgroupFormatter(); + mFormatterMap.put(Protocol.AM, analogTalkgroupFormatter); + mFormatterMap.put(Protocol.NBFM, analogTalkgroupFormatter); + mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter()); mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter()); mFormatterMap.put(Protocol.FLEETSYNC, new FleetsyncTalkgroupFormatter()); @@ -57,7 +59,6 @@ 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/audio/AudioModule.java b/src/main/java/io/github/dsheirer/audio/AudioModule.java index 2d984e68..5819e010 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AudioModule.java @@ -141,11 +141,16 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis @Override public void receive(SquelchStateEvent event) { - mSquelchState = event.getSquelchState(); + SquelchState squelchState = event.getSquelchState(); - if(mSquelchState == SquelchState.SQUELCH) + if(mSquelchState != squelchState) { - closeAudioSegment(); + mSquelchState = squelchState; + + if(mSquelchState == SquelchState.SQUELCH) + { + closeAudioSegment(); + } } } } diff --git a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java index 9313eab3..e7df429b 100644 --- a/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,21 +21,16 @@ package io.github.dsheirer.buffer; import java.util.Arrays; /** - * Circular sample buffer - allocates a buffer and stores samples in a circular - * fashion, overwriting older samples with newly arrived samples. Initially - * fills buffer with 0-valued samples - * - * Can be used as a delay-type buffer, to delay samples by the 'size' amount + * Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with + * newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value. */ public class FloatCircularBuffer { private float[] mBuffer; private int mBufferPointer = 0; - private float mOldestSample; /** * Creates a circular double buffer of the specified size and all entries filled with the specified initial value. - * * @param size of the buffer * @param initialFillValue to fill the buffer entries */ @@ -47,7 +42,6 @@ public class FloatCircularBuffer /** * Creates a circular double buffer of the specified size and all entries filled with an initial value of zero. - * * @param size of the buffer */ public FloatCircularBuffer(int size) @@ -55,33 +49,34 @@ public class FloatCircularBuffer this(size, 0.0f); } - public float[] getBuffer() + /** + * Resets the delay buffer to an all zeros state + */ + public void reset(float fillValue) { - return mBuffer; + Arrays.fill(mBuffer, fillValue); } /** - * Gets the current buffer value and overwrites that value position in the buffer with the new value. + * Puts the new value into the buffer and returns the oldest buffer value that it replaced * * @param newValue to add to the buffer * @return oldest value that was overwritten by the new value */ - public float getAndPut(float newValue) + public float get(float newValue) { - mOldestSample = mBuffer[mBufferPointer]; - put(newValue); - return mOldestSample; - } + float oldestSample = mBuffer[mBufferPointer]; - /** - * Puts the value into the buffer, updates the pointer and returns the buffer value at the pointer position. - * @param sample to add - * @return next pointed sample. - */ - public float putAndGet(float sample) - { - put(sample); - return mBuffer[mBufferPointer]; + mBuffer[mBufferPointer] = newValue; + + mBufferPointer++; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + + return oldestSample; } public float[] getAll() @@ -105,13 +100,70 @@ public class FloatCircularBuffer /** * Places the new value into the buffer, overwriting the oldest value - * * @param newValue to add to the buffer */ public void put(float newValue) { mBuffer[mBufferPointer] = newValue; + mBufferPointer++; - mBufferPointer %= mBuffer.length; + + if(mBufferPointer >= mBuffer.length) + { + mBufferPointer = 0; + } + } + + /** + * Returns the maximum from the values currently in the buffer + */ + public float max(float referenceValue) + { + float max = referenceValue; + + for(int x = 0; x < mBuffer.length; x++) + { + if(mBuffer[x] > max) + { + max = mBuffer[x]; + } + } + +// System.out.println("Max [" + max + "] from " + Arrays.toString(mBuffer)); + + return max; + } + + /** + * Returns the minimum from the values currently in the buffer + */ + public float min() + { + float min = 0.0f; + + for(int x = 0; x < mBuffer.length; x++) + { + if(mBuffer[x] < min) + { + min = mBuffer[x]; + } + } + + return min; + } + + /** + * Calculates the average/mean of the values contained in this buffer + */ + public float mean() + { + double accumulator = 0.0; + + for(float value: mBuffer) + { + accumulator += value; + } + + return (float)(accumulator / mBuffer.length); } } diff --git a/src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java b/src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java deleted file mode 100644 index 14a7a97a..00000000 --- a/src/main/java/io/github/dsheirer/buffer/RealCircularBuffer.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2022 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.buffer; - -/** - * Circular sample buffer - allocates a buffer and stores samples in a circular - * fashion, overwriting older samples with newly arrived samples. - * - * Can be used as a delay-type buffer, to delay samples by the 'size' amount - */ -public class RealCircularBuffer -{ - float[] mBuffer; - int mBufferPointer = 0; - - public RealCircularBuffer(int size) - { - mBuffer = new float[size]; - } - - public int getSize() - { - return mBuffer.length; - } - - public float[] getBuffer() - { - return mBuffer; - } - - /** - * Puts the new value into the buffer and returns the oldest buffer value - * that it replaced - * - * @param newValue - * @return - */ - public float putAndGet(float newValue) - { - float oldestSample = mBuffer[mBufferPointer]; - - put(newValue); - - return oldestSample; - } - - /** - * Adds the new value to this buffer. - */ - public void put(float newValue) - { - mBuffer[mBufferPointer] = newValue; - - mBufferPointer++; - - if(mBufferPointer >= mBuffer.length) - { - mBufferPointer = 0; - } - } - - /** - * Returns the sample at the specified index, where index 0 is the oldest sample and index (length - 1) is the - * newest sample. - * @param index in range of 0 to length-1 - * @return value at indexed position - */ - - public float get(int index) - { - if(index < mBuffer.length) - { - int pointer = mBufferPointer + index; - - if(pointer >= mBuffer.length) - { - pointer -= mBuffer.length; - } - - return mBuffer[pointer]; - } - else - { - throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" + - mBuffer.length + "]"); - } - } - - /** - * Returns the contents of the circular buffer unwrapped where index 0 is the oldest sample and index length-1 - * is the newest sample. - * - * @return array of samples - */ - public float[] get() - { - float[] samples = new float[mBuffer.length]; - - System.arraycopy(mBuffer, mBufferPointer, samples, 0, mBuffer.length - mBufferPointer); - - if(mBufferPointer != 0) - { - System.arraycopy(mBuffer, 0, samples, mBuffer.length - mBufferPointer, mBufferPointer); - } - - return samples; - } -} diff --git a/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java b/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java index e824d19d..0b47e0ef 100644 --- a/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java +++ b/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java @@ -73,7 +73,7 @@ public class DecoderStateEvent { StringBuilder sb = new StringBuilder(); - sb.append("Decoder State Event - source[").append(mSource.getClass()) + sb.append("Decoder State Event - source[").append(mSource != null ? mSource.getClass() : "null") .append("] event[").append(mEvent) .append("] state[").append(mState) .append("] timeslot[").append(mTimeslot) diff --git a/src/main/java/io/github/dsheirer/dsp/am/AmDemodulatorCalibration.java b/src/main/java/io/github/dsheirer/dsp/am/AmDemodulatorCalibration.java new file mode 100644 index 00000000..99d84af3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/am/AmDemodulatorCalibration.java @@ -0,0 +1,286 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; + +import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator; +import io.github.dsheirer.dsp.magnitude.MagnitudeFactory; +import io.github.dsheirer.vector.calibrate.Calibration; +import io.github.dsheirer.vector.calibrate.CalibrationException; +import io.github.dsheirer.vector.calibrate.CalibrationType; +import io.github.dsheirer.vector.calibrate.Implementation; +import org.apache.commons.math3.stat.descriptive.moment.Mean; + +/** + * Calibrates AM demodulator options + */ +public class AmDemodulatorCalibration extends Calibration +{ + private static final int BUFFER_SIZE = 2048; + private static final int ITERATION_DURATION_MS = 1000; + private static final int WARMUP_ITERATIONS = 5; + private static final int TEST_ITERATIONS = 5; + private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator(); + private IAmDemodulator mScalarDemodulator = new ScalarAMDemodulator(500.0f); + private IAmDemodulator mVectorDemodulator64 = new VectorAMDemodulator64(500.0f); + private IAmDemodulator mVectorDemodulator128 = new VectorAMDemodulator128(500.0f); + private IAmDemodulator mVectorAMDemodulator256 = new VectorAMDemodulator256(500.0f); + private IAmDemodulator mVectorAMDemodulator512 = new VectorAMDemodulator512(500.0f); + + + /** + * Constructs an instance + */ + public AmDemodulatorCalibration() + { + super(CalibrationType.AM_DEMODULATOR); + } + + @Override public void calibrate() throws CalibrationException + { + float[] magnitude = getFloatSamples(BUFFER_SIZE); + + Mean scalarMean = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testScalar(magnitude); + scalarMean.increment(score); + } + + mLog.info("AM DEMODULATOR WARMUP - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult())); + + Mean vector64Mean = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector64(magnitude); + vector64Mean.increment(score); + } + + mLog.info("AM DEMODULATOR WARMUP - VECTOR 64: " + DECIMAL_FORMAT.format(vector64Mean.getResult())); + + Mean vector128Mean = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector128(magnitude); + vector128Mean.increment(score); + } + + mLog.info("AM DEMODULATOR WARMUP - VECTOR 128: " + DECIMAL_FORMAT.format(vector128Mean.getResult())); + + Mean vector256Mean = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector256(magnitude); + vector256Mean.increment(score); + } + + mLog.info("AM DEMODULATOR WARMUP - VECTOR 256: " + DECIMAL_FORMAT.format(vector256Mean.getResult())); + + Mean vector512Mean = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector512(magnitude); + vector512Mean.increment(score); + } + + mLog.info("AM DEMODULATOR WARMUP - VECTOR 512: " + DECIMAL_FORMAT.format(vector512Mean.getResult())); + + //Start tests + scalarMean.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testScalar(magnitude); + scalarMean.increment(score); + } + + mLog.info("AM DEMODULATOR - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult())); + + vector64Mean.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector64(magnitude); + vector64Mean.increment(score); + } + + mLog.info("AM DEMODULATOR - VECTOR 64: " + DECIMAL_FORMAT.format(vector64Mean.getResult())); + + vector128Mean.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector128(magnitude); + vector128Mean.increment(score); + } + + mLog.info("AM DEMODULATOR - VECTOR 128: " + DECIMAL_FORMAT.format(vector128Mean.getResult())); + + vector256Mean.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector256(magnitude); + vector256Mean.increment(score); + } + + mLog.info("AM DEMODULATOR - VECTOR 256: " + DECIMAL_FORMAT.format(vector256Mean.getResult())); + + vector512Mean.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector512(magnitude); + vector512Mean.increment(score); + } + + mLog.info("AM DEMODULATOR - VECTOR 512: " + DECIMAL_FORMAT.format(vector512Mean.getResult())); + + Implementation optimal = Implementation.SCALAR; + double optimalResult = scalarMean.getResult(); + + if(vector64Mean.getResult() > optimalResult) + { + optimal = Implementation.VECTOR_SIMD_64; + optimalResult = vector64Mean.getResult(); + } + + if(vector128Mean.getResult() > optimalResult) + { + optimal = Implementation.VECTOR_SIMD_128; + optimalResult = vector128Mean.getResult(); + } + + if(vector256Mean.getResult() > optimalResult) + { + optimal = Implementation.VECTOR_SIMD_256; + optimalResult = vector256Mean.getResult(); + } + + if(vector512Mean.getResult() > optimalResult) + { + optimal = Implementation.VECTOR_SIMD_512; + } + + setImplementation(optimal); + + mLog.info("AM DEMODULATOR - SET OPTIMAL IMPLEMENTATION TO:" + getImplementation()); + } + + private long testScalar(float[] magnitude) + { + + double accumulator = 0.0; + long count = 0; + + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mScalarDemodulator.demodulateMagnitude(magnitude); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector64(float[] magnitude) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorDemodulator64.demodulateMagnitude(magnitude); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector128(float[] magnitude) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorDemodulator128.demodulateMagnitude(magnitude); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector256(float[] magnitude) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorAMDemodulator256.demodulateMagnitude(magnitude); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector512(float[] magnitude) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorAMDemodulator512.demodulateMagnitude(magnitude); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + public static void main(String[] args) + { + AmDemodulatorCalibration calibration = new AmDemodulatorCalibration(); + + try + { + calibration.calibrate(); + } + catch(Exception e) + { + mLog.error("Error during calibration", e); + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/am/AmDemodulatorFactory.java b/src/main/java/io/github/dsheirer/dsp/am/AmDemodulatorFactory.java new file mode 100644 index 00000000..a0734e79 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/am/AmDemodulatorFactory.java @@ -0,0 +1,64 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; + +import io.github.dsheirer.vector.calibrate.CalibrationManager; +import io.github.dsheirer.vector.calibrate.CalibrationType; +import io.github.dsheirer.vector.calibrate.Implementation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory for creating AM demodulators + */ +public class AmDemodulatorFactory +{ + private static final Logger mLog = LoggerFactory.getLogger(AmDemodulatorFactory.class); + + /** + * Creates the optimal AM demodulator using calibration data to select the optimal + * implementation from scalar and vector options. + * @return demodulator instance + */ + public static IAmDemodulator getAmDemodulator(float gain) + { + Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.AM_DEMODULATOR); + + switch(implementation) + { + case VECTOR_SIMD_64: + return new VectorAMDemodulator64(gain); + case VECTOR_SIMD_128: + return new VectorAMDemodulator128(gain); + case VECTOR_SIMD_256: + return new VectorAMDemodulator256(gain); + case VECTOR_SIMD_512: + return new VectorAMDemodulator512(gain); + case VECTOR_SIMD_PREFERRED: + return new VectorAMDemodulator64(gain); + case SCALAR: + case UNCALIBRATED: + return new ScalarAMDemodulator(gain); + default: + mLog.warn("Unrecognized optimal operation for AM demodulator: " + implementation.name()); + return new ScalarAMDemodulator(gain); + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/am/IAmDemodulator.java b/src/main/java/io/github/dsheirer/dsp/am/IAmDemodulator.java index a6c5bb25..baffb8a8 100644 --- a/src/main/java/io/github/dsheirer/dsp/am/IAmDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/am/IAmDemodulator.java @@ -1,3 +1,22 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; /** @@ -6,10 +25,9 @@ package io.github.dsheirer.dsp.am; public interface IAmDemodulator { /** - * Demodulate the complex I & Q sample arrays - * @param i samples - * @param q samples - * @return demodulated samples. + * Demodulates an array of vector squared magnitudes. + * @param magnitude values that are squared. + * @return demodulated samples */ - float[] demodulate(float[] i, float[] q); + float[] demodulateMagnitude(float[] magnitude); } diff --git a/src/main/java/io/github/dsheirer/dsp/am/AMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/am/ScalarAMDemodulator.java similarity index 82% rename from src/main/java/io/github/dsheirer/dsp/am/AMDemodulator.java rename to src/main/java/io/github/dsheirer/dsp/am/ScalarAMDemodulator.java index 6c5ddf0f..5df451f3 100644 --- a/src/main/java/io/github/dsheirer/dsp/am/AMDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/am/ScalarAMDemodulator.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,7 +23,7 @@ import org.apache.commons.math3.util.FastMath; /** * Performs AM demodulation on baseband I/Q samples to produce demodulated float output. */ -public class AMDemodulator implements IAmDemodulator +public class ScalarAMDemodulator implements IAmDemodulator { private float mGain; @@ -32,7 +32,7 @@ public class AMDemodulator implements IAmDemodulator * * @param gain to apply to demodulated output samples. */ - public AMDemodulator(float gain) + public ScalarAMDemodulator(float gain) { mGain = gain; } @@ -57,13 +57,13 @@ public class AMDemodulator implements IAmDemodulator return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain; } - @Override public float[] demodulate(float[] i, float[] q) + @Override + public float[] demodulateMagnitude(float[] magnitude) { - float[] demodulated = new float[i.length]; - - for(int x = 0; x < i.length; x++) + float[] demodulated = new float[magnitude.length]; + for(int x = 0; x < magnitude.length; x++) { - demodulated[x] = (float)FastMath.sqrt((i[x] * i[x]) + (q[x] * q[x])) * mGain; + demodulated[x] = (float)FastMath.sqrt(magnitude[x]) * mGain; } return demodulated; diff --git a/src/main/java/io/github/dsheirer/dsp/am/SquelchingAMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/am/SquelchingAMDemodulator.java new file mode 100644 index 00000000..6b92daf7 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/am/SquelchingAMDemodulator.java @@ -0,0 +1,161 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; + +import io.github.dsheirer.dsp.fm.ISquelchingDemodulator; +import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator; +import io.github.dsheirer.dsp.magnitude.MagnitudeFactory; +import io.github.dsheirer.dsp.squelch.AdaptiveSquelch; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.SourceEvent; + +/** + * AM demodulator with integrated squelch control. + */ +public class SquelchingAMDemodulator implements ISquelchingDemodulator, Listener +{ + private static final float ZERO = 0.0f; + private final IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator(); + private final IAmDemodulator mAmDemodulator; + private final AdaptiveSquelch mAdaptiveSquelch; + private boolean mSquelchChanged = false; + + /** + * Constructs an instance + * @param gain to apply to the demodulated AM samples (e.g. 500.0f) + * @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 squelchAutoTrack to enable the squelch noise floor auto tracking feature. + */ + public SquelchingAMDemodulator(float gain, float alpha, float squelchThreshold, boolean squelchAutoTrack) + { + mAmDemodulator = AmDemodulatorFactory.getAmDemodulator(gain); + mAdaptiveSquelch = new AdaptiveSquelch(alpha, squelchThreshold, squelchAutoTrack); + } + + /** + * Set or update the sample rate for the squelch to adjust the power level notification rate. + * @param sampleRate in hertz + */ + public void setSampleRate(int sampleRate) + { + mAdaptiveSquelch.setSampleRate(sampleRate); + } + + /** + * Registers the listener to receive notifications of squelch change events from the power squelch. + */ + public void setSourceEventListener(Listener listener) + { + mAdaptiveSquelch.setSourceEventListener(listener); + } + + /** + * Demodulates the complex sample arrays. + * @param i inphase sample array + * @param q quadrature sample array + * @return demodulated AM samples with gain applied. + */ + public float[] demodulate(float[] i, float[] q) + { + mAdaptiveSquelch.setSquelchChanged(false); + setSquelchChanged(false); + + float[] magnitude = mMagnitudeCalculator.calculate(i, q); + float[] demodulated = mAmDemodulator.demodulateMagnitude(magnitude); + + for(int x = 0; x < i.length; x++) + { + mAdaptiveSquelch.process(magnitude[x]); + + if(!(mAdaptiveSquelch.isUnmuted())) + { + demodulated[x] = ZERO; + } + + if(mAdaptiveSquelch.isSquelchChanged()) + { + setSquelchChanged(true); + } + } + + return demodulated; + } + + /** + * Sets the threshold for squelch control + * @param threshold (dB) + */ + public void setSquelchThreshold(float threshold) + { + mAdaptiveSquelch.setSquelchThreshold(threshold); + } + + @Override + public void setSquelchAutoTrack(boolean autoTrack) + { + mAdaptiveSquelch.setSquelchAutoTrack(autoTrack); + } + + /** + * 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 mAdaptiveSquelch.isMuted(); + } + + /** + * Process source events initiated by the timer and end-user. + * @param sourceEvent to process. + */ + @Override + public void receive(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + //Only forward squelch threshold & auto-track request events + case REQUEST_CURRENT_SQUELCH_THRESHOLD: + case REQUEST_CHANGE_SQUELCH_THRESHOLD: + case REQUEST_CURRENT_SQUELCH_AUTO_TRACK: + case REQUEST_CHANGE_SQUELCH_AUTO_TRACK: + { + mAdaptiveSquelch.receive(sourceEvent); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator128.java b/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator128.java new file mode 100644 index 00000000..7bd7b87d --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator128.java @@ -0,0 +1,83 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; + +import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator; +import io.github.dsheirer.dsp.magnitude.MagnitudeFactory; +import io.github.dsheirer.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorSpecies; +import org.apache.commons.math3.util.FastMath; + +/** + * Performs AM demodulation on baseband I/Q samples to produce demodulated float output. + */ +public class VectorAMDemodulator128 implements IAmDemodulator +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_128; + private float mGain; + private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator(); + + /** + * Constructs this demodulator where the specified gain is applied to demodulated output samples. + * + * @param gain to apply to demodulated output samples. + */ + public VectorAMDemodulator128(float gain) + { + mGain = gain; + } + + /** + * Sets the gain to the specified level. + */ + public void setGain(float gain) + { + mGain = gain; + } + + /** + * Demodulates the comples I/Q sample and returns the demodulated output sample. + * + * @param inphase sample + * @param quadrature sample + * @return AM demodulated sample + */ + public float demodulate(float inphase, float quadrature) + { + return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain; + } + + @Override + public float[] demodulateMagnitude(float[] magnitude) + { + VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES); + FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain); + float[] demodulated = new float[magnitude.length]; + + FloatVector vector; + for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length()) + { + vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x); + vector.sqrt().mul(gain).intoArray(demodulated, x); + } + + return demodulated; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator256.java b/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator256.java new file mode 100644 index 00000000..dc050355 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator256.java @@ -0,0 +1,83 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; + +import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator; +import io.github.dsheirer.dsp.magnitude.MagnitudeFactory; +import io.github.dsheirer.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorSpecies; +import org.apache.commons.math3.util.FastMath; + +/** + * Performs AM demodulation on baseband I/Q samples to produce demodulated float output. + */ +public class VectorAMDemodulator256 implements IAmDemodulator +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_256; + private float mGain; + private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator(); + + /** + * Constructs this demodulator where the specified gain is applied to demodulated output samples. + * + * @param gain to apply to demodulated output samples. + */ + public VectorAMDemodulator256(float gain) + { + mGain = gain; + } + + /** + * Sets the gain to the specified level. + */ + public void setGain(float gain) + { + mGain = gain; + } + + /** + * Demodulates the comples I/Q sample and returns the demodulated output sample. + * + * @param inphase sample + * @param quadrature sample + * @return AM demodulated sample + */ + public float demodulate(float inphase, float quadrature) + { + return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain; + } + + @Override + public float[] demodulateMagnitude(float[] magnitude) + { + VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES); + FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain); + float[] demodulated = new float[magnitude.length]; + + FloatVector vector; + for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length()) + { + vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x); + vector.sqrt().mul(gain).intoArray(demodulated, x); + } + + return demodulated; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator512.java b/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator512.java new file mode 100644 index 00000000..c7ed1772 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator512.java @@ -0,0 +1,83 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; + +import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator; +import io.github.dsheirer.dsp.magnitude.MagnitudeFactory; +import io.github.dsheirer.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorSpecies; +import org.apache.commons.math3.util.FastMath; + +/** + * Performs AM demodulation on baseband I/Q samples to produce demodulated float output. + */ +public class VectorAMDemodulator512 implements IAmDemodulator +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_512; + private float mGain; + private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator(); + + /** + * Constructs this demodulator where the specified gain is applied to demodulated output samples. + * + * @param gain to apply to demodulated output samples. + */ + public VectorAMDemodulator512(float gain) + { + mGain = gain; + } + + /** + * Sets the gain to the specified level. + */ + public void setGain(float gain) + { + mGain = gain; + } + + /** + * Demodulates the comples I/Q sample and returns the demodulated output sample. + * + * @param inphase sample + * @param quadrature sample + * @return AM demodulated sample + */ + public float demodulate(float inphase, float quadrature) + { + return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain; + } + + @Override + public float[] demodulateMagnitude(float[] magnitude) + { + VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES); + FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain); + float[] demodulated = new float[magnitude.length]; + + FloatVector vector; + for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length()) + { + vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x); + vector.sqrt().mul(gain).intoArray(demodulated, x); + } + + return demodulated; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator64.java b/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator64.java new file mode 100644 index 00000000..60cd8fa3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/am/VectorAMDemodulator64.java @@ -0,0 +1,83 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; + +import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator; +import io.github.dsheirer.dsp.magnitude.MagnitudeFactory; +import io.github.dsheirer.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorSpecies; +import org.apache.commons.math3.util.FastMath; + +/** + * Performs AM demodulation on baseband I/Q samples to produce demodulated float output. + */ +public class VectorAMDemodulator64 implements IAmDemodulator +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_64; + private float mGain; + private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator(); + + /** + * Constructs this demodulator where the specified gain is applied to demodulated output samples. + * + * @param gain to apply to demodulated output samples. + */ + public VectorAMDemodulator64(float gain) + { + mGain = gain; + } + + /** + * Sets the gain to the specified level. + */ + public void setGain(float gain) + { + mGain = gain; + } + + /** + * Demodulates the comples I/Q sample and returns the demodulated output sample. + * + * @param inphase sample + * @param quadrature sample + * @return AM demodulated sample + */ + public float demodulate(float inphase, float quadrature) + { + return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain; + } + + @Override + public float[] demodulateMagnitude(float[] magnitude) + { + VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES); + FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain); + float[] demodulated = new float[magnitude.length]; + + FloatVector vector; + for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length()) + { + vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x); + vector.sqrt().mul(gain).intoArray(demodulated, x); + } + + return demodulated; + } +} 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 index a7557113..23b10aef 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/iir/SinglePoleIirFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/iir/SinglePoleIirFilter.java @@ -1,3 +1,22 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.filter.iir; /** @@ -40,7 +59,7 @@ public class SinglePoleIirFilter * @param sample to process/filter * @return filtered output */ - public double filter(float sample) + public float filter(float sample) { mOutput = (mOutput * mOneMinusAlpha) + (mAlpha * sample); return mOutput; @@ -49,7 +68,7 @@ public class SinglePoleIirFilter /** * Current output value */ - public double getValue() + public float getValue() { return mOutput; } diff --git a/src/main/java/io/github/dsheirer/dsp/fm/FmDemodulatorFactory.java b/src/main/java/io/github/dsheirer/dsp/fm/FmDemodulatorFactory.java index f57780c3..33014c24 100644 --- a/src/main/java/io/github/dsheirer/dsp/fm/FmDemodulatorFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/fm/FmDemodulatorFactory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -37,40 +37,23 @@ public class FmDemodulatorFactory * implementation from scalar and vector options. * @return demodulator instance */ - public static IFmDemodulator getFmDemodulator() + public static IDemodulator getFmDemodulator() { Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.FM_DEMODULATOR); switch(implementation) { - case VECTOR_SIMD_PREFERRED: - return new VectorFMDemodulator(); + case VECTOR_SIMD_64: + return new VectorFMDemodulator64(); + case VECTOR_SIMD_128: + return new VectorFMDemodulator128(); + case VECTOR_SIMD_256: + return new VectorFMDemodulator256(); + case VECTOR_SIMD_512: + return new VectorFMDemodulator512(); case SCALAR: - return new ScalarFMDemodulator(); default: - mLog.warn("Unrecognized optimal operation for FM demodulator: " + implementation.name()); return new ScalarFMDemodulator(); } } - - /** - * Creates the optimal Squelching FM demodulator using calibration data to select the optimal - * implementation from scalar and vector options. - * @return demodulator instance - */ - public static ISquelchingFmDemodulator getSquelchingFmDemodulator(float alpha, float threshold, int ramp) - { - Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.FM_DEMODULATOR); - - switch(implementation) - { - case VECTOR_SIMD_PREFERRED: - return new VectorSquelchingFMDemodulator(alpha, threshold, ramp); - case SCALAR: - return new ScalarSquelchingFMDemodulator(alpha, threshold, ramp); - default: - mLog.warn("Unrecognized optimal operation for Squelching FM demodulator: " + implementation.name()); - return new ScalarSquelchingFMDemodulator(alpha, threshold, ramp); - } - } } diff --git a/src/main/java/io/github/dsheirer/dsp/fm/IDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/IDemodulator.java new file mode 100644 index 00000000..1889108b --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fm/IDemodulator.java @@ -0,0 +1,34 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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; + +/** + * Interface for demodulator of complex sample arrays. + */ +public interface IDemodulator +{ + /** + * Demodulate the complex sample array and return an array of real audio samples. + * @param i samples to demodulate + * @param q samples to demodulate + * @return demodulated samples. + */ + float[] demodulate(float[] i, float[] q); +} diff --git a/src/main/java/io/github/dsheirer/dsp/fm/IFmDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/IFmDemodulator.java deleted file mode 100644 index a1fec0e7..00000000 --- a/src/main/java/io/github/dsheirer/dsp/fm/IFmDemodulator.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.dsheirer.dsp.fm; - -/** - * Interface for FM demodulator - */ -public interface IFmDemodulator -{ - /** - * Demodulate the complex sample array and return an array of real audio samples. - * @param samples to demodulate - * @return demodulated samples. - */ - float[] demodulate(float[] i, float[] q); -} diff --git a/src/main/java/io/github/dsheirer/dsp/fm/ISquelchingDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/ISquelchingDemodulator.java new file mode 100644 index 00000000..6427affe --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fm/ISquelchingDemodulator.java @@ -0,0 +1,69 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.sample.Listener; +import io.github.dsheirer.source.SourceEvent; + +/** + * Interface for FM demodulator that provides squelch control. + */ +public interface ISquelchingDemodulator extends IDemodulator +{ + /** + * Sets the sample rate so that the squelch can adjust the channel power level broadcast notification rate + * @param sampleRate in hertz + */ + void setSampleRate(int sampleRate); + + /** + * Indicates if the squelch state has changed. + */ + boolean isSquelchChanged(); + + /** + * Indicates if the squelch state is currently muted + */ + boolean isMuted(); + + /** + * Sets the threshold to use for power squelch + * @param threshold in decibels + */ + void setSquelchThreshold(float threshold); + + /** + * Enables or disables the squelch auto-tracking feature where the squelch threshold is auto-adjusted based on the + * observed channel power when the signal is not present (ie squelch state). + * @param autoTrack true to enable + */ + void setSquelchAutoTrack(boolean autoTrack); + + /** + * Registers the listener to receive notifications of squelch change events from the power squelch. + */ + void setSourceEventListener(Listener listener); + + /** + * Receives a source event + */ + void receive(SourceEvent sourceEvent); +} + diff --git a/src/main/java/io/github/dsheirer/dsp/fm/ISquelchingFmDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/ISquelchingFmDemodulator.java deleted file mode 100644 index fb6c9896..00000000 --- a/src/main/java/io/github/dsheirer/dsp/fm/ISquelchingFmDemodulator.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.github.dsheirer.dsp.fm; - -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.source.SourceEvent; - -/** - * Interface for FM demodulator that provides squelch control. - */ -public interface ISquelchingFmDemodulator extends IFmDemodulator -{ - /** - * Indicates if the squelch state has changed. - */ - boolean isSquelchChanged(); - - /** - * Indicates if the squelch state is currently muted - */ - boolean isMuted(); - - /** - * Sets the threshold to use for power squelch - * @param threshold in decibels - */ - void setSquelchThreshold(double threshold); - - /** - * Resets the demodulator - */ - void reset(); - - /** - * Registers the listener to receive notifications of squelch change events from the power squelch. - */ - void setSourceEventListener(Listener listener); - - /** - * Receives a source event - */ - void receive(SourceEvent sourceEvent); -} - diff --git a/src/main/java/io/github/dsheirer/dsp/fm/ScalarFMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/ScalarFMDemodulator.java index 8491ec4e..d7efde28 100644 --- a/src/main/java/io/github/dsheirer/dsp/fm/ScalarFMDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/fm/ScalarFMDemodulator.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,16 +18,15 @@ */ package io.github.dsheirer.dsp.fm; -import io.github.dsheirer.sample.complex.Complex; import org.apache.commons.math3.util.FastMath; /** * FM Demodulator for demodulating complex samples and producing demodulated floating point samples. */ -public class ScalarFMDemodulator implements IFmDemodulator +public class ScalarFMDemodulator implements IDemodulator { - private float mPreviousI = 0.0f; - private float mPreviousQ = 0.0f; + protected float mPreviousI = 0.0f; + protected float mPreviousQ = 0.0f; protected float mGain; /** @@ -47,50 +46,6 @@ public class ScalarFMDemodulator implements IFmDemodulator mGain = gain; } - /** - * Demodulates the I/Q sample via complex sample multiplication by the complex conjugates of the most recently - * demodulated complex sample (ie previous sample). Each new sample overwrites the previously stored sample to - * allow future invocations of the method to simply use a new sample value. - * - * @param currentI of the sample - * @param currentQ of the sample - * @return demodulated sample - */ - public float demodulate(float currentI, float currentQ) - { - /** - * Multiply the current sample against the complex conjugate of the - * previous sample to derive the phase delta between the two samples - * - * Negating the previous sample quadrature produces the conjugate - */ - double inphase = (currentI * mPreviousI) - (currentQ * -mPreviousQ); - double quadrature = (currentQ * mPreviousI) + (currentI * -mPreviousQ); - - double angle = 0.0f; - - //Check for divide by zero - if(inphase != 0) - { - /** - * Use the arc-tangent of quadrature divided by inphase to - * get the phase angle (+/-) which was directly manipulated by the - * original message waveform during the modulation. This value now - * serves as the instantaneous amplitude of the demodulated signal - */ - double denominator = 1.0d / inphase; - angle = FastMath.atan((double)quadrature * denominator); - } - - /** - * Store the current sample to use during the next iteration - */ - mPreviousI = currentI; - mPreviousQ = currentQ; - - return (float)(angle * mGain); - } - public float[] demodulate(float[] i, float[] q) { float[] demodulated = new float[i.length]; @@ -135,35 +90,6 @@ public class ScalarFMDemodulator implements IFmDemodulator return demodulated; } - /** - * Demodulates the complex samples and returns the demodulated value. - * @param previous - * @param current - * @return - */ - public static double demodulate(Complex previous, Complex current) - { - double inphase = (current.inphase() * previous.inphase()) - (current.quadrature() * -previous.quadrature()); - double quadrature = (current.quadrature() * previous.inphase()) + (current.inphase() * -previous.quadrature()); - - double angle = 0.0f; - - //Check for divide by zero - if(inphase != 0) - { - /** - * Use the arc-tangent of quadrature divided by inphase to - * get the phase angle (+/-) which was directly manipulated by the - * original message waveform during the modulation. This value now - * serves as the instantaneous amplitude of the demodulated signal - */ - double denominator = 1.0d / inphase; - angle = FastMath.atan((double)quadrature * denominator); - } - - return angle; - } - public void dispose() { //no-op diff --git a/src/main/java/io/github/dsheirer/dsp/fm/ScalarSquelchingFMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/ScalarSquelchingFMDemodulator.java deleted file mode 100644 index 6845b757..00000000 --- a/src/main/java/io/github/dsheirer/dsp/fm/ScalarSquelchingFMDemodulator.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2022 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.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 ScalarSquelchingFMDemodulator extends ScalarFMDemodulator implements ISquelchingFmDemodulator, Listener -{ - private PowerSquelch mPowerSquelch; - private boolean mSquelchChanged = false; - - /** - * Creates an FM demodulator instance with a default gain of 1.0. - */ - public ScalarSquelchingFMDemodulator(float alpha, float 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); - } - - /** - * 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/fm/SquelchingFMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/SquelchingFMDemodulator.java new file mode 100644 index 00000000..eaee00de --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fm/SquelchingFMDemodulator.java @@ -0,0 +1,161 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.magnitude.IMagnitudeCalculator; +import io.github.dsheirer.dsp.magnitude.MagnitudeFactory; +import io.github.dsheirer.dsp.squelch.AdaptiveSquelch; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.SourceEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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 implements ISquelchingDemodulator, Listener +{ + private static final Logger mLog = LoggerFactory.getLogger(SquelchingFMDemodulator.class); + private static final float ZERO = 0.0f; + private final AdaptiveSquelch mAdaptiveSquelch; + private final IMagnitudeCalculator mMagnitude = MagnitudeFactory.getMagnitudeCalculator(); + private final IDemodulator mFmDemodulator = FmDemodulatorFactory.getFmDemodulator(); + private boolean mSquelchChanged = false; + + /** + * Creates an FM demodulator instance with a default gain of 1.0. + * + * @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. + * causes immediate mute and unmute. Set to higher count to prevent mute/unmute flapping. + * @param squelchAutoTrack to enable the squelch noise floor auto-tracking feature. + */ + public SquelchingFMDemodulator(float alpha, float squelchThreshold, boolean squelchAutoTrack) + { + mAdaptiveSquelch = new AdaptiveSquelch(alpha, squelchThreshold, squelchAutoTrack); + } + + /** + * Set or update the sample rate for the squelch to adjust the power level notification rate. + * @param sampleRate in hertz + */ + public void setSampleRate(int sampleRate) + { + mAdaptiveSquelch.setSampleRate(sampleRate); + } + + /** + * Registers the listener to receive notifications of squelch change events from the power squelch. + */ + public void setSourceEventListener(Listener listener) + { + mAdaptiveSquelch.setSourceEventListener(listener); + } + + /** + * Demodulates the complex (I/Q) sample arrays + * @param i inphase samples + * @param q quadrature samples + * @return demodulated real samples + */ + @Override + public float[] demodulate(float[] i, float[] q) + { + setSquelchChanged(false); + + float[] demodulated = mFmDemodulator.demodulate(i, q); + float[] magnitude = mMagnitude.calculate(i, q); + + for(int x = 0; x < magnitude.length; x++) + { + mAdaptiveSquelch.process(magnitude[x]); + + if(!(mAdaptiveSquelch.isUnmuted())) + { + demodulated[x] = ZERO; + } + + if(mAdaptiveSquelch.isSquelchChanged()) + { + setSquelchChanged(true); + } + } + + return demodulated; + } + + /** + * Sets the threshold for squelch control + * @param threshold (dB) + */ + public void setSquelchThreshold(float threshold) + { + mAdaptiveSquelch.setSquelchThreshold(threshold); + } + + @Override + public void setSquelchAutoTrack(boolean autoTrack) + { + mAdaptiveSquelch.setSquelchAutoTrack(autoTrack); + } + + /** + * 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 mAdaptiveSquelch.isMuted(); + } + + @Override + public void receive(SourceEvent sourceEvent) + { + switch(sourceEvent.getEvent()) + { + //Only forward squelch threshold and auto-track current & change requests + case REQUEST_CURRENT_SQUELCH_THRESHOLD: + case REQUEST_CHANGE_SQUELCH_THRESHOLD: + case REQUEST_CURRENT_SQUELCH_AUTO_TRACK: + case REQUEST_CHANGE_SQUELCH_AUTO_TRACK: + mAdaptiveSquelch.receive(sourceEvent); + break; + } + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator.java index 0d0a89a3..1ecb865b 100644 --- a/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,17 +26,12 @@ import jdk.incubator.vector.VectorSpecies; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.text.DecimalFormat; -import java.util.Arrays; -import java.util.Random; - /** * FM demodulator that uses JDK 17+ SIMD vector intrinsics */ -public class VectorFMDemodulator implements IFmDemodulator +public abstract class VectorFMDemodulator implements IDemodulator { private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator.class); - protected static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_PREFERRED; protected static final float ZERO = 0.0f; private static final int BUFFER_OVERLAP = 1; @@ -48,9 +43,16 @@ public class VectorFMDemodulator implements IFmDemodulator { } + /** + * Vector species as specified by the implementing sub-class. + */ + protected abstract VectorSpecies getVectorSpecies(); + @Override public float[] demodulate(float[] i, float[] q) { - VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); +// VectorSpecies species= getVectorSpecies(); + + VectorUtilities.checkComplexArrayLength(i, q, getVectorSpecies()); if(mIBuffer.length != (i.length + BUFFER_OVERLAP)) { @@ -70,12 +72,12 @@ public class VectorFMDemodulator implements IFmDemodulator FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ; - for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length()) + for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += getVectorSpecies().length()) { - previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer); - previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer); - currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1); - currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1); + previousI = FloatVector.fromArray(getVectorSpecies(), mIBuffer, bufferPointer); + previousQ = FloatVector.fromArray(getVectorSpecies(), mQBuffer, bufferPointer); + currentI = FloatVector.fromArray(getVectorSpecies(), mIBuffer, bufferPointer + 1); + currentQ = FloatVector.fromArray(getVectorSpecies(), mQBuffer, bufferPointer + 1); demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg())); demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg())); @@ -89,53 +91,4 @@ public class VectorFMDemodulator implements IFmDemodulator return demodulated; } - - public static void main(String[] args) - { - int iterations = 3_000_000; - int sampleSize = 2048; - - Random random = new Random(); - - float[] iSamples = new float[sampleSize]; - float[] qSamples = new float[sampleSize]; - for(int x = 0; x < iSamples.length; x++) - { - iSamples[x] = random.nextFloat() * 2.0f - 1.0f; - qSamples[x] = random.nextFloat() * 2.0f - 1.0f; - } - - boolean validation = false; - - ScalarFMDemodulator legacy = new ScalarFMDemodulator(); - VectorFMDemodulator vector = new VectorFMDemodulator(); - - if(validation) - { - float[] legacySamples = legacy.demodulate(iSamples, qSamples); - float[] vectorSamples = vector.demodulate(iSamples, qSamples); - System.out.println("LEGACY:" + Arrays.toString(legacySamples)); - System.out.println("VECTOR:" + Arrays.toString(vectorSamples)); - } - else - { - System.out.println("Test Starting ..."); - long start = System.currentTimeMillis(); - - double accumulator = 0.0; - - for(int i = 0; i < iterations; i++) - { - float[] samples = legacy.demodulate(iSamples, qSamples); -// float[] samples = vector.demodulate(iSamples, qSamples); - accumulator += samples[3]; - } - - double elapsed = System.currentTimeMillis() - start; - - DecimalFormat df = new DecimalFormat("0.000"); - System.out.println("Accumulator: " + accumulator); - System.out.println("Test Complete. Elapsed Time: " + df.format(elapsed / 1000.0d) + " seconds"); - } - } } diff --git a/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator128.java b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator128.java new file mode 100644 index 00000000..9ad93591 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator128.java @@ -0,0 +1,88 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FM demodulator that uses JDK 17+ SIMD vector intrinsics + */ +public class VectorFMDemodulator128 implements IDemodulator +{ + private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator128.class); + protected static final float ZERO = 0.0f; + private static final int BUFFER_OVERLAP = 1; + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_128; + + //Initialize buffers to be non-null and resize once the first buffer arrives + private float[] mIBuffer = new float[1]; + private float[] mQBuffer = new float[1]; + + public VectorFMDemodulator128() + { + } + + @Override public float[] demodulate(float[] i, float[] q) + { + VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); + + if(mIBuffer.length != (i.length + BUFFER_OVERLAP)) + { + mIBuffer = new float[i.length + BUFFER_OVERLAP]; + mQBuffer = new float[q.length + BUFFER_OVERLAP]; + } + + //Copy last sample to beginning of buffer arrays + mIBuffer[0] = mIBuffer[mIBuffer.length - 1]; + mQBuffer[0] = mQBuffer[mQBuffer.length - 1]; + + //Copy new samples to end of buffer arrays + System.arraycopy(i, 0, mIBuffer, 1, i.length); + System.arraycopy(q, 0, mQBuffer, 1, q.length); + + float[] demodulated = new float[i.length]; + + FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ; + + for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length()) + { + previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer); + previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer); + currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1); + currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1); + + demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg())); + demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg())); + + //Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors + demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO)); + + demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN); + demod.intoArray(demodulated, bufferPointer); + } + + return demodulated; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator256.java b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator256.java new file mode 100644 index 00000000..fee855ae --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator256.java @@ -0,0 +1,88 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FM demodulator that uses JDK 17+ SIMD vector intrinsics + */ +public class VectorFMDemodulator256 implements IDemodulator +{ + private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator256.class); + protected static final float ZERO = 0.0f; + private static final int BUFFER_OVERLAP = 1; + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_256; + + //Initialize buffers to be non-null and resize once the first buffer arrives + private float[] mIBuffer = new float[1]; + private float[] mQBuffer = new float[1]; + + public VectorFMDemodulator256() + { + } + + @Override public float[] demodulate(float[] i, float[] q) + { + VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); + + if(mIBuffer.length != (i.length + BUFFER_OVERLAP)) + { + mIBuffer = new float[i.length + BUFFER_OVERLAP]; + mQBuffer = new float[q.length + BUFFER_OVERLAP]; + } + + //Copy last sample to beginning of buffer arrays + mIBuffer[0] = mIBuffer[mIBuffer.length - 1]; + mQBuffer[0] = mQBuffer[mQBuffer.length - 1]; + + //Copy new samples to end of buffer arrays + System.arraycopy(i, 0, mIBuffer, 1, i.length); + System.arraycopy(q, 0, mQBuffer, 1, q.length); + + float[] demodulated = new float[i.length]; + + FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ; + + for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length()) + { + previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer); + previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer); + currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1); + currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1); + + demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg())); + demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg())); + + //Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors + demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO)); + + demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN); + demod.intoArray(demodulated, bufferPointer); + } + + return demodulated; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator512.java b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator512.java new file mode 100644 index 00000000..644a501c --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator512.java @@ -0,0 +1,88 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FM demodulator that uses JDK 17+ SIMD vector intrinsics + */ +public class VectorFMDemodulator512 implements IDemodulator +{ + private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator512.class); + protected static final float ZERO = 0.0f; + private static final int BUFFER_OVERLAP = 1; + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_512; + + //Initialize buffers to be non-null and resize once the first buffer arrives + private float[] mIBuffer = new float[1]; + private float[] mQBuffer = new float[1]; + + public VectorFMDemodulator512() + { + } + + @Override public float[] demodulate(float[] i, float[] q) + { + VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); + + if(mIBuffer.length != (i.length + BUFFER_OVERLAP)) + { + mIBuffer = new float[i.length + BUFFER_OVERLAP]; + mQBuffer = new float[q.length + BUFFER_OVERLAP]; + } + + //Copy last sample to beginning of buffer arrays + mIBuffer[0] = mIBuffer[mIBuffer.length - 1]; + mQBuffer[0] = mQBuffer[mQBuffer.length - 1]; + + //Copy new samples to end of buffer arrays + System.arraycopy(i, 0, mIBuffer, 1, i.length); + System.arraycopy(q, 0, mQBuffer, 1, q.length); + + float[] demodulated = new float[i.length]; + + FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ; + + for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length()) + { + previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer); + previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer); + currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1); + currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1); + + demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg())); + demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg())); + + //Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors + demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO)); + + demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN); + demod.intoArray(demodulated, bufferPointer); + } + + return demodulated; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator64.java b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator64.java new file mode 100644 index 00000000..20fb1ffa --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/fm/VectorFMDemodulator64.java @@ -0,0 +1,88 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorOperators; +import jdk.incubator.vector.VectorSpecies; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FM demodulator that uses JDK 17+ SIMD vector intrinsics + */ +public class VectorFMDemodulator64 implements IDemodulator +{ + private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator64.class); + protected static final float ZERO = 0.0f; + private static final int BUFFER_OVERLAP = 1; + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_64; + + //Initialize buffers to be non-null and resize once the first buffer arrives + private float[] mIBuffer = new float[1]; + private float[] mQBuffer = new float[1]; + + public VectorFMDemodulator64() + { + } + + @Override public float[] demodulate(float[] i, float[] q) + { + VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); + + if(mIBuffer.length != (i.length + BUFFER_OVERLAP)) + { + mIBuffer = new float[i.length + BUFFER_OVERLAP]; + mQBuffer = new float[q.length + BUFFER_OVERLAP]; + } + + //Copy last sample to beginning of buffer arrays + mIBuffer[0] = mIBuffer[mIBuffer.length - 1]; + mQBuffer[0] = mQBuffer[mQBuffer.length - 1]; + + //Copy new samples to end of buffer arrays + System.arraycopy(i, 0, mIBuffer, 1, i.length); + System.arraycopy(q, 0, mQBuffer, 1, q.length); + + float[] demodulated = new float[i.length]; + + FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ; + + for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length()) + { + previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer); + previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer); + currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1); + currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1); + + demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg())); + demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg())); + + //Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors + demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO)); + + demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN); + demod.intoArray(demodulated, bufferPointer); + } + + return demodulated; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/fm/VectorSquelchingFMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/fm/VectorSquelchingFMDemodulator.java deleted file mode 100644 index 915b7cbd..00000000 --- a/src/main/java/io/github/dsheirer/dsp/fm/VectorSquelchingFMDemodulator.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2022 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.source.SourceEvent; -import io.github.dsheirer.vector.VectorUtilities; -import jdk.incubator.vector.FloatVector; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.DecimalFormat; -import java.util.Arrays; -import java.util.Random; - -/** - * 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 VectorSquelchingFMDemodulator extends VectorFMDemodulator - implements ISquelchingFmDemodulator, Listener -{ - private static final Logger mLog = LoggerFactory.getLogger(VectorSquelchingFMDemodulator.class); - private static final float TWO = 2.0f; - private PowerSquelch mPowerSquelch; - private boolean mSquelchChanged = false; - - /** - * Creates an FM demodulator instance with a default gain of 1.0. - */ - public VectorSquelchingFMDemodulator(float alpha, float threshold, int ramp) - { - mPowerSquelch = new PowerSquelch(alpha, threshold, ramp); - } - - public void reset() - { - } - - /** - * 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 (I/Q) sample arrays - * @param i inphase samples - * @param q quadrature samples - * @return demodulated real samples - */ - @Override - public float[] demodulate(float[] i, float[] q) - { - setSquelchChanged(false); - - float[] demodulated = super.demodulate(i, q); - float[] magnitude = getMagnitude(i, q); - - for(int x = 0; x < i.length; x++) - { - mPowerSquelch.process(magnitude[x]); - - if(!(mPowerSquelch.isUnmuted() || mPowerSquelch.isDecay())) - { - demodulated[x] = ZERO; - } - - if(mPowerSquelch.isSquelchChanged()) - { - setSquelchChanged(true); - } - } - - return demodulated; - } - - /** - * Calculates the magnitude of the complex I/Q sample arrays using SIMD vector intrinsics. - * @param i sample array - * @param q sample array - * @return magnitude array - */ - public static float[] getMagnitude(float[] i, float[] q) - { - VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); - - float[] magnitudes = new float[i.length]; - FloatVector iVector, qVector; - - for(int samplePointer = 0; samplePointer < i.length; samplePointer += VECTOR_SPECIES.length()) - { - iVector = FloatVector.fromArray(VECTOR_SPECIES, i, samplePointer); - qVector = FloatVector.fromArray(VECTOR_SPECIES, q, samplePointer); - iVector.pow(TWO).add(qVector.pow(TWO)).intoArray(magnitudes, samplePointer); - } - - return magnitudes; - } - - /** - * 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); - } - } - - public static void main(String[] args) - { - int iterations = 3_000_000; - int sampleSize = 2048; - - Random random = new Random(); - - float[] iSamples = new float[sampleSize]; - float[] qSamples = new float[sampleSize]; - for(int x = 0; x < iSamples.length; x++) - { - iSamples[x] = random.nextFloat() * 2.0f - 1.0f; - qSamples[x] = random.nextFloat() * 2.0f - 1.0f; - } - - - float POWER_SQUELCH_ALPHA_DECAY = 0.0004f; - float POWER_SQUELCH_THRESHOLD_DB = -78.0f; - int POWER_SQUELCH_RAMP = 4; - - ScalarFMDemodulator fm = new ScalarFMDemodulator(); - VectorFMDemodulator vectorFM = new VectorFMDemodulator(); - ScalarSquelchingFMDemodulator sqLegacy = new ScalarSquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY, POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP); - VectorSquelchingFMDemodulator vectorSq = new VectorSquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY, POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP); - - boolean validation = false; - - if(validation) - { - float[] noSqFmSamples = fm.demodulate(iSamples, qSamples); - float[] vectorNoSqFmSamples = vectorFM.demodulate(iSamples, qSamples); - float[] sqFmSamples = sqLegacy.demodulate(iSamples, qSamples); - float[] vectorSqFmSamples = vectorSq.demodulate(iSamples, qSamples); - System.out.println(" NO SQ FM:" + Arrays.toString(noSqFmSamples)); - System.out.println("VECTOR NO SQ FM:" + Arrays.toString(vectorNoSqFmSamples)); - System.out.println(" SQ FM:" + Arrays.toString(sqFmSamples)); - System.out.println(" VECTOR SQ FM:" + Arrays.toString(vectorSqFmSamples)); - } - else - { - System.out.println("Test Starting ..."); - long start = System.currentTimeMillis(); - - double accumulator = 0.0; - - for(int i = 0; i < iterations; i++) - { -// float[] samples = sqLegacy.demodulate(iSamples, qSamples); - float[] samples = vectorSq.demodulate(iSamples, qSamples); - accumulator += samples[3]; - } - - double elapsed = System.currentTimeMillis() - start; - - DecimalFormat df = new DecimalFormat("0.000"); - System.out.println("Accumulator: " + accumulator); - System.out.println("Test Complete. Elapsed Time: " + df.format(elapsed / 1000.0d) + " seconds"); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/dsp/gain/AudioGainAndDcFilter.java b/src/main/java/io/github/dsheirer/dsp/gain/AudioGainAndDcFilter.java new file mode 100644 index 00000000..149f5356 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/gain/AudioGainAndDcFilter.java @@ -0,0 +1,139 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.gain; + +import org.apache.commons.math3.util.FastMath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Audio gain that normalizes audio amplitude against an objective amplitude value where the audio amplitude values + * normally fall in the range of -1.0 to 1.0. + * + * Removes DC bias on a per audio sample buffer basis. + * + * This control is designed to work on a per-call basis by monitoring the maximum observed amplitude and adjusting the + * gain to normalize the amplitude toward the objective amplitude value. This control is designed to be reset at the + * beginning/end of each call segment so that the max observed amplitude is relevant to the current call segment. + * + * Min and max gains should be sized to constrain the applied value during periods of extended silence at the + * beginning of an audio segment or where a single amplitude spike might erroneously affect gain across the segment. + */ +public class AudioGainAndDcFilter +{ + private static final Logger mLog = LoggerFactory.getLogger(AudioGainAndDcFilter.class); + private static final float MAX_AMPLITUDE = 0.95f; + private static final float GAIN_LOOP_BANDWIDTH = 0.0015f; + private float mMinGain; + private float mMaxGain; + private float mCurrentGain; + private float mObjectiveGain; + private float mObjectiveAmplitude; + private float mMaxObservedAmplitude; + + /** + * Constructs an instance + * @param minGain to apply to the incoming sample stream. + * @param maxGain to apply + * @param objectiveAmplitude to achieve by adjusting gain between min and max. + */ + public AudioGainAndDcFilter(float minGain, float maxGain, float objectiveAmplitude) + { + mMinGain = minGain; + mMaxGain = maxGain; + mObjectiveAmplitude = objectiveAmplitude; + reset(); + } + + /** + * Resets this gain control to prepare for the next audio call/segment. + */ + public void reset() + { + mMaxObservedAmplitude = 0.0f; + mObjectiveGain = 1.0f; + mCurrentGain = 1.0f; + } + + /** + * Process a buffer of audio samples and apply gain. + * @param samples to adjust. + * @return amplified audio samples + */ + public float[] process(float[] samples) + { + float currentAmplitude; + float dcAccumulator = 0.0f; + + //Decay the max observed value by 10% each buffer so that an initial spike doesn't carry across all buffers + mMaxObservedAmplitude *= 0.9f; + + for(float sample: samples) + { + dcAccumulator += sample; + currentAmplitude = FastMath.min(Math.abs(sample), 2.0f); + + if(currentAmplitude > mMaxObservedAmplitude) + { + mMaxObservedAmplitude = currentAmplitude; + } + } + + mObjectiveGain = mObjectiveAmplitude / mMaxObservedAmplitude; + + if(mObjectiveGain > mMaxGain) + { + mObjectiveGain = mMaxGain; + } + else if(mObjectiveGain < mMinGain) + { + mObjectiveGain = mMinGain; + } + + float dcOffset = dcAccumulator / samples.length; + float gain = mCurrentGain; + float objective = mObjectiveGain; + float[] processed = new float[samples.length]; + boolean objectiveAchieved = (gain == objective); + float amplified; + + for(int x = 0; x < samples.length; x++) + { + if(!objectiveAchieved) + { + gain += ((objective - gain) * GAIN_LOOP_BANDWIDTH); + if(Math.abs(objective - gain) < 0.00005f) + { + gain = objective; + objectiveAchieved = true; + } + } + + amplified = (samples[x] - dcOffset) * gain; + amplified = FastMath.min(amplified, MAX_AMPLITUDE); + amplified = FastMath.max(amplified, -MAX_AMPLITUDE); + processed[x] = amplified; + } + + mCurrentGain = gain; + mObjectiveGain = objective; + return processed; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java b/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java index ac3d81d6..caf91721 100644 --- a/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java +++ b/src/main/java/io/github/dsheirer/dsp/gain/AutomaticGainControl.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,88 +19,80 @@ package io.github.dsheirer.dsp.gain; -import io.github.dsheirer.buffer.DoubleCircularBuffer; -import io.github.dsheirer.buffer.RealCircularBuffer; +import io.github.dsheirer.buffer.FloatCircularBuffer; +import io.github.dsheirer.source.wave.RealWaveSource; import org.apache.commons.math3.util.FastMath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.File; + +/** + * Audio automatic gain control. + */ public class AutomaticGainControl { - private static final double SAMPLE_RATE = 48000; - + private static final Logger mLog = LoggerFactory.getLogger(AutomaticGainControl.class); + private static final float SAMPLE_RATE = 8000; /* Signal delay line - time delay in seconds */ -// private static final double DELAY_TIME_CONSTANT = 0.015; - private static final double DELAY_TIME_CONSTANT = 0.01; - + private static final float DELAY_TIME_CONSTANT = 0.015f; /* Peak detector window - time delay in seconds */ - private static final double WINDOW_TIME_CONSTANT = 0.018; - + private static final float WINDOW_TIME_CONSTANT = 0.018f; /* Attack time constants in seconds */ - private static final double ATTACK_RISE_TIME_CONSTANT = 0.002; - private static final double ATTACK_FALL_TIME_CONSTANT = 0.005; - - private static final double ATTACK_RISE_ALPHA = 1.0 - - FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT); - - private static final double ATTACK_FALL_ALPHA = 1.0 - - FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT); - + private static final float ATTACK_RISE_TIME_CONSTANT = 0.002f; + private static final float ATTACK_FALL_TIME_CONSTANT = 0.005f; + private static final float ATTACK_RISE_ALPHA = 1.0f - (float)FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT); + private static final float ATTACK_FALL_ALPHA = 1.0f - (float)FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT); /* AGC decay value in milliseconds (20 to 5000) */ - private static final double DECAY = 200; - - /* Ratio between rise and fall times of decay time constants - adjust for - * best action with SSB */ - private static final double DECAY_RISEFALL_RATIO = 0.3; - - private static final double DECAY_RISE_ALPHA = 1.0 - - FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO)); - - private static final double DECAY_FALL_ALPHA = 1.0 - - FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001)); - - /* Hang timer release decay time constant in seconds */ - @SuppressWarnings("unused") - private static final double RELEASE_TIME_CONSTANT = 0.05; - - /* Specifies the AGC Knee in dB if AGC is active (nominal range -160 to 0 dB) */ - private static final double THRESHOLD = -100; - - /* Limit output to about 3db of maximum */ - private static final double AGC_OUT_SCALE = 0.7; - + private static final float DECAY = 200.0f; + /* Ratio between rise and fall times of decay time constants - adjust for best action with SSB */ + private static final float DECAY_RISEFALL_RATIO = 0.3f; + private static final float DECAY_RISE_ALPHA = 1.0f - (float)FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO)); + private static final float DECAY_FALL_ALPHA = 1.f - (float)FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001)); /* Keep max input and output the same */ - private static final double MAX_AMPLITUDE = 32767.0; //1.0; - private static final double MAX_MANUAL_AMPLITUDE = 32767.0; //1.0; - + private static final float MAX_AMPLITUDE = 1.0f; //1.0; + private static final float MAX_MANUAL_AMPLITUDE = 1.0f; //1.0; /* Specifies AGC manual gain in dB if AGC is not active ( 0 to 100 dB) */ - private static final double MANUAL_GAIN = 0.0; - - private static final double MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE * - FastMath.pow(10.0, MANUAL_GAIN / 20.0); - + private static final float MANUAL_GAIN = 0.0f; + private static final float MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE * (float)FastMath.pow(10.0, MANUAL_GAIN / 20.0); + /* Limit output to about 3db of maximum */ + private static final float AGC_OUT_SCALE = 0.95f; //0.7f; /* Specifies dB reduction in output at knee from max output level (0 - 10dB) */ - private static final double SLOPE_FACTOR = 2.0; - - private static final double KNEE = THRESHOLD / 20.0; - - private static final double GAIN_SLOPE = SLOPE_FACTOR / 100.0; - - private static final double FIXED_GAIN = AGC_OUT_SCALE * - FastMath.pow(10.0, KNEE * (GAIN_SLOPE - 1.0)); - + private static final float SLOPE_FACTOR = 2.0f; + /* Specifies the threshold when the AGC kicks in (nominal range -160 to 0 dB) */ + private static final float THRESHOLD = -20.0f; //-20.0f + private static final float KNEE = THRESHOLD / 20.0f; //20.0f + private static final float GAIN_SLOPE = SLOPE_FACTOR / 100.0f; + private static final float FIXED_GAIN = AGC_OUT_SCALE * (float)FastMath.pow(10.0, KNEE * (GAIN_SLOPE - 1.0)); /* Constant for calc log() so that a value of 0 magnitude = -8 */ - private static final double MIN_CONSTANT = 3.2767E-4; - + private static final float MIN_CONSTANT = 3.2767E-4f; private boolean mAGCEnabled = true; + private float mPeakMagnitude = -5.0f; + private float mAttackAverage = 0.0f; + private float mDecayAverage = 0.0f; + private FloatCircularBuffer mDelayBuffer = new FloatCircularBuffer((int)(SAMPLE_RATE * DELAY_TIME_CONSTANT)); + private FloatCircularBuffer mMagnitudeBuffer = new FloatCircularBuffer((int)(SAMPLE_RATE * WINDOW_TIME_CONSTANT), mPeakMagnitude); + private float mMaxGain = 0f; + private float mMinGain = 100000f; - private double mPeakMagnitude = 0.0; - private double mAttackAverage = 0.0; - private double mDecayAverage = 0.0; - - private RealCircularBuffer mDelayBuffer = new RealCircularBuffer((int)(SAMPLE_RATE * DELAY_TIME_CONSTANT)); - private DoubleCircularBuffer mMagnitudeBuffer = new DoubleCircularBuffer((int)(SAMPLE_RATE * WINDOW_TIME_CONSTANT)); - + /** + * Construct an instance + */ public AutomaticGainControl() { + System.out.println("Fixed gain: " + FIXED_GAIN + " Knee: " + KNEE + " Gain Slope:" + GAIN_SLOPE); + System.out.println("Manual gain: " + MANUAL_AGC_GAIN); + } + + public void reset() + { + mLog.info("Previous - Min Gain: " + mMinGain + " Max Gain: " + mMaxGain); + mLog.info("Resetting - Peak:" + mPeakMagnitude + " Attack:" + mAttackAverage + " Decay:" + mDecayAverage + " Mag Max:" + mMagnitudeBuffer.max(mPeakMagnitude)); + mPeakMagnitude = -8.0f; + mAttackAverage = -8.0f; + mDecayAverage = -8.0f; + mDelayBuffer.reset(0.0f); + mMagnitudeBuffer.reset(mPeakMagnitude); } /** @@ -130,15 +122,14 @@ public class AutomaticGainControl */ public float process(float currentSample) { - float delayedSample = mDelayBuffer.putAndGet(currentSample); + float delayedSample = mDelayBuffer.get(currentSample); - double gain = MANUAL_AGC_GAIN; + float gain = MANUAL_AGC_GAIN; if(mAGCEnabled) { - double currentMagnitude = FastMath.log10(FastMath.abs(currentSample) + MIN_CONSTANT) - FastMath.log10(MAX_AMPLITUDE); - - double delayedMagnitude = mMagnitudeBuffer.get(currentMagnitude); + float currentMagnitude = (float)(FastMath.log10(FastMath.abs(currentSample) + MIN_CONSTANT) - FastMath.log10(MAX_AMPLITUDE)); + float delayedMagnitude = mMagnitudeBuffer.get(currentMagnitude); if(currentMagnitude > mPeakMagnitude) { @@ -148,29 +139,30 @@ public class AutomaticGainControl else if(delayedMagnitude == mPeakMagnitude) { /* If delayed magnitude is the current peak, then find a new peak */ - mPeakMagnitude = mMagnitudeBuffer.max(); + mPeakMagnitude = mMagnitudeBuffer.max(-3.4845634f); } /* Exponential decay mode */ if(mPeakMagnitude > mAttackAverage) { - mAttackAverage = ((1.0 - ATTACK_RISE_ALPHA) * mAttackAverage) + (ATTACK_RISE_ALPHA * mPeakMagnitude); + mAttackAverage = ((1.0f - ATTACK_RISE_ALPHA) * mAttackAverage) + (ATTACK_RISE_ALPHA * mPeakMagnitude); } else { - mAttackAverage = ((1.0 - ATTACK_FALL_ALPHA) * mAttackAverage) + (ATTACK_FALL_ALPHA * mPeakMagnitude); + mAttackAverage = ((1.0f - ATTACK_FALL_ALPHA) * mAttackAverage) + (ATTACK_FALL_ALPHA * mPeakMagnitude); } if(mPeakMagnitude > mDecayAverage) { - mDecayAverage = ((1.0 - DECAY_RISE_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude); + mDecayAverage = ((1.0f - DECAY_RISE_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude); } else { - mDecayAverage = ((1.0 - DECAY_FALL_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude); + mDecayAverage = ((1.0f - DECAY_FALL_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude); } - double magnitude = FastMath.max(mAttackAverage, mDecayAverage); + + float magnitude = FastMath.max(mAttackAverage, mDecayAverage); if(magnitude < KNEE) { @@ -178,11 +170,24 @@ public class AutomaticGainControl } else { - gain = AGC_OUT_SCALE * FastMath.pow(10.0, magnitude * (GAIN_SLOPE - 1.0)); + gain = AGC_OUT_SCALE * (float)FastMath.pow(10.0, magnitude * (GAIN_SLOPE - 1.0)); } + + System.out.println("Current Mag: " + currentMagnitude + " Delay Mag: " + delayedMagnitude + + " Peak:" + mPeakMagnitude + " Gain:" + gain + + " Attack Avg:" + mAttackAverage + " Decay Avg:" + mDecayAverage); } - return (float)(delayedSample * gain); + if(gain < mMinGain) + { + mMinGain = gain; + } + if(gain > mMaxGain) + { + mMaxGain = gain; + } + + return delayedSample * gain; } /** @@ -202,4 +207,5 @@ public class AutomaticGainControl { return mAGCEnabled; } + } diff --git a/src/main/java/io/github/dsheirer/dsp/magnitude/IMagnitudeCalculator.java b/src/main/java/io/github/dsheirer/dsp/magnitude/IMagnitudeCalculator.java new file mode 100644 index 00000000..8acba8c2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/magnitude/IMagnitudeCalculator.java @@ -0,0 +1,28 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.magnitude; + +/** + * Interface for implementations that calculate magnitude + */ +public interface IMagnitudeCalculator +{ + float[] calculate(float[] i, float[] q); +} diff --git a/src/main/java/io/github/dsheirer/dsp/magnitude/MagnitudeFactory.java b/src/main/java/io/github/dsheirer/dsp/magnitude/MagnitudeFactory.java new file mode 100644 index 00000000..9e43ea90 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/magnitude/MagnitudeFactory.java @@ -0,0 +1,59 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.magnitude; + +import io.github.dsheirer.vector.calibrate.CalibrationManager; +import io.github.dsheirer.vector.calibrate.CalibrationType; +import io.github.dsheirer.vector.calibrate.Implementation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory for creating magnitude implementations + */ +public class MagnitudeFactory +{ + private static final Logger mLog = LoggerFactory.getLogger(MagnitudeFactory.class); + + /** + * Creates a magnitude implementation using calibration data to select the optimal + * implementation from scalar and vector options. + * @return demodulator instance + */ + public static IMagnitudeCalculator getMagnitudeCalculator() + { + Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.MAGNITUDE); + + switch(implementation) + { + case VECTOR_SIMD_64: + return new VectorMagnitudeCalculator64(); + case VECTOR_SIMD_128: + return new VectorMagnitudeCalculator128(); + case VECTOR_SIMD_256: + return new VectorMagnitudeCalculator256(); + case VECTOR_SIMD_512: + return new VectorMagnitudeCalculator512(); + case SCALAR: + default: + return new ScalarMagnitudeCalculator(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/magnitude/ScalarMagnitudeCalculator.java b/src/main/java/io/github/dsheirer/dsp/magnitude/ScalarMagnitudeCalculator.java new file mode 100644 index 00000000..0bcad644 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/magnitude/ScalarMagnitudeCalculator.java @@ -0,0 +1,39 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.magnitude; + +/** + * Scalar implementation of magnitude + */ +public class ScalarMagnitudeCalculator implements IMagnitudeCalculator +{ + @Override + public float[] calculate(float[] i, float[] q) + { + float[] magnitude = new float[i.length]; + + for(int x = 0; x < i.length; x++) + { + magnitude[x] = ((i[x] * i[x]) + (q[x] * q[x])); + } + + return magnitude; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator128.java b/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator128.java new file mode 100644 index 00000000..45c15839 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator128.java @@ -0,0 +1,50 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.magnitude; + +import io.github.dsheirer.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorSpecies; + +/** + * Vector implementation of magnitude calculation. + */ +public class VectorMagnitudeCalculator128 implements IMagnitudeCalculator +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_128; + + @Override + public float[] calculate(float[] i, float[] q) + { + VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); + float[] magnitude = new float[i.length]; + + FloatVector iVector, qVector, result; + for(int x = 0; x < i.length; x += VECTOR_SPECIES.length()) + { + iVector = FloatVector.fromArray(VECTOR_SPECIES, i, x); + qVector = FloatVector.fromArray(VECTOR_SPECIES, q, x); + result = iVector.mul(iVector).add(qVector.mul(qVector)); + result.intoArray(magnitude, x); + } + + return magnitude; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator256.java b/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator256.java new file mode 100644 index 00000000..b3e5eed6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator256.java @@ -0,0 +1,50 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.magnitude; + +import io.github.dsheirer.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorSpecies; + +/** + * Vector implementation of magnitude calculation. + */ +public class VectorMagnitudeCalculator256 implements IMagnitudeCalculator +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_256; + + @Override + public float[] calculate(float[] i, float[] q) + { + VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); + float[] magnitude = new float[i.length]; + + FloatVector iVector, qVector, result; + for(int x = 0; x < i.length; x += VECTOR_SPECIES.length()) + { + iVector = FloatVector.fromArray(VECTOR_SPECIES, i, x); + qVector = FloatVector.fromArray(VECTOR_SPECIES, q, x); + result = iVector.mul(iVector).add(qVector.mul(qVector)); + result.intoArray(magnitude, x); + } + + return magnitude; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator512.java b/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator512.java new file mode 100644 index 00000000..91a60f76 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator512.java @@ -0,0 +1,50 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.magnitude; + +import io.github.dsheirer.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorSpecies; + +/** + * Vector implementation of magnitude calculation. + */ +public class VectorMagnitudeCalculator512 implements IMagnitudeCalculator +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_512; + + @Override + public float[] calculate(float[] i, float[] q) + { + VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); + float[] magnitude = new float[i.length]; + + FloatVector iVector, qVector, result; + for(int x = 0; x < i.length; x += VECTOR_SPECIES.length()) + { + iVector = FloatVector.fromArray(VECTOR_SPECIES, i, x); + qVector = FloatVector.fromArray(VECTOR_SPECIES, q, x); + result = iVector.mul(iVector).add(qVector.mul(qVector)); + result.intoArray(magnitude, x); + } + + return magnitude; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator64.java b/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator64.java new file mode 100644 index 00000000..3ad1c6fb --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/magnitude/VectorMagnitudeCalculator64.java @@ -0,0 +1,50 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.magnitude; + +import io.github.dsheirer.vector.VectorUtilities; +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.VectorSpecies; + +/** + * Vector implementation of magnitude calculation. + */ +public class VectorMagnitudeCalculator64 implements IMagnitudeCalculator +{ + private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_64; + + @Override + public float[] calculate(float[] i, float[] q) + { + VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES); + float[] magnitude = new float[i.length]; + + FloatVector iVector, qVector, result; + for(int x = 0; x < i.length; x += VECTOR_SPECIES.length()) + { + iVector = FloatVector.fromArray(VECTOR_SPECIES, i, x); + qVector = FloatVector.fromArray(VECTOR_SPECIES, q, x); + result = iVector.mul(iVector).add(qVector.mul(qVector)); + result.intoArray(magnitude, x); + } + + return magnitude; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/squelch/AdaptiveSquelch.java b/src/main/java/io/github/dsheirer/dsp/squelch/AdaptiveSquelch.java new file mode 100644 index 00000000..ec35e24d --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/squelch/AdaptiveSquelch.java @@ -0,0 +1,324 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.squelch; + +import io.github.dsheirer.dsp.filter.iir.SinglePoleIirFilter; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.source.SourceEvent; +import java.time.Duration; +import org.apache.commons.math3.util.FastMath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Adaptive squelch control that optionally tracks the noise floor and auto-adjusts squelch threshold. + * + * Initially modeled on: https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/simple_squelch_cc_impl.cc + */ +public class AdaptiveSquelch implements Listener +{ + private static final Logger mLog = LoggerFactory.getLogger(AdaptiveSquelch.class); + private static final long AUTO_ADJUST_PERIOD_MS = Duration.ofSeconds(5).toMillis(); + private SinglePoleIirFilter mFilter; + private float mPower = 0.0f; + private boolean mSquelch; + private float mSquelchThreshold; + private Float mNoiseFloor; + private float mSquelchBuffer; + private boolean mSquelchChanged = false; + private int mPowerLevelBroadcastCount = 0; + private int mPowerLevelBroadcastThreshold; + private boolean mAutoTrackNoiseFloor; + private long mLastAutoAdjustTimestamp; + 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 + * + * @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 autoTrack the noise floor when squelched and periodically readjust the squelch threshold. + */ + public AdaptiveSquelch(float alpha, float squelchThreshold, boolean autoTrack) + { + mFilter = new SinglePoleIirFilter(alpha); + setSquelchThreshold(squelchThreshold); + mAutoTrackNoiseFloor = autoTrack; + mLastAutoAdjustTimestamp = System.currentTimeMillis(); + mPowerLevelBroadcastThreshold = 25000; //Based on a default sample rate of 50 kHz, so 2x/second + } + + /** + * 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 float getSquelchThreshold() + { + return toDb(mSquelchThreshold); + } + + /** + * Formats the value as decibels + * @param value to format + * @return formatted value. + */ + private float toDb(float value) + { + return 10.0f * (float)FastMath.log10(value); + } + + /** + * Converts from a decibel value + * @param valueDb in decibels + * @return not decibels + */ + private float fromDb(float valueDb) + { + return (float)FastMath.pow(10.0, valueDb / 10.0); + } + + /** + * Enables or disables the squelch auto-tracking feature where the squelch threshold is auto-adjusted based on the + * observed channel power when the signal is not present (ie squelch state). + * @param autoTrack true to turn on auto-track + */ + public void setSquelchAutoTrack(boolean autoTrack) + { + mAutoTrackNoiseFloor = autoTrack; + broadcast(SourceEvent.squelchAutoTrack(getSquelchAutoTrack())); + + } + + /** + * Current squelch auto-track setting. + * @return true if auto-track is enabled. + */ + public boolean getSquelchAutoTrack() + { + return mAutoTrackNoiseFloor; + } + + /** + * Sets the squelch threshold + * @param squelchThreshold in decibels + */ + public void setSquelchThreshold(float squelchThreshold) + { + float squelchThresholdDb = fromDb(squelchThreshold); + setSquelchThreshold(squelchThresholdDb, true); + } + + /** + * Sets the squelch threshold. This method is synchronized to coordinate changes requested by the user/UI thread + * and auto-track changes made by the stream processing thread. + * + * @param squelchThreshold value + * @param resetNoiseFloor to reset the noise floor and squelch threshold + */ + private synchronized void setSquelchThreshold(float squelchThreshold, boolean resetNoiseFloor) + { + mSquelchThreshold = squelchThreshold; + + if(resetNoiseFloor) + { + //Set noise floor to null so that both the noise floor and the squelch buffer get updated after first power reading + mNoiseFloor = null; + } + + broadcast(SourceEvent.squelchThreshold(null, getSquelchThreshold())); + } + + /** + * Sets the squelch state + * @param squelch true to squelch and false to unsquelch + */ + private void setSquelch(boolean squelch) + { + mSquelch = squelch; + mSquelchChanged = true; + } + + /** + * 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(float inphase, float quadrature) + { + process(inphase * inphase + quadrature * quadrature); + } + + /** + * Processes a complex IQ sample and changes squelch state when the signal power is above or below the + * threshold value. + * @param magnitude of a complex sample (inphase * inphase + quadrature * quadrature) + */ + public void process(float magnitude) + { + mPower = mFilter.filter(magnitude); + + if(mSquelch && mPower >= mSquelchThreshold) + { + setSquelch(false); + } + else if(!mSquelch && mPower < mSquelchThreshold) + { + setSquelch(true); + } + + mPowerLevelBroadcastCount++; + if(mPowerLevelBroadcastCount % mPowerLevelBroadcastThreshold == 0) + { + mPowerLevelBroadcastCount = 0; + broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPower))); + + if(mSquelch) + { + adjustSquelchThreshold(); + } + } + } + + /** + * With the user specified squelch threshold, establishes a squelch buffer (delta) from the actual noise floor and + * then automatically updates the squelch threshold to be the current noise floor plus this threshold value. + * + * Note: this method should only be invoked from a squelch (true) state. + */ + private void adjustSquelchThreshold() + { + if(mAutoTrackNoiseFloor) + { + if(mNoiseFloor == null) + { + //We set the squelch buffer just once, after the squelch threshold is set and noise floor is set to null + mSquelchBuffer = mSquelchThreshold - mPower; + } + + mNoiseFloor = mPower; + + if((System.currentTimeMillis() - mLastAutoAdjustTimestamp) > AUTO_ADJUST_PERIOD_MS) + { + setSquelchThreshold(mNoiseFloor + mSquelchBuffer, false); + mLastAutoAdjustTimestamp = System.currentTimeMillis(); + } + } + } + + /** + * Indicates if the current state is muted + */ + public boolean isMuted() + { + return mSquelch; + } + + /** + * Indicates if the current state is unmuted + */ + public boolean isUnmuted() + { + return !mSquelch; + } + + /** + * Current power level + * @return current power level in dB + */ + public float getPower() + { + return (float)(10.0 * Math.log10(mPower)); + } + + /** + * 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) + { + switch(sourceEvent.getEvent()) + { + case REQUEST_CHANGE_SQUELCH_THRESHOLD: + setSquelchThreshold(sourceEvent.getValue().floatValue()); + break; + case REQUEST_CURRENT_SQUELCH_THRESHOLD: + broadcast(SourceEvent.squelchThreshold(null, getSquelchThreshold())); + break; + case REQUEST_CHANGE_SQUELCH_AUTO_TRACK: + setSquelchAutoTrack(sourceEvent.getValue().intValue() == 1 ? true : false); + break; + case REQUEST_CURRENT_SQUELCH_AUTO_TRACK: + broadcast(SourceEvent.squelchAutoTrack(getSquelchAutoTrack())); + break; + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/squelch/ISquelchConfiguration.java b/src/main/java/io/github/dsheirer/dsp/squelch/ISquelchConfiguration.java index 4bb72aa0..b957433d 100644 --- a/src/main/java/io/github/dsheirer/dsp/squelch/ISquelchConfiguration.java +++ b/src/main/java/io/github/dsheirer/dsp/squelch/ISquelchConfiguration.java @@ -16,4 +16,16 @@ public interface ISquelchConfiguration * @return threshold (dB) */ int getSquelchThreshold(); + + /** + * Enable or disable the squelch noise floor auto-track feature. + * @param autoTrack true to enable. + */ + void setSquelchAutoTrack(boolean autoTrack); + + /** + * Indicates if the squelch noise floor auto-track feature is enabled. + * @return true if enabled. + */ + boolean isSquelchAutoTrack(); } diff --git a/src/main/java/io/github/dsheirer/dsp/squelch/PowerSquelch.java b/src/main/java/io/github/dsheirer/dsp/squelch/PowerSquelch.java index 8a59b6f7..5bd9f445 100644 --- a/src/main/java/io/github/dsheirer/dsp/squelch/PowerSquelch.java +++ b/src/main/java/io/github/dsheirer/dsp/squelch/PowerSquelch.java @@ -1,8 +1,28 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.squelch; import io.github.dsheirer.dsp.filter.iir.SinglePoleIirFilter; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.source.SourceEvent; +import org.apache.commons.math3.util.FastMath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,8 +37,8 @@ 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 float mPower = 0.0f; + private float mSquelchThreshold; private int mRampThreshold; private int mRampCount; private boolean mSquelchChanged = false; @@ -42,7 +62,7 @@ public class PowerSquelch implements Listener * @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(float alpha, double squelchThreshold, int ramp) + public PowerSquelch(float alpha, float squelchThreshold, int ramp) { mFilter = new SinglePoleIirFilter(alpha); setSquelchThreshold(squelchThreshold); @@ -64,9 +84,9 @@ public class PowerSquelch implements Listener * Squelch threshold value * @return value in decibels */ - public double getSquelchThreshold() + public float getSquelchThreshold() { - return 10.0 * Math.log10(mSquelchThreshold); + return 10.0f * (float)FastMath.log10(mSquelchThreshold); } /** @@ -75,7 +95,7 @@ public class PowerSquelch implements Listener */ public void setSquelchThreshold(double squelchThreshold) { - mSquelchThreshold = Math.pow(10.0, squelchThreshold / 10.0); + mSquelchThreshold = (float) FastMath.pow(10.0, squelchThreshold / 10.0); broadcast(SourceEvent.squelchThreshold(null, squelchThreshold)); } @@ -268,7 +288,7 @@ public class PowerSquelch implements Listener { if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD) { - setSquelchThreshold(sourceEvent.getValue().doubleValue()); + setSquelchThreshold(sourceEvent.getValue().floatValue()); } else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD) { diff --git a/src/main/java/io/github/dsheirer/gui/instrument/chart/RealSampleLineChart.java b/src/main/java/io/github/dsheirer/gui/instrument/chart/RealSampleLineChart.java index 41ea1522..1f3c2002 100644 --- a/src/main/java/io/github/dsheirer/gui/instrument/chart/RealSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/gui/instrument/chart/RealSampleLineChart.java @@ -18,7 +18,7 @@ */ package io.github.dsheirer.gui.instrument.chart; -import io.github.dsheirer.buffer.RealCircularBuffer; +import io.github.dsheirer.buffer.FloatCircularBuffer; import io.github.dsheirer.sample.Listener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -34,7 +34,7 @@ public class RealSampleLineChart extends LineChart implements Listener private final static Logger mLog = LoggerFactory.getLogger(RealSampleLineChart.class); private ObservableList> mSamples = FXCollections.observableArrayList(); private Series mSampleSeries = new Series<>("Samples", mSamples); - private RealCircularBuffer mRealCircularBuffer; + private FloatCircularBuffer mRealCircularBuffer; public RealSampleLineChart(int length, double tickUnit) { @@ -50,7 +50,7 @@ public class RealSampleLineChart extends LineChart implements Listener private void init(int length) { - mRealCircularBuffer = new RealCircularBuffer(length); + mRealCircularBuffer = new FloatCircularBuffer(length); for(int x = 0; x < length; x++) { @@ -74,7 +74,7 @@ public class RealSampleLineChart extends LineChart implements Listener mRealCircularBuffer.put(sample); } - float[] bufferSamples = mRealCircularBuffer.get(); + float[] bufferSamples = mRealCircularBuffer.getAll(); for(int x = 0; x < bufferSamples.length; x++) { 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 00933b14..0ac8dca3 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 @@ -677,6 +677,10 @@ public class AliasItemEditor extends Editor mAddIdentifierButton.setMaxWidth(Double.MAX_VALUE); mAddIdentifierButton.setDisable(true); + Menu amMenu = new ProtocolMenu(Protocol.AM); + amMenu.getItems().add(new AddTalkgroupItem(Protocol.AM)); + amMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.AM)); + Menu p25Menu = new ProtocolMenu(Protocol.APCO25); p25Menu.getItems().add(new AddTalkgroupItem(Protocol.APCO25)); p25Menu.getItems().add(new AddTalkgroupRangeItem(Protocol.APCO25)); @@ -712,6 +716,7 @@ public class AliasItemEditor extends Editor Menu nbfmMenu = new ProtocolMenu(Protocol.NBFM); nbfmMenu.getItems().add(new AddTalkgroupItem(Protocol.NBFM)); + nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM)); Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT); passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT)); @@ -725,7 +730,7 @@ public class AliasItemEditor extends Editor Menu lojackMenu = new ProtocolMenu(Protocol.LOJACK); lojackMenu.getItems().add(new AddLojackItem()); - mAddIdentifierButton.getItems().addAll(p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu, + mAddIdentifierButton.getItems().addAll(amMenu, p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu, nbfmMenu, passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu); } 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 150912a3..efdc4b6f 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,6 +27,8 @@ import io.github.dsheirer.gui.control.PrefixIdentFormatter; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.identifier.IntegerFormat; import io.github.dsheirer.protocol.Protocol; +import java.util.ArrayList; +import java.util.List; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.HPos; @@ -38,9 +40,6 @@ import javafx.scene.layout.GridPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; - /** * Editor for talkgroup alias identifiers */ @@ -238,6 +237,10 @@ public class TalkgroupEditor extends IdentifierEditor private void loadTalkgroupDetails() { mTalkgroupDetails.clear(); + mTalkgroupDetails.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF), + "Format: 1 - 65535")); + mTalkgroupDetails.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF), + "Format: 1 - FFFF")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535), "Format: 0 - 65535")); mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535), diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/AMConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/AMConfigurationEditor.java index 98b09b38..93a4e65b 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/AMConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/AMConfigurationEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,25 +19,44 @@ 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.eventlog.EventLogConfigurationEditor; import io.github.dsheirer.gui.playlist.source.FrequencyEditor; import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.am.DecodeConfigAM; import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration; import io.github.dsheirer.module.decode.config.DecodeConfiguration; +import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM; +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 io.github.dsheirer.source.tuner.manager.TunerManager; +import java.util.ArrayList; +import java.util.List; +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; import org.controlsfx.control.ToggleSwitch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,12 +67,23 @@ import org.slf4j.LoggerFactory; public class AMConfigurationEditor extends ChannelConfigurationEditor { private final static Logger mLog = LoggerFactory.getLogger(AMConfigurationEditor.class); + private TitledPane mSourcePane; private TitledPane mDecoderPane; private TitledPane mRecordPane; - private TitledPane mSourcePane; - private ToggleSwitch mAudioRecordSwitch; + private TitledPane mEventLogPane; private ToggleSwitch mBasebandRecordSwitch; private SourceConfigurationEditor mSourceConfigurationEditor; + private SegmentedButton mBandwidthButton; + private TextField mTalkgroupField; + private TextField mSquelchThresholdField; + private ToggleSwitch mSquelchAutoTrackSwitch; + private TextFormatter mTalkgroupTextFormatter; + private IntegerFormatter mSquelchTextFormatter = new IntegerFormatter((int)DbPowerMeter.DEFAULT_MINIMUM_POWER, + (int)DbPowerMeter.DEFAULT_MAXIMUM_POWER); + 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 @@ -67,6 +97,7 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor super(playlistManager, tunerManager, userPreferences); getTitledPanesBox().getChildren().add(getSourcePane()); getTitledPanesBox().getChildren().add(getDecoderPane()); + getTitledPanesBox().getChildren().add(getEventLogPane()); getTitledPanesBox().getChildren().add(getRecordPane()); } @@ -93,13 +124,75 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor { mDecoderPane = new TitledPane(); mDecoderPane.setText("Decoder: AM"); - mDecoderPane.setExpanded(false); - mDecoderPane.setDisable(true); + mDecoderPane.setExpanded(true); + + GridPane gridPane = new GridPane(); + gridPane.setPadding(new Insets(10,10,10,10)); + gridPane.setHgap(10); + gridPane.setVgap(10); + + int row = 0; + + Label bandwidthLabel = new Label("Channel Bandwidth"); + GridPane.setHalignment(bandwidthLabel, HPos.RIGHT); + GridPane.setConstraints(bandwidthLabel, 0, row); + gridPane.getChildren().add(bandwidthLabel); + + GridPane.setConstraints(getBandwidthButton(), 1, row++, 3, 1); + gridPane.getChildren().add(getBandwidthButton()); + + Label squelchLabel = new Label("Squelch Threshold"); + GridPane.setHalignment(squelchLabel, HPos.RIGHT); + GridPane.setConstraints(squelchLabel, 0, row); + gridPane.getChildren().add(squelchLabel); + + GridPane.setConstraints(getSquelchThresholdField(), 1, row); + gridPane.getChildren().add(getSquelchThresholdField()); + + Label autoTrackLabel = new Label("Squelch Auto-Track"); + GridPane.setHalignment(autoTrackLabel, HPos.RIGHT); + GridPane.setConstraints(autoTrackLabel, 2, row); + gridPane.getChildren().add(autoTrackLabel); + + GridPane.setConstraints(getSquelchAutoTrackSwitch(), 3, row); + GridPane.setHalignment(getSquelchAutoTrackSwitch(), HPos.LEFT); + gridPane.getChildren().add(getSquelchAutoTrackSwitch()); + + Label talkgroupLabel = new Label("Talkgroup To Assign"); + GridPane.setHalignment(talkgroupLabel, HPos.RIGHT); + GridPane.setConstraints(talkgroupLabel, 4, row); + gridPane.getChildren().add(talkgroupLabel); + + GridPane.setConstraints(getTalkgroupField(), 5, row); + 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, + //so detect when the pane is expanded and refresh the config view + mDecoderPane.expandedProperty().addListener((observable, oldValue, newValue) -> { + if(newValue) + { + //Reset the config so the editor gets updated + setDecoderConfiguration(getItem().getDecodeConfiguration()); + } + }); } return mDecoderPane; } + private TitledPane getEventLogPane() + { + if(mEventLogPane == null) + { + mEventLogPane = new TitledPane("Logging", getEventLogConfigurationEditor()); + mEventLogPane.setExpanded(false); + } + + return mEventLogPane; + } + private TitledPane getRecordPane() { if(mRecordPane == null) @@ -113,14 +206,6 @@ public class AMConfigurationEditor 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.getChildren().add(getBasebandRecordSwitch()); @@ -149,17 +234,20 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor return mSourceConfigurationEditor; } - private ToggleSwitch getAudioRecordSwitch() + private EventLogConfigurationEditor getEventLogConfigurationEditor() { - if(mAudioRecordSwitch == null) + if(mEventLogConfigurationEditor == null) { - mAudioRecordSwitch = new ToggleSwitch(); - mAudioRecordSwitch.setDisable(true); - mAudioRecordSwitch.setTextAlignment(TextAlignment.RIGHT); - mAudioRecordSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + List types = new ArrayList<>(); + types.add(EventLogType.CALL_EVENT); + types.add(EventLogType.DECODED_MESSAGE); + + mEventLogConfigurationEditor = new EventLogConfigurationEditor(types); + mEventLogConfigurationEditor.setPadding(new Insets(5,5,5,5)); + mEventLogConfigurationEditor.modifiedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); } - return mAudioRecordSwitch; + return mEventLogConfigurationEditor; } private ToggleSwitch getBasebandRecordSwitch() @@ -176,19 +264,179 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor return mBasebandRecordSwitch; } - @Override - protected void setDecoderConfiguration(DecodeConfiguration config) + private SegmentedButton getBandwidthButton() { - if(config instanceof DecodeConfigAM) + if(mBandwidthButton == null) { - DecodeConfigAM decodeConfig = (DecodeConfigAM)config; - getAudioRecordSwitch().setDisable(false); - getAudioRecordSwitch().selectedProperty().set(decodeConfig.getRecordAudio()); + mBandwidthButton = new SegmentedButton(); + mBandwidthButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK); + mBandwidthButton.setDisable(true); + + for(DecodeConfigNBFM.Bandwidth bandwidth : DecodeConfigNBFM.Bandwidth.AM_BANDWIDTHS) + { + ToggleButton toggleButton = new ToggleButton(bandwidth.toString()); + toggleButton.setUserData(bandwidth); + mBandwidthButton.getButtons().add(toggleButton); + } + + 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 DecodeConfigAM config) + { + //Capture current modified state so that we can reapply after adjusting control states + boolean modified = modifiedProperty().get(); + + DecodeConfigAM.Bandwidth bandwidth = config.getBandwidth(); + if(bandwidth == null) + { + bandwidth = DecodeConfigNBFM.Bandwidth.BW_15_0; + } + + for(Toggle toggle: getBandwidthButton().getToggleGroup().getToggles()) + { + toggle.setSelected(toggle.getUserData() == bandwidth); + } + + modifiedProperty().set(modified); + } + } + }); + } + + return mBandwidthButton; + } + + private TextField getTalkgroupField() + { + if(mTalkgroupField == null) + { + mTalkgroupField = new TextField(); + mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter); + } + + return mTalkgroupField; + } + + private TextField getSquelchThresholdField() + { + if(mSquelchThresholdField == null) + { + mSquelchThresholdField = new TextField(); + mSquelchThresholdField.setTextFormatter(mSquelchTextFormatter); + mSquelchTextFormatter.valueProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mSquelchThresholdField; + } + + /** + * Squelch noise floor auto-track feature. + */ + private ToggleSwitch getSquelchAutoTrackSwitch() + { + if(mSquelchAutoTrackSwitch == null) + { + mSquelchAutoTrackSwitch = new ToggleSwitch(); + mSquelchAutoTrackSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mSquelchAutoTrackSwitch; + } + + /** + * 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.AM); + + if(format == null) + { + format = IntegerFormat.DECIMAL; + } + + if(format == IntegerFormat.DECIMAL) + { + mTalkgroupTextFormatter = mDecimalFormatter; + getTalkgroupField().setTooltip(new Tooltip("1 - 65,535")); } else { - getAudioRecordSwitch().setDisable(true); - getAudioRecordSwitch().selectedProperty().set(false); + 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); + } + } + + @Override + protected void setDecoderConfiguration(DecodeConfiguration config) + { + if(config instanceof DecodeConfigAM decodeConfigAM) + { + getBandwidthButton().setDisable(false); + final DecodeConfigAM.Bandwidth bandwidth = (decodeConfigAM.getBandwidth() != null ? + decodeConfigAM.getBandwidth() : DecodeConfigNBFM.Bandwidth.BW_15_0); + + for(Toggle toggle: getBandwidthButton().getToggleGroup().getToggles()) + { + toggle.setSelected(toggle.getUserData() == bandwidth); + } + + updateTextFormatter(decodeConfigAM.getTalkgroup()); + getSquelchThresholdField().setDisable(false); + mSquelchTextFormatter.setValue(decodeConfigAM.getSquelchThreshold()); + getSquelchAutoTrackSwitch().setDisable(false); + getSquelchAutoTrackSwitch().setSelected(decodeConfigAM.isSquelchAutoTrack()); + + } + else + { + getBandwidthButton().setDisable(true); + + for(Toggle toggle: getBandwidthButton().getToggleGroup().getToggles()) + { + toggle.setSelected(false); + } + + updateTextFormatter(0); + getTalkgroupField().setDisable(true); + getSquelchThresholdField().setDisable(true); + getSquelchAutoTrackSwitch().setDisable(true); + getSquelchAutoTrackSwitch().setSelected(false); } } @@ -197,16 +445,34 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor { DecodeConfigAM config; - if(getItem().getDecodeConfiguration() instanceof DecodeConfigAM) + if(getItem().getDecodeConfiguration() instanceof DecodeConfigAM decodeConfigAM) { - config = (DecodeConfigAM)getItem().getDecodeConfiguration(); + config = decodeConfigAM; } else { config = new DecodeConfigAM(); } - config.setRecordAudio(getAudioRecordSwitch().isSelected()); + DecodeConfigAM.Bandwidth bandwidth = DecodeConfigAM.Bandwidth.BW_15_0; + + if(getBandwidthButton().getToggleGroup().getSelectedToggle() != null) + { + bandwidth = (DecodeConfigAM.Bandwidth)getBandwidthButton().getToggleGroup().getSelectedToggle().getUserData(); + } + + config.setBandwidth(bandwidth); + + Integer talkgroup = mTalkgroupTextFormatter.getValue(); + + if(talkgroup == null) + { + talkgroup = 1; + } + + config.setTalkgroup(talkgroup); + config.setSquelchThreshold(mSquelchTextFormatter.getValue()); + config.setSquelchAutoTrack(getSquelchAutoTrackSwitch().isSelected()); getItem().setDecodeConfiguration(config); } 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 4dfe1dae..4b70ad21 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,6 +40,8 @@ import io.github.dsheirer.record.RecorderType; import io.github.dsheirer.record.config.RecordConfiguration; import io.github.dsheirer.source.config.SourceConfiguration; import io.github.dsheirer.source.tuner.manager.TunerManager; +import java.util.ArrayList; +import java.util.List; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; @@ -59,9 +61,6 @@ import org.controlsfx.control.ToggleSwitch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; - /** * Narrow-Band FM channel configuration editor */ @@ -75,6 +74,7 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor private TitledPane mSourcePane; private TextField mTalkgroupField; private TextField mSquelchThresholdField; + private ToggleSwitch mSquelchAutoTrackSwitch; private TextFormatter mTalkgroupTextFormatter; private IntegerFormatter mSquelchTextFormatter = new IntegerFormatter((int)DbPowerMeter.DEFAULT_MINIMUM_POWER, (int)DbPowerMeter.DEFAULT_MAXIMUM_POWER); @@ -151,12 +151,21 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor GridPane.setConstraints(getSquelchThresholdField(), 3, 0); gridPane.getChildren().add(getSquelchThresholdField()); + Label autoTrackLabel = new Label("Squelch Auto-Track"); + GridPane.setHalignment(autoTrackLabel, HPos.RIGHT); + GridPane.setConstraints(autoTrackLabel, 4, 0); + gridPane.getChildren().add(autoTrackLabel); + + GridPane.setConstraints(getSquelchAutoTrackSwitch(), 5, 0); + GridPane.setHalignment(getSquelchAutoTrackSwitch(), HPos.LEFT); + gridPane.getChildren().add(getSquelchAutoTrackSwitch()); + Label talkgroupLabel = new Label("Talkgroup To Assign"); GridPane.setHalignment(talkgroupLabel, HPos.RIGHT); - GridPane.setConstraints(talkgroupLabel, 4, 0); + GridPane.setConstraints(talkgroupLabel, 6, 0); gridPane.getChildren().add(talkgroupLabel); - GridPane.setConstraints(getTalkgroupField(), 5, 0); + GridPane.setConstraints(getTalkgroupField(), 7, 0); gridPane.getChildren().add(getTalkgroupField()); mDecoderPane.setContent(gridPane); @@ -274,7 +283,7 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor mBandwidthButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK); mBandwidthButton.setDisable(true); - for(DecodeConfigNBFM.Bandwidth bandwidth : DecodeConfigNBFM.Bandwidth.values()) + for(DecodeConfigNBFM.Bandwidth bandwidth : DecodeConfigNBFM.Bandwidth.FM_BANDWIDTHS) { ToggleButton toggleButton = new ToggleButton(bandwidth.toString()); toggleButton.setUserData(bandwidth); @@ -338,11 +347,26 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor { mSquelchThresholdField = new TextField(); mSquelchThresholdField.setTextFormatter(mSquelchTextFormatter); + mSquelchTextFormatter.valueProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); } return mSquelchThresholdField; } + /** + * Squelch noise floor auto-track feature. + */ + private ToggleSwitch getSquelchAutoTrackSwitch() + { + if(mSquelchAutoTrackSwitch == null) + { + mSquelchAutoTrackSwitch = new ToggleSwitch(); + mSquelchAutoTrackSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true)); + } + + return mSquelchAutoTrackSwitch; + } + /** * Updates the talkgroup editor's text formatter. * @param value to set in the control. @@ -421,8 +445,10 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor } updateTextFormatter(decodeConfigNBFM.getTalkgroup()); - + getSquelchThresholdField().setDisable(false); mSquelchTextFormatter.setValue(decodeConfigNBFM.getSquelchThreshold()); + getSquelchAutoTrackSwitch().setDisable(false); + getSquelchAutoTrackSwitch().setSelected(decodeConfigNBFM.isSquelchAutoTrack()); } else { @@ -435,6 +461,9 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor updateTextFormatter(0); getTalkgroupField().setDisable(true); + getSquelchThresholdField().setDisable(true); + getSquelchAutoTrackSwitch().setDisable(true); + getSquelchAutoTrackSwitch().setSelected(false); } } @@ -470,6 +499,7 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor config.setTalkgroup(talkgroup); config.setSquelchThreshold(mSquelchTextFormatter.getValue()); + config.setSquelchAutoTrack(getSquelchAutoTrackSwitch().isSelected()); getItem().setDecodeConfiguration(config); } diff --git a/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java b/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java index 8de8071d..d9abbeb2 100644 --- a/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java +++ b/src/main/java/io/github/dsheirer/gui/power/ChannelPowerPanel.java @@ -55,6 +55,7 @@ import org.slf4j.LoggerFactory; import javax.swing.Icon; import javax.swing.JButton; +import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JPanel; @@ -94,6 +95,7 @@ public class ChannelPowerPanel extends JPanel implements Listener broadcast(SourceEvent.requestSquelchThreshold(null, mSquelchThreshold + 1))); valuePanel.add(mSquelchUpButton); Icon iconDown = IconFontSwing.buildIcon(FontAwesome.ANGLE_DOWN, 12); mSquelchDownButton = new JButton(iconDown); + mSquelchDownButton.setToolTipText("Decreases the squelch threshold value."); mSquelchDownButton.setEnabled(false); mSquelchDownButton.addActionListener(e -> broadcast(SourceEvent.requestSquelchThreshold(null, mSquelchThreshold - 1))); - valuePanel.add(mSquelchDownButton); + valuePanel.add(mSquelchDownButton, "wrap"); + + mSquelchAutoTrackCheckBox = new JCheckBox("Auto Track"); + mSquelchAutoTrackCheckBox.setToolTipText("Enable or disable monitoring of the noise floor to auto-adjust the " + + "squelch threshold value maintaining a consistent level/buffer above the noise floor"); + mSquelchAutoTrackCheckBox.setEnabled(false); + mSquelchAutoTrackCheckBox.addActionListener(e -> + { + broadcast(SourceEvent.requestSquelchAutoTrack(mSquelchAutoTrackCheckBox.isSelected())); + }); + valuePanel.add(mSquelchAutoTrackCheckBox, "span,left"); add(valuePanel); add(new JSeparator(JSeparator.VERTICAL)); @@ -165,7 +182,7 @@ public class ChannelPowerPanel extends JPanel implements Listener { @@ -340,6 +357,22 @@ public class ChannelPowerPanel extends JPanel implements Listener + { + boolean autoTrack = sourceEvent.getValue().intValue() == 1 ? true : false; + setConfigSquelchAutoTrack(autoTrack); + EventQueue.invokeLater(() -> { + mSquelchAutoTrackCheckBox.setSelected(autoTrack); + mSquelchAutoTrackCheckBox.setEnabled(true); + }); + } } } } 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 27c47a91..9669a535 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,7 +23,6 @@ import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.alias.action.AliasActionManager; import io.github.dsheirer.audio.AbstractAudioModule; import io.github.dsheirer.audio.AudioModule; -import io.github.dsheirer.channel.state.AlwaysUnsquelchedDecoderState; import io.github.dsheirer.channel.state.State; import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.controller.channel.Channel.ChannelType; @@ -36,6 +35,7 @@ import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.MessageDirection; import io.github.dsheirer.module.Module; import io.github.dsheirer.module.decode.am.AMDecoder; +import io.github.dsheirer.module.decode.am.AMDecoderState; import io.github.dsheirer.module.decode.am.DecodeConfigAM; import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration; import io.github.dsheirer.module.decode.config.DecodeConfiguration; @@ -88,23 +88,20 @@ import io.github.dsheirer.module.decode.passport.PassportMessageFilter; import io.github.dsheirer.module.decode.tait.Tait1200Decoder; import io.github.dsheirer.module.decode.tait.Tait1200DecoderState; import io.github.dsheirer.module.decode.traffic.TrafficChannelManager; -import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule; import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.source.SourceType; import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; import io.github.dsheirer.source.tuner.channel.rotation.ChannelRotationMonitor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.ArrayList; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class DecoderFactory { private final static Logger mLog = LoggerFactory.getLogger(DecoderFactory.class); - private static final double AM_CHANNEL_BANDWIDTH = 3000.0; private static final double FM_CHANNEL_BANDWIDTH = 12500.0; private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0; @@ -313,29 +310,23 @@ public class DecoderFactory } 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) { - modules.add(new AMDecoder(decodeConfig)); - modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.AM, channel.getName())); - AudioModule audioModuleAM = new AudioModule(aliasList); - modules.add(audioModuleAM); - - //Check if the user wants all audio recorded .. - if(((DecodeConfigAM)decodeConfig).getRecordAudio()) + if(!(decodeConfig instanceof DecodeConfigAM)) { - audioModuleAM.setRecordAudio(true); + throw new IllegalArgumentException("Can't create AM decoder - unrecognized decode config type: " + + (decodeConfig != null ? decodeConfig.getClass() : "null/empty")); } - if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER) - { - modules.add(new AMDemodulatorModule(AM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE)); - } + DecodeConfigAM decodeConfigAM = (DecodeConfigAM) decodeConfig; + modules.add(new AMDecoder(decodeConfigAM)); + modules.add(new AMDecoderState(channel.getName(), decodeConfigAM)); + modules.add(new AudioModule(aliasList)); } /** @@ -506,8 +497,6 @@ public class DecoderFactory public static DecodeConfiguration getDecodeConfiguration(DecoderType decoder) { - DecodeConfiguration retVal; - switch(decoder) { case AM: @@ -545,7 +534,10 @@ public class DecoderFactory case AM: DecodeConfigAM copyAM = new DecodeConfigAM(); DecodeConfigAM origAM = (DecodeConfigAM)config; - copyAM.setRecordAudio(origAM.getRecordAudio()); + copyAM.setBandwidth(origAM.getBandwidth()); + copyAM.setTalkgroup(origAM.getTalkgroup()); + copyAM.setSquelchThreshold(origAM.getSquelchThreshold()); + copyAM.setSquelchAutoTrack(origAM.isSquelchAutoTrack()); return copyAM; case DMR: return new DecodeConfigDMR(); @@ -571,6 +563,9 @@ public class DecoderFactory DecodeConfigNBFM origNBFM = (DecodeConfigNBFM)config; DecodeConfigNBFM copyNBFM = new DecodeConfigNBFM(); copyNBFM.setBandwidth(origNBFM.getBandwidth()); + copyNBFM.setSquelchThreshold(origNBFM.getSquelchThreshold()); + copyNBFM.setSquelchAutoTrack(origNBFM.isSquelchAutoTrack()); + copyNBFM.setTalkgroup(origNBFM.getTalkgroup()); return copyNBFM; case P25_PHASE1: DecodeConfigP25Phase1 originalP25 = (DecodeConfigP25Phase1)config; diff --git a/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java index 73f9a824..c86e5d2a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoder.java @@ -1,52 +1,75 @@ -/******************************************************************************* - * 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 - ******************************************************************************/ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; +import io.github.dsheirer.dsp.am.SquelchingAMDemodulator; +import io.github.dsheirer.dsp.gain.AudioGainAndDcFilter; 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.module.decode.analog.SquelchingAnalogDecoder; -public class AMDecoder extends PrimaryDecoder +/** + * Analog AM radio decoder module with integrated squelching control + */ +public class AMDecoder extends SquelchingAnalogDecoder { - public AMDecoder( DecodeConfiguration config ) - { - super( config ); - } + private static final float DEMODULATOR_GAIN = 250.0f; + private static final float SQUELCH_ALPHA_DECAY = 0.0004f; + private static final float MINIMUM_GAIN = 0.5f; + private static final float MAXIMUM_GAIN = 16.0f; + private static final float OBJECTIVE_AUDIO_AMPLITUDE = 0.75f; + private AudioGainAndDcFilter mAGC = new AudioGainAndDcFilter(MINIMUM_GAIN, MAXIMUM_GAIN, OBJECTIVE_AUDIO_AMPLITUDE); - @Override - public DecoderType getDecoderType() + /** + * Constructs an instance + * @param config for the AM channel. + */ + public AMDecoder(DecodeConfigAM config) { - return DecoderType.AM; + super(config, new SquelchingAMDemodulator(DEMODULATOR_GAIN, SQUELCH_ALPHA_DECAY, + config.getSquelchThreshold(), config.isSquelchAutoTrack())); } - @Override - public void reset() - { - } - @Override - public void start() + public DecoderType getDecoderType() { + return DecoderType.AM; } - @Override - public void stop() - { - } + /** + * Overrides the broadcast of the final demodulated audio samples so that we can apply AGC + * @param demodulatedSamples to broadcast + */ + @Override + protected void broadcast(float[] demodulatedSamples) + { + //Apply audio gain and rebroadcast + super.broadcast(mAGC.process(demodulatedSamples)); + } + + /** + * Overrides the notify call start so that we can reset the AGC + */ + @Override + protected void notifyCallStart() + { + mAGC.reset(); + super.notifyCallStart(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/am/AMDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoderState.java new file mode 100644 index 00000000..39f49c0f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/am/AMDecoderState.java @@ -0,0 +1,79 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; + +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.string.SimpleStringIdentifier; +import io.github.dsheirer.module.decode.DecoderType; +import io.github.dsheirer.module.decode.analog.AnalogDecoderState; + +/** + * AM decoder state + */ +public class AMDecoderState extends AnalogDecoderState +{ + private String mChannelName; + private Identifier mChannelNameIdentifier; + private Identifier mTalkgroupIdentifier; + + /** + * Constructs an instance + * @param channelName to use for this channel + * @param decodeConfig with talkgroup identifier + */ + public AMDecoderState(String channelName, DecodeConfigAM decodeConfig) + { + mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : "AM CHANNEL"; + mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.CONFIGURATION, Form.CHANNEL_NAME, Role.ANY); + mTalkgroupIdentifier = new AMTalkgroup(decodeConfig.getTalkgroup()); + } + + @Override + public DecoderType getDecoderType() + { + return DecoderType.AM; + } + + @Override + protected Identifier getChannelNameIdentifier() + { + return mChannelNameIdentifier; + } + + @Override + protected Identifier getTalkgroupIdentifier() + { + return mTalkgroupIdentifier; + } + + @Override + public String getActivitySummary() + { + StringBuilder sb = new StringBuilder(); + sb.append("Activity Summary\n"); + sb.append("\tDecoder: AM"); + sb.append("\n\n"); + return sb.toString(); + } + +} diff --git a/src/main/java/io/github/dsheirer/module/decode/am/AMTalkgroup.java b/src/main/java/io/github/dsheirer/module/decode/am/AMTalkgroup.java new file mode 100644 index 00000000..3bfe05c3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/am/AMTalkgroup.java @@ -0,0 +1,46 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.am; + +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; +import io.github.dsheirer.protocol.Protocol; + +/** + * AM talkgroup. Note: this is a logical identifier that is assignable from a user-specified + * configuration so that AM audio events are compatible with other features of the sdrtrunk system. + */ +public class AMTalkgroup extends TalkgroupIdentifier +{ + /** + * Constructs an instance + * @param value 1-65,535 + */ + public AMTalkgroup(int value) + { + super(value, Role.TO); + } + + @Override + public Protocol getProtocol() + { + return Protocol.AM; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/am/DecodeConfigAM.java b/src/main/java/io/github/dsheirer/module/decode/am/DecodeConfigAM.java index ca9cc4ce..bfbe2633 100644 --- a/src/main/java/io/github/dsheirer/module/decode/am/DecodeConfigAM.java +++ b/src/main/java/io/github/dsheirer/module/decode/am/DecodeConfigAM.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,33 +14,35 @@ * * 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.am; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.module.decode.config.DecodeConfiguration; +import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog; import io.github.dsheirer.source.tuner.channel.ChannelSpecification; -public class DecodeConfigAM extends DecodeConfiguration +/** + * AM decoder configuration + */ +public class DecodeConfigAM extends DecodeConfigAnalog { - private boolean mRecordAudio = false; - public DecodeConfigAM() { } - @JacksonXmlProperty(isAttribute = true, localName = "recordAudio") - public boolean getRecordAudio() + @JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance") + public DecoderType getDecoderType() { - return mRecordAudio; + return DecoderType.AM; } - public void setRecordAudio(boolean recordAudio) + @Override + protected Bandwidth getDefaultBandwidth() { - mRecordAudio = recordAudio; + return Bandwidth.BW_15_0; } /** @@ -51,13 +52,20 @@ public class DecodeConfigAM extends DecodeConfiguration @Override public ChannelSpecification getChannelSpecification() { - return new ChannelSpecification(25000.0, - 3000, 3000.0, 5000.0); - } - - @JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance") - public DecoderType getDecoderType() - { - return DecoderType.AM; + switch(getBandwidth()) + { + case BW_3_0: + return new ChannelSpecification(25000.0, 3000, 1500.0, 1700.0); + case BW_5_0: + return new ChannelSpecification(25000.0, 5000, 2500.0, 2700.0); + case BW_8_33: + return new ChannelSpecification(25000.0, 10000, 5000.0, 7000.0); + case BW_15_0: + return new ChannelSpecification(25000.0, 15000, 7500.0, 9500.0); + case BW_25_0: + return new ChannelSpecification(25000.0, 25000, 12500.0, 14500.0); + default: + throw new IllegalArgumentException("Unrecognized AM bandwidth value: " + getBandwidth()); + } } } diff --git a/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java new file mode 100644 index 00000000..dcfb966a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java @@ -0,0 +1,177 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.analog; + +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.Identifier; +import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.message.IMessage; +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; + +/** + * Abstract analog decoder channel state - provides the minimum channel state functionality + */ +public abstract class AnalogDecoderState extends DecoderState implements ISourceEventListener +{ + private final static Logger mLog = LoggerFactory.getLogger(AnalogDecoderState.class); + private Listener mSourceEventListener = new SourceEventListener(); + private DecodeEvent mDecodeEvent; + private IChannelDescriptor mChannelDescriptor = null; + + public AnalogDecoderState() + { + } + + /** + * Channel name identifier provided by the subclass implementation. + */ + protected abstract Identifier getChannelNameIdentifier(); + + protected abstract Identifier getTalkgroupIdentifier(); + + @Override + public void receiveDecoderStateEvent(DecoderStateEvent event) + { + switch(event.getEvent()) + { + case REQUEST_RESET -> + { + getIdentifierCollection().update(getChannelNameIdentifier()); + } + 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(getTalkgroupIdentifier()); + + if(mDecodeEvent == null) + { + mDecodeEvent = DecodeEvent.builder(System.currentTimeMillis()) + .channel(mChannelDescriptor) + .eventDescription("CALL") + .details(getDecoderType().name()) + .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; + } + + //Remove any user identifiers from the identifier collection + resetState(); + } + + @Override + public void start() + { + getIdentifierCollection().update(getChannelNameIdentifier()); + } + + @Override + public void stop() {} + @Override + public void init() {} + @Override + public void receive(IMessage t) {} + + @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/analog/DecodeConfigAnalog.java b/src/main/java/io/github/dsheirer/module/decode/analog/DecodeConfigAnalog.java new file mode 100644 index 00000000..31559ac1 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/analog/DecodeConfigAnalog.java @@ -0,0 +1,207 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.analog; + +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.config.DecodeConfiguration; +import io.github.dsheirer.source.tuner.channel.ChannelSpecification; +import java.util.EnumSet; + +/** + * Decoder configuration for an squelching analog channel + */ +public abstract class DecodeConfigAnalog extends DecodeConfiguration implements ISquelchConfiguration +{ + private Bandwidth mBandwidth; + private int mTalkgroup = 1; + private int mSquelchThreshold = -78; + private boolean mSquelchAutoTrack = true; + + /** + * Constructs an instance + */ + public DecodeConfigAnalog() + { + mBandwidth = getDefaultBandwidth(); + } + + protected abstract Bandwidth getDefaultBandwidth(); + + /** + * Channel bandwidth + */ + @JacksonXmlProperty(isAttribute = true, localName = "bandwidth") + public Bandwidth getBandwidth() + { + return mBandwidth; + } + + @JsonIgnore + @Override + public ChannelSpecification getChannelSpecification() + { + if(mBandwidth == Bandwidth.BW_12_5) + { + return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0); + } + else + { + return new ChannelSpecification(50000.0, 25000, 12500.0, 13500.0); + } + } + + /** + * Sets the channel bandwidth + */ + public void setBandwidth(Bandwidth bandwidth) + { + mBandwidth = bandwidth; + } + + /** + * Talkgroup to associate with audio produced by the NBFM decoder + */ + @JacksonXmlProperty(isAttribute = true, localName = "talkgroup") + public int getTalkgroup() + { + return mTalkgroup; + } + + /** + * Sets the talkgroup identifier to attach to any demodulated audio streams + * @param talkgroup (1-65,535) + */ + public void setTalkgroup(int talkgroup) + { + if(talkgroup < 1 || talkgroup > 65535) + { + throw new IllegalArgumentException("Valid talkgroup range is 1 - 65,535"); + } + + mTalkgroup = talkgroup; + } + + /** + * Sets squelch threshold + * @param threshold (dB) + */ + @JacksonXmlProperty(isAttribute = true, localName = "squelch") + @Override + public void setSquelchThreshold(int threshold) + { + mSquelchThreshold = threshold; + } + + /** + * Squelch threshold + * @return threshold (dB) + */ + @Override + public int getSquelchThreshold() + { + return mSquelchThreshold; + } + + /** + * Enable or disable the squelch noise floor auto-track feature. + * @param autoTrack true to enable. + */ + @JacksonXmlProperty(isAttribute = true, localName = "autoTrack") + @Override + public void setSquelchAutoTrack(boolean autoTrack) + { + mSquelchAutoTrack = autoTrack; + } + + /** + * Indicates if the squelch noise floor auto-track feature is enabled. + * @return true if enabled. + */ + @Override + public boolean isSquelchAutoTrack() + { + return mSquelchAutoTrack; + } + + public enum Bandwidth + { + BW_3_0("3.0 kHz", 3000.0), + BW_5_0("5.0 kHz", 5000.0), + BW_8_33("8.33 kHz", 8333.0), + BW_12_5("12.5 kHz", 12500.0), + BW_15_0("15.0 kHz", 15000.0), + BW_25_0("25.0 kHz", 25000.0); + + private String mLabel; + private double mValue; + + /** + * Constructs an instance + * @param label for display + * @param value of the entry in Hertz + */ + Bandwidth(String label, double value) + { + mLabel = label; + mValue = value; + } + + //AM demodulator channel bandwidth options + public static EnumSet AM_BANDWIDTHS = EnumSet.of(BW_3_0, BW_5_0, BW_8_33, BW_15_0, BW_25_0); + + //FM demodulator channel bandwidth options + public static EnumSet FM_BANDWIDTHS = EnumSet.of(BW_12_5, BW_25_0); + + /** + * Indicates if this entry is valid for the AM decoder. + */ + public boolean isAM() + { + return AM_BANDWIDTHS.contains(this); + } + + /** + * Indicates if this entry is valid for the FM decoder. + */ + public boolean isFM() + { + return FM_BANDWIDTHS.contains(this); + } + + /** + * Overrides the default toString to provide a pretty label. + */ + @Override + public String toString() + { + return mLabel; + } + + /** + * Value of the bandwidth + * @return value in Hertz + */ + public double getValue() + { + return mValue; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/analog/SquelchingAnalogDecoder.java b/src/main/java/io/github/dsheirer/module/decode/analog/SquelchingAnalogDecoder.java new file mode 100644 index 00000000..889e739f --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/analog/SquelchingAnalogDecoder.java @@ -0,0 +1,410 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.analog; + +import io.github.dsheirer.channel.state.DecoderStateEvent; +import io.github.dsheirer.channel.state.IDecoderStateEventProvider; +import io.github.dsheirer.channel.state.State; +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory; +import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter; +import io.github.dsheirer.dsp.filter.design.FilterDesignException; +import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; +import io.github.dsheirer.dsp.filter.fir.real.IRealFilter; +import io.github.dsheirer.dsp.filter.resample.RealResampler; +import io.github.dsheirer.dsp.fm.ISquelchingDemodulator; +import io.github.dsheirer.dsp.window.WindowType; +import io.github.dsheirer.module.decode.PrimaryDecoder; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexSamples; +import io.github.dsheirer.sample.complex.IComplexSamplesListener; +import io.github.dsheirer.sample.real.IRealBufferProvider; +import io.github.dsheirer.source.ISourceEventListener; +import io.github.dsheirer.source.ISourceEventProvider; +import io.github.dsheirer.source.SourceEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract analog decoder module with integrated squelch control + */ +public abstract class SquelchingAnalogDecoder extends PrimaryDecoder implements ISourceEventListener, ISourceEventProvider, + IComplexSamplesListener, Listener, IRealBufferProvider, + IDecoderStateEventProvider +{ + private final static Logger mLog = LoggerFactory.getLogger(SquelchingAnalogDecoder.class); + private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0; + private IRealFilter mIBasebandFilter; + private IRealFilter mQBasebandFilter; + private IRealDecimationFilter mIDecimationFilter; + private IRealDecimationFilter mQDecimationFilter; + private ISquelchingDemodulator mDemodulator; + private RealResampler mResampler; + private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor(); + private Listener mResampledBufferListener; + private Listener mDecoderStateEventListener; + private double mChannelBandwidth; + private double mOutputSampleRate = DEMODULATED_AUDIO_SAMPLE_RATE; + protected boolean mSquelch = true; + + /** + * Constructs an instance + * + * @param config to setup the decoder + * @param squelchingDemodulator implementation (e.g. AM or FM). + */ + public SquelchingAnalogDecoder(DecodeConfigAnalog config, ISquelchingDemodulator squelchingDemodulator) + { + super(config); + mDemodulator = squelchingDemodulator; + mChannelBandwidth = config.getBandwidth().getValue(); + mDemodulator.setSquelchThreshold(config.getSquelchThreshold()); + } + + /** + * Implements the ISourceEventListener interface + */ + @Override + public Listener getSourceEventListener() + { + return mSourceEventProcessor; + } + + /** + * Module interface methods - unused. + */ + @Override + public void reset() + { + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } + + /** + * Broadcasts the demodulated audio samples to the registered listener. + * @param demodulatedSamples to broadcast + */ + protected void broadcast(float[] demodulatedSamples) + { + if(mResampledBufferListener != null) + { + mResampledBufferListener.receive(demodulatedSamples); + } + } + + /** + * Implements the IRealBufferProvider interface to register a listener for demodulated audio samples. + * + * @param listener to receive demodulated audio sample buffers. + */ + @Override + public void setBufferListener(Listener listener) + { + mResampledBufferListener = listener; + } + + /** + * Implements the IRealBufferProvider interface to deregister a listener from receiving demodulated audio samples. + */ + @Override + public void removeBufferListener() + { + mResampledBufferListener = null; + } + + /** + * Implements the IComplexSampleListener interface to receive a stream of complex sample buffers. + */ + @Override + public Listener getComplexSamplesListener() + { + return this; + } + + /** + * Implements the Listener interface to receive a stream of complex sample buffers + */ + @Override + public void receive(ComplexSamples samples) + { + if(mIDecimationFilter == null || mQDecimationFilter == null) + { + throw new IllegalStateException("NBFM demodulator module must receive a sample rate change source " + + "event before it can process complex sample buffers"); + } + + float[] decimatedI = mIDecimationFilter.decimateReal(samples.i()); + float[] decimatedQ = mQDecimationFilter.decimateReal(samples.q()); + + float[] filteredI = mIBasebandFilter.filter(decimatedI); + float[] filteredQ = mQBasebandFilter.filter(decimatedQ); + + float[] demodulated = mDemodulator.demodulate(filteredI, filteredQ); + + if(mResampler != null) + { + //Squelch changed while processing this audio buffer + if(mDemodulator.isSquelchChanged()) + { + if(mDemodulator.isMuted()) + { + //Demodulator says to mute and we're already muted/squelched = Continue IDLE + if(mSquelch) + { + notifyIdle(); + } + //Demodulator says to mute and we are unmuted = Squelch and End Call + else + { + mSquelch = true; + notifyCallEnd(); + } + } + else + { + //Demodulator says to unmute and we're muted/squelched = Unmute and Start Call + if(mSquelch) + { + mSquelch = false; + notifyCallStart(); + } + //Demodulator says to unmute and we're already unmuted = Continue Call + else + { + notifyCallContinuation(); + } + + mResampler.resample(demodulated); + } + } + else + { + //Demodulator says squelch state didn't change and we're muted/squelched = Continue IDLE + if(mSquelch) + { + notifyIdle(); + } + //Demodulator says squelch state didn't change and we're unmuted/unsquelched = Continue CALL + else + { + notifyCallContinuation(); + mResampler.resample(demodulated); + } + } + } + else + { + //This shouldn't happen + notifyIdle(); + } + + + } + + /** + * Broadcasts a call start state event + */ + protected 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) + { + switch(sourceEvent.getEvent()) + { + //Forward these requests to the sub-class implementation (AM or FM demod) to handle + case REQUEST_CURRENT_SQUELCH_THRESHOLD: + case REQUEST_CHANGE_SQUELCH_THRESHOLD: + case REQUEST_CURRENT_SQUELCH_AUTO_TRACK: + case REQUEST_CHANGE_SQUELCH_AUTO_TRACK: + mDemodulator.receive(sourceEvent); + break; + case NOTIFICATION_SAMPLE_RATE_CHANGE: + if(mIBasebandFilter != null) + { + mIBasebandFilter = null; + mQBasebandFilter = null; + } + + double sampleRate = sourceEvent.getValue().doubleValue(); + + int decimationRate = 0; + double decimatedSampleRate = sampleRate; + + if(sampleRate / 2 >= (mChannelBandwidth * 2)) + { + decimationRate = 2; + + while(sampleRate / decimationRate / 2 >= (mChannelBandwidth * 2)) + { + decimationRate *= 2; + } + } + + if(decimationRate > 0) + { + decimatedSampleRate /= decimationRate; + } + + mIDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate); + mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate); + + if((decimatedSampleRate < (2.0 * mChannelBandwidth))) + { + throw new IllegalStateException(getDecoderType().name() + " demodulator with channel bandwidth [" + + mChannelBandwidth + "] requires a channel sample rate of [" + (2.0 * mChannelBandwidth + + "] - sample rate of [" + decimatedSampleRate + "] is not supported")); + } + + mDemodulator.setSampleRate((int) decimatedSampleRate); + + int passBandStop = (int) (mChannelBandwidth * .8); + int stopBandStart = (int) mChannelBandwidth; + + float[] coefficients = null; + + FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() + .sampleRate(decimatedSampleRate * 2) + .gridDensity(16) + .oddLength(true) + .passBandCutoff(passBandStop) + .passBandAmplitude(1.0) + .passBandRipple(0.01) + .stopBandStart(stopBandStart) + .stopBandAmplitude(0.0) + .stopBandRipple(0.005) //Approximately 90 dB attenuation + .build(); + + try + { + coefficients = FilterFactory.getTaps(specification); + } + catch(FilterDesignException fde) + { + mLog.error("Couldn't design demodulator remez filter for sample rate [" + sampleRate + + "] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart + + "] - will proceed using sinc (low-pass) filter"); + } + + if(coefficients == null) + { + mLog.info("Unable to use remez filter designer for sample rate [" + decimatedSampleRate + + "] pass band stop [" + passBandStop + + "] and stop band start [" + stopBandStart + "] - will proceed using simple low pass filter design"); + coefficients = FilterFactory.getLowPass(decimatedSampleRate, passBandStop, stopBandStart, 60, + WindowType.HAMMING, true); + } + + mIBasebandFilter = FilterFactory.getRealFilter(coefficients); + mQBasebandFilter = FilterFactory.getRealFilter(coefficients); + + mResampler = new RealResampler(decimatedSampleRate, mOutputSampleRate, 4192, 512); + mResampler.setListener(resampled -> broadcast(resampled)); + break; + } + } + } +} 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 43a6a7b2..3d1ac92e 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 @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,27 +14,21 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * ***************************************************************************** + * **************************************************************************** */ package io.github.dsheirer.module.decode.nbfm; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import io.github.dsheirer.dsp.squelch.ISquelchConfiguration; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.module.decode.config.DecodeConfiguration; +import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog; import io.github.dsheirer.source.tuner.channel.ChannelSpecification; /** * Decoder configuration for an NBFM channel */ -public class DecodeConfigNBFM extends DecodeConfiguration implements ISquelchConfiguration +public class DecodeConfigNBFM extends DecodeConfigAnalog { - private Bandwidth mBandwidth = Bandwidth.BW_12_5; - private Boolean mRecordAudio; - private int mTalkgroup = 1; - private int mSquelchThreshold = -60; - public DecodeConfigNBFM() { } @@ -46,116 +39,27 @@ public class DecodeConfigNBFM extends DecodeConfiguration implements ISquelchCon return DecoderType.NBFM; } - /** - * Channel bandwidth - */ - @JacksonXmlProperty(isAttribute = true, localName = "bandwidth") - public Bandwidth getBandwidth() + @Override + protected Bandwidth getDefaultBandwidth() { - return mBandwidth; + return Bandwidth.BW_12_5; } + /** + * Channel sample stream specification. + */ @JsonIgnore @Override public ChannelSpecification getChannelSpecification() { - if(mBandwidth == Bandwidth.BW_12_5) + switch(getBandwidth()) { - return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0); - } - else - { - return new ChannelSpecification(50000.0, 25000, 12500.0, 13500.0); - } - } - - /** - * Sets the channel bandwidth - */ - public void setBandwidth(Bandwidth bandwidth) - { - mBandwidth = bandwidth; - } - -// @Deprecated //No longer used: 20211107 -// @JacksonXmlProperty(isAttribute = true, localName = "recordAudio") -// public boolean getRecordAudio() -// { -// return false; -// } -// -// @Deprecated //No longer used: 20211107 -// public void setRecordAudio(boolean recordAudio) -// { -// } -// - /** - * Talkgroup to associate with audio produced by the NBFM decoder - */ - @JacksonXmlProperty(isAttribute = true, localName = "talkgroup") - public int getTalkgroup() - { - return mTalkgroup; - } - - /** - * Sets the talkgroup identifier to attach to any demodulated audio streams - * @param talkgroup (1-65,535) - */ - public void setTalkgroup(int talkgroup) - { - if(talkgroup < 1 || talkgroup > 65535) - { - throw new IllegalArgumentException("Valid talkgroup range is 1 - 65,535"); - } - - mTalkgroup = talkgroup; - } - - /** - * Sets squelch threshold - * @param threshold (dB) - */ - @JacksonXmlProperty(isAttribute = true, localName = "squelch") - @Override - public void setSquelchThreshold(int threshold) - { - mSquelchThreshold = threshold; - } - - /** - * Squelch threshold - * @return threshold (dB) - */ - @Override - public int getSquelchThreshold() - { - return mSquelchThreshold; - } - - public enum Bandwidth - { - BW_12_5("12.5 kHz", 12500.0), - BW_25_0("25.0 kHz", 25000.0); - - private String mLabel; - private double mValue; - - Bandwidth(String label, double value) - { - mLabel = label; - mValue = value; - } - - @Override - public String toString() - { - return mLabel; - } - - public double getValue() - { - return mValue; + case BW_12_5: + return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0); + case BW_25_0: + return new ChannelSpecification(50000.0, 25000, 12500.0, 13500.0); + default: + throw new IllegalArgumentException("Unrecognized FM bandwidth value: " + getBandwidth()); } } } 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 4d789244..bde9bb1b 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 @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,352 +18,34 @@ */ package io.github.dsheirer.module.decode.nbfm; -import io.github.dsheirer.channel.state.DecoderStateEvent; -import io.github.dsheirer.channel.state.IDecoderStateEventProvider; -import io.github.dsheirer.channel.state.State; -import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory; -import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter; -import io.github.dsheirer.dsp.filter.design.FilterDesignException; -import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; -import io.github.dsheirer.dsp.filter.fir.real.IRealFilter; -import io.github.dsheirer.dsp.filter.resample.RealResampler; -import io.github.dsheirer.dsp.fm.FmDemodulatorFactory; -import io.github.dsheirer.dsp.fm.ISquelchingFmDemodulator; -import io.github.dsheirer.dsp.window.WindowType; +import io.github.dsheirer.dsp.fm.SquelchingFMDemodulator; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.module.decode.PrimaryDecoder; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexSamples; -import io.github.dsheirer.sample.complex.IComplexSamplesListener; -import io.github.dsheirer.sample.real.IRealBufferProvider; -import io.github.dsheirer.source.ISourceEventListener; -import io.github.dsheirer.source.ISourceEventProvider; -import io.github.dsheirer.source.SourceEvent; +import io.github.dsheirer.module.decode.analog.SquelchingAnalogDecoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Decoder module with integrated narrowband FM (12.5 or 25.0 kHz channel) demodulator + * Narrow Band FM Decoder module with integrated squelch control. */ -public class NBFMDecoder extends PrimaryDecoder implements ISourceEventListener, ISourceEventProvider, - IComplexSamplesListener, Listener, IRealBufferProvider, - IDecoderStateEventProvider +public class NBFMDecoder extends SquelchingAnalogDecoder { - private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoder.class); - private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0; - private static final float POWER_SQUELCH_ALPHA_DECAY = 0.0004f; - private static final float POWER_SQUELCH_THRESHOLD_DB = -78.0f; - private static final int POWER_SQUELCH_RAMP = 4; + private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoder.class); + private static final float SQUELCH_ALPHA_DECAY = 0.0004f; - private IRealFilter mIBasebandFilter; - private IRealFilter mQBasebandFilter; - private IRealDecimationFilter mIDecimationFilter; - private IRealDecimationFilter mQDecimationFilter; - private ISquelchingFmDemodulator mDemodulator; - private RealResampler mResampler; - private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor(); - private Listener mResampledBufferListener; - 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 = FmDemodulatorFactory.getSquelchingFmDemodulator(POWER_SQUELCH_ALPHA_DECAY, - POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP); - mDemodulator.setSquelchThreshold(config.getSquelchThreshold()); - } - - @Override - public DecoderType getDecoderType() + /** + * Constructs an instance + * + * @param config to setup the decoder + */ + public NBFMDecoder(DecodeConfigNBFM config) { - return DecoderType.NBFM; + super(config, new SquelchingFMDemodulator(SQUELCH_ALPHA_DECAY, config.getSquelchThreshold(), + config.isSquelchAutoTrack())); } - @Override - public Listener getComplexSamplesListener() - { - return this; - } - - @Override - public Listener getSourceEventListener() - { - return mSourceEventProcessor; - } - - @Override - public void reset() - { - mDemodulator.reset(); - } - - @Override - public void start() - { - } - - @Override - public void stop() - { - } - - @Override - public void setBufferListener(Listener listener) - { - mResampledBufferListener = listener; - } - - @Override - public void removeBufferListener() - { - mResampledBufferListener = null; - } - - @Override - public void receive(ComplexSamples samples) - { - if(mIDecimationFilter == null || mQDecimationFilter == null) - { - throw new IllegalStateException("NBFM demodulator module must receive a sample rate change source " + - "event before it can process complex sample buffers"); - } - - float[] decimatedI = mIDecimationFilter.decimateReal(samples.i()); - float[] decimatedQ = mQDecimationFilter.decimateReal(samples.q()); - - float[] filteredI = mIBasebandFilter.filter(decimatedI); - float[] filteredQ = mQBasebandFilter.filter(decimatedQ); - - float[] demodulated = mDemodulator.demodulate(filteredI, filteredQ); - - if(mResampler != null) - { - //If we're currently squelched and the squelch state changed while demodulating the baseband samples, - // then un-squelch so we can send this buffer - if(mSquelch && mDemodulator.isSquelchChanged()) - { - mSquelch = false; - notifyCallStart(); - } - - //Either send the demodulated buffer to the resampler for distro, or decrement the user count - if(mSquelch) - { - notifyIdle(); - } - else - { - mResampler.resample(demodulated); - notifyCallContinuation(); - } - - //Set to squelch if necessary to close out the audio buffers - if(!mSquelch && mDemodulator.isMuted()) - { - mSquelch = true; - notifyCallEnd(); - } - } - else - { - notifyIdle(); - } - } - - /** - * Broadcasts a call start state event - */ - private void notifyCallStart() - { - broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.START, State.CALL, 0)); - } - - /** - * Broadcasts a call continuation state event - */ - private void notifyCallContinuation() - { - broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.CALL, 0)); - } - - /** - * Broadcasts a call end state event - */ - private void notifyCallEnd() - { - broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.END, State.CALL, 0)); - } - - /** - * Broadcasts an idle notification - */ - private void notifyIdle() - { - broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.IDLE, 0)); - } - - /** - * Broadcasts the decoder state event to an optional registered listener - */ - private void broadcast(DecoderStateEvent event) - { - if(mDecoderStateEventListener != null) - { - mDecoderStateEventListener.receive(event); - } - } - - /** - * Sets the decoder state listener - */ - @Override - public void setDecoderStateListener(Listener 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(mIBasebandFilter != null) - { - mIBasebandFilter = null; - mQBasebandFilter = null; - } - - double sampleRate = sourceEvent.getValue().doubleValue(); - - int decimationRate = 0; - double decimatedSampleRate = sampleRate; - - if(sampleRate / 2 >= (mChannelBandwidth * 2)) - { - decimationRate = 2; - - while(sampleRate / decimationRate / 2 >= (mChannelBandwidth * 2)) - { - decimationRate *= 2; - } - } - - if(decimationRate > 0) - { - decimatedSampleRate /= decimationRate; - } - - mIDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate); - mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate); - - if((decimatedSampleRate < (2.0 * mChannelBandwidth))) - { - throw new IllegalStateException("FM Demodulator with channel bandwidth [" + mChannelBandwidth + - "] requires a channel sample rate of [" + (2.0 * mChannelBandwidth + "] - sample rate of [" + - decimatedSampleRate + "] is not supported")); - } - - int passBandStop = (int)(mChannelBandwidth * .8); - int stopBandStart = (int)mChannelBandwidth; - - float[] coefficients = null; - - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate(decimatedSampleRate * 2) - .gridDensity(16) - .oddLength(true) - .passBandCutoff(passBandStop) - .passBandAmplitude(1.0) - .passBandRipple(0.01) - .stopBandStart(stopBandStart) - .stopBandAmplitude(0.0) - .stopBandRipple(0.005) //Approximately 90 dB attenuation - .build(); - - try - { - coefficients = FilterFactory.getTaps(specification); - } - catch(FilterDesignException fde) - { - mLog.error("Couldn't design FM demodulator remez filter for sample rate [" + sampleRate + - "] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart + - "] - will proceed using sinc (low-pass) filter"); - } - - if(coefficients == null) - { - mLog.info("Unable to use remez filter designer for sample rate [" + decimatedSampleRate + - "] pass band stop [" + passBandStop + - "] and stop band start [" + stopBandStart + "] - will proceed using simple low pass filter design"); - coefficients = FilterFactory.getLowPass(decimatedSampleRate, passBandStop, stopBandStart, 60, - WindowType.HAMMING, true); - } - - mIBasebandFilter = FilterFactory.getRealFilter(coefficients); - mQBasebandFilter = FilterFactory.getRealFilter(coefficients); - - mResampler = new RealResampler(decimatedSampleRate, mOutputSampleRate, 4192, 512); - - mResampler.setListener(resampled -> - { - if(mResampledBufferListener != null) - { - mResampledBufferListener.receive(resampled); - } - }); - } - else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD) - { - //Send request for squelch threshold change to the demodulator/squelch controller. - mDemodulator.receive(sourceEvent); - } - else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD) - { - mDemodulator.receive(sourceEvent); - } - } - } + @Override + public DecoderType getDecoderType() + { + return DecoderType.NBFM; + } } 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 index fac97418..e6cb4299 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/nbfm/NBFMDecoderState.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,42 +16,31 @@ * along with this program. If not, see * **************************************************************************** */ + package io.github.dsheirer.module.decode.nbfm; -import io.github.dsheirer.channel.IChannelDescriptor; -import io.github.dsheirer.channel.state.DecoderState; -import io.github.dsheirer.channel.state.DecoderStateEvent; -import io.github.dsheirer.channel.state.State; import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierClass; -import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.string.SimpleStringIdentifier; -import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; -import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.DecoderType; -import io.github.dsheirer.module.decode.event.DecodeEvent; -import io.github.dsheirer.module.decode.p25.identifier.channel.StandardChannel; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.source.ISourceEventListener; -import io.github.dsheirer.source.SourceEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.github.dsheirer.module.decode.analog.AnalogDecoderState; /** - * Narrow Band FM decoder channel state - provides the minimum channel state functionality + * NBFM decoder state */ -public class NBFMDecoderState extends DecoderState implements ISourceEventListener +public class NBFMDecoderState extends AnalogDecoderState { - private final static Logger mLog = LoggerFactory.getLogger(NBFMDecoderState.class); - private Listener mSourceEventListener = new SourceEventListener(); private String mChannelName; - private DecodeEvent mDecodeEvent; private Identifier mChannelNameIdentifier; private Identifier mTalkgroupIdentifier; - private IChannelDescriptor mChannelDescriptor = null; + /** + * Constructs an instance + * @param channelName to use for this channel + * @param decodeConfig with talkgroup identifier + */ public NBFMDecoderState(String channelName, DecodeConfigNBFM decodeConfig) { mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : "NBFM CHANNEL"; @@ -59,116 +48,6 @@ public class NBFMDecoderState extends DecoderState implements ISourceEventListen mTalkgroupIdentifier = new NBFMTalkgroup(decodeConfig.getTalkgroup()); } - @Override - public void init() - { - } - - @Override - public String getActivitySummary() - { - StringBuilder sb = new StringBuilder(); - - sb.append("Activity Summary\n"); - sb.append("\tDecoder: NBFM"); - sb.append("\n\n"); - - return sb.toString(); - } - - @Override - public void receive(IMessage t) - { - /* Not implemented */ - } - - @Override - public void receiveDecoderStateEvent(DecoderStateEvent event) - { - switch(event.getEvent()) - { - case REQUEST_RESET -> - { - getIdentifierCollection().update(mChannelNameIdentifier); - } - case START -> - { - if(event.getState() == State.CALL) - { - startCallEvent(); - } - } - case END -> - { - if(event.getState() == State.CALL) - { - endCallEvent(); - } - } - case CONTINUATION -> - { - if(event.getState() == State.CALL) - { - continueCallEvent(); - } - else - { - endCallEvent(); - } - } - } - } - - /** - * Creates/starts a decode call evnet. - */ - private void startCallEvent() - { - getIdentifierCollection().update(mTalkgroupIdentifier); - - if(mDecodeEvent == null) - { - mDecodeEvent = DecodeEvent.builder(System.currentTimeMillis()) - .channel(mChannelDescriptor) - .eventDescription("CALL") - .details("NBFM") - .identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers())) - .build(); - - broadcast(mDecodeEvent); - } - } - - /** - * Continues (or starts) the call decode event and updates the current timestamp - */ - private void continueCallEvent() - { - if(mDecodeEvent == null) - { - startCallEvent(); - } - - mDecodeEvent.update(System.currentTimeMillis()); - broadcast(mDecodeEvent); - } - - /** - * Ends the call decode event - */ - private void endCallEvent() - { - if(mDecodeEvent != null) - { - mDecodeEvent.end(System.currentTimeMillis()); - broadcast(mDecodeEvent); - mDecodeEvent = null; - } - - //Remote any user identifiers from the identifier collection - resetState(); - } - @Override public DecoderType getDecoderType() { @@ -176,34 +55,24 @@ public class NBFMDecoderState extends DecoderState implements ISourceEventListen } @Override - public void start() + protected Identifier getChannelNameIdentifier() { - getIdentifierCollection().update(mChannelNameIdentifier); + return mChannelNameIdentifier; } @Override - public void stop() + protected Identifier getTalkgroupIdentifier() { + return mTalkgroupIdentifier; } @Override - public Listener getSourceEventListener() + public String getActivitySummary() { - 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()); - } - } + StringBuilder sb = new StringBuilder(); + sb.append("Activity Summary\n"); + sb.append("\tDecoder: NBFM"); + sb.append("\n\n"); + return sb.toString(); } } diff --git a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java b/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java deleted file mode 100644 index cd400b5c..00000000 --- a/src/main/java/io/github/dsheirer/module/demodulate/am/AMDemodulatorModule.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2022 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.demodulate.am; - -import io.github.dsheirer.dsp.am.AMDemodulator; -import io.github.dsheirer.dsp.filter.FilterFactory; -import io.github.dsheirer.dsp.filter.design.FilterDesignException; -import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; -import io.github.dsheirer.dsp.filter.fir.real.IRealFilter; -import io.github.dsheirer.dsp.filter.resample.RealResampler; -import io.github.dsheirer.dsp.gain.AutomaticGainControl; -import io.github.dsheirer.dsp.window.WindowType; -import io.github.dsheirer.module.Module; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.ComplexSamples; -import io.github.dsheirer.sample.complex.IComplexSamplesListener; -import io.github.dsheirer.sample.real.IRealBufferProvider; -import io.github.dsheirer.source.ISourceEventListener; -import io.github.dsheirer.source.SourceEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * AM Demodulator Module for demodulating complex baseband channel sample buffers and producing demodulated audio - * at a specified output sample rate, normally 8 kHz. - *

- * This module requires a sample rate SourceEvent prior to processing baseband sample buffers in order to configure - * internal filters and the resampler. - */ -public class AMDemodulatorModule extends Module - implements ISourceEventListener, IComplexSamplesListener, IRealBufferProvider, Listener -{ - private final static Logger mLog = LoggerFactory.getLogger(AMDemodulatorModule.class); - private IRealFilter mIBasebandFilter; - private IRealFilter mQBasebandFilter; - - private AMDemodulator mDemodulator; - private IRealFilter mLowPassFilter; - private AutomaticGainControl mAGC = new AutomaticGainControl(); - private double mChannelBandwidth; - private double mOutputSampleRate; - private RealResampler mResampler; - private Listener mResampledReusableBufferListener; - private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor(); - - /** - * Constructs an AM demodulator module - * - * @param channelBandwidth to use in filtering the baseband input buffers - * @param outputSampleRate specifies the resampled output sample rate for demodulated audio - */ - public AMDemodulatorModule(double channelBandwidth, double outputSampleRate) - { - mChannelBandwidth = channelBandwidth; - mOutputSampleRate = outputSampleRate; - - mDemodulator = new AMDemodulator(500.0f); - - float[] lowPassCoefficients = null; - - FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder().sampleRate(mOutputSampleRate) - .gridDensity(16).oddLength(true).passBandCutoff(3000).passBandAmplitude(1.0).passBandRipple(0.01) - .stopBandStart(3500).stopBandAmplitude(0.0).stopBandRipple(0.028).build(); - - try - { - lowPassCoefficients = FilterFactory.getTaps(specification); - } - catch(FilterDesignException fde) - { - mLog.error("Couldn't design AM demodulator remez filter for sample rate [" + mOutputSampleRate - + "] pass frequency [3000] and stop frequency [3500] - using sinc filter"); - } - - if(lowPassCoefficients == null) - { - lowPassCoefficients = FilterFactory.getLowPass(mOutputSampleRate, 3000, 3500, 60, WindowType.HAMMING, true); - } - - mLowPassFilter = FilterFactory.getRealFilter(lowPassCoefficients); - } - - @Override public void receive(ComplexSamples samples) - { - float[] i = mIBasebandFilter.filter(samples.i()); - float[] q = mQBasebandFilter.filter(samples.q()); - float[] demodulated = mDemodulator.demodulate(i, q); - mResampler.resample(demodulated); - } - - /** - * Processes resampled audio buffers produced by the resampler. - * - * @param resampledAudio buffer to process. - */ - private void processResampledAudio(float[] resampledAudio) - { - if(mResampledReusableBufferListener != null) - { - float[] filtered = mLowPassFilter.filter(resampledAudio); - float[] amplified = mAGC.process(filtered); - mResampledReusableBufferListener.receive(amplified); - } - } - - @Override public Listener getComplexSamplesListener() - { - return this; - } - - @Override public void reset() - { - } - - @Override public void start() - { - } - - @Override public void stop() - { - } - - @Override public void setBufferListener(Listener listener) - { - mResampledReusableBufferListener = listener; - } - - @Override public void removeBufferListener() - { - mResampledReusableBufferListener = null; - } - - @Override public Listener getSourceEventListener() - { - return mSourceEventProcessor; - } - - /** - * Monitors sample rate change source event(s) to setup the initial I/Q filter and resampler - */ - public class SourceEventProcessor implements Listener - { - @Override public void receive(SourceEvent sourceEvent) - { - if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE) - { - if(mIBasebandFilter != null || mQBasebandFilter != null) - { - mIBasebandFilter = null; - mQBasebandFilter = null; - } - - double sampleRate = sourceEvent.getValue().doubleValue(); - - if((sampleRate < (2.0 * mChannelBandwidth))) - { - throw new IllegalStateException("AM Demodulator with channel bandwidth [" + mChannelBandwidth - + "] requires a channel sample rate of [" + (2.0 * mChannelBandwidth - + "] - sample rate of [" + sampleRate + "] is not supported")); - } - - double cutoff = sampleRate / 4.0; - int passBandStop = (int)cutoff - 500; - int stopBandStart = (int)cutoff + 500; - - float[] coefficients = 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(); - - try - { - coefficients = FilterFactory.getTaps(specification); - } - catch(FilterDesignException fde) - { - mLog.error("Couldn't design AM demodulator remez filter for sample rate [" + sampleRate - + "] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart - + "] - using sinc filter"); - } - - if(coefficients == null) - { - coefficients = FilterFactory.getLowPass(sampleRate, passBandStop, stopBandStart, 60, - WindowType.HAMMING, true); - } - - mIBasebandFilter = FilterFactory.getRealFilter(coefficients); - mQBasebandFilter = FilterFactory.getRealFilter(coefficients); - mResampler = new RealResampler(sampleRate, mOutputSampleRate, 8192, 512); - mResampler.setListener(resampledBuffer -> processResampledAudio(resampledBuffer)); - } - } - } -} 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 362232d4..d7af75a6 100644 --- a/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java +++ b/src/main/java/io/github/dsheirer/preference/identifier/TalkgroupFormatPreference.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer * - * * ****************************************************************************** - * * 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 - * * ***************************************************************************** + * 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; @@ -29,6 +26,7 @@ import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.preference.Preference; import io.github.dsheirer.preference.PreferenceType; import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter; +import io.github.dsheirer.preference.identifier.talkgroup.AnalogTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupFormatter; import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter; @@ -37,13 +35,12 @@ import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormat import io.github.dsheirer.preference.identifier.talkgroup.PassportTalkgroupFormatter; import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.sample.Listener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.EnumMap; import java.util.Map; import java.util.Set; import java.util.prefs.Preferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Specifies and applies user preferences for formatting identifiers @@ -149,6 +146,7 @@ public class TalkgroupFormatPreference extends Preference case LTR_NET: case MPT1327: return IntegerFormat.FORMATTED; + case AM: case APCO25: case DMR: case MDC1200: @@ -169,6 +167,7 @@ public class TalkgroupFormatPreference extends Preference case LTR_NET: case MPT1327: return IntegerFormat.DECIMAL_FORMATTED; + case AM: case APCO25: case DMR: case MDC1200: @@ -187,6 +186,7 @@ public class TalkgroupFormatPreference extends Preference case MDC1200: case PASSPORT: return false; + case AM: case APCO25: case DMR: case FLEETSYNC: @@ -314,6 +314,9 @@ public class TalkgroupFormatPreference extends Preference { switch(talkgroupIdentifier.getProtocol()) { + case AM: + return AnalogTalkgroupFormatter.format(talkgroupIdentifier, getTalkgroupFormat(Protocol.AM), + isTalkgroupFixedWidth(Protocol.AM)); case APCO25: return APCO25TalkgroupFormatter.format(talkgroupIdentifier, getTalkgroupFormat(Protocol.APCO25), isTalkgroupFixedWidth(Protocol.APCO25)); @@ -332,6 +335,9 @@ public class TalkgroupFormatPreference extends Preference case MPT1327: return MPT1327TalkgroupFormatter.format(talkgroupIdentifier, getTalkgroupFormat(Protocol.MPT1327), isTalkgroupFixedWidth(Protocol.MPT1327)); + case NBFM: + return AnalogTalkgroupFormatter.format(talkgroupIdentifier, getTalkgroupFormat(Protocol.NBFM), + isTalkgroupFixedWidth(Protocol.NBFM)); case PASSPORT: return PassportTalkgroupFormatter.format(talkgroupIdentifier, getTalkgroupFormat(Protocol.PASSPORT), isTalkgroupFixedWidth(Protocol.PASSPORT)); diff --git a/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NBFMTalkgroupFormatter.java b/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/AnalogTalkgroupFormatter.java similarity index 94% rename from src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NBFMTalkgroupFormatter.java rename to src/main/java/io/github/dsheirer/preference/identifier/talkgroup/AnalogTalkgroupFormatter.java index 2535c156..31ca2bd1 100644 --- a/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/NBFMTalkgroupFormatter.java +++ b/src/main/java/io/github/dsheirer/preference/identifier/talkgroup/AnalogTalkgroupFormatter.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2019 Dennis Sheirer + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,7 +14,7 @@ * * 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; @@ -23,7 +22,7 @@ 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 class AnalogTalkgroupFormatter extends AbstractIntegerFormatter { public static final int DECIMAL_WIDTH = 5; public static final int HEXADECIMAL_WIDTH = 4; diff --git a/src/main/java/io/github/dsheirer/protocol/Protocol.java b/src/main/java/io/github/dsheirer/protocol/Protocol.java index 99612938..8e4c6950 100644 --- a/src/main/java/io/github/dsheirer/protocol/Protocol.java +++ b/src/main/java/io/github/dsheirer/protocol/Protocol.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer * - * * ****************************************************************************** - * * 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 - * * ***************************************************************************** + * 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.protocol; @@ -28,6 +25,7 @@ import java.util.EnumSet; */ public enum Protocol { + AM("AM", "AM", 0), APCO25("APCO-25", "APCO25PHASE1", 9600), APCO25_PHASE2("APCO-25 P2", "APCO25PHASE2", 12000), ARS("ARS", "ARS", 0), @@ -51,7 +49,6 @@ public enum Protocol private String mFileNameLabel; private int mBitRate; - Protocol(String label, String fileNameLabel, int bitRate) { mLabel = label; @@ -59,7 +56,7 @@ public enum Protocol mBitRate = bitRate; } - public static EnumSet TALKGROUP_PROTOCOLS = EnumSet.of(APCO25, DMR, FLEETSYNC, LTR, LTR_NET, MDC1200, + public static EnumSet TALKGROUP_PROTOCOLS = EnumSet.of(AM, APCO25, DMR, FLEETSYNC, LTR, LTR_NET, MDC1200, 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 96c686b9..2254b216 100644 --- a/src/main/java/io/github/dsheirer/source/SourceEvent.java +++ b/src/main/java/io/github/dsheirer/source/SourceEvent.java @@ -45,6 +45,7 @@ public class SourceEvent NOTIFICATION_RECORDING_FILE_LOADED, NOTIFICATION_SAMPLE_RATE_CHANGE, NOTIFICATION_SQUELCH_THRESHOLD, + NOTIFICATION_SQUELCH_AUTO_TRACK, NOTIFICATION_STOP_SAMPLE_STREAM, NOTIFICATION_TUNER_SHUTDOWN, NOTIFICATION_ERROR_STATE, @@ -54,6 +55,8 @@ public class SourceEvent REQUEST_FREQUENCY_SELECTION, REQUEST_CHANGE_SQUELCH_THRESHOLD, REQUEST_CURRENT_SQUELCH_THRESHOLD, + REQUEST_CHANGE_SQUELCH_AUTO_TRACK, + REQUEST_CURRENT_SQUELCH_AUTO_TRACK, REQUEST_START_SAMPLE_STREAM, REQUEST_STOP_SAMPLE_STREAM; @@ -428,12 +431,11 @@ public class SourceEvent /** * Requests the current squelch threshold value - * @param source optional * @return source event */ - public static SourceEvent requestCurrentSquelchThreshold(Source source) + public static SourceEvent requestCurrentSquelchThreshold() { - return new SourceEvent(Event.REQUEST_CURRENT_SQUELCH_THRESHOLD, source, 0); + return new SourceEvent(Event.REQUEST_CURRENT_SQUELCH_THRESHOLD, null, 0); } /** @@ -447,6 +449,35 @@ public class SourceEvent return new SourceEvent(Event.REQUEST_CHANGE_SQUELCH_THRESHOLD, source, thresholdDb); } + /** + * Creates a new request squelch auto track status source event. + * @return new source event + */ + public static SourceEvent requestCurrentSquelchAutoTrack() + { + return new SourceEvent(Event.REQUEST_CURRENT_SQUELCH_AUTO_TRACK, null, 0); + } + + /** + * Creates a new squelch auto-track notification + * @param autoTrack true or false. + * @return source event + */ + public static SourceEvent squelchAutoTrack(boolean autoTrack) + { + return new SourceEvent(Event.NOTIFICATION_SQUELCH_AUTO_TRACK, null, autoTrack ? 1 : 0); + } + + /** + * Creates a new squelch auto-track request + * @param autoTrack to enable (true) or disable auto-track. + * @return source event + */ + public static SourceEvent requestSquelchAutoTrack(boolean autoTrack) + { + return new SourceEvent(Event.REQUEST_CHANGE_SQUELCH_AUTO_TRACK, null, autoTrack ? 1 : 0); + } + @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 941f0c6c..29dbd3bf 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 @@ -1,18 +1,21 @@ -/******************************************************************************* - * sdr-trunk - * Copyright (C) 2014-2018 Dennis Sheirer +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer * - * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any - * later version. + * 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. + * 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 - * - ******************************************************************************/ + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ package io.github.dsheirer.source.tuner.channel; /** @@ -29,8 +32,8 @@ public class ChannelSpecification * Constructs a channel specification instance. * @param minimumSampleRate requested for the channel * @param bandwidth - * @param passFrequency for the pass band of the channel - * @param stopFrequency for the stop band of teh channel + * @param passFrequency for the pass band of the channel, usually half of the bandwidth value. + * @param stopFrequency for the stop band of teh channel, usually slightly more than half of the bandwidth value. */ public ChannelSpecification(double minimumSampleRate, int bandwidth, double passFrequency, double stopFrequency) { 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 ee2fd464..dc2e5ff0 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 @@ -164,8 +164,11 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc case NOTIFICATION_FREQUENCY_ROTATION_SUCCESS: case NOTIFICATION_CHANNEL_POWER: case NOTIFICATION_SQUELCH_THRESHOLD: + case NOTIFICATION_SQUELCH_AUTO_TRACK: case REQUEST_CHANGE_SQUELCH_THRESHOLD: case REQUEST_CURRENT_SQUELCH_THRESHOLD: + case REQUEST_CHANGE_SQUELCH_AUTO_TRACK: + case REQUEST_CURRENT_SQUELCH_AUTO_TRACK: //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 d6d74a99..82bedb03 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -47,6 +47,7 @@ public class ComplexWaveSource extends Source implements IControllableFileSource private final static Logger mLog = LoggerFactory.getLogger(ComplexWaveSource.class); private IFrameLocationListener mFrameLocationListener; + private int mBufferSampleCount = 65536; //Complex samples per buffer private int mBytesPerFrame; private int mFrameCounter = 0; private long mFrequency = 0; @@ -119,7 +120,8 @@ public class ComplexWaveSource extends Source implements IControllableFileSource */ public int getBufferSampleCount() { - return (int)(getSampleRate() / 20.0d); + return mBufferSampleCount; +// return (int)(getSampleRate() / 20.0d); } @Override @@ -139,12 +141,11 @@ public class ComplexWaveSource extends Source implements IControllableFileSource if(mAutoReplay) { - long intervalMilliseconds = 50; //20 intervals per second - double framesPerInterval = getSampleRate() / 20.0d; - mReplayController = ThreadPool.SCHEDULED.scheduleAtFixedRate(new ReplayController(framesPerInterval), + double sampleRate = getSampleRate(); + double buffersPerSecond = (sampleRate / mBufferSampleCount); + long intervalMilliseconds = (long)(1000.0 / buffersPerSecond); + mReplayController = ThreadPool.SCHEDULED.scheduleAtFixedRate(new ReplayController(mBufferSampleCount), 0, intervalMilliseconds, TimeUnit.MILLISECONDS); - - } } diff --git a/src/main/java/io/github/dsheirer/vector/VectorUtilities.java b/src/main/java/io/github/dsheirer/vector/VectorUtilities.java index 05b459d5..3c6099b4 100644 --- a/src/main/java/io/github/dsheirer/vector/VectorUtilities.java +++ b/src/main/java/io/github/dsheirer/vector/VectorUtilities.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -63,8 +63,22 @@ public class VectorUtilities { if(i.length % species.length() != 0) { - throw new IllegalArgumentException("I/Q buffer lengths [" + i.length + "] must be a power of 2 multiple of SIMD lane width [" + - species.length() + "]"); + throw new IllegalArgumentException("I/Q buffer lengths [" + i.length + "] must be a power of 2 multiple of " + + "SIMD lane width [" + species.length() + "]"); + } + } + + /** + * Checks the I/Q sample array length to be an integer multiple of the SIMD lane width. + * @param array samples + * @param species used for SIMD operations + */ + public static void checkArrayLength(float[] array, VectorSpecies species) + { + if(array.length % species.length() != 0) + { + throw new IllegalArgumentException("Buffer array length [" + array.length + "] must be a power of 2 multiple " + + "of SIMD lane width [" + species.length() + "]"); } } diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationManager.java b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationManager.java index e40faaf8..0dac3acb 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationManager.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,13 +19,13 @@ package io.github.dsheirer.vector.calibrate; +import io.github.dsheirer.dsp.am.AmDemodulatorCalibration; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.calibration.VectorCalibrationPreference; import io.github.dsheirer.vector.calibrate.airspy.AirspySampleConverterCalibration; import io.github.dsheirer.vector.calibrate.airspy.AirspyUnpackedCalibration; import io.github.dsheirer.vector.calibrate.airspy.AirspyUnpackedInterleavedCalibration; import io.github.dsheirer.vector.calibrate.demodulator.FmDemodulatorCalibration; -import io.github.dsheirer.vector.calibrate.demodulator.SquelchingFmDemodulatorCalibration; import io.github.dsheirer.vector.calibrate.filter.FirFilterCalibration; import io.github.dsheirer.vector.calibrate.filter.RealDcRemovalCalibration; import io.github.dsheirer.vector.calibrate.filter.RealHalfBand11TapFilterCalibration; @@ -35,19 +35,19 @@ import io.github.dsheirer.vector.calibrate.filter.RealHalfBand63TapFilterCalibra import io.github.dsheirer.vector.calibrate.filter.RealHalfBandDefaultFilterCalibration; import io.github.dsheirer.vector.calibrate.gain.ComplexGainCalibration; import io.github.dsheirer.vector.calibrate.gain.ComplexGainControlCalibration; +import io.github.dsheirer.vector.calibrate.magnitude.MagnitudeCalibration; import io.github.dsheirer.vector.calibrate.mixer.ComplexMixerCalibration; import io.github.dsheirer.vector.calibrate.oscillator.ComplexOscillatorCalibration; import io.github.dsheirer.vector.calibrate.oscillator.RealOscillatorCalibration; import io.github.dsheirer.vector.calibrate.window.WindowCalibration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Determines the optimal (scalar vs vector) class to use for the current CPU architecture. @@ -99,12 +99,14 @@ public class CalibrationManager sInstance.add(new AirspySampleConverterCalibration()); sInstance.add(new AirspyUnpackedCalibration()); sInstance.add(new AirspyUnpackedInterleavedCalibration()); + sInstance.add(new AmDemodulatorCalibration()); sInstance.add(new ComplexGainCalibration()); sInstance.add(new ComplexGainControlCalibration()); sInstance.add(new ComplexOscillatorCalibration()); sInstance.add(new ComplexMixerCalibration()); sInstance.add(new FirFilterCalibration()); sInstance.add(new FmDemodulatorCalibration()); + sInstance.add(new MagnitudeCalibration()); sInstance.add(new RealDcRemovalCalibration()); sInstance.add(new RealHalfBand11TapFilterCalibration()); sInstance.add(new RealHalfBand15TapFilterCalibration()); @@ -112,7 +114,6 @@ public class CalibrationManager sInstance.add(new RealHalfBand63TapFilterCalibration()); sInstance.add(new RealHalfBandDefaultFilterCalibration()); sInstance.add(new RealOscillatorCalibration()); - sInstance.add(new SquelchingFmDemodulatorCalibration()); // sInstance.add(new HilbertCalibration()); //Not currently used sInstance.add(new WindowCalibration()); //Not currently used diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java index 3a0d77c7..edab2d4d 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,6 +35,7 @@ public enum CalibrationType AIRSPY_SAMPLE_CONVERTER("Airspy Sample Converter", 1), AIRSPY_UNPACKED_INTERLEAVED_ITERATOR("Airspy Unpacked Interleaved Iterator", 1), AIRSPY_UNPACKED_ITERATOR("Airspy Unpacked Iterator", 1), + AM_DEMODULATOR("AM Demodulator", 1), DC_REMOVAL_REAL("Real DC Removal Filter", 1), FILTER_FIR("FIR Filter", 1), FILTER_HALF_BAND_REAL_11_TAP("Real Half-Band Decimation Filter - 11 Tap", 1), @@ -42,9 +43,10 @@ public enum CalibrationType FILTER_HALF_BAND_REAL_23_TAP("Real Half-Band Decimation Filter - 23 Tap", 1), FILTER_HALF_BAND_REAL_63_TAP("Real Half-Band Decimation Filter - 63 Tap", 1), FILTER_HALF_BAND_REAL_DEFAULT("Real Half-Band Decimation Filter - Default", 1), - FM_DEMODULATOR("FM Demodulator", 1), + FM_DEMODULATOR("FM Demodulator", 2), GAIN_CONTROL_COMPLEX("Complex Gain Control", 1), HILBERT_TRANSFORM("Hilbert Transform", 1), + MAGNITUDE("Magnitude", 1), MIXER_COMPLEX("Complex Mixer", 1), OSCILLATOR_REAL("Real Oscillator", 1), SQUELCHING_FM_DEMODULATOR("Squelching FM Demodulator", 1), diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/demodulator/FmDemodulatorCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/demodulator/FmDemodulatorCalibration.java index 20ae919f..52f0d851 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/demodulator/FmDemodulatorCalibration.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/demodulator/FmDemodulatorCalibration.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2023 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,9 +19,12 @@ package io.github.dsheirer.vector.calibrate.demodulator; -import io.github.dsheirer.dsp.fm.IFmDemodulator; +import io.github.dsheirer.dsp.fm.IDemodulator; import io.github.dsheirer.dsp.fm.ScalarFMDemodulator; -import io.github.dsheirer.dsp.fm.VectorFMDemodulator; +import io.github.dsheirer.dsp.fm.VectorFMDemodulator128; +import io.github.dsheirer.dsp.fm.VectorFMDemodulator256; +import io.github.dsheirer.dsp.fm.VectorFMDemodulator512; +import io.github.dsheirer.dsp.fm.VectorFMDemodulator64; import io.github.dsheirer.vector.calibrate.Calibration; import io.github.dsheirer.vector.calibrate.CalibrationException; import io.github.dsheirer.vector.calibrate.CalibrationType; @@ -37,9 +40,11 @@ public class FmDemodulatorCalibration extends Calibration private static final int ITERATION_DURATION_MS = 1000; private static final int WARMUP_ITERATIONS = 5; private static final int TEST_ITERATIONS = 5; - private IFmDemodulator mScalarDemodulator = new ScalarFMDemodulator(); - private IFmDemodulator mVectorDemodulator = new VectorFMDemodulator(); - + private IDemodulator mScalarDemodulator = new ScalarFMDemodulator(); + private IDemodulator mVectorDemodulator64 = new VectorFMDemodulator64(); + private IDemodulator mVectorDemodulator128 = new VectorFMDemodulator128(); + private IDemodulator mVectorDemodulator256 = new VectorFMDemodulator256(); + private IDemodulator mVectorDemodulator512 = new VectorFMDemodulator512(); /** * Constructs an instance @@ -64,15 +69,45 @@ public class FmDemodulatorCalibration extends Calibration mLog.info("FM DEMODULATOR WARMUP - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult())); - Mean vectorMean = new Mean(); + Mean vectorMean64 = new Mean(); for(int x = 0; x < WARMUP_ITERATIONS; x++) { - long score = testVector(i, q); - vectorMean.increment(score); + long score = testVector64(i, q); + vectorMean64.increment(score); } - mLog.info("FM DEMODULATOR WARMUP - VECTOR: " + DECIMAL_FORMAT.format(vectorMean.getResult())); + mLog.info("FM DEMODULATOR WARMUP - VECTOR 64: " + DECIMAL_FORMAT.format(vectorMean64.getResult())); + + Mean vectorMean128 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector128(i, q); + vectorMean128.increment(score); + } + + mLog.info("FM DEMODULATOR WARMUP - VECTOR 128: " + DECIMAL_FORMAT.format(vectorMean128.getResult())); + + Mean vectorMean256 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector256(i, q); + vectorMean256.increment(score); + } + + mLog.info("FM DEMODULATOR WARMUP - VECTOR 256: " + DECIMAL_FORMAT.format(vectorMean256.getResult())); + + Mean vectorMean512 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector512(i, q); + vectorMean512.increment(score); + } + + mLog.info("FM DEMODULATOR WARMUP - VECTOR 512: " + DECIMAL_FORMAT.format(vectorMean512.getResult())); //Start tests scalarMean.clear(); @@ -85,25 +120,74 @@ public class FmDemodulatorCalibration extends Calibration mLog.info("FM DEMODULATOR - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult())); - vectorMean.clear(); + vectorMean64.clear(); for(int x = 0; x < TEST_ITERATIONS; x++) { - long score = testVector(i, q); - vectorMean.increment(score); + long score = testVector64(i, q); + vectorMean64.increment(score); } - mLog.info("FM DEMODULATOR - VECTOR: " + DECIMAL_FORMAT.format(vectorMean.getResult())); + mLog.info("FM DEMODULATOR - VECTOR 64: " + DECIMAL_FORMAT.format(vectorMean64.getResult())); - if(scalarMean.getResult() > vectorMean.getResult()) + vectorMean128.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) { - setImplementation(Implementation.SCALAR); + long score = testVector128(i, q); + vectorMean128.increment(score); } - else + + mLog.info("FM DEMODULATOR - VECTOR 128: " + DECIMAL_FORMAT.format(vectorMean128.getResult())); + + vectorMean256.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) { - setImplementation(Implementation.VECTOR_SIMD_PREFERRED); + long score = testVector256(i, q); + vectorMean256.increment(score); } + mLog.info("FM DEMODULATOR - VECTOR 256: " + DECIMAL_FORMAT.format(vectorMean256.getResult())); + + vectorMean512.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector512(i, q); + vectorMean512.increment(score); + } + + mLog.info("FM DEMODULATOR - VECTOR 512: " + DECIMAL_FORMAT.format(vectorMean512.getResult())); + + Implementation best = Implementation.SCALAR; + double bestScore = scalarMean.getResult(); + + if(vectorMean64.getResult() > bestScore) + { + bestScore = vectorMean64.getResult(); + best = Implementation.VECTOR_SIMD_64; + } + + if(vectorMean128.getResult() > bestScore) + { + bestScore = vectorMean128.getResult(); + best = Implementation.VECTOR_SIMD_128; + } + + if(vectorMean256.getResult() > bestScore) + { + bestScore = vectorMean256.getResult(); + best = Implementation.VECTOR_SIMD_256; + } + + if(vectorMean512.getResult() > bestScore) + { + best = Implementation.VECTOR_SIMD_512; + } + + setImplementation(best); + mLog.info("FM DEMODULATOR - SET OPTIMAL IMPLEMENTATION TO:" + getImplementation()); } @@ -124,7 +208,7 @@ public class FmDemodulatorCalibration extends Calibration return count + (long)(accumulator * 0); } - private long testVector(float[] i, float[] q) + private long testVector64(float[] i, float[] q) { double accumulator = 0.0; long count = 0; @@ -132,11 +216,73 @@ public class FmDemodulatorCalibration extends Calibration while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { - float[] demodulated = mVectorDemodulator.demodulate(i, q); + float[] demodulated = mVectorDemodulator64.demodulate(i, q); accumulator += demodulated[1]; count++; } return count + (long)(accumulator * 0); } + + private long testVector128(float[] i, float[] q) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorDemodulator128.demodulate(i, q); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector256(float[] i, float[] q) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorDemodulator256.demodulate(i, q); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector512(float[] i, float[] q) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorDemodulator512.demodulate(i, q); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + public static void main(String[] args) + { + FmDemodulatorCalibration calibration = new FmDemodulatorCalibration(); + + try + { + calibration.calibrate(); + } + catch(Exception e) + { + mLog.error("Error during calibration", e); + } + } } diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/demodulator/SquelchingFmDemodulatorCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/demodulator/SquelchingFmDemodulatorCalibration.java deleted file mode 100644 index 1380a571..00000000 --- a/src/main/java/io/github/dsheirer/vector/calibrate/demodulator/SquelchingFmDemodulatorCalibration.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2022 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.vector.calibrate.demodulator; - -import io.github.dsheirer.dsp.fm.IFmDemodulator; -import io.github.dsheirer.dsp.fm.ScalarSquelchingFMDemodulator; -import io.github.dsheirer.dsp.fm.VectorSquelchingFMDemodulator; -import io.github.dsheirer.vector.calibrate.Calibration; -import io.github.dsheirer.vector.calibrate.CalibrationException; -import io.github.dsheirer.vector.calibrate.CalibrationType; -import io.github.dsheirer.vector.calibrate.Implementation; -import org.apache.commons.math3.stat.descriptive.moment.Mean; - -/** - * Calibrates squelching FM demodulator options - */ -public class SquelchingFmDemodulatorCalibration extends Calibration -{ - private static final float POWER_SQUELCH_ALPHA_DECAY = 0.0004f; - private static final float POWER_SQUELCH_THRESHOLD_DB = -78.0f; - private static final int POWER_SQUELCH_RAMP = 4; - private static final int BUFFER_SIZE = 2048; - private static final int ITERATION_DURATION_MS = 1000; - private static final int WARMUP_ITERATIONS = 5; - private static final int TEST_ITERATIONS = 5; - - private IFmDemodulator mScalar = new ScalarSquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY, - POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP); - private IFmDemodulator mVector = new VectorSquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY, - POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP); - - /** - * Constructs an instance - */ - public SquelchingFmDemodulatorCalibration() - { - super(CalibrationType.SQUELCHING_FM_DEMODULATOR); - } - - @Override public void calibrate() throws CalibrationException - { - float[] i = getFloatSamples(BUFFER_SIZE); - float[] q = getFloatSamples(BUFFER_SIZE); - - Mean scalarMean = new Mean(); - - for(int x = 0; x < WARMUP_ITERATIONS; x++) - { - long score = testScalar(i, q); - scalarMean.increment(score); - } - - mLog.info("SQUELCHING FM DEMODULATOR WARMUP - SCALAR:" + DECIMAL_FORMAT.format(scalarMean.getResult())); - - Mean vectorMean = new Mean(); - - for(int x = 0; x < WARMUP_ITERATIONS; x++) - { - long score = testVector(i, q); - vectorMean.increment(score); - } - - mLog.info("SQUELCHING FM DEMODULATOR WARMUP - VECTOR:" + DECIMAL_FORMAT.format(vectorMean.getResult())); - - //Test begins ... - scalarMean.clear(); - - for(int x = 0; x < TEST_ITERATIONS; x++) - { - long score = testScalar(i, q); - scalarMean.increment(score); - } - - mLog.info("SQUELCHING FM DEMODULATOR - SCALAR:" + DECIMAL_FORMAT.format(scalarMean.getResult())); - - vectorMean.clear(); - - for(int x = 0; x < TEST_ITERATIONS; x++) - { - long score = testVector(i, q); - vectorMean.increment(score); - } - - mLog.info("SQUELCHING FM DEMODULATOR - VECTOR:" + DECIMAL_FORMAT.format(vectorMean.getResult())); - - if(scalarMean.getResult() > vectorMean.getResult()) - { - setImplementation(Implementation.SCALAR); - } - else - { - setImplementation(Implementation.VECTOR_SIMD_PREFERRED); - } - - mLog.info("SQUELCHING FM DEMODULATOR - SET OPTIMAL IMPLEMENTATION TO:" + getImplementation()); - } - - private long testScalar(float[] i, float[] q) - { - double accumulator = 0.0; - long count = 0; - - long start = System.currentTimeMillis(); - - while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) - { - float[] demodulated = mScalar.demodulate(i, q); - accumulator += demodulated[1]; - count++; - } - - return count + (long)(accumulator * 0); - } - - private long testVector(float[] i, float[] q) - { - double accumulator = 0.0; - long count = 0; - - long start = System.currentTimeMillis(); - - while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) - { - float[] demodulated = mVector.demodulate(i, q); - accumulator += demodulated[1]; - count++; - } - - return count + (long)(accumulator * 0); - } -} diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/magnitude/MagnitudeCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/magnitude/MagnitudeCalibration.java new file mode 100644 index 00000000..e30e6d86 --- /dev/null +++ b/src/main/java/io/github/dsheirer/vector/calibrate/magnitude/MagnitudeCalibration.java @@ -0,0 +1,288 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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.vector.calibrate.magnitude; + +import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator; +import io.github.dsheirer.dsp.magnitude.ScalarMagnitudeCalculator; +import io.github.dsheirer.dsp.magnitude.VectorMagnitudeCalculator128; +import io.github.dsheirer.dsp.magnitude.VectorMagnitudeCalculator256; +import io.github.dsheirer.dsp.magnitude.VectorMagnitudeCalculator512; +import io.github.dsheirer.dsp.magnitude.VectorMagnitudeCalculator64; +import io.github.dsheirer.vector.calibrate.Calibration; +import io.github.dsheirer.vector.calibrate.CalibrationException; +import io.github.dsheirer.vector.calibrate.CalibrationType; +import io.github.dsheirer.vector.calibrate.Implementation; +import org.apache.commons.math3.stat.descriptive.moment.Mean; + +/** + * Calibrates FM demodulator options + */ +public class MagnitudeCalibration extends Calibration +{ + private static final int BUFFER_SIZE = 2048; + private static final int ITERATION_DURATION_MS = 1000; + private static final int WARMUP_ITERATIONS = 5; + private static final int TEST_ITERATIONS = 5; + private final IMagnitudeCalculator mScalarMagnitude = new ScalarMagnitudeCalculator(); + private final IMagnitudeCalculator mVectorMagnitude64 = new VectorMagnitudeCalculator64(); + private final IMagnitudeCalculator mVectorMagnitude128 = new VectorMagnitudeCalculator128(); + private final IMagnitudeCalculator mVectorMagnitude256 = new VectorMagnitudeCalculator256(); + private final IMagnitudeCalculator mVectorMagnitude512 = new VectorMagnitudeCalculator512(); + + /** + * Constructs an instance + */ + public MagnitudeCalibration() + { + super(CalibrationType.MAGNITUDE); + } + + @Override public void calibrate() throws CalibrationException + { + float[] i = getFloatSamples(BUFFER_SIZE); + float[] q = getFloatSamples(BUFFER_SIZE); + + Mean scalarMean = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testScalar(i, q); + scalarMean.increment(score); + } + + mLog.info("MAGNITUDE WARMUP - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult())); + + Mean vectorMean64 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector64(i, q); + vectorMean64.increment(score); + } + + mLog.info("MAGNITUDE WARMUP - VECTOR 64: " + DECIMAL_FORMAT.format(vectorMean64.getResult())); + + Mean vectorMean128 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector128(i, q); + vectorMean128.increment(score); + } + + mLog.info("MAGNITUDE WARMUP - VECTOR 128: " + DECIMAL_FORMAT.format(vectorMean128.getResult())); + + Mean vectorMean256 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector256(i, q); + vectorMean256.increment(score); + } + + mLog.info("MAGNITUDE WARMUP - VECTOR 256: " + DECIMAL_FORMAT.format(vectorMean256.getResult())); + + Mean vectorMean512 = new Mean(); + + for(int x = 0; x < WARMUP_ITERATIONS; x++) + { + long score = testVector512(i, q); + vectorMean512.increment(score); + } + + mLog.info("MAGNITUDE WARMUP - VECTOR 512: " + DECIMAL_FORMAT.format(vectorMean512.getResult())); + + //Start tests + scalarMean.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testScalar(i, q); + scalarMean.increment(score); + } + + mLog.info("MAGNITUDE - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult())); + + vectorMean64.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector64(i, q); + vectorMean64.increment(score); + } + + mLog.info("MAGNITUDE - VECTOR 64: " + DECIMAL_FORMAT.format(vectorMean64.getResult())); + + vectorMean128.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector128(i, q); + vectorMean128.increment(score); + } + + mLog.info("MAGNITUDE - VECTOR 128: " + DECIMAL_FORMAT.format(vectorMean128.getResult())); + + vectorMean256.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector256(i, q); + vectorMean256.increment(score); + } + + mLog.info("MAGNITUDE - VECTOR 256: " + DECIMAL_FORMAT.format(vectorMean256.getResult())); + + vectorMean512.clear(); + + for(int x = 0; x < TEST_ITERATIONS; x++) + { + long score = testVector512(i, q); + vectorMean512.increment(score); + } + + mLog.info("MAGNITUDE - VECTOR 512: " + DECIMAL_FORMAT.format(vectorMean512.getResult())); + + Implementation best = Implementation.SCALAR; + double bestScore = scalarMean.getResult(); + + if(vectorMean64.getResult() > bestScore) + { + bestScore = vectorMean64.getResult(); + best = Implementation.VECTOR_SIMD_64; + } + + if(vectorMean128.getResult() > bestScore) + { + bestScore = vectorMean128.getResult(); + best = Implementation.VECTOR_SIMD_128; + } + + if(vectorMean256.getResult() > bestScore) + { + bestScore = vectorMean256.getResult(); + best = Implementation.VECTOR_SIMD_256; + } + + if(vectorMean512.getResult() > bestScore) + { + best = Implementation.VECTOR_SIMD_512; + } + + setImplementation(best); + + mLog.info("MAGNITUDE - SET OPTIMAL IMPLEMENTATION TO:" + getImplementation()); + } + + private long testScalar(float[] i, float[] q) + { + double accumulator = 0.0; + long count = 0; + + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mScalarMagnitude.calculate(i, q); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector64(float[] i, float[] q) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorMagnitude64.calculate(i, q); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector128(float[] i, float[] q) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorMagnitude128.calculate(i, q); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector256(float[] i, float[] q) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorMagnitude256.calculate(i, q); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + private long testVector512(float[] i, float[] q) + { + double accumulator = 0.0; + long count = 0; + long start = System.currentTimeMillis(); + + while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) + { + float[] demodulated = mVectorMagnitude512.calculate(i, q); + accumulator += demodulated[1]; + count++; + } + + return count + (long)(accumulator * 0); + } + + public static void main(String[] args) + { + MagnitudeCalibration calibration = new MagnitudeCalibration(); + + try + { + calibration.calibrate(); + } + catch(Exception e) + { + mLog.error("Error during calibration", e); + } + } +}