diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/DPF-Prymula-audioplugins/dpf/distrho/src/jackbridge/WebBridge.hpp	Mon Oct 16 21:53:34 2023 +0200
@@ -0,0 +1,495 @@
+/*
+ * Web Audio + MIDI Bridge for DPF
+ * Copyright (C) 2021-2022 Filipe Coelho <falktx@falktx.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any purpose with
+ * or without fee is hereby granted, provided that the above copyright notice and this
+ * permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
+ * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
+ * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+ * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
+ * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef WEB_BRIDGE_HPP_INCLUDED
+#define WEB_BRIDGE_HPP_INCLUDED
+
+#include "NativeBridge.hpp"
+
+#include <emscripten/emscripten.h>
+
+struct WebBridge : NativeBridge {
+#if DISTRHO_PLUGIN_NUM_INPUTS > 0
+    bool captureAvailable = false;
+#endif
+#if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
+    bool playbackAvailable = false;
+#endif
+    bool active = false;
+    double timestamp = 0;
+
+    WebBridge()
+    {
+       #if DISTRHO_PLUGIN_NUM_INPUTS > 0
+        captureAvailable = EM_ASM_INT({
+            if (typeof(navigator.mediaDevices) !== 'undefined' && typeof(navigator.mediaDevices.getUserMedia) !== 'undefined')
+                return 1;
+            if (typeof(navigator.webkitGetUserMedia) !== 'undefined')
+                return 1;
+            return false;
+        }) != 0;
+       #endif
+
+       #if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
+        playbackAvailable = EM_ASM_INT({
+            if (typeof(AudioContext) !== 'undefined')
+                return 1;
+            if (typeof(webkitAudioContext) !== 'undefined')
+                return 1;
+            return 0;
+        }) != 0;
+       #endif
+
+       #if DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
+        midiAvailable = EM_ASM_INT({
+            return typeof(navigator.requestMIDIAccess) === 'function' ? 1 : 0;
+        }) != 0;
+       #endif
+    }
+
+    bool open(const char*) override
+    {
+        // early bail out if required features are not supported
+       #if DISTRHO_PLUGIN_NUM_INPUTS > 0
+        if (!captureAvailable)
+        {
+           #if DISTRHO_PLUGIN_NUM_OUTPUTS == 0
+            d_stderr2("Audio capture is not supported");
+            return false;
+           #else
+            if (!playbackAvailable)
+            {
+                d_stderr2("Audio capture and playback are not supported");
+                return false;
+            }
+            d_stderr2("Audio capture is not supported, but can still use playback");
+           #endif
+        }
+       #endif
+
+       #if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
+        if (!playbackAvailable)
+        {
+            d_stderr2("Audio playback is not supported");
+            return false;
+        }
+       #endif
+
+        const bool initialized = EM_ASM_INT({
+            if (typeof(Module['WebAudioBridge']) === 'undefined') {
+                Module['WebAudioBridge'] = {};
+            }
+
+            var WAB = Module['WebAudioBridge'];
+            if (!WAB.audioContext) {
+                if (typeof(AudioContext) !== 'undefined') {
+                    WAB.audioContext = new AudioContext();
+                } else if (typeof(webkitAudioContext) !== 'undefined') {
+                    WAB.audioContext = new webkitAudioContext();
+                }
+            }
+
+            return WAB.audioContext === undefined ? 0 : 1;
+        }) != 0;
+        
+        if (!initialized)
+        {
+            d_stderr2("Failed to initialize web audio");
+            return false;
+        }
+
+        bufferSize = EM_ASM_INT({
+            var WAB = Module['WebAudioBridge'];
+            return WAB['minimizeBufferSize'] ? 256 : 2048;
+        });
+        sampleRate = EM_ASM_INT_V({
+            var WAB = Module['WebAudioBridge'];
+            return WAB.audioContext.sampleRate;
+        });
+
+        allocBuffers(true, true);
+
+        EM_ASM({
+            var numInputs = $0;
+            var numOutputs = $1;
+            var bufferSize = $2;
+            var WAB = Module['WebAudioBridge'];
+
+            var realBufferSize = WAB['minimizeBufferSize'] ? 2048 : bufferSize;
+            var divider = realBufferSize / bufferSize;
+
+            // main processor
+            WAB.processor = WAB.audioContext['createScriptProcessor'](realBufferSize, numInputs, numOutputs);
+            WAB.processor['onaudioprocess'] = function (e) {
+                // var timestamp = performance.now();
+                for (var k = 0; k < divider; ++k) {
+                    for (var i = 0; i < numInputs; ++i) {
+                        var buffer = e['inputBuffer']['getChannelData'](i);
+                        for (var j = 0; j < bufferSize; ++j) {
+                            // setValue($3 + ((bufferSize * i) + j) * 4, buffer[j], 'float');
+                            HEAPF32[$3 + (((bufferSize * i) + j) << 2) >> 2] = buffer[bufferSize * k + j];
+                        }
+                    }
+                    dynCall('vi', $4, [$5]);
+                    for (var i = 0; i < numOutputs; ++i) {
+                        var buffer = e['outputBuffer']['getChannelData'](i);
+                        var offset = bufferSize * (numInputs + i);
+                        for (var j = 0; j < bufferSize; ++j) {
+                            buffer[bufferSize * k + j] = HEAPF32[$3 + ((offset + j) << 2) >> 2];
+                        }
+                    }
+                }
+            };
+
+            // connect to output
+            WAB.processor['connect'](WAB.audioContext['destination']);
+
+            // resume/start playback on first click
+            document.addEventListener('click', function(e) {
+                var WAB = Module['WebAudioBridge'];
+                if (WAB.audioContext.state === 'suspended')
+                    WAB.audioContext.resume();
+            });
+        }, DISTRHO_PLUGIN_NUM_INPUTS_2, DISTRHO_PLUGIN_NUM_OUTPUTS_2, bufferSize, audioBufferStorage, WebAudioCallback, this);
+
+        return true;
+    }
+
+    bool close() override
+    {
+        freeBuffers();
+        return true;
+    }
+
+    bool activate() override
+    {
+        active = true;
+        return true;
+    }
+
+    bool deactivate() override
+    {
+        active = false;
+        return true;
+    }
+
+    bool supportsAudioInput() const override
+    {
+       #if DISTRHO_PLUGIN_NUM_INPUTS > 0
+        return captureAvailable;
+       #else
+        return false;
+       #endif
+    }
+
+    bool isAudioInputEnabled() const override
+    {
+       #if DISTRHO_PLUGIN_NUM_INPUTS > 0
+        return EM_ASM_INT({ return Module['WebAudioBridge'].captureStreamNode ? 1 : 0 }) != 0;
+       #else
+        return false;
+       #endif
+    }
+
+    bool requestAudioInput() override
+    {
+        DISTRHO_SAFE_ASSERT_RETURN(DISTRHO_PLUGIN_NUM_INPUTS > 0, false);
+
+        EM_ASM({
+            var numInputs = $0;
+            var WAB = Module['WebAudioBridge'];
+
+            var constraints = {};
+            // we need to use this weird awkward way for objects, otherwise build fails
+            constraints['audio'] = true;
+            constraints['video'] = false;
+            constraints['autoGainControl'] = {};
+            constraints['autoGainControl']['ideal'] = false;
+            constraints['echoCancellation'] = {};
+            constraints['echoCancellation']['ideal'] = false;
+            constraints['noiseSuppression'] = {};
+            constraints['noiseSuppression']['ideal'] = false;
+            constraints['channelCount'] = {};
+            constraints['channelCount']['min'] = 0;
+            constraints['channelCount']['ideal'] = numInputs;
+            constraints['latency'] = {};
+            constraints['latency']['min'] = 0;
+            constraints['latency']['ideal'] = 0;
+            constraints['sampleSize'] = {};
+            constraints['sampleSize']['min'] = 8;
+            constraints['sampleSize']['max'] = 32;
+            constraints['sampleSize']['ideal'] = 16;
+            // old property for chrome
+            constraints['googAutoGainControl'] = false;
+
+            var success = function(stream) {
+                var track = stream.getAudioTracks()[0];
+
+                // try to force as much as we can
+                track.applyConstraints({'autoGainControl': { 'exact': false } })
+                .then(function(){console.log("Mic/Input auto-gain control has been disabled")})
+                .catch(function(){console.log("Cannot disable Mic/Input auto-gain")});
+
+                track.applyConstraints({'echoCancellation': { 'exact': false } })
+                .then(function(){console.log("Mic/Input echo-cancellation has been disabled")})
+                .catch(function(){console.log("Cannot disable Mic/Input echo-cancellation")});
+
+                track.applyConstraints({'noiseSuppression': { 'exact': false } })
+                .then(function(){console.log("Mic/Input noise-suppression has been disabled")})
+                .catch(function(){console.log("Cannot disable Mic/Input noise-suppression")});
+
+                track.applyConstraints({'googAutoGainControl': { 'exact': false } })
+                .then(function(){})
+                .catch(function(){});
+
+                WAB.captureStreamNode = WAB.audioContext['createMediaStreamSource'](stream);
+                WAB.captureStreamNode.connect(WAB.processor);
+            };
+            var fail = function() {
+            };
+
+            if (navigator.mediaDevices !== undefined && navigator.mediaDevices.getUserMedia !== undefined) {
+                navigator.mediaDevices.getUserMedia(constraints).then(success).catch(fail);
+            } else if (navigator.webkitGetUserMedia !== undefined) {
+                navigator.webkitGetUserMedia(constraints, success, fail);
+            }
+        }, DISTRHO_PLUGIN_NUM_INPUTS_2);
+
+        return true;
+    }
+
+    bool supportsBufferSizeChanges() const override
+    {
+        return true;
+    }
+
+    bool requestBufferSizeChange(const uint32_t newBufferSize) override
+    {
+        // try to create new processor first
+        bool success = EM_ASM_INT({
+            var numInputs = $0;
+            var numOutputs = $1;
+            var newBufferSize = $2;
+            var WAB = Module['WebAudioBridge'];
+
+            try {
+                WAB.newProcessor = WAB.audioContext['createScriptProcessor'](newBufferSize, numInputs, numOutputs);
+            } catch (e) {
+                return 0;
+            }
+
+            // got new processor, disconnect old one
+            WAB.processor['disconnect'](WAB.audioContext['destination']);
+
+            if (WAB.captureStreamNode)
+                WAB.captureStreamNode.disconnect(WAB.processor);
+
+            return 1;
+        }, DISTRHO_PLUGIN_NUM_INPUTS_2, DISTRHO_PLUGIN_NUM_OUTPUTS_2, newBufferSize) != 0;
+
+        if (!success)
+            return false;
+
+        bufferSize = newBufferSize;
+        freeBuffers();
+        allocBuffers(true, true);
+
+        if (bufferSizeCallback != nullptr)
+            bufferSizeCallback(newBufferSize, jackBufferSizeArg);
+
+        EM_ASM({
+            var numInputsR = $0;
+            var numInputs = $1;
+            var numOutputs = $2;
+            var bufferSize = $3;
+            var WAB = Module['WebAudioBridge'];
+
+            // store the new processor
+            delete WAB.processor;
+            WAB.processor = WAB.newProcessor;
+            delete WAB.newProcessor;
+
+            // setup new processor the same way as old one
+            WAB.processor['onaudioprocess'] = function (e) {
+                // var timestamp = performance.now();
+                for (var i = 0; i < numInputs; ++i) {
+                    var buffer = e['inputBuffer']['getChannelData'](i);
+                    for (var j = 0; j < bufferSize; ++j) {
+                        // setValue($3 + ((bufferSize * i) + j) * 4, buffer[j], 'float');
+                        HEAPF32[$4 + (((bufferSize * i) + j) << 2) >> 2] = buffer[j];
+                    }
+                }
+                dynCall('vi', $5, [$6]);
+                for (var i = 0; i < numOutputs; ++i) {
+                    var buffer = e['outputBuffer']['getChannelData'](i);
+                    var offset = bufferSize * (numInputsR + i);
+                    for (var j = 0; j < bufferSize; ++j) {
+                        buffer[j] = HEAPF32[$3 + ((offset + j) << 2) >> 2];
+                    }
+                }
+            };
+
+            // connect to output
+            WAB.processor['connect'](WAB.audioContext['destination']);
+
+            // and input, if available
+            if (WAB.captureStreamNode)
+                WAB.captureStreamNode.connect(WAB.processor);
+
+        }, DISTRHO_PLUGIN_NUM_INPUTS, DISTRHO_PLUGIN_NUM_INPUTS_2, DISTRHO_PLUGIN_NUM_OUTPUTS_2, bufferSize, audioBufferStorage, WebAudioCallback, this);
+
+        return true;
+    }
+
+    bool isMIDIEnabled() const override
+    {
+       #if DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
+        return EM_ASM_INT({ return Module['WebAudioBridge'].midi ? 1 : 0 }) != 0;
+       #else
+        return false;
+       #endif
+    }
+
+    bool requestMIDI() override
+    {
+       #if DISTRHO_PLUGIN_WANT_MIDI_INPUT || DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
+        if (midiAvailable)
+        {
+            EM_ASM({
+                var useInput = !!$0;
+                var useOutput = !!$1;
+                var maxSize = $2;
+                var WAB = Module['WebAudioBridge'];
+
+                var offset = Module._malloc(maxSize);
+
+                var inputCallback = function(event) {
+                    if (event.data.length > maxSize)
+                        return;
+                    var buffer = new Uint8Array(Module.HEAPU8.buffer, offset, maxSize);
+                    buffer.set(event.data);
+                    dynCall('viiif', $3, [$4, buffer.byteOffset, event.data.length, event.timeStamp]);
+                };
+                var stateCallback = function(event) {
+                    if (event.port.state === 'connected' && event.port.connection === 'open') {
+                        if (useInput && event.port.type === 'input') {
+                            if (event.port.name.indexOf('Midi Through') < 0)
+                                event.port.onmidimessage = inputCallback;
+                        } else if (useOutput && event.port.type === 'output') {
+                            event.port.open();
+                        }
+                    }
+                };
+
+                var success = function(midi) {
+                    WAB.midi = midi;
+                    midi.onstatechange = stateCallback;
+                    if (useInput) {
+                        midi.inputs.forEach(function(port) {
+                            if (port.name.indexOf('Midi Through') < 0)
+                                port.onmidimessage = inputCallback;
+                        });
+                    }
+                    if (useOutput) {
+                        midi.outputs.forEach(function(port) {
+                            port.open();
+                        });
+                    }
+                };
+                var fail = function(why) {
+                    console.log("midi access failed:", why);
+                };
+
+                navigator.requestMIDIAccess().then(success, fail);
+            }, DISTRHO_PLUGIN_WANT_MIDI_INPUT, DISTRHO_PLUGIN_WANT_MIDI_OUTPUT, kMaxMIDIInputMessageSize, WebMIDICallback, this);
+
+            return true;
+        }
+        else
+       #endif
+        {
+            d_stderr2("MIDI is not supported");
+            return false;
+        }
+    }
+
+    static void WebAudioCallback(void* const userData /* , const double timestamp */)
+    {
+        WebBridge* const self = static_cast<WebBridge*>(userData);
+        // self->timestamp = timestamp;
+
+        const uint numFrames = self->bufferSize;
+
+        if (self->jackProcessCallback != nullptr && self->active)
+        {
+            self->jackProcessCallback(numFrames, self->jackProcessArg);
+
+           #if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
+            if (self->midiAvailable && self->midiOutBuffer.isDataAvailableForReading())
+            {
+                static_assert(kMaxMIDIInputMessageSize + 1u == 4, "change code if bumping this value");
+                uint32_t offset = 0;
+                uint8_t bytes[4] = {};
+                double timestamp = EM_ASM_DOUBLE({ return performance.now(); });
+
+                while (self->midiOutBuffer.isDataAvailableForReading() &&
+                       self->midiOutBuffer.readCustomData(bytes, ARRAY_SIZE(bytes)))
+                {
+                    offset = self->midiOutBuffer.readUInt();
+
+                    EM_ASM({
+                        var WAB = Module['WebAudioBridge'];
+                        if (WAB.midi) {
+                            var timestamp = $5 + $0;
+                            var size = $1;
+                            WAB.midi.outputs.forEach(function(port) {
+                                if (port.state !== 'disconnected') {
+                                    port.send(size == 3 ? [ $2, $3, $4 ] :
+                                              size == 2 ? [ $2, $3 ] :
+                                              [ $2 ], timestamp);
+                                }
+                            });
+                        }
+                    }, offset, bytes[0], bytes[1], bytes[2], bytes[3], timestamp);
+                }
+
+                self->midiOutBuffer.clearData();
+            }
+           #endif
+        }
+        else
+        {
+            for (uint i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS_2; ++i)
+                std::memset(self->audioBuffers[DISTRHO_PLUGIN_NUM_INPUTS + i], 0, sizeof(float)*numFrames);
+        }
+    }
+
+   #if DISTRHO_PLUGIN_WANT_MIDI_INPUT
+    static void WebMIDICallback(void* const userData, uint8_t* const data, const int len, const double timestamp)
+    {
+        DISTRHO_SAFE_ASSERT_RETURN(len > 0 && len <= (int)kMaxMIDIInputMessageSize,);
+
+        WebBridge* const self = static_cast<WebBridge*>(userData);
+
+        // TODO timestamp handling
+        self->midiInBufferPending.writeByte(static_cast<uint8_t>(len));
+        self->midiInBufferPending.writeCustomData(data, kMaxMIDIInputMessageSize);
+        self->midiInBufferPending.commitWrite();
+    }
+   #endif
+};
+
+#endif // WEB_BRIDGE_HPP_INCLUDED