diff --git a/core/src/config.cpp b/core/src/config.cpp index 3c167bbb..896fd78f 100644 --- a/core/src/config.cpp +++ b/core/src/config.cpp @@ -63,6 +63,7 @@ void ConfigManager::release(bool changed) { void ConfigManager::autoSaveWorker(ConfigManager* _this) { while (_this->autoSaveEnabled) { if (!_this->mtx.try_lock()) { + spdlog::warn("ConfigManager locked, waiting..."); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); continue; } diff --git a/core/src/dsp/block.h b/core/src/dsp/block.h index 865897c8..e4fdfa53 100644 --- a/core/src/dsp/block.h +++ b/core/src/dsp/block.h @@ -119,4 +119,61 @@ namespace dsp { } }; + + class Squelch : public Block { + public: + Squelch() : Block({1}, {1}, this, worker) {} + + void init(stream* input, int blockSize) { + in[0] = input; + inputBlockSize[0] = blockSize; + out[0]->setMaxLatency(blockSize * 2); + outputBlockSize[0] = blockSize; + level = -50.0f; + } + + float level; + int onCount; + int offCount; + + private: + static void worker(Squelch* _this) { + int blockSize = _this->inputBlockSize[0]; + stream* in = _this->in[0]; + stream* out = _this->out[0]; + complex_t* buf = new complex_t[blockSize]; + + int _on = 0, _off = 0; + bool active = false; + + while (true) { + if (in->read(buf, blockSize) < 0) { break; }; + for (int i = 0; i < blockSize; i++) { + if (log10(sqrt((buf[i].i*buf[i].i) + (buf[i].q*buf[i].q))) * 10.0f > _this->level) { + _on++; + _off = 0; + } + else { + _on = 0; + _off++; + } + if (_on >= _this->onCount && !active) { + _on = _this->onCount; + active = true; + } + if (_off >= _this->offCount && active) { + _off = _this->offCount; + active = false; + } + if (!active) { + buf[i].i = 0.0f; + buf[i].q = 0.0f; + } + } + if (out->write(buf, blockSize) < 0) { break; }; + } + delete[] buf; + } + + }; }; \ No newline at end of file diff --git a/core/src/dsp/correction.h b/core/src/dsp/correction.h index af39f77c..dc6db7bc 100644 --- a/core/src/dsp/correction.h +++ b/core/src/dsp/correction.h @@ -63,26 +63,95 @@ namespace dsp { bool bypass; private: + // static void _worker(DCBiasRemover* _this) { + // complex_t* buf = new complex_t[_this->_bufferSize]; + // float ibias = 0.0f; + // float qbias = 0.0f; + // while (true) { + // if (_this->_in->read(buf, _this->_bufferSize) < 0) { break; }; + // if (_this->bypass) { + // if (_this->output.write(buf, _this->_bufferSize) < 0) { break; }; + // continue; + // } + // for (int i = 0; i < _this->_bufferSize; i++) { + // ibias += buf[i].i; + // qbias += buf[i].q; + // } + // ibias /= _this->_bufferSize; + // qbias /= _this->_bufferSize; + // for (int i = 0; i < _this->_bufferSize; i++) { + // buf[i].i -= ibias; + // buf[i].q -= qbias; + // } + // if (_this->output.write(buf, _this->_bufferSize) < 0) { break; }; + // } + // delete[] buf; + // } + static void _worker(DCBiasRemover* _this) { complex_t* buf = new complex_t[_this->_bufferSize]; - float ibias = 0.0f; - float qbias = 0.0f; + complex_t* mixBuf = new complex_t[_this->_bufferSize]; + + float currentPhase = 0.0f; + float lastPhase = 0.0f; + double phase = 0.0f; + while (true) { + float ibias = 0.0f; + float qbias = 0.0f; if (_this->_in->read(buf, _this->_bufferSize) < 0) { break; }; if (_this->bypass) { if (_this->output.write(buf, _this->_bufferSize) < 0) { break; }; continue; } + + // Detect the frequency of the signal + double avgDiff = 0.0f; + // for (int i = 0; i < _this->_bufferSize; i++) { + // currentPhase = fast_arctan2(buf[i].i, buf[i].q); + // float diff = currentPhase - lastPhase; + // if (diff > 3.1415926535f) { diff -= 2 * 3.1415926535f; } + // else if (diff <= -3.1415926535f) { diff += 2 * 3.1415926535f; } + // avgDiff += diff; + // lastPhase = currentPhase; + // } + // avgDiff /= (double)_this->_bufferSize; + // avgDiff /= (double)_this->_bufferSize; + + // Average the samples to "filter" the signal to the block frequency for (int i = 0; i < _this->_bufferSize; i++) { ibias += buf[i].i; qbias += buf[i].q; } ibias /= _this->_bufferSize; qbias /= _this->_bufferSize; + + // Get the phase difference from the last block + currentPhase = fast_arctan2(ibias, qbias); + float diff = currentPhase - lastPhase; + if (diff > 3.1415926535f) { diff -= 2 * 3.1415926535f; } + else if (diff <= -3.1415926535f) { diff += 2 * 3.1415926535f; } + avgDiff += diff; + lastPhase = currentPhase; + avgDiff /= (double)_this->_bufferSize; + + // Generate a correction signal using the phase difference for (int i = 0; i < _this->_bufferSize; i++) { - buf[i].i -= ibias; - buf[i].q -= qbias; + mixBuf[i].i = sin(phase); + mixBuf[i].q = cos(phase); + phase -= avgDiff; + phase = fmodl(phase, 2.0 * 3.1415926535); } + + // Mix the correction signal with the original signal to shift the unwanted signal + // to the center. Also, null out the real component so that symetric + // frequencies are removed (at least I hope...) + float tq; + for (int i = 0; i < _this->_bufferSize; i++) { + buf[i].i = ((mixBuf[i].i * buf[i].q) + (mixBuf[i].q * buf[i].i)) * 1.4142; + buf[i].q = 0; + } + if (_this->output.write(buf, _this->_bufferSize) < 0) { break; }; } delete[] buf; diff --git a/core/src/gui/main_window.cpp b/core/src/gui/main_window.cpp index 71fb33bf..e1071a22 100644 --- a/core/src/gui/main_window.cpp +++ b/core/src/gui/main_window.cpp @@ -92,6 +92,10 @@ void windowInit() { credits::init(); + core::configManager.aquire(); + gui::menu.order = core::configManager.conf["menuOrder"].get>(); + core::configManager.release(); + gui::menu.registerEntry("Source", sourecmenu::draw, NULL); gui::menu.registerEntry("Audio", audiomenu::draw, NULL); gui::menu.registerEntry("Scripting", scriptingmenu::draw, NULL); @@ -117,7 +121,6 @@ void windowInit() { displaymenu::init(); // Load last source configuration - // Also add a loading screen // Adjustable "snap to grid" for each VFO // Finish the recorder module @@ -129,10 +132,9 @@ void windowInit() { // Do VFO in two steps: First sample rate conversion, then filtering // And a module add/remove/change order menu - // get rid of watchers and use if() instead - // Switch to double for all frequecies and bandwidth // Update UI settings + core::configManager.aquire(); fftMin = core::configManager.conf["min"]; fftMax = core::configManager.conf["max"]; gui::waterfall.setFFTMin(fftMin); @@ -159,8 +161,6 @@ void windowInit() { fftHeight = core::configManager.conf["fftHeight"]; gui::waterfall.setFFTHeight(fftHeight); - gui::menu.order = core::configManager.conf["menuOrder"].get>(); - core::configManager.release(); } @@ -297,10 +297,6 @@ void drawWindow() { core::configManager.release(true); } - if (dcbias.changed()) { - sigpath::signalPath.setDCBiasCorrection(dcbias.val); - } - int _fftHeight = gui::waterfall.getFFTHeight(); if (fftHeight != _fftHeight) { fftHeight = _fftHeight; @@ -418,6 +414,9 @@ void drawWindow() { ImGui::Text("Framerate: %.1f FPS", ImGui::GetIO().Framerate); ImGui::Text("Center Frequency: %.0 Hz", gui::waterfall.getCenterFrequency()); ImGui::Text("Source name: %s", sourceName.c_str()); + if (ImGui::Checkbox("Test technique", &dcbias.val)) { + sigpath::signalPath.setDCBiasCorrection(dcbias.val); + } ImGui::Spacing(); } diff --git a/core/src/gui/waterfall.cpp b/core/src/gui/waterfall.cpp index 5b64e46e..86c5f644 100644 --- a/core/src/gui/waterfall.cpp +++ b/core/src/gui/waterfall.cpp @@ -229,7 +229,8 @@ namespace ImGui { } int refCenter = mousePos.x - (widgetPos.x + 50); if (refCenter >= 0 && refCenter < dataWidth && mousePos.y > widgetPos.y && mousePos.y < (widgetPos.y + widgetSize.y)) { - vfo->setOffset(((((double)refCenter / ((double)dataWidth / 2.0)) - 1.0) * (viewBandwidth / 2.0)) + viewOffset); + double off = ((((double)refCenter / ((double)dataWidth / 2.0)) - 1.0) * (viewBandwidth / 2.0)) + viewOffset; + vfo->setOffset(round(off / vfo->snapInterval) * vfo->snapInterval); } } diff --git a/file_source/src/main.cpp b/file_source/src/main.cpp index 6aae4b04..7eafb9e0 100644 --- a/file_source/src/main.cpp +++ b/file_source/src/main.cpp @@ -32,7 +32,7 @@ public: handler.stream = &stream; sigpath::sourceManager.registerSource("File", &handler); - reader = new WavReader("D:/satpic/raw_recordings/NOAA-18_09-08-2018_21-39-00_baseband_NR.wav"); + reader = new WavReader("D:/Downloads/20120416_glv_594_2.wav"); spdlog::info("Samplerate: {0}, Bit depth: {1}, Channel count: {2}", reader->getSampleRate(), reader->getBitDepth(), reader->getChannelCount()); diff --git a/prepare_root.bat b/prepare_root.bat index 2ed7730e..dea93998 100644 --- a/prepare_root.bat +++ b/prepare_root.bat @@ -1,4 +1,6 @@ echo OFF copy /b/v/y build\Release\* root\ -copy /b/v/y build\modules\radio\Release\radio.dll root\modules\radio.dll -copy /b/v/y build\modules\recorder\Release\recorder.dll root\modules\recorder.dll +copy /b/v/y build\radio\Release\radio.dll root\modules\radio.dll +copy /b/v/y build\recorder\Release\recorder.dll root\modules\recorder.dll +copy /b/v/y build\rtl_tcp_source\Release\rtl_tcp_source.dll root\modules\rtl_tcp_source.dll +copy /b/v/y build\soapy\Release\soapy.dll root\modules\soapy.dll \ No newline at end of file diff --git a/radio/src/main.cpp b/radio/src/main.cpp index 7d4a34ca..d5f913a6 100644 --- a/radio/src/main.cpp +++ b/radio/src/main.cpp @@ -66,7 +66,7 @@ private: if (ImGui::RadioButton(CONCAT("AM##_", _this->name), _this->demod == 2) && _this->demod != 2) { _this->demod = 2; _this->bandWidth = 12500; - _this->bandWidthMin = 6250; + _this->bandWidthMin = 1500; _this->bandWidthMax = 12500; _this->sigPath.setDemodulator(SigPath::DEMOD_AM, _this->bandWidth); } @@ -120,6 +120,9 @@ private: _this->bandWidth = std::clamp(_this->bandWidth, _this->bandWidthMin, _this->bandWidthMax); _this->sigPath.setBandwidth(_this->bandWidth); } + + ImGui::SliderFloat(CONCAT("##_squelch_select_", _this->name), &_this->sigPath.squelch.level, -100, 0); + ImGui::PopItemWidth(); } diff --git a/radio/src/path.cpp b/radio/src/path.cpp index e2a8c87c..97f52962 100644 --- a/radio/src/path.cpp +++ b/radio/src/path.cpp @@ -5,7 +5,7 @@ SigPath::SigPath() { } -int SigPath::sampleRateChangeHandler(void* ctx, float sampleRate) { +int SigPath::sampleRateChangeHandler(void* ctx, double sampleRate) { SigPath* _this = (SigPath*)ctx; _this->outputSampleRate = sampleRate; _this->audioResamp.stop(); @@ -29,15 +29,21 @@ void SigPath::init(std::string vfoName, uint64_t sampleRate, int blockSize) { _demod = DEMOD_FM; _deemp = DEEMP_50US; bandwidth = 200000; + demodOutputSamplerate = 200000; // TODO: Set default VFO options // TODO: ajust deemphasis for different output sample rates // TODO: Add a mono to stereo for different modes - demod.init(vfo->output, 100000, 200000, 800); - amDemod.init(vfo->output, 50); - ssbDemod.init(vfo->output, 6000, 3000, 22); - cpx2stereo.init(vfo->output, 22); + squelch.init(vfo->output, 800); + squelch.level = 40; + squelch.onCount = 1; + squelch.offCount = 2560; + + demod.init(squelch.out[0], 100000, 200000, 800); + amDemod.init(squelch.out[0], 50); + ssbDemod.init(squelch.out[0], 6000, 3000, 22); + cpx2stereo.init(squelch.out[0], 22); audioResamp.init(&demod.output, 200000, 48000, 800); deemp.init(&audioResamp.output, 800, 50e-6, 48000); @@ -91,10 +97,13 @@ void SigPath::setDemodulator(int demId, float bandWidth) { spdlog::error("UNIMPLEMENTED DEMODULATOR IN SigPath::setDemodulator (stop)"); } _demod = demId; + + squelch.stop(); // Set input of the audio resampler // TODO: Set bandwidth from argument if (demId == DEMOD_FM) { + demodOutputSamplerate = 200000; vfo->setSampleRate(200000, bandwidth); demod.setBlockSize(vfo->getOutputBlockSize()); demod.setSampleRate(200000); @@ -107,6 +116,7 @@ void SigPath::setDemodulator(int demId, float bandWidth) { demod.start(); } else if (demId == DEMOD_NFM) { + demodOutputSamplerate = 16000; vfo->setSampleRate(16000, bandwidth); demod.setBlockSize(vfo->getOutputBlockSize()); demod.setSampleRate(16000); @@ -119,6 +129,7 @@ void SigPath::setDemodulator(int demId, float bandWidth) { demod.start(); } else if (demId == DEMOD_AM) { + demodOutputSamplerate = 125000; vfo->setSampleRate(12500, bandwidth); amDemod.setBlockSize(vfo->getOutputBlockSize()); audioResamp.setInput(&amDemod.output); @@ -129,6 +140,7 @@ void SigPath::setDemodulator(int demId, float bandWidth) { amDemod.start(); } else if (demId == DEMOD_USB) { + demodOutputSamplerate = 6000; vfo->setSampleRate(6000, bandwidth); ssbDemod.setBlockSize(vfo->getOutputBlockSize()); ssbDemod.setMode(dsp::SSBDemod::MODE_USB); @@ -140,6 +152,7 @@ void SigPath::setDemodulator(int demId, float bandWidth) { ssbDemod.start(); } else if (demId == DEMOD_LSB) { + demodOutputSamplerate = 6000; vfo->setSampleRate(6000, bandwidth); ssbDemod.setBlockSize(vfo->getOutputBlockSize()); ssbDemod.setMode(dsp::SSBDemod::MODE_LSB); @@ -151,6 +164,7 @@ void SigPath::setDemodulator(int demId, float bandWidth) { ssbDemod.start(); } else if (demId == DEMOD_DSB) { + demodOutputSamplerate = 6000; vfo->setSampleRate(6000, bandwidth); ssbDemod.setBlockSize(vfo->getOutputBlockSize()); ssbDemod.setMode(dsp::SSBDemod::MODE_DSB); @@ -162,6 +176,7 @@ void SigPath::setDemodulator(int demId, float bandWidth) { ssbDemod.start(); } else if (demId == DEMOD_RAW) { + demodOutputSamplerate = 10000; vfo->setSampleRate(10000, bandwidth); cpx2stereo.setBlockSize(vfo->getOutputBlockSize()); //audioResamp.setInput(&cpx2stereo.output); @@ -174,6 +189,9 @@ void SigPath::setDemodulator(int demId, float bandWidth) { spdlog::error("UNIMPLEMENTED DEMODULATOR IN SigPath::setDemodulator (start): {0}", demId); } + squelch.setBlockSize(vfo->getOutputBlockSize()); + squelch.start(); + deemp.setBlockSize(audioResamp.getOutputBlockSize()); audioResamp.start(); @@ -242,12 +260,15 @@ void SigPath::setBandwidth(float bandWidth) { if (audioBw != _audioBw) { audioBw = _audioBw; audioResamp.stop(); - audioResamp.setInputSampleRate(6000, vfo->getOutputBlockSize(), audioBw, audioBw); + audioResamp.setFilterParams(audioBw, audioBw); + audioResamp.setBlockSize(vfo->getOutputBlockSize()); + //audioResamp.setInputSampleRate(demodOutputSamplerate, vfo->getOutputBlockSize(), audioBw, audioBw); audioResamp.start(); } } void SigPath::start() { + squelch.start(); demod.start(); audioResamp.start(); deemp.start(); diff --git a/radio/src/path.h b/radio/src/path.h index f3f6b822..fdf02956 100644 --- a/radio/src/path.h +++ b/radio/src/path.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -44,14 +45,17 @@ public: dsp::FMDeemphasis deemp; + dsp::Squelch squelch; private: - static int sampleRateChangeHandler(void* ctx, float sampleRate); + static int sampleRateChangeHandler(void* ctx, double sampleRate); VFOManager::VFO* vfo; dsp::stream input; + + // Demodulators dsp::FMDemodulator demod; dsp::AMDemodulator amDemod; @@ -66,6 +70,7 @@ private: float sampleRate; float bandwidth; + float demodOutputSamplerate; float outputSampleRate; int blockSize; int _demod; diff --git a/recorder/src/main.cpp b/recorder/src/main.cpp index 60c72efc..e88ef2a8 100644 --- a/recorder/src/main.cpp +++ b/recorder/src/main.cpp @@ -25,7 +25,7 @@ void streamRemovedHandler(void* ctx) { } -void sampleRateChanged(void* ctx, float sampleRate, int blockSize) { +void sampleRateChanged(void* ctx, double sampleRate, int blockSize) { } diff --git a/root/config.json b/root/config.json index f96b5c9d..2a7a2e93 100644 --- a/root/config.json +++ b/root/config.json @@ -1,34 +1,43 @@ { "audio": { "Radio": { - "device": "Speakers (Realtek High Definiti", + "device": "CABLE Input (VB-Audio Virtual Cable)", "sampleRate": 48000.0, - "volume": 0.4354838728904724 + "volume": 0.42578125 + }, + "Radio 1": { + "device": "CABLE-A Input (VB-Audio Cable A)", + "sampleRate": 48000.0, + "volume": 1.0 + }, + "Radio 2": { + "device": "CABLE Input (VB-Audio Virtual Cable)", + "sampleRate": 48000.0, + "volume": 1.0 } }, "bandPlan": "General", "bandPlanEnabled": true, "fftHeight": 300, - "frequency": 91000000, + "frequency": 98391106, "max": 0.0, "maximized": false, + "menuOrder": [ + "Source", + "Radio", + "Recorder", + "Audio", + "Scripting", + "Band Plan", + "Display" + ], "menuWidth": 300, - "min": -70.0, + "min": -72.05882263183594, "showWaterfall": true, - "source": "HackRF One #0 901868dc282c8f8b", - "sourceSettings": { - "HackRF One #0 901868dc282c8f8b": { - "gains": { - "AMP": 0.0, - "LNA": 0.0, - "VGA": 0.0 - }, - "sampleRate": 8000000 - } - }, + "source": "", + "sourceSettings": {}, "windowSize": { "h": 720, "w": 1280 - }, - "menuOrder": ["Source", "Radio", "Recorder", "Audio", "Band Plan", "Display"] + } } \ No newline at end of file diff --git a/root/module_list.json b/root/module_list.json index f7d192fd..6e593fcc 100644 --- a/root/module_list.json +++ b/root/module_list.json @@ -1,4 +1,6 @@ { "Radio": "./modules/radio.dll", - "Recorder": "./modules/recorder.dll" -} \ No newline at end of file + "Recorder": "./modules/recorder.dll", + "Soapy": "./modules/soapy.dll", + "RTLTCPSource": "./modules/rtl_tcp_source.dll" +} diff --git a/root/radio_config.json b/root/radio_config.json new file mode 100644 index 00000000..c6ea5a59 --- /dev/null +++ b/root/radio_config.json @@ -0,0 +1,5 @@ +{ + "Radio 1": { + "demodulator":1 + } +} \ No newline at end of file diff --git a/root/sdrpp_0.2.5_alpha_preview.zip b/root/sdrpp_0.2.5_alpha_preview.zip deleted file mode 100644 index 99c3377b..00000000 Binary files a/root/sdrpp_0.2.5_alpha_preview.zip and /dev/null differ diff --git a/root/soapy_source_config.json b/root/soapy_source_config.json new file mode 100644 index 00000000..376198a8 --- /dev/null +++ b/root/soapy_source_config.json @@ -0,0 +1,4 @@ +{ + "device": "", + "devices": {} +} \ No newline at end of file diff --git a/root_dev/config.json b/root_dev/config.json index 803e2e07..c3ddd76d 100644 --- a/root_dev/config.json +++ b/root_dev/config.json @@ -1,27 +1,43 @@ -{ - "audio": {}, - "bandPlan": "General", - "bandPlanEnabled": true, - "fftHeight": 300, - "frequency": 94930000, - "max": 0.0, - "maximized": false, - "menuOrder": [ - "Source", - "Radio", - "Recorder", - "Audio", - "Scripting", - "Band Plan", - "Display" - ], - "menuWidth": 300, - "min": -51.47058868408203, - "showWaterfall": true, - "source": "", - "sourceSettings": {}, - "windowSize": { - "h": 1053, - "w": 959 - } +{ + "audio": { + "Radio": { + "device": "Speakers (Realtek High Definiti", + "sampleRate": 48000.0, + "volume": 0.60546875 + }, + "Radio 1": { + "device": "CABLE-A Input (VB-Audio Cable A)", + "sampleRate": 48000.0, + "volume": 1.0 + }, + "Radio 2": { + "device": "CABLE Input (VB-Audio Virtual Cable)", + "sampleRate": 48000.0, + "volume": 1.0 + } + }, + "bandPlan": "General", + "bandPlanEnabled": true, + "fftHeight": 300, + "frequency": 98983691, + "max": 0.0, + "maximized": false, + "menuOrder": [ + "Source", + "Radio", + "Recorder", + "Audio", + "Scripting", + "Band Plan", + "Display" + ], + "menuWidth": 300, + "min": -72.05882263183594, + "showWaterfall": true, + "source": "", + "sourceSettings": {}, + "windowSize": { + "h": 720, + "w": 1280 + } } \ No newline at end of file diff --git a/root_dev/module_list.json b/root_dev/module_list.json index 5a29e31f..446b3f19 100644 --- a/root_dev/module_list.json +++ b/root_dev/module_list.json @@ -1,6 +1,7 @@ { - "Radio": "./radio/radio.so", - "Recorder": "./recorder/recorder.so", - "Soapy": "./soapy/soapy.so", - "RTLTCPSource": "./rtl_tcp_source/rtl_tcp_source.so" + "Radio": "./radio/Release/radio.dll", + "Recorder": "./recorder/Release/recorder.dll", + "Soapy": "./soapy/Release/soapy.dll", + "RTLTCPSource": "./rtl_tcp_source/Release/rtl_tcp_source.dll", + "FileSource": "./file_source/Release/file_source.dll" } diff --git a/root_dev/soapy_source_config.json b/root_dev/soapy_source_config.json index 9f57e6a2..b682a34a 100644 --- a/root_dev/soapy_source_config.json +++ b/root_dev/soapy_source_config.json @@ -1,22 +1,29 @@ -{ - "device": "HackRF One #0 901868dc282c8f8b", - "devices": { - "Generic RTL2832U OEM :: 00000001": { - "gains": { - "TUNER": 12.817999839782715 - }, - "sampleRate": 2560000.0 - }, - "HackRF One #0 901868dc282c8f8b": { - "gains": { - "AMP": 0.0, - "LNA": 24.711999893188477, - "VGA": 15.906000137329102 - }, - "sampleRate": 8000000.0 - }, - "PulseAudio": { - "sampleRate": 96000.0 - } - } +{ + "device": "Generic RTL2832U OEM :: 00000001", + "devices": { + "AirSpy HF+ [c852435de0224af7]": { + "gains": { + "LNA": 6.0, + "RF": 0.0 + }, + "sampleRate": 768000.0 + }, + "Generic RTL2832U OEM :: 00000001": { + "gains": { + "TUNER": 49.599998474121094 + }, + "sampleRate": 2560000.0 + }, + "HackRF One #0 901868dc282c8f8b": { + "gains": { + "AMP": 0.0, + "LNA": 24.711999893188477, + "VGA": 15.906000137329102 + }, + "sampleRate": 8000000.0 + }, + "PulseAudio": { + "sampleRate": 96000.0 + } + } } \ No newline at end of file