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
parent
f67d41d313
commit
ba1c7b7213
@ -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
|
@ -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); |
@ -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); |
||||
} |
||||
} |
@ -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> |
@ -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…
Reference in new issue