2015-06-19 08:27:29 +02:00
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Copyright (C) 2015 Edouard Griffiths, F4EXB. //
|
|
|
|
// //
|
|
|
|
// This program is free software; you can redistribute it and/or modify //
|
|
|
|
// it under the terms of the GNU General Public License as published by //
|
|
|
|
// the Free Software Foundation as version 3 of the License, or //
|
2019-04-11 14:32:15 +02:00
|
|
|
// (at your option) any later version. //
|
2015-06-19 08:27:29 +02:00
|
|
|
// //
|
|
|
|
// This program is distributed in the hope that it will be useful, //
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
|
|
|
// GNU General Public License V3 for more details. //
|
|
|
|
// //
|
|
|
|
// You should have received a copy of the GNU General Public License //
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2017-09-16 04:50:25 +02:00
|
|
|
#include <math.h>
|
2015-06-19 08:27:29 +02:00
|
|
|
#include "dsp/afsquelch.h"
|
|
|
|
|
2017-09-16 04:50:25 +02:00
|
|
|
#undef M_PI
|
2018-04-14 21:45:45 +02:00
|
|
|
#define M_PI 3.14159265358979323846
|
2017-09-16 04:50:25 +02:00
|
|
|
|
2018-04-14 21:45:45 +02:00
|
|
|
AFSquelch::AFSquelch() :
|
2017-05-12 23:10:54 +02:00
|
|
|
m_nbAvg(128),
|
2018-04-14 21:45:45 +02:00
|
|
|
m_N(24),
|
|
|
|
m_sampleRate(48000),
|
2015-09-12 16:34:57 +02:00
|
|
|
m_samplesProcessed(0),
|
2017-05-13 15:07:56 +02:00
|
|
|
m_samplesAvgProcessed(0),
|
2015-09-12 16:34:57 +02:00
|
|
|
m_maxPowerIndex(0),
|
2018-04-14 21:45:45 +02:00
|
|
|
m_nTones(2),
|
2015-09-13 11:56:14 +02:00
|
|
|
m_samplesAttack(0),
|
2015-09-12 16:34:57 +02:00
|
|
|
m_attackCount(0),
|
2015-09-13 11:56:14 +02:00
|
|
|
m_samplesDecay(0),
|
2015-09-12 16:34:57 +02:00
|
|
|
m_decayCount(0),
|
2017-05-31 00:30:00 +02:00
|
|
|
m_squelchCount(0),
|
2015-09-12 16:34:57 +02:00
|
|
|
m_isOpen(false),
|
|
|
|
m_threshold(0.0)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2015-09-12 16:34:57 +02:00
|
|
|
m_k = new double[m_nTones];
|
|
|
|
m_coef = new double[m_nTones];
|
2017-05-13 16:05:47 +02:00
|
|
|
m_toneSet = new double[m_nTones];
|
2015-09-12 16:34:57 +02:00
|
|
|
m_u0 = new double[m_nTones];
|
|
|
|
m_u1 = new double[m_nTones];
|
|
|
|
m_power = new double[m_nTones];
|
2017-05-13 15:07:56 +02:00
|
|
|
m_movingAverages.resize(m_nTones, MovingAverage<double>(m_nbAvg, 0.0f));
|
2015-09-12 16:34:57 +02:00
|
|
|
|
2017-05-16 15:36:30 +02:00
|
|
|
for (unsigned int j = 0; j < m_nTones; ++j)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2018-04-14 21:45:45 +02:00
|
|
|
m_toneSet[j] = j == 0 ? 1000.0 : 6000.0;
|
|
|
|
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);
|
2017-05-13 15:07:56 +02:00
|
|
|
m_u0[j] = 0.0;
|
|
|
|
m_u1[j] = 0.0;
|
|
|
|
m_power[j] = 0.0;
|
2017-05-31 23:36:02 +02:00
|
|
|
m_movingAverages[j].fill(0.0);
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
AFSquelch::~AFSquelch()
|
|
|
|
{
|
2015-09-12 16:34:57 +02:00
|
|
|
delete[] m_k;
|
|
|
|
delete[] m_coef;
|
|
|
|
delete[] m_toneSet;
|
|
|
|
delete[] m_u0;
|
|
|
|
delete[] m_u1;
|
|
|
|
delete[] m_power;
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|
|
|
|
|
2017-05-16 15:36:30 +02:00
|
|
|
void AFSquelch::setCoefficients(
|
|
|
|
unsigned int N,
|
|
|
|
unsigned int nbAvg,
|
2018-04-14 21:45:45 +02:00
|
|
|
unsigned int sampleRate,
|
|
|
|
unsigned int samplesAttack,
|
|
|
|
unsigned int samplesDecay,
|
|
|
|
const double *tones)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2016-02-28 10:53:37 +01:00
|
|
|
m_N = N; // save the basic parameters for use during analysis
|
2015-09-13 11:56:14 +02:00
|
|
|
m_nbAvg = nbAvg;
|
2018-04-14 21:45:45 +02:00
|
|
|
m_sampleRate = sampleRate;
|
|
|
|
m_samplesAttack = samplesAttack;
|
|
|
|
m_samplesDecay = samplesDecay;
|
2017-05-13 15:07:56 +02:00
|
|
|
m_movingAverages.resize(m_nTones, MovingAverage<double>(m_nbAvg, 0.0));
|
2017-05-13 12:10:03 +02:00
|
|
|
m_samplesProcessed = 0;
|
2017-05-13 15:07:56 +02:00
|
|
|
m_samplesAvgProcessed = 0;
|
2017-05-13 12:10:03 +02:00
|
|
|
m_maxPowerIndex = 0;
|
|
|
|
m_attackCount = 0;
|
|
|
|
m_decayCount = 0;
|
2017-05-31 00:30:00 +02:00
|
|
|
m_squelchCount = 0;
|
2017-05-13 12:10:03 +02:00
|
|
|
m_isOpen = false;
|
|
|
|
m_threshold = 0.0;
|
2015-06-19 08:27:29 +02:00
|
|
|
|
|
|
|
// for each of the frequencies (tones) of interest calculate
|
|
|
|
// k and the associated filter coefficient as per the Goertzel
|
2017-05-13 16:01:41 +02:00
|
|
|
// algorithm. Note: we are using a real value (as opposed to
|
2015-06-19 08:27:29 +02:00
|
|
|
// an integer as described in some references. k is retained
|
|
|
|
// for later display. The tone set is specified in the
|
|
|
|
// constructor. Notice that the resulting coefficients are
|
|
|
|
// independent of N.
|
2018-04-14 21:45:45 +02:00
|
|
|
|
2017-05-16 15:36:30 +02:00
|
|
|
for (unsigned int j = 0; j < m_nTones; ++j)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2018-04-14 21:45:45 +02:00
|
|
|
m_toneSet[j] = tones[j] < ((double) m_sampleRate) * 0.4 ? tones[j] : ((double) m_sampleRate) * 0.4; // guarantee 80% Nyquist rate
|
2015-09-12 16:34:57 +02:00
|
|
|
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);
|
2017-05-13 15:07:56 +02:00
|
|
|
m_u0[j] = 0.0;
|
|
|
|
m_u1[j] = 0.0;
|
|
|
|
m_power[j] = 0.0;
|
2017-05-31 23:36:02 +02:00
|
|
|
m_movingAverages[j].fill(0.0);
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Analyze an input signal
|
2017-05-13 16:05:47 +02:00
|
|
|
bool AFSquelch::analyze(double sample)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
|
|
|
|
2015-09-13 11:56:14 +02:00
|
|
|
feedback(sample); // Goertzel feedback
|
2015-06-19 08:27:29 +02:00
|
|
|
|
2017-05-13 15:07:56 +02:00
|
|
|
if (m_samplesProcessed < m_N) // completed a block of N
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2017-05-13 15:07:56 +02:00
|
|
|
m_samplesProcessed++;
|
|
|
|
return false;
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2017-05-13 15:07:56 +02:00
|
|
|
feedForward(); // calculate the power at each tone
|
|
|
|
m_samplesProcessed = 0;
|
|
|
|
|
|
|
|
if (m_samplesAvgProcessed < m_nbAvg)
|
|
|
|
{
|
|
|
|
m_samplesAvgProcessed++;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return true; // have a result
|
|
|
|
}
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-13 16:05:47 +02:00
|
|
|
void AFSquelch::feedback(double in)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
|
|
|
double t;
|
|
|
|
|
|
|
|
// feedback for each tone
|
2017-05-16 15:36:30 +02:00
|
|
|
for (unsigned int j = 0; j < m_nTones; ++j)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2015-09-12 16:34:57 +02:00
|
|
|
t = m_u0[j];
|
|
|
|
m_u0[j] = in + (m_coef[j] * m_u0[j]) - m_u1[j];
|
|
|
|
m_u1[j] = t;
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void AFSquelch::feedForward()
|
|
|
|
{
|
2017-05-16 15:36:30 +02:00
|
|
|
for (unsigned int j = 0; j < m_nTones; ++j)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2015-09-12 16:34:57 +02:00
|
|
|
m_power[j] = (m_u0[j] * m_u0[j]) + (m_u1[j] * m_u1[j]) - (m_coef[j] * m_u0[j] * m_u1[j]);
|
2015-09-13 11:56:14 +02:00
|
|
|
m_movingAverages[j].feed(m_power[j]);
|
2017-05-13 16:01:41 +02:00
|
|
|
m_u0[j] = 0.0;
|
|
|
|
m_u1[j] = 0.0; // reset for next block.
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
evaluate();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void AFSquelch::reset()
|
|
|
|
{
|
2017-05-16 15:36:30 +02:00
|
|
|
for (unsigned int j = 0; j < m_nTones; ++j)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2017-06-06 01:53:52 +02:00
|
|
|
m_u0[j] = 0.0;
|
|
|
|
m_u1[j] = 0.0;
|
|
|
|
m_power[j] = 0.0;
|
|
|
|
m_movingAverages[j].fill(0.0);
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|
|
|
|
|
2015-09-12 16:34:57 +02:00
|
|
|
m_samplesProcessed = 0;
|
|
|
|
m_maxPowerIndex = 0;
|
|
|
|
m_isOpen = false;
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-09-13 11:56:14 +02:00
|
|
|
bool AFSquelch::evaluate()
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2017-05-13 15:07:56 +02:00
|
|
|
double maxPower = 0.0;
|
2015-06-19 08:27:29 +02:00
|
|
|
double minPower;
|
|
|
|
int minIndex = 0, maxIndex = 0;
|
|
|
|
|
2017-05-16 15:36:30 +02:00
|
|
|
for (unsigned int j = 0; j < m_nTones; ++j)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2015-09-13 11:56:14 +02:00
|
|
|
if (m_movingAverages[j].sum() > maxPower)
|
|
|
|
{
|
|
|
|
maxPower = m_movingAverages[j].sum();
|
2015-06-19 08:27:29 +02:00
|
|
|
maxIndex = j;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-30 20:31:15 +02:00
|
|
|
if (maxPower == 0.0)
|
|
|
|
{
|
|
|
|
return m_isOpen;
|
|
|
|
}
|
|
|
|
|
2015-06-19 08:27:29 +02:00
|
|
|
minPower = maxPower;
|
|
|
|
|
2017-05-16 15:36:30 +02:00
|
|
|
for (unsigned int j = 0; j < m_nTones; ++j)
|
2015-06-19 08:27:29 +02:00
|
|
|
{
|
2015-09-13 11:56:14 +02:00
|
|
|
if (m_movingAverages[j].sum() < minPower) {
|
|
|
|
minPower = m_movingAverages[j].sum();
|
2015-06-19 08:27:29 +02:00
|
|
|
minIndex = j;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-31 00:30:00 +02:00
|
|
|
// m_isOpen = ((minPower/maxPower < m_threshold) && (minIndex > maxIndex));
|
|
|
|
|
|
|
|
if ((minPower/maxPower < m_threshold) && (minIndex > maxIndex)) // open condition
|
|
|
|
{
|
|
|
|
if (m_squelchCount < m_samplesAttack + m_samplesDecay)
|
|
|
|
{
|
|
|
|
m_squelchCount++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (m_squelchCount > m_samplesAttack)
|
|
|
|
{
|
|
|
|
m_squelchCount--;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
m_squelchCount = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
m_isOpen = (m_squelchCount >= m_samplesAttack) ;
|
|
|
|
|
|
|
|
// if ((minPower/maxPower < m_threshold) && (minIndex > maxIndex)) // open condition
|
|
|
|
// {
|
|
|
|
// if ((m_samplesAttack > 0) && (m_attackCount < m_samplesAttack))
|
|
|
|
// {
|
|
|
|
// m_isOpen = false;
|
|
|
|
// m_attackCount++;
|
|
|
|
// }
|
|
|
|
// else
|
|
|
|
// {
|
|
|
|
// m_isOpen = true;
|
|
|
|
// m_decayCount = 0;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// else
|
|
|
|
// {
|
|
|
|
// if ((m_samplesDecay > 0) && (m_decayCount < m_samplesDecay))
|
|
|
|
// {
|
|
|
|
// m_isOpen = true;
|
|
|
|
// m_decayCount++;
|
|
|
|
// }
|
|
|
|
// else
|
|
|
|
// {
|
|
|
|
// m_isOpen = false;
|
|
|
|
// m_attackCount = 0;
|
|
|
|
// }
|
|
|
|
// }
|
2015-09-13 11:56:14 +02:00
|
|
|
|
|
|
|
return m_isOpen;
|
|
|
|
}
|
|
|
|
|
|
|
|
void AFSquelch::setThreshold(double threshold)
|
|
|
|
{
|
|
|
|
qDebug("AFSquelch::setThreshold: threshold: %f", threshold);
|
|
|
|
m_threshold = threshold;
|
2017-06-06 02:16:28 +02:00
|
|
|
reset();
|
2015-06-19 08:27:29 +02:00
|
|
|
}
|