mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
197 lines
6.5 KiB
C++
197 lines
6.5 KiB
C++
|
|
// Simple bargraph dB meter
|
||
|
|
// Originally implemented by Edson Pereira PY2SDR
|
||
|
|
//
|
||
|
|
|
||
|
|
#include "SignalMeter.h"
|
||
|
|
#include "moc_SignalMeter.cpp"
|
||
|
|
#include <QFontMetrics>
|
||
|
|
#include <QHBoxLayout>
|
||
|
|
#include <QLabel>
|
||
|
|
#include <QPainter>
|
||
|
|
#include <QPolygon>
|
||
|
|
#include <QVBoxLayout>
|
||
|
|
#include <boost/circular_buffer.hpp>
|
||
|
|
|
||
|
|
// Meter component, which displays to the right of the scale, as a
|
||
|
|
// level gauge with a peak hold indicator. Displays green when the
|
||
|
|
// level is good, yellow when it's too low, red when it's too high.
|
||
|
|
|
||
|
|
class SignalMeter::Meter final : public QWidget {
|
||
|
|
public:
|
||
|
|
static constexpr int MAX = 100;
|
||
|
|
static constexpr int LO = 15;
|
||
|
|
static constexpr int HI = 85;
|
||
|
|
|
||
|
|
// We handle the peak hold calculation using a circular buffer that
|
||
|
|
// always contains the last 10 values; initially, it'll contain 10
|
||
|
|
// zeroes. The current meter reading is whatever the last value we
|
||
|
|
// put into the buffer was, and the peak hold level is the largest
|
||
|
|
// value in the buffer at any moment.
|
||
|
|
|
||
|
|
explicit Meter(QWidget *parent) : QWidget{parent} {}
|
||
|
|
|
||
|
|
QSize sizeHint() const override { return {10, 100}; }
|
||
|
|
|
||
|
|
int last() const { return m_values.back(); }
|
||
|
|
int peak() const { return m_peak; }
|
||
|
|
int max() const { return m_max; }
|
||
|
|
|
||
|
|
// Caller has provided us with exciting new information. Since GUI
|
||
|
|
// components are in general not thread-safe, we don't need to be
|
||
|
|
// concerned about locking here; this function can only be called
|
||
|
|
// by the setValue() function of the SignalMeter that created us,
|
||
|
|
// which is defined as a slot, should that need to be done by a
|
||
|
|
// non-GUI thread.
|
||
|
|
//
|
||
|
|
// This will get called very frequently, often sequentially with
|
||
|
|
// identical values, so to avoid needless repaints, we do need to
|
||
|
|
// take some care here to ensure that something actually did change
|
||
|
|
// such that we'd need to update.
|
||
|
|
|
||
|
|
void setValue(const int value, const int valueMax) {
|
||
|
|
auto const oldLast = last();
|
||
|
|
auto const oldPeak = peak();
|
||
|
|
auto const oldMax = max();
|
||
|
|
|
||
|
|
m_values.push_back(std::clamp(value, 0, MAX));
|
||
|
|
m_peak = *std::max_element(m_values.begin(), m_values.end());
|
||
|
|
m_max = valueMax;
|
||
|
|
|
||
|
|
if (last() != oldLast || peak() != oldPeak || max() != oldMax)
|
||
|
|
update();
|
||
|
|
}
|
||
|
|
|
||
|
|
protected:
|
||
|
|
// Draw the level bar, which might be of zero height, coloring it
|
||
|
|
// appropriately if we're above or below a warning threshold. If
|
||
|
|
// our peak level is non-zero, also draw the peak hold indicator.
|
||
|
|
|
||
|
|
void paintEvent(QPaintEvent *) override {
|
||
|
|
QPainter p{this};
|
||
|
|
p.setPen(Qt::NoPen);
|
||
|
|
|
||
|
|
if (m_max > HI) {
|
||
|
|
p.setBrush(Qt::red);
|
||
|
|
} else if (m_peak < LO) {
|
||
|
|
p.setBrush(Qt::yellow);
|
||
|
|
} else {
|
||
|
|
p.setBrush(Qt::green);
|
||
|
|
}
|
||
|
|
|
||
|
|
auto const target = contentsRect();
|
||
|
|
auto const scaled = [&target](auto const value) {
|
||
|
|
return QPoint{
|
||
|
|
target.left(),
|
||
|
|
static_cast<int>(target.top() + target.height() -
|
||
|
|
value / (double)MAX * target.height())};
|
||
|
|
};
|
||
|
|
|
||
|
|
p.drawRect(QRect{scaled(last()), target.bottomRight()});
|
||
|
|
|
||
|
|
if (peak()) {
|
||
|
|
p.setBrush(Qt::white);
|
||
|
|
p.setRenderHint(QPainter::Antialiasing);
|
||
|
|
p.translate(scaled(peak()));
|
||
|
|
p.drawPolygon(
|
||
|
|
QPolygon{{target.width(), -4}, {target.width(), 4}, {0, 0}});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
boost::circular_buffer<int> m_values{10};
|
||
|
|
int m_peak;
|
||
|
|
int m_max;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Scale component, which displays to the left of the meter.
|
||
|
|
|
||
|
|
class SignalMeter::Scale final : public QWidget {
|
||
|
|
private:
|
||
|
|
static constexpr int text_indent = 2;
|
||
|
|
static constexpr int tick_length = 4;
|
||
|
|
static constexpr std::size_t tick_range = 10;
|
||
|
|
static constexpr std::size_t tick_count = Meter::MAX / tick_range;
|
||
|
|
|
||
|
|
public:
|
||
|
|
explicit Scale(QWidget *parent) : QWidget{parent} {
|
||
|
|
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::MinimumExpanding);
|
||
|
|
}
|
||
|
|
|
||
|
|
QSize sizeHint() const override { return minimumSizeHint(); }
|
||
|
|
|
||
|
|
QSize minimumSizeHint() const override {
|
||
|
|
QFontMetrics metrics{font(), this};
|
||
|
|
return {metrics.horizontalAdvance("00+") + text_indent + tick_length,
|
||
|
|
static_cast<int>(metrics.height() * tick_count)};
|
||
|
|
}
|
||
|
|
|
||
|
|
protected:
|
||
|
|
void paintEvent(QPaintEvent *) override {
|
||
|
|
auto const target = contentsRect();
|
||
|
|
auto const metrics = QFontMetrics(font(), this);
|
||
|
|
auto const margin = metrics.height() / 2;
|
||
|
|
auto const offset = metrics.height() / 4;
|
||
|
|
auto const span = target.height() - metrics.height();
|
||
|
|
|
||
|
|
QPainter p{this};
|
||
|
|
p.setPen(Qt::white);
|
||
|
|
|
||
|
|
p.drawLine(target.right(), target.top() + margin, target.right(),
|
||
|
|
target.bottom() - margin);
|
||
|
|
|
||
|
|
for (std::size_t tick = 0; tick <= tick_count; ++tick) {
|
||
|
|
p.save();
|
||
|
|
p.translate(target.right() - tick_length,
|
||
|
|
target.top() + margin + tick * span / tick_count);
|
||
|
|
p.drawLine(0, 0, tick_length, 0);
|
||
|
|
if (tick & 1) {
|
||
|
|
auto const text =
|
||
|
|
QString::number(Meter::MAX - tick * tick_range);
|
||
|
|
p.drawText(-(text_indent + metrics.horizontalAdvance(text)),
|
||
|
|
offset, text);
|
||
|
|
}
|
||
|
|
p.restore();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Signal meter implementation; displays as a scaled level meter above
|
||
|
|
// a level value display.
|
||
|
|
|
||
|
|
SignalMeter::SignalMeter(QWidget *parent)
|
||
|
|
: QWidget{parent}, m_scale{new Scale{this}}, m_meter{new Meter{this}},
|
||
|
|
m_value{new QLabel{this}} {
|
||
|
|
auto outer_layout = new QVBoxLayout;
|
||
|
|
outer_layout->setSpacing(8);
|
||
|
|
|
||
|
|
auto inner_layout = new QHBoxLayout;
|
||
|
|
inner_layout->setContentsMargins(9, 0, 9, 0);
|
||
|
|
inner_layout->setSpacing(0);
|
||
|
|
|
||
|
|
auto label_layout = new QHBoxLayout;
|
||
|
|
label_layout->setSpacing(4);
|
||
|
|
|
||
|
|
auto const margin = QFontMetrics(m_scale->font(), m_scale).height() / 2;
|
||
|
|
|
||
|
|
m_meter->setContentsMargins(0, margin, 0, margin);
|
||
|
|
m_meter->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||
|
|
|
||
|
|
m_value->setAlignment(Qt::AlignRight);
|
||
|
|
|
||
|
|
inner_layout->addWidget(m_scale);
|
||
|
|
inner_layout->addWidget(m_meter);
|
||
|
|
|
||
|
|
label_layout->addWidget(m_value);
|
||
|
|
label_layout->addWidget(new QLabel("dB", this));
|
||
|
|
|
||
|
|
outer_layout->addLayout(inner_layout);
|
||
|
|
outer_layout->addLayout(label_layout);
|
||
|
|
|
||
|
|
setLayout(outer_layout);
|
||
|
|
}
|
||
|
|
|
||
|
|
void SignalMeter::setValue(const float value, const float valueMax) {
|
||
|
|
m_meter->setValue(value, valueMax);
|
||
|
|
m_value->setText(QString::number(value, 'f', 0));
|
||
|
|
}
|