mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user