Conversation
- Add NPS Graph - Fix VCM_END_ROW_CHANGED not usable in Dialog Menus. - Fix typedef class macro.
| class DialogChartProperties::GraphWidget : public GuiWidget { | ||
| public: | ||
| ~GraphWidget() override; | ||
| explicit GraphWidget(GuiContext* gui); | ||
|
|
||
| void updateGraph(); | ||
|
|
||
| void onDraw() override; | ||
|
|
||
| private: | ||
| DialogChartProperties* myDialog; | ||
| std::vector<int> data; | ||
| double peak = 0.0; | ||
| int scale = 1; | ||
| double endTime = 0.0; | ||
| }; | ||
|
|
||
| DialogChartProperties::GraphWidget::~GraphWidget() = default; | ||
|
|
||
| DialogChartProperties::GraphWidget::GraphWidget(GuiContext* gui) | ||
| : GuiWidget(gui) { | ||
| width_ = 340; | ||
| height_ = 100; | ||
| } | ||
|
|
||
| void DialogChartProperties::GraphWidget::updateGraph() { | ||
| if (gNotes->empty()) { | ||
| return; | ||
| } | ||
|
|
||
| endTime = gTempo->rowToTime(gSimfile->getEndRow()); | ||
|
|
||
| int buckets = max(0, static_cast<int>(endTime)) + 1; | ||
| scale = max(1, static_cast<int>(ceil(buckets / 300))); | ||
| if (scale > 1) buckets = static_cast<int>(buckets / scale) + 1; | ||
|
|
||
| peak = 0; | ||
| data.resize(buckets); | ||
| for (int i = 0; i < buckets; i++) data[i] = 0; | ||
|
|
||
| for (auto& note : *gNotes) { | ||
| if (note.isMine || note.isWarped || note.isFake) continue; | ||
| int bucket = static_cast<int>((note.time + 0.5) / scale); | ||
| data[bucket]++; | ||
| } | ||
|
|
||
| for (int i = 0; i < buckets; i++) { | ||
| peak = max(peak, static_cast<double>(data[i])); | ||
| } | ||
| } | ||
|
|
||
| void DialogChartProperties::GraphWidget::onDraw() { | ||
| if (gNotes->empty()) { | ||
| Draw::fill(rect_, Color32(20, 20, 20, 255)); | ||
| return; | ||
| } | ||
|
|
||
| int count = data.size(); | ||
| double barWidth = (static_cast<double>(width_) / count); | ||
| int w = barWidth + 1; | ||
|
|
||
| auto batch = Renderer::batchC(); | ||
| Draw::fill(rect_, Color32(20, 20, 20, 255)); | ||
|
|
||
| for (int i = 0; i < count; ++i) { | ||
| int h = data[i] / peak * height_; | ||
| int x = rect_.x + static_cast<int>(i * barWidth); | ||
| int y = rect_.y + height_ - h; | ||
|
|
||
| Draw::fill(&batch, {x, y, w, h}, Color32(80, 80, 80, 255)); | ||
| } | ||
|
|
||
| double time = min(endTime, gView->getCursorTime()); | ||
| int x = (time / endTime) * width_; | ||
| Draw::fill(&batch, {rect_.x + x, rect_.y, 1, height_}, | ||
| Color32(160, 160, 160, 255)); | ||
|
|
||
| batch.flush(); | ||
|
|
||
| TextStyle textStyle; | ||
| std::string info = Str::fmt("Peak: %1 NPS").arg(peak / scale, 0, 0).str; | ||
| Text::arrange(Text::TL, textStyle, info.c_str()); | ||
| Text::draw(vec2i{rect_.x + 4, rect_.y + 2}); | ||
| } |
There was a problem hiding this comment.
There were several issues with a time-based approach (random jots in the graph on consistent stream density, averaging applied to an entire bucket which makes marathon breakdowns incomprehensible) so I tried rewriting it with measure-based calculations, then converting everything back to time.
This keeps SM-style behavior. I'm not sure if sampling every scale measures is wholly congruent with SM, but the NPS graphs appear to be very similar. That could use some more testing--it might be best to pick the highest NPS within a bucket instead. But this should get 95%+ of cases accurate.
| class DialogChartProperties::GraphWidget : public GuiWidget { | |
| public: | |
| ~GraphWidget() override; | |
| explicit GraphWidget(GuiContext* gui); | |
| void updateGraph(); | |
| void onDraw() override; | |
| private: | |
| DialogChartProperties* myDialog; | |
| std::vector<int> data; | |
| double peak = 0.0; | |
| int scale = 1; | |
| double endTime = 0.0; | |
| }; | |
| DialogChartProperties::GraphWidget::~GraphWidget() = default; | |
| DialogChartProperties::GraphWidget::GraphWidget(GuiContext* gui) | |
| : GuiWidget(gui) { | |
| width_ = 340; | |
| height_ = 100; | |
| } | |
| void DialogChartProperties::GraphWidget::updateGraph() { | |
| if (gNotes->empty()) { | |
| return; | |
| } | |
| endTime = gTempo->rowToTime(gSimfile->getEndRow()); | |
| int buckets = max(0, static_cast<int>(endTime)) + 1; | |
| scale = max(1, static_cast<int>(ceil(buckets / 300))); | |
| if (scale > 1) buckets = static_cast<int>(buckets / scale) + 1; | |
| peak = 0; | |
| data.resize(buckets); | |
| for (int i = 0; i < buckets; i++) data[i] = 0; | |
| for (auto& note : *gNotes) { | |
| if (note.isMine || note.isWarped || note.isFake) continue; | |
| int bucket = static_cast<int>((note.time + 0.5) / scale); | |
| data[bucket]++; | |
| } | |
| for (int i = 0; i < buckets; i++) { | |
| peak = max(peak, static_cast<double>(data[i])); | |
| } | |
| } | |
| void DialogChartProperties::GraphWidget::onDraw() { | |
| if (gNotes->empty()) { | |
| Draw::fill(rect_, Color32(20, 20, 20, 255)); | |
| return; | |
| } | |
| int count = data.size(); | |
| double barWidth = (static_cast<double>(width_) / count); | |
| int w = barWidth + 1; | |
| auto batch = Renderer::batchC(); | |
| Draw::fill(rect_, Color32(20, 20, 20, 255)); | |
| for (int i = 0; i < count; ++i) { | |
| int h = data[i] / peak * height_; | |
| int x = rect_.x + static_cast<int>(i * barWidth); | |
| int y = rect_.y + height_ - h; | |
| Draw::fill(&batch, {x, y, w, h}, Color32(80, 80, 80, 255)); | |
| } | |
| double time = min(endTime, gView->getCursorTime()); | |
| int x = (time / endTime) * width_; | |
| Draw::fill(&batch, {rect_.x + x, rect_.y, 1, height_}, | |
| Color32(160, 160, 160, 255)); | |
| batch.flush(); | |
| TextStyle textStyle; | |
| std::string info = Str::fmt("Peak: %1 NPS").arg(peak / scale, 0, 0).str; | |
| Text::arrange(Text::TL, textStyle, info.c_str()); | |
| Text::draw(vec2i{rect_.x + 4, rect_.y + 2}); | |
| } | |
| class DialogChartProperties::GraphWidget : public GuiWidget { | |
| public: | |
| ~GraphWidget() override; | |
| explicit GraphWidget(GuiContext* gui); | |
| void updateGraph(); | |
| void onDraw() override; | |
| private: | |
| DialogChartProperties* myDialog; | |
| std::vector<int> data; | |
| double peak = 0.0; | |
| int scale = 1; | |
| int endMeasure = 0; | |
| double endTime = 0.0; | |
| }; | |
| DialogChartProperties::GraphWidget::~GraphWidget() = default; | |
| DialogChartProperties::GraphWidget::GraphWidget(GuiContext* gui) | |
| : GuiWidget(gui) { | |
| width_ = 340; | |
| height_ = 100; | |
| } | |
| void DialogChartProperties::GraphWidget::updateGraph() { | |
| if (gNotes->empty()) { | |
| return; | |
| } | |
| endMeasure = (gSimfile->getEndRow() - 1) / (ROWS_PER_BEAT * 4) + 1; | |
| endTime = gTempo->rowToTime(gSimfile->getEndRow()); | |
| scale = endMeasure / width_; | |
| int buckets = endMeasure; | |
| // Just calculate everything for charts with <680 measures. | |
| if (scale > 2) | |
| buckets = buckets / scale + 1; | |
| else | |
| scale = 1; | |
| peak = 0; | |
| data.resize(buckets); | |
| for (int i = 0; i < buckets; i++) data[i] = 0; | |
| for (auto& note : *gNotes) { | |
| int measure = note.row / (ROWS_PER_BEAT * 4); | |
| if (note.isMine || note.isWarped || note.isFake || | |
| (scale > 1 && measure % scale != 0)) | |
| continue; | |
| data[measure / scale]++; | |
| } | |
| // The peak calculation isn't always accurate if there is scaling, | |
| // but it should be good enough. | |
| for (int i = 0; i < buckets; i++) { | |
| peak = | |
| max(peak, static_cast<double>( | |
| data[i] / (gTempo->rowToTime((i * scale + 1) * 192) - | |
| gTempo->rowToTime(i * scale * 192)))); | |
| } | |
| } | |
| void DialogChartProperties::GraphWidget::onDraw() { | |
| if (gNotes->empty()) { | |
| Draw::fill(rect_, Color32(20, 20, 20, 255)); | |
| return; | |
| } | |
| int count = data.size(); | |
| double barWidth = (static_cast<double>(width_) / count); | |
| int w = barWidth + 1; | |
| auto batch = Renderer::batchC(); | |
| Draw::fill(rect_, Color32(20, 20, 20, 255)); | |
| for (int i = 0; i < count; ++i) { | |
| int h = | |
| static_cast<int>(round(data[i] / | |
| (gTempo->rowToTime((i * scale + 1) * 192) - | |
| gTempo->rowToTime(i * scale * 192)) / | |
| peak * height_)); | |
| int x = rect_.x + static_cast<int>(gTempo->rowToTime(i * scale * 192) / | |
| endTime * width_); | |
| int y = rect_.y + height_ - h; | |
| Draw::fill(&batch, {x, y, w, h}, Color32(80, 80, 80, 255)); | |
| } | |
| double time = min(endTime, gView->getCursorTime()); | |
| int x = (time / endTime) * width_; | |
| Draw::fill(&batch, {rect_.x + x, rect_.y, 1, height_}, | |
| Color32(160, 160, 160, 255)); | |
| batch.flush(); | |
| TextStyle textStyle; | |
| std::string info = Str::fmt("Peak: %1 NPS").arg(peak, 1, 1).str; | |
| Text::arrange(Text::TL, textStyle, info.c_str()); | |
| Text::draw(vec2i{rect_.x + 4, rect_.y + 2}); | |
| } | |
| } |
uvcat7
left a comment
There was a problem hiding this comment.
My suggested approach to fix the NPS sampling is not necessarily the best solution--would appreciate you giving that a look-over.
Also, the Peak NPS text can cover up the graph, so it should be moved outside of the graph.
|
I wasn't really sure how best to approach it either, but at the time the goal was to get a overview of the NPS to show noticeable spikes and other outliers. The scaling is also not ideal, but without the it, it can cause render issues with a bar being smaller then 1px on much longer files. The text for peak was more of an after thought, as peak value was initially just used for scaling the bar heights themselves, I'll see if I can move it above or below and make it not feel out of place. |
Oh, I thought it was for performance reasons. In that case we could just sample everything but only render the highest NPS in the number of measures in each pixel, which should handle every possible case. I do like having each measure always be one "bar" on the graph, since for shorter files it makes the measure density more intuitive. Not sure if you like it though. I'll check the ITGMania solution out this week sometime since it might be informative. |
|
Ya, it also helped cut down excessive drawn rectangles on much longer marathon files. |
_
Add a NPS Graph into Chart Properties.
Each bar represents one second, scale by every 5 minutes to keep the overall density low.
Also shows the current song progress as a visual aid.
Fix VCM_END_ROW_CHANGED not usable in Dialog Menus.
notifyChangescalls dialogs first, before Simfile can add a change forVCM_END_ROW_CHANGED, which gets cleared at the end of the function. This causes dialogs to miss the changes due to order of operations.Only
gSimfile->onChangescurrently triggers an additional change flag in the processing order, but it might be worth revising to capture new changes duringnotifyChangesand add it to the next call.