diff --git a/core/src/dsp/clock_recovery/fd.h b/core/src/dsp/clock_recovery/fd.h new file mode 100644 index 00000000..01759c91 --- /dev/null +++ b/core/src/dsp/clock_recovery/fd.h @@ -0,0 +1,189 @@ +#pragma once +#include "../processor.h" +#include "../loop/phase_control_loop.h" +#include "../taps/windowed_sinc.h" +#include "../multirate/polyphase_bank.h" +#include "../math/step.h" + +namespace dsp::clock_recovery { + class FD : public Processor { + using base_type = Processor ; + public: + FD() {} + + FD(stream* in, double omega, double omegaGain, double muGain, double omegaRelLimit, int interpPhaseCount = 128, int interpTapCount = 8) { init(in, omega, omegaGain, muGain, omegaRelLimit, interpPhaseCount, interpTapCount); } + + ~FD() { + if (!base_type::_block_init) { return; } + base_type::stop(); + dsp::multirate::freePolyphaseBank(interpBank); + buffer::free(buffer); + } + + void init(stream* in, double omega, double omegaGain, double muGain, double omegaRelLimit, int interpPhaseCount = 128, int interpTapCount = 8) { + _omega = omega; + _omegaGain = omegaGain; + _muGain = muGain; + _omegaRelLimit = omegaRelLimit; + _interpPhaseCount = interpPhaseCount; + _interpTapCount = interpTapCount; + + pcl.init(_muGain, _omegaGain, 0.0, 0.0, 1.0, _omega, _omega * (1.0 - omegaRelLimit), _omega * (1.0 + omegaRelLimit)); + generateInterpTaps(); + buffer = buffer::alloc(STREAM_BUFFER_SIZE + _interpTapCount); + bufStart = &buffer[_interpTapCount - 1]; + + base_type::init(in); + } + + void setOmega(double omega) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + base_type::tempStop(); + _omega = omega; + offset = 0; + pcl.phase = 0.0f; + pcl.freq = _omega; + pcl.setFreqLimits(_omega * (1.0 - _omegaRelLimit), _omega * (1.0 + _omegaRelLimit)); + base_type::tempStart(); + } + + void setOmegaGain(double omegaGain) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + _omegaGain = omegaGain; + pcl.setCoefficients(_muGain, _omegaGain); + } + + void setMuGain(double muGain) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + _muGain = muGain; + pcl.setCoefficients(_muGain, _omegaGain); + } + + void setOmegaRelLimit(double omegaRelLimit) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + _omegaRelLimit = omegaRelLimit; + pcl.setFreqLimits(_omega * (1.0 - _omegaRelLimit), _omega * (1.0 + _omegaRelLimit)); + } + + void setInterpParams(int interpPhaseCount, int interpTapCount) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + base_type::tempStop(); + _interpPhaseCount = interpPhaseCount; + _interpTapCount = interpTapCount; + dsp::multirate::freePolyphaseBank(interpBank); + buffer::free(buffer); + generateInterpTaps(); + buffer = buffer::alloc(STREAM_BUFFER_SIZE + _interpTapCount); + bufStart = &buffer[_interpTapCount - 1]; + base_type::tempStart(); + } + + void reset() { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + base_type::tempStop(); + offset = 0; + pcl.phase = 0.0f; + pcl.freq = _omega; + base_type::tempStart(); + } + + inline int process(int count, const float* in, float* out) { + // Copy data to work buffer + memcpy(bufStart, in, count * sizeof(float)); + + // Process all samples + int outCount = 0; + while (offset < count) { + float error; + float outVal; + float dfdt; + + // Calculate new output value + int phase = std::clamp(floorf(pcl.phase * (float)_interpPhaseCount), 0, _interpPhaseCount - 1); + volk_32f_x2_dot_prod_32f(&outVal, &buffer[offset], interpBank.phases[phase], _interpTapCount); + out[outCount++] = outVal; + + // Calculate derivative of the signal + if (phase == 0) { + float fT1; + volk_32f_x2_dot_prod_32f(&fT1, &buffer[offset], interpBank.phases[phase+1], _interpTapCount); + dfdt = fT1 - outVal; + } + else if (phase == _interpPhaseCount - 1) { + float fT_1; + volk_32f_x2_dot_prod_32f(&fT_1, &buffer[offset], interpBank.phases[phase-1], _interpTapCount); + dfdt = outVal - fT_1; + } + else { + float fT_1; + float fT1; + volk_32f_x2_dot_prod_32f(&fT_1, &buffer[offset], interpBank.phases[phase-1], _interpTapCount); + volk_32f_x2_dot_prod_32f(&fT1, &buffer[offset], interpBank.phases[phase+1], _interpTapCount); + dfdt = (fT1 - fT_1) * 0.5f; + } + + // Calculate error + error = dfdt * math::step(outVal); + + // Clamp symbol phase error + if (error > 1.0f) { error = 1.0f; } + if (error < -1.0f) { error = -1.0f; } + + // Advance symbol offset and phase + pcl.advance(error); + float delta = floorf(pcl.phase); + offset += delta; + pcl.phase -= delta; + } + offset -= count; + + // Update delay buffer + memmove(buffer, &buffer[count], (_interpTapCount - 1) * sizeof(float)); + + return outCount; + } + + int run() { + int count = base_type::_in->read(); + if (count < 0) { return -1; } + + int outCount = process(count, base_type::_in->readBuf, base_type::out.writeBuf); + + // Swap if some data was generated + base_type::_in->flush(); + if (outCount) { + if (!base_type::out.swap(outCount)) { return -1; } + } + return outCount; + } + + loop::PhaseControlLoop pcl; + + protected: + void generateInterpTaps() { + double bw = 0.5 / (double)_interpPhaseCount; + dsp::tap lp = dsp::taps::windowedSinc(_interpPhaseCount * _interpTapCount, dsp::math::freqToOmega(bw, 1.0), dsp::window::nuttall, _interpPhaseCount); + interpBank = dsp::multirate::buildPolyphaseBank(_interpPhaseCount, lp); + taps::free(lp); + } + + dsp::multirate::PolyphaseBank interpBank; + + double _omega; + double _omegaGain; + double _muGain; + double _omegaRelLimit; + int _interpPhaseCount; + int _interpTapCount; + + int offset = 0; + float* buffer; + float* bufStart; + }; +} \ No newline at end of file diff --git a/core/src/dsp/clock_recovery/mm.h b/core/src/dsp/clock_recovery/mm.h index 9c84a8c8..60f78e30 100644 --- a/core/src/dsp/clock_recovery/mm.h +++ b/core/src/dsp/clock_recovery/mm.h @@ -178,7 +178,7 @@ namespace dsp::clock_recovery { } dsp::multirate::PolyphaseBank interpBank; - loop::PhaseControlLoop pcl; + loop::PhaseControlLoop pcl; double _omega; double _omegaGain; diff --git a/core/src/dsp/demod/broadcast_fm.h b/core/src/dsp/demod/broadcast_fm.h index f0269829..7eff4472 100644 --- a/core/src/dsp/demod/broadcast_fm.h +++ b/core/src/dsp/demod/broadcast_fm.h @@ -12,6 +12,7 @@ #include "../math/multiply.h" #include "../math/add.h" #include "../math/subtract.h" +#include "../multirate/rational_resampler.h" namespace dsp::demod { class BroadcastFM : public Processor { @@ -19,7 +20,7 @@ namespace dsp::demod { public: BroadcastFM() {} - BroadcastFM(stream* in, double deviation, double samplerate, bool stereo = true, bool lowPass = true) { init(in, deviation, samplerate, stereo, lowPass); } + BroadcastFM(stream* in, double deviation, double samplerate, bool stereo = true, bool lowPass = true, bool rdsOut = false) { init(in, deviation, samplerate, stereo, lowPass); } ~BroadcastFM() { if (!base_type::_block_init) { return; } @@ -31,11 +32,12 @@ namespace dsp::demod { taps::free(audioFirTaps); } - virtual void init(stream* in, double deviation, double samplerate, bool stereo = true, bool lowPass = true) { + virtual void init(stream* in, double deviation, double samplerate, bool stereo = true, bool lowPass = true, bool rdsOut = false) { _deviation = deviation; _samplerate = samplerate; _stereo = stereo; _lowPass = lowPass; + _rdsOut = rdsOut; demod.init(NULL, _deviation, _samplerate); pilotFirTaps = taps::bandPass(18750.0, 19250.0, 3000.0, _samplerate, true); @@ -47,6 +49,7 @@ namespace dsp::demod { audioFirTaps = taps::lowPass(15000.0, 4000.0, _samplerate); alFir.init(NULL, audioFirTaps); arFir.init(NULL, audioFirTaps); + rdsResamp.init(NULL, samplerate, 5000.0); lmr = buffer::alloc(STREAM_BUFFER_SIZE); l = buffer::alloc(STREAM_BUFFER_SIZE); @@ -56,6 +59,7 @@ namespace dsp::demod { lmrDelay.out.free(); arFir.out.free(); alFir.out.free(); + rdsResamp.out.free(); base_type::init(in); } @@ -88,6 +92,8 @@ namespace dsp::demod { alFir.setTaps(audioFirTaps); arFir.setTaps(audioFirTaps); + rdsResamp.setInSamplerate(samplerate); + reset(); base_type::tempStart(); } @@ -110,6 +116,15 @@ namespace dsp::demod { base_type::tempStart(); } + void setRDSOut(bool rdsOut) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + base_type::tempStop(); + _rdsOut = rdsOut; + reset(); + base_type::tempStart(); + } + void reset() { assert(base_type::_block_init); std::lock_guard lck(base_type::ctrlMtx); @@ -124,7 +139,7 @@ namespace dsp::demod { base_type::tempStart(); } - inline int process(int count, complex_t* in, stereo_t* out) { + inline int process(int count, complex_t* in, stereo_t* out, int& rdsOutCount, float* rdsout = NULL) { // Demodulate demod.process(count, in, demod.out.writeBuf); if (_stereo) { @@ -139,10 +154,19 @@ namespace dsp::demod { lprDelay.process(count, demod.out.writeBuf, demod.out.writeBuf); lmrDelay.process(count, rtoc.out.writeBuf, rtoc.out.writeBuf); - // Double and conjugate PLL output to down convert the L-R signal - math::Multiply::process(count, pilotPLL.out.writeBuf, pilotPLL.out.writeBuf, pilotPLL.out.writeBuf); + // conjugate PLL output to down convert twice the L-R signal math::Conjugate::process(count, pilotPLL.out.writeBuf, pilotPLL.out.writeBuf); math::Multiply::process(count, rtoc.out.writeBuf, pilotPLL.out.writeBuf, rtoc.out.writeBuf); + math::Multiply::process(count, rtoc.out.writeBuf, pilotPLL.out.writeBuf, rtoc.out.writeBuf); + + // Do RDS demod + if (_rdsOut) { + // Since the PLL output is no longer needed after this, use it as the output + math::Multiply::process(count, rtoc.out.writeBuf, pilotPLL.out.writeBuf, pilotPLL.out.writeBuf); + convert::ComplexToReal::process(count, pilotPLL.out.writeBuf, rdsout); + volk_32f_s32f_multiply_32f(rdsout, rdsout, 100.0, count); + rdsOutCount = rdsResamp.process(count, rdsout, rdsout); + } // Convert output back to real for further processing convert::ComplexToReal::process(count, rtoc.out.writeBuf, lmr); @@ -180,18 +204,25 @@ namespace dsp::demod { int count = base_type::_in->read(); if (count < 0) { return -1; } - process(count, base_type::_in->readBuf, base_type::out.writeBuf); + int rdsOutCount = 0; + process(count, base_type::_in->readBuf, base_type::out.writeBuf, rdsOutCount, rdsOut.writeBuf); base_type::_in->flush(); if (!base_type::out.swap(count)) { return -1; } + if (rdsOutCount && _rdsOut) { + if (!rdsOut.swap(rdsOutCount)) { return -1; } + } return count; } + stream rdsOut; + protected: double _deviation; double _samplerate; bool _stereo; - bool _lowPass = true; + bool _lowPass; + bool _rdsOut; Quadrature demod; tap pilotFirTaps; @@ -203,6 +234,7 @@ namespace dsp::demod { tap audioFirTaps; filter::FIR arFir; filter::FIR alFir; + multirate::RationalResampler rdsResamp; float* lmr; float* l; diff --git a/core/src/dsp/digital/binary_slicer.h b/core/src/dsp/digital/binary_slicer.h new file mode 100644 index 00000000..edf63944 --- /dev/null +++ b/core/src/dsp/digital/binary_slicer.h @@ -0,0 +1,31 @@ +#pragma once +#include "../processor.h" + +namespace dsp::digital { + class BinarySlicer : public Processor { + using base_type = Processor; + public: + BinarySlicer() {} + + BinarySlicer(stream *in) { base_type::init(in); } + + static inline int process(int count, const float* in, uint8_t* out) { + // TODO: Switch to volk + for (int i = 0; i < count; i++) { + out[i] = in[i] > 0.0f; + } + return count; + } + + int run() { + int count = base_type::_in->read(); + if (count < 0) { return -1; } + + process(count, base_type::_in->readBuf, base_type::out.writeBuf); + + base_type::_in->flush(); + if (!base_type::out.swap(count)) { return -1; } + return count; + } + }; +} \ No newline at end of file diff --git a/core/src/dsp/digital/differentia_decoder.h b/core/src/dsp/digital/differentia_decoder.h new file mode 100644 index 00000000..c524c9eb --- /dev/null +++ b/core/src/dsp/digital/differentia_decoder.h @@ -0,0 +1,64 @@ +#pragma once +#include "../processor.h" + +namespace dsp::digital { + class DifferentialDecoder : public Processor { + using base_type = Processor; + public: + DifferentialDecoder() {} + + DifferentialDecoder(stream *in) { base_type::init(in); } + + void init(stream *in, uint8_t modulus, uint8_t initSym = 0) { + _modulus = modulus; + _initSym = initSym; + + last = _initSym; + + base_type::init(in); + } + + void setModulus(uint8_t modulus) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + _modulus = modulus; + } + + void setInitSym(uint8_t initSym) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + _initSym = initSym; + } + + void reset() { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + base_type::tempStop(); + last = _initSym; + base_type::tempStart(); + } + + inline int process(int count, const uint8_t* in, uint8_t* out) { + for (int i = 0; i < count; i++) { + out[i] = (in[i] - last) % _modulus; + } + return count; + } + + int run() { + int count = base_type::_in->read(); + if (count < 0) { return -1; } + + process(count, base_type::_in->readBuf, base_type::out.writeBuf); + + base_type::_in->flush(); + if (!base_type::out.swap(count)) { return -1; } + return count; + } + + protected: + uint8_t last; + uint8_t _initSym; + uint8_t _modulus; + }; +} \ No newline at end of file diff --git a/core/src/dsp/digital/differential_decoder.h b/core/src/dsp/digital/differential_decoder.h new file mode 100644 index 00000000..0aa54c7e --- /dev/null +++ b/core/src/dsp/digital/differential_decoder.h @@ -0,0 +1,65 @@ +#pragma once +#include "../processor.h" + +namespace dsp::digital { + class DifferentialDecoder : public Processor { + using base_type = Processor; + public: + DifferentialDecoder() {} + + DifferentialDecoder(stream *in) { base_type::init(in); } + + void init(stream *in, uint8_t modulus, uint8_t initSym = 0) { + _modulus = modulus; + _initSym = initSym; + + last = _initSym; + + base_type::init(in); + } + + void setModulus(uint8_t modulus) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + _modulus = modulus; + } + + void setInitSym(uint8_t initSym) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + _initSym = initSym; + } + + void reset() { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + base_type::tempStop(); + last = _initSym; + base_type::tempStart(); + } + + inline int process(int count, const uint8_t* in, uint8_t* out) { + for (int i = 0; i < count; i++) { + out[i] = (in[i] - last + _modulus) % _modulus; + last = in[i]; + } + return count; + } + + int run() { + int count = base_type::_in->read(); + if (count < 0) { return -1; } + + process(count, base_type::_in->readBuf, base_type::out.writeBuf); + + base_type::_in->flush(); + if (!base_type::out.swap(count)) { return -1; } + return count; + } + + protected: + uint8_t last; + uint8_t _initSym; + uint8_t _modulus; + }; +} \ No newline at end of file diff --git a/core/src/dsp/digital/manchester_decoder.h b/core/src/dsp/digital/manchester_decoder.h new file mode 100644 index 00000000..119489af --- /dev/null +++ b/core/src/dsp/digital/manchester_decoder.h @@ -0,0 +1,47 @@ +#pragma once +#include "../processor.h" + +namespace dsp::digital { + class ManchesterDecoder : public Processor { + using base_type = Processor; + public: + ManchesterDecoder() {} + + ManchesterDecoder(stream *in) { base_type::init(in); } + + void reset() { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + base_type::tempStop(); + offset = 0; + base_type::tempStart(); + } + + inline int process(int count, const uint8_t* in, uint8_t* out) { + // TODO: NOT THIS BULLSHIT + int outCount = 0; + for (; offset < count; offset += 2) { + out[outCount++] = in[offset]; + } + offset -= count; + return outCount; + } + + int run() { + int count = base_type::_in->read(); + if (count < 0) { return -1; } + + int outCount = process(count, base_type::_in->readBuf, base_type::out.writeBuf); + + // Swap if some data was generated + base_type::_in->flush(); + if (outCount) { + if (!base_type::out.swap(outCount)) { return -1; } + } + return outCount; + } + + protected: + int offset = 0; + }; +} \ No newline at end of file diff --git a/core/src/gui/style.h b/core/src/gui/style.h index 5f75b8e0..edd79b42 100644 --- a/core/src/gui/style.h +++ b/core/src/gui/style.h @@ -1,13 +1,13 @@ #pragma once #include #include +#include namespace style { - extern ImFont* baseFont; - extern ImFont* bigFont; - extern ImFont* hugeFont; - - extern float uiScale; + SDRPP_EXPORT ImFont* baseFont; + SDRPP_EXPORT ImFont* bigFont; + SDRPP_EXPORT ImFont* hugeFont; + SDRPP_EXPORT float uiScale; bool setDefaultStyle(std::string resDir); bool loadFonts(std::string resDir); diff --git a/decoder_modules/meteor_demodulator/src/main.cpp b/decoder_modules/meteor_demodulator/src/main.cpp index 9002c286..300f3ad6 100644 --- a/decoder_modules/meteor_demodulator/src/main.cpp +++ b/decoder_modules/meteor_demodulator/src/main.cpp @@ -56,7 +56,7 @@ public: config.release(created); vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, 150000, INPUT_SAMPLE_RATE, 150000, 150000, true); - demod.init(vfo->output, 72000.0f, INPUT_SAMPLE_RATE, 33, 0.6f, 0.1f, 0.005f, (0.01 * 0.01) / 4.0, 0.01); + demod.init(vfo->output, 72000.0f, INPUT_SAMPLE_RATE, 33, 0.6f, 0.1f, 0.005f, 1e-6, 0.01); split.init(&demod.out); split.bindStream(&symSinkStream); split.bindStream(&sinkStream); diff --git a/decoder_modules/radio/src/demodulators/wfm.h b/decoder_modules/radio/src/demodulators/wfm.h index 7938586c..2761b28e 100644 --- a/decoder_modules/radio/src/demodulators/wfm.h +++ b/decoder_modules/radio/src/demodulators/wfm.h @@ -1,6 +1,15 @@ #pragma once #include "../demod.h" #include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace demod { class WFM : public Demodulator { @@ -13,12 +22,17 @@ namespace demod { ~WFM() { stop(); + gui::waterfall.onFFTRedraw.unbindHandler(&fftRedrawHandler); } void init(std::string name, ConfigManager* config, dsp::stream* input, double bandwidth, double audioSR) { this->name = name; _config = config; + fftRedrawHandler.handler = fftRedraw; + fftRedrawHandler.ctx = this; + gui::waterfall.onFFTRedraw.bindHandler(&fftRedrawHandler); + // Load config _config->acquire(); bool modified = false; @@ -28,18 +42,36 @@ namespace demod { if (config->conf[name][getName()].contains("lowPass")) { _lowPass = config->conf[name][getName()]["lowPass"]; } + if (config->conf[name][getName()].contains("rds")) { + _rds = config->conf[name][getName()]["rds"]; + } _config->release(modified); // Define structure - demod.init(input, bandwidth / 2.0f, getIFSampleRate(), _stereo, _lowPass); + demod.init(input, bandwidth / 2.0f, getIFSampleRate(), _stereo, _lowPass, _rds); + recov.init(&demod.rdsOut, 5000.0 / 2375, omegaGain, muGain, 0.01); + slice.init(&recov.out); + manch.init(&slice.out); + diff.init(&manch.out, 2); + hs.init(&diff.out, rdsHandler, this); } void start() { demod.start(); + recov.start(); + slice.start(); + manch.start(); + diff.start(); + hs.start(); } void stop() { demod.stop(); + recov.stop(); + slice.stop(); + manch.stop(); + diff.stop(); + hs.stop(); } void showMenu() { @@ -55,6 +87,21 @@ namespace demod { _config->conf[name][getName()]["lowPass"] = _lowPass; _config->release(true); } + if (ImGui::Checkbox(("Decode RDS##_radio_wfm_rds_" + name).c_str(), &_rds)) { + demod.setRDSOut(_rds); + _config->acquire(); + _config->conf[name][getName()]["rds"] = _rds; + _config->release(true); + } + + // if (_rds) { + // if (rdsDecode.countryCodeValid()) { ImGui::Text("Country code: %d", rdsDecode.getCountryCode()); } + // if (rdsDecode.programCoverageValid()) { ImGui::Text("Program coverage: %d", rdsDecode.getProgramCoverage()); } + // if (rdsDecode.programRefNumberValid()) { ImGui::Text("Reference number: %d", rdsDecode.getProgramRefNumber()); } + // if (rdsDecode.programTypeValid()) { ImGui::Text("Program type: %d", rdsDecode.getProgramType()); } + // if (rdsDecode.PSNameValid()) { ImGui::Text("Program name: [%s]", rdsDecode.getPSName().c_str()); } + // if (rdsDecode.radioTextValid()) { ImGui::Text("Radiotext: [%s]", rdsDecode.getRadioText().c_str()); } + // } } void setBandwidth(double bandwidth) { @@ -93,12 +140,68 @@ namespace demod { } private: + static void rdsHandler(uint8_t* data, int count, void* ctx) { + WFM* _this = (WFM*)ctx; + _this->rdsDecode.process(data, count); + } + + static void fftRedraw(ImGui::WaterFall::FFTRedrawArgs args, void* ctx) { + WFM* _this = (WFM*)ctx; + if (!_this->_rds) { return; } + + // Generate string depending on RDS mode + char buf[256]; + if (_this->rdsDecode.PSNameValid() && _this->rdsDecode.radioTextValid()) { + sprintf(buf, "RDS: %s - %s", _this->rdsDecode.getPSName().c_str(), _this->rdsDecode.getRadioText().c_str()); + } + else if (_this->rdsDecode.PSNameValid()) { + sprintf(buf, "RDS: %s", _this->rdsDecode.getPSName().c_str()); + } + else if (_this->rdsDecode.radioTextValid()) { + sprintf(buf, "RDS: %s", _this->rdsDecode.getRadioText().c_str()); + } + else { + return; + } + + // Calculate paddings + ImVec2 min = args.min; + min.x += 5.0f * style::uiScale; + min.y += 5.0f * style::uiScale; + ImVec2 tmin = min; + tmin.x += 5.0f * style::uiScale; + tmin.y += 5.0f * style::uiScale; + ImVec2 tmax = ImGui::CalcTextSize(buf); + tmax.x += tmin.x; + tmax.y += tmin.y; + ImVec2 max = tmax; + max.x += 5.0f * style::uiScale; + max.y += 5.0f * style::uiScale; + + // Draw back drop + args.window->DrawList->AddRectFilled(min, max, IM_COL32(0, 0, 0, 128)); + + // Draw text + args.window->DrawList->AddText(tmin, IM_COL32(255, 255, 0, 255), buf); + } + dsp::demod::BroadcastFM demod; + dsp::clock_recovery::FD recov; + dsp::digital::BinarySlicer slice; + dsp::digital::ManchesterDecoder manch; + dsp::digital::DifferentialDecoder diff; + dsp::sink::Handler hs; + EventHandler fftRedrawHandler; + + rds::RDSDecoder rdsDecode; ConfigManager* _config = NULL; bool _stereo = false; bool _lowPass = true; + bool _rds = false; + float muGain = 0.01; + float omegaGain = (0.01*0.01)/4.0; std::string name; }; diff --git a/decoder_modules/radio/src/radio_module.h b/decoder_modules/radio/src/radio_module.h index 65057d30..cfa236e6 100644 --- a/decoder_modules/radio/src/radio_module.h +++ b/decoder_modules/radio/src/radio_module.h @@ -232,16 +232,19 @@ private: } // Noise blanker - if (ImGui::Checkbox(("Noise blanker (W.I.P.)##_radio_nb_ena_" + _this->name).c_str(), &_this->nbEnabled)) { - _this->setNBEnabled(_this->nbEnabled); + if (_this->nbAllowed) { + if (ImGui::Checkbox(("Noise blanker (W.I.P.)##_radio_nb_ena_" + _this->name).c_str(), &_this->nbEnabled)) { + _this->setNBEnabled(_this->nbEnabled); + } + if (!_this->nbEnabled && _this->enabled) { style::beginDisabled(); } + ImGui::SameLine(); + ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX()); + if (ImGui::SliderFloat(("##_radio_nb_lvl_" + _this->name).c_str(), &_this->nbLevel, _this->MIN_NB, _this->MAX_NB, "%.3fdB")) { + _this->setNBLevel(_this->nbLevel); + } + if (!_this->nbEnabled && _this->enabled) { style::endDisabled(); } } - if (!_this->nbEnabled && _this->enabled) { style::beginDisabled(); } - ImGui::SameLine(); - ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX()); - if (ImGui::SliderFloat(("##_radio_nb_lvl_" + _this->name).c_str(), &_this->nbLevel, _this->MIN_NB, _this->MAX_NB, "%.3fdB")) { - _this->setNBLevel(_this->nbLevel); - } - if (!_this->nbEnabled && _this->enabled) { style::endDisabled(); } + // Squelch if (ImGui::Checkbox(("Squelch##_radio_sqelch_ena_" + _this->name).c_str(), &_this->squelchEnabled)) { @@ -422,7 +425,7 @@ private: // Configure noise blanker nb.setRate(500.0 / ifSamplerate); setNBLevel(nbLevel); - setNBEnabled(nbEnabled); + setNBEnabled(nbAllowed&& nbEnabled); // Configure FM IF Noise Reduction setIFNRPreset((selectedDemodID == RADIO_DEMOD_NFM) ? ifnrPresets[fmIFPresetId] : IFNR_PRESET_BROADCAST); diff --git a/decoder_modules/radio/src/rds.cpp b/decoder_modules/radio/src/rds.cpp new file mode 100644 index 00000000..c947e97c --- /dev/null +++ b/decoder_modules/radio/src/rds.cpp @@ -0,0 +1,251 @@ +#include "rds.h" +#include +#include +#include + +namespace rds { + std::map SYNDROMES = { + { 0b1111011000, BLOCK_TYPE_A }, + { 0b1111010100, BLOCK_TYPE_B }, + { 0b1001011100, BLOCK_TYPE_C }, + { 0b1111001100, BLOCK_TYPE_CP }, + { 0b1001011000, BLOCK_TYPE_D } + }; + + std::map OFFSETS = { + { BLOCK_TYPE_A, 0b0011111100 }, + { BLOCK_TYPE_B, 0b0110011000 }, + { BLOCK_TYPE_C, 0b0101101000 }, + { BLOCK_TYPE_CP, 0b1101010000 }, + { BLOCK_TYPE_D, 0b0110110100 } + }; + + // This parity check matrix is given in annex B2.1 of the specificiation + const uint16_t PARITY_CHECK_MAT[] = { + 0b1000000000, + 0b0100000000, + 0b0010000000, + 0b0001000000, + 0b0000100000, + 0b0000010000, + 0b0000001000, + 0b0000000100, + 0b0000000010, + 0b0000000001, + 0b1011011100, + 0b0101101110, + 0b0010110111, + 0b1010000111, + 0b1110011111, + 0b1100010011, + 0b1101010101, + 0b1101110110, + 0b0110111011, + 0b1000000001, + 0b1111011100, + 0b0111101110, + 0b0011110111, + 0b1010100111, + 0b1110001111, + 0b1100011011 + }; + + const int BLOCK_LEN = 26; + const int DATA_LEN = 16; + const int POLY_LEN = 10; + const uint16_t LFSR_POLY = 0x5B9; + const uint16_t IN_POLY = 0x31B; + + void RDSDecoder::process(uint8_t* symbols, int count) { + for (int i = 0; i < count; i++) { + // Shift in the bit + shiftReg = ((shiftReg << 1) & 0x3FFFFFF) | (symbols[i] & 1); + + // Skip if we need to shift in new data + if (--skip > 0) { + continue; + } + + // Calculate the syndrome and update sync status + uint16_t syn = calcSyndrome(shiftReg); + auto synIt = SYNDROMES.find(syn); + bool knownSyndrome = synIt != SYNDROMES.end(); + sync = std::clamp(knownSyndrome ? ++sync : --sync, 0, 4); + + // if (knownSyndrome) { + // printf("Found known syn: %04X\n", syn); + // } + + // If we're still no longer in sync, try to resync + if (!sync) { continue; } + + // Figure out which block we've got + BlockType type; + if (knownSyndrome) { + type = SYNDROMES[syn]; + } + else { + type = (BlockType)((lastType + 1) % _BLOCK_TYPE_COUNT); + } + + // Save block while correcting errors (NOT YET) + blocks[type] = shiftReg;//correctErrors(shiftReg, type); + + //printf("Block type: %d, Sync: %d, KnownSyndrome: %s, contGroup: %d, offset: %d\n", type, sync, knownSyndrome ? "yes" : "no", contGroup, i); + + // Update continous group count + if (type == BLOCK_TYPE_A) { contGroup = 1; } + else if (type == BLOCK_TYPE_B && lastType == BLOCK_TYPE_A) { contGroup++; } + else if ((type == BLOCK_TYPE_C || type == BLOCK_TYPE_CP) && lastType == BLOCK_TYPE_B) { contGroup++; } + else if (type == BLOCK_TYPE_D && (lastType == BLOCK_TYPE_C || lastType == BLOCK_TYPE_CP)) { contGroup++; } + else { contGroup = 0; } + + // If we've got an entire group, process it + if (contGroup >= 4) { + contGroup = 0; + decodeGroup(); + } + + // // Remember the last block type and skip to new block + lastType = type; + skip = BLOCK_LEN; + } + } + + uint16_t RDSDecoder::calcSyndrome(uint32_t block) { + // Perform vector/matrix dot product between block and parity matrix + uint16_t syn = 0; + for(int i = 0; i < BLOCK_LEN; i++) { + syn ^= PARITY_CHECK_MAT[BLOCK_LEN - 1 - i] * ((block >> i) & 1); + } + return syn; + } + + uint32_t RDSDecoder::correctErrors(uint32_t block, BlockType type) { + // Init the syndrome and output + uint16_t syn = 0; + uint32_t out = block; + + // Subtract the offset from block + block ^= (uint32_t)OFFSETS[type]; + + // Feed in the data + for (int i = BLOCK_LEN - 1; i >= 0; i--) { + // Shift the syndrome and keep the output + uint8_t outBit = (syn >> (POLY_LEN - 1)) & 1; + syn = (syn << 1) & 0b1111111111; + + // Apply LFSR polynomial + syn ^= LFSR_POLY * outBit; + + // Apply input polynomial. + syn ^= IN_POLY * ((block >> i) & 1); + } + + // Use the syndrome register to do error correction + // TODO: For some reason there's always zeros in the syn when starting + uint8_t errorFound = 0; + for (int i = DATA_LEN - 1; i >= 0; i--) { + // Check if the 5 leftmost bits are all zero + errorFound |= !(syn & 0b11111); + + // Write output + uint8_t outBit = (syn >> (POLY_LEN - 1)) & 1; + out ^= (errorFound & outBit) << (i + POLY_LEN); + + // Shift syndrome + syn = (syn << 1) & 0b1111111111; + syn ^= LFSR_POLY * outBit * !errorFound; + } + + // TODO: mark block as irrecoverable if too damaged + + if (errorFound) { + printf("Error found\n"); + } + + return out; + } + + void RDSDecoder::decodeGroup() { + std::lock_guard lck(groupMtx); + auto now = std::chrono::high_resolution_clock::now(); + anyGroupLastUpdate = now; + + // Decode PI code + countryCode = (blocks[BLOCK_TYPE_A] >> 22) & 0xF; + programCoverage = (AreaCoverage)((blocks[BLOCK_TYPE_A] >> 18) & 0xF); + programRefNumber = (blocks[BLOCK_TYPE_A] >> 10) & 0xFF; + + // Decode group type and version + uint8_t groupType = (blocks[BLOCK_TYPE_B] >> 22) & 0xF; + GroupVersion groupVer = (GroupVersion)((blocks[BLOCK_TYPE_B] >> 21) & 1); + + // Decode traffic program and program type + trafficProgram = (blocks[BLOCK_TYPE_B] >> 20) & 1; + programType = (ProgramType)((blocks[BLOCK_TYPE_B] >> 15) & 0x1F); + + if (groupType == 0) { + group0LastUpdate = now; + trafficAnnouncement = (blocks[BLOCK_TYPE_B] >> 14) & 1; + music = (blocks[BLOCK_TYPE_B] >> 13) & 1; + uint8_t diBit = (blocks[BLOCK_TYPE_B] >> 12) & 1; + uint8_t offset = ((blocks[BLOCK_TYPE_B] >> 10) & 0b11); + uint8_t diOffset = 3 - offset; + uint8_t psOffset = offset * 2; + + if (groupVer == GROUP_VER_A) { + alternateFrequency = (blocks[BLOCK_TYPE_C] >> 10) & 0xFFFF; + } + + // Write DI bit to the decoder identification + decoderIdent &= ~(1 << diOffset); + decoderIdent |= (diBit << diOffset); + + // Write chars at offset the PSName + programServiceName[psOffset] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF; + programServiceName[psOffset + 1] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF; + } + else if (groupType == 2) { + group2LastUpdate = now; + // Get char offset and write chars in the Radiotext + bool nAB = (blocks[BLOCK_TYPE_B] >> 14) & 1; + uint8_t offset = (blocks[BLOCK_TYPE_B] >> 10) & 0xF; + + // Clear text field if the A/B flag changed + if (nAB != rtAB) { + radioText = " "; + } + rtAB = nAB; + + // Write char at offset in Radiotext + if (groupVer == GROUP_VER_A) { + uint8_t rtOffset = offset * 4; + radioText[rtOffset] = (blocks[BLOCK_TYPE_C] >> 18) & 0xFF; + radioText[rtOffset + 1] = (blocks[BLOCK_TYPE_C] >> 10) & 0xFF; + radioText[rtOffset + 2] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF; + radioText[rtOffset + 3] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF; + } + else { + uint8_t rtOffset = offset * 2; + radioText[rtOffset] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF; + radioText[rtOffset + 1] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF; + } + } + } + + bool RDSDecoder::anyGroupValid() { + auto now = std::chrono::high_resolution_clock::now(); + return (std::chrono::duration_cast(now - anyGroupLastUpdate)).count() < 5000.0; + } + + bool RDSDecoder::group0Valid() { + auto now = std::chrono::high_resolution_clock::now(); + return (std::chrono::duration_cast(now - group0LastUpdate)).count() < 5000.0; + } + + bool RDSDecoder::group2Valid() { + auto now = std::chrono::high_resolution_clock::now(); + return (std::chrono::duration_cast(now - group2LastUpdate)).count() < 5000.0; + } +} \ No newline at end of file diff --git a/decoder_modules/radio/src/rds.h b/decoder_modules/radio/src/rds.h new file mode 100644 index 00000000..4c775b24 --- /dev/null +++ b/decoder_modules/radio/src/rds.h @@ -0,0 +1,179 @@ +#pragma once +#include +#include +#include +#include + +namespace rds { + enum BlockType { + BLOCK_TYPE_A, + BLOCK_TYPE_B, + BLOCK_TYPE_C, + BLOCK_TYPE_CP, + BLOCK_TYPE_D, + _BLOCK_TYPE_COUNT + }; + + enum GroupVersion { + GROUP_VER_A, + GROUP_VER_B + }; + + enum AreaCoverage { + AREA_COVERAGE_LOCAL, + AREA_COVERAGE_INTERNATIONAL, + AREA_COVERAGE_NATIONAL, + AREA_COVERAGE_SUPRA_NATIONAL, + AREA_COVERAGE_REGIONAL1, + AREA_COVERAGE_REGIONAL2, + AREA_COVERAGE_REGIONAL3, + AREA_COVERAGE_REGIONAL4, + AREA_COVERAGE_REGIONAL5, + AREA_COVERAGE_REGIONAL6, + AREA_COVERAGE_REGIONAL7, + AREA_COVERAGE_REGIONAL8, + AREA_COVERAGE_REGIONAL9, + AREA_COVERAGE_REGIONAL10, + AREA_COVERAGE_REGIONAL11, + AREA_COVERAGE_REGIONAL12 + }; + + enum ProgramType { + // US Types + PROGRAM_TYPE_US_NONE = 0, + PROGRAM_TYPE_US_NEWS = 1, + PROGRAM_TYPE_US_INFOMATION = 2, + PROGRAM_TYPE_US_SPORTS = 3, + PROGRAM_TYPE_US_TALK = 4, + PROGRAM_TYPE_US_ROCK = 5, + PROGRAM_TYPE_US_CLASSIC_ROCK = 6, + PROGRAM_TYPE_US_ADULT_HITS = 7, + PROGRAM_TYPE_US_SOFT_ROCK = 8, + PROGRAM_TYPE_US_TOP_40 = 9, + PROGRAM_TYPE_US_COUNTRY = 10, + PROGRAM_TYPE_US_OLDIES = 11, + PROGRAM_TYPE_US_SOFT = 12, + PROGRAM_TYPE_US_NOSTALGIA = 13, + PROGRAM_TYPE_US_JAZZ = 14, + PROGRAM_TYPE_US_CLASSICAL = 15, + PROGRAM_TYPE_US_RHYTHM_AND_BLUES = 16, + PROGRAM_TYPE_US_SOFT_RHYTHM_AND_BLUES = 17, + PROGRAM_TYPE_US_FOREIGN_LANGUAGE = 18, + PROGRAM_TYPE_US_RELIGIOUS_MUSIC = 19, + PROGRAM_TYPE_US_RELIGIOUS_TALK = 20, + PROGRAM_TYPE_US_PERSONALITY = 21, + PROGRAM_TYPE_US_PUBLIC = 22, + PROGRAM_TYPE_US_COLLEGE = 23, + PROGRAM_TYPE_US_UNASSIGNED0 = 24, + PROGRAM_TYPE_US_UNASSIGNED1 = 25, + PROGRAM_TYPE_US_UNASSIGNED2 = 26, + PROGRAM_TYPE_US_UNASSIGNED3 = 27, + PROGRAM_TYPE_US_UNASSIGNED4 = 28, + PROGRAM_TYPE_US_WEATHER = 29, + PROGRAM_TYPE_US_EMERGENCY_TEST = 30, + PROGRAM_TYPE_US_EMERGENCY = 31, + + // EU Types + PROGRAM_TYPE_EU_NONE = 0, + PROGRAM_TYPE_EU_NEWS = 1, + PROGRAM_TYPE_EU_CURRENT_AFFAIRS = 2, + PROGRAM_TYPE_EU_INFORMATION = 3, + PROGRAM_TYPE_EU_SPORTS = 4, + PROGRAM_TYPE_EU_EDUCATION = 5, + PROGRAM_TYPE_EU_DRAMA = 6, + PROGRAM_TYPE_EU_CULTURE = 7, + PROGRAM_TYPE_EU_SCIENCE = 8, + PROGRAM_TYPE_EU_VARIED = 9, + PROGRAM_TYPE_EU_POP_MUSIC = 10, + PROGRAM_TYPE_EU_ROCK_MUSIC = 11, + PROGRAM_TYPE_EU_EASY_LISTENING_MUSIC = 12, + PROGRAM_TYPE_EU_LIGHT_CLASSICAL = 13, + PROGRAM_TYPE_EU_SERIOUS_CLASSICAL = 14, + PROGRAM_TYPE_EU_OTHER_MUSIC = 15, + PROGRAM_TYPE_EU_WEATHER = 16, + PROGRAM_TYPE_EU_FINANCE = 17, + PROGRAM_TYPE_EU_CHILDRENS_PROGRAM = 18, + PROGRAM_TYPE_EU_SOCIAL_AFFAIRS = 19, + PROGRAM_TYPE_EU_RELIGION = 20, + PROGRAM_TYPE_EU_PHONE_IN = 21, + PROGRAM_TYPE_EU_TRAVEL = 22, + PROGRAM_TYPE_EU_LEISURE = 23, + PROGRAM_TYPE_EU_JAZZ_MUSIC = 24, + PROGRAM_TYPE_EU_COUNTRY_MUSIC = 25, + PROGRAM_TYPE_EU_NATIONAL_MUSIC = 26, + PROGRAM_TYPE_EU_OLDIES_MUSIC = 27, + PROGRAM_TYPE_EU_FOLK_MUSIC = 28, + PROGRAM_TYPE_EU_DOCUMENTARY = 29, + PROGRAM_TYPE_EU_ALARM_TEST = 30, + PROGRAM_TYPE_EU_ALARM = 31 + }; + + enum DecoderIdentification { + DECODER_IDENT_STEREO = (1 << 0), + DECODER_IDENT_ARTIFICIAL_HEAD = (1 << 1), + DECODER_IDENT_COMPRESSED = (1 << 2), + DECODER_IDENT_VARIABLE_PTY = (1 << 0) + }; + + class RDSDecoder { + public: + void process(uint8_t* symbols, int count); + + bool countryCodeValid() { std::lock_guard lck(groupMtx); return anyGroupValid(); } + uint8_t getCountryCode() { std::lock_guard lck(groupMtx); return countryCode; } + bool programCoverageValid() { std::lock_guard lck(groupMtx); return anyGroupValid(); } + uint8_t getProgramCoverage() { std::lock_guard lck(groupMtx); return programCoverage; } + bool programRefNumberValid() { std::lock_guard lck(groupMtx); return anyGroupValid(); } + uint8_t getProgramRefNumber() { std::lock_guard lck(groupMtx); return programRefNumber; } + bool programTypeValid() { std::lock_guard lck(groupMtx); return anyGroupValid(); } + ProgramType getProgramType() { std::lock_guard lck(groupMtx); return programType; } + + bool musicValid() { std::lock_guard lck(groupMtx); return group0Valid(); } + bool getMusic() { std::lock_guard lck(groupMtx); return music; } + bool PSNameValid() { std::lock_guard lck(groupMtx); return group0Valid(); } + std::string getPSName() { std::lock_guard lck(groupMtx); return programServiceName; } + + bool radioTextValid() { std::lock_guard lck(groupMtx); return group2Valid(); } + std::string getRadioText() { std::lock_guard lck(groupMtx); return radioText; } + + private: + static uint16_t calcSyndrome(uint32_t block); + static uint32_t correctErrors(uint32_t block, BlockType type); + void decodeGroup(); + + bool anyGroupValid(); + bool group0Valid(); + bool group2Valid(); + + // State machine + uint32_t shiftReg = 0; + int sync = 0; + int skip = 0; + BlockType lastType = BLOCK_TYPE_A; + int contGroup = 0; + uint32_t blocks[_BLOCK_TYPE_COUNT]; + + // All groups + std::mutex groupMtx; + std::chrono::steady_clock::time_point anyGroupLastUpdate; + uint8_t countryCode; + AreaCoverage programCoverage; + uint8_t programRefNumber; + bool trafficProgram; + ProgramType programType; + + // Group type 0 + std::chrono::steady_clock::time_point group0LastUpdate; + bool trafficAnnouncement; + bool music; + uint8_t decoderIdent; + uint16_t alternateFrequency; + std::string programServiceName = " "; + + // Group type 2 + std::chrono::steady_clock::time_point group2LastUpdate; + bool rtAB = false; + std::string radioText = " "; + + }; +} \ No newline at end of file diff --git a/make_windows_package.ps1 b/make_windows_package.ps1 index 6ab11b80..f6de9490 100644 --- a/make_windows_package.ps1 +++ b/make_windows_package.ps1 @@ -56,10 +56,10 @@ cp $build_dir/sink_modules/network_sink/Release/network_sink.dll sdrpp_windows_x # Copy decoder modules -# cp $build_dir/decoder_modules/m17_decoder/Release/m17_decoder.dll sdrpp_windows_x64/modules/ -# cp "C:/Program Files/codec2/lib/libcodec2.dll" sdrpp_windows_x64/ +cp $build_dir/decoder_modules/m17_decoder/Release/m17_decoder.dll sdrpp_windows_x64/modules/ +cp "C:/Program Files/codec2/lib/libcodec2.dll" sdrpp_windows_x64/ -# cp $build_dir/decoder_modules/meteor_demodulator/Release/meteor_demodulator.dll sdrpp_windows_x64/modules/ +cp $build_dir/decoder_modules/meteor_demodulator/Release/meteor_demodulator.dll sdrpp_windows_x64/modules/ cp $build_dir/decoder_modules/radio/Release/radio.dll sdrpp_windows_x64/modules/