File: SignalMeter.cpp

package info (click to toggle)
js8call 2.5.1%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 24,720 kB
  • sloc: cpp: 562,655; sh: 898; python: 132; ansic: 102; makefile: 4
file content (196 lines) | stat: -rw-r--r-- 6,637 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// 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));
}