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
- Enhance WFM (stereo, RDS?)
- Even more demods ...
- Support for Airspy
- Support for Hack-RF

View File

@ -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;
};

View File

@ -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<Real> 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

View File

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

View File

@ -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;

View File

@ -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<Real> 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

View File

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

View File

@ -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;
}
}
}

View File

@ -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;
}