From 34942340a380292b24360bdd6fab897dcbd834fa Mon Sep 17 00:00:00 2001 From: f4exb Date: Sat, 12 Sep 2015 16:34:57 +0200 Subject: [PATCH] NFM demod: back to the basics --- Readme.md | 2 +- include-gpl/dsp/afsquelch.h | 45 ++++---- include-gpl/dsp/agc.h | 25 +---- plugins/channel/am/amdemod.cpp | 1 - plugins/channel/nfm/nfmdemod.cpp | 35 +++--- plugins/channel/nfm/nfmdemod.h | 3 +- plugins/channel/nfm/nfmdemodgui.ui | 2 +- sdrbase/dsp/afsquelch.cpp | 166 +++++++++++++++-------------- sdrbase/dsp/agc.cpp | 49 ++------- 9 files changed, 131 insertions(+), 197 deletions(-) diff --git a/Readme.md b/Readme.md index 5277342d5..65b0e549a 100644 --- a/Readme.md +++ b/Readme.md @@ -168,5 +168,5 @@ Assuming Debian Jessie is used: - Tx support with the BladeRF - Enhance WFM (stereo, RDS?) - Even more demods ... - - Support for Airspy + - Support for Hack-RF diff --git a/include-gpl/dsp/afsquelch.h b/include-gpl/dsp/afsquelch.h index a6d588e06..3989f9688 100644 --- a/include-gpl/dsp/afsquelch.h +++ b/include-gpl/dsp/afsquelch.h @@ -27,7 +27,10 @@ public: // Constructors and Destructor AFSquelch(); // allows user defined tone pair - AFSquelch(unsigned int nbTones, const Real *tones); + AFSquelch(unsigned int nbTones, + const Real *tones, + int samplesAttack = 0, + int samplesDecay = 0); virtual ~AFSquelch(); // setup the basic parameters and coefficients @@ -39,7 +42,7 @@ public: // set the detection threshold void setThreshold(double _threshold) { - threshold = _threshold; + m_threshold = _threshold; } // analyze a sample set and optionally filter @@ -49,11 +52,11 @@ public: // get the tone set const Real *getToneSet() const { - return toneSet; + return m_toneSet; } bool open() const { - return isOpen; + return m_isOpen; } void reset(); // reset the analysis algorithm @@ -64,23 +67,23 @@ protected: void evaluate(); private: - int N; - int sampleRate; - int samplesProcessed; - int maxPowerIndex; - int nTones; - int samplesAttack; - int attackCount; - int samplesDecay; - int decayCount; - bool isOpen; - double threshold; - double *k; - double *coef; - Real *toneSet; - double *u0; - double *u1; - double *power; + int m_N; + int m_sampleRate; + int m_samplesProcessed; + int m_maxPowerIndex; + int m_nTones; + int m_samplesAttack; + int m_attackCount; + int m_samplesDecay; + int m_decayCount; + bool m_isOpen; + double m_threshold; + double *m_k; + double *m_coef; + Real *m_toneSet; + double *m_u0; + double *m_u1; + double *m_power; }; diff --git a/include-gpl/dsp/agc.h b/include-gpl/dsp/agc.h index 2fe4175cb..2877d5ac0 100644 --- a/include-gpl/dsp/agc.h +++ b/include-gpl/dsp/agc.h @@ -20,11 +20,8 @@ public: void resize(int historySize, Real R); Real getValue(); - Real getDelayedValue(); + Real getAverage(); virtual void feed(Complex& ci) = 0; - virtual Real returnedDelayedValue() const = 0; - void openedSquelch(); - void closedSquelch(); protected: Real m_u0; @@ -32,7 +29,6 @@ protected: MovingAverage m_moving_average; // Averaging engine. The stack length conditions the smoothness of AGC. int m_historySize; int m_count; - static const int m_mult = 4; // squelch delay multiplicator }; class MagSquaredAGC : public AGC @@ -42,7 +38,6 @@ public: MagSquaredAGC(int historySize, Real R); virtual ~MagSquaredAGC(); virtual void feed(Complex& ci); - virtual Real returnedDelayedValue() const { return m_u0; } }; class MagAGC : public AGC @@ -52,7 +47,6 @@ public: MagAGC(int historySize, Real R); virtual ~MagAGC(); virtual void feed(Complex& ci); - virtual Real returnedDelayedValue() const { return m_u0; } }; class AlphaAGC : public AGC @@ -64,9 +58,6 @@ public: virtual ~AlphaAGC(); void resize(int historySize, Real R, Real alpha); virtual void feed(Complex& ci); - virtual Real returnedDelayedValue() const { return 1; } - void openedSquelch(); - void closedSquelch(); private: Real m_alpha; bool m_squelchOpen; @@ -118,20 +109,6 @@ public: } } - void openedSquelch() - { - m_squelchOpen = true; - } - - void closedSquelch() - { - if (m_squelchOpen) - { - //m_moving_average.fill(m_fill); // Valgrind optim - m_squelchOpen = false; - } - } - private: bool m_squelchOpen; // open for processing Real m_fill; // refill average at this level diff --git a/plugins/channel/am/amdemod.cpp b/plugins/channel/am/amdemod.cpp index ceab0d7a4..6d4bdad4b 100644 --- a/plugins/channel/am/amdemod.cpp +++ b/plugins/channel/am/amdemod.cpp @@ -113,7 +113,6 @@ void AMDemod::feed(const SampleVector::const_iterator& begin, const SampleVector } else { - m_volumeAGC.closedSquelch(); sample = 0; } diff --git a/plugins/channel/nfm/nfmdemod.cpp b/plugins/channel/nfm/nfmdemod.cpp index 596c56682..2fcc838e5 100644 --- a/plugins/channel/nfm/nfmdemod.cpp +++ b/plugins/channel/nfm/nfmdemod.cpp @@ -26,14 +26,13 @@ #include "dsp/pidcontroller.h" #include "dsp/dspengine.h" -static const Real afSqTones[2] = {1200.0, 6000.0}; // {1200.0, 8000.0}; +static const Real afSqTones[2] = {1200.0, 8000.0}; // {1200.0, 8000.0}; MESSAGE_CLASS_DEFINITION(NFMDemod::MsgConfigureNFMDemod, Message) NFMDemod::NFMDemod() : m_ctcssIndex(0), m_sampleCount(0), - m_afSquelch(2, afSqTones), m_squelchOpen(false), m_audioFifo(4, 48000), m_settingsMutex(QMutex::Recursive) @@ -54,14 +53,11 @@ NFMDemod::NFMDemod() : m_audioBuffer.resize(1<<14); m_audioBufferFill = 0; - m_movingAverage.resize(16, 0); - m_agcLevel = 0.0625; // 0.003 - //m_AGC.resize(480, m_agcLevel, 0, 0.1*m_agcLevel); - m_AGC.resize(600, m_agcLevel*m_agcLevel); //, 0.3); + m_movingAverage.resize(240, 0); + m_agcLevel = 1.0; + m_AGC.resize(240, m_agcLevel); m_ctcssDetector.setCoefficients(3000, 6000.0); // 0.5s / 2 Hz resolution - m_afSquelch.setCoefficients(24, 48000.0, 5, 1); // 4000 Hz span, 250us - m_afSquelch.setThreshold(0.001); DSPEngine::instance()->addAudioSink(&m_audioFifo); } @@ -165,7 +161,7 @@ void NFMDemod::feed(const SampleVector::const_iterator& begin, const SampleVecto Real qp = ci.imag() - m_m2Sample.imag(); Real h1 = m_m1Sample.real() * qp; Real h2 = m_m1Sample.imag() * ip; - Real demod = (h1 - h2) * 2; // 10000 (multiply by 2^16 after demod) + Real demod = (h1 - h2) * 1; // 10000 (multiply by 2^16 after demod) m_m2Sample = m_m1Sample; m_m1Sample = ci; @@ -173,12 +169,7 @@ void NFMDemod::feed(const SampleVector::const_iterator& begin, const SampleVecto // AF processing - if(m_afSquelch.analyze(&demod)) - { - m_squelchOpen = m_afSquelch.open(); - } - - if (m_squelchOpen) + if (m_AGC.getAverage() > m_squelchLevel) { if (m_running.m_ctcssOn) { @@ -218,10 +209,8 @@ void NFMDemod::feed(const SampleVector::const_iterator& begin, const SampleVecto { demod = m_bandpass.filter(demod); demod *= m_running.m_volume; - sample = demod * ((1<<18)/301) * m_AGC.getDelayedValue(); // denominator = bandpass filter number of taps + sample = demod * 4; // denominator = bandpass filter number of taps } - - m_AGC.openedSquelch(); } else { @@ -231,7 +220,6 @@ void NFMDemod::feed(const SampleVector::const_iterator& begin, const SampleVecto m_ctcssIndex = 0; } - m_AGC.closedSquelch(); sample = 0; } @@ -354,10 +342,11 @@ void NFMDemod::apply() if (m_config.m_squelch != m_running.m_squelch) { - m_squelchLevel = pow(10.0, m_config.m_squelch / 10.0); - m_squelchLevel *= m_squelchLevel; - m_afSquelch.setThreshold(m_squelchLevel); - m_afSquelch.reset(); + // input is a power level in dB + // m_squelchLevel = pow(10.0, m_config.m_squelch / 10.0); + m_squelchLevel = pow(10.0, m_config.m_squelch / 20.0); // to magnitude + + //m_squelchLevel *= m_squelchLevel; } m_running.m_inputSampleRate = m_config.m_inputSampleRate; diff --git a/plugins/channel/nfm/nfmdemod.h b/plugins/channel/nfm/nfmdemod.h index b7eff886b..4fa26f632 100644 --- a/plugins/channel/nfm/nfmdemod.h +++ b/plugins/channel/nfm/nfmdemod.h @@ -156,14 +156,13 @@ private: double m_squelchLevel; //int m_squelchState; - AFSquelch m_afSquelch; bool m_squelchOpen; Real m_lastArgument; Complex m_m1Sample; Complex m_m2Sample; MovingAverage m_movingAverage; - MagSquaredAGC m_AGC; + MagAGC m_AGC; Real m_agcLevel; // AGC will aim to this level Real m_agcFloor; // AGC will not go below this level diff --git a/plugins/channel/nfm/nfmdemodgui.ui b/plugins/channel/nfm/nfmdemodgui.ui index d40cd3dce..739c06707 100644 --- a/plugins/channel/nfm/nfmdemodgui.ui +++ b/plugins/channel/nfm/nfmdemodgui.ui @@ -131,7 +131,7 @@ - -60 + -100 0 diff --git a/sdrbase/dsp/afsquelch.cpp b/sdrbase/dsp/afsquelch.cpp index 91568f2ec..2087d435d 100644 --- a/sdrbase/dsp/afsquelch.cpp +++ b/sdrbase/dsp/afsquelch.cpp @@ -18,73 +18,73 @@ #include "dsp/afsquelch.h" AFSquelch::AFSquelch() : - N(0), - sampleRate(0), - samplesProcessed(0), - maxPowerIndex(0), - nTones(2), - samplesAttack(0), - attackCount(0), - samplesDecay(0), - decayCount(0), - isOpen(false), - threshold(0.0) + m_N(0), + m_sampleRate(0), + m_samplesProcessed(0), + m_maxPowerIndex(0), + m_nTones(2), + m_samplesAttack(0), + m_attackCount(0), + m_samplesDecay(0), + m_decayCount(0), + m_isOpen(false), + m_threshold(0.0) { - k = new double[nTones]; - coef = new double[nTones]; - toneSet = new Real[nTones]; - u0 = new double[nTones]; - u1 = new double[nTones]; - power = new double[nTones]; + m_k = new double[m_nTones]; + m_coef = new double[m_nTones]; + m_toneSet = new Real[m_nTones]; + m_u0 = new double[m_nTones]; + m_u1 = new double[m_nTones]; + m_power = new double[m_nTones]; - toneSet[0] = 2000.0; - toneSet[1] = 10000.0; + m_toneSet[0] = 2000.0; + m_toneSet[1] = 10000.0; } -AFSquelch::AFSquelch(unsigned int nbTones, const Real *tones) : - N(0), - sampleRate(0), - samplesProcessed(0), - maxPowerIndex(0), - nTones(nbTones), - samplesAttack(0), - attackCount(0), - samplesDecay(0), - decayCount(0), - isOpen(false), - threshold(0.0) +AFSquelch::AFSquelch(unsigned int nbTones, const Real *tones, int samplesAttack, int samplesDecay) : + m_N(0), + m_sampleRate(0), + m_samplesProcessed(0), + m_maxPowerIndex(0), + m_nTones(nbTones), + m_samplesAttack(samplesAttack), + m_attackCount(0), + m_samplesDecay(samplesDecay), + m_decayCount(0), + m_isOpen(false), + m_threshold(0.0) { - k = new double[nTones]; - coef = new double[nTones]; - toneSet = new Real[nTones]; - u0 = new double[nTones]; - u1 = new double[nTones]; - power = new double[nTones]; + m_k = new double[m_nTones]; + m_coef = new double[m_nTones]; + m_toneSet = new Real[m_nTones]; + m_u0 = new double[m_nTones]; + m_u1 = new double[m_nTones]; + m_power = new double[m_nTones]; - for (int j = 0; j < nTones; ++j) + for (int j = 0; j < m_nTones; ++j) { - toneSet[j] = tones[j]; + m_toneSet[j] = tones[j]; } } AFSquelch::~AFSquelch() { - delete[] k; - delete[] coef; - delete[] toneSet; - delete[] u0; - delete[] u1; - delete[] power; + delete[] m_k; + delete[] m_coef; + delete[] m_toneSet; + delete[] m_u0; + delete[] m_u1; + delete[] m_power; } void AFSquelch::setCoefficients(int _N, int _samplerate, int _samplesAttack, int _samplesDecay ) { - N = _N; // save the basic parameters for use during analysis - sampleRate = _samplerate; - samplesAttack = _samplesAttack; - samplesDecay = _samplesDecay; + m_N = _N; // save the basic parameters for use during analysis + m_sampleRate = _samplerate; + m_samplesAttack = _samplesAttack; + m_samplesDecay = _samplesDecay; // for each of the frequencies (tones) of interest calculate // k and the associated filter coefficient as per the Goertzel @@ -93,10 +93,10 @@ void AFSquelch::setCoefficients(int _N, int _samplerate, int _samplesAttack, int // for later display. The tone set is specified in the // constructor. Notice that the resulting coefficients are // independent of N. - for (int j = 0; j < nTones; ++j) + for (int j = 0; j < m_nTones; ++j) { - k[j] = ((double)N * toneSet[j]) / (double)sampleRate; - coef[j] = 2.0 * cos((2.0 * M_PI * toneSet[j])/(double)sampleRate); + m_k[j] = ((double)m_N * m_toneSet[j]) / (double)m_sampleRate; + m_coef[j] = 2.0 * cos((2.0 * M_PI * m_toneSet[j])/(double)m_sampleRate); } } @@ -106,12 +106,12 @@ bool AFSquelch::analyze(Real *sample) { feedback(*sample); // Goertzel feedback - samplesProcessed += 1; + m_samplesProcessed += 1; - if (samplesProcessed == N) // completed a block of N + if (m_samplesProcessed == m_N) // completed a block of N { feedForward(); // calculate the power at each tone - samplesProcessed = 0; + m_samplesProcessed = 0; return true; // have a result } else @@ -126,21 +126,21 @@ void AFSquelch::feedback(Real in) double t; // feedback for each tone - for (int j = 0; j < nTones; ++j) + for (int j = 0; j < m_nTones; ++j) { - t = u0[j]; - u0[j] = in + (coef[j] * u0[j]) - u1[j]; - u1[j] = t; + t = m_u0[j]; + m_u0[j] = in + (m_coef[j] * m_u0[j]) - m_u1[j]; + m_u1[j] = t; } } void AFSquelch::feedForward() { - for (int j = 0; j < nTones; ++j) + for (int j = 0; j < m_nTones; ++j) { - power[j] = (u0[j] * u0[j]) + (u1[j] * u1[j]) - (coef[j] * u0[j] * u1[j]); - u0[j] = u1[j] = 0.0; // reset for next block. + m_power[j] = (m_u0[j] * m_u0[j]) + (m_u1[j] * m_u1[j]) - (m_coef[j] * m_u0[j] * m_u1[j]); + m_u0[j] = m_u1[j] = 0.0; // reset for next block. } evaluate(); @@ -149,14 +149,14 @@ void AFSquelch::feedForward() void AFSquelch::reset() { - for (int j = 0; j < nTones; ++j) + for (int j = 0; j < m_nTones; ++j) { - power[j] = u0[j] = u1[j] = 0.0; // reset + m_power[j] = m_u0[j] = m_u1[j] = 0.0; // reset } - samplesProcessed = 0; - maxPowerIndex = 0; - isOpen = false; + m_samplesProcessed = 0; + m_maxPowerIndex = 0; + m_isOpen = false; } @@ -166,49 +166,51 @@ void AFSquelch::evaluate() double minPower; int minIndex = 0, maxIndex = 0; - for (int j = 0; j < nTones; ++j) + for (int j = 0; j < m_nTones; ++j) { - if (power[j] > maxPower) { - maxPower = power[j]; + if (m_power[j] > maxPower) { + maxPower = m_power[j]; maxIndex = j; } } minPower = maxPower; - for (int j = 0; j < nTones; ++j) + for (int j = 0; j < m_nTones; ++j) { - if (power[j] < minPower) { - minPower = power[j]; + if (m_power[j] < minPower) { + minPower = m_power[j]; minIndex = j; } } // principle is to open if power is uneven because noise gives even power - bool open = ((maxPower - minPower) > threshold) && (minIndex > maxIndex); + bool open = ((maxPower - minPower) > m_threshold); // && (minIndex > maxIndex); if (open) { - if (samplesAttack && (attackCount < samplesAttack)) + if (m_samplesAttack && (m_attackCount < m_samplesAttack)) { - attackCount++; + m_isOpen = false; + m_attackCount++; } else { - isOpen = true; - decayCount = 0; + m_isOpen = true; + m_decayCount = 0; } } else { - if (samplesDecay && (decayCount < samplesDecay)) + if (m_samplesDecay && (m_decayCount < m_samplesDecay)) { - decayCount++; + m_isOpen = true; + m_decayCount++; } else { - isOpen = false; - attackCount = 0; + m_isOpen = false; + m_attackCount = 0; } } } diff --git a/sdrbase/dsp/agc.cpp b/sdrbase/dsp/agc.cpp index 5630c993a..6ecbc8729 100644 --- a/sdrbase/dsp/agc.cpp +++ b/sdrbase/dsp/agc.cpp @@ -40,36 +40,11 @@ Real AGC::getValue() return m_u0; } -Real AGC::getDelayedValue() +Real AGC::getAverage() { - if (m_count < m_historySize*m_mult) - { - return 0; - } - else - { - return returnedDelayedValue(); - } + return m_moving_average.average(); } -void AGC::openedSquelch() -{ - if (m_count < m_historySize*m_mult) - { - m_count++; - } - - m_u0 = m_R / m_moving_average.average(); -} - -void AGC::closedSquelch() -{ - //m_moving_average.fill(m_R); // Valgrind optim - m_count = 0; - m_u0 = m_R / m_moving_average.average(); -} - - MagSquaredAGC::MagSquaredAGC() : AGC() {} @@ -83,9 +58,10 @@ MagSquaredAGC::~MagSquaredAGC() void MagSquaredAGC::feed(Complex& ci) { - ci *= m_u0; Real magsq = ci.real()*ci.real() + ci.imag()*ci.imag(); m_moving_average.feed(magsq); + m_u0 = m_R / m_moving_average.average(); + ci *= m_u0; } @@ -102,9 +78,10 @@ MagAGC::~MagAGC() void MagAGC::feed(Complex& ci) { - ci *= m_u0; Real mag = sqrt(ci.real()*ci.real() + ci.imag()*ci.imag()); m_moving_average.feed(mag); + m_u0 = m_R / m_moving_average.average(); + ci *= m_u0; } @@ -140,7 +117,6 @@ void AlphaAGC::resize(int historySize, Real R, Real alpha) void AlphaAGC::feed(Complex& ci) { - ci *= m_u0; Real magsq = ci.real()*ci.real() + ci.imag()*ci.imag(); if (m_squelchOpen && (magsq)) @@ -152,16 +128,5 @@ void AlphaAGC::feed(Complex& ci) //m_squelchOpen = true; m_moving_average.feed(magsq); } -} - -void AlphaAGC::openedSquelch() -{ - AGC::openedSquelch(); - m_squelchOpen = true; -} - -void AlphaAGC::closedSquelch() -{ - AGC::closedSquelch(); - m_squelchOpen = false; + ci *= m_u0; }