mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 01:57:39 -05:00
##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:
@@ -29,12 +29,14 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class DecimationFilterFactory
|
public class DecimationFilterFactory
|
||||||
{
|
{
|
||||||
private static final Logger mLog = LoggerFactory.getLogger(DecimationFilterFactory.class);
|
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
|
* 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
|
* 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
|
* @param decimationRate requested @see SUPPORTED_RATES
|
||||||
* @return constructed decimation filter
|
* @return constructed decimation filter
|
||||||
@@ -66,6 +68,10 @@ public class DecimationFilterFactory
|
|||||||
return new RealDecimateX512Filter();
|
return new RealDecimateX512Filter();
|
||||||
case 1024:
|
case 1024:
|
||||||
return new RealDecimateX1024Filter();
|
return new RealDecimateX1024Filter();
|
||||||
|
case 2048:
|
||||||
|
return new RealDecimateX2048Filter();
|
||||||
|
case 4096:
|
||||||
|
return new RealDecimateX4096Filter();
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unsupported decimation rate: " + decimationRate +
|
throw new IllegalArgumentException("Unsupported decimation rate: " + decimationRate +
|
||||||
". Supported decimation rates are:" + Arrays.toString(SUPPORTED_RATES));
|
". Supported decimation rates are:" + Arrays.toString(SUPPORTED_RATES));
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,14 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
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
|
public enum BladeRfSampleRate
|
||||||
{
|
{
|
||||||
@@ -36,7 +43,21 @@ public enum BladeRfSampleRate
|
|||||||
RATE_15_0(15_000_000, "15.00 MHz"),
|
RATE_15_0(15_000_000, "15.00 MHz"),
|
||||||
RATE_20_0(20_000_000, "20.00 MHz"),
|
RATE_20_0(20_000_000, "20.00 MHz"),
|
||||||
RATE_25_0(25_000_000, "25.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;
|
public static final BladeRfSampleRate DEFAULT = RATE_10_0;
|
||||||
private final int mRate;
|
private final int mRate;
|
||||||
@@ -56,6 +77,14 @@ public enum BladeRfSampleRate
|
|||||||
return mRate;
|
return mRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this rate requires the oversample feature and 8-bit samples.
|
||||||
|
*/
|
||||||
|
public boolean isOversampled()
|
||||||
|
{
|
||||||
|
return mRate > RATE_61_44.mRate;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
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
|
* @param maximumRate in Hertz
|
||||||
* @return rates, lowest to highest
|
* @return rates, lowest to highest
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import io.github.dsheirer.source.tuner.TunerClass;
|
|||||||
public class BladeRfTuner extends Tuner
|
public class BladeRfTuner extends Tuner
|
||||||
{
|
{
|
||||||
private static final int BITS_PER_SAMPLE = 32;
|
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
|
* Constructs an instance
|
||||||
@@ -72,12 +74,13 @@ public class BladeRfTuner extends Tuner
|
|||||||
@Override
|
@Override
|
||||||
public double getSampleSize()
|
public double getSampleSize()
|
||||||
{
|
{
|
||||||
return 12.0;
|
BladeRfSampleRate rate = getController().getBladeRfSampleRate();
|
||||||
|
return rate != null && rate.isOversampled() ? SAMPLE_SIZE_8_BIT : SAMPLE_SIZE_12_BIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMaximumUSBBitsPerSecond()
|
public int getMaximumUSBBitsPerSecond()
|
||||||
{
|
{
|
||||||
return BladeRfSampleRate.RATE_30_0.getRate() * BITS_PER_SAMPLE;
|
return BladeRfSampleRate.RATE_61_44.getRate() * BITS_PER_SAMPLE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,17 @@ import io.github.dsheirer.bladerf.Channel;
|
|||||||
import io.github.dsheirer.bladerf.ChannelLayout;
|
import io.github.dsheirer.bladerf.ChannelLayout;
|
||||||
import io.github.dsheirer.bladerf.DeviceInfo;
|
import io.github.dsheirer.bladerf.DeviceInfo;
|
||||||
import io.github.dsheirer.bladerf.DeviceSpeed;
|
import io.github.dsheirer.bladerf.DeviceSpeed;
|
||||||
|
import io.github.dsheirer.bladerf.Feature;
|
||||||
import io.github.dsheirer.bladerf.GainMode;
|
import io.github.dsheirer.bladerf.GainMode;
|
||||||
import io.github.dsheirer.bladerf.GainModeInfo;
|
import io.github.dsheirer.bladerf.GainModeInfo;
|
||||||
import io.github.dsheirer.bladerf.Range;
|
import io.github.dsheirer.bladerf.Range;
|
||||||
import io.github.dsheirer.bladerf.SampleBuffer;
|
import io.github.dsheirer.bladerf.SampleBuffer;
|
||||||
import io.github.dsheirer.bladerf.SampleFormat;
|
import io.github.dsheirer.bladerf.SampleFormat;
|
||||||
import io.github.dsheirer.bladerf.StreamConfig;
|
import io.github.dsheirer.bladerf.StreamConfig;
|
||||||
|
import io.github.dsheirer.bladerf.Version;
|
||||||
import io.github.dsheirer.buffer.FloatNativeBuffer;
|
import io.github.dsheirer.buffer.FloatNativeBuffer;
|
||||||
import io.github.dsheirer.buffer.INativeBuffer;
|
import io.github.dsheirer.buffer.INativeBuffer;
|
||||||
|
import io.github.dsheirer.buffer.SignedByteNativeBuffer;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.source.SourceException;
|
import io.github.dsheirer.source.SourceException;
|
||||||
import io.github.dsheirer.source.tuner.ITunerErrorListener;
|
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
|
* 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.
|
* 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
|
public class BladeRfTunerController extends TunerController
|
||||||
{
|
{
|
||||||
@@ -75,10 +84,16 @@ public class BladeRfTunerController extends TunerController
|
|||||||
private static final Channel CHANNEL = Channel.RX0;
|
private static final Channel CHANNEL = Channel.RX0;
|
||||||
private static final float SAMPLE_SCALE = 1.0f / 2048.0f;
|
private static final float SAMPLE_SCALE = 1.0f / 2048.0f;
|
||||||
private static final SampleFormat SAMPLE_FORMAT = SampleFormat.SC16_Q11;
|
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 int BUFFER_SAMPLE_COUNT = 65536;
|
||||||
private static final StreamConfig STREAM_CONFIG = new StreamConfig(32, BUFFER_SAMPLE_COUNT, 16);
|
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 long USB2_MAXIMUM_BYTES_PER_SECOND = 32_000_000L;
|
||||||
private static final double MAXIMUM_USABLE_BANDWIDTH_PERCENT = 0.90;
|
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 static final long STREAM_CHECK_INTERVAL_SECONDS = 1;
|
||||||
|
|
||||||
private final int mBus;
|
private final int mBus;
|
||||||
@@ -89,6 +104,7 @@ public class BladeRfTunerController extends TunerController
|
|||||||
private ScheduledFuture<?> mStreamCheck;
|
private ScheduledFuture<?> mStreamCheck;
|
||||||
private List<BladeRfSampleRate> mSampleRates = List.of();
|
private List<BladeRfSampleRate> mSampleRates = List.of();
|
||||||
private BladeRfSampleRate mSampleRate;
|
private BladeRfSampleRate mSampleRate;
|
||||||
|
private boolean mOversampled;
|
||||||
private List<GainModeInfo> mGainModes = List.of();
|
private List<GainModeInfo> mGainModes = List.of();
|
||||||
private Range mGainRange;
|
private Range mGainRange;
|
||||||
private long mMinimumTunableFrequency = MINIMUM_TUNABLE_FREQUENCY_HZ;
|
private long mMinimumTunableFrequency = MINIMUM_TUNABLE_FREQUENCY_HZ;
|
||||||
@@ -136,6 +152,8 @@ public class BladeRfTunerController extends TunerController
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
mDevice = BladeRf.open(findDevice());
|
mDevice = BladeRf.open(findDevice());
|
||||||
|
mSampleRate = null;
|
||||||
|
mOversampled = false;
|
||||||
|
|
||||||
if(!mDevice.isFpgaConfigured())
|
if(!mDevice.isFpgaConfigured())
|
||||||
{
|
{
|
||||||
@@ -270,12 +288,69 @@ public class BladeRfTunerController extends TunerController
|
|||||||
|
|
||||||
if(device.speed() != DeviceSpeed.SUPER)
|
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;
|
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.
|
* 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
|
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());
|
mDevice.setSampleRate(CHANNEL, rate.getRate());
|
||||||
long requested = (long)(rate.getRate() * MAXIMUM_USABLE_BANDWIDTH_PERCENT);
|
|
||||||
long bandwidth = mDevice.setBandwidth(CHANNEL, requested);
|
if(rate.isOversampled())
|
||||||
setUsableBandwidthPercentage(Math.min(MAXIMUM_USABLE_BANDWIDTH_PERCENT,
|
{
|
||||||
(double)bandwidth / rate.getRate()));
|
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;
|
mSampleRate = rate;
|
||||||
mSamplesPerMillisecond = rate.getRate() / 1000.0f;
|
mSamplesPerMillisecond = rate.getRate() / 1000.0f;
|
||||||
mFrequencyController.setSampleRate(rate.getRate());
|
mFrequencyController.setSampleRate(rate.getRate());
|
||||||
@@ -465,7 +562,7 @@ public class BladeRfTunerController extends TunerController
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if(streaming)
|
if(streaming && mDevice != null)
|
||||||
{
|
{
|
||||||
startStream();
|
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.
|
* Gain modes supported by the device.
|
||||||
*/
|
*/
|
||||||
@@ -679,7 +803,9 @@ public class BladeRfTunerController extends TunerController
|
|||||||
{
|
{
|
||||||
try
|
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,
|
mStreamCheck = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::checkStream, STREAM_CHECK_INTERVAL_SECONDS,
|
||||||
STREAM_CHECK_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
STREAM_CHECK_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
@@ -746,7 +872,16 @@ public class BladeRfTunerController extends TunerController
|
|||||||
{
|
{
|
||||||
try
|
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)
|
catch(RuntimeException e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
|
|||||||
|
|
||||||
private void init()
|
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(new JLabel("Tuner:"));
|
||||||
add(getTunerIdLabel());
|
add(getTunerIdLabel());
|
||||||
@@ -101,7 +101,7 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
|
|||||||
|
|
||||||
add(getButtonPanel(), "span,align left");
|
add(getButtonPanel(), "span,align left");
|
||||||
|
|
||||||
add(new JSeparator(), "span,growx,push");
|
add(new JSeparator(), "span,growx");
|
||||||
|
|
||||||
add(new JLabel("Frequency (MHz):"));
|
add(new JLabel("Frequency (MHz):"));
|
||||||
add(getFrequencyPanel(), "wrap");
|
add(getFrequencyPanel(), "wrap");
|
||||||
@@ -112,7 +112,7 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
|
|||||||
add(new JLabel());
|
add(new JLabel());
|
||||||
add(getBiasTCheckBox(), "wrap");
|
add(getBiasTCheckBox(), "wrap");
|
||||||
|
|
||||||
add(new JSeparator(), "span,growx,push");
|
add(new JSeparator(), "span,growx");
|
||||||
add(new JLabel("Gain Control"), "wrap");
|
add(new JLabel("Gain Control"), "wrap");
|
||||||
|
|
||||||
add(new JLabel("Mode:"));
|
add(new JLabel("Mode:"));
|
||||||
@@ -121,6 +121,9 @@ public class BladeRfTunerEditor extends TunerEditor<BladeRfTuner, BladeRfTunerCo
|
|||||||
add(new JLabel("Gain:"));
|
add(new JLabel("Gain:"));
|
||||||
add(getGainSlider());
|
add(getGainSlider());
|
||||||
add(getGainValueLabel());
|
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
|
@Override
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
|
|||||||
//Maximum number of filled buffers for the blocking queue
|
//Maximum number of filled buffers for the blocking queue
|
||||||
private static final int BUFFER_MAX_CAPACITY = 600;
|
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 Dispatcher<T> mBufferDispatcher;
|
||||||
private final ComplexMixer mFrequencyCorrectionMixer;
|
private final ComplexMixer mFrequencyCorrectionMixer;
|
||||||
private final IRealDecimationFilter mIDecimationFilter;
|
private final IRealDecimationFilter mIDecimationFilter;
|
||||||
@@ -59,6 +69,15 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
|
|||||||
private final long mFrequencyOffset;
|
private final long mFrequencyOffset;
|
||||||
private volatile long mFrequencyCorrection;
|
private volatile long mFrequencyCorrection;
|
||||||
private final AtomicBoolean mPendingMixerUpdate = new AtomicBoolean();
|
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.
|
* 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
|
//The decimation filters copy the mixed samples, unless there's no decimation and the mixed samples are the output
|
||||||
mReuseMixerOutput = decimation > 1;
|
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)
|
//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);
|
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());
|
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
|
decimate(basebanded.i(), basebanded.q(), basebanded.timestamp());
|
||||||
if(mSamplesListener != null)
|
|
||||||
{
|
|
||||||
mLog.error("Error dispatching complex samples to listener [" + mSamplesListener + "]");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,25 @@ public class DecimationFilterFactoryTest
|
|||||||
assertEquals(2, DecimationFilterFactory.getDecimationRate(4));
|
assertEquals(2, DecimationFilterFactory.getDecimationRate(4));
|
||||||
assertEquals(4, DecimationFilterFactory.getDecimationRate(5));
|
assertEquals(4, DecimationFilterFactory.getDecimationRate(5));
|
||||||
assertEquals(64, DecimationFilterFactory.getDecimationRate(96));
|
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
|
@Test
|
||||||
void aliasRejection()
|
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;
|
double sampleRate = DECIMATED_SAMPLE_RATE * decimation;
|
||||||
|
|
||||||
|
|||||||
@@ -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.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.assertInstanceOf;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.github.dsheirer.bladerf.GainMode;
|
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.TunerClass;
|
||||||
import io.github.dsheirer.source.tuner.TunerType;
|
import io.github.dsheirer.source.tuner.TunerType;
|
||||||
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
|
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
|
||||||
@@ -64,15 +69,85 @@ class BladeRfTunerTest
|
|||||||
assertEquals(BladeRfSampleRate.RATE_1_25, BladeRfSampleRate.getClosest(rates, 1));
|
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
|
@Test
|
||||||
void sampleRatesDivideIntoPolyphaseChannels()
|
void sampleRatesDivideIntoPolyphaseChannels()
|
||||||
{
|
{
|
||||||
for(BladeRfSampleRate rate : BladeRfSampleRate.values())
|
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
|
@Test
|
||||||
void configurationSurvivesSerialization() throws Exception
|
void configurationSurvivesSerialization() throws Exception
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,9 +20,18 @@ package io.github.dsheirer.source.tuner.channel;
|
|||||||
|
|
||||||
import io.github.dsheirer.buffer.FloatNativeBuffer;
|
import io.github.dsheirer.buffer.FloatNativeBuffer;
|
||||||
import io.github.dsheirer.buffer.INativeBuffer;
|
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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.
|
* 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());
|
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.
|
* Test: concurrent tuner frequency and channel correction updates are both reflected in the mixer.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user