@soundor/vst3-host 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CMakeLists.txt +194 -0
  2. package/CMakePresets.json +115 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +17 -0
  5. package/README.md +40 -2
  6. package/cmake/Vst3HostDependencies.cmake +179 -0
  7. package/dist/cli.d.mts +1 -0
  8. package/dist/cli.mjs +88 -0
  9. package/dist/cli.mjs.map +1 -0
  10. package/dist/index.d.mts +586 -0
  11. package/dist/index.d.mts.map +1 -0
  12. package/dist/index.mjs +1 -0
  13. package/dist/prepare-ayjTVhfk.mjs +1149 -0
  14. package/dist/prepare-ayjTVhfk.mjs.map +1 -0
  15. package/native/src/Bundle.cpp +253 -0
  16. package/native/src/Bundle.h +32 -0
  17. package/native/src/EditorArea.h +92 -0
  18. package/native/src/EditorAreaMac.mm +52 -0
  19. package/native/src/EditorAreaWindows.cpp +66 -0
  20. package/native/src/EditorAreaX11.cpp +512 -0
  21. package/native/src/EditorWindow.h +72 -0
  22. package/native/src/EditorWindowMac.mm +134 -0
  23. package/native/src/EditorWindowWindows.cpp +148 -0
  24. package/native/src/EventLoop.h +50 -0
  25. package/native/src/EventLoopLinux.cpp +204 -0
  26. package/native/src/EventLoopMac.mm +146 -0
  27. package/native/src/EventLoopWindows.cpp +132 -0
  28. package/native/src/HostContext.cpp +217 -0
  29. package/native/src/HostContext.h +116 -0
  30. package/native/src/HostInterface.cpp +1026 -0
  31. package/native/src/HostInterface.h +138 -0
  32. package/native/src/HostWindow.cpp +901 -0
  33. package/native/src/HostWindow.h +187 -0
  34. package/native/src/Ipc.cpp +158 -0
  35. package/native/src/Ipc.h +91 -0
  36. package/native/src/Json.cpp +426 -0
  37. package/native/src/Json.h +85 -0
  38. package/native/src/LiveAudio.cpp +416 -0
  39. package/native/src/LiveAudio.h +121 -0
  40. package/native/src/Main.cpp +178 -0
  41. package/native/src/Paths.h +20 -0
  42. package/native/src/Plugin.cpp +1865 -0
  43. package/native/src/Plugin.h +321 -0
  44. package/native/src/Session.cpp +656 -0
  45. package/native/src/Session.h +82 -0
  46. package/native/src/Settings.cpp +93 -0
  47. package/native/src/Settings.h +23 -0
  48. package/native/src/Version.h.in +11 -0
  49. package/native/src/X11Library.cpp +127 -0
  50. package/native/src/X11Library.h +83 -0
  51. package/native/tools/Embed.cpp +49 -0
  52. package/package.json +58 -3
  53. package/protocol.json +7 -0
  54. package/targets.json +33 -0
@@ -0,0 +1,1026 @@
1
+ #include "HostInterface.h"
2
+
3
+ #include <SDL3/SDL.h>
4
+ #include <imgui.h>
5
+
6
+ #include <algorithm>
7
+ #include <array>
8
+ #include <cctype>
9
+ #include <cmath>
10
+ #include <cstdio>
11
+
12
+ namespace soundor::vst3host
13
+ {
14
+ namespace
15
+ {
16
+ // The web runtime's palette (shell/style.ts).
17
+ constexpr ImU32 background = IM_COL32(0x0b, 0x0c, 0x0f, 0xff);
18
+ constexpr ImU32 surface = IM_COL32(0x15, 0x16, 0x1a, 0xff);
19
+ constexpr ImU32 line = IM_COL32(0x2a, 0x2c, 0x33, 0xff);
20
+ constexpr ImU32 text = IM_COL32(0xe6, 0xe7, 0xea, 0xff);
21
+ constexpr ImU32 muted = IM_COL32(0x8b, 0x8f, 0x98, 0xff);
22
+ constexpr ImU32 accent = IM_COL32(0x3a, 0x7b, 0xfd, 0xff);
23
+ constexpr ImU32 accentSoft = IM_COL32(0x3a, 0x7b, 0xfd, 0x33);
24
+ constexpr ImU32 accentText = IM_COL32(0x8f, 0xb3, 0xff, 0xff);
25
+ constexpr ImU32 hover = IM_COL32(0xff, 0xff, 0xff, 0x14);
26
+ constexpr ImU32 good = IM_COL32(0x34, 0xc7, 0x7b, 0xff);
27
+ constexpr ImU32 warn = IM_COL32(0xf2, 0xb1, 0x3a, 0xff);
28
+ constexpr ImU32 bad = IM_COL32(0xff, 0x6b, 0x5e, 0xff);
29
+ constexpr ImU32 dock = IM_COL32(0x1d, 0x1f, 0x24, 0xe0);
30
+
31
+ constexpr std::array<const char*, 7> timeSignatures { "2/4", "3/4", "4/4", "5/4", "6/8", "7/8", "12/8" };
32
+ constexpr int keyboardLow = 48; // C3
33
+ constexpr int keyboardHigh = 72; // C5
34
+
35
+ enum class Icon
36
+ {
37
+ play,
38
+ pause,
39
+ stop,
40
+ speaker,
41
+ muted,
42
+ panel,
43
+ close,
44
+ };
45
+
46
+ ImVec4 color(ImU32 value)
47
+ {
48
+ return ImGui::ColorConvertU32ToFloat4(value);
49
+ }
50
+
51
+ // The web runtime's icons, drawn in their 24 × 24 box.
52
+ void drawIcon(ImDrawList* list, Icon icon, ImVec2 center, float size, ImU32 colour)
53
+ {
54
+ const float unit = size / 24.0F;
55
+ const ImVec2 origin(center.x - size / 2, center.y - size / 2);
56
+ auto at = [&](float x, float y) { return ImVec2(origin.x + x * unit, origin.y + y * unit); };
57
+ const float stroke = 2.0F * unit;
58
+ auto speaker = [&]
59
+ {
60
+ const std::array<ImVec2, 6> body { at(4, 9.5F), at(7.5F, 9.5F), at(12, 5.5F),
61
+ at(12, 18.5F), at(7.5F, 14.5F), at(4, 14.5F) };
62
+ list->AddConvexPolyFilled(body.data(), static_cast<int>(body.size()), colour);
63
+ };
64
+ switch (icon)
65
+ {
66
+ case Icon::play:
67
+ list->AddTriangleFilled(at(8, 5.5F), at(18.5F, 12), at(8, 18.5F), colour);
68
+ break;
69
+ case Icon::pause:
70
+ list->AddRectFilled(at(7, 5), at(10.5F, 19), colour);
71
+ list->AddRectFilled(at(13.5F, 5), at(17, 19), colour);
72
+ break;
73
+ case Icon::stop:
74
+ list->AddRectFilled(at(6.5F, 6.5F), at(17.5F, 17.5F), colour);
75
+ break;
76
+ case Icon::speaker:
77
+ speaker();
78
+ list->PathArcTo(at(12, 12), 4 * unit, -0.85F, 0.85F, 12);
79
+ list->PathStroke(colour, stroke);
80
+ list->PathArcTo(at(11, 12), 7.5F * unit, -0.9F, 0.9F, 16);
81
+ list->PathStroke(colour, stroke);
82
+ break;
83
+ case Icon::muted:
84
+ speaker();
85
+ list->AddLine(at(16, 9.5F), at(21, 14.5F), colour, stroke);
86
+ list->AddLine(at(21, 9.5F), at(16, 14.5F), colour, stroke);
87
+ break;
88
+ case Icon::panel:
89
+ list->AddLine(at(4, 7), at(14, 7), colour, stroke);
90
+ list->AddLine(at(18, 7), at(20, 7), colour, stroke);
91
+ list->AddLine(at(4, 17), at(8, 17), colour, stroke);
92
+ list->AddLine(at(12, 17), at(20, 17), colour, stroke);
93
+ list->AddCircle(at(16, 7), 2 * unit, colour, 12, stroke);
94
+ list->AddCircle(at(10, 17), 2 * unit, colour, 12, stroke);
95
+ break;
96
+ case Icon::close:
97
+ list->AddLine(at(6, 6), at(18, 18), colour, stroke);
98
+ list->AddLine(at(18, 6), at(6, 18), colour, stroke);
99
+ break;
100
+ }
101
+ }
102
+
103
+ // The Soundor icon, in its 108 × 108 box (viewBox 6 6 108 108).
104
+ void drawLogo(ImDrawList* list, ImVec2 origin, float size)
105
+ {
106
+ const float unit = size / 108.0F;
107
+ auto at = [&](float x, float y) { return ImVec2(origin.x + (x - 6) * unit, origin.y + (y - 6) * unit); };
108
+ list->AddRectFilled(origin, ImVec2(origin.x + size, origin.y + size), IM_COL32(0xf2, 0xf4, 0xf7, 0xff),
109
+ 30 * unit);
110
+ const ImU32 ink = IM_COL32(0x0e, 0x0f, 0x12, 0xff);
111
+ const std::array<ImVec2, 3> left { at(38, 42), at(24, 60), at(38, 78) };
112
+ const std::array<ImVec2, 3> right { at(82, 42), at(96, 60), at(82, 78) };
113
+ list->AddPolyline(left.data(), 3, ink, 7 * unit);
114
+ list->AddPolyline(right.data(), 3, ink, 7 * unit);
115
+ const ImU32 orange = IM_COL32(0xff, 0x5a, 0x1f, 0xff);
116
+ const std::array<std::array<float, 3>, 5> bars {
117
+ { { 48, 64, 56 }, { 54, 69, 51 }, { 60, 74, 46 }, { 66, 68, 52 }, { 72, 63, 57 } }
118
+ };
119
+ for (const auto& [x, from, to] : bars)
120
+ list->AddLine(at(x, from), at(x, to), orange, 4.5F * unit);
121
+ }
122
+
123
+ /** The position as bar.beat (1-based), in the time signature's beats. */
124
+ std::string barBeat(const Transport& transport, double sampleRate)
125
+ {
126
+ const double quarters = static_cast<double>(transport.samplePosition) / sampleRate * transport.tempo / 60.0;
127
+ const double beatLength = 4.0 / transport.denominator;
128
+ const double barLength = transport.numerator * beatLength;
129
+ const double bar = std::floor(quarters / barLength);
130
+ const double beat = std::floor((quarters - bar * barLength) / beatLength);
131
+ char buffer[32];
132
+ std::snprintf(buffer, sizeof buffer, "%.0f.%.0f", bar + 1, beat + 1);
133
+ return buffer;
134
+ }
135
+
136
+ bool blackKey(int pitch)
137
+ {
138
+ const int note = pitch % 12;
139
+ return note == 1 || note == 3 || note == 6 || note == 8 || note == 10;
140
+ }
141
+ } // namespace
142
+
143
+ HostInterface::HostInterface(EventLoop& loop, HostModel& model, SDL_Window* window, ImFont* semibold, bool bare)
144
+ : bare_(bare),
145
+ loop_(loop),
146
+ model_(model),
147
+ window_(window),
148
+ semibold_(semibold),
149
+ panelOpen_(! bare && model.settings.panelOpen)
150
+ {
151
+ }
152
+
153
+ bool HostInterface::animating() const
154
+ {
155
+ return wantedSize_.has_value() || model_.audio.running() || meters_[0] > 0.001F || meters_[1] > 0.001F;
156
+ }
157
+
158
+ void HostInterface::startAudio()
159
+ {
160
+ audioFailure_.clear();
161
+ try
162
+ {
163
+ model_.audio.start();
164
+ }
165
+ catch (const std::exception& error)
166
+ {
167
+ audioFailure_ = error.what();
168
+ }
169
+ audioChanged();
170
+ }
171
+
172
+ void HostInterface::audioChanged() const
173
+ {
174
+ model_.emit("audioChanged", model_.audio.status());
175
+ }
176
+
177
+ void HostInterface::play()
178
+ {
179
+ if (! model_.audio.running())
180
+ startAudio();
181
+ model_.plugin.editTransport([](Transport& transport) { transport.playing = true; });
182
+ }
183
+
184
+ void HostInterface::noteOn(int pitch)
185
+ {
186
+ if (! model_.audio.running())
187
+ startAudio();
188
+ noteOff();
189
+ Vst::Event event {};
190
+ event.type = Vst::Event::kNoteOnEvent;
191
+ event.noteOn.pitch = static_cast<int16>(pitch);
192
+ event.noteOn.velocity = 0.8F;
193
+ event.noteOn.noteId = -1;
194
+ model_.plugin.queueEvent(event);
195
+ heldNote_ = pitch;
196
+ }
197
+
198
+ void HostInterface::noteOff()
199
+ {
200
+ if (heldNote_ < 0)
201
+ return;
202
+ Vst::Event event {};
203
+ event.type = Vst::Event::kNoteOffEvent;
204
+ event.noteOff.pitch = static_cast<int16>(heldNote_);
205
+ event.noteOff.velocity = 0.8F;
206
+ event.noteOff.noteId = -1;
207
+ model_.plugin.queueEvent(event);
208
+ heldNote_ = -1;
209
+ }
210
+
211
+ void HostInterface::chooseFile()
212
+ {
213
+ static const SDL_DialogFileFilter filters[] = { { "WAV audio", "wav;wave" } };
214
+ SDL_ShowOpenFileDialog(
215
+ [](void* self, const char* const* files, int /*filter*/)
216
+ {
217
+ // Any thread; the interface's work is on the main one.
218
+ if (files == nullptr || files[0] == nullptr)
219
+ return;
220
+ auto* interface = static_cast<HostInterface*>(self);
221
+ const std::string path = files[0];
222
+ interface->loop_.post(
223
+ [interface, path]
224
+ {
225
+ interface->audioFailure_.clear();
226
+ try
227
+ {
228
+ interface->model_.audio.setInput(AudioInput::file, path);
229
+ }
230
+ catch (const std::exception& error)
231
+ {
232
+ interface->audioFailure_ = error.what();
233
+ }
234
+ interface->audioChanged();
235
+ });
236
+ },
237
+ this, window_, filters, 1, nullptr, false);
238
+ }
239
+
240
+ // ── Drawing ──────────────────────────────────────────────────────────
241
+
242
+ void HostInterface::draw(const InterfaceLayout& layout)
243
+ {
244
+ scale_ = layout.scale;
245
+ if (bare_)
246
+ {
247
+ if (layout.editorImage != nullptr)
248
+ drawEditorImage(layout);
249
+ return;
250
+ }
251
+ if (parameterGeneration_ != model_.plugin.parameterGeneration())
252
+ {
253
+ parameterGeneration_ = model_.plugin.parameterGeneration();
254
+ parameters_ = model_.plugin.hostParameters();
255
+ // The bypass a plugin framework adds beside the plugin's own, of
256
+ // the same name: the panel shows one, as the web panel does.
257
+ std::erase_if(parameters_,
258
+ [this](const HostParameter& parameter)
259
+ {
260
+ return parameter.bypass
261
+ && std::ranges::any_of(parameters_,
262
+ [&parameter](const HostParameter& other) {
263
+ return ! other.bypass && other.title == parameter.title;
264
+ });
265
+ });
266
+ }
267
+ // Preserve the meter's release time across different display rates.
268
+ const float decay = std::pow(0.85F, ImGui::GetIO().DeltaTime / 0.033F);
269
+ for (int side = 0; side < 2; ++side)
270
+ meters_[side] = std::max(model_.audio.takePeak(side), meters_[side] * decay);
271
+ if (heldNote_ >= 0 && ! ImGui::IsMouseDown(ImGuiMouseButton_Left))
272
+ noteOff();
273
+
274
+ drawStage(layout);
275
+ drawDock(layout);
276
+ if (layout.resizable)
277
+ drawResizeGrip(layout);
278
+ if (panelOpen_)
279
+ drawPanel(layout);
280
+ }
281
+
282
+ std::optional<std::pair<int, int>> HostInterface::takeEditorSize()
283
+ {
284
+ return std::exchange(wantedSize_, std::nullopt);
285
+ }
286
+
287
+ std::pair<float, float> HostInterface::pointer() const
288
+ {
289
+ float x = 0;
290
+ float y = 0;
291
+ int windowX = 0;
292
+ int windowY = 0;
293
+ // The window's own coordinates, inside or outside it.
294
+ SDL_GetGlobalMouseState(&x, &y);
295
+ SDL_GetWindowPosition(window_, &windowX, &windowY);
296
+ return { x - static_cast<float>(windowX), y - static_cast<float>(windowY) };
297
+ }
298
+
299
+ void HostInterface::drawResizeGrip(const InterfaceLayout& layout)
300
+ {
301
+ const ImVec2 origin(layout.editorX + layout.editorWidth, layout.editorY + layout.editorHeight);
302
+ const float size = 14 * scale_;
303
+ ImGui::SetNextWindowPos(origin);
304
+ ImGui::SetNextWindowSize(ImVec2(size, size));
305
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
306
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
307
+ ImGui::Begin("resize-grip", nullptr,
308
+ ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings
309
+ | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus);
310
+ ImGui::InvisibleButton("Resize the plugin view", ImVec2(size, size));
311
+ const bool hovered = ImGui::IsItemHovered();
312
+ const bool active = ImGui::IsItemActive();
313
+ gripActive_ = active;
314
+ if (hovered || active)
315
+ ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNWSE);
316
+ ImGui::GetWindowDrawList()->AddTriangleFilled(ImVec2(origin.x + 3 * scale_, origin.y + 11 * scale_),
317
+ ImVec2(origin.x + 11 * scale_, origin.y + 3 * scale_),
318
+ ImVec2(origin.x + 11 * scale_, origin.y + 11 * scale_),
319
+ hovered || active ? text : muted);
320
+ if (ImGui::IsItemActivated())
321
+ {
322
+ const auto [x, y] = pointer();
323
+ dragX_ = x - origin.x;
324
+ dragY_ = y - origin.y;
325
+ dragWanted_ = { static_cast<int>(layout.editorWidth), static_cast<int>(layout.editorHeight) };
326
+ }
327
+ // Also on the release: it can come in one frame with the last move.
328
+ if (active || ImGui::IsItemDeactivated())
329
+ {
330
+ // The corner stays under the pointer, as the web host's handle
331
+ // does: the editor is centred, so it grows twice as far as the
332
+ // pointer moves, and stops at the stage's room. The window never
333
+ // grows for it.
334
+ const auto [x, y] = pointer();
335
+ const float cornerX = x - dragX_;
336
+ const float cornerY = y - dragY_;
337
+ const float pad = padding * scale_;
338
+ const float dock = dockSpace * scale_;
339
+ const float stage = layout.width - layout.panelWidth;
340
+ const float roomX = stage - 2 * pad;
341
+ const float roomY = layout.height - pad - dock;
342
+ const float width = std::min(2 * (cornerX - stage / 2), std::max(roomX, layout.editorWidth));
343
+ const float height =
344
+ std::min(2 * (cornerY - (layout.height + pad - dock) / 2), std::max(roomY, layout.editorHeight));
345
+ const std::pair wanted { std::max(1, static_cast<int>(std::round(width))),
346
+ std::max(1, static_cast<int>(std::round(height))) };
347
+ if (wanted != dragWanted_)
348
+ {
349
+ dragWanted_ = wanted;
350
+ wantedSize_ = wanted;
351
+ }
352
+ }
353
+ else
354
+ ImGui::SetItemTooltip("Resize the plugin view (%d × %d)", static_cast<int>(layout.editorWidth),
355
+ static_cast<int>(layout.editorHeight));
356
+ ImGui::End();
357
+ ImGui::PopStyleVar(2);
358
+ }
359
+
360
+ void HostInterface::drawStage(const InterfaceLayout& layout) const
361
+ {
362
+ ImDrawList* list = ImGui::GetBackgroundDrawList();
363
+ const float stageWidth = layout.width - layout.panelWidth;
364
+ const float right = layout.editorX + layout.editorWidth;
365
+ const float bottom = layout.editorY + layout.editorHeight;
366
+ list->AddRectFilled(ImVec2(0, 0), ImVec2(stageWidth, layout.editorY), background);
367
+ list->AddRectFilled(ImVec2(0, bottom), ImVec2(stageWidth, layout.height), background);
368
+ list->AddRectFilled(ImVec2(0, layout.editorY), ImVec2(layout.editorX, bottom), background);
369
+ list->AddRectFilled(ImVec2(right, layout.editorY), ImVec2(stageWidth, bottom), background);
370
+ // The web stage's dot grid.
371
+ const float step = 20 * scale_;
372
+ const ImU32 dot = IM_COL32(0xff, 0xff, 0xff, 0x0d);
373
+ const auto rows = static_cast<int>(layout.height / step);
374
+ const auto columns = static_cast<int>(stageWidth / step);
375
+ for (int row = 0; row <= rows; ++row)
376
+ for (int column = 0; column <= columns; ++column)
377
+ {
378
+ const float x = (static_cast<float>(column) + 0.5F) * step;
379
+ const float y = (static_cast<float>(row) + 0.5F) * step;
380
+ if (x + scale_ <= layout.editorX || x >= right || y + scale_ <= layout.editorY || y >= bottom)
381
+ list->AddRectFilled(ImVec2(x, y), ImVec2(x + scale_, y + scale_), dot);
382
+ }
383
+ // A frame around the editor; the editor itself covers the inside.
384
+ const ImVec2 min(layout.editorX, layout.editorY);
385
+ const ImVec2 max(layout.editorX + layout.editorWidth, layout.editorY + layout.editorHeight);
386
+ for (int ring = 1; ring <= 6; ++ring)
387
+ {
388
+ const auto spread = static_cast<float>(ring) * 3 * scale_;
389
+ list->AddRect(ImVec2(min.x - spread, min.y - spread + 4 * scale_),
390
+ ImVec2(max.x + spread, max.y + spread + 8 * scale_), IM_COL32(0, 0, 0, 40 - ring * 6),
391
+ 6 * scale_ + spread, 3 * scale_);
392
+ }
393
+ list->AddRect(ImVec2(min.x - 1, min.y - 1), ImVec2(max.x + 1, max.y + 1), line, 0, 1.0F);
394
+ if (layout.editorImage != nullptr)
395
+ drawEditorImage(layout);
396
+ }
397
+
398
+ void HostInterface::drawEditorImage(const InterfaceLayout& layout)
399
+ {
400
+ ImGui::GetBackgroundDrawList()->AddImage(
401
+ reinterpret_cast<ImTextureID>(layout.editorImage), ImVec2(layout.editorImageX, layout.editorImageY),
402
+ ImVec2(layout.editorImageX + layout.editorImageWidth, layout.editorImageY + layout.editorImageHeight),
403
+ ImVec2(layout.editorImageU0, layout.editorImageV0), ImVec2(layout.editorImageU1, layout.editorImageV1));
404
+ }
405
+
406
+ void HostInterface::drawDock(const InterfaceLayout& layout)
407
+ {
408
+ const float stageWidth = layout.width - layout.panelWidth;
409
+ const float height = 42 * scale_;
410
+ const float width = dockWidth_ * scale_;
411
+ ImGui::SetNextWindowPos(
412
+ ImVec2(std::floor((stageWidth - width) / 2), std::floor(layout.height - 16 * scale_ - height)));
413
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 12 * scale_);
414
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4 * scale_, 4 * scale_));
415
+ ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4 * scale_, 0));
416
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1);
417
+ ImGui::PushStyleColor(ImGuiCol_WindowBg, color(dock));
418
+ ImGui::PushStyleColor(ImGuiCol_Border, color(line));
419
+ ImGui::Begin("dock", nullptr,
420
+ ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings
421
+ | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing
422
+ | ImGuiWindowFlags_NoBringToFrontOnFocus);
423
+ ImDrawList* list = ImGui::GetWindowDrawList();
424
+ const float button = 32 * scale_;
425
+
426
+ auto iconButton = [&](const char* id, Icon icon, const char* tip, bool pressed, ImU32 tint = text)
427
+ {
428
+ const bool clicked = ImGui::InvisibleButton(id, ImVec2(button, button));
429
+ const ImVec2 min = ImGui::GetItemRectMin();
430
+ const ImVec2 max = ImGui::GetItemRectMax();
431
+ if (pressed)
432
+ list->AddRectFilled(min, max, accentSoft, 8 * scale_);
433
+ else if (ImGui::IsItemHovered())
434
+ list->AddRectFilled(min, max, hover, 8 * scale_);
435
+ drawIcon(list, icon, ImVec2((min.x + max.x) / 2, (min.y + max.y) / 2), 18 * scale_,
436
+ pressed ? accentText : tint);
437
+ ImGui::SetItemTooltip("%s", tip);
438
+ ImGui::SameLine();
439
+ return clicked;
440
+ };
441
+ auto divider = [&]
442
+ {
443
+ const ImVec2 at = ImGui::GetCursorScreenPos();
444
+ const float x = at.x + 4 * scale_;
445
+ list->AddLine(ImVec2(x, at.y + 6 * scale_), ImVec2(x, at.y + button - 6 * scale_), line, 1);
446
+ ImGui::Dummy(ImVec2(9 * scale_, button));
447
+ ImGui::SameLine();
448
+ };
449
+
450
+ LiveAudio& audio = model_.audio;
451
+ const bool failed = ! audio.running() && ! audioFailure_.empty();
452
+ const bool silent = audio.muted();
453
+ const char* audioTip = failed ? "Retry audio" : ! audio.running() ? "Start audio" : silent ? "Unmute" : "Mute";
454
+ if (iconButton("audio", silent ? Icon::muted : Icon::speaker, audioTip, audio.running() && silent,
455
+ failed ? bad
456
+ : audio.running() ? text
457
+ : muted))
458
+ {
459
+ if (! audio.running())
460
+ startAudio();
461
+ else
462
+ {
463
+ audio.setMuted(! silent);
464
+ audioChanged();
465
+ }
466
+ }
467
+
468
+ // Volume: a thin track and a knob, with a meter under the track.
469
+ {
470
+ const ImVec2 size(84 * scale_, button);
471
+ ImGui::InvisibleButton("volume", size);
472
+ const ImVec2 min = ImGui::GetItemRectMin();
473
+ const float left = min.x + 6 * scale_;
474
+ const float right = min.x + size.x - 6 * scale_;
475
+ if (ImGui::IsItemActive())
476
+ {
477
+ const float value = (ImGui::GetIO().MousePos.x - left) / (right - left);
478
+ audio.setVolume(std::clamp(value, 0.0F, 1.0F));
479
+ model_.settings.volume = audio.volume();
480
+ }
481
+ if (ImGui::IsItemDeactivated())
482
+ {
483
+ model_.settings.save();
484
+ audioChanged();
485
+ }
486
+ const float y = min.y + size.y / 2;
487
+ const auto volume = static_cast<float>(audio.volume());
488
+ const float knob = left + (right - left) * volume;
489
+ list->AddRectFilled(ImVec2(left, y - 2 * scale_), ImVec2(right, y + 2 * scale_), line, 2 * scale_);
490
+ list->AddRectFilled(ImVec2(left, y - 2 * scale_), ImVec2(knob, y + 2 * scale_), silent ? muted : accent,
491
+ 2 * scale_);
492
+ const float level = std::min(1.0F, std::max(meters_[0], meters_[1]));
493
+ if (level > 0.001F)
494
+ list->AddRectFilled(ImVec2(left, y + 5 * scale_), ImVec2(left + (right - left) * level, y + 7 * scale_),
495
+ level > 0.99F ? bad
496
+ : level > 0.7F ? warn
497
+ : good,
498
+ scale_);
499
+ list->AddCircleFilled(ImVec2(knob, y), 6 * scale_,
500
+ ImGui::IsItemHovered() || ImGui::IsItemActive() ? IM_COL32(0xff, 0xff, 0xff, 0xff)
501
+ : text);
502
+ ImGui::SetItemTooltip("Output volume");
503
+ ImGui::SameLine();
504
+ }
505
+ divider();
506
+
507
+ const Transport transport = model_.plugin.transport();
508
+ if (iconButton("play", transport.playing ? Icon::pause : Icon::play, transport.playing ? "Pause" : "Play",
509
+ transport.playing))
510
+ {
511
+ if (transport.playing)
512
+ model_.plugin.editTransport([](Transport& edited) { edited.playing = false; });
513
+ else
514
+ play();
515
+ }
516
+ if (iconButton("stop", Icon::stop, "Stop", false))
517
+ model_.plugin.editTransport(
518
+ [](Transport& edited)
519
+ {
520
+ edited.playing = false;
521
+ edited.samplePosition = 0;
522
+ });
523
+ {
524
+ const std::string position = barBeat(transport, model_.plugin.setup().sampleRate);
525
+ const ImVec2 size(48 * scale_, button);
526
+ const ImVec2 at = ImGui::GetCursorScreenPos();
527
+ ImGui::Dummy(size);
528
+ const ImVec2 textSize = ImGui::CalcTextSize(position.c_str());
529
+ list->AddText(ImVec2(at.x + (size.x - textSize.x) / 2, at.y + (size.y - textSize.y) / 2),
530
+ transport.playing ? text : muted, position.c_str());
531
+ ImGui::SetItemTooltip("Position (bar.beat)");
532
+ ImGui::SameLine();
533
+ }
534
+ divider();
535
+ if (iconButton("panel", Icon::panel, "Host settings", panelOpen_))
536
+ {
537
+ panelOpen_ = ! panelOpen_;
538
+ model_.settings.panelOpen = panelOpen_;
539
+ model_.settings.save();
540
+ }
541
+ ImGui::NewLine();
542
+ dockWidth_ = ImGui::GetWindowSize().x / scale_;
543
+ ImGui::End();
544
+ ImGui::PopStyleColor(2);
545
+ ImGui::PopStyleVar(4);
546
+ }
547
+
548
+ bool HostInterface::section(const char* title, bool collapsible)
549
+ {
550
+ // Sections are divided as the web panel's: a line across the panel.
551
+ ImGui::Dummy(ImVec2(0, 6 * scale_));
552
+ if (! firstSection_)
553
+ {
554
+ const float y = ImGui::GetCursorScreenPos().y;
555
+ const float left = ImGui::GetWindowPos().x;
556
+ ImGui::GetWindowDrawList()->AddLine(ImVec2(left, y), ImVec2(left + ImGui::GetWindowWidth(), y), line, 1);
557
+ ImGui::Dummy(ImVec2(0, 8 * scale_));
558
+ }
559
+ firstSection_ = false;
560
+ std::string label = title;
561
+ for (char& c : label)
562
+ c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
563
+ ImGui::PushFont(semibold_, 11);
564
+ ImGui::PushStyleColor(ImGuiCol_Text, color(muted));
565
+ bool open = true;
566
+ if (collapsible)
567
+ {
568
+ ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
569
+ ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0));
570
+ open = ImGui::TreeNodeEx(label.c_str(),
571
+ ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanAvailWidth);
572
+ ImGui::PopStyleColor(2);
573
+ }
574
+ else
575
+ ImGui::TextUnformatted(label.c_str());
576
+ ImGui::PopStyleColor();
577
+ ImGui::PopFont();
578
+ if (open)
579
+ ImGui::Dummy(ImVec2(0, 2 * scale_));
580
+ return open;
581
+ }
582
+
583
+ bool HostInterface::rangeSlider(const char* id, double& value, double defaultValue) const
584
+ {
585
+ // A range input, as the web panel's: a track, its filled part and a knob.
586
+ const float height = 20 * scale_;
587
+ const ImVec2 size(ImGui::GetContentRegionAvail().x, height);
588
+ ImGui::InvisibleButton(id, size);
589
+ const ImVec2 min = ImGui::GetItemRectMin();
590
+ const float knobRadius = 7 * scale_;
591
+ const float left = min.x + knobRadius;
592
+ const float right = min.x + size.x - knobRadius;
593
+ bool changed = false;
594
+ const bool enabled = (ImGui::GetItemFlags() & ImGuiItemFlags_Disabled) == 0;
595
+ if (ImGui::IsItemActive() && enabled)
596
+ {
597
+ const double next = std::clamp((ImGui::GetIO().MousePos.x - left) / (right - left), 0.0F, 1.0F);
598
+ changed = next != value;
599
+ value = next;
600
+ }
601
+ if (enabled && ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
602
+ {
603
+ value = defaultValue;
604
+ changed = true;
605
+ }
606
+ ImDrawList* list = ImGui::GetWindowDrawList();
607
+ const float y = min.y + height / 2;
608
+ const float knob = left + (right - left) * static_cast<float>(value);
609
+ list->AddRectFilled(ImVec2(left, y - 2 * scale_), ImVec2(right, y + 2 * scale_), line, 2 * scale_);
610
+ list->AddRectFilled(ImVec2(left, y - 2 * scale_), ImVec2(knob, y + 2 * scale_), enabled ? accent : muted,
611
+ 2 * scale_);
612
+ list->AddCircleFilled(ImVec2(knob, y), knobRadius,
613
+ ! enabled ? muted
614
+ : ImGui::IsItemHovered() || ImGui::IsItemActive() ? IM_COL32(0xff, 0xff, 0xff, 0xff)
615
+ : text);
616
+ return changed;
617
+ }
618
+
619
+ void HostInterface::drawPanel(const InterfaceLayout& layout)
620
+ {
621
+ const float x = layout.width - layout.panelWidth;
622
+ ImGui::SetNextWindowPos(ImVec2(x, 0));
623
+ ImGui::SetNextWindowSize(ImVec2(layout.panelWidth, layout.height));
624
+ ImGui::PushStyleColor(ImGuiCol_WindowBg, color(surface));
625
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
626
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
627
+ ImGui::Begin("panel", nullptr,
628
+ ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings
629
+ | ImGuiWindowFlags_NoBringToFrontOnFocus);
630
+ ImDrawList* list = ImGui::GetWindowDrawList();
631
+ list->AddLine(ImVec2(x, 0), ImVec2(x, layout.height), line, 1);
632
+
633
+ // Header: the logo, the plugin's name and the close button.
634
+ {
635
+ const float height = 52 * scale_;
636
+ const ImVec2 origin = ImGui::GetCursorScreenPos();
637
+ drawLogo(list, ImVec2(origin.x + 16 * scale_, origin.y + 12 * scale_), 28 * scale_);
638
+ ImGui::PushFont(semibold_, 13);
639
+ const std::string name = model_.plugin.name();
640
+ list->AddText(ImVec2(origin.x + 54 * scale_, origin.y + 9 * scale_), text, name.c_str());
641
+ ImGui::PopFont();
642
+ ImGui::PushFont(nullptr, 11);
643
+ list->AddText(ImVec2(origin.x + 54 * scale_, origin.y + 28 * scale_), muted, "Soundor VST3 host");
644
+ ImGui::PopFont();
645
+ ImGui::SetCursorScreenPos(ImVec2(origin.x + layout.panelWidth - 40 * scale_, origin.y + 10 * scale_));
646
+ const float button = 32 * scale_;
647
+ if (ImGui::InvisibleButton("close", ImVec2(button, button)))
648
+ {
649
+ panelOpen_ = false;
650
+ model_.settings.panelOpen = false;
651
+ model_.settings.save();
652
+ }
653
+ const ImVec2 min = ImGui::GetItemRectMin();
654
+ const ImVec2 max = ImGui::GetItemRectMax();
655
+ if (ImGui::IsItemHovered())
656
+ list->AddRectFilled(min, max, hover, 8 * scale_);
657
+ drawIcon(list, Icon::close, ImVec2((min.x + max.x) / 2, (min.y + max.y) / 2), 18 * scale_, text);
658
+ ImGui::SetItemTooltip("Close");
659
+ ImGui::SetCursorScreenPos(ImVec2(origin.x, origin.y + height));
660
+ list->AddLine(ImVec2(x, origin.y + height), ImVec2(layout.width, origin.y + height), line, 1);
661
+ ImGui::Dummy(ImVec2(1, 1));
662
+ }
663
+
664
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16 * scale_, 8 * scale_));
665
+ ImGui::BeginChild("body", ImVec2(0, 0), ImGuiChildFlags_AlwaysUseWindowPadding);
666
+ ImGui::PushItemWidth(-1);
667
+ firstSection_ = true;
668
+ drawTransport();
669
+ drawAudio();
670
+ if (model_.plugin.hasEventInput())
671
+ drawKeyboard();
672
+ drawParameters();
673
+ drawPlugin();
674
+ drawSettings();
675
+ ImGui::PopItemWidth();
676
+ ImGui::EndChild();
677
+ ImGui::PopStyleVar();
678
+
679
+ ImGui::End();
680
+ ImGui::PopStyleVar(2);
681
+ ImGui::PopStyleColor();
682
+ }
683
+
684
+ namespace
685
+ {
686
+ /** A row: a muted label, and a control of `width` units at the right. */
687
+ void rowLabel(const char* label, float width, float scale)
688
+ {
689
+ const float right = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x;
690
+ ImGui::AlignTextToFramePadding();
691
+ ImGui::TextColored(color(muted), "%s", label);
692
+ ImGui::SameLine(right - width * scale);
693
+ ImGui::SetNextItemWidth(width * scale);
694
+ }
695
+ } // namespace
696
+
697
+ void HostInterface::drawTransport()
698
+ {
699
+ if (! section("Transport"))
700
+ return;
701
+ const Transport transport = model_.plugin.transport();
702
+ auto tempo = static_cast<float>(transport.tempo);
703
+ rowLabel("Tempo (BPM)", 72, scale_);
704
+ if (ImGui::DragFloat("##tempo", &tempo, 0.25F, 20, 999, "%.0f", ImGuiSliderFlags_AlwaysClamp))
705
+ model_.plugin.editTransport([tempo](Transport& edited) { edited.tempo = tempo; });
706
+ ImGui::SetItemTooltip("Drag, or double-click to type");
707
+
708
+ char current[16];
709
+ std::snprintf(current, sizeof current, "%d/%d", transport.numerator, transport.denominator);
710
+ rowLabel("Time signature", 132, scale_);
711
+ if (ImGui::BeginCombo("##signature", current))
712
+ {
713
+ for (const char* signature : timeSignatures)
714
+ if (ImGui::Selectable(signature, std::string_view(signature) == current))
715
+ {
716
+ int numerator = 4;
717
+ int denominator = 4;
718
+ std::sscanf(signature, "%d/%d", &numerator, &denominator);
719
+ model_.plugin.editTransport(
720
+ [numerator, denominator](Transport& edited)
721
+ {
722
+ edited.numerator = numerator;
723
+ edited.denominator = denominator;
724
+ });
725
+ }
726
+ ImGui::EndCombo();
727
+ }
728
+ }
729
+
730
+ void HostInterface::drawAudio()
731
+ {
732
+ if (! section("Audio"))
733
+ return;
734
+ LiveAudio& audio = model_.audio;
735
+ static constexpr std::array<std::pair<AudioInput, const char*>, 4> inputs { {
736
+ { AudioInput::oscillator, "Oscillator" },
737
+ { AudioInput::file, "Audio file…" },
738
+ { AudioInput::microphone, "Microphone" },
739
+ { AudioInput::none, "No input" },
740
+ } };
741
+ const char* currentLabel = "No input";
742
+ for (const auto& [input, label] : inputs)
743
+ if (input == audio.input())
744
+ currentLabel = label;
745
+ std::string preview = currentLabel;
746
+ if (audio.input() == AudioInput::file && ! audio.fileName().empty())
747
+ preview = audio.fileName();
748
+ rowLabel("Input", 132, scale_);
749
+ if (ImGui::BeginCombo("##input", preview.c_str()))
750
+ {
751
+ for (const auto& [input, label] : inputs)
752
+ if (ImGui::Selectable(label, input == audio.input()))
753
+ {
754
+ audioFailure_.clear();
755
+ if (input == AudioInput::file)
756
+ chooseFile();
757
+ else
758
+ {
759
+ audio.setInput(input);
760
+ audioChanged();
761
+ }
762
+ }
763
+ ImGui::EndCombo();
764
+ }
765
+
766
+ ImGui::PushFont(nullptr, 12);
767
+ const Setup& setup = model_.plugin.setup();
768
+ ImGui::TextColored(color(muted), "%g kHz · %d samples", setup.sampleRate / 1000, setup.blockSize);
769
+ if (audio.running())
770
+ {
771
+ const std::string device = audio.device();
772
+ ImGui::TextColored(color(muted), "Audio on%s%s", device.empty() ? "" : " · ", device.c_str());
773
+ if (audio.input() == AudioInput::microphone && ! model_.plugin.transport().playing)
774
+ ImGui::TextColored(color(muted), "Press Play to use the microphone");
775
+ }
776
+ else
777
+ ImGui::TextColored(color(muted), "Audio off");
778
+ for (const std::string& failure : { audio.error(), audio.inputError(), audioFailure_ })
779
+ if (! failure.empty())
780
+ {
781
+ ImGui::PushStyleColor(ImGuiCol_Text, color(bad));
782
+ ImGui::TextWrapped("%s", failure.c_str());
783
+ ImGui::PopStyleColor();
784
+ }
785
+ ImGui::PopFont();
786
+ ImGui::Spacing();
787
+ if (ImGui::Button(audio.running() ? "Stop audio###audio-power" : "Start audio###audio-power",
788
+ ImVec2(132 * scale_, 0)))
789
+ {
790
+ if (audio.running())
791
+ {
792
+ audio.stop();
793
+ audioChanged();
794
+ }
795
+ else
796
+ startAudio();
797
+ }
798
+ }
799
+
800
+ void HostInterface::drawKeyboard()
801
+ {
802
+ if (! section("Keyboard"))
803
+ return;
804
+ ImDrawList* list = ImGui::GetWindowDrawList();
805
+ const float width = ImGui::GetContentRegionAvail().x;
806
+ const float height = 56 * scale_;
807
+ const ImVec2 origin = ImGui::GetCursorScreenPos();
808
+ ImGui::InvisibleButton("keyboard", ImVec2(width, height));
809
+ const bool pressed = ImGui::IsItemActive();
810
+
811
+ int whites = 0;
812
+ for (int pitch = keyboardLow; pitch <= keyboardHigh; ++pitch)
813
+ whites += blackKey(pitch) ? 0 : 1;
814
+ const float whiteWidth = width / static_cast<float>(whites);
815
+ const float blackWidth = whiteWidth * 0.6F;
816
+ const float blackHeight = height * 0.6F;
817
+
818
+ // Where each key is, white keys first, then the black ones over them.
819
+ struct Key
820
+ {
821
+ int pitch;
822
+ ImVec2 min;
823
+ ImVec2 max;
824
+ };
825
+ std::vector<Key> keys;
826
+ int white = 0;
827
+ for (int pitch = keyboardLow; pitch <= keyboardHigh; ++pitch)
828
+ if (! blackKey(pitch))
829
+ {
830
+ const float left = origin.x + static_cast<float>(white++) * whiteWidth;
831
+ keys.push_back({ pitch, ImVec2(left, origin.y), ImVec2(left + whiteWidth, origin.y + height) });
832
+ }
833
+ white = 0;
834
+ for (int pitch = keyboardLow; pitch <= keyboardHigh; ++pitch)
835
+ {
836
+ if (! blackKey(pitch))
837
+ {
838
+ ++white;
839
+ continue;
840
+ }
841
+ const float center = origin.x + static_cast<float>(white) * whiteWidth;
842
+ keys.push_back({ pitch, ImVec2(center - blackWidth / 2, origin.y),
843
+ ImVec2(center + blackWidth / 2, origin.y + blackHeight) });
844
+ }
845
+
846
+ int under = -1;
847
+ const ImVec2 mouse = ImGui::GetIO().MousePos;
848
+ for (auto it = keys.rbegin(); it != keys.rend() && pressed; ++it)
849
+ if (mouse.x >= it->min.x && mouse.x < it->max.x && mouse.y >= it->min.y && mouse.y < it->max.y)
850
+ {
851
+ under = it->pitch;
852
+ break;
853
+ }
854
+ if (pressed && under >= 0 && under != heldNote_)
855
+ noteOn(under);
856
+
857
+ for (const auto& key : keys)
858
+ {
859
+ const bool black = blackKey(key.pitch);
860
+ const bool held = key.pitch == heldNote_;
861
+ const ImU32 fill = held ? accent
862
+ : black ? IM_COL32(0x0e, 0x0f, 0x12, 0xff)
863
+ : IM_COL32(0xe6, 0xe7, 0xea, 0xff);
864
+ list->AddRectFilled(key.min, key.max, fill, black ? 2 * scale_ : 3 * scale_,
865
+ ImDrawFlags_RoundCornersBottom);
866
+ if (! black)
867
+ list->AddRect(key.min, key.max, line, 3 * scale_, 1.0F, ImDrawFlags_RoundCornersBottom);
868
+ }
869
+ ImGui::PushFont(nullptr, 11);
870
+ ImGui::TextColored(color(muted), "C3 to C5");
871
+ ImGui::PopFont();
872
+ }
873
+
874
+ bool HostInterface::drawToggle(const char* title, bool on) const
875
+ {
876
+ const ImVec2 origin = ImGui::GetCursorScreenPos();
877
+ const float width = ImGui::GetContentRegionAvail().x;
878
+ const float height = ImGui::GetTextLineHeight();
879
+ const bool pressed = ImGui::InvisibleButton("##toggle", ImVec2(width, height));
880
+ ImDrawList* list = ImGui::GetWindowDrawList();
881
+ list->AddText(origin, ImGui::GetColorU32(ImGuiCol_Text), title);
882
+ // A browser's dark checkbox, which the web panel shows.
883
+ const float box = 13 * scale_;
884
+ const ImVec2 min(origin.x + width - box, origin.y + (height - box) / 2);
885
+ const ImVec2 max(min.x + box, min.y + box);
886
+ const float rounding = 2 * scale_;
887
+ const ImU32 fill = IM_COL32(0x3b, 0x3b, 0x3b, 0xff);
888
+ if (on)
889
+ {
890
+ list->AddRectFilled(min, max, IM_COL32(0x99, 0xc8, 0xff, 0xff), rounding);
891
+ const ImVec2 points[] = { ImVec2(min.x + box * 0.22F, min.y + box * 0.52F),
892
+ ImVec2(min.x + box * 0.42F, min.y + box * 0.72F),
893
+ ImVec2(min.x + box * 0.78F, min.y + box * 0.28F) };
894
+ list->AddPolyline(points, 3, fill, 1.6F * scale_);
895
+ }
896
+ else
897
+ {
898
+ list->AddRectFilled(min, max, fill, rounding);
899
+ list->AddRect(min, max, IM_COL32(0x85, 0x85, 0x85, 0xff), rounding, scale_);
900
+ }
901
+ return pressed;
902
+ }
903
+
904
+ void HostInterface::drawParameters()
905
+ {
906
+ char title[64];
907
+ std::snprintf(title, sizeof title, "Parameters (%zu)", parameters_.size());
908
+ if (! section(title))
909
+ return;
910
+ if (parameters_.empty())
911
+ {
912
+ ImGui::TextColored(color(muted), "None");
913
+ ImGui::Spacing();
914
+ return;
915
+ }
916
+ Plugin& plugin = model_.plugin;
917
+ const float sliderHeight =
918
+ ImGui::GetTextLineHeightWithSpacing() + ImGui::GetFrameHeightWithSpacing() + 6 * scale_;
919
+ const float toggleHeight = ImGui::GetTextLineHeight() + 12 * scale_;
920
+ // Rows out of sight take their height and draw nothing: a plugin can
921
+ // have thousands, of two heights.
922
+ for (const HostParameter& parameter : parameters_)
923
+ {
924
+ const float rowHeight = parameter.stepCount == 1 ? toggleHeight : sliderHeight;
925
+ if (! ImGui::IsRectVisible(ImVec2(ImGui::GetContentRegionAvail().x, rowHeight)))
926
+ {
927
+ ImGui::SetCursorPosY(ImGui::GetCursorPosY() + rowHeight);
928
+ continue;
929
+ }
930
+ ImGui::PushID(static_cast<int>(parameter.id));
931
+ const double value = plugin.normalizedValue(parameter.id);
932
+ const std::string display = plugin.valueText(parameter.id, value);
933
+ const float top = ImGui::GetCursorPosY();
934
+ const float right = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x;
935
+
936
+ ImGui::BeginDisabled(parameter.readOnly);
937
+ if (parameter.stepCount == 1)
938
+ {
939
+ // As the web panel's: the name, and a checkbox at the row's end.
940
+ bool on = value >= 0.5;
941
+ if (drawToggle(parameter.title.c_str(), on))
942
+ plugin.editParameter(parameter.id, on ? 0.0 : 1.0);
943
+ }
944
+ else
945
+ {
946
+ std::string shown = display;
947
+ if (! parameter.units.empty())
948
+ shown += " " + parameter.units;
949
+ ImGui::TextUnformatted(parameter.title.c_str());
950
+ const float shownWidth = ImGui::CalcTextSize(shown.c_str()).x;
951
+ ImGui::SameLine(std::max(right - shownWidth, ImGui::GetCursorPosX() + 8 * scale_));
952
+ ImGui::TextColored(color(muted), "%s", shown.c_str());
953
+ if (parameter.stepCount > 1 && parameter.stepCount <= 128 && parameter.list)
954
+ {
955
+ if (ImGui::BeginCombo("##value", display.c_str()))
956
+ {
957
+ const int current = static_cast<int>(std::lround(value * parameter.stepCount));
958
+ for (int step = 0; step <= parameter.stepCount; ++step)
959
+ {
960
+ const double normalized = static_cast<double>(step) / parameter.stepCount;
961
+ const std::string label = plugin.valueText(parameter.id, normalized);
962
+ ImGui::PushID(step);
963
+ if (ImGui::Selectable(label.c_str(), step == current))
964
+ plugin.editParameter(parameter.id, normalized);
965
+ ImGui::PopID();
966
+ }
967
+ ImGui::EndCombo();
968
+ }
969
+ }
970
+ else
971
+ {
972
+ double next = value;
973
+ if (rangeSlider("##value", next, parameter.defaultValue))
974
+ {
975
+ if (parameter.stepCount > 1)
976
+ next = std::round(next * parameter.stepCount) / parameter.stepCount;
977
+ plugin.editParameter(parameter.id, next);
978
+ }
979
+ ImGui::SetItemTooltip("Double-click: the default value");
980
+ }
981
+ }
982
+ ImGui::EndDisabled();
983
+ ImGui::SetCursorPosY(top + rowHeight);
984
+ ImGui::PopID();
985
+ }
986
+ }
987
+
988
+ void HostInterface::drawPlugin()
989
+ {
990
+ if (! section("Plugin", true))
991
+ return;
992
+ Plugin& plugin = model_.plugin;
993
+ ImGui::PushFont(nullptr, 12);
994
+ auto fact = [&](const char* label, const std::string& value)
995
+ {
996
+ ImGui::TextColored(color(muted), "%s", label);
997
+ ImGui::SameLine(96 * scale_);
998
+ ImGui::TextWrapped("%s", value.c_str());
999
+ };
1000
+ fact("Vendor", plugin.vendor());
1001
+ fact("Version", plugin.version());
1002
+ fact("Inputs", std::to_string(plugin.mainInputChannels()) + " channels");
1003
+ fact("Outputs", std::to_string(plugin.mainOutputChannels()) + " channels");
1004
+ fact("Latency", std::to_string(plugin.latencySamples()) + " samples");
1005
+ fact("MIDI input", plugin.hasEventInput() ? "yes" : "no");
1006
+ ImGui::PopFont();
1007
+ }
1008
+
1009
+ void HostInterface::drawSettings()
1010
+ {
1011
+ if (! section("Settings", true))
1012
+ return;
1013
+ bool withInterface = model_.settings.presentation == "host";
1014
+ if (ImGui::Checkbox("Open plugins with this interface", &withInterface))
1015
+ {
1016
+ model_.settings.presentation = withInterface ? "host" : "editor";
1017
+ model_.settings.save();
1018
+ }
1019
+ ImGui::PushFont(nullptr, 11);
1020
+ ImGui::PushStyleColor(ImGuiCol_Text, color(muted));
1021
+ ImGui::TextWrapped("When off, the next plugin opens in a window of its editor alone. A launch that asks for "
1022
+ "a presentation gets it.");
1023
+ ImGui::PopStyleColor();
1024
+ ImGui::PopFont();
1025
+ }
1026
+ } // namespace soundor::vst3host