Skip to content
jariseon edited this page Oct 4, 2018 · 2 revisions

Porting OBXD JUCE Plugin to the web

This page describes the steps i took when porting OBXD to the web. There are five steps in all, but luckily none of this is rocket science. It should be possible to adapt this procedure in other JUCE plugin porting efforts.

1. WAM wrapper

WAM wrapper operates as a bridge between JUCE and Web Audio API AudioWorkletProcessor. Its implementation is in WAM/cpp/obxd.cpp and WAM/cpp/obxd.h. The code is commented, and its methods map almost directly to juce::PluginProcessor counterparts.

2. changes to OBXD source code

only two files were modified.

  1. opengl module was commented out from JuceLibraryCode/JuceHeader.h
  2. Source/PluginProcessor.cpp contains three modifications:
  • processBlock() was upgraded to JUCE 5
  • createEditor() returns nullptr because GUI is implemented using web technologies
  • setCurrentProgramStateInformation() uses raw data without XML wrapping

after these modifications OBXD cpp integration was done, and it was possible to build the WASM module as described in the building section (see README in main repo).

Note: if you are porting another JUCE plugin, just modify TARGET, SOURCES, and EXPORT_NAME of JSFLAGS in the Makefile and in build.sh, accordingly. The idea is to namespace classes so that they can coexist with other WAMs. For example, to port myplugin, replace each occurrence of

  • obxd with myplugin in Makefile TARGET and build.sh
  • OBXD with MYPLUGIN in Makefile EXPORT_NAME and build.sh
  • follow the same logic in obxd.js and obxd-awp.js (see below)

3. interfacing Web Audio API

WAM runs as a Web Audio API AudioWorklet, which consists of two classes: AudioWorkletProcessor runs hardcore DSP inside browser's audio thread, while AudioWorkletNode exposes a JS interface to the processor and its GUI on browser's main thread.

WAM/web/worklet/obxd-awp.js inherits WAMProcessor (which in turn inherits AudioWorkletProcessor) to hide most complexity involved when interfacing a WASM module. The constructor in OBXD simply passes WASM module to its superclass, and defines its audio io configuration.

WAM/web/obxd.js is more involved. It derives from WAMController (which in turn inherits AudioWorkletNode) to implement the main thread counterpart of the processor. Here are its main highlights:

  • importScripts() loads scripts into AudioWorkletGlobalScope, thereby initializing the processor
  • loadBank() loads and parses VST fxb bank into raw preset data
  • selectPatch() and setPatch() change current preset
  • current preset persists with get/setState()

See code comments for more info. After implementing these methods, a headless OBXD node is ready to be inserted into the Web Audio API audio graph.

4. integration into a web page

WAM/web/index.html first calls importScripts, creates an OBXD WAMController instance in the main thread, and connects it into the Web Audio API graph. It then loads the frontpanel GUI (see below), creates a virtual midi keyboard and inserts those into DOM. Finally, the code loads a preset bank and selects a patch from it.

5. GUI

Current JUCE WASM library implementation does not support JUCE Components and friends. I reimplemented OBXD frontpanel from scratch using web technologies, reusing native png assets for background, knobs and toggles. The frontpanel is implemented in WAM/web/obxd.html using templated Custom HTML Elements. JUCE knob and toggle widgets are reimplemented in WAM/web/libs/wam-knob.js and WAM/web/libs/wam-toggle.js classes. widgets are initialized to current patch state in wam-obxd.setPatch() method. user interaction triggers wam-obxd._oncontrol method calls, which are transformed into obxd.setParam() calls, and eventually routed to the processor c++ implementation's setParam() method.

conclusion

OBXD DSP was remarkably easy to port. Most time was spent with GUI reimplementation. although work is underway to also streamline JUCE GUI porting, it might not be a bad idea to consider implementing native JUCE GUIs using webviews.