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