NFM demod: new discriminator and optional FM deviatoin based squelch

This commit is contained in:
f4exb 2017-03-05 06:22:05 +01:00
parent b805cc89c9
commit 2318419716
10 changed files with 219 additions and 31 deletions

6
debian/changelog vendored
View File

@ -1,3 +1,9 @@
sdrangel (3.3.0-1) unstable; urgency=medium
* NFM demod: new discriminator and optional FM deviatoin based squelch
-- Edouard Griffiths, F4EXB <f4exb06@gmail.com> Thu, 02 Mar 2017 23:14:18 +0100
sdrangel (3.2.0-1) unstable; urgency=medium
* ATV demodulator for amateur Analog TV

View File

@ -55,6 +55,7 @@ NFMDemod::NFMDemod() :
m_config.m_afBandwidth = 3000;
m_config.m_fmDeviation = 2000;
m_config.m_squelchGate = 5; // 10s of ms at 48000 Hz sample rate. Corresponds to 2400 for AGC attack
m_config.m_deltaSquelch = false;
m_config.m_squelch = -30.0;
m_config.m_volume = 1.0;
m_config.m_ctcssOn = false;
@ -68,7 +69,7 @@ NFMDemod::NFMDemod() :
m_agcLevel = 1.0;
m_AGC.resize(m_squelchGate, m_agcLevel);
m_movingAverage.resize(16, 0);
m_movingAverage.resize(32, 0);
m_ctcssDetector.setCoefficients(3000, 6000.0); // 0.5s / 2 Hz resolution
m_afSquelch.setCoefficients(24, 600, 48000.0, 200, 0); // 4000 Hz span, 250us, 100ms attack
@ -87,6 +88,7 @@ void NFMDemod::configure(MessageQueue* messageQueue,
int fmDeviation,
Real volume,
int squelchGate,
bool deltaSquelch,
Real squelch,
bool ctcssOn,
bool audioMute)
@ -96,6 +98,7 @@ void NFMDemod::configure(MessageQueue* messageQueue,
fmDeviation,
volume,
squelchGate,
deltaSquelch,
squelch,
ctcssOn,
audioMute);
@ -154,8 +157,9 @@ void NFMDemod::feed(const SampleVector::const_iterator& begin, const SampleVecto
//double magsqRaw = m_AGC.getMagSq();
long double magsqRaw; // = ci.real()*ci.real() + c.imag()*c.imag();
Real deviation;
Real demod = m_phaseDiscri.phaseDiscriminator3(ci, magsqRaw);
Real demod = m_phaseDiscri.phaseDiscriminatorDelta(ci, magsqRaw, deviation);
Real magsq = magsqRaw / (1<<30);
m_movingAverage.feed(magsq);
@ -174,20 +178,28 @@ void NFMDemod::feed(const SampleVector::const_iterator& begin, const SampleVecto
// AF processing
if (m_movingAverage.average() > m_squelchLevel)
if ( (m_running.m_deltaSquelch && ((deviation > m_squelchLevel) || (deviation < -m_squelchLevel))) ||
(!m_running.m_deltaSquelch && (m_movingAverage.average() < m_squelchLevel)) )
{
if (m_squelchCount < m_squelchGate)
{
m_squelchCount++;
}
if (m_squelchCount < m_squelchGate)
{
m_squelchCount = 0; // return to 0
}
else
{
m_squelchCount--; // grace period
}
}
else
{
m_squelchCount = 0;
if (m_squelchCount < m_squelchGate + 2)
{
m_squelchCount++;
}
}
//squelchOpen = (getMag() > m_squelchLevel);
m_squelchOpen = m_squelchCount == m_squelchGate; // wait for AGC to stabilize
m_squelchOpen = m_squelchCount >= m_squelchGate; // wait for AGC to stabilize
/*
if (m_afSquelch.analyze(demod))
@ -322,6 +334,7 @@ bool NFMDemod::handleMessage(const Message& cmd)
m_config.m_fmDeviation = cfg.getFMDeviation();
m_config.m_volume = cfg.getVolume();
m_config.m_squelchGate = cfg.getSquelchGate();
m_config.m_deltaSquelch = cfg.getDeltaSquelch();
m_config.m_squelch = cfg.getSquelch();
m_config.m_ctcssOn = cfg.getCtcssOn();
m_config.m_audioMute = cfg.getAudioMute();
@ -332,8 +345,9 @@ bool NFMDemod::handleMessage(const Message& cmd)
<< " m_afBandwidth: " << m_config.m_afBandwidth
<< " m_fmDeviation: " << m_config.m_fmDeviation
<< " m_volume: " << m_config.m_volume
<< " m_squelchGate" << m_config.m_squelchGate
<< " m_squelch: " << m_config.m_squelch
<< " m_squelchGate: " << m_config.m_squelchGate
<< " m_deltaSquelch: " << m_config.m_deltaSquelch
<< " m_squelch: " << m_squelchLevel
<< " m_ctcssOn: " << m_config.m_ctcssOn
<< " m_audioMute: " << m_config.m_audioMute;
@ -360,13 +374,13 @@ void NFMDemod::apply()
m_interpolator.create(16, m_config.m_inputSampleRate, m_config.m_rfBandwidth / 2.2);
m_interpolatorDistanceRemain = 0;
m_interpolatorDistance = (Real) m_config.m_inputSampleRate / (Real) m_config.m_audioSampleRate;
m_phaseDiscri.setFMScaling(m_config.m_rfBandwidth / (float) m_config.m_fmDeviation);
m_phaseDiscri.setFMScaling((2.0f*m_config.m_rfBandwidth) / (float) m_config.m_fmDeviation);
m_settingsMutex.unlock();
}
if (m_config.m_fmDeviation != m_running.m_fmDeviation)
{
m_phaseDiscri.setFMScaling(m_config.m_rfBandwidth / (float) m_config.m_fmDeviation);
m_phaseDiscri.setFMScaling((2.0f*m_config.m_rfBandwidth) / (float) m_config.m_fmDeviation);
}
if ((m_config.m_afBandwidth != m_running.m_afBandwidth) ||
@ -384,12 +398,16 @@ void NFMDemod::apply()
m_squelchCount = 0; // reset squelch open counter
}
if (m_config.m_squelch != m_running.m_squelch)
if ((m_config.m_squelch != m_running.m_squelch) ||
(m_config.m_deltaSquelch != m_running.m_deltaSquelch))
{
// input is a value in tenths of dB
m_squelchLevel = std::pow(10.0, m_config.m_squelch / 10.0);
if (m_config.m_deltaSquelch) { // input is a value in negative millis
m_squelchLevel = - m_config.m_squelch / 1000.0;
} else { // input is a value in centi-Bels
m_squelchLevel = std::pow(10.0, m_config.m_squelch / 100.0);
}
//m_squelchLevel *= m_squelchLevel;
m_afSquelch.setThreshold(m_squelchLevel);
//m_afSquelch.setThreshold(m_squelchLevel);
}
m_running.m_inputSampleRate = m_config.m_inputSampleRate;
@ -398,7 +416,8 @@ void NFMDemod::apply()
m_running.m_afBandwidth = m_config.m_afBandwidth;
m_running.m_fmDeviation = m_config.m_fmDeviation;
m_running.m_squelchGate = m_config.m_squelchGate;
m_running.m_squelch = m_config.m_squelch;
m_running.m_deltaSquelch = m_config.m_deltaSquelch;
m_running.m_squelch = m_config.m_squelch;
m_running.m_volume = m_config.m_volume;
m_running.m_audioSampleRate = m_config.m_audioSampleRate;
m_running.m_ctcssOn = m_config.m_ctcssOn;

View File

@ -46,6 +46,7 @@ public:
int fmDeviation,
Real volume,
int squelchGate,
bool deltaSquelch,
Real squelch,
bool ctcssOn,
bool audioMute);
@ -92,6 +93,7 @@ private:
int getFMDeviation() const { return m_fmDeviation; }
Real getVolume() const { return m_volume; }\
int getSquelchGate() const { return m_squelchGate; }
bool getDeltaSquelch() const { return m_deltaSquelch; }
Real getSquelch() const { return m_squelch; }
bool getCtcssOn() const { return m_ctcssOn; }
bool getAudioMute() const { return m_audioMute; }
@ -101,11 +103,21 @@ private:
int fmDeviation,
Real volume,
int squelchGate,
bool deltaSquelch,
Real squelch,
bool ctcssOn,
bool audioMute)
{
return new MsgConfigureNFMDemod(rfBandwidth, afBandwidth, fmDeviation, volume, squelchGate, squelch, ctcssOn, audioMute);
return new MsgConfigureNFMDemod(
rfBandwidth,
afBandwidth,
fmDeviation,
volume,
squelchGate,
deltaSquelch,
squelch,
ctcssOn,
audioMute);
}
private:
@ -114,6 +126,7 @@ private:
int m_fmDeviation;
Real m_volume;
int m_squelchGate;
bool m_deltaSquelch;
Real m_squelch;
bool m_ctcssOn;
bool m_audioMute;
@ -123,6 +136,7 @@ private:
int fmDeviation,
Real volume,
int squelchGate,
bool deltaSquelch,
Real squelch,
bool ctcssOn,
bool audioMute) :
@ -132,6 +146,7 @@ private:
m_fmDeviation(fmDeviation),
m_volume(volume),
m_squelchGate(squelchGate),
m_deltaSquelch(deltaSquelch),
m_squelch(squelch),
m_ctcssOn(ctcssOn),
m_audioMute(audioMute)
@ -156,6 +171,7 @@ private:
Real m_afBandwidth;
int m_fmDeviation;
int m_squelchGate;
bool m_deltaSquelch;
Real m_squelch;
Real m_volume;
bool m_ctcssOn;
@ -170,6 +186,7 @@ private:
m_afBandwidth(-1),
m_fmDeviation(1),
m_squelchGate(1),
m_deltaSquelch(false),
m_squelch(0),
m_volume(0),
m_ctcssOn(false),

View File

@ -89,6 +89,7 @@ QByteArray NFMDemodGUI::serialize() const
s.writeBool(9, ui->ctcssOn->isChecked());
s.writeBool(10, ui->audioMute->isChecked());
s.writeS32(11, ui->squelchGate->value());
s.writeBool(12, ui->deltaSquelch->isChecked());
return s.final();
}
@ -136,6 +137,8 @@ bool NFMDemodGUI::deserialize(const QByteArray& data)
ui->audioMute->setChecked(boolTmp);
d.readS32(11, &tmp, 5);
ui->squelchGate->setValue(tmp);
d.readBool(12, &boolTmp, false);
ui->deltaSquelch->setChecked(boolTmp);
blockApplySettings(false);
m_channelMarker.blockSignals(false);
@ -208,9 +211,33 @@ void NFMDemodGUI::on_squelchGate_valueChanged(int value)
applySettings();
}
void NFMDemodGUI::on_deltaSquelch_toggled(bool checked)
{
if (ui->deltaSquelch->isChecked())
{
ui->squelchText->setText(QString("%1").arg((-ui->squelch->value()) / 10.0, 0, 'f', 1));
ui->squelchText->setToolTip(tr("Squelch deviation threshold (%)"));
}
else
{
ui->squelchText->setText(QString("%1").arg(ui->squelch->value() / 10.0, 0, 'f', 1));
ui->squelchText->setToolTip(tr("Squelch power threshold (dB)"));
}
applySettings();
}
void NFMDemodGUI::on_squelch_valueChanged(int value)
{
ui->squelchText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1));
if (ui->deltaSquelch->isChecked())
{
ui->squelchText->setText(QString("%1").arg(-value / 10.0, 0, 'f', 1));
ui->squelchText->setToolTip(tr("Squelch deviation threshold (%)"));
}
else
{
ui->squelchText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1));
ui->squelchText->setToolTip(tr("Squelch power threshold (dB)"));
}
applySettings();
}
@ -311,6 +338,9 @@ NFMDemodGUI::NFMDemodGUI(PluginAPI* pluginAPI, DeviceSourceAPI *deviceAPI, QWidg
m_deviceAPI->addChannelMarker(&m_channelMarker);
m_deviceAPI->addRollupWidget(this);
QChar delta = QChar(0x94, 0x03);
ui->deltaSquelch->setText(delta);
applySettings();
}
@ -347,7 +377,8 @@ void NFMDemodGUI::applySettings()
m_fmDev[ui->rfBW->currentIndex()],
ui->volume->value() / 10.0f,
ui->squelchGate->value(), // in 10ths of ms
ui->squelch->value() / 10.0f,
ui->deltaSquelch->isChecked(),
ui->squelch->value(), // -1000 -> 0
ui->ctcssOn->isChecked(),
ui->audioMute->isChecked());
}

View File

@ -47,6 +47,7 @@ private slots:
void on_afBW_valueChanged(int value);
void on_volume_valueChanged(int value);
void on_squelchGate_valueChanged(int value);
void on_deltaSquelch_toggled(bool checked);
void on_squelch_valueChanged(int value);
void on_ctcss_currentIndexChanged(int index);
void on_ctcssOn_toggled(bool checked);

View File

@ -53,7 +53,16 @@
<property name="spacing">
<number>3</number>
</property>
<property name="margin">
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
@ -390,6 +399,25 @@
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="deltaSquelch">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Toggle frequency deviation (on) or channel power (off) based squelch</string>
</property>
<property name="text">
<string>D</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="squelch">
<property name="maximumSize">
@ -422,13 +450,13 @@
<widget class="QLabel" name="squelchText">
<property name="minimumSize">
<size>
<width>0</width>
<width>34</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>40</width>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
@ -596,6 +624,11 @@
<header>gui/levelmeter.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../sdrbase/resources/res.qrc"/>

View File

@ -7,7 +7,7 @@
const PluginDescriptor NFMPlugin::m_pluginDescriptor = {
QString("NFM Demodulator"),
QString("3.2.0"),
QString("3.3.0"),
QString("(c) Edouard Griffiths, F4EXB"),
QString("https://github.com/f4exb/sdrangel"),
true,

View File

@ -51,6 +51,29 @@ public:
return (std::atan2(d.imag(), d.real()) / M_PI) * m_fmScaling;
}
/**
* Discriminator with phase detection using atan2 and frequency by derivation.
* This yields a precise deviation to sample rate ratio: Sample rate => +/-1.0
*/
Real phaseDiscriminatorDelta(const Complex& sample, long double& magsq, Real& fmDev)
{
Real fltI = sample.real();
Real fltQ = sample.imag();
magsq = fltI*fltI + fltQ*fltQ;
Real curArg = atan2_approximation2((float) fltQ, (float) fltI);
fmDev = (curArg - m_prevArg) / M_PI;
m_prevArg = curArg;
if (fmDev < -1.0f) {
fmDev += 2.0f;
} else if (fmDev > 1.0f) {
fmDev -= 2.0f;
}
return fmDev * m_fmScaling;
}
/**
* Alternative without atan at the expense of a slight distorsion on very wideband signals
* http://www.embedded.com/design/configurable-systems/4212086/DSP-Tricks--Frequency-demodulation-algorithms-
@ -73,14 +96,14 @@ public:
/**
* Second alternative
*/
Real phaseDiscriminator3(const Complex& sample, long double& magsq)
Real phaseDiscriminator3(const Complex& sample, long double& magsq, Real& fltVal)
{
Real fltI = sample.real();
Real fltQ = sample.imag();
double fltNorm;
Real fltNormI;
Real fltNormQ;
Real fltVal;
//Real fltVal;
magsq = fltI*fltI + fltQ*fltQ;
fltNorm = std::sqrt(magsq);
@ -91,7 +114,7 @@ public:
fltVal = m_fltPreviousI*(fltNormQ - m_fltPreviousQ2);
fltVal -= m_fltPreviousQ*(fltNormI - m_fltPreviousI2);
fltVal += 2.0f;
fltVal /= 2.0f; // normally it is /4
fltVal /= 4.0f; // normally it is /4
m_fltPreviousQ2 = m_fltPreviousQ;
m_fltPreviousI2 = m_fltPreviousI;
@ -110,7 +133,65 @@ private:
Real m_fltPreviousQ;
Real m_fltPreviousI2;
Real m_fltPreviousQ2;
Real m_prevArg;
float atan2_approximation1(float y, float x)
{
//http://pubs.opengroup.org/onlinepubs/009695399/functions/atan2.html
//Volkan SALMA
const float ONEQTR_PI = M_PI / 4.0;
const float THRQTR_PI = 3.0 * M_PI / 4.0;
float r, angle;
float abs_y = std::fabs(y) + 1e-10f; // kludge to prevent 0/0 condition
if ( x < 0.0f )
{
r = (x + abs_y) / (abs_y - x);
angle = THRQTR_PI;
}
else
{
r = (x - abs_y) / (x + abs_y);
angle = ONEQTR_PI;
}
angle += (0.1963f * r * r - 0.9817f) * r;
if ( y < 0.0f )
return( -angle ); // negate if in quad III or IV
else
return( angle );
}
#define PI_FLOAT 3.14159265f
#define PIBY2_FLOAT 1.5707963f
// |error| < 0.005
float atan2_approximation2( float y, float x )
{
if ( x == 0.0f )
{
if ( y > 0.0f ) return PIBY2_FLOAT;
if ( y == 0.0f ) return 0.0f;
return -PIBY2_FLOAT;
}
float atan;
float z = y/x;
if ( std::fabs( z ) < 1.0f )
{
atan = z/(1.0f + 0.28f*z*z);
if ( x < 0.0f )
{
if ( y < 0.0f ) return atan - PI_FLOAT;
return atan + PI_FLOAT;
}
}
else
{
atan = PIBY2_FLOAT - z/(z*z + 0.28f);
if ( y < 0.0f ) return atan - PI_FLOAT;
}
return atan;
}
};
#endif /* INCLUDE_DSP_PHASEDISCRI_H_ */

View File

@ -84,7 +84,7 @@
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Version 3.2.0 - Copyright (C) 2015-2017 Edouard Griffiths, F4EXB. &lt;/p&gt;&lt;p&gt;Code at &lt;a href=&quot;https://github.com/f4exb/sdrangel&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://github.com/f4exb/sdrangel&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;Many thanks to the original developers:&lt;/p&gt;&lt;p&gt;The osmocom developer team - especially horizon, Hoernchen &amp;amp; tnt.&lt;/p&gt;&lt;p&gt;Christian Daniel from maintech GmbH.&lt;/p&gt;&lt;p&gt;John Greb (hexameron) for the contributions in &lt;a href=&quot;https://github.com/hexameron/rtl-sdrangelove&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;RTL-SDRangelove&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;The following rules apply to the SDRangel main application and libsdrbase:&lt;br/&gt;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; either version 2 of the License, or (at your option) any later version. 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. You should have received a copy of the GNU General Public License along with this program. If not, see &lt;a href=&quot;http://www.gnu.org/licenses/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;http://www.gnu.org/licenses/&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;For the license of installed plugins, look into the plugin list.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Version 3.3.0 - Copyright (C) 2015-2017 Edouard Griffiths, F4EXB. &lt;/p&gt;&lt;p&gt;Code at &lt;a href=&quot;https://github.com/f4exb/sdrangel&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://github.com/f4exb/sdrangel&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;Many thanks to the original developers:&lt;/p&gt;&lt;p&gt;The osmocom developer team - especially horizon, Hoernchen &amp;amp; tnt.&lt;/p&gt;&lt;p&gt;Christian Daniel from maintech GmbH.&lt;/p&gt;&lt;p&gt;John Greb (hexameron) for the contributions in &lt;a href=&quot;https://github.com/hexameron/rtl-sdrangelove&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;RTL-SDRangelove&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;The following rules apply to the SDRangel main application and libsdrbase:&lt;br/&gt;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; either version 2 of the License, or (at your option) any later version. 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. You should have received a copy of the GNU General Public License along with this program. If not, see &lt;a href=&quot;http://www.gnu.org/licenses/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;http://www.gnu.org/licenses/&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;For the license of installed plugins, look into the plugin list.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>

View File

@ -453,9 +453,9 @@ void MainWindow::createStatusBar()
{
QString qtVersionStr = QString("Qt %1 ").arg(QT_VERSION_STR);
#if QT_VERSION >= 0x050400
m_showSystemWidget = new QLabel("SDRangel v3.2.0 " + qtVersionStr + QSysInfo::prettyProductName(), this);
m_showSystemWidget = new QLabel("SDRangel v3.3.0 " + qtVersionStr + QSysInfo::prettyProductName(), this);
#else
m_showSystemWidget = new QLabel("SDRangel v3.2.0 " + qtVersionStr, this);
m_showSystemWidget = new QLabel("SDRangel v3.3.0 " + qtVersionStr, this);
#endif
statusBar()->addPermanentWidget(m_showSystemWidget);