iPlug2 - C++ Audio Plug-in Framework
Loading...
Searching...
No Matches
IPlugWasmDSP.cpp
1/*
2 ==============================================================================
3
4 This file is part of the iPlug 2 library. Copyright (C) the iPlug 2 developers.
5
6 See LICENSE.txt for more info.
7
8 ==============================================================================
9*/
10
11#include "IPlugWasmDSP.h"
12
13#include <atomic>
14#include <unordered_map>
15#include <emscripten.h>
16#include <emscripten/bind.h>
17
18using namespace iplug;
19using namespace emscripten;
20
21// Instance registry for multi-instance support
22// Each AudioWorkletProcessor gets its own IPlugWasmDSP instance
23static std::unordered_map<int, IPlugWasmDSP*> sInstances;
24static std::atomic<int> sNextInstanceId{1};
25
26// Helper to get instance by ID
27static IPlugWasmDSP* GetInstance(int instanceId)
28{
29 auto it = sInstances.find(instanceId);
30 return (it != sInstances.end()) ? it->second : nullptr;
31}
32
33IPlugWasmDSP::IPlugWasmDSP(const InstanceInfo& info, const Config& config)
34: IPlugAPIBase(config, kAPIWAM) // Reuse WAM API type for compatibility
35, IPlugProcessor(config, kAPIWAM)
36, mInstanceId(0)
37{
38 int nInputs = MaxNChannels(ERoute::kInput);
39 int nOutputs = MaxNChannels(ERoute::kOutput);
40
41 SetChannelConnections(ERoute::kInput, 0, nInputs, true);
42 SetChannelConnections(ERoute::kOutput, 0, nOutputs, true);
43}
44
45IPlugWasmDSP::~IPlugWasmDSP()
46{
47 // Remove from registry if registered
48 if (mInstanceId != 0)
49 {
50 sInstances.erase(mInstanceId);
51 }
52}
53
54void IPlugWasmDSP::SetInstanceId(int instanceId)
55{
56 mInstanceId = instanceId;
57 if (instanceId != 0)
58 {
59 sInstances[instanceId] = this;
60 }
61}
62
63void IPlugWasmDSP::Init(int sampleRate, int blockSize)
64{
65 DBGMSG("IPlugWasmDSP::Init(%d, %d) instance=%d\n", sampleRate, blockSize, mInstanceId);
66
67 SetSampleRate(sampleRate);
68 SetBlockSize(blockSize);
69
70 OnParamReset(kReset);
71 OnReset();
72}
73
74void IPlugWasmDSP::ProcessBlock(sample** inputs, sample** outputs, int nFrames)
75{
76 SetChannelConnections(ERoute::kInput, 0, MaxNChannels(ERoute::kInput), !IsInstrument());
77 SetChannelConnections(ERoute::kOutput, 0, MaxNChannels(ERoute::kOutput), true);
78 AttachBuffers(ERoute::kInput, 0, NChannelsConnected(ERoute::kInput), inputs, nFrames);
79 AttachBuffers(ERoute::kOutput, 0, NChannelsConnected(ERoute::kOutput), outputs, nFrames);
80
81 // ENTER_PARAMS_MUTEX/LEAVE_PARAMS_MUTEX: In single-threaded Emscripten builds
82 // (without pthreads), these are no-ops. With pthreads enabled, they protect
83 // parameter access during ProcessBuffers. The AudioWorklet runs on a separate
84 // thread from the main thread, but parameter messages arrive via postMessage
85 // which is serialized, so the mutex primarily guards against concurrent
86 // parameter changes from within ProcessBuffers itself (e.g., meta-parameters).
87 ENTER_PARAMS_MUTEX
88 ProcessBuffers(0.0f, nFrames);
89 LEAVE_PARAMS_MUTEX
90}
91
93{
94 // Flush queued parameter changes from DSP to UI
95 while (mParamChangeFromProcessor.ElementsAvailable())
96 {
97 ParamTuple p;
98 mParamChangeFromProcessor.Pop(p);
99 SendParameterValueFromDelegate(p.idx, p.value, false);
100 }
101
102 // Flush queued MIDI messages from DSP to UI
103 while (mMidiMsgsFromProcessor.ElementsAvailable())
104 {
105 IMidiMsg msg;
106 mMidiMsgsFromProcessor.Pop(msg);
107 SendMidiMsgFromDelegate(msg);
108 }
109
110 OnIdle();
111}
112
113void IPlugWasmDSP::OnParamMessage(int paramIdx, double value)
114{
115 ENTER_PARAMS_MUTEX
116 SetParameterValue(paramIdx, value);
117 LEAVE_PARAMS_MUTEX
118}
119
120void IPlugWasmDSP::OnMidiMessage(int status, int data1, int data2)
121{
122 IMidiMsg msg = {0, (uint8_t)status, (uint8_t)data1, (uint8_t)data2};
123 ProcessMidiMsg(msg);
124}
125
126void IPlugWasmDSP::OnSysexMessage(const uint8_t* pData, int size)
127{
128 ISysEx sysex = {0, pData, size};
129 ProcessSysEx(sysex);
130}
131
132void IPlugWasmDSP::OnArbitraryMessage(int msgTag, int ctrlTag, int dataSize, const void* pData)
133{
134 OnMessage(msgTag, ctrlTag, dataSize, pData);
135}
136
138{
139 // Queue MIDI message to be sent to UI on idle tick
140 mMidiMsgsFromProcessor.Push(msg);
141 return true;
142}
143
145{
146 // Post SysEx to UI via instance-specific port
147 EM_ASM({
148 var instances = Module._instancePorts;
149 if (instances && instances[$0]) {
150 var data = new Uint8Array($2);
151 data.set(HEAPU8.subarray($1, $1 + $2));
152 instances[$0].postMessage({
153 verb: 'SSMFD',
154 data: data.buffer
155 });
156 }
157 }, mInstanceId, reinterpret_cast<intptr_t>(msg.mData), msg.mSize);
158 return true;
159}
160
161void IPlugWasmDSP::SendControlValueFromDelegate(int ctrlTag, double normalizedValue)
162{
163 // Try SAB first for low-latency visualization data
164 bool usedSAB = EM_ASM_INT({
165 var processors = Module._instanceProcessors;
166 if (processors && processors[$0] && processors[$0].sabBuffer) {
167 var proc = processors[$0];
168 // Pack value as float
169 var ptr = Module._malloc(4);
170 Module.HEAPF32[ptr >> 2] = $2;
171 var result = proc._writeSABMessage(0, $1, 0, ptr, 4);
172 Module._free(ptr);
173 return result ? 1 : 0;
174 }
175 return 0;
176 }, mInstanceId, ctrlTag, normalizedValue);
177
178 // Fallback to postMessage
179 if (!usedSAB) {
180 EM_ASM({
181 var instances = Module._instancePorts;
182 if (instances && instances[$0]) {
183 instances[$0].postMessage({
184 verb: 'SCVFD',
185 ctrlTag: $1,
186 value: $2
187 });
188 }
189 }, mInstanceId, ctrlTag, normalizedValue);
190 }
191}
192
193void IPlugWasmDSP::SendControlMsgFromDelegate(int ctrlTag, int msgTag, int dataSize, const void* pData)
194{
195 // Try SAB first for low-latency visualization data
196 bool usedSAB = false;
197 if (dataSize > 0 && pData)
198 {
199 usedSAB = EM_ASM_INT({
200 var processors = Module._instanceProcessors;
201 if (processors && processors[$0] && processors[$0].sabBuffer) {
202 return processors[$0]._writeSABMessage(1, $1, $2, $3, $4) ? 1 : 0;
203 }
204 return 0;
205 }, mInstanceId, ctrlTag, msgTag, reinterpret_cast<intptr_t>(pData), dataSize);
206 }
207
208 // Fallback to postMessage
209 if (!usedSAB)
210 {
211 if (dataSize > 0 && pData)
212 {
213 EM_ASM({
214 var instances = Module._instancePorts;
215 if (instances && instances[$0]) {
216 var data = new Uint8Array($4);
217 data.set(HEAPU8.subarray($3, $3 + $4));
218 instances[$0].postMessage({
219 verb: 'SCMFD',
220 ctrlTag: $1,
221 msgTag: $2,
222 data: data.buffer
223 });
224 }
225 }, mInstanceId, ctrlTag, msgTag, reinterpret_cast<intptr_t>(pData), dataSize);
226 }
227 else
228 {
229 EM_ASM({
230 var instances = Module._instancePorts;
231 if (instances && instances[$0]) {
232 instances[$0].postMessage({
233 verb: 'SCMFD',
234 ctrlTag: $1,
235 msgTag: $2,
236 data: null
237 });
238 }
239 }, mInstanceId, ctrlTag, msgTag);
240 }
241 }
242}
243
244void IPlugWasmDSP::SendParameterValueFromDelegate(int paramIdx, double value, bool normalized)
245{
246 EM_ASM({
247 var instances = Module._instancePorts;
248 if (instances && instances[$0]) {
249 instances[$0].postMessage({
250 verb: 'SPVFD',
251 paramIdx: $1,
252 value: $2
253 });
254 }
255 }, mInstanceId, paramIdx, value);
256}
257
258void IPlugWasmDSP::SendMidiMsgFromDelegate(const IMidiMsg& msg)
259{
260 EM_ASM({
261 var instances = Module._instancePorts;
262 if (instances && instances[$0]) {
263 instances[$0].postMessage({
264 verb: 'SMMFD',
265 status: $1,
266 data1: $2,
267 data2: $3
268 });
269 }
270 }, mInstanceId, msg.mStatus, msg.mData1, msg.mData2);
271}
272
273void IPlugWasmDSP::SendArbitraryMsgFromDelegate(int msgTag, int dataSize, const void* pData)
274{
275 // Try SAB first for low-latency visualization data
276 bool usedSAB = false;
277 if (dataSize > 0 && pData)
278 {
279 usedSAB = EM_ASM_INT({
280 var processors = Module._instanceProcessors;
281 if (processors && processors[$0] && processors[$0].sabBuffer) {
282 return processors[$0]._writeSABMessage(2, 0, $1, $2, $3) ? 1 : 0;
283 }
284 return 0;
285 }, mInstanceId, msgTag, reinterpret_cast<intptr_t>(pData), dataSize);
286 }
287
288 // Fallback to postMessage
289 if (!usedSAB)
290 {
291 if (dataSize > 0 && pData)
292 {
293 EM_ASM({
294 var instances = Module._instancePorts;
295 if (instances && instances[$0]) {
296 var data = new Uint8Array($3);
297 data.set(HEAPU8.subarray($2, $2 + $3));
298 instances[$0].postMessage({
299 verb: 'SAMFD',
300 msgTag: $1,
301 data: data.buffer
302 });
303 }
304 }, mInstanceId, msgTag, reinterpret_cast<intptr_t>(pData), dataSize);
305 }
306 else
307 {
308 EM_ASM({
309 var instances = Module._instancePorts;
310 if (instances && instances[$0]) {
311 instances[$0].postMessage({
312 verb: 'SAMFD',
313 msgTag: $1,
314 data: null
315 });
316 }
317 }, mInstanceId, msgTag);
318 }
319 }
320}
321
322// Forward declaration for MakePlug
323BEGIN_IPLUG_NAMESPACE
324extern IPlugWasmDSP* MakePlug(const InstanceInfo& info);
325END_IPLUG_NAMESPACE
326
327// Static wrapper functions for Emscripten bindings
328// All functions now take instanceId as first parameter for multi-instance support
329
331static int _createInstance()
332{
333 IPlugWasmDSP* pInstance = iplug::MakePlug(iplug::InstanceInfo());
334 if (!pInstance) return 0;
335
336 int instanceId = sNextInstanceId.fetch_add(1);
337 pInstance->SetInstanceId(instanceId);
338
339 // Initialize port/processor registries if needed
340 EM_ASM({
341 if (!Module._instancePorts) Module._instancePorts = {};
342 if (!Module._instanceProcessors) Module._instanceProcessors = {};
343 });
344
345 return instanceId;
346}
347
349static void _destroyInstance(int instanceId)
350{
351 IPlugWasmDSP* pInstance = GetInstance(instanceId);
352 if (pInstance)
353 {
354 // Clean up JS references
355 EM_ASM({
356 if (Module._instancePorts) delete Module._instancePorts[$0];
357 if (Module._instanceProcessors) delete Module._instanceProcessors[$0];
358 }, instanceId);
359
360 delete pInstance;
361 }
362}
363
364static void _init(int instanceId, int sampleRate, int blockSize)
365{
366 IPlugWasmDSP* pInstance = GetInstance(instanceId);
367 if (pInstance) pInstance->Init(sampleRate, blockSize);
368}
369
370static void _processBlock(int instanceId, uintptr_t inputPtrs, uintptr_t outputPtrs, int nFrames)
371{
372 IPlugWasmDSP* pInstance = GetInstance(instanceId);
373 if (pInstance)
374 {
375 sample** inputs = reinterpret_cast<sample**>(inputPtrs);
376 sample** outputs = reinterpret_cast<sample**>(outputPtrs);
377 pInstance->ProcessBlock(inputs, outputs, nFrames);
378 }
379}
380
381static void _onParam(int instanceId, int paramIdx, double value)
382{
383 IPlugWasmDSP* pInstance = GetInstance(instanceId);
384 if (pInstance) pInstance->OnParamMessage(paramIdx, value);
385}
386
387static void _onMidi(int instanceId, int status, int data1, int data2)
388{
389 IPlugWasmDSP* pInstance = GetInstance(instanceId);
390 if (pInstance) pInstance->OnMidiMessage(status, data1, data2);
391}
392
393static void _onSysex(int instanceId, uintptr_t pData, int size)
394{
395 IPlugWasmDSP* pInstance = GetInstance(instanceId);
396 if (pInstance)
397 {
398 const uint8_t* pDataPtr = reinterpret_cast<const uint8_t*>(pData);
399 pInstance->OnSysexMessage(pDataPtr, size);
400 }
401}
402
403static void _onArbitraryMsg(int instanceId, int msgTag, int ctrlTag, int dataSize, uintptr_t pData)
404{
405 IPlugWasmDSP* pInstance = GetInstance(instanceId);
406 if (pInstance)
407 {
408 const void* pDataPtr = reinterpret_cast<const void*>(pData);
409 pInstance->OnArbitraryMessage(msgTag, ctrlTag, dataSize, pDataPtr);
410 }
411}
412
413static void _onIdleTick(int instanceId)
414{
415 IPlugWasmDSP* pInstance = GetInstance(instanceId);
416 if (pInstance) pInstance->OnIdleTick();
417}
418
419static int _getNumInputChannels(int instanceId)
420{
421 IPlugWasmDSP* pInstance = GetInstance(instanceId);
422 return pInstance ? pInstance->GetNumInputChannels() : 0;
423}
424
425static int _getNumOutputChannels(int instanceId)
426{
427 IPlugWasmDSP* pInstance = GetInstance(instanceId);
428 return pInstance ? pInstance->GetNumOutputChannels() : 0;
429}
430
431static bool _isInstrument(int instanceId)
432{
433 IPlugWasmDSP* pInstance = GetInstance(instanceId);
434 return pInstance ? pInstance->IsPlugInstrument() : false;
435}
436
437static int _getNumParams(int instanceId)
438{
439 IPlugWasmDSP* pInstance = GetInstance(instanceId);
440 return pInstance ? pInstance->NParams() : 0;
441}
442
443static double _getParamDefault(int instanceId, int paramIdx)
444{
445 IPlugWasmDSP* pInstance = GetInstance(instanceId);
446 if (pInstance && paramIdx >= 0 && paramIdx < pInstance->NParams())
447 return pInstance->GetParam(paramIdx)->GetDefault(true);
448 return 0.0;
449}
450
451static std::string _getParamName(int instanceId, int paramIdx)
452{
453 IPlugWasmDSP* pInstance = GetInstance(instanceId);
454 if (pInstance && paramIdx >= 0 && paramIdx < pInstance->NParams())
455 return pInstance->GetParam(paramIdx)->GetName();
456 return "";
457}
458
459static std::string _getParamLabel(int instanceId, int paramIdx)
460{
461 IPlugWasmDSP* pInstance = GetInstance(instanceId);
462 if (pInstance && paramIdx >= 0 && paramIdx < pInstance->NParams())
463 return pInstance->GetParam(paramIdx)->GetLabel();
464 return "";
465}
466
467static double _getParamMin(int instanceId, int paramIdx)
468{
469 IPlugWasmDSP* pInstance = GetInstance(instanceId);
470 if (pInstance && paramIdx >= 0 && paramIdx < pInstance->NParams())
471 return pInstance->GetParam(paramIdx)->GetMin();
472 return 0.0;
473}
474
475static double _getParamMax(int instanceId, int paramIdx)
476{
477 IPlugWasmDSP* pInstance = GetInstance(instanceId);
478 if (pInstance && paramIdx >= 0 && paramIdx < pInstance->NParams())
479 return pInstance->GetParam(paramIdx)->GetMax();
480 return 1.0;
481}
482
483static double _getParamStep(int instanceId, int paramIdx)
484{
485 IPlugWasmDSP* pInstance = GetInstance(instanceId);
486 if (pInstance && paramIdx >= 0 && paramIdx < pInstance->NParams())
487 return pInstance->GetParam(paramIdx)->GetStep();
488 return 0.001;
489}
490
491static double _getParamValue(int instanceId, int paramIdx)
492{
493 IPlugWasmDSP* pInstance = GetInstance(instanceId);
494 if (pInstance && paramIdx >= 0 && paramIdx < pInstance->NParams())
495 return pInstance->GetParam(paramIdx)->Value();
496 return 0.0;
497}
498
499static std::string _getPluginName(int instanceId)
500{
501 IPlugWasmDSP* pInstance = GetInstance(instanceId);
502 return pInstance ? pInstance->GetPluginName() : "";
503}
504
505static std::string _getPluginInfoJSON(int instanceId)
506{
507 IPlugWasmDSP* pInstance = GetInstance(instanceId);
508 if (!pInstance) return "{}";
509
510 std::string json = "{";
511 json += "\"instanceId\":" + std::to_string(instanceId) + ",";
512 json += "\"name\":\"" + std::string(pInstance->GetPluginName()) + "\",";
513 json += "\"numInputChannels\":" + std::to_string(pInstance->GetNumInputChannels()) + ",";
514 json += "\"numOutputChannels\":" + std::to_string(pInstance->GetNumOutputChannels()) + ",";
515 json += "\"isInstrument\":" + std::string(pInstance->IsPlugInstrument() ? "true" : "false") + ",";
516 json += "\"params\":[";
517
518 int nParams = pInstance->NParams();
519 for (int i = 0; i < nParams; i++)
520 {
521 IParam* pParam = pInstance->GetParam(i);
522 if (i > 0) json += ",";
523 json += "{";
524 json += "\"idx\":" + std::to_string(i) + ",";
525 json += "\"name\":\"" + std::string(pParam->GetName()) + "\",";
526 json += "\"label\":\"" + std::string(pParam->GetLabel()) + "\",";
527 json += "\"min\":" + std::to_string(pParam->GetMin()) + ",";
528 json += "\"max\":" + std::to_string(pParam->GetMax()) + ",";
529 json += "\"default\":" + std::to_string(pParam->GetDefault()) + ",";
530 json += "\"step\":" + std::to_string(pParam->GetStep()) + ",";
531 json += "\"value\":" + std::to_string(pParam->Value());
532 json += "}";
533 }
534
535 json += "]}";
536 return json;
537}
538
539EMSCRIPTEN_BINDINGS(IPlugWasmDSP) {
540 function("createInstance", &_createInstance);
541 function("destroyInstance", &_destroyInstance);
542 function("init", &_init);
543 function("processBlock", &_processBlock);
544 function("onParam", &_onParam);
545 function("onMidi", &_onMidi);
546 function("onSysex", &_onSysex);
547 function("onArbitraryMsg", &_onArbitraryMsg);
548 function("onIdleTick", &_onIdleTick);
549 function("getNumInputChannels", &_getNumInputChannels);
550 function("getNumOutputChannels", &_getNumOutputChannels);
551 function("isInstrument", &_isInstrument);
552 function("getNumParams", &_getNumParams);
553 function("getParamDefault", &_getParamDefault);
554 function("getParamName", &_getParamName);
555 function("getParamLabel", &_getParamLabel);
556 function("getParamMin", &_getParamMin);
557 function("getParamMax", &_getParamMax);
558 function("getParamStep", &_getParamStep);
559 function("getParamValue", &_getParamValue);
560 function("getPluginName", &_getPluginName);
561 function("getPluginInfoJSON", &_getPluginInfoJSON);
562}
IPlug's parameter class.
double GetStep() const
Returns the parameter's step size.
double GetDefault(bool normalized=false) const
Returns the parameter's default value.
double GetMin() const
Returns the parameter's minimum value.
const char * GetLabel() const
Returns the parameter's label.
const char * GetName() const
Returns the parameter's name.
double Value() const
Gets a readable value of the parameter.
double GetMax() const
Returns the parameter's maximum value.
The base class of an IPlug plug-in, which interacts with the different plug-in APIs.
Definition: IPlugAPIBase.h:43
virtual void OnIdle()
Override this method to get an "idle"" call on the main thread.
Definition: IPlugAPIBase.h:111
void SetParameterValue(int paramIdx, double normalizedValue)
SetParameterValue is called from the UI in the middle of a parameter change gesture (possibly via del...
The base class for IPlug Audio Processing.
virtual void ProcessMidiMsg(const IMidiMsg &msg)
Override this method to handle incoming MIDI messages.
bool IsInstrument() const
int NChannelsConnected(ERoute direction) const
virtual void ProcessSysEx(const ISysEx &msg)
Override this method to handle incoming MIDI System Exclusive (SysEx) messages.
int MaxNChannels(ERoute direction) const
virtual void OnReset()
Override this method in your plug-in class to do something prior to playback etc.
Hybrid DSP class - AudioWorklet processor for split DSP/UI builds.
Definition: IPlugWasmDSP.h:35
void Init(int sampleRate, int blockSize)
Initialize the DSP processor.
bool SendSysEx(const ISysEx &msg) override
Send a single MIDI System Exclusive (SysEx) message // TODO: info about what thread should this be ca...
void SetInstanceId(int instanceId)
Set the instance ID (called after construction by createInstance binding)
int GetNumOutputChannels() const
Get the number of output channels.
Definition: IPlugWasmDSP.h:82
void OnIdleTick()
Called on idle tick to flush queued messages to UI.
bool SendMidiMsg(const IMidiMsg &msg) override
Send a single MIDI message // TODO: info about what thread should this be called on or not called on!
int GetNumInputChannels() const
Get the number of input channels.
Definition: IPlugWasmDSP.h:79
bool IsPlugInstrument() const
Check if plugin is an instrument (synth)
Definition: IPlugWasmDSP.h:85
void ProcessBlock(sample **inputs, sample **outputs, int nFrames) override
Process a block of audio.
const char * GetPluginName() const
Encapsulates a MIDI message and provides helper functions.
Definition: IPlugMidi.h:31
A struct for dealing with SysEx messages.
Definition: IPlugMidi.h:539
In certain cases we need to queue parameter changes for transferral between threads.
Definition: IPlugStructs.h:33