#35 Updates AM & NBFM decoders to use Adaptive Squelch, a power squelch that includes an adaptive noise floor monitor. Implements vectorized magnitude operations for AM & FM decoders. Adds talkgroup assignment and squelch controls to AM channel configuration editor. Implements new AM gain control that attempts to normalize audio gain quickly at beginning of call/transmission and maintains that gain across lifecycle of the call and resets the gain for the next call. (#1540)

Notes on usage:
- When using either AM or FM squelch control use either the Heterodyne channelizer or if using the Polyphase channelizer, avoid using trunk tracking decoders like DMR or P25 or set the squelch threshold high enough to avoid changes in noise floor from polyphase channelizer where it can vary between using the one channel or the two-channel combined channel sources.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2023-05-07 07:27:44 -04:00
committed by GitHub
parent 5654254cbb
commit 4f8b8e978b
74 changed files with 4630 additions and 2012 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<SourceEvent> listener);
/**
* Receives a source event
*/
void receive(SourceEvent sourceEvent);
}

View File

@@ -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<SourceEvent> listener);
/**
* Receives a source event
*/
void receive(SourceEvent sourceEvent);
}

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.dsp.squelch.PowerSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.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<SourceEvent>
{
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<SourceEvent> 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);
}
}
}

View File

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

View File

@@ -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<Float> 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<Float> getVectorSpecies();
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
// VectorSpecies<Float> 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");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.dsp.squelch.PowerSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.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<SourceEvent>
{
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<SourceEvent> 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");
}
}
}

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.magnitude;
/**
* Interface for implementations that calculate magnitude
*/
public interface IMagnitudeCalculator
{
float[] calculate(float[] i, float[] q);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<SourceEvent>
{
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<SourceEvent> mSourceEventListener;
/**
* Constructs an instance
*
* Testing against a 12.5 kHz analog FM modulated signal, the following parameters provided a
* good responsiveness. A threshold of -80.0 dB seemed to trigger significant flapping during un-squelching.
* - alpha: 0.0001
* - threshold: 78.0 dB
*
* @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<SourceEvent> listener)
{
mSourceEventListener = listener;
}
/**
* Broadcasts the source event to an optional register listener
*/
private void broadcast(SourceEvent event)
{
if(mSourceEventListener != null)
{
mSourceEventListener.receive(event);
}
}
/**
* Primary method to receive requests for squelch threshold change
*/
@Override
public void receive(SourceEvent sourceEvent)
{
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;
}
}
}

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<SourceEvent>
private static final Logger mLog = LoggerFactory.getLogger(PowerSquelch.class);
private State mState = State.MUTE;
private SinglePoleIirFilter mFilter;
private double mPower = 0.0f;
private double mSquelchThreshold;
private 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<SourceEvent>
* @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<SourceEvent>
* 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<SourceEvent>
*/
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<SourceEvent>
{
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)
{

View File

@@ -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<float[]>
private final static Logger mLog = LoggerFactory.getLogger(RealSampleLineChart.class);
private ObservableList<Data<Integer,Float>> mSamples = FXCollections.observableArrayList();
private Series<Integer,Float> 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<float[]>
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<float[]>
mRealCircularBuffer.put(sample);
}
float[] bufferSamples = mRealCircularBuffer.get();
float[] bufferSamples = mRealCircularBuffer.getAll();
for(int x = 0; x < bufferSamples.length; x++)
{

View File

@@ -677,6 +677,10 @@ public class AliasItemEditor extends Editor<Alias>
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<Alias>
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<Alias>
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);
}

View File

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

View File

@@ -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<Integer> 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<EventLogType> 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<Toggle>()
{
@Override
public void onChanged(Change<? extends Toggle> c)
{
//This change event happens when the toggles are added -- we don't need to inspect the change event
if(getItem() != null && getItem().getDecodeConfiguration() instanceof 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<Integer>
{
@Override
public void changed(ObservableValue<? extends Integer> 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);
}

View File

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

View File

@@ -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<ProcessingChai
private JButton mSquelchUpButton;
private JButton mSquelchDownButton;
private JButton mLogIndexesButton;
private JCheckBox mSquelchAutoTrackCheckBox;
private double mSquelchThreshold;
private boolean mPanelVisible = false;
private boolean mDftProcessing = false;
@@ -114,10 +116,12 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
valuePanel.setLayout(new MigLayout("", "[right][left][][]", ""));
mPeakLabel = new JLabel("0");
mPeakLabel.setToolTipText("Current peak power level in decibels.");
valuePanel.add(new JLabel("Peak:"));
valuePanel.add(mPeakLabel, "wrap");
mPowerLabel = new JLabel("0");
mPowerLabel.setToolTipText("Current Power level in decibels");
valuePanel.add(new JLabel("Power:"));
valuePanel.add(mPowerLabel, "wrap");
@@ -125,21 +129,34 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
mSquelchLabel.setEnabled(false);
valuePanel.add(mSquelchLabel);
mSquelchValueLabel = new JLabel(NOT_AVAILABLE);
mSquelchValueLabel.setToolTipText("Squelch threshold value in decibels");
mSquelchValueLabel.setEnabled(false);
valuePanel.add(mSquelchValueLabel, "wrap");
IconFontSwing.register(FontAwesome.getIconFont());
Icon iconUp = IconFontSwing.buildIcon(FontAwesome.ANGLE_UP, 12);
mSquelchUpButton = new JButton(iconUp);
mSquelchUpButton.setToolTipText("Increases the squelch threshold value");
mSquelchUpButton.setEnabled(false);
mSquelchUpButton.addActionListener(e -> broadcast(SourceEvent.requestSquelchThreshold(null, mSquelchThreshold + 1)));
valuePanel.add(mSquelchUpButton);
Icon iconDown = IconFontSwing.buildIcon(FontAwesome.ANGLE_DOWN, 12);
mSquelchDownButton = new JButton(iconDown);
mSquelchDownButton.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<ProcessingChai
});
mNoiseFloorSpinner = new JSpinner(mNoiseFloorSpinnerModel);
labelPanel.add(mNoiseFloorSpinner);
labelPanel.add(new JLabel("Noise Floor"));
labelPanel.add(new JLabel("Spectral Display Noise Floor"));
mLogIndexesButton = new JButton("Log Settings");
mLogIndexesButton.addActionListener(e -> {
@@ -340,6 +357,22 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
}
}
/**
* Updates the channel configuration squelch auto-track feature setting.
* @param autoTrack true to enable.
*/
private void setConfigSquelchAutoTrack(boolean autoTrack)
{
Channel channel = mPlaylistManager.getChannelProcessingManager().getChannel(mProcessingChain);
if(channel != null && channel.getDecodeConfiguration() instanceof ISquelchConfiguration)
{
ISquelchConfiguration configuration = (ISquelchConfiguration)channel.getDecodeConfiguration();
configuration.setSquelchAutoTrack(autoTrack);
mPlaylistManager.schedulePlaylistSave();
}
}
private void broadcast(SourceEvent sourceEvent)
{
if(mProcessingChain != null)
@@ -372,6 +405,8 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
mSquelchValueLabel.setEnabled(false);
mSquelchUpButton.setEnabled(false);
mSquelchDownButton.setEnabled(false);
mSquelchAutoTrackCheckBox.setEnabled(false);
mSquelchAutoTrackCheckBox.setSelected(false);
}
/**
@@ -420,7 +455,8 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
updateFFTProcessing();
broadcast(SourceEvent.requestCurrentSquelchThreshold(null));
broadcast(SourceEvent.requestCurrentSquelchAutoTrack());
broadcast(SourceEvent.requestCurrentSquelchThreshold());
}
@@ -475,6 +511,15 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
updatePllFrequency(sourceEvent.getValue().longValue());
}
}
case NOTIFICATION_SQUELCH_AUTO_TRACK ->
{
boolean autoTrack = sourceEvent.getValue().intValue() == 1 ? true : false;
setConfigSquelchAutoTrack(autoTrack);
EventQueue.invokeLater(() -> {
mSquelchAutoTrackCheckBox.setSelected(autoTrack);
mSquelchAutoTrackCheckBox.setEnabled(true);
});
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<SourceEvent> 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<SourceEvent> getSourceEventListener()
{
return mSourceEventListener;
}
/**
* Monitors source events to capture the channel frequency for use in decode events.
*/
private class SourceEventListener implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE)
{
mChannelDescriptor = new StandardChannel(sourceEvent.getValue().longValue());
}
}
}
}

View File

@@ -0,0 +1,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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Bandwidth> 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<Bandwidth> 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;
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<ComplexSamples>, 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<float[]> mResampledBufferListener;
private Listener<DecoderStateEvent> 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<SourceEvent> 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<float[]> 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<ComplexSamples> getComplexSamplesListener()
{
return this;
}
/**
* Implements the Listener<ComplexSample> 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<DecoderStateEvent> listener)
{
mDecoderStateEventListener = listener;
}
/**
* Removes the decoder state event listener
*/
@Override
public void removeDecoderStateListener()
{
mDecoderStateEventListener = null;
}
/**
* Registers the listener to receive squelch change events from the demodulator/squelch controller.
*/
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mDemodulator.setSourceEventListener(listener);
}
/**
* De-registers the listener
*/
@Override
public void removeSourceEventListener()
{
mDemodulator.setSourceEventListener(null);
}
/**
* Monitors sample rate change source event(s) to setup the initial I/Q filter and passes squelch threshold
* change requests down to the demodulator.
*/
public class SourceEventProcessor implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
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;
}
}
}
}

View File

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

View File

@@ -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<ComplexSamples>, 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<float[]> mResampledBufferListener;
private Listener<DecoderStateEvent> mDecoderStateEventListener;
private double mChannelBandwidth;
private double mOutputSampleRate = DEMODULATED_AUDIO_SAMPLE_RATE;
private boolean mSquelch = true;
/**
* Constructs an instance
* @param config to setup the decoder
*/
public NBFMDecoder( DecodeConfigNBFM config )
{
super( config );
mChannelBandwidth = config.getBandwidth().getValue();
mDemodulator = 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<ComplexSamples> getComplexSamplesListener()
{
return this;
}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return mSourceEventProcessor;
}
@Override
public void reset()
{
mDemodulator.reset();
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void setBufferListener(Listener<float[]> 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<DecoderStateEvent> listener)
{
mDecoderStateEventListener = listener;
}
/**
* Removes the decoder state event listener
*/
@Override
public void removeDecoderStateListener()
{
mDecoderStateEventListener = null;
}
/**
* Registers the listener to receive squelch change events from the demodulator/squelch controller.
*/
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mDemodulator.setSourceEventListener(listener);
}
/**
* De-registers the listener
*/
@Override
public void removeSourceEventListener()
{
mDemodulator.setSourceEventListener(null);
}
/**
* Monitors sample rate change source event(s) to setup the initial I/Q filter and passes squelch threshold
* change requests down to the demodulator.
*/
public class SourceEventProcessor implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_SAMPLE_RATE_CHANGE)
{
if(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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.nbfm;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.p25.identifier.channel.StandardChannel;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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<SourceEvent> 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<SourceEvent> getSourceEventListener()
public String getActivitySummary()
{
return mSourceEventListener;
}
/**
* Monitors source events to capture the channel frequency for use in decode events.
*/
private class SourceEventListener implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE)
{
mChannelDescriptor = new StandardChannel(sourceEvent.getValue().longValue());
}
}
StringBuilder sb = new StringBuilder();
sb.append("Activity Summary\n");
sb.append("\tDecoder: NBFM");
sb.append("\n\n");
return sb.toString();
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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.
* <p>
* 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<ComplexSamples>
{
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<float[]> 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<ComplexSamples> getComplexSamplesListener()
{
return this;
}
@Override public void reset()
{
}
@Override public void start()
{
}
@Override public void stop()
{
}
@Override public void setBufferListener(Listener<float[]> listener)
{
mResampledReusableBufferListener = listener;
}
@Override public void removeBufferListener()
{
mResampledReusableBufferListener = null;
}
@Override public Listener<SourceEvent> getSourceEventListener()
{
return mSourceEventProcessor;
}
/**
* Monitors sample rate change source event(s) to setup the initial I/Q filter and resampler
*/
public class SourceEventProcessor implements Listener<SourceEvent>
{
@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));
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.preference.identifier;
@@ -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));

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.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<Protocol> TALKGROUP_PROTOCOLS = EnumSet.of(APCO25, DMR, FLEETSYNC, LTR, LTR_NET, MDC1200,
public static EnumSet<Protocol> TALKGROUP_PROTOCOLS = EnumSet.of(AM, APCO25, DMR, FLEETSYNC, LTR, LTR_NET, MDC1200,
MPT1327, NBFM, PASSPORT);
public static EnumSet<Protocol> RADIO_ID_PROTOCOLS = EnumSet.of(APCO25, DMR, PASSPORT);

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.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)
{

View File

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

View File

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

View File

@@ -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<Float> 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() + "]");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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