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