WebBridge.hpp (20426B)
1 /* 2 * Web Audio + MIDI Bridge for DPF 3 * Copyright (C) 2021-2023 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['fakeSmallBufferSize'] ? 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 numInputsR = $0; 127 var numInputs = $1; 128 var numOutputs = $2; 129 var bufferSize = $3; 130 var WAB = Module['WebAudioBridge']; 131 132 var realBufferSize = WAB['fakeSmallBufferSize'] ? 2048 : bufferSize; 133 var divider = realBufferSize / bufferSize; 134 135 // main processor 136 WAB.processor = WAB.audioContext['createScriptProcessor'](realBufferSize, numInputs, numOutputs); 137 WAB.processor['onaudioprocess'] = function (e) { 138 // var timestamp = performance.now(); 139 if (e['inputBuffer'].length != e['outputBuffer'].length || e['inputBuffer'].length != bufferSize) { 140 console.log("invalid buffer size!", e['inputBuffer'].length, e['inputBuffer'].length, bufferSize); 141 } 142 if (e['inputBuffer'].numberOfChannels != numInputs) { 143 console.log("invalid number of input channels!", e['inputBuffer'].numberOfChannels, numInputs); 144 } 145 if (e['outputBuffer'].numberOfChannels != numOutputs) { 146 console.log("invalid number of output channels!", e['outputBuffer'].numberOfChannels, numOutputs); 147 } 148 for (var k = 0; k < divider; ++k) { 149 for (var i = 0; i < numInputs; ++i) { 150 var buffer = e['inputBuffer']['getChannelData'](i); 151 for (var j = 0; j < bufferSize; ++j) { 152 // setValue($4 + ((bufferSize * i) + j) * 4, buffer[bufferSize * k + j], 'float'); 153 HEAPF32[$4 + (((bufferSize * i) + j) << 2) >> 2] = buffer[bufferSize * k + j]; 154 } 155 } 156 dynCall('vi', $5, [$6]); 157 for (var i = 0; i < numOutputs; ++i) { 158 var buffer = e['outputBuffer']['getChannelData'](i); 159 var offset = bufferSize * (numInputsR + i); 160 for (var j = 0; j < bufferSize; ++j) { 161 buffer[bufferSize * k + j] = HEAPF32[$4 + ((offset + j) << 2) >> 2]; 162 } 163 } 164 } 165 }; 166 167 // connect to output 168 WAB.processor['connect'](WAB.audioContext['destination']); 169 170 // resume/start playback on first click 171 document.addEventListener('click', function(e) { 172 var WAB = Module['WebAudioBridge']; 173 if (WAB.audioContext.state === 'suspended') 174 WAB.audioContext.resume(); 175 }); 176 }, DISTRHO_PLUGIN_NUM_INPUTS, DISTRHO_PLUGIN_NUM_INPUTS_2, DISTRHO_PLUGIN_NUM_OUTPUTS_2, bufferSize, audioBufferStorage, WebAudioCallback, this); 177 178 return true; 179 } 180 181 bool close() override 182 { 183 freeBuffers(); 184 return true; 185 } 186 187 bool activate() override 188 { 189 active = true; 190 return true; 191 } 192 193 bool deactivate() override 194 { 195 active = false; 196 return true; 197 } 198 199 bool supportsAudioInput() const override 200 { 201 #if DISTRHO_PLUGIN_NUM_INPUTS > 0 202 return captureAvailable; 203 #else 204 return false; 205 #endif 206 } 207 208 bool isAudioInputEnabled() const override 209 { 210 #if DISTRHO_PLUGIN_NUM_INPUTS > 0 211 return EM_ASM_INT({ return Module['WebAudioBridge'].captureStreamNode ? 1 : 0 }) != 0; 212 #else 213 return false; 214 #endif 215 } 216 217 bool requestAudioInput() override 218 { 219 DISTRHO_SAFE_ASSERT_RETURN(DISTRHO_PLUGIN_NUM_INPUTS > 0, false); 220 221 EM_ASM({ 222 var numInputs = $0; 223 var WAB = Module['WebAudioBridge']; 224 225 var constraints = {}; 226 // we need to use this weird awkward way for objects, otherwise build fails 227 constraints['audio'] = true; 228 constraints['video'] = false; 229 constraints['autoGainControl'] = {}; 230 constraints['autoGainControl']['ideal'] = false; 231 constraints['echoCancellation'] = {}; 232 constraints['echoCancellation']['ideal'] = false; 233 constraints['noiseSuppression'] = {}; 234 constraints['noiseSuppression']['ideal'] = false; 235 constraints['channelCount'] = {}; 236 constraints['channelCount']['min'] = 0; 237 constraints['channelCount']['ideal'] = numInputs; 238 constraints['latency'] = {}; 239 constraints['latency']['min'] = 0; 240 constraints['latency']['ideal'] = 0; 241 constraints['sampleSize'] = {}; 242 constraints['sampleSize']['min'] = 8; 243 constraints['sampleSize']['max'] = 32; 244 constraints['sampleSize']['ideal'] = 16; 245 // old property for chrome 246 constraints['googAutoGainControl'] = false; 247 248 var success = function(stream) { 249 var tracks = stream.getAudioTracks(); 250 251 // try to force as much as we can 252 for (var i in tracks) { 253 var track = tracks[i]; 254 255 track.applyConstraints({'autoGainControl': { 'exact': false } }) 256 .then(function(){console.log("Mic/Input auto-gain control has been disabled")}) 257 .catch(function(){console.log("Cannot disable Mic/Input auto-gain")}); 258 259 track.applyConstraints({'echoCancellation': { 'exact': false } }) 260 .then(function(){console.log("Mic/Input echo-cancellation has been disabled")}) 261 .catch(function(){console.log("Cannot disable Mic/Input echo-cancellation")}); 262 263 track.applyConstraints({'noiseSuppression': { 'exact': false } }) 264 .then(function(){console.log("Mic/Input noise-suppression has been disabled")}) 265 .catch(function(){console.log("Cannot disable Mic/Input noise-suppression")}); 266 267 track.applyConstraints({'googAutoGainControl': { 'exact': false } }) 268 .then(function(){}) 269 .catch(function(){}); 270 } 271 272 WAB.captureStreamNode = WAB.audioContext['createMediaStreamSource'](stream); 273 WAB.captureStreamNode.connect(WAB.processor); 274 }; 275 var fail = function() { 276 }; 277 278 if (navigator.mediaDevices !== undefined && navigator.mediaDevices.getUserMedia !== undefined) { 279 navigator.mediaDevices.getUserMedia(constraints).then(success).catch(fail); 280 } else if (navigator.webkitGetUserMedia !== undefined) { 281 navigator.webkitGetUserMedia(constraints, success, fail); 282 } 283 }, DISTRHO_PLUGIN_NUM_INPUTS_2); 284 285 return true; 286 } 287 288 bool supportsBufferSizeChanges() const override 289 { 290 return true; 291 } 292 293 bool requestBufferSizeChange(const uint32_t newBufferSize) override 294 { 295 // try to create new processor first 296 bool success = EM_ASM_INT({ 297 var numInputs = $0; 298 var numOutputs = $1; 299 var newBufferSize = $2; 300 var WAB = Module['WebAudioBridge']; 301 302 try { 303 WAB.newProcessor = WAB.audioContext['createScriptProcessor'](newBufferSize, numInputs, numOutputs); 304 } catch (e) { 305 return 0; 306 } 307 308 // got new processor, disconnect old one 309 WAB.processor['disconnect'](WAB.audioContext['destination']); 310 311 if (WAB.captureStreamNode) 312 WAB.captureStreamNode.disconnect(WAB.processor); 313 314 return 1; 315 }, DISTRHO_PLUGIN_NUM_INPUTS_2, DISTRHO_PLUGIN_NUM_OUTPUTS_2, newBufferSize) != 0; 316 317 if (!success) 318 return false; 319 320 bufferSize = newBufferSize; 321 freeBuffers(); 322 allocBuffers(true, true); 323 324 if (bufferSizeCallback != nullptr) 325 bufferSizeCallback(newBufferSize, jackBufferSizeArg); 326 327 EM_ASM({ 328 var numInputsR = $0; 329 var numInputs = $1; 330 var numOutputs = $2; 331 var bufferSize = $3; 332 var WAB = Module['WebAudioBridge']; 333 334 // store the new processor 335 delete WAB.processor; 336 WAB.processor = WAB.newProcessor; 337 delete WAB.newProcessor; 338 339 var realBufferSize = WAB['fakeSmallBufferSize'] ? 2048 : bufferSize; 340 var divider = realBufferSize / bufferSize; 341 342 // setup new processor the same way as old one 343 WAB.processor['onaudioprocess'] = function (e) { 344 // var timestamp = performance.now(); 345 if (e['inputBuffer'].length != e['outputBuffer'].length || e['inputBuffer'].length != bufferSize) { 346 console.log("invalid buffer size!", e['inputBuffer'].length, e['inputBuffer'].length, bufferSize); 347 } 348 if (e['inputBuffer'].numberOfChannels != numInputs) { 349 console.log("invalid number of input channels!", e['inputBuffer'].numberOfChannels, numInputs); 350 } 351 if (e['outputBuffer'].numberOfChannels != numOutputs) { 352 console.log("invalid number of output channels!", e['outputBuffer'].numberOfChannels, numOutputs); 353 } 354 for (var k = 0; k < divider; ++k) { 355 for (var i = 0; i < numInputs; ++i) { 356 var buffer = e['inputBuffer']['getChannelData'](i); 357 for (var j = 0; j < bufferSize; ++j) { 358 // setValue($4 + ((bufferSize * i) + j) * 4, buffer[bufferSize * k + j], 'float'); 359 HEAPF32[$4 + (((bufferSize * i) + j) << 2) >> 2] = buffer[bufferSize * k + j]; 360 } 361 } 362 dynCall('vi', $5, [$6]); 363 for (var i = 0; i < numOutputs; ++i) { 364 var buffer = e['outputBuffer']['getChannelData'](i); 365 var offset = bufferSize * (numInputsR + i); 366 for (var j = 0; j < bufferSize; ++j) { 367 buffer[bufferSize * k + j] = HEAPF32[$4 + ((offset + j) << 2) >> 2]; 368 } 369 } 370 } 371 }; 372 373 // connect to output 374 WAB.processor['connect'](WAB.audioContext['destination']); 375 376 // and input, if available 377 if (WAB.captureStreamNode) 378 WAB.captureStreamNode.connect(WAB.processor); 379 380 }, DISTRHO_PLUGIN_NUM_INPUTS, DISTRHO_PLUGIN_NUM_INPUTS_2, DISTRHO_PLUGIN_NUM_OUTPUTS_2, bufferSize, audioBufferStorage, WebAudioCallback, this); 381 382 return true; 383 } 384 385 bool isMIDIEnabled() const override 386 { 387 #if DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_MIDI_OUTPUT 388 return EM_ASM_INT({ return Module['WebAudioBridge'].midi ? 1 : 0 }) != 0; 389 #else 390 return false; 391 #endif 392 } 393 394 bool requestMIDI() override 395 { 396 #if DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_MIDI_OUTPUT 397 if (midiAvailable) 398 { 399 EM_ASM({ 400 var useInput = !!$0; 401 var useOutput = !!$1; 402 var maxSize = $2; 403 var WAB = Module['WebAudioBridge']; 404 405 var offset = Module._malloc(maxSize); 406 407 var inputCallback = function(event) { 408 if (event.data.length > maxSize) 409 return; 410 var buffer = new Uint8Array(Module.HEAPU8.buffer, offset, maxSize); 411 buffer.set(event.data); 412 dynCall('viiif', $3, [$4, buffer.byteOffset, event.data.length, event.timeStamp]); 413 }; 414 var stateCallback = function(event) { 415 if (event.port.state === 'connected' && event.port.connection === 'open') { 416 if (useInput && event.port.type === 'input') { 417 if (event.port.name.indexOf('Midi Through') < 0) 418 event.port.onmidimessage = inputCallback; 419 } else if (useOutput && event.port.type === 'output') { 420 event.port.open(); 421 } 422 } 423 }; 424 425 var success = function(midi) { 426 WAB.midi = midi; 427 midi.onstatechange = stateCallback; 428 if (useInput) { 429 midi.inputs.forEach(function(port) { 430 if (port.name.indexOf('Midi Through') < 0) 431 port.onmidimessage = inputCallback; 432 }); 433 } 434 if (useOutput) { 435 midi.outputs.forEach(function(port) { 436 port.open(); 437 }); 438 } 439 }; 440 var fail = function(why) { 441 console.log("midi access failed:", why); 442 }; 443 444 navigator.requestMIDIAccess().then(success, fail); 445 }, DISTRHO_PLUGIN_WANT_MIDI_INPUT, DISTRHO_PLUGIN_WANT_MIDI_OUTPUT, kMaxMIDIInputMessageSize, WebMIDICallback, this); 446 447 return true; 448 } 449 else 450 #endif 451 { 452 d_stderr2("MIDI is not supported"); 453 return false; 454 } 455 } 456 457 static void WebAudioCallback(void* const userData /* , const double timestamp */) 458 { 459 WebBridge* const self = static_cast<WebBridge*>(userData); 460 // self->timestamp = timestamp; 461 462 const uint numFrames = self->bufferSize; 463 464 if (self->jackProcessCallback != nullptr && self->active) 465 { 466 self->jackProcessCallback(numFrames, self->jackProcessArg); 467 468 #if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT 469 if (self->midiAvailable && self->midiOutBuffer.isDataAvailableForReading()) 470 { 471 static_assert(kMaxMIDIInputMessageSize + 1u == 4, "change code if bumping this value"); 472 uint32_t offset = 0; 473 uint8_t bytes[4] = {}; 474 double timestamp = EM_ASM_DOUBLE({ return performance.now(); }); 475 476 while (self->midiOutBuffer.isDataAvailableForReading() && 477 self->midiOutBuffer.readCustomData(bytes, ARRAY_SIZE(bytes))) 478 { 479 offset = self->midiOutBuffer.readUInt(); 480 481 EM_ASM({ 482 var WAB = Module['WebAudioBridge']; 483 if (WAB.midi) { 484 var timestamp = $5 + $0; 485 var size = $1; 486 WAB.midi.outputs.forEach(function(port) { 487 if (port.state !== 'disconnected') { 488 port.send(size == 3 ? [ $2, $3, $4 ] : 489 size == 2 ? [ $2, $3 ] : 490 [ $2 ], timestamp); 491 } 492 }); 493 } 494 }, offset, bytes[0], bytes[1], bytes[2], bytes[3], timestamp); 495 } 496 497 self->midiOutBuffer.clearData(); 498 } 499 #endif 500 } 501 else 502 { 503 for (uint i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS_2; ++i) 504 std::memset(self->audioBuffers[DISTRHO_PLUGIN_NUM_INPUTS + i], 0, sizeof(float)*numFrames); 505 } 506 } 507 508 #if DISTRHO_PLUGIN_WANT_MIDI_INPUT 509 static void WebMIDICallback(void* const userData, uint8_t* const data, const int len, double /*timestamp*/) 510 { 511 DISTRHO_SAFE_ASSERT_RETURN(len > 0 && len <= (int)kMaxMIDIInputMessageSize,); 512 513 WebBridge* const self = static_cast<WebBridge*>(userData); 514 515 self->midiInBufferPending.writeByte(static_cast<uint8_t>(len)); 516 // TODO timestamp 517 // self->midiInBufferPending.writeDouble(timestamp); 518 self->midiInBufferPending.writeCustomData(data, len); 519 for (uint8_t i=len; i<kMaxMIDIInputMessageSize; ++i) 520 self->midiInBufferPending.writeByte(0); 521 self->midiInBufferPending.commitWrite(); 522 } 523 #endif 524 }; 525 526 #endif // WEB_BRIDGE_HPP_INCLUDED