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/Monitoring/EventRing.hpp>
6#include <Process/Monitoring/MonitorLink.hpp>
7#include <Process/Process.hpp>
8
9#include <Scenario/Document/Interval/IntervalModel.hpp>
10
11#include <Effect/EffectLayer.hpp>
12#include <Effect/EffectLayout.hpp>
13
14#include <score/application/GUIApplicationContext.hpp>
15#include <score/model/Skin.hpp>
16
17#include <Process/Monitoring/Monitor.hpp>
18#include <ossia/detail/flat_map.hpp>
19#include <ossia/network/value/value_conversion.hpp>
20
21#include <QAction>
22#include <QApplication>
23#include <QClipboard>
24#include <QFontMetrics>
25#include <QMenu>
26#include <QPainter>
27#include <QPainterPath>
28
29#include <halp/audio.hpp>
30#include <halp/controls.hpp>
31#include <halp/meta.hpp>
32#include <halp/midi.hpp>
33
34#include <Ui/Monitor/LayerHelpers.hpp>
35#include <Ui/Monitor/Ring.hpp>
36#include <Ui/Monitor/MidiText.hpp>
37#include <Ui/Monitor/Paint.hpp>
38
39#include <algorithm>
40#include <array>
41#include <cmath>
42#include <iterator>
43#include <optional>
44#include <vector>
45
76{
77enum class Mode
78{
79 PianoRoll,
80 EventList,
81 Controllers,
82 Activity,
83 Expression
84};
85enum class NotePalette
86{
87 Green,
88 Inferno,
89 Viridis,
90 Magma,
91 Turbo,
92 Channels
93};
94
95inline constexpr std::string_view mode_names[]{
96 "Piano roll", "Event list", "Controllers", "Activity", "Expression"};
97
100{
101 uint64_t seq{};
102 double t{};
103 uint8_t status{};
104 uint8_t d1{};
105 uint8_t d2{};
106 uint8_t flags{};
108 uint32_t size{};
109};
110
113{
114 std::array<std::array<uint64_t, 2>, 16> bits{};
115 int held{};
116 int stuck{};
117 double now{};
118 double window{};
119
120 bool test(int chan, int pitch) const noexcept
121 {
122 return bits[chan][pitch / 64] & (uint64_t(1) << (pitch % 64));
123 }
124};
125
131
132struct Node
133{
134 halp_meta(name, "MIDI display")
135 halp_meta(c_name, "MidiDisplay")
136 halp_meta(category, "Monitoring/Midi")
137 halp_meta(author, "ossia score")
138 halp_meta(manual_url, "")
139 halp_meta(description, "Visualize the MIDI messages going through a port")
140 halp_meta(uuid, "a2b6c7f1-3d54-4e9a-9c21-8f0d5e73b104")
141 halp_flag(fully_custom_item);
142
143 static double recommended_height() { return 150.; }
144
145 struct
146 {
147 halp::midi_bus<"in"> midi;
149 halp::time_chooser<"Window", halp::range{0.5, 60., 8.}> window;
150
151 struct : halp::combobox_t<"Mode", Mode>
152 {
153 struct range
154 {
155 std::string_view values[5]{
156 mode_names[0], mode_names[1], mode_names[2], mode_names[3], mode_names[4]};
157 Mode init{Mode::PianoRoll};
158 };
159 } mode;
160 struct : halp::spinbox_i32<"Channel", halp::range{0, 16, 0}>
161 {
162 halp_meta(description, "Show only this channel; 0 shows all of them")
163 } channel;
164 struct : halp::toggle_t<bool, "Hide clock", halp::toggle_setup{.init = true}>
165 {
166 halp_meta(description, "Leave timing clock and active sensing out of the event list")
167 } hide_clock;
168 struct : halp::combobox_t<"Palette", NotePalette>
169 {
170 halp_meta(description, "Colours of the notes: by velocity, or by channel")
171 struct range
172 {
173 std::string_view values[6]{"Green", "Inferno", "Viridis", "Magma", "Turbo", "Channels"};
174 NotePalette init{NotePalette::Green};
175 };
176 } palette;
177 } inputs;
178
179 struct
180 {
181 // Not written: the view reads the monitor. Declared so that documents
182 // keep the outlet they were saved with.
183 struct : halp::val_port<"events", std::optional<std::vector<float>>>
184 {
185 enum widget
186 {
187 control
188 };
189 } events;
190 } outputs;
191
192 static constexpr double heartbeat_seconds = 0.03;
193 // A note held for longer than this without a note-off is reported as stuck.
194 static constexpr double stuck_after_seconds = 2.;
195
196 enum event_flag : int
197 {
198 flag_none = 0,
199 flag_retrigger = 1, // note-on while the same note was already held
200 flag_orphan = 2, // note-off for a note that was not held
201 };
202
204 {
205 bool active{};
206 float on{};
207 };
208
209 std::shared_ptr<MidiMonitor> monitor = std::make_shared<MidiMonitor>();
210 double m_rate = 48000.;
211 int64_t m_frames = 0;
212 double m_last_push = -1e9;
213 uint64_t m_seq = 0;
214 // Pairing is done here rather than in the layer: this sees every message from
215 // the first one on, while the layer only sees what came while it was shown.
216 std::array<std::array<held_note, 128>, 16> m_held_notes{};
217 HeldNotes m_held_state;
218
219 void prepare(halp::setup s) noexcept
220 {
221 m_rate = s.rate > 0 ? s.rate : 48000.;
222 m_frames = 0;
223 m_last_push = -1e9;
224 m_seq = 0;
225 m_held_notes = {};
226 }
227
228 using tick = halp::tick_musical;
229 void operator()(halp::tick_musical t)
230 {
231 const double t0 = m_frames / m_rate;
232 const double t1 = (m_frames + t.frames) / m_rate;
233 m_frames += t.frames;
234
235 const bool watched = monitor->watched();
236 bool got_message = false;
237 for(const auto& m : inputs.midi)
238 {
239 if(m.bytes.empty())
240 continue;
241
242 const double ts = t0 + double(m.timestamp) / m_rate;
243 const auto n = m.bytes.size();
244 const uint8_t status = m.bytes[0];
245 const uint8_t d1 = n > 1 ? m.bytes[1] : 0;
246 const uint8_t d2 = n > 2 ? m.bytes[2] : 0;
247 const auto flags = uint8_t(track(status, d1, d2, float(ts)));
248 if(watched)
249 monitor->events.push({m_seq, ts, status, d1, d2, flags, uint32_t(n)});
250 m_seq++;
251 got_message = true;
252 }
253
254 if(watched && (got_message || (t1 - m_last_push) >= heartbeat_seconds))
255 {
256 auto& h = m_held_state;
257 h = {};
258 for(int chan = 0; chan < 16; chan++)
259 {
260 for(int pitch = 0; pitch < 128; pitch++)
261 {
262 const auto& note = m_held_notes[chan][pitch];
263 if(!note.active)
264 continue;
265 h.bits[chan][pitch / 64] |= uint64_t(1) << (pitch % 64);
266 h.held++;
267 h.stuck += (t1 - note.on) > stuck_after_seconds;
268 }
269 }
270 h.now = t1;
271 h.window = inputs.window.value;
272 monitor->held.produce(h);
273 m_last_push = t1;
274 }
275 }
276
279 int track(uint8_t status, uint8_t d1, uint8_t d2, float t) noexcept
280 {
281 using T = libremidi::message_type;
282 const int chan = status & 0x0F;
283 switch(Ui::Monitor::midiMessageType(status, d1, d2))
284 {
285 case T::NOTE_ON: {
286 auto& note = m_held_notes[chan][d1];
287 const bool was_held = note.active;
288 note = {true, t};
289 return was_held ? flag_retrigger : flag_none;
290 }
291 case T::NOTE_OFF: {
292 auto& note = m_held_notes[chan][d1];
293 if(!note.active)
294 return flag_orphan;
295 note = {};
296 return flag_none;
297 }
298 default:
299 return flag_none;
300 }
301 }
302
308{
309 double x0, w, px_per_s, right;
310 TimeAxis(double now, double win, double x0, double w)
311 : x0{x0}
312 , w{w}
313 , px_per_s{w / win}
314 , right{std::floor(now * px_per_s)}
315 {
316 }
317 double operator()(double t) const noexcept
318 {
319 return std::clamp(x0 + w - (right - std::floor(t * px_per_s)), x0, x0 + w);
320 }
321};
322
324 {
325 public:
326 static constexpr double left_gutter = 34.;
327 static constexpr double header_height = 18.;
328 static constexpr double system_lane_height = 9.;
329 static constexpr int default_low_pitch = 48;
330 static constexpr int default_high_pitch = 72;
331 static constexpr int max_log = 4096;
332
333 using Event = MidiEvent;
334
335 struct Note
336 {
337 double on{};
338 double off{};
339 uint8_t chan{};
340 uint8_t pitch{};
341 uint8_t vel{};
342 bool retriggered{}; // note-on while already held: the note-off went missing
343 };
344
345 struct OpenNote
346 {
347 bool active{};
348 double on{};
349 uint8_t vel{};
350 };
351
353 {
354 double t{};
355 uint8_t chan{};
356 uint8_t pitch{};
357 };
358
360 struct Lane
361 {
362 boost::circular_buffer<std::pair<double, float>> points;
363 };
364
365 Scenario::IntervalModel* m_interval{};
366 Process::ControlInlet* m_window_inlet{};
367 const Process::ControlInlet* m_mode_inlet{};
368 const Process::ControlInlet* m_channel_inlet{};
369 const Process::ControlInlet* m_clock_inlet{};
370 const Process::ControlInlet* m_palette_inlet{};
371 score::QGraphicsCombo* m_switch{};
372 Process::MonitorLink* m_link{};
373 HeldNotes m_held_bits;
375 ossia::flat_map<int, Lane> m_lanes;
377 std::array<double, 16> m_last_activity{};
378 std::array<uint8_t, 16> m_last_status{};
380 std::array<float, 16> m_bend{};
381 std::array<float, 16> m_timbre{};
383 std::array<std::array<float, 128>, 16> m_note_pressure{};
385 {
386 double t;
387 float pitch;
388 float pressure;
389 float timbre;
390 };
392 ossia::flat_map<int, boost::circular_buffer<ExpressionPoint>> m_paths;
393
395 struct LogEntry
396 {
397 Event e;
398 QString time, text, bytes;
399 };
400 boost::circular_buffer<LogEntry> m_log;
401 boost::circular_buffer<Note> m_notes;
402 boost::circular_buffer<OrphanOff> m_orphans;
403 std::array<std::array<OpenNote, 128>, 16> m_open{};
404
406 mutable QPolygonF m_polyline;
407
408 double m_now = 0.;
409 // Held / stuck counts come from the node: it has seen the whole stream.
410 double m_window = 0.;
411 int m_held = 0;
412 int m_stuck = 0;
413
414 Layer(
415 const Process::ProcessModel& process, const Process::Context& doc,
416 QGraphicsItem* parent)
417 : Process::EffectLayerView{parent}
418 , m_interval{Scenario::closestParentInterval(process.parent())}
419 {
420 setAcceptedMouseButtons({});
421 setToolTip(tr("Green: a note that was played and released.\n"
422 "Amber, running to the right edge: currently held.\n"
423 "Red hatched: held for more than 2s without a note-off.\n"
424 "Orange: a note-on while that note was already held.\n"
425 "Magenta cross: a note-off for a note that was not held.\n"
426 "Bottom lane: non-note messages.\n"
427 "Right-click to copy the message log."));
428
429 Ui::Monitor::showInlet(process, 0, doc, this, this);
430
431 m_window_inlet = static_cast<Process::ControlInlet*>(process.inlets()[1]);
432 connect(
433 m_window_inlet, &Process::ControlInlet::valueChanged, this,
434 [this](const ossia::value&) { update(); });
435
436 if(m_interval)
437 {
438 connect(
439 m_interval, &Scenario::IntervalModel::executionEvent, this,
440 [this](Scenario::IntervalExecutionEvent ev) {
441 switch(ev)
442 {
443 case Scenario::IntervalExecutionEvent::Stopped:
444 reset();
445 break;
446 default:
447 break;
448 }
449 });
450 }
451
452 m_mode_inlet = Ui::Monitor::controlInlet(process, 2);
453 m_switch = Ui::Monitor::ModeSwitch::make(m_mode_inlet, doc, this, this);
454 m_channel_inlet = Ui::Monitor::controlInlet(process, 3);
455 m_clock_inlet = Ui::Monitor::controlInlet(process, 4);
456 m_palette_inlet = Ui::Monitor::controlInlet(process, 5);
457 Ui::Monitor::repaintOn(this, m_mode_inlet, m_channel_inlet, m_clock_inlet, m_palette_inlet);
458
459 m_link = new Process::MonitorLink{
460 process, *this,
461 [this](Process::Monitor& m) { refresh(static_cast<MidiMonitor&>(m)); },
462 [this](Process::Monitor*) { reset(); }, this};
463 }
464
465 Mode mode() const
466 {
467 return Ui::Monitor::enumValue(m_mode_inlet, Mode::PianoRoll, Mode::Expression);
468 }
470 int channelFilter() const
471 {
472 return std::clamp(int(Ui::Monitor::floatValue(m_channel_inlet, 0.f)), 0, 16) - 1;
473 }
474 bool shows(const Event& e) const
475 {
476 const int f = channelFilter();
477 if(f >= 0 && ((e.status & 0xF0) == 0xF0 || (e.status & 0x0F) != f))
478 return false;
479 return true;
480 }
481
482 double window() const noexcept
483 {
484 if(!m_window_inlet)
485 return 8.;
486 // The node's, once it has sent one: a synced window is only in seconds
487 // there. Until then, the free value of the port.
488 const double w = m_window > 0.
489 ? m_window
490 : ossia::convert<ossia::vec2f>(m_window_inlet->value())[0];
491 return std::isfinite(w) && w > 0.05 ? w : 8.;
492 }
493
494 void reset()
495 {
496 m_log.clear();
497 m_notes.clear();
498 m_orphans.clear();
499 m_open = {};
500 m_now = 0.;
501 m_held = 0;
502 m_stuck = 0;
503 m_held_bits = {};
504 m_lanes.clear();
505 m_paths.clear();
506 m_last_activity.fill(-1e9);
507 m_bend.fill(0.f);
508 m_timbre.fill(0.f);
509 m_note_pressure = {};
510 update();
511 }
512
513 void refresh(MidiMonitor& mon)
514 {
515 bool changed = false;
516 HeldNotes h;
517 if(mon.held.consume(h))
518 {
519 // The node restarted (new playback): our timeline is stale
520 if(h.now < m_now)
521 reset();
522 m_held_bits = h;
523 m_now = h.now;
524 m_held = h.held;
525 m_stuck = h.stuck;
526 m_window = h.window;
527 changed = true;
528 }
529 mon.events.drain([&](const Event& e) {
530 apply(e);
531 track(e);
533 m_log,
534 LogEntry{
535 e, QString::number(e.t, 'f', 3),
536 Ui::Monitor::describeMidi(e.status, e.d1, e.d2, e.size, true),
537 Ui::Monitor::midiBytesText(e.status, e.d1, e.d2, e.size)},
538 max_log);
539 changed = true;
540 });
541 if(changed)
542 {
543 trim();
544 update();
545 }
546 }
547
549 void track(const Event& e)
550 {
551 using T = libremidi::message_type;
552 const auto type = Ui::Monitor::midiMessageType(e.status, e.d1, e.d2);
554 return;
555 const int chan = e.status & 0x0F;
556 m_last_activity[chan] = e.t;
557 m_last_status[chan] = e.status;
558
559 auto lane = [&](int key, float v) {
560 auto& l = m_lanes[chan * 256 + key];
561 Ui::Monitor::pushRing(l.points, std::pair{e.t, v});
562 };
563 auto& pressure = m_note_pressure[chan];
564 // A point on the path of a sounding note, from its current expression
565 auto point = [&](int pitch) {
566 if(m_open[chan][pitch].active)
568 m_paths[chan * 128 + pitch],
570 e.t, float(pitch + m_bend[chan] * mpe_bend_range), pressure[pitch],
571 m_timbre[chan]});
572 };
573 // What applies to the whole channel shapes every note sounding on it
574 auto every_note = [&] {
575 for(int pitch = 0; pitch < 128; pitch++)
576 point(pitch);
577 };
578
579 switch(type)
580 {
581 case T::CONTROL_CHANGE:
582 lane(e.d1, e.d2 / 127.f);
583 if(e.d1 == 74)
584 {
585 m_timbre[chan] = e.d2 / 127.f;
586 every_note();
587 }
588 break;
589 case T::PITCH_BEND: {
590 const float bend = (int(e.d1) + (int(e.d2) << 7) - 8192) / 8192.f;
591 lane(128, bend * 0.5f + 0.5f);
592 m_bend[chan] = bend;
593 every_note();
594 break;
595 }
596 case T::AFTERTOUCH:
597 lane(129, e.d1 / 127.f);
598 pressure.fill(e.d1 / 127.f);
599 every_note();
600 break;
601 case T::POLY_PRESSURE:
602 pressure[e.d1] = e.d2 / 127.f;
603 point(e.d1);
604 break;
605 case T::NOTE_ON:
606 m_paths[chan * 128 + e.d1].clear();
607 pressure[e.d1] = e.d2 / 127.f;
608 point(e.d1);
609 break;
610 default:
611 break;
612 }
613 }
614
616 static constexpr float mpe_bend_range = 48.f;
617
618 // The flags are computed by the node against the full stream. This only
619 // turns them into geometry: an unmatched message here means our window
620 // simply does not reach far enough back, which is not an anomaly.
621 void apply(const Event& e)
622 {
623 using T = libremidi::message_type;
624 const int chan = e.status & 0x0F;
625 switch(Ui::Monitor::midiMessageType(e.status, e.d1, e.d2))
626 {
627 case T::NOTE_ON: {
628 auto& open = m_open[chan][e.d1];
629 if(open.active)
630 {
632 m_notes, Note{open.on, e.t, uint8_t(chan), e.d1, open.vel,
633 bool(e.flags & flag_retrigger)});
634 }
635 open = {true, e.t, e.d2};
636 break;
637 }
638 case T::NOTE_OFF: {
639 if(e.flags & flag_orphan)
640 {
641 Ui::Monitor::pushRing(m_orphans, OrphanOff{e.t, uint8_t(chan), e.d1});
642 break;
643 }
644
645 auto& open = m_open[chan][e.d1];
646 if(open.active)
647 {
649 m_notes, Note{open.on, e.t, uint8_t(chan), e.d1, open.vel, false});
650 open = {};
651 }
652 break;
653 }
654 default:
655 break;
656 }
657 }
658
659 void trim()
660 {
661 // Keep a bit more than the largest possible window so that changing the
662 // window slider does not blank the past.
663 const double horizon = m_now - 61.;
664 while(!m_notes.empty() && m_notes.front().off < horizon)
665 m_notes.pop_front();
666 while(!m_orphans.empty() && m_orphans.front().t < horizon)
667 m_orphans.pop_front();
668 for(auto& [k, l] : m_lanes)
669 while(l.points.size() > 1 && l.points[1].first < horizon)
670 l.points.pop_front();
671 for(auto it = m_paths.begin(); it != m_paths.end();)
672 {
673 while(!it->second.empty() && it->second.front().t < horizon)
674 it->second.pop_front();
675 it = it->second.empty() ? m_paths.erase(it) : std::next(it);
676 }
677 }
678
679 int held_count() const noexcept { return m_held; }
680 int stuck_count() const noexcept { return m_stuck; }
681
682 QString logText() const
683 {
684 QString out;
685 for(const auto& l : m_log)
686 {
687 out += QString::asprintf("%9.4f ", double(l.e.t));
688 out += l.text;
689 out += QLatin1Char('\n');
690 }
691 return out;
692 }
693
694 static QColor kind_color(libremidi::message_type k) noexcept
695 {
696 using T = libremidi::message_type;
697 switch(k)
698 {
699 case T::CONTROL_CHANGE:
700 return QColor(90, 160, 230);
701 case T::PITCH_BEND:
702 return QColor(180, 130, 230);
703 case T::PROGRAM_CHANGE:
704 return QColor(230, 190, 90);
705 case T::POLY_PRESSURE:
706 case T::AFTERTOUCH:
707 return QColor(120, 200, 190);
708 default:
709 return QColor(150, 150, 150);
710 }
711 }
712
713 void paint_impl(QPainter* p) const override
714 {
715 switch(mode())
716 {
717 case Mode::PianoRoll:
718 paintRoll(p, false);
719 break;
720 case Mode::Expression:
721 paintRoll(p, true);
722 break;
723 case Mode::EventList:
724 paintList(p);
725 break;
726 case Mode::Controllers:
727 paintLanes(p);
728 break;
729 case Mode::Activity:
730 paintActivity(p);
731 break;
732 }
733 }
734
735 void paintRoll(QPainter* p, bool expression) const
736 {
737 const auto rect = boundingRect();
738 const double W = rect.width();
739 const double H = rect.height();
740 if(W <= left_gutter + 8. || H <= header_height + system_lane_height + 8.)
741 return;
742
743 const double roll_x = left_gutter;
744 const double roll_w = W - left_gutter - 2.;
745 const double roll_y = header_height;
746 const double roll_h = H - header_height - system_lane_height;
747
748 const double win = window();
749 const double t_min = m_now - win;
750
751 const auto to_x = TimeAxis(m_now, win, roll_x, roll_w);
752
753 // Vertical pitch range: fit what is on screen, with an octave minimum
754 int lo = 127, hi = 0;
755 const auto extend = [&](int pitch) {
756 lo = std::min(lo, pitch);
757 hi = std::max(hi, pitch);
758 };
759 for(const auto& n : m_notes)
760 if(n.off >= t_min)
761 extend(n.pitch);
762 for(const auto& chan : m_open)
763 for(int pitch = 0; pitch < 128; pitch++)
764 if(chan[pitch].active)
765 extend(pitch);
766
767 if(lo > hi)
768 {
769 lo = default_low_pitch;
770 hi = default_high_pitch;
771 }
772 lo = std::max(0, lo - 1);
773 hi = std::min(127, hi + 1);
774 if(hi - lo < 12)
775 {
776 const int mid = (lo + hi) / 2;
777 lo = std::max(0, mid - 6);
778 hi = std::min(127, lo + 12);
779 }
780
781 const int rows = hi - lo + 1;
782 const double row_h = roll_h / rows;
783 const auto to_y = [&](int pitch) { return roll_y + (hi - pitch) * row_h; };
784
785 p->save();
786 p->setRenderHint(QPainter::Antialiasing, false);
787
788 draw_grid(p, roll_x, roll_w, row_h, lo, hi, to_y);
789 if(expression)
790 draw_expression(p, row_h, t_min, to_x, to_y);
791 else
792 draw_notes(p, row_h, t_min, to_x, to_y);
793 draw_orphans(p, row_h, t_min, to_x, to_y);
794 draw_system_lane(p, roll_x, H - system_lane_height, roll_w, t_min, to_x);
795 draw_header(p, W);
796
797 p->restore();
798 }
799
800 private:
801 void draw_grid(
802 QPainter* p, double roll_x, double roll_w, double row_h, int lo, int hi,
803 auto to_y) const
804 {
805 QFont f = Ui::Monitor::smallFont();
806 p->setFont(f);
807
808 for(int pitch = lo; pitch <= hi; pitch++)
809 {
810 if(pitch % 12 != 0)
811 continue;
812
813 const double line_y = to_y(pitch) + row_h;
814
815 p->setPen(QColor(72, 72, 80));
816 p->drawLine(QPointF(roll_x, line_y), QPointF(roll_x + roll_w, line_y));
817
818 // Centred on the line itself
819 p->setPen(QColor(140, 140, 150));
820 constexpr double h = 10.;
821 p->drawText(
822 QRectF(0, line_y - h / 2., roll_x - 4, h), Qt::AlignRight | Qt::AlignVCenter,
824 }
825 }
826
827 void draw_notes(
828 QPainter* p, double row_h, double t_min, auto to_x, auto to_y) const
829 {
830 p->setPen(Qt::NoPen);
831 const double bar_h = std::max(1., row_h - 1.);
832
833 const int filter = channelFilter();
834 for(const auto& n : m_notes)
835 {
836 if(n.off < t_min || (filter >= 0 && n.chan != filter))
837 continue;
838 const double x0 = to_x(n.on);
839 const double x1 = std::max(to_x(n.off), x0 + 1.);
840 QColor col = n.retriggered ? QColor(240, 160, 40) : note_color(n.vel, n.chan);
841 p->fillRect(QRectF(x0, to_y(n.pitch), x1 - x0, bar_h), col);
842 }
843
844 // Notes still held: they run all the way to "now". If they have been held
845 // for longer than the threshold, this is the missing-note-off symptom.
846 for(int chan = 0; chan < 16; chan++)
847 {
848 if(filter >= 0 && chan != filter)
849 continue;
850 for(int pitch = 0; pitch < 128; pitch++)
851 {
852 const auto& open = m_open[chan][pitch];
853 if(!open.active)
854 continue;
855
856 const double x0 = to_x(open.on);
857 const double x1 = to_x(m_now);
858 const bool stuck = (m_now - open.on) > stuck_after_seconds;
859 const QRectF r(x0, to_y(pitch), std::max(1., x1 - x0), bar_h);
860
861 if(stuck)
862 {
863 p->fillRect(r, QBrush(QColor(220, 40, 40), Qt::BDiagPattern));
864 p->fillRect(QRectF(r.left(), r.top(), 2., r.height()), QColor(255, 80, 80));
865 }
866 else
867 {
868 p->fillRect(r, QColor(230, 190, 60));
869 }
870 }
871 }
872 }
873
874 void draw_orphans(
875 QPainter* p, double row_h, double t_min, auto to_x, auto to_y) const
876 {
877 p->setPen(QPen(QColor(255, 90, 200), 1.));
878 const double s = std::min(4., std::max(2., row_h));
879 for(const auto& o : m_orphans)
880 {
881 if(o.t < t_min)
882 continue;
883 const double x = to_x(o.t);
884 const double y = to_y(o.pitch) + row_h / 2.;
885 p->drawLine(QPointF(x - s, y - s), QPointF(x + s, y + s));
886 p->drawLine(QPointF(x - s, y + s), QPointF(x + s, y - s));
887 }
888 }
889
890 void draw_system_lane(
891 QPainter* p, double x0, double y, double w, double t_min, auto to_x) const
892 {
893 p->setPen(QColor(72, 72, 80));
894 p->drawLine(QPointF(x0, y), QPointF(x0 + w, y));
895
896 using T = libremidi::message_type;
897 const auto first = std::lower_bound(
898 m_log.begin(), m_log.end(), t_min,
899 [](const LogEntry& l, double t) { return l.e.t < t; });
900 for(auto it = first; it != m_log.end(); ++it)
901 {
902 const auto& e = it->e;
903 const auto k = Ui::Monitor::midiMessageType(e.status, e.d1, e.d2);
904 if(k == T::NOTE_ON || k == T::NOTE_OFF)
905 continue;
906 p->setPen(kind_color(k));
907 const double x = to_x(e.t);
908 p->drawLine(QPointF(x, y + 1.), QPointF(x, y + system_lane_height - 1.));
909 }
910 }
911
912 void draw_header(QPainter* p, double W) const
913 {
914 const QFont f = Ui::Monitor::smallFont();
915 p->setFont(f);
916
917 const QFontMetricsF fm{f};
918 const auto band = [&](double x, double w) { return QRectF(x, 0, w, header_height); };
919 constexpr double gap = 8.;
920
921 // After the inlet and the mode switch
922 double x = m_switch ? m_switch->x() + m_switch->boundingRect().width() + gap
924
925 // Stuck notes are a problem to see at once; the rest shows on hover
926 const bool hovered = isUnderMouse();
927 if(hovered)
928 {
929 const auto held = QStringLiteral("held %1").arg(held_count());
930 p->setPen(QColor(160, 160, 168));
931 p->drawText(band(x, fm.horizontalAdvance(held)), Qt::AlignVCenter, held);
932 x += fm.horizontalAdvance(held) + gap;
933 }
934
935 if(const int stuck = stuck_count(); stuck > 0)
936 {
937 const auto txt = QStringLiteral("STUCK %1").arg(stuck);
938 p->setPen(QColor(255, 90, 90));
939 p->drawText(band(x, fm.horizontalAdvance(txt)), Qt::AlignVCenter, txt);
940 x += fm.horizontalAdvance(txt) + gap;
941 }
942
943 if(hovered && !m_log.empty())
944 {
945 const auto& txt = m_log.back().text;
946 const double avail = W - 2. - x;
947 if(fm.horizontalAdvance(txt) <= avail)
948 {
949 p->setPen(QColor(130, 130, 140));
950 p->drawText(band(x, avail), Qt::AlignRight | Qt::AlignVCenter, txt);
951 }
952 }
953 }
954
958 void draw_expression(QPainter* p, double row_h, double t_min, auto to_x, auto to_y) const
959 {
960 p->setRenderHint(QPainter::Antialiasing, true);
961 constexpr int steps = 32;
962 const auto quantize = [](float v) { return int(std::clamp(v, 0.f, 1.f) * (steps - 1) + 0.5f); };
963 const auto pen_for = [](int timbre, int pressure) {
964 const double t = double(timbre) / (steps - 1), w = double(pressure) / (steps - 1);
965 return QPen(
966 QColor::fromHsvF(0.6 - 0.6 * t, 0.8, 0.95), 1. + 6. * w, Qt::SolidLine,
967 Qt::RoundCap);
968 };
969 const int filter = channelFilter();
970 auto& run = m_polyline;
971 for(const auto& [key, path] : m_paths)
972 {
973 const int chan = key / 128;
974 if(path.empty() || (filter >= 0 && chan != filter))
975 continue;
976 // From the last point before the window, which still shapes its left edge
977 auto it = std::lower_bound(
978 path.begin(), path.end(), t_min,
979 [](const ExpressionPoint& pt, double t) { return pt.t < t; });
980 if(it != path.begin())
981 --it;
982
983 run.resize(0);
984 int run_key = -1;
985 for(; it != path.end(); ++it)
986 {
987 const auto& pt = *it;
988 // Fractional pitch: the row of the note plus its bend
989 const double y = to_y(int(std::floor(pt.pitch)))
990 - (pt.pitch - std::floor(pt.pitch)) * row_h + row_h / 2;
991 const QPointF here{to_x(pt.t), y};
992 // A segment takes the expression of the point it leads to
993 const int k = quantize(pt.timbre) * steps + quantize(pt.pressure);
994 if(k != run_key && run.size() > 1)
995 {
996 p->setPen(pen_for(run_key / steps, run_key % steps));
997 p->drawPolyline(run);
998 const QPointF last = run.back();
999 run.resize(0);
1000 run << last;
1001 }
1002 run_key = k;
1003 run << here;
1004 }
1005 // Still sounding: run to now
1006 if(!run.isEmpty() && m_open[chan][key % 128].active)
1007 run << QPointF{to_x(m_now), run.back().y()};
1008 if(run.size() > 1)
1009 {
1010 p->setPen(pen_for(run_key / steps, run_key % steps));
1011 p->drawPolyline(run);
1012 }
1013 }
1014 p->setRenderHint(QPainter::Antialiasing, false);
1015 }
1016
1017 void paintList(QPainter* p) const
1018 {
1019 using namespace Ui::Monitor;
1020 const auto r = boundingRect();
1021 p->save();
1022 p->fillRect(r, background());
1023 QFont f = score::Skin::instance().MonoFontSmall;
1024 f.setPixelSize(10);
1025 p->setFont(f);
1026 const QFontMetricsF fm{f};
1027 const double lh = fm.height();
1028 const int lines = int((r.height() - header_height) / lh);
1029 const bool hide_clock = boolValue(m_clock_inlet, true);
1030 draw_header(p, r.width());
1031 // Newest at the top
1032 int row = 0;
1033 for(auto it = m_log.rbegin(); it != m_log.rend() && row < lines; ++it)
1034 {
1035 const auto& e = it->e;
1036 if(!shows(e))
1037 continue;
1038 if(hide_clock && (e.status == 0xF8 || e.status == 0xFE))
1039 continue;
1040 const double y = header_height + row * lh;
1041 const auto type = Ui::Monitor::midiMessageType(e.status, e.d1, e.d2);
1042 p->setPen(QColor(120, 120, 130));
1043 p->drawText(QRectF(4, y, 70, lh), Qt::AlignLeft | Qt::AlignVCenter, it->time);
1044 p->setPen(e.flags ? QColor(255, 120, 120) : kind_color(type).lighter(130));
1045 p->drawText(QRectF(74, y, r.width() - 160, lh), Qt::AlignLeft | Qt::AlignVCenter, it->text);
1046 p->setPen(QColor(110, 110, 120));
1047 p->drawText(QRectF(r.width() - 90, y, 86, lh), Qt::AlignRight | Qt::AlignVCenter, it->bytes);
1048 row++;
1049 }
1050 p->restore();
1051 }
1052
1053 void paintLanes(QPainter* p) const
1054 {
1055 using namespace Ui::Monitor;
1056 const auto r = boundingRect();
1057 p->save();
1058 p->fillRect(r, background());
1059 draw_header(p, r.width());
1060 const int filter = channelFilter();
1061 std::vector<std::pair<int, const Lane*>> shown;
1062 for(const auto& [key, lane] : m_lanes)
1063 if(filter < 0 || key / 256 == filter)
1064 shown.emplace_back(key, &lane);
1065 if(shown.empty())
1066 {
1067 drawLabel(*p, r, Qt::AlignCenter, QStringLiteral("no controllers yet"));
1068 p->restore();
1069 return;
1070 }
1071 const double top = header_height, lh = (r.height() - top) / shown.size();
1072 const double x0 = left_gutter, w = r.width() - left_gutter - 2;
1073 const double win = window(), t_min = m_now - win;
1074 const auto to_x = TimeAxis(m_now, win, x0, w);
1075 for(std::size_t i = 0; i < shown.size(); i++)
1076 {
1077 const auto [key, lane] = shown[i];
1078 const QRectF row{x0, top + i * lh, w, lh};
1079 const int chan = key / 256, cc = key % 256;
1080 const QString name = cc == 128 ? QStringLiteral("bend") : cc == 129 ? QStringLiteral("press") : QStringLiteral("cc%1").arg(cc);
1081 p->setPen(gridColor());
1082 p->drawLine(row.bottomLeft(), row.bottomRight());
1083 drawLabel(*p, QRectF(0, row.top(), x0 - 2, std::min(lh, 22.)), Qt::AlignRight | Qt::AlignVCenter,
1084 name + (lh >= 20 ? QStringLiteral("\nch%1").arg(chan + 1) : QString{}));
1085 if(cc == 128)
1086 {
1087 p->setPen(majorGridColor());
1088 p->drawLine(QPointF{row.left(), row.center().y()}, QPointF{row.right(), row.center().y()});
1089 }
1090 // Held values: steps, from the last one set before the window
1091 auto& line = m_polyline;
1092 line.resize(0);
1093 const auto& pts = lane->points;
1094 auto it = std::lower_bound(
1095 pts.begin(), pts.end(), t_min,
1096 [](const std::pair<double, float>& pt, double t) { return pt.first < t; });
1097 if(it != pts.begin())
1098 --it;
1099 for(; it != pts.end(); ++it)
1100 {
1101 const auto [t, v] = *it;
1102 const double y = row.bottom() - 1 - v * (row.height() - 2);
1103 if(!line.isEmpty())
1104 line << QPointF{to_x(t), line.back().y()};
1105 line << QPointF{to_x(t), y};
1106 }
1107 if(!line.isEmpty())
1108 line << QPointF{to_x(m_now), line.back().y()};
1109 p->setRenderHint(QPainter::Antialiasing, true);
1110 p->setPen(QPen(channelColor(chan), 1.3));
1111 p->drawPolyline(line);
1112 p->setRenderHint(QPainter::Antialiasing, false);
1113 if(!lane->points.empty() && lh >= 12)
1114 drawLabel(*p, row.adjusted(0, 0, -2, 0), Qt::AlignRight | Qt::AlignTop,
1115 QString::number(cc == 128 ? int(lane->points.back().second * 16383) - 8192 : int(lane->points.back().second * 127)));
1116 }
1117 p->restore();
1118 }
1119
1120 void paintActivity(QPainter* p) const
1121 {
1122 using namespace Ui::Monitor;
1123 const auto r = boundingRect();
1124 p->save();
1125 p->fillRect(r, background());
1126 draw_header(p, r.width());
1127 // Sixteen channel lights: bright when a message just came, fading over a second
1128 const double top = header_height + 2;
1129 const double led_w = (r.width() - 4) / 16.;
1130 const double led_h = std::min(18., led_w);
1131 for(int c = 0; c < 16; c++)
1132 {
1133 const double age = m_now - m_last_activity[c];
1134 const double glow = std::clamp(1. - age, 0., 1.);
1135 QColor col = kind_color(Ui::Monitor::midiMessageType(m_last_status[c], 0, 1));
1136 if((m_last_status[c] & 0xF0) == 0x90 || (m_last_status[c] & 0xF0) == 0x80)
1137 col = QColor(76, 200, 90);
1138 col = QColor::fromRgbF(
1139 0.15 + (col.redF() - 0.15) * glow, 0.15 + (col.greenF() - 0.15) * glow,
1140 0.17 + (col.blueF() - 0.17) * glow);
1141 const QRectF led{2 + c * led_w + 1, top, led_w - 2, led_h};
1142 p->fillRect(led, col);
1143 if(led_w >= 14)
1144 {
1145 p->setPen(glow > 0.5 ? Qt::black : textColor());
1146 p->setFont(smallFont());
1147 p->drawText(led, Qt::AlignCenter, QString::number(c + 1));
1148 }
1149 }
1150 // Keyboard of the held notes, all channels or the chosen one
1151 const QRectF kb{2, top + led_h + 6, r.width() - 4, r.height() - top - led_h - 8};
1152 if(kb.height() < 8)
1153 {
1154 p->restore();
1155 return;
1156 }
1157 const int filter = channelFilter();
1158 auto held = [&](int pitch) {
1159 for(int c = 0; c < 16; c++)
1160 if((filter < 0 || c == filter) && m_held_bits.test(c, pitch))
1161 return true;
1162 return false;
1163 };
1164 // White keys across, black keys over them
1165 static constexpr bool black[12]{0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0};
1166 int whites = 0;
1167 for(int pitch = 21; pitch <= 108; pitch++)
1168 whites += !black[pitch % 12];
1169 const double ww = kb.width() / whites;
1170 double x = kb.left();
1171 std::vector<std::pair<double, int>> blacks;
1172 for(int pitch = 21; pitch <= 108; pitch++)
1173 {
1174 if(black[pitch % 12])
1175 {
1176 blacks.emplace_back(x, pitch);
1177 continue;
1178 }
1179 const QRectF key{x, kb.top(), ww - 1, kb.height()};
1180 p->fillRect(key, held(pitch) ? QColor(230, 190, 60) : QColor(225, 225, 225));
1181 if(pitch % 12 == 0 && ww >= 7)
1182 {
1183 p->setPen(QColor(90, 90, 90));
1184 p->setFont(smallFont());
1185 p->drawText(key.adjusted(0, 0, 0, -2), Qt::AlignHCenter | Qt::AlignBottom, Ui::Monitor::midiNoteName(pitch));
1186 }
1187 x += ww;
1188 }
1189 for(auto [bx, pitch] : blacks)
1190 p->fillRect(QRectF(bx - ww * 0.3, kb.top(), ww * 0.6, kb.height() * 0.6),
1191 held(pitch) ? QColor(240, 160, 40) : QColor(30, 30, 30));
1192 p->restore();
1193 }
1194
1195 QColor note_color(int vel, int chan) const noexcept
1196 {
1197 using namespace Ui::Monitor;
1198 const double t = std::clamp(vel / 127., 0., 1.);
1199 // The low end of the maps is too dark against the background
1200 auto from = [&](ColorMap m) { return QColor::fromRgb(colorMap(m)[int(40 + t * 215)]); };
1201 switch(enumValue(m_palette_inlet, NotePalette::Green, NotePalette::Channels))
1202 {
1203 case NotePalette::Inferno:
1204 return from(ColorMap::Inferno);
1205 case NotePalette::Viridis:
1206 return from(ColorMap::Viridis);
1207 case NotePalette::Magma:
1208 return from(ColorMap::Magma);
1209 case NotePalette::Turbo:
1210 return from(ColorMap::Turbo);
1211 case NotePalette::Channels:
1212 // Brighter with velocity
1213 return QColor::fromHsvF(std::fmod(chan * 0.618034, 1.), 0.65, 0.55 + 0.45 * t);
1214 default:
1215 return QColor(
1216 60 + int(80 * t), 150 + int(90 * t), 90 + int(40 * (1. - t)), 200 + int(55 * t));
1217 }
1218 }
1219 };
1220
1222 {
1223 using Process::EffectLayerPresenter::EffectLayerPresenter;
1224 void fillContextMenu(
1225 QMenu& menu, QPoint, QPointF, const Process::LayerContextMenuManager&) override
1226 {
1227 auto* copy = menu.addAction(tr("Copy MIDI log"));
1228 connect(copy, &QAction::triggered, this, [this] {
1229 qApp->clipboard()->setText(static_cast<Layer*>(this->m_view)->logText());
1230 });
1231
1232 auto* clear = menu.addAction(tr("Clear MIDI log"));
1233 connect(clear, &QAction::triggered, this, [this] {
1234 static_cast<Layer*>(this->m_view)->reset();
1235 });
1236 }
1237 };
1238};
1239}
Definition Port.hpp:247
Definition EffectLayer.hpp:33
Definition EffectLayer.hpp:17
Definition EventRing.hpp:27
Definition Monitor.hpp:59
Definition LayerContextMenu.hpp:38
The Process class.
Definition score-lib-process/Process/Process.hpp:82
Definition IntervalModel.hpp:50
Definition QGraphicsCombo.hpp:24
HeldNotes
A note still held at the end of the loop.
Definition MidiLoop.hpp:25
Base classes and tools to implement processes and layers.
Definition JSONVisitor.hpp:1115
Main plug-in of score.
Definition ConnectionsList.hpp:22
MIDI display: a scrolling monitor for whatever goes through a MIDI port.
Definition MidiDisplay.hpp:76
Definition BufferDecode.hpp:16
QColor channelColor(int i)
Colours for the n-th channel or trace, distinct on a dark background.
Definition Paint.hpp:69
void pushRing(boost::circular_buffer< T > &ring, V &&v, std::size_t limit=std::numeric_limits< std::size_t >::max())
Definition Ring.hpp:17
void repaintOn(Item *item, C *... controls)
Repaints item whenever one of the given controls changes in the model.
Definition LayerHelpers.hpp:87
QString midiBytesText(uint8_t status, uint8_t d1, uint8_t d2, uint32_t size)
The bytes of a message in hexadecimal, or its size for a SysEx.
Definition MidiText.hpp:137
ColorMap
Definition Paint.hpp:122
QString describeMidi(uint8_t status, uint8_t d1, uint8_t d2, uint32_t size, bool aligned)
Definition MidiText.hpp:57
constexpr bool isChannelMessage(libremidi::message_type t) noexcept
Whether the message belongs to a channel, as opposed to the system ones.
Definition MidiText.hpp:46
void drawLabel(QPainter &p, const QRectF &r, int flags, const QString &text, QColor c=textColor())
Draws text in the small monitor font, flags as for QPainter::drawText.
Definition Paint.hpp:370
constexpr libremidi::message_type midiMessageType(uint8_t status, uint8_t d1, uint8_t d2) noexcept
Definition MidiText.hpp:30
void showInlet(const Process::ProcessModel &p, std::size_t index, const Process::Context &doc, QGraphicsItem *parent, QObject *owner, double y=firstInletY)
Definition LayerHelpers.hpp:72
const Process::ControlInlet * controlInlet(const Process::ProcessModel &p, std::size_t index) noexcept
Definition LayerHelpers.hpp:36
QString midiNoteName(int pitch)
The name of a MIDI note, middle C (60) being C4; "?" out of 0 to 127.
Definition MidiText.hpp:15
Definition ProcessContext.hpp:12
static score::QGraphicsCombo * make(const Process::ControlInlet *mode, const Process::Context &ctx, QGraphicsItem *parent, QObject *owner)
The switch for mode, a combo box control, or null when it is not one.
Definition score-lib-process/Process/Monitoring/ModeSwitch.hpp:39
static constexpr QPointF position
Where views put it: right of the inlets of their top-left corner.
Definition score-lib-process/Process/Monitoring/ModeSwitch.hpp:23
Definition Monitor.hpp:27
Which notes are held on each channel.
Definition MidiDisplay.hpp:113
One message as the node saw it.
Definition MidiDisplay.hpp:100
uint32_t size
Total size of the message: more than 3 for a SysEx.
Definition MidiDisplay.hpp:108
Definition MidiDisplay.hpp:127
Definition MidiDisplay.hpp:385
A continuous controller over time: CC, pitch bend, pressure.
Definition MidiDisplay.hpp:361
A message and its text, formatted once when it arrives.
Definition MidiDisplay.hpp:396
Definition MidiDisplay.hpp:336
Definition MidiDisplay.hpp:346
Definition MidiDisplay.hpp:353
Definition MidiDisplay.hpp:324
ossia::flat_map< int, Lane > m_lanes
Key: channel * 256 + controller; 128 is pitch bend, 129 channel pressure.
Definition MidiDisplay.hpp:375
std::array< std::array< float, 128 >, 16 > m_note_pressure
Per channel and note: velocity, then channel or polyphonic pressure.
Definition MidiDisplay.hpp:383
std::array< double, 16 > m_last_activity
Per channel: when it last received something, and what.
Definition MidiDisplay.hpp:377
std::array< float, 16 > m_bend
Per channel, the state an MPE note is shaped by.
Definition MidiDisplay.hpp:380
static constexpr float mpe_bend_range
Semitones of a full pitch bend for an MPE member channel.
Definition MidiDisplay.hpp:616
QPolygonF m_polyline
Reused by the painting, which would otherwise allocate one per lane and note.
Definition MidiDisplay.hpp:406
int channelFilter() const
The channel shown, 0 to 15, or -1 for all.
Definition MidiDisplay.hpp:470
ossia::flat_map< int, boost::circular_buffer< ExpressionPoint > > m_paths
Per channel and note, the path of a sounding MPE note.
Definition MidiDisplay.hpp:392
void track(const Event &e)
Controllers, activity and MPE expression.
Definition MidiDisplay.hpp:549
Definition MidiDisplay.hpp:1222
Definition MidiDisplay.hpp:308
Definition MidiDisplay.hpp:204
Definition MidiDisplay.hpp:133
halp::time_chooser<"Window", halp::range{0.5, 60., 8.}> window
How much of the past is shown: seconds, or a note value (bars).
Definition MidiDisplay.hpp:149
int track(uint8_t status, uint8_t d1, uint8_t d2, float t) noexcept
Definition MidiDisplay.hpp:279
Definition MIDISync.hpp:149