git.s-ol.nu isomorphic-kb-explorer / 9d4923c
add synthesizer s-ol 27 days ago
4 changed file(s) with 649 addition(s) and 9 deletion(s). Raw diff Collapse all Expand all
00 import * as layout from './layout.js';
11 import * as pattern from './pattern.js';
2 import * as synth from './synth.js';
23
34 const sqrt3 = Math.sqrt(3);
45 const sqrt32 = sqrt3 / 2;
4243 const bg = document.getElementById('canvas-bg').getContext('2d');
4344 const fg = document.getElementById('canvas-fg').getContext('2d');
4445
45 let mousePos = [0, 0];
46 main.onmousemove = (e) => {
47 mousePos = [e.clientX - main.clientWidth/2, e.clientY - main.clientHeight/2];
48 };
49
5046 document.body.onkeydown = (e) => {
5147 switch (e.key) {
5248 case 'q': layout.turn(-1); break;
5349 case 'e': layout.turn(1); break;
5450 }
51 };
52
53 main.onpointerdown =
54 main.onpointermove = (e) => {
55 if (e.type.endsWith('move')) {
56 if (!e.target.hasPointerCapture(e.pointerId)) return;
57 } else {
58 e.target.setPointerCapture(e.pointerId);
59 }
60
61 e.preventDefault();
62
63 const [qq, rr] = layout.getSteps();
64 const pos = [e.clientX - main.clientWidth/2, e.clientY - main.clientHeight/2];
65 const [q, r] = px2hex(rotate(pos, layout.getRot()));
66 const note = q*qq + r*rr;
67
68 synth.on(`ptr-${e.pointerId}`, note);
69 };
70 main.onpointerup = (e) => {
71 e.preventDefault();
72
73 synth.off(`ptr-${e.pointerId}`);
5574 };
5675
5776 let lastCanvasSize = 0;
124143 const stepIndex = steps.indexOf(note);
125144
126145 hexagon(bg);
127 if (stepIndex > -1) {
146 if (synth.isOn(note)) {
147 bg.fillStyle = '#e3baf5';
148 bg.fill();
149
150 bg.beginPath();
151 bg.arc(0, 0, size*0.65, 0, Math.PI*2);
152 } else if (stepIndex > -1) {
128153 bg.fillStyle = '#bae3f5';
129154 bg.fill();
130155
151176 }
152177 }
153178
154 // const mouse = px2hex(rotate(mousePos, rot));
155 // bg.arc(...hex2px(mouse), 10, 0, 2*Math.PI);
156
157179 document.body.style.setProperty('--global-rot', `${rot}rad`);
158180 requestAnimationFrame(draw);
159181 };
0 // (c) Copyright 2017, Sean Connelly (@voidqk), http://syntheti.cc
1 // MIT License
2 // Project Home: https://github.com/voidqk/simple-js-synth
3
4 function SimpleJSSynth(dest, opts){
5 // `dest` is the AudioNode destination
6 // `opts` is an object; see notes further down for meaning and range of values.
7 // {
8 // osc1type : 'sine'|'square'|'sawtooth'|'triangle', // type of wave
9 // osc1vol : 0 to 1, // oscillator volume (linear)
10 // osc1tune : 0, // relative tuning (semitones)
11 // osc2type, osc2vol, osc2tune, // settings for osc2
12 // osc3type, osc3vol, osc3tune, // settings for osc3
13 // attack : 0 to inf, // attack time (seconds)
14 // decay : 0 to inf, // decay time (seconds)
15 // sustain : 0 to 1, // sustain (fraction of max vol)
16 // susdecay : 0 to inf, // decay during sustain (seconds)
17 // cutoff : -inf to inf // filter cutoff (relative semitones)
18 // }
19
20 var ctx = dest.context; // get the WebAudio context
21
22 //
23 // Osc1 ---> Osc1 Gain ---+
24 // |
25 // Osc2 ---> Osc2 Gain ---+---> Envelope Gain ---> Filter --> Destination
26 // |
27 // Osc3 ---> Osc3 Gain ---+
28 //
29
30 var filter = ctx.createBiquadFilter();
31 filter.type = 'lowpass';
32 filter.frequency.setValueAtTime(22050, ctx.currentTime);
33 filter.Q.setValueAtTime(0.5, ctx.currentTime);
34
35 var my = filter; // the returned object is the filter
36
37 var gain = ctx.createGain();
38 gain.gain.setValueAtTime(0, ctx.currentTime);
39 gain.connect(filter);
40
41 function oscgain(v, def){
42 var g = ctx.createGain();
43 v = typeof v === 'number' ? v : def;
44 g.gain.setValueAtTime(0, ctx.currentTime);
45 g.connect(gain);
46 return { node: g, base: v };
47 }
48 var osc1gain = oscgain(opts.osc1vol, 0.8);
49 var osc2gain = oscgain(opts.osc2vol, 0.6);
50 var osc3gain = oscgain(opts.osc3vol, 0.4);
51
52 function osctype(type, g){
53 var osc = ctx.createOscillator();
54 osc.type = typeof type === 'string' ? type : 'sine';
55 osc.connect(g);
56 return osc;
57 }
58 var osc1 = osctype(opts.osc1type, osc1gain.node);
59 var osc2 = osctype(opts.osc2type, osc2gain.node);
60 var osc3 = osctype(opts.osc3type, osc3gain.node);
61
62 function calctune(t){
63 if (typeof t !== 'number')
64 return 1;
65 return Math.pow(2, t / 12);
66 }
67 var tune1 = calctune(opts.osc1tune);
68 var tune2 = calctune(opts.osc2tune);
69 var tune3 = calctune(opts.osc3tune);
70 var cutoff = calctune(opts.cutoff);
71
72 var attack = typeof opts.attack == 'number' ? opts.attack : 0.1;
73 var decay = typeof opts.decay == 'number' ? opts.decay : 0.2;
74 var sustain = typeof opts.sustain == 'number' ? opts.sustain : 0.5;
75 var susdecay = typeof opts.susdecay == 'number' ? opts.susdecay : 10;
76
77 // clamp the values a bit
78 var eps = 0.001;
79 if (attack < eps)
80 attack = eps;
81 if (decay < eps)
82 decay = eps;
83 if (sustain < eps)
84 sustain = eps;
85 if (susdecay < eps)
86 susdecay = eps;
87
88 var basefreq = 0;
89 var silent = 0;
90 var ndown = false;
91
92 osc1.start();
93 osc2.start();
94 osc3.start();
95
96 my.connect(dest);
97
98 my.noteOn = function(freq, vol){
99 ndown = true;
100 basefreq = freq;
101 var now = ctx.currentTime;
102 osc1.frequency.setValueAtTime(freq * tune1, now);
103 osc2.frequency.setValueAtTime(freq * tune2, now);
104 osc3.frequency.setValueAtTime(freq * tune3, now);
105 filter.frequency.setValueAtTime(Math.min(freq * cutoff, 22050), now);
106 osc1gain.node.gain.setValueAtTime(vol * osc1gain.base, now);
107 osc2gain.node.gain.setValueAtTime(vol * osc2gain.base, now);
108 osc3gain.node.gain.setValueAtTime(vol * osc3gain.base, now);
109 var v = gain.gain.value;
110 gain.gain.cancelScheduledValues(now);
111 gain.gain.setValueAtTime(v, now);
112 var hitpeak = now + attack;
113 var hitsus = hitpeak + decay * (1 - sustain);
114 silent = hitsus + susdecay;
115 gain.gain.linearRampToValueAtTime(1, hitpeak);
116 gain.gain.linearRampToValueAtTime(sustain, hitsus);
117 gain.gain.linearRampToValueAtTime(0.000001, silent);
118 };
119
120 my.bend = function(semitones){
121 var b = basefreq * Math.pow(2, semitones / 12);
122 var now = ctx.currentTime;
123 osc1.frequency.setTargetAtTime(b * tune1, now, 0.1);
124 osc2.frequency.setTargetAtTime(b * tune2, now, 0.1);
125 osc3.frequency.setTargetAtTime(b * tune3, now, 0.1);
126 };
127
128 my.noteOff = function(){
129 ndown = false;
130 var now = ctx.currentTime;
131 var v = gain.gain.value;
132 gain.gain.cancelScheduledValues(now);
133 gain.gain.setValueAtTime(v, now);
134 silent = now + decay * v;
135 gain.gain.linearRampToValueAtTime(0.000001, silent);
136 };
137
138 my.isReady = function(){
139 return ctx.currentTime >= silent && !ndown;
140 };
141
142 my.stop = function(){
143 ndown = false;
144 var now = ctx.currentTime;
145 osc1gain.node.gain.setValueAtTime(0.000001, now);
146 osc2gain.node.gain.setValueAtTime(0.000001, now);
147 osc3gain.node.gain.setValueAtTime(0.000001, now);
148 silent = 0;
149 };
150
151 my.destroy = function(){
152 ndown = false;
153 silent = 0;
154 osc1.stop();
155 osc2.stop();
156 osc3.stop();
157 my.disconnect();
158 };
159
160 return my;
161 }
162
163 export default SimpleJSSynth;
0 <!doctype html>
1 <html lang="en">
2 <!--
3 (c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc
4 MIT License
5 Project Home: https://github.com/voidqk/simple-js-synth
6 -->
7 <head>
8 <title>Simple JS Synth [demo]</title>
9 <style>
10 html, body { font-family: sans-serif; }
11 table, tr {
12 border-collapse: collapse;
13 padding: 0;
14 }
15 th, td {
16 padding: 5px;
17 border: 1px solid #777;
18 text-align: center;
19 }
20 pre {
21 text-align: left;
22 margin: 0 10px;
23 }
24 #osc1vol , #osc2vol , #osc3vol { width: 120px; }
25 #osc1tune, #osc2tune, #osc3tune { width: 280px; }
26 #attack , #decay , #sustain , #susdecay { width: 500px; }
27 #cutoff { width: 400px; }
28 </style>
29 </head>
30 <body>
31 <pre>
32 (c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc
33 MIT License
34 Project Home: https://github.com/velipso/simple-js-synth
35 </pre>
36 <table>
37 <tbody>
38 <tr>
39 <th onclick="MM();">Oscillator</th>
40 <th>Type</th>
41 <th>Volume</th>
42 <th>Tune</th>
43 <td rowspan="11"><pre id="raw"></pre></td>
44 </tr>
45 <tr>
46 <td>1</td>
47 <td><select id="osc1type">
48 <option value="sine">sine</option>
49 <option value="square">square</option>
50 <option value="triangle">triangle</option>
51 <option value="sawtooth">sawtooth</option>
52 </select></td>
53 <td>0 <input type="range" min="0" max="50" value="20" id="osc1vol" /> 0.5</td>
54 <td>-12 <input type="range" min="-120" max="120" value="0" id="osc1tune" /> +12</td>
55 </tr>
56 <tr>
57 <td>2</td>
58 <td><select id="osc2type">
59 <option value="sine">sine</option>
60 <option value="square" selected="selected">square</option>
61 <option value="triangle">triangle</option>
62 <option value="sawtooth">sawtooth</option>
63 </select></td>
64 <td>0 <input type="range" min="0" max="50" value="10" id="osc2vol" /> 0.5</td>
65 <td>-12 <input type="range" min="-120" max="120" value="120" id="osc2tune" /> +12</td>
66 </tr>
67 <tr>
68 <td>3</td>
69 <td><select id="osc3type">
70 <option value="sine">sine</option>
71 <option value="square">square</option>
72 <option value="triangle">triangle</option>
73 <option value="sawtooth">sawtooth</option>
74 </select></td>
75 <td>0 <input type="range" min="0" max="50" value="5" id="osc3vol" /> 0.5</td>
76 <td>-12 <input type="range" min="-120" max="120" value="-120" id="osc3tune" /> +12</td>
77 </tr>
78 <tr>
79 <th colspan="4" style="border-top: 2px solid #777;">Envelope</th>
80 </tr>
81 <tr>
82 <th>Attack</th>
83 <td colspan="3">0 <input type="range" min="0" max="200" value="0" id="attack" /> 2</td>
84 </tr>
85 <tr>
86 <th>Decay</th>
87 <td colspan="3">0 <input type="range" min="0" max="500" value="30" id="decay" /> 5</td>
88 </tr>
89 <tr>
90 <th>Sustain</th>
91 <td colspan="3">0 <input type="range" min="0" max="100" value="50" id="sustain" /> 1</td>
92 </tr>
93 <tr>
94 <th>Sus. Decay</th>
95 <td colspan="3">0 <input type="range" min="0" max="100" value="10" id="susdecay" /> 10</td>
96 </tr>
97 <tr>
98 <td colspan="4" style="padding: 0; line-height: 0;"><canvas id="env" width="1440" height="200" style="width: 720px; height: 100px;"></canvas></td>
99 </tr>
100 <tr style="border-top: 2px solid #777;">
101 <th id="cutoff_txt">Cutoff</th>
102 <td><select id="filter">
103 <option value="1">Enable</option>
104 <option value="0">Disable</option>
105 </select></td>
106 <td colspan="2">-12 <input type="range" min="-120" max="480" value="360" id="cutoff" /> +48</td>
107 </tr>
108 </tbody>
109 </table>
110 <br />
111 <script type="module">
112 import SimpleJSSynth from './simple-js-synth.js';
113
114 // create our WebAudio context
115 var AudioContext = window.AudioContext || window.webkitAudioContext;
116 var actx = new AudioContext();
117
118 // store the global bend amount for future notes to play at
119 var globalbend = 0;
120
121 // create a pool of monophonic synths to turn it into polyphonic
122 var pool = [];
123
124 // store our options to detect changes
125 var lastopts = null;
126
127 function reload(){
128 // create our options object based on the form
129 function osctype(id){
130 var sel = document.getElementById('osc' + id + 'type');
131 return sel.options[sel.selectedIndex].value;
132 }
133 function getnum(id, base){
134 return parseFloat(document.getElementById(id).value) / base;
135 }
136 var docutoff = document.getElementById('filter').selectedIndex == 0;
137 var opts = {
138 osc1type: osctype(1),
139 osc1vol : getnum('osc1vol', 100),
140 osc1tune: getnum('osc1tune', 10),
141 osc2type: osctype(2),
142 osc2vol : getnum('osc2vol', 100),
143 osc2tune: getnum('osc2tune', 10),
144 osc3type: osctype(3),
145 osc3vol : getnum('osc3vol', 100),
146 osc3tune: getnum('osc3tune', 10),
147 attack : getnum('attack', 100),
148 decay : getnum('decay', 100),
149 sustain : getnum('sustain', 100),
150 susdecay: getnum('susdecay', 10),
151 cutoff : docutoff ? getnum('cutoff', 10) : 1000
152 };
153
154 // if our options haven't changed from before, then return immediately
155 if (lastopts !== null &&
156 lastopts.osc1type === opts.osc1type &&
157 lastopts.osc1vol === opts.osc1vol &&
158 lastopts.osc1tune === opts.osc1tune &&
159 lastopts.osc2type === opts.osc2type &&
160 lastopts.osc2vol === opts.osc2vol &&
161 lastopts.osc2tune === opts.osc2tune &&
162 lastopts.osc3type === opts.osc3type &&
163 lastopts.osc3vol === opts.osc3vol &&
164 lastopts.osc3tune === opts.osc3tune &&
165 lastopts.attack === opts.attack &&
166 lastopts.decay === opts.decay &&
167 lastopts.sustain === opts.sustain &&
168 lastopts.susdecay === opts.susdecay &&
169 lastopts.cutoff === opts.cutoff)
170 return;
171
172 // output JSON object to UI
173 document.getElementById('raw').innerHTML = JSON.stringify(opts, null, 3);
174
175 // change UI to reflect whether cutoff is enabled
176 document.getElementById('cutoff_txt').style.color = docutoff ? '#000' : '#999';
177 document.getElementById('cutoff').disabled = !docutoff;
178
179 // draw the envelope
180 (function(){
181 var cnv = document.getElementById('env');
182 var ctx = cnv.getContext('2d');
183 var pps = 170; // pixels per second
184 var attack_x = opts.attack * pps;
185 var sustain_x = attack_x + (1 - opts.sustain) * opts.decay * pps;
186 var sustain_y = (1 - opts.sustain) * cnv.height;
187 var endsus_x = sustain_x + 2 * pps;
188 var endsus_y = (1 - opts.sustain * (1 - 2 / Math.max(opts.susdecay, 0.001))) * cnv.height;
189 var decay_x = endsus_x + opts.sustain * 0.5 * opts.decay * pps;
190 ctx.fillStyle = '#fff';
191 ctx.fillRect(0, 0, cnv.width, cnv.height);
192 ctx.fillStyle = '#eee';
193 ctx.fillRect(endsus_x, 0, cnv.width, cnv.height);
194 ctx.beginPath();
195 ctx.moveTo(0, cnv.height);
196 ctx.lineTo(attack_x, 0);
197 ctx.lineTo(sustain_x, sustain_y);
198 ctx.lineTo(endsus_x, endsus_y);
199 ctx.lineTo(decay_x, cnv.height);
200 ctx.strokeStyle = '#000';
201 ctx.setLineDash([]);
202 ctx.lineWidth = 4;
203 ctx.stroke();
204 ctx.beginPath();
205 ctx.moveTo(attack_x, 0);
206 ctx.lineTo(attack_x, cnv.height);
207 ctx.moveTo(sustain_x, 0);
208 ctx.lineTo(sustain_x, cnv.height);
209 ctx.moveTo(endsus_x, 0);
210 ctx.lineTo(endsus_x, cnv.height);
211 ctx.setLineDash([5, 5]);
212 ctx.lineWidth = 2;
213 ctx.stroke();
214 ctx.beginPath();
215 ctx.arc(sustain_x, sustain_y, 10, 0, Math.PI * 2);
216 ctx.stroke();
217 ctx.beginPath();
218 ctx.arc(endsus_x, endsus_y, 10, 0, Math.PI * 2);
219 ctx.stroke();
220 })();
221
222 // destroy all synths currently in the pool
223 while (pool.length > 0)
224 pool.pop().destroy(); // pop a synth and destroy it
225
226 // recreate the pool based on the new options
227 for (var i = 0; i < 20; i++)
228 pool.push(SimpleJSSynth(actx.destination, opts));
229
230 // save the options for later comparison
231 lastopts = opts;
232 }
233
234 // attach update events to reload
235 (['osc1type', 'osc1vol', 'osc1tune', 'osc2type', 'osc2vol', 'osc2tune', 'osc3type', 'osc3vol',
236 'osc3tune', 'attack', 'decay', 'sustain', 'susdecay', 'filter', 'cutoff']).forEach(function(id){
237 document.getElementById(id).addEventListener('change', reload);
238 });
239
240 // check our options every 200ms
241 setInterval(reload, 200);
242
243 // fired when a note is hit, either via the UI, or via MIDI
244 function noteHit(freq, vol){
245 actx.resume();
246
247 for (var i = 0; i < pool.length; i++){
248 // search the pool of synths that are ready for a new note
249 if (pool[i].isReady()){
250 // trigger and return the synth
251 pool[i].noteOn(freq, vol);
252 return pool[i];
253 }
254 }
255 // no synths available, so don't do anything, and return a junk object
256 return {
257 noteOff: function(){},
258 bend: function(){}
259 };
260 }
261
262 // track which notes are currently playing
263 var mididown = [];
264 for (var i = 0; i < 128; i++)
265 mididown.push(false);
266
267 // called when a note is hit or released via UI or MIDI
268 function midiHit(note, vel, down){
269 // silence the note
270 if (mididown[note]){
271 mididown[note].noteOff();
272 mididown[note] = false;
273 }
274 // if we're pressing down, play the note
275 if (down){
276 var freq = 440 * Math.pow(2, (note - 69) / 12); // convert note to frequency
277 var vol = vel / 127; // convert velocity to volume
278 mididown[note] = noteHit(freq, vol);
279 // if we have a global bend, apply it immediately
280 if (globalbend != 0)
281 mididown[note].bend(globalbend);
282 }
283 // if the note is in the range of the UI, then color the appropriate div
284 if (note >= 36 && note < 84)
285 divs[note - 36].style.background = down ? '#aaf' : '#eef';
286 }
287
288 // called when MIDI sends a bend message
289 function midiBend(amt){
290 // set the global bend of +-2 semitones
291 globalbend = amt * 2;
292
293 // scan all playing notes and bend them accordingly
294 for (var i = 0; i < 128; i++){
295 if (mididown[i])
296 mididown[i].bend(globalbend);
297 }
298 }
299
300 // called when MIDI has initialized
301 function midiInit(midi){
302 function midiHook(){
303 var inputs = midi.inputs.values();
304 for (var input = inputs.next(); input && !input.done; input = inputs.next())
305 input.value.onmidimessage = midiEvent;
306 }
307 midiHook();
308 midi.onstatechange = midiHook;
309 }
310
311 // called when MIDI fails to initialize... don't do anything
312 function midiReject(){
313 console.warn('MIDI failed to initialize... oh well');
314 }
315
316 // called when a MIDI message is received
317 function midiEvent(ev){
318 if (ev.data.length < 2)
319 return;
320 // look for note on, note off, and pitch bend messages
321 if ((ev.data[0] & 0xF0) == 0x90)
322 midiHit(ev.data[1], ev.data[2], true);
323 else if ((ev.data[0] & 0xF0) == 0x80)
324 midiHit(ev.data[1], 0, false);
325 else if ((ev.data[0] & 0xF0) == 0xE0){
326 if (ev.data[1] == 0 && ev.data[2] == 0x40)
327 midiBend(0);
328 else
329 midiBend((ev.data[1] | (ev.data[2] << 7)) * 2 / 0x3FFF - 1);
330 }
331 }
332
333 // request MIDI access
334 if (navigator.requestMIDIAccess)
335 navigator.requestMIDIAccess().then(midiInit, midiReject);
336
337 // create our 48 divs for the user to click
338 var divs = [];
339 for (var i = 0; i < 48; i++){
340 if (i == 12 || i == 24 || i == 36)
341 document.body.appendChild(document.createElement('br'));
342 (function(div, i){
343 // create the DIV
344 divs.push(div);
345 div.style.display = 'inline-block';
346 div.style.margin = '1px';
347 div.style.padding = '15px';
348 div.style.border = '1px solid #000';
349 div.style.cursor = 'default';
350 div.style.backgroundColor = '#eef';
351 div.appendChild(document.createTextNode(
352 (['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'])[i % 12] +
353 (Math.floor(i / 12) + 2)));
354 document.body.appendChild(div);
355
356 // hook mouse events to the DIV
357 function cancel(e){
358 e.preventDefault();
359 e.stopPropagation();
360 return false;
361 }
362 function mousedown(e){
363 // if the mouse is pressed, simulate a MIDI note on
364 midiHit(36 + i, 100, true);
365 return cancel(e);
366 }
367 function mouseup(e){
368 // if the mouse is released, simulate a MIDI note off
369 midiHit(36 + i, 0, false);
370 return cancel(e);
371 }
372 div.addEventListener('mousemove', cancel);
373 div.addEventListener('mousedown', mousedown);
374 div.addEventListener('mouseup', mouseup);
375 div.addEventListener('mouseout', mouseup);
376 div.addEventListener('touchmove', cancel);
377 div.addEventListener('touchstart', mousedown);
378 div.addEventListener('touchend', mouseup);
379 div.addEventListener('touchleave', mouseup);
380 div.addEventListener('selectstart', cancel);
381 div.unselectable = 'on';
382 div.style.mozUserSelect = 'none';
383 div.style.webkitUserSelect = 'none';
384 div.style.userSelect = 'none';
385 })(document.createElement('div'), i);
386 }
387
388 function MM(){
389 var i = 0, p = 0, s = 'MMMOM]QMM]\\MLMMMOM]QMM]\\ORlT]ORlT]\\]QOLOLMLMOLpA';
390 function n(){
391 var c = s.charCodeAt(i++) - 65, m = 70 + c % 11, z = Math.floor(c / 11);
392 if (p > 70) midiHit(p, 0, false);
393 if (m > 70) midiHit(m, 100, true );
394 if (z > 0) setTimeout(n, z * 187);
395 p = m;
396 }
397 n();
398 }
399 </script>
400 </body>
401 </html>
0 import SimpleJSSynth from './simple-js-synth.js';
1 import * as pattern from './pattern.js';
2
3 const ctx = new window.AudioContext();
4
5 const opts = {};
6 const voices = [];
7 const tones = [];
8
9 export const getOptions = () => opts;
10
11 export const setOptions = (o) => {
12 Object.assign(opts, o);
13
14 while (voices.length)
15 voices.pop().destroy();
16
17 for (let i = 0; i < opts.polyphony; i++)
18 voices.push(SimpleJSSynth(ctx.destination, opts));
19 };
20
21 setOptions({
22 osc1type: "sine", osc1vol: 0.2, osc1tune: 0,
23 osc2type: "square", osc2vol: 0.14, osc2tune: 12,
24 osc3type: "sine", osc3vol: 0.05, osc3tune: -6.7,
25 attack: 0, decay: 0.3, sustain: 0.5, susdecay: 5,
26 cutoff: 36,
27
28 polyphony: 8,
29 });
30
31 export const on = (key, note, vol=1) => {
32 ctx.resume()
33
34 if (tones[key]?.note === note) return;
35 off(key);
36
37 const freq = 110.0 * Math.pow(2, note / pattern.getLength());
38
39 const voice = voices.find(s => s.isReady());
40 if (!voice) return;
41
42 voice.noteOn(freq, vol);
43 tones[key] = off && { note, voice };
44 };
45
46 export const off = (key) => {
47 tones[key]?.voice.noteOff();
48 delete tones[key];
49 };
50
51 export const isOn = (note) => Object.values(tones).some(t => t.note === note);