#2592 Use jAirspyHF library for AirspyHF tuners. (#2593)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-11-07 03:34:37 -05:00
committed by GitHub
parent 6cd2cea30b
commit 9ca4ae4d8f
9 changed files with 560 additions and 859 deletions

View File

@@ -124,8 +124,9 @@ dependencies {
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'commons-io:commons-io:2.22.0'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jairspy:1.0.12' //Airspy tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jairspyhf:1.8.1' //Airspy HF+ tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jhydrasdr:1.1.4' //HydraSDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jrtl:1.4.0.1' //RTL-SDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'

View File

@@ -17,6 +17,14 @@ dependencyResolutionManagement {
content { includeModule 'io.github.dsheirer', 'jairspy' }
}
//jAirspyHF is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jAirspyHF/releases/download'
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jairspyhf' }
}
//jRTL is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jRTL/releases/download'

View File

@@ -1,132 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.AbstractNativeBuffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.Iterator;
/**
* Native buffer implementation for Airspy HF+ & Discovery tuners.
*/
public class AirspyHfNativeBuffer extends AbstractNativeBuffer
{
public static final float SCALE = 1.0f / 32768.0f;
private short[] mInterleavedSamples;
private float mAverageDc;
/**
* Constructs an instance.
* @param timestamp of the first sample of the buffer
* @param interleavedSamples pairs of 16-bit complex samples.
*/
public AirspyHfNativeBuffer(long timestamp, float samplesPerMillisecond, float averageDc, short[] interleavedSamples)
{
super(timestamp, samplesPerMillisecond);
mInterleavedSamples = interleavedSamples;
mAverageDc = averageDc;
}
@Override
public Iterator<ComplexSamples> iterator()
{
return new IteratorComplexSamples();
}
@Override
public Iterator<InterleavedComplexSamples> iteratorInterleaved()
{
return new IteratorInterleaved();
}
@Override
public int sampleCount()
{
return mInterleavedSamples.length / 2;
}
/**
* Scalar implementation of complex samples buffer iterator
*/
public class IteratorComplexSamples implements Iterator<ComplexSamples>
{
private boolean mHasNext = true;
@Override
public boolean hasNext()
{
return mHasNext;
}
@Override
public ComplexSamples next()
{
int length = mInterleavedSamples.length / 2;
float[] i = new float[length];
float[] q = new float[length];
int index = 0;
for(int x = 0; x < mInterleavedSamples.length; x += 2)
{
index = x / 2;
//Native ordering is in Q, I, Q, I ... reverse it to I, Q, I, Q
i[index] = (mInterleavedSamples[x + 1] * SCALE) - mAverageDc;
q[index] = (mInterleavedSamples[x] * SCALE) - mAverageDc;
}
mHasNext = false;
return new ComplexSamples(i, q, getTimestamp());
}
}
/**
* Scalar implementation of interleaved sample buffer iterator.
*/
public class IteratorInterleaved implements Iterator<InterleavedComplexSamples>
{
private boolean mHasNext = true;
@Override
public boolean hasNext()
{
return mHasNext;
}
@Override
public InterleavedComplexSamples next()
{
float[] samples = new float[mInterleavedSamples.length];
for(int x = 0; x < mInterleavedSamples.length; x += 2)
{
//Native ordering is in Q, I, Q, I ... reverse it to I, Q, I, Q
samples[x] = (mInterleavedSamples[x + 1] * SCALE) - mAverageDc;
samples[x + 1] = (mInterleavedSamples[x] * SCALE) - mAverageDc;
}
mHasNext = false;
return new InterleavedComplexSamples(samples, getTimestamp());
}
}
}

View File

@@ -1,83 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
import io.github.dsheirer.buffer.DcCorrectionManager;
import io.github.dsheirer.buffer.INativeBuffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
/**
* Airspy HF+ native buffer factory.
*
* Note: the tuner provides 1024 samples in each transfer where samples are 16-bit interleaved complex.
*/
public class AirspyHfNativeBufferFactory extends AbstractNativeBufferFactory
{
private DcCorrectionManager mDcCorrectionManager = new DcCorrectionManager();
/**
* Constructs an instance
*/
public AirspyHfNativeBufferFactory()
{
}
/**
* Converts the samples byte buffer into a native buffer and calculates DC offset
* @param samples byte array copied from native memory
* @param timestamp of the samples
* @return constructed native buffer
*/
@Override
public INativeBuffer getBuffer(ByteBuffer samples, long timestamp)
{
ShortBuffer shortBuffer = samples.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short[] converted = new short[shortBuffer.capacity()];
shortBuffer.get(converted);
if(mDcCorrectionManager.shouldCalculateDc())
{
calculateDc(converted);
}
return new AirspyHfNativeBuffer(timestamp, getSamplesPerMillisecond(), mDcCorrectionManager.getAverageDc(), converted);
}
/**
* Calculates the average DC in the sample stream so that it can be subtracted from the samples when the
* native buffer is used.
* @param samples containing DC offset
*/
private void calculateDc(short[] samples)
{
float dcAccumulator = 0;
for(short sample: samples)
{
dcAccumulator += sample;
}
dcAccumulator /= samples.length;
mDcCorrectionManager.adjust(dcAccumulator * AirspyHfNativeBuffer.SCALE);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* 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
@@ -24,7 +24,6 @@ import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.ui.TunerEditor;
import java.io.IOException;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -147,11 +146,9 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
if(hasTuner())
{
getTunerIdLabel().setText("Airspy " + getTuner().getController().getBoardId() + " SER# " +
getTuner().getController().getSerialNumber());
getTuner().getController().getSerialNumber() + " FW " + getTuner().getController().getFirmwareVersion());
//Permanently disable the sample rates combo and force 912kHz usage - during dev testing, attempts to use
//the other sample rates produced inconsistent results.
getSampleRateCombo().setEnabled(false);
getSampleRateCombo().setEnabled(!getTuner().getTunerController().isLockedSampleRate());
getSampleRateCombo().removeAllItems();
for(AirspyHfSampleRate sampleRate: getTuner().getController().getAvailableSampleRates())
{
@@ -162,7 +159,7 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
getLnaToggleButton().setEnabled(true);
getLnaToggleButton().setSelected(getTuner().getController().getLna());
getSampleRateCombo().setSelectedItem(getTuner().getController().getCurrentAirspySampleRate());
getAttenuationCombo().setEnabled(true);
getAttenuationCombo().setEnabled(!getTuner().getController().getAgc());
getAttenuationCombo().setSelectedItem(getTuner().getController().getAttenuation());
}
else
@@ -260,6 +257,7 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
if(mAttenuationCombo == null)
{
mAttenuationCombo = new JComboBox<>(Attenuation.values());
mAttenuationCombo.setToolTipText("Attenuation applies when the AGC is off");
mAttenuationCombo.setEnabled(false);
mAttenuationCombo.addActionListener(e -> {
if(!isLoading())
@@ -270,12 +268,12 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
getTuner().getController().setAttenuation(selected);
save();
}
catch(IOException ioe)
catch(SourceException se)
{
mLog.error("Error setting Airspy Hf attenuation [" + selected + "]", ioe);
mLog.error("Error setting Airspy Hf attenuation [" + selected + "]", se);
JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
"Airspy Tuner Controller - couldn't apply attenuation setting [" +
selected + "] " + ioe.getLocalizedMessage());
selected + "] " + se.getLocalizedMessage());
}
}
});
@@ -300,13 +298,14 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
try
{
getTuner().getController().setAgc(mAgcToggleButton.isSelected());
getAttenuationCombo().setEnabled(!mAgcToggleButton.isSelected());
save();
}
catch(IOException ioe)
catch(SourceException se)
{
mLog.error("Error setting Airspy HF AGC", ioe);
mLog.error("Error setting Airspy HF AGC", se);
JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
"Airspy HF Tuner Controller - couldn't change AGC setting" + ioe.getLocalizedMessage());
"Airspy HF Tuner Controller - couldn't change AGC setting" + se.getLocalizedMessage());
}
}
});
@@ -333,11 +332,11 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
getTuner().getController().setLna(mLnaToggleButton.isSelected());
save();
}
catch(IOException ioe)
catch(SourceException se)
{
mLog.error("Error setting Airspy HF LNA", ioe);
mLog.error("Error setting Airspy HF LNA", se);
JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
"Airspy HF Tuner Controller - couldn't change LNA setting" + ioe.getLocalizedMessage());
"Airspy HF Tuner Controller - couldn't change LNA setting" + se.getLocalizedMessage());
}
}
});

View File

@@ -1,65 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.source.tuner.airspy.hf;
/**
* Requests/Commands supported by the Airspy HF
*/
public enum Request
{
INVALID(0),
RECEIVER_MODE(1),
SET_FREQUENCY(2),
GET_SAMPLE_RATES(3),
SET_SAMPLE_RATE(4),
CONFIG_READ(5),
CONFIG_WRITE(6),
GET_SERIAL_NUMBER_BOARD_ID(7),
SET_USER_OUTPUT(8),
GET_VERSION_STRING(9),
SET_HF_AGC(10),
SET_HF_AGC_THRESHOLD(11),
SET_HF_ATT(12),
SET_HF_LNA(13),
GET_SAMPLE_RATE_ARCHITECTURES(14),
GET_FILTER_GAIN(15),
GET_FREQUENCY_DELTA(16),
SET_VCTCXO_CALIBRATION(17);
private int mValue;
/**
* Constructs an instance
* @param value of the entry
*/
Request(int value)
{
mValue = value;
}
/**
* Value associated with the entry
* @return value cast to a byte.
*/
public byte getRequest()
{
return (byte)mValue;
}
}

View File

@@ -1,33 +0,0 @@
package io.github.dsheirer.source.tuner.airspy.hf;
/**
* Responses to Airspy HF requests
*/
public enum Response
{
SUCCESS(0),
ERROR(-1),
UNSUPPORTED(-2);
private int mValue;
Response(int value)
{
mValue = value;
}
/**
* Lookup the enumeration entry from the value.
* @param value to lookup
* @return entry or UNSUPPORTED if there are no matches.
*/
public static Response fromValue(int value)
{
return switch(value)
{
case 0 -> SUCCESS;
case -1 -> ERROR;
default -> UNSUPPORTED;
};
}
}

View File

@@ -0,0 +1 @@
ATTR{idVendor}=="03eb", ATTR{idProduct}=="800c", SYMLINK+="airspyhf-%k", MODE="660", GROUP="plugdev"