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;
362 boost::circular_buffer<std::pair<double, float>> points;
378 std::array<uint8_t, 16> m_last_status{};
381 std::array<float, 16> m_timbre{};
392 ossia::flat_map<int, boost::circular_buffer<ExpressionPoint>>
m_paths;
398 QString time, text, bytes;
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{};
410 double m_window = 0.;
416 QGraphicsItem* parent)
417 :
Process::EffectLayerView{parent}
418 , m_interval{
Scenario::closestParentInterval(process.parent())}
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."));
433 m_window_inlet, &Process::ControlInlet::valueChanged,
this,
434 [
this](
const ossia::value&) { update(); });
439 m_interval, &Scenario::IntervalModel::executionEvent,
this,
440 [
this](Scenario::IntervalExecutionEvent ev) {
443 case Scenario::IntervalExecutionEvent::Stopped:
467 return Ui::Monitor::enumValue(m_mode_inlet, Mode::PianoRoll, Mode::Expression);
472 return std::clamp(
int(Ui::Monitor::floatValue(m_channel_inlet, 0.f)), 0, 16) - 1;
474 bool shows(
const Event& e)
const
477 if(f >= 0 && ((e.status & 0xF0) == 0xF0 || (e.status & 0x0F) != f))
482 double window() const noexcept
488 const double w = m_window > 0.
490 : ossia::convert<ossia::vec2f>(m_window_inlet->value())[0];
491 return std::isfinite(w) && w > 0.05 ? w : 8.;
513 void refresh(MidiMonitor& mon)
515 bool changed =
false;
517 if(mon.held.consume(h))
529 mon.events.drain([&](
const Event& e) {
535 e, QString::number(e.t,
'f', 3),
551 using T = libremidi::message_type;
555 const int chan = e.status & 0x0F;
557 m_last_status[chan] = e.status;
559 auto lane = [&](
int key,
float v) {
560 auto& l =
m_lanes[chan * 256 + key];
565 auto point = [&](
int pitch) {
566 if(m_open[chan][pitch].active)
574 auto every_note = [&] {
575 for(
int pitch = 0; pitch < 128; pitch++)
581 case T::CONTROL_CHANGE:
582 lane(e.d1, e.d2 / 127.f);
585 m_timbre[chan] = e.d2 / 127.f;
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);
597 lane(129, e.d1 / 127.f);
598 pressure.fill(e.d1 / 127.f);
601 case T::POLY_PRESSURE:
602 pressure[e.d1] = e.d2 / 127.f;
606 m_paths[chan * 128 + e.d1].clear();
607 pressure[e.d1] = e.d2 / 127.f;
621 void apply(
const Event& e)
623 using T = libremidi::message_type;
624 const int chan = e.status & 0x0F;
628 auto& open = m_open[chan][e.d1];
632 m_notes,
Note{open.on, e.t, uint8_t(chan), e.d1, open.vel,
633 bool(e.flags & flag_retrigger)});
635 open = {
true, e.t, e.d2};
639 if(e.flags & flag_orphan)
645 auto& open = m_open[chan][e.d1];
649 m_notes,
Note{open.on, e.t, uint8_t(chan), e.d1, open.vel,
false});
663 const double horizon = m_now - 61.;
664 while(!m_notes.empty() && m_notes.front().off < horizon)
666 while(!m_orphans.empty() && m_orphans.front().t < horizon)
667 m_orphans.pop_front();
669 while(l.points.size() > 1 && l.points[1].first < horizon)
670 l.points.pop_front();
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);
679 int held_count() const noexcept {
return m_held; }
680 int stuck_count() const noexcept {
return m_stuck; }
682 QString logText()
const
685 for(
const auto& l : m_log)
687 out += QString::asprintf(
"%9.4f ",
double(l.e.t));
689 out += QLatin1Char(
'\n');
694 static QColor kind_color(libremidi::message_type k)
noexcept
696 using T = libremidi::message_type;
699 case T::CONTROL_CHANGE:
700 return QColor(90, 160, 230);
702 return QColor(180, 130, 230);
703 case T::PROGRAM_CHANGE:
704 return QColor(230, 190, 90);
705 case T::POLY_PRESSURE:
707 return QColor(120, 200, 190);
709 return QColor(150, 150, 150);
713 void paint_impl(QPainter* p)
const override
717 case Mode::PianoRoll:
720 case Mode::Expression:
723 case Mode::EventList:
726 case Mode::Controllers:
735 void paintRoll(QPainter* p,
bool expression)
const
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.)
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;
748 const double win = window();
749 const double t_min = m_now - win;
751 const auto to_x = TimeAxis(m_now, win, roll_x, roll_w);
754 int lo = 127, hi = 0;
755 const auto extend = [&](
int pitch) {
756 lo = std::min(lo, pitch);
757 hi = std::max(hi, pitch);
759 for(
const auto& n : m_notes)
762 for(
const auto& chan : m_open)
763 for(int pitch = 0; pitch < 128; pitch++)
764 if(chan[pitch].active)
769 lo = default_low_pitch;
770 hi = default_high_pitch;
772 lo = std::max(0, lo - 1);
773 hi = std::min(127, hi + 1);
776 const int mid = (lo + hi) / 2;
777 lo = std::max(0, mid - 6);
778 hi = std::min(127, lo + 12);
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; };
786 p->setRenderHint(QPainter::Antialiasing,
false);
788 draw_grid(p, roll_x, roll_w, row_h, lo, hi, to_y);
790 draw_expression(p, row_h, t_min, to_x, to_y);
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);
802 QPainter* p,
double roll_x,
double roll_w,
double row_h,
int lo,
int hi,
805 QFont f = Ui::Monitor::smallFont();
808 for(
int pitch = lo; pitch <= hi; pitch++)
813 const double line_y = to_y(pitch) + row_h;
815 p->setPen(QColor(72, 72, 80));
816 p->drawLine(QPointF(roll_x, line_y), QPointF(roll_x + roll_w, line_y));
819 p->setPen(QColor(140, 140, 150));
820 constexpr double h = 10.;
822 QRectF(0, line_y - h / 2., roll_x - 4, h), Qt::AlignRight | Qt::AlignVCenter,
828 QPainter* p,
double row_h,
double t_min,
auto to_x,
auto to_y)
const
830 p->setPen(Qt::NoPen);
831 const double bar_h = std::max(1., row_h - 1.);
834 for(
const auto& n : m_notes)
836 if(n.off < t_min || (filter >= 0 && n.chan != filter))
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);
846 for(
int chan = 0; chan < 16; chan++)
848 if(filter >= 0 && chan != filter)
850 for(
int pitch = 0; pitch < 128; pitch++)
852 const auto& open = m_open[chan][pitch];
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);
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));
868 p->fillRect(r, QColor(230, 190, 60));
875 QPainter* p,
double row_h,
double t_min,
auto to_x,
auto to_y)
const
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)
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));
890 void draw_system_lane(
891 QPainter* p,
double x0,
double y,
double w,
double t_min,
auto to_x)
const
893 p->setPen(QColor(72, 72, 80));
894 p->drawLine(QPointF(x0, y), QPointF(x0 + w, y));
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)
902 const auto& e = it->e;
904 if(k == T::NOTE_ON || k == T::NOTE_OFF)
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.));
912 void draw_header(QPainter* p,
double W)
const
914 const QFont f = Ui::Monitor::smallFont();
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.;
922 double x = m_switch ? m_switch->x() + m_switch->boundingRect().width() + gap
926 const bool hovered = isUnderMouse();
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;
935 if(
const int stuck = stuck_count(); stuck > 0)
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;
943 if(hovered && !m_log.empty())
945 const auto& txt = m_log.back().text;
946 const double avail = W - 2. - x;
947 if(fm.horizontalAdvance(txt) <= avail)
949 p->setPen(QColor(130, 130, 140));
950 p->drawText(band(x, avail), Qt::AlignRight | Qt::AlignVCenter, txt);
958 void draw_expression(QPainter* p,
double row_h,
double t_min,
auto to_x,
auto to_y)
const
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);
966 QColor::fromHsvF(0.6 - 0.6 * t, 0.8, 0.95), 1. + 6. * w, Qt::SolidLine,
971 for(
const auto& [key, path] :
m_paths)
973 const int chan = key / 128;
974 if(path.empty() || (filter >= 0 && chan != filter))
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())
985 for(; it != path.end(); ++it)
987 const auto& pt = *it;
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};
993 const int k = quantize(pt.timbre) * steps + quantize(pt.pressure);
994 if(k != run_key && run.size() > 1)
996 p->setPen(pen_for(run_key / steps, run_key % steps));
997 p->drawPolyline(run);
998 const QPointF last = run.back();
1006 if(!run.isEmpty() && m_open[chan][key % 128].active)
1007 run << QPointF{to_x(m_now), run.back().y()};
1010 p->setPen(pen_for(run_key / steps, run_key % steps));
1011 p->drawPolyline(run);
1014 p->setRenderHint(QPainter::Antialiasing,
false);
1017 void paintList(QPainter* p)
const
1020 const auto r = boundingRect();
1022 p->fillRect(r, background());
1023 QFont f = score::Skin::instance().MonoFontSmall;
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());
1033 for(
auto it = m_log.rbegin(); it != m_log.rend() && row < lines; ++it)
1035 const auto& e = it->e;
1038 if(hide_clock && (e.status == 0xF8 || e.status == 0xFE))
1040 const double y = header_height + row * lh;
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);
1053 void paintLanes(QPainter* p)
const
1056 const auto r = boundingRect();
1058 p->fillRect(r, background());
1059 draw_header(p, r.width());
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);
1067 drawLabel(*p, r, Qt::AlignCenter, QStringLiteral(
"no controllers yet"));
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++)
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{}));
1087 p->setPen(majorGridColor());
1088 p->drawLine(QPointF{row.left(), row.center().y()}, QPointF{row.right(), row.center().y()});
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())
1099 for(; it != pts.end(); ++it)
1101 const auto [t, v] = *it;
1102 const double y = row.bottom() - 1 - v * (row.height() - 2);
1104 line << QPointF{to_x(t), line.back().y()};
1105 line << QPointF{to_x(t), y};
1108 line << QPointF{to_x(m_now), line.back().y()};
1109 p->setRenderHint(QPainter::Antialiasing,
true);
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)));
1120 void paintActivity(QPainter* p)
const
1123 const auto r = boundingRect();
1125 p->fillRect(r, background());
1126 draw_header(p, r.width());
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++)
1134 const double glow = std::clamp(1. - age, 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);
1145 p->setPen(glow > 0.5 ? Qt::black : textColor());
1146 p->setFont(smallFont());
1147 p->drawText(led, Qt::AlignCenter, QString::number(c + 1));
1151 const QRectF kb{2, top + led_h + 6, r.width() - 4, r.height() - top - led_h - 8};
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))
1165 static constexpr bool black[12]{0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 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++)
1174 if(black[pitch % 12])
1176 blacks.emplace_back(x, pitch);
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)
1183 p->setPen(QColor(90, 90, 90));
1184 p->setFont(smallFont());
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));
1195 QColor note_color(
int vel,
int chan)
const noexcept
1198 const double t = std::clamp(vel / 127., 0., 1.);
1200 auto from = [&](
ColorMap m) {
return QColor::fromRgb(colorMap(m)[
int(40 + t * 215)]); };
1201 switch(enumValue(m_palette_inlet, NotePalette::Green, NotePalette::Channels))
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:
1213 return QColor::fromHsvF(std::fmod(chan * 0.618034, 1.), 0.65, 0.55 + 0.45 * t);
1216 60 +
int(80 * t), 150 +
int(90 * t), 90 +
int(40 * (1. - t)), 200 +
int(55 * t));