Loading...
Searching...
No Matches
MidiDisplay.hpp
1#pragma once
2#include <Process/Dataflow/Port.hpp>
3#include <Process/Dataflow/PortFactory.hpp>
4#include <Process/Dataflow/PortItem.hpp>
5#include <Process/Process.hpp>
6
7#include <Scenario/Document/Interval/IntervalModel.hpp>
8
9#include <Effect/EffectLayer.hpp>
10#include <Effect/EffectLayout.hpp>
11
12#include <score/application/GUIApplicationContext.hpp>
13#include <score/model/Skin.hpp>
14
15#include <ossia/network/value/value_conversion.hpp>
16
17#include <QAction>
18#include <QApplication>
19#include <QClipboard>
20#include <QFontMetrics>
21#include <QMenu>
22#include <QPainter>
23
24#include <halp/audio.hpp>
25#include <halp/controls.hpp>
26#include <halp/meta.hpp>
27#include <halp/midi.hpp>
28
29#include <algorithm>
30#include <array>
31#include <cmath>
32#include <deque>
33#include <iterator>
34#include <optional>
35#include <vector>
36
75{
76struct Node
77{
78 halp_meta(name, "MIDI display")
79 halp_meta(c_name, "MidiDisplay")
80 halp_meta(category, "Monitoring")
81 halp_meta(author, "ossia score")
82 halp_meta(manual_url, "")
83 halp_meta(description, "Visualize the MIDI messages going through a port")
84 halp_meta(uuid, "a2b6c7f1-3d54-4e9a-9c21-8f0d5e73b104")
85 halp_flag(fully_custom_item);
86
87 static double recommended_height() { return 150.; }
88
89 struct
90 {
91 halp::midi_bus<"in"> midi;
92 halp::hslider_f32<"Window", halp::range{0.5f, 60.f, 8.f}> window;
93 } inputs;
94
95 struct
96 {
97 // [now, held, stuck, (seq, time, status, data1, data2, flags)...]
98 struct : halp::val_port<"events", std::optional<std::vector<float>>>
99 {
100 enum widget
101 {
102 control
103 };
104 } events;
105 } outputs;
106
107 // How much history is packed in every push. Must comfortably exceed the UI
108 // polling period, see the note above.
109 static constexpr double retain_seconds = 1.0;
110 static constexpr double heartbeat_seconds = 0.03;
111 static constexpr int max_events = 512;
112 static constexpr int fields_per_event = 6;
113 static constexpr int header_fields = 3;
114 // A note held for longer than this without a note-off is reported as stuck.
115 static constexpr double stuck_after_seconds = 2.;
116
117 enum event_flag : int
118 {
119 flag_none = 0,
120 flag_retrigger = 1, // note-on while the same note was already held
121 flag_orphan = 2, // note-off for a note that was not held
122 };
123
125 {
126 bool active{};
127 float on{};
128 };
129
130 double m_rate = 48000.;
131 int64_t m_frames = 0;
132 double m_last_push = -1e9;
133 float m_seq = 0.f;
134 std::deque<std::array<float, fields_per_event>> m_ring;
135 // Pairing is done here rather than in the layer: this sees every message from
136 // the first one on, while the layer only ever holds the last retain_seconds
137 // and is rebuilt from scratch whenever playback restarts. Deriving "unmatched
138 // note-off" or "note-on while held" from a truncated window invents both.
139 std::array<std::array<held_note, 128>, 16> m_held_notes{};
140
141 void prepare(halp::setup s) noexcept
142 {
143 m_rate = s.rate > 0 ? s.rate : 48000.;
144 m_frames = 0;
145 m_last_push = -1e9;
146 m_seq = 0.f;
147 m_ring.clear();
148 m_held_notes = {};
149 }
150
151 using tick = halp::tick_musical;
152 void operator()(halp::tick_musical t)
153 {
154 outputs.events.value.reset();
155
156 const double t0 = m_frames / m_rate;
157 const double t1 = (m_frames + t.frames) / m_rate;
158 m_frames += t.frames;
159
160 bool got_message = false;
161 for(const auto& m : inputs.midi)
162 {
163 if(m.bytes.empty())
164 continue;
165
166 const double ts = t0 + double(m.timestamp) / m_rate;
167 const auto n = m.bytes.size();
168 const uint8_t status = m.bytes[0];
169 const uint8_t d1 = n > 1 ? m.bytes[1] : 0;
170 const uint8_t d2 = n > 2 ? m.bytes[2] : 0;
171
172 m_ring.push_back(
173 {m_seq, float(ts), float(status), float(d1), float(d2),
174 float(track(status, d1, d2, float(ts)))});
175 m_seq += 1.f;
176 got_message = true;
177 }
178
179 while(!m_ring.empty()
180 && ((t1 - m_ring.front()[1]) > retain_seconds
181 || std::ssize(m_ring) > max_events))
182 m_ring.pop_front();
183
184 if(got_message || (t1 - m_last_push) >= heartbeat_seconds)
185 {
186 int held = 0, stuck = 0;
187 for(const auto& chan : m_held_notes)
188 {
189 for(const auto& note : chan)
190 {
191 if(!note.active)
192 continue;
193 held++;
194 stuck += (t1 - note.on) > stuck_after_seconds;
195 }
196 }
197
198 std::vector<float> payload;
199 payload.reserve(header_fields + fields_per_event * m_ring.size());
200 payload.push_back(float(t1));
201 payload.push_back(float(held));
202 payload.push_back(float(stuck));
203 for(const auto& e : m_ring)
204 payload.insert(payload.end(), e.begin(), e.end());
205
206 outputs.events.value = std::move(payload);
207 m_last_push = t1;
208 }
209 }
210
213 int track(uint8_t status, uint8_t d1, uint8_t d2, float t) noexcept
214 {
215 const int chan = status & 0x0F;
216 switch(kind_of(status, d2))
217 {
218 case msg_kind::note_on: {
219 auto& note = m_held_notes[chan][d1];
220 const bool was_held = note.active;
221 note = {true, t};
222 return was_held ? flag_retrigger : flag_none;
223 }
224 case msg_kind::note_off: {
225 auto& note = m_held_notes[chan][d1];
226 if(!note.active)
227 return flag_orphan;
228 note = {};
229 return flag_none;
230 }
231 default:
232 return flag_none;
233 }
234 }
235
236 enum class msg_kind
237 {
238 note_on,
239 note_off,
240 poly_pressure,
241 control_change,
242 program_change,
243 aftertouch,
244 pitch_bend,
245 system
246 };
247
248 static msg_kind kind_of(int status, int data2) noexcept
249 {
250 switch(status & 0xF0)
251 {
252 case 0x80:
253 return msg_kind::note_off;
254 case 0x90:
255 // Running-status note-off: note-on with a null velocity
256 return data2 > 0 ? msg_kind::note_on : msg_kind::note_off;
257 case 0xA0:
258 return msg_kind::poly_pressure;
259 case 0xB0:
260 return msg_kind::control_change;
261 case 0xC0:
262 return msg_kind::program_change;
263 case 0xD0:
264 return msg_kind::aftertouch;
265 case 0xE0:
266 return msg_kind::pitch_bend;
267 default:
268 return msg_kind::system;
269 }
270 }
271
272 static QString note_name(int pitch)
273 {
274 static const char* const names[12]
275 = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
276 if(pitch < 0 || pitch > 127)
277 return QStringLiteral("?");
278 return QString::fromLatin1(names[pitch % 12]) + QString::number(pitch / 12 - 1);
279 }
280
282 {
283 public:
284 static constexpr double left_gutter = 34.;
285 static constexpr double header_height = 13.;
286 static constexpr double system_lane_height = 9.;
287 static constexpr int default_low_pitch = 48;
288 static constexpr int default_high_pitch = 72;
289 static constexpr int max_log = 256;
290
291 struct Event
292 {
293 float seq{};
294 float t{};
295 uint8_t status{};
296 uint8_t d1{};
297 uint8_t d2{};
298 uint8_t flags{};
299 };
300
301 struct Note
302 {
303 float on{};
304 float off{};
305 uint8_t chan{};
306 uint8_t pitch{};
307 uint8_t vel{};
308 bool retriggered{}; // note-on while already held: the note-off went missing
309 };
310
311 struct OpenNote
312 {
313 bool active{};
314 float on{};
315 uint8_t vel{};
316 };
317
319 {
320 float t{};
321 uint8_t chan{};
322 uint8_t pitch{};
323 };
324
325 Scenario::IntervalModel* m_interval{};
326 Process::ControlInlet* m_window_inlet{};
327
328 std::deque<Event> m_log;
329 std::deque<Note> m_notes;
330 std::deque<OrphanOff> m_orphans;
331 std::array<std::array<OpenNote, 128>, 16> m_open{};
332
333 float m_now = 0.f;
334 float m_last_seq = -1.f;
335 // Held / stuck counts come from the node: it has seen the whole stream.
336 int m_held = 0;
337 int m_stuck = 0;
338
339 Layer(
340 const Process::ProcessModel& process, const Process::Context& doc,
341 QGraphicsItem* parent)
342 : Process::EffectLayerView{parent}
343 , m_interval{Scenario::closestParentInterval(process.parent())}
344 {
345 setAcceptedMouseButtons({});
346 setToolTip(tr("Green: a note that was played and released.\n"
347 "Amber, running to the right edge: currently held.\n"
348 "Red hatched: held for more than 2s without a note-off.\n"
349 "Orange: a note-on while that note was already held.\n"
350 "Magenta cross: a note-off for a note that was not held.\n"
351 "Bottom lane: non-note messages.\n"
352 "Right-click to copy the message log."));
353
354 const Process::PortFactoryList& portFactory
356
357 // FullyCustomItem means the node item does not lay out any port: we have
358 // to instantiate the ones we want reachable ourselves.
359 auto& midi_inlet = *process.inlets()[0];
360 if(auto* fact = portFactory.get(midi_inlet.concreteKey()))
361 if(auto* item = fact->makePortItem(midi_inlet, doc, this, this))
362 item->setPos(0, 5);
363
364 m_window_inlet = static_cast<Process::ControlInlet*>(process.inlets()[1]);
365 connect(
366 m_window_inlet, &Process::ControlInlet::valueChanged, this,
367 [this](const ossia::value&) { update(); });
368
369 if(m_interval)
370 {
371 connect(
372 m_interval, &Scenario::IntervalModel::executionEvent, this,
373 [this](Scenario::IntervalExecutionEvent ev) {
374 switch(ev)
375 {
376 case Scenario::IntervalExecutionEvent::Stopped:
377 reset();
378 break;
379 default:
380 break;
381 }
382 });
383 }
384
385 auto* events_outlet
386 = static_cast<Process::ControlOutlet*>(process.outlets().back());
387 connect(
388 events_outlet, &Process::ControlOutlet::valueChanged, this,
389 [this](const ossia::value& v) { on_events(v); });
390 }
391
392 double window() const noexcept
393 {
394 if(!m_window_inlet)
395 return 8.;
396 const double w = ossia::convert<float>(m_window_inlet->value());
397 return std::isfinite(w) && w > 0.05 ? w : 8.;
398 }
399
400 void reset()
401 {
402 m_log.clear();
403 m_notes.clear();
404 m_orphans.clear();
405 m_open = {};
406 m_now = 0.f;
407 m_last_seq = -1.f;
408 m_held = 0;
409 m_stuck = 0;
410 update();
411 }
412
413 void on_events(const ossia::value& v)
414 {
415 auto* list = v.target<std::vector<ossia::value>>();
416 if(!list || list->empty())
417 return;
418
419 const int N = std::ssize(*list);
420 if(N < header_fields || (N - header_fields) % fields_per_event != 0)
421 return;
422
423 const float now = ossia::convert<float>((*list)[0]);
424
425 // The node restarted (new playback): our timeline is stale.
426 if(now < m_now)
427 reset();
428 m_now = now;
429 m_held = ossia::convert<int>((*list)[1]);
430 m_stuck = ossia::convert<int>((*list)[2]);
431
432 for(int i = header_fields; i + fields_per_event <= N; i += fields_per_event)
433 {
434 const float seq = ossia::convert<float>((*list)[i]);
435 if(seq <= m_last_seq)
436 continue;
437 m_last_seq = seq;
438
439 Event e;
440 e.seq = seq;
441 e.t = ossia::convert<float>((*list)[i + 1]);
442 e.status = uint8_t(std::clamp(ossia::convert<float>((*list)[i + 2]), 0.f, 255.f));
443 e.d1 = uint8_t(std::clamp(ossia::convert<float>((*list)[i + 3]), 0.f, 127.f));
444 e.d2 = uint8_t(std::clamp(ossia::convert<float>((*list)[i + 4]), 0.f, 127.f));
445 e.flags = uint8_t(std::clamp(ossia::convert<float>((*list)[i + 5]), 0.f, 255.f));
446
447 apply(e);
448
449 m_log.push_back(e);
450 if(std::ssize(m_log) > max_log)
451 m_log.pop_front();
452 }
453
454 trim();
455 update();
456 }
457
458 // The flags are computed by the node against the full stream. This only
459 // turns them into geometry: an unmatched message here means our window
460 // simply does not reach far enough back, which is not an anomaly.
461 void apply(const Event& e)
462 {
463 const int chan = e.status & 0x0F;
464 switch(kind_of(e.status, e.d2))
465 {
466 case msg_kind::note_on: {
467 auto& open = m_open[chan][e.d1];
468 if(open.active)
469 {
470 m_notes.push_back(
471 {open.on, e.t, uint8_t(chan), e.d1, open.vel,
472 bool(e.flags & flag_retrigger)});
473 }
474 open = {true, e.t, e.d2};
475 break;
476 }
477 case msg_kind::note_off: {
478 if(e.flags & flag_orphan)
479 {
480 m_orphans.push_back({e.t, uint8_t(chan), e.d1});
481 break;
482 }
483
484 auto& open = m_open[chan][e.d1];
485 if(open.active)
486 {
487 m_notes.push_back({open.on, e.t, uint8_t(chan), e.d1, open.vel, false});
488 open = {};
489 }
490 break;
491 }
492 default:
493 break;
494 }
495 }
496
497 void trim()
498 {
499 // Keep a bit more than the largest possible window so that changing the
500 // window slider does not blank the past.
501 const float horizon = m_now - 61.f;
502 while(!m_notes.empty() && m_notes.front().off < horizon)
503 m_notes.pop_front();
504 while(!m_orphans.empty() && m_orphans.front().t < horizon)
505 m_orphans.pop_front();
506 }
507
508 int held_count() const noexcept { return m_held; }
509 int stuck_count() const noexcept { return m_stuck; }
510
511 QString logText() const
512 {
513 QString out;
514 for(const auto& e : m_log)
515 {
516 out += QString::asprintf("%9.4f ", double(e.t));
517 out += describe(e);
518 out += QLatin1Char('\n');
519 }
520 return out;
521 }
522
523 static QString describe(const Event& e)
524 {
525 const int chan = (e.status & 0x0F) + 1;
526 switch(kind_of(e.status, e.d2))
527 {
528 case msg_kind::note_on:
529 return QStringLiteral("ch%1 Note On %2 (%3) vel %4")
530 .arg(chan, 2)
531 .arg(int(e.d1), 3)
532 .arg(note_name(e.d1))
533 .arg(int(e.d2));
534 case msg_kind::note_off:
535 return QStringLiteral("ch%1 Note Off %2 (%3) vel %4")
536 .arg(chan, 2)
537 .arg(int(e.d1), 3)
538 .arg(note_name(e.d1))
539 .arg(int(e.d2));
540 case msg_kind::poly_pressure:
541 return QStringLiteral("ch%1 Poly Aft. %2 -> %3")
542 .arg(chan, 2)
543 .arg(int(e.d1), 3)
544 .arg(int(e.d2));
545 case msg_kind::control_change:
546 return QStringLiteral("ch%1 CC %2 -> %3")
547 .arg(chan, 2)
548 .arg(int(e.d1), 3)
549 .arg(int(e.d2));
550 case msg_kind::program_change:
551 return QStringLiteral("ch%1 Program %2").arg(chan, 2).arg(int(e.d1), 3);
552 case msg_kind::aftertouch:
553 return QStringLiteral("ch%1 Aftertouch %2").arg(chan, 2).arg(int(e.d1), 3);
554 case msg_kind::pitch_bend:
555 return QStringLiteral("ch%1 Pitch Bend %2")
556 .arg(chan, 2)
557 .arg(int(e.d1) + (int(e.d2) << 7) - 8192);
558 default:
559 return QStringLiteral(" System 0x%1 %2 %3")
560 .arg(int(e.status), 2, 16, QLatin1Char('0'))
561 .arg(int(e.d1), 3)
562 .arg(int(e.d2), 3);
563 }
564 }
565
566 static QColor kind_color(msg_kind k) noexcept
567 {
568 switch(k)
569 {
570 case msg_kind::control_change:
571 return QColor(90, 160, 230);
572 case msg_kind::pitch_bend:
573 return QColor(180, 130, 230);
574 case msg_kind::program_change:
575 return QColor(230, 190, 90);
576 case msg_kind::poly_pressure:
577 case msg_kind::aftertouch:
578 return QColor(120, 200, 190);
579 default:
580 return QColor(150, 150, 150);
581 }
582 }
583
584 void paint_impl(QPainter* p) const override
585 {
586 const auto rect = boundingRect();
587 const double W = rect.width();
588 const double H = rect.height();
589 if(W <= left_gutter + 8. || H <= header_height + system_lane_height + 8.)
590 return;
591
592 const double roll_x = left_gutter;
593 const double roll_w = W - left_gutter - 2.;
594 const double roll_y = header_height;
595 const double roll_h = H - header_height - system_lane_height;
596
597 const double win = window();
598 const double t_min = m_now - win;
599
600 const auto to_x = [&](double t) {
601 return roll_x + std::clamp((t - t_min) / win, 0., 1.) * roll_w;
602 };
603
604 // Vertical pitch range: fit what is on screen, with an octave minimum
605 int lo = 127, hi = 0;
606 const auto extend = [&](int pitch) {
607 lo = std::min(lo, pitch);
608 hi = std::max(hi, pitch);
609 };
610 for(const auto& n : m_notes)
611 if(n.off >= t_min)
612 extend(n.pitch);
613 for(const auto& chan : m_open)
614 for(int pitch = 0; pitch < 128; pitch++)
615 if(chan[pitch].active)
616 extend(pitch);
617
618 if(lo > hi)
619 {
620 lo = default_low_pitch;
621 hi = default_high_pitch;
622 }
623 lo = std::max(0, lo - 1);
624 hi = std::min(127, hi + 1);
625 if(hi - lo < 12)
626 {
627 const int mid = (lo + hi) / 2;
628 lo = std::max(0, mid - 6);
629 hi = std::min(127, lo + 12);
630 }
631
632 const int rows = hi - lo + 1;
633 const double row_h = roll_h / rows;
634 const auto to_y = [&](int pitch) { return roll_y + (hi - pitch) * row_h; };
635
636 p->save();
637 p->setRenderHint(QPainter::Antialiasing, false);
638
639 draw_grid(p, roll_x, roll_w, row_h, lo, hi, to_y);
640 draw_notes(p, row_h, t_min, to_x, to_y);
641 draw_orphans(p, row_h, t_min, to_x, to_y);
642 draw_system_lane(p, roll_x, H - system_lane_height, roll_w, t_min, to_x);
643 draw_header(p, W);
644
645 p->restore();
646 }
647
648 private:
649 void draw_grid(
650 QPainter* p, double roll_x, double roll_w, double row_h, int lo, int hi,
651 auto to_y) const
652 {
653 QFont f = p->font();
654 f.setPixelSize(8);
655 p->setFont(f);
656
657 for(int pitch = lo; pitch <= hi; pitch++)
658 {
659 if(pitch % 12 != 0)
660 continue;
661
662 const double line_y = to_y(pitch) + row_h;
663
664 p->setPen(QColor(72, 72, 80));
665 p->drawLine(QPointF(roll_x, line_y), QPointF(roll_x + roll_w, line_y));
666
667 // Centred on the line itself
668 p->setPen(QColor(140, 140, 150));
669 constexpr double h = 10.;
670 p->drawText(
671 QRectF(0, line_y - h / 2., roll_x - 4, h), Qt::AlignRight | Qt::AlignVCenter,
672 note_name(pitch));
673 }
674 }
675
676 void draw_notes(
677 QPainter* p, double row_h, double t_min, auto to_x, auto to_y) const
678 {
679 p->setPen(Qt::NoPen);
680 const double bar_h = std::max(1., row_h - 1.);
681
682 for(const auto& n : m_notes)
683 {
684 if(n.off < t_min)
685 continue;
686 const double x0 = to_x(n.on);
687 const double x1 = std::max(to_x(n.off), x0 + 1.);
688 QColor col = n.retriggered ? QColor(240, 160, 40) : velocity_color(n.vel);
689 p->fillRect(QRectF(x0, to_y(n.pitch), x1 - x0, bar_h), col);
690 }
691
692 // Notes still held: they run all the way to "now". If they have been held
693 // for longer than the threshold, this is the missing-note-off symptom.
694 for(int chan = 0; chan < 16; chan++)
695 {
696 for(int pitch = 0; pitch < 128; pitch++)
697 {
698 const auto& open = m_open[chan][pitch];
699 if(!open.active)
700 continue;
701
702 const double x0 = to_x(open.on);
703 const double x1 = to_x(m_now);
704 const bool stuck = (m_now - open.on) > stuck_after_seconds;
705 const QRectF r(x0, to_y(pitch), std::max(1., x1 - x0), bar_h);
706
707 if(stuck)
708 {
709 p->fillRect(r, QBrush(QColor(220, 40, 40), Qt::BDiagPattern));
710 p->fillRect(QRectF(r.left(), r.top(), 2., r.height()), QColor(255, 80, 80));
711 }
712 else
713 {
714 p->fillRect(r, QColor(230, 190, 60));
715 }
716 }
717 }
718 }
719
720 void draw_orphans(
721 QPainter* p, double row_h, double t_min, auto to_x, auto to_y) const
722 {
723 p->setPen(QPen(QColor(255, 90, 200), 1.));
724 const double s = std::min(4., std::max(2., row_h));
725 for(const auto& o : m_orphans)
726 {
727 if(o.t < t_min)
728 continue;
729 const double x = to_x(o.t);
730 const double y = to_y(o.pitch) + row_h / 2.;
731 p->drawLine(QPointF(x - s, y - s), QPointF(x + s, y + s));
732 p->drawLine(QPointF(x - s, y + s), QPointF(x + s, y - s));
733 }
734 }
735
736 void draw_system_lane(
737 QPainter* p, double x0, double y, double w, double t_min, auto to_x) const
738 {
739 p->setPen(QColor(72, 72, 80));
740 p->drawLine(QPointF(x0, y), QPointF(x0 + w, y));
741
742 for(const auto& e : m_log)
743 {
744 if(e.t < t_min)
745 continue;
746 const auto k = kind_of(e.status, e.d2);
747 if(k == msg_kind::note_on || k == msg_kind::note_off)
748 continue;
749 p->setPen(kind_color(k));
750 const double x = to_x(e.t);
751 p->drawLine(QPointF(x, y + 1.), QPointF(x, y + system_lane_height - 1.));
752 }
753 }
754
755 void draw_header(QPainter* p, double W) const
756 {
757 QFont f = p->font();
758 f.setPixelSize(9);
759 p->setFont(f);
760
761 const QFontMetricsF fm{f};
762 const auto band = [&](double x, double w) { return QRectF(x, 0, w, header_height); };
763 constexpr double gap = 8.;
764
765 double x = 2.;
766
767 const auto held = QStringLiteral("held %1").arg(held_count());
768 p->setPen(QColor(160, 160, 168));
769 p->drawText(band(x, fm.horizontalAdvance(held)), Qt::AlignVCenter, held);
770 x += fm.horizontalAdvance(held) + gap;
771
772 if(const int stuck = stuck_count(); stuck > 0)
773 {
774 const auto txt = QStringLiteral("STUCK %1").arg(stuck);
775 p->setPen(QColor(255, 90, 90));
776 p->drawText(band(x, fm.horizontalAdvance(txt)), Qt::AlignVCenter, txt);
777 x += fm.horizontalAdvance(txt) + gap;
778 }
779
780 if(!m_log.empty())
781 {
782 const auto txt = describe(m_log.back());
783 const double avail = W - 2. - x;
784 if(avail > fm.horizontalAdvance(QStringLiteral("ch16 Note Off ")))
785 {
786 p->setPen(QColor(130, 130, 140));
787 p->drawText(
788 band(x, avail), Qt::AlignRight | Qt::AlignVCenter,
789 fm.elidedText(txt, Qt::ElideRight, avail));
790 }
791 }
792 }
793
794 static QColor velocity_color(int vel) noexcept
795 {
796 const double t = std::clamp(vel / 127., 0., 1.);
797 return QColor(
798 60 + int(80 * t), 150 + int(90 * t), 90 + int(40 * (1. - t)), 200 + int(55 * t));
799 }
800 };
801
803 {
804 using Process::EffectLayerPresenter::EffectLayerPresenter;
805 void fillContextMenu(
806 QMenu& menu, QPoint, QPointF, const Process::LayerContextMenuManager&) override
807 {
808 auto* copy = menu.addAction(tr("Copy MIDI log"));
809 connect(copy, &QAction::triggered, this, [this] {
810 qApp->clipboard()->setText(static_cast<Layer*>(this->m_view)->logText());
811 });
812
813 auto* clear = menu.addAction(tr("Clear MIDI log"));
814 connect(clear, &QAction::triggered, this, [this] {
815 static_cast<Layer*>(this->m_view)->reset();
816 });
817 }
818 };
819};
820}
Definition Port.hpp:206
Definition Port.hpp:427
Definition EffectLayer.hpp:32
Definition EffectLayer.hpp:16
Definition LayerContextMenu.hpp:38
Definition PortFactory.hpp:82
The Process class.
Definition score-lib-process/Process/Process.hpp:62
Definition IntervalModel.hpp:50
FactoryType * get(const key_type &k) const noexcept
Get a particular factory from its ConcreteKey.
Definition InterfaceList.hpp:128
Base classes and tools to implement processes and layers.
Definition JSONVisitor.hpp:1115
Main plug-in of score.
Definition score-plugin-dataflow/Dataflow/PortItem.hpp:13
MIDI display: a scrolling monitor for whatever goes through a MIDI port.
Definition MidiDisplay.hpp:75
Definition ProcessContext.hpp:12
Definition MidiDisplay.hpp:292
Definition MidiDisplay.hpp:302
Definition MidiDisplay.hpp:312
Definition MidiDisplay.hpp:319
Definition MidiDisplay.hpp:282
Definition MidiDisplay.hpp:803
Definition MidiDisplay.hpp:125
Definition MidiDisplay.hpp:77
int track(uint8_t status, uint8_t d1, uint8_t d2, float t) noexcept
Definition MidiDisplay.hpp:213
const T & interfaces() const
Access to a specific interface list.
Definition ApplicationContext.hpp:70