aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2023-09-02 11:02:33 +0000
committers-ol <s+removethis@s-ol.nu>2023-09-02 11:05:09 +0000
commit9d4923c01b565e8cdfc01214ef0b762924cd9714 (patch)
treeef00b651c46ddb471bf63fcf044c553e3541e4a6
parentshow index-in-pattern (diff)
downloadisomorphic-kb-explorer-9d4923c01b565e8cdfc01214ef0b762924cd9714.tar.gz
isomorphic-kb-explorer-9d4923c01b565e8cdfc01214ef0b762924cd9714.zip
add synthesizer
-rw-r--r--main.js40
-rw-r--r--simple-js-synth.js164
-rw-r--r--synth.html402
-rw-r--r--synth.js52
4 files changed, 649 insertions, 9 deletions
diff --git a/main.js b/main.js
index 4ebd1f5..29d3988 100644
--- a/main.js
+++ b/main.js
@@ -1,5 +1,6 @@
import * as layout from './layout.js';
import * as pattern from './pattern.js';
+import * as synth from './synth.js';
const sqrt3 = Math.sqrt(3);
const sqrt32 = sqrt3 / 2;
@@ -43,11 +44,6 @@ const main = document.querySelector('main');
const bg = document.getElementById('canvas-bg').getContext('2d');
const fg = document.getElementById('canvas-fg').getContext('2d');
-let mousePos = [0, 0];
-main.onmousemove = (e) => {
- mousePos = [e.clientX - main.clientWidth/2, e.clientY - main.clientHeight/2];
-};
-
document.body.onkeydown = (e) => {
switch (e.key) {
case 'q': layout.turn(-1); break;
@@ -55,6 +51,29 @@ document.body.onkeydown = (e) => {
}
};
+main.onpointerdown =
+main.onpointermove = (e) => {
+ if (e.type.endsWith('move')) {
+ if (!e.target.hasPointerCapture(e.pointerId)) return;
+ } else {
+ e.target.setPointerCapture(e.pointerId);
+ }
+
+ e.preventDefault();
+
+ const [qq, rr] = layout.getSteps();
+ const pos = [e.clientX - main.clientWidth/2, e.clientY - main.clientHeight/2];
+ const [q, r] = px2hex(rotate(pos, layout.getRot()));
+ const note = q*qq + r*rr;
+
+ synth.on(`ptr-${e.pointerId}`, note);
+};
+main.onpointerup = (e) => {
+ e.preventDefault();
+
+ synth.off(`ptr-${e.pointerId}`);
+};
+
let lastCanvasSize = 0;
const updateForeground = (canvasSize) => {
@@ -125,7 +144,13 @@ const draw = () => {
const stepIndex = steps.indexOf(note);
hexagon(bg);
- if (stepIndex > -1) {
+ if (synth.isOn(note)) {
+ bg.fillStyle = '#e3baf5';
+ bg.fill();
+
+ bg.beginPath();
+ bg.arc(0, 0, size*0.65, 0, Math.PI*2);
+ } else if (stepIndex > -1) {
bg.fillStyle = '#bae3f5';
bg.fill();
@@ -152,9 +177,6 @@ const draw = () => {
}
}
- // const mouse = px2hex(rotate(mousePos, rot));
- // bg.arc(...hex2px(mouse), 10, 0, 2*Math.PI);
-
document.body.style.setProperty('--global-rot', `${rot}rad`);
requestAnimationFrame(draw);
};
diff --git a/simple-js-synth.js b/simple-js-synth.js
new file mode 100644
index 0000000..77a206d
--- /dev/null
+++ b/simple-js-synth.js
@@ -0,0 +1,164 @@
+// (c) Copyright 2017, Sean Connelly (@voidqk), http://syntheti.cc
+// MIT License
+// Project Home: https://github.com/voidqk/simple-js-synth
+
+function SimpleJSSynth(dest, opts){
+ // `dest` is the AudioNode destination
+ // `opts` is an object; see notes further down for meaning and range of values.
+ // {
+ // osc1type : 'sine'|'square'|'sawtooth'|'triangle', // type of wave
+ // osc1vol : 0 to 1, // oscillator volume (linear)
+ // osc1tune : 0, // relative tuning (semitones)
+ // osc2type, osc2vol, osc2tune, // settings for osc2
+ // osc3type, osc3vol, osc3tune, // settings for osc3
+ // attack : 0 to inf, // attack time (seconds)
+ // decay : 0 to inf, // decay time (seconds)
+ // sustain : 0 to 1, // sustain (fraction of max vol)
+ // susdecay : 0 to inf, // decay during sustain (seconds)
+ // cutoff : -inf to inf // filter cutoff (relative semitones)
+ // }
+
+ var ctx = dest.context; // get the WebAudio context
+
+ //
+ // Osc1 ---> Osc1 Gain ---+
+ // |
+ // Osc2 ---> Osc2 Gain ---+---> Envelope Gain ---> Filter --> Destination
+ // |
+ // Osc3 ---> Osc3 Gain ---+
+ //
+
+ var filter = ctx.createBiquadFilter();
+ filter.type = 'lowpass';
+ filter.frequency.setValueAtTime(22050, ctx.currentTime);
+ filter.Q.setValueAtTime(0.5, ctx.currentTime);
+
+ var my = filter; // the returned object is the filter
+
+ var gain = ctx.createGain();
+ gain.gain.setValueAtTime(0, ctx.currentTime);
+ gain.connect(filter);
+
+ function oscgain(v, def){
+ var g = ctx.createGain();
+ v = typeof v === 'number' ? v : def;
+ g.gain.setValueAtTime(0, ctx.currentTime);
+ g.connect(gain);
+ return { node: g, base: v };
+ }
+ var osc1gain = oscgain(opts.osc1vol, 0.8);
+ var osc2gain = oscgain(opts.osc2vol, 0.6);
+ var osc3gain = oscgain(opts.osc3vol, 0.4);
+
+ function osctype(type, g){
+ var osc = ctx.createOscillator();
+ osc.type = typeof type === 'string' ? type : 'sine';
+ osc.connect(g);
+ return osc;
+ }
+ var osc1 = osctype(opts.osc1type, osc1gain.node);
+ var osc2 = osctype(opts.osc2type, osc2gain.node);
+ var osc3 = osctype(opts.osc3type, osc3gain.node);
+
+ function calctune(t){
+ if (typeof t !== 'number')
+ return 1;
+ return Math.pow(2, t / 12);
+ }
+ var tune1 = calctune(opts.osc1tune);
+ var tune2 = calctune(opts.osc2tune);
+ var tune3 = calctune(opts.osc3tune);
+ var cutoff = calctune(opts.cutoff);
+
+ var attack = typeof opts.attack == 'number' ? opts.attack : 0.1;
+ var decay = typeof opts.decay == 'number' ? opts.decay : 0.2;
+ var sustain = typeof opts.sustain == 'number' ? opts.sustain : 0.5;
+ var susdecay = typeof opts.susdecay == 'number' ? opts.susdecay : 10;
+
+ // clamp the values a bit
+ var eps = 0.001;
+ if (attack < eps)
+ attack = eps;
+ if (decay < eps)
+ decay = eps;
+ if (sustain < eps)
+ sustain = eps;
+ if (susdecay < eps)
+ susdecay = eps;
+
+ var basefreq = 0;
+ var silent = 0;
+ var ndown = false;
+
+ osc1.start();
+ osc2.start();
+ osc3.start();
+
+ my.connect(dest);
+
+ my.noteOn = function(freq, vol){
+ ndown = true;
+ basefreq = freq;
+ var now = ctx.currentTime;
+ osc1.frequency.setValueAtTime(freq * tune1, now);
+ osc2.frequency.setValueAtTime(freq * tune2, now);
+ osc3.frequency.setValueAtTime(freq * tune3, now);
+ filter.frequency.setValueAtTime(Math.min(freq * cutoff, 22050), now);
+ osc1gain.node.gain.setValueAtTime(vol * osc1gain.base, now);
+ osc2gain.node.gain.setValueAtTime(vol * osc2gain.base, now);
+ osc3gain.node.gain.setValueAtTime(vol * osc3gain.base, now);
+ var v = gain.gain.value;
+ gain.gain.cancelScheduledValues(now);
+ gain.gain.setValueAtTime(v, now);
+ var hitpeak = now + attack;
+ var hitsus = hitpeak + decay * (1 - sustain);
+ silent = hitsus + susdecay;
+ gain.gain.linearRampToValueAtTime(1, hitpeak);
+ gain.gain.linearRampToValueAtTime(sustain, hitsus);
+ gain.gain.linearRampToValueAtTime(0.000001, silent);
+ };
+
+ my.bend = function(semitones){
+ var b = basefreq * Math.pow(2, semitones / 12);
+ var now = ctx.currentTime;
+ osc1.frequency.setTargetAtTime(b * tune1, now, 0.1);
+ osc2.frequency.setTargetAtTime(b * tune2, now, 0.1);
+ osc3.frequency.setTargetAtTime(b * tune3, now, 0.1);
+ };
+
+ my.noteOff = function(){
+ ndown = false;
+ var now = ctx.currentTime;
+ var v = gain.gain.value;
+ gain.gain.cancelScheduledValues(now);
+ gain.gain.setValueAtTime(v, now);
+ silent = now + decay * v;
+ gain.gain.linearRampToValueAtTime(0.000001, silent);
+ };
+
+ my.isReady = function(){
+ return ctx.currentTime >= silent && !ndown;
+ };
+
+ my.stop = function(){
+ ndown = false;
+ var now = ctx.currentTime;
+ osc1gain.node.gain.setValueAtTime(0.000001, now);
+ osc2gain.node.gain.setValueAtTime(0.000001, now);
+ osc3gain.node.gain.setValueAtTime(0.000001, now);
+ silent = 0;
+ };
+
+ my.destroy = function(){
+ ndown = false;
+ silent = 0;
+ osc1.stop();
+ osc2.stop();
+ osc3.stop();
+ my.disconnect();
+ };
+
+ return my;
+}
+
+export default SimpleJSSynth;
diff --git a/synth.html b/synth.html
new file mode 100644
index 0000000..4d486a5
--- /dev/null
+++ b/synth.html
@@ -0,0 +1,402 @@
+<!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>
diff --git a/synth.js b/synth.js
new file mode 100644
index 0000000..74dc051
--- /dev/null
+++ b/synth.js
@@ -0,0 +1,52 @@
+import SimpleJSSynth from './simple-js-synth.js';
+import * as pattern from './pattern.js';
+
+const ctx = new window.AudioContext();
+
+const opts = {};
+const voices = [];
+const tones = [];
+
+export const getOptions = () => opts;
+
+export const setOptions = (o) => {
+ Object.assign(opts, o);
+
+ while (voices.length)
+ voices.pop().destroy();
+
+ for (let i = 0; i < opts.polyphony; i++)
+ voices.push(SimpleJSSynth(ctx.destination, opts));
+};
+
+setOptions({
+ osc1type: "sine", osc1vol: 0.2, osc1tune: 0,
+ osc2type: "square", osc2vol: 0.14, osc2tune: 12,
+ osc3type: "sine", osc3vol: 0.05, osc3tune: -6.7,
+ attack: 0, decay: 0.3, sustain: 0.5, susdecay: 5,
+ cutoff: 36,
+
+ polyphony: 8,
+});
+
+export const on = (key, note, vol=1) => {
+ ctx.resume()
+
+ if (tones[key]?.note === note) return;
+ off(key);
+
+ const freq = 110.0 * Math.pow(2, note / pattern.getLength());
+
+ const voice = voices.find(s => s.isReady());
+ if (!voice) return;
+
+ voice.noteOn(freq, vol);
+ tones[key] = off && { note, voice };
+};
+
+export const off = (key) => {
+ tones[key]?.voice.noteOff();
+ delete tones[key];
+};
+
+export const isOn = (note) => Object.values(tones).some(t => t.note === note);