@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.
- 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,1865 @@
|
|
|
1
|
+
#include "Plugin.h"
|
|
2
|
+
|
|
3
|
+
#include "Bundle.h"
|
|
4
|
+
#include "pluginterfaces/base/funknownimpl.h"
|
|
5
|
+
#include "pluginterfaces/gui/iplugviewcontentscalesupport.h"
|
|
6
|
+
#include "pluginterfaces/vst/ivstprocesscontext.h"
|
|
7
|
+
#include "pluginterfaces/vst/vstspeaker.h"
|
|
8
|
+
#include "public.sdk/source/common/memorystream.h"
|
|
9
|
+
#include "public.sdk/source/vst/hosting/connectionproxy.h"
|
|
10
|
+
#include "public.sdk/source/vst/hosting/eventlist.h"
|
|
11
|
+
#include "public.sdk/source/vst/hosting/parameterchanges.h"
|
|
12
|
+
#include "public.sdk/source/vst/utility/stringconvert.h"
|
|
13
|
+
|
|
14
|
+
#include <algorithm>
|
|
15
|
+
#include <array>
|
|
16
|
+
#include <cmath>
|
|
17
|
+
#include <cstdlib>
|
|
18
|
+
#include <cstring>
|
|
19
|
+
#include <limits>
|
|
20
|
+
#include <map>
|
|
21
|
+
#include <sstream>
|
|
22
|
+
|
|
23
|
+
#if ! defined(_WIN32)
|
|
24
|
+
#include <dlfcn.h>
|
|
25
|
+
#endif
|
|
26
|
+
|
|
27
|
+
namespace soundor::vst3host
|
|
28
|
+
{
|
|
29
|
+
using namespace Steinberg::Vst;
|
|
30
|
+
|
|
31
|
+
namespace
|
|
32
|
+
{
|
|
33
|
+
std::string classIdString(const VST3::UID& uid)
|
|
34
|
+
{
|
|
35
|
+
return uid.toString(false);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
std::string arrangementName(SpeakerArrangement arrangement)
|
|
39
|
+
{
|
|
40
|
+
const char* name = SpeakerArr::getSpeakerArrangementString(arrangement, false);
|
|
41
|
+
if (name != nullptr && *name != '\0' && std::string_view(name) != "Unknown")
|
|
42
|
+
return name;
|
|
43
|
+
std::ostringstream hex;
|
|
44
|
+
hex << "0x" << std::hex << arrangement;
|
|
45
|
+
return hex.str();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
std::string busTypeName(int32 type)
|
|
49
|
+
{
|
|
50
|
+
return type == kMain ? "main" : "aux";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
std::string tresultName(tresult result)
|
|
54
|
+
{
|
|
55
|
+
switch (result)
|
|
56
|
+
{
|
|
57
|
+
case kResultOk:
|
|
58
|
+
return "kResultOk";
|
|
59
|
+
case kResultFalse:
|
|
60
|
+
return "kResultFalse";
|
|
61
|
+
case kInvalidArgument:
|
|
62
|
+
return "kInvalidArgument";
|
|
63
|
+
case kNotImplemented:
|
|
64
|
+
return "kNotImplemented";
|
|
65
|
+
case kInternalError:
|
|
66
|
+
return "kInternalError";
|
|
67
|
+
case kNotInitialized:
|
|
68
|
+
return "kNotInitialized";
|
|
69
|
+
case kOutOfMemory:
|
|
70
|
+
return "kOutOfMemory";
|
|
71
|
+
default:
|
|
72
|
+
return std::to_string(result);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
std::string processModeName(int32 mode)
|
|
77
|
+
{
|
|
78
|
+
return mode == kOffline ? "offline" : mode == kPrefetch ? "prefetch" : "realtime";
|
|
79
|
+
}
|
|
80
|
+
} // namespace
|
|
81
|
+
|
|
82
|
+
Plugin::Plugin(EventLoop& loop, bool ui, PluginCallbacks callbacks)
|
|
83
|
+
: loop_(loop), ui_(ui), callbacks_(std::move(callbacks))
|
|
84
|
+
{
|
|
85
|
+
#if defined(__linux__)
|
|
86
|
+
runLoop_ = owned(new RunLoop(loop_));
|
|
87
|
+
hostContext_ = owned(new HostApplication(runLoop_->unknownCast()));
|
|
88
|
+
#else
|
|
89
|
+
hostContext_ = owned(new HostApplication(nullptr));
|
|
90
|
+
#endif
|
|
91
|
+
componentHandler_ = owned(new ComponentHandler(*this));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Plugin::~Plugin()
|
|
95
|
+
{
|
|
96
|
+
unload();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
void Plugin::requireLoaded() const
|
|
100
|
+
{
|
|
101
|
+
if (component_ == nullptr)
|
|
102
|
+
throw Failure("invalid-state", "no plugin is loaded");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Loading ──────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
void Plugin::load(const std::string& path, const std::string& className, const std::string& classId,
|
|
108
|
+
const Setup& setup)
|
|
109
|
+
{
|
|
110
|
+
const std::scoped_lock processing(processing_);
|
|
111
|
+
if (module_ != nullptr)
|
|
112
|
+
throw Failure("invalid-state", "a plugin is already loaded; launch another host for another plugin");
|
|
113
|
+
|
|
114
|
+
const BundleCheck check = checkBundle(path);
|
|
115
|
+
if (! check.code.empty())
|
|
116
|
+
{
|
|
117
|
+
Json details = Json::object();
|
|
118
|
+
Json architectures = Json::array();
|
|
119
|
+
for (const auto& architecture : check.architectures)
|
|
120
|
+
architectures.push(architecture);
|
|
121
|
+
details.set("architectures", std::move(architectures));
|
|
122
|
+
details.set("hostArchitecture", hostArchitecture());
|
|
123
|
+
throw Failure(check.code, check.message, std::move(details));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#if ! defined(_WIN32)
|
|
127
|
+
// Resolve every symbol now: the SDK's loader binds lazily, and a
|
|
128
|
+
// plugin missing a symbol would end the process at its first call.
|
|
129
|
+
void* probe = dlopen(check.binary.c_str(), RTLD_NOW | RTLD_LOCAL);
|
|
130
|
+
if (probe == nullptr)
|
|
131
|
+
{
|
|
132
|
+
const char* reason = dlerror(); // NOLINT(concurrency-mt-unsafe): main thread only
|
|
133
|
+
throw Failure("load-failed", "the plugin's binary could not be loaded: "
|
|
134
|
+
+ std::string(reason != nullptr ? reason : check.binary));
|
|
135
|
+
}
|
|
136
|
+
// Kept open until the SDK has its own handle, so it is loaded once.
|
|
137
|
+
const std::unique_ptr<void, int (*)(void*)> probeHandle(probe, &dlclose);
|
|
138
|
+
#endif
|
|
139
|
+
std::string error;
|
|
140
|
+
auto module = VST3::Hosting::Module::create(path, error);
|
|
141
|
+
if (module == nullptr)
|
|
142
|
+
throw Failure("load-failed", "the plugin's module could not be loaded from " + path
|
|
143
|
+
+ (error.empty() ? std::string() : ": " + error));
|
|
144
|
+
|
|
145
|
+
const auto& factory = module->getFactory();
|
|
146
|
+
factory.setHostContext(hostContext_.get());
|
|
147
|
+
|
|
148
|
+
std::vector<VST3::Hosting::ClassInfo> candidates;
|
|
149
|
+
Json available = Json::array();
|
|
150
|
+
for (const auto& info : factory.classInfos())
|
|
151
|
+
{
|
|
152
|
+
if (info.category() != kVstAudioEffectClass)
|
|
153
|
+
continue;
|
|
154
|
+
Json entry = Json::object();
|
|
155
|
+
entry.set("name", info.name());
|
|
156
|
+
entry.set("classId", classIdString(info.ID()));
|
|
157
|
+
available.push(std::move(entry));
|
|
158
|
+
if ((className.empty() || info.name() == className)
|
|
159
|
+
&& (classId.empty() || classIdString(info.ID()) == classId))
|
|
160
|
+
candidates.push_back(info);
|
|
161
|
+
}
|
|
162
|
+
Json details = Json::object();
|
|
163
|
+
details.set("classes", available);
|
|
164
|
+
if (available.asArray().empty())
|
|
165
|
+
throw Failure("unsupported-plugin", path + " has no VST3 audio processor class", std::move(details));
|
|
166
|
+
if (candidates.empty())
|
|
167
|
+
throw Failure("no-matching-class",
|
|
168
|
+
"no audio processor class in " + path + " matches "
|
|
169
|
+
+ (className.empty() ? "class id " + classId : "\"" + className + "\""),
|
|
170
|
+
std::move(details));
|
|
171
|
+
if (candidates.size() > 1)
|
|
172
|
+
{
|
|
173
|
+
std::string names;
|
|
174
|
+
for (const auto& candidate : candidates)
|
|
175
|
+
names += (names.empty() ? "" : ", ") + ("\"" + candidate.name() + "\"");
|
|
176
|
+
throw Failure("ambiguous-class",
|
|
177
|
+
path + " has several audio processor classes (" + names
|
|
178
|
+
+ "); choose one with `className` or `classId`",
|
|
179
|
+
std::move(details));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module_ = std::move(module);
|
|
183
|
+
path_ = path;
|
|
184
|
+
class_ = candidates.front();
|
|
185
|
+
setup_ = setup;
|
|
186
|
+
try
|
|
187
|
+
{
|
|
188
|
+
createInstance();
|
|
189
|
+
}
|
|
190
|
+
catch (...)
|
|
191
|
+
{
|
|
192
|
+
unload();
|
|
193
|
+
throw;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
void Plugin::createInstance()
|
|
198
|
+
{
|
|
199
|
+
const auto& factory = module_->getFactory();
|
|
200
|
+
const std::string name = class_.name();
|
|
201
|
+
|
|
202
|
+
component_ = factory.createInstance<IComponent>(class_.ID());
|
|
203
|
+
if (component_ == nullptr)
|
|
204
|
+
throw Failure("load-failed", "the plugin's factory did not create \"" + name + "\"");
|
|
205
|
+
const tresult initialized = component_->initialize(hostContext_.get());
|
|
206
|
+
if (initialized != kResultOk)
|
|
207
|
+
{
|
|
208
|
+
component_ = nullptr;
|
|
209
|
+
throw Failure("initialization-failed", "\"" + name
|
|
210
|
+
+ "\" failed to initialize (IComponent::initialize returned "
|
|
211
|
+
+ tresultName(initialized) + ")");
|
|
212
|
+
}
|
|
213
|
+
processor_ = U::cast<IAudioProcessor>(component_);
|
|
214
|
+
if (processor_ == nullptr)
|
|
215
|
+
throw Failure("unsupported-plugin", "\"" + name + "\" is not an audio processor (no IAudioProcessor)");
|
|
216
|
+
|
|
217
|
+
// A single-component plugin is its own controller; otherwise the
|
|
218
|
+
// controller is a second class of the same factory.
|
|
219
|
+
controller_ = U::cast<IEditController>(component_);
|
|
220
|
+
separateController_ = controller_ == nullptr;
|
|
221
|
+
if (separateController_)
|
|
222
|
+
{
|
|
223
|
+
TUID controllerId {};
|
|
224
|
+
if (component_->getControllerClassId(controllerId) == kResultOk)
|
|
225
|
+
{
|
|
226
|
+
controller_ = factory.createInstance<IEditController>(VST3::UID::fromTUID(controllerId));
|
|
227
|
+
if (controller_ != nullptr)
|
|
228
|
+
{
|
|
229
|
+
const tresult result = controller_->initialize(hostContext_.get());
|
|
230
|
+
if (result != kResultOk)
|
|
231
|
+
{
|
|
232
|
+
controller_ = nullptr;
|
|
233
|
+
throw Failure("initialization-failed",
|
|
234
|
+
"the controller of \"" + name
|
|
235
|
+
+ "\" failed to initialize (IEditController::initialize returned "
|
|
236
|
+
+ tresultName(result) + ")");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (controller_ != nullptr)
|
|
243
|
+
{
|
|
244
|
+
controller_->setComponentHandler(componentHandler_);
|
|
245
|
+
if (separateController_)
|
|
246
|
+
{
|
|
247
|
+
auto componentPoint = U::cast<IConnectionPoint>(component_);
|
|
248
|
+
auto controllerPoint = U::cast<IConnectionPoint>(controller_);
|
|
249
|
+
if (componentPoint != nullptr && controllerPoint != nullptr)
|
|
250
|
+
{
|
|
251
|
+
componentConnection_ = owned(new ConnectionProxy(componentPoint));
|
|
252
|
+
controllerConnection_ = owned(new ConnectionProxy(controllerPoint));
|
|
253
|
+
componentConnection_->connect(controllerPoint);
|
|
254
|
+
controllerConnection_->connect(componentPoint);
|
|
255
|
+
}
|
|
256
|
+
// The controller starts from the component's state.
|
|
257
|
+
auto stream = owned(new MemoryStream());
|
|
258
|
+
if (component_->getState(stream) == kResultOk)
|
|
259
|
+
{
|
|
260
|
+
stream->seek(0, IBStream::kIBSeekSet, nullptr);
|
|
261
|
+
controller_->setComponentState(stream);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
midiMapping_ = U::cast<IMidiMapping>(controller_);
|
|
265
|
+
parameterCount_ = controller_->getParameterCount();
|
|
266
|
+
}
|
|
267
|
+
++parameterGeneration_;
|
|
268
|
+
|
|
269
|
+
if (processor_->canProcessSampleSize(kSample32) != kResultTrue)
|
|
270
|
+
throw Failure("unsupported-plugin", "\"" + name + "\" cannot process 32-bit float samples");
|
|
271
|
+
|
|
272
|
+
negotiate(setup_);
|
|
273
|
+
activate();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
void Plugin::destroyInstance()
|
|
277
|
+
{
|
|
278
|
+
closeEditor();
|
|
279
|
+
deactivate();
|
|
280
|
+
if (componentConnection_ != nullptr)
|
|
281
|
+
static_cast<ConnectionProxy*>(componentConnection_.get())->disconnect();
|
|
282
|
+
if (controllerConnection_ != nullptr)
|
|
283
|
+
static_cast<ConnectionProxy*>(controllerConnection_.get())->disconnect();
|
|
284
|
+
componentConnection_ = nullptr;
|
|
285
|
+
controllerConnection_ = nullptr;
|
|
286
|
+
midiMapping_ = nullptr;
|
|
287
|
+
if (controller_ != nullptr)
|
|
288
|
+
{
|
|
289
|
+
controller_->setComponentHandler(nullptr);
|
|
290
|
+
if (separateController_)
|
|
291
|
+
controller_->terminate();
|
|
292
|
+
}
|
|
293
|
+
controller_ = nullptr;
|
|
294
|
+
processor_ = nullptr;
|
|
295
|
+
if (component_ != nullptr)
|
|
296
|
+
component_->terminate();
|
|
297
|
+
component_ = nullptr;
|
|
298
|
+
audioInputs_.clear();
|
|
299
|
+
audioOutputs_.clear();
|
|
300
|
+
eventInputs_.clear();
|
|
301
|
+
eventOutputs_.clear();
|
|
302
|
+
const std::scoped_lock lock(pendingMutex_);
|
|
303
|
+
pending_.clear();
|
|
304
|
+
pendingEvents_.clear();
|
|
305
|
+
pendingRestart_ = 0;
|
|
306
|
+
parameterCount_ = 0;
|
|
307
|
+
++parameterGeneration_;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
void Plugin::unload()
|
|
311
|
+
{
|
|
312
|
+
const std::scoped_lock processing(processing_);
|
|
313
|
+
destroyInstance();
|
|
314
|
+
#if defined(__linux__)
|
|
315
|
+
// Handlers live in the module: release them before it goes.
|
|
316
|
+
runLoop_->clear();
|
|
317
|
+
#endif
|
|
318
|
+
class_ = {};
|
|
319
|
+
module_ = nullptr;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
void Plugin::recreate()
|
|
323
|
+
{
|
|
324
|
+
const std::scoped_lock processing(processing_);
|
|
325
|
+
requireLoaded();
|
|
326
|
+
destroyInstance();
|
|
327
|
+
{
|
|
328
|
+
const std::scoped_lock lock(transportMutex_);
|
|
329
|
+
transport_ = {};
|
|
330
|
+
++transportEdits_;
|
|
331
|
+
}
|
|
332
|
+
continuousSamples_ = 0;
|
|
333
|
+
createInstance();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
void Plugin::reset()
|
|
337
|
+
{
|
|
338
|
+
const std::scoped_lock processing(processing_);
|
|
339
|
+
requireLoaded();
|
|
340
|
+
deactivate();
|
|
341
|
+
continuousSamples_ = 0;
|
|
342
|
+
activate();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
void Plugin::configure(const Setup& setup)
|
|
346
|
+
{
|
|
347
|
+
const std::scoped_lock processing(processing_);
|
|
348
|
+
requireLoaded();
|
|
349
|
+
deactivate();
|
|
350
|
+
try
|
|
351
|
+
{
|
|
352
|
+
negotiate(setup);
|
|
353
|
+
setup_ = setup;
|
|
354
|
+
}
|
|
355
|
+
catch (...)
|
|
356
|
+
{
|
|
357
|
+
// Back to the previous, working setup.
|
|
358
|
+
negotiate(setup_);
|
|
359
|
+
activate();
|
|
360
|
+
throw;
|
|
361
|
+
}
|
|
362
|
+
activate();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Buses and activation ─────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
void Plugin::readBuses()
|
|
368
|
+
{
|
|
369
|
+
auto read = [this](MediaType media, BusDirection direction, std::vector<Bus>& buses)
|
|
370
|
+
{
|
|
371
|
+
const int32 count = component_->getBusCount(media, direction);
|
|
372
|
+
buses.assign(static_cast<size_t>(std::max(count, 0)), Bus {});
|
|
373
|
+
for (int32 index = 0; index < count; ++index)
|
|
374
|
+
{
|
|
375
|
+
BusInfo info {};
|
|
376
|
+
Bus& bus = buses[static_cast<size_t>(index)];
|
|
377
|
+
if (component_->getBusInfo(media, direction, index, info) == kResultOk)
|
|
378
|
+
{
|
|
379
|
+
bus.name = Steinberg::Vst::StringConvert::convert(info.name);
|
|
380
|
+
bus.type = info.busType;
|
|
381
|
+
bus.channels = info.channelCount;
|
|
382
|
+
bus.defaultActive = (info.flags & BusInfo::kDefaultActive) != 0;
|
|
383
|
+
}
|
|
384
|
+
if (media == kAudio)
|
|
385
|
+
{
|
|
386
|
+
SpeakerArrangement arrangement = 0;
|
|
387
|
+
if (processor_->getBusArrangement(direction, index, arrangement) == kResultOk)
|
|
388
|
+
{
|
|
389
|
+
bus.arrangement = arrangement;
|
|
390
|
+
bus.channels = SpeakerArr::getChannelCount(arrangement);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
read(kAudio, kInput, audioInputs_);
|
|
396
|
+
read(kAudio, kOutput, audioOutputs_);
|
|
397
|
+
read(kEvent, kInput, eventInputs_);
|
|
398
|
+
read(kEvent, kOutput, eventOutputs_);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
void Plugin::negotiate(const Setup& setup)
|
|
402
|
+
{
|
|
403
|
+
readBuses();
|
|
404
|
+
const std::string name = class_.name();
|
|
405
|
+
|
|
406
|
+
auto check = [&](const std::vector<std::optional<BusRequest>>& requests, const std::vector<Bus>& buses,
|
|
407
|
+
const char* direction)
|
|
408
|
+
{
|
|
409
|
+
if (requests.size() > buses.size())
|
|
410
|
+
throw Failure("unsupported-configuration", std::to_string(requests.size()) + " audio " + direction
|
|
411
|
+
+ " buses were requested; \"" + name + "\" has "
|
|
412
|
+
+ std::to_string(buses.size()));
|
|
413
|
+
};
|
|
414
|
+
check(setup.inputs, audioInputs_, "input");
|
|
415
|
+
check(setup.outputs, audioOutputs_, "output");
|
|
416
|
+
|
|
417
|
+
// Arrangements: the request where there is one, else what the plugin
|
|
418
|
+
// has, offered together as setBusArrangements requires.
|
|
419
|
+
auto wanted = [](const std::vector<std::optional<BusRequest>>& requests, const std::vector<Bus>& buses)
|
|
420
|
+
{
|
|
421
|
+
std::vector<SpeakerArrangement> result;
|
|
422
|
+
for (size_t index = 0; index < buses.size(); ++index)
|
|
423
|
+
{
|
|
424
|
+
const auto& request = index < requests.size() ? requests[index] : std::nullopt;
|
|
425
|
+
result.push_back(request && request->arrangement ? *request->arrangement : buses[index].arrangement);
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
};
|
|
429
|
+
auto inputs = wanted(setup.inputs, audioInputs_);
|
|
430
|
+
auto outputs = wanted(setup.outputs, audioOutputs_);
|
|
431
|
+
bool changed = false;
|
|
432
|
+
for (size_t i = 0; i < inputs.size(); ++i)
|
|
433
|
+
changed = changed || inputs[i] != audioInputs_[i].arrangement;
|
|
434
|
+
for (size_t i = 0; i < outputs.size(); ++i)
|
|
435
|
+
changed = changed || outputs[i] != audioOutputs_[i].arrangement;
|
|
436
|
+
if (changed)
|
|
437
|
+
{
|
|
438
|
+
processor_->setBusArrangements(inputs.data(), static_cast<int32>(inputs.size()), outputs.data(),
|
|
439
|
+
static_cast<int32>(outputs.size()));
|
|
440
|
+
readBuses();
|
|
441
|
+
// A plugin that refuses may adapt to something else instead:
|
|
442
|
+
// what counts is whether the requested arrangements hold now.
|
|
443
|
+
auto verify = [&](const std::vector<std::optional<BusRequest>>& requests, const std::vector<Bus>& buses,
|
|
444
|
+
const char* direction)
|
|
445
|
+
{
|
|
446
|
+
for (size_t index = 0; index < requests.size(); ++index)
|
|
447
|
+
{
|
|
448
|
+
const auto& request = requests[index];
|
|
449
|
+
if (! request || ! request->arrangement || *request->arrangement == buses[index].arrangement)
|
|
450
|
+
continue;
|
|
451
|
+
throw Failure("unsupported-configuration",
|
|
452
|
+
"\"" + name + "\" does not accept " + arrangementName(*request->arrangement)
|
|
453
|
+
+ " on audio " + direction + " bus " + std::to_string(index) + " (\""
|
|
454
|
+
+ buses[index].name + "\"); it proposes "
|
|
455
|
+
+ arrangementName(buses[index].arrangement));
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
verify(setup.inputs, audioInputs_, "input");
|
|
459
|
+
verify(setup.outputs, audioOutputs_, "output");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
auto activateBuses = [&](MediaType media, BusDirection direction,
|
|
463
|
+
const std::vector<std::optional<BusRequest>>& requests, std::vector<Bus>& buses)
|
|
464
|
+
{
|
|
465
|
+
for (size_t index = 0; index < buses.size(); ++index)
|
|
466
|
+
{
|
|
467
|
+
const auto& request = index < requests.size() ? requests[index] : std::nullopt;
|
|
468
|
+
// Main buses and those the plugin marks default-active; every
|
|
469
|
+
// event bus, so that MIDI reaches the plugin.
|
|
470
|
+
bool active = media == kEvent || buses[index].type == kMain || buses[index].defaultActive;
|
|
471
|
+
if (request && request->active)
|
|
472
|
+
active = *request->active;
|
|
473
|
+
component_->activateBus(media, direction, static_cast<int32>(index), active);
|
|
474
|
+
buses[index].active = active;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
static const std::vector<std::optional<BusRequest>> none;
|
|
478
|
+
activateBuses(kAudio, kInput, setup.inputs, audioInputs_);
|
|
479
|
+
activateBuses(kAudio, kOutput, setup.outputs, audioOutputs_);
|
|
480
|
+
activateBuses(kEvent, kInput, none, eventInputs_);
|
|
481
|
+
activateBuses(kEvent, kOutput, none, eventOutputs_);
|
|
482
|
+
|
|
483
|
+
ProcessSetup processSetup { setup.processMode, kSample32, setup.blockSize, setup.sampleRate };
|
|
484
|
+
const tresult result = processor_->setupProcessing(processSetup);
|
|
485
|
+
if (result != kResultOk)
|
|
486
|
+
throw Failure("unsupported-configuration",
|
|
487
|
+
"\"" + name + "\" refused " + std::to_string(setup.sampleRate) + " Hz with blocks of "
|
|
488
|
+
+ std::to_string(setup.blockSize) + " frames in " + processModeName(setup.processMode)
|
|
489
|
+
+ " mode (setupProcessing returned " + tresultName(result) + ")");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
void Plugin::activate()
|
|
493
|
+
{
|
|
494
|
+
if (active_)
|
|
495
|
+
return;
|
|
496
|
+
const tresult result = component_->setActive(true);
|
|
497
|
+
if (result != kResultOk)
|
|
498
|
+
throw Failure("initialization-failed", "\"" + class_.name() + "\" failed to activate (setActive returned "
|
|
499
|
+
+ tresultName(result) + ")");
|
|
500
|
+
active_ = true;
|
|
501
|
+
processor_->setProcessing(true);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
void Plugin::deactivate()
|
|
505
|
+
{
|
|
506
|
+
if (! active_)
|
|
507
|
+
return;
|
|
508
|
+
processor_->setProcessing(false);
|
|
509
|
+
component_->setActive(false);
|
|
510
|
+
active_ = false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── Plugin requests (IComponentHandler, IPlugFrame) ──────────────────
|
|
514
|
+
|
|
515
|
+
void Plugin::beginEdit(ParamID id)
|
|
516
|
+
{
|
|
517
|
+
Json params = Json::object();
|
|
518
|
+
params.set("id", id);
|
|
519
|
+
loop_.post([this, params] { callbacks_.emit("beginEdit", params); });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
void Plugin::performEdit(ParamID id, ParamValue value)
|
|
523
|
+
{
|
|
524
|
+
{
|
|
525
|
+
const std::scoped_lock lock(pendingMutex_);
|
|
526
|
+
pending_.push_back({ id, value, 0 });
|
|
527
|
+
}
|
|
528
|
+
Json params = Json::object();
|
|
529
|
+
params.set("id", id);
|
|
530
|
+
params.set("value", value);
|
|
531
|
+
loop_.post([this, params] { callbacks_.emit("performEdit", params); });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
void Plugin::endEdit(ParamID id)
|
|
535
|
+
{
|
|
536
|
+
Json params = Json::object();
|
|
537
|
+
params.set("id", id);
|
|
538
|
+
loop_.post([this, params] { callbacks_.emit("endEdit", params); });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
void Plugin::restartComponent(int32 flags)
|
|
542
|
+
{
|
|
543
|
+
loop_.post(
|
|
544
|
+
[this, flags]
|
|
545
|
+
{
|
|
546
|
+
pendingRestart_ |= flags;
|
|
547
|
+
if (! busy)
|
|
548
|
+
applyPendingRestarts();
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
void Plugin::applyPendingRestarts()
|
|
553
|
+
{
|
|
554
|
+
const std::scoped_lock processing(processing_);
|
|
555
|
+
const int32 flags = std::exchange(pendingRestart_, 0);
|
|
556
|
+
if (flags == 0 || component_ == nullptr)
|
|
557
|
+
return;
|
|
558
|
+
// Latency, I/O and reload restarts need the plugin deactivated and
|
|
559
|
+
// reactivated; they happen between process calls, never during one.
|
|
560
|
+
if ((flags & (kParamTitlesChanged | kReloadComponent)) != 0 && controller_ != nullptr)
|
|
561
|
+
{
|
|
562
|
+
parameterCount_ = controller_->getParameterCount();
|
|
563
|
+
++parameterGeneration_;
|
|
564
|
+
}
|
|
565
|
+
if ((flags & (kLatencyChanged | kIoChanged | kReloadComponent)) != 0 && active_)
|
|
566
|
+
{
|
|
567
|
+
deactivate();
|
|
568
|
+
if ((flags & kIoChanged) != 0)
|
|
569
|
+
negotiate(setup_);
|
|
570
|
+
activate();
|
|
571
|
+
}
|
|
572
|
+
Json params = Json::object();
|
|
573
|
+
params.set("flags", flags);
|
|
574
|
+
Json names = Json::array();
|
|
575
|
+
const std::array<std::pair<int32, const char*>, 6> known { { { kReloadComponent, "reloadComponent" },
|
|
576
|
+
{ kIoChanged, "ioChanged" },
|
|
577
|
+
{ kParamValuesChanged, "paramValuesChanged" },
|
|
578
|
+
{ kLatencyChanged, "latencyChanged" },
|
|
579
|
+
{ kParamTitlesChanged, "paramTitlesChanged" },
|
|
580
|
+
{ kMidiCCAssignmentChanged,
|
|
581
|
+
"midiCCAssignmentChanged" } } };
|
|
582
|
+
for (const auto& [flag, name] : known)
|
|
583
|
+
if ((flags & flag) != 0)
|
|
584
|
+
names.push(name);
|
|
585
|
+
params.set("reasons", std::move(names));
|
|
586
|
+
params.set("latencySamples", static_cast<int64>(processor_->getLatencySamples()));
|
|
587
|
+
callbacks_.emit("restart", std::move(params));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
void Plugin::setDirty(bool dirty)
|
|
591
|
+
{
|
|
592
|
+
Json params = Json::object();
|
|
593
|
+
params.set("dirty", dirty);
|
|
594
|
+
callbacks_.emit("dirty", std::move(params));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
void Plugin::requestOpenEditor()
|
|
598
|
+
{
|
|
599
|
+
callbacks_.emit("editorRequested", Json::object());
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
bool Plugin::resizeView(IPlugView* view, ViewRect* size)
|
|
603
|
+
{
|
|
604
|
+
if (view != view_.get() || window_ == nullptr || resizing_)
|
|
605
|
+
return false;
|
|
606
|
+
resizing_ = true;
|
|
607
|
+
keepToRoom(*size);
|
|
608
|
+
window_->constrainContentSize(size->getWidth(), size->getHeight());
|
|
609
|
+
view_->onSize(size);
|
|
610
|
+
resizing_ = false;
|
|
611
|
+
Json params = editorInfo();
|
|
612
|
+
callbacks_.emit("editorResized", std::move(params));
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ── Information ──────────────────────────────────────────────────────
|
|
617
|
+
|
|
618
|
+
Json Plugin::busesJson() const
|
|
619
|
+
{
|
|
620
|
+
auto list = [](const std::vector<Bus>& buses, bool audio)
|
|
621
|
+
{
|
|
622
|
+
Json result = Json::array();
|
|
623
|
+
for (size_t index = 0; index < buses.size(); ++index)
|
|
624
|
+
{
|
|
625
|
+
const Bus& bus = buses[index];
|
|
626
|
+
Json entry = Json::object();
|
|
627
|
+
entry.set("index", static_cast<int>(index));
|
|
628
|
+
entry.set("name", bus.name);
|
|
629
|
+
entry.set("type", busTypeName(bus.type));
|
|
630
|
+
entry.set("channels", bus.channels);
|
|
631
|
+
if (audio)
|
|
632
|
+
entry.set("arrangement", arrangementName(bus.arrangement));
|
|
633
|
+
entry.set("active", bus.active);
|
|
634
|
+
entry.set("defaultActive", bus.defaultActive);
|
|
635
|
+
result.push(std::move(entry));
|
|
636
|
+
}
|
|
637
|
+
return result;
|
|
638
|
+
};
|
|
639
|
+
Json buses = Json::object();
|
|
640
|
+
buses.set("audioInputs", list(audioInputs_, true));
|
|
641
|
+
buses.set("audioOutputs", list(audioOutputs_, true));
|
|
642
|
+
buses.set("eventInputs", list(eventInputs_, false));
|
|
643
|
+
buses.set("eventOutputs", list(eventOutputs_, false));
|
|
644
|
+
return buses;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
Json Plugin::info() const
|
|
648
|
+
{
|
|
649
|
+
requireLoaded();
|
|
650
|
+
Json result = Json::object();
|
|
651
|
+
result.set("path", path_);
|
|
652
|
+
result.set("name", class_.name());
|
|
653
|
+
result.set("classId", classIdString(class_.ID()));
|
|
654
|
+
result.set("vendor", class_.vendor());
|
|
655
|
+
result.set("version", class_.version());
|
|
656
|
+
result.set("sdkVersion", class_.sdkVersion());
|
|
657
|
+
Json categories = Json::array();
|
|
658
|
+
for (const auto& category : class_.subCategories())
|
|
659
|
+
categories.push(category);
|
|
660
|
+
result.set("subCategories", std::move(categories));
|
|
661
|
+
const auto factory = module_->getFactory().info();
|
|
662
|
+
Json factoryInfo = Json::object();
|
|
663
|
+
factoryInfo.set("vendor", factory.vendor());
|
|
664
|
+
factoryInfo.set("url", factory.url());
|
|
665
|
+
factoryInfo.set("email", factory.email());
|
|
666
|
+
result.set("factory", std::move(factoryInfo));
|
|
667
|
+
Json classes = Json::array();
|
|
668
|
+
for (const auto& info : module_->getFactory().classInfos())
|
|
669
|
+
{
|
|
670
|
+
if (info.category() != kVstAudioEffectClass)
|
|
671
|
+
continue;
|
|
672
|
+
Json entry = Json::object();
|
|
673
|
+
entry.set("name", info.name());
|
|
674
|
+
entry.set("classId", classIdString(info.ID()));
|
|
675
|
+
classes.push(std::move(entry));
|
|
676
|
+
}
|
|
677
|
+
result.set("classes", std::move(classes));
|
|
678
|
+
result.set("separateController", separateController_);
|
|
679
|
+
result.set("hasController", controller_ != nullptr);
|
|
680
|
+
result.set("sampleRate", setup_.sampleRate);
|
|
681
|
+
result.set("blockSize", setup_.blockSize);
|
|
682
|
+
result.set("processMode", processModeName(setup_.processMode));
|
|
683
|
+
result.set("buses", busesJson());
|
|
684
|
+
result.set("parameterCount", controller_ != nullptr ? controller_->getParameterCount() : 0);
|
|
685
|
+
const Json processing = latencyAndTail();
|
|
686
|
+
result.set("latencySamples", *processing.find("latencySamples"));
|
|
687
|
+
result.set("tailSamples", *processing.find("tailSamples"));
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
Json Plugin::latencyAndTail() const
|
|
692
|
+
{
|
|
693
|
+
requireLoaded();
|
|
694
|
+
Json result = Json::object();
|
|
695
|
+
result.set("latencySamples", static_cast<int64>(processor_->getLatencySamples()));
|
|
696
|
+
const uint32 tail = processor_->getTailSamples();
|
|
697
|
+
if (tail == kInfiniteTail)
|
|
698
|
+
result.set("tailSamples", "infinite");
|
|
699
|
+
else
|
|
700
|
+
result.set("tailSamples", static_cast<int64>(tail));
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
Json Plugin::parameterJson(int32 index) const
|
|
705
|
+
{
|
|
706
|
+
ParameterInfo info {};
|
|
707
|
+
controller_->getParameterInfo(index, info);
|
|
708
|
+
const ParamValue value = controller_->getParamNormalized(info.id);
|
|
709
|
+
Json entry = Json::object();
|
|
710
|
+
entry.set("id", info.id);
|
|
711
|
+
entry.set("title", Steinberg::Vst::StringConvert::convert(info.title));
|
|
712
|
+
entry.set("shortTitle", Steinberg::Vst::StringConvert::convert(info.shortTitle));
|
|
713
|
+
entry.set("units", Steinberg::Vst::StringConvert::convert(info.units));
|
|
714
|
+
entry.set("stepCount", info.stepCount);
|
|
715
|
+
entry.set("defaultNormalized", info.defaultNormalizedValue);
|
|
716
|
+
entry.set("unitId", info.unitId);
|
|
717
|
+
Json flags = Json::object();
|
|
718
|
+
flags.set("canAutomate", (info.flags & ParameterInfo::kCanAutomate) != 0);
|
|
719
|
+
flags.set("readOnly", (info.flags & ParameterInfo::kIsReadOnly) != 0);
|
|
720
|
+
flags.set("wrap", (info.flags & ParameterInfo::kIsWrapAround) != 0);
|
|
721
|
+
flags.set("list", (info.flags & ParameterInfo::kIsList) != 0);
|
|
722
|
+
flags.set("hidden", (info.flags & ParameterInfo::kIsHidden) != 0);
|
|
723
|
+
flags.set("programChange", (info.flags & ParameterInfo::kIsProgramChange) != 0);
|
|
724
|
+
flags.set("bypass", (info.flags & ParameterInfo::kIsBypass) != 0);
|
|
725
|
+
entry.set("flags", std::move(flags));
|
|
726
|
+
entry.set("normalized", value);
|
|
727
|
+
entry.set("plain", controller_->normalizedParamToPlain(info.id, value));
|
|
728
|
+
String128 display {};
|
|
729
|
+
if (controller_->getParamStringByValue(info.id, value, display) == kResultOk)
|
|
730
|
+
entry.set("display", Steinberg::Vst::StringConvert::convert(display));
|
|
731
|
+
else
|
|
732
|
+
entry.set("display", nullptr);
|
|
733
|
+
return entry;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
Json Plugin::parameters() const
|
|
737
|
+
{
|
|
738
|
+
requireLoaded();
|
|
739
|
+
Json result = Json::array();
|
|
740
|
+
if (controller_ == nullptr)
|
|
741
|
+
return result;
|
|
742
|
+
const int32 count = controller_->getParameterCount();
|
|
743
|
+
for (int32 index = 0; index < count; ++index)
|
|
744
|
+
result.push(parameterJson(index));
|
|
745
|
+
return result;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
Json Plugin::parameter(ParamID id) const
|
|
749
|
+
{
|
|
750
|
+
requireLoaded();
|
|
751
|
+
if (controller_ != nullptr)
|
|
752
|
+
{
|
|
753
|
+
const int32 count = controller_->getParameterCount();
|
|
754
|
+
for (int32 index = 0; index < count; ++index)
|
|
755
|
+
{
|
|
756
|
+
ParameterInfo info {};
|
|
757
|
+
if (controller_->getParameterInfo(index, info) == kResultOk && info.id == id)
|
|
758
|
+
return parameterJson(index);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
throw Failure("unknown-parameter", "\"" + class_.name() + "\" has no parameter with id " + std::to_string(id)
|
|
762
|
+
+ " (getParameters lists them)");
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
Json Plugin::setParameter(ParamID id, std::optional<double> normalized, std::optional<double> plain)
|
|
766
|
+
{
|
|
767
|
+
const Json current = parameter(id);
|
|
768
|
+
if (current.find("flags")->find("readOnly")->asBool())
|
|
769
|
+
throw Failure("invalid-request", "parameter " + std::to_string(id) + " (\""
|
|
770
|
+
+ current.find("title")->asString() + "\") is read-only");
|
|
771
|
+
double value = 0;
|
|
772
|
+
if (normalized)
|
|
773
|
+
{
|
|
774
|
+
if (! std::isfinite(*normalized) || *normalized < 0 || *normalized > 1)
|
|
775
|
+
throw Failure("invalid-request",
|
|
776
|
+
"a normalized parameter value is between 0 and 1, not " + std::to_string(*normalized));
|
|
777
|
+
value = *normalized;
|
|
778
|
+
}
|
|
779
|
+
else
|
|
780
|
+
{
|
|
781
|
+
const double given = plain.value_or(std::nan(""));
|
|
782
|
+
if (! std::isfinite(given))
|
|
783
|
+
throw Failure("invalid-request", "a plain parameter value must be finite");
|
|
784
|
+
value = std::clamp(controller_->plainParamToNormalized(id, given), 0.0, 1.0);
|
|
785
|
+
}
|
|
786
|
+
controller_->setParamNormalized(id, value);
|
|
787
|
+
{
|
|
788
|
+
const std::scoped_lock lock(pendingMutex_);
|
|
789
|
+
pending_.push_back({ id, value, 0 });
|
|
790
|
+
}
|
|
791
|
+
return parameter(id);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ── State ────────────────────────────────────────────────────────────
|
|
795
|
+
|
|
796
|
+
void Plugin::flushParameters()
|
|
797
|
+
{
|
|
798
|
+
// A process call without samples carries pending parameter changes
|
|
799
|
+
// to the processor, as hosts do when they are not playing.
|
|
800
|
+
{
|
|
801
|
+
const std::scoped_lock lock(pendingMutex_);
|
|
802
|
+
if (pending_.empty())
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
ProcessJob job;
|
|
806
|
+
job.inputs.resize(audioInputs_.size());
|
|
807
|
+
for (size_t bus = 0; bus < audioInputs_.size(); ++bus)
|
|
808
|
+
job.inputs[bus].assign(static_cast<size_t>(audioInputs_[bus].channels), nullptr);
|
|
809
|
+
runProcess(job);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
std::vector<uint8_t> Plugin::getState(size_t& componentSize)
|
|
813
|
+
{
|
|
814
|
+
const std::scoped_lock processing(processing_);
|
|
815
|
+
requireLoaded();
|
|
816
|
+
flushParameters();
|
|
817
|
+
auto component = owned(new MemoryStream());
|
|
818
|
+
tresult result = component_->getState(component);
|
|
819
|
+
if (result != kResultOk)
|
|
820
|
+
throw Failure("plugin-error", "the plugin could not save its state (IComponent::getState returned "
|
|
821
|
+
+ tresultName(result) + ")");
|
|
822
|
+
auto controller = owned(new MemoryStream());
|
|
823
|
+
if (controller_ != nullptr)
|
|
824
|
+
{
|
|
825
|
+
result = controller_->getState(controller);
|
|
826
|
+
if (result != kResultOk && result != kNotImplemented)
|
|
827
|
+
throw Failure("plugin-error", "the plugin's controller could not save its state "
|
|
828
|
+
"(IEditController::getState returned "
|
|
829
|
+
+ tresultName(result) + ")");
|
|
830
|
+
}
|
|
831
|
+
componentSize = static_cast<size_t>(component->getSize());
|
|
832
|
+
std::vector<uint8_t> bytes(componentSize + static_cast<size_t>(controller->getSize()));
|
|
833
|
+
if (componentSize > 0)
|
|
834
|
+
std::memcpy(bytes.data(), component->getData(), componentSize);
|
|
835
|
+
if (controller->getSize() > 0)
|
|
836
|
+
std::memcpy(bytes.data() + componentSize, controller->getData(),
|
|
837
|
+
static_cast<size_t>(controller->getSize()));
|
|
838
|
+
return bytes;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
void Plugin::setState(const uint8_t* component, size_t componentSize, const uint8_t* controller,
|
|
842
|
+
size_t controllerSize)
|
|
843
|
+
{
|
|
844
|
+
const std::scoped_lock processing(processing_);
|
|
845
|
+
requireLoaded();
|
|
846
|
+
// MemoryStream reads the given bytes without taking ownership.
|
|
847
|
+
auto componentStream =
|
|
848
|
+
owned(new MemoryStream(const_cast<uint8_t*>(component), static_cast<TSize>(componentSize)));
|
|
849
|
+
const tresult result = component_->setState(componentStream);
|
|
850
|
+
if (result != kResultOk)
|
|
851
|
+
throw Failure("plugin-error",
|
|
852
|
+
"the plugin rejected the state (IComponent::setState returned " + tresultName(result) + ")");
|
|
853
|
+
if (controller_ != nullptr)
|
|
854
|
+
{
|
|
855
|
+
componentStream->seek(0, IBStream::kIBSeekSet, nullptr);
|
|
856
|
+
controller_->setComponentState(componentStream);
|
|
857
|
+
if (controllerSize > 0)
|
|
858
|
+
{
|
|
859
|
+
auto controllerStream =
|
|
860
|
+
owned(new MemoryStream(const_cast<uint8_t*>(controller), static_cast<TSize>(controllerSize)));
|
|
861
|
+
const tresult controllerResult = controller_->setState(controllerStream);
|
|
862
|
+
if (controllerResult != kResultOk && controllerResult != kNotImplemented)
|
|
863
|
+
throw Failure("plugin-error",
|
|
864
|
+
"the plugin's controller rejected its state (IEditController::setState returned "
|
|
865
|
+
+ tresultName(controllerResult) + ")");
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// The processor now holds these values: earlier edits are obsolete.
|
|
869
|
+
const std::scoped_lock lock(pendingMutex_);
|
|
870
|
+
pending_.clear();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ── Processing ───────────────────────────────────────────────────────
|
|
874
|
+
|
|
875
|
+
namespace
|
|
876
|
+
{
|
|
877
|
+
int64 integerField(const Json& object, const char* name, int64 fallback, int64 minimum, int64 maximum,
|
|
878
|
+
const std::string& where)
|
|
879
|
+
{
|
|
880
|
+
const Json* value = object.find(name);
|
|
881
|
+
if (value == nullptr || value->isNull())
|
|
882
|
+
return fallback;
|
|
883
|
+
if (! value->isNumber() || value->asNumber() != std::floor(value->asNumber())
|
|
884
|
+
|| value->asNumber() < static_cast<double>(minimum) || value->asNumber() > static_cast<double>(maximum))
|
|
885
|
+
throw Failure("invalid-request", where + "." + name + " must be an integer from "
|
|
886
|
+
+ std::to_string(minimum) + " to " + std::to_string(maximum));
|
|
887
|
+
return static_cast<int64>(value->asNumber());
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
double numberField(const Json& object, const char* name, double fallback, double minimum, double maximum,
|
|
891
|
+
const std::string& where)
|
|
892
|
+
{
|
|
893
|
+
const Json* value = object.find(name);
|
|
894
|
+
if (value == nullptr || value->isNull())
|
|
895
|
+
return fallback;
|
|
896
|
+
if (! value->isNumber() || ! std::isfinite(value->asNumber()) || value->asNumber() < minimum
|
|
897
|
+
|| value->asNumber() > maximum)
|
|
898
|
+
{
|
|
899
|
+
std::ostringstream range;
|
|
900
|
+
range << minimum << " to " << maximum;
|
|
901
|
+
throw Failure("invalid-request", where + "." + name + " must be a number from " + range.str());
|
|
902
|
+
}
|
|
903
|
+
return value->asNumber();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
constexpr int64 maxFrames = int64 { 1 } << 31U;
|
|
907
|
+
} // namespace
|
|
908
|
+
|
|
909
|
+
Transport Plugin::parseTransport(const Json& transport, const std::string& where) const
|
|
910
|
+
{
|
|
911
|
+
if (! transport.isObject())
|
|
912
|
+
throw Failure("invalid-request", where + " must be an object");
|
|
913
|
+
Transport next = this->transport();
|
|
914
|
+
if (const Json* playing = transport.find("playing"); playing != nullptr)
|
|
915
|
+
{
|
|
916
|
+
if (! playing->isBool())
|
|
917
|
+
throw Failure("invalid-request", where + ".playing must be a boolean");
|
|
918
|
+
next.playing = playing->asBool();
|
|
919
|
+
}
|
|
920
|
+
next.tempo = numberField(transport, "tempo", next.tempo, 1, 999, where);
|
|
921
|
+
if (const Json* signature = transport.find("timeSignature"); signature != nullptr)
|
|
922
|
+
{
|
|
923
|
+
if (! signature->isObject())
|
|
924
|
+
throw Failure("invalid-request", where + ".timeSignature must be { numerator, denominator }");
|
|
925
|
+
next.numerator = static_cast<int32>(
|
|
926
|
+
integerField(*signature, "numerator", next.numerator, 1, 64, where + ".timeSignature"));
|
|
927
|
+
next.denominator = static_cast<int32>(
|
|
928
|
+
integerField(*signature, "denominator", next.denominator, 1, 64, where + ".timeSignature"));
|
|
929
|
+
}
|
|
930
|
+
const bool hasSamples = transport.find("samplePosition") != nullptr;
|
|
931
|
+
const bool hasMusic = transport.find("musicalPosition") != nullptr;
|
|
932
|
+
if (hasSamples && hasMusic)
|
|
933
|
+
throw Failure("invalid-request", where + " takes samplePosition or musicalPosition, not both");
|
|
934
|
+
if (hasSamples)
|
|
935
|
+
next.samplePosition =
|
|
936
|
+
integerField(transport, "samplePosition", 0, -(int64 { 1 } << 52U), int64 { 1 } << 52U, where);
|
|
937
|
+
if (hasMusic)
|
|
938
|
+
{
|
|
939
|
+
const double quarters = numberField(transport, "musicalPosition", 0, -1e12, 1e12, where);
|
|
940
|
+
next.samplePosition = static_cast<int64>(std::llround(quarters * 60.0 / next.tempo * setup_.sampleRate));
|
|
941
|
+
}
|
|
942
|
+
return next;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
Json Plugin::transportJson() const
|
|
946
|
+
{
|
|
947
|
+
const Transport now = this->transport();
|
|
948
|
+
Json transport = Json::object();
|
|
949
|
+
transport.set("playing", now.playing);
|
|
950
|
+
transport.set("tempo", now.tempo);
|
|
951
|
+
Json signature = Json::object();
|
|
952
|
+
signature.set("numerator", now.numerator);
|
|
953
|
+
signature.set("denominator", now.denominator);
|
|
954
|
+
transport.set("timeSignature", std::move(signature));
|
|
955
|
+
transport.set("samplePosition", now.samplePosition);
|
|
956
|
+
transport.set("musicalPosition",
|
|
957
|
+
static_cast<double>(now.samplePosition) / setup_.sampleRate * now.tempo / 60.0);
|
|
958
|
+
return transport;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
Json Plugin::setTransport(const Json& params)
|
|
962
|
+
{
|
|
963
|
+
requireLoaded();
|
|
964
|
+
const Transport next = parseTransport(params, "setTransport");
|
|
965
|
+
editTransport([&](Transport& transport) { transport = next; });
|
|
966
|
+
return transportJson();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
std::unique_ptr<ProcessJob> Plugin::prepareProcess(const Json& params, Payload&& payload)
|
|
970
|
+
{
|
|
971
|
+
requireLoaded();
|
|
972
|
+
auto job = std::make_unique<ProcessJob>();
|
|
973
|
+
job->frames = integerField(params, "frames", -1, 0, maxFrames, "process");
|
|
974
|
+
if (job->frames < 0)
|
|
975
|
+
throw Failure("invalid-request", "process.frames is required");
|
|
976
|
+
|
|
977
|
+
// The payload holds the provided input buses' channels in order, each
|
|
978
|
+
// `frames` float32 samples; `inputs` lists the channel count of each
|
|
979
|
+
// bus, 0 for an omitted bus (silence).
|
|
980
|
+
const Json* inputs = params.find("inputs");
|
|
981
|
+
if (inputs != nullptr && ! inputs->isArray())
|
|
982
|
+
throw Failure("invalid-request", "process.inputs must be an array of channel counts");
|
|
983
|
+
const size_t providedBuses = inputs != nullptr ? inputs->asArray().size() : 0;
|
|
984
|
+
if (providedBuses > audioInputs_.size())
|
|
985
|
+
throw Failure("invalid-request", "process.inputs has " + std::to_string(providedBuses)
|
|
986
|
+
+ " buses; the plugin has " + std::to_string(audioInputs_.size())
|
|
987
|
+
+ " audio input buses");
|
|
988
|
+
size_t expectedBytes = 0;
|
|
989
|
+
job->inputs.resize(audioInputs_.size());
|
|
990
|
+
std::vector<size_t> offsets;
|
|
991
|
+
for (size_t bus = 0; bus < audioInputs_.size(); ++bus)
|
|
992
|
+
{
|
|
993
|
+
const Bus& info = audioInputs_[bus];
|
|
994
|
+
int64 channels = 0;
|
|
995
|
+
if (bus < providedBuses)
|
|
996
|
+
{
|
|
997
|
+
const Json& count = inputs->asArray()[bus];
|
|
998
|
+
if (! count.isNumber() || count.asNumber() != std::floor(count.asNumber()) || count.asNumber() < 0
|
|
999
|
+
|| count.asNumber() > 1024)
|
|
1000
|
+
throw Failure("invalid-request",
|
|
1001
|
+
"process.inputs[" + std::to_string(bus) + "] must be a channel count");
|
|
1002
|
+
channels = static_cast<int64>(count.asNumber());
|
|
1003
|
+
}
|
|
1004
|
+
job->inputs[bus].assign(static_cast<size_t>(info.channels), nullptr);
|
|
1005
|
+
if (channels == 0)
|
|
1006
|
+
continue;
|
|
1007
|
+
if (! info.active)
|
|
1008
|
+
throw Failure("invalid-request", "audio input bus " + std::to_string(bus) + " (\"" + info.name
|
|
1009
|
+
+ "\") is inactive; omit its audio or activate it");
|
|
1010
|
+
if (channels != info.channels)
|
|
1011
|
+
throw Failure("invalid-request", "audio input bus " + std::to_string(bus) + " (\"" + info.name
|
|
1012
|
+
+ "\") has " + std::to_string(info.channels) + " channels ("
|
|
1013
|
+
+ arrangementName(info.arrangement) + "); "
|
|
1014
|
+
+ std::to_string(channels) + " were given");
|
|
1015
|
+
for (int64 channel = 0; channel < channels; ++channel)
|
|
1016
|
+
{
|
|
1017
|
+
offsets.push_back(expectedBytes);
|
|
1018
|
+
expectedBytes += static_cast<size_t>(job->frames) * sizeof(float);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (payload.size() != expectedBytes)
|
|
1022
|
+
throw Failure("invalid-request", "the request carries " + std::to_string(payload.size())
|
|
1023
|
+
+ " bytes of audio; its inputs and frames describe "
|
|
1024
|
+
+ std::to_string(expectedBytes));
|
|
1025
|
+
job->input = std::move(payload);
|
|
1026
|
+
size_t next = 0;
|
|
1027
|
+
for (size_t bus = 0; bus < providedBuses; ++bus)
|
|
1028
|
+
{
|
|
1029
|
+
if (inputs->asArray()[bus].asNumber() == 0)
|
|
1030
|
+
continue;
|
|
1031
|
+
for (auto& channel : job->inputs[bus])
|
|
1032
|
+
channel = reinterpret_cast<const float*>(job->input.data() + offsets[next++]);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Outputs: every active output bus, as many channels as it has.
|
|
1036
|
+
size_t outputBytes = 0;
|
|
1037
|
+
for (const Bus& bus : audioOutputs_)
|
|
1038
|
+
{
|
|
1039
|
+
job->outputChannels.push_back(bus.active ? bus.channels : 0);
|
|
1040
|
+
if (bus.active)
|
|
1041
|
+
outputBytes += static_cast<size_t>(bus.channels) * static_cast<size_t>(job->frames) * sizeof(float);
|
|
1042
|
+
}
|
|
1043
|
+
if (outputBytes > std::numeric_limits<uint32_t>::max())
|
|
1044
|
+
throw Failure("message-too-large", "the output of " + std::to_string(job->frames)
|
|
1045
|
+
+ " frames does not fit one response; process fewer frames");
|
|
1046
|
+
job->output.resize(outputBytes);
|
|
1047
|
+
|
|
1048
|
+
const std::string pluginName = class_.name();
|
|
1049
|
+
if (const Json* changes = params.find("parameterChanges"); changes != nullptr && ! changes->isNull())
|
|
1050
|
+
{
|
|
1051
|
+
if (! changes->isArray())
|
|
1052
|
+
throw Failure("invalid-request", "process.parameterChanges must be an array");
|
|
1053
|
+
for (size_t index = 0; index < changes->asArray().size(); ++index)
|
|
1054
|
+
{
|
|
1055
|
+
const Json& change = changes->asArray()[index];
|
|
1056
|
+
const std::string where = "process.parameterChanges[" + std::to_string(index) + "]";
|
|
1057
|
+
if (! change.isObject())
|
|
1058
|
+
throw Failure("invalid-request", where + " must be an object");
|
|
1059
|
+
const auto id = static_cast<ParamID>(integerField(change, "id", -1, 0, 0xFFFFFFFFLL, where));
|
|
1060
|
+
if (change.find("id") == nullptr)
|
|
1061
|
+
throw Failure("invalid-request", where + ".id is required");
|
|
1062
|
+
parameter(id); // exists
|
|
1063
|
+
job->changes.push_back(
|
|
1064
|
+
{ id, numberField(change, "value", -1, 0, 1, where),
|
|
1065
|
+
integerField(change, "time", 0, 0, job->frames - 1 < 0 ? 0 : job->frames - 1, where) });
|
|
1066
|
+
if (job->changes.back().value < 0)
|
|
1067
|
+
throw Failure("invalid-request", where + ".value (normalized, 0 to 1) is required");
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
std::vector<std::string> warnings;
|
|
1072
|
+
if (const Json* events = params.find("events"); events != nullptr && ! events->isNull())
|
|
1073
|
+
{
|
|
1074
|
+
if (! events->isArray())
|
|
1075
|
+
throw Failure("invalid-request", "process.events must be an array");
|
|
1076
|
+
for (size_t index = 0; index < events->asArray().size(); ++index)
|
|
1077
|
+
{
|
|
1078
|
+
const Json& source = events->asArray()[index];
|
|
1079
|
+
const std::string where = "process.events[" + std::to_string(index) + "]";
|
|
1080
|
+
if (! source.isObject() || source.find("type") == nullptr || ! source.find("type")->isString())
|
|
1081
|
+
throw Failure("invalid-request", where + " must be an object with a type");
|
|
1082
|
+
const std::string type = source.find("type")->asString();
|
|
1083
|
+
const int64 time = integerField(source, "time", 0, 0, job->frames - 1 < 0 ? 0 : job->frames - 1, where);
|
|
1084
|
+
const auto bus = static_cast<int32>(integerField(source, "bus", 0, 0, 1023, where));
|
|
1085
|
+
const auto channel = static_cast<int16>(integerField(source, "channel", 0, 0, 15, where));
|
|
1086
|
+
|
|
1087
|
+
if (type == "controlChange" || type == "pitchBend" || type == "channelPressure")
|
|
1088
|
+
{
|
|
1089
|
+
// MIDI controllers reach VST3 plugins as parameters, through
|
|
1090
|
+
// the controller's IMidiMapping.
|
|
1091
|
+
CtrlNumber controllerNumber = kPitchBend;
|
|
1092
|
+
double value = 0;
|
|
1093
|
+
if (type == "controlChange")
|
|
1094
|
+
{
|
|
1095
|
+
controllerNumber =
|
|
1096
|
+
static_cast<CtrlNumber>(integerField(source, "controller", -1, 0, 127, where));
|
|
1097
|
+
if (controllerNumber < 0)
|
|
1098
|
+
throw Failure("invalid-request", where + ".controller (0 to 127) is required");
|
|
1099
|
+
value = numberField(source, "value", 0, 0, 1, where);
|
|
1100
|
+
}
|
|
1101
|
+
else if (type == "pitchBend")
|
|
1102
|
+
value = (numberField(source, "value", 0, -1, 1, where) + 1) / 2;
|
|
1103
|
+
else
|
|
1104
|
+
{
|
|
1105
|
+
controllerNumber = kAfterTouch;
|
|
1106
|
+
value = numberField(source, "value", 0, 0, 1, where);
|
|
1107
|
+
}
|
|
1108
|
+
ParamID id = 0;
|
|
1109
|
+
if (midiMapping_ != nullptr
|
|
1110
|
+
&& midiMapping_->getMidiControllerAssignment(bus, channel, controllerNumber, id) == kResultTrue)
|
|
1111
|
+
job->changes.push_back({ id, value, time });
|
|
1112
|
+
else
|
|
1113
|
+
{
|
|
1114
|
+
std::string warning = where;
|
|
1115
|
+
warning += ": \"";
|
|
1116
|
+
warning += pluginName;
|
|
1117
|
+
warning += "\" maps no parameter to ";
|
|
1118
|
+
warning += type;
|
|
1119
|
+
if (type == "controlChange")
|
|
1120
|
+
warning += " " + std::to_string(controllerNumber);
|
|
1121
|
+
warning += " on channel " + std::to_string(channel) + "; it was dropped";
|
|
1122
|
+
warnings.push_back(std::move(warning));
|
|
1123
|
+
}
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (static_cast<size_t>(bus) >= eventInputs_.size())
|
|
1128
|
+
{
|
|
1129
|
+
std::string message = where;
|
|
1130
|
+
message += ": \"" + pluginName + "\" has " + std::to_string(eventInputs_.size());
|
|
1131
|
+
message += " event input buses; it takes no events on bus " + std::to_string(bus);
|
|
1132
|
+
throw Failure("invalid-request", message);
|
|
1133
|
+
}
|
|
1134
|
+
Event event {};
|
|
1135
|
+
event.busIndex = bus;
|
|
1136
|
+
event.sampleOffset = static_cast<int32>(time);
|
|
1137
|
+
event.flags = Event::kIsLive;
|
|
1138
|
+
const auto noteId = static_cast<int32>(integerField(source, "noteId", -1, -1, 0x7FFFFFFF, where));
|
|
1139
|
+
const auto pitch = static_cast<int16>(integerField(source, "pitch", -1, 0, 127, where));
|
|
1140
|
+
if (type == "noteOn" || type == "noteOff" || type == "polyPressure")
|
|
1141
|
+
{
|
|
1142
|
+
if (pitch < 0)
|
|
1143
|
+
throw Failure("invalid-request", where + ".pitch (0 to 127) is required");
|
|
1144
|
+
}
|
|
1145
|
+
if (type == "noteOn")
|
|
1146
|
+
{
|
|
1147
|
+
event.type = Event::kNoteOnEvent;
|
|
1148
|
+
event.noteOn = { channel,
|
|
1149
|
+
pitch,
|
|
1150
|
+
static_cast<float>(numberField(source, "tuning", 0, -1200, 1200, where)),
|
|
1151
|
+
static_cast<float>(numberField(source, "velocity", 1, 0, 1, where)),
|
|
1152
|
+
static_cast<int32>(integerField(source, "length", 0, 0, maxFrames, where)),
|
|
1153
|
+
noteId };
|
|
1154
|
+
}
|
|
1155
|
+
else if (type == "noteOff")
|
|
1156
|
+
{
|
|
1157
|
+
event.type = Event::kNoteOffEvent;
|
|
1158
|
+
event.noteOff = { channel, pitch,
|
|
1159
|
+
static_cast<float>(numberField(source, "velocity", 0, 0, 1, where)), noteId,
|
|
1160
|
+
static_cast<float>(numberField(source, "tuning", 0, -1200, 1200, where)) };
|
|
1161
|
+
}
|
|
1162
|
+
else if (type == "polyPressure")
|
|
1163
|
+
{
|
|
1164
|
+
event.type = Event::kPolyPressureEvent;
|
|
1165
|
+
event.polyPressure = { channel, pitch,
|
|
1166
|
+
static_cast<float>(numberField(source, "pressure", 0, 0, 1, where)),
|
|
1167
|
+
noteId };
|
|
1168
|
+
}
|
|
1169
|
+
else
|
|
1170
|
+
{
|
|
1171
|
+
std::string message = where;
|
|
1172
|
+
message += ".type \"" + type;
|
|
1173
|
+
message += "\" is not one of noteOn, noteOff, polyPressure, controlChange, pitchBend, "
|
|
1174
|
+
"channelPressure";
|
|
1175
|
+
throw Failure("invalid-request", message);
|
|
1176
|
+
}
|
|
1177
|
+
job->events.push_back(event);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
std::stable_sort(job->events.begin(), job->events.end(),
|
|
1181
|
+
[](const Event& a, const Event& b) { return a.sampleOffset < b.sampleOffset; });
|
|
1182
|
+
std::stable_sort(job->changes.begin(), job->changes.end(),
|
|
1183
|
+
[](const ParameterChange& a, const ParameterChange& b) { return a.time < b.time; });
|
|
1184
|
+
if (job->frames == 0 && (! job->events.empty() || ! job->changes.empty()))
|
|
1185
|
+
{
|
|
1186
|
+
// Parameter changes may be flushed with zero frames; events need a frame.
|
|
1187
|
+
if (! job->events.empty())
|
|
1188
|
+
throw Failure("invalid-request", "events need at least one frame to be processed");
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (const Json* transport = params.find("transport"); transport != nullptr && ! transport->isNull())
|
|
1192
|
+
job->transport = parseTransport(*transport, "process.transport");
|
|
1193
|
+
|
|
1194
|
+
job->warnings = std::move(warnings);
|
|
1195
|
+
return job;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
void Plugin::runProcess(ProcessJob& job)
|
|
1199
|
+
{
|
|
1200
|
+
// This call's own copy of the transport, written back at the end
|
|
1201
|
+
// unless something changed it meanwhile.
|
|
1202
|
+
Transport transport;
|
|
1203
|
+
uint64_t edits = 0;
|
|
1204
|
+
{
|
|
1205
|
+
const std::scoped_lock lock(transportMutex_);
|
|
1206
|
+
if (job.transport)
|
|
1207
|
+
{
|
|
1208
|
+
transport_ = *job.transport;
|
|
1209
|
+
++transportEdits_;
|
|
1210
|
+
}
|
|
1211
|
+
transport = transport_;
|
|
1212
|
+
edits = transportEdits_;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
std::vector<ParameterChange> changes;
|
|
1216
|
+
std::vector<Event> events;
|
|
1217
|
+
{
|
|
1218
|
+
// Edits and notes made since the last call land at its first frame.
|
|
1219
|
+
const std::scoped_lock lock(pendingMutex_);
|
|
1220
|
+
changes.swap(pending_);
|
|
1221
|
+
events.swap(pendingEvents_);
|
|
1222
|
+
}
|
|
1223
|
+
changes.insert(changes.end(), job.changes.begin(), job.changes.end());
|
|
1224
|
+
events.insert(events.end(), job.events.begin(), job.events.end());
|
|
1225
|
+
|
|
1226
|
+
const int64 block = setup_.blockSize;
|
|
1227
|
+
const auto inputBuses = static_cast<int32>(audioInputs_.size());
|
|
1228
|
+
const auto outputBuses = static_cast<int32>(audioOutputs_.size());
|
|
1229
|
+
|
|
1230
|
+
// Silence for omitted or inactive input buses and somewhere to write
|
|
1231
|
+
// for inactive output buses: plugins may touch every bus's channels.
|
|
1232
|
+
std::vector<std::vector<std::vector<float>>> scratchIn(audioInputs_.size());
|
|
1233
|
+
std::vector<std::vector<std::vector<float>>> scratchOut(audioOutputs_.size());
|
|
1234
|
+
std::vector<std::vector<float*>> inPointers(audioInputs_.size());
|
|
1235
|
+
std::vector<std::vector<float*>> outPointers(audioOutputs_.size());
|
|
1236
|
+
std::vector<AudioBusBuffers> inBuffers(audioInputs_.size());
|
|
1237
|
+
std::vector<AudioBusBuffers> outBuffers(audioOutputs_.size());
|
|
1238
|
+
for (size_t bus = 0; bus < audioInputs_.size(); ++bus)
|
|
1239
|
+
{
|
|
1240
|
+
const auto channels = static_cast<size_t>(audioInputs_[bus].channels);
|
|
1241
|
+
scratchIn[bus].assign(channels, std::vector<float>(static_cast<size_t>(block)));
|
|
1242
|
+
inPointers[bus].resize(channels);
|
|
1243
|
+
}
|
|
1244
|
+
size_t outputOffset = 0;
|
|
1245
|
+
std::vector<std::vector<float*>> outputChannels(audioOutputs_.size());
|
|
1246
|
+
for (size_t bus = 0; bus < audioOutputs_.size(); ++bus)
|
|
1247
|
+
{
|
|
1248
|
+
const auto channels = static_cast<size_t>(audioOutputs_[bus].channels);
|
|
1249
|
+
scratchOut[bus].assign(channels, std::vector<float>(static_cast<size_t>(block)));
|
|
1250
|
+
outPointers[bus].resize(channels);
|
|
1251
|
+
if (job.outputChannels.size() > bus && job.outputChannels[bus] > 0)
|
|
1252
|
+
for (size_t channel = 0; channel < channels; ++channel)
|
|
1253
|
+
{
|
|
1254
|
+
outputChannels[bus].push_back(reinterpret_cast<float*>(job.output.data() + outputOffset));
|
|
1255
|
+
outputOffset += static_cast<size_t>(job.frames) * sizeof(float);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
ParameterChanges inputChanges(static_cast<int32>(std::max<size_t>(changes.size(), 1)));
|
|
1260
|
+
ParameterChanges outputChanges(std::max(parameterCount_, 1));
|
|
1261
|
+
EventList inputEvents(static_cast<int32>(std::max<size_t>(events.size(), 1)));
|
|
1262
|
+
EventList outputEvents(1024);
|
|
1263
|
+
ProcessContext context {};
|
|
1264
|
+
|
|
1265
|
+
size_t nextChange = 0;
|
|
1266
|
+
size_t nextEvent = 0;
|
|
1267
|
+
int64 start = 0;
|
|
1268
|
+
do
|
|
1269
|
+
{
|
|
1270
|
+
const int64 frames = std::min(block, job.frames - start);
|
|
1271
|
+
for (size_t bus = 0; bus < audioInputs_.size(); ++bus)
|
|
1272
|
+
{
|
|
1273
|
+
for (size_t channel = 0; channel < inPointers[bus].size(); ++channel)
|
|
1274
|
+
{
|
|
1275
|
+
const float* source = job.inputs[bus][channel];
|
|
1276
|
+
float* scratch = scratchIn[bus][channel].data();
|
|
1277
|
+
// The plugin gets its own copy: it may process in place.
|
|
1278
|
+
if (source != nullptr)
|
|
1279
|
+
std::memcpy(scratch, source + start, static_cast<size_t>(frames) * sizeof(float));
|
|
1280
|
+
else
|
|
1281
|
+
std::memset(scratch, 0, static_cast<size_t>(frames) * sizeof(float));
|
|
1282
|
+
inPointers[bus][channel] = scratch;
|
|
1283
|
+
}
|
|
1284
|
+
inBuffers[bus].numChannels = static_cast<int32>(inPointers[bus].size());
|
|
1285
|
+
inBuffers[bus].channelBuffers32 = inPointers[bus].data();
|
|
1286
|
+
inBuffers[bus].silenceFlags = 0;
|
|
1287
|
+
}
|
|
1288
|
+
for (size_t bus = 0; bus < audioOutputs_.size(); ++bus)
|
|
1289
|
+
{
|
|
1290
|
+
for (size_t channel = 0; channel < outPointers[bus].size(); ++channel)
|
|
1291
|
+
{
|
|
1292
|
+
// A call without frames has no output buffer: the plugin
|
|
1293
|
+
// still gets valid channels.
|
|
1294
|
+
float* target = outputChannels[bus].empty() || job.frames == 0
|
|
1295
|
+
? scratchOut[bus][channel].data()
|
|
1296
|
+
: outputChannels[bus][channel] + start;
|
|
1297
|
+
std::memset(target, 0, static_cast<size_t>(frames) * sizeof(float));
|
|
1298
|
+
outPointers[bus][channel] = target;
|
|
1299
|
+
}
|
|
1300
|
+
outBuffers[bus].numChannels = static_cast<int32>(outPointers[bus].size());
|
|
1301
|
+
outBuffers[bus].channelBuffers32 = outPointers[bus].data();
|
|
1302
|
+
outBuffers[bus].silenceFlags = 0;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
inputChanges.clearQueue();
|
|
1306
|
+
while (nextChange < changes.size() && (changes[nextChange].time < start + frames || frames == 0))
|
|
1307
|
+
{
|
|
1308
|
+
const auto& change = changes[nextChange++];
|
|
1309
|
+
int32 queueIndex = 0;
|
|
1310
|
+
if (auto* queue = inputChanges.addParameterData(change.id, queueIndex))
|
|
1311
|
+
{
|
|
1312
|
+
int32 pointIndex = 0;
|
|
1313
|
+
queue->addPoint(static_cast<int32>(change.time - start), change.value, pointIndex);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
inputEvents.clear();
|
|
1317
|
+
while (nextEvent < events.size() && events[nextEvent].sampleOffset < start + frames)
|
|
1318
|
+
{
|
|
1319
|
+
Event event = events[nextEvent++];
|
|
1320
|
+
event.sampleOffset -= static_cast<int32>(start);
|
|
1321
|
+
inputEvents.addEvent(event);
|
|
1322
|
+
}
|
|
1323
|
+
outputChanges.clearQueue();
|
|
1324
|
+
outputEvents.clear();
|
|
1325
|
+
|
|
1326
|
+
const double quarters =
|
|
1327
|
+
static_cast<double>(transport.samplePosition) / setup_.sampleRate * transport.tempo / 60.0;
|
|
1328
|
+
const double barLength = 4.0 * transport.numerator / transport.denominator;
|
|
1329
|
+
context.state = ProcessContext::kTempoValid | ProcessContext::kTimeSigValid
|
|
1330
|
+
| ProcessContext::kProjectTimeMusicValid | ProcessContext::kBarPositionValid
|
|
1331
|
+
| ProcessContext::kContTimeValid | (transport.playing ? ProcessContext::kPlaying : 0);
|
|
1332
|
+
context.sampleRate = setup_.sampleRate;
|
|
1333
|
+
context.projectTimeSamples = transport.samplePosition;
|
|
1334
|
+
context.continousTimeSamples = continuousSamples_;
|
|
1335
|
+
context.projectTimeMusic = quarters;
|
|
1336
|
+
context.barPositionMusic = std::floor(quarters / barLength) * barLength;
|
|
1337
|
+
context.tempo = transport.tempo;
|
|
1338
|
+
context.timeSigNumerator = transport.numerator;
|
|
1339
|
+
context.timeSigDenominator = transport.denominator;
|
|
1340
|
+
|
|
1341
|
+
ProcessData data;
|
|
1342
|
+
data.processMode = setup_.processMode;
|
|
1343
|
+
data.symbolicSampleSize = kSample32;
|
|
1344
|
+
data.numSamples = static_cast<int32>(frames);
|
|
1345
|
+
data.numInputs = inputBuses;
|
|
1346
|
+
data.numOutputs = outputBuses;
|
|
1347
|
+
data.inputs = inBuffers.empty() ? nullptr : inBuffers.data();
|
|
1348
|
+
data.outputs = outBuffers.empty() ? nullptr : outBuffers.data();
|
|
1349
|
+
data.inputParameterChanges = &inputChanges;
|
|
1350
|
+
data.outputParameterChanges = &outputChanges;
|
|
1351
|
+
data.inputEvents = &inputEvents;
|
|
1352
|
+
data.outputEvents = eventOutputs_.empty() ? nullptr : &outputEvents;
|
|
1353
|
+
data.processContext = &context;
|
|
1354
|
+
|
|
1355
|
+
const tresult result = processor_->process(data);
|
|
1356
|
+
if (result != kResultOk)
|
|
1357
|
+
{
|
|
1358
|
+
job.error = "the plugin's process call failed at frame " + std::to_string(start)
|
|
1359
|
+
+ " (IAudioProcessor::process returned " + tresultName(result) + ")";
|
|
1360
|
+
break;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
for (int32 index = 0; index < outputChanges.getParameterCount(); ++index)
|
|
1364
|
+
{
|
|
1365
|
+
IParamValueQueue* queue = outputChanges.getParameterData(index);
|
|
1366
|
+
for (int32 point = 0; queue != nullptr && point < queue->getPointCount(); ++point)
|
|
1367
|
+
{
|
|
1368
|
+
int32 offset = 0;
|
|
1369
|
+
ParamValue value = 0;
|
|
1370
|
+
if (queue->getPoint(point, offset, value) == kResultOk)
|
|
1371
|
+
job.outputChanges.push_back({ queue->getParameterId(), value, start + offset });
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
for (int32 index = 0; index < outputEvents.getEventCount(); ++index)
|
|
1375
|
+
{
|
|
1376
|
+
Event event {};
|
|
1377
|
+
if (outputEvents.getEvent(index, event) == kResultOk)
|
|
1378
|
+
{
|
|
1379
|
+
event.sampleOffset += static_cast<int32>(start);
|
|
1380
|
+
job.outputEvents.push_back(event);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (transport.playing)
|
|
1385
|
+
transport.samplePosition += frames;
|
|
1386
|
+
continuousSamples_ += frames;
|
|
1387
|
+
start += frames;
|
|
1388
|
+
} while (start < job.frames);
|
|
1389
|
+
|
|
1390
|
+
const std::scoped_lock lock(transportMutex_);
|
|
1391
|
+
if (transportEdits_ == edits)
|
|
1392
|
+
transport_ = transport;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
Json Plugin::finishProcess(ProcessJob& job)
|
|
1396
|
+
{
|
|
1397
|
+
if (! job.error.empty())
|
|
1398
|
+
throw Failure("plugin-error", job.error);
|
|
1399
|
+
|
|
1400
|
+
// The controller follows what the processor was given and reported.
|
|
1401
|
+
if (controller_ != nullptr)
|
|
1402
|
+
{
|
|
1403
|
+
std::map<ParamID, double> last;
|
|
1404
|
+
for (const auto& change : job.changes)
|
|
1405
|
+
last[change.id] = change.value;
|
|
1406
|
+
for (const auto& change : job.outputChanges)
|
|
1407
|
+
last[change.id] = change.value;
|
|
1408
|
+
for (const auto& [id, value] : last)
|
|
1409
|
+
controller_->setParamNormalized(id, value);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
Json result = Json::object();
|
|
1413
|
+
result.set("frames", job.frames);
|
|
1414
|
+
Json outputs = Json::array();
|
|
1415
|
+
for (const int32 channels : job.outputChannels)
|
|
1416
|
+
outputs.push(channels);
|
|
1417
|
+
result.set("outputs", std::move(outputs));
|
|
1418
|
+
|
|
1419
|
+
Json events = Json::array();
|
|
1420
|
+
for (const Event& event : job.outputEvents)
|
|
1421
|
+
{
|
|
1422
|
+
Json entry = Json::object();
|
|
1423
|
+
entry.set("time", event.sampleOffset);
|
|
1424
|
+
entry.set("bus", event.busIndex);
|
|
1425
|
+
switch (event.type)
|
|
1426
|
+
{
|
|
1427
|
+
case Event::kNoteOnEvent:
|
|
1428
|
+
entry.set("type", "noteOn");
|
|
1429
|
+
entry.set("channel", event.noteOn.channel);
|
|
1430
|
+
entry.set("pitch", event.noteOn.pitch);
|
|
1431
|
+
entry.set("velocity", static_cast<double>(event.noteOn.velocity));
|
|
1432
|
+
entry.set("noteId", event.noteOn.noteId);
|
|
1433
|
+
break;
|
|
1434
|
+
case Event::kNoteOffEvent:
|
|
1435
|
+
entry.set("type", "noteOff");
|
|
1436
|
+
entry.set("channel", event.noteOff.channel);
|
|
1437
|
+
entry.set("pitch", event.noteOff.pitch);
|
|
1438
|
+
entry.set("velocity", static_cast<double>(event.noteOff.velocity));
|
|
1439
|
+
entry.set("noteId", event.noteOff.noteId);
|
|
1440
|
+
break;
|
|
1441
|
+
case Event::kPolyPressureEvent:
|
|
1442
|
+
entry.set("type", "polyPressure");
|
|
1443
|
+
entry.set("channel", event.polyPressure.channel);
|
|
1444
|
+
entry.set("pitch", event.polyPressure.pitch);
|
|
1445
|
+
entry.set("pressure", static_cast<double>(event.polyPressure.pressure));
|
|
1446
|
+
entry.set("noteId", event.polyPressure.noteId);
|
|
1447
|
+
break;
|
|
1448
|
+
case Event::kLegacyMIDICCOutEvent:
|
|
1449
|
+
entry.set("type", "midiCC");
|
|
1450
|
+
entry.set("channel", event.midiCCOut.channel);
|
|
1451
|
+
entry.set("controller", static_cast<int>(event.midiCCOut.controlNumber));
|
|
1452
|
+
entry.set("value", event.midiCCOut.value);
|
|
1453
|
+
entry.set("value2", event.midiCCOut.value2);
|
|
1454
|
+
break;
|
|
1455
|
+
default:
|
|
1456
|
+
entry.set("type", "other");
|
|
1457
|
+
entry.set("vstType", event.type);
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
events.push(std::move(entry));
|
|
1461
|
+
}
|
|
1462
|
+
result.set("events", std::move(events));
|
|
1463
|
+
|
|
1464
|
+
Json changes = Json::array();
|
|
1465
|
+
for (const auto& change : job.outputChanges)
|
|
1466
|
+
{
|
|
1467
|
+
Json entry = Json::object();
|
|
1468
|
+
entry.set("id", change.id);
|
|
1469
|
+
entry.set("value", change.value);
|
|
1470
|
+
entry.set("time", change.time);
|
|
1471
|
+
changes.push(std::move(entry));
|
|
1472
|
+
}
|
|
1473
|
+
result.set("parameterChanges", std::move(changes));
|
|
1474
|
+
|
|
1475
|
+
result.set("transport", transportJson());
|
|
1476
|
+
Json warnings = Json::array();
|
|
1477
|
+
for (const auto& warning : job.warnings)
|
|
1478
|
+
warnings.push(warning);
|
|
1479
|
+
result.set("warnings", std::move(warnings));
|
|
1480
|
+
return result;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// ── Editor ───────────────────────────────────────────────────────────
|
|
1484
|
+
|
|
1485
|
+
Json Plugin::editorInfo() const
|
|
1486
|
+
{
|
|
1487
|
+
Json result = Json::object();
|
|
1488
|
+
result.set("open", view_ != nullptr);
|
|
1489
|
+
if (view_ == nullptr)
|
|
1490
|
+
return result;
|
|
1491
|
+
ViewRect rect {};
|
|
1492
|
+
view_->getSize(&rect);
|
|
1493
|
+
result.set("width", rect.getWidth());
|
|
1494
|
+
result.set("height", rect.getHeight());
|
|
1495
|
+
result.set("resizable", view_->canResize() == kResultTrue);
|
|
1496
|
+
result.set("scale", scale_);
|
|
1497
|
+
result.set("platformType", window_->platformType());
|
|
1498
|
+
result.set("presentation", window_->presentation());
|
|
1499
|
+
const auto [windowWidth, windowHeight] = window_->windowSize();
|
|
1500
|
+
Json window = Json::object();
|
|
1501
|
+
window.set("width", windowWidth);
|
|
1502
|
+
window.set("height", windowHeight);
|
|
1503
|
+
result.set("window", std::move(window));
|
|
1504
|
+
if (const auto frames = window_->interfaceFrames())
|
|
1505
|
+
result.set("interfaceFrames", static_cast<double>(*frames));
|
|
1506
|
+
return result;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
Json Plugin::openEditor(std::optional<double> scale)
|
|
1510
|
+
{
|
|
1511
|
+
requireLoaded();
|
|
1512
|
+
if (! ui_)
|
|
1513
|
+
throw Failure("headless", "this host runs headless; launch it with `editor: true` to open editors");
|
|
1514
|
+
if (view_ != nullptr)
|
|
1515
|
+
return editorInfo();
|
|
1516
|
+
if (controller_ == nullptr)
|
|
1517
|
+
throw Failure("no-editor", "\"" + class_.name() + "\" has no controller, so no editor");
|
|
1518
|
+
auto view = owned(controller_->createView(ViewType::kEditor));
|
|
1519
|
+
if (view == nullptr)
|
|
1520
|
+
throw Failure("no-editor", "\"" + class_.name() + "\" has no editor (createView returned nothing)");
|
|
1521
|
+
|
|
1522
|
+
ViewRect rect {};
|
|
1523
|
+
if (view->getSize(&rect) != kResultOk || rect.getWidth() <= 0 || rect.getHeight() <= 0)
|
|
1524
|
+
rect = ViewRect(0, 0, 640, 480);
|
|
1525
|
+
|
|
1526
|
+
const int generation = ++editorGeneration_;
|
|
1527
|
+
EditorWindow::Callbacks callbacks;
|
|
1528
|
+
callbacks.resized = [this, generation](int width, int height)
|
|
1529
|
+
{
|
|
1530
|
+
if (generation != editorGeneration_ || view_ == nullptr || resizing_)
|
|
1531
|
+
return;
|
|
1532
|
+
// Constraint checks may call IPlugFrame::resizeView themselves.
|
|
1533
|
+
// Guard the whole negotiation, including native window callbacks.
|
|
1534
|
+
resizing_ = true;
|
|
1535
|
+
ViewRect size(0, 0, width, height);
|
|
1536
|
+
view_->checkSizeConstraint(&size);
|
|
1537
|
+
// Resize the native container before the editor paints its new
|
|
1538
|
+
// size, including when no constraint correction was necessary.
|
|
1539
|
+
window_->constrainContentSize(size.getWidth(), size.getHeight());
|
|
1540
|
+
view_->onSize(&size);
|
|
1541
|
+
resizing_ = false;
|
|
1542
|
+
callbacks_.emit("editorResized", editorInfo());
|
|
1543
|
+
};
|
|
1544
|
+
callbacks.closed = [this, generation]
|
|
1545
|
+
{
|
|
1546
|
+
if (generation != editorGeneration_ || view_ == nullptr)
|
|
1547
|
+
return;
|
|
1548
|
+
closeEditor();
|
|
1549
|
+
callbacks_.emit("editorClosed", Json::object());
|
|
1550
|
+
};
|
|
1551
|
+
callbacks.focusChanged = [this, generation](bool focused)
|
|
1552
|
+
{
|
|
1553
|
+
if (generation == editorGeneration_ && view_ != nullptr)
|
|
1554
|
+
view_->onFocus(focused);
|
|
1555
|
+
};
|
|
1556
|
+
callbacks.scaleChanged = [this, generation](double next)
|
|
1557
|
+
{
|
|
1558
|
+
if (generation != editorGeneration_ || view_ == nullptr)
|
|
1559
|
+
return;
|
|
1560
|
+
scale_ = next;
|
|
1561
|
+
if (auto support = U::cast<IPlugViewContentScaleSupport>(view_))
|
|
1562
|
+
support->setContentScaleFactor(static_cast<IPlugViewContentScaleSupport::ScaleFactor>(next));
|
|
1563
|
+
tellWindowEditorLimits();
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
std::string error;
|
|
1567
|
+
auto window =
|
|
1568
|
+
callbacks_.createWindow(class_.name(), rect.getWidth(), rect.getHeight(), std::move(callbacks), error);
|
|
1569
|
+
if (window == nullptr)
|
|
1570
|
+
throw Failure("no-display", error);
|
|
1571
|
+
if (view->isPlatformTypeSupported(window->platformType()) != kResultTrue)
|
|
1572
|
+
throw Failure("unsupported-plugin",
|
|
1573
|
+
"\"" + class_.name() + "\"'s editor does not support " + window->platformType() + " windows");
|
|
1574
|
+
|
|
1575
|
+
frame_ = owned(new PlugFrame(*this,
|
|
1576
|
+
#if defined(__linux__)
|
|
1577
|
+
runLoop_->unknownCast()
|
|
1578
|
+
#else
|
|
1579
|
+
nullptr
|
|
1580
|
+
#endif
|
|
1581
|
+
));
|
|
1582
|
+
view->setFrame(frame_);
|
|
1583
|
+
// Scale: the requested one, else the window's (Windows' DPI). On
|
|
1584
|
+
// macOS views are in points and the factor is not used.
|
|
1585
|
+
scale_ = scale.value_or(window->scale());
|
|
1586
|
+
#if ! defined(__APPLE__)
|
|
1587
|
+
if (auto support = U::cast<IPlugViewContentScaleSupport>(view))
|
|
1588
|
+
support->setContentScaleFactor(static_cast<IPlugViewContentScaleSupport::ScaleFactor>(scale_));
|
|
1589
|
+
#endif
|
|
1590
|
+
window->setResizable(view->canResize() == kResultTrue);
|
|
1591
|
+
view_ = view;
|
|
1592
|
+
window_ = std::move(window);
|
|
1593
|
+
const tresult attached = view_->attached(window_->parentHandle(), window_->platformType());
|
|
1594
|
+
if (attached != kResultOk)
|
|
1595
|
+
{
|
|
1596
|
+
view_->setFrame(nullptr);
|
|
1597
|
+
view_ = nullptr;
|
|
1598
|
+
frame_ = nullptr;
|
|
1599
|
+
window_.reset();
|
|
1600
|
+
throw Failure("plugin-error", "\"" + class_.name() + "\"'s editor failed to attach (attached returned "
|
|
1601
|
+
+ tresultName(attached) + ")");
|
|
1602
|
+
}
|
|
1603
|
+
tellWindowEditorLimits();
|
|
1604
|
+
// The size may change while attaching (scale, restored size).
|
|
1605
|
+
if (view_->getSize(&rect) == kResultOk && rect.getWidth() > 0 && rect.getHeight() > 0)
|
|
1606
|
+
window_->setContentSize(rect.getWidth(), rect.getHeight());
|
|
1607
|
+
window_->show();
|
|
1608
|
+
return editorInfo();
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
void Plugin::closeEditor()
|
|
1612
|
+
{
|
|
1613
|
+
if (view_ == nullptr)
|
|
1614
|
+
return;
|
|
1615
|
+
++editorGeneration_;
|
|
1616
|
+
view_->removed();
|
|
1617
|
+
view_->setFrame(nullptr);
|
|
1618
|
+
view_ = nullptr;
|
|
1619
|
+
frame_ = nullptr;
|
|
1620
|
+
window_.reset();
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
void Plugin::previewEditorSize(int width, int height)
|
|
1624
|
+
{
|
|
1625
|
+
if (view_ == nullptr || view_->canResize() != kResultTrue || resizing_)
|
|
1626
|
+
return;
|
|
1627
|
+
resizing_ = true;
|
|
1628
|
+
ViewRect size(0, 0, std::max(width, 1), std::max(height, 1));
|
|
1629
|
+
view_->checkSizeConstraint(&size);
|
|
1630
|
+
keepToRoom(size);
|
|
1631
|
+
window_->constrainContentSize(size.getWidth(), size.getHeight());
|
|
1632
|
+
resizing_ = false;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
void Plugin::previewEditorDrag(int width, int height, int lastWidth, int lastHeight)
|
|
1636
|
+
{
|
|
1637
|
+
if (view_ == nullptr || view_->canResize() != kResultTrue || resizing_)
|
|
1638
|
+
return;
|
|
1639
|
+
resizing_ = true;
|
|
1640
|
+
constexpr int rounding = 2;
|
|
1641
|
+
constexpr int unlimited = 1 << 20;
|
|
1642
|
+
width = std::max(width, 1);
|
|
1643
|
+
height = std::max(height, 1);
|
|
1644
|
+
ViewRect size(0, 0, width, height);
|
|
1645
|
+
view_->checkSizeConstraint(&size);
|
|
1646
|
+
if (height == lastHeight && width != lastWidth && std::abs(size.getWidth() - width) > rounding)
|
|
1647
|
+
{
|
|
1648
|
+
ViewRect free(0, 0, width, unlimited);
|
|
1649
|
+
view_->checkSizeConstraint(&free);
|
|
1650
|
+
if (std::abs(free.getWidth() - width) <= rounding)
|
|
1651
|
+
size = free;
|
|
1652
|
+
}
|
|
1653
|
+
else if (width == lastWidth && height != lastHeight && std::abs(size.getHeight() - height) > rounding)
|
|
1654
|
+
{
|
|
1655
|
+
ViewRect free(0, 0, unlimited, height);
|
|
1656
|
+
view_->checkSizeConstraint(&free);
|
|
1657
|
+
if (std::abs(free.getHeight() - height) <= rounding)
|
|
1658
|
+
size = free;
|
|
1659
|
+
}
|
|
1660
|
+
window_->constrainContentSize(size.getWidth(), size.getHeight());
|
|
1661
|
+
resizing_ = false;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
void Plugin::tellWindowEditorLimits()
|
|
1665
|
+
{
|
|
1666
|
+
// VST3 has no call for these: the editor's answers for sizes it cannot
|
|
1667
|
+
// be.
|
|
1668
|
+
resizing_ = true;
|
|
1669
|
+
auto answer = [this](int width, int height)
|
|
1670
|
+
{
|
|
1671
|
+
ViewRect size(0, 0, width, height);
|
|
1672
|
+
view_->checkSizeConstraint(&size);
|
|
1673
|
+
return std::pair { std::max(size.getWidth(), 1), std::max(size.getHeight(), 1) };
|
|
1674
|
+
};
|
|
1675
|
+
const auto smallest = answer(1, 1);
|
|
1676
|
+
const auto largest = answer(1 << 20, 1 << 20);
|
|
1677
|
+
resizing_ = false;
|
|
1678
|
+
window_->setEditorMinimum(smallest.first, smallest.second);
|
|
1679
|
+
window_->setEditorMaximum(largest.first, largest.second);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
void Plugin::keepToRoom(ViewRect& size)
|
|
1683
|
+
{
|
|
1684
|
+
const auto room = window_->editorRoom();
|
|
1685
|
+
if (! room)
|
|
1686
|
+
return;
|
|
1687
|
+
if (size.getWidth() <= room->first && size.getHeight() <= room->second)
|
|
1688
|
+
return;
|
|
1689
|
+
auto fits = [&room](const ViewRect& rect)
|
|
1690
|
+
{ return rect.getWidth() <= room->first && rect.getHeight() <= room->second; };
|
|
1691
|
+
// Each side on its own, if the editor's shape is free.
|
|
1692
|
+
const int width = size.getWidth();
|
|
1693
|
+
const int height = size.getHeight();
|
|
1694
|
+
ViewRect sides(0, 0, std::min(width, room->first), std::min(height, room->second));
|
|
1695
|
+
view_->checkSizeConstraint(&sides);
|
|
1696
|
+
if (fits(sides))
|
|
1697
|
+
{
|
|
1698
|
+
size = sides;
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
// An editor that keeps its aspect ratio asked for more: its shape.
|
|
1702
|
+
const double fit =
|
|
1703
|
+
std::min(static_cast<double>(room->first) / width, static_cast<double>(room->second) / height);
|
|
1704
|
+
ViewRect shaped(0, 0, static_cast<int>(width * fit), static_cast<int>(height * fit));
|
|
1705
|
+
view_->checkSizeConstraint(&shaped);
|
|
1706
|
+
size = shaped;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
Json Plugin::requestEditorSize(int width, int height)
|
|
1710
|
+
{
|
|
1711
|
+
if (view_ == nullptr || view_->canResize() != kResultTrue || resizing_)
|
|
1712
|
+
return editorInfo();
|
|
1713
|
+
resizing_ = true;
|
|
1714
|
+
ViewRect size(0, 0, std::max(width, 1), std::max(height, 1));
|
|
1715
|
+
view_->checkSizeConstraint(&size);
|
|
1716
|
+
keepToRoom(size);
|
|
1717
|
+
window_->constrainContentSize(size.getWidth(), size.getHeight());
|
|
1718
|
+
view_->onSize(&size);
|
|
1719
|
+
resizing_ = false;
|
|
1720
|
+
return editorInfo();
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
Json Plugin::resizeEditor(int width, int height)
|
|
1724
|
+
{
|
|
1725
|
+
if (view_ == nullptr)
|
|
1726
|
+
throw Failure("invalid-state", "no editor is open");
|
|
1727
|
+
if (view_->canResize() != kResultTrue)
|
|
1728
|
+
throw Failure("invalid-state", "\"" + class_.name() + "\"'s editor cannot be resized");
|
|
1729
|
+
if (width <= 0 || height <= 0)
|
|
1730
|
+
throw Failure("invalid-request", "an editor size is positive");
|
|
1731
|
+
resizing_ = true;
|
|
1732
|
+
ViewRect size(0, 0, width, height);
|
|
1733
|
+
view_->checkSizeConstraint(&size);
|
|
1734
|
+
window_->setContentSize(size.getWidth(), size.getHeight());
|
|
1735
|
+
view_->onSize(&size);
|
|
1736
|
+
resizing_ = false;
|
|
1737
|
+
return editorInfo();
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// ── Live audio and the host interface ────────────────────────────────
|
|
1741
|
+
|
|
1742
|
+
int32 Plugin::mainInputChannels() const
|
|
1743
|
+
{
|
|
1744
|
+
for (const auto& bus : audioInputs_)
|
|
1745
|
+
if (bus.type == kMain)
|
|
1746
|
+
return bus.active ? bus.channels : 0;
|
|
1747
|
+
return 0;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
int32 Plugin::mainOutputChannels() const
|
|
1751
|
+
{
|
|
1752
|
+
for (const auto& bus : audioOutputs_)
|
|
1753
|
+
if (bus.type == kMain)
|
|
1754
|
+
return bus.active ? bus.channels : 0;
|
|
1755
|
+
return 0;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
std::vector<int32> Plugin::inputBusChannels() const
|
|
1759
|
+
{
|
|
1760
|
+
std::vector<int32> result;
|
|
1761
|
+
result.reserve(audioInputs_.size());
|
|
1762
|
+
for (const auto& bus : audioInputs_)
|
|
1763
|
+
result.push_back(bus.channels);
|
|
1764
|
+
return result;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
std::vector<int32> Plugin::outputBusChannels() const
|
|
1768
|
+
{
|
|
1769
|
+
std::vector<int32> result;
|
|
1770
|
+
result.reserve(audioOutputs_.size());
|
|
1771
|
+
for (const auto& bus : audioOutputs_)
|
|
1772
|
+
result.push_back(bus.channels);
|
|
1773
|
+
return result;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
uint32 Plugin::latencySamples() const
|
|
1777
|
+
{
|
|
1778
|
+
return processor_ != nullptr ? processor_->getLatencySamples() : 0;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
std::vector<HostParameter> Plugin::hostParameters() const
|
|
1782
|
+
{
|
|
1783
|
+
std::vector<HostParameter> result;
|
|
1784
|
+
if (controller_ == nullptr)
|
|
1785
|
+
return result;
|
|
1786
|
+
const int32 count = controller_->getParameterCount();
|
|
1787
|
+
for (int32 index = 0; index < count; ++index)
|
|
1788
|
+
{
|
|
1789
|
+
ParameterInfo info {};
|
|
1790
|
+
if (controller_->getParameterInfo(index, info) != kResultOk || (info.flags & ParameterInfo::kIsHidden) != 0)
|
|
1791
|
+
continue;
|
|
1792
|
+
HostParameter parameter;
|
|
1793
|
+
parameter.id = info.id;
|
|
1794
|
+
parameter.title = Steinberg::Vst::StringConvert::convert(info.title);
|
|
1795
|
+
parameter.units = Steinberg::Vst::StringConvert::convert(info.units);
|
|
1796
|
+
parameter.stepCount = info.stepCount;
|
|
1797
|
+
parameter.defaultValue = info.defaultNormalizedValue;
|
|
1798
|
+
parameter.readOnly = (info.flags & ParameterInfo::kIsReadOnly) != 0;
|
|
1799
|
+
parameter.list = (info.flags & ParameterInfo::kIsList) != 0;
|
|
1800
|
+
parameter.bypass = (info.flags & ParameterInfo::kIsBypass) != 0;
|
|
1801
|
+
result.push_back(std::move(parameter));
|
|
1802
|
+
}
|
|
1803
|
+
return result;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
double Plugin::normalizedValue(ParamID id) const
|
|
1807
|
+
{
|
|
1808
|
+
return controller_ != nullptr ? controller_->getParamNormalized(id) : 0;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
std::string Plugin::valueText(ParamID id, double normalized) const
|
|
1812
|
+
{
|
|
1813
|
+
if (controller_ == nullptr)
|
|
1814
|
+
return {};
|
|
1815
|
+
String128 display {};
|
|
1816
|
+
if (controller_->getParamStringByValue(id, normalized, display) != kResultOk)
|
|
1817
|
+
{
|
|
1818
|
+
std::ostringstream text;
|
|
1819
|
+
text.precision(3);
|
|
1820
|
+
text << controller_->normalizedParamToPlain(id, normalized);
|
|
1821
|
+
return text.str();
|
|
1822
|
+
}
|
|
1823
|
+
return Steinberg::Vst::StringConvert::convert(display);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
void Plugin::editParameter(ParamID id, double normalized)
|
|
1827
|
+
{
|
|
1828
|
+
if (controller_ == nullptr || ! std::isfinite(normalized))
|
|
1829
|
+
return;
|
|
1830
|
+
normalized = std::clamp(normalized, 0.0, 1.0);
|
|
1831
|
+
controller_->setParamNormalized(id, normalized);
|
|
1832
|
+
const std::scoped_lock lock(pendingMutex_);
|
|
1833
|
+
pending_.push_back({ id, normalized, 0 });
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
void Plugin::queueEvent(const Event& event)
|
|
1837
|
+
{
|
|
1838
|
+
const std::scoped_lock lock(pendingMutex_);
|
|
1839
|
+
pendingEvents_.push_back(event);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
Transport Plugin::transport() const
|
|
1843
|
+
{
|
|
1844
|
+
const std::scoped_lock lock(transportMutex_);
|
|
1845
|
+
return transport_;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
void Plugin::editTransport(const std::function<void(Transport&)>& edit)
|
|
1849
|
+
{
|
|
1850
|
+
const std::scoped_lock lock(transportMutex_);
|
|
1851
|
+
edit(transport_);
|
|
1852
|
+
++transportEdits_;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
void Plugin::applyOutputChanges(const std::vector<ParameterChange>& changes)
|
|
1856
|
+
{
|
|
1857
|
+
if (controller_ == nullptr)
|
|
1858
|
+
return;
|
|
1859
|
+
std::map<ParamID, double> last;
|
|
1860
|
+
for (const auto& change : changes)
|
|
1861
|
+
last[change.id] = change.value;
|
|
1862
|
+
for (const auto& [id, value] : last)
|
|
1863
|
+
controller_->setParamNormalized(id, value);
|
|
1864
|
+
}
|
|
1865
|
+
} // namespace soundor::vst3host
|