commit da582de935521d60c0c74a6bca087f6a4ed55c9e
parent c52ee8cfa8378e62ab59d6c8da343ec85a1380ff
Author: falkTX <falktx@falktx.com>
Date: Thu, 29 Dec 2022 01:07:29 +0000
Initial implementation of modgui over wasm
Signed-off-by: falkTX <falktx@falktx.com>
Diffstat:
7 files changed, 589 insertions(+), 8 deletions(-)
diff --git a/Makefile.plugins.mk b/Makefile.plugins.mk
@@ -68,6 +68,10 @@ ifeq ($(HAVE_SDL2),true)
BASE_FLAGS += -DHAVE_SDL2
endif
+ifneq ($(MODGUI_CLASS_NAME),)
+BASE_FLAGS += -DDISTRHO_PLUGIN_MODGUI_CLASS_NAME='"$(MODGUI_CLASS_NAME)"'
+endif
+
# always needed
ifneq ($(HAIKU_OR_MACOS_OR_WASM_OR_WINDOWS),true)
ifneq ($(STATIC_BUILD),true)
@@ -525,6 +529,100 @@ $(lv2_ui): $(OBJS_UI) $(BUILD_DIR)/DistrhoUIMain_LV2.cpp.o $(DGL_LIB)
$(SILENT)$(CXX) $^ $(BUILD_CXX_FLAGS) $(LINK_FLAGS) $(EXTRA_LIBS) $(EXTRA_UI_LIBS) $(DGL_LIBS) $(SHARED) $(SYMBOLS_LV2UI) -o $@
# ---------------------------------------------------------------------------------------------------------------------
+# LV2 modgui
+
+ifeq ($(MODGUI_BUILD),true)
+ifeq ($(MODGUI_CLASS_NAME),)
+$(error MODGUI_CLASS_NAME undefined)
+endif
+endif
+
+# clear all possible flags coming from DPF, while keeping any extra flags specified for this build
+MODGUI_IGNORED_FLAGS = -fdata-sections
+MODGUI_IGNORED_FLAGS += -ffast-math
+MODGUI_IGNORED_FLAGS += -ffunction-sections
+MODGUI_IGNORED_FLAGS += -fno-gnu-unique
+MODGUI_IGNORED_FLAGS += -fprefetch-loop-arrays
+MODGUI_IGNORED_FLAGS += -fvisibility=hidden
+MODGUI_IGNORED_FLAGS += -fvisibility-inlines-hidden
+MODGUI_IGNORED_FLAGS += -fPIC
+MODGUI_IGNORED_FLAGS += -ldl
+MODGUI_IGNORED_FLAGS += -mfpmath=sse
+MODGUI_IGNORED_FLAGS += -msse
+MODGUI_IGNORED_FLAGS += -msse2
+MODGUI_IGNORED_FLAGS += -mtune=generic
+MODGUI_IGNORED_FLAGS += -pipe
+MODGUI_IGNORED_FLAGS += -std=gnu99
+MODGUI_IGNORED_FLAGS += -std=gnu++11
+MODGUI_IGNORED_FLAGS += -DDISTRHO_PLUGIN_MODGUI_CLASS_NAME='"$(MODGUI_CLASS_NAME)"'
+MODGUI_IGNORED_FLAGS += -DDGL_OPENGL
+MODGUI_IGNORED_FLAGS += -DGL_SILENCE_DEPRECATION=1
+MODGUI_IGNORED_FLAGS += -DHAVE_ALSA
+MODGUI_IGNORED_FLAGS += -DHAVE_DGL
+MODGUI_IGNORED_FLAGS += -DHAVE_JACK
+MODGUI_IGNORED_FLAGS += -DHAVE_LIBLO
+MODGUI_IGNORED_FLAGS += -DHAVE_OPENGL
+MODGUI_IGNORED_FLAGS += -DHAVE_PULSEAUDIO
+MODGUI_IGNORED_FLAGS += -DHAVE_RTAUDIO
+MODGUI_IGNORED_FLAGS += -DHAVE_SDL2
+MODGUI_IGNORED_FLAGS += -DNDEBUG
+MODGUI_IGNORED_FLAGS += -DPIC
+MODGUI_IGNORED_FLAGS += -I.
+MODGUI_IGNORED_FLAGS += -I$(DPF_PATH)/distrho
+MODGUI_IGNORED_FLAGS += -I$(DPF_PATH)/dgl
+MODGUI_IGNORED_FLAGS += -I$(MOD_WORKDIR)/modduo-static/staging/usr/include
+MODGUI_IGNORED_FLAGS += -I$(MOD_WORKDIR)/modduox-static/staging/usr/include
+MODGUI_IGNORED_FLAGS += -I$(MOD_WORKDIR)/moddwarf/staging/usr/include
+MODGUI_IGNORED_FLAGS += -L$(MOD_WORKDIR)/modduo-static/staging/usr/lib
+MODGUI_IGNORED_FLAGS += -L$(MOD_WORKDIR)/modduox-static/staging/usr/lib
+MODGUI_IGNORED_FLAGS += -L$(MOD_WORKDIR)/moddwarf/staging/usr/lib
+MODGUI_IGNORED_FLAGS += -MD
+MODGUI_IGNORED_FLAGS += -MP
+MODGUI_IGNORED_FLAGS += -O2
+MODGUI_IGNORED_FLAGS += -O3
+MODGUI_IGNORED_FLAGS += -Wall
+MODGUI_IGNORED_FLAGS += -Wextra
+MODGUI_IGNORED_FLAGS += -Wl,-O1,--as-needed,--gc-sections
+MODGUI_IGNORED_FLAGS += -Wl,-dead_strip,-dead_strip_dylibs
+MODGUI_IGNORED_FLAGS += -Wl,-x
+MODGUI_IGNORED_FLAGS += -Wl,--gc-sections
+MODGUI_IGNORED_FLAGS += -Wl,--no-undefined
+MODGUI_IGNORED_FLAGS += -Wl,--strip-all
+MODGUI_IGNORED_FLAGS += -Wno-deprecated-declarations
+MODGUI_IGNORED_FLAGS += $(DGL_FLAGS)
+MODGUI_CFLAGS = $(filter-out $(MODGUI_IGNORED_FLAGS),$(BUILD_C_FLAGS)) -D__MOD_DEVICES__
+MODGUI_CXXFLAGS = $(filter-out $(MODGUI_IGNORED_FLAGS),$(BUILD_CXX_FLAGS)) -D__MOD_DEVICES__
+MODGUI_LDFLAGS = $(filter-out $(MODGUI_IGNORED_FLAGS),$(LINK_FLAGS))
+
+$(TARGET_DIR)/$(NAME).lv2/modgui/module.js: $(OBJS_UI) $(BUILD_DIR)/DistrhoUIMain_LV2.cpp.o $(DGL_LIB)
+ -@mkdir -p $(shell dirname $@)
+ @echo "Creating LV2 plugin modgui for $(NAME)"
+ $(SILENT)$(CXX) $^ $(LINK_FLAGS) $(EXTRA_LIBS) $(EXTRA_UI_LIBS) $(DGL_LIBS) \
+ -sALLOW_TABLE_GROWTH -sMODULARIZE=1 -sMAIN_MODULE=2 -sDISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=0 \
+ -sEXPORTED_FUNCTIONS="['_malloc','_free','_modgui_init','_modgui_param_set','_modgui_patch_set','_modgui_cleanup']" \
+ -sEXPORTED_RUNTIME_METHODS=['addFunction','lengthBytesUTF8','stringToUTF8','UTF8ToString'] \
+ -sEXPORT_NAME="Module_$(MODGUI_CLASS_NAME)" \
+ -o $@
+
+modgui:
+ $(MAKE) $(TARGET_DIR)/$(NAME).lv2/modgui/module.js \
+ EXE_WRAPPER= \
+ FILE_BROWSER_DISABLED=true \
+ HAVE_OPENGL=true \
+ MODGUI_BUILD=true \
+ NOOPT=true \
+ PKG_CONFIG=false \
+ USE_GLES2=true \
+ AR=emar \
+ CC=emcc \
+ CXX=em++ \
+ CFLAGS="$(MODGUI_CFLAGS)" \
+ CXXFLAGS="$(MODGUI_CXXFLAGS)" \
+ LDFLAGS="$(MODGUI_LDFLAGS)"
+
+.PHONY: modgui
+
+# ---------------------------------------------------------------------------------------------------------------------
# VST2
vst2 vst: $(vst2) $(vst2files)
diff --git a/cmake/DPF-plugin.cmake b/cmake/DPF-plugin.cmake
@@ -87,6 +87,9 @@ include(CMakeParseArguments)
# list of sources which are part of the UI
# empty indicates the plugin does not have UI
#
+# `MODGUI_CLASS_NAME`
+# class name to use for modgui builds
+#
# `MONOLITHIC`
# build LV2 as a single binary for UI and DSP
#
@@ -95,7 +98,7 @@ include(CMakeParseArguments)
#
function(dpf_add_plugin NAME)
set(options MONOLITHIC NO_SHARED_RESOURCES)
- set(oneValueArgs UI_TYPE)
+ set(oneValueArgs MODGUI_CLASS_NAME UI_TYPE)
set(multiValueArgs FILES_COMMON FILES_DSP FILES_UI TARGETS)
cmake_parse_arguments(_dpf_plugin "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
@@ -134,6 +137,10 @@ function(dpf_add_plugin NAME)
target_include_directories("${NAME}" PUBLIC
"${DPF_ROOT_DIR}/distrho")
+ if(_dpf_plugin_MODGUI_CLASS_NAME)
+ target_compile_definitions("${NAME}" PUBLIC "DISTRHO_PLUGIN_MODGUI_CLASS_NAME=\"${_dpf_plugin_MODGUI_CLASS_NAME}\"")
+ endif()
+
if((NOT WIN32) AND (NOT APPLE) AND (NOT HAIKU))
target_link_libraries("${NAME}" PRIVATE "dl")
endif()
diff --git a/distrho/src/DistrhoPluginLV2export.cpp b/distrho/src/DistrhoPluginLV2export.cpp
@@ -42,7 +42,11 @@
# include "mod-license.h"
#endif
-#ifndef DISTRHO_OS_WINDOWS
+#ifdef DISTRHO_OS_WINDOWS
+# include <direct.h>
+#else
+# include <sys/stat.h>
+# include <sys/types.h>
# include <unistd.h>
#endif
@@ -944,11 +948,11 @@ void lv2_generate_ttl(const char* const basename)
}
}
-#ifdef DISTRHO_PLUGIN_BRAND
+ #ifdef DISTRHO_PLUGIN_BRAND
// MOD
pluginString += " mod:brand \"" DISTRHO_PLUGIN_BRAND "\" ;\n";
pluginString += " mod:label \"" DISTRHO_PLUGIN_NAME "\" ;\n\n";
-#endif
+ #endif
// name
{
@@ -1214,6 +1218,295 @@ void lv2_generate_ttl(const char* const basename)
std::cout << " done!" << std::endl;
}
+#if DISTRHO_PLUGIN_USES_MODGUI
+ {
+ std::cout << "Writing modgui.ttl..."; std::cout.flush();
+ std::fstream modguiFile("modgui.ttl", std::ios::out);
+
+ String modguiString;
+ modguiString += "@prefix lv2: <" LV2_CORE_PREFIX "> .\n";
+ modguiString += "@prefix modgui: <http://moddevices.com/ns/modgui#> .\n";
+ modguiString += "\n";
+
+ modguiString += "<" DISTRHO_PLUGIN_URI ">\n";
+ modguiString += " modgui:gui [\n";
+ #ifdef DISTRHO_PLUGIN_BRAND
+ modguiString += " modgui:brand \"" DISTRHO_PLUGIN_BRAND "\" ;\n";
+ #endif
+ modguiString += " modgui:label \"" DISTRHO_PLUGIN_NAME "\" ;\n";
+ modguiString += " modgui:resourcesDirectory <modgui> ;\n";
+ modguiString += " modgui:iconTemplate <modgui/icon.html> ;\n";
+ modguiString += " modgui:javascript <modgui/javascript.js> ;\n";
+ modguiString += " modgui:stylesheet <modgui/stylesheet.css> ;\n";
+ modguiString += " modgui:screenshot <modgui/screenshot.png> ;\n";
+ modguiString += " modgui:thumbnail <modgui/thumbnail.png> ;\n";
+
+ uint32_t numParametersOutputs = 0;
+ for (uint32_t i=0, count=plugin.getParameterCount(); i < count; ++i)
+ {
+ if (plugin.isParameterOutput(i))
+ ++numParametersOutputs;
+ }
+ if (numParametersOutputs != 0)
+ {
+ modguiString += " modgui:monitoredOutputs [\n";
+ for (uint32_t i=0, j=0, count=plugin.getParameterCount(); i < count; ++i)
+ {
+ if (!plugin.isParameterOutput(i))
+ continue;
+ modguiString += " lv2:symbol \"" + plugin.getParameterSymbol(i) + "\" ;\n";
+ if (++j != numParametersOutputs)
+ modguiString += " ] , [\n";
+ }
+ modguiString += " ] ;\n";
+ }
+
+ modguiString += " ] .\n";
+
+ modguiFile << modguiString;
+ modguiFile.close();
+ std::cout << " done!" << std::endl;
+ }
+
+ #ifdef DISTRHO_OS_WINDOWS
+ ::_mkdir("modgui");
+ #else
+ ::mkdir("modgui", 0755);
+ #endif
+
+ {
+ std::cout << "Writing modgui/javascript.js..."; std::cout.flush();
+ std::fstream jsFile("modgui/javascript.js", std::ios::out);
+
+ String jsString;
+ jsString += "function(e,f){\n";
+ jsString += "'use strict';\nvar ps=[";
+
+ for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS; ++i)
+ jsString += "'lv2_" + plugin.getAudioPort(false, i).symbol + "',";
+ for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS; ++i)
+ jsString += "'lv2_" + plugin.getAudioPort(true, i).symbol + "',";
+ #if DISTRHO_LV2_USE_EVENTS_IN
+ jsString += "'lv2_events_in',";
+ #endif
+ #if DISTRHO_LV2_USE_EVENTS_OUT
+ jsString += "'lv2_events_out',";
+ #endif
+ #if DISTRHO_PLUGIN_WANT_LATENCY
+ jsString += "'lv2_latency',";
+ #endif
+
+ int32_t enabledIndex = INT32_MAX;
+ for (uint32_t i=0, count=plugin.getParameterCount(); i < count; ++i)
+ {
+ jsString += "'" + plugin.getParameterSymbol(i) + "',";
+ if (plugin.getParameterDesignation(i) == kParameterDesignationBypass)
+ enabledIndex = i;
+ }
+ jsString += "];\n";
+ jsString += "var ei=" + String(enabledIndex != INT32_MAX ? enabledIndex : -1) + ";\n\n";
+ jsString += "if(e.type==='start'){\n";
+ jsString += "e.data.p={p:{},c:{},};\n\n";
+ jsString += "var err=[];\n";
+ jsString += "if(typeof(WebAssembly)==='undefined'){err.push('WebAssembly unsupported');}\n";
+ jsString += "else{\n";
+ jsString += "if(!WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])))";
+ jsString += "err.push('Bulk Memory Operations unsupported');\n";
+ jsString += "if(!WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,2,8,1,1,97,1,98,3,127,1,6,6,1,127,1,65,0,11,7,5,1,1,97,3,1])))";
+ jsString += "err.push('Importable/Exportable mutable globals unsupported');\n";
+ jsString += "}\n";
+ jsString += "if(err.length!==0){/* errors err.join('<br>')*/return;}\n\n";
+ jsString += "var s=document.createElement('script');\n";
+ jsString += "s.setAttribute('async',true);\n";
+ jsString += "s.setAttribute('src','/resources/module.js?uri='+escape(\"" DISTRHO_PLUGIN_URI "\")+'&r='+VERSION/*f.get_custom_resource_filename('module.js')*/);\n";
+ jsString += "s.setAttribute('type','text/javascript');\n";
+ jsString += "s.onload=function(){\n";
+ jsString += " Module_" DISTRHO_PLUGIN_MODGUI_CLASS_NAME "({\n";
+ jsString += " locateFile: function(p,_){return '/resources/'+p+'?uri='+escape(\"" DISTRHO_PLUGIN_URI "\")+'&r='+VERSION/*return f.get_custom_resource_filename(p);*/},\n";
+ jsString += " postRun:function(m){\n";
+ jsString += " var cn=e.icon.attr('mod-instance').replaceAll('/','_');\n";
+ jsString += " var cnl=m.lengthBytesUTF8(cn) + 1;\n";
+ jsString += " var cna=m._malloc(cnl);\n";
+ jsString += " m.stringToUTF8(cn, cna, cnl);\n";
+ jsString += " e.icon.find('canvas')[0].id=cn;\n";
+ jsString += " var a=m.addFunction(function(i,v){f.set_port_value(ps[i],v);},'vif');\n";
+ jsString += " var b=m.addFunction(function(u,v){f.patch_set(m.UTF8ToString(u),'s',m.UTF8ToString(v));},'vpp');\n";
+ jsString += " var h=m._modgui_init(cna,a,b);\n";
+ jsString += " m._free(cna);\n";
+ jsString += " e.data.h=h;\n";
+ jsString += " e.data.m=m;\n";
+ jsString += " for(var u in e.data.p.p){\n";
+ jsString += " var ul=m.lengthBytesUTF8(u)+1,ua=m._malloc(ul),v=e.data.p.p[u],vl=m.lengthBytesUTF8(v)+1,va=m._malloc(vl);\n";
+ jsString += " m.stringToUTF8(u,ua,ul);\n";
+ jsString += " m.stringToUTF8(v,va,vl);\n";
+ jsString += " m._modgui_patch_set(h, ua, va);\n";
+ jsString += " m._free(ua);\n";
+ jsString += " m._free(va);\n";
+ jsString += " }\n";
+ jsString += " for(var symbol in e.data.p.c){m._modgui_param_set(h,ps.indexOf(symbol),e.data.p.c[symbol]);}\n";
+ jsString += " delete e.data.p;\n";
+ jsString += " window.dispatchEvent(new Event('resize'));\n";
+ jsString += " },\n";
+ jsString += " canvas:(function(){var c=e.icon.find('canvas')[0];c.addEventListener('webglcontextlost',function(e2){alert('WebGL context lost. You will need to reload the page.');e2.preventDefault();},false);return c;})(),\n";
+ jsString += " });\n";
+ jsString += "};\n";
+ jsString += "document.head.appendChild(s);\n\n";
+ jsString += "}else if(e.type==='change'){\n\n";
+ jsString += "if(e.data.h && e.data.m){\n";
+ jsString += " var m=e.data.m;\n";
+ jsString += " if(e.uri){\n";
+ jsString += " var ul=m.lengthBytesUTF8(e.uri)+1,ua=m._malloc(ul),vl=m.lengthBytesUTF8(e.value)+1,va=m._malloc(vl);\n";
+ jsString += " m.stringToUTF8(e.uri,ua,ul);\n";
+ jsString += " m.stringToUTF8(e.value,va,vl);\n";
+ jsString += " m._modgui_patch_set(e.data.h,ua,va);\n";
+ jsString += " m._free(ua);\n";
+ jsString += " m._free(va);\n";
+ jsString += " }else if(e.symbol===':bypass'){return;\n";
+ jsString += " }else{m._modgui_param_set(e.data.h,ps.indexOf(e.symbol),e.value);}\n";
+ jsString += "}else{\n";
+ jsString += " if(e.symbol===':bypass')return;\n";
+ jsString += " if(e.uri){e.data.p.p[e.uri]=e.value;}else{e.data.p.c[e.symbol]=e.value;}\n";
+ jsString += "}\n\n";
+ jsString += "}\n}\n";
+ jsFile << jsString;
+ jsFile.close();
+ std::cout << " done!" << std::endl;
+ }
+
+ {
+ std::cout << "Writing modgui/icon.html..."; std::cout.flush();
+ std::fstream iconFile("modgui/icon.html", std::ios::out);
+
+ iconFile << "<div class='" DISTRHO_PLUGIN_MODGUI_CLASS_NAME " mod-pedal'>" << std::endl;
+ iconFile << " <div mod-role='drag-handle' class='mod-drag-handle'></div>" << std::endl;
+ iconFile << " <div class='mod-plugin-title'><h1>{{#brand}}{{brand}} | {{/brand}}{{label}}</h1></div>" << std::endl;
+ iconFile << " <div class='mod-light on' mod-role='bypass-light'></div>" << std::endl;
+ iconFile << " <div class='mod-control-group mod-switch'>" << std::endl;
+ iconFile << " <div class='mod-control-group mod-switch-image mod-port transport' mod-role='bypass' mod-widget='film'></div>" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << " <div class='canvas_wrapper'>" << std::endl;
+ iconFile << " <canvas oncontextmenu='event.preventDefault()' tabindex=-1></canvas>" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << " <div class='mod-pedal-input'>" << std::endl;
+ iconFile << " {{#effect.ports.audio.input}}" << std::endl;
+ iconFile << " <div class='mod-input mod-input-disconnected' title='{{name}}' mod-role='input-audio-port' mod-port-symbol='{{symbol}}'>" << std::endl;
+ iconFile << " <div class='mod-pedal-input-image'></div>" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << " {{/effect.ports.audio.input}}" << std::endl;
+ iconFile << " {{#effect.ports.midi.input}}" << std::endl;
+ iconFile << " <div class='mod-input mod-input-disconnected' title='{{name}}' mod-role='input-midi-port' mod-port-symbol='{{symbol}}'>" << std::endl;
+ iconFile << " <div class='mod-pedal-input-image'></div>" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << " {{/effect.ports.midi.input}}" << std::endl;
+ iconFile << " {{#effect.ports.cv.input}}" << std::endl;
+ iconFile << " <div class='mod-input mod-input-disconnected' title='{{name}}' mod-role='input-cv-port' mod-port-symbol='{{symbol}}'>" << std::endl;
+ iconFile << " <div class='mod-pedal-input-image'></div>" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << " {{/effect.ports.cv.input}}" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << " <div class='mod-pedal-output'>" << std::endl;
+ iconFile << " {{#effect.ports.audio.output}}" << std::endl;
+ iconFile << " <div class='mod-output mod-output-disconnected' title='{{name}}' mod-role='output-audio-port' mod-port-symbol='{{symbol}}'>" << std::endl;
+ iconFile << " <div class='mod-pedal-output-image'></div>" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << " {{/effect.ports.audio.output}}" << std::endl;
+ iconFile << " {{#effect.ports.midi.output}}" << std::endl;
+ iconFile << " <div class='mod-output mod-output-disconnected' title='{{name}}' mod-role='output-midi-port' mod-port-symbol='{{symbol}}'>" << std::endl;
+ iconFile << " <div class='mod-pedal-output-image'></div>" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << " {{/effect.ports.midi.output}}" << std::endl;
+ iconFile << " {{#effect.ports.cv.output}}" << std::endl;
+ iconFile << " <div class='mod-output mod-output-disconnected' title='{{name}}' mod-role='output-cv-port' mod-port-symbol='{{symbol}}'>" << std::endl;
+ iconFile << " <div class='mod-pedal-output-image'></div>" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << " {{/effect.ports.cv.output}}" << std::endl;
+ iconFile << " </div>" << std::endl;
+ iconFile << "</div>" << std::endl;
+
+ iconFile.close();
+ std::cout << " done!" << std::endl;
+ }
+
+ {
+ std::cout << "Writing modgui/stylesheet.css..."; std::cout.flush();
+ std::fstream stylesheetFile("modgui/stylesheet.css", std::ios::out);
+
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal{" << std::endl;
+ stylesheetFile << " padding:0;" << std::endl;
+ stylesheetFile << " margin:0;" << std::endl;
+ stylesheetFile << " width:" + String(DISTRHO_UI_DEFAULT_WIDTH) + "px;" << std::endl;
+ stylesheetFile << " height:" + String(DISTRHO_UI_DEFAULT_HEIGHT + 50) + "px;" << std::endl;
+ stylesheetFile << " background:#2a2e32;" << std::endl;
+ stylesheetFile << " border-radius:20px 20px 0 0;" << std::endl;
+ stylesheetFile << " color:#fff;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .canvas_wrapper{" << std::endl;
+ stylesheetFile << " --device-pixel-ratio:1;" << std::endl;
+ stylesheetFile << " /*image-rendering:pixelated;*/" << std::endl;
+ stylesheetFile << " /*image-rendering:crisp-edges;*/" << std::endl;
+ stylesheetFile << " background:#000;" << std::endl;
+ stylesheetFile << " position:absolute;" << std::endl;
+ stylesheetFile << " top:50px;" << std::endl;
+ stylesheetFile << " transform-origin:0 0 0;" << std::endl;
+ stylesheetFile << " transform:scale(calc(1/var(--device-pixel-ratio)));" << std::endl;
+ stylesheetFile << " width:" + String(DISTRHO_UI_DEFAULT_WIDTH) + "px;" << std::endl;
+ stylesheetFile << " height:" + String(DISTRHO_UI_DEFAULT_HEIGHT) + "px;" << std::endl;
+ stylesheetFile << " z-index:21;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "/*" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .canvas_wrapper:focus-within{" << std::endl;
+ stylesheetFile << " z-index:21;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "*/" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-plugin-title{" << std::endl;
+ stylesheetFile << " position:absolute;" << std::endl;
+ stylesheetFile << " text-align:center;" << std::endl;
+ stylesheetFile << " width:100%;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal h1{" << std::endl;
+ stylesheetFile << " font-size:20px;" << std::endl;
+ stylesheetFile << " font-weight:bold;" << std::endl;
+ stylesheetFile << " line-height:50px;" << std::endl;
+ stylesheetFile << " margin:0;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-control-group{" << std::endl;
+ stylesheetFile << " position:absolute;" << std::endl;
+ stylesheetFile << " left:5px;" << std::endl;
+ stylesheetFile << " z-index:35;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-pedal-input," << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-pedal-output{" << std::endl;
+ stylesheetFile << " top:75px;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-audio-input," << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-audio-output{" << std::endl;
+ stylesheetFile << " margin-bottom:25px;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .jack-disconnected{" << std::endl;
+ stylesheetFile << " top:0px!important;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-switch-image{" << std::endl;
+ stylesheetFile << " background-image: url(/img/switch.png);" << std::endl;
+ stylesheetFile << " background-position: left center;" << std::endl;
+ stylesheetFile << " background-repeat: no-repeat;" << std::endl;
+ stylesheetFile << " background-size: auto 50px;" << std::endl;
+ stylesheetFile << " font-weight: bold;" << std::endl;
+ stylesheetFile << " width: 100px;" << std::endl;
+ stylesheetFile << " height: 50px;" << std::endl;
+ stylesheetFile << " cursor: pointer;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-switch-image.off{" << std::endl;
+ stylesheetFile << " background-position: right center !important;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+ stylesheetFile << "." DISTRHO_PLUGIN_MODGUI_CLASS_NAME ".mod-pedal .mod-switch-image.on{" << std::endl;
+ stylesheetFile << " background-position: left center !important;" << std::endl;
+ stylesheetFile << "}" << std::endl;
+
+ stylesheetFile.close();
+ std::cout << " done!" << std::endl;
+ }
+#endif
+
// ---------------------------------------------
#if DISTRHO_PLUGIN_HAS_UI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
diff --git a/distrho/src/DistrhoUILV2.cpp b/distrho/src/DistrhoUILV2.cpp
@@ -715,4 +715,186 @@ const LV2UI_Descriptor* lv2ui_descriptor(uint32_t index)
return (index == 0) ? &sLv2UiDescriptor : nullptr;
}
+#if defined(__MOD_DEVICES__) && defined(__EMSCRIPTEN__)
+#include <emscripten/html5.h>
+#include <string>
+
+typedef void (*_custom_param_set)(uint32_t port_index, float value);
+typedef void (*_custom_patch_set)(const char* uri, const char* value);
+
+struct ModguiHandle {
+ LV2UI_Handle handle;
+ _custom_param_set param_set;
+ _custom_patch_set patch_set;
+};
+
+enum URIs {
+ kUriNull,
+ kUriAtomEventTransfer,
+ kUriDpfKeyValue,
+};
+
+static std::vector<std::string> kURIs;
+
+static LV2_URID lv2_urid_map(LV2_URID_Map_Handle, const char* const uri)
+{
+ for (size_t i=0, size=kURIs.size(); i<size; ++i)
+ {
+ if (kURIs[i] == uri)
+ return i;
+ }
+
+ kURIs.push_back(uri);
+ return kURIs.size() - 1u;
+}
+
+static const char* lv2_urid_unmap(LV2_URID_Map_Handle, const LV2_URID urid)
+{
+ return kURIs[urid].c_str();
+}
+
+static void lv2ui_write_function(LV2UI_Controller controller,
+ uint32_t port_index,
+ uint32_t buffer_size,
+ uint32_t port_protocol,
+ const void* buffer)
+{
+ DISTRHO_SAFE_ASSERT_RETURN(buffer_size >= 1,);
+
+ // d_stdout("lv2ui_write_function %p %u %u %u %p", controller, port_index, buffer_size, port_protocol, buffer);
+ ModguiHandle* const mhandle = static_cast<ModguiHandle*>(controller);
+
+ switch (port_protocol)
+ {
+ case kUriNull:
+ mhandle->param_set(port_index, *static_cast<const float*>(buffer));
+ break;
+ case kUriAtomEventTransfer:
+ if (const LV2_Atom* const atom = static_cast<const LV2_Atom*>(buffer))
+ {
+ // d_stdout("lv2ui_write_function %u %u:%s", atom->size, atom->type, kURIs[atom->type].c_str());
+
+ // if (kURIs[atom->type] == "urn:distrho:KeyValueState")
+ {
+ const char* const key = (const char*)(atom + 1);
+ const char* const value = key + (std::strlen(key) + 1U);
+ // d_stdout("lv2ui_write_function %s %s", key, value);
+
+ String urikey;
+ urikey = DISTRHO_PLUGIN_URI "#";
+ urikey += key;
+
+ mhandle->patch_set(urikey, value);
+ }
+ }
+ break;
+ }
+}
+
+static void app_idle(void* const handle)
+{
+ static_cast<UiLv2*>(handle)->lv2ui_idle();
+}
+
+DISTRHO_PLUGIN_EXPORT
+LV2UI_Handle modgui_init(const char* const className, _custom_param_set param_set, _custom_patch_set patch_set)
+{
+ d_stdout("init \"%s\"", className);
+ DISTRHO_SAFE_ASSERT_RETURN(className != nullptr, nullptr);
+
+ static LV2_URID_Map uridMap = { nullptr, lv2_urid_map };
+ static LV2_URID_Unmap uridUnmap = { nullptr, lv2_urid_unmap };
+
+ // known first URIDs, matching URIs
+ if (kURIs.empty())
+ {
+ kURIs.push_back("");
+ kURIs.push_back("http://lv2plug.in/ns/ext/atom#eventTransfer");
+ kURIs.push_back(DISTRHO_PLUGIN_LV2_STATE_PREFIX "KeyValueState");
+ }
+
+ static float sampleRateValue = 48000.f;
+ static LV2_Options_Option options[3] = {
+ {
+ LV2_OPTIONS_INSTANCE,
+ 0,
+ uridMap.map(uridMap.handle, LV2_PARAMETERS__sampleRate),
+ sizeof(float),
+ uridMap.map(uridMap.handle, LV2_ATOM__Float),
+ &sampleRateValue
+ },
+ {}
+ };
+
+ static const LV2_Feature optionsFt = { LV2_OPTIONS__options, static_cast<void*>(options) };
+ static const LV2_Feature uridMapFt = { LV2_URID__map, static_cast<void*>(&uridMap) };
+ static const LV2_Feature uridUnmapFt = { LV2_URID__unmap, static_cast<void*>(&uridUnmap) };
+
+ static const LV2_Feature* features[] = {
+ &optionsFt,
+ &uridMapFt,
+ &uridUnmapFt,
+ nullptr
+ };
+
+ ModguiHandle* const mhandle = new ModguiHandle;
+ mhandle->handle = nullptr;
+ mhandle->param_set = param_set;
+ mhandle->patch_set = patch_set;
+
+ LV2UI_Widget widget;
+ const LV2UI_Handle handle = lv2ui_instantiate(&sLv2UiDescriptor,
+ DISTRHO_PLUGIN_URI,
+ "", // bundlePath
+ lv2ui_write_function,
+ mhandle,
+ &widget,
+ features);
+ mhandle->handle = handle;
+
+ static_cast<UiLv2*>(handle)->lv2ui_show();
+ emscripten_set_interval(app_idle, 1000.0/60, handle);
+
+ return mhandle;
+}
+
+DISTRHO_PLUGIN_EXPORT
+void modgui_param_set(const LV2UI_Handle handle, const uint32_t index, const float value)
+{
+ lv2ui_port_event(static_cast<ModguiHandle*>(handle)->handle, index, sizeof(float), kUriNull, &value);
+}
+
+DISTRHO_PLUGIN_EXPORT
+void modgui_patch_set(const LV2UI_Handle handle, const char* const uri, const char* const value)
+{
+ static const constexpr uint32_t URI_PREFIX_LEN = sizeof(DISTRHO_PLUGIN_URI);
+ DISTRHO_SAFE_ASSERT_RETURN(std::strncmp(uri, DISTRHO_PLUGIN_URI "#", URI_PREFIX_LEN) == 0,);
+
+ const uint32_t keySize = std::strlen(uri + URI_PREFIX_LEN) + 1;
+ const uint32_t valueSize = std::strlen(value) + 1;
+ const uint32_t atomSize = sizeof(LV2_Atom) + keySize + valueSize;
+
+ LV2_Atom* const atom = static_cast<LV2_Atom*>(std::malloc(atomSize));
+ atom->size = atomSize;
+ atom->type = kUriDpfKeyValue;
+
+ std::memcpy(static_cast<uint8_t*>(static_cast<void*>(atom + 1)), uri + URI_PREFIX_LEN, keySize);
+ std::memcpy(static_cast<uint8_t*>(static_cast<void*>(atom + 1)) + keySize, value, valueSize);
+
+ lv2ui_port_event(static_cast<ModguiHandle*>(handle)->handle,
+ DISTRHO_PLUGIN_NUM_INPUTS + DISTRHO_PLUGIN_NUM_OUTPUTS, // events input port
+ atomSize, kUriAtomEventTransfer, atom);
+
+ std::free(atom);
+}
+
+DISTRHO_PLUGIN_EXPORT
+void modgui_cleanup(const LV2UI_Handle handle)
+{
+ d_stdout("cleanup");
+ lv2ui_cleanup(static_cast<ModguiHandle*>(handle)->handle);
+ delete static_cast<ModguiHandle*>(handle);
+}
+#endif
+
// -----------------------------------------------------------------------
diff --git a/examples/Info/CMakeLists.txt b/examples/Info/CMakeLists.txt
@@ -3,6 +3,7 @@
dpf_add_plugin(d_info
TARGETS jack lv2 vst2 vst3 clap
+ MODGUI_CLASS_NAME distrho_examples_info
FILES_DSP
InfoExamplePlugin.cpp
FILES_UI
diff --git a/examples/Info/DistrhoPluginInfo.h b/examples/Info/DistrhoPluginInfo.h
@@ -26,6 +26,7 @@
#define DISTRHO_PLUGIN_IS_RT_SAFE 1
#define DISTRHO_PLUGIN_NUM_INPUTS 2
#define DISTRHO_PLUGIN_NUM_OUTPUTS 2
+#define DISTRHO_PLUGIN_USES_MODGUI 1
#define DISTRHO_PLUGIN_WANT_TIMEPOS 1
#define DISTRHO_UI_DEFAULT_WIDTH 405
#define DISTRHO_UI_DEFAULT_HEIGHT 256
@@ -36,10 +37,6 @@
// only checking if supported, not actually used
#define DISTRHO_PLUGIN_WANT_PARAMETER_VALUE_CHANGE_REQUEST 1
-#ifdef __MOD_DEVICES__
-#define DISTRHO_PLUGIN_USES_MODGUI 1
-#endif
-
enum Parameters {
kParameterBufferSize = 0,
kParameterCanRequestParameterValueChanges,
diff --git a/examples/Info/Makefile b/examples/Info/Makefile
@@ -18,6 +18,9 @@ FILES_DSP = \
FILES_UI = \
InfoExampleUI.cpp
+# require for modgui builds
+MODGUI_CLASS_NAME = distrho_examples_info
+
# --------------------------------------------------------------
# Do some magic