Loading...
Searching...
No Matches
AudioScope.hpp
1#pragma once
2#include <Process/Monitoring/MonitorLink.hpp>
3#include <Process/Monitoring/SampleRing.hpp>
4
5#include <Effect/EffectLayer.hpp>
6
7#include <ossia/audio/meters.hpp>
8
9#include <Ui/Monitor/LayerHelpers.hpp>
10#include <Ui/Monitor/Paint.hpp>
11#include <halp/audio.hpp>
12#include <halp/controls.hpp>
13#include <halp/meta.hpp>
14
15#include <array>
16#include <cmath>
17#include <numbers>
18#include <vector>
19
20namespace Process
21{
22class ControlInlet;
23}
24
25namespace Ui::AudioScope
26{
27enum class Mode
28{
29 Oscilloscope,
30 XY,
31 Goniometer,
32 Correlation
33};
34inline constexpr std::string_view mode_names[]{
35 "Oscilloscope", "XY", "Goniometer", "Correlation"};
36
37enum class Trigger
38{
39 Free,
40 Rising,
41 Falling,
42 Pitch
43};
44inline constexpr std::string_view trigger_names[]{"Free", "Rising", "Falling", "Pitch"};
45
46enum class Polar
47{
48 Lissajous,
49 PolarSample,
50 PolarLevel
51};
52inline constexpr std::string_view polar_names[]{"Lissajous", "Polar sample", "Polar level"};
53
56inline constexpr double max_window_ms = 300.;
57
59{
60 Process::SampleRing ring{1 << 18};
61 std::atomic<float> correlation{};
62};
63
69inline int findTrigger(const float* x, int last, float level, bool rising) noexcept
70{
71 const float hyst = 0.01f;
72 for(int i = last; i > 0; i--)
73 {
74 if(rising ? (x[i - 1] < level && x[i] >= level) : (x[i - 1] > level && x[i] <= level))
75 {
76 // Confirm that the signal was clearly on the other side before
77 for(int j = i - 1; j >= std::max(0, i - 64); j--)
78 if(rising ? x[j] < level - hyst : x[j] > level + hyst)
79 return i;
80 }
81 }
82 return -1;
83}
84
91inline double estimatePeriod(const float* x, int n) noexcept
92{
93 if(n < 4)
94 return 0.;
95 double mean = 0;
96 for(int i = 0; i < n; i++)
97 mean += x[i];
98 mean /= n;
99 double swing = 0;
100 for(int i = 0; i < n; i++)
101 swing = std::max(swing, std::abs(x[i] - mean));
102 if(swing < 1e-6)
103 return 0.;
104 const double arm = mean - 0.1 * swing;
105
106 std::array<double, 64> crossings{};
107 int count = 0;
108 bool armed = false;
109 for(int i = 1; i < n && count < int(crossings.size()); i++)
110 {
111 if(x[i] < arm)
112 armed = true;
113 const double a = x[i - 1] - mean, b = x[i] - mean;
114 if(armed && a < 0. && b >= 0.)
115 {
116 crossings[count++] = (i - 1) + (-a) / (b - a);
117 armed = false;
118 }
119 }
120 if(count < 3)
121 return 0.;
122 std::array<double, 63> d{};
123 for(int i = 1; i < count; i++)
124 d[i - 1] = crossings[i] - crossings[i - 1];
125 std::nth_element(d.begin(), d.begin() + (count - 1) / 2, d.begin() + (count - 1));
126 return d[(count - 1) / 2];
127}
128
129struct Layer;
130
131struct Node
132{
133 halp_meta(name, "Audio scope")
134 halp_meta(c_name, "AudioScope")
135 halp_meta(category, "Monitoring/Audio")
136 halp_meta(author, "ossia score")
137 halp_meta(manual_url, "")
138 halp_meta(
139 description,
140 "Oscilloscope with edge and pitch triggers, XY display, goniometer and "
141 "phase correlation meter")
142 halp_meta(uuid, "57966fbd-574f-48cc-9e26-74397352f3dd")
143 halp_flag(fully_custom_item);
145
146 struct
147 {
148 halp::dynamic_audio_bus<"Audio", double> audio;
149
150 struct : halp::combobox_t<"Mode", Mode>
151 {
152 struct range
153 {
154 std::string_view values[4]{
155 mode_names[0], mode_names[1], mode_names[2], mode_names[3]};
156 Mode init{Mode::Oscilloscope};
157 };
158 } mode;
159 struct : halp::spinbox_f32<"Window", halp::range{0.1, max_window_ms, 20.}>
160 {
161 halp_meta(description, "Milliseconds shown across the oscilloscope")
162 } window;
163 struct : halp::combobox_t<"Trigger", Trigger>
164 {
165 halp_meta(
166 description,
167 "Free running, on a rising or falling edge of the first channel, or "
168 "synchronized to its period, two periods across")
169 struct range
170 {
171 std::string_view values[4]{
172 trigger_names[0], trigger_names[1], trigger_names[2], trigger_names[3]};
173 Trigger init{Trigger::Rising};
174 };
175 } trigger;
176 struct : halp::spinbox_f32<"Level", halp::range{-1., 1., 0.}>
177 {
178 halp_meta(description, "Value the trigger edge crosses")
179 } level;
180 struct : halp::hslider_f32<"Persistence", halp::range{0., 0.98, 0.}>
181 {
182 halp_meta(description, "How long traces linger, as on a phosphor screen")
183 } persistence;
184 struct : halp::spinbox_f32<"Gain", halp::range{-24., 48., 0.}>
185 {
186 halp_meta(description, "Vertical gain, in dB")
187 } gain;
188 struct : halp::toggle<"Auto gain">
189 {
190 halp_meta(description, "Scale to the signal's recent peak")
191 } auto_gain;
192 struct : halp::spinbox_i32<"X channel", halp::range{0, 63, 0}>
193 {
194 halp_meta(description, "Channel on the horizontal axis of XY, left channel of the goniometer and correlation")
195 } x_channel;
196 struct : halp::spinbox_i32<"Y channel", halp::range{0, 63, 1}>
197 {
198 halp_meta(description, "Channel on the vertical axis of XY, right channel of the goniometer and correlation")
199 } y_channel;
200 struct : halp::combobox_t<"Goniometer", Polar>
201 {
202 struct range
203 {
204 std::string_view values[3]{polar_names[0], polar_names[1], polar_names[2]};
205 Polar init{Polar::Lissajous};
206 };
207 } polar;
208 } inputs;
209
210 struct
211 {
212 struct : halp::val_port<"Correlation", float>
213 {
214 halp_meta(description, "Phase correlation of the two channels, -1 to 1")
215 } correlation;
216 struct : halp::val_port<"Balance", float>
217 {
218 halp_meta(description, "Right minus left RMS level, in dB")
219 } balance;
220 } outputs;
221
222 std::shared_ptr<ScopeMonitor> monitor = std::make_shared<ScopeMonitor>();
223 ossia::meters::correlation corr;
224 ossia::meters::rms rms_l, rms_r;
225 double rate{};
226
229 void prepare(halp::setup s)
230 {
231 const double r = s.rate > 0 ? s.rate : 48000.;
232 if(r == rate)
233 return;
234 rate = r;
235 monitor->ring.set_rate(rate);
236 corr.prepare(rate, 0.3);
237 rms_l.prepare(rate, 0.3);
238 rms_r.prepare(rate, 0.3);
239 }
240
241 void operator()(int frames)
242 {
243 const int nch = inputs.audio.channels;
244 if(nch <= 0 || frames <= 0)
245 return;
246
247 if(monitor->watched())
248 monitor->ring.write(nch, frames, [&](int c) { return inputs.audio[c]; });
249
250 const int xc = std::clamp(inputs.x_channel.value, 0, nch - 1);
251 const int yc = std::clamp(inputs.y_channel.value, 0, nch - 1);
252 corr.process(inputs.audio[xc], inputs.audio[yc], frames);
253 rms_l.process(inputs.audio[xc], frames);
254 rms_r.process(inputs.audio[yc], frames);
255 const float k = float(corr.value());
256 monitor->correlation.store(k, std::memory_order_relaxed);
257 outputs.correlation.value = k;
258 outputs.balance.value = float(
259 ossia::detail::LinearGainToDecibels(std::max(rms_r.level(), 1e-6))
260 - ossia::detail::LinearGainToDecibels(std::max(rms_l.level(), 1e-6)));
261 }
262};
263
265{
266 static constexpr int max_shown_channels = 8;
268 static constexpr double history_per_second = 30.;
269
270 Layer(
271 const Process::ProcessModel& process, const Process::Context& doc,
272 QGraphicsItem* parent);
273
274 void paint_impl(QPainter* p) const override;
276 void mousePressEvent(QGraphicsSceneMouseEvent* ev) override;
277
278private:
279 Mode mode() const;
280 Trigger trigger() const;
281 Polar polar() const;
282 double gain() const;
283 bool autoGain() const;
284 float persistence() const;
285
286 void reset();
287 void refresh(ScopeMonitor& mon);
288 void refreshScope(const Process::SampleRing& ring);
289 void refreshDots(const Process::SampleRing& ring);
290 void refreshCorrelation(const Process::SampleRing& ring);
291 double currentGain() const;
292 static void traceInto(
293 Ui::Monitor::DensityImage& img, const std::vector<float>& x, double w, double h,
294 double g);
295
296 QRectF scopeRect() const;
297 QRectF squareRect() const;
298 void paintScope(QPainter* p) const;
299 static void drawTrace(
300 QPainter* p, const QRectF& r, const std::vector<float>& x, double g, QColor col);
301 void paintDots(QPainter* p, bool gonio) const;
302 void paintCorrelation(QPainter* p, const QRectF& area, bool history = true) const;
303
304 const Process::ControlInlet* m_mode{};
305 const Process::ControlInlet* m_window{};
306 const Process::ControlInlet* m_trigger{};
307 const Process::ControlInlet* m_level{};
308 const Process::ControlInlet* m_persistence{};
309 const Process::ControlInlet* m_gain{};
310 const Process::ControlInlet* m_auto{};
311 const Process::ControlInlet* m_x{};
312 const Process::ControlInlet* m_y{};
313 const Process::ControlInlet* m_polar{};
314 Process::MonitorLink* m_link{};
315 const score::CommandStackFacade& m_stack;
316 Ui::Monitor::DoubleClick m_doubleClick;
317
318 // Oscilloscope: the samples on screen, per channel
319 std::vector<std::vector<float>> m_trace;
320 std::vector<float> m_search;
321 double m_window_ms{};
322 bool m_triggered{};
323 double m_period{};
325 std::vector<Ui::Monitor::DensityImage> m_phosphor;
326
327 // XY and goniometer: every sample since the last refresh
329 std::vector<float> m_xs, m_ys;
330 uint64_t m_read{};
331 double m_peak{1e-3};
332 std::array<float, 90> m_polar_level{};
333
334 std::array<float, 300> m_corr_history{};
335 int m_corr_pos{};
336 uint64_t m_corr_written{};
337 float m_corr{};
338 double m_rate{48000.};
339 int m_channels{};
340};
341
342}
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
Base classes and tools to implement processes and layers.
Definition JSONVisitor.hpp:1115
Definition ProcessContext.hpp:12
Definition Monitor.hpp:27
Definition AudioScope.hpp:265
void mousePressEvent(QGraphicsSceneMouseEvent *ev) override
A double click sets the gain to fit what the signal reaches now.
Definition AudioScope.cpp:193
static constexpr double history_per_second
Entries of the correlation history per second of signal.
Definition AudioScope.hpp:268
Definition AudioScope.hpp:132
void prepare(halp::setup s)
Definition AudioScope.hpp:229
Definition AudioScope.hpp:59
Definition Paint.hpp:276
Definition MIDISync.hpp:149