comparison DPF-Prymula-audioplugins/dpf/distrho/src/jackbridge/WebBridge.hpp @ 3:84e66ea83026

DPF-Prymula-audioplugins-0.231015-2
author prymula <prymula76@outlook.com>
date Mon, 16 Oct 2023 21:53:34 +0200
parents
children
comparison
equal deleted inserted replaced
2:cf2cb71d31dd 3:84e66ea83026
1 /*
2 * Web Audio + MIDI Bridge for DPF
3 * Copyright (C) 2021-2022 Filipe Coelho <falktx@falktx.com>
4 *
5 * Permission to use, copy, modify, and/or distribute this software for any purpose with
6 * or without fee is hereby granted, provided that the above copyright notice and this
7 * permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
10 * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
11 * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
12 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
13 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
14 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17 #ifndef WEB_BRIDGE_HPP_INCLUDED
18 #define WEB_BRIDGE_HPP_INCLUDED
19
20 #include "NativeBridge.hpp"
21
22 #include <emscripten/emscripten.h>
23
24 struct WebBridge : NativeBridge {
25 #if DISTRHO_PLUGIN_NUM_INPUTS > 0
26 bool captureAvailable = false;
27 #endif
28 #if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
29 bool playbackAvailable = false;
30 #endif
31 bool active = false;
32 double timestamp = 0;
33
34 WebBridge()
35 {
36 #if DISTRHO_PLUGIN_NUM_INPUTS > 0
37 captureAvailable = EM_ASM_INT({
38 if (typeof(navigator.mediaDevices) !== 'undefined' && typeof(navigator.mediaDevices.getUserMedia) !== 'undefined')
39 return 1;
40 if (typeof(navigator.webkitGetUserMedia) !== 'undefined')
41 return 1;
42 return false;
43 }) != 0;
44 #endif
45
46 #if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
47 playbackAvailable = EM_ASM_INT({
48 if (typeof(AudioContext) !== 'undefined')
49 return 1;
50 if (typeof(webkitAudioContext) !== 'undefined')
51 return 1;
52 return 0;
53 }) != 0;
54 #endif
55
56 #if DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
57 midiAvailable = EM_ASM_INT({
58 return typeof(navigator.requestMIDIAccess) === 'function' ? 1 : 0;
59 }) != 0;
60 #endif
61 }
62
63 bool open(const char*) override
64 {
65 // early bail out if required features are not supported
66 #if DISTRHO_PLUGIN_NUM_INPUTS > 0
67 if (!captureAvailable)
68 {
69 #if DISTRHO_PLUGIN_NUM_OUTPUTS == 0
70 d_stderr2("Audio capture is not supported");
71 return false;
72 #else
73 if (!playbackAvailable)
74 {
75 d_stderr2("Audio capture and playback are not supported");
76 return false;
77 }
78 d_stderr2("Audio capture is not supported, but can still use playback");
79 #endif
80 }
81 #endif
82
83 #if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
84 if (!playbackAvailable)
85 {
86 d_stderr2("Audio playback is not supported");
87 return false;
88 }
89 #endif
90
91 const bool initialized = EM_ASM_INT({
92 if (typeof(Module['WebAudioBridge']) === 'undefined') {
93 Module['WebAudioBridge'] = {};
94 }
95
96 var WAB = Module['WebAudioBridge'];
97 if (!WAB.audioContext) {
98 if (typeof(AudioContext) !== 'undefined') {
99 WAB.audioContext = new AudioContext();
100 } else if (typeof(webkitAudioContext) !== 'undefined') {
101 WAB.audioContext = new webkitAudioContext();
102 }
103 }
104
105 return WAB.audioContext === undefined ? 0 : 1;
106 }) != 0;
107
108 if (!initialized)
109 {
110 d_stderr2("Failed to initialize web audio");
111 return false;
112 }
113
114 bufferSize = EM_ASM_INT({
115 var WAB = Module['WebAudioBridge'];
116 return WAB['minimizeBufferSize'] ? 256 : 2048;
117 });
118 sampleRate = EM_ASM_INT_V({
119 var WAB = Module['WebAudioBridge'];
120 return WAB.audioContext.sampleRate;
121 });
122
123 allocBuffers(true, true);
124
125 EM_ASM({
126 var numInputs = $0;
127 var numOutputs = $1;
128 var bufferSize = $2;
129 var WAB = Module['WebAudioBridge'];
130
131 var realBufferSize = WAB['minimizeBufferSize'] ? 2048 : bufferSize;
132 var divider = realBufferSize / bufferSize;
133
134 // main processor
135 WAB.processor = WAB.audioContext['createScriptProcessor'](realBufferSize, numInputs, numOutputs);
136 WAB.processor['onaudioprocess'] = function (e) {
137 // var timestamp = performance.now();
138 for (var k = 0; k < divider; ++k) {
139 for (var i = 0; i < numInputs; ++i) {
140 var buffer = e['inputBuffer']['getChannelData'](i);
141 for (var j = 0; j < bufferSize; ++j) {
142 // setValue($3 + ((bufferSize * i) + j) * 4, buffer[j], 'float');
143 HEAPF32[$3 + (((bufferSize * i) + j) << 2) >> 2] = buffer[bufferSize * k + j];
144 }
145 }
146 dynCall('vi', $4, [$5]);
147 for (var i = 0; i < numOutputs; ++i) {
148 var buffer = e['outputBuffer']['getChannelData'](i);
149 var offset = bufferSize * (numInputs + i);
150 for (var j = 0; j < bufferSize; ++j) {
151 buffer[bufferSize * k + j] = HEAPF32[$3 + ((offset + j) << 2) >> 2];
152 }
153 }
154 }
155 };
156
157 // connect to output
158 WAB.processor['connect'](WAB.audioContext['destination']);
159
160 // resume/start playback on first click
161 document.addEventListener('click', function(e) {
162 var WAB = Module['WebAudioBridge'];
163 if (WAB.audioContext.state === 'suspended')
164 WAB.audioContext.resume();
165 });
166 }, DISTRHO_PLUGIN_NUM_INPUTS_2, DISTRHO_PLUGIN_NUM_OUTPUTS_2, bufferSize, audioBufferStorage, WebAudioCallback, this);
167
168 return true;
169 }
170
171 bool close() override
172 {
173 freeBuffers();
174 return true;
175 }
176
177 bool activate() override
178 {
179 active = true;
180 return true;
181 }
182
183 bool deactivate() override
184 {
185 active = false;
186 return true;
187 }
188
189 bool supportsAudioInput() const override
190 {
191 #if DISTRHO_PLUGIN_NUM_INPUTS > 0
192 return captureAvailable;
193 #else
194 return false;
195 #endif
196 }
197
198 bool isAudioInputEnabled() const override
199 {
200 #if DISTRHO_PLUGIN_NUM_INPUTS > 0
201 return EM_ASM_INT({ return Module['WebAudioBridge'].captureStreamNode ? 1 : 0 }) != 0;
202 #else
203 return false;
204 #endif
205 }
206
207 bool requestAudioInput() override
208 {
209 DISTRHO_SAFE_ASSERT_RETURN(DISTRHO_PLUGIN_NUM_INPUTS > 0, false);
210
211 EM_ASM({
212 var numInputs = $0;
213 var WAB = Module['WebAudioBridge'];
214
215 var constraints = {};
216 // we need to use this weird awkward way for objects, otherwise build fails
217 constraints['audio'] = true;
218 constraints['video'] = false;
219 constraints['autoGainControl'] = {};
220 constraints['autoGainControl']['ideal'] = false;
221 constraints['echoCancellation'] = {};
222 constraints['echoCancellation']['ideal'] = false;
223 constraints['noiseSuppression'] = {};
224 constraints['noiseSuppression']['ideal'] = false;
225 constraints['channelCount'] = {};
226 constraints['channelCount']['min'] = 0;
227 constraints['channelCount']['ideal'] = numInputs;
228 constraints['latency'] = {};
229 constraints['latency']['min'] = 0;
230 constraints['latency']['ideal'] = 0;
231 constraints['sampleSize'] = {};
232 constraints['sampleSize']['min'] = 8;
233 constraints['sampleSize']['max'] = 32;
234 constraints['sampleSize']['ideal'] = 16;
235 // old property for chrome
236 constraints['googAutoGainControl'] = false;
237
238 var success = function(stream) {
239 var track = stream.getAudioTracks()[0];
240
241 // try to force as much as we can
242 track.applyConstraints({'autoGainControl': { 'exact': false } })
243 .then(function(){console.log("Mic/Input auto-gain control has been disabled")})
244 .catch(function(){console.log("Cannot disable Mic/Input auto-gain")});
245
246 track.applyConstraints({'echoCancellation': { 'exact': false } })
247 .then(function(){console.log("Mic/Input echo-cancellation has been disabled")})
248 .catch(function(){console.log("Cannot disable Mic/Input echo-cancellation")});
249
250 track.applyConstraints({'noiseSuppression': { 'exact': false } })
251 .then(function(){console.log("Mic/Input noise-suppression has been disabled")})
252 .catch(function(){console.log("Cannot disable Mic/Input noise-suppression")});
253
254 track.applyConstraints({'googAutoGainControl': { 'exact': false } })
255 .then(function(){})
256 .catch(function(){});
257
258 WAB.captureStreamNode = WAB.audioContext['createMediaStreamSource'](stream);
259 WAB.captureStreamNode.connect(WAB.processor);
260 };
261 var fail = function() {
262 };
263
264 if (navigator.mediaDevices !== undefined && navigator.mediaDevices.getUserMedia !== undefined) {
265 navigator.mediaDevices.getUserMedia(constraints).then(success).catch(fail);
266 } else if (navigator.webkitGetUserMedia !== undefined) {
267 navigator.webkitGetUserMedia(constraints, success, fail);
268 }
269 }, DISTRHO_PLUGIN_NUM_INPUTS_2);
270
271 return true;
272 }
273
274 bool supportsBufferSizeChanges() const override
275 {
276 return true;
277 }
278
279 bool requestBufferSizeChange(const uint32_t newBufferSize) override
280 {
281 // try to create new processor first
282 bool success = EM_ASM_INT({
283 var numInputs = $0;
284 var numOutputs = $1;
285 var newBufferSize = $2;
286 var WAB = Module['WebAudioBridge'];
287
288 try {
289 WAB.newProcessor = WAB.audioContext['createScriptProcessor'](newBufferSize, numInputs, numOutputs);
290 } catch (e) {
291 return 0;
292 }
293
294 // got new processor, disconnect old one
295 WAB.processor['disconnect'](WAB.audioContext['destination']);
296
297 if (WAB.captureStreamNode)
298 WAB.captureStreamNode.disconnect(WAB.processor);
299
300 return 1;
301 }, DISTRHO_PLUGIN_NUM_INPUTS_2, DISTRHO_PLUGIN_NUM_OUTPUTS_2, newBufferSize) != 0;
302
303 if (!success)
304 return false;
305
306 bufferSize = newBufferSize;
307 freeBuffers();
308 allocBuffers(true, true);
309
310 if (bufferSizeCallback != nullptr)
311 bufferSizeCallback(newBufferSize, jackBufferSizeArg);
312
313 EM_ASM({
314 var numInputsR = $0;
315 var numInputs = $1;
316 var numOutputs = $2;
317 var bufferSize = $3;
318 var WAB = Module['WebAudioBridge'];
319
320 // store the new processor
321 delete WAB.processor;
322 WAB.processor = WAB.newProcessor;
323 delete WAB.newProcessor;
324
325 // setup new processor the same way as old one
326 WAB.processor['onaudioprocess'] = function (e) {
327 // var timestamp = performance.now();
328 for (var i = 0; i < numInputs; ++i) {
329 var buffer = e['inputBuffer']['getChannelData'](i);
330 for (var j = 0; j < bufferSize; ++j) {
331 // setValue($3 + ((bufferSize * i) + j) * 4, buffer[j], 'float');
332 HEAPF32[$4 + (((bufferSize * i) + j) << 2) >> 2] = buffer[j];
333 }
334 }
335 dynCall('vi', $5, [$6]);
336 for (var i = 0; i < numOutputs; ++i) {
337 var buffer = e['outputBuffer']['getChannelData'](i);
338 var offset = bufferSize * (numInputsR + i);
339 for (var j = 0; j < bufferSize; ++j) {
340 buffer[j] = HEAPF32[$3 + ((offset + j) << 2) >> 2];
341 }
342 }
343 };
344
345 // connect to output
346 WAB.processor['connect'](WAB.audioContext['destination']);
347
348 // and input, if available
349 if (WAB.captureStreamNode)
350 WAB.captureStreamNode.connect(WAB.processor);
351
352 }, DISTRHO_PLUGIN_NUM_INPUTS, DISTRHO_PLUGIN_NUM_INPUTS_2, DISTRHO_PLUGIN_NUM_OUTPUTS_2, bufferSize, audioBufferStorage, WebAudioCallback, this);
353
354 return true;
355 }
356
357 bool isMIDIEnabled() const override
358 {
359 #if DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
360 return EM_ASM_INT({ return Module['WebAudioBridge'].midi ? 1 : 0 }) != 0;
361 #else
362 return false;
363 #endif
364 }
365
366 bool requestMIDI() override
367 {
368 #if DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
369 if (midiAvailable)
370 {
371 EM_ASM({
372 var useInput = !!$0;
373 var useOutput = !!$1;
374 var maxSize = $2;
375 var WAB = Module['WebAudioBridge'];
376
377 var offset = Module._malloc(maxSize);
378
379 var inputCallback = function(event) {
380 if (event.data.length > maxSize)
381 return;
382 var buffer = new Uint8Array(Module.HEAPU8.buffer, offset, maxSize);
383 buffer.set(event.data);
384 dynCall('viiif', $3, [$4, buffer.byteOffset, event.data.length, event.timeStamp]);
385 };
386 var stateCallback = function(event) {
387 if (event.port.state === 'connected' && event.port.connection === 'open') {
388 if (useInput && event.port.type === 'input') {
389 if (event.port.name.indexOf('Midi Through') < 0)
390 event.port.onmidimessage = inputCallback;
391 } else if (useOutput && event.port.type === 'output') {
392 event.port.open();
393 }
394 }
395 };
396
397 var success = function(midi) {
398 WAB.midi = midi;
399 midi.onstatechange = stateCallback;
400 if (useInput) {
401 midi.inputs.forEach(function(port) {
402 if (port.name.indexOf('Midi Through') < 0)
403 port.onmidimessage = inputCallback;
404 });
405 }
406 if (useOutput) {
407 midi.outputs.forEach(function(port) {
408 port.open();
409 });
410 }
411 };
412 var fail = function(why) {
413 console.log("midi access failed:", why);
414 };
415
416 navigator.requestMIDIAccess().then(success, fail);
417 }, DISTRHO_PLUGIN_WANT_MIDI_INPUT, DISTRHO_PLUGIN_WANT_MIDI_OUTPUT, kMaxMIDIInputMessageSize, WebMIDICallback, this);
418
419 return true;
420 }
421 else
422 #endif
423 {
424 d_stderr2("MIDI is not supported");
425 return false;
426 }
427 }
428
429 static void WebAudioCallback(void* const userData /* , const double timestamp */)
430 {
431 WebBridge* const self = static_cast<WebBridge*>(userData);
432 // self->timestamp = timestamp;
433
434 const uint numFrames = self->bufferSize;
435
436 if (self->jackProcessCallback != nullptr && self->active)
437 {
438 self->jackProcessCallback(numFrames, self->jackProcessArg);
439
440 #if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
441 if (self->midiAvailable && self->midiOutBuffer.isDataAvailableForReading())
442 {
443 static_assert(kMaxMIDIInputMessageSize + 1u == 4, "change code if bumping this value");
444 uint32_t offset = 0;
445 uint8_t bytes[4] = {};
446 double timestamp = EM_ASM_DOUBLE({ return performance.now(); });
447
448 while (self->midiOutBuffer.isDataAvailableForReading() &&
449 self->midiOutBuffer.readCustomData(bytes, ARRAY_SIZE(bytes)))
450 {
451 offset = self->midiOutBuffer.readUInt();
452
453 EM_ASM({
454 var WAB = Module['WebAudioBridge'];
455 if (WAB.midi) {
456 var timestamp = $5 + $0;
457 var size = $1;
458 WAB.midi.outputs.forEach(function(port) {
459 if (port.state !== 'disconnected') {
460 port.send(size == 3 ? [ $2, $3, $4 ] :
461 size == 2 ? [ $2, $3 ] :
462 [ $2 ], timestamp);
463 }
464 });
465 }
466 }, offset, bytes[0], bytes[1], bytes[2], bytes[3], timestamp);
467 }
468
469 self->midiOutBuffer.clearData();
470 }
471 #endif
472 }
473 else
474 {
475 for (uint i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS_2; ++i)
476 std::memset(self->audioBuffers[DISTRHO_PLUGIN_NUM_INPUTS + i], 0, sizeof(float)*numFrames);
477 }
478 }
479
480 #if DISTRHO_PLUGIN_WANT_MIDI_INPUT
481 static void WebMIDICallback(void* const userData, uint8_t* const data, const int len, const double timestamp)
482 {
483 DISTRHO_SAFE_ASSERT_RETURN(len > 0 && len <= (int)kMaxMIDIInputMessageSize,);
484
485 WebBridge* const self = static_cast<WebBridge*>(userData);
486
487 // TODO timestamp handling
488 self->midiInBufferPending.writeByte(static_cast<uint8_t>(len));
489 self->midiInBufferPending.writeCustomData(data, kMaxMIDIInputMessageSize);
490 self->midiInBufferPending.commitWrite();
491 }
492 #endif
493 };
494
495 #endif // WEB_BRIDGE_HPP_INCLUDED