-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathscript.js
641 lines (601 loc) · 24.1 KB
/
script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
///////////////////////////////////////////////////////////////////////////////
// global variables
// the entire web midi system's access: https://www.w3.org/TR/webmidi/#idl-def-MIDIAccess
let midiAccess;
// where to send notes: https://www.w3.org/TR/webmidi/#idl-def-MIDIOutput
// initialized to a dummy object
let midiOutput = {
send: () => {
console.error("Midi is unavailable");
}
};
// where to receive notes (if it's not from computer keyboard)
// initialized to dummy
let midiInput = {
data: []
};
// which channel to send events over
let midiChannel = 0;
// which velocity to send
let midiVelocity = 96;
// how much to transpose the events
let transpose = 0;
// our midi engine (initialized with dummy midi output)
let engine = new Engine(midiOutput);
// our selected keyboard layout
let keyboardLayout;
// which keys are *currently* held down and their requisite note values
let pressedKeys = {};
///////////////////////////////////////////////////////////////////////////////
// after the window is loaded ---> initialize
window.addEventListener("load", initialize);
function initialize() {
// check if midi available and request midi access (on success)
if (navigator.requestMIDIAccess) {
navigator.requestMIDIAccess().then(midiAccessSuccess, midiAccessFailure);
}
initializeLogging();
initializeKeyboardLayoutSelect(keyboardLayouts);
initializeArpeggiatorGateInput();
initializeArpeggiatorTimeDivisionButtons();
initializeArpeggiatorModeButtons();
initializeDelayTimeInput();
initializeDelayRepeatInput();
initializeBPMInput();
initializeProgramChangeSelect();
initializeOctaveSelect();
initializeChannelSelect();
initializeLatchButton();
// connect key presses/releases to midi note on/off events
window.addEventListener("keydown", keyPressed);
window.addEventListener("keyup", keyReleased);
}
function keyPressed(e) {
e.preventDefault();
let keyNotPressed = pressedKeys[e.key] === undefined;
let keyHasPitch = keyboardLayout[e.key] !== undefined;
if (keyNotPressed && keyHasPitch) {
// if we got a pitch from the layout and it's not already pressed
// create a new note and save it in the pressed keys
let pitch = keyboardLayout[e.key] + transpose;
let note = new Note(pitch, midiVelocity, midiChannel);;
pressedKeys[e.key] = note;
// and trigger a note on in our engine
engine.noteOn(note.pitch, note.velocity, note.channel);
}
// other possible key behavior here
switch (e.key) {
case "Backspace":
engine.releaseEverything();
break;
case "Delete":
engine.hailMary();
break;
case "Escape": // toggle latch mode
document.querySelector("#latchSection button").dispatchEvent(
new Event("click"));
break;
case "PageUp":
document.querySelector("#programChangeSection select").dispatchEvent(
new Event("next"));
break;
case "PageDown":
document.querySelector("#programChangeSection select").dispatchEvent(
new Event("previous"));
break;
case "F5":
location.reload();
break;
}
if (e.altKey) {
switch (e.key) {
case "ArrowUp":
// increase gate time
document.querySelector("#arpeggiatorGateSection").dispatchEvent(
new Event("next"));
break;
case "ArrowDown":
// decrease gate time
document.querySelector("#arpeggiatorGateSection").dispatchEvent(
new Event("previous"));
break;
case "ArrowRight":
// next arp type
document.querySelector("#arpeggiatorModeSection").dispatchEvent(
new Event("next"));
break;
case "ArrowLeft":
// previous arp type
document.querySelector("#arpeggiatorModeSection").dispatchEvent(
new Event("previous"));
break;
}
}
if (e.shiftKey) {
switch (e.key) {
case "ArrowUp":
// increase delay repeats
document.querySelector("#delayRepeatSection").dispatchEvent(
new Event("next"));
break;
case "ArrowDown":
// decrease delay repeats
document.querySelector("#delayRepeatSection").dispatchEvent(
new Event("previous"));
break;
case "ArrowRight":
// increase delay time
document.querySelector("#delayTimeSection").dispatchEvent(
new Event("next"));
break;
case "ArrowLeft":
// decrease delay time
document.querySelector("#delayTimeSection").dispatchEvent(
new Event("previous"));
break;
}
}
if (e.ctrlKey) {
switch (e.key) {
case "ArrowUp":
// increase octave
document.querySelector("#octaveSelect").dispatchEvent(
new Event("next"));
break;
case "ArrowDown":
// decrease octave
document.querySelector("#octaveSelect").dispatchEvent(
new Event("previous"));
break;
case "ArrowRight":
// previous layout
document.querySelector("#keyboardLayoutSelect").dispatchEvent(
new Event("next"));
break;
case "ArrowLeft":
// next layout
document.querySelector("#keyboardLayoutSelect").dispatchEvent(
new Event("previous"));
break;
}
}
}
function keyReleased(e) {
let note = pressedKeys[e.key];
if (note) {
// if we found a note to release, release then delete it
engine.noteOff(note.pitch, note.velocity, note.channel);
delete pressedKeys[e.key];
}
}
// on midi access successfully acquired
function midiAccessSuccess(midiAccess) {
// save reference to midi access
midiAccess = midiAccess;
// save reference to first midi output (which our callbacks will send data through)
// this is a hacky... method to get the first value of the outputs map (works on ubuntu at least)
midiOutput = midiAccess.outputs.get(midiAccess.outputs.keys().next().value);
// initialize devices select UI (to display the available midi input/output
initializeDevicesSelect(midiAccess);
}
// on midi access unsuccessfully acquired
function midiAccessFailure() {
console.error("Could not initialize midi")
}
// populate midi devices on UI, select a default output device
// hook a change callback to our select elements
function initializeDevicesSelect(midiAccess) {
initializeOutputDeviceSelect(midiAccess);
initializeInputDeviceSelect(midiAccess);
}
function initializeOutputDeviceSelect(midiAccess) {
let selectElement = document.querySelector("#outputDeviceSelect");
// for each entry in available midi output
for (var entry of midiAccess.outputs) {
// append an option to our device select element
let output = entry[1];
let option = document.createElement("option");
option.text = output.name;
selectElement.add(option);
}
// register a change callback for this device select element
selectElement.addEventListener("change", (e) => {
let selectedMidiOutputName = e.target.value;
for (let entry of midiAccess.outputs) {
let output = entry[1];
if (output.name == selectedMidiOutputName) {
midiOutput = output;
engine.setMidiOutput(output);
return;
}
}
});
// pick a default midi output
selectElement.selectedIndex = "0";
selectElement.dispatchEvent(new Event("change"));
}
function initializeInputDeviceSelect(midiAccess) {
let selectElement = document.querySelector("#inputDeviceSelect");
let numberOfInputs = 0;
// for each entry in available midi inputs
for (var entry of midiAccess.inputs) {
// append an option to our device select element
let input = entry[1];
let option = document.createElement("option");
option.text = input.name;
selectElement.add(option);
numberOfInputs += 1;
}
// register a change callback for this device select element
selectElement.addEventListener("change", (e) => {
// remove old midi input's handler
midiInput.onmidimessage = undefined;
// find the new midi input and register our handler there
let selectedMidiInputName = e.target.value;
for (let entry of midiAccess.inputs) {
let input = entry[1];
if (input.name == selectedMidiInputName) {
input.onmidimessage = midiInputCallback;
midiInput = input;
}
}
});
// pick a default midi input, preferably different than the output
if (numberOfInputs > 1) {
selectElement.selectedIndex = "1";
} else {
selectElement.selectedIndex = "0";
}
selectElement.dispatchEvent(new Event("change"));
}
// hand off input midi messages into the engine
// IT SHOULD BE NOTED, when the midi input device is the same as the midi
// output device (names match) input midi messages will not go through.
// It creates a feedback loop. I did this. You really don't want to do this.
function midiInputCallback(midiMessage) {
if (midiInput.name == engine.midiOutput.name) {
return;
}
let channel = midiMessage.data[0] & 0xf;
switch (midiMessage.data[0] >> 4) {
case 0x9: // note on when velocity > 0 and note off otherwise
if (midiMessage.data[2] > 0) {
engine.noteOn(midiMessage.data[1], midiMessage.data[2], channel);
} else {
engine.noteOff(midiMessage.data[1], midiMessage.data[2], channel);
}
break;
case 0x8: // note off
engine.noteOff(midiMessage.data[1], midiMessage.data[2], channel);
break;
case 0xc: // program change
engine.programChange(midiMessage.data[1], channel);
break;
}
}
// populate keyboard layouts on UI, select a layout, hook a change callback to our select element
function initializeKeyboardLayoutSelect(keyboardLayouts) {
let selectElement = document.querySelector("#keyboardLayoutSelect");
let numberOfLayouts = 0;
// create layout name options for our keyboard layout select element
Object.keys(keyboardLayouts).forEach(keyboardLayoutName => {
let option = document.createElement("option");
option.text = keyboardLayoutName;
selectElement.add(option);
numberOfLayouts += 1;
});
// attach a callback for changes
selectElement.addEventListener("change", (e) => {
keyboardLayout = keyboardLayouts[e.target.value];
});
// pick a default option
selectElement.selectedIndex = 0;
selectElement.dispatchEvent(new Event("change"));
// attach callbacks for next and previous events
selectElement.addEventListener("next", (e) => {
selectElement.selectedIndex = (selectElement.selectedIndex + 1) % numberOfLayouts;
selectElement.dispatchEvent(new Event("change"));
});
selectElement.addEventListener("previous", (e) => {
if (selectElement.selectedIndex > 0) {
selectElement.selectedIndex -= 1;
} else {
selectElement.selectedIndex = numberOfLayouts - 1;
}
selectElement.dispatchEvent(new Event("change"));
});
}
// attach a callback to the engine which watches midi traffic
function initializeLogging() {
let element = document.querySelector("#log");
// arbitrarily chosen number of logging lines
let numberOfLoggingLines = 64;
let loggingLines = new Array(numberOfLoggingLines);
engine.setLoggingCallback((midiBytesArray) => {
// convert the midi bytes into a string suitable for display
let line = midiBytesArray.map(b => b.toString().padStart(2, "0")).join(" ");
// out with the old and in with the new
loggingLines.unshift(line);
loggingLines.pop();
// display
element.innerHTML = loggingLines.join("</br>");
});
}
function initializeOctaveSelect() {
let selectElement = document.querySelector("#octaveSelect");
// add a callback to the select element
selectElement.addEventListener("change", (e) => {
// we receive octave 4...
// c4 = midi 60 = 12*(4 + 1)
transpose = 12 * (parseInt(e.target.value) + 1);
});
// select a default option, let's pick whichever octave 3
let options = document.querySelectorAll("#octaveSection option");
let defaultOption;
options.forEach(option => {
if (parseInt(option.value) == 3) {
defaultOption = option;
}
});
selectElement.selectedIndex = defaultOption.index;
selectElement.dispatchEvent(new Event("change"));
// attach callbacks for next and previous events
selectElement.addEventListener("next", (e) => {
selectElement.selectedIndex = (selectElement.selectedIndex + 1) % selectElement.length;
selectElement.dispatchEvent(new Event("change"));
});
selectElement.addEventListener("previous", (e) => {
if (selectElement.selectedIndex > 0) {
selectElement.selectedIndex -= 1;
} else {
selectElement.selectedIndex = selectElement.length - 1;
}
selectElement.dispatchEvent(new Event("change"));
});
}
function initializeChannelSelect() {
selectElement = document.querySelector("#channelSection select");
// add a callback to the select element
selectElement.addEventListener("change", (e) => {
midiChannel = parseInt(e.target.value);
});
// select first
selectElement.selectedIndex = 0;
selectElement.dispatchEvent(new Event("change"));
}
function initializeBPMInput() {
let inputElement = document.querySelector("#bpmSection input");
let labelElement = document.querySelector("#bpmSection label");
// add callback to input element change
inputElement.addEventListener("input", (e) => {
let v = parseFloat(e.target.value);
// input[type=number] will APPARENTLY send invalid input outside of the
// min/max specified. Our engine should handle it no problemo, the
// problemo is that the element will display an incorrect value there.
// We display whatever the engine allowed to occur.
engine.setBPM(v);
// update our UI
labelElement.textContent = engine.getBPM();
});
// fire initial event to set it
inputElement.dispatchEvent(new Event("input"));
}
function initializeArpeggiatorGateInput() {
let inputElement = document.querySelector("#arpeggiatorGateSection input");
let labelElement = document.querySelector("#arpeggiatorGateSection label");
// add callback to input element change
inputElement.addEventListener("input", (e) => {
let v = parseFloat(e.target.value);
// send value into engine
engine.setArpeggiatorGateTime(v);
// update our UI
labelElement.textContent = Math.trunc(v * 100).toString() + "%";
});
// fire initial event to set it
inputElement.dispatchEvent(new Event("input"));
// register more previous next event callbacks on this section
element = document.querySelector("#arpeggiatorGateSection");
element.addEventListener("next", (e) => {
inputElement.value = parseFloat(inputElement.value) + parseFloat(inputElement.step)
inputElement.dispatchEvent(new Event("input"));
});
element.addEventListener("previous", (e) => {
inputElement.value = parseFloat(inputElement.value) - parseFloat(inputElement.step)
inputElement.dispatchEvent(new Event("input"));
});
}
function initializeDelayTimeInput() {
let inputElement = document.querySelector("#delayTimeSection input");
let labelElement = document.querySelector("#delayTimeSection label");
// add callback to input element change
inputElement.addEventListener("input", (e) => {
let v = parseInt(e.target.value);
// send value into engine
engine.setDelayTimeInMilliseconds(v);
// update our UI
labelElement.textContent = v.toString() + "ms";
});
// fire initial event to set it
inputElement.dispatchEvent(new Event("input"));
// register more previous next event callbacks on this section
element = document.querySelector("#delayTimeSection");
element.addEventListener("next", (e) => {
inputElement.value = parseFloat(inputElement.value) + parseFloat(inputElement.step)
inputElement.dispatchEvent(new Event("input"));
});
element.addEventListener("previous", (e) => {
inputElement.value = parseFloat(inputElement.value) - parseFloat(inputElement.step)
inputElement.dispatchEvent(new Event("input"));
});
}
function initializeDelayRepeatInput() {
let inputElement = document.querySelector("#delayRepeatSection input");
let labelElement = document.querySelector("#delayRepeatSection label");
// add callback to input element change
inputElement.addEventListener("input", (e) => {
let v = parseInt(e.target.value);
// send value into engine
engine.setDelayRepeats(v);
// update our UI
labelElement.textContent = v.toString();
});
// fire initial event to set it
inputElement.dispatchEvent(new Event("input"));
// register more previous next event callbacks on this section
element = document.querySelector("#delayRepeatSection");
element.addEventListener("next", (e) => {
inputElement.value = parseFloat(inputElement.value) + parseFloat(inputElement.step)
inputElement.dispatchEvent(new Event("input"));
});
element.addEventListener("previous", (e) => {
inputElement.value = parseFloat(inputElement.value) - parseFloat(inputElement.step)
inputElement.dispatchEvent(new Event("input"));
});
}
function initializeArpeggiatorModeButtons() {
let element = document.querySelector("#arpeggiatorModeSection");
let buttons = Array.from(document.querySelectorAll("#arpeggiatorModeSection button"));
let lastSelectedButtonIndex = buttons.findIndex((b) => b.id === "arpeggiatorOff");
// handle button click events
element.addEventListener("click", (e) => {
// return if we're not responding to a button
if (e.target.tagName.toLowerCase() !== "button") {
return;
}
switch (e.target.id) {
case "arpeggiatorOff":
engine.setArpeggiatorMode(ArpeggiatorMode.OFF);
break;
case "arpeggiatorUp":
engine.setArpeggiatorMode(ArpeggiatorMode.UP);
break;
case "arpeggiatorDown":
engine.setArpeggiatorMode(ArpeggiatorMode.DOWN);
break;
case "arpeggiatorUpDown":
engine.setArpeggiatorMode(ArpeggiatorMode.UP_DOWN);
break;
case "arpeggiatorOrder":
engine.setArpeggiatorMode(ArpeggiatorMode.ORDER);
break;
case "arpeggiatorReverse":
engine.setArpeggiatorMode(ArpeggiatorMode.REVERSE);
break;
case "arpeggiatorRandom":
engine.setArpeggiatorMode(ArpeggiatorMode.RANDOM);
break;
case "arpeggiatorRandom2":
engine.setArpeggiatorMode(ArpeggiatorMode.RANDOM_2);
break;
case "arpeggiatorUp2":
engine.setArpeggiatorMode(ArpeggiatorMode.UP_2);
break;
case "arpeggiatorDown2":
engine.setArpeggiatorMode(ArpeggiatorMode.DOWN_2);
break;
};
// update button style
selectAndUnselectButtons(e.target, buttons[lastSelectedButtonIndex]);
lastSelectedButtonIndex = buttons.findIndex(b => b.id === e.target.id);
});
// select the default button
buttons[lastSelectedButtonIndex].click();
// handle next event
element.addEventListener("next", (e) => {
let i = (lastSelectedButtonIndex + 1) % buttons.length;
buttons[i].click();
});
// handle previous event
element.addEventListener("previous", (e) => {
let i = (lastSelectedButtonIndex > 0) ?
((lastSelectedButtonIndex - 1) % buttons.length) :
buttons.length - 1;
buttons[i].click();
});
}
function initializeArpeggiatorTimeDivisionButtons() {
let element = document.querySelector("#arpeggiatorTimeDivisionSection");
let lastSelectedButton = document.querySelector("#sixteenthNote");
element.addEventListener("click", (e) => {
// return if we're not responding to a button
if (e.target.tagName.toLowerCase() !== "button") {
return;
}
switch (e.target.id) {
case "quarterNote":
engine.setArpeggiatorTimeDivision(TimeDivision.QUARTER_NOTE);
break;
case "eighthNote":
engine.setArpeggiatorTimeDivision(TimeDivision.EIGHTH_NOTE);
break;
case "sixteenthNote":
engine.setArpeggiatorTimeDivision(TimeDivision.SIXTEENTH_NOTE);
break;
case "eighthNoteTriplet":
engine.setArpeggiatorTimeDivision(TimeDivision.EIGHTH_NOTE_TRIPLET);
break;
case "sixteenthNoteTriplet":
engine.setArpeggiatorTimeDivision(TimeDivision.SIXTEENTH_NOTE_TRIPLET);
break;
};
// update button style
selectAndUnselectButtons(e.target, lastSelectedButton);
lastSelectedButton = e.target;
});
// select the default button
lastSelectedButton.click();
}
function initializeLatchButton() {
let button = document.querySelector("#latchSection button");
button.addEventListener("click", (e) => {
if (e.target.textContent.toLowerCase() === "off") {
engine.setLatch(true);
e.target.textContent = "on";
selectAndUnselectButtons(e.target, undefined);
} else {
engine.setLatch(false);
e.target.textContent = "off";
selectAndUnselectButtons(undefined, e.target);
}
});
}
function initializeProgramChangeSelect() {
let selectElement = document.querySelector("#programChangeSection select");
// add a callback to the select element
selectElement.addEventListener("change", (e) => {
// parse the value from this button and return on weird
let programChangeValue = parseInt(e.target.value);
if (programChangeValue == undefined) {
return;
}
// send the message
engine.programChange(programChangeValue, midiChannel);
});
// attach callbacks for next and previous events
selectElement.addEventListener("next", (e) => {
selectElement.selectedIndex = (selectElement.selectedIndex + 1) % selectElement.length;
selectElement.dispatchEvent(new Event("change"));
});
selectElement.addEventListener("previous", (e) => {
if (selectElement.selectedIndex > 0) {
selectElement.selectedIndex -= 1;
} else {
selectElement.selectedIndex = selectElement.length - 1;
}
selectElement.dispatchEvent(new Event("change"));
});
}
// selects and unselects two buttons
// if only the 1st button is specified, it just selects it
// this is only meant to be used as a helper function for components which
// track which button was just clicked and which was last clicked
function selectAndUnselectButtons(selectedButton, unselectedButton) {
if (unselectedButton) {
unselectedButton.style.border = "";
unselectedButton.style.background = "";
}
if (selectedButton) {
selectedButton.style.border = "var(--selected-border)";
selectedButton.style.background = "var(--selected-background-color)";
}
}