Loading...
Searching...
No Matches
VUMeter.hpp
1#pragma once
2#include <Process/Monitoring/MonitorLink.hpp>
3
4#include <Effect/EffectLayer.hpp>
5
6#include <ossia/audio/meters.hpp>
7#include <Process/Monitoring/Monitor.hpp>
8
9#include <Ui/Monitor/ChannelLayout.hpp>
10#include <Ui/Monitor/Paint.hpp>
11#include <halp/audio.hpp>
12#include <halp/controls.hpp>
13#include <halp/meta.hpp>
14#include <score_plugin_ui_export.h>
15
16#include <array>
17#include <cmath>
18#include <memory>
19#include <optional>
20#include <vector>
21
22namespace Process
23{
24class ControlInlet;
25}
26
27namespace Ui::VUMeter
28{
29enum class Mode
30{
31 PeakRms,
32 TruePeak,
33 VU,
34 PpmDin,
35 PpmNordic,
36 PpmBbc,
37 PpmEbu,
38 K20,
39 K14,
40 K12,
41 Surround,
42 Bits
43};
44
45static constexpr int max_channels = 64;
46
49struct Frame
50{
51 int channels{};
52 Mode mode{};
54 std::array<float, max_channels> level{};
56 std::array<float, max_channels> secondary{};
57 std::array<float, max_channels> hold{};
59 std::array<float, max_channels> ring_correlation{};
61 std::array<uint32_t, max_channels> bits{};
62 std::array<float, max_channels> dc{};
63 uint64_t clipped{};
64};
65
67{
70 std::atomic_bool reset_clips{};
71};
72
74inline constexpr double kOffset(Mode m) noexcept
75{
76 return m == Mode::K20 ? 20 : m == Mode::K14 ? 14 : m == Mode::K12 ? 12 : 0;
77}
78
81inline double toScale(Mode m, double lin, double reference) noexcept
82{
83 const double db = ossia::detail::LinearGainToDecibels(std::max(lin, 1e-10));
84 switch(m)
85 {
86 case Mode::VU:
87 case Mode::PpmNordic:
88 case Mode::PpmEbu:
89 return db - reference;
90 case Mode::PpmDin:
91 // Alignment level reads -9 on a DIN meter
92 return db - reference - 9.;
93 case Mode::PpmBbc:
94 // Marks 1 to 7, 4 dB apart, 4 at alignment level
95 return 4. + (db - reference) / 4.;
96 default:
97 return db + kOffset(m);
98 }
99}
100
103struct Scale
104{
105 double lo{}, hi{};
106 double warn{}, over{};
107 QString unit;
108 std::vector<Ui::Monitor::Tick> ticks;
109 Mode mode{};
110
111 double value(double lin, double reference) const noexcept
112 {
113 return toScale(mode, lin, reference);
114 }
115};
116
117SCORE_PLUGIN_UI_EXPORT Scale scaleFor(Mode m, double floor, double ceiling);
118
119inline constexpr std::string_view mode_names[]{
120 "Peak + RMS", "True peak", "VU", "PPM DIN", "PPM Nordic", "PPM BBC",
121 "PPM EBU", "K-20", "K-14", "K-12", "Surround", "Bits"};
122
123struct Layer;
124
125struct Node
126{
127 halp_meta(name, "Audio meter")
128 halp_meta(c_name, "VUMeter")
129 halp_meta(category, "Monitoring/Audio")
130 halp_meta(author, "ossia score")
131 halp_meta(manual_url, "")
132 halp_meta(
133 description,
134 "Multichannel level meter: sample peak and RMS, true peak, VU, IEC "
135 "programme meters (DIN, Nordic, BBC, EBU), K-System, surround and bit "
136 "usage")
137 halp_meta(uuid, "0d0a3152-8ee9-4472-8a97-457b8bd6e56a")
138 halp_flag(fully_custom_item);
140
141 struct
142 {
143 halp::dynamic_audio_bus<"Audio", double> audio;
144
145 struct : halp::combobox_t<"Mode", Mode>
146 {
147 halp_meta(description, "Which meter: each follows its own standard's ballistics and scale.")
148 struct range
149 {
150 std::string_view values[std::size(mode_names)]{
151 mode_names[0], mode_names[1], mode_names[2], mode_names[3],
152 mode_names[4], mode_names[5], mode_names[6], mode_names[7],
153 mode_names[8], mode_names[9], mode_names[10], mode_names[11]};
154 Mode init{Mode::PeakRms};
155 };
156 } mode;
157
158 struct : halp::spinbox_f32<"Reference", halp::range{-40., 0., -18.}>
159 {
160 halp_meta(
161 description,
162 "Alignment level in dBFS: what reads 0 VU, TEST on Nordic and EBU "
163 "meters, 4 on BBC meters and -9 on DIN meters.")
164 } reference;
165
166 struct : halp::spinbox_f32<"Floor", halp::range{-140., -6., -60.}>
167 {
168 halp_meta(description, "Bottom of the dBFS scale")
169 } floor;
170 struct : halp::spinbox_f32<"Ceiling", halp::range{-6., 24., 0.}>
171 {
172 halp_meta(description, "Top of the dBFS scale")
173 } ceiling;
174
175 struct : halp::spinbox_f32<"Hold", halp::range{0., 30., 1.5}>
176 {
177 halp_meta(description, "Seconds a peak stays marked before falling")
178 } hold;
179 struct : halp::spinbox_f32<"Fall", halp::range{1., 60., 20.}>
180 {
181 halp_meta(description, "Fall rate of sample and true peak bars, in dB/s")
182 } fall;
183
184 halp::impulse_button<"Reset clips"> reset;
185
186 struct : halp::combobox_t<"Layout", Ui::Monitor::ChannelLayout>
187 {
188 halp_meta(description, "Speaker layout: channel names and surround positions")
189 struct range
190 {
191 std::string_view values[7]{
192 Ui::Monitor::channel_layout_names[0], Ui::Monitor::channel_layout_names[1],
193 Ui::Monitor::channel_layout_names[2], Ui::Monitor::channel_layout_names[3],
194 Ui::Monitor::channel_layout_names[4], Ui::Monitor::channel_layout_names[5],
195 Ui::Monitor::channel_layout_names[6]};
196 Ui::Monitor::ChannelLayout init{Ui::Monitor::ChannelLayout::Auto};
197 };
198 } layout;
199 } inputs;
200
201 struct
202 {
204 struct : halp::val_port<"Levels", std::optional<std::vector<float>>>
205 {
206 halp_meta(description, "Level of each channel on the current scale, about 30 times a second")
207 } levels;
208 struct : halp::val_port<"Clip", bool>
209 {
210 halp_meta(description, "Whether any channel went over full scale since the last reset")
211 } clip;
212 } outputs;
213
214 std::shared_ptr<MeterMonitor> monitor = std::make_shared<MeterMonitor>();
215
216 struct Channel
217 {
218 ossia::meters::sample_peak peak;
219 ossia::meters::rms rms;
220 ossia::meters::vu vu;
221 ossia::meters::ppm ppm;
222 ossia::meters::true_peak tp;
223 ossia::meters::peak_hold hold;
224 ossia::meters::correlation ring_corr;
225 double tp_level{};
226 double dc{};
227 uint32_t bits{};
228 };
229 std::vector<Channel> channels = std::vector<Channel>(max_channels);
232 ossia::meters::ppm ppm_type_i, ppm_type_ii;
233 ossia::meters::true_peak tp_proto;
234
235 double rate{};
236 Mode current{Mode::PeakRms};
237 float current_hold{-1}, current_fall{-1};
238 double tp_fall{};
239 uint64_t clipped{};
240 double since_output{};
241 Frame frame;
242
245 void prepare(halp::setup s)
246 {
247 const double r = s.rate > 0 ? s.rate : 48000.;
248 if(r == rate)
249 return;
250 rate = r;
251 ppm_type_i.prepare(rate, ossia::meters::ppm::type_i);
252 ppm_type_ii.prepare(rate, ossia::meters::ppm::type_ii);
253 tp_proto.prepare(rate);
254 reset_followers();
255 }
256
257 void reset_followers() noexcept
258 {
259 const auto& ppm_proto
260 = (current == Mode::PpmDin || current == Mode::PpmNordic) ? ppm_type_i
261 : ppm_type_ii;
262 const bool k = current == Mode::K20 || current == Mode::K14 || current == Mode::K12;
263 for(auto& c : channels)
264 {
265 c.rms.prepare(rate, k ? 0.6 : 0.3, k);
266 c.vu.prepare(rate);
267 c.ppm = ppm_proto;
268 c.ppm.env = 0;
269 c.tp = tp_proto;
270 c.ring_corr.prepare(rate, 0.3);
271 c.peak.level = 0;
272 c.hold.value = 0;
273 c.hold.since = 0;
274 c.tp_level = 0;
275 c.dc = 0;
276 c.bits = 0;
277 }
279 }
280
282 void set_ballistics() noexcept
283 {
284 const double fall = ossia::meters::fall_coefficient(inputs.fall.value, rate);
285 for(auto& c : channels)
286 {
287 c.peak.fall = fall;
288 c.hold.hold_frames = inputs.hold.value * rate;
289 c.hold.fall_per_frame = fall;
290 }
291 tp_fall = fall;
292 current_hold = inputs.hold.value;
293 current_fall = inputs.fall.value;
294 }
295
296 void operator()(int frames)
297 {
298 const int nch = std::min(inputs.audio.channels, max_channels);
299 if(nch == 0 || frames <= 0)
300 return;
301
302 if(inputs.mode.value != current)
303 {
304 current = inputs.mode.value;
305 reset_followers();
306 }
307 else if(inputs.hold.value != current_hold || inputs.fall.value != current_fall)
308 {
310 }
311 if(inputs.reset || monitor->reset_clips.exchange(false, std::memory_order_relaxed))
312 clipped = 0;
313
314 const double dc_coef = 1. - std::exp(-frames / (1. * rate));
315
316 for(int c = 0; c < nch; c++)
317 {
318 auto& ch = channels[c];
319 const double* in = inputs.audio[c];
320 ch.peak.process(in, frames);
321 double block_peak = 0;
322 for(int i = 0; i < frames; i++)
323 block_peak = std::max(block_peak, std::abs(in[i]));
324
325 float level{}, secondary{};
326 double clip_level = block_peak;
327 switch(current)
328 {
329 case Mode::PeakRms:
330 case Mode::Surround:
331 ch.rms.process(in, frames);
332 level = ch.peak.level;
333 secondary = ch.rms.level();
334 ch.hold.feed(block_peak, frames);
335 break;
336 case Mode::TruePeak:
337 ch.tp.process(in, frames);
338 ch.tp_level = std::max(ch.tp.block_peak, ch.tp_level * std::pow(tp_fall, frames));
339 level = ch.tp_level;
340 secondary = ch.peak.level;
341 clip_level = ch.tp.block_peak;
342 ch.hold.feed(ch.tp.block_peak, frames);
343 break;
344 case Mode::VU:
345 ch.vu.process(in, frames);
346 level = ch.vu.level();
347 ch.hold.feed(level, frames);
348 break;
349 case Mode::PpmDin:
350 case Mode::PpmNordic:
351 case Mode::PpmBbc:
352 case Mode::PpmEbu:
353 ch.ppm.process(in, frames);
354 level = ch.ppm.level();
355 ch.hold.feed(level, frames);
356 break;
357 case Mode::K20:
358 case Mode::K14:
359 case Mode::K12:
360 ch.rms.process(in, frames);
361 level = ch.rms.level();
362 secondary = ch.peak.level;
363 ch.hold.feed(block_peak, frames);
364 break;
365 case Mode::Bits: {
366 level = ch.peak.level;
367 double sum = 0;
368 uint32_t bits = 0;
369 for(int i = 0; i < frames; i++)
370 {
371 sum += in[i];
372 const auto q = (int64_t)std::llround(std::clamp(in[i], -1., 1.) * 8388607.);
373 bits |= uint32_t(q < 0 ? -q : q);
374 }
375 ch.bits = bits;
376 ch.dc += dc_coef * (sum / frames - ch.dc);
377 ch.hold.feed(block_peak, frames);
378 break;
379 }
380 }
381 if(clip_level >= 1.)
382 clipped |= (uint64_t(1) << c);
383
384 frame.level[c] = level;
385 frame.secondary[c] = secondary;
386 frame.hold[c] = ch.hold.value;
387 frame.bits[c] = ch.bits;
388 frame.dc[c] = ch.dc;
389 }
390
391 if(current == Mode::Surround)
392 {
393 const auto r = Ui::Monitor::ring(inputs.layout.value, nch);
394 for(int i = 0; i < r.size; i++)
395 {
396 const int a = r.channel[i], b = r.channel[(i + 1) % r.size];
397 auto& corr = channels[a].ring_corr;
398 corr.process(inputs.audio[a], inputs.audio[b], frames);
399 frame.ring_correlation[i] = corr.value();
400 }
401 }
402
403 frame.channels = nch;
404 frame.mode = current;
405 frame.clipped = clipped;
406 if(monitor->watched())
407 monitor->frames.produce(frame);
408
409 outputs.clip.value = clipped != 0;
410
411 // The levels are for patching: a few lists a second are plenty, and
412 // each one is a new message
413 since_output += frames;
414 if(since_output >= rate / 30.)
415 {
416 since_output = 0;
417 auto& out = outputs.levels.value.emplace(nch);
418 for(int c = 0; c < nch; c++)
419 out[c] = float(toScale(current, frame.level[c], inputs.reference.value));
420 }
421 }
422};
423
425{
426 static constexpr double label_width = 30.;
428 static constexpr double header = 18.;
429 static constexpr double top_margin = 28.;
430 static constexpr double bottom_margin = 12.;
431
432 Layer(
433 const Process::ProcessModel& process, const Process::Context& doc,
434 QGraphicsItem* parent);
435
436 void mousePressEvent(QGraphicsSceneMouseEvent* ev) override;
437 void paint_impl(QPainter* p) const override;
438
439private:
440 struct StripGeometry
441 {
442 double start{}, width{}, spacing{};
443 };
444
445 Mode mode() const noexcept;
446 double reference() const noexcept;
447 Ui::Monitor::ChannelLayout layout() const noexcept;
448 void updateScale();
449
450 StripGeometry strips(const QRectF& area, int nch) const;
451 void drawBars(QPainter* p, const QRectF& area) const;
452 void drawNeedles(QPainter* p, const QRectF& area) const;
453 void drawBits(QPainter* p, const QRectF& area) const;
454 void drawSurround(QPainter* p, const QRectF& r) const;
455
456 const Process::ControlInlet* m_mode{};
457 const Process::ControlInlet* m_reference{};
458 const Process::ControlInlet* m_floor{};
459 const Process::ControlInlet* m_ceiling{};
460 const Process::ControlInlet* m_layout{};
461 Process::MonitorLink* m_link{};
462 Frame m_frame;
464 Scale m_scale;
465};
466}
Definition Port.hpp:247
Definition EffectLayer.hpp:17
Definition Monitor.hpp:59
The Process class.
Definition score-lib-process/Process/Process.hpp:82
Base classes and tools to implement processes and layers.
Definition JSONVisitor.hpp:1115
Definition ProcessContext.hpp:12
Definition Monitor.hpp:27
Definition VUMeter.hpp:50
std::array< float, max_channels > level
The level the bar shows.
Definition VUMeter.hpp:54
std::array< float, max_channels > ring_correlation
Correlation of each channel with the next one around the listener.
Definition VUMeter.hpp:59
std::array< uint32_t, max_channels > bits
Bits used by the samples of each channel, as 24-bit integers.
Definition VUMeter.hpp:61
std::array< float, max_channels > secondary
A second, thinner bar: RMS under the peak, peak beside the RMS.
Definition VUMeter.hpp:56
Definition VUMeter.hpp:425
static constexpr double header
The mode switch, then the clip indicators.
Definition VUMeter.hpp:428
Definition VUMeter.hpp:67
std::atomic_bool reset_clips
Set by the view to clear the clip indicators.
Definition VUMeter.hpp:70
Definition VUMeter.hpp:217
Definition VUMeter.hpp:126
ossia::meters::ppm ppm_type_i
Definition VUMeter.hpp:232
void set_ballistics() noexcept
Hold time and fall rate: the peak followers keep their level.
Definition VUMeter.hpp:282
void prepare(halp::setup s)
Definition VUMeter.hpp:245
Definition VUMeter.hpp:104
Definition MIDISync.hpp:149