From 23184197167a57ef9e55a501d1875b2430acdd5d Mon Sep 17 00:00:00 2001 From: f4exb Date: Sun, 5 Mar 2017 06:22:05 +0100 Subject: [PATCH] NFM demod: new discriminator and optional FM deviatoin based squelch --- debian/changelog | 6 ++ plugins/channelrx/demodnfm/nfmdemod.cpp | 55 +++++++++----- plugins/channelrx/demodnfm/nfmdemod.h | 19 ++++- plugins/channelrx/demodnfm/nfmdemodgui.cpp | 35 ++++++++- plugins/channelrx/demodnfm/nfmdemodgui.h | 1 + plugins/channelrx/demodnfm/nfmdemodgui.ui | 39 +++++++++- plugins/channelrx/demodnfm/nfmplugin.cpp | 2 +- sdrbase/dsp/phasediscri.h | 87 +++++++++++++++++++++- sdrbase/gui/aboutdialog.ui | 2 +- sdrbase/mainwindow.cpp | 4 +- 10 files changed, 219 insertions(+), 31 deletions(-) diff --git a/debian/changelog b/debian/changelog index 77dced3fa..45cf143da 100644 --- a/debian/changelog +++ b/debian/changelog @@ -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 Thu, 02 Mar 2017 23:14:18 +0100 + sdrangel (3.2.0-1) unstable; urgency=medium * ATV demodulator for amateur Analog TV diff --git a/plugins/channelrx/demodnfm/nfmdemod.cpp b/plugins/channelrx/demodnfm/nfmdemod.cpp index 1d583f041..8c3d24a36 100644 --- a/plugins/channelrx/demodnfm/nfmdemod.cpp +++ b/plugins/channelrx/demodnfm/nfmdemod.cpp @@ -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; diff --git a/plugins/channelrx/demodnfm/nfmdemod.h b/plugins/channelrx/demodnfm/nfmdemod.h index ef8c9046b..88e0ff1cc 100644 --- a/plugins/channelrx/demodnfm/nfmdemod.h +++ b/plugins/channelrx/demodnfm/nfmdemod.h @@ -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), diff --git a/plugins/channelrx/demodnfm/nfmdemodgui.cpp b/plugins/channelrx/demodnfm/nfmdemodgui.cpp index 56a883008..6e6cade4f 100644 --- a/plugins/channelrx/demodnfm/nfmdemodgui.cpp +++ b/plugins/channelrx/demodnfm/nfmdemodgui.cpp @@ -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()); } diff --git a/plugins/channelrx/demodnfm/nfmdemodgui.h b/plugins/channelrx/demodnfm/nfmdemodgui.h index f9887e6d1..b4aebd08c 100644 --- a/plugins/channelrx/demodnfm/nfmdemodgui.h +++ b/plugins/channelrx/demodnfm/nfmdemodgui.h @@ -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); diff --git a/plugins/channelrx/demodnfm/nfmdemodgui.ui b/plugins/channelrx/demodnfm/nfmdemodgui.ui index e357c13ea..9cbce67ba 100644 --- a/plugins/channelrx/demodnfm/nfmdemodgui.ui +++ b/plugins/channelrx/demodnfm/nfmdemodgui.ui @@ -53,7 +53,16 @@ 3 - + + 2 + + + 2 + + + 2 + + 2 @@ -390,6 +399,25 @@ + + + + + 24 + 24 + + + + Toggle frequency deviation (on) or channel power (off) based squelch + + + D + + + true + + + @@ -422,13 +450,13 @@ - 0 + 34 0 - 40 + 16777215 16777215 @@ -596,6 +624,11 @@
gui/levelmeter.h
1 + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
diff --git a/plugins/channelrx/demodnfm/nfmplugin.cpp b/plugins/channelrx/demodnfm/nfmplugin.cpp index 19076811f..2545989f1 100644 --- a/plugins/channelrx/demodnfm/nfmplugin.cpp +++ b/plugins/channelrx/demodnfm/nfmplugin.cpp @@ -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, diff --git a/sdrbase/dsp/phasediscri.h b/sdrbase/dsp/phasediscri.h index fba979973..dcad85f54 100644 --- a/sdrbase/dsp/phasediscri.h +++ b/sdrbase/dsp/phasediscri.h @@ -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_ */ diff --git a/sdrbase/gui/aboutdialog.ui b/sdrbase/gui/aboutdialog.ui index 16cb3ccfb..3f0cd6634 100644 --- a/sdrbase/gui/aboutdialog.ui +++ b/sdrbase/gui/aboutdialog.ui @@ -84,7 +84,7 @@ - <html><head/><body><p>Version 3.2.0 - Copyright (C) 2015-2017 Edouard Griffiths, F4EXB. </p><p>Code at <a href="https://github.com/f4exb/sdrangel"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/f4exb/sdrangel</span></a></p><p>Many thanks to the original developers:</p><p>The osmocom developer team - especially horizon, Hoernchen &amp; tnt.</p><p>Christian Daniel from maintech GmbH.</p><p>John Greb (hexameron) for the contributions in <a href="https://github.com/hexameron/rtl-sdrangelove"><span style=" text-decoration: underline; color:#0000ff;">RTL-SDRangelove</span></a></p><p>The following rules apply to the SDRangel main application and libsdrbase:<br/>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 <a href="http://www.gnu.org/licenses/"><span style=" text-decoration: underline; color:#0000ff;">http://www.gnu.org/licenses/</span></a>.</p><p>For the license of installed plugins, look into the plugin list.</p></body></html> + <html><head/><body><p>Version 3.3.0 - Copyright (C) 2015-2017 Edouard Griffiths, F4EXB. </p><p>Code at <a href="https://github.com/f4exb/sdrangel"><span style=" text-decoration: underline; color:#0000ff;">https://github.com/f4exb/sdrangel</span></a></p><p>Many thanks to the original developers:</p><p>The osmocom developer team - especially horizon, Hoernchen &amp; tnt.</p><p>Christian Daniel from maintech GmbH.</p><p>John Greb (hexameron) for the contributions in <a href="https://github.com/hexameron/rtl-sdrangelove"><span style=" text-decoration: underline; color:#0000ff;">RTL-SDRangelove</span></a></p><p>The following rules apply to the SDRangel main application and libsdrbase:<br/>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 <a href="http://www.gnu.org/licenses/"><span style=" text-decoration: underline; color:#0000ff;">http://www.gnu.org/licenses/</span></a>.</p><p>For the license of installed plugins, look into the plugin list.</p></body></html> true diff --git a/sdrbase/mainwindow.cpp b/sdrbase/mainwindow.cpp index 0a37841d3..1ccf55e53 100644 --- a/sdrbase/mainwindow.cpp +++ b/sdrbase/mainwindow.cpp @@ -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);