From 2748c13142b7e637e6338d1a378f9f360c9c5c42 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Fri, 3 Dec 2021 19:46:09 +0100 Subject: [PATCH] new radio code + fixes --- .github/workflows/build_all.yml | 1 - core/src/dsp/demodulator.h | 4 +- core/src/dsp/noise_reduction.h | 131 ++++++++ decoder_modules/new_radio/CMakeLists.txt | 21 ++ decoder_modules/new_radio/src/demod.h | 31 ++ .../new_radio/src/demodulators/wfm.h | 120 ++++++++ decoder_modules/new_radio/src/main.cpp | 30 ++ decoder_modules/new_radio/src/radio_module.h | 281 ++++++++++++++++++ 8 files changed, 616 insertions(+), 3 deletions(-) create mode 100644 core/src/dsp/noise_reduction.h create mode 100644 decoder_modules/new_radio/CMakeLists.txt create mode 100644 decoder_modules/new_radio/src/demod.h create mode 100644 decoder_modules/new_radio/src/demodulators/wfm.h create mode 100644 decoder_modules/new_radio/src/main.cpp create mode 100644 decoder_modules/new_radio/src/radio_module.h diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index cd690150..db5f635d 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -342,7 +342,6 @@ jobs: mv sdrpp_ubuntu_groovy_amd64/sdrpp_debian_amd64.deb sdrpp_all/sdrpp_ubuntu_groovy_amd64.deb && mv sdrpp_ubuntu_hirsute_amd64/sdrpp_debian_amd64.deb sdrpp_all/sdrpp_ubuntu_hirsute_amd64.deb && mv sdrpp_ubuntu_impish_amd64/sdrpp_debian_amd64.deb sdrpp_all/sdrpp_ubuntu_impish_amd64.deb - mv sdrpp_raspios_bullseye_arm32/sdrpp_raspios_arm32.deb sdrpp_all/sdrpp_raspios_bullseye_arm32.deb - uses: actions/upload-artifact@v2 with: diff --git a/core/src/dsp/demodulator.h b/core/src/dsp/demodulator.h index 25f6f865..4ef49610 100644 --- a/core/src/dsp/demodulator.h +++ b/core/src/dsp/demodulator.h @@ -805,9 +805,9 @@ namespace dsp { generic_hier_block::_block_init = true; } - void setInput(stream* input) { + void setInput(stream* input) { assert(generic_hier_block::_block_init); - r2c.setInput(input); + demod.setInput(input); } void setDeviation(float deviation) { diff --git a/core/src/dsp/noise_reduction.h b/core/src/dsp/noise_reduction.h new file mode 100644 index 00000000..52334e6d --- /dev/null +++ b/core/src/dsp/noise_reduction.h @@ -0,0 +1,131 @@ +#pragma once +#include +#include +#include + +#define NR_TAP_COUNT 4096 + +namespace dsp { + class FFTNoiseReduction : public generic_block { + public: + FFTNoiseReduction() {} + + FFTNoiseReduction(stream* in) { init(in); } + + ~FFTNoiseReduction() { + if (!generic_block::_block_init) { return; } + generic_block::stop(); + fftwf_destroy_plan(forwardPlan); + fftwf_destroy_plan(backwardPlan); + fftwf_free(delay); + fftwf_free(fft_in); + fftwf_free(fft_window); + fftwf_free(amp_buf); + fftwf_free(fft_cout); + fftwf_free(fft_fout); + } + + void init(stream* in) { + _in = in; + + delay = (float*)fftwf_malloc(sizeof(float)*STREAM_BUFFER_SIZE); + fft_in = (float*)fftwf_malloc(sizeof(float)*NR_TAP_COUNT); + fft_window = (float*)fftwf_malloc(sizeof(float)*NR_TAP_COUNT); + amp_buf = (float*)fftwf_malloc(sizeof(float)*NR_TAP_COUNT); + fft_cout = (complex_t*)fftwf_malloc(sizeof(complex_t)*NR_TAP_COUNT); + fft_fout = (float*)fftwf_malloc(sizeof(float)*NR_TAP_COUNT); + delay_start = &delay[NR_TAP_COUNT]; + + memset(delay, 0, sizeof(float)*STREAM_BUFFER_SIZE); + memset(fft_in, 0, sizeof(float)*NR_TAP_COUNT); + memset(amp_buf, 0, sizeof(float)*NR_TAP_COUNT); + memset(fft_cout, 0, sizeof(complex_t)*NR_TAP_COUNT); + memset(fft_fout, 0, sizeof(float)*NR_TAP_COUNT); + + for (int i = 0; i < NR_TAP_COUNT; i++) { + fft_window[i] = window_function::blackman(i, NR_TAP_COUNT - 1); + } + + forwardPlan = fftwf_plan_dft_r2c_1d(NR_TAP_COUNT, fft_in, (fftwf_complex*)fft_cout, FFTW_ESTIMATE); + backwardPlan = fftwf_plan_dft_c2r_1d(NR_TAP_COUNT, (fftwf_complex*)fft_cout, fft_fout, FFTW_ESTIMATE); + + generic_block::registerInput(_in); + generic_block::registerOutput(&out); + generic_block::_block_init = true; + } + + void setInput(stream* in) { + assert(generic_block::_block_init); + std::lock_guard lck(generic_block::ctrlMtx); + generic_block::tempStop(); + generic_block::unregisterInput(_in); + _in = in; + generic_block::registerInput(_in); + generic_block::tempStart(); + } + + int run() { + int count = _in->read(); + if (count < 0) { return -1; } + + // Bypass + if (!bypass) { + memcpy(out.writeBuf, _in->readBuf, count * sizeof(float)); + _in->flush(); + if (!out.swap(count)) { return -1; } + return count; + } + + // Write to delay buffer + memcpy(delay_start, _in->readBuf, count * sizeof(float)); + + // Iterate the FFT + for (int i = 0; i < count; i++) { + // Apply windows + volk_32f_x2_multiply_32f(fft_in, &delay[i], fft_window, NR_TAP_COUNT); + + // Do forward FFT + fftwf_execute(forwardPlan); + + // Process bins here + volk_32fc_magnitude_32f(amp_buf, (lv_32fc_t*)fft_cout, NR_TAP_COUNT/2); + for (int j = 1; j < NR_TAP_COUNT/2; j++) { + if (log10f(amp_buf[0]) < level) { + fft_cout[j] = {0, 0}; + } + } + + // Do reverse FFT and get first element + fftwf_execute(backwardPlan); + out.writeBuf[i] = fft_fout[NR_TAP_COUNT/2]; + } + + volk_32f_s32f_multiply_32f(out.writeBuf, out.writeBuf, 1.0f/(float)NR_TAP_COUNT, count); + + // Copy last values to delay + memmove(delay, &delay[count], NR_TAP_COUNT * sizeof(float)); + + _in->flush(); + if (!out.swap(count)) { return -1; } + return count; + } + + bool bypass = true; + stream out; + + float level = 0.0f; + + private: + stream* _in; + fftwf_plan forwardPlan; + fftwf_plan backwardPlan; + float* delay; + float* fft_in; + float* fft_window; + float* amp_buf; + float* delay_start; + complex_t* fft_cout; + float* fft_fout; + + }; +} \ No newline at end of file diff --git a/decoder_modules/new_radio/CMakeLists.txt b/decoder_modules/new_radio/CMakeLists.txt new file mode 100644 index 00000000..4e1f2268 --- /dev/null +++ b/decoder_modules/new_radio/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.13) +project(new_radio) + +file(GLOB_RECURSE SRC "src/*.cpp") + +add_library(new_radio SHARED ${SRC}) +target_link_libraries(new_radio PRIVATE sdrpp_core) +set_target_properties(new_radio PROPERTIES PREFIX "") + +target_include_directories(new_radio PRIVATE "src/") + +if (MSVC) + target_compile_options(new_radio PRIVATE /O2 /Ob2 /std:c++17 /EHsc) +elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(new_radio PRIVATE -O3 -std=c++17 -Wno-unused-command-line-argument -undefined dynamic_lookup) +else () + target_compile_options(new_radio PRIVATE -O3 -std=c++17) +endif () + +# Install directives +install(TARGETS new_radio DESTINATION lib/sdrpp/plugins) \ No newline at end of file diff --git a/decoder_modules/new_radio/src/demod.h b/decoder_modules/new_radio/src/demod.h new file mode 100644 index 00000000..0d80d210 --- /dev/null +++ b/decoder_modules/new_radio/src/demod.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include +#include +#include "radio_module.h" + +namespace demod { + class Demodulator { + public: + virtual ~Demodulator() {} + virtual void init(std::string name, ConfigManager* config, dsp::stream* input, double bandwidth) = 0; + virtual void start() = 0; + virtual void stop() = 0; + virtual void showMenu() = 0; + virtual void setBandwidth(double bandwidth) = 0; + virtual void setInput(dsp::stream* input) = 0; + virtual const char* getName() = 0; + virtual double getIFSampleRate() = 0; + virtual double getAFSampleRate() = 0; + virtual double getDefaultBandwidth() = 0; + virtual double getMinBandwidth() = 0; + virtual double getMaxBandwidth() = 0; + virtual double getMaxAFBandwidth() = 0; + virtual double getDefaultSnapInterval() = 0; + virtual int getVFOReference() = 0; + virtual dsp::stream* getOutput() = 0; + }; +} + +#include "demodulators/wfm.h" \ No newline at end of file diff --git a/decoder_modules/new_radio/src/demodulators/wfm.h b/decoder_modules/new_radio/src/demodulators/wfm.h new file mode 100644 index 00000000..db9f56d1 --- /dev/null +++ b/decoder_modules/new_radio/src/demodulators/wfm.h @@ -0,0 +1,120 @@ +#pragma once +#include "../demod.h" +#include +#include + +namespace demod { + class WFM : public Demodulator { + public: + WFM() {} + + WFM(std::string name, ConfigManager* config, dsp::stream* input, double bandwidth) { + init(name, config, input, bandwidth); + } + + ~WFM() { + stop(); + } + + void init(std::string name, ConfigManager* config, dsp::stream* input, double bandwidth) { + this->name = name; + _config = config; + + // Load config + _config->acquire(); + bool modified =false; + if (!config->conf[name].contains(getName())) { + config->conf[name][getName()]["deempMode"] = 0; + config->conf[name][getName()]["stereo"] = false; + modified = true; + } + if (config->conf[name][getName()].contains("deempMode")) { + deempMode = config->conf[name][getName()]["deempMode"]; + } + if (config->conf[name][getName()].contains("stereo")) { + stereo = config->conf[name][getName()]["stereo"]; + } + _config->release(modified); + + // Define structure + demod.init(input, getIFSampleRate(), bandwidth / 2.0f); + demodStereo.init(input, getIFSampleRate(), bandwidth / 2.0f); + //deemp.init(stereo ? demodStereo.out : &demod.out, getAFSampleRate(), 50e-6); + //deemp.bypass = false; + } + + void start() { + stereo ? demodStereo.start() : demod.start(); + //deemp.start(); + } + + void stop() { + demod.stop(); + demodStereo.stop(); + //deemp.stop(); + } + + void showMenu() { + if (ImGui::Checkbox(("Stereo##_radio_wfm_stereo_" + name).c_str(), &stereo)) { + setStereo(stereo); + _config->acquire(); + _config->conf[name][getName()]["stereo"] = stereo; + _config->release(true); + } + //ImGui::Checkbox("Deemp bypass", &deemp.bypass); + } + + void setBandwidth(double bandwidth) { + demod.setDeviation(bandwidth / 2.0f); + demodStereo.setDeviation(bandwidth / 2.0f); + } + + void setInput(dsp::stream* input) { + demod.setInput(input); + demodStereo.setInput(input); + } + + // ============= INFO ============= + + const char* getName() { return "WFM"; } + double getIFSampleRate() { return 250000.0; } + double getAFSampleRate() { return getIFSampleRate(); } + double getDefaultBandwidth() { return 150000.0; } + double getMinBandwidth() { return 50000.0; } + double getMaxBandwidth() { return getIFSampleRate(); } + double getMaxAFBandwidth() { return 16000.0; } + double getDefaultSnapInterval() { return 100000.0; } + int getVFOReference() { return ImGui::WaterfallVFO::REF_CENTER; } + dsp::stream* getOutput() { return demodStereo.out; } + + // ============= DEDICATED FUNCTIONS ============= + + void setStereo(bool _stereo) { + stereo = _stereo; + if (stereo) { + demod.stop(); + //deemp.setInput(demodStereo.out); + demodStereo.start(); + } + else { + demodStereo.stop(); + //deemp.setInput(&demod.out); + demod.start(); + } + } + + private: + dsp::FMDemod demod; + dsp::StereoFMDemod demodStereo; + //dsp::BFMDeemp deemp; + + RadioModule* _rad = NULL; + ConfigManager* _config = NULL; + + int deempMode; + bool stereo; + + std::string name; + + }; +} \ No newline at end of file diff --git a/decoder_modules/new_radio/src/main.cpp b/decoder_modules/new_radio/src/main.cpp new file mode 100644 index 00000000..c8667395 --- /dev/null +++ b/decoder_modules/new_radio/src/main.cpp @@ -0,0 +1,30 @@ +#include "radio_module.h" +#include + +SDRPP_MOD_INFO { + /* Name: */ "new_radio", + /* Description: */ "Analog radio decoder", + /* Author: */ "Ryzerth", + /* Version: */ 2, 0, 0, + /* Max instances */ -1 +}; + +MOD_EXPORT void _INIT_() { + json def = json({}); + config.setPath(options::opts.root + "/new_radio_config.json"); + config.load(def); + config.enableAutoSave(); +} + +MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) { + return new RadioModule(name); +} + +MOD_EXPORT void _DELETE_INSTANCE_(void* instance) { + delete (RadioModule*)instance; +} + +MOD_EXPORT void _END_() { + config.disableAutoSave(); + config.save(); +} \ No newline at end of file diff --git a/decoder_modules/new_radio/src/radio_module.h b/decoder_modules/new_radio/src/radio_module.h new file mode 100644 index 00000000..2d5db47d --- /dev/null +++ b/decoder_modules/new_radio/src/radio_module.h @@ -0,0 +1,281 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +ConfigManager config; + +#define CONCAT(a, b) ((std::string(a) + b).c_str()) + +class RadioModule; +#include "demod.h" + +class RadioModule : public ModuleManager::Instance { +public: + RadioModule(std::string name) { + this->name = name; + + // Initialize the config if it doesn't exist + config.acquire(); + if (!config.conf.contains(name)) { + config.conf[name]["selectedDemodId"] = 1; + } + selectedDemodID = config.conf[name]["selectedDemodId"]; + config.release(true); + + // Create demodulator instances + demods.fill(NULL); + demods[RADIO_DEMOD_WFM] = new demod::WFM(); + + // Initialize the VFO + vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, 200000, 200000, 50000, 200000, false); + onUserChangedBandwidthHandler.handler = vfoUserChangedBandwidthHandler; + onUserChangedBandwidthHandler.ctx = this; + vfo->wtfVFO->onUserChangedBandwidth.bindHandler(&onUserChangedBandwidthHandler); + + // Initialize the sink + srChangeHandler.ctx = this; + srChangeHandler.handler = sampleRateChangeHandler; + stream.init(&deemp.out, &srChangeHandler, audioSampleRate); + sigpath::sinkManager.registerStream(name, &stream); + + // Load configuration for all demodulators + for (auto& demod : demods) { + if (!demod) { continue; } + + // Default config + double bw = demod->getDefaultBandwidth(); + if (!config.conf[name].contains(demod->getName())) { + config.conf[name][demod->getName()]["bandwidth"] = bw; + config.conf[name][demod->getName()]["snapInterval"] = demod->getDefaultSnapInterval(); + config.conf[name][demod->getName()]["squelchLevel"] = MIN_SQUELCH; + } + bw = std::clamp(bw, demod->getMinBandwidth(), demod->getMaxBandwidth()); + + // Initialize + demod->init(name, &config, &squelch.out, bw); + } + + // Initialize DSP + squelch.init(vfo->output, MIN_SQUELCH); + win.init(24000, 24000, 48000); + resamp.init(NULL, &win, 250000, 48000); + deemp.init(&resamp.out, 48000, 50e-6); + deemp.bypass = false; + + // Select the demodulator + selectDemodByID((DemodID)selectedDemodID); + + // Start DSP + squelch.start(); + resamp.start(); + deemp.start(); + stream.start(); + + gui::menu.registerEntry(name, menuHandler, this, NULL); + } + + ~RadioModule() { + gui::menu.removeEntry(name); + } + + void postInit() {} + + void enable() { + enabled = true; + } + + void disable() { + enabled = false; + } + + bool isEnabled() { + return enabled; + } + + std::string name; + + enum DemodID { + RADIO_DEMOD_NFM, + RADIO_DEMOD_WFM, + RADIO_DEMOD_AM, + RADIO_DEMOD_DSB, + RADIO_DEMOD_USB, + RADIO_DEMOD_CW, + RADIO_DEMOD_LSB, + RADIO_DEMOD_RAW, + _RADIO_DEMOD_COUNT, + }; + +private: + static void menuHandler(void* ctx) { + RadioModule* _this = (RadioModule*)ctx; + + if (!_this->enabled) { style::beginDisabled(); } + + float menuWidth = ImGui::GetContentRegionAvailWidth(); + ImGui::BeginGroup(); + + // TODO: Change VFO ref in signal path + + ImGui::Columns(4, CONCAT("RadioModeColumns##_", _this->name), false); + if (ImGui::RadioButton(CONCAT("NFM##_", _this->name), _this->selectedDemodID == 0) && _this->selectedDemodID != 0) { + _this->selectDemodByID(RADIO_DEMOD_NFM); + } + if (ImGui::RadioButton(CONCAT("WFM##_", _this->name), _this->selectedDemodID == 1) && _this->selectedDemodID != 1) { + _this->selectDemodByID(RADIO_DEMOD_WFM); + } + ImGui::NextColumn(); + if (ImGui::RadioButton(CONCAT("AM##_", _this->name), _this->selectedDemodID == 2) && _this->selectedDemodID != 2) { + _this->selectDemodByID(RADIO_DEMOD_AM); + } + if (ImGui::RadioButton(CONCAT("DSB##_", _this->name), _this->selectedDemodID == 3) && _this->selectedDemodID != 3) { + _this->selectDemodByID(RADIO_DEMOD_DSB); + } + ImGui::NextColumn(); + if (ImGui::RadioButton(CONCAT("USB##_", _this->name), _this->selectedDemodID == 4) && _this->selectedDemodID != 4) { + _this->selectDemodByID(RADIO_DEMOD_USB); + } + if (ImGui::RadioButton(CONCAT("CW##_", _this->name), _this->selectedDemodID == 5) && _this->selectedDemodID != 5) { + _this->selectDemodByID(RADIO_DEMOD_CW); + }; + ImGui::NextColumn(); + if (ImGui::RadioButton(CONCAT("LSB##_", _this->name), _this->selectedDemodID == 6) && _this->selectedDemodID != 6) { + _this->selectDemodByID(RADIO_DEMOD_LSB); + } + if (ImGui::RadioButton(CONCAT("RAW##_", _this->name), _this->selectedDemodID == 7) && _this->selectedDemodID != 7) { + _this->selectDemodByID(RADIO_DEMOD_RAW); + }; + ImGui::Columns(1, CONCAT("EndRadioModeColumns##_", _this->name), false); + + ImGui::EndGroup(); + + _this->selectedDemod->showMenu(); + + ImGui::LeftLabel("Squelch"); + ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX()); + if (ImGui::SliderFloat(("##_radio_sqelch_" + _this->name).c_str(), &_this->squelchLevel, _this->MIN_SQUELCH, _this->MAX_SQUELCH, "%.3fdB")) { + _this->squelch.setLevel(_this->squelchLevel); + config.acquire(); + config.conf[_this->name][_this->selectedDemod->getName()]["squelchLevel"] = _this->squelchLevel; + config.release(true); + } + + if (!_this->enabled) { style::endDisabled(); } + } + + void selectDemodByID(DemodID id) { + demod::Demodulator* demod = demods[id]; + if (!demod) { + spdlog::error("Demodulator {0} not implemented", id); + return; + } + selectedDemodID = id; + selectDemod(demod); + } + + void selectDemod(demod::Demodulator* demod) { + // Stopcurrently selected demodulator and select new + if (selectedDemod) { selectedDemod->stop(); } + selectedDemod = demod; + + // Load config + bandwidth = selectedDemod->getDefaultBandwidth(); + double minBandwidth = selectedDemod->getMinBandwidth(); + double maxBandwidth = selectedDemod->getMaxBandwidth(); + snapInterval = selectedDemod->getDefaultSnapInterval(); + squelchLevel = MIN_SQUELCH; + if (config.conf[name][selectedDemod->getName()].contains("snapInterval")) { + bandwidth = config.conf[name][selectedDemod->getName()]["bandwidth"]; + bandwidth = std::clamp(bandwidth, minBandwidth, maxBandwidth); + } + if (config.conf[name][selectedDemod->getName()].contains("snapInterval")) { + snapInterval = config.conf[name][selectedDemod->getName()]["snapInterval"]; + } + if (config.conf[name][selectedDemod->getName()].contains("squelchLevel")) { + squelchLevel = config.conf[name][selectedDemod->getName()]["squelchLevel"]; + } + + // Configure VFO + if (vfo) { + vfo->setBandwidthLimits(minBandwidth, maxBandwidth, false); + vfo->setReference(selectedDemod->getVFOReference()); + vfo->setSnapInterval(snapInterval); + vfo->setSampleRate(selectedDemod->getIFSampleRate(), bandwidth); + } + + // Configure squelch + squelch.setLevel(squelchLevel); + + // Configure resampler + resamp.stop(); + resamp.setInput(selectedDemod->getOutput()); + resamp.setInSampleRate(selectedDemod->getAFSampleRate()); + setAudioSampleRate(audioSampleRate); + resamp.start(); + + // Start new demodulator + selectedDemod->start(); + } + + void setBandwidth(double bw) { + bandwidth = bw; + if (!selectedDemod) { return; } + selectedDemod->setBandwidth(bandwidth); + config.acquire(); + config.conf[name][selectedDemod->getName()]["bandwidth"] = bandwidth; + config.release(true); + } + + void setAudioSampleRate(double sr) { + audioSampleRate = sr; + if (!selectedDemod) { return; } + float audioBW = std::min(selectedDemod->getMaxAFBandwidth(), audioSampleRate / 2.0f); + resamp.stop(); + resamp.setOutSampleRate(audioSampleRate); + win.setSampleRate(audioSampleRate * resamp.getInterpolation()); + win.setCutoff(audioBW); + win.setTransWidth(audioBW); + resamp.updateWindow(&win); + resamp.start(); + } + + static void vfoUserChangedBandwidthHandler(double newBw, void* ctx) { + RadioModule* _this = (RadioModule*)ctx; + _this->setBandwidth(newBw); + } + + static void sampleRateChangeHandler(float sampleRate, void* ctx) { + RadioModule* _this = (RadioModule*)ctx; + _this->setAudioSampleRate(sampleRate); + } + + EventHandler onUserChangedBandwidthHandler; + VFOManager::VFO* vfo; + dsp::Squelch squelch; + + dsp::filter_window::BlackmanWindow win; + dsp::PolyphaseResampler resamp; + dsp::BFMDeemp deemp; + + EventHandler srChangeHandler; + SinkManager::Stream stream; + + std::array demods; + demod::Demodulator* selectedDemod = NULL; + + double audioSampleRate = 48000.0; + double bandwidth; + double snapInterval; + float squelchLevel; + int selectedDemodID = 1; + + const double MIN_SQUELCH = -100.0; + const double MAX_SQUELCH = 0.0; + + bool enabled = true; + +}; \ No newline at end of file