DPF

DISTRHO Plugin Framework
Log | Files | Refs | Submodules | README | LICENSE

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