From 85bfd212578c8bf1a53bac71e5d83043f5a80e82 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Fri, 30 Oct 2026 08:42:49 -0400 Subject: [PATCH] 2537 Complex Vector Oscillator (256/512) Lanes Can Drift Apart Over Time (#2538) * #2535 Heterodyne Channelizer enhancements and corrections. * #2537 Complex vector oscillator SIMD lane drift. Realigns the SIMD lanes every 262,144 samples. At frequencies where the rotation for a set of lanes is nearly a whole or half turn, rounding affects each lane differently and the lanes drift apart until the next frequency change, producing spurs at multiples of the sample rate divided by the lane count. Lane phase error at 3125 Hz with a 50 kHz sample rate was about -38 dBc after one hour and is now about -83 dBc with 16 lanes and -73 dBc with 8 lanes. Co-Authored-By: Claude Opus 5.5 * #2537 test updates * #2537 test updates --------- Co-authored-by: denny Co-authored-by: Claude Opus 5.5 --- .../buffer/NativeSampleDelayBuffer.java | 3 +- .../dsheirer/buffer/SharedNativeBuffer.java | 114 ++++++++ .../channel/ChannelProcessingManager.java | 5 + .../event/ChannelStartProcessingRequest.java | 20 ++ .../dsheirer/dsp/filter/FilterFactory.java | 15 +- .../decimate/DecimationFilterFactory.java | 17 +- .../decimate/IRealDecimationFilter.java | 12 + .../decimate/RealDecimateX1024Filter.java | 4 +- .../decimate/RealDecimateX128Filter.java | 4 +- .../decimate/RealDecimateX16Filter.java | 4 +- .../decimate/RealDecimateX256Filter.java | 4 +- .../filter/decimate/RealDecimateX2Filter.java | 14 +- .../decimate/RealDecimateX32Filter.java | 4 +- .../filter/decimate/RealDecimateX4Filter.java | 4 +- .../decimate/RealDecimateX512Filter.java | 4 +- .../decimate/RealDecimateX64Filter.java | 4 +- .../filter/decimate/RealDecimateX8Filter.java | 4 +- .../RealHalfBandDecimationFilter.java | 8 +- ...alHalfBandDecimationFilter11Tap128Bit.java | 8 +- ...alHalfBandDecimationFilter11Tap256Bit.java | 8 +- ...alHalfBandDecimationFilter11Tap512Bit.java | 8 +- ...ealHalfBandDecimationFilter11Tap64Bit.java | 8 +- ...torRealHalfBandDecimationFilter128Bit.java | 8 +- ...alHalfBandDecimationFilter15Tap128Bit.java | 8 +- ...alHalfBandDecimationFilter15Tap256Bit.java | 8 +- ...alHalfBandDecimationFilter15Tap512Bit.java | 8 +- ...ealHalfBandDecimationFilter15Tap64Bit.java | 8 +- ...alHalfBandDecimationFilter23Tap128Bit.java | 8 +- ...alHalfBandDecimationFilter23Tap256Bit.java | 8 +- ...alHalfBandDecimationFilter23Tap512Bit.java | 8 +- ...ealHalfBandDecimationFilter23Tap64Bit.java | 8 +- ...torRealHalfBandDecimationFilter256Bit.java | 8 +- ...torRealHalfBandDecimationFilter512Bit.java | 8 +- ...alHalfBandDecimationFilter63Tap128Bit.java | 8 +- ...alHalfBandDecimationFilter63Tap256Bit.java | 8 +- ...alHalfBandDecimationFilter63Tap512Bit.java | 8 +- ...ealHalfBandDecimationFilter63Tap64Bit.java | 8 +- ...ctorRealHalfBandDecimationFilter64Bit.java | 8 +- ...ealHalfBandDecimationFilterDefaultBit.java | 8 +- .../dsheirer/dsp/mixer/ComplexMixer.java | 31 +- .../dsp/mixer/ScalarComplexMixer.java | 15 +- .../dsp/mixer/VectorComplexMixer.java | 17 +- .../dsp/oscillator/AWGNOscillator.java | 19 +- .../dsp/oscillator/IComplexOscillator.java | 7 + .../oscillator/ScalarComplexOscillator.java | 9 +- .../oscillator/VectorComplexOscillator.java | 75 ++++- .../decode/dmr/DMRTrafficChannelManager.java | 6 +- .../nxdn/NXDNTrafficChannelManager.java | 7 +- .../decode/p25/P25TrafficChannelManager.java | 1 + .../channel/HalfBandTunerChannelSource.java | 58 +++- .../tuner/channel/TunerChannelSource.java | 14 +- .../HeterodyneChannelSourceManager.java | 160 ++++++++--- .../vector/calibrate/CalibrationType.java | 4 +- .../ComplexOscillatorCalibration.java | 9 +- .../buffer/SharedNativeBufferTest.java | 120 ++++++++ .../decimate/DecimationFilterFactoryTest.java | 158 +++++++++++ .../decimate/IntermediateOutputReuseTest.java | 113 ++++++++ .../VectorComplexOscillatorTest.java | 139 +++++++++ .../HalfBandTunerChannelSourceTest.java | 99 +++++++ .../HeterodyneChannelSourceManagerTest.java | 268 ++++++++++++++++++ 60 files changed, 1570 insertions(+), 171 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/buffer/SharedNativeBuffer.java create mode 100644 src/test/java/io/github/dsheirer/buffer/SharedNativeBufferTest.java create mode 100644 src/test/java/io/github/dsheirer/dsp/filter/decimate/DecimationFilterFactoryTest.java create mode 100644 src/test/java/io/github/dsheirer/dsp/filter/decimate/IntermediateOutputReuseTest.java create mode 100644 src/test/java/io/github/dsheirer/dsp/oscillator/VectorComplexOscillatorTest.java create mode 100644 src/test/java/io/github/dsheirer/source/tuner/channel/HalfBandTunerChannelSourceTest.java create mode 100644 src/test/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManagerTest.java diff --git a/src/main/java/io/github/dsheirer/buffer/NativeSampleDelayBuffer.java b/src/main/java/io/github/dsheirer/buffer/NativeSampleDelayBuffer.java index c25792bf..4e2b7042 100644 --- a/src/main/java/io/github/dsheirer/buffer/NativeSampleDelayBuffer.java +++ b/src/main/java/io/github/dsheirer/buffer/NativeSampleDelayBuffer.java @@ -116,7 +116,8 @@ public class NativeSampleDelayBuffer implements Listener actionRequest = mActionQueue.poll(); } - mBroadcaster.receive(samples); + //The delay queue stores the unconverted buffer since it's more compact than the converted samples + mBroadcaster.receive(mBroadcaster.getListenerCount() > 1 ? new SharedNativeBuffer(samples) : samples); //Store the new buffer in the delay queue and increment the pointer mDelayBuffer[mDelayBufferPointer++] = samples; diff --git a/src/main/java/io/github/dsheirer/buffer/SharedNativeBuffer.java b/src/main/java/io/github/dsheirer/buffer/SharedNativeBuffer.java new file mode 100644 index 00000000..8b95d3e4 --- /dev/null +++ b/src/main/java/io/github/dsheirer/buffer/SharedNativeBuffer.java @@ -0,0 +1,114 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ +package io.github.dsheirer.buffer; + +import io.github.dsheirer.sample.complex.ComplexSamples; +import io.github.dsheirer.sample.complex.InterleavedComplexSamples; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Native buffer wrapper that lets multiple channels share one conversion of the native samples. Each channel's + * iterator returns the same complex samples instances, on the channel's own thread, so consumers must not modify the + * samples. The converted samples are released when the channels release this wrapper. + */ +public class SharedNativeBuffer implements INativeBuffer +{ + private final INativeBuffer mNativeBuffer; + private final List mConverted = new ArrayList<>(); + private Iterator mConverter; + + /** + * Constructs an instance + * @param nativeBuffer to convert + */ + public SharedNativeBuffer(INativeBuffer nativeBuffer) + { + mNativeBuffer = nativeBuffer; + } + + /** + * Indicates if the converted samples include the index, converting the next samples as needed. + */ + private synchronized boolean hasConverted(int index) + { + if(mConverter == null) + { + mConverter = mNativeBuffer.iterator(); + } + + if(index == mConverted.size() && mConverter.hasNext()) + { + mConverted.add(mConverter.next()); + } + + return index < mConverted.size(); + } + + private synchronized ComplexSamples getConverted(int index) + { + return mConverted.get(index); + } + + @Override + public Iterator iterator() + { + return new Iterator<>() + { + private int mIndex = 0; + + @Override + public boolean hasNext() + { + return hasConverted(mIndex); + } + + @Override + public ComplexSamples next() + { + if(!hasNext()) + { + throw new NoSuchElementException("No more samples"); + } + + return getConverted(mIndex++); + } + }; + } + + @Override + public Iterator iteratorInterleaved() + { + return mNativeBuffer.iteratorInterleaved(); + } + + @Override + public int sampleCount() + { + return mNativeBuffer.sampleCount(); + } + + @Override + public long getTimestamp() + { + return mNativeBuffer.getTimestamp(); + } +} diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java index 3d1dceba..792c04ee 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java @@ -418,6 +418,11 @@ public class ChannelProcessingManager implements Listener throw new ChannelException("No Tuner Available"); } + if(source instanceof TunerChannelSource tunerChannelSource) + { + tunerChannelSource.setStartTimestamp(request.getStartTimestamp()); + } + ProcessingChain processingChain = new ProcessingChain(channel, mAliasModel); //Certain decoders aggregate the decode events in the parent channel that also includes any events produced diff --git a/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java b/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java index 620eb5bf..7fad7c24 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java +++ b/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java @@ -42,6 +42,7 @@ public class ChannelStartProcessingRequest extends ModuleEventBusMessage private DecodeEventHistory mParentDecodeEventHistory; private DecodeEventHistory mChildDecodeEventHistory; private boolean mPersistentAttempt; + private long mStartTimestamp; /** * Constructs an instance @@ -109,6 +110,25 @@ public class ChannelStartProcessingRequest extends ModuleEventBusMessage return mPersistentAttempt; } + /** + * Sets the timestamp of the channel grant so that a tuner channel source with a sample delay buffer can preload the + * channel with the samples that occurred after the grant, while the grant was being decoded and processed. + * @param timestamp of the channel grant in milliseconds since epoch. + */ + public void setStartTimestamp(long timestamp) + { + mStartTimestamp = timestamp; + } + + /** + * Timestamp for the oldest samples requested for the channel. + * @return timestamp in milliseconds since epoch, or 0 if the channel should start with current samples. + */ + public long getStartTimestamp() + { + return mStartTimestamp; + } + /** * Channel to start processing * @return channel to start diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index bd06a9f9..12453286 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -1619,7 +1619,8 @@ public class FilterFactory /** * Creates a symmetrical half-band filter where all odd-indexed coefficients except center are zero - * valued. + * valued. The non-center coefficients are normalized to sum to 0.5 to compensate for the window, which provides + * unity gain at DC and a null at the Nyquist frequency. * @param length N is odd where m is an integer multiple in the formula: N = 4m + 3 (e.g. 7, 11, 15, etc.) * @param windowType to apply to the filter. * @return coefficients @@ -1652,6 +1653,18 @@ public class FilterFactory } } + double sum = 0.0; + + for(int x = 0; x < length; x += 2) + { + sum += taps[x]; + } + + for(int x = 0; x < length; x += 2) + { + taps[x] = (float)(taps[x] * 0.5 / sum); + } + return taps; } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/DecimationFilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/DecimationFilterFactory.java index 0bd3ade0..8a5ed842 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/DecimationFilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/DecimationFilterFactory.java @@ -19,6 +19,7 @@ package io.github.dsheirer.dsp.filter.decimate; +import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,11 +29,12 @@ import org.slf4j.LoggerFactory; public class DecimationFilterFactory { private static final Logger mLog = LoggerFactory.getLogger(DecimationFilterFactory.class); - private static final int[] SUPPORTED_RATES = new int[]{0,2,4,8,16,32,64,128,256,512,1024}; + private static final int[] SUPPORTED_RATES = new int[]{1,2,4,8,16,32,64,128,256,512,1024}; /** - * Creates a real-valued decimation filter for float array sample buffers providing greater than 100 dB of - * attenuation for out of band signal aliases. Supports power of 2 decimation rates up to 1024. + * Creates a real-valued decimation filter for float array sample buffers providing approximately 70 dB of + * attenuation for out of band signal aliases, when the signal bandwidth is no more than half of the decimated + * sample rate. Supports power of 2 decimation rates up to 1024. * * @param decimationRate requested @see SUPPORTED_RATES * @return constructed decimation filter @@ -66,20 +68,21 @@ public class DecimationFilterFactory return new RealDecimateX1024Filter(); default: throw new IllegalArgumentException("Unsupported decimation rate: " + decimationRate + - ". Supported decimation rates are:" + SUPPORTED_RATES); + ". Supported decimation rates are:" + Arrays.toString(SUPPORTED_RATES)); } } /** * Finds the greatest factor of 2 decimation rate that is less than the requested rate. * @param requestedRate for decimation - * @return factor of 2 decimation rate that is closest to the requested rate + * @return factor of 2 decimation rate that is closest to the requested rate, or 1 (no decimation) when the + * requested rate is too small to allow decimation. */ public static int getDecimationRate(int requestedRate) { if(requestedRate < 0) { - throw new IllegalArgumentException("Requested decimation rate must be greater than 2"); + throw new IllegalArgumentException("Requested decimation rate must not be negative"); } for(int x = SUPPORTED_RATES.length - 1; x >= 0; x--) @@ -90,6 +93,6 @@ public class DecimationFilterFactory } } - return 2; + return 1; } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/IRealDecimationFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/IRealDecimationFilter.java index a595afb5..c9b99e86 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/IRealDecimationFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/IRealDecimationFilter.java @@ -25,4 +25,16 @@ package io.github.dsheirer.dsp.filter.decimate; public interface IRealDecimationFilter { float[] decimateReal(float[] samples); + + /** + * Decimates the samples using the output array when the implementation supports it, to avoid an allocation. This + * is for intermediate results where the caller retains ownership of the output array. + * @param samples to decimate + * @param output array to reuse, sized to the decimated sample count + * @return decimated samples, which may be a different array than the output argument. + */ + default float[] decimateReal(float[] samples, float[] output) + { + return decimateReal(samples); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX1024Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX1024Filter.java index 030d8c5c..fd65fadb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX1024Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX1024Filter.java @@ -31,6 +31,7 @@ public class RealDecimateX1024Filter extends RealDecimateX512Filter private static final int DECIMATE_BY_1024_FILTER_LENGTH = 11; private static final WindowType DECIMATE_BY_1024_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; + private float[] mOutput; /** * Constructs the decimation filter. @@ -47,6 +48,7 @@ public class RealDecimateX1024Filter extends RealDecimateX512Filter validate(samples, VALIDATION_LENGTH); //Decimate by this filter, then by the parent decimation filter - return super.decimateReal(mFilter.decimateReal(samples)); + mOutput = getIntermediateOutput(mOutput, samples.length / 2); + return super.decimateReal(mFilter.decimateReal(samples, mOutput)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX128Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX128Filter.java index b6579a5c..a2789033 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX128Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX128Filter.java @@ -31,6 +31,7 @@ public class RealDecimateX128Filter extends RealDecimateX64Filter private static final int DECIMATE_BY_128_FILTER_LENGTH = 11; private static final WindowType DECIMATE_BY_128_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; + private float[] mOutput; /** * Constructs the decimation filter. @@ -47,6 +48,7 @@ public class RealDecimateX128Filter extends RealDecimateX64Filter validate(samples, VALIDATION_LENGTH); //Decimate by this filter, then by the parent decimation filter - return super.decimateReal(mFilter.decimateReal(samples)); + mOutput = getIntermediateOutput(mOutput, samples.length / 2); + return super.decimateReal(mFilter.decimateReal(samples, mOutput)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX16Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX16Filter.java index 5f9296c6..96b5c33b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX16Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX16Filter.java @@ -31,6 +31,7 @@ public class RealDecimateX16Filter extends RealDecimateX8Filter private static final int DECIMATE_BY_16_FILTER_LENGTH = 15; private static final WindowType DECIMATE_BY_16_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; + private float[] mOutput; /** * Constructs the decimation filter. @@ -47,6 +48,7 @@ public class RealDecimateX16Filter extends RealDecimateX8Filter validate(samples, VALIDATION_LENGTH); //Decimate by this filter, then by the parent decimation filter - return super.decimateReal(mFilter.decimateReal(samples)); + mOutput = getIntermediateOutput(mOutput, samples.length / 2); + return super.decimateReal(mFilter.decimateReal(samples, mOutput)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX256Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX256Filter.java index 98e15199..7c726ed0 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX256Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX256Filter.java @@ -31,6 +31,7 @@ public class RealDecimateX256Filter extends RealDecimateX128Filter private static final int DECIMATE_BY_256_FILTER_LENGTH = 11; private static final WindowType DECIMATE_BY_256_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; + private float[] mOutput; /** * Constructs the decimation filter. @@ -47,6 +48,7 @@ public class RealDecimateX256Filter extends RealDecimateX128Filter validate(samples, VALIDATION_LENGTH); //Decimate by this filter, then by the parent decimation filter - return super.decimateReal(mFilter.decimateReal(samples)); + mOutput = getIntermediateOutput(mOutput, samples.length / 2); + return super.decimateReal(mFilter.decimateReal(samples, mOutput)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX2Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX2Filter.java index 9cc41832..6606725c 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX2Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX2Filter.java @@ -28,7 +28,7 @@ import io.github.dsheirer.dsp.window.WindowType; public class RealDecimateX2Filter implements IRealDecimationFilter { private static final int DECIMATE_BY_2_FILTER_LENGTH = 63; - private static final WindowType DECIMATE_BY_2_WINDOW_TYPE = WindowType.HAMMING; + private static final WindowType DECIMATE_BY_2_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; /** @@ -45,6 +45,18 @@ public class RealDecimateX2Filter implements IRealDecimationFilter return mFilter.decimateReal(samples); } + /** + * Provides an array to reuse for the output of an intermediate decimation stage. The output of the final decimate + * by 2 stage isn't reused since that array is passed to the consumer. + * @param output array from the previous invocation, or null + * @param length required + * @return the array when it has the required length, or a new array. + */ + protected static float[] getIntermediateOutput(float[] output, int length) + { + return output != null && output.length == length ? output : new float[length]; + } + /** * Validates that the samples length is an integer multiple of the specified validation multiple argument. * @param samples to validate diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX32Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX32Filter.java index 1a6bc192..8ccf044b 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX32Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX32Filter.java @@ -31,6 +31,7 @@ public class RealDecimateX32Filter extends RealDecimateX16Filter private static final int DECIMATE_BY_32_FILTER_LENGTH = 11; private static final WindowType DECIMATE_BY_32_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; + private float[] mOutput; /** * Constructs the decimation filter. @@ -47,6 +48,7 @@ public class RealDecimateX32Filter extends RealDecimateX16Filter validate(samples, VALIDATION_LENGTH); //Decimate by this filter, then by the parent decimation filter - return super.decimateReal(mFilter.decimateReal(samples)); + mOutput = getIntermediateOutput(mOutput, samples.length / 2); + return super.decimateReal(mFilter.decimateReal(samples, mOutput)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX4Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX4Filter.java index 469beae1..400f18e8 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX4Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX4Filter.java @@ -31,6 +31,7 @@ public class RealDecimateX4Filter extends RealDecimateX2Filter private static final int DECIMATE_BY_4_FILTER_LENGTH = 23; private static final WindowType DECIMATE_BY_4_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; + private float[] mOutput; /** * Constructs the decimation filter. @@ -47,6 +48,7 @@ public class RealDecimateX4Filter extends RealDecimateX2Filter validate(samples, VALIDATION_LENGTH); //Decimate by this filter, then by the parent decimation filter - return super.decimateReal(mFilter.decimateReal(samples)); + mOutput = getIntermediateOutput(mOutput, samples.length / 2); + return super.decimateReal(mFilter.decimateReal(samples, mOutput)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX512Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX512Filter.java index 70e78dfb..11ac0313 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX512Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX512Filter.java @@ -31,6 +31,7 @@ public class RealDecimateX512Filter extends RealDecimateX256Filter private static final int DECIMATE_BY_512_FILTER_LENGTH = 11; private static final WindowType DECIMATE_BY_512_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; + private float[] mOutput; /** * Constructs the decimation filter. @@ -47,6 +48,7 @@ public class RealDecimateX512Filter extends RealDecimateX256Filter validate(samples, VALIDATION_LENGTH); //Decimate by this filter, then by the parent decimation filter - return super.decimateReal(mFilter.decimateReal(samples)); + mOutput = getIntermediateOutput(mOutput, samples.length / 2); + return super.decimateReal(mFilter.decimateReal(samples, mOutput)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX64Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX64Filter.java index d18bff5d..656896b4 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX64Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX64Filter.java @@ -31,6 +31,7 @@ public class RealDecimateX64Filter extends RealDecimateX32Filter private static final int DECIMATE_BY_64_FILTER_LENGTH = 11; private static final WindowType DECIMATE_BY_64_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; + private float[] mOutput; /** * Constructs the decimation filter. @@ -47,6 +48,7 @@ public class RealDecimateX64Filter extends RealDecimateX32Filter validate(samples, VALIDATION_LENGTH); //Decimate by this filter, then by the parent decimation filter - return super.decimateReal(mFilter.decimateReal(samples)); + mOutput = getIntermediateOutput(mOutput, samples.length / 2); + return super.decimateReal(mFilter.decimateReal(samples, mOutput)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX8Filter.java b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX8Filter.java index 89267400..44e13795 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX8Filter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/decimate/RealDecimateX8Filter.java @@ -31,6 +31,7 @@ public class RealDecimateX8Filter extends RealDecimateX4Filter private static final int DECIMATE_BY_8_FILTER_LENGTH = 15; private static final WindowType DECIMATE_BY_8_WINDOW_TYPE = WindowType.BLACKMAN; private IRealDecimationFilter mFilter; + private float[] mOutput; /** * Constructs the decimation filter. @@ -47,6 +48,7 @@ public class RealDecimateX8Filter extends RealDecimateX4Filter validate(samples, VALIDATION_LENGTH); //Decimate by this filter, then by the parent decimation filter - return super.decimateReal(mFilter.decimateReal(samples)); + mOutput = getIntermediateOutput(mOutput, samples.length / 2); + return super.decimateReal(mFilter.decimateReal(samples, mOutput)); } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/RealHalfBandDecimationFilter.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/RealHalfBandDecimationFilter.java index 82210c3b..b69f5c23 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/RealHalfBandDecimationFilter.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/RealHalfBandDecimationFilter.java @@ -59,6 +59,12 @@ public class RealHalfBandDecimationFilter implements IRealDecimationFilter } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -87,8 +93,6 @@ public class RealHalfBandDecimationFilter implements IRealDecimationFilter //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - float accumulator = 0.0f; int half = mBufferOverlap / 2; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap128Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap128Bit.java index 0068225d..b16ca0b3 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap128Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap128Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter11Tap128Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter11Tap128Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 4); FloatVector filter3 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 8); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap256Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap256Bit.java index aad736bf..b89a0832 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap256Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap256Bit.java @@ -65,6 +65,12 @@ public class VectorRealHalfBandDecimationFilter11Tap256Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -93,8 +99,6 @@ public class VectorRealHalfBandDecimationFilter11Tap256Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 8); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap512Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap512Bit.java index 6bf03c6e..6fdc0df0 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap512Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap512Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter11Tap512Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter11Tap512Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector product; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap64Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap64Bit.java index ecbcc1df..c9cf1193 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap64Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter11Tap64Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter11Tap64Bit implements IRealDecima } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter11Tap64Bit implements IRealDecima //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 2); FloatVector filter3 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 4); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter128Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter128Bit.java index 87c251ae..b5999dbb 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter128Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter128Bit.java @@ -67,6 +67,12 @@ public class VectorRealHalfBandDecimationFilter128Bit implements IRealDecimation } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -95,8 +101,6 @@ public class VectorRealHalfBandDecimationFilter128Bit implements IRealDecimation //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector accumulator, buffer, filter; for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer += 2) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap128Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap128Bit.java index 18932032..cf63590d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap128Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap128Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter15Tap128Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter15Tap128Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 4); FloatVector filter3 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 8); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap256Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap256Bit.java index 41a32c92..7b3e717d 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap256Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap256Bit.java @@ -65,6 +65,12 @@ public class VectorRealHalfBandDecimationFilter15Tap256Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -93,8 +99,6 @@ public class VectorRealHalfBandDecimationFilter15Tap256Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 8); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap512Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap512Bit.java index b34294b2..af977ae6 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap512Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap512Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter15Tap512Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter15Tap512Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector product; diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap64Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap64Bit.java index e5a358f8..13259a9e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap64Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter15Tap64Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter15Tap64Bit implements IRealDecima } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter15Tap64Bit implements IRealDecima //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 2); FloatVector filter3 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 4); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap128Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap128Bit.java index bab951a0..f3f2b5e3 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap128Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap128Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter23Tap128Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter23Tap128Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 4); FloatVector filter3 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 8); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap256Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap256Bit.java index 91fa52cf..5617bb77 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap256Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap256Bit.java @@ -65,6 +65,12 @@ public class VectorRealHalfBandDecimationFilter23Tap256Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -93,8 +99,6 @@ public class VectorRealHalfBandDecimationFilter23Tap256Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 8); FloatVector filter3 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 16); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap512Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap512Bit.java index f0704b0a..5a1ee9bf 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap512Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap512Bit.java @@ -67,6 +67,12 @@ public class VectorRealHalfBandDecimationFilter23Tap512Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -95,8 +101,6 @@ public class VectorRealHalfBandDecimationFilter23Tap512Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 16); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap64Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap64Bit.java index 97286c50..e6c193d7 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap64Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter23Tap64Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter23Tap64Bit implements IRealDecima } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter23Tap64Bit implements IRealDecima //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector filter1 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 0); FloatVector filter2 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 2); FloatVector filter3 = FloatVector.fromArray(VECTOR_SPECIES, mCoefficients, 4); diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter256Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter256Bit.java index b725780e..82b4eaf2 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter256Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter256Bit.java @@ -67,6 +67,12 @@ public class VectorRealHalfBandDecimationFilter256Bit implements IRealDecimation } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -95,8 +101,6 @@ public class VectorRealHalfBandDecimationFilter256Bit implements IRealDecimation //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector accumulator, buffer, filter; for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer += 2) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter512Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter512Bit.java index e05bec7b..924c1e8e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter512Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter512Bit.java @@ -67,6 +67,12 @@ public class VectorRealHalfBandDecimationFilter512Bit implements IRealDecimation } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -95,8 +101,6 @@ public class VectorRealHalfBandDecimationFilter512Bit implements IRealDecimation //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector accumulator, buffer, filter; for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer += 2) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap128Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap128Bit.java index ef863db3..50b93ccf 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap128Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap128Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter63Tap128Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter63Tap128Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector accumulator, buffer, filter; for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer += 2) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap256Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap256Bit.java index d9139180..79989e87 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap256Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap256Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter63Tap256Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter63Tap256Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector accumulator, buffer, filter; for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer += 2) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap512Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap512Bit.java index 9ca9acee..bff8a8c5 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap512Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap512Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter63Tap512Bit implements IRealDecim } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter63Tap512Bit implements IRealDecim //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector accumulator, buffer, filter; for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer += 2) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap64Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap64Bit.java index 0a49b7ca..51f1a922 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap64Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter63Tap64Bit.java @@ -66,6 +66,12 @@ public class VectorRealHalfBandDecimationFilter63Tap64Bit implements IRealDecima } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -94,8 +100,6 @@ public class VectorRealHalfBandDecimationFilter63Tap64Bit implements IRealDecima //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector accumulator, buffer, filter; for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer += 2) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter64Bit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter64Bit.java index ffb82a5f..842cd3dc 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter64Bit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilter64Bit.java @@ -67,6 +67,12 @@ public class VectorRealHalfBandDecimationFilter64Bit implements IRealDecimationF } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -95,8 +101,6 @@ public class VectorRealHalfBandDecimationFilter64Bit implements IRealDecimationF //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector accumulator, buffer, filter; for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer += 2) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilterDefaultBit.java b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilterDefaultBit.java index be317504..e7222f14 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilterDefaultBit.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/halfband/VectorRealHalfBandDecimationFilterDefaultBit.java @@ -67,6 +67,12 @@ public class VectorRealHalfBandDecimationFilterDefaultBit implements IRealDecima } public float[] decimateReal(float[] samples) + { + return decimateReal(samples, new float[samples.length / 2]); + } + + @Override + public float[] decimateReal(float[] samples, float[] filtered) { if(samples.length % 2 != 0) { @@ -95,8 +101,6 @@ public class VectorRealHalfBandDecimationFilterDefaultBit implements IRealDecima //Copy new sample array into end of buffer System.arraycopy(samples, 0, mBuffer, mBufferOverlap, samples.length); - float[] filtered = new float[samples.length / 2]; - FloatVector accumulator, buffer, filter; for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer += 2) diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/ComplexMixer.java b/src/main/java/io/github/dsheirer/dsp/mixer/ComplexMixer.java index ac98cb08..44ad1c24 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/ComplexMixer.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/ComplexMixer.java @@ -125,7 +125,36 @@ public abstract class ComplexMixer * Mixes the complex I & Q samples with samples generated from an oscillator. * @param i complex samples to mix * @param q complex samples to mix + * @param timestamp of the first sample * @return mixed samples */ - public abstract ComplexSamples mix(float[] i, float[] q, long timestamp); + public ComplexSamples mix(float[] i, float[] q, long timestamp) + { + ComplexSamples mixer = generate(i.length, timestamp); + mix(i, q, mixer.i(), mixer.q()); + return mixer; + } + + /** + * Mixes the complex samples into the output arrays, to avoid an allocation when the caller can reuse the arrays. + * @param samples to mix + * @param iOutput array to receive the mixed inphase samples, the same length as the samples. + * @param qOutput array to receive the mixed quadrature samples, the same length as the samples. + * @return mixed samples that use the output arrays + */ + public ComplexSamples mix(ComplexSamples samples, float[] iOutput, float[] qOutput) + { + mOscillator.generateComplexSamples(iOutput, qOutput); + mix(samples.i(), samples.q(), iOutput, qOutput); + return new ComplexSamples(iOutput, qOutput, samples.timestamp()); + } + + /** + * Mixes the complex I & Q samples with the oscillator samples and stores the results in the oscillator arrays. + * @param iSamples complex samples to mix + * @param qSamples complex samples to mix + * @param iMixer oscillator samples, replaced with the mixed samples + * @param qMixer oscillator samples, replaced with the mixed samples + */ + protected abstract void mix(float[] iSamples, float[] qSamples, float[] iMixer, float[] qMixer); } diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/ScalarComplexMixer.java b/src/main/java/io/github/dsheirer/dsp/mixer/ScalarComplexMixer.java index aa39afe7..ba4355f5 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/ScalarComplexMixer.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/ScalarComplexMixer.java @@ -49,19 +49,14 @@ public class ScalarComplexMixer extends ComplexMixer } /** - * Mixes the complex I & Q samples with samples generated from an oscillator. + * Mixes the complex I & Q samples with the oscillator samples and stores the results in the oscillator arrays. * @param iSamples complex samples to mix * @param qSamples complex samples to mix - * @param timestamp of the first sample - * @return mixed samples + * @param iMixer oscillator samples, replaced with the mixed samples + * @param qMixer oscillator samples, replaced with the mixed samples */ - @Override public ComplexSamples mix(float[] iSamples, float[] qSamples, long timestamp) + @Override protected void mix(float[] iSamples, float[] qSamples, float[] iMixer, float[] qMixer) { - ComplexSamples mixer = generate(iSamples.length, timestamp); - - float[] iMixer = mixer.i(); - float[] qMixer = mixer.q(); - float inphase, quadrature; for(int x = 0; x < iSamples.length; x++) @@ -71,8 +66,6 @@ public class ScalarComplexMixer extends ComplexMixer iMixer[x] = inphase; qMixer[x] = quadrature; } - - return mixer; } /** diff --git a/src/main/java/io/github/dsheirer/dsp/mixer/VectorComplexMixer.java b/src/main/java/io/github/dsheirer/dsp/mixer/VectorComplexMixer.java index c8cfea15..cde94985 100644 --- a/src/main/java/io/github/dsheirer/dsp/mixer/VectorComplexMixer.java +++ b/src/main/java/io/github/dsheirer/dsp/mixer/VectorComplexMixer.java @@ -19,7 +19,6 @@ package io.github.dsheirer.dsp.mixer; -import io.github.dsheirer.sample.complex.ComplexSamples; import io.github.dsheirer.vector.VectorUtilities; import jdk.incubator.vector.FloatVector; import jdk.incubator.vector.VectorSpecies; @@ -42,22 +41,16 @@ public class VectorComplexMixer extends ComplexMixer } /** - * Mixes the complex I & Q samples with samples generated from an oscillator. + * Mixes the complex I & Q samples with the oscillator samples and stores the results in the oscillator arrays. * @param iSamples complex samples to mix * @param qSamples complex samples to mix - * @param timestamp for the first sample - * @return mixed samples + * @param iMixer oscillator samples, replaced with the mixed samples + * @param qMixer oscillator samples, replaced with the mixed samples */ - @Override public ComplexSamples mix(float[] iSamples, float[] qSamples, long timestamp) + @Override protected void mix(float[] iSamples, float[] qSamples, float[] iMixer, float[] qMixer) { VectorUtilities.checkComplexArrayLength(iSamples, qSamples, VECTOR_SPECIES); - //Reuse this complex samples buffer to store the results and return to caller - ComplexSamples mixer = generate(iSamples.length, timestamp); - - float[] iMixer = mixer.i(); - float[] qMixer = mixer.q(); - FloatVector iS, qS, iM, qM; for(int x = 0; x < iSamples.length; x += VECTOR_SPECIES.length()) @@ -71,7 +64,5 @@ public class VectorComplexMixer extends ComplexMixer iM.mul(iS).sub(qM.mul(qS)).intoArray(iMixer, x); qM.mul(iS).add(iM.mul(qS)).intoArray(qMixer, x); } - - return mixer; } } diff --git a/src/main/java/io/github/dsheirer/dsp/oscillator/AWGNOscillator.java b/src/main/java/io/github/dsheirer/dsp/oscillator/AWGNOscillator.java index a6d32018..607d1b77 100644 --- a/src/main/java/io/github/dsheirer/dsp/oscillator/AWGNOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/oscillator/AWGNOscillator.java @@ -68,15 +68,22 @@ public class AWGNOscillator implements IComplexOscillator @Override public ComplexSamples generateComplexSamples(int sampleCount, long timestamp) { - ComplexSamples samples = mComplexOscillator.generateComplexSamples(sampleCount, timestamp); + float[] i = new float[sampleCount]; + float[] q = new float[sampleCount]; + generateComplexSamples(i, q); + return new ComplexSamples(i, q, timestamp); + } - for(int x = 0; x < samples.i().length; x++) + @Override + public void generateComplexSamples(float[] i, float[] q) + { + mComplexOscillator.generateComplexSamples(i, q); + + for(int x = 0; x < i.length; x++) { - samples.i()[x] += nextNoise(); - samples.q()[x] += nextNoise(); + i[x] += nextNoise(); + q[x] += nextNoise(); } - - return samples; } @Override diff --git a/src/main/java/io/github/dsheirer/dsp/oscillator/IComplexOscillator.java b/src/main/java/io/github/dsheirer/dsp/oscillator/IComplexOscillator.java index e774e885..5934a8a1 100644 --- a/src/main/java/io/github/dsheirer/dsp/oscillator/IComplexOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/oscillator/IComplexOscillator.java @@ -37,4 +37,11 @@ public interface IComplexOscillator extends IOscillator * @return generated samples */ ComplexSamples generateComplexSamples(int sampleCount, long timestamp); + + /** + * Generates complex samples into the arrays, filling each array. + * @param i array to receive the inphase samples + * @param q array to receive the quadrature samples, the same length as the inphase array + */ + void generateComplexSamples(float[] i, float[] q); } diff --git a/src/main/java/io/github/dsheirer/dsp/oscillator/ScalarComplexOscillator.java b/src/main/java/io/github/dsheirer/dsp/oscillator/ScalarComplexOscillator.java index 29f8d08f..01828d50 100644 --- a/src/main/java/io/github/dsheirer/dsp/oscillator/ScalarComplexOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/oscillator/ScalarComplexOscillator.java @@ -100,6 +100,13 @@ public class ScalarComplexOscillator extends AbstractOscillator implements IComp { float[] iSamples = new float[sampleCount]; float[] qSamples = new float[sampleCount]; + generateComplexSamples(iSamples, qSamples); + return new ComplexSamples(iSamples, qSamples, timestamp); + } + + @Override + public void generateComplexSamples(float[] iSamples, float[] qSamples) + { float cosineAngle = (float)FastMath.cos(getAnglePerSample()); float sineAngle = (float)FastMath.sin(getAnglePerSample()); @@ -132,7 +139,5 @@ public class ScalarComplexOscillator extends AbstractOscillator implements IComp mPreviousInphase = previousInphase; mPreviousQuadrature = previousQuadrature; - - return new ComplexSamples(iSamples, qSamples, timestamp); } } diff --git a/src/main/java/io/github/dsheirer/dsp/oscillator/VectorComplexOscillator.java b/src/main/java/io/github/dsheirer/dsp/oscillator/VectorComplexOscillator.java index cea9b836..32367f5c 100644 --- a/src/main/java/io/github/dsheirer/dsp/oscillator/VectorComplexOscillator.java +++ b/src/main/java/io/github/dsheirer/dsp/oscillator/VectorComplexOscillator.java @@ -39,10 +39,15 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp { private static final Logger mLog = LoggerFactory.getLogger(VectorComplexOscillator.class); private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_PREFERRED; + //Number of samples between SIMD lane realignments. + private static final int LANE_ALIGNMENT_INTERVAL = 4096; private float[] mPreviousInphases; private float[] mPreviousQuadratures; private float[] mGainInitials; //Set to 3.0f as the first constant in the gain calculation + //Note: this must not have an initializer because the parent constructor sets it via the update() method + private volatile boolean mLaneUpdateRequired; + private int mSamplesSinceLaneUpdate; /** * Constructs an instance @@ -54,26 +59,54 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp { super(frequency, sampleRate); + mPreviousInphases = new float[VECTOR_SPECIES.length()]; + mPreviousQuadratures = new float[VECTOR_SPECIES.length()]; + mPreviousInphases[0] = 1.0f; + mGainInitials = new float[VECTOR_SPECIES.length()]; Arrays.fill(mGainInitials, 3.0f); } + /** + * Flags the SIMD lanes for update on the next sample generation, since this method can be invoked from a thread + * other than the sample generation thread, and the sample generation thread stores the lane state to the previous + * sample arrays when it completes. + */ @Override protected void update() { super.update(); + mLaneUpdateRequired = true; + } + + /** + * Updates the SIMD lanes when the frequency or sample rate has changed, and periodically to realign the lanes. At + * frequencies where the rotation for a set of lanes is nearly a whole or half turn, rounding affects each lane + * differently and the lanes would otherwise drift apart until the next frequency change, producing spurs at + * multiples of the sample rate divided by the lane count. Note: this must only be invoked on the sample generation + * thread. + * @param sampleCount that will be generated following this update check. + */ + private void updateLanes(int sampleCount) + { + mSamplesSinceLaneUpdate += sampleCount; + + //Only clear the flag after reading it as set, otherwise a periodic realignment could clear a concurrent update + if(mLaneUpdateRequired) + { + //Clear the flag before reading the angle so that a concurrent update can't be missed. + mLaneUpdateRequired = false; + } + else if(mSamplesSinceLaneUpdate < LANE_ALIGNMENT_INTERVAL) + { + return; + } + + mSamplesSinceLaneUpdate = 0; float cosineAngle = (float)FastMath.cos(getAnglePerSample()); float sineAngle = (float)FastMath.sin(getAnglePerSample()); - if(mPreviousInphases == null || mPreviousQuadratures == null) - { - mPreviousInphases = new float[VECTOR_SPECIES.length()]; - mPreviousQuadratures = new float[VECTOR_SPECIES.length()]; - - mPreviousInphases[0] = 1.0f; - } - float gain; //Setup the previous sample arrays where each index is offset one sample of rotation from the previous sample. @@ -101,6 +134,8 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp "] must be a power of 2 and a multiple of the SIMD lane width [" + VECTOR_SPECIES.length() + "]"); } + updateLanes(sampleCount); + float[] samples = new float[sampleCount * 2]; FloatVector previousInphase = FloatVector.fromArray(VECTOR_SPECIES, mPreviousInphases, 0); FloatVector previousQuadrature = FloatVector.fromArray(VECTOR_SPECIES, mPreviousQuadratures, 0); @@ -118,7 +153,7 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp { if(++gainCounter % 10 == 0) { - gain = gainInitials.sub(previousInphase.pow(2.0f).add(previousQuadrature.pow(2.0f))).div(2.0f); + gain = gainInitials.sub(previousInphase.mul(previousInphase).add(previousQuadrature.mul(previousQuadrature))).div(2.0f); inphase = previousInphase.mul(cosAngle).sub(previousQuadrature.mul(sinAngle)).mul(gain); quadrature = previousInphase.mul(sinAngle).add(previousQuadrature.mul(cosAngle)).mul(gain); } @@ -149,14 +184,28 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp */ @Override public ComplexSamples generateComplexSamples(int sampleCount, long timestamp) { + float[] iSamples = new float[sampleCount]; + float[] qSamples = new float[sampleCount]; + generateComplexSamples(iSamples, qSamples); + return new ComplexSamples(iSamples, qSamples, timestamp); + } + + /** + * Generates complex samples into the arrays. + * @param iSamples to fill, with a length that is a multiple of the SIMD lane width. + * @param qSamples to fill, with the same length as the inphase array. + */ + @Override public void generateComplexSamples(float[] iSamples, float[] qSamples) + { + int sampleCount = iSamples.length; + if(sampleCount % VECTOR_SPECIES.length() != 0) { throw new IllegalArgumentException("Requested sample count [" + sampleCount + "] must be a power of 2 and a multiple of the SIMD lane width [" + VECTOR_SPECIES.length() + "]"); } - float[] iSamples = new float[sampleCount]; - float[] qSamples = new float[sampleCount]; + updateLanes(sampleCount); FloatVector previousInphase = FloatVector.fromArray(VECTOR_SPECIES, mPreviousInphases, 0); FloatVector previousQuadrature = FloatVector.fromArray(VECTOR_SPECIES, mPreviousQuadratures, 0); @@ -170,7 +219,7 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp for(int samplePointer = 0; samplePointer < sampleCount; samplePointer += VECTOR_SPECIES.length()) { - gain = gainInitials.sub(previousInphase.pow(2.0f).add(previousQuadrature.pow(2.0f))).div(2.0f); + gain = gainInitials.sub(previousInphase.mul(previousInphase).add(previousQuadrature.mul(previousQuadrature))).div(2.0f); inphase = previousInphase.mul(cosAngle).sub(previousQuadrature.mul(sinAngle)).mul(gain); quadrature = previousInphase.mul(sinAngle).add(previousQuadrature.mul(cosAngle)).mul(gain); @@ -183,7 +232,5 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp previousInphase.intoArray(mPreviousInphases, 0); previousQuadrature.intoArray(mPreviousQuadratures, 0); - - return new ComplexSamples(iSamples, qSamples, timestamp); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java index 6dab5959..9bc95265 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java @@ -489,8 +489,10 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I config.setChannelGrantEvent(event); } - getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, channel, - identifierCollection, this)); + ChannelStartProcessingRequest request = new ChannelStartProcessingRequest(trafficChannel, channel, + identifierCollection, this); + request.setStartTimestamp(timestamp); + getInterModuleEventBus().post(request); } else { diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java index 660334d3..ecd6164a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNTrafficChannelManager.java @@ -371,7 +371,7 @@ public class NXDNTrafficChannelManager extends TrafficChannelManager implements if(traffic != null) { - requestTrafficChannelStart(traffic, channel, ic, tracker); + requestTrafficChannelStart(traffic, channel, ic, tracker, timestamp); } else { @@ -583,7 +583,7 @@ public class NXDNTrafficChannelManager extends TrafficChannelManager implements if(traffic != null) { - requestTrafficChannelStart(traffic, channel, ic, tracker); + requestTrafficChannelStart(traffic, channel, ic, tracker, timestamp); } else { @@ -609,7 +609,7 @@ public class NXDNTrafficChannelManager extends TrafficChannelManager implements * @param tracker to update with channel allocation flag */ private void requestTrafficChannelStart(Channel trafficChannel, NXDNChannel nxdnChannel, - IdentifierCollection ic, NXDNChannelEventTracker tracker) + IdentifierCollection ic, NXDNChannelEventTracker tracker, long timestamp) { if(nxdnChannel != null && nxdnChannel.getDownlinkFrequency() > 0 && getInterModuleEventBus() != null) { @@ -623,6 +623,7 @@ public class NXDNTrafficChannelManager extends TrafficChannelManager implements mAllocatedTrafficChannelMap.put(nxdnChannel.getDownlinkFrequency(), trafficChannel); ChannelStartProcessingRequest startChannelRequest = new ChannelStartProcessingRequest(trafficChannel, nxdnChannel, ic, this); + startChannelRequest.setStartTimestamp(timestamp); startChannelRequest.addPreloadDataContent(new NXDNChannelInfoPreloadData(mChannelAccessInformation, mChannelFrequencies)); getInterModuleEventBus().post(startChannelRequest); tracker.setTrafficChannelAllocated(true); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java index 857b70c8..cf63436c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java @@ -1182,6 +1182,7 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I ChannelStartProcessingRequest startChannelRequest = new ChannelStartProcessingRequest(trafficChannel, apco25Channel, identifierCollection, this); + startChannelRequest.setStartTimestamp(timestamp); startChannelRequest.addPreloadDataContent(new PatchGroupPreLoadDataContent(identifierCollection, timestamp)); startChannelRequest.addPreloadDataContent(new P25FrequencyBandPreloadDataContent(mFrequencyBandMap.values())); getInterModuleEventBus().post(startChannelRequest); diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/HalfBandTunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/HalfBandTunerChannelSource.java index 7c2059d1..8229b914 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/HalfBandTunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/HalfBandTunerChannelSource.java @@ -30,6 +30,7 @@ import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager; import io.github.dsheirer.util.Dispatcher; import java.util.Iterator; +import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,11 +50,15 @@ public class HalfBandTunerChannelSource extends TunerCh private final ComplexMixer mFrequencyCorrectionMixer; private final IRealDecimationFilter mIDecimationFilter; private final IRealDecimationFilter mQDecimationFilter; + private final boolean mReuseMixerOutput; + private float[] mIMixerOutput; + private float[] mQMixerOutput; private Listener mSamplesListener; private final double mChannelSampleRate; - private long mChannelFrequency; + private volatile long mChannelFrequency; private final long mFrequencyOffset; - private long mFrequencyCorrection; + private volatile long mFrequencyCorrection; + private final AtomicBoolean mPendingMixerUpdate = new AtomicBoolean(); /** * Constructs a frequency translating and CIC decimating channel source. @@ -80,8 +85,11 @@ public class HalfBandTunerChannelSource extends TunerCh mIDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimation); mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimation); + //The decimation filters copy the mixed samples, unless there's no decimation and the mixed samples are the output + mReuseMixerOutput = decimation > 1; + //Set dispatcher to process 1/10 of estimated sample arrival rate, 20 times per second (up to 200% per interval) - mBufferDispatcher = new Dispatcher(threadName, 50, getHeartbeatManager()); + mBufferDispatcher = new Dispatcher(threadName, 50, getHeartbeatManager(), BUFFER_MAX_CAPACITY); mBufferDispatcher.setListener(new NativeBufferProcessor()); //Setup the frequency mixer to the current source frequency @@ -92,21 +100,22 @@ public class HalfBandTunerChannelSource extends TunerCh } /** - * Sets and applies the channel decoder requested frequency correction. + * Sets the channel decoder requested frequency correction, which is applied on the sample processing thread. * @param correction requested by the downstream processors. */ @Override public void setFrequencyCorrection(long correction) { mFrequencyCorrection = correction; - updateMixerFrequencyOffset(); + mPendingMixerUpdate.set(true); } @Override public void start() { - super.start(); + //The dispatcher has to be running to accept delayed sample buffers preloaded in response to the start request mBufferDispatcher.start(); + super.start(); } @Override @@ -131,7 +140,8 @@ public class HalfBandTunerChannelSource extends TunerCh } /** - * Sets/updates the center frequency for the sample streaming being sent from the producer. + * Sets/updates the center frequency for the sample streaming being sent from the producer, which is applied on + * the sample processing thread. * * @param frequency in hertz */ @@ -139,12 +149,14 @@ public class HalfBandTunerChannelSource extends TunerCh public void setFrequency(long frequency) { mChannelFrequency = frequency; - updateMixerFrequencyOffset(); + mPendingMixerUpdate.set(true); } /** * Calculates the local mixer frequency offset from the tuned frequency, - * channel's requested frequency, and channel frequency correction. + * channel's requested frequency, and channel frequency correction. This is only invoked on the sample processing + * thread so that the latest values from the tuner and channel frequency correction threads are applied together + * and the mixer is never updated while it is generating samples. */ private void updateMixerFrequencyOffset() { @@ -189,6 +201,27 @@ public class HalfBandTunerChannelSource extends TunerCh return mChannelSampleRate; } + /** + * Mixes the samples to baseband, reusing the mixer output arrays when they aren't passed to the consumer. + * @param samples to mix, which aren't modified since they can be shared with other channels. + * @return mixed samples + */ + private ComplexSamples mix(ComplexSamples samples) + { + if(!mReuseMixerOutput) + { + return mFrequencyCorrectionMixer.mix(samples); + } + + if(mIMixerOutput == null || mIMixerOutput.length != samples.i().length) + { + mIMixerOutput = new float[samples.i().length]; + mQMixerOutput = new float[samples.q().length]; + } + + return mFrequencyCorrectionMixer.mix(samples, mIMixerOutput, mQMixerOutput); + } + /** * Processes native buffers received from the dispatcher and sends to the registered listener */ @@ -203,7 +236,12 @@ public class HalfBandTunerChannelSource extends TunerCh while(iterator.hasNext()) { - ComplexSamples basebanded = mFrequencyCorrectionMixer.mix(iterator.next()); + if(mPendingMixerUpdate.compareAndSet(true, false)) + { + updateMixerFrequencyOffset(); + } + + ComplexSamples basebanded = mix(iterator.next()); float[] i = mIDecimationFilter.decimateReal(basebanded.i()); float[] q = mQDecimationFilter.decimateReal(basebanded.q()); diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java index 21e89bf6..1a61f40b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/TunerChannelSource.java @@ -40,6 +40,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc private Listener mConsumerSourceEventListener; protected String mThreadName; private final ChannelFrequencyErrorManager mChannelFrequencyErrorManager; + private volatile long mStartTimestamp; /** * Tuner Channel Source is a Digital Drop Channel (DDC) abstract class that defines the minimum functionality @@ -111,6 +112,16 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc return mTunerChannel; } + /** + * Sets the timestamp for the oldest samples to provide to this channel when it starts. This only applies to + * sources where the producer incorporates a sample delay buffer. + * @param timestamp in milliseconds since epoch, or 0 to start with current samples. + */ + public void setStartTimestamp(long timestamp) + { + mStartTimestamp = timestamp; + } + /** * Starts this tuner channel source producing sample stream. */ @@ -120,7 +131,8 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc //Broadcast current frequency and sample rate so consumer can configure correctly broadcastConsumerSourceEvent(SourceEvent.frequencyChange(this, getFrequency(), "Startup")); - broadcastProducerSourceEvent(SourceEvent.startSampleStreamRequest(this)); + broadcastProducerSourceEvent(SourceEvent.startSampleStreamRequest(this, + mStartTimestamp > 0 ? mStartTimestamp : System.currentTimeMillis())); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java index 7910533d..109af3a3 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java @@ -32,7 +32,9 @@ import io.github.dsheirer.source.tuner.channel.TunerChannel; import io.github.dsheirer.source.tuner.channel.TunerChannelSource; import io.github.dsheirer.source.tuner.frequency.TunerFrequencyErrorManager; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; @@ -49,7 +51,8 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager private final static int DELAY_BUFFER_DURATION_MILLISECONDS = 2000; private List mChannelSources = new CopyOnWriteArrayList<>(); - private SortedSet mTunerChannels = new TreeSet<>(); + //Channel sources that are registered with the delay buffer, guarded by the tuner controller lock + private Set mStartedChannelSources = new HashSet<>(); private TunerController mTunerController; private ChannelSourceEventProcessor mChannelSourceEventProcessor = new ChannelSourceEventProcessor(); private NativeSampleDelayBuffer mSampleDelayBuffer; @@ -75,7 +78,7 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager public String getStateDescription() { StringBuilder sb = new StringBuilder(); - sb.append("Heterodyne Channel Source Manager Providing [").append(mTunerChannels.size()).append("] Channels"); + sb.append("Heterodyne Channel Source Manager Providing [").append(getTunerChannelCount()).append("] Channels"); sb.append("\n\tTuner Controller Frequency: ").append(mTunerController.getFrequency()); for(HalfBandTunerChannelSource channelSource: mChannelSources) { @@ -102,16 +105,28 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager } } + /** + * Tuner channels for the channel sources. Note: tuner channels are ordered by frequency, so channel sources that + * share a frequency have a single entry. + * @return new set of tuner channels. + */ @Override public SortedSet getTunerChannels() { - return mTunerChannels; + SortedSet tunerChannels = new TreeSet<>(); + + for(HalfBandTunerChannelSource channelSource: mChannelSources) + { + tunerChannels.add(channelSource.getTunerChannel()); + } + + return tunerChannels; } @Override public int getTunerChannelCount() { - return mTunerChannels.size(); + return mChannelSources.size(); } @Override @@ -129,9 +144,17 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager try { - if(CenterFrequencyCalculator.canTune(tunerChannel, mTunerController, mTunerChannels)) + if(CenterFrequencyCalculator.canTune(tunerChannel, mTunerController, getTunerChannels())) { - //Attempt to create the channel source first, in case we get a filter design exception + //Update the tuner center frequency first, so that a channel isn't provided if the tuner can't support it + SortedSet tunerChannels = getTunerChannels(); + tunerChannels.add(tunerChannel); + + if(!updateTunerFrequency(tunerChannels)) + { + return null; + } + HalfBandTunerChannelSource tunerChannelSource = new HalfBandTunerChannelSource(mChannelSourceEventProcessor, tunerChannel, mTunerController.getSampleRate(), channelSpecification, threadName, getTunerFrequencyErrorManager()); @@ -142,10 +165,6 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager //Set the current tuner frequency tunerChannelSource.setFrequency(mTunerController.getFrequency()); - //Add to the channel list and update the tuner center frequency as needed - mTunerChannels.add(tunerChannel); - updateTunerFrequency(); - //Lock the tuner controller frequency and sample rate mTunerController.setLockedSampleRate(true); @@ -176,18 +195,21 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager } /** - * Calculates a new center frequency and updates the tuner center frequency + * Calculates a new center frequency and updates the tuner center frequency when the tuner isn't tuned for the + * tuner channels. + * @param tunerChannels that the tuner must provide + * @return true if the tuner is tuned for the tuner channels. */ - private void updateTunerFrequency() + private boolean updateTunerFrequency(SortedSet tunerChannels) { - if(!mTunerController.isTunedFor(getTunerChannels())) + if(!mTunerController.isTunedFor(tunerChannels)) { - long centerFrequency = CenterFrequencyCalculator.getCenterFrequency(mTunerController, getTunerChannels()); + long centerFrequency = CenterFrequencyCalculator.getCenterFrequency(mTunerController, tunerChannels); if(centerFrequency == CenterFrequencyCalculator.INVALID_FREQUENCY) { mLog.error("Couldn't calculate center frequency for tuner and tuner channels"); - return; + return false; } if(centerFrequency != mTunerController.getFrequency()) @@ -199,9 +221,12 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager catch(SourceException se) { mLog.error("Couldn't update tuner center frequency to " + centerFrequency, se); + return false; } } } + + return true; } @Override @@ -214,9 +239,18 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager updateTunerFrequency(tunerSourceEvent.getValue().longValue()); //Clear the delay buffer since any delayed samples will be centered on the previous frequency - if(mSampleDelayBuffer != null) + mTunerController.getLock().lock(); + + try { - mSampleDelayBuffer.clear(); + if(mSampleDelayBuffer != null) + { + mSampleDelayBuffer.clear(); + } + } + finally + { + mTunerController.getLock().unlock(); } break; case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: @@ -291,7 +325,7 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager */ private void stopDelayBuffer() { - if(mSampleDelayBuffer != null && !mSampleDelayBuffer.hasListeners()) + if(mSampleDelayBuffer != null) { mTunerController.removeBufferListener(mSampleDelayBuffer); mSampleDelayBuffer.dispose(); @@ -299,6 +333,72 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager } } + /** + * Registers the channel source with the delay buffer to start the flow of sample buffers to the channel. + * @param channelSource to start + * @param timestamp of the oldest delayed sample buffers to preload to the channel + */ + private void startChannelSource(HalfBandTunerChannelSource channelSource, long timestamp) + { + mTunerController.getLock().lock(); + + try + { + startDelayBuffer(); + + //The start sample stream request contains a start timestamp and the delay buffer + //will preload the channel with delayed sample buffers that either contain the + //timestamp or occur later/newer than the timestamp. + mSampleDelayBuffer.addListener(channelSource, timestamp); + mStartedChannelSources.add(channelSource); + } + finally + { + mTunerController.getLock().unlock(); + } + } + + /** + * Stops the flow of sample buffers to the channel source and releases the tuner channel. Stops the delay buffer + * when this is the last started channel. + * + * Note: the delay buffer applies listener removal on the next sample buffer, so the started channel sources set + * is used to detect the last channel instead of the delay buffer's listeners. + * @param channelSource to stop, which may not have been started. + */ + private void stopChannelSource(HalfBandTunerChannelSource channelSource) + { + mTunerController.getLock().lock(); + + try + { + if(mStartedChannelSources.remove(channelSource)) + { + mSampleDelayBuffer.removeListener(channelSource); + + if(mStartedChannelSources.isEmpty()) + { + stopDelayBuffer(); + } + } + + mChannelSources.remove(channelSource); + channelSource.dispose(); + + //Unlock the tuner controller if there are no more channels + if(getTunerChannelCount() == 0) + { + mTunerController.setLockedSampleRate(false); + } + + broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); + } + finally + { + mTunerController.getLock().unlock(); + } + } + /** * Processes channel source events */ @@ -309,34 +409,16 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager { switch(sourceEvent.getEvent()) { -//TODO: protect start/stop processing with a reentrant lock case REQUEST_START_SAMPLE_STREAM: - if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource) + if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource halfBandSource) { - startDelayBuffer(); - - //The start sample stream request contains a start timestamp and the delay buffer - //will preload the channel with delayed sample buffers that either contain the - //timestamp or occur later/newer than the timestamp. - mSampleDelayBuffer.addListener((HalfBandTunerChannelSource)sourceEvent.getSource(), - sourceEvent.getValue().longValue()); + startChannelSource(halfBandSource, sourceEvent.getValue().longValue()); } break; case REQUEST_STOP_SAMPLE_STREAM: if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource halfBandSource) { - mSampleDelayBuffer.removeListener(halfBandSource); - stopDelayBuffer(); - mChannelSources.remove(halfBandSource); - mTunerChannels.remove(halfBandSource.getTunerChannel()); - halfBandSource.dispose(); - - //Unlock the tuner controller if there are no more channels - if(getTunerChannelCount() == 0) - { - mTunerController.setLockedSampleRate(false); - } - broadcast(SourceEvent.channelCountChange(getTunerChannelCount())); + stopChannelSource(halfBandSource); } break; case NOTIFICATION_CHANNEL_COUNT_CHANGE: diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java index 7bcc6b64..f8f97192 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/CalibrationType.java @@ -29,7 +29,7 @@ public enum CalibrationType { //There's an oddity with these two vector implementations where it takes ~30x longer when it runs //after any other calibrations ... so we always sort to top to run this one first - OSCILLATOR_COMPLEX("Complex Oscillator", 1), + OSCILLATOR_COMPLEX("Complex Oscillator", 2), GAIN_COMPLEX("Complex Gain", 1), AM_DEMODULATOR("AM Demodulator", 1), @@ -48,7 +48,7 @@ public enum CalibrationType HILBERT_TRANSFORM("Hilbert Transform", 1), INTERPOLATOR("Interpolator", 1), MAGNITUDE("Magnitude", 1), - MIXER_COMPLEX("Complex Mixer", 1), + MIXER_COMPLEX("Complex Mixer", 2), NXDN_SOFT_SYNC_DETECTOR("NXDN Soft Sync Detector", 3), OSCILLATOR_REAL("Real Oscillator", 1), P25P1_SOFT_SYNC_DETECTOR("P25 Phase 1 Soft Sync Detector", 2), diff --git a/src/main/java/io/github/dsheirer/vector/calibrate/oscillator/ComplexOscillatorCalibration.java b/src/main/java/io/github/dsheirer/vector/calibrate/oscillator/ComplexOscillatorCalibration.java index 68a1e233..30d7a550 100644 --- a/src/main/java/io/github/dsheirer/vector/calibrate/oscillator/ComplexOscillatorCalibration.java +++ b/src/main/java/io/github/dsheirer/vector/calibrate/oscillator/ComplexOscillatorCalibration.java @@ -22,6 +22,7 @@ package io.github.dsheirer.vector.calibrate.oscillator; import io.github.dsheirer.dsp.oscillator.IComplexOscillator; import io.github.dsheirer.dsp.oscillator.ScalarComplexOscillator; import io.github.dsheirer.dsp.oscillator.VectorComplexOscillator; +import io.github.dsheirer.sample.complex.ComplexSamples; import io.github.dsheirer.vector.calibrate.Calibration; import io.github.dsheirer.vector.calibrate.CalibrationException; import io.github.dsheirer.vector.calibrate.CalibrationType; @@ -123,8 +124,8 @@ public class ComplexOscillatorCalibration extends Calibration while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { - float[] generated = mScalarOscillator.generate(BUFFER_SIZE); - accumulator += generated[0]; + ComplexSamples generated = mScalarOscillator.generateComplexSamples(BUFFER_SIZE, 0); + accumulator += generated.i()[0]; count++; } @@ -144,8 +145,8 @@ public class ComplexOscillatorCalibration extends Calibration while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS) { - float[] generated = mVectorOscillator.generate(BUFFER_SIZE); - accumulator += generated[0]; + ComplexSamples generated = mVectorOscillator.generateComplexSamples(BUFFER_SIZE, 0); + accumulator += generated.i()[0]; count++; } diff --git a/src/test/java/io/github/dsheirer/buffer/SharedNativeBufferTest.java b/src/test/java/io/github/dsheirer/buffer/SharedNativeBufferTest.java new file mode 100644 index 00000000..3f141317 --- /dev/null +++ b/src/test/java/io/github/dsheirer/buffer/SharedNativeBufferTest.java @@ -0,0 +1,120 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ +package io.github.dsheirer.buffer; + +import io.github.dsheirer.sample.complex.ComplexSamples; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Verifies that channels sharing a native buffer receive the same converted samples. + */ +public class SharedNativeBufferTest +{ + private static byte[] samples() + { + byte[] samples = new byte[8192 * 8]; + new Random(1).nextBytes(samples); + return samples; + } + + private static List read(INativeBuffer buffer) + { + List fragments = new ArrayList<>(); + Iterator iterator = buffer.iterator(); + + while(iterator.hasNext()) + { + fragments.add(iterator.next()); + } + + return fragments; + } + + /** + * Test: the samples match the wrapped buffer and each consumer gets the same instances. + */ + @Test + void consumersShareOneConversion() + { + ByteNativeBuffer nativeBuffer = new ByteNativeBuffer(samples(), 1000L, 0.0f, 2400.0f); + SharedNativeBuffer shared = new SharedNativeBuffer(nativeBuffer); + List expected = read(nativeBuffer); + List first = read(shared); + List second = read(shared); + + assertEquals(expected.size(), first.size()); + assertEquals(expected.size(), second.size()); + + for(int x = 0; x < expected.size(); x++) + { + assertArrayEquals(expected.get(x).i(), first.get(x).i()); + assertArrayEquals(expected.get(x).q(), first.get(x).q()); + assertEquals(expected.get(x).timestamp(), first.get(x).timestamp()); + assertSame(first.get(x), second.get(x)); + } + } + + /** + * Test: concurrent consumers each receive every fragment in order. + */ + @Test + void concurrentConsumersReceiveAllFragments() throws Exception + { + ByteNativeBuffer nativeBuffer = new ByteNativeBuffer(samples(), 1000L, 0.0f, 2400.0f); + int fragmentCount = read(nativeBuffer).size(); + ExecutorService executor = Executors.newFixedThreadPool(4); + + try + { + for(int trial = 0; trial < 200; trial++) + { + SharedNativeBuffer shared = new SharedNativeBuffer(nativeBuffer); + List>> results = new ArrayList<>(); + + for(int consumer = 0; consumer < 4; consumer++) + { + results.add(executor.submit(() -> read(shared))); + } + + List first = results.getFirst().get(); + assertEquals(fragmentCount, first.size()); + + for(Future> result : results) + { + assertEquals(first, result.get()); + } + } + } + finally + { + executor.shutdownNow(); + } + } +} diff --git a/src/test/java/io/github/dsheirer/dsp/filter/decimate/DecimationFilterFactoryTest.java b/src/test/java/io/github/dsheirer/dsp/filter/decimate/DecimationFilterFactoryTest.java new file mode 100644 index 00000000..93928cc1 --- /dev/null +++ b/src/test/java/io/github/dsheirer/dsp/filter/decimate/DecimationFilterFactoryTest.java @@ -0,0 +1,158 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ +package io.github.dsheirer.dsp.filter.decimate; + +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.window.WindowType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies decimation rate selection and the alias rejection of the decimation filters. + */ +public class DecimationFilterFactoryTest +{ + private static final double BANDWIDTH = 12500.0; + private static final double DECIMATED_SAMPLE_RATE = 25000.0; + private static final double MINIMUM_ALIAS_REJECTION_DB = 68.0; + + /** + * Calculates the filter's gain for a real tone, from the output power after the filter settles. + */ + private static double gainDb(int decimation, double frequency, double sampleRate) + { + IRealDecimationFilter filter = DecimationFilterFactory.getRealDecimationFilter(decimation); + int blockSize = Math.max(4096, decimation * 8); + int blocks = Math.max(64, 262144 / blockSize); + double phaseStep = 2.0 * Math.PI * frequency / sampleRate; + long sample = 0; + double power = 0.0; + int count = 0; + + for(int block = 0; block < blocks; block++) + { + float[] input = new float[blockSize]; + + for(int x = 0; x < blockSize; x++) + { + input[x] = (float)Math.cos(phaseStep * sample++); + } + + float[] output = filter.decimateReal(input); + + if(block > blocks / 2) + { + for(float value : output) + { + power += value * value; + count++; + } + } + } + + //A real tone with unity amplitude has an average power of 0.5 + return 10.0 * Math.log10(power / count / 0.5); + } + + /** + * Test: the requested rate is decimated by the largest supported rate that is less than the requested rate. + */ + @Test + void decimationRate() + { + assertEquals(2, DecimationFilterFactory.getDecimationRate(3)); + assertEquals(2, DecimationFilterFactory.getDecimationRate(4)); + assertEquals(4, DecimationFilterFactory.getDecimationRate(5)); + assertEquals(64, DecimationFilterFactory.getDecimationRate(96)); + assertEquals(1024, DecimationFilterFactory.getDecimationRate(4096)); + } + + /** + * Test: rates that are too small to decimate apply no decimation, instead of a zero rate or a rate that decimates + * below the requested sample rate. + */ + @Test + void noDecimationWhenRateIsTooSmall() + { + for(int requestedRate : new int[]{0, 1, 2}) + { + assertEquals(1, DecimationFilterFactory.getDecimationRate(requestedRate)); + } + } + + /** + * Test: negative rates are rejected. + */ + @Test + void negativeRateIsRejected() + { + assertThrows(IllegalArgumentException.class, () -> DecimationFilterFactory.getDecimationRate(-1)); + } + + /** + * Test: half-band filters have unity gain at DC and a null at the Nyquist frequency, across filter lengths. + */ + @Test + void halfBandDcGainAndNyquistNull() + { + for(int length : new int[]{11, 15, 23, 63}) + { + float[] coefficients = FilterFactory.getHalfBand(length, WindowType.BLACKMAN); + double dc = 0.0; + double nyquist = 0.0; + + for(int x = 0; x < coefficients.length; x++) + { + dc += coefficients[x]; + nyquist += (x % 2 == 0) ? coefficients[x] : -coefficients[x]; + } + + assertEquals(1.0, dc, 1e-6); + assertEquals(0.0, nyquist, 1e-6); + } + } + + /** + * Test: tones that alias into the channel are rejected and tones within the channel are passed, for the band edge + * of the final stage and for tones near the Nyquist frequency of the first stage. + */ + @Test + void aliasRejection() + { + for(int decimation = 2; decimation <= 512; decimation *= 2) + { + double sampleRate = DECIMATED_SAMPLE_RATE * decimation; + + assertEquals(0.0, gainDb(decimation, BANDWIDTH / 2.0, sampleRate), 0.1); + + //Closest alias into the channel at the final stage + double finalStageAlias = DECIMATED_SAMPLE_RATE - (BANDWIDTH / 2.0); + assertTrue(gainDb(decimation, finalStageAlias, sampleRate) < -MINIMUM_ALIAS_REJECTION_DB); + + if(decimation > 2) + { + double firstStageAlias = (sampleRate / 2.0) - (BANDWIDTH / 2.0); + assertTrue(gainDb(decimation, firstStageAlias, sampleRate) < -MINIMUM_ALIAS_REJECTION_DB); + } + } + } +} diff --git a/src/test/java/io/github/dsheirer/dsp/filter/decimate/IntermediateOutputReuseTest.java b/src/test/java/io/github/dsheirer/dsp/filter/decimate/IntermediateOutputReuseTest.java new file mode 100644 index 00000000..884f720d --- /dev/null +++ b/src/test/java/io/github/dsheirer/dsp/filter/decimate/IntermediateOutputReuseTest.java @@ -0,0 +1,113 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ +package io.github.dsheirer.dsp.filter.decimate; + +import io.github.dsheirer.dsp.filter.FilterFactory; +import io.github.dsheirer.dsp.filter.halfband.RealHalfBandDecimationFilter; +import io.github.dsheirer.dsp.mixer.ComplexMixer; +import io.github.dsheirer.dsp.mixer.ScalarComplexMixer; +import io.github.dsheirer.dsp.window.WindowType; +import io.github.dsheirer.sample.complex.ComplexSamples; +import java.util.Random; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Verifies that reusing arrays for intermediate results doesn't change the results. + */ +public class IntermediateOutputReuseTest +{ + private static float[] samples(int length, long seed) + { + Random random = new Random(seed); + float[] samples = new float[length]; + + for(int x = 0; x < length; x++) + { + samples[x] = random.nextFloat() - 0.5f; + } + + return samples; + } + + /** + * Test: a half-band filter uses the output array and produces the same results as an allocating filter. + */ + @Test + void halfBandOutputArray() + { + float[] coefficients = FilterFactory.getHalfBand(23, WindowType.BLACKMAN); + RealHalfBandDecimationFilter allocating = new RealHalfBandDecimationFilter(coefficients); + RealHalfBandDecimationFilter reusing = new RealHalfBandDecimationFilter(coefficients); + float[] output = new float[1024]; + + for(int block = 0; block < 4; block++) + { + float[] samples = samples(2048, block); + assertSame(output, reusing.decimateReal(samples, output)); + assertArrayEquals(allocating.decimateReal(samples), output); + } + } + + /** + * Test: consecutive results from a decimation cascade are independent arrays, since consumers retain them. + */ + @Test + void cascadeOutputIsNotReused() + { + IRealDecimationFilter filter = DecimationFilterFactory.getRealDecimationFilter(16); + float[] first = filter.decimateReal(samples(2048, 1)); + float[] copy = first.clone(); + float[] second = filter.decimateReal(samples(2048, 2)); + + assertNotSame(first, second); + assertArrayEquals(copy, first); + } + + /** + * Test: mixing into reused arrays matches the allocating mix, and the input isn't modified. + */ + @Test + void mixerOutputArrays() + { + ComplexMixer allocating = new ScalarComplexMixer(100000, 2400000); + ComplexMixer reusing = new ScalarComplexMixer(100000, 2400000); + float[] i = new float[2048]; + float[] q = new float[2048]; + + for(int block = 0; block < 4; block++) + { + ComplexSamples samples = new ComplexSamples(samples(2048, block), samples(2048, block + 10), block); + float[] iOriginal = samples.i().clone(); + ComplexSamples expected = allocating.mix(samples); + ComplexSamples mixed = reusing.mix(samples, i, q); + + assertSame(i, mixed.i()); + assertSame(q, mixed.q()); + assertEquals(block, mixed.timestamp()); + assertArrayEquals(expected.i(), mixed.i()); + assertArrayEquals(expected.q(), mixed.q()); + assertArrayEquals(iOriginal, samples.i()); + } + } +} diff --git a/src/test/java/io/github/dsheirer/dsp/oscillator/VectorComplexOscillatorTest.java b/src/test/java/io/github/dsheirer/dsp/oscillator/VectorComplexOscillatorTest.java new file mode 100644 index 00000000..5f1c6d20 --- /dev/null +++ b/src/test/java/io/github/dsheirer/dsp/oscillator/VectorComplexOscillatorTest.java @@ -0,0 +1,139 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ +package io.github.dsheirer.dsp.oscillator; + +import io.github.dsheirer.sample.complex.ComplexSamples; +import jdk.incubator.vector.FloatVector; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that the vector complex oscillator produces a clean tone, including across frequency changes. + */ +public class VectorComplexOscillatorTest +{ + private static final double SAMPLE_RATE = 2_400_000.0; + private static final int BUFFER_SIZE = FloatVector.SPECIES_PREFERRED.length() * 256; + private static final double MAXIMUM_ERROR_RADIANS = 0.001; + + /** + * Largest deviation of the sample to sample rotation from the rotation expected for the frequency. + */ + private static double rotationError(ComplexSamples samples, double frequency) + { + double expected = 2.0 * Math.PI * frequency / SAMPLE_RATE; + float[] i = samples.i(); + float[] q = samples.q(); + double error = 0.0; + + for(int x = 1; x < i.length; x++) + { + double rotation = Math.atan2(q[x], i[x]) - Math.atan2(q[x - 1], i[x - 1]); + error = Math.max(error, Math.abs(Math.IEEEremainder(rotation - expected, 2.0 * Math.PI))); + } + + return error; + } + + /** + * Test: output is a unit magnitude tone at the requested frequency, for the mixer's sample generation method. + */ + @Test + void generatesTone() + { + VectorComplexOscillator oscillator = new VectorComplexOscillator(400_000, SAMPLE_RATE); + ComplexSamples samples = oscillator.generateComplexSamples(BUFFER_SIZE, 0); + + assertTrue(rotationError(samples, 400_000) < MAXIMUM_ERROR_RADIANS); + + for(int x = 0; x < samples.i().length; x++) + { + assertEquals(1.0, Math.hypot(samples.i()[x], samples.q()[x]), 0.001); + } + } + + /** + * Test: the SIMD lanes follow a frequency change, without a phase discontinuity at the change. + */ + @Test + void frequencyChangeIsPhaseContinuous() + { + VectorComplexOscillator oscillator = new VectorComplexOscillator(600_000, SAMPLE_RATE); + ComplexSamples before = oscillator.generateComplexSamples(BUFFER_SIZE, 0); + oscillator.setFrequency(-300_000); + ComplexSamples after = oscillator.generateComplexSamples(BUFFER_SIZE, 0); + + assertTrue(rotationError(after, -300_000) < MAXIMUM_ERROR_RADIANS); + + int last = before.i().length - 1; + double step = Math.atan2(after.q()[0], after.i()[0]) - Math.atan2(before.q()[last], before.i()[last]); + double error = Math.abs(Math.IEEEremainder(step, 2.0 * Math.PI)); + assertTrue(error <= (2.0 * Math.PI * 600_000 / SAMPLE_RATE) * FloatVector.SPECIES_PREFERRED.length() + 0.001); + } + + /** + * Test: the SIMD lanes stay aligned over a long run at frequencies where the rotation for a set of lanes is nearly + * a whole turn or half turn, which is small enough for rounding to affect each lane differently. The frequencies + * that are affected depend on the lane width of the hardware. + */ + @Test + void lanesRemainAlignedAtRationalFrequencies() + { + int buffers = 30_000_000 / BUFFER_SIZE; + + for(double divisor: new double[]{8.0, 16.0, 32.0}) + { + double frequency = SAMPLE_RATE / divisor; + VectorComplexOscillator oscillator = new VectorComplexOscillator(frequency, SAMPLE_RATE); + + for(int x = 0; x < buffers; x++) + { + assertTrue(rotationError(oscillator.generateComplexSamples(BUFFER_SIZE, 0), frequency) < MAXIMUM_ERROR_RADIANS); + } + } + } + + /** + * Test: a frequency change from another thread during sample generation doesn't leave the SIMD lanes spaced for + * the previous frequency. + */ + @Test + void concurrentFrequencyChangeUpdatesLanes() throws Exception + { + for(int trial = 0; trial < 500; trial++) + { + VectorComplexOscillator oscillator = new VectorComplexOscillator(600_000, SAMPLE_RATE); + oscillator.generateComplexSamples(BUFFER_SIZE, 0); + + Thread thread = new Thread(() -> oscillator.setFrequency(-300_000)); + thread.start(); + + while(thread.isAlive()) + { + oscillator.generateComplexSamples(BUFFER_SIZE, 0); + } + + //The first buffer can overlap the change, so that it is the second buffer that must be clean + oscillator.generateComplexSamples(BUFFER_SIZE, 0); + assertTrue(rotationError(oscillator.generateComplexSamples(BUFFER_SIZE, 0), -300_000) < MAXIMUM_ERROR_RADIANS); + } + } +} diff --git a/src/test/java/io/github/dsheirer/source/tuner/channel/HalfBandTunerChannelSourceTest.java b/src/test/java/io/github/dsheirer/source/tuner/channel/HalfBandTunerChannelSourceTest.java new file mode 100644 index 00000000..71ba1ee0 --- /dev/null +++ b/src/test/java/io/github/dsheirer/source/tuner/channel/HalfBandTunerChannelSourceTest.java @@ -0,0 +1,99 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2023 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ +package io.github.dsheirer.source.tuner.channel; + +import io.github.dsheirer.buffer.FloatNativeBuffer; +import io.github.dsheirer.buffer.INativeBuffer; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Verifies that the heterodyne channel source applies frequency updates on the sample processing thread. + */ +public class HalfBandTunerChannelSourceTest +{ + private static final long CHANNEL_FREQUENCY = 460_000_000L; + private static final long TUNER_FREQUENCY = 460_400_000L; + private static final double SAMPLE_RATE = 2_400_000.0; + + private static HalfBandTunerChannelSource source() throws Exception + { + HalfBandTunerChannelSource source = new HalfBandTunerChannelSource<>(sourceEvent -> {}, + new TunerChannel(CHANNEL_FREQUENCY, 12500), SAMPLE_RATE, + new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0), "test", null); + source.setListener(samples -> {}); + return source; + } + + private static void process(HalfBandTunerChannelSource source) + { + source.new NativeBufferProcessor().receive(new FloatNativeBuffer(new float[8192], 0L, 0.0f)); + } + + /** + * Test: the mixer only changes on the sample processing thread, using both the tuner frequency and the correction. + */ + @Test + void frequencyUpdatesAreAppliedDuringSampleProcessing() throws Exception + { + HalfBandTunerChannelSource source = source(); + + source.setFrequency(TUNER_FREQUENCY); + source.setFrequencyCorrection(100); + assertEquals(0, source.getMixerFrequency()); + + process(source); + assertEquals(TUNER_FREQUENCY - CHANNEL_FREQUENCY + 100, source.getMixerFrequency()); + } + + /** + * Test: a sample rate that is too low to decimate passes through at the original sample rate. + */ + @Test + void lowSampleRateIsNotDecimated() throws Exception + { + HalfBandTunerChannelSource source = new HalfBandTunerChannelSource<>(sourceEvent -> {}, + new TunerChannel(CHANNEL_FREQUENCY, 12500), 50000.0, + new ChannelSpecification(50000.0, 12500, 6500.0, 7200.0), "test", null); + + assertEquals(50000.0, source.getSampleRate()); + } + + /** + * Test: concurrent tuner frequency and channel correction updates are both reflected in the mixer. + */ + @Test + void concurrentUpdatesAreNotLost() throws Exception + { + for(int trial = 0; trial < 1000; trial++) + { + HalfBandTunerChannelSource source = source(); + Thread tuner = new Thread(() -> source.setFrequency(TUNER_FREQUENCY)); + Thread correction = new Thread(() -> source.setFrequencyCorrection(100)); + tuner.start(); + correction.start(); + tuner.join(); + correction.join(); + + process(source); + assertEquals(TUNER_FREQUENCY - CHANNEL_FREQUENCY + 100, source.getMixerFrequency()); + } + } +} diff --git a/src/test/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManagerTest.java b/src/test/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManagerTest.java new file mode 100644 index 00000000..09147a76 --- /dev/null +++ b/src/test/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManagerTest.java @@ -0,0 +1,268 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ +package io.github.dsheirer.source.tuner.manager; + +import io.github.dsheirer.buffer.FloatNativeBuffer; +import io.github.dsheirer.buffer.INativeBuffer; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.ComplexSamples; +import io.github.dsheirer.source.SourceException; +import io.github.dsheirer.source.tuner.channel.ChannelSpecification; +import io.github.dsheirer.source.tuner.channel.TunerChannel; +import io.github.dsheirer.source.tuner.channel.TunerChannelSource; +import io.github.dsheirer.source.tuner.test.TestTunerController; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies the channel start/stop lifecycle of the heterodyne channel source manager. + */ +public class HeterodyneChannelSourceManagerTest +{ + private static final long FREQUENCY = 460_000_000L; + private static final ChannelSpecification SPECIFICATION = new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0); + + /** + * Tuner controller that tracks sample buffer listeners, without generating any sample buffers. + */ + private static class ListenerTrackingTunerController extends TestTunerController + { + private final Set> mListeners = new HashSet<>(); + private boolean mTuningFails = false; + + /** + * Sends a sample buffer to each of the listeners. + */ + void deliver(long timestamp) + { + new ArrayList<>(mListeners).forEach(listener -> listener.receive(new FloatNativeBuffer(new float[4096], + timestamp, 0.0f))); + } + + @Override + public void setFrequency(long frequency) throws SourceException + { + if(mTuningFails) + { + throw new SourceException("Tuner frequency cannot be set"); + } + + super.setFrequency(frequency); + } + + @Override + public void addBufferListener(Listener listener) + { + mListeners.add(listener); + } + + @Override + public void removeBufferListener(Listener listener) + { + mListeners.remove(listener); + } + + @Override + public boolean hasBufferListeners() + { + return !mListeners.isEmpty(); + } + } + + private static TunerChannelSource source(HeterodyneChannelSourceManager manager, long frequency) + { + TunerChannelSource source = manager.getSource(new TunerChannel(frequency, 12500), SPECIFICATION, "test"); + assertNotNull(source); + return source; + } + + /** + * Test: the tuner stops streaming when the last channel stops, before any further sample buffers arrive. + */ + @Test + void lastChannelStopDeregistersFromTuner() + { + ListenerTrackingTunerController controller = new ListenerTrackingTunerController(); + HeterodyneChannelSourceManager manager = new HeterodyneChannelSourceManager(controller); + TunerChannelSource first = source(manager, FREQUENCY); + TunerChannelSource second = source(manager, FREQUENCY + 25000); + + first.start(); + second.start(); + assertTrue(controller.hasBufferListeners()); + + first.stop(); + assertTrue(controller.hasBufferListeners(), "tuner keeps streaming while a channel is running"); + + second.stop(); + assertFalse(controller.hasBufferListeners(), "tuner stops streaming after the last channel stops"); + assertEquals(0, manager.getTunerChannelCount()); + assertFalse(controller.isLockedSampleRate()); + } + + /** + * Test: streaming restarts for a channel that starts after the previous last channel stopped. + */ + @Test + void channelStartAfterLastChannelStopRegistersWithTuner() + { + ListenerTrackingTunerController controller = new ListenerTrackingTunerController(); + HeterodyneChannelSourceManager manager = new HeterodyneChannelSourceManager(controller); + + TunerChannelSource first = source(manager, FREQUENCY); + first.start(); + first.stop(); + + TunerChannelSource second = source(manager, FREQUENCY); + second.start(); + assertTrue(controller.hasBufferListeners()); + second.stop(); + assertFalse(controller.hasBufferListeners()); + } + + /** + * Test: no channel is provided when the tuner can't retune for it, and the running channel is unaffected. + */ + @Test + void failedRetuneDoesNotProvideChannel() + { + ListenerTrackingTunerController controller = new ListenerTrackingTunerController(); + HeterodyneChannelSourceManager manager = new HeterodyneChannelSourceManager(controller); + TunerChannelSource running = source(manager, FREQUENCY); + running.start(); + + controller.mTuningFails = true; + + //Within the tuner bandwidth of the running channel, but outside of the current center frequency's bandwidth + assertNull(manager.getSource(new TunerChannel(FREQUENCY + 2_000_000L, 12500), SPECIFICATION, "test")); + assertEquals(1, manager.getTunerChannelCount()); + assertTrue(controller.isLockedSampleRate()); + running.stop(); + } + + /** + * Test: a channel started with a grant timestamp is preloaded with the delayed samples from that timestamp forward, + * and in order, ahead of the current samples. + */ + @Test + void channelIsPreloadedFromStartTimestamp() throws Exception + { + ListenerTrackingTunerController controller = new ListenerTrackingTunerController(); + HeterodyneChannelSourceManager manager = new HeterodyneChannelSourceManager(controller); + TunerChannelSource running = source(manager, FREQUENCY); + running.start(); + + //The first buffer applies the running channel's pending start request + for(long timestamp = 1000; timestamp <= 5000; timestamp += 1000) + { + controller.deliver(timestamp); + } + + TunerChannelSource traffic = source(manager, FREQUENCY + 25000); + CountDownLatch latch = new CountDownLatch(4); + List timestamps = new CopyOnWriteArrayList<>(); + traffic.setListener((ComplexSamples samples) -> { + timestamps.add(samples.timestamp()); + latch.countDown(); + }); + traffic.setStartTimestamp(3000); + traffic.start(); + + controller.deliver(6000); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertEquals(List.of(3000L, 4000L, 5000L, 6000L), timestamps); + + traffic.stop(); + running.stop(); + } + + /** + * Test: channels that share a frequency are counted separately, and the tuner channel, the sample rate lock and the + * tuner stream remain until the last of those channels stops. + */ + @Test + void channelsWithSameFrequencyAreTrackedSeparately() + { + ListenerTrackingTunerController controller = new ListenerTrackingTunerController(); + HeterodyneChannelSourceManager manager = new HeterodyneChannelSourceManager(controller); + TunerChannelSource first = source(manager, FREQUENCY); + TunerChannelSource second = source(manager, FREQUENCY); + first.start(); + second.start(); + assertEquals(2, manager.getTunerChannelCount()); + + first.stop(); + assertEquals(1, manager.getTunerChannelCount()); + assertEquals(1, manager.getTunerChannels().size()); + assertTrue(controller.isLockedSampleRate()); + assertTrue(controller.hasBufferListeners()); + + second.stop(); + assertEquals(0, manager.getTunerChannelCount()); + assertTrue(manager.getTunerChannels().isEmpty()); + assertFalse(controller.isLockedSampleRate()); + assertFalse(controller.hasBufferListeners()); + } + + /** + * Test: stopping a channel that was never started releases its tuner channel. + */ + @Test + void stopWithoutStartReleasesTunerChannel() + { + ListenerTrackingTunerController controller = new ListenerTrackingTunerController(); + HeterodyneChannelSourceManager manager = new HeterodyneChannelSourceManager(controller); + + source(manager, FREQUENCY).stop(); + + assertEquals(0, manager.getTunerChannelCount()); + assertFalse(controller.isLockedSampleRate()); + } + + /** + * Test: stopping a never started channel doesn't stop the sample buffers for a running channel. + */ + @Test + void stopWithoutStartLeavesRunningChannelStreaming() + { + ListenerTrackingTunerController controller = new ListenerTrackingTunerController(); + HeterodyneChannelSourceManager manager = new HeterodyneChannelSourceManager(controller); + TunerChannelSource running = source(manager, FREQUENCY); + TunerChannelSource neverStarted = source(manager, FREQUENCY + 25000); + + running.start(); + neverStarted.stop(); + + assertTrue(controller.hasBufferListeners()); + running.stop(); + } +}