Skip to content

Commit

Permalink
Merge pull request #2 from jariseon/master
Browse files Browse the repository at this point in the history
audioworklet / wasm version
  • Loading branch information
jariseon authored Mar 29, 2018
2 parents ce77bd7 + fd0e76b commit d056aa3
Show file tree
Hide file tree
Showing 59 changed files with 1,170 additions and 17,008 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/dx7/presets/*.syx
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,42 @@
# webdx7
virtual Yamaha DX7 synth in a browser
# webdx7 (AudioWorklet/WASM edition)
virtual Yamaha DX7 synth in a browser.

[demo](https://webaudiomodules.org/wamsynths/dx7)

other WAM demos at [webaudiomodules.org/wamsynths](https://webaudiomodules.org/wamsynths/)

Please note that low latency AudioWorklets require [Chrome Canary 64](https://www.google.com/chrome/browser/canary.html) (or later) and setting a flag as explained [here](https://googlechromelabs.github.io/web-audio-samples/audio-worklet/). Other stable browsers are enabled with this [polyfill](https://github.com/jariseon/audioworklet-polyfill).

## info
This repo contains a work-in-progress implementation of webdx7 in WebAssembly. The binary runs in AudioWorklet. webdx7 is built on top of [Web Audio Modules (WAMs) API](https://webaudiomodules.org), which is currently extended to support AudioWorklets and WebAssembly.

The code here includes pure hacks to work around limitations in current AudioWorklet browser implementation, and should definitely not be considered best practice :) WAMs API will be updated as AudioWorklets mature.

## prerequisites
* WASM [toolchain](http://webassembly.org/getting-started/developers-guide/)
* [node.js](https://nodejs.org/en/download/)

## building

### #1 wasm compilation
```
cd build
export PATH=$PATH:/to/emsdk/where/emmake/resides
emmake make
```
step #1 creates two files, **dx7.wasm** and **dx7.js**. WASM binary cannot currently be loaded into AudioWorkletProcessor (AWP) directly, so let's encode it into a JS Uint8Array in step #2.

### #2 encoding
```
node encode-wasm.js dx7.wasm
```
step #2 produces **dx7.wasm.js** file, which can be loaded into AWP.


### done
We have now **dx7.wasm.js** (from step #2) and its loader **dx7.js** (from step #1). Copy these files to `dist/dx7/wasm` folder, and copy some DX7 sysex files into `dist/dx7/presets`. See readme there for instructions.

Finally open `dist/dx7.html` in a WASM-enabled browser and enjoy cool authentic FM sounds straight in browser. Works with MIDI and embedded virtual keyboard.



more at http://webaudiomodules.org
18 changes: 18 additions & 0 deletions build/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Web Audio Modules
# wasm makefile for msfa DX7

TARGET = ./dx7.js
API = ../src/wamsdk
MSFA = ../src/c/msfa

SRC = ../src/c/dx7.cc $(API)/processor.cpp \
$(MSFA)/synth_unit.cc $(MSFA)/ringbuffer.cc $(MSFA)/patch.cc \
$(MSFA)/lfo.cc $(MSFA)/dx7note.cc $(MSFA)/freqlut.cc $(MSFA)/sin.cc $(MSFA)/exp2.cc \
$(MSFA)/fm_core.cc $(MSFA)/pitchenv.cc $(MSFA)/env.cc $(MSFA)/fm_op_kernel.cc

CFLAGS = -I$(API) -I$(MSFA) -Wno-logical-op-parentheses
LDFLAGS = -O2
JSFLAGS = -s ALLOW_MEMORY_GROWTH=1 -s WASM=1 -s BINARYEN_ASYNC_COMPILATION=0 -s EXPORT_NAME="'AudioWorkletGlobalScope.WAM.DX7'"

$(TARGET): $(OBJECTS)
$(CC) $(CFLAGS) $(LDFLAGS) $(JSFLAGS) -o $@ $(SRC)
17 changes: 17 additions & 0 deletions build/encode-wasm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
if (process.argv.length != 3) {
console.log("usage: node encode-wasm.js mymodule.wasm");
return;
}

let wasmName = process.argv[2];
let name = wasmName.substr(0, wasmName.length - 5).toUpperCase();

// thanks to Steven Yi / Csound
//
fs = require('fs');
let wasmData = fs.readFileSync(wasmName);
let wasmStr = wasmData.join(",");
let wasmOut = "AudioWorkletGlobalScope.WAM = AudioWorkletGlobalScope.WAM || {}\n";
wasmOut += "AudioWorkletGlobalScope.WAM." + name + " = { ENVIRONMENT: 'WEB' }\n";
wasmOut += "AudioWorkletGlobalScope.WAM." + name + ".wasmBinary = new Uint8Array([" + wasmStr + "]);";
fs.writeFileSync(wasmName + ".js", wasmOut);
136 changes: 136 additions & 0 deletions dist/audioworker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// AudioWorklet polyfill
// Jari Kleimola 2017-18 (jari@webaudiomodules.org)
//
var AWGS = { processors:[] }

// --------------------------------------------------------------------------
//
//
AWGS.AudioWorkletGlobalScope = function () {
var ctors = {}; // node name to processor definition map

function registerOnWorker(name, ctor) {
if (!ctors[name]) {
ctors[name] = ctor;
postMessage({ type:"register", name:name, descriptor:ctor.parameterDescriptors });
}
else {
postMessage({ type:"state", node:nodeID, state:"error" });
throw new Error("AlreadyRegistered");
}
};

function constructOnWorker (name, port, options) {
if (ctors[name]) {
options = options || {}
options._port = port;
var processor = new ctors[name](options);
if (!(processor instanceof AudioWorkletProcessor)) {
postMessage({ type:"state", node:nodeID, state:"error" });
throw new Error("InvalidStateError");
}
return processor;
}
else {
postMessage({ type:"state", node:nodeID, state:"error" });
throw new Error("NotSupportedException");
}
}

class AudioWorkletProcessorPolyfill {
constructor (options) { this.port = options._port; }
process (inputs, outputs, params) {}
}

return {
'AudioWorkletProcessor': AudioWorkletProcessorPolyfill,
'registerProcessor': registerOnWorker,
'_createProcessor': constructOnWorker
}
}


AudioWorkletGlobalScope = AWGS.AudioWorkletGlobalScope();
AudioWorkletProcessor = AudioWorkletGlobalScope.AudioWorkletProcessor;
registerProcessor = AudioWorkletGlobalScope.registerProcessor;
sampleRate = 44100;
hasSAB = true;

onmessage = function (e) {
var msg = e.data;
switch (msg.type) {

case "init":
sampleRate = AudioWorkletGlobalScope.sampleRate = msg.sampleRate;
break;

case "import":
importScripts(msg.url);
postMessage({ type:"load", url:msg.url });
break;

case "createProcessor":
// -- slice io to match with SPN bufferlength
var a = msg.args;
var slices = [];
var buflen = 128;
var numSlices = (a.options.samplesPerBuffer/buflen)|0;

hasSAB = (a.bufferCount === undefined);
if (hasSAB) {
for (var i=0; i<numSlices; i++) {
var sliceStart = i * buflen;
var sliceEnd = sliceStart + buflen;

// -- create io buses
function createBus (buffers) {
var ports = [];
for (var iport=0; iport<buffers.length; iport++) {
var port = [];
for (var channel=0; channel<buffers[iport].length; channel++) {
var buf = new Float32Array(buffers[iport][channel]);
port.push(buf.subarray(sliceStart, sliceEnd));
}
ports.push(port);
}
return ports;
}
var inbus = createBus(a.bus.input);
var outbus = createBus(a.bus.output);

slices.push({ inbus:inbus, outbus:outbus });
}
}

// -- create processor
var processor = AudioWorkletGlobalScope._createProcessor(a.name, e.ports[0], a.options);
processor.node = a.node;
processor.id = AWGS.processors.length;
processor.numSlices = numSlices;
AWGS.processors.push({ awp:processor, slices:slices });
postMessage({ type:"state", node:a.node, processor:processor.id, state:"running" });
break;

case "process":
var processor = AWGS.processors[msg.processor];
if (processor) {
if (hasSAB) {
for (var i=0; i<processor.slices.length; i++) {
var slice = processor.slices[i];
processor.awp.process(slice.inbus, slice.outbus, []);
}
}
else if (msg.buf.byteLength) {
var buf = new Float32Array(msg.buf);
var start = 0;
for (var i=0; i<processor.awp.numSlices; i++) {
var arr = buf.subarray(start, start + 128);
processor.awp.process([], [[arr]], []);
start += 128;
}
postMessage({ buf:msg.buf, type:"process", node:processor.awp.node }, [msg.buf]);
}
}
break;
}
}
Loading

0 comments on commit d056aa3

Please sign in to comment.