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