mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
2537 Complex Vector Oscillator (256/512) Lanes Can Drift Apart Over Time (#2538)
* #2535 Heterodyne Channelizer enhancements and corrections. * #2537 Complex vector oscillator SIMD lane drift. Realigns the SIMD lanes every 262,144 samples. At frequencies where the rotation for a set of lanes is nearly a whole or half turn, rounding affects each lane differently and the lanes drift apart until the next frequency change, producing spurs at multiples of the sample rate divided by the lane count. Lane phase error at 3125 Hz with a 50 kHz sample rate was about -38 dBc after one hour and is now about -83 dBc with 16 lanes and -73 dBc with 8 lanes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #2537 test updates * #2537 test updates --------- Co-authored-by: denny <dennis.sheirer@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,8 @@ public class NativeSampleDelayBuffer implements Listener<INativeBuffer>
|
||||
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;
|
||||
|
||||
114
src/main/java/io/github/dsheirer/buffer/SharedNativeBuffer.java
Normal file
114
src/main/java/io/github/dsheirer/buffer/SharedNativeBuffer.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
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<ComplexSamples> mConverted = new ArrayList<>();
|
||||
private Iterator<ComplexSamples> 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<ComplexSamples> 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<InterleavedComplexSamples> iteratorInterleaved()
|
||||
{
|
||||
return mNativeBuffer.iteratorInterleaved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sampleCount()
|
||||
{
|
||||
return mNativeBuffer.sampleCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mNativeBuffer.getTimestamp();
|
||||
}
|
||||
}
|
||||
@@ -418,6 +418,11 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,15 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorComplexOscillator.class);
|
||||
private static final VectorSpecies<Float> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<T extends INativeBuffer> 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<ComplexSamples> 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<T extends INativeBuffer> 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<T extends INativeBuffer> 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<T extends INativeBuffer> 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<T extends INativeBuffer> 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<T extends INativeBuffer> 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<T extends INativeBuffer> 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());
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
|
||||
private Listener<SourceEvent> 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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<HalfBandTunerChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
|
||||
private SortedSet<TunerChannel> mTunerChannels = new TreeSet<>();
|
||||
//Channel sources that are registered with the delay buffer, guarded by the tuner controller lock
|
||||
private Set<HalfBandTunerChannelSource> 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<TunerChannel> getTunerChannels()
|
||||
{
|
||||
return mTunerChannels;
|
||||
SortedSet<TunerChannel> 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<TunerChannel> 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<TunerChannel> 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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
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<ComplexSamples> read(INativeBuffer buffer)
|
||||
{
|
||||
List<ComplexSamples> fragments = new ArrayList<>();
|
||||
Iterator<ComplexSamples> 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<ComplexSamples> expected = read(nativeBuffer);
|
||||
List<ComplexSamples> first = read(shared);
|
||||
List<ComplexSamples> 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<Future<List<ComplexSamples>>> results = new ArrayList<>();
|
||||
|
||||
for(int consumer = 0; consumer < 4; consumer++)
|
||||
{
|
||||
results.add(executor.submit(() -> read(shared)));
|
||||
}
|
||||
|
||||
List<ComplexSamples> first = results.getFirst().get();
|
||||
assertEquals(fragmentCount, first.size());
|
||||
|
||||
for(Future<List<ComplexSamples>> result : results)
|
||||
{
|
||||
assertEquals(first, result.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.dsp.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
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<INativeBuffer> source() throws Exception
|
||||
{
|
||||
HalfBandTunerChannelSource<INativeBuffer> 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<INativeBuffer> 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<INativeBuffer> 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<INativeBuffer> 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<INativeBuffer> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
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<Listener<INativeBuffer>> 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<INativeBuffer> listener)
|
||||
{
|
||||
mListeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeBufferListener(Listener<INativeBuffer> 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<Long> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user