NFM demod: back to the basics

This commit is contained in:
f4exb 2015-09-12 16:34:57 +02:00
parent c6b2730456
commit 34942340a3
9 changed files with 131 additions and 197 deletions

View File

@ -168,5 +168,5 @@ Assuming Debian Jessie is used:
- Tx support with the BladeRF - Tx support with the BladeRF
- Enhance WFM (stereo, RDS?) - Enhance WFM (stereo, RDS?)
- Even more demods ... - Even more demods ...
- Support for Airspy - Support for Hack-RF

View File

@ -27,7 +27,10 @@ public:
// Constructors and Destructor // Constructors and Destructor
AFSquelch(); AFSquelch();
// allows user defined tone pair // 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(); virtual ~AFSquelch();
// setup the basic parameters and coefficients // setup the basic parameters and coefficients
@ -39,7 +42,7 @@ public:
// set the detection threshold // set the detection threshold
void setThreshold(double _threshold) { void setThreshold(double _threshold) {
threshold = _threshold; m_threshold = _threshold;
} }
// analyze a sample set and optionally filter // analyze a sample set and optionally filter
@ -49,11 +52,11 @@ public:
// get the tone set // get the tone set
const Real *getToneSet() const const Real *getToneSet() const
{ {
return toneSet; return m_toneSet;
} }
bool open() const { bool open() const {
return isOpen; return m_isOpen;
} }
void reset(); // reset the analysis algorithm void reset(); // reset the analysis algorithm
@ -64,23 +67,23 @@ protected:
void evaluate(); void evaluate();
private: private:
int N; int m_N;
int sampleRate; int m_sampleRate;
int samplesProcessed; int m_samplesProcessed;
int maxPowerIndex; int m_maxPowerIndex;
int nTones; int m_nTones;
int samplesAttack; int m_samplesAttack;
int attackCount; int m_attackCount;
int samplesDecay; int m_samplesDecay;
int decayCount; int m_decayCount;
bool isOpen; bool m_isOpen;
double threshold; double m_threshold;
double *k; double *m_k;
double *coef; double *m_coef;
Real *toneSet; Real *m_toneSet;
double *u0; double *m_u0;
double *u1; double *m_u1;
double *power; double *m_power;
}; };

View File

@ -20,11 +20,8 @@ public:
void resize(int historySize, Real R); void resize(int historySize, Real R);
Real getValue(); Real getValue();
Real getDelayedValue(); Real getAverage();
virtual void feed(Complex& ci) = 0; virtual void feed(Complex& ci) = 0;
virtual Real returnedDelayedValue() const = 0;
void openedSquelch();
void closedSquelch();
protected: protected:
Real m_u0; Real m_u0;
@ -32,7 +29,6 @@ protected:
MovingAverage<Real> m_moving_average; // Averaging engine. The stack length conditions the smoothness of AGC. MovingAverage<Real> m_moving_average; // Averaging engine. The stack length conditions the smoothness of AGC.
int m_historySize; int m_historySize;
int m_count; int m_count;
static const int m_mult = 4; // squelch delay multiplicator
}; };
class MagSquaredAGC : public AGC class MagSquaredAGC : public AGC
@ -42,7 +38,6 @@ public:
MagSquaredAGC(int historySize, Real R); MagSquaredAGC(int historySize, Real R);
virtual ~MagSquaredAGC(); virtual ~MagSquaredAGC();
virtual void feed(Complex& ci); virtual void feed(Complex& ci);
virtual Real returnedDelayedValue() const { return m_u0; }
}; };
class MagAGC : public AGC class MagAGC : public AGC
@ -52,7 +47,6 @@ public:
MagAGC(int historySize, Real R); MagAGC(int historySize, Real R);
virtual ~MagAGC(); virtual ~MagAGC();
virtual void feed(Complex& ci); virtual void feed(Complex& ci);
virtual Real returnedDelayedValue() const { return m_u0; }
}; };
class AlphaAGC : public AGC class AlphaAGC : public AGC
@ -64,9 +58,6 @@ public:
virtual ~AlphaAGC(); virtual ~AlphaAGC();
void resize(int historySize, Real R, Real alpha); void resize(int historySize, Real R, Real alpha);
virtual void feed(Complex& ci); virtual void feed(Complex& ci);
virtual Real returnedDelayedValue() const { return 1; }
void openedSquelch();
void closedSquelch();
private: private:
Real m_alpha; Real m_alpha;
bool m_squelchOpen; 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: private:
bool m_squelchOpen; // open for processing bool m_squelchOpen; // open for processing
Real m_fill; // refill average at this level Real m_fill; // refill average at this level

View File

@ -113,7 +113,6 @@ void AMDemod::feed(const SampleVector::const_iterator& begin, const SampleVector
} }
else else
{ {
m_volumeAGC.closedSquelch();
sample = 0; sample = 0;
} }

View File

@ -26,14 +26,13 @@
#include "dsp/pidcontroller.h" #include "dsp/pidcontroller.h"
#include "dsp/dspengine.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) MESSAGE_CLASS_DEFINITION(NFMDemod::MsgConfigureNFMDemod, Message)
NFMDemod::NFMDemod() : NFMDemod::NFMDemod() :
m_ctcssIndex(0), m_ctcssIndex(0),
m_sampleCount(0), m_sampleCount(0),
m_afSquelch(2, afSqTones),
m_squelchOpen(false), m_squelchOpen(false),
m_audioFifo(4, 48000), m_audioFifo(4, 48000),
m_settingsMutex(QMutex::Recursive) m_settingsMutex(QMutex::Recursive)
@ -54,14 +53,11 @@ NFMDemod::NFMDemod() :
m_audioBuffer.resize(1<<14); m_audioBuffer.resize(1<<14);
m_audioBufferFill = 0; m_audioBufferFill = 0;
m_movingAverage.resize(16, 0); m_movingAverage.resize(240, 0);
m_agcLevel = 0.0625; // 0.003 m_agcLevel = 1.0;
//m_AGC.resize(480, m_agcLevel, 0, 0.1*m_agcLevel); m_AGC.resize(240, m_agcLevel);
m_AGC.resize(600, m_agcLevel*m_agcLevel); //, 0.3);
m_ctcssDetector.setCoefficients(3000, 6000.0); // 0.5s / 2 Hz resolution 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); 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 qp = ci.imag() - m_m2Sample.imag();
Real h1 = m_m1Sample.real() * qp; Real h1 = m_m1Sample.real() * qp;
Real h2 = m_m1Sample.imag() * ip; 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_m2Sample = m_m1Sample;
m_m1Sample = ci; m_m1Sample = ci;
@ -173,12 +169,7 @@ void NFMDemod::feed(const SampleVector::const_iterator& begin, const SampleVecto
// AF processing // AF processing
if(m_afSquelch.analyze(&demod)) if (m_AGC.getAverage() > m_squelchLevel)
{
m_squelchOpen = m_afSquelch.open();
}
if (m_squelchOpen)
{ {
if (m_running.m_ctcssOn) 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_bandpass.filter(demod);
demod *= m_running.m_volume; 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 else
{ {
@ -231,7 +220,6 @@ void NFMDemod::feed(const SampleVector::const_iterator& begin, const SampleVecto
m_ctcssIndex = 0; m_ctcssIndex = 0;
} }
m_AGC.closedSquelch();
sample = 0; sample = 0;
} }
@ -354,10 +342,11 @@ void NFMDemod::apply()
if (m_config.m_squelch != m_running.m_squelch) if (m_config.m_squelch != m_running.m_squelch)
{ {
m_squelchLevel = pow(10.0, m_config.m_squelch / 10.0); // input is a power level in dB
m_squelchLevel *= m_squelchLevel; // m_squelchLevel = pow(10.0, m_config.m_squelch / 10.0);
m_afSquelch.setThreshold(m_squelchLevel); m_squelchLevel = pow(10.0, m_config.m_squelch / 20.0); // to magnitude
m_afSquelch.reset();
//m_squelchLevel *= m_squelchLevel;
} }
m_running.m_inputSampleRate = m_config.m_inputSampleRate; m_running.m_inputSampleRate = m_config.m_inputSampleRate;

View File

@ -156,14 +156,13 @@ private:
double m_squelchLevel; double m_squelchLevel;
//int m_squelchState; //int m_squelchState;
AFSquelch m_afSquelch;
bool m_squelchOpen; bool m_squelchOpen;
Real m_lastArgument; Real m_lastArgument;
Complex m_m1Sample; Complex m_m1Sample;
Complex m_m2Sample; Complex m_m2Sample;
MovingAverage<Real> m_movingAverage; MovingAverage<Real> m_movingAverage;
MagSquaredAGC m_AGC; MagAGC m_AGC;
Real m_agcLevel; // AGC will aim to this level Real m_agcLevel; // AGC will aim to this level
Real m_agcFloor; // AGC will not go below this level Real m_agcFloor; // AGC will not go below this level

View File

@ -131,7 +131,7 @@
<item row="4" column="4"> <item row="4" column="4">
<widget class="QSlider" name="squelch"> <widget class="QSlider" name="squelch">
<property name="minimum"> <property name="minimum">
<number>-60</number> <number>-100</number>
</property> </property>
<property name="maximum"> <property name="maximum">
<number>0</number> <number>0</number>

View File

@ -18,73 +18,73 @@
#include "dsp/afsquelch.h" #include "dsp/afsquelch.h"
AFSquelch::AFSquelch() : AFSquelch::AFSquelch() :
N(0), m_N(0),
sampleRate(0), m_sampleRate(0),
samplesProcessed(0), m_samplesProcessed(0),
maxPowerIndex(0), m_maxPowerIndex(0),
nTones(2), m_nTones(2),
samplesAttack(0), m_samplesAttack(0),
attackCount(0), m_attackCount(0),
samplesDecay(0), m_samplesDecay(0),
decayCount(0), m_decayCount(0),
isOpen(false), m_isOpen(false),
threshold(0.0) m_threshold(0.0)
{ {
k = new double[nTones]; m_k = new double[m_nTones];
coef = new double[nTones]; m_coef = new double[m_nTones];
toneSet = new Real[nTones]; m_toneSet = new Real[m_nTones];
u0 = new double[nTones]; m_u0 = new double[m_nTones];
u1 = new double[nTones]; m_u1 = new double[m_nTones];
power = new double[nTones]; m_power = new double[m_nTones];
toneSet[0] = 2000.0; m_toneSet[0] = 2000.0;
toneSet[1] = 10000.0; m_toneSet[1] = 10000.0;
} }
AFSquelch::AFSquelch(unsigned int nbTones, const Real *tones) : AFSquelch::AFSquelch(unsigned int nbTones, const Real *tones, int samplesAttack, int samplesDecay) :
N(0), m_N(0),
sampleRate(0), m_sampleRate(0),
samplesProcessed(0), m_samplesProcessed(0),
maxPowerIndex(0), m_maxPowerIndex(0),
nTones(nbTones), m_nTones(nbTones),
samplesAttack(0), m_samplesAttack(samplesAttack),
attackCount(0), m_attackCount(0),
samplesDecay(0), m_samplesDecay(samplesDecay),
decayCount(0), m_decayCount(0),
isOpen(false), m_isOpen(false),
threshold(0.0) m_threshold(0.0)
{ {
k = new double[nTones]; m_k = new double[m_nTones];
coef = new double[nTones]; m_coef = new double[m_nTones];
toneSet = new Real[nTones]; m_toneSet = new Real[m_nTones];
u0 = new double[nTones]; m_u0 = new double[m_nTones];
u1 = new double[nTones]; m_u1 = new double[m_nTones];
power = new double[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() AFSquelch::~AFSquelch()
{ {
delete[] k; delete[] m_k;
delete[] coef; delete[] m_coef;
delete[] toneSet; delete[] m_toneSet;
delete[] u0; delete[] m_u0;
delete[] u1; delete[] m_u1;
delete[] power; delete[] m_power;
} }
void AFSquelch::setCoefficients(int _N, int _samplerate, int _samplesAttack, int _samplesDecay ) void AFSquelch::setCoefficients(int _N, int _samplerate, int _samplesAttack, int _samplesDecay )
{ {
N = _N; // save the basic parameters for use during analysis m_N = _N; // save the basic parameters for use during analysis
sampleRate = _samplerate; m_sampleRate = _samplerate;
samplesAttack = _samplesAttack; m_samplesAttack = _samplesAttack;
samplesDecay = _samplesDecay; m_samplesDecay = _samplesDecay;
// for each of the frequencies (tones) of interest calculate // for each of the frequencies (tones) of interest calculate
// k and the associated filter coefficient as per the Goertzel // 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 // for later display. The tone set is specified in the
// constructor. Notice that the resulting coefficients are // constructor. Notice that the resulting coefficients are
// independent of N. // 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; m_k[j] = ((double)m_N * m_toneSet[j]) / (double)m_sampleRate;
coef[j] = 2.0 * cos((2.0 * M_PI * toneSet[j])/(double)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 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 feedForward(); // calculate the power at each tone
samplesProcessed = 0; m_samplesProcessed = 0;
return true; // have a result return true; // have a result
} }
else else
@ -126,21 +126,21 @@ void AFSquelch::feedback(Real in)
double t; double t;
// feedback for each tone // feedback for each tone
for (int j = 0; j < nTones; ++j) for (int j = 0; j < m_nTones; ++j)
{ {
t = u0[j]; t = m_u0[j];
u0[j] = in + (coef[j] * u0[j]) - u1[j]; m_u0[j] = in + (m_coef[j] * m_u0[j]) - m_u1[j];
u1[j] = t; m_u1[j] = t;
} }
} }
void AFSquelch::feedForward() 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]); m_power[j] = (m_u0[j] * m_u0[j]) + (m_u1[j] * m_u1[j]) - (m_coef[j] * m_u0[j] * m_u1[j]);
u0[j] = u1[j] = 0.0; // reset for next block. m_u0[j] = m_u1[j] = 0.0; // reset for next block.
} }
evaluate(); evaluate();
@ -149,14 +149,14 @@ void AFSquelch::feedForward()
void AFSquelch::reset() 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; m_samplesProcessed = 0;
maxPowerIndex = 0; m_maxPowerIndex = 0;
isOpen = false; m_isOpen = false;
} }
@ -166,49 +166,51 @@ void AFSquelch::evaluate()
double minPower; double minPower;
int minIndex = 0, maxIndex = 0; int minIndex = 0, maxIndex = 0;
for (int j = 0; j < nTones; ++j) for (int j = 0; j < m_nTones; ++j)
{ {
if (power[j] > maxPower) { if (m_power[j] > maxPower) {
maxPower = power[j]; maxPower = m_power[j];
maxIndex = j; maxIndex = j;
} }
} }
minPower = maxPower; minPower = maxPower;
for (int j = 0; j < nTones; ++j) for (int j = 0; j < m_nTones; ++j)
{ {
if (power[j] < minPower) { if (m_power[j] < minPower) {
minPower = power[j]; minPower = m_power[j];
minIndex = j; minIndex = j;
} }
} }
// principle is to open if power is uneven because noise gives even power // 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 (open)
{ {
if (samplesAttack && (attackCount < samplesAttack)) if (m_samplesAttack && (m_attackCount < m_samplesAttack))
{ {
attackCount++; m_isOpen = false;
m_attackCount++;
} }
else else
{ {
isOpen = true; m_isOpen = true;
decayCount = 0; m_decayCount = 0;
} }
} }
else else
{ {
if (samplesDecay && (decayCount < samplesDecay)) if (m_samplesDecay && (m_decayCount < m_samplesDecay))
{ {
decayCount++; m_isOpen = true;
m_decayCount++;
} }
else else
{ {
isOpen = false; m_isOpen = false;
attackCount = 0; m_attackCount = 0;
} }
} }
} }

View File

@ -40,36 +40,11 @@ Real AGC::getValue()
return m_u0; return m_u0;
} }
Real AGC::getDelayedValue() Real AGC::getAverage()
{ {
if (m_count < m_historySize*m_mult) return m_moving_average.average();
{
return 0;
}
else
{
return returnedDelayedValue();
}
} }
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() : MagSquaredAGC::MagSquaredAGC() :
AGC() AGC()
{} {}
@ -83,9 +58,10 @@ MagSquaredAGC::~MagSquaredAGC()
void MagSquaredAGC::feed(Complex& ci) void MagSquaredAGC::feed(Complex& ci)
{ {
ci *= m_u0;
Real magsq = ci.real()*ci.real() + ci.imag()*ci.imag(); Real magsq = ci.real()*ci.real() + ci.imag()*ci.imag();
m_moving_average.feed(magsq); 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) void MagAGC::feed(Complex& ci)
{ {
ci *= m_u0;
Real mag = sqrt(ci.real()*ci.real() + ci.imag()*ci.imag()); Real mag = sqrt(ci.real()*ci.real() + ci.imag()*ci.imag());
m_moving_average.feed(mag); 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) void AlphaAGC::feed(Complex& ci)
{ {
ci *= m_u0;
Real magsq = ci.real()*ci.real() + ci.imag()*ci.imag(); Real magsq = ci.real()*ci.real() + ci.imag()*ci.imag();
if (m_squelchOpen && (magsq)) if (m_squelchOpen && (magsq))
@ -152,16 +128,5 @@ void AlphaAGC::feed(Complex& ci)
//m_squelchOpen = true; //m_squelchOpen = true;
m_moving_average.feed(magsq); m_moving_average.feed(magsq);
} }
} ci *= m_u0;
void AlphaAGC::openedSquelch()
{
AGC::openedSquelch();
m_squelchOpen = true;
}
void AlphaAGC::closedSquelch()
{
AGC::closedSquelch();
m_squelchOpen = false;
} }