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;
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{};
334 float m_last_seq = -1.f;
341 QGraphicsItem* parent)
342 :
Process::EffectLayerView{parent}
343 , m_interval{
Scenario::closestParentInterval(process.parent())}
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."));
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))
366 m_window_inlet, &Process::ControlInlet::valueChanged,
this,
367 [
this](
const ossia::value&) { update(); });
372 m_interval, &Scenario::IntervalModel::executionEvent,
this,
373 [
this](Scenario::IntervalExecutionEvent ev) {
376 case Scenario::IntervalExecutionEvent::Stopped:
388 events_outlet, &Process::ControlOutlet::valueChanged,
this,
389 [
this](
const ossia::value& v) { on_events(v); });
392 double window() const noexcept
396 const double w = ossia::convert<float>(m_window_inlet->value());
397 return std::isfinite(w) && w > 0.05 ? w : 8.;
413 void on_events(
const ossia::value& v)
415 auto* list = v.target<std::vector<ossia::value>>();
416 if(!list || list->empty())
419 const int N = std::ssize(*list);
420 if(N < header_fields || (N - header_fields) % fields_per_event != 0)
423 const float now = ossia::convert<float>((*list)[0]);
429 m_held = ossia::convert<int>((*list)[1]);
430 m_stuck = ossia::convert<int>((*list)[2]);
432 for(
int i = header_fields; i + fields_per_event <= N; i += fields_per_event)
434 const float seq = ossia::convert<float>((*list)[i]);
435 if(seq <= m_last_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));
450 if(std::ssize(m_log) > max_log)
461 void apply(
const Event& e)
463 const int chan = e.status & 0x0F;
464 switch(kind_of(e.status, e.d2))
466 case msg_kind::note_on: {
467 auto& open = m_open[chan][e.d1];
471 {open.on, e.t, uint8_t(chan), e.d1, open.vel,
472 bool(e.flags & flag_retrigger)});
474 open = {
true, e.t, e.d2};
477 case msg_kind::note_off: {
478 if(e.flags & flag_orphan)
480 m_orphans.push_back({e.t, uint8_t(chan), e.d1});
484 auto& open = m_open[chan][e.d1];
487 m_notes.push_back({open.on, e.t, uint8_t(chan), e.d1, open.vel,
false});
501 const float horizon = m_now - 61.f;
502 while(!m_notes.empty() && m_notes.front().off < horizon)
504 while(!m_orphans.empty() && m_orphans.front().t < horizon)
505 m_orphans.pop_front();
508 int held_count() const noexcept {
return m_held; }
509 int stuck_count() const noexcept {
return m_stuck; }
511 QString logText()
const
514 for(
const auto& e : m_log)
516 out += QString::asprintf(
"%9.4f ",
double(e.t));
518 out += QLatin1Char(
'\n');
523 static QString describe(
const Event& e)
525 const int chan = (e.status & 0x0F) + 1;
526 switch(kind_of(e.status, e.d2))
528 case msg_kind::note_on:
529 return QStringLiteral(
"ch%1 Note On %2 (%3) vel %4")
532 .arg(note_name(e.d1))
534 case msg_kind::note_off:
535 return QStringLiteral(
"ch%1 Note Off %2 (%3) vel %4")
538 .arg(note_name(e.d1))
540 case msg_kind::poly_pressure:
541 return QStringLiteral(
"ch%1 Poly Aft. %2 -> %3")
545 case msg_kind::control_change:
546 return QStringLiteral(
"ch%1 CC %2 -> %3")
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")
557 .arg(
int(e.d1) + (
int(e.d2) << 7) - 8192);
559 return QStringLiteral(
" System 0x%1 %2 %3")
560 .arg(
int(e.status), 2, 16, QLatin1Char(
'0'))
566 static QColor kind_color(msg_kind k)
noexcept
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);
580 return QColor(150, 150, 150);
584 void paint_impl(QPainter* p)
const override
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.)
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;
597 const double win = window();
598 const double t_min = m_now - win;
600 const auto to_x = [&](
double t) {
601 return roll_x + std::clamp((t - t_min) / win, 0., 1.) * roll_w;
605 int lo = 127, hi = 0;
606 const auto extend = [&](
int pitch) {
607 lo = std::min(lo, pitch);
608 hi = std::max(hi, pitch);
610 for(
const auto& n : m_notes)
613 for(
const auto& chan : m_open)
614 for(int pitch = 0; pitch < 128; pitch++)
615 if(chan[pitch].active)
620 lo = default_low_pitch;
621 hi = default_high_pitch;
623 lo = std::max(0, lo - 1);
624 hi = std::min(127, hi + 1);
627 const int mid = (lo + hi) / 2;
628 lo = std::max(0, mid - 6);
629 hi = std::min(127, lo + 12);
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; };
637 p->setRenderHint(QPainter::Antialiasing,
false);
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);
650 QPainter* p,
double roll_x,
double roll_w,
double row_h,
int lo,
int hi,
657 for(
int pitch = lo; pitch <= hi; pitch++)
662 const double line_y = to_y(pitch) + row_h;
664 p->setPen(QColor(72, 72, 80));
665 p->drawLine(QPointF(roll_x, line_y), QPointF(roll_x + roll_w, line_y));
668 p->setPen(QColor(140, 140, 150));
669 constexpr double h = 10.;
671 QRectF(0, line_y - h / 2., roll_x - 4, h), Qt::AlignRight | Qt::AlignVCenter,
677 QPainter* p,
double row_h,
double t_min,
auto to_x,
auto to_y)
const
679 p->setPen(Qt::NoPen);
680 const double bar_h = std::max(1., row_h - 1.);
682 for(
const auto& n : m_notes)
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);
694 for(
int chan = 0; chan < 16; chan++)
696 for(
int pitch = 0; pitch < 128; pitch++)
698 const auto& open = m_open[chan][pitch];
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);
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));
714 p->fillRect(r, QColor(230, 190, 60));
721 QPainter* p,
double row_h,
double t_min,
auto to_x,
auto to_y)
const
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)
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));
736 void draw_system_lane(
737 QPainter* p,
double x0,
double y,
double w,
double t_min,
auto to_x)
const
739 p->setPen(QColor(72, 72, 80));
740 p->drawLine(QPointF(x0, y), QPointF(x0 + w, y));
742 for(
const auto& e : m_log)
746 const auto k = kind_of(e.status, e.d2);
747 if(k == msg_kind::note_on || k == msg_kind::note_off)
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.));
755 void draw_header(QPainter* p,
double W)
const
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.;
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;
772 if(
const int stuck = stuck_count(); stuck > 0)
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;
782 const auto txt = describe(m_log.back());
783 const double avail = W - 2. - x;
784 if(avail > fm.horizontalAdvance(QStringLiteral(
"ch16 Note Off ")))
786 p->setPen(QColor(130, 130, 140));
788 band(x, avail), Qt::AlignRight | Qt::AlignVCenter,
789 fm.elidedText(txt, Qt::ElideRight, avail));
794 static QColor velocity_color(
int vel)
noexcept
796 const double t = std::clamp(vel / 127., 0., 1.);
798 60 +
int(80 * t), 150 +
int(90 * t), 90 +
int(40 * (1. - t)), 200 +
int(55 * t));