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

@@ -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.
*/