@marmooo/midy 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/esm/midy-GM1.d.ts +13 -50
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +164 -160
- package/esm/midy-GM2.d.ts +27 -83
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +390 -275
- package/esm/midy-GMLite.d.ts +12 -49
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +170 -163
- package/esm/midy.d.ts +31 -107
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +423 -306
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +13 -50
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +164 -160
- package/script/midy-GM2.d.ts +27 -83
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +390 -275
- package/script/midy-GMLite.d.ts +12 -49
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +170 -163
- package/script/midy.d.ts +31 -107
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +423 -306
package/esm/midy-GMLite.js
CHANGED
|
@@ -1,59 +1,13 @@
|
|
|
1
1
|
import { parseMidi } from "midi-file";
|
|
2
2
|
import { parse, SoundFont } from "@marmooo/soundfont-parser";
|
|
3
|
-
// 2-3 times faster than Map
|
|
4
|
-
class SparseMap {
|
|
5
|
-
constructor(size) {
|
|
6
|
-
this.data = new Array(size);
|
|
7
|
-
this.activeIndices = [];
|
|
8
|
-
}
|
|
9
|
-
set(key, value) {
|
|
10
|
-
if (this.data[key] === undefined) {
|
|
11
|
-
this.activeIndices.push(key);
|
|
12
|
-
}
|
|
13
|
-
this.data[key] = value;
|
|
14
|
-
}
|
|
15
|
-
get(key) {
|
|
16
|
-
return this.data[key];
|
|
17
|
-
}
|
|
18
|
-
delete(key) {
|
|
19
|
-
if (this.data[key] !== undefined) {
|
|
20
|
-
this.data[key] = undefined;
|
|
21
|
-
const index = this.activeIndices.indexOf(key);
|
|
22
|
-
if (index !== -1) {
|
|
23
|
-
this.activeIndices.splice(index, 1);
|
|
24
|
-
}
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
has(key) {
|
|
30
|
-
return this.data[key] !== undefined;
|
|
31
|
-
}
|
|
32
|
-
get size() {
|
|
33
|
-
return this.activeIndices.length;
|
|
34
|
-
}
|
|
35
|
-
clear() {
|
|
36
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
37
|
-
const key = this.activeIndices[i];
|
|
38
|
-
this.data[key] = undefined;
|
|
39
|
-
}
|
|
40
|
-
this.activeIndices = [];
|
|
41
|
-
}
|
|
42
|
-
*[Symbol.iterator]() {
|
|
43
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
44
|
-
const key = this.activeIndices[i];
|
|
45
|
-
yield [key, this.data[key]];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
forEach(callback) {
|
|
49
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
50
|
-
const key = this.activeIndices[i];
|
|
51
|
-
callback(this.data[key], key, this);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
3
|
class Note {
|
|
56
4
|
constructor(noteNumber, velocity, startTime, voice, voiceParams) {
|
|
5
|
+
Object.defineProperty(this, "index", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: -1
|
|
10
|
+
});
|
|
57
11
|
Object.defineProperty(this, "bufferSource", {
|
|
58
12
|
enumerable: true,
|
|
59
13
|
configurable: true,
|
|
@@ -127,7 +81,7 @@ const defaultControllerState = {
|
|
|
127
81
|
modulationDepth: { type: 128 + 1, defaultValue: 0 },
|
|
128
82
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
129
83
|
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
130
|
-
pan: { type: 128 + 10, defaultValue:
|
|
84
|
+
pan: { type: 128 + 10, defaultValue: 64 / 127 },
|
|
131
85
|
expression: { type: 128 + 11, defaultValue: 1 },
|
|
132
86
|
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
133
87
|
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
@@ -338,7 +292,7 @@ export class MidyGMLite {
|
|
|
338
292
|
initSoundFontTable() {
|
|
339
293
|
const table = new Array(128);
|
|
340
294
|
for (let i = 0; i < 128; i++) {
|
|
341
|
-
table[i] = new
|
|
295
|
+
table[i] = new Map();
|
|
342
296
|
}
|
|
343
297
|
return table;
|
|
344
298
|
}
|
|
@@ -390,10 +344,10 @@ export class MidyGMLite {
|
|
|
390
344
|
return {
|
|
391
345
|
currentBufferSource: null,
|
|
392
346
|
isDrum: false,
|
|
393
|
-
...this.constructor.channelSettings,
|
|
394
347
|
state: new ControllerState(),
|
|
348
|
+
...this.constructor.channelSettings,
|
|
395
349
|
...this.setChannelAudioNodes(audioContext),
|
|
396
|
-
scheduledNotes:
|
|
350
|
+
scheduledNotes: [],
|
|
397
351
|
sustainNotes: [],
|
|
398
352
|
};
|
|
399
353
|
});
|
|
@@ -438,24 +392,20 @@ export class MidyGMLite {
|
|
|
438
392
|
}
|
|
439
393
|
return bufferSource;
|
|
440
394
|
}
|
|
441
|
-
async scheduleTimelineEvents(t,
|
|
395
|
+
async scheduleTimelineEvents(t, resumeTime, queueIndex) {
|
|
442
396
|
while (queueIndex < this.timeline.length) {
|
|
443
397
|
const event = this.timeline[queueIndex];
|
|
444
398
|
if (event.startTime > t + this.lookAhead)
|
|
445
399
|
break;
|
|
446
|
-
const
|
|
400
|
+
const delay = this.startDelay - resumeTime;
|
|
401
|
+
const startTime = event.startTime + delay;
|
|
447
402
|
switch (event.type) {
|
|
448
|
-
case "noteOn":
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
case "noteOff": {
|
|
455
|
-
const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
456
|
-
if (notePromise) {
|
|
457
|
-
this.notePromises.push(notePromise);
|
|
458
|
-
}
|
|
403
|
+
case "noteOn": {
|
|
404
|
+
const noteOffEvent = {
|
|
405
|
+
...event.noteOffEvent,
|
|
406
|
+
startTime: event.noteOffEvent.startTime + delay,
|
|
407
|
+
};
|
|
408
|
+
await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
|
|
459
409
|
break;
|
|
460
410
|
}
|
|
461
411
|
case "controller":
|
|
@@ -488,44 +438,53 @@ export class MidyGMLite {
|
|
|
488
438
|
this.isPaused = false;
|
|
489
439
|
this.startTime = this.audioContext.currentTime;
|
|
490
440
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
491
|
-
let
|
|
441
|
+
let resumeTime = this.resumeTime - this.startTime;
|
|
492
442
|
this.notePromises = [];
|
|
493
443
|
const schedulePlayback = async () => {
|
|
494
444
|
if (queueIndex >= this.timeline.length) {
|
|
495
445
|
await Promise.all(this.notePromises);
|
|
496
446
|
this.notePromises = [];
|
|
497
|
-
this.exclusiveClassNotes.
|
|
447
|
+
this.exclusiveClassNotes.fill(undefined);
|
|
448
|
+
this.drumExclusiveClassNotes.fill(undefined);
|
|
498
449
|
this.audioBufferCache.clear();
|
|
450
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
451
|
+
this.resetAllStates(i);
|
|
452
|
+
}
|
|
499
453
|
resolve();
|
|
500
454
|
return;
|
|
501
455
|
}
|
|
502
456
|
const now = this.audioContext.currentTime;
|
|
503
|
-
const t = now +
|
|
504
|
-
queueIndex = await this.scheduleTimelineEvents(t,
|
|
457
|
+
const t = now + resumeTime;
|
|
458
|
+
queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
|
|
505
459
|
if (this.isPausing) {
|
|
506
460
|
await this.stopNotes(0, true, now);
|
|
507
461
|
this.notePromises = [];
|
|
508
|
-
resolve();
|
|
509
462
|
this.isPausing = false;
|
|
510
463
|
this.isPaused = true;
|
|
464
|
+
resolve();
|
|
511
465
|
return;
|
|
512
466
|
}
|
|
513
467
|
else if (this.isStopping) {
|
|
514
468
|
await this.stopNotes(0, true, now);
|
|
515
469
|
this.notePromises = [];
|
|
516
470
|
this.exclusiveClassNotes.fill(undefined);
|
|
471
|
+
this.drumExclusiveClassNotes.fill(undefined);
|
|
517
472
|
this.audioBufferCache.clear();
|
|
518
|
-
|
|
473
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
474
|
+
this.resetAllStates(i);
|
|
475
|
+
}
|
|
519
476
|
this.isStopping = false;
|
|
520
477
|
this.isPaused = false;
|
|
478
|
+
resolve();
|
|
521
479
|
return;
|
|
522
480
|
}
|
|
523
481
|
else if (this.isSeeking) {
|
|
524
482
|
this.stopNotes(0, true, now);
|
|
525
483
|
this.exclusiveClassNotes.fill(undefined);
|
|
484
|
+
this.drumExclusiveClassNotes.fill(undefined);
|
|
526
485
|
this.startTime = this.audioContext.currentTime;
|
|
527
486
|
queueIndex = this.getQueueIndex(this.resumeTime);
|
|
528
|
-
|
|
487
|
+
resumeTime = this.resumeTime - this.startTime;
|
|
529
488
|
this.isSeeking = false;
|
|
530
489
|
await schedulePlayback();
|
|
531
490
|
}
|
|
@@ -611,17 +570,52 @@ export class MidyGMLite {
|
|
|
611
570
|
prevTempoTicks = event.ticks;
|
|
612
571
|
}
|
|
613
572
|
}
|
|
573
|
+
const activeNotes = new Array(this.channels.length * 128);
|
|
574
|
+
for (let i = 0; i < activeNotes.length; i++) {
|
|
575
|
+
activeNotes[i] = [];
|
|
576
|
+
}
|
|
577
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
578
|
+
const event = timeline[i];
|
|
579
|
+
switch (event.type) {
|
|
580
|
+
case "noteOn": {
|
|
581
|
+
const index = event.channel * 128 + event.noteNumber;
|
|
582
|
+
activeNotes[index].push(event);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case "noteOff": {
|
|
586
|
+
const index = event.channel * 128 + event.noteNumber;
|
|
587
|
+
const noteOn = activeNotes[index].pop();
|
|
588
|
+
if (noteOn) {
|
|
589
|
+
noteOn.noteOffEvent = event;
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
const eventString = JSON.stringify(event, null, 2);
|
|
593
|
+
console.warn(`noteOff without matching noteOn: ${eventString}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
614
598
|
return { instruments, timeline };
|
|
615
599
|
}
|
|
600
|
+
stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
|
|
601
|
+
const channel = this.channels[channelNumber];
|
|
602
|
+
const promises = [];
|
|
603
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
604
|
+
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
|
|
605
|
+
this.notePromises.push(promise);
|
|
606
|
+
promises.push(promise);
|
|
607
|
+
});
|
|
608
|
+
return Promise.all(promises);
|
|
609
|
+
}
|
|
616
610
|
stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
|
|
617
611
|
const channel = this.channels[channelNumber];
|
|
618
612
|
const promises = [];
|
|
619
613
|
this.processScheduledNotes(channel, (note) => {
|
|
620
|
-
const promise = this.scheduleNoteOff(channelNumber, note
|
|
614
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
|
|
621
615
|
this.notePromises.push(promise);
|
|
622
616
|
promises.push(promise);
|
|
623
617
|
});
|
|
624
|
-
channel.scheduledNotes
|
|
618
|
+
channel.scheduledNotes = [];
|
|
625
619
|
return Promise.all(promises);
|
|
626
620
|
}
|
|
627
621
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -642,9 +636,6 @@ export class MidyGMLite {
|
|
|
642
636
|
if (!this.isPlaying)
|
|
643
637
|
return;
|
|
644
638
|
this.isStopping = true;
|
|
645
|
-
for (let i = 0; i < this.channels.length; i++) {
|
|
646
|
-
this.resetAllStates(i);
|
|
647
|
-
}
|
|
648
639
|
}
|
|
649
640
|
pause() {
|
|
650
641
|
if (!this.isPlaying || this.isPaused)
|
|
@@ -679,35 +670,31 @@ export class MidyGMLite {
|
|
|
679
670
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
680
671
|
}
|
|
681
672
|
processScheduledNotes(channel, callback) {
|
|
682
|
-
channel.scheduledNotes
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
getActiveNotes(channel, scheduleTime) {
|
|
692
|
-
const activeNotes = new SparseMap(128);
|
|
693
|
-
channel.scheduledNotes.forEach((noteList) => {
|
|
694
|
-
const activeNote = this.getActiveNote(noteList, scheduleTime);
|
|
695
|
-
if (activeNote) {
|
|
696
|
-
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
697
|
-
}
|
|
698
|
-
});
|
|
699
|
-
return activeNotes;
|
|
673
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
674
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
675
|
+
const note = scheduledNotes[i];
|
|
676
|
+
if (!note)
|
|
677
|
+
continue;
|
|
678
|
+
if (note.ending)
|
|
679
|
+
continue;
|
|
680
|
+
callback(note);
|
|
681
|
+
}
|
|
700
682
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
683
|
+
processActiveNotes(channel, scheduleTime, callback) {
|
|
684
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
685
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
686
|
+
const note = scheduledNotes[i];
|
|
704
687
|
if (!note)
|
|
705
|
-
|
|
688
|
+
continue;
|
|
689
|
+
if (note.ending)
|
|
690
|
+
continue;
|
|
691
|
+
const noteOffEvent = note.noteOffEvent;
|
|
692
|
+
if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
|
|
693
|
+
continue;
|
|
706
694
|
if (scheduleTime < note.startTime)
|
|
707
695
|
continue;
|
|
708
|
-
|
|
696
|
+
callback(note);
|
|
709
697
|
}
|
|
710
|
-
return noteList[0];
|
|
711
698
|
}
|
|
712
699
|
cbToRatio(cb) {
|
|
713
700
|
return Math.pow(10, cb / 200);
|
|
@@ -854,6 +841,7 @@ export class MidyGMLite {
|
|
|
854
841
|
this.setVolumeEnvelope(note, now);
|
|
855
842
|
this.setFilterEnvelope(note, now);
|
|
856
843
|
this.setPitchEnvelope(note, now);
|
|
844
|
+
this.updateDetune(channel, note, now);
|
|
857
845
|
if (0 < state.modulationDepth) {
|
|
858
846
|
this.startModulation(channel, note, now);
|
|
859
847
|
}
|
|
@@ -870,7 +858,7 @@ export class MidyGMLite {
|
|
|
870
858
|
if (prev) {
|
|
871
859
|
const [prevNote, prevChannelNumber] = prev;
|
|
872
860
|
if (prevNote && !prevNote.ending) {
|
|
873
|
-
this.scheduleNoteOff(prevChannelNumber, prevNote
|
|
861
|
+
this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
|
|
874
862
|
startTime, true);
|
|
875
863
|
}
|
|
876
864
|
}
|
|
@@ -886,12 +874,12 @@ export class MidyGMLite {
|
|
|
886
874
|
const index = drumExclusiveClass * this.channels.length + channelNumber;
|
|
887
875
|
const prevNote = this.drumExclusiveClassNotes[index];
|
|
888
876
|
if (prevNote && !prevNote.ending) {
|
|
889
|
-
this.scheduleNoteOff(channelNumber, prevNote
|
|
877
|
+
this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
|
|
890
878
|
startTime, true);
|
|
891
879
|
}
|
|
892
880
|
this.drumExclusiveClassNotes[index] = note;
|
|
893
881
|
}
|
|
894
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
882
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
895
883
|
const channel = this.channels[channelNumber];
|
|
896
884
|
const bankNumber = channel.bank;
|
|
897
885
|
const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
|
|
@@ -903,6 +891,7 @@ export class MidyGMLite {
|
|
|
903
891
|
return;
|
|
904
892
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
905
893
|
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
894
|
+
note.noteOffEvent = noteOffEvent;
|
|
906
895
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
907
896
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
908
897
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -911,33 +900,32 @@ export class MidyGMLite {
|
|
|
911
900
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
912
901
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
913
902
|
const scheduledNotes = channel.scheduledNotes;
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
else {
|
|
919
|
-
notes = [note];
|
|
920
|
-
scheduledNotes.set(noteNumber, notes);
|
|
921
|
-
}
|
|
922
|
-
if (this.isDrumNoteOffException(channel, noteNumber)) {
|
|
903
|
+
note.index = scheduledNotes.length;
|
|
904
|
+
scheduledNotes.push(note);
|
|
905
|
+
if (channel.isDrum) {
|
|
923
906
|
const stopTime = startTime + note.bufferSource.buffer.duration;
|
|
924
|
-
const index = notes.length - 1;
|
|
925
907
|
const promise = new Promise((resolve) => {
|
|
926
908
|
note.bufferSource.onended = () => {
|
|
927
|
-
|
|
909
|
+
scheduledNotes[note.index] = undefined;
|
|
910
|
+
this.disconnectNote(note);
|
|
928
911
|
resolve();
|
|
929
912
|
};
|
|
930
913
|
note.bufferSource.stop(stopTime);
|
|
931
914
|
});
|
|
932
915
|
this.notePromises.push(promise);
|
|
933
916
|
}
|
|
917
|
+
else if (noteOffEvent) {
|
|
918
|
+
const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
|
|
919
|
+
if (notePromise) {
|
|
920
|
+
this.notePromises.push(notePromise);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
934
923
|
}
|
|
935
924
|
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
936
925
|
scheduleTime ??= this.audioContext.currentTime;
|
|
937
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
|
|
926
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
|
|
938
927
|
}
|
|
939
|
-
disconnectNote(note
|
|
940
|
-
scheduledNotes[index] = null;
|
|
928
|
+
disconnectNote(note) {
|
|
941
929
|
note.bufferSource.disconnect();
|
|
942
930
|
note.filterNode.disconnect();
|
|
943
931
|
note.volumeEnvelopeNode.disconnect();
|
|
@@ -947,8 +935,7 @@ export class MidyGMLite {
|
|
|
947
935
|
note.modulationLFO.stop();
|
|
948
936
|
}
|
|
949
937
|
}
|
|
950
|
-
stopNote(
|
|
951
|
-
const note = scheduledNotes[index];
|
|
938
|
+
stopNote(channel, note, endTime, stopTime) {
|
|
952
939
|
note.volumeEnvelopeNode.gain
|
|
953
940
|
.cancelScheduledValues(endTime)
|
|
954
941
|
.linearRampToValueAtTime(0, stopTime);
|
|
@@ -958,46 +945,52 @@ export class MidyGMLite {
|
|
|
958
945
|
}, stopTime);
|
|
959
946
|
return new Promise((resolve) => {
|
|
960
947
|
note.bufferSource.onended = () => {
|
|
961
|
-
|
|
948
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
949
|
+
this.disconnectNote(note);
|
|
962
950
|
resolve();
|
|
963
951
|
};
|
|
964
952
|
note.bufferSource.stop(stopTime);
|
|
965
953
|
});
|
|
966
954
|
}
|
|
967
|
-
scheduleNoteOff(channelNumber,
|
|
955
|
+
scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
|
|
968
956
|
const channel = this.channels[channelNumber];
|
|
969
957
|
if (channel.isDrum)
|
|
970
958
|
return;
|
|
971
959
|
if (!force && 0.5 <= channel.state.sustainPedal)
|
|
972
960
|
return;
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
961
|
+
const volRelease = endTime + note.voiceParams.volRelease;
|
|
962
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
963
|
+
note.filterNode.frequency
|
|
964
|
+
.cancelScheduledValues(endTime)
|
|
965
|
+
.linearRampToValueAtTime(0, modRelease);
|
|
966
|
+
const stopTime = Math.min(volRelease, modRelease);
|
|
967
|
+
return this.stopNote(channel, note, endTime, stopTime);
|
|
968
|
+
}
|
|
969
|
+
findNoteOffTarget(channel, noteNumber) {
|
|
970
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
976
971
|
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
977
972
|
const note = scheduledNotes[i];
|
|
978
973
|
if (!note)
|
|
979
974
|
continue;
|
|
980
975
|
if (note.ending)
|
|
981
976
|
continue;
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
note
|
|
985
|
-
.cancelScheduledValues(endTime)
|
|
986
|
-
.linearRampToValueAtTime(0, modRelease);
|
|
987
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
988
|
-
return this.stopNote(endTime, stopTime, scheduledNotes, i);
|
|
977
|
+
if (note.noteNumber !== noteNumber)
|
|
978
|
+
continue;
|
|
979
|
+
return note;
|
|
989
980
|
}
|
|
990
981
|
}
|
|
991
982
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
992
983
|
scheduleTime ??= this.audioContext.currentTime;
|
|
993
|
-
|
|
984
|
+
const channel = this.channels[channelNumber];
|
|
985
|
+
const note = this.findNoteOffTarget(channel, noteNumber);
|
|
986
|
+
return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
|
|
994
987
|
}
|
|
995
988
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
996
989
|
const velocity = halfVelocity * 2;
|
|
997
990
|
const channel = this.channels[channelNumber];
|
|
998
991
|
const promises = [];
|
|
999
992
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1000
|
-
const promise = this.
|
|
993
|
+
const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
|
|
1001
994
|
promises.push(promise);
|
|
1002
995
|
}
|
|
1003
996
|
channel.sustainNotes = [];
|
|
@@ -1153,20 +1146,20 @@ export class MidyGMLite {
|
|
|
1153
1146
|
});
|
|
1154
1147
|
}
|
|
1155
1148
|
createControlChangeHandlers() {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1149
|
+
const handlers = new Array(128);
|
|
1150
|
+
handlers[1] = this.setModulationDepth;
|
|
1151
|
+
handlers[6] = this.dataEntryMSB;
|
|
1152
|
+
handlers[7] = this.setVolume;
|
|
1153
|
+
handlers[10] = this.setPan;
|
|
1154
|
+
handlers[11] = this.setExpression;
|
|
1155
|
+
handlers[38] = this.dataEntryLSB;
|
|
1156
|
+
handlers[64] = this.setSustainPedal;
|
|
1157
|
+
handlers[100] = this.setRPNLSB;
|
|
1158
|
+
handlers[101] = this.setRPNMSB;
|
|
1159
|
+
handlers[120] = this.allSoundOff;
|
|
1160
|
+
handlers[121] = this.resetAllControllers;
|
|
1161
|
+
handlers[123] = this.allNotesOff;
|
|
1162
|
+
return handlers;
|
|
1170
1163
|
}
|
|
1171
1164
|
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1172
1165
|
const handler = this.controlChangeHandlers[controllerType];
|
|
@@ -1308,22 +1301,29 @@ export class MidyGMLite {
|
|
|
1308
1301
|
}
|
|
1309
1302
|
allSoundOff(channelNumber, _value, scheduleTime) {
|
|
1310
1303
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1311
|
-
return this.
|
|
1304
|
+
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
1312
1305
|
}
|
|
1313
1306
|
resetAllStates(channelNumber) {
|
|
1307
|
+
const scheduleTime = this.audioContext.currentTime;
|
|
1314
1308
|
const channel = this.channels[channelNumber];
|
|
1315
1309
|
const state = channel.state;
|
|
1316
|
-
|
|
1317
|
-
|
|
1310
|
+
const entries = Object.entries(defaultControllerState);
|
|
1311
|
+
for (const [key, { type, defaultValue }] of entries) {
|
|
1312
|
+
if (128 <= type) {
|
|
1313
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1314
|
+
}
|
|
1315
|
+
else {
|
|
1316
|
+
state[key] = defaultValue;
|
|
1317
|
+
}
|
|
1318
1318
|
}
|
|
1319
|
-
for (const
|
|
1320
|
-
channel[
|
|
1319
|
+
for (const key of Object.keys(this.constructor.channelSettings)) {
|
|
1320
|
+
channel[key] = this.constructor.channelSettings[key];
|
|
1321
1321
|
}
|
|
1322
1322
|
this.mode = "GM1";
|
|
1323
1323
|
}
|
|
1324
1324
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
|
|
1325
|
-
resetAllControllers(channelNumber) {
|
|
1326
|
-
const
|
|
1325
|
+
resetAllControllers(channelNumber, _value, scheduleTime) {
|
|
1326
|
+
const keys = [
|
|
1327
1327
|
"pitchWheel",
|
|
1328
1328
|
"expression",
|
|
1329
1329
|
"modulationDepth",
|
|
@@ -1331,10 +1331,17 @@ export class MidyGMLite {
|
|
|
1331
1331
|
];
|
|
1332
1332
|
const channel = this.channels[channelNumber];
|
|
1333
1333
|
const state = channel.state;
|
|
1334
|
-
for (let i = 0; i <
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1334
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1335
|
+
const key = keys[i];
|
|
1336
|
+
const { type, defaultValue } = defaultControllerState[key];
|
|
1337
|
+
if (128 <= type) {
|
|
1338
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
state[key] = defaultValue;
|
|
1342
|
+
}
|
|
1337
1343
|
}
|
|
1344
|
+
this.setPitchBend(channelNumber, 8192, scheduleTime);
|
|
1338
1345
|
const settingTypes = [
|
|
1339
1346
|
"rpnMSB",
|
|
1340
1347
|
"rpnLSB",
|
|
@@ -1346,7 +1353,7 @@ export class MidyGMLite {
|
|
|
1346
1353
|
}
|
|
1347
1354
|
allNotesOff(channelNumber, _value, scheduleTime) {
|
|
1348
1355
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1349
|
-
return this.
|
|
1356
|
+
return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
|
|
1350
1357
|
}
|
|
1351
1358
|
handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
|
|
1352
1359
|
switch (data[2]) {
|