##2544 bladeRF supports up to 122 MHz sample rate. Adds new 2048 and 4096 half band decimation filters. (#2545)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-11-01 04:24:22 -05:00
committed by GitHub
parent 276f4a316a
commit 866aadc8c3
11 changed files with 660 additions and 31 deletions

View File

@@ -29,12 +29,14 @@ import org.slf4j.LoggerFactory;
public class DecimationFilterFactory
{
private static final Logger mLog = LoggerFactory.getLogger(DecimationFilterFactory.class);
private static final int[] SUPPORTED_RATES = new int[]{1,2,4,8,16,32,64,128,256,512,1024};
public static final int MAXIMUM_DECIMATION_RATE = 4096;
private static final int[] SUPPORTED_RATES = new int[]{1,2,4,8,16,32,64,128,256,512,1024,2048,MAXIMUM_DECIMATION_RATE};
/**
* Creates a real-valued decimation filter for float array sample buffers providing approximately 70 dB of
* attenuation for out of band signal aliases, when the signal bandwidth is no more than half of the decimated
* sample rate. Supports power of 2 decimation rates up to 1024.
* sample rate. Supports power of 2 decimation rates up to 4096. Sample buffers must be an integer multiple of the
* decimation rate.
*
* @param decimationRate requested @see SUPPORTED_RATES
* @return constructed decimation filter
@@ -66,6 +68,10 @@ public class DecimationFilterFactory
return new RealDecimateX512Filter();
case 1024:
return new RealDecimateX1024Filter();
case 2048:
return new RealDecimateX2048Filter();
case 4096:
return new RealDecimateX4096Filter();
default:
throw new IllegalArgumentException("Unsupported decimation rate: " + decimationRate +
". Supported decimation rates are:" + Arrays.toString(SUPPORTED_RATES));

View File

@@ -0,0 +1,54 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.decimate;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.window.WindowType;
/**
* Decimate by 2048 filter for real valued sample buffers.
*/
public class RealDecimateX2048Filter extends RealDecimateX1024Filter
{
private static final int VALIDATION_LENGTH = 2048;
private static final int DECIMATE_BY_2048_FILTER_LENGTH = 11;
private static final WindowType DECIMATE_BY_2048_WINDOW_TYPE = WindowType.BLACKMAN;
private IRealDecimationFilter mFilter;
private float[] mOutput;
/**
* Constructs the decimation filter.
*/
public RealDecimateX2048Filter()
{
mFilter = FilterFactory.getRealDecimationFilter(DECIMATE_BY_2048_FILTER_LENGTH,
DECIMATE_BY_2048_WINDOW_TYPE);
}
@Override
public float[] decimateReal(float[] samples)
{
validate(samples, VALIDATION_LENGTH);
//Decimate by this filter, then by the parent decimation filter
mOutput = getIntermediateOutput(mOutput, samples.length / 2);
return super.decimateReal(mFilter.decimateReal(samples, mOutput));
}
}

View File

@@ -0,0 +1,54 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.decimate;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.window.WindowType;
/**
* Decimate by 4096 filter for real valued sample buffers.
*/
public class RealDecimateX4096Filter extends RealDecimateX2048Filter
{
private static final int VALIDATION_LENGTH = 4096;
private static final int DECIMATE_BY_4096_FILTER_LENGTH = 11;
private static final WindowType DECIMATE_BY_4096_WINDOW_TYPE = WindowType.BLACKMAN;
private IRealDecimationFilter mFilter;
private float[] mOutput;
/**
* Constructs the decimation filter.
*/
public RealDecimateX4096Filter()
{
mFilter = FilterFactory.getRealDecimationFilter(DECIMATE_BY_4096_FILTER_LENGTH,
DECIMATE_BY_4096_WINDOW_TYPE);
}
@Override
public float[] decimateReal(float[] samples)
{
validate(samples, VALIDATION_LENGTH);
//Decimate by this filter, then by the parent decimation filter
mOutput = getIntermediateOutput(mOutput, samples.length / 2);
return super.decimateReal(mFilter.decimateReal(samples, mOutput));
}
}

View File

@@ -23,7 +23,14 @@ import java.util.ArrayList;
import java.util.List;
/**
* Sample rates offered for bladeRF tuners. Each is a multiple of the 25 kHz polyphase channel spacing.
* Sample rates offered for bladeRF tuners, up to the 122.88 MHz maximum of the bladeRF 2.0. Use getRates() to get the
* rates supported by a specific device, since the bladeRF 1.0, USB 2.0 connections and older FPGA images have lower
* maximums.
*
* Rates above 61.44 MHz use the AD9361 oversample feature, which requires 8-bit samples.
*
* Each rate is a multiple of the 25 kHz polyphase channel spacing, except for the 61.44 and 122.88 MHz rates. The
* channelizer uses slightly wider channels at those rates.
*/
public enum BladeRfSampleRate
{
@@ -36,7 +43,21 @@ public enum BladeRfSampleRate
RATE_15_0(15_000_000, "15.00 MHz"),
RATE_20_0(20_000_000, "20.00 MHz"),
RATE_25_0(25_000_000, "25.00 MHz"),
RATE_30_0(30_000_000, "30.00 MHz");
RATE_30_0(30_000_000, "30.00 MHz"),
RATE_35_0(35_000_000, "35.00 MHz"),
RATE_40_0(40_000_000, "40.00 MHz"),
RATE_45_0(45_000_000, "45.00 MHz"),
RATE_50_0(50_000_000, "50.00 MHz"),
RATE_55_0(55_000_000, "55.00 MHz"),
RATE_60_0(60_000_000, "60.00 MHz"),
RATE_61_44(61_440_000, "61.44 MHz"),
RATE_70_0(70_000_000, "70.00 MHz (8-bit)"),
RATE_80_0(80_000_000, "80.00 MHz (8-bit)"),
RATE_90_0(90_000_000, "90.00 MHz (8-bit)"),
RATE_100_0(100_000_000, "100.00 MHz (8-bit)"),
RATE_110_0(110_000_000, "110.00 MHz (8-bit)"),
RATE_120_0(120_000_000, "120.00 MHz (8-bit)"),
RATE_122_88(122_880_000, "122.88 MHz (8-bit)");
public static final BladeRfSampleRate DEFAULT = RATE_10_0;
private final int mRate;
@@ -56,6 +77,14 @@ public enum BladeRfSampleRate
return mRate;
}
/**
* Indicates if this rate requires the oversample feature and 8-bit samples.
*/
public boolean isOversampled()
{
return mRate > RATE_61_44.mRate;
}
@Override
public String toString()
{
@@ -63,7 +92,7 @@ public enum BladeRfSampleRate
}
/**
* Sample rates within the device maximum, which is the lower of the hardware limit and what the USB link sustains.
* Sample rates within the device maximum, which is the lower of what the hardware, FPGA image and USB link support.
* @param maximumRate in Hertz
* @return rates, lowest to highest
*/

View File

@@ -30,6 +30,8 @@ import io.github.dsheirer.source.tuner.TunerClass;
public class BladeRfTuner extends Tuner
{
private static final int BITS_PER_SAMPLE = 32;
private static final double SAMPLE_SIZE_12_BIT = 12.0;
private static final double SAMPLE_SIZE_8_BIT = 8.0;
/**
* Constructs an instance
@@ -72,12 +74,13 @@ public class BladeRfTuner extends Tuner
@Override
public double getSampleSize()
{
return 12.0;
BladeRfSampleRate rate = getController().getBladeRfSampleRate();
return rate != null && rate.isOversampled() ? SAMPLE_SIZE_8_BIT : SAMPLE_SIZE_12_BIT;
}
@Override
public int getMaximumUSBBitsPerSecond()
{
return BladeRfSampleRate.RATE_30_0.getRate() * BITS_PER_SAMPLE;
return BladeRfSampleRate.RATE_61_44.getRate() * BITS_PER_SAMPLE;
}
}

View File

@@ -28,14 +28,17 @@ import io.github.dsheirer.bladerf.Channel;
import io.github.dsheirer.bladerf.ChannelLayout;
import io.github.dsheirer.bladerf.DeviceInfo;
import io.github.dsheirer.bladerf.DeviceSpeed;
import io.github.dsheirer.bladerf.Feature;
import io.github.dsheirer.bladerf.GainMode;
import io.github.dsheirer.bladerf.GainModeInfo;
import io.github.dsheirer.bladerf.Range;
import io.github.dsheirer.bladerf.SampleBuffer;
import io.github.dsheirer.bladerf.SampleFormat;
import io.github.dsheirer.bladerf.StreamConfig;
import io.github.dsheirer.bladerf.Version;
import io.github.dsheirer.buffer.FloatNativeBuffer;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.SignedByteNativeBuffer;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.ITunerErrorListener;
@@ -62,6 +65,12 @@ import org.usb4java.LibUsb;
*
* Only the first receive channel (RX1 connector) is used. The bladeRF 2.0 has a second receive channel, but it shares
* the local oscillator and sample rate with the first, so it couldn't be tuned as an independent tuner.
*
* Sample rates above 61.44 MHz use the AD9361 oversample feature with 8-bit samples. These are only offered with FPGA
* version 0.15.0 or later, because starting an 8-bit sample stream on an older FPGA crashes in libbladeRF.
*
* The device is reopened when changing between oversampled and normal sample rates. Disabling the oversample feature
* doesn't fully restore the RFIC. The device reports the correct settings, but streams at the wrong rate or times out.
*/
public class BladeRfTunerController extends TunerController
{
@@ -75,10 +84,16 @@ public class BladeRfTunerController extends TunerController
private static final Channel CHANNEL = Channel.RX0;
private static final float SAMPLE_SCALE = 1.0f / 2048.0f;
private static final SampleFormat SAMPLE_FORMAT = SampleFormat.SC16_Q11;
private static final SampleFormat OVERSAMPLE_SAMPLE_FORMAT = SampleFormat.SC8_Q7;
private static final int OVERSAMPLE_MINIMUM_FPGA_MAJOR = 0;
private static final int OVERSAMPLE_MINIMUM_FPGA_MINOR = 15;
private static final int BUFFER_SAMPLE_COUNT = 65536;
private static final StreamConfig STREAM_CONFIG = new StreamConfig(32, BUFFER_SAMPLE_COUNT, 16);
private static final StreamConfig OVERSAMPLE_STREAM_CONFIG = new StreamConfig(64, BUFFER_SAMPLE_COUNT, 32);
private static final long USB2_MAXIMUM_BYTES_PER_SECOND = 32_000_000L;
private static final double MAXIMUM_USABLE_BANDWIDTH_PERCENT = 0.90;
//Measured with a carrier swept across the band at 122.88 MHz. The analog filter is 3 dB down at about +/- 33 MHz.
private static final double OVERSAMPLE_USABLE_BANDWIDTH_HZ = 66_000_000.0;
private static final long STREAM_CHECK_INTERVAL_SECONDS = 1;
private final int mBus;
@@ -89,6 +104,7 @@ public class BladeRfTunerController extends TunerController
private ScheduledFuture<?> mStreamCheck;
private List<BladeRfSampleRate> mSampleRates = List.of();
private BladeRfSampleRate mSampleRate;
private boolean mOversampled;
private List<GainModeInfo> mGainModes = List.of();
private Range mGainRange;
private long mMinimumTunableFrequency = MINIMUM_TUNABLE_FREQUENCY_HZ;
@@ -136,6 +152,8 @@ public class BladeRfTunerController extends TunerController
try
{
mDevice = BladeRf.open(findDevice());
mSampleRate = null;
mOversampled = false;
if(!mDevice.isFpgaConfigured())
{
@@ -270,12 +288,69 @@ public class BladeRfTunerController extends TunerController
if(device.speed() != DeviceSpeed.SUPER)
{
maximum = Math.min(maximum, USB2_MAXIMUM_BYTES_PER_SECOND / SAMPLE_FORMAT.bytesPerSample());
return Math.min(maximum, USB2_MAXIMUM_BYTES_PER_SECOND / SAMPLE_FORMAT.bytesPerSample());
}
if(isOversampleSupported(device.fpgaVersion()))
{
maximum = Math.max(maximum, getOversampleMaximumSampleRate(device));
}
else
{
mLog.info("bladeRF sample rates limited to [" + maximum + "] Hz - FPGA version [" + device.fpgaVersion() +
"] doesn't support 8-bit samples. Upgrade to FPGA version 0.15.0 or later for higher sample rates.");
}
return maximum;
}
/**
* Indicates if the FPGA version supports the 8-bit samples that are required for the oversample feature.
* @param fpga version
* @return true if supported
*/
static boolean isOversampleSupported(Version fpga)
{
return fpga.major() > OVERSAMPLE_MINIMUM_FPGA_MAJOR ||
(fpga.major() == OVERSAMPLE_MINIMUM_FPGA_MAJOR && fpga.minor() >= OVERSAMPLE_MINIMUM_FPGA_MINOR);
}
/**
* Usable portion of an oversampled rate, which is limited by the analog filter that stays in the signal path.
* @param rate that is oversampled
* @return usable bandwidth percentage
*/
static double getOversampleUsableBandwidthPercentage(BladeRfSampleRate rate)
{
return Math.min(MAXIMUM_USABLE_BANDWIDTH_PERCENT, OVERSAMPLE_USABLE_BANDWIDTH_HZ / rate.getRate());
}
/**
* Highest sample rate that the device reports while the oversample feature is enabled.
* @return maximum rate, or zero if the device doesn't have the oversample feature, such as the bladeRF 1.0.
*/
private static long getOversampleMaximumSampleRate(BladeRfDevice device) throws BladeRfException
{
try
{
device.enableFeature(Feature.OVERSAMPLE, true);
}
catch(BladeRfException e)
{
mLog.info("bladeRF oversample feature isn't available - " + e.getMessage());
return 0;
}
try
{
return device.sampleRateRange(CHANNEL).max();
}
finally
{
device.enableFeature(Feature.DEFAULT, true);
}
}
/**
* Finds the libbladeRF description of the device at this controller's USB bus and port.
*/
@@ -450,11 +525,33 @@ public class BladeRfTunerController extends TunerController
try
{
if(rate.isOversampled() != mOversampled)
{
reopenDevice();
if(rate.isOversampled())
{
//Bandwidth has to be set first since it can't be reliably set with oversample enabled.
mDevice.setBandwidth(CHANNEL, mDevice.bandwidthRange(CHANNEL).max());
mDevice.enableFeature(Feature.OVERSAMPLE, true);
mOversampled = true;
}
}
mDevice.setSampleRate(CHANNEL, rate.getRate());
long requested = (long)(rate.getRate() * MAXIMUM_USABLE_BANDWIDTH_PERCENT);
long bandwidth = mDevice.setBandwidth(CHANNEL, requested);
setUsableBandwidthPercentage(Math.min(MAXIMUM_USABLE_BANDWIDTH_PERCENT,
(double)bandwidth / rate.getRate()));
if(rate.isOversampled())
{
setUsableBandwidthPercentage(getOversampleUsableBandwidthPercentage(rate));
}
else
{
long requested = (long)(rate.getRate() * MAXIMUM_USABLE_BANDWIDTH_PERCENT);
long bandwidth = mDevice.setBandwidth(CHANNEL, requested);
setUsableBandwidthPercentage(Math.min(MAXIMUM_USABLE_BANDWIDTH_PERCENT,
(double)bandwidth / rate.getRate()));
}
mSampleRate = rate;
mSamplesPerMillisecond = rate.getRate() / 1000.0f;
mFrequencyController.setSampleRate(rate.getRate());
@@ -465,7 +562,7 @@ public class BladeRfTunerController extends TunerController
}
finally
{
if(streaming)
if(streaming && mDevice != null)
{
startStream();
}
@@ -478,6 +575,33 @@ public class BladeRfTunerController extends TunerController
}
}
/**
* Closes and reopens the device and restores the settings that were reset, except for the sample rate and bandwidth
* which the caller sets. The sample stream must be stopped.
*/
private void reopenDevice() throws SourceException, BladeRfException
{
long frequency = mFrequencyController.getTunedFrequency();
BladeRfDevice device = mDevice;
mDevice = null;
mOversampled = false;
closeDevice(device, false);
mDevice = BladeRf.open(findDevice());
mDevice.setFrequency(CHANNEL, frequency);
mDevice.setGainMode(CHANNEL, mGainMode);
if(mGainMode == GainMode.MANUAL)
{
mDevice.setGain(CHANNEL, mGain);
}
if(mBiasT)
{
mDevice.setBiasTee(CHANNEL, true);
}
}
/**
* Gain modes supported by the device.
*/
@@ -679,7 +803,9 @@ public class BladeRfTunerController extends TunerController
{
try
{
mStream = mDevice.startRx(ChannelLayout.RX_X1, SAMPLE_FORMAT, STREAM_CONFIG, this::receive);
mStream = mOversampled ?
mDevice.startRx(ChannelLayout.RX_X1, OVERSAMPLE_SAMPLE_FORMAT, OVERSAMPLE_STREAM_CONFIG, this::receive) :
mDevice.startRx(ChannelLayout.RX_X1, SAMPLE_FORMAT, STREAM_CONFIG, this::receive);
mStreamCheck = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::checkStream, STREAM_CHECK_INTERVAL_SECONDS,
STREAM_CHECK_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
@@ -746,7 +872,16 @@ public class BladeRfTunerController extends TunerController
{
try
{
broadcast(new FloatNativeBuffer(scale(buffer.copyShorts()), System.currentTimeMillis(), mSamplesPerMillisecond));
long timestamp = System.currentTimeMillis();
if(buffer.format() == OVERSAMPLE_SAMPLE_FORMAT)
{
broadcast(new SignedByteNativeBuffer(buffer.copyBytes(), timestamp, 0.0f, 0.0f, mSamplesPerMillisecond));
}
else
{
broadcast(new FloatNativeBuffer(scale(buffer.copyShorts()), timestamp, mSamplesPerMillisecond));
}
}
catch(RuntimeException e)
{

View File

@@ -90,7 +90,7 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
private void init()
{
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]", "[][][][][][][][][][grow]"));
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]", "[][][][][][][][][][][][grow]"));
add(new JLabel("Tuner:"));
add(getTunerIdLabel());
@@ -101,7 +101,7 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
add(getButtonPanel(), "span,align left");
add(new JSeparator(), "span,growx,push");
add(new JSeparator(), "span,growx");
add(new JLabel("Frequency (MHz):"));
add(getFrequencyPanel(), "wrap");
@@ -112,7 +112,7 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
add(new JLabel());
add(getBiasTCheckBox(), "wrap");
add(new JSeparator(), "span,growx,push");
add(new JSeparator(), "span,growx");
add(new JLabel("Gain Control"), "wrap");
add(new JLabel("Mode:"));
@@ -121,6 +121,9 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
//Empty row that takes all extra vertical space so that the controls stay stacked at the top of the editor.
add(new JLabel(), "span,pushy");
}
@Override

View File

@@ -46,6 +46,16 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
//Maximum number of filled buffers for the blocking queue
private static final int BUFFER_MAX_CAPACITY = 600;
//Decimation rate where a 2048 sample fragment produces too few samples for the consumer, so the decimated samples
//are assembled into longer buffers. Decoders apply additional decimation that requires a buffer length that's a
//multiple of their decimation.
private static final int ASSEMBLED_OUTPUT_DECIMATION = 1024;
private static final int ASSEMBLED_OUTPUT_LENGTH = 64;
//Decimation rate where sample fragments can be too short for the filter, which requires a buffer length that's a
//multiple of the decimation rate, so the mixed samples are assembled before decimation.
private static final int ASSEMBLED_INPUT_DECIMATION = 2048;
private final Dispatcher<T> mBufferDispatcher;
private final ComplexMixer mFrequencyCorrectionMixer;
private final IRealDecimationFilter mIDecimationFilter;
@@ -59,6 +69,15 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
private final long mFrequencyOffset;
private volatile long mFrequencyCorrection;
private final AtomicBoolean mPendingMixerUpdate = new AtomicBoolean();
private final boolean mAssembleOutput;
private final float[] mInputI;
private final float[] mInputQ;
private int mInputPointer;
private long mInputTimestamp;
private float[] mOutputI;
private float[] mOutputQ;
private int mOutputPointer;
private long mOutputTimestamp;
/**
* Constructs a frequency translating and CIC decimating channel source.
@@ -87,6 +106,9 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
//The decimation filters copy the mixed samples, unless there's no decimation and the mixed samples are the output
mReuseMixerOutput = decimation > 1;
mAssembleOutput = decimation >= ASSEMBLED_OUTPUT_DECIMATION;
mInputI = decimation >= ASSEMBLED_INPUT_DECIMATION ? new float[decimation] : null;
mInputQ = decimation >= ASSEMBLED_INPUT_DECIMATION ? new float[decimation] : null;
//Set dispatcher to process 1/10 of estimated sample arrival rate, 20 times per second (up to 200% per interval)
mBufferDispatcher = new Dispatcher(threadName, 50, getHeartbeatManager(), BUFFER_MAX_CAPACITY);
@@ -242,23 +264,113 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
}
ComplexSamples basebanded = mix(iterator.next());
float[] i = mIDecimationFilter.decimateReal(basebanded.i());
float[] q = mQDecimationFilter.decimateReal(basebanded.q());
try
if(mInputI != null)
{
mSamplesListener.receive(new ComplexSamples(i, q, basebanded.timestamp()));
assembleInput(basebanded);
}
catch(Throwable t)
else
{
//The listener can be made null and cause the error - only log if we have a non-null listener
if(mSamplesListener != null)
{
mLog.error("Error dispatching complex samples to listener [" + mSamplesListener + "]");
}
decimate(basebanded.i(), basebanded.q(), basebanded.timestamp());
}
}
}
}
/**
* Assembles the mixed samples into buffers of the decimation rate length and decimates each full buffer.
* @param samples that were mixed, which are copied since the mixer output arrays can be reused.
*/
private void assembleInput(ComplexSamples samples)
{
int pointer = 0;
while(pointer < samples.i().length)
{
if(mInputPointer == 0)
{
mInputTimestamp = samples.timestamp();
}
int length = Math.min(samples.i().length - pointer, mInputI.length - mInputPointer);
System.arraycopy(samples.i(), pointer, mInputI, mInputPointer, length);
System.arraycopy(samples.q(), pointer, mInputQ, mInputPointer, length);
pointer += length;
mInputPointer += length;
if(mInputPointer == mInputI.length)
{
mInputPointer = 0;
decimate(mInputI, mInputQ, mInputTimestamp);
}
}
}
/**
* Decimates the samples and sends them to the listener.
*/
private void decimate(float[] i, float[] q, long timestamp)
{
output(mIDecimationFilter.decimateReal(i), mQDecimationFilter.decimateReal(q), timestamp);
}
/**
* Sends the decimated samples to the listener, assembling them into larger buffers at the maximum decimation.
* @param i decimated samples
* @param q decimated samples
* @param timestamp of the first sample
*/
private void output(float[] i, float[] q, long timestamp)
{
if(!mAssembleOutput)
{
dispatch(new ComplexSamples(i, q, timestamp));
return;
}
int pointer = 0;
while(pointer < i.length)
{
if(mOutputPointer == 0)
{
//New arrays each time since the assembled arrays are passed to the consumer
mOutputI = new float[ASSEMBLED_OUTPUT_LENGTH];
mOutputQ = new float[ASSEMBLED_OUTPUT_LENGTH];
mOutputTimestamp = timestamp;
}
int length = Math.min(i.length - pointer, ASSEMBLED_OUTPUT_LENGTH - mOutputPointer);
System.arraycopy(i, pointer, mOutputI, mOutputPointer, length);
System.arraycopy(q, pointer, mOutputQ, mOutputPointer, length);
pointer += length;
mOutputPointer += length;
if(mOutputPointer == ASSEMBLED_OUTPUT_LENGTH)
{
mOutputPointer = 0;
dispatch(new ComplexSamples(mOutputI, mOutputQ, mOutputTimestamp));
}
}
}
/**
* Sends the samples to the listener.
*/
private void dispatch(ComplexSamples samples)
{
try
{
mSamplesListener.receive(samples);
}
catch(Throwable t)
{
//The listener can be made null and cause the error - only log if we have a non-null listener
if(mSamplesListener != null)
{
mLog.error("Error dispatching complex samples to listener [" + mSamplesListener + "]");
}
}
}
}
}

View File

@@ -83,7 +83,25 @@ public class DecimationFilterFactoryTest
assertEquals(2, DecimationFilterFactory.getDecimationRate(4));
assertEquals(4, DecimationFilterFactory.getDecimationRate(5));
assertEquals(64, DecimationFilterFactory.getDecimationRate(96));
assertEquals(1024, DecimationFilterFactory.getDecimationRate(4096));
assertEquals(1024, DecimationFilterFactory.getDecimationRate(2048));
assertEquals(2048, DecimationFilterFactory.getDecimationRate(4096));
assertEquals(4096, DecimationFilterFactory.getDecimationRate(4097));
assertEquals(4096, DecimationFilterFactory.getDecimationRate(100000));
}
/**
* Test: each decimation rate reduces the sample count by that rate and rejects a length that isn't a multiple.
*/
@Test
void highestRatesDecimateAndValidateLength()
{
for(int decimation : new int[]{1024, 2048, 4096})
{
assertEquals(1, DecimationFilterFactory.getRealDecimationFilter(decimation)
.decimateReal(new float[decimation]).length);
assertThrows(IllegalArgumentException.class, () -> DecimationFilterFactory
.getRealDecimationFilter(decimation).decimateReal(new float[decimation / 2]));
}
}
/**
@@ -138,7 +156,7 @@ public class DecimationFilterFactoryTest
@Test
void aliasRejection()
{
for(int decimation = 2; decimation <= 512; decimation *= 2)
for(int decimation = 2; decimation <= DecimationFilterFactory.MAXIMUM_DECIMATION_RATE; decimation *= 2)
{
double sampleRate = DECIMATED_SAMPLE_RATE * decimation;

View File

@@ -20,11 +20,16 @@ package io.github.dsheirer.source.tuner.bladerf;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.bladerf.GainMode;
import io.github.dsheirer.bladerf.Version;
import io.github.dsheirer.buffer.SignedByteNativeBuffer;
import io.github.dsheirer.dsp.filter.channelizer.ComplexPolyphaseChannelizerM2;
import io.github.dsheirer.source.tuner.TunerClass;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
@@ -64,15 +69,85 @@ class BladeRfTunerTest
assertEquals(BladeRfSampleRate.RATE_1_25, BladeRfSampleRate.getClosest(rates, 1));
}
@Test
void sampleRatesFollowTheDeviceMaximum()
{
//bladeRF 2.0 on USB 3.0 with FPGA version 0.15.0 or later
assertEquals(List.of(BladeRfSampleRate.values()), BladeRfSampleRate.getRates(122_880_000));
//bladeRF 2.0 on USB 3.0 with an older FPGA version
assertEquals(BladeRfSampleRate.RATE_61_44, BladeRfSampleRate.getRates(61_440_000).getLast());
//bladeRF 1.0 on USB 3.0
assertEquals(BladeRfSampleRate.RATE_40_0, BladeRfSampleRate.getRates(40_000_000).getLast());
//USB 2.0
assertEquals(BladeRfSampleRate.RATE_7_5, BladeRfSampleRate.getRates(8_000_000).getLast());
}
@Test
void savedRateAboveTheDeviceMaximumIsReduced()
{
List<BladeRfSampleRate> rates = BladeRfSampleRate.getRates(40_000_000);
assertEquals(BladeRfSampleRate.RATE_40_0, BladeRfSampleRate.getClosest(rates,
BladeRfSampleRate.RATE_122_88.getRate()));
}
@Test
void onlyRatesAbove61MhzAreOversampled()
{
assertFalse(BladeRfSampleRate.RATE_61_44.isOversampled());
assertTrue(BladeRfSampleRate.RATE_70_0.isOversampled());
assertTrue(BladeRfSampleRate.RATE_122_88.isOversampled());
}
@Test
void oversampledUsableBandwidthIsLimitedByTheAnalogFilter()
{
assertEquals(0.90, BladeRfTunerController.getOversampleUsableBandwidthPercentage(BladeRfSampleRate.RATE_70_0));
assertEquals(66_000_000, BladeRfTunerController.getOversampleUsableBandwidthPercentage(
BladeRfSampleRate.RATE_122_88) * BladeRfSampleRate.RATE_122_88.getRate(), 1.0);
}
@Test
void oversampleRequiresFpga15()
{
assertFalse(BladeRfTunerController.isOversampleSupported(new Version(0, 14, 0, "0.14.0")));
assertTrue(BladeRfTunerController.isOversampleSupported(new Version(0, 15, 0, "0.15.0")));
assertTrue(BladeRfTunerController.isOversampleSupported(new Version(0, 16, 3, "0.16.3")));
assertTrue(BladeRfTunerController.isOversampleSupported(new Version(1, 0, 0, "1.0.0")));
}
@Test
void streamBufferFitsTheEightBitNativeBuffer()
{
int sampleCount = 65536;
assertEquals(sampleCount, new SignedByteNativeBuffer(new byte[sampleCount * 2], 0, 0, 0, 1).sampleCount());
}
@Test
void sampleRatesDivideIntoPolyphaseChannels()
{
for(BladeRfSampleRate rate : BladeRfSampleRate.values())
{
assertEquals(0, rate.getRate() % 50_000, rate.toString());
if(rate != BladeRfSampleRate.RATE_61_44 && rate != BladeRfSampleRate.RATE_122_88)
{
assertEquals(0, rate.getRate() % 50_000, rate.toString());
}
int channelCount = ComplexPolyphaseChannelizerM2.getChannelCount(rate.getRate());
assertEquals(0, channelCount % 2, rate.toString());
assertTrue((double)rate.getRate() / channelCount >= 25_000.0, rate.toString());
}
}
@Test
void maximumUsbRateDoesNotOverflow()
{
assertEquals(1_966_080_000L, BladeRfSampleRate.RATE_61_44.getRate() * 32L);
assertTrue(BladeRfSampleRate.RATE_61_44.getRate() * 32L <= Integer.MAX_VALUE);
assertEquals(1_966_080_000L, BladeRfSampleRate.RATE_122_88.getRate() * 16L);
}
@Test
void configurationSurvivesSerialization() throws Exception
{

View File

@@ -20,9 +20,18 @@ package io.github.dsheirer.source.tuner.channel;
import io.github.dsheirer.buffer.FloatNativeBuffer;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.SignedByteNativeBuffer;
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Verifies that the heterodyne channel source applies frequency updates on the sample processing thread.
@@ -76,6 +85,137 @@ public class HalfBandTunerChannelSourceTest
assertEquals(50000.0, source.getSampleRate());
}
private static HalfBandTunerChannelSource<INativeBuffer> source(double sampleRate) throws Exception
{
return new HalfBandTunerChannelSource<>(sourceEvent -> {}, new TunerChannel(CHANNEL_FREQUENCY, 12500), sampleRate,
new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0), "test", null);
}
/**
* Test: a decoder can apply more decimation at the highest source decimation rates, where a 2048 sample fragment
* produces 2 samples or less, since these are assembled into larger buffers for the decoder.
*/
@Test
void highDecimationProvidesBuffersThatDecodersCanDecimate() throws Exception
{
//Tuner sample rates that select a decimation of 1024, 2048 and 4096
for(int[] rates : new int[][]{{30_000_000, 1024}, {60_000_000, 2048}, {120_000_000, 4096}})
{
for(int decoderDecimation : new int[]{2, 4, 8, 16, 32, 64})
{
HalfBandTunerChannelSource<INativeBuffer> source = source(rates[0]);
IRealDecimationFilter filter = DecimationFilterFactory.getRealDecimationFilter(decoderDecimation);
List<Throwable> errors = new ArrayList<>();
AtomicInteger sampleCount = new AtomicInteger();
source.setListener(samples ->
{
try
{
sampleCount.addAndGet(samples.i().length);
filter.decimateReal(samples.i());
}
catch(Throwable t)
{
errors.add(t);
}
});
//Each native buffer has 65536 samples in 2048 sample fragments
int nativeBuffers = 64 * rates[1] / 65536;
for(int x = 0; x < nativeBuffers; x++)
{
source.new NativeBufferProcessor().receive(new SignedByteNativeBuffer(new byte[131072], 0L, 0, 0, 1));
}
assertEquals(List.of(), errors);
assertEquals((double)rates[0] / rates[1], source.getSampleRate());
assertEquals(64 * (nativeBuffers * 65536 / rates[1] / 64), sampleCount.get());
}
}
}
/**
* Test: assembled buffers preserve the timestamp of the first sample in each buffer.
*/
@Test
void assembledBuffersPreserveTimestamps() throws Exception
{
HalfBandTunerChannelSource<INativeBuffer> source = source(120_000_000.0);
List<ComplexSamples> buffers = new ArrayList<>();
source.setListener(buffers::add);
//Each native buffer has 32768 samples that produce 8 decimated samples, so 12 native buffers provide 96 samples
for(long timestamp = 1000; timestamp <= 12000; timestamp += 1000)
{
source.new NativeBufferProcessor().receive(new FloatNativeBuffer(new float[65536], timestamp, 0.0f));
}
assertEquals(1, buffers.size());
assertEquals(64, buffers.getFirst().i().length);
assertEquals(64, buffers.getFirst().q().length);
assertEquals(1000L, buffers.getFirst().timestamp());
}
/**
* Test: assembling sample fragments doesn't change the samples, so the output matches decimating complete blocks,
* including when sample buffers end with a short fragment.
*/
@Test
void assembledInputMatchesContinuousDecimation() throws Exception
{
int decimation = 4096;
//Native buffers provide a 2048 sample fragment and a 1024 sample fragment, which straddle the filter blocks
int fragment = 3072;
int fragments = 800;
float[] interleaved = new float[fragment * fragments * 2];
for(int x = 0; x < fragment * fragments; x++)
{
double phase = 2.0 * Math.PI * 3000.0 * x / 120_000_000.0;
interleaved[x * 2] = (float)Math.cos(phase);
interleaved[x * 2 + 1] = (float)Math.sin(phase);
}
HalfBandTunerChannelSource<INativeBuffer> source = source(120_000_000.0);
List<ComplexSamples> buffers = new ArrayList<>();
source.setListener(buffers::add);
for(int x = 0; x < fragments; x++)
{
float[] samples = Arrays.copyOfRange(interleaved, x * fragment * 2, (x + 1) * fragment * 2);
source.new NativeBufferProcessor().receive(new FloatNativeBuffer(samples, 0L, 0.0f));
}
IRealDecimationFilter expectedI = DecimationFilterFactory.getRealDecimationFilter(decimation);
float[] blockI = new float[decimation];
List<Float> expected = new ArrayList<>();
for(int x = 0; x + decimation <= fragment * fragments; x += decimation)
{
for(int y = 0; y < decimation; y++)
{
blockI[y] = interleaved[(x + y) * 2];
}
expected.add(expectedI.decimateReal(blockI)[0]);
}
int pointer = 0;
for(ComplexSamples buffer : buffers)
{
for(float sample : buffer.i())
{
assertEquals(expected.get(pointer++), sample, 1e-4f);
}
}
assertEquals(expected.size() / 64 * 64, pointer);
assertTrue(pointer > 0);
}
/**
* Test: concurrent tuner frequency and channel correction updates are both reflected in the mixer.
*/