@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/script/midy-GM2.js
CHANGED
|
@@ -3,60 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MidyGM2 = void 0;
|
|
4
4
|
const midi_file_1 = require("midi-file");
|
|
5
5
|
const soundfont_parser_1 = require("@marmooo/soundfont-parser");
|
|
6
|
-
// 2-3 times faster than Map
|
|
7
|
-
class SparseMap {
|
|
8
|
-
constructor(size) {
|
|
9
|
-
this.data = new Array(size);
|
|
10
|
-
this.activeIndices = [];
|
|
11
|
-
}
|
|
12
|
-
set(key, value) {
|
|
13
|
-
if (this.data[key] === undefined) {
|
|
14
|
-
this.activeIndices.push(key);
|
|
15
|
-
}
|
|
16
|
-
this.data[key] = value;
|
|
17
|
-
}
|
|
18
|
-
get(key) {
|
|
19
|
-
return this.data[key];
|
|
20
|
-
}
|
|
21
|
-
delete(key) {
|
|
22
|
-
if (this.data[key] !== undefined) {
|
|
23
|
-
this.data[key] = undefined;
|
|
24
|
-
const index = this.activeIndices.indexOf(key);
|
|
25
|
-
if (index !== -1) {
|
|
26
|
-
this.activeIndices.splice(index, 1);
|
|
27
|
-
}
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
has(key) {
|
|
33
|
-
return this.data[key] !== undefined;
|
|
34
|
-
}
|
|
35
|
-
get size() {
|
|
36
|
-
return this.activeIndices.length;
|
|
37
|
-
}
|
|
38
|
-
clear() {
|
|
39
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
40
|
-
const key = this.activeIndices[i];
|
|
41
|
-
this.data[key] = undefined;
|
|
42
|
-
}
|
|
43
|
-
this.activeIndices = [];
|
|
44
|
-
}
|
|
45
|
-
*[Symbol.iterator]() {
|
|
46
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
47
|
-
const key = this.activeIndices[i];
|
|
48
|
-
yield [key, this.data[key]];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
forEach(callback) {
|
|
52
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
53
|
-
const key = this.activeIndices[i];
|
|
54
|
-
callback(this.data[key], key, this);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
6
|
class Note {
|
|
59
7
|
constructor(noteNumber, velocity, startTime, voice, voiceParams) {
|
|
8
|
+
Object.defineProperty(this, "index", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: -1
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "noteOffEvent", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: void 0
|
|
19
|
+
});
|
|
60
20
|
Object.defineProperty(this, "bufferSource", {
|
|
61
21
|
enumerable: true,
|
|
62
22
|
configurable: true,
|
|
@@ -141,11 +101,11 @@ class Note {
|
|
|
141
101
|
writable: true,
|
|
142
102
|
value: void 0
|
|
143
103
|
});
|
|
144
|
-
Object.defineProperty(this, "
|
|
104
|
+
Object.defineProperty(this, "portamentoNoteNumber", {
|
|
145
105
|
enumerable: true,
|
|
146
106
|
configurable: true,
|
|
147
107
|
writable: true,
|
|
148
|
-
value:
|
|
108
|
+
value: -1
|
|
149
109
|
});
|
|
150
110
|
this.noteNumber = noteNumber;
|
|
151
111
|
this.velocity = velocity;
|
|
@@ -200,7 +160,7 @@ const defaultControllerState = {
|
|
|
200
160
|
portamentoTime: { type: 128 + 5, defaultValue: 0 },
|
|
201
161
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
202
162
|
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
203
|
-
pan: { type: 128 + 10, defaultValue:
|
|
163
|
+
pan: { type: 128 + 10, defaultValue: 64 / 127 },
|
|
204
164
|
expression: { type: 128 + 11, defaultValue: 1 },
|
|
205
165
|
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
206
166
|
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
@@ -208,14 +168,6 @@ const defaultControllerState = {
|
|
|
208
168
|
portamento: { type: 128 + 65, defaultValue: 0 },
|
|
209
169
|
sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
|
|
210
170
|
softPedal: { type: 128 + 67, defaultValue: 0 },
|
|
211
|
-
filterResonance: { type: 128 + 71, defaultValue: 0.5 },
|
|
212
|
-
releaseTime: { type: 128 + 72, defaultValue: 0.5 },
|
|
213
|
-
attackTime: { type: 128 + 73, defaultValue: 0.5 },
|
|
214
|
-
brightness: { type: 128 + 74, defaultValue: 0.5 },
|
|
215
|
-
decayTime: { type: 128 + 75, defaultValue: 0.5 },
|
|
216
|
-
vibratoRate: { type: 128 + 76, defaultValue: 0.5 },
|
|
217
|
-
vibratoDepth: { type: 128 + 77, defaultValue: 0.5 },
|
|
218
|
-
vibratoDelay: { type: 128 + 78, defaultValue: 0.5 },
|
|
219
171
|
reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
|
|
220
172
|
chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
|
|
221
173
|
// dataIncrement: { type: 128 + 96, defaultValue: 0 },
|
|
@@ -490,7 +442,7 @@ class MidyGM2 {
|
|
|
490
442
|
initSoundFontTable() {
|
|
491
443
|
const table = new Array(128);
|
|
492
444
|
for (let i = 0; i < 128; i++) {
|
|
493
|
-
table[i] = new
|
|
445
|
+
table[i] = new Map();
|
|
494
446
|
}
|
|
495
447
|
return table;
|
|
496
448
|
}
|
|
@@ -537,18 +489,24 @@ class MidyGM2 {
|
|
|
537
489
|
merger,
|
|
538
490
|
};
|
|
539
491
|
}
|
|
492
|
+
resetChannelTable(channel) {
|
|
493
|
+
this.resetControlTable(channel.controlTable);
|
|
494
|
+
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
495
|
+
channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
|
|
496
|
+
channel.keyBasedInstrumentControlTable.fill(0); // [-64, 63]
|
|
497
|
+
}
|
|
540
498
|
createChannels(audioContext) {
|
|
541
499
|
const channels = Array.from({ length: this.numChannels }, () => {
|
|
542
500
|
return {
|
|
543
501
|
currentBufferSource: null,
|
|
544
502
|
isDrum: false,
|
|
545
|
-
...this.constructor.channelSettings,
|
|
546
503
|
state: new ControllerState(),
|
|
547
|
-
|
|
504
|
+
...this.constructor.channelSettings,
|
|
548
505
|
...this.setChannelAudioNodes(audioContext),
|
|
549
|
-
scheduledNotes:
|
|
506
|
+
scheduledNotes: [],
|
|
550
507
|
sustainNotes: [],
|
|
551
|
-
sostenutoNotes:
|
|
508
|
+
sostenutoNotes: [],
|
|
509
|
+
controlTable: this.initControlTable(),
|
|
552
510
|
scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
|
|
553
511
|
channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
|
|
554
512
|
keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
|
|
@@ -595,46 +553,20 @@ class MidyGM2 {
|
|
|
595
553
|
}
|
|
596
554
|
return bufferSource;
|
|
597
555
|
}
|
|
598
|
-
|
|
599
|
-
const endEvent = this.timeline[queueIndex];
|
|
600
|
-
if (!this.channels[endEvent.channel].portamento)
|
|
601
|
-
return;
|
|
602
|
-
const endTime = endEvent.startTime;
|
|
603
|
-
let target;
|
|
604
|
-
while (++queueIndex < this.timeline.length) {
|
|
605
|
-
const event = this.timeline[queueIndex];
|
|
606
|
-
if (endTime !== event.startTime)
|
|
607
|
-
break;
|
|
608
|
-
if (event.type !== "noteOn")
|
|
609
|
-
continue;
|
|
610
|
-
if (!target || event.noteNumber < target.noteNumber) {
|
|
611
|
-
target = event;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
return target;
|
|
615
|
-
}
|
|
616
|
-
async scheduleTimelineEvents(t, offset, queueIndex) {
|
|
556
|
+
async scheduleTimelineEvents(t, resumeTime, queueIndex) {
|
|
617
557
|
while (queueIndex < this.timeline.length) {
|
|
618
558
|
const event = this.timeline[queueIndex];
|
|
619
559
|
if (event.startTime > t + this.lookAhead)
|
|
620
560
|
break;
|
|
621
|
-
const
|
|
561
|
+
const delay = this.startDelay - resumeTime;
|
|
562
|
+
const startTime = event.startTime + delay;
|
|
622
563
|
switch (event.type) {
|
|
623
|
-
case "noteOn":
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
case "noteOff": {
|
|
630
|
-
const portamentoTarget = this.findPortamentoTarget(queueIndex);
|
|
631
|
-
if (portamentoTarget)
|
|
632
|
-
portamentoTarget.portamento = true;
|
|
633
|
-
const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
|
|
634
|
-
portamentoTarget?.noteNumber);
|
|
635
|
-
if (notePromise) {
|
|
636
|
-
this.notePromises.push(notePromise);
|
|
637
|
-
}
|
|
564
|
+
case "noteOn": {
|
|
565
|
+
const noteOffEvent = {
|
|
566
|
+
...event.noteOffEvent,
|
|
567
|
+
startTime: event.noteOffEvent.startTime + delay,
|
|
568
|
+
};
|
|
569
|
+
await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
|
|
638
570
|
break;
|
|
639
571
|
}
|
|
640
572
|
case "controller":
|
|
@@ -670,7 +602,7 @@ class MidyGM2 {
|
|
|
670
602
|
this.isPaused = false;
|
|
671
603
|
this.startTime = this.audioContext.currentTime;
|
|
672
604
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
673
|
-
let
|
|
605
|
+
let resumeTime = this.resumeTime - this.startTime;
|
|
674
606
|
this.notePromises = [];
|
|
675
607
|
const schedulePlayback = async () => {
|
|
676
608
|
if (queueIndex >= this.timeline.length) {
|
|
@@ -679,18 +611,21 @@ class MidyGM2 {
|
|
|
679
611
|
this.exclusiveClassNotes.fill(undefined);
|
|
680
612
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
681
613
|
this.audioBufferCache.clear();
|
|
614
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
615
|
+
this.resetAllStates(i);
|
|
616
|
+
}
|
|
682
617
|
resolve();
|
|
683
618
|
return;
|
|
684
619
|
}
|
|
685
620
|
const now = this.audioContext.currentTime;
|
|
686
|
-
const t = now +
|
|
687
|
-
queueIndex = await this.scheduleTimelineEvents(t,
|
|
621
|
+
const t = now + resumeTime;
|
|
622
|
+
queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
|
|
688
623
|
if (this.isPausing) {
|
|
689
624
|
await this.stopNotes(0, true, now);
|
|
690
625
|
this.notePromises = [];
|
|
691
|
-
resolve();
|
|
692
626
|
this.isPausing = false;
|
|
693
627
|
this.isPaused = true;
|
|
628
|
+
resolve();
|
|
694
629
|
return;
|
|
695
630
|
}
|
|
696
631
|
else if (this.isStopping) {
|
|
@@ -699,9 +634,12 @@ class MidyGM2 {
|
|
|
699
634
|
this.exclusiveClassNotes.fill(undefined);
|
|
700
635
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
701
636
|
this.audioBufferCache.clear();
|
|
702
|
-
|
|
637
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
638
|
+
this.resetAllStates(i);
|
|
639
|
+
}
|
|
703
640
|
this.isStopping = false;
|
|
704
641
|
this.isPaused = false;
|
|
642
|
+
resolve();
|
|
705
643
|
return;
|
|
706
644
|
}
|
|
707
645
|
else if (this.isSeeking) {
|
|
@@ -710,7 +648,7 @@ class MidyGM2 {
|
|
|
710
648
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
711
649
|
this.startTime = this.audioContext.currentTime;
|
|
712
650
|
queueIndex = this.getQueueIndex(this.resumeTime);
|
|
713
|
-
|
|
651
|
+
resumeTime = this.resumeTime - this.startTime;
|
|
714
652
|
this.isSeeking = false;
|
|
715
653
|
await schedulePlayback();
|
|
716
654
|
}
|
|
@@ -832,17 +770,52 @@ class MidyGM2 {
|
|
|
832
770
|
prevTempoTicks = event.ticks;
|
|
833
771
|
}
|
|
834
772
|
}
|
|
773
|
+
const activeNotes = new Array(this.channels.length * 128);
|
|
774
|
+
for (let i = 0; i < activeNotes.length; i++) {
|
|
775
|
+
activeNotes[i] = [];
|
|
776
|
+
}
|
|
777
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
778
|
+
const event = timeline[i];
|
|
779
|
+
switch (event.type) {
|
|
780
|
+
case "noteOn": {
|
|
781
|
+
const index = event.channel * 128 + event.noteNumber;
|
|
782
|
+
activeNotes[index].push(event);
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
case "noteOff": {
|
|
786
|
+
const index = event.channel * 128 + event.noteNumber;
|
|
787
|
+
const noteOn = activeNotes[index].pop();
|
|
788
|
+
if (noteOn) {
|
|
789
|
+
noteOn.noteOffEvent = event;
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
const eventString = JSON.stringify(event, null, 2);
|
|
793
|
+
console.warn(`noteOff without matching noteOn: ${eventString}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
835
798
|
return { instruments, timeline };
|
|
836
799
|
}
|
|
800
|
+
stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
|
|
801
|
+
const channel = this.channels[channelNumber];
|
|
802
|
+
const promises = [];
|
|
803
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
804
|
+
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
|
|
805
|
+
this.notePromises.push(promise);
|
|
806
|
+
promises.push(promise);
|
|
807
|
+
});
|
|
808
|
+
return Promise.all(promises);
|
|
809
|
+
}
|
|
837
810
|
stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
|
|
838
811
|
const channel = this.channels[channelNumber];
|
|
839
812
|
const promises = [];
|
|
840
813
|
this.processScheduledNotes(channel, (note) => {
|
|
841
|
-
const promise = this.scheduleNoteOff(channelNumber, note
|
|
814
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
|
|
842
815
|
this.notePromises.push(promise);
|
|
843
816
|
promises.push(promise);
|
|
844
817
|
});
|
|
845
|
-
channel.scheduledNotes
|
|
818
|
+
channel.scheduledNotes = [];
|
|
846
819
|
return Promise.all(promises);
|
|
847
820
|
}
|
|
848
821
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -863,9 +836,6 @@ class MidyGM2 {
|
|
|
863
836
|
if (!this.isPlaying)
|
|
864
837
|
return;
|
|
865
838
|
this.isStopping = true;
|
|
866
|
-
for (let i = 0; i < this.channels.length; i++) {
|
|
867
|
-
this.resetAllStates(i);
|
|
868
|
-
}
|
|
869
839
|
}
|
|
870
840
|
pause() {
|
|
871
841
|
if (!this.isPlaying || this.isPaused)
|
|
@@ -900,35 +870,31 @@ class MidyGM2 {
|
|
|
900
870
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
901
871
|
}
|
|
902
872
|
processScheduledNotes(channel, callback) {
|
|
903
|
-
channel.scheduledNotes
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
getActiveNotes(channel, scheduleTime) {
|
|
913
|
-
const activeNotes = new SparseMap(128);
|
|
914
|
-
channel.scheduledNotes.forEach((noteList) => {
|
|
915
|
-
const activeNote = this.getActiveNote(noteList, scheduleTime);
|
|
916
|
-
if (activeNote) {
|
|
917
|
-
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
918
|
-
}
|
|
919
|
-
});
|
|
920
|
-
return activeNotes;
|
|
873
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
874
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
875
|
+
const note = scheduledNotes[i];
|
|
876
|
+
if (!note)
|
|
877
|
+
continue;
|
|
878
|
+
if (note.ending)
|
|
879
|
+
continue;
|
|
880
|
+
callback(note);
|
|
881
|
+
}
|
|
921
882
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
883
|
+
processActiveNotes(channel, scheduleTime, callback) {
|
|
884
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
885
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
886
|
+
const note = scheduledNotes[i];
|
|
925
887
|
if (!note)
|
|
926
|
-
|
|
888
|
+
continue;
|
|
889
|
+
if (note.ending)
|
|
890
|
+
continue;
|
|
891
|
+
const noteOffEvent = note.noteOffEvent;
|
|
892
|
+
if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
|
|
893
|
+
continue;
|
|
927
894
|
if (scheduleTime < note.startTime)
|
|
928
895
|
continue;
|
|
929
|
-
|
|
896
|
+
callback(note);
|
|
930
897
|
}
|
|
931
|
-
return noteList[0];
|
|
932
898
|
}
|
|
933
899
|
createConvolutionReverbImpulse(audioContext, decay, preDecay) {
|
|
934
900
|
const sampleRate = audioContext.sampleRate;
|
|
@@ -1093,25 +1059,95 @@ class MidyGM2 {
|
|
|
1093
1059
|
}
|
|
1094
1060
|
updateDetune(channel, note, scheduleTime) {
|
|
1095
1061
|
const noteDetune = this.calcNoteDetune(channel, note);
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
.
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1062
|
+
const pitchControl = this.getPitchControl(channel, note);
|
|
1063
|
+
const detune = channel.detune + noteDetune + pitchControl;
|
|
1064
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1065
|
+
const startTime = note.startTime;
|
|
1066
|
+
const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
|
|
1067
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1068
|
+
note.bufferSource.detune
|
|
1069
|
+
.cancelScheduledValues(scheduleTime)
|
|
1070
|
+
.setValueAtTime(detune - deltaCent, scheduleTime)
|
|
1071
|
+
.linearRampToValueAtTime(detune, portamentoTime);
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
note.bufferSource.detune
|
|
1075
|
+
.cancelScheduledValues(scheduleTime)
|
|
1076
|
+
.setValueAtTime(detune, scheduleTime);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
getPortamentoTime(channel, note) {
|
|
1080
|
+
const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
|
|
1081
|
+
const value = Math.ceil(channel.state.portamentoTime * 127);
|
|
1082
|
+
return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
|
|
1083
|
+
}
|
|
1084
|
+
getPitchIncrementSpeed(value) {
|
|
1085
|
+
const points = [
|
|
1086
|
+
[0, 1000],
|
|
1087
|
+
[6, 100],
|
|
1088
|
+
[16, 20],
|
|
1089
|
+
[32, 10],
|
|
1090
|
+
[48, 5],
|
|
1091
|
+
[64, 2.5],
|
|
1092
|
+
[80, 1],
|
|
1093
|
+
[96, 0.4],
|
|
1094
|
+
[112, 0.15],
|
|
1095
|
+
[127, 0.01],
|
|
1096
|
+
];
|
|
1097
|
+
const logPoints = new Array(points.length);
|
|
1098
|
+
for (let i = 0; i < points.length; i++) {
|
|
1099
|
+
const [x, y] = points[i];
|
|
1100
|
+
if (value === x)
|
|
1101
|
+
return y;
|
|
1102
|
+
logPoints[i] = [x, Math.log(y)];
|
|
1103
|
+
}
|
|
1104
|
+
let startIndex = 0;
|
|
1105
|
+
for (let i = 1; i < logPoints.length; i++) {
|
|
1106
|
+
if (value <= logPoints[i][0]) {
|
|
1107
|
+
startIndex = i - 1;
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const [x0, y0] = logPoints[startIndex];
|
|
1112
|
+
const [x1, y1] = logPoints[startIndex + 1];
|
|
1113
|
+
const h = x1 - x0;
|
|
1114
|
+
const t = (value - x0) / h;
|
|
1115
|
+
let m0, m1;
|
|
1116
|
+
if (startIndex === 0) {
|
|
1117
|
+
m0 = (y1 - y0) / h;
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
const [xPrev, yPrev] = logPoints[startIndex - 1];
|
|
1121
|
+
m0 = (y1 - yPrev) / (x1 - xPrev);
|
|
1122
|
+
}
|
|
1123
|
+
if (startIndex === logPoints.length - 2) {
|
|
1124
|
+
m1 = (y1 - y0) / h;
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
const [xNext, yNext] = logPoints[startIndex + 2];
|
|
1128
|
+
m1 = (yNext - y0) / (xNext - x0);
|
|
1129
|
+
}
|
|
1130
|
+
// Cubic Hermite Spline
|
|
1131
|
+
const t2 = t * t;
|
|
1132
|
+
const t3 = t2 * t;
|
|
1133
|
+
const h00 = 2 * t3 - 3 * t2 + 1;
|
|
1134
|
+
const h10 = t3 - 2 * t2 + t;
|
|
1135
|
+
const h01 = -2 * t3 + 3 * t2;
|
|
1136
|
+
const h11 = t3 - t2;
|
|
1137
|
+
const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
|
|
1138
|
+
return Math.exp(y);
|
|
1139
|
+
}
|
|
1140
|
+
setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
|
|
1106
1141
|
const { voiceParams, startTime } = note;
|
|
1107
|
-
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation)
|
|
1142
|
+
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
|
|
1143
|
+
(1 + this.getAmplitudeControl(channel));
|
|
1108
1144
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1109
1145
|
const volDelay = startTime + voiceParams.volDelay;
|
|
1110
|
-
const
|
|
1146
|
+
const volAttack = volDelay + voiceParams.volAttack;
|
|
1147
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
1111
1148
|
note.volumeEnvelopeNode.gain
|
|
1112
1149
|
.cancelScheduledValues(scheduleTime)
|
|
1113
|
-
.setValueAtTime(
|
|
1114
|
-
.linearRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1150
|
+
.setValueAtTime(sustainVolume, volHold);
|
|
1115
1151
|
}
|
|
1116
1152
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
1117
1153
|
const { voiceParams, startTime } = note;
|
|
@@ -1130,6 +1166,12 @@ class MidyGM2 {
|
|
|
1130
1166
|
.setValueAtTime(attackVolume, volHold)
|
|
1131
1167
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
1132
1168
|
}
|
|
1169
|
+
setPortamentoPitchEnvelope(note, scheduleTime) {
|
|
1170
|
+
const baseRate = note.voiceParams.playbackRate;
|
|
1171
|
+
note.bufferSource.playbackRate
|
|
1172
|
+
.cancelScheduledValues(scheduleTime)
|
|
1173
|
+
.setValueAtTime(baseRate, scheduleTime);
|
|
1174
|
+
}
|
|
1133
1175
|
setPitchEnvelope(note, scheduleTime) {
|
|
1134
1176
|
const { voiceParams } = note;
|
|
1135
1177
|
const baseRate = voiceParams.playbackRate;
|
|
@@ -1157,19 +1199,20 @@ class MidyGM2 {
|
|
|
1157
1199
|
const maxFrequency = 20000; // max Hz of initialFilterFc
|
|
1158
1200
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
1159
1201
|
}
|
|
1160
|
-
|
|
1202
|
+
setPortamentoFilterEnvelope(channel, note, scheduleTime) {
|
|
1161
1203
|
const state = channel.state;
|
|
1162
1204
|
const { voiceParams, noteNumber, startTime } = note;
|
|
1163
1205
|
const softPedalFactor = 1 -
|
|
1164
1206
|
(0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1207
|
+
const baseCent = voiceParams.initialFilterFc +
|
|
1208
|
+
this.getFilterCutoffControl(channel);
|
|
1209
|
+
const baseFreq = this.centToHz(baseCent) * softPedalFactor;
|
|
1167
1210
|
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
|
|
1168
1211
|
const sustainFreq = baseFreq +
|
|
1169
1212
|
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
1170
1213
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
1171
1214
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
1172
|
-
const portamentoTime = startTime + this.getPortamentoTime(channel);
|
|
1215
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1173
1216
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1174
1217
|
note.filterNode.frequency
|
|
1175
1218
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1254,7 +1297,7 @@ class MidyGM2 {
|
|
|
1254
1297
|
return audioBuffer;
|
|
1255
1298
|
}
|
|
1256
1299
|
}
|
|
1257
|
-
async createNote(channel, voice, noteNumber, velocity, startTime,
|
|
1300
|
+
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
1258
1301
|
const now = this.audioContext.currentTime;
|
|
1259
1302
|
const state = channel.state;
|
|
1260
1303
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
@@ -1270,20 +1313,24 @@ class MidyGM2 {
|
|
|
1270
1313
|
type: "lowpass",
|
|
1271
1314
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
1272
1315
|
});
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1316
|
+
const prevNote = channel.scheduledNotes.at(-1);
|
|
1317
|
+
if (prevNote && prevNote.noteNumber !== noteNumber) {
|
|
1318
|
+
note.portamentoNoteNumber = prevNote.noteNumber;
|
|
1319
|
+
}
|
|
1320
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1321
|
+
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
1322
|
+
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
1323
|
+
this.setPortamentoPitchEnvelope(note, now);
|
|
1277
1324
|
}
|
|
1278
1325
|
else {
|
|
1279
|
-
note.portamento = false;
|
|
1280
1326
|
this.setVolumeEnvelope(channel, note, now);
|
|
1281
1327
|
this.setFilterEnvelope(channel, note, now);
|
|
1328
|
+
this.setPitchEnvelope(note, now);
|
|
1282
1329
|
}
|
|
1330
|
+
this.updateDetune(channel, note, now);
|
|
1283
1331
|
if (0 < state.vibratoDepth) {
|
|
1284
1332
|
this.startVibrato(channel, note, now);
|
|
1285
1333
|
}
|
|
1286
|
-
this.setPitchEnvelope(note, now);
|
|
1287
1334
|
if (0 < state.modulationDepth) {
|
|
1288
1335
|
this.startModulation(channel, note, now);
|
|
1289
1336
|
}
|
|
@@ -1329,9 +1376,8 @@ class MidyGM2 {
|
|
|
1329
1376
|
if (prev) {
|
|
1330
1377
|
const [prevNote, prevChannelNumber] = prev;
|
|
1331
1378
|
if (prevNote && !prevNote.ending) {
|
|
1332
|
-
this.scheduleNoteOff(prevChannelNumber, prevNote
|
|
1333
|
-
startTime, true
|
|
1334
|
-
undefined);
|
|
1379
|
+
this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
|
|
1380
|
+
startTime, true);
|
|
1335
1381
|
}
|
|
1336
1382
|
}
|
|
1337
1383
|
this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
|
|
@@ -1350,9 +1396,8 @@ class MidyGM2 {
|
|
|
1350
1396
|
channelNumber;
|
|
1351
1397
|
const prevNote = this.drumExclusiveClassNotes[index];
|
|
1352
1398
|
if (prevNote && !prevNote.ending) {
|
|
1353
|
-
this.scheduleNoteOff(channelNumber, prevNote
|
|
1354
|
-
startTime, true
|
|
1355
|
-
undefined);
|
|
1399
|
+
this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
|
|
1400
|
+
startTime, true);
|
|
1356
1401
|
}
|
|
1357
1402
|
this.drumExclusiveClassNotes[index] = note;
|
|
1358
1403
|
}
|
|
@@ -1360,10 +1405,10 @@ class MidyGM2 {
|
|
|
1360
1405
|
if (!channel.isDrum)
|
|
1361
1406
|
return false;
|
|
1362
1407
|
const programNumber = channel.programNumber;
|
|
1363
|
-
return (programNumber === 48 && noteNumber === 88) ||
|
|
1364
|
-
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84);
|
|
1408
|
+
return !((programNumber === 48 && noteNumber === 88) ||
|
|
1409
|
+
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
1365
1410
|
}
|
|
1366
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime,
|
|
1411
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
1367
1412
|
const channel = this.channels[channelNumber];
|
|
1368
1413
|
const bankNumber = this.calcBank(channel, channelNumber);
|
|
1369
1414
|
const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
|
|
@@ -1374,7 +1419,8 @@ class MidyGM2 {
|
|
|
1374
1419
|
if (!voice)
|
|
1375
1420
|
return;
|
|
1376
1421
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
1377
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime,
|
|
1422
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
1423
|
+
note.noteOffEvent = noteOffEvent;
|
|
1378
1424
|
note.gainL.connect(channel.gainL);
|
|
1379
1425
|
note.gainR.connect(channel.gainR);
|
|
1380
1426
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -1383,33 +1429,39 @@ class MidyGM2 {
|
|
|
1383
1429
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1384
1430
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1385
1431
|
const scheduledNotes = channel.scheduledNotes;
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
notes.push(note);
|
|
1389
|
-
}
|
|
1390
|
-
else {
|
|
1391
|
-
notes = [note];
|
|
1392
|
-
scheduledNotes.set(noteNumber, notes);
|
|
1393
|
-
}
|
|
1432
|
+
note.index = scheduledNotes.length;
|
|
1433
|
+
scheduledNotes.push(note);
|
|
1394
1434
|
if (this.isDrumNoteOffException(channel, noteNumber)) {
|
|
1395
1435
|
const stopTime = startTime + note.bufferSource.buffer.duration;
|
|
1396
|
-
const index = notes.length - 1;
|
|
1397
1436
|
const promise = new Promise((resolve) => {
|
|
1398
1437
|
note.bufferSource.onended = () => {
|
|
1399
|
-
|
|
1438
|
+
scheduledNotes[note.index] = undefined;
|
|
1439
|
+
this.disconnectNote(note);
|
|
1400
1440
|
resolve();
|
|
1401
1441
|
};
|
|
1402
1442
|
note.bufferSource.stop(stopTime);
|
|
1403
1443
|
});
|
|
1404
1444
|
this.notePromises.push(promise);
|
|
1405
1445
|
}
|
|
1446
|
+
else if (noteOffEvent) {
|
|
1447
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1448
|
+
const portamentoTime = this.getPortamentoTime(channel, note);
|
|
1449
|
+
const portamentoEndTime = startTime + portamentoTime;
|
|
1450
|
+
const notePromise = this.scheduleNoteOff(channelNumber, note, 0, // velocity
|
|
1451
|
+
Math.max(noteOffEvent.startTime, portamentoEndTime), false);
|
|
1452
|
+
this.notePromises.push(notePromise);
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
|
|
1456
|
+
this.notePromises.push(notePromise);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1406
1459
|
}
|
|
1407
1460
|
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1408
1461
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1409
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime,
|
|
1462
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
|
|
1410
1463
|
}
|
|
1411
|
-
disconnectNote(note
|
|
1412
|
-
scheduledNotes[index] = null;
|
|
1464
|
+
disconnectNote(note) {
|
|
1413
1465
|
note.bufferSource.disconnect();
|
|
1414
1466
|
note.filterNode.disconnect();
|
|
1415
1467
|
note.volumeEnvelopeNode.disconnect();
|
|
@@ -1432,8 +1484,7 @@ class MidyGM2 {
|
|
|
1432
1484
|
note.chorusEffectsSend.disconnect();
|
|
1433
1485
|
}
|
|
1434
1486
|
}
|
|
1435
|
-
stopNote(
|
|
1436
|
-
const note = scheduledNotes[index];
|
|
1487
|
+
stopNote(channel, note, endTime, stopTime) {
|
|
1437
1488
|
note.volumeEnvelopeNode.gain
|
|
1438
1489
|
.cancelScheduledValues(endTime)
|
|
1439
1490
|
.linearRampToValueAtTime(0, stopTime);
|
|
@@ -1443,64 +1494,57 @@ class MidyGM2 {
|
|
|
1443
1494
|
}, stopTime);
|
|
1444
1495
|
return new Promise((resolve) => {
|
|
1445
1496
|
note.bufferSource.onended = () => {
|
|
1446
|
-
|
|
1497
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
1498
|
+
this.disconnectNote(note);
|
|
1447
1499
|
resolve();
|
|
1448
1500
|
};
|
|
1449
1501
|
note.bufferSource.stop(stopTime);
|
|
1450
1502
|
});
|
|
1451
1503
|
}
|
|
1452
|
-
scheduleNoteOff(channelNumber,
|
|
1504
|
+
scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
|
|
1453
1505
|
const channel = this.channels[channelNumber];
|
|
1454
|
-
if (this.isDrumNoteOffException(channel, noteNumber))
|
|
1506
|
+
if (this.isDrumNoteOffException(channel, note.noteNumber))
|
|
1455
1507
|
return;
|
|
1456
1508
|
const state = channel.state;
|
|
1457
1509
|
if (!force) {
|
|
1458
1510
|
if (0.5 <= state.sustainPedal)
|
|
1459
1511
|
return;
|
|
1460
|
-
if (channel.
|
|
1512
|
+
if (0.5 <= channel.state.sostenutoPedal)
|
|
1461
1513
|
return;
|
|
1462
1514
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1515
|
+
const volRelease = endTime + note.voiceParams.volRelease;
|
|
1516
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1517
|
+
note.filterNode.frequency
|
|
1518
|
+
.cancelScheduledValues(endTime)
|
|
1519
|
+
.linearRampToValueAtTime(0, modRelease);
|
|
1520
|
+
const stopTime = Math.min(volRelease, modRelease);
|
|
1521
|
+
return this.stopNote(channel, note, endTime, stopTime);
|
|
1522
|
+
}
|
|
1523
|
+
findNoteOffTarget(channel, noteNumber) {
|
|
1524
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
1466
1525
|
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
1467
1526
|
const note = scheduledNotes[i];
|
|
1468
1527
|
if (!note)
|
|
1469
1528
|
continue;
|
|
1470
1529
|
if (note.ending)
|
|
1471
1530
|
continue;
|
|
1472
|
-
if (
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
note.filterNode.frequency
|
|
1476
|
-
.cancelScheduledValues(endTime)
|
|
1477
|
-
.linearRampToValueAtTime(0, modRelease);
|
|
1478
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1479
|
-
return this.stopNote(endTime, stopTime, scheduledNotes, i);
|
|
1480
|
-
}
|
|
1481
|
-
else {
|
|
1482
|
-
const portamentoTime = endTime + this.getPortamentoTime(channel);
|
|
1483
|
-
const deltaNote = portamentoNoteNumber - noteNumber;
|
|
1484
|
-
const baseRate = note.voiceParams.playbackRate;
|
|
1485
|
-
const targetRate = baseRate * Math.pow(2, deltaNote / 12);
|
|
1486
|
-
note.bufferSource.playbackRate
|
|
1487
|
-
.cancelScheduledValues(endTime)
|
|
1488
|
-
.linearRampToValueAtTime(targetRate, portamentoTime);
|
|
1489
|
-
return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
|
|
1490
|
-
}
|
|
1531
|
+
if (note.noteNumber !== noteNumber)
|
|
1532
|
+
continue;
|
|
1533
|
+
return note;
|
|
1491
1534
|
}
|
|
1492
1535
|
}
|
|
1493
1536
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1494
1537
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1495
|
-
|
|
1496
|
-
|
|
1538
|
+
const channel = this.channels[channelNumber];
|
|
1539
|
+
const note = this.findNoteOffTarget(channel, noteNumber);
|
|
1540
|
+
return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
|
|
1497
1541
|
}
|
|
1498
1542
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
1499
1543
|
const velocity = halfVelocity * 2;
|
|
1500
1544
|
const channel = this.channels[channelNumber];
|
|
1501
1545
|
const promises = [];
|
|
1502
1546
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1503
|
-
const promise = this.
|
|
1547
|
+
const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
|
|
1504
1548
|
promises.push(promise);
|
|
1505
1549
|
}
|
|
1506
1550
|
channel.sustainNotes = [];
|
|
@@ -1510,12 +1554,14 @@ class MidyGM2 {
|
|
|
1510
1554
|
const velocity = halfVelocity * 2;
|
|
1511
1555
|
const channel = this.channels[channelNumber];
|
|
1512
1556
|
const promises = [];
|
|
1557
|
+
const sostenutoNotes = channel.sostenutoNotes;
|
|
1513
1558
|
channel.state.sostenutoPedal = 0;
|
|
1514
|
-
|
|
1515
|
-
const
|
|
1559
|
+
for (let i = 0; i < sostenutoNotes.length; i++) {
|
|
1560
|
+
const note = sostenutoNotes[i];
|
|
1561
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
|
|
1516
1562
|
promises.push(promise);
|
|
1517
|
-
}
|
|
1518
|
-
channel.sostenutoNotes
|
|
1563
|
+
}
|
|
1564
|
+
channel.sostenutoNotes = [];
|
|
1519
1565
|
return promises;
|
|
1520
1566
|
}
|
|
1521
1567
|
handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
|
|
@@ -1565,10 +1611,10 @@ class MidyGM2 {
|
|
|
1565
1611
|
channel.detune += pressureDepth * (next - prev);
|
|
1566
1612
|
}
|
|
1567
1613
|
const table = channel.channelPressureTable;
|
|
1568
|
-
this.
|
|
1614
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1569
1615
|
this.setControllerParameters(channel, note, table);
|
|
1570
1616
|
});
|
|
1571
|
-
|
|
1617
|
+
this.applyVoiceParams(channel, 13);
|
|
1572
1618
|
}
|
|
1573
1619
|
handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
|
|
1574
1620
|
const pitchBend = msb * 128 + lsb;
|
|
@@ -1745,6 +1791,7 @@ class MidyGM2 {
|
|
|
1745
1791
|
state.set(channel.state.array);
|
|
1746
1792
|
state[2] = velocity / 127;
|
|
1747
1793
|
state[3] = noteNumber / 127;
|
|
1794
|
+
state[13] = state.channelPressure / 127;
|
|
1748
1795
|
return state;
|
|
1749
1796
|
}
|
|
1750
1797
|
applyVoiceParams(channel, controllerType, scheduleTime) {
|
|
@@ -1771,8 +1818,8 @@ class MidyGM2 {
|
|
|
1771
1818
|
if (key in voiceParams)
|
|
1772
1819
|
noteVoiceParams[key] = voiceParams[key];
|
|
1773
1820
|
}
|
|
1774
|
-
if (note.
|
|
1775
|
-
this.
|
|
1821
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1822
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1776
1823
|
}
|
|
1777
1824
|
else {
|
|
1778
1825
|
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
@@ -1795,32 +1842,32 @@ class MidyGM2 {
|
|
|
1795
1842
|
});
|
|
1796
1843
|
}
|
|
1797
1844
|
createControlChangeHandlers() {
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1845
|
+
const handlers = new Array(128);
|
|
1846
|
+
handlers[0] = this.setBankMSB;
|
|
1847
|
+
handlers[1] = this.setModulationDepth;
|
|
1848
|
+
handlers[5] = this.setPortamentoTime;
|
|
1849
|
+
handlers[6] = this.dataEntryMSB;
|
|
1850
|
+
handlers[7] = this.setVolume;
|
|
1851
|
+
handlers[10] = this.setPan;
|
|
1852
|
+
handlers[11] = this.setExpression;
|
|
1853
|
+
handlers[32] = this.setBankLSB;
|
|
1854
|
+
handlers[38] = this.dataEntryLSB;
|
|
1855
|
+
handlers[64] = this.setSustainPedal;
|
|
1856
|
+
handlers[65] = this.setPortamento;
|
|
1857
|
+
handlers[66] = this.setSostenutoPedal;
|
|
1858
|
+
handlers[67] = this.setSoftPedal;
|
|
1859
|
+
handlers[91] = this.setReverbSendLevel;
|
|
1860
|
+
handlers[93] = this.setChorusSendLevel;
|
|
1861
|
+
handlers[100] = this.setRPNLSB;
|
|
1862
|
+
handlers[101] = this.setRPNMSB;
|
|
1863
|
+
handlers[120] = this.allSoundOff;
|
|
1864
|
+
handlers[121] = this.resetAllControllers;
|
|
1865
|
+
handlers[123] = this.allNotesOff;
|
|
1866
|
+
handlers[124] = this.omniOff;
|
|
1867
|
+
handlers[125] = this.omniOn;
|
|
1868
|
+
handlers[126] = this.monoOn;
|
|
1869
|
+
handlers[127] = this.polyOn;
|
|
1870
|
+
return handlers;
|
|
1824
1871
|
}
|
|
1825
1872
|
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1826
1873
|
const handler = this.controlChangeHandlers[controllerType];
|
|
@@ -1857,9 +1904,33 @@ class MidyGM2 {
|
|
|
1857
1904
|
channel.state.modulationDepth = modulation / 127;
|
|
1858
1905
|
this.updateModulation(channel, scheduleTime);
|
|
1859
1906
|
}
|
|
1860
|
-
|
|
1907
|
+
updatePortamento(channel, scheduleTime) {
|
|
1908
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1909
|
+
if (0.5 <= channel.state.portamento) {
|
|
1910
|
+
if (0 <= note.portamentoNoteNumber) {
|
|
1911
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
1912
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1913
|
+
this.setPortamentoPitchEnvelope(note, scheduleTime);
|
|
1914
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
else {
|
|
1918
|
+
if (0 <= note.portamentoNoteNumber) {
|
|
1919
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1920
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
1921
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1922
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
|
|
1861
1928
|
const channel = this.channels[channelNumber];
|
|
1929
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1862
1930
|
channel.state.portamentoTime = portamentoTime / 127;
|
|
1931
|
+
if (channel.isDrum)
|
|
1932
|
+
return;
|
|
1933
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1863
1934
|
}
|
|
1864
1935
|
setKeyBasedVolume(channel, scheduleTime) {
|
|
1865
1936
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1945,11 +2016,13 @@ class MidyGM2 {
|
|
|
1945
2016
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
1946
2017
|
}
|
|
1947
2018
|
}
|
|
1948
|
-
setPortamento(channelNumber, value) {
|
|
2019
|
+
setPortamento(channelNumber, value, scheduleTime) {
|
|
1949
2020
|
const channel = this.channels[channelNumber];
|
|
1950
2021
|
if (channel.isDrum)
|
|
1951
2022
|
return;
|
|
2023
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1952
2024
|
channel.state.portamento = value / 127;
|
|
2025
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1953
2026
|
}
|
|
1954
2027
|
setSostenutoPedal(channelNumber, value, scheduleTime) {
|
|
1955
2028
|
const channel = this.channels[channelNumber];
|
|
@@ -1958,7 +2031,11 @@ class MidyGM2 {
|
|
|
1958
2031
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1959
2032
|
channel.state.sostenutoPedal = value / 127;
|
|
1960
2033
|
if (64 <= value) {
|
|
1961
|
-
|
|
2034
|
+
const sostenutoNotes = [];
|
|
2035
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2036
|
+
sostenutoNotes.push(note);
|
|
2037
|
+
});
|
|
2038
|
+
channel.sostenutoNotes = sostenutoNotes;
|
|
1962
2039
|
}
|
|
1963
2040
|
else {
|
|
1964
2041
|
this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
|
|
@@ -1968,12 +2045,13 @@ class MidyGM2 {
|
|
|
1968
2045
|
const channel = this.channels[channelNumber];
|
|
1969
2046
|
if (channel.isDrum)
|
|
1970
2047
|
return;
|
|
2048
|
+
const state = channel.state;
|
|
1971
2049
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1972
|
-
|
|
2050
|
+
state.softPedal = softPedal / 127;
|
|
1973
2051
|
this.processScheduledNotes(channel, (note) => {
|
|
1974
|
-
if (note.
|
|
1975
|
-
this.
|
|
1976
|
-
this.
|
|
2052
|
+
if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2053
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2054
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1977
2055
|
}
|
|
1978
2056
|
else {
|
|
1979
2057
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
@@ -2170,24 +2248,32 @@ class MidyGM2 {
|
|
|
2170
2248
|
}
|
|
2171
2249
|
allSoundOff(channelNumber, _value, scheduleTime) {
|
|
2172
2250
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2173
|
-
return this.
|
|
2251
|
+
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
2174
2252
|
}
|
|
2175
2253
|
resetAllStates(channelNumber) {
|
|
2254
|
+
const scheduleTime = this.audioContext.currentTime;
|
|
2176
2255
|
const channel = this.channels[channelNumber];
|
|
2177
2256
|
const state = channel.state;
|
|
2178
|
-
|
|
2179
|
-
|
|
2257
|
+
const entries = Object.entries(defaultControllerState);
|
|
2258
|
+
for (const [key, { type, defaultValue }] of entries) {
|
|
2259
|
+
if (128 <= type) {
|
|
2260
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2261
|
+
}
|
|
2262
|
+
else {
|
|
2263
|
+
state[key] = defaultValue;
|
|
2264
|
+
}
|
|
2180
2265
|
}
|
|
2181
|
-
for (const
|
|
2182
|
-
channel[
|
|
2266
|
+
for (const key of Object.keys(this.constructor.channelSettings)) {
|
|
2267
|
+
channel[key] = this.constructor.channelSettings[key];
|
|
2183
2268
|
}
|
|
2269
|
+
this.resetChannelTable(channel);
|
|
2184
2270
|
this.mode = "GM2";
|
|
2185
2271
|
this.masterFineTuning = 0; // cb
|
|
2186
2272
|
this.masterCoarseTuning = 0; // cb
|
|
2187
2273
|
}
|
|
2188
2274
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
|
|
2189
|
-
resetAllControllers(channelNumber) {
|
|
2190
|
-
const
|
|
2275
|
+
resetAllControllers(channelNumber, _value, scheduleTime) {
|
|
2276
|
+
const keys = [
|
|
2191
2277
|
"channelPressure",
|
|
2192
2278
|
"pitchWheel",
|
|
2193
2279
|
"expression",
|
|
@@ -2199,10 +2285,17 @@ class MidyGM2 {
|
|
|
2199
2285
|
];
|
|
2200
2286
|
const channel = this.channels[channelNumber];
|
|
2201
2287
|
const state = channel.state;
|
|
2202
|
-
for (let i = 0; i <
|
|
2203
|
-
const
|
|
2204
|
-
|
|
2288
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2289
|
+
const key = keys[i];
|
|
2290
|
+
const { type, defaultValue } = defaultControllerState[key];
|
|
2291
|
+
if (128 <= type) {
|
|
2292
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2293
|
+
}
|
|
2294
|
+
else {
|
|
2295
|
+
state[key] = defaultValue;
|
|
2296
|
+
}
|
|
2205
2297
|
}
|
|
2298
|
+
this.setPitchBend(channelNumber, 8192, scheduleTime);
|
|
2206
2299
|
const settingTypes = [
|
|
2207
2300
|
"rpnMSB",
|
|
2208
2301
|
"rpnLSB",
|
|
@@ -2214,7 +2307,7 @@ class MidyGM2 {
|
|
|
2214
2307
|
}
|
|
2215
2308
|
allNotesOff(channelNumber, _value, scheduleTime) {
|
|
2216
2309
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2217
|
-
return this.
|
|
2310
|
+
return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
|
|
2218
2311
|
}
|
|
2219
2312
|
omniOff(channelNumber, value, scheduleTime) {
|
|
2220
2313
|
this.allNotesOff(channelNumber, value, scheduleTime);
|
|
@@ -2431,7 +2524,7 @@ class MidyGM2 {
|
|
|
2431
2524
|
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2432
2525
|
}
|
|
2433
2526
|
getReverbTime(value) {
|
|
2434
|
-
return Math.
|
|
2527
|
+
return Math.exp((value - 40) * 0.025);
|
|
2435
2528
|
}
|
|
2436
2529
|
// mean free path equation
|
|
2437
2530
|
// https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
|
|
@@ -2588,6 +2681,8 @@ class MidyGM2 {
|
|
|
2588
2681
|
if (!channelBitmap[i])
|
|
2589
2682
|
continue;
|
|
2590
2683
|
const channel = this.channels[i];
|
|
2684
|
+
if (channel.isDrum)
|
|
2685
|
+
continue;
|
|
2591
2686
|
for (let j = 0; j < 12; j++) {
|
|
2592
2687
|
const centValue = data[j + 7] - 64;
|
|
2593
2688
|
channel.scaleOctaveTuningTable[j] = centValue;
|
|
@@ -2624,7 +2719,13 @@ class MidyGM2 {
|
|
|
2624
2719
|
setControllerParameters(channel, note, table) {
|
|
2625
2720
|
if (table[0] !== 64)
|
|
2626
2721
|
this.updateDetune(channel, note);
|
|
2627
|
-
if (
|
|
2722
|
+
if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
|
|
2723
|
+
if (table[1] !== 64)
|
|
2724
|
+
this.setPortamentoFilterEnvelope(channel, note);
|
|
2725
|
+
if (table[2] !== 64)
|
|
2726
|
+
this.setPortamentoVolumeEnvelope(channel, note);
|
|
2727
|
+
}
|
|
2728
|
+
else {
|
|
2628
2729
|
if (table[1] !== 64)
|
|
2629
2730
|
this.setFilterEnvelope(channel, note);
|
|
2630
2731
|
if (table[2] !== 64)
|
|
@@ -2639,7 +2740,10 @@ class MidyGM2 {
|
|
|
2639
2740
|
}
|
|
2640
2741
|
handlePressureSysEx(data, tableName) {
|
|
2641
2742
|
const channelNumber = data[4];
|
|
2642
|
-
const
|
|
2743
|
+
const channel = this.channels[channelNumber];
|
|
2744
|
+
if (channel.isDrum)
|
|
2745
|
+
return;
|
|
2746
|
+
const table = channel[tableName];
|
|
2643
2747
|
for (let i = 5; i < data.length - 1; i += 2) {
|
|
2644
2748
|
const pp = data[i];
|
|
2645
2749
|
const rr = data[i + 1];
|
|
@@ -2649,8 +2753,13 @@ class MidyGM2 {
|
|
|
2649
2753
|
initControlTable() {
|
|
2650
2754
|
const channelCount = 128;
|
|
2651
2755
|
const slotSize = 6;
|
|
2652
|
-
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2653
2756
|
const table = new Uint8Array(channelCount * slotSize);
|
|
2757
|
+
return this.resetControlTable(table);
|
|
2758
|
+
}
|
|
2759
|
+
resetControlTable(table) {
|
|
2760
|
+
const channelCount = 128;
|
|
2761
|
+
const slotSize = 6;
|
|
2762
|
+
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2654
2763
|
for (let ch = 0; ch < channelCount; ch++) {
|
|
2655
2764
|
const offset = ch * slotSize;
|
|
2656
2765
|
table.set(defaultValues, offset);
|
|
@@ -2667,8 +2776,11 @@ class MidyGM2 {
|
|
|
2667
2776
|
}
|
|
2668
2777
|
handleControlChangeSysEx(data) {
|
|
2669
2778
|
const channelNumber = data[4];
|
|
2779
|
+
const channel = this.channels[channelNumber];
|
|
2780
|
+
if (channel.isDrum)
|
|
2781
|
+
return;
|
|
2670
2782
|
const controllerType = data[5];
|
|
2671
|
-
const table =
|
|
2783
|
+
const table = channel.controlTable[controllerType];
|
|
2672
2784
|
for (let i = 6; i < data.length - 1; i += 2) {
|
|
2673
2785
|
const pp = data[i];
|
|
2674
2786
|
const rr = data[i + 1];
|
|
@@ -2682,8 +2794,11 @@ class MidyGM2 {
|
|
|
2682
2794
|
}
|
|
2683
2795
|
handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
|
|
2684
2796
|
const channelNumber = data[4];
|
|
2797
|
+
const channel = this.channels[channelNumber];
|
|
2798
|
+
if (channel.isDrum)
|
|
2799
|
+
return;
|
|
2685
2800
|
const keyNumber = data[5];
|
|
2686
|
-
const table =
|
|
2801
|
+
const table = channel.keyBasedInstrumentControlTable;
|
|
2687
2802
|
for (let i = 6; i < data.length - 1; i += 2) {
|
|
2688
2803
|
const controllerType = data[i];
|
|
2689
2804
|
const value = data[i + 1];
|