Mercurial > hg > pub > prymula > com
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