mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -05:00
Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.audio.playback;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for audio provider sample conversion
|
||||
*/
|
||||
class AudioProviderPcmTest
|
||||
{
|
||||
@Test
|
||||
void outOfRangeSamplesAreLimitedInsteadOfWrapping()
|
||||
{
|
||||
assertEquals(Short.MAX_VALUE, AudioProvider.toPcm(1.35f));
|
||||
assertEquals(-Short.MAX_VALUE, AudioProvider.toPcm(-26.0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inRangeSamplesAreScaled()
|
||||
{
|
||||
assertEquals(0, AudioProvider.toPcm(0.0f));
|
||||
assertEquals(16383, AudioProvider.toPcm(0.5f));
|
||||
assertEquals(Short.MAX_VALUE, AudioProvider.toPcm(1.0f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for the AM audio normalizer
|
||||
*/
|
||||
class AmAudioNormalizerTest
|
||||
{
|
||||
private static final double SAMPLE_RATE = 25000.0;
|
||||
private static final float MODULATION = 0.8f;
|
||||
|
||||
private static float envelope(float carrier, int sample)
|
||||
{
|
||||
return carrier * (1.0f + MODULATION * (float)Math.cos(2.0 * Math.PI * 1000.0 / SAMPLE_RATE * sample));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputFollowsModulationDepthAtAnyCarrierLevel()
|
||||
{
|
||||
for(float carrier: new float[]{0.0005f, 0.002f, 0.01f, 0.03f, 0.1f})
|
||||
{
|
||||
checkOutput(carrier);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkOutput(float carrier)
|
||||
{
|
||||
AmAudioNormalizer normalizer = new AmAudioNormalizer(SAMPLE_RATE);
|
||||
double sum = 0.0;
|
||||
float peak = 0.0f;
|
||||
int settle = (int)SAMPLE_RATE;
|
||||
int count = (int)SAMPLE_RATE;
|
||||
|
||||
for(int x = 0; x < settle + count; x++)
|
||||
{
|
||||
float audio = normalizer.process(envelope(carrier, x));
|
||||
|
||||
if(x >= settle)
|
||||
{
|
||||
sum += audio;
|
||||
peak = Math.max(peak, Math.abs(audio));
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(0.6f, peak, 0.02f, "Peak");
|
||||
assertEquals(0.0, sum / count, 0.002, "DC");
|
||||
}
|
||||
|
||||
@Test
|
||||
void transmissionStartHasNoTransient()
|
||||
{
|
||||
AmAudioNormalizer normalizer = new AmAudioNormalizer(SAMPLE_RATE);
|
||||
|
||||
for(int x = 0; x < (int)SAMPLE_RATE; x++)
|
||||
{
|
||||
assertTrue(Math.abs(normalizer.process(envelope(0.01f, x))) < 0.8f, "Transient at sample " + x);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void noiseAfterCarrierDropIsNotAmplifiedToCarrierLevel()
|
||||
{
|
||||
AmAudioNormalizer normalizer = new AmAudioNormalizer(SAMPLE_RATE);
|
||||
|
||||
for(int x = 0; x < (int)SAMPLE_RATE; x++)
|
||||
{
|
||||
normalizer.process(envelope(0.01f, x));
|
||||
}
|
||||
|
||||
java.util.Random random = new java.util.Random(1);
|
||||
float peak = 0.0f;
|
||||
|
||||
//Noise is 40 dB below the carrier. Skip the carrier estimate decay.
|
||||
for(int x = 0; x < (int)(SAMPLE_RATE * 0.5); x++)
|
||||
{
|
||||
float audio = normalizer.process(0.0001f * (1.0f + random.nextFloat()));
|
||||
|
||||
if(x > (int)(SAMPLE_RATE * 0.2))
|
||||
{
|
||||
peak = Math.max(peak, Math.abs(audio));
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(peak < 0.05f, "Noise peak " + peak);
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputIsLimited()
|
||||
{
|
||||
AmAudioNormalizer normalizer = new AmAudioNormalizer(SAMPLE_RATE);
|
||||
|
||||
for(int x = 0; x < 1000; x++)
|
||||
{
|
||||
normalizer.process(0.001f);
|
||||
}
|
||||
|
||||
assertTrue(normalizer.process(1.0f) <= 0.95f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for the AM demodulator squelch transitions
|
||||
*/
|
||||
class SquelchingAMDemodulatorTest
|
||||
{
|
||||
private static final int SAMPLE_RATE = 25000;
|
||||
private static final int BUFFER = 1024;
|
||||
private static final double TONE = 1000.0;
|
||||
|
||||
/**
|
||||
* Demodulates silence, then a transmission, then silence.
|
||||
*/
|
||||
private static float[] transmission(float carrier)
|
||||
{
|
||||
SquelchingAMDemodulator demodulator = new SquelchingAMDemodulator(-78.0f, false, 6000.0);
|
||||
demodulator.setSampleRate(SAMPLE_RATE);
|
||||
int buffers = 150;
|
||||
float[] audio = new float[BUFFER * buffers];
|
||||
|
||||
for(int b = 0; b < buffers; b++)
|
||||
{
|
||||
float[] i = new float[BUFFER];
|
||||
float[] q = new float[BUFFER];
|
||||
|
||||
if(b >= 30 && b < 100)
|
||||
{
|
||||
for(int x = 0; x < BUFFER; x++)
|
||||
{
|
||||
int sample = b * BUFFER + x;
|
||||
i[x] = carrier * (1.0f + 0.8f * (float)Math.cos(2.0 * Math.PI * TONE * sample / SAMPLE_RATE));
|
||||
}
|
||||
}
|
||||
|
||||
System.arraycopy(demodulator.demodulate(i, q), 0, audio, b * BUFFER, BUFFER);
|
||||
}
|
||||
|
||||
return audio;
|
||||
}
|
||||
|
||||
@Test
|
||||
void transmissionHasNoClicks()
|
||||
{
|
||||
for(float carrier: new float[]{0.001f, 0.01f, 0.1f})
|
||||
{
|
||||
float[] audio = transmission(carrier);
|
||||
float peak = 0.0f;
|
||||
float step = 0.0f;
|
||||
|
||||
for(int x = 1; x < audio.length; x++)
|
||||
{
|
||||
peak = Math.max(peak, Math.abs(audio[x]));
|
||||
step = Math.max(step, Math.abs(audio[x] - audio[x - 1]));
|
||||
}
|
||||
|
||||
//Expected 80% modulated peak is 0.6 and the largest step of the 1 kHz tone is 0.15
|
||||
assertTrue(peak < 0.7f, "Peak " + peak + " for carrier " + carrier);
|
||||
assertTrue(step < 0.2f, "Step " + step + " for carrier " + carrier);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void audioEndsWithTheTransmission()
|
||||
{
|
||||
float[] audio = transmission(0.01f);
|
||||
int last = 0;
|
||||
|
||||
for(int x = 0; x < audio.length; x++)
|
||||
{
|
||||
if(audio[x] != 0.0f)
|
||||
{
|
||||
last = x;
|
||||
}
|
||||
}
|
||||
|
||||
//Audio is delayed by 32 ms, so it can't run past the end of the transmission plus the delay.
|
||||
int end = 100 * BUFFER;
|
||||
assertTrue(last <= end + SAMPLE_RATE * 0.032, "Last audio sample " + last + " for end at " + end);
|
||||
assertEquals(0.0f, audio[audio.length - 1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for the channel filter design
|
||||
*/
|
||||
class ChannelFilterDesignerTest
|
||||
{
|
||||
private static final double[] BANDWIDTHS = {3000.0, 5000.0, 6000.0, 7500.0, 8333.0, 12500.0, 15000.0, 25000.0};
|
||||
|
||||
/**
|
||||
* Each channel filter must be valid across decimated sample rates, which includes rates where the remez designer
|
||||
* previously produced a filter with a large response spike at half of the sample rate.
|
||||
*/
|
||||
@Test
|
||||
void channelFilterResponseIsValidForDecimatedSampleRates()
|
||||
{
|
||||
for(double bandwidth: BANDWIDTHS)
|
||||
{
|
||||
for(double sampleRate = bandwidth; sampleRate < 4.0 * bandwidth; sampleRate += bandwidth / 32.0)
|
||||
{
|
||||
check(sampleRate, bandwidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void tunerDerivedSampleRates()
|
||||
{
|
||||
check(11718.75, 3000.0);
|
||||
check(19531.25, 5000.0);
|
||||
check(50000.0, 15000.0);
|
||||
check(29296.875, 7500.0);
|
||||
check(48828.125, 12500.0);
|
||||
check(25000.0, 7500.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* These sample rates use the fallback filter design, which must keep the same bandwidth as the remez design.
|
||||
*/
|
||||
@Test
|
||||
void fallbackFilterKeepsChannelBandwidth()
|
||||
{
|
||||
checkPassBand(11718.75, 3000.0);
|
||||
checkPassBand(19531.25, 5000.0);
|
||||
checkPassBand(50000.0, 15000.0);
|
||||
}
|
||||
|
||||
private static void checkPassBand(double sampleRate, double bandwidth)
|
||||
{
|
||||
float[] coefficients = ChannelFilterDesigner.design(sampleRate, bandwidth);
|
||||
double response = ChannelFilterDesigner.getResponseDb(coefficients, bandwidth * .4, sampleRate) -
|
||||
ChannelFilterDesigner.getResponseDb(coefficients, 0.0, sampleRate);
|
||||
assertTrue(response > -1.0, "Pass band edge response [" + response + " dB] at sample rate [" + sampleRate + "]");
|
||||
}
|
||||
|
||||
private static void check(double sampleRate, double bandwidth)
|
||||
{
|
||||
float[] coefficients = ChannelFilterDesigner.design(sampleRate, bandwidth);
|
||||
assertTrue(ChannelFilterDesigner.isValidFilterResponse(coefficients, sampleRate, (int)(bandwidth * .4), (int)(bandwidth * .5)),
|
||||
"Filter response for sample rate [" + sampleRate + "] bandwidth [" + bandwidth + "]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void responseCheckRejectsSpikeAtNyquist()
|
||||
{
|
||||
//Low-pass moving average with an added alternating sequence that peaks at half of the sample rate.
|
||||
float[] coefficients = new float[31];
|
||||
|
||||
for(int x = 0; x < coefficients.length; x++)
|
||||
{
|
||||
coefficients[x] = 1.0f / coefficients.length + (x % 2 == 0 ? 0.1f : -0.1f);
|
||||
}
|
||||
|
||||
assertTrue(!ChannelFilterDesigner.isValidFilterResponse(coefficients, 25000.0, 3000, 4000));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for the adaptive squelch
|
||||
*/
|
||||
class AdaptiveSquelchTest
|
||||
{
|
||||
private static final int SAMPLE_RATE = 25000;
|
||||
private static final float THRESHOLD_DB = -78.0f;
|
||||
private static final float NOISE = (float)Math.pow(10.0, -9.0); //-90 dB
|
||||
|
||||
private static AdaptiveSquelch squelch(int sampleRate, boolean autoTrack)
|
||||
{
|
||||
AdaptiveSquelch squelch = new AdaptiveSquelch(THRESHOLD_DB, autoTrack, 6000.0);
|
||||
squelch.setSampleRate(sampleRate);
|
||||
return squelch;
|
||||
}
|
||||
|
||||
private static void process(AdaptiveSquelch squelch, float power, double seconds, int sampleRate)
|
||||
{
|
||||
for(int x = 0; x < (int)(seconds * sampleRate); x++)
|
||||
{
|
||||
squelch.process(power);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seconds of samples at the power level until the audio is disabled and until the squelch closes.
|
||||
*/
|
||||
private static double[] closeTimes(float signal, int sampleRate)
|
||||
{
|
||||
AdaptiveSquelch squelch = squelch(sampleRate, false);
|
||||
process(squelch, signal, 0.5, sampleRate);
|
||||
assertTrue(squelch.isUnmuted());
|
||||
double audio = -1.0;
|
||||
|
||||
for(int x = 0; x < sampleRate; x++)
|
||||
{
|
||||
squelch.process(NOISE);
|
||||
|
||||
if(audio < 0.0 && !squelch.isAudioEnabled())
|
||||
{
|
||||
audio = (double)x / sampleRate;
|
||||
}
|
||||
|
||||
if(squelch.isMuted())
|
||||
{
|
||||
return new double[]{audio, (double)x / sampleRate};
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError("Squelch didn't close");
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeTimeIsIndependentOfSignalLevelAndSampleRate()
|
||||
{
|
||||
for(int sampleRate: new int[]{6250, 25000, 50000})
|
||||
{
|
||||
for(float signal: new float[]{1E-7f, 1E-6f, 1E-4f, 1E-2f})
|
||||
{
|
||||
double[] times = closeTimes(signal, sampleRate);
|
||||
assertEquals(0.0175, times[0], 0.0085, "Audio disable " + signal + " at " + sampleRate);
|
||||
assertEquals(0.1175, times[1], 0.0085, "Close " + signal + " at " + sampleRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void startsMuted()
|
||||
{
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, false);
|
||||
assertTrue(squelch.isMuted());
|
||||
assertFalse(squelch.isAudioEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortNoiseBurstDoesNotOpen()
|
||||
{
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, false);
|
||||
process(squelch, NOISE, 0.1, SAMPLE_RATE);
|
||||
process(squelch, 1E-4f, 0.005, SAMPLE_RATE);
|
||||
process(squelch, NOISE, 0.1, SAMPLE_RATE);
|
||||
assertTrue(squelch.isMuted());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hysteresisHoldsOpenBelowOpenThreshold()
|
||||
{
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, false);
|
||||
float threshold = (float)Math.pow(10.0, THRESHOLD_DB / 10.0);
|
||||
process(squelch, threshold * 1.2f, 0.1, SAMPLE_RATE);
|
||||
assertTrue(squelch.isUnmuted());
|
||||
process(squelch, threshold * 0.6f, 2.0, SAMPLE_RATE); //2 dB below open, above close
|
||||
assertTrue(squelch.isAudioEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fadeShorterThanHangKeepsCallOpen()
|
||||
{
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, false);
|
||||
process(squelch, 1E-5f, 0.1, SAMPLE_RATE);
|
||||
process(squelch, NOISE, 0.06, SAMPLE_RATE);
|
||||
assertFalse(squelch.isAudioEnabled());
|
||||
assertTrue(squelch.isUnmuted());
|
||||
process(squelch, 1E-5f, 0.03, SAMPLE_RATE);
|
||||
assertTrue(squelch.isAudioEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowFrequencyModulationDoesNotDisableAudio()
|
||||
{
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, false);
|
||||
float threshold = (float)Math.pow(10.0, THRESHOLD_DB / 10.0);
|
||||
process(squelch, threshold * 2.0f, 0.1, SAMPLE_RATE);
|
||||
|
||||
//Carrier 3 dB over the open threshold with 100% modulation at 100 Hz
|
||||
for(int x = 0; x < SAMPLE_RATE * 2; x++)
|
||||
{
|
||||
double envelope = 1.0 + Math.cos(2.0 * Math.PI * 100.0 * x / SAMPLE_RATE);
|
||||
squelch.process((float)(threshold * 2.0 / 1.5 * envelope * envelope));
|
||||
assertTrue(squelch.isAudioEnabled(), "Audio disabled at sample " + x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample powers with the exponential distribution of complex gaussian noise.
|
||||
*/
|
||||
private static void noise(AdaptiveSquelch squelch, float power, double seconds, int sampleRate, Random random)
|
||||
{
|
||||
for(int x = 0; x < (int)(seconds * sampleRate); x++)
|
||||
{
|
||||
squelch.process((float)(-Math.log(1.0 - random.nextDouble()) * power));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample powers for a steady carrier with noise 20 dB below the carrier
|
||||
*/
|
||||
private static void carrier(AdaptiveSquelch squelch, float power, double seconds, int sampleRate, Random random)
|
||||
{
|
||||
double sigma = Math.sqrt(power * 0.01 / 2.0);
|
||||
double amplitude = Math.sqrt(power);
|
||||
|
||||
for(int x = 0; x < (int)(seconds * sampleRate); x++)
|
||||
{
|
||||
double i = amplitude + sigma * random.nextGaussian();
|
||||
double q = sigma * random.nextGaussian();
|
||||
squelch.process((float)(i * i + q * q));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void thresholdBelowNoiseFloorDoesNotLatchOpen()
|
||||
{
|
||||
Random random = new Random(1);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
float noise = (float)Math.pow(10.0, -7.4); //-74 dB is above the -78 dB threshold
|
||||
|
||||
for(int x = 0; x < SAMPLE_RATE * 30; x++)
|
||||
{
|
||||
squelch.process((float)(-Math.log(1.0 - random.nextDouble()) * noise));
|
||||
assertTrue(squelch.isMuted(), "Opened at sample " + x);
|
||||
}
|
||||
|
||||
assertEquals(-68.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transmissionOpensOverLearnedNoiseFloor()
|
||||
{
|
||||
Random random = new Random(2);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
noise(squelch, 4E-8f, 2.0, SAMPLE_RATE, random); //-74 dB
|
||||
carrier(squelch, 4E-7f, 1.0, SAMPLE_RATE, random); //-64 dB
|
||||
assertTrue(squelch.isAudioEnabled());
|
||||
noise(squelch, 4E-8f, 0.5, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isMuted());
|
||||
assertEquals(-68.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void userMarginAboveMinimumIsKept()
|
||||
{
|
||||
Random random = new Random(3);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
noise(squelch, 1E-9f, 2.0, SAMPLE_RATE, random); //-90 dB, with a -78 dB threshold
|
||||
assertEquals(THRESHOLD_DB, squelch.getSquelchThreshold(), 0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoTrackKeepsDecibelMarginWhenNoiseFloorRises()
|
||||
{
|
||||
Random random = new Random(4);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
noise(squelch, 1E-9f, 2.0, SAMPLE_RATE, random); //-90 dB
|
||||
|
||||
//Raise the noise floor 16 dB in steps that stay under the squelch threshold.
|
||||
for(int step = 1; step <= 8; step++)
|
||||
{
|
||||
noise(squelch, 1E-9f * (float)Math.pow(10.0, step * 0.2), 11.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isMuted(), "Opened at step " + step);
|
||||
}
|
||||
|
||||
assertEquals(THRESHOLD_DB + 16.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void steadyCarrierPresentAtStartPassesUntilTimeoutThenMutes()
|
||||
{
|
||||
Random random = new Random(5);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
carrier(squelch, 2.5E-7f, 59.0, SAMPLE_RATE, random); //-66 dB
|
||||
assertTrue(squelch.isAudioEnabled());
|
||||
carrier(squelch, 2.5E-7f, 3.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isMuted());
|
||||
carrier(squelch, 2.5E-7f, 30.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isMuted());
|
||||
}
|
||||
|
||||
@Test
|
||||
void steadyCarrierAfterNoisePassesUntilTimeoutThenMutes()
|
||||
{
|
||||
Random random = new Random(6);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
noise(squelch, 4E-8f, 2.0, SAMPLE_RATE, random);
|
||||
carrier(squelch, 4E-7f, 59.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isAudioEnabled());
|
||||
carrier(squelch, 4E-7f, 3.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isMuted());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transmissionsAreHeardAfterCarrierTimeoutEnds()
|
||||
{
|
||||
Random random = new Random(7);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
noise(squelch, 4E-8f, 2.0, SAMPLE_RATE, random);
|
||||
carrier(squelch, 4E-7f, 63.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isMuted());
|
||||
|
||||
noise(squelch, 4E-8f, 1.0, SAMPLE_RATE, random);
|
||||
assertEquals(-68.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
carrier(squelch, 4E-7f, 1.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isAudioEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoTrackDisabledUsesUserThreshold()
|
||||
{
|
||||
Random random = new Random(8);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, false);
|
||||
noise(squelch, 4E-8f, 2.0, SAMPLE_RATE, random); //Noise above the threshold opens the squelch
|
||||
assertTrue(squelch.isUnmuted());
|
||||
assertEquals(THRESHOLD_DB, squelch.getSquelchThreshold(), 0.01f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the threshold and returns the user threshold notification value.
|
||||
*/
|
||||
private static double requestThreshold(AdaptiveSquelch squelch, double threshold)
|
||||
{
|
||||
List<SourceEvent> events = new ArrayList<>();
|
||||
squelch.setSourceEventListener(events::add);
|
||||
squelch.receive(SourceEvent.requestSquelchThreshold(null, threshold));
|
||||
squelch.setSourceEventListener(null);
|
||||
return events.stream().filter(event -> event.getEvent() == SourceEvent.Event.NOTIFICATION_SQUELCH_USER_THRESHOLD)
|
||||
.findFirst().orElseThrow().getValue().doubleValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void thresholdChangeDuringTimedOutCarrierIsRelativeToNoiseFloor()
|
||||
{
|
||||
Random random = new Random(9);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
noise(squelch, 4E-8f, 2.0, SAMPLE_RATE, random); //-74 dB with a -68 dB threshold
|
||||
carrier(squelch, 4E-7f, 63.0, SAMPLE_RATE, random); //-64 dB with a -58 dB threshold
|
||||
assertEquals(-58.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
|
||||
//One dB up from the displayed threshold saves one dB over the -68 dB threshold, not -57 dB
|
||||
assertEquals(-67.0, requestThreshold(squelch, squelch.getSquelchThreshold() + 1.0), 0.5);
|
||||
assertEquals(-57.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
|
||||
//The setting applies when the carrier ends
|
||||
noise(squelch, 4E-8f, 1.0, SAMPLE_RATE, random);
|
||||
assertEquals(-67.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void thresholdChangeBelowMinimumMarginIsLimited()
|
||||
{
|
||||
Random random = new Random(10);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
noise(squelch, 4E-8f, 2.0, SAMPLE_RATE, random); //-74 dB
|
||||
assertEquals(-68.0, requestThreshold(squelch, -72.0), 0.5);
|
||||
assertEquals(-68.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void thresholdChangeWithAutoTrackDisabledIsUnchanged()
|
||||
{
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, false);
|
||||
assertEquals(-72.0, requestThreshold(squelch, -72.0), 0.01);
|
||||
assertEquals(-72.0f, squelch.getSquelchThreshold(), 0.01f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample powers for a carrier with noise 20 dB below the carrier and bursts of low frequency voice-like modulation.
|
||||
*/
|
||||
private static void voiceCarrier(AdaptiveSquelch squelch, float power, double seconds, int sampleRate, Random random)
|
||||
{
|
||||
double sigma = Math.sqrt(power * 0.01 / 2.0);
|
||||
double amplitude = Math.sqrt(power);
|
||||
|
||||
for(int x = 0; x < (int)(seconds * sampleRate); x++)
|
||||
{
|
||||
double time = (double)x / sampleRate;
|
||||
double modulation = 0.3 * Math.cos(2.0 * Math.PI * 500.0 * time) + 0.2 * Math.cos(2.0 * Math.PI * 800.0 * time);
|
||||
double i = amplitude * (1.0 + modulation) + sigma * random.nextGaussian();
|
||||
double q = sigma * random.nextGaussian();
|
||||
squelch.process((float)(i * i + q * q));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void carrierWithVoiceModulationDoesNotTimeOut()
|
||||
{
|
||||
Random random = new Random(11);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
noise(squelch, 4E-8f, 2.0, SAMPLE_RATE, random);
|
||||
voiceCarrier(squelch, 4E-7f, 190.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isAudioEnabled());
|
||||
assertEquals(-68.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void voiceOnTimedOutCarrierReopensSquelch()
|
||||
{
|
||||
Random random = new Random(12);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
noise(squelch, 4E-8f, 2.0, SAMPLE_RATE, random);
|
||||
carrier(squelch, 4E-7f, 63.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isMuted());
|
||||
voiceCarrier(squelch, 4E-7f, 4.0, SAMPLE_RATE, random);
|
||||
assertEquals(-68.0f, squelch.getSquelchThreshold(), 0.5f);
|
||||
assertTrue(squelch.isAudioEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void voiceOnCarrierPresentAtStartReopensSquelch()
|
||||
{
|
||||
Random random = new Random(13);
|
||||
AdaptiveSquelch squelch = squelch(SAMPLE_RATE, true);
|
||||
carrier(squelch, 4E-7f, 63.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isMuted());
|
||||
voiceCarrier(squelch, 4E-7f, 4.0, SAMPLE_RATE, random);
|
||||
assertTrue(squelch.isAudioEnabled());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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 static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Random;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for the voice modulation detector
|
||||
*/
|
||||
class VoiceModulationDetectorTest
|
||||
{
|
||||
private static final double[] BANDWIDTHS = {3000.0, 5000.0, 6000.0, 8333.0, 15000.0, 25000.0};
|
||||
|
||||
/**
|
||||
* Fills the detector with a carrier having white envelope noise and optional modulation.
|
||||
*/
|
||||
private static boolean analyze(double sampleRate, double bandwidth, double modulation, Random random)
|
||||
{
|
||||
int samples = (int)(sampleRate / 2.0);
|
||||
VoiceModulationDetector detector = new VoiceModulationDetector(sampleRate, bandwidth, samples);
|
||||
|
||||
for(int x = 0; x < samples; x++)
|
||||
{
|
||||
double time = x / sampleRate;
|
||||
detector.receive((float)(1.0 + modulation * Math.cos(2.0 * Math.PI * 450.0 * time) + 0.1 * random.nextGaussian()));
|
||||
}
|
||||
|
||||
return detector.analyze();
|
||||
}
|
||||
|
||||
@Test
|
||||
void flatEnvelopeNoiseIsNotVoice()
|
||||
{
|
||||
Random random = new Random(1);
|
||||
|
||||
for(double bandwidth: BANDWIDTHS)
|
||||
{
|
||||
for(double sampleRate: new double[]{25000.0, 39062.5, 50000.0})
|
||||
{
|
||||
for(int x = 0; x < 50; x++)
|
||||
{
|
||||
assertFalse(analyze(sampleRate, bandwidth, 0.0, random), "Bandwidth " + bandwidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowFrequencyModulationIsVoice()
|
||||
{
|
||||
Random random = new Random(2);
|
||||
|
||||
for(double bandwidth: BANDWIDTHS)
|
||||
{
|
||||
assertTrue(analyze(39062.5, bandwidth, 0.2, random), "Bandwidth " + bandwidth);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialBufferIsNotAnalyzed()
|
||||
{
|
||||
VoiceModulationDetector detector = new VoiceModulationDetector(25000.0, 6000.0, 12500);
|
||||
detector.receive(1.0f);
|
||||
assertFalse(detector.analyze());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
|
||||
import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog.Bandwidth;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for the channel sample rates supported by the AM decoder.
|
||||
*/
|
||||
class AMDecoderSampleRateTest
|
||||
{
|
||||
private static final int BUFFER = 2048;
|
||||
|
||||
private static AMDecoder decoder(Bandwidth bandwidth, double sampleRate)
|
||||
{
|
||||
DecodeConfigAM config = new DecodeConfigAM();
|
||||
config.setBandwidth(bandwidth);
|
||||
AMDecoder decoder = new AMDecoder(config);
|
||||
decoder.getSourceEventListener().receive(SourceEvent.sampleRateChange(sampleRate));
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultBandwidthIsSixKilohertz()
|
||||
{
|
||||
assertEquals(Bandwidth.BW_6_0, new DecodeConfigAM().getBandwidth());
|
||||
assertTrue(Bandwidth.BW_6_0.isAM());
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimumChannelSampleRateIsAtLeastTwiceTheBandwidth()
|
||||
{
|
||||
for(Bandwidth bandwidth: Bandwidth.AM_BANDWIDTHS)
|
||||
{
|
||||
DecodeConfigAM config = new DecodeConfigAM();
|
||||
config.setBandwidth(bandwidth);
|
||||
assertTrue(config.getChannelSpecification().getMinimumSampleRate() >= 2.0 * bandwidth.getValue(), bandwidth.name());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Follows the heterodyne channel source decimation selection for tuner sample rates, including 1.8 and 3.6 MHz which
|
||||
* previously produced an unsupported 28,125 Hz sample rate for the default 15 kHz bandwidth.
|
||||
*/
|
||||
@Test
|
||||
void heterodyneChannelRatesAreSupported()
|
||||
{
|
||||
double[] tunerRates = {1.8E6, 2.0E6, 2.4E6, 2.56E6, 3.0E6, 3.2E6, 3.6E6, 6.0E6, 7.2E6, 8.0E6, 10.0E6, 20.0E6};
|
||||
|
||||
for(Bandwidth bandwidth: Bandwidth.AM_BANDWIDTHS)
|
||||
{
|
||||
DecodeConfigAM config = new DecodeConfigAM();
|
||||
config.setBandwidth(bandwidth);
|
||||
ChannelSpecification specification = config.getChannelSpecification();
|
||||
|
||||
for(double tunerRate: tunerRates)
|
||||
{
|
||||
int decimation = DecimationFilterFactory.getDecimationRate((int)(tunerRate / specification.getMinimumSampleRate()));
|
||||
decoder(bandwidth, tunerRate / decimation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyphase channel rates don't use the channel specification minimum rate.
|
||||
*/
|
||||
@Test
|
||||
void ratesBelowTwiceTheBandwidthDecodeAudio()
|
||||
{
|
||||
for(double sampleRate: new double[]{25000.0, 28125.0})
|
||||
{
|
||||
AMDecoder decoder = decoder(Bandwidth.BW_15_0, sampleRate);
|
||||
List<float[]> audio = new ArrayList<>();
|
||||
decoder.setBufferListener(audio::add);
|
||||
int sample = 0;
|
||||
|
||||
for(int b = 0; b < (int)(sampleRate * 2.0 / BUFFER); b++)
|
||||
{
|
||||
float[] i = new float[BUFFER];
|
||||
float[] q = new float[BUFFER];
|
||||
|
||||
for(int x = 0; x < BUFFER; x++)
|
||||
{
|
||||
i[x] = 0.01f * (1.0f + 0.8f * (float)Math.cos(2.0 * Math.PI * 1000.0 * sample++ / sampleRate));
|
||||
}
|
||||
|
||||
decoder.receive(new ComplexSamples(i, q, 0L));
|
||||
}
|
||||
|
||||
float peak = 0.0f;
|
||||
|
||||
for(float[] buffer: audio.subList(audio.size() / 2, audio.size()))
|
||||
{
|
||||
for(float value: buffer)
|
||||
{
|
||||
peak = Math.max(peak, Math.abs(value));
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(0.6f, peak, 0.05f, "Peak at sample rate " + sampleRate);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void rateBelowTheBandwidthIsRejected()
|
||||
{
|
||||
assertThrows(IllegalStateException.class, () -> decoder(Bandwidth.BW_25_0, 20000.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for the source event requests that the squelch controls use to get the current squelch settings.
|
||||
*/
|
||||
class AMDecoderSquelchRequestTest
|
||||
{
|
||||
private static List<SourceEvent> events(DecodeConfigAM config, SourceEvent request)
|
||||
{
|
||||
AMDecoder decoder = new AMDecoder(config);
|
||||
List<SourceEvent> events = new ArrayList<>();
|
||||
decoder.setSourceEventListener(events::add);
|
||||
decoder.getSourceEventListener().receive(request);
|
||||
return events;
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentThresholdRequestIsAnswered()
|
||||
{
|
||||
DecodeConfigAM config = new DecodeConfigAM();
|
||||
config.setSquelchThreshold(-70);
|
||||
List<SourceEvent> events = events(config, SourceEvent.requestCurrentSquelchThreshold());
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(SourceEvent.Event.NOTIFICATION_SQUELCH_THRESHOLD, events.getFirst().getEvent());
|
||||
assertEquals(-70.0, events.getFirst().getValue().doubleValue(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentAutoTrackRequestIsAnswered()
|
||||
{
|
||||
DecodeConfigAM config = new DecodeConfigAM();
|
||||
config.setSquelchAutoTrack(false);
|
||||
List<SourceEvent> events = events(config, SourceEvent.requestCurrentSquelchAutoTrack());
|
||||
assertEquals(1, events.size());
|
||||
assertEquals(SourceEvent.Event.NOTIFICATION_SQUELCH_AUTO_TRACK, events.getFirst().getEvent());
|
||||
assertEquals(0, events.getFirst().getValue().intValue());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user