Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebAssembly (wasm) target for scsynth (rebased) #5571

Open
wants to merge 45 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
da79abc
Rebase against latest dev
Sciss Dec 21, 2020
c0d7801
step: some ugens compile (?)
Sciss Dec 21, 2020
39d761f
step
Sciss Dec 22, 2020
db1079d
disable boost shared memory
Sciss Dec 22, 2020
d575af2
pretend the file system is unix
Sciss Dec 22, 2020
ae303db
update readme
Sciss Dec 22, 2020
8716cdb
thoughts on how to add the new Audio Driver
Sciss Dec 22, 2020
3bb7d94
step. add blank SC_WebAudio.cpp ; change 'openal' to 'webaudio'
Sciss Dec 22, 2020
e98cdfc
update submodules
Sciss Dec 22, 2020
ca8ba10
step: dummy impl of SC_WebAudio.cpp
Sciss Dec 23, 2020
79580e4
add HTML target
Sciss Dec 23, 2020
b5e74bd
try to run scsynth with args
Sciss Dec 23, 2020
0fee496
the hairy stuff starts
Sciss Dec 23, 2020
5363920
fix linker/boost problem
Sciss Dec 23, 2020
2eb2d38
giving up on cmake
Sciss Dec 26, 2020
2977d55
step; disable assertion in boost
Sciss Dec 26, 2020
91ff7e7
try with Embind. we get sr and buffer size
Sciss Dec 26, 2020
3c6e574
store audio context
Sciss Dec 26, 2020
b40c8e6
step. allow omission of -u, -t; set em main loop
Sciss Dec 27, 2020
c0b6f2e
explicit UI boot
Sciss Dec 27, 2020
8b8e5fe
add print-error-to-console hook
Sciss Dec 29, 2020
fa0dc84
driver setup ok from web audio point of view
Sciss Dec 29, 2020
8074701
begin main-loop analogous to Jack and PortAudio
Sciss Dec 29, 2020
81b1444
we may have a running driver -- but can't test yet without osc
Sciss Dec 30, 2020
bbcc7c9
introduce WebInPort (dummy for now)
Sciss Dec 30, 2020
9777a98
audio and osc basically (almost) working. shared-array-buffer prob
Sciss Dec 30, 2020
d9f7246
example OSC messages; UGens are not 'installed'!
Sciss Dec 30, 2020
ba410c1
plugins are statically linking!
Sciss Dec 30, 2020
5e76c6f
undo debug printing; update read-me
Sciss Dec 30, 2020
b697941
improve README_WASM.md; continue with SC_WebPortIn reply
Sciss Dec 31, 2020
901493e
thought on web_reply_func
Sciss Dec 31, 2020
bddef2c
implement OSC replies
Sciss Dec 31, 2020
ffa88bf
audio input works
Sciss Jan 1, 2021
ca5971a
typo
Sciss Jan 1, 2021
eefea04
fix bug in several DelayUGens Ctors
Sciss Jan 1, 2021
f6183f4
prepare for clang-format; web-port-in destructor
Sciss Jan 1, 2021
f33e326
Format with clang-format
Sciss Jan 1, 2021
d87808b
clang-format-8 instead of -9
Sciss Jan 1, 2021
b12dc73
fork of nova-tt no longer needed
Sciss Jan 1, 2021
a0a8ef8
web-port-in: unused ip is not a 'todo'
Sciss Jan 1, 2021
97b0e7e
reenable boost assertion (no longer failing)
Sciss Jan 1, 2021
1324761
remove todo comments from CMakeLists.txt
Sciss Jan 1, 2021
b390f10
more symmetric OSC interface
Sciss Jan 2, 2021
eec5073
increase static heap size
Sciss Feb 4, 2021
64cabdb
remove updates to various submodules
dylans Sep 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ package/*.asc
.idea
cmake-build-*

# VS Code/Codium files
.vscode/

# Python/PyCharm files
venv
__pycache__
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ else() # ARM platforms do not have SSE
set(SSE2 OFF)
endif()

set(AUDIOAPI "default" CACHE STRING "Audio API to use (one of {default,coreaudio,jack,portaudio,bela})")
set(AUDIOAPI "default" CACHE STRING "Audio API to use (one of {default,coreaudio,jack,portaudio,bela|openal|webaudio})")

if (AUDIOAPI STREQUAL jack)
# here we check for JACK metadata API
Expand Down Expand Up @@ -255,7 +255,7 @@ endif()
#############################################
# some default libraries

if (NOT WIN32)
if ((NOT WIN32) AND (NOT EMSCRIPTEN))
find_package(Pthreads)

if (NOT PTHREADS_FOUND)
Expand Down
222 changes: 222 additions & 0 deletions README_WASM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# SuperCollider on WebAssembly

This file documents the current status on getting the project to build for [WebAssembly](https://webassembly.org/) (wasm).

## Introduction

WebAssembly is a near-native format for running code on the client-side of the browser. That is, similar to JavaScript for
which a runtime environment is provided virtually by every mainstream browser, this relatively new standard competes in a
similar space, allowing for more performance oriented code to be executed and targetting lower-level languages such as
C or C++.

Compiling SuperCollider to wasm means that a real-time generative sound piece can run in a user's browser without
the need to install any software or to set up a streaming server. Other computer music systems like PD and CSound have also
explored this possibility. Essentially, one takes an existing code base and adapts it to use the
[Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) where
normally a driver such as Port Audio or Jack would sit. Any interactive "control" of the application then goes through
JavaScript which can bridge to the wasm application.

As a side note, wasm can also run stand-alone in a native container outside of a browser, which may or may not be,
depending on the application, an interesting format to compile to, as it allows to deliver a binary in a cross-platform manner.
Since SuperCollider depends on maximising performance, this avenue is currently not explored, but it may be interesting
for processor architectures which are otherwise not supported by the native machine code builds.

## Overview

Current, the SuperCollider __server__ (scsynth) can be embedded using the WebAssembly architecture. In the future, this
may extend to __sclang__ as well (see 'clients' section).

The build process is similar to the regular CMake build, but using the [Emscripten SDK](https://emscripten.org) instead.
Emscripten allows to build C and C++ applications through a customised LLVM compiler (Clang), emitting wasm binaries.

The following limitations are currently in place:

- _libsndfile_ is disabled, thus accessing sound files and using UGens such as `DiskIn` does not work. This can be
solved in the future.
- the X11-based UGens, `MouseX`, `MouseY`, etc., are not available.
- the deprecated [ScriptProcessorNode](https://developer.mozilla.org/en-US/docs/Web/API/ScriptProcessorNode) Web Audio API
is used, which means that the DSP loop runs in the main JavaScript thread, being prone to drop outs as the browser's
user interface is used. In the future, one might look into using
[AudioWorkletProcessor](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor) -- but this complicates
the build process, as need to generate a dedicated wasm binary per thread, which then need to communicate with each
other. Emscripten already uses different workers for the
phtreads, and it is unclear how pthreads and audio worklets would coexist.
- enabling _pthreads_ (the standard UNIX threads used by regular scsynth) means that a so-called `SharedArrayBuffer`
is used for the static heap memory of the application. This feature is enabled on Chrome/Chromium, but in Firefox it
is disabled for security reasons _unless_ your site sets [COOP and COEP](https://web.dev/coop-coep/) for strong isolation.
That means making the wasm application run in [different browsers](https://caniuse.com/sharedarraybuffer) may pose
additional efforts in terms of configuring the web server.
- the web [AudioContext](https://developer.mozilla.org/en-US/docs/Web/API/AudioContext) only does processing when there
has been _user interaction_, in order to avoid that malicious websites bombard the user with sound without prior
permission. This is solved by requiring the user to press a 'boot' button first, for example.

## Building

One needs to install [emscripten](https://emscripten.org), see
[download instructions](https://emscripten.org/docs/getting_started/downloads.html).
The installation described there should work:

$ ./emsdk install latest
$ ./emsdk activate latest
$ source ./emsdk_env.sh

Then you prefix `cmake` by `emcmake`, e.g.:

$ emcmake cmake -DSC_EL=no -DSUPERNOVA=no ... -s USE_PTHREADS=1 -s WASM=1 --target scsynth ..

The full set of flags and settings for emcmake can be seen in the provided build scripts:

- `./wasm/configure.sh` runs emcmake with reasonable parameters
- `./wasm/make.sh` runs the 'make' task for target 'scsynth'.

Both scripts are called subsequently when running `build.sh`. For example:

$ mkdir build
$ cd build
$ ../wasm/build.sh

Technically, what is built is `server/scsynth/scsynth.*` with a runtime library `*.js`, the actual binary `*.wasm`
and a wrapping HTML `*.html`. The standard plugins (UGens) are statically linked and thus available.

When successful, the `make.sh` script also takes care of copying the .js and .wasm files emitted by the process
from `server/scsynth/` to `../wasm/`. In other words, the `wasm` directory contains everything necessary to
execute scsynth in the browser (see section 'Running').

_Note:_ Sometimes, when running `make.sh` for a while, there appears an error:

> System LLVM compiler cannot be used to build with Emscripten! Check

The reason for this is unclear at the moment -- removing and recreating the `build` directory and running the full
build again "solves" this.

## Running

The file `wasm/index.html` takes care of loading scsynth. It can be used as a template for more customised running.
To use this index, start a local webserver inside the `wasm` directory, for example:

$ cd ../wasm/
$ python -m SimpleHTTPServer

Then open the server's address, e.g. `127.0.0.1:8000`. in the browser (note: if you use `0.0.0.0`, Chromium will
not allow microphone access).

Emscripten uses a JavaScript object named `Module` to contain all information on the wasm binary. For example,
`Module.arguments` can be used to specify the command line arguments for scsynth. By default, we set
`-u 57110 -D 0 -i 0 -o 2`; scsynth.wasm uses the UDP switch to open an OSC communication for the client. This is not
a regular UDP connection, but a more simple mechanism (see 'Clients' section). The `-D 0` switch prevents scsynth
from trying to initially "load" synthdefs from a file system which does not exist in this way in the browser.
If you want to access the microphone input, you would set for example `-i 2`.

The `index.html` includes a [JavaScript OSC library](https://github.com/colinbdclark/osc.js) and defines a few utility functions:

sendOSC(name, args...)

sendOSC_t(name, typeTags, args...)

They can be used to talk to the server after it has been booted. For example, you can print the node tree:

sendOSC("/g_dumpTree", 0, 1)

or

sendOSC_t("/g_dumpTree", "ii", 0, 1)

Some of the SuperCollider OSC API works with either float `"f"` or int `"i"` arguments, and the `sendOSC` treats all numbers
as floats. However, some commands do not work properly without int arguments, therefore it is preferred to use the
`sendOSC_t` function and explicitly specifiy the type tags.

d_bubbles()

Sends a `/d_recv` command with the well-known `"AnalogBubbles"` example by James McCartney. You can then start a synth using:

s_bubbles()

which is equivalent to:

sendOSC_t("/s_new", "siii", "AnalogBubbles", 1000, 1, 0)

If you want to use other synthdefs, you have to compile them to their binary blobs (as `Uint8rray`) and send them like this:

sendOSC_t("/d_recv", "b", blob)

The blob can be obtained for example from sclang:

SynthDef("gcd", {
var mx = LFNoise1.kr(0.1).range(-20, 20);
var my = LFNoise1.kr(0.1).range(-20, 20);
var sn = SinOsc.ar(SinOsc.kr(0.1) * 20 lcm: [mx, my] * 30 + 500) * 0.1;
Out.ar(0, sn);
}).asBytes.printOn(Post); nil

Copying the result from the post window and replacing `Int8Array[ ... ]` by `Uint8Array.from([ ... ])`.

To enable OSC dump:

dumpOSC(1)

To set controls on a synth:

sendOSC_t("/n_set", "isf", 1000, "freq2", 444.1)

To free the synth:

sendOSC_t("/n_free", "i", 1000)

See the 'Server Command Reference' help file for an overview of OSC commands.

## Clients

A [client](https://supercollider.github.io/community/systems-interfacing-with-sc) currently has to communicate to scsynth via
the `Module` JavaScript object, and while in the future an `sclang.wasm` is possible, clients for now are to be running in
JavaScript (or be written in a language that can compile to JavaScript). While one can write directly JS code that uses the
OSC interface, more viable candidates may be:

- [ScalaCollider](https://www.sciss.de/scalaCollider/) -- while it cross-compiles to JavaScript, the OSC interface will have be
to adapted
- [supercollider.js](https://crucialfelix.github.io/supercolliderjs/) -- this was designed to run in Node.js and communicate with
a regular scsynth process, but probably it will be simple to adapt it for usage with scsynth.wasm.
- probably clients written in Python, like [supriya](https://github.com/josiah-wolf-oberholtzer/supriya), and in
[other languages](https://github.com/jashkenas/coffeescript/wiki/List-of-languages-that-compile-to-JS) could potentially
run in the browser as well
- [Processing](https://processing.org/) may also be an interesting option to communicate with scsynth.wasm.

scsynth.wasm is started with a port number (defaults to 57110) for OSC communication.
The interface is available through `Module.oscDriver`:

var od = Module.oscDriver;
od[server].receive(client, data);

Where `client` and `server` are virtual port numbers, for example `57120` and `57110` respectively. The client or source port number
allows to receive OSC replies from the server. To do so, an OSC end-point function must be registered:

od[57120] = { receive: function(server, data) { console.log("Received data from " + server) }};

The OSC data is always a plain `Uint8Array` which must be properly encoded and decoded, for example in JavaScript using the
[osc.js](https://github.com/colinbdclark/osc.js/) library mentioned above (`osc.writePacket()`, `osc.readPacket()`). You can
look at the definition of `sendOSC_t` in `index.html` to see how package encoding works. For example:

od[57120] = { receive: function(addr, data) {
var msg = osc.readPacket(data, {});
console.log("REPLY from " + addr + ": " + JSON.stringify(msg, null, 4));
}};

## Source Code

The main additions to the SuperCollider C++ source code are the classes `SC_WebAudioDriver` (file `server/scsynth/SC_WebAudio.cpp`)
and `SC_WebInPort` (in file `server/scsynth/SC_ComPort.cpp`), implementing the Web Audio API driver bridge and the OSC replacement
respectively. Throughout, conditional compilation is done by preprocessor `#ifdef` and `#ifndef` statements using the symbol
`__EMSCRIPTEN__`. In the file `server/scsynth/CMakeList.txt`, the symbol to look out for is `EMSCRIPTEN`.

Currently, dynamic heap growth is disabled, as it implies a couple of problems with sharing memory between WASM and JavaScript
(address pointers may change). That is, the total heap size is specified at build time. This is done in the `CMakeList.txt` with
flag `-s INITIAL_MEMORY=<size>`. With default settings, scsynth runs at 32 MB heap memory. Currently 64 MB are specified to allow
for non-trivial allocation of buffers. If scsynth runs out of heap memory at runtime, an `OOM` exception is thrown.

## Performance Considerations

The current build is not optimised for speed or code size. This is a task for the future.

`-s DISABLE_EXCEPTION_CATCHING=0` -- this allegedly causes performance issues, so should eventually be removed again.

Disabling the catching means we can have meaningful exception message ('what') print-outs in the browser console. This is
usually needed when trying to understand why something crashed.
2 changes: 1 addition & 1 deletion common/SC_Filesystem_unix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
/*
* SC_Filesystem implementation for Linux/FreeBSD/OpenBSD.
*/
#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__)
#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__EMSCRIPTEN__)

# include "SC_Filesystem.hpp"

Expand Down
5 changes: 5 additions & 0 deletions common/SC_OscUtils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ static void dumpOSC(int mode, int size, char* inData) {
static void DumpReplyAddress(ReplyAddress *inReplyAddress)
{
scprintf("mAddress %s\n", inReplyAddress->mAddress.to_string().c_str());
# ifdef __EMSCRIPTEN__
if (inReplyAddress->mProtocol == kWeb)
scprintf("mProtocol Web\n");
else
# endif
if (inReplyAddress->mProtocol == kUDP)
scprintf("mProtocol UDP\n");
else
Expand Down
9 changes: 8 additions & 1 deletion common/SC_ReplyImpl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@

#include <boost/asio.hpp>

enum Protocol { kUDP, kTCP };
enum Protocol {
kUDP,
kTCP
#ifdef __EMSCRIPTEN__
,
kWeb
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have documentation for kWeb protocols? What does this look like? Limitations?

};

struct ReplyAddress {
boost::asio::ip::address mAddress;
Expand Down
7 changes: 7 additions & 0 deletions external_libraries/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ if(NOT SYSTEM_BOOST) # we compile boost ourselves
aux_source_directory(boost/libs/thread/src/win32/ boost_thread_platform )
elseif(PTHREADS_FOUND)
aux_source_directory(boost/libs/thread/src/pthread/ boost_thread_platform )
elseif(EMSCRIPTEN)
aux_source_directory(boost/libs/thread/src/pthread/ boost_thread_platform )
else()
message(SEND_ERROR "we need to implement boost_thread")
endif()
Expand Down Expand Up @@ -48,6 +50,11 @@ if(NOT SYSTEM_BOOST) # we compile boost ourselves
APPEND PROPERTY LINK_FLAGS "-flto -flto-report")
endif()

if(EMSCRIPTEN)
set_property(TARGET boost_program_options_lib boost_system_lib boost_filesystem_lib
APPEND PROPERTY COMPILE_FLAGS "-pthread -s USE_PTHREADS=1 -s WASM=1")
endif()

add_library(boost_thread_lib STATIC EXCLUDE_FROM_ALL ${boost_thread_src} ${boost_thread_platform})
target_link_libraries(boost_thread_lib boost_system_lib)
# target_compile_definitions(boost_thread PUBLIC BOOST_THREAD_DONT_PROVIDE_INTERRUPTIONS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
//////////////////////////////////////////////////////
//Check for XSI shared memory objects. They are available in nearly all UNIX platforms
//////////////////////////////////////////////////////
#if !defined(__QNXNTO__) && !defined(__ANDROID__) && !defined(__HAIKU__) && !(__VXWORKS__)
#if !defined(__QNXNTO__) && !defined(__ANDROID__) && !defined(__HAIKU__) && !(__VXWORKS__) && !defined(__EMSCRIPTEN__)
#define BOOST_INTERPROCESS_XSI_SHARED_MEMORY_OBJECTS
#endif

Expand Down
5 changes: 5 additions & 0 deletions include/common/SC_Endian.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
# include <endian.h>
# include <netinet/in.h>

#elif defined(__EMSCRIPTEN__)

# include <endian.h>
# include <netinet/in.h>

#else

# error cannot find endianess on this platform
Expand Down
4 changes: 3 additions & 1 deletion lang/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,14 @@ if(AUDIOAPI STREQUAL "default")
set(AUDIOAPI coreaudio)
elseif(WIN32)
set(AUDIOAPI portaudio)
elseif(EMSCRIPTEN)
set(AUDIOAPI webaudio)
else()
set(AUDIOAPI jack)
endif(APPLE)
endif()

if(NOT AUDIOAPI MATCHES "^(jack|coreaudio|portaudio|bela)$")
if(NOT AUDIOAPI MATCHES "^(jack|coreaudio|portaudio|bela|openal|webaudio)$")
message(FATAL_ERROR "Unrecognised audio API: ${AUDIOAPI}")
endif()

Expand Down
1 change: 1 addition & 0 deletions server/plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -343,4 +343,5 @@ endif()
set_property(TARGET ${plugins} ${supernova_plugins} PROPERTY FOLDER UnitGenerators)

set(plugins "${plugins}" PARENT_SCOPE)
set(plugin_sources "${plugin_sources}" PARENT_SCOPE)
set(supernova_plugins "${supernova_plugins}" PARENT_SCOPE)