Add experimental Web Audio port

This patch opens an instance of the synthesizer in a web page,
using Web Audio's ScriptProcessorNode calling into asm.js to generate
the audio samples (and optionally Web MIDI for keyboard playing).

Performance is reasonably good but glitches asbolutely can happen.
webaudio
Raph Levien 7 years ago
parent f67d41d313
commit ba1c7b7213
  1. 10
      app/src/main/jni/Makefile
  2. 2
      app/src/main/jni/aligned_buf.h
  3. 115
      app/src/main/jni/audio.js
  4. 2
      app/src/main/jni/dx7note.h
  5. 52
      app/src/main/jni/emscripten.cc
  6. 4
      app/src/main/jni/env.cc
  7. 2
      app/src/main/jni/env.h
  8. 45
      app/src/main/jni/index.html
  9. 2
      app/src/main/jni/pitchenv.cc
  10. 14
      app/src/main/jni/synth.css
  11. 16
      app/src/main/jni/synthcore.js
  12. BIN
      app/src/main/jni/synthcore.js.mem
  13. 86
      app/src/main/jni/ui.js

@ -0,0 +1,10 @@
# Makefile for emscripten
SRC := dx7note.cc env.cc exp2.cc fm_core.cc fm_op_kernel.cc freqlut.cc \
lfo.cc log2.cc patch.cc pitchenv.cc resofilter.cc ringbuffer.cc \
sawtooth.cc sin.cc synth_unit.cc emscripten.cc
EXP := "['_synth_create','_synth_get_samples','_synth_send_midi']"
synthcore.js: $(SRC)
em++ $^ -o $@ -s EXPORTED_FUNCTIONS=$(EXP) -O2

@ -21,6 +21,8 @@
#ifndef __ALIGNED_BUF_H
#define __ALIGNED_BUF_H
#include <stddef.h>
template<typename T, size_t size, size_t alignment = 16>
class AlignedBuf {
public:

@ -0,0 +1,115 @@
// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict';
var audio = null; // global audio context
class Audio {
constructor(synthUnit, sendMidi) {
this._synthUnit = synthUnit;
this._sendMidi = sendMidi;
}
sendMidi(data) {
var midiBuf = Module._malloc(data.length);
Module.writeArrayToMemory(data, midiBuf);
this._sendMidi(this._synthUnit, midiBuf, data.length);
Module._free(midiBuf);
}
}
function start() {
var ctx = new AudioContext();
// Initialize the emscripten core
var sampleRate = ctx.sampleRate;
var synthUnit = Module.ccall('synth_create', 'number', ['number'], [sampleRate]);
console.log('synth pointer =', synthUnit);
var getSamples = Module.cwrap('synth_get_samples', null, ['number', 'number', 'number']);
var sendMidi = Module.cwrap('synth_send_midi', null, ['number', 'number', 'number']);
var scriptNode = ctx.createScriptProcessor(256, 0, 1);
var bufSize = scriptNode.bufferSize;
var xferBuf = Module._malloc(bufSize * 2);
scriptNode.onaudioprocess = function(audioProcessingEvent) {
//console.log(audioProcessingEvent);
getSamples(synthUnit, bufSize, xferBuf);
var buf = audioProcessingEvent.outputBuffer.getChannelData(0);
for (var i = 0; i < bufSize; i++) {
buf[i] = Module.getValue(xferBuf + i*2, 'i16') * (1.0/32768);
}
}
scriptNode.connect(ctx.destination);
audio = new Audio(synthUnit, sendMidi);
var midiAccess = null;
var midiIn = null;
function midiStateChange() {
if (midiIn != null) {
midiIn.onmidimessage = null;
}
var inputs = midiAccess.inputs.values();
for (var input = inputs.next(); input && !input.done; input = inputs.next()) {
// we just always take the first one in the list; could be more
// sophisticated but whatevs.
midiIn = input.value;
break;
}
if (midiIn != null) {
midiIn.onmidimessage = onMIDIMessage;
}
}
function onMIDIMessage(message) {
var data = message.data;
console.log('midi:', data);
if (data[0] == 0x90 || data[0] == 0x80) {
var shiftedNote = data[1] - 48;
var note = notes[shiftedNote];
if (note != null && data[0] == 0x90) {
note.note_on();
}
if (note != null && data[0] == 0x80) {
note.note_off();
}
}
audio.sendMidi(data);
}
function onMIDIStarted(midi) {
midiAccess = midi;
midi.onstatechange = midiStateChange;
midiStateChange();
}
function onMIDISystemError(err) {
console.log("MIDI error: " + err);
}
// MIDI
navigator.requestMIDIAccess().then( onMIDIStarted, onMIDISystemError );
}
function audioInit() {
if (window.emscriptenRuntimeReady) {
start();
} else {
window.onEmscriptenRuntimeReady = start;
}
}
window.addEventListener('load', audioInit);

@ -23,6 +23,8 @@
// It will continue to evolve a bit, as note-stealing logic, scaling,
// and real-time control of parameters live here.
#include <stdint.h>
#include "env.h"
#include "pitchenv.h"
#include "fm_core.h"

@ -0,0 +1,52 @@
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// C-wrapped API entrypoints for use from Emscripten
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#else
#define EMSCRIPTEN_KEEPALIVE
#endif
#include "synth.h"
#include "synth_unit.h"
struct Synth {
RingBuffer *buf;
SynthUnit *synth_unit;
};
extern "C" {
EMSCRIPTEN_KEEPALIVE
Synth *synth_create(int sample_rate) {
Synth *synth = new Synth();
synth->buf = new RingBuffer();
synth->synth_unit = new SynthUnit(synth->buf);
synth->synth_unit->Init(sample_rate);
return synth;
}
EMSCRIPTEN_KEEPALIVE
void synth_get_samples(Synth *synth, int n_samples, int16_t *buffer) {
synth->synth_unit->GetSamples(n_samples, buffer);
}
EMSCRIPTEN_KEEPALIVE
void synth_send_midi(Synth *synth, const uint8_t *bytes, int size) {
synth->buf->Write(bytes, size);
}
}

@ -17,8 +17,6 @@
#include "synth.h"
#include "env.h"
using namespace std;
void Env::init(const int r[4], const int l[4], int32_t ol, int rate_scaling) {
for (int i = 0; i < 4; i++) {
rates_[i] = r[i];
@ -32,7 +30,7 @@ void Env::init(const int r[4], const int l[4], int32_t ol, int rate_scaling) {
}
int32_t Env::getsample() {
if (ix_ < 3 || (ix_ < 4) && !down_) {
if (ix_ < 3 || ((ix_ < 4) && !down_)) {
if (rising_) {
const int jumptarget = 1716;
if (level_ < (jumptarget << 16)) {

@ -17,6 +17,8 @@
#ifndef __ENV_H
#define __ENV_H
#include <stdint.h>
// DX7 envelope generation
class Env {

@ -0,0 +1,45 @@
<!doctype html>
<!--
Copyright 2017 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Synthesizer testbed</title>
<link rel="stylesheet" href="synth.css">
</head>
<body>
<h1>Synthesizer testbed</h1>
<script>
var Module = {
onRuntimeInitialized: function () {
console.log('emscripten runtime initialized');
window.emscriptenRuntimeReady = true;
if (window.onEmscriptenRuntimeReady) {
window.onEmscriptenRuntimeReady();
}
}
}
</script>
<script src="synthcore.js"></script>
<script src="audio.js"></script>
<script src="ui.js"></script>
<p>No UI yet. Connect a MIDI keyboard (using Web MIDI). Use the inspector
to see if MIDI events are getting through.</p>
<p>Port of <a href="https://github.com/google/music-synthesizer-for-android">music-synthesizer-for-android</a> using Emscripten.</p>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width='750px' height='300px' id="svg">
</svg>
</body>
</html>

@ -53,7 +53,7 @@ void PitchEnv::set(const int r[4], const int l[4]) {
}
int32_t PitchEnv::getsample() {
if (ix_ < 3 || (ix_ < 4) && !down_) {
if (ix_ < 3 || ((ix_ < 4) && !down_)) {
if (rising_) {
level_ += inc_;
if (level_ >= targetlevel_) {

@ -0,0 +1,14 @@
svg .white {
fill: white;
stroke: gray;
}
svg .black {
fill: black;
stroke: gray;
}
svg .pressed {
fill: #80a0c0;
stroke: gray;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -0,0 +1,86 @@
// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict';
var svgns = "http://www.w3.org/2000/svg";
var noteX = [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12];
var noteY = [0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0];
var xscale = 25;
var yscale = 70;
var notes = {};
class Note {
constructor(rect, cls) {
this.rect = rect;
this.cls = cls;
}
note_on() {
this.rect.setAttribute("class", "pressed");
}
note_off() {
this.rect.setAttribute("class", this.cls);
}
}
function addNote(parent, noteNum) {
var octave = Math.floor(noteNum / 12);
var noteId = noteNum % 12;
var x = 1 + xscale * (octave * 14 + noteX[noteId]);
var y = 100 - yscale * noteY[noteId];
var rect = document.createElementNS(svgns, "rect");
rect.setAttribute("x", x);
rect.setAttribute("y", y);
rect.setAttribute("rx", 5);
rect.setAttribute("ry", 5);
rect.setAttribute("width", 2 * xscale - 2);
rect.setAttribute("height", yscale - 2);
var cls = noteY[noteId] ? "black" : "white";
rect.setAttribute("class", cls);
parent.appendChild(rect);
rect.addEventListener('mouseover', function(e) {
console.log('mouseover', noteNum);
});
rect.addEventListener('mousedown', function(e) {
//e.target.setCapture();
e.preventDefault();
rect.setAttribute("class", "pressed");
console.log(noteNum, 'down');
audio.sendMidi(new Uint8Array([0x90, noteNum + 48, 64]));
function upEvent(e) {
e.preventDefault();
rect.setAttribute("class", cls);
console.log(noteNum, 'up');
audio.sendMidi(new Uint8Array([0x80, noteNum + 48, 64]));
document.removeEventListener('mouseup', upEvent);
}
document.addEventListener('mouseup', upEvent);
});
notes[noteNum] = new Note(rect, cls);
}
function uiInit() {
var svg = document.getElementById('svg');
var svgns = "http://www.w3.org/2000/svg";
for (var note = 0; note < 25; note++) {
addNote(svg, note);
}
}
window.addEventListener('load', uiInit);
Loading…
Cancel
Save