diff --git a/CMakeLists.txt b/CMakeLists.txt index c7639a793..8adf24af2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,6 +144,7 @@ set(sdrbase_SOURCES sdrbase/gui/glspectrum.cpp sdrbase/gui/glspectrumgui.cpp sdrbase/gui/indicator.cpp + sdrbase/gui/levelmeter.cpp sdrbase/gui/mypositiondialog.cpp sdrbase/gui/pluginsdialog.cpp sdrbase/gui/audiodialog.cpp @@ -249,6 +250,7 @@ set(sdrbase_HEADERS sdrbase/gui/glspectrum.h sdrbase/gui/glspectrumgui.h sdrbase/gui/indicator.h + sdrbase/gui/levelmeter.h sdrbase/gui/mypositiondialog.h sdrbase/gui/physicalunit.h sdrbase/gui/pluginsdialog.h diff --git a/plugins/channeltx/modnfm/nfmmod.cpp b/plugins/channeltx/modnfm/nfmmod.cpp index c97f5f521..6accb07ca 100644 --- a/plugins/channeltx/modnfm/nfmmod.cpp +++ b/plugins/channeltx/modnfm/nfmmod.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include "dsp/dspengine.h" #include "dsp/pidcontroller.h" @@ -32,6 +33,7 @@ MESSAGE_CLASS_DEFINITION(NFMMod::MsgConfigureFileSourceStreamTiming, Message) MESSAGE_CLASS_DEFINITION(NFMMod::MsgReportFileSourceStreamData, Message) MESSAGE_CLASS_DEFINITION(NFMMod::MsgReportFileSourceStreamTiming, Message) +const int NFMMod::m_levelNbSamples = 480; // every 10ms NFMMod::NFMMod() : m_modPhasor(0.0f), @@ -40,7 +42,10 @@ NFMMod::NFMMod() : m_fileSize(0), m_recordLength(0), m_sampleRate(48000), - m_afInput(NFMModInputNone) + m_afInput(NFMModInputNone), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f) { setObjectName("NFMod"); @@ -127,6 +132,7 @@ void NFMMod::modulateSample() Real t; pullAF(t); + calculateLevel(t); m_modPhasor += (m_running.m_fmDeviation / (float) m_running.m_audioSampleRate) * m_bandpass.filter(t) * (M_PI / 1208.0f); m_modSample.real(cos(m_modPhasor) * 32678.0f); @@ -173,7 +179,7 @@ void NFMMod::pullAF(Real& sample) break; case NFMModInputAudio: m_audioFifo.read(reinterpret_cast(audioSample), 1, 10); - sample = ((audioSample[0] + audioSample[1]) / 131072.0f) * m_running.m_volumeFactor; + sample = ((audioSample[0] + audioSample[1]) / 65536.0f) * m_running.m_volumeFactor; break; case NFMModInputNone: default: @@ -182,6 +188,25 @@ void NFMMod::pullAF(Real& sample) } } +void NFMMod::calculateLevel(Real& sample) +{ + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), sample); + m_levelSum += sample * sample; + m_levelCalcCount++; + } + else + { + qreal rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + //qDebug("NFMMod::calculateLevel: %f %f", rmsLevel, m_peakLevel); + emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples); + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + void NFMMod::start() { qDebug() << "NFMMod::start: m_outputSampleRate: " << m_config.m_outputSampleRate diff --git a/plugins/channeltx/modnfm/nfmmod.h b/plugins/channeltx/modnfm/nfmmod.h index 3ff477d1e..72bc09e08 100644 --- a/plugins/channeltx/modnfm/nfmmod.h +++ b/plugins/channeltx/modnfm/nfmmod.h @@ -191,6 +191,16 @@ public: Real getMagSq() const { return m_magsq; } +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + + private: class MsgConfigureNFMMod : public Message { @@ -304,9 +314,14 @@ private: int m_sampleRate; NFMModInputAF m_afInput; + quint32 m_levelCalcCount; + Real m_peakLevel; + Real m_levelSum; + static const int m_levelNbSamples; void apply(); void pullAF(Real& sample); + void calculateLevel(Real& sample); void modulateSample(); void openFileStream(); void seekFileStream(int seekPercentage); diff --git a/plugins/channeltx/modnfm/nfmmodgui.cpp b/plugins/channeltx/modnfm/nfmmodgui.cpp index 7b2b5a2be..f6dee3915 100644 --- a/plugins/channeltx/modnfm/nfmmodgui.cpp +++ b/plugins/channeltx/modnfm/nfmmodgui.cpp @@ -78,7 +78,7 @@ void NFMModGUI::resetToDefaults() ui->afBW->setValue(3); ui->fmDev->setValue(50); ui->toneFrequency->setValue(100); - ui->micVolume->setValue(10); + ui->volume->setValue(10); ui->deltaFrequency->setValue(0); blockApplySettings(false); @@ -94,7 +94,7 @@ QByteArray NFMModGUI::serialize() const s.writeS32(4, ui->fmDev->value()); s.writeU32(5, m_channelMarker.getColor().rgb()); s.writeS32(6, ui->toneFrequency->value()); - s.writeS32(7, ui->micVolume->value()); + s.writeS32(7, ui->volume->value()); return s.final(); } @@ -134,7 +134,7 @@ bool NFMModGUI::deserialize(const QByteArray& data) d.readS32(6, &tmp, 100); ui->toneFrequency->setValue(tmp); d.readS32(7, &tmp, 10); - ui->micVolume->setValue(tmp); + ui->volume->setValue(tmp); blockApplySettings(false); m_channelMarker.blockSignals(false); @@ -227,9 +227,9 @@ void NFMModGUI::on_fmDev_valueChanged(int value) applySettings(); } -void NFMModGUI::on_micVolume_valueChanged(int value) +void NFMModGUI::on_volume_valueChanged(int value) { - ui->micVolumeText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1)); + ui->volumeText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1)); applySettings(); } @@ -386,6 +386,7 @@ NFMModGUI::NFMModGUI(PluginAPI* pluginAPI, DeviceSinkAPI *deviceAPI, QWidget* pa applySettings(); connect(m_nfmMod->getOutputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); + connect(m_nfmMod, SIGNAL(levelChanged(qreal, qreal, int)), ui->volumeMeter, SLOT(levelChanged(qreal, qreal, int))); } NFMModGUI::~NFMModGUI() @@ -422,7 +423,7 @@ void NFMModGUI::applySettings() ui->afBW->value() * 1000.0, ui->fmDev->value() * 100.0f, // value is in '100 Hz ui->toneFrequency->value() * 10.0f, - ui->micVolume->value() / 10.0f, + ui->volume->value() / 10.0f, ui->audioMute->isChecked(), ui->playLoop->isChecked()); } diff --git a/plugins/channeltx/modnfm/nfmmodgui.h b/plugins/channeltx/modnfm/nfmmodgui.h index 2c9bd7ac7..0f0091b29 100644 --- a/plugins/channeltx/modnfm/nfmmodgui.h +++ b/plugins/channeltx/modnfm/nfmmodgui.h @@ -65,7 +65,7 @@ private slots: void on_afBW_valueChanged(int value); void on_fmDev_valueChanged(int value); void on_toneFrequency_valueChanged(int value); - void on_micVolume_valueChanged(int value); + void on_volume_valueChanged(int value); void on_audioMute_toggled(bool checked); void on_tone_toggled(bool checked); void on_mic_toggled(bool checked); diff --git a/plugins/channeltx/modnfm/nfmmodgui.ui b/plugins/channeltx/modnfm/nfmmodgui.ui index 44d237d04..0f83e2df1 100644 --- a/plugins/channeltx/modnfm/nfmmodgui.ui +++ b/plugins/channeltx/modnfm/nfmmodgui.ui @@ -50,16 +50,7 @@ 3 - - 2 - - - 2 - - - 2 - - + 2 @@ -329,6 +320,78 @@ + + + + Qt::Horizontal + + + + + + + + + Vol + + + + + + + + 24 + 24 + + + + Audio input volume + + + 100 + + + 1 + + + 10 + + + + + + + + 25 + 0 + + + + Audio input volume level + + + + + + 1.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + @@ -426,66 +489,15 @@ - - - - Qt::Vertical - - - - - - - Vol - - - - - - - - 24 - 24 - - - - Audio input volume - - - 100 - - - 1 - - - 10 - - - - - - - - 25 - 0 - - - - Audio input volume level - - - - - - 1.0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - + + + + Qt::Horizontal + + + @@ -670,6 +682,12 @@ QToolButton
gui/buttonswitch.h
+ + LevelMeter + QWidget +
gui/levelmeter.h
+ 1 +
diff --git a/sdrbase/gui/levelmeter.cpp b/sdrbase/gui/levelmeter.cpp new file mode 100644 index 000000000..245afb48e --- /dev/null +++ b/sdrbase/gui/levelmeter.cpp @@ -0,0 +1,174 @@ +/**************************************************************************** + * Copyright (C) 2016 Edouard Griffiths, F4EXB + * Modifications made to: + * - use the widget horizontally + * - differentiate each area with a different color +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "gui/levelmeter.h" + +#include + +#include +#include +#include + +// Constants +const int RedrawInterval = 100; // ms +const qreal PeakDecayRate = 0.001; +const int PeakHoldLevelDuration = 2000; // ms + +LevelMeter::LevelMeter(QWidget *parent) + : QWidget(parent) + , m_rmsLevel(0.0) + , m_peakLevel(0.0) + , m_decayedPeakLevel(0.0) + , m_peakDecayRate(PeakDecayRate) + , m_peakHoldLevel(0.0) + , m_redrawTimer(new QTimer(this)) + , m_rmsColor(Qt::green) // m_rmsColor(Qt::red) + , m_decayedPeakColor(Qt::yellow) + , m_peakColor(255, 0, 0, 255) // m_peakColor(255, 200, 200, 255) +{ + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + setMinimumWidth(30); + + connect(m_redrawTimer, SIGNAL(timeout()), this, SLOT(redrawTimerExpired())); + m_redrawTimer->start(RedrawInterval); +} + +LevelMeter::~LevelMeter() +{ + +} + +void LevelMeter::reset() +{ + m_rmsLevel = 0.0; + m_peakLevel = 0.0; + update(); +} + +void LevelMeter::levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples) +{ + // Smooth the RMS signal + const qreal smooth = pow(qreal(0.9), static_cast(numSamples) / 256); // TODO: remove this magic number + m_rmsLevel = (m_rmsLevel * smooth) + (rmsLevel * (1.0 - smooth)); + + if (peakLevel > m_decayedPeakLevel) { + m_peakLevel = peakLevel; + m_decayedPeakLevel = peakLevel; + m_peakLevelChanged.start(); + } + + if (peakLevel > m_peakHoldLevel) { + m_peakHoldLevel = peakLevel; + m_peakHoldLevelChanged.start(); + } + + update(); +} + +void LevelMeter::redrawTimerExpired() +{ + // Decay the peak signal + const int elapsedMs = m_peakLevelChanged.elapsed(); + const qreal decayAmount = m_peakDecayRate * elapsedMs; + if (decayAmount < m_peakLevel) + m_decayedPeakLevel = m_peakLevel - decayAmount; + else + m_decayedPeakLevel = 0.0; + + // Check whether to clear the peak hold level + if (m_peakHoldLevelChanged.elapsed() > PeakHoldLevelDuration) + m_peakHoldLevel = 0.0; + + update(); +} + +void LevelMeter::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.fillRect(rect(), Qt::black); + + QRect bar = rect(); + + // old + +// bar.setTop(rect().top() + (1.0 - m_peakHoldLevel) * rect().height()); +// bar.setBottom(bar.top() + 5); +// painter.fillRect(bar, m_rmsColor); +// bar.setBottom(rect().bottom()); +// +// bar.setTop(rect().top() + (1.0 - m_decayedPeakLevel) * rect().height()); +// painter.fillRect(bar, m_peakColor); +// +// bar.setTop(rect().top() + (1.0 - m_rmsLevel) * rect().height()); +// painter.fillRect(bar, m_rmsColor); + + // old v.2 + +// bar.setTop(rect().top() + (1.0 - m_peakHoldLevel) * rect().height()); +// bar.setBottom(bar.top() + 5); +// painter.fillRect(bar, m_peakColor); +// bar.setBottom(rect().bottom()); +// +// bar.setTop(rect().top() + (1.0 - m_decayedPeakLevel) * rect().height()); +// painter.fillRect(bar, m_decayedPeakColor); +// +// bar.setTop(rect().top() + (1.0 - m_rmsLevel) * rect().height()); +// painter.fillRect(bar, m_rmsColor); + + // new + + bar.setRight(rect().right() - (1.0 - m_peakHoldLevel) * rect().width()); + bar.setLeft(bar.right() - 5); + painter.fillRect(bar, m_peakColor); + bar.setLeft(rect().left()); + + bar.setRight(rect().right() - (1.0 - m_decayedPeakLevel) * rect().width()); + painter.fillRect(bar, m_decayedPeakColor); + + bar.setRight(rect().right() - (1.0 - m_rmsLevel) * rect().width()); + painter.fillRect(bar, m_rmsColor); + +} diff --git a/sdrbase/gui/levelmeter.h b/sdrbase/gui/levelmeter.h new file mode 100644 index 000000000..d154dfa9c --- /dev/null +++ b/sdrbase/gui/levelmeter.h @@ -0,0 +1,124 @@ +/**************************************************************************** + * Copyright (C) 2016 Edouard Griffiths, F4EXB + * Modifications made to: + * - use the widget horizontally + * - differentiate each area with a different color +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SDRBASE_GUI_LEVELMETER_H_ +#define SDRBASE_GUI_LEVELMETER_H_ + +#include +#include +#include + +/** + * Widget which displays a vertical audio level meter, indicating the + * RMS and peak levels of the window of audio samples most recently analyzed + * by the Engine. + */ +class LevelMeter : public QWidget +{ + Q_OBJECT + +public: + explicit LevelMeter(QWidget *parent = 0); + ~LevelMeter(); + + void paintEvent(QPaintEvent *event); + +public slots: + void reset(); + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private slots: + void redrawTimerExpired(); + +private: + /** + * Height of RMS level bar. + * Range 0.0 - 1.0. + */ + qreal m_rmsLevel; + + /** + * Most recent peak level. + * Range 0.0 - 1.0. + */ + qreal m_peakLevel; + + /** + * Height of peak level bar. + * This is calculated by decaying m_peakLevel depending on the + * elapsed time since m_peakLevelChanged, and the value of m_decayRate. + */ + qreal m_decayedPeakLevel; + + /** + * Time at which m_peakLevel was last changed. + */ + QTime m_peakLevelChanged; + + /** + * Rate at which peak level bar decays. + * Expressed in level units / millisecond. + */ + qreal m_peakDecayRate; + + /** + * High watermark of peak level. + * Range 0.0 - 1.0. + */ + qreal m_peakHoldLevel; + + /** + * Time at which m_peakHoldLevel was last changed. + */ + QTime m_peakHoldLevelChanged; + + QTimer *m_redrawTimer; + + QColor m_rmsColor; + QColor m_peakColor; + QColor m_decayedPeakColor; + +}; + +#endif /* SDRBASE_GUI_LEVELMETER_H_ */