#1351 Restores semi-accurate timestamps of sample buffers to ensure P25 & DMR decoded messages have accurate timestamps and MBE recording voice frames have accurate timestamps. (#1357)

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2022-12-17 06:39:35 -05:00
committed by GitHub
parent 047dec6da1
commit bc462b9652
79 changed files with 607 additions and 733 deletions

View File

@@ -0,0 +1,70 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;
/**
* Base native buffer class.
*/
public abstract class AbstractNativeBuffer implements INativeBuffer
{
private long mTimestamp;
private float mSamplesPerMillisecond;
/**
* Constructs an instance
* @param timestamp for the start of this buffer
* @param samplesPerMillisecond to calculate the time offset of sub-buffers extracted from this native buffer.
*/
public AbstractNativeBuffer(long timestamp, float samplesPerMillisecond)
{
mTimestamp = timestamp;
mSamplesPerMillisecond = samplesPerMillisecond;
}
/**
* Timestamp for the start of this buffer
* @return timestamp in milliseconds
*/
@Override
public long getTimestamp()
{
return mTimestamp;
}
/**
* Quantity of samples representing one millisecond of sample data, used for calculating fragment timestamp offsets.
* @return samples per millisecond count.
*/
public float getSamplesPerMillisecond()
{
return mSamplesPerMillisecond;
}
/**
* Calculates the timestamp for a samples buffer fragment based on where it is extracted from relative to the
* native buffer starting timestamp.
* @param samplesPointer for the start of the fragment. Note: this value will be divided by 2 to account for I/Q sample pairs.
* @return timestamp adjusted to the fragment or sub-buffer start sample.
*/
protected long getFragmentTimestamp(int samplesPointer)
{
return getTimestamp() + (long)(samplesPointer / 2 / getSamplesPerMillisecond());
}
}

View File

@@ -0,0 +1,44 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;
/**
* Base class for native buffer factories.
*/
public abstract class AbstractNativeBufferFactory implements INativeBufferFactory
{
private float mSamplesPerMillisecond = 0.0f;
@Override
public void setSamplesPerMillisecond(float samplesPerMillisecond)
{
mSamplesPerMillisecond = samplesPerMillisecond;
}
/**
* Quantity of I/Q sample pairs per milli-second at the current sample rate to use in calculating an accurate
* timestamp for sub-buffer that are generated from the native buffer.
* @return samples per millisecond.
*/
public float getSamplesPerMillisecond()
{
return mSamplesPerMillisecond;
}
}

View File

@@ -27,7 +27,7 @@ import java.util.Iterator;
* Native buffer sample array wrapper class that provides access to a stream of either interleaved or
* non-interleaved complex sample buffers converted from the raw byte sample array.
*/
public class ByteNativeBuffer implements INativeBuffer
public class ByteNativeBuffer extends AbstractNativeBuffer
{
private static final int FRAGMENT_SIZE = 2048;
private final static float[] LOOKUP_VALUES;
@@ -46,16 +46,17 @@ public class ByteNativeBuffer implements INativeBuffer
}
private byte[] mSamples;
private long mTimestamp;
/**
* Constructs an instance
* @param samples to process
* @param timestamp of the samples
* @param averageDc measured from sample stream
* @param samplesPerMillisecond to calculate derivative timestamps for sub-buffers.
*/
public ByteNativeBuffer(byte[] samples, long timestamp, float averageDc)
public ByteNativeBuffer(byte[] samples, long timestamp, float averageDc, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % FRAGMENT_SIZE != 0)
{
@@ -63,7 +64,6 @@ public class ByteNativeBuffer implements INativeBuffer
}
mSamples = samples;
mTimestamp = timestamp;
mAverageDc = averageDc;
}
@@ -73,12 +73,6 @@ public class ByteNativeBuffer implements INativeBuffer
return mSamples.length / 2;
}
@Override
public long getTimestamp()
{
return mTimestamp;
}
@Override
public Iterator<ComplexSamples> iterator()
{
@@ -104,6 +98,8 @@ public class ByteNativeBuffer implements INativeBuffer
@Override
public ComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);
float[] i = new float[FRAGMENT_SIZE];
float[] q = new float[FRAGMENT_SIZE];
int samplesOffset = mSamplesPointer;
@@ -115,8 +111,7 @@ public class ByteNativeBuffer implements INativeBuffer
}
mSamplesPointer = samplesOffset;
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
@@ -136,6 +131,8 @@ public class ByteNativeBuffer implements INativeBuffer
@Override
public InterleavedComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);
float[] converted = new float[FRAGMENT_SIZE * 2];
int samplesPointer = mSamplesPointer;
@@ -147,7 +144,7 @@ public class ByteNativeBuffer implements INativeBuffer
mSamplesPointer = samplesPointer;
return new InterleavedComplexSamples(converted, mTimestamp);
return new InterleavedComplexSamples(converted, timestamp);
}
}
}

View File

@@ -24,7 +24,7 @@ import java.nio.ByteBuffer;
/**
* Implements a factory for creating ByteNativeBuffer instances
*/
public class ByteNativeBufferFactory implements INativeBufferFactory
public class ByteNativeBufferFactory extends AbstractNativeBufferFactory
{
private DcCorrectionManager mDcCorrectionManager = new DcCorrectionManager();
@@ -39,7 +39,7 @@ public class ByteNativeBufferFactory implements INativeBufferFactory
calculateDc(copy);
}
return new ByteNativeBuffer(copy, timestamp, mDcCorrectionManager.getAverageDc());
return new ByteNativeBuffer(copy, timestamp, mDcCorrectionManager.getAverageDc(), getSamplesPerMillisecond());
}
/**

View File

@@ -22,35 +22,24 @@ package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.Iterator;
/**
* Simple native buffer implementation that simply wraps a single, existing, non-native sample buffer.
*/
public class FloatNativeBuffer implements INativeBuffer
public class FloatNativeBuffer extends AbstractNativeBuffer
{
private float[] mInterleavedComplexSamples;
private long mTimestamp;
/**
* Constructs an instance
* @param complexSamples to wrap
* @param complexSamples interleaved to wrap
* @param timestamp for the buffer
*/
public FloatNativeBuffer(float[] complexSamples, long timestamp)
public FloatNativeBuffer(float[] complexSamples, long timestamp, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
mInterleavedComplexSamples = complexSamples;
mTimestamp = timestamp;
}
/**
* Constructs an instance
* @param complexSamples to wrap
*/
public FloatNativeBuffer(float[] complexSamples)
{
this(complexSamples, System.currentTimeMillis());
}
/**
@@ -58,9 +47,9 @@ public class FloatNativeBuffer implements INativeBuffer
* @param samples to wrap
* @param timestamp for the buffer
*/
public FloatNativeBuffer(ComplexSamples samples, long timestamp)
public FloatNativeBuffer(ComplexSamples samples)
{
this(SampleUtils.interleave(samples), timestamp);
this(SampleUtils.interleave(samples), samples.timestamp(), 0.0f);
}
/**
@@ -69,7 +58,7 @@ public class FloatNativeBuffer implements INativeBuffer
*/
public FloatNativeBuffer(InterleavedComplexSamples samples)
{
this(samples.samples(), samples.timestamp());
this(samples.samples(), samples.timestamp(), 0.0f);
}
@Override
@@ -90,12 +79,6 @@ public class FloatNativeBuffer implements INativeBuffer
return mInterleavedComplexSamples.length / 2;
}
@Override
public long getTimestamp()
{
return mTimestamp;
}
private class ComplexSamplesIterator implements Iterator<ComplexSamples>
{
private boolean mEmpty;
@@ -115,7 +98,7 @@ public class FloatNativeBuffer implements INativeBuffer
}
mEmpty = true;
return SampleUtils.deinterleave(mInterleavedComplexSamples);
return SampleUtils.deinterleave(mInterleavedComplexSamples, getTimestamp());
}
}
@@ -139,7 +122,7 @@ public class FloatNativeBuffer implements INativeBuffer
}
mEmpty = true;
return new InterleavedComplexSamples(mInterleavedComplexSamples, mTimestamp);
return new InterleavedComplexSamples(mInterleavedComplexSamples, getTimestamp());
}
}
}

View File

@@ -33,7 +33,15 @@ public interface INativeBufferFactory
*
* @param samples byte array copied from native memory
* @param timestamp of the samples
* @param samplesPerMillisecond to calculate timestamp offset for child buffers.
* @return instance
*/
INativeBuffer getBuffer(ByteBuffer samples, long timestamp);
/**
* Sets the samples per millisecond rate based on the current sample rate.
*
* @param samplesPerMillisecond to calculate timestamp offset for child buffers.
*/
void setSamplesPerMillisecond(float samplesPerMillisecond);
}

View File

@@ -21,14 +21,13 @@ package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.Iterator;
/**
* Native buffer sample array wrapper class that provides access to a stream of either interleaved or
* non-interleaved complex sample buffers converted from the raw byte sample array.
*/
public class SignedByteNativeBuffer implements INativeBuffer
public class SignedByteNativeBuffer extends AbstractNativeBuffer
{
private static final int FRAGMENT_SIZE = 2048;
private final static float[] LOOKUP_VALUES;
@@ -47,7 +46,6 @@ public class SignedByteNativeBuffer implements INativeBuffer
}
private byte[] mSamples;
private long mTimestamp;
private float mIAverageDc;
private float mQAverageDc;
@@ -57,9 +55,12 @@ public class SignedByteNativeBuffer implements INativeBuffer
* @param timestamp of the samples
* @param iAverageDc of the sample stream
* @param qAverageDc of the sample stream
* @param samplesPerMillisecond to calculate sub-buffer timestamps
*/
public SignedByteNativeBuffer(byte[] samples, long timestamp, float iAverageDc, float qAverageDc)
public SignedByteNativeBuffer(byte[] samples, long timestamp, float iAverageDc, float qAverageDc, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % FRAGMENT_SIZE != 0)
{
@@ -67,17 +68,10 @@ public class SignedByteNativeBuffer implements INativeBuffer
}
mSamples = samples;
mTimestamp = timestamp;
mIAverageDc = iAverageDc;
mQAverageDc = qAverageDc;
}
@Override
public long getTimestamp()
{
return mTimestamp;
}
@Override
public int sampleCount()
{
@@ -112,6 +106,8 @@ public class SignedByteNativeBuffer implements INativeBuffer
@Override
public ComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);
float[] i = new float[FRAGMENT_SIZE];
float[] q = new float[FRAGMENT_SIZE];
int samplesOffset = mSamplesPointer;
@@ -123,7 +119,7 @@ public class SignedByteNativeBuffer implements INativeBuffer
}
mSamplesPointer = samplesOffset;
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
@@ -143,6 +139,8 @@ public class SignedByteNativeBuffer implements INativeBuffer
@Override
public InterleavedComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);
float[] converted = new float[FRAGMENT_SIZE * 2];
int samplesOffset = mSamplesPointer;
@@ -155,7 +153,7 @@ public class SignedByteNativeBuffer implements INativeBuffer
mSamplesPointer = samplesOffset;
return new InterleavedComplexSamples(converted, mTimestamp);
return new InterleavedComplexSamples(converted, timestamp);
}
}
}

View File

@@ -25,7 +25,7 @@ import java.util.concurrent.TimeUnit;
/**
* Implements a factory for creating SignedByteNativeBuffer instances
*/
public class SignedByteNativeBufferFactory implements INativeBufferFactory
public class SignedByteNativeBufferFactory extends AbstractNativeBufferFactory
{
/**
* DC removal calculations will run once a minute
@@ -78,7 +78,7 @@ public class SignedByteNativeBufferFactory implements INativeBufferFactory
calculateDc(copy);
}
return new SignedByteNativeBuffer(copy, timestamp, mIAverageDc, mQAverageDc);
return new SignedByteNativeBuffer(copy, timestamp, mIAverageDc, mQAverageDc, getSamplesPerMillisecond());
}
/**

View File

@@ -20,7 +20,6 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;
import java.util.Iterator;
/**
@@ -42,8 +41,9 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
protected float[] mQBuffer = new float[FRAGMENT_SIZE + Q_OVERLAP];
protected short[] mSamples;
protected int mSamplesPointer = 0;
protected long mTimestamp;
protected float mAverageDc;
private long mTimestamp;
private float mSamplesPerMillisecond;
/**
* Constructs an instance
@@ -52,8 +52,10 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
* @param residualQ samples from previous buffer
* @param averageDc as measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp,
float samplesPerMillisecond)
{
if(residualI.length != I_OVERLAP || residualQ.length != Q_OVERLAP)
{
@@ -73,6 +75,7 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
mAverageDc = averageDc;
mSamples = samples;
mTimestamp = timestamp;
mSamplesPerMillisecond = samplesPerMillisecond;
//Transfer and scale the residual I & Q samples from the previous buffer
for(int i = 0; i < residualI.length; i++)
@@ -86,6 +89,17 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
}
}
/**
* Calculates the timestamp for a samples buffer fragment based on where it is extracted from relative to the
* native buffer starting timestamp.
* @param samplesPointer for the start of the fragment. Note: this value will be divided by 2 to account for I/Q sample pairs.
* @return timestamp adjusted to the fragment or sub-buffer start sample.
*/
protected long getFragmentTimestamp(int samplesPointer)
{
return mTimestamp + (long)(samplesPointer / 2 / mSamplesPerMillisecond);
}
@Override
public boolean hasNext()
{

View File

@@ -20,7 +20,6 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
/**
@@ -36,10 +35,11 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -50,6 +50,8 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
for(int x = 0; x < FRAGMENT_SIZE; x++)
@@ -91,6 +93,6 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.util.Arrays;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
@@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -55,6 +56,7 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -121,6 +123,6 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.util.Arrays;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
@@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -55,6 +56,7 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -115,6 +117,6 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.util.Arrays;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
@@ -42,10 +41,12 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
//Arrange the last 8 taps of the filter into the higher indices of an array to align for SIMD
mFilterPart2 = new float[16];
System.arraycopy(COEFFICIENTS, 16, mFilterPart2, 8, 8);
@@ -59,6 +60,8 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -117,6 +120,6 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.util.Arrays;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
@@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -55,6 +56,8 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -133,6 +136,6 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -34,11 +34,12 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -49,6 +50,7 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
for(int x = 0; x < FRAGMENT_SIZE; x++)
@@ -89,6 +91,6 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -54,6 +55,7 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -118,6 +120,6 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -54,6 +55,7 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -112,6 +114,6 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -40,11 +40,12 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
//Arrange the last 8 taps of the filter into the higher indices of an array to align for SIMD
mFilterPart2 = new float[16];
@@ -59,6 +60,7 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -115,6 +117,6 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -54,6 +55,7 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -130,6 +132,6 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -19,23 +19,21 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.AbstractNativeBuffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.vector.calibrate.Implementation;
import java.util.Iterator;
/**
* Native buffer scalar implementation for Airspy non-packed samples.
*/
public class AirspyNativeBuffer implements INativeBuffer
public class AirspyNativeBuffer extends AbstractNativeBuffer
{
private short[] mSamples;
private short[] mResidualI;
private short[] mResidualQ;
private float mAverageDc;
private long mTimestamp;
private Implementation mInterleavedImplementation;
private Implementation mNonInterleavedImplementation;
@@ -48,11 +46,14 @@ public class AirspyNativeBuffer implements INativeBuffer
* @param timestamp of the buffer
* @param interleavedImplementation optimal, scalar vs vector SIMD
* @param nonInterleavedImplementation optimal, scalar vs vector SIMD
* @param samplesPerMillisecond used to calculate sub-buffer fragment timestamp offsets from the start of this buffer.
*/
public AirspyNativeBuffer(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, Implementation interleavedImplementation,
Implementation nonInterleavedImplementation)
Implementation nonInterleavedImplementation, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % AirspyBufferIterator.FRAGMENT_SIZE != 0)
{
@@ -64,7 +65,6 @@ public class AirspyNativeBuffer implements INativeBuffer
mResidualI = residualI;
mResidualQ = residualQ;
mAverageDc = averageDc;
mTimestamp = timestamp;
mInterleavedImplementation = interleavedImplementation;
mNonInterleavedImplementation = nonInterleavedImplementation;
}
@@ -75,22 +75,16 @@ public class AirspyNativeBuffer implements INativeBuffer
return mSamples.length / 2;
}
@Override
public long getTimestamp()
{
return mTimestamp;
}
@Override
public Iterator<ComplexSamples> iterator()
{
return switch(mInterleavedImplementation)
{
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
};
}
@@ -99,11 +93,11 @@ public class AirspyNativeBuffer implements INativeBuffer
{
return switch(mInterleavedImplementation)
{
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
};
}
}

View File

@@ -19,19 +19,18 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.INativeBufferFactory;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Implements a factory for creating SignedByteNativeBuffer instances
*/
public class AirspyNativeBufferFactory implements INativeBufferFactory
public class AirspyNativeBufferFactory extends AbstractNativeBufferFactory
{
private boolean mSamplePacking = false;
private short[] mResidualI = new short[AirspyBufferIterator.I_OVERLAP];
@@ -106,7 +105,7 @@ public class AirspyNativeBufferFactory implements INativeBufferFactory
INativeBuffer nativeBuffer = new AirspyNativeBuffer(samples,
Arrays.copyOf(mResidualI, mResidualI.length),
Arrays.copyOf(mResidualQ, mResidualQ.length), mConverter.getAverageDc(), timestamp,
mInterleavedIteratorImplementation, mNonInterleavedIteratorImplementation);
mInterleavedIteratorImplementation, mNonInterleavedIteratorImplementation, getSamplesPerMillisecond());
extractResidual(samples);

View File

@@ -46,6 +46,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
private double mChannelSampleRate;
private long mIndexCenterFrequency;
private long mChannelFrequencyCorrection;
private long mCurrentSamplesTimestamp;
private ReentrantLock mOutputProcessorLock = new ReentrantLock();
/**
@@ -106,6 +107,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
public void receive(ComplexSamples complexSamples)
{
mStreamHeartbeatProcessor.receive(complexSamples);
mCurrentSamplesTimestamp = complexSamples.timestamp();
}
/**
@@ -222,7 +224,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
{
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList);
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList, mCurrentSamplesTimestamp);
}
}
finally

View File

@@ -156,7 +156,7 @@ public class TwoChannelSynthesizerM2
mTopBlockFlag = !mTopBlockFlag;
}
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, channelBuffer1.timestamp());
}
/**

View File

@@ -33,6 +33,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
private Dispatcher<List<float[]>> mChannelResultsDispatcher;
protected Listener<ComplexSamples> mComplexSamplesListener;
private int mInputChannelCount;
private long mCurrentSampleTimestamp = System.currentTimeMillis();
/**
* Base class for polyphase channelizer output channel processing. Provides built-in frequency translation
@@ -49,6 +50,15 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
mChannelResultsDispatcher.setListener(floats -> process(floats));
}
/**
* Timestamp for the current series of samples.
* @return time in milliseconds to use with assembled complex sample buffers.
*/
protected long getCurrentSampleTimestamp()
{
return mCurrentSampleTimestamp;
}
@Override
public void start()
{
@@ -81,9 +91,10 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
}
@Override
public void receiveChannelResults(List<float[]> channelResultsList)
public void receiveChannelResults(List<float[]> channelResultsList, long timestamp)
{
mChannelResultsDispatcher.receive(channelResultsList);
mCurrentSampleTimestamp = timestamp;
}
/**

View File

@@ -36,8 +36,9 @@ public interface IPolyphaseChannelOutputProcessor
/**
* Receive and enqueue output results from the polyphase analysis channelizer
* @param channelResults to enqueue
* @param timestamp for the first channel results buffer
*/
void receiveChannelResults(List<float[]> channelResults);
void receiveChannelResults(List<float[]> channelResults, long timestamp);
/**
* Listener to receive assembled complex samples buffers

View File

@@ -65,7 +65,8 @@ public abstract class MixerAssembler
/**
* Access the fully assembled buffer when hasBuffer() indicates that a buffer is ready.
* @param timestamp to use for the assembled complex samples buffer
* @return true if a full buffer is assembled and ready.
*/
public abstract ComplexSamples getBuffer();
public abstract ComplexSamples getBuffer(long timestamp);
}

View File

@@ -61,9 +61,9 @@ public class OneChannelMixerAssembler extends MixerAssembler
* @return assembled buffer.
*/
@Override
public ComplexSamples getBuffer()
public ComplexSamples getBuffer(long timestamp)
{
ComplexSamples buffer = mSampleAssembler.getBufferAndReset();
ComplexSamples buffer = mSampleAssembler.getBufferAndReset(timestamp);
if(getMixer().hasFrequency())
{

View File

@@ -90,7 +90,7 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor
if(mMixerAssembler.hasBuffer())
{
ComplexSamples buffer = mMixerAssembler.getBuffer();
ComplexSamples buffer = mMixerAssembler.getBuffer(getCurrentSampleTimestamp());
if(mComplexSamplesListener != null)
{

View File

@@ -76,9 +76,9 @@ public class SampleAssembler
/**
* Extracts the full buffer from this assembler and resets the assembler to accept more samples.
*/
public ComplexSamples getBufferAndReset()
public ComplexSamples getBufferAndReset(long timestamp)
{
ComplexSamples buffer = new ComplexSamples(mI, mQ);
ComplexSamples buffer = new ComplexSamples(mI, mQ, timestamp);
reset();
return buffer;
}

View File

@@ -76,13 +76,14 @@ public class TwoChannelMixerAssembler extends MixerAssembler
/**
* Access the assembled buffer. Frequency offset and gain are applied to the returned buffer.
* @param timestamp to use for the assembled buffer
* @return assembled buffer.
*/
@Override
public ComplexSamples getBuffer()
public ComplexSamples getBuffer(long timestamp)
{
ComplexSamples channel1 = mChannel1SampleAssembler.getBufferAndReset();
ComplexSamples channel2 = mChannel2SampleAssembler.getBufferAndReset();
ComplexSamples channel1 = mChannel1SampleAssembler.getBufferAndReset(timestamp);
ComplexSamples channel2 = mChannel2SampleAssembler.getBufferAndReset(timestamp);
//Synthesize two channels into one, reducing sample rate by 2 so it's same as single channel rate
ComplexSamples buffer = mTwoChannelSynthesizer.process(channel1, channel2);

View File

@@ -90,7 +90,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
* Extract the channel from the channel results array, apply frequency translation, and deliver the
* extracted frequency-corrected channel I/Q sample set to the complex sample listener.
*
* @param lists to process containing a list of a list of an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
* @param channelResultsList to process containing a list of a list of an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
*/
@Override
public void process(List<float[]> channelResultsList)
@@ -102,7 +102,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
if(mMixerAssembler.hasBuffer())
{
ComplexSamples buffer = mMixerAssembler.getBuffer();
ComplexSamples buffer = mMixerAssembler.getBuffer(getCurrentSampleTimestamp());
if(mComplexSamplesListener != null)
{

View File

@@ -80,9 +80,10 @@ public abstract class HilbertTransform
/**
* Converts the real sample array to complex samples as half the sample rate
* @param samples to convert
* @param timestamp of the first sample
* @return converted samples
*/
public abstract ComplexSamples filter(float[] samples);
public abstract ComplexSamples filter(float[] samples, long timestamp);
/**
* Converts the half-band filter coefficients for use as hilbert transform filter coefficients. Sets all

View File

@@ -1,248 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.hilbert;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
public class LegacyHilbertTransform
{
private static final Logger mLog = LoggerFactory.getLogger(LegacyHilbertTransform.class);
private float[] mHilbertFilter;
private float[] mBuffer;
private int mBufferSize;
private int mBufferPointer;
private int[][] mIndexMap;
private int mMapHeight;
private int mCenterTapIndex;
private float mCenterCoefficent;
private boolean mInvertFlag = false;
/**
* Hilbert transform filter used for converting real-valued samples into
* complex valued samples using frequency translation (FS/4) and a half-band
* filter.
*
* This filter uses a circular sample buffer and a pre-calculated
* index map to correctly map the filter coefficients to the circular delay
* buffer contents as each new sample is added to the buffer and processed.
*
* Half-band filter coefficients used in this filter must be of length N
* where (N + 1) is a multiple of 4. This filter uses a pre-defined half
* band filter with 47 coefficients.
*
* This transform process is described in Understanding Digital Signal
* Processing, Lyons, 3e, 2011, sections 13.1.2 and 13.1.3 (p 674-678) and
* implemented as described in Section 13.37.1 and 13.37.2 (p 802-804)
*/
public LegacyHilbertTransform()
{
convertHalfBandToHilbert(HilbertTransform.HALF_BAND_FILTER_47_TAP);
mBufferSize = mHilbertFilter.length + 1;
mBuffer = new float[mBufferSize];
mCenterCoefficent = mHilbertFilter[mHilbertFilter.length / 2];
generateIndexMap(mHilbertFilter.length);
}
/**
* Filters the continuous real-sample stream by treating the stream as a
* set of interleaved values and applying two hilbert filters against each
* interleaved set (I & Q). Frequency translation has already been
* pre-applied to the hilbert filter coefficients.
*
* Filtering of the inphase component is a simple multiply by the center
* coefficient (0.5) since all of the other coefficients are zero-valued.
*
* Filtering of the quadrature component uses a folded FIR structure and
* expects the hilbert coefficients to be symmetric with all negative valued
* coefficients below the center coefficient and all positive-valued coefficients
* above the center coefficient. So, convolution only applies the filter
* coefficient against the difference of the two samples.
*
* ( coefficient * value1 ) + (-coefficient * value2 ) = coefficient * ( value1 - value2 )
*
* Performs frequency translation by FS/2 on the final filtered values by
* applying a sequence of 1,-1 (sign change) to each I/Q output sample.
*/
public float[] filter(float[] samples)
{
float accumulator;
for(int y = 0; y < samples.length; y += 2)
{
insert(samples[y]);
insert(samples[y + 1]);
accumulator = 0.0f;
int index = mBufferPointer / 2;
for(int x = 0; x < mHilbertFilter.length / 2; x += 2)
{
accumulator += mHilbertFilter[x] *
(mBuffer[mIndexMap[index][x + 1]] -
mBuffer[mIndexMap[index][x]]);
}
//Perform FS/2 frequency translation on the final filtered values
if(mInvertFlag)
{
//inphase
samples[y] = -(mBuffer[mIndexMap[index][mCenterTapIndex]]);
//quadrature
samples[y + 1] = -accumulator;
}
else
{
//inphase
samples[y] = mBuffer[mIndexMap[index][mCenterTapIndex]];
//quadrature
samples[y + 1] = accumulator;
}
mInvertFlag = !mInvertFlag;
}
return samples;
}
/**
* Inserts the sample into the circular buffer, overwriting the oldest value
*/
private void insert(float sample)
{
mBuffer[mBufferPointer++] = sample;
mBufferPointer = mBufferPointer % mBufferSize;
}
/**
* Creates an index map to support efficient lookup of sample indexes from
* the circular buffer.
*
* The first index of the map corresponds to the current buffer pointer
* setting.
*
* The second index is structured in sample pairs to align with
* the hilbert filter coefficients. For example, the first iteration of
* the filter would use filter coefficient 0 and the index map values 0 and
* 1 would be pointing to samples 46 and 0.
*
* The final element of each index set is the index of the center sample.
*/
private void generateIndexMap(int size)
{
mMapHeight = size / 2 + 1;
int mapWidth = mMapHeight + 1;
mIndexMap = new int[mMapHeight][mapWidth];
//Setup the first row with buffer pointer index 0 as a starting point
for(int x = 0; x < mapWidth - 1; x += 2)
{
mIndexMap[0][x] = size - 1 - x;
mIndexMap[0][x + 1] = x;
}
//Place the center index at the end of the array
mCenterTapIndex = mapWidth - 1;
mIndexMap[0][mCenterTapIndex] = size / 2;
//For each subsequent row, increment the previous row's value by 2,
//wrapping as needed, to keep the values between 0 and size - 1
for(int x = 1; x < mMapHeight; x++)
{
for(int y = 0; y < mapWidth; y++)
{
mIndexMap[x][y] = mIndexMap[x - 1][y] + 2;
if(mIndexMap[x][y] >= size)
{
mIndexMap[x][y] -= size + 1;
//Special handling for center tap wrap around
if(y == mCenterTapIndex && mIndexMap[x][y] < 0)
{
mIndexMap[x][y] = size;
}
}
}
}
}
/**
* Logs the index map contents
*/
public void logIndexMap()
{
for(int[] indexSet : mIndexMap)
{
mLog.debug("Row:" + Arrays.toString(indexSet));
}
}
/**
* Converts the half-band filter coefficients for use as hilbert transform
* filter coefficients. Sets all even-numbered coefficients left of center
* to negative and all even-numbered coefficients right of center to positive
* which applies a FS/4 frequency translation to the coefficients.
*
* Note: even though the coefficients above the center coefficient are being
* defined, they are unused during the convolution process since we're using
* a folded FIR structure to exploit the symmetric nature of the half-band
* filter coefficients. The polarity difference between the upper and lower
* halves is accounted for in the filter() method.
*
* Note: we apply a 2.0 gain to the coefficients to compensate for the loss
* of splitting the signal via two filters.
*/
private void convertHalfBandToHilbert(float[] coefficients)
{
mHilbertFilter = new float[coefficients.length];
int middle = coefficients.length / 2;
for(int x = 0; x < coefficients.length; x++)
{
if(x < middle)
{
mHilbertFilter[x] = 2.0f * -FastMath.abs(coefficients[x]);
}
else if(x > middle)
{
mHilbertFilter[x] = 2.0f * FastMath.abs(coefficients[x]);
}
else
{
mHilbertFilter[x] = 2.0f * coefficients[x];
}
}
}
}

View File

@@ -23,7 +23,6 @@ import io.github.dsheirer.dsp.oscillator.IRealOscillator;
import io.github.dsheirer.dsp.oscillator.OscillatorFactory;
import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
/**
@@ -35,9 +34,10 @@ public class ScalarHilbertTransform extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;
@@ -52,7 +52,7 @@ public class ScalarHilbertTransform extends HilbertTransform
mQBuffer = qTemp;
}
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
System.arraycopy(deinterleaved.q(), 0, mQBuffer, mQOverlap, deinterleaved.q().length);
@@ -78,7 +78,7 @@ public class ScalarHilbertTransform extends HilbertTransform
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
public static void main(String[] args)
@@ -104,8 +104,8 @@ public class ScalarHilbertTransform extends HilbertTransform
if(validate)
{
ComplexSamples scalar = hilbertTransform.filter(realSamples);
ComplexSamples vector = vector512.filter(realSamples);
ComplexSamples scalar = hilbertTransform.filter(realSamples, 0l);
ComplexSamples vector = vector512.filter(realSamples, start);
System.out.println("SI: " + Arrays.toString(scalar.i()));
System.out.println("VI: " + Arrays.toString(vector.i()));
@@ -119,7 +119,7 @@ public class ScalarHilbertTransform extends HilbertTransform
{
// ComplexSamples filtered = hilbertTransform.filter(realSamples);
// ComplexSamples filtered = vectorDefault.filter(realSamples);
ComplexSamples filtered = vector512.filter(realSamples);
ComplexSamples filtered = vector512.filter(realSamples, start);
// ComplexSamples filtered = vector256.filter(realSamples);
// ComplexSamples filtered = vector128.filter(realSamples);
// ComplexSamples filtered = vector64.filter(realSamples);

View File

@@ -37,9 +37,10 @@ public class VectorHilbertTransform128Bits extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;
@@ -54,7 +55,7 @@ public class VectorHilbertTransform128Bits extends HilbertTransform
mQBuffer = qTemp;
}
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
@@ -91,6 +92,6 @@ public class VectorHilbertTransform128Bits extends HilbertTransform
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -37,9 +37,10 @@ public class VectorHilbertTransform256Bits extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;
@@ -54,7 +55,7 @@ public class VectorHilbertTransform256Bits extends HilbertTransform
mQBuffer = qTemp;
}
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
@@ -85,6 +86,6 @@ public class VectorHilbertTransform256Bits extends HilbertTransform
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -22,12 +22,11 @@ package io.github.dsheirer.dsp.filter.hilbert;
import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.vector.VectorUtilities;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.util.Arrays;
/**
* Implements a Hilbert transform using vector SIMD operations against a pre-defined 47-tap filter.
* As described in Understanding Digital Signal Processing, Lyons, 3e, 2011, Section 13.37.2 (p 804-805)
@@ -53,9 +52,10 @@ public class VectorHilbertTransform512Bits extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;
@@ -70,7 +70,7 @@ public class VectorHilbertTransform512Bits extends HilbertTransform
mQBuffer = qTemp;
}
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
@@ -103,6 +103,6 @@ public class VectorHilbertTransform512Bits extends HilbertTransform
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -37,9 +37,10 @@ public class VectorHilbertTransform64Bits extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;
@@ -54,7 +55,7 @@ public class VectorHilbertTransform64Bits extends HilbertTransform
mQBuffer = qTemp;
}
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
@@ -103,6 +104,6 @@ public class VectorHilbertTransform64Bits extends HilbertTransform
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -37,9 +37,10 @@ public class VectorHilbertTransformDefaultBits extends HilbertTransform
/**
* Filters the real samples array into complex samples at half the sample rate.
* @param realSamples to filter
* @param timestamp of the first sample
* @return complex samples at half the sample rate
*/
public ComplexSamples filter(float[] realSamples)
public ComplexSamples filter(float[] realSamples, long timestamp)
{
int bufferLength = realSamples.length / 2;
@@ -54,7 +55,7 @@ public class VectorHilbertTransformDefaultBits extends HilbertTransform
mQBuffer = qTemp;
}
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
@@ -84,6 +85,6 @@ public class VectorHilbertTransformDefaultBits extends HilbertTransform
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.gain.complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
@@ -46,7 +65,7 @@ public abstract class ComplexGain
*/
public ComplexSamples apply(ComplexSamples samples)
{
return apply(samples.i(), samples.q());
return apply(samples.i(), samples.q(), samples.timestamp());
}
/**
@@ -56,7 +75,8 @@ public abstract class ComplexGain
* original sample buffer.
* @param i samples to amplify
* @param q samples to amplify
* @param timestamp of the first sample
* @return amplified samples
*/
public abstract ComplexSamples apply(float[] i, float q[]);
public abstract ComplexSamples apply(float[] i, float q[], long timestamp);
}

View File

@@ -18,13 +18,9 @@
*/
package io.github.dsheirer.dsp.gain.complex;
import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.text.DecimalFormat;
import java.util.Arrays;
public class ComplexGainControl implements IComplexGainControl
{
public static final float OBJECTIVE_ENVELOPE = 1.0f;
@@ -48,9 +44,10 @@ public class ComplexGainControl implements IComplexGainControl
* gain for the single sample in the buffer that has the largest envelope.
* @param i samples
* @param q samples
* @param timestamp of the first sample
* @return processed/amplified buffer
*/
@Override public ComplexSamples process(float[] i, float[] q)
@Override public ComplexSamples process(float[] i, float[] q, long timestamp)
{
float maxEnvelope = MINIMUM_ENVELOPE;
@@ -70,45 +67,6 @@ public class ComplexGainControl implements IComplexGainControl
qProcessed[x] = q[x] * gain;
}
return new ComplexSamples(iProcessed, qProcessed);
}
public static void main(String[] args)
{
int sampleSize = 2048;
ComplexSamples samples = SampleUtils.generateComplexRandomVectorLength(sampleSize);
ComplexGainControl scalar = new ComplexGainControl();
VectorComplexGainControl vector = new VectorComplexGainControl();
boolean validate = false;
if(validate)
{
ComplexSamples scalarSamples = scalar.process(samples.i(), samples.q());
ComplexSamples vectorSamples = vector.process(samples.i(), samples.q());
System.out.println("SCALAR I: " + Arrays.toString(scalarSamples.i()));
System.out.println("VECTOR I: " + Arrays.toString(vectorSamples.i()));
System.out.println("SCALAR Q: " + Arrays.toString(scalarSamples.q()));
System.out.println("VECTOR Q: " + Arrays.toString(vectorSamples.q()));
}
else
{
double accumulator = 0.0d;
int iterations = 10_000_000;
long start = System.currentTimeMillis();
for(int x = 0; x < iterations; x++)
{
// ComplexSamples processed = scalar.process(samples.i(), samples.q());
ComplexSamples processed = vector.process(samples.i(), samples.q());
accumulator += processed.i()[3];
}
double elapsed = System.currentTimeMillis() - start;
DecimalFormat df = new DecimalFormat("0.000");
System.out.println("Accumulator: " + accumulator);
System.out.println("Test Complete. Elapsed Time: " + df.format(elapsed / 1000.0d) + " seconds");
}
return new ComplexSamples(iProcessed, qProcessed, timestamp);
}
}

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.gain.complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
@@ -13,5 +32,5 @@ public interface IComplexGainControl
* @param q samples
* @return amplified gain samples
*/
ComplexSamples process(float[] i, float[] q);
ComplexSamples process(float[] i, float[] q, long timestamp);
}

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.gain.complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
@@ -22,7 +41,7 @@ public class ScalarComplexGain extends ComplexGain
* @param q samples to amplify
* @return amplified samples
*/
@Override public ComplexSamples apply(float[] i, float[] q)
@Override public ComplexSamples apply(float[] i, float[] q, long timestamp)
{
for(int x = 0; x < i.length; x++)
{
@@ -30,6 +49,6 @@ public class ScalarComplexGain extends ComplexGain
q[x] *= mGain;
}
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.gain.complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
@@ -27,7 +46,7 @@ public class VectorComplexGain extends ComplexGain
* @param q samples to amplify
* @return amplified samples.
*/
@Override public ComplexSamples apply(float[] i, float[] q)
@Override public ComplexSamples apply(float[] i, float[] q, long timestamp)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
@@ -37,6 +56,6 @@ public class VectorComplexGain extends ComplexGain
FloatVector.fromArray(VECTOR_SPECIES, q, x).mul(mGain).intoArray(q, x);
}
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.gain.complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
@@ -18,7 +37,7 @@ public class VectorComplexGainControl implements IComplexGainControl
private static final float MINIMUM_ENVELOPE = 0.0001f;
private static final float ENVELOPE_ESTIMATE = 0.4f;
@Override public ComplexSamples process(float[] i, float[] q)
@Override public ComplexSamples process(float[] i, float[] q, long timestamp)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
@@ -47,6 +66,6 @@ public class VectorComplexGainControl implements IComplexGainControl
FloatVector.fromArray(VECTOR_SPECIES, q, bufferPointer).mul(gain).intoArray(qProcessed, bufferPointer);
}
return new ComplexSamples(iProcessed, qProcessed);
return new ComplexSamples(iProcessed, qProcessed, timestamp);
}
}

View File

@@ -84,11 +84,12 @@ public abstract class ComplexMixer
/**
* Generates complex samples from the underlying oscillator
* @param sampleCount to generate
* @param timestamp of the first sample
* @return complex samples
*/
protected ComplexSamples generate(int sampleCount)
protected ComplexSamples generate(int sampleCount, long timestamp)
{
return mOscillator.generateComplexSamples(sampleCount);
return mOscillator.generateComplexSamples(sampleCount, timestamp);
}
/**
@@ -98,7 +99,7 @@ public abstract class ComplexMixer
*/
public ComplexSamples mix(ComplexSamples samples)
{
return mix(samples.i(), samples.q());
return mix(samples.i(), samples.q(), samples.timestamp());
}
/**
@@ -117,7 +118,5 @@ public abstract class ComplexMixer
* @param q complex samples to mix
* @return mixed samples
*/
public abstract ComplexSamples mix(float[] i, float[] q);
public abstract ComplexSamples mix(float[] i, float[] q, long timestamp);
}

View File

@@ -52,11 +52,12 @@ public class ScalarComplexMixer extends ComplexMixer
* Mixes the complex I & Q samples with samples generated from an oscillator.
* @param iSamples complex samples to mix
* @param qSamples complex samples to mix
* @param timestamp of the first sample
* @return mixed samples
*/
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples)
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples, long timestamp)
{
ComplexSamples mixer = generate(iSamples.length);
ComplexSamples mixer = generate(iSamples.length, timestamp);
float[] iMixer = mixer.i();
float[] qMixer = mixer.q();
@@ -82,7 +83,7 @@ public class ScalarComplexMixer extends ComplexMixer
@Override
public ComplexSamples mix(InterleavedComplexSamples samples)
{
ComplexSamples mixer = generate(samples.samples().length / 2);
ComplexSamples mixer = generate(samples.samples().length / 2, samples.timestamp());
float[] iMixer = mixer.i();
float[] qMixer = mixer.q();

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.mixer;
import io.github.dsheirer.sample.complex.ComplexSamples;
@@ -26,14 +45,15 @@ public class VectorComplexMixer extends ComplexMixer
* Mixes the complex I & Q samples with samples generated from an oscillator.
* @param iSamples complex samples to mix
* @param qSamples complex samples to mix
* @param timestamp for the first sample
* @return mixed samples
*/
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples)
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples, long timestamp)
{
VectorUtilities.checkComplexArrayLength(iSamples, qSamples, VECTOR_SPECIES);
//Reuse this complex samples buffer to store the results and return to caller
ComplexSamples mixer = generate(iSamples.length);
ComplexSamples mixer = generate(iSamples.length, timestamp);
float[] iMixer = mixer.i();
float[] qMixer = mixer.q();

View File

@@ -33,7 +33,8 @@ public interface IComplexOscillator extends IOscillator
/**
* Generates the specified number of complex samples into a complex samples object.
* @param sampleCount number of complex samples to generate.
* @param timestamp for the first sample
* @return generated samples
*/
ComplexSamples generateComplexSamples(int sampleCount);
ComplexSamples generateComplexSamples(int sampleCount, long timestamp);
}

View File

@@ -96,7 +96,7 @@ public class ScalarComplexOscillator extends AbstractOscillator implements IComp
}
@Override
public ComplexSamples generateComplexSamples(int sampleCount)
public ComplexSamples generateComplexSamples(int sampleCount, long timestamp)
{
float[] iSamples = new float[sampleCount];
float[] qSamples = new float[sampleCount];
@@ -133,6 +133,6 @@ public class ScalarComplexOscillator extends AbstractOscillator implements IComp
mPreviousInphase = previousInphase;
mPreviousQuadrature = previousQuadrature;
return new ComplexSamples(iSamples, qSamples);
return new ComplexSamples(iSamples, qSamples, timestamp);
}
}

View File

@@ -21,14 +21,13 @@ package io.github.dsheirer.dsp.oscillator;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.vector.VectorUtilities;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
/**
* Complex oscillator that uses JDK17 SIMD vector operations to generate complex sample arrays.
*
@@ -145,9 +144,10 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp
/**
* Generates complex samples.
* @param sampleCount number of samples to generate and length of the resulting float array.
* @param timestamp of the first sample
* @return generated samples
*/
@Override public ComplexSamples generateComplexSamples(int sampleCount)
@Override public ComplexSamples generateComplexSamples(int sampleCount, long timestamp)
{
if(sampleCount % VECTOR_SPECIES.length() != 0)
{
@@ -184,6 +184,6 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp
previousInphase.intoArray(mPreviousInphases, 0);
previousQuadrature.intoArray(mPreviousQuadratures, 0);
return new ComplexSamples(iSamples, qSamples);
return new ComplexSamples(iSamples, qSamples, timestamp);
}
}

View File

@@ -34,6 +34,9 @@ import io.github.dsheirer.spectrum.DFTSize;
import io.github.dsheirer.spectrum.SpectrumPanel;
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
import io.github.dsheirer.util.ThreadPool;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.util.concurrent.TimeUnit;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -45,9 +48,6 @@ import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.util.concurrent.TimeUnit;
public class SynthesizerViewer extends JFrame
{
@@ -281,15 +281,15 @@ public class SynthesizerViewer extends JFrame
@Override
public void run()
{
ComplexSamples channel1Buffer = getChannel1ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle);
ComplexSamples channel2Buffer = getChannel2ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle);
ComplexSamples channel1Buffer = getChannel1ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle, 0l);
ComplexSamples channel2Buffer = getChannel2ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle, 0l);
ComplexSamples synthesizedBuffer = mSynthesizer.process(channel1Buffer, channel2Buffer);
getChannel1Panel().receive(new FloatNativeBuffer(channel1Buffer, System.currentTimeMillis()));
getChannel1Panel().receive(new FloatNativeBuffer(channel1Buffer));
getChannel2Panel().receive(new FloatNativeBuffer(channel2Buffer, System.currentTimeMillis()));
getChannel2Panel().receive(new FloatNativeBuffer(channel2Buffer));
getSpectrumPanel().receive(new FloatNativeBuffer(synthesizedBuffer, System.currentTimeMillis()));
getSpectrumPanel().receive(new FloatNativeBuffer(synthesizedBuffer));
}
}

View File

@@ -42,12 +42,11 @@ import io.github.dsheirer.sample.complex.IComplexSamplesListener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DMR decoder module.
@@ -147,7 +146,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(System.currentTimeMillis());
mMessageFramer.setCurrentTime(samples.timestamp());
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
@@ -155,7 +154,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
//Process buffer for power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

View File

@@ -53,20 +53,20 @@ public class DMRDecoderInstrumented extends DMRDecoder
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(System.currentTimeMillis());
mMessageFramer.setCurrentTime(samples.timestamp());
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
if(mFilteredSymbolListener != null)
{
mFilteredSymbolListener.receive(new ComplexSamples(i, q));
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
}
//Process buffer for power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 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/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
@@ -35,11 +32,10 @@ import io.github.dsheirer.dsp.psk.pll.PLLBandwidth;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class P25P1DecoderC4FM extends P25P1Decoder
{
@@ -110,7 +106,7 @@ public class P25P1DecoderC4FM extends P25P1Decoder
//Process the buffer for power meter measurements (before gain is applied)
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

View File

@@ -49,20 +49,20 @@ public class P25P1DecoderC4FMInstrumented extends P25P1DecoderC4FM
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(System.currentTimeMillis());
mMessageFramer.setCurrentTime(samples.timestamp());
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
if(mFilteredSymbolListener != null)
{
mFilteredSymbolListener.receive(new ComplexSamples(i, q));
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
}
//Process the buffer for power meter measurements (before gain is applied)
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 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/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
@@ -34,11 +31,10 @@ import io.github.dsheirer.dsp.window.WindowType;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class P25P1DecoderLSM extends P25P1Decoder
{
@@ -107,7 +103,7 @@ public class P25P1DecoderLSM extends P25P1Decoder
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(System.currentTimeMillis());
mMessageFramer.setCurrentTime(samples.timestamp());
//The filter will decrement the user count when finished
float[] i = mIBasebandFilter.filter(samples.i());
@@ -116,7 +112,7 @@ public class P25P1DecoderLSM extends P25P1Decoder
//Process the buffer for power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

View File

@@ -50,7 +50,7 @@ public class P25P1DecoderLSMInstrumented extends P25P1DecoderLSM
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setCurrentTime(System.currentTimeMillis());
mMessageFramer.setCurrentTime(samples.timestamp());
//The filter will decrement the user count when finished
float[] i = mIBasebandFilter.filter(samples.i());
@@ -58,13 +58,13 @@ public class P25P1DecoderLSMInstrumented extends P25P1DecoderLSM
if(mFilteredSymbolListener != null)
{
mFilteredSymbolListener.receive(new ComplexSamples(i, q));
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
}
//Process the buffer for power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 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/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2;
@@ -40,11 +37,10 @@ import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameter
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* P25 Phase 2 HDQPSK 2-timeslot Decoder
@@ -121,7 +117,7 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate
//Process the buffer for power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

View File

@@ -71,7 +71,7 @@ public class P25P2DecoderHDQPSKInstrumented extends P25P2DecoderHDQPSK
//Process the buffer for power measurements
mPowerMonitor.process(i, q);
ComplexSamples amplified = mAGC.process(i, q);
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
mQPSKDemodulator.receive(amplified);
}

View File

@@ -28,14 +28,14 @@ import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.util.Dispatcher;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
/**
* WAVE audio recorder module for recording complex (I&Q) samples to a wave file
@@ -46,7 +46,7 @@ public class ComplexSamplesWaveRecorder extends Module implements IComplexSample
private final static Logger mLog = LoggerFactory.getLogger(ComplexSamplesWaveRecorder.class);
private Dispatcher<ComplexSamples> mBufferProcessor = new Dispatcher<>(500,
"sdrtrunk complex wave recorder", new ComplexSamples(new float[0], new float[0]));
"sdrtrunk complex wave recorder", new ComplexSamples(new float[0], new float[0], System.currentTimeMillis()));
private AtomicBoolean mRunning = new AtomicBoolean();
private BufferWaveWriter mWriter;

View File

@@ -21,9 +21,8 @@ package io.github.dsheirer.sample;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
import org.apache.commons.math3.util.FastMath;
import java.util.Random;
import org.apache.commons.math3.util.FastMath;
public class SampleUtils
{
@@ -33,9 +32,10 @@ public class SampleUtils
* Converts from an interleaved complex sample array to a sample record with the
* I and Q in separate arrays.
* @param samples that are interleaved complex samples
* @param timestamp of the samples
* @return deinterleaved complex samples instance
*/
public static ComplexSamples deinterleave(float[] samples)
public static ComplexSamples deinterleave(float[] samples, long timestamp)
{
float[] i = new float[samples.length / 2];
float[] q = new float[samples.length / 2];
@@ -49,7 +49,7 @@ public class SampleUtils
q[x] = samples[offset + 1];
}
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
/**
@@ -94,7 +94,7 @@ public class SampleUtils
q[x] = (float)Math.sin(angle);
}
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, System.currentTimeMillis());
}
/**
@@ -119,7 +119,7 @@ public class SampleUtils
q[x] *= gain;
}
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, System.currentTimeMillis());
}
public static Complex multiply(Complex a, Complex b)

View File

@@ -20,11 +20,10 @@ package io.github.dsheirer.sample.adapter;
import io.github.dsheirer.buffer.FloatNativeBuffer;
import io.github.dsheirer.buffer.INativeBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converts 16-bit/2-byte little endian byte data into a reusable buffer of complex float sample data
@@ -62,7 +61,7 @@ public class ComplexShortAdapter implements ISampleAdapter<INativeBuffer>
long now = System.currentTimeMillis();
return new FloatNativeBuffer(convertedSamples, now);
return new FloatNativeBuffer(convertedSamples, now, 192.0f);
}
/**

View File

@@ -22,7 +22,7 @@ package io.github.dsheirer.sample.complex;
/**
* Wrapper for a complex sample array where I and Q samples are in separate arrays.
*/
public record ComplexSamples(float[] i, float[] q)
public record ComplexSamples(float[] i, float[] q, long timestamp)
{
/**
* Converts this non-interleaved complex samples to interleaved.

View File

@@ -28,7 +28,6 @@ import java.util.Iterator;
public class ComplexSamplesNativeBufferAdapter implements INativeBuffer
{
private ComplexSamples mComplexSamples;
private long mTimestamp = System.currentTimeMillis();
/**
* Constructs an instance
@@ -60,7 +59,7 @@ public class ComplexSamplesNativeBufferAdapter implements INativeBuffer
@Override
public long getTimestamp()
{
return mTimestamp;
return mComplexSamples.timestamp();
}
/**

View File

@@ -1,95 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.sample.complex;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Processes I & Q sample arrays into uniform length complex samples buffers.
*/
public class ComplexSamplesRepackager
{
private int mSize;
private float[] mIResidual = new float[0];
private float[] mQResidual = new float[0];
/**
* Constructs an instance.
* @param size or length of the output complex samples.
*/
public ComplexSamplesRepackager(int size)
{
mSize = size;
}
/**
* Combines the I & Q sample arrays with any residual leftover from the previous invocation and
* chunks the arrays up into uniform length complex sample buffers, storing any leftover samples
* as residual to use in the next processing invocation. If the length of the sample arguments
* combined with any residual is not enough to create at least one buffer, those samples will be
* added to the residual and used in the next operation, and no complex sample buffer will be
* returned.
*
* @param i samples array to repackage
* @param q samples array to repackage
* @return zero or more complex samples of uniform length.
*/
public List<ComplexSamples> process(float[] i, float[] q)
{
float[] iCombined = new float[mIResidual.length + i.length];
float[] qCombined = new float[mQResidual.length + q.length];
if(mIResidual.length > 0)
{
System.arraycopy(mIResidual, 0, iCombined, 0, mIResidual.length);
System.arraycopy(mQResidual, 0, qCombined, 0, mQResidual.length);
}
System.arraycopy(i, 0, iCombined, mIResidual.length, i.length);
System.arraycopy(q, 0, qCombined, mQResidual.length, q.length);
int processed = 0;
List<ComplexSamples> samples = new ArrayList<>();
while((processed + mSize) < iCombined.length)
{
float[] iChunk = Arrays.copyOfRange(iCombined, processed, processed + mSize);
float[] qChunk = Arrays.copyOfRange(qCombined, processed, processed + mSize);
samples.add(new ComplexSamples(iChunk, qChunk));
processed += mSize;
}
if(processed < iCombined.length)
{
mIResidual = Arrays.copyOfRange(iCombined, processed, iCombined.length);
mQResidual = Arrays.copyOfRange(qCombined, processed, qCombined.length);
}
else
{
mIResidual = new float[0];
mQResidual = new float[0];
}
return samples;
}
}

View File

@@ -41,6 +41,6 @@ public record InterleavedComplexSamples(float[] samples, long timestamp) impleme
q[x] = samples()[x / 2 + 1];
}
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp());
}
}

View File

@@ -26,6 +26,12 @@ import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.usb.USBTunerController;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import org.apache.commons.io.EndianUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,12 +39,6 @@ import org.usb4java.LibUsb;
import org.usb4java.LibUsbException;
import javax.usb.UsbException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
/**
* Airspy tuner controller
@@ -272,6 +272,7 @@ public class AirspyTunerController extends USBTunerController
*/
public void setSampleRate(AirspySampleRate rate) throws LibUsbException, UsbException, SourceException
{
getNativeBufferFactory().setSamplesPerMillisecond((float)rate.getRate() / 1000.0f);
if(rate.getRate() != mSampleRate)
{
int result = readByte(Command.SET_SAMPLE_RATE, 0, rate.getIndex(), true);

View File

@@ -29,11 +29,10 @@ import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.util.Dispatcher;
import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Iterator;
/**
* Complex sample source that provides a frequency-translated and decimated sample buffer stream. Uses a
* CIC decimation filter that requires the decimation rate to be an integer multiple. Sample buffer processing
@@ -229,7 +228,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
ComplexSamples basebanded = mFrequencyCorrectionMixer.mix(iterator.next());
float[] i = mIDecimationFilter.decimateReal(basebanded.i());
float[] q = mQDecimationFilter.decimateReal(basebanded.q());
mStreamHeartbeatProcessor.receive(new ComplexSamples(i, q));
mStreamHeartbeatProcessor.receive(new ComplexSamples(i, q, basebanded.timestamp()));
}
}
}

View File

@@ -25,6 +25,10 @@ import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.usb.USBTunerController;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.EnumSet;
import org.apache.commons.io.EndianUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,10 +36,6 @@ import org.usb4java.LibUsb;
import org.usb4java.LibUsbException;
import javax.usb.UsbException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.EnumSet;
public class HackRFTunerController extends USBTunerController
{
@@ -404,6 +404,7 @@ public class HackRFTunerController extends USBTunerController
mFrequencyController.setSampleRate(rate.getRate());
setBasebandFilter(rate.getFilter());
mSampleRate = rate;
getNativeBufferFactory().setSamplesPerMillisecond((float)getSampleRate() / 1000.0f);
}
public void setSampleRateManual(int frequency, int divider) throws UsbException

View File

@@ -27,6 +27,12 @@ import io.github.dsheirer.source.tuner.TunerFactory;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.usb.USBTunerController;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.usb4java.DeviceHandle;
@@ -35,12 +41,6 @@ import org.usb4java.LibUsbException;
import javax.usb.UsbDisconnectedException;
import javax.usb.UsbException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;
/**
* RTL-2832 tuner controller implementation.
@@ -953,6 +953,9 @@ public class RTL2832TunerController extends USBTunerController
setSampleRateFilters(sampleRate.getRate());
mSampleRate = sampleRate;
mFrequencyController.setSampleRate(sampleRate.getRate());
//Updates native buffer factory so that sample buffers can be accurately timestamped
getNativeBufferFactory().setSamplesPerMillisecond(sampleRate.getRate() / 1000.0f);
}
finally
{

View File

@@ -25,13 +25,12 @@ import io.github.dsheirer.dsp.oscillator.OscillatorFactory;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class SampleGenerator
{
private final static Logger mLog = LoggerFactory.getLogger(SampleGenerator.class);
@@ -189,7 +188,7 @@ public class SampleGenerator
long now = System.currentTimeMillis();
FloatNativeBuffer buffer = new FloatNativeBuffer(samples, now);
FloatNativeBuffer buffer = new FloatNativeBuffer(samples, now, 0.0f);
mNativeBufferBroadcaster.broadcast(buffer);

View File

@@ -28,6 +28,11 @@ import io.github.dsheirer.source.IFrameLocationListener;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.util.ThreadPool;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,11 +41,6 @@ import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class ComplexWaveSource extends Source implements IControllableFileSource, AutoCloseable
{
@@ -276,7 +276,8 @@ public class ComplexWaveSource extends Source implements IControllableFileSource
}
float[] samples = ConversionUtils.convertFromSigned16BitSamples(buffer);
mListener.receive(new FloatNativeBuffer(samples, System.currentTimeMillis()));
mListener.receive(new FloatNativeBuffer(samples, System.currentTimeMillis(),
mInputStream.getFormat().getSampleRate() / 1000.0f));
}
}
}

View File

@@ -214,7 +214,7 @@ public class AirspyUnpackedCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
AirspyBufferIteratorScalar iterator = new AirspyBufferIteratorScalar(samples, residualI,
residualQ, 0.0f, System.currentTimeMillis());
residualQ, 0.0f, System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{
@@ -235,7 +235,7 @@ public class AirspyUnpackedCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
AirspyBufferIteratorVector64Bits iterator = new AirspyBufferIteratorVector64Bits(samples, residualI,
residualQ, 0.0f, System.currentTimeMillis());
residualQ, 0.0f, System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{
@@ -255,7 +255,7 @@ public class AirspyUnpackedCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
AirspyBufferIteratorVector128Bits iterator = new AirspyBufferIteratorVector128Bits(samples, residualI,
residualQ, 0.0f, System.currentTimeMillis());
residualQ, 0.0f, System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{
@@ -276,7 +276,8 @@ public class AirspyUnpackedCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
AirspyBufferIteratorVector256Bits iterator =
new AirspyBufferIteratorVector256Bits(samples, residualI, residualQ, 0.0f, System.currentTimeMillis());
new AirspyBufferIteratorVector256Bits(samples, residualI, residualQ, 0.0f,
System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{
@@ -297,7 +298,8 @@ public class AirspyUnpackedCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
AirspyBufferIteratorVector512Bits iterator =
new AirspyBufferIteratorVector512Bits(samples, residualI, residualQ, 0.0f, System.currentTimeMillis());
new AirspyBufferIteratorVector512Bits(samples, residualI, residualQ, 0.0f,
System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{

View File

@@ -225,7 +225,7 @@ public class AirspyUnpackedInterleavedCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
AirspyInterleavedBufferIteratorScalar iterator = new AirspyInterleavedBufferIteratorScalar(samples, residualI,
residualQ, 0.0f, System.currentTimeMillis());
residualQ, 0.0f, System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{
@@ -246,7 +246,7 @@ public class AirspyUnpackedInterleavedCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
AirspyInterleavedBufferIteratorVector64Bits iterator = new AirspyInterleavedBufferIteratorVector64Bits(samples, residualI,
residualQ, 0.0f, System.currentTimeMillis());
residualQ, 0.0f, System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{
@@ -267,7 +267,7 @@ public class AirspyUnpackedInterleavedCalibration extends Calibration
for(int x = 0; x < ITERATION_DURATION_MS; x++)
{
AirspyInterleavedBufferIteratorVector128Bits iterator = new AirspyInterleavedBufferIteratorVector128Bits(samples, residualI,
residualQ, 0.0f, System.currentTimeMillis());
residualQ, 0.0f, System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{
@@ -288,7 +288,7 @@ public class AirspyUnpackedInterleavedCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
AirspyInterleavedBufferIteratorVector256Bits iterator =
new AirspyInterleavedBufferIteratorVector256Bits(samples, residualI, residualQ, 0.0f, System.currentTimeMillis());
new AirspyInterleavedBufferIteratorVector256Bits(samples, residualI, residualQ, 0.0f, System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{
@@ -309,7 +309,8 @@ public class AirspyUnpackedInterleavedCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
AirspyInterleavedBufferIteratorVector512Bits iterator =
new AirspyInterleavedBufferIteratorVector512Bits(samples, residualI, residualQ, 0.0f, System.currentTimeMillis());
new AirspyInterleavedBufferIteratorVector512Bits(samples, residualI, residualQ, 0.0f,
System.currentTimeMillis(), 0.0f);
while(iterator.hasNext() && ((System.currentTimeMillis() - start) < ITERATION_DURATION_MS))
{

View File

@@ -121,7 +121,7 @@ public class ComplexGainCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples amplified = scalar.apply(i, q);
ComplexSamples amplified = scalar.apply(i, q, start);
accumulator += amplified.i()[2];
count++;
}
@@ -140,7 +140,7 @@ public class ComplexGainCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples amplified = scalar.apply(i, q);
ComplexSamples amplified = scalar.apply(i, q, start);
accumulator += amplified.i()[2];
count++;
}

View File

@@ -116,7 +116,7 @@ public class ComplexGainControlCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples amplified = mScalarGainControl.process(i, q);
ComplexSamples amplified = mScalarGainControl.process(i, q, 0l);
accumulator += amplified.i()[2];
count++;
}
@@ -133,7 +133,7 @@ public class ComplexGainControlCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples amplified = mVectorGainControl.process(i, q);
ComplexSamples amplified = mVectorGainControl.process(i, q, 0l);
accumulator += amplified.i()[2];
count++;
}

View File

@@ -255,7 +255,7 @@ public class HilbertCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples complex = mScalar.filter(samples);
ComplexSamples complex = mScalar.filter(samples, start);
accumulator += complex.i()[0];
count++;
}
@@ -272,7 +272,7 @@ public class HilbertCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples complex = mVectorPreferred.filter(samples);
ComplexSamples complex = mVectorPreferred.filter(samples, start);
accumulator += complex.i()[0];
count++;
}
@@ -289,7 +289,7 @@ public class HilbertCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples complex = mVector512.filter(samples);
ComplexSamples complex = mVector512.filter(samples, start);
accumulator += complex.i()[0];
count++;
}
@@ -306,7 +306,7 @@ public class HilbertCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples complex = mVector256.filter(samples);
ComplexSamples complex = mVector256.filter(samples, start);
accumulator += complex.i()[0];
count++;
}
@@ -323,7 +323,7 @@ public class HilbertCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples complex = mVector128.filter(samples);
ComplexSamples complex = mVector128.filter(samples, start);
accumulator += complex.i()[0];
count++;
}
@@ -340,7 +340,7 @@ public class HilbertCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples complex = mVector64.filter(samples);
ComplexSamples complex = mVector64.filter(samples, start);
accumulator += complex.i()[0];
count++;
}

View File

@@ -115,7 +115,7 @@ public class ComplexMixerCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples mixed = mScalarComplexMixer.mix(i, q);
ComplexSamples mixed = mScalarComplexMixer.mix(i, q, start);
accumulator += mixed.i()[2];
count++;
}
@@ -131,7 +131,7 @@ public class ComplexMixerCalibration extends Calibration
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
ComplexSamples mixed = mVectorComplexMixer.mix(i, q);
ComplexSamples mixed = mVectorComplexMixer.mix(i, q, start);
accumulator += mixed.i()[2];
count++;
}