@soundor/vst3-host 0.0.0-stage → 0.1.1
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.
- package/CMakeLists.txt +194 -0
- package/CMakePresets.json +115 -0
- package/LICENSE +21 -0
- package/NOTICE.md +17 -0
- package/README.md +40 -2
- package/cmake/Vst3HostDependencies.cmake +179 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +88 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +586 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1 -0
- package/dist/prepare-ayjTVhfk.mjs +1149 -0
- package/dist/prepare-ayjTVhfk.mjs.map +1 -0
- package/native/src/Bundle.cpp +253 -0
- package/native/src/Bundle.h +32 -0
- package/native/src/EditorArea.h +92 -0
- package/native/src/EditorAreaMac.mm +52 -0
- package/native/src/EditorAreaWindows.cpp +66 -0
- package/native/src/EditorAreaX11.cpp +512 -0
- package/native/src/EditorWindow.h +72 -0
- package/native/src/EditorWindowMac.mm +134 -0
- package/native/src/EditorWindowWindows.cpp +148 -0
- package/native/src/EventLoop.h +50 -0
- package/native/src/EventLoopLinux.cpp +204 -0
- package/native/src/EventLoopMac.mm +146 -0
- package/native/src/EventLoopWindows.cpp +132 -0
- package/native/src/HostContext.cpp +217 -0
- package/native/src/HostContext.h +116 -0
- package/native/src/HostInterface.cpp +1026 -0
- package/native/src/HostInterface.h +138 -0
- package/native/src/HostWindow.cpp +901 -0
- package/native/src/HostWindow.h +187 -0
- package/native/src/Ipc.cpp +158 -0
- package/native/src/Ipc.h +91 -0
- package/native/src/Json.cpp +426 -0
- package/native/src/Json.h +85 -0
- package/native/src/LiveAudio.cpp +416 -0
- package/native/src/LiveAudio.h +121 -0
- package/native/src/Main.cpp +178 -0
- package/native/src/Paths.h +20 -0
- package/native/src/Plugin.cpp +1865 -0
- package/native/src/Plugin.h +321 -0
- package/native/src/Session.cpp +656 -0
- package/native/src/Session.h +82 -0
- package/native/src/Settings.cpp +93 -0
- package/native/src/Settings.h +23 -0
- package/native/src/Version.h.in +11 -0
- package/native/src/X11Library.cpp +127 -0
- package/native/src/X11Library.h +83 -0
- package/native/tools/Embed.cpp +49 -0
- package/package.json +58 -3
- package/protocol.json +7 -0
- 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
|
+
[¶meter](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
|