<!doctype html>
<html lang="en">
<!--
(c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc
MIT License
Project Home: https://github.com/voidqk/simple-js-synth
-->
<head>
<title>Simple JS Synth [demo]</title>
<style>
html, body { font-family: sans-serif; }
table, tr {
border-collapse: collapse;
padding: 0;
}
th, td {
padding: 5px;
border: 1px solid #777;
text-align: center;
}
pre {
text-align: left;
margin: 0 10px;
}
#osc1vol , #osc2vol , #osc3vol { width: 120px; }
#osc1tune, #osc2tune, #osc3tune { width: 280px; }
#attack , #decay , #sustain , #susdecay { width: 500px; }
#cutoff { width: 400px; }
</style>
</head>
<body>
<pre>
(c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc
MIT License
Project Home: https://github.com/velipso/simple-js-synth
</pre>
<table>
<tbody>
<tr>
<th onclick="MM();">Oscillator</th>
<th>Type</th>
<th>Volume</th>
<th>Tune</th>
<td rowspan="11"><pre id="raw"></pre></td>
</tr>
<tr>
<td>1</td>
<td><select id="osc1type">
<option value="sine">sine</option>
<option value="square">square</option>
<option value="triangle">triangle</option>
<option value="sawtooth">sawtooth</option>
</select></td>
<td>0 <input type="range" min="0" max="50" value="20" id="osc1vol" /> 0.5</td>
<td>-12 <input type="range" min="-120" max="120" value="0" id="osc1tune" /> +12</td>
</tr>
<tr>
<td>2</td>
<td><select id="osc2type">
<option value="sine">sine</option>
<option value="square" selected="selected">square</option>
<option value="triangle">triangle</option>
<option value="sawtooth">sawtooth</option>
</select></td>
<td>0 <input type="range" min="0" max="50" value="10" id="osc2vol" /> 0.5</td>
<td>-12 <input type="range" min="-120" max="120" value="120" id="osc2tune" /> +12</td>
</tr>
<tr>
<td>3</td>
<td><select id="osc3type">
<option value="sine">sine</option>
<option value="square">square</option>
<option value="triangle">triangle</option>
<option value="sawtooth">sawtooth</option>
</select></td>
<td>0 <input type="range" min="0" max="50" value="5" id="osc3vol" /> 0.5</td>
<td>-12 <input type="range" min="-120" max="120" value="-120" id="osc3tune" /> +12</td>
</tr>
<tr>
<th colspan="4" style="border-top: 2px solid #777;">Envelope</th>
</tr>
<tr>
<th>Attack</th>
<td colspan="3">0 <input type="range" min="0" max="200" value="0" id="attack" /> 2</td>
</tr>
<tr>
<th>Decay</th>
<td colspan="3">0 <input type="range" min="0" max="500" value="30" id="decay" /> 5</td>
</tr>
<tr>
<th>Sustain</th>
<td colspan="3">0 <input type="range" min="0" max="100" value="50" id="sustain" /> 1</td>
</tr>
<tr>
<th>Sus. Decay</th>
<td colspan="3">0 <input type="range" min="0" max="100" value="10" id="susdecay" /> 10</td>
</tr>
<tr>
<td colspan="4" style="padding: 0; line-height: 0;"><canvas id="env" width="1440" height="200" style="width: 720px; height: 100px;"></canvas></td>
</tr>
<tr style="border-top: 2px solid #777;">
<th id="cutoff_txt">Cutoff</th>
<td><select id="filter">
<option value="1">Enable</option>
<option value="0">Disable</option>
</select></td>
<td colspan="2">-12 <input type="range" min="-120" max="480" value="360" id="cutoff" /> +48</td>
</tr>
</tbody>
</table>
<br />
<script type="module">
import SimpleJSSynth from './simple-js-synth.js';
// create our WebAudio context
var AudioContext = window.AudioContext || window.webkitAudioContext;
var actx = new AudioContext();
// store the global bend amount for future notes to play at
var globalbend = 0;
// create a pool of monophonic synths to turn it into polyphonic
var pool = [];
// store our options to detect changes
var lastopts = null;
function reload(){
// create our options object based on the form
function osctype(id){
var sel = document.getElementById('osc' + id + 'type');
return sel.options[sel.selectedIndex].value;
}
function getnum(id, base){
return parseFloat(document.getElementById(id).value) / base;
}
var docutoff = document.getElementById('filter').selectedIndex == 0;
var opts = {
osc1type: osctype(1),
osc1vol : getnum('osc1vol', 100),
osc1tune: getnum('osc1tune', 10),
osc2type: osctype(2),
osc2vol : getnum('osc2vol', 100),
osc2tune: getnum('osc2tune', 10),
osc3type: osctype(3),
osc3vol : getnum('osc3vol', 100),
osc3tune: getnum('osc3tune', 10),
attack : getnum('attack', 100),
decay : getnum('decay', 100),
sustain : getnum('sustain', 100),
susdecay: getnum('susdecay', 10),
cutoff : docutoff ? getnum('cutoff', 10) : 1000
};
// if our options haven't changed from before, then return immediately
if (lastopts !== null &&
lastopts.osc1type === opts.osc1type &&
lastopts.osc1vol === opts.osc1vol &&
lastopts.osc1tune === opts.osc1tune &&
lastopts.osc2type === opts.osc2type &&
lastopts.osc2vol === opts.osc2vol &&
lastopts.osc2tune === opts.osc2tune &&
lastopts.osc3type === opts.osc3type &&
lastopts.osc3vol === opts.osc3vol &&
lastopts.osc3tune === opts.osc3tune &&
lastopts.attack === opts.attack &&
lastopts.decay === opts.decay &&
lastopts.sustain === opts.sustain &&
lastopts.susdecay === opts.susdecay &&
lastopts.cutoff === opts.cutoff)
return;
// output JSON object to UI
document.getElementById('raw').innerHTML = JSON.stringify(opts, null, 3);
// change UI to reflect whether cutoff is enabled
document.getElementById('cutoff_txt').style.color = docutoff ? '#000' : '#999';
document.getElementById('cutoff').disabled = !docutoff;
// draw the envelope
(function(){
var cnv = document.getElementById('env');
var ctx = cnv.getContext('2d');
var pps = 170; // pixels per second
var attack_x = opts.attack * pps;
var sustain_x = attack_x + (1 - opts.sustain) * opts.decay * pps;
var sustain_y = (1 - opts.sustain) * cnv.height;
var endsus_x = sustain_x + 2 * pps;
var endsus_y = (1 - opts.sustain * (1 - 2 / Math.max(opts.susdecay, 0.001))) * cnv.height;
var decay_x = endsus_x + opts.sustain * 0.5 * opts.decay * pps;
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, cnv.width, cnv.height);
ctx.fillStyle = '#eee';
ctx.fillRect(endsus_x, 0, cnv.width, cnv.height);
ctx.beginPath();
ctx.moveTo(0, cnv.height);
ctx.lineTo(attack_x, 0);
ctx.lineTo(sustain_x, sustain_y);
ctx.lineTo(endsus_x, endsus_y);
ctx.lineTo(decay_x, cnv.height);
ctx.strokeStyle = '#000';
ctx.setLineDash([]);
ctx.lineWidth = 4;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(attack_x, 0);
ctx.lineTo(attack_x, cnv.height);
ctx.moveTo(sustain_x, 0);
ctx.lineTo(sustain_x, cnv.height);
ctx.moveTo(endsus_x, 0);
ctx.lineTo(endsus_x, cnv.height);
ctx.setLineDash([5, 5]);
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(sustain_x, sustain_y, 10, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.arc(endsus_x, endsus_y, 10, 0, Math.PI * 2);
ctx.stroke();
})();
// destroy all synths currently in the pool
while (pool.length > 0)
pool.pop().destroy(); // pop a synth and destroy it
// recreate the pool based on the new options
for (var i = 0; i < 20; i++)
pool.push(SimpleJSSynth(actx.destination, opts));
// save the options for later comparison
lastopts = opts;
}
// attach update events to reload
(['osc1type', 'osc1vol', 'osc1tune', 'osc2type', 'osc2vol', 'osc2tune', 'osc3type', 'osc3vol',
'osc3tune', 'attack', 'decay', 'sustain', 'susdecay', 'filter', 'cutoff']).forEach(function(id){
document.getElementById(id).addEventListener('change', reload);
});
// check our options every 200ms
setInterval(reload, 200);
// fired when a note is hit, either via the UI, or via MIDI
function noteHit(freq, vol){
actx.resume();
for (var i = 0; i < pool.length; i++){
// search the pool of synths that are ready for a new note
if (pool[i].isReady()){
// trigger and return the synth
pool[i].noteOn(freq, vol);
return pool[i];
}
}
// no synths available, so don't do anything, and return a junk object
return {
noteOff: function(){},
bend: function(){}
};
}
// track which notes are currently playing
var mididown = [];
for (var i = 0; i < 128; i++)
mididown.push(false);
// called when a note is hit or released via UI or MIDI
function midiHit(note, vel, down){
// silence the note
if (mididown[note]){
mididown[note].noteOff();
mididown[note] = false;
}
// if we're pressing down, play the note
if (down){
var freq = 440 * Math.pow(2, (note - 69) / 12); // convert note to frequency
var vol = vel / 127; // convert velocity to volume
mididown[note] = noteHit(freq, vol);
// if we have a global bend, apply it immediately
if (globalbend != 0)
mididown[note].bend(globalbend);
}
// if the note is in the range of the UI, then color the appropriate div
if (note >= 36 && note < 84)
divs[note - 36].style.background = down ? '#aaf' : '#eef';
}
// called when MIDI sends a bend message
function midiBend(amt){
// set the global bend of +-2 semitones
globalbend = amt * 2;
// scan all playing notes and bend them accordingly
for (var i = 0; i < 128; i++){
if (mididown[i])
mididown[i].bend(globalbend);
}
}
// called when MIDI has initialized
function midiInit(midi){
function midiHook(){
var inputs = midi.inputs.values();
for (var input = inputs.next(); input && !input.done; input = inputs.next())
input.value.onmidimessage = midiEvent;
}
midiHook();
midi.onstatechange = midiHook;
}
// called when MIDI fails to initialize... don't do anything
function midiReject(){
console.warn('MIDI failed to initialize... oh well');
}
// called when a MIDI message is received
function midiEvent(ev){
if (ev.data.length < 2)
return;
// look for note on, note off, and pitch bend messages
if ((ev.data[0] & 0xF0) == 0x90)
midiHit(ev.data[1], ev.data[2], true);
else if ((ev.data[0] & 0xF0) == 0x80)
midiHit(ev.data[1], 0, false);
else if ((ev.data[0] & 0xF0) == 0xE0){
if (ev.data[1] == 0 && ev.data[2] == 0x40)
midiBend(0);
else
midiBend((ev.data[1] | (ev.data[2] << 7)) * 2 / 0x3FFF - 1);
}
}
// request MIDI access
if (navigator.requestMIDIAccess)
navigator.requestMIDIAccess().then(midiInit, midiReject);
// create our 48 divs for the user to click
var divs = [];
for (var i = 0; i < 48; i++){
if (i == 12 || i == 24 || i == 36)
document.body.appendChild(document.createElement('br'));
(function(div, i){
// create the DIV
divs.push(div);
div.style.display = 'inline-block';
div.style.margin = '1px';
div.style.padding = '15px';
div.style.border = '1px solid #000';
div.style.cursor = 'default';
div.style.backgroundColor = '#eef';
div.appendChild(document.createTextNode(
(['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'])[i % 12] +
(Math.floor(i / 12) + 2)));
document.body.appendChild(div);
// hook mouse events to the DIV
function cancel(e){
e.preventDefault();
e.stopPropagation();
return false;
}
function mousedown(e){
// if the mouse is pressed, simulate a MIDI note on
midiHit(36 + i, 100, true);
return cancel(e);
}
function mouseup(e){
// if the mouse is released, simulate a MIDI note off
midiHit(36 + i, 0, false);
return cancel(e);
}
div.addEventListener('mousemove', cancel);
div.addEventListener('mousedown', mousedown);
div.addEventListener('mouseup', mouseup);
div.addEventListener('mouseout', mouseup);
div.addEventListener('touchmove', cancel);
div.addEventListener('touchstart', mousedown);
div.addEventListener('touchend', mouseup);
div.addEventListener('touchleave', mouseup);
div.addEventListener('selectstart', cancel);
div.unselectable = 'on';
div.style.mozUserSelect = 'none';
div.style.webkitUserSelect = 'none';
div.style.userSelect = 'none';
})(document.createElement('div'), i);
}
function MM(){
var i = 0, p = 0, s = 'MMMOM]QMM]\\MLMMMOM]QMM]\\ORlT]ORlT]\\]QOLOLMLMOLpA';
function n(){
var c = s.charCodeAt(i++) - 65, m = 70 + c % 11, z = Math.floor(c / 11);
if (p > 70) midiHit(p, 0, false);
if (m > 70) midiHit(m, 100, true );
if (z > 0) setTimeout(n, z * 187);
p = m;
}
n();
}
</script>
</body>
</html>