Loading...
Searching...
No Matches
Spectrum.hpp
1#pragma once
2#include <Process/Monitoring/MonitorLink.hpp>
3
4#include <Ui/Monitor/LayerHelpers.hpp>
5#include <Process/Monitoring/SampleRing.hpp>
6
7#include <Effect/EffectLayer.hpp>
8
9#include <QColor>
10#include <QElapsedTimer>
11#include <QImage>
12
13#include <Ui/Monitor/SpectrumAnalysis.hpp>
14#include <halp/audio.hpp>
15#include <halp/controls.hpp>
16#include <halp/meta.hpp>
17
18#include <array>
19#include <cmath>
20#include <optional>
21#include <vector>
22
23namespace Process
24{
25class ControlInlet;
26}
27
28namespace Ui::Spectrum
29{
30enum class Mode
31{
32 Analyser,
33 RTA,
34 Spectrogram
35};
36inline constexpr std::string_view mode_names[]{"Analyser", "RTA", "Spectrogram"};
37
38enum class Size
39{
40 S1024,
41 S2048,
42 S4096,
43 S8192,
44 S16384,
45 S32768
46};
47inline constexpr std::string_view size_names[]{"1024", "2048", "4096", "8192", "16384", "32768"};
48
49inline constexpr std::string_view window_names[]{"Hann", "Blackman-Harris", "Flat top"};
50
51enum class Slope
52{
53 Flat,
54 Pink,
55 Tilt45
56};
57inline constexpr std::string_view slope_names[]{"0 dB/oct", "3 dB/oct", "4.5 dB/oct"};
58
59enum class Smoothing
60{
61 Off,
62 Twelfth,
63 Sixth,
64 Third
65};
66inline constexpr std::string_view smoothing_names[]{"Off", "1/12 oct", "1/6 oct", "1/3 oct"};
67
68enum class Channels
69{
70 Sum,
71 Each,
72 MidSide,
73 Left,
74 Right
75};
76inline constexpr std::string_view channels_names[]{"Sum", "Each", "Mid and side", "Left", "Right"};
77
79{
80 Process::SampleRing ring{1 << 17};
81};
82
83inline double slopeDbPerOctave(Slope s)
84{
85 return s == Slope::Pink ? 3. : s == Slope::Tilt45 ? 4.5 : 0.;
86}
87
88struct Layer;
89
90struct Node
91{
92 halp_meta(name, "Spectrum")
93 halp_meta(c_name, "SpectrumAnalyser")
94 halp_meta(category, "Monitoring/Audio")
95 halp_meta(author, "ossia score")
96 halp_meta(manual_url, "")
97 halp_meta(
98 description,
99 "Spectrum analyser, third-octave RTA and spectrogram, calibrated "
100 "so that a full-scale sine reads 0 dBFS")
101 halp_meta(uuid, "277e8d4c-afea-490f-bf45-21ac2180871c")
102 halp_flag(fully_custom_item);
104
105 struct
106 {
107 halp::dynamic_audio_bus<"Audio", double> audio;
108 struct : halp::combobox_t<"Mode", Mode>
109 {
110 struct range
111 {
112 std::string_view values[3]{mode_names[0], mode_names[1], mode_names[2]};
113 Mode init{Mode::Analyser};
114 };
115 } mode;
116 struct : halp::combobox_t<"FFT size", Size>
117 {
118 halp_meta(description, "Larger: finer frequency resolution, slower response")
119 struct range
120 {
121 std::string_view values[6]{
122 size_names[0], size_names[1], size_names[2],
123 size_names[3], size_names[4], size_names[5]};
124 Size init{Size::S8192};
125 };
126 } size;
127 struct : halp::combobox_t<"Window", Ui::Monitor::Window>
128 {
129 halp_meta(description, "Hann for general use, Blackman-Harris for dynamic range, flat top for exact levels")
130 struct range
131 {
132 std::string_view values[3]{window_names[0], window_names[1], window_names[2]};
133 Ui::Monitor::Window init{Ui::Monitor::Window::Hann};
134 };
135 } window;
136 struct : halp::spinbox_f32<"Averaging", halp::range{0., 10., 0.3}>
137 {
138 halp_meta(description, "Seconds of exponential averaging")
139 } averaging;
140 struct : halp::combobox_t<"Slope", Slope>
141 {
142 halp_meta(description, "Tilt around 1 kHz: 3 dB/oct shows pink noise flat")
143 struct range
144 {
145 std::string_view values[3]{slope_names[0], slope_names[1], slope_names[2]};
146 Slope init{Slope::Flat};
147 };
148 } slope;
149 struct : halp::combobox_t<"Smoothing", Smoothing>
150 {
151 struct range
152 {
153 std::string_view values[4]{
154 smoothing_names[0], smoothing_names[1], smoothing_names[2], smoothing_names[3]};
155 Smoothing init{Smoothing::Off};
156 };
157 } smoothing;
158 struct : halp::spinbox_f32<"Floor", halp::range{-160., -20., -100.}>
159 {
160 } floor;
161 struct : halp::spinbox_f32<"Ceiling", halp::range{-60., 24., 0.}>
162 {
163 } ceiling;
164 struct : halp::combobox_t<"Channels", Channels>
165 {
166 struct range
167 {
168 std::string_view values[5]{
169 channels_names[0], channels_names[1], channels_names[2], channels_names[3],
170 channels_names[4]};
171 Channels init{Channels::Sum};
172 };
173 } channels;
174 halp::toggle<"Peak hold"> peak_hold;
175 } inputs;
176
177 struct
178 {
179 struct : halp::val_port<"Peak", float>
180 {
181 halp_meta(description, "Frequency of the strongest component, in Hz")
182 } peak;
183 struct : halp::val_port<"Centroid", float>
184 {
185 halp_meta(description, "Spectral centroid, in Hz")
186 } centroid;
188 struct : halp::val_port<"Bands", std::optional<std::vector<float>>>
189 {
190 halp_meta(description, "Levels of the 31 third-octave bands from 20 Hz to 20 kHz, in dBFS")
191 } bands;
192 } outputs;
193
194 std::shared_ptr<SpectrumMonitor> monitor = std::make_shared<SpectrumMonitor>();
196 std::vector<double> history;
197 std::vector<double> ordered;
198 int write_pos{};
199 double since{};
200 double rate{};
201
204 void prepare(halp::setup s)
205 {
206 const double r = s.rate > 0 ? s.rate : 48000.;
207 if(r == rate)
208 return;
209 rate = r;
210 monitor->ring.set_rate(rate);
211 analysis.prepare(4096, rate, Ui::Monitor::Window::Hann);
212 history.assign(4096, 0.);
213 ordered.assign(4096, 0.);
214 write_pos = 0;
215 }
216
217 void operator()(int frames)
218 {
219 const int nch = inputs.audio.channels;
220 if(nch <= 0 || frames <= 0 || history.empty())
221 return;
222 if(monitor->watched())
223 monitor->ring.write(nch, frames, [&](int c) { return inputs.audio[c]; });
224
225 // The outlets: a mono mix analysed twenty times a second
226 const int n = int(history.size());
227 for(int i = 0; i < frames; i++)
228 {
229 double x = 0;
230 for(int c = 0; c < nch; c++)
231 x += inputs.audio[c][i];
232 history[write_pos] = x / nch;
233 write_pos = (write_pos + 1) % n;
234 }
235 since += frames;
236 if(since < rate / 20.)
237 return;
238 since = 0;
239 for(int i = 0; i < n; i++)
240 ordered[i] = history[(write_pos + i) % n];
241 analysis.analyse(ordered.data());
242
243 const auto& p = analysis.power;
244 int best = 1;
245 double num = 0, den = 0;
246 for(int b = 1; b < int(p.size()); b++)
247 {
248 if(p[b] > p[best])
249 best = b;
250 num += p[b] * analysis.binFrequency(b);
251 den += p[b];
252 }
253 outputs.peak.value = float(analysis.binFrequency(best));
254 outputs.centroid.value = den > 0 ? float(num / den) : 0.f;
255 auto& bands = outputs.bands.value.emplace(Ui::Monitor::ThirdOctave::count);
256 for(int i = 0; i < Ui::Monitor::ThirdOctave::count; i++)
257 bands[i] = float(10. * std::log10(std::max(
258 analysis.bandPower(
259 Ui::Monitor::ThirdOctave::lower(i), Ui::Monitor::ThirdOctave::upper(i)),
260 1e-20)));
261 }
262};
263
265{
266 static constexpr double fmin = 20.;
267 static constexpr double left_margin = 30.;
268 static constexpr double bottom_margin = 14.;
270 static constexpr double columns_per_second = 50.;
271
272 struct Curve
273 {
274 std::vector<double> avg;
275 std::vector<double> peak;
277 std::vector<double> now;
278 QColor color;
279 QString name;
280 };
281
283 struct Display
284 {
285 Smoothing smoothing{};
286 double slope{};
287 };
288
289 Layer(
290 const Process::ProcessModel& process, const Process::Context& doc,
291 QGraphicsItem* parent);
292
293 void hoverMoveEvent(QGraphicsSceneHoverEvent* ev) override;
294 void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override;
295 void paint_impl(QPainter* p) const override;
297 void mousePressEvent(QGraphicsSceneMouseEvent* ev) override;
298
299private:
300 Mode mode() const;
301 int fftSize() const;
302 double floorDb() const;
303 double ceilingDb() const;
304 double fmax() const { return std::min(20000., m_rate / 2.); }
305 Display display() const;
306
307 void refresh(SpectrumMonitor& mon);
308 double levelAt(const Display& d, const std::vector<double>& power, double f0, double f1) const;
309 QRectF plotRect() const;
310 double xOfFreq(const QRectF& r, double f) const;
311 double freqOfX(const QRectF& r, double x) const;
312 double yOfDb(const QRectF& r, double db) const;
313 void addSonogramColumns(int count);
314
315 void paintGrid(QPainter* p, const QRectF& r) const;
316 void paintAnalyser(QPainter* p) const;
317 void paintRta(QPainter* p) const;
318 void paintSonogram(QPainter* p) const;
319
320 const Process::ControlInlet* m_mode{};
321 const Process::ControlInlet* m_size{};
322 const Process::ControlInlet* m_window{};
323 const Process::ControlInlet* m_avg{};
324 const Process::ControlInlet* m_slope{};
325 const Process::ControlInlet* m_smooth{};
326 const Process::ControlInlet* m_floor{};
327 const Process::ControlInlet* m_ceiling{};
328 const Process::ControlInlet* m_channels{};
329 const Process::ControlInlet* m_hold{};
330 Process::MonitorLink* m_link{};
331 const score::CommandStackFacade& m_stack;
332 Ui::Monitor::DoubleClick m_doubleClick;
333
335 std::vector<std::vector<float>> m_in;
336 std::vector<float> m_mix;
337 std::vector<Curve> m_curves;
338 QElapsedTimer m_clock;
339
340 // Spectrogram: a ring of columns, one per 1 / columns_per_second of signal
341 QImage m_sonogram;
342 QImage m_colour_bar;
343 int m_column{};
344 uint64_t m_column_written{};
345
346 double m_rate{48000.};
347 std::optional<QPointF> m_hover;
348};
349
350}
Definition Port.hpp:247
Definition EffectLayer.hpp:17
The Process class.
Definition score-lib-process/Process/Process.hpp:82
Definition SampleRing.hpp:35
Definition LayerHelpers.hpp:103
A small abstraction layer over the score::CommandStack.
Definition CommandStackFacade.hpp:21
Utilities and base classes for 1D curves.
Definition FocusDispatcher.hpp:12
Base classes and tools to implement processes and layers.
Definition JSONVisitor.hpp:1115
Definition ProcessContext.hpp:12
Definition Monitor.hpp:27
Definition SpectrumAnalysis.hpp:33
void analyse(const T *x) noexcept
Analyses size samples; x must hold at least that many.
Definition SpectrumAnalysis.hpp:92
double bandPower(std::span< const double > p, double f0, double f1) const noexcept
Definition SpectrumAnalysis.hpp:115
std::vector< double > power
Definition SpectrumAnalysis.hpp:43
std::vector< double > now
The last analysis, unaveraged: what the spectrogram reads.
Definition Spectrum.hpp:277
What the curves are drawn with, read from the controls once per paint.
Definition Spectrum.hpp:284
Definition Spectrum.hpp:265
static constexpr double columns_per_second
Columns of the spectrogram per second of signal.
Definition Spectrum.hpp:270
void mousePressEvent(QGraphicsSceneMouseEvent *ev) override
A double click fits Floor and Ceiling to the levels shown.
Definition Spectrum.cpp:85
Definition Spectrum.hpp:91
void prepare(halp::setup s)
Definition Spectrum.hpp:204
Definition Spectrum.hpp:79
Definition MIDISync.hpp:149