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:
Denny Sheirer
2026-10-30 08:42:49 -04:00
committed by GitHub
parent 9ebbf957a2
commit 85bfd21257
60 changed files with 1570 additions and 171 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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