NFM Modulator: basic input volume gauge

This commit is contained in:
f4exb 2016-12-02 02:00:53 +01:00
parent ab60cac358
commit 7a07b56b01
8 changed files with 436 additions and 77 deletions

View File

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

View File

@ -19,6 +19,7 @@
#include <QMutexLocker>
#include <stdio.h>
#include <complex.h>
#include <algorithm>
#include <dsp/upchannelizer.h>
#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<quint8*>(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

View File

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

View File

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

View File

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

View File

@ -50,16 +50,7 @@
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>2</number>
</property>
<item>
@ -329,6 +320,78 @@
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="volumeLayout">
<item>
<widget class="QLabel" name="volLabel">
<property name="text">
<string>Vol</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="volume">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Audio input volume</string>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="volumeText">
<property name="minimumSize">
<size>
<width>25</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Audio input volume level</string>
</property>
<property name="statusTip">
<string/>
</property>
<property name="text">
<string>1.0</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="LevelMeter" name="volumeMeter" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="recordFileSelectLayout">
<item>
@ -426,66 +489,15 @@
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="volLabel">
<property name="text">
<string>Vol</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="micVolume">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Audio input volume</string>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="micVolumeText">
<property name="minimumSize">
<size>
<width>25</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Audio input volume level</string>
</property>
<property name="statusTip">
<string/>
</property>
<property name="text">
<string>1.0</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="fileNameLayout">
<item>
@ -670,6 +682,12 @@
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
<customwidget>
<class>LevelMeter</class>
<extends>QWidget</extends>
<header>gui/levelmeter.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../../sdrbase/resources/res.qrc"/>

174
sdrbase/gui/levelmeter.cpp Normal file
View File

@ -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 <math.h>
#include <QPainter>
#include <QTimer>
#include <QDebug>
// 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<qreal>(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);
}

124
sdrbase/gui/levelmeter.h Normal file
View File

@ -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 <QTime>
#include <QWidget>
#include <dsp/dsptypes.h>
/**
* 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_ */