git.s-ol.nu isomorphic-kb-explorer / main synth.html
main

Tree @main (Download .tar.gz)

synth.html @mainraw · history · blame

<!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>