From 9d8a2f780bb7c1f842297d423aee7081d7f0f3b6 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 18 Dec 2021 17:54:26 -0500 Subject: [PATCH] GUI: add a wavetable editor --- CMakeLists.txt | 1 + src/engine/engine.cpp | 12 ++++ src/engine/engine.h | 3 + src/gui/gui.cpp | 44 ++++++++++-- src/gui/gui.h | 7 ++ src/gui/plot_nolerp.cpp | 149 ++++++++++++++++++++++++++++++++++++++++ src/gui/plot_nolerp.h | 3 + 7 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 src/gui/plot_nolerp.cpp create mode 100644 src/gui/plot_nolerp.h diff --git a/CMakeLists.txt b/CMakeLists.txt index bad117aa..ace4cb52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,7 @@ extern/imgui/backends/imgui_impl_sdl.cpp extern/imgui/misc/cpp/imgui_stdlib.cpp extern/igfd/ImGuiFileDialog.cpp +src/gui/plot_nolerp.cpp src/gui/font_main.cpp src/gui/font_pat.cpp src/gui/gui.cpp) diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 100ee7c2..c9861c06 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -180,6 +180,18 @@ bool DivEngine::isSTDSystem(DivSystem sys) { return (sys!=DIV_SYSTEM_ARCADE && sys!=DIV_SYSTEM_YMU759); } +int DivEngine::getWaveRes(DivSystem sys) { + switch (sys) { + case DIV_SYSTEM_GB: + return 15; + case DIV_SYSTEM_PCE: + return 31; + default: + return 31; + } + return 31; +} + const char* chanNames[11][17]={ {"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8", "Channel 9", "Channel 10", "Channel 11", "Channel 12", "Channel 13", "Channel 14", "Channel 15", "Channel 16", "PCM"}, // YMU759 {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "Square 1", "Square 2", "Square 3", "Noise"}, // Genesis diff --git a/src/engine/engine.h b/src/engine/engine.h index 2ba7e182..f5cdc18d 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -134,6 +134,9 @@ class DivEngine { // is STD system bool isSTDSystem(DivSystem sys); + // get wave resolution + int getWaveRes(DivSystem sys); + // is channel muted bool isChannelMuted(int chan); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index b891b175..027fada9 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -8,6 +8,7 @@ #include "imgui.h" #include "imgui_internal.h" #include "ImGuiFileDialog.h" +#include "plot_nolerp.h" #include "misc/cpp/imgui_stdlib.h" #include #include @@ -635,11 +636,9 @@ void FurnaceGUI::drawWaveList() { for (int i=0; i<(int)e->song.wave.size(); i++) { DivWavetable* wave=e->song.wave[i]; for (int i=0; ilen; i++) { - wavePreview[i<<2]=wave->data[i]; - wavePreview[1+(i<<2)]=wave->data[i]; - wavePreview[2+(i<<2)]=wave->data[i]; - wavePreview[3+(i<<2)]=wave->data[i]; + wavePreview[i]=wave->data[i]; } + if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; if (ImGui::Selectable(fmt::sprintf("%.2x##_WAVE%d\n",i,i).c_str(),curWave==i)) { curWave=i; } @@ -649,7 +648,7 @@ void FurnaceGUI::drawWaveList() { } } ImGui::SameLine(); - ImGui::PlotLines(fmt::sprintf("##_WAVEP%d",i).c_str(),wavePreview,wave->len*4,0,NULL,0,32); + PlotNoLerp(fmt::sprintf("##_WAVEP%d",i).c_str(),wavePreview,wave->len+1,0,NULL,0,e->getWaveRes(e->song.system)); } } if (ImGui::IsWindowFocused()) curWindow=GUI_WINDOW_WAVE_LIST; @@ -658,7 +657,30 @@ void FurnaceGUI::drawWaveList() { void FurnaceGUI::drawWaveEdit() { if (!waveEditOpen) return; + float wavePreview[256]; if (ImGui::Begin("Wavetable Editor",&waveEditOpen)) { + if (curWave<0 || curWave>=(int)e->song.wave.size()) { + ImGui::Text("no wavetable selected"); + } else { + DivWavetable* wave=e->song.wave[curWave]; + for (int i=0; ilen; i++) { + wavePreview[i]=wave->data[i]; + } + if (wave->len>0) wavePreview[wave->len]=wave->data[wave->len-1]; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); + ImVec2 contentRegion=ImGui::GetContentRegionAvail(); + PlotNoLerp("##Waveform",wavePreview,wave->len+1,0,NULL,0,e->getWaveRes(e->song.system),contentRegion); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + waveDragStart=ImGui::GetItemRectMin(); + waveDragAreaSize=contentRegion; + waveDragMin=0; + waveDragMax=e->getWaveRes(e->song.system); + waveDragLen=wave->len; + waveDragActive=true; + waveDragTarget=wave->data; + } + ImGui::PopStyleVar(); + } } if (ImGui::IsWindowFocused()) curWindow=GUI_WINDOW_WAVE_EDIT; ImGui::End(); @@ -1319,10 +1341,22 @@ bool FurnaceGUI::loop() { *macroLoopDragTarget=x; } } + if (waveDragActive) { + if (waveDragLen>0) { + int x=(ev.motion.x-waveDragStart.x)*waveDragLen/waveDragAreaSize.x; + if (x<0) x=0; + if (x>=waveDragLen) x=waveDragLen-1; + int y=round(waveDragMax-((ev.motion.y-waveDragStart.y)*(double(waveDragMax-waveDragMin)/(double)waveDragAreaSize.y))); + if (y>waveDragMax) y=waveDragMax; + if (yValues + (size_t)idx * plot_data->Stride); + return v; +} + +int PlotNoLerpEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 frame_size) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + return -1; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true); + if (frame_size.x == 0.0f) + frame_size.x = ImGui::CalcItemWidth(); + if (frame_size.y == 0.0f) + frame_size.y = label_size.y + (style.FramePadding.y * 2); + + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); + const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0)); + ImGui::ItemSize(total_bb, style.FramePadding.y); + if (!ImGui::ItemAdd(total_bb, 0, &frame_bb)) + return -1; + const bool hovered = ImGui::ItemHoverable(frame_bb, id); + + // Determine scale from values if not specified + if (scale_min == FLT_MAX || scale_max == FLT_MAX) + { + float v_min = FLT_MAX; + float v_max = -FLT_MAX; + for (int i = 0; i < values_count; i++) + { + const float v = values_getter(data, i); + if (v != v) // Ignore NaN values + continue; + v_min = ImMin(v_min, v); + v_max = ImMax(v_max, v); + } + if (scale_min == FLT_MAX) + scale_min = v_min; + if (scale_max == FLT_MAX) + scale_max = v_max; + } + + ImGui::RenderFrame(frame_bb.Min, frame_bb.Max, ImGui::GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + + const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1; + int idx_hovered = -1; + if (values_count >= values_count_min) + { + int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); + int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); + + // Tooltip on hover + if (hovered && inner_bb.Contains(g.IO.MousePos)) + { + const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f); + const int v_idx = (int)(t * item_count); + IM_ASSERT(v_idx >= 0 && v_idx < values_count); + + const float v0 = values_getter(data, (v_idx + values_offset) % values_count); + const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count); + if (plot_type == ImGuiPlotType_Lines) + ImGui::SetTooltip("%d: %8.4g", v_idx, v0); + else if (plot_type == ImGuiPlotType_Histogram) + ImGui::SetTooltip("%d: %8.4g", v_idx, v0); + idx_hovered = v_idx; + } + + const float t_step = 1.0f / (float)res_w; + const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min)); + + float v0 = values_getter(data, (0 + values_offset) % values_count); + float t0 = 0.0f; + ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle + float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (1 + scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands + + const ImU32 col_base = ImGui::GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram); + const ImU32 col_hovered = ImGui::GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered); + + for (int n = 0; n < res_w; n++) + { + const float t1 = t0 + t_step; + const int v1_idx = (int)(t0 * item_count + 0.5f); + IM_ASSERT(v1_idx >= 0 && v1_idx < values_count); + const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count); + const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) ); + + // NB: Draw calls are merged together by the DrawList system. Still, we should render our batch are lower level to save a bit of CPU. + ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0); + ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, tp1); + ImVec2 pos2 = ImLerp(inner_bb.Min, inner_bb.Max, tp0); + ImVec2 pos3 = ImLerp(inner_bb.Min, inner_bb.Max, tp1); + pos1.y=pos0.y; + pos2.x=pos3.x; + if (plot_type == ImGuiPlotType_Lines) + { + window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); + window->DrawList->AddLine(pos2, pos3, idx_hovered == v1_idx ? col_hovered : col_base); + } + else if (plot_type == ImGuiPlotType_Histogram) + { + if (pos1.x >= pos0.x + 2.0f) + pos1.x -= 1.0f; + window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); + } + + t0 = t1; + tp0 = tp1; + } + } + + // Text overlay + if (overlay_text) + ImGui::RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f)); + + if (label_size.x > 0.0f) + ImGui::RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); + + // Return hovered index or -1 if none are hovered. + // This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx(). + return idx_hovered; +} + +void PlotNoLerp(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) +{ + FurnacePlotArrayGetterData data(values, stride); + PlotNoLerpEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} \ No newline at end of file diff --git a/src/gui/plot_nolerp.h b/src/gui/plot_nolerp.h new file mode 100644 index 00000000..8e4ea4ae --- /dev/null +++ b/src/gui/plot_nolerp.h @@ -0,0 +1,3 @@ +#include "imgui.h" + +void PlotNoLerp(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); \ No newline at end of file