@marmooo/midy 0.3.1 → 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/esm/midy-GM1.d.ts +11 -50
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +150 -169
- package/esm/midy-GM2.d.ts +25 -83
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +376 -285
- package/esm/midy-GMLite.d.ts +10 -49
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +155 -171
- package/esm/midy.d.ts +28 -106
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +405 -313
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +11 -50
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +150 -169
- package/script/midy-GM2.d.ts +25 -83
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +376 -285
- package/script/midy-GMLite.d.ts +10 -49
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +155 -171
- package/script/midy.d.ts +28 -106
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +405 -313
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,13 +770,37 @@ 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
|
}
|
|
837
800
|
stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
|
|
838
801
|
const channel = this.channels[channelNumber];
|
|
839
802
|
const promises = [];
|
|
840
|
-
|
|
841
|
-
activeNotes.forEach((note) => {
|
|
803
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
842
804
|
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
|
|
843
805
|
this.notePromises.push(promise);
|
|
844
806
|
promises.push(promise);
|
|
@@ -849,11 +811,11 @@ class MidyGM2 {
|
|
|
849
811
|
const channel = this.channels[channelNumber];
|
|
850
812
|
const promises = [];
|
|
851
813
|
this.processScheduledNotes(channel, (note) => {
|
|
852
|
-
const promise = this.scheduleNoteOff(channelNumber, note
|
|
814
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
|
|
853
815
|
this.notePromises.push(promise);
|
|
854
816
|
promises.push(promise);
|
|
855
817
|
});
|
|
856
|
-
channel.scheduledNotes
|
|
818
|
+
channel.scheduledNotes = [];
|
|
857
819
|
return Promise.all(promises);
|
|
858
820
|
}
|
|
859
821
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -874,9 +836,6 @@ class MidyGM2 {
|
|
|
874
836
|
if (!this.isPlaying)
|
|
875
837
|
return;
|
|
876
838
|
this.isStopping = true;
|
|
877
|
-
for (let i = 0; i < this.channels.length; i++) {
|
|
878
|
-
this.resetAllStates(i);
|
|
879
|
-
}
|
|
880
839
|
}
|
|
881
840
|
pause() {
|
|
882
841
|
if (!this.isPlaying || this.isPaused)
|
|
@@ -911,37 +870,31 @@ class MidyGM2 {
|
|
|
911
870
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
912
871
|
}
|
|
913
872
|
processScheduledNotes(channel, callback) {
|
|
914
|
-
channel.scheduledNotes
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
getActiveNotes(channel, scheduleTime) {
|
|
926
|
-
const activeNotes = new SparseMap(128);
|
|
927
|
-
channel.scheduledNotes.forEach((noteList) => {
|
|
928
|
-
const activeNote = this.getActiveNote(noteList, scheduleTime);
|
|
929
|
-
if (activeNote) {
|
|
930
|
-
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
931
|
-
}
|
|
932
|
-
});
|
|
933
|
-
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
|
+
}
|
|
934
882
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
883
|
+
processActiveNotes(channel, scheduleTime, callback) {
|
|
884
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
885
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
886
|
+
const note = scheduledNotes[i];
|
|
938
887
|
if (!note)
|
|
939
|
-
|
|
888
|
+
continue;
|
|
889
|
+
if (note.ending)
|
|
890
|
+
continue;
|
|
891
|
+
const noteOffEvent = note.noteOffEvent;
|
|
892
|
+
if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
|
|
893
|
+
continue;
|
|
940
894
|
if (scheduleTime < note.startTime)
|
|
941
895
|
continue;
|
|
942
|
-
|
|
896
|
+
callback(note);
|
|
943
897
|
}
|
|
944
|
-
return noteList[0];
|
|
945
898
|
}
|
|
946
899
|
createConvolutionReverbImpulse(audioContext, decay, preDecay) {
|
|
947
900
|
const sampleRate = audioContext.sampleRate;
|
|
@@ -1106,25 +1059,95 @@ class MidyGM2 {
|
|
|
1106
1059
|
}
|
|
1107
1060
|
updateDetune(channel, note, scheduleTime) {
|
|
1108
1061
|
const noteDetune = this.calcNoteDetune(channel, note);
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
.
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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) {
|
|
1119
1141
|
const { voiceParams, startTime } = note;
|
|
1120
|
-
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation)
|
|
1142
|
+
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
|
|
1143
|
+
(1 + this.getAmplitudeControl(channel));
|
|
1121
1144
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1122
1145
|
const volDelay = startTime + voiceParams.volDelay;
|
|
1123
|
-
const
|
|
1146
|
+
const volAttack = volDelay + voiceParams.volAttack;
|
|
1147
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
1124
1148
|
note.volumeEnvelopeNode.gain
|
|
1125
1149
|
.cancelScheduledValues(scheduleTime)
|
|
1126
|
-
.setValueAtTime(
|
|
1127
|
-
.linearRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1150
|
+
.setValueAtTime(sustainVolume, volHold);
|
|
1128
1151
|
}
|
|
1129
1152
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
1130
1153
|
const { voiceParams, startTime } = note;
|
|
@@ -1143,6 +1166,12 @@ class MidyGM2 {
|
|
|
1143
1166
|
.setValueAtTime(attackVolume, volHold)
|
|
1144
1167
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
1145
1168
|
}
|
|
1169
|
+
setPortamentoPitchEnvelope(note, scheduleTime) {
|
|
1170
|
+
const baseRate = note.voiceParams.playbackRate;
|
|
1171
|
+
note.bufferSource.playbackRate
|
|
1172
|
+
.cancelScheduledValues(scheduleTime)
|
|
1173
|
+
.setValueAtTime(baseRate, scheduleTime);
|
|
1174
|
+
}
|
|
1146
1175
|
setPitchEnvelope(note, scheduleTime) {
|
|
1147
1176
|
const { voiceParams } = note;
|
|
1148
1177
|
const baseRate = voiceParams.playbackRate;
|
|
@@ -1170,19 +1199,20 @@ class MidyGM2 {
|
|
|
1170
1199
|
const maxFrequency = 20000; // max Hz of initialFilterFc
|
|
1171
1200
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
1172
1201
|
}
|
|
1173
|
-
|
|
1202
|
+
setPortamentoFilterEnvelope(channel, note, scheduleTime) {
|
|
1174
1203
|
const state = channel.state;
|
|
1175
1204
|
const { voiceParams, noteNumber, startTime } = note;
|
|
1176
1205
|
const softPedalFactor = 1 -
|
|
1177
1206
|
(0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1207
|
+
const baseCent = voiceParams.initialFilterFc +
|
|
1208
|
+
this.getFilterCutoffControl(channel);
|
|
1209
|
+
const baseFreq = this.centToHz(baseCent) * softPedalFactor;
|
|
1180
1210
|
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
|
|
1181
1211
|
const sustainFreq = baseFreq +
|
|
1182
1212
|
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
1183
1213
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
1184
1214
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
1185
|
-
const portamentoTime = startTime + this.getPortamentoTime(channel);
|
|
1215
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1186
1216
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1187
1217
|
note.filterNode.frequency
|
|
1188
1218
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1267,7 +1297,7 @@ class MidyGM2 {
|
|
|
1267
1297
|
return audioBuffer;
|
|
1268
1298
|
}
|
|
1269
1299
|
}
|
|
1270
|
-
async createNote(channel, voice, noteNumber, velocity, startTime,
|
|
1300
|
+
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
1271
1301
|
const now = this.audioContext.currentTime;
|
|
1272
1302
|
const state = channel.state;
|
|
1273
1303
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
@@ -1283,20 +1313,24 @@ class MidyGM2 {
|
|
|
1283
1313
|
type: "lowpass",
|
|
1284
1314
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
1285
1315
|
});
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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);
|
|
1290
1324
|
}
|
|
1291
1325
|
else {
|
|
1292
|
-
note.portamento = false;
|
|
1293
1326
|
this.setVolumeEnvelope(channel, note, now);
|
|
1294
1327
|
this.setFilterEnvelope(channel, note, now);
|
|
1328
|
+
this.setPitchEnvelope(note, now);
|
|
1295
1329
|
}
|
|
1330
|
+
this.updateDetune(channel, note, now);
|
|
1296
1331
|
if (0 < state.vibratoDepth) {
|
|
1297
1332
|
this.startVibrato(channel, note, now);
|
|
1298
1333
|
}
|
|
1299
|
-
this.setPitchEnvelope(note, now);
|
|
1300
1334
|
if (0 < state.modulationDepth) {
|
|
1301
1335
|
this.startModulation(channel, note, now);
|
|
1302
1336
|
}
|
|
@@ -1342,9 +1376,8 @@ class MidyGM2 {
|
|
|
1342
1376
|
if (prev) {
|
|
1343
1377
|
const [prevNote, prevChannelNumber] = prev;
|
|
1344
1378
|
if (prevNote && !prevNote.ending) {
|
|
1345
|
-
this.scheduleNoteOff(prevChannelNumber, prevNote
|
|
1346
|
-
startTime, true
|
|
1347
|
-
undefined);
|
|
1379
|
+
this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
|
|
1380
|
+
startTime, true);
|
|
1348
1381
|
}
|
|
1349
1382
|
}
|
|
1350
1383
|
this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
|
|
@@ -1363,9 +1396,8 @@ class MidyGM2 {
|
|
|
1363
1396
|
channelNumber;
|
|
1364
1397
|
const prevNote = this.drumExclusiveClassNotes[index];
|
|
1365
1398
|
if (prevNote && !prevNote.ending) {
|
|
1366
|
-
this.scheduleNoteOff(channelNumber, prevNote
|
|
1367
|
-
startTime, true
|
|
1368
|
-
undefined);
|
|
1399
|
+
this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
|
|
1400
|
+
startTime, true);
|
|
1369
1401
|
}
|
|
1370
1402
|
this.drumExclusiveClassNotes[index] = note;
|
|
1371
1403
|
}
|
|
@@ -1376,7 +1408,7 @@ class MidyGM2 {
|
|
|
1376
1408
|
return !((programNumber === 48 && noteNumber === 88) ||
|
|
1377
1409
|
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
1378
1410
|
}
|
|
1379
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime,
|
|
1411
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
1380
1412
|
const channel = this.channels[channelNumber];
|
|
1381
1413
|
const bankNumber = this.calcBank(channel, channelNumber);
|
|
1382
1414
|
const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
|
|
@@ -1387,7 +1419,8 @@ class MidyGM2 {
|
|
|
1387
1419
|
if (!voice)
|
|
1388
1420
|
return;
|
|
1389
1421
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
1390
|
-
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;
|
|
1391
1424
|
note.gainL.connect(channel.gainL);
|
|
1392
1425
|
note.gainR.connect(channel.gainR);
|
|
1393
1426
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -1396,20 +1429,13 @@ class MidyGM2 {
|
|
|
1396
1429
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1397
1430
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1398
1431
|
const scheduledNotes = channel.scheduledNotes;
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
noteList.push(note);
|
|
1402
|
-
}
|
|
1403
|
-
else {
|
|
1404
|
-
noteList = [note];
|
|
1405
|
-
scheduledNotes.set(noteNumber, noteList);
|
|
1406
|
-
}
|
|
1432
|
+
note.index = scheduledNotes.length;
|
|
1433
|
+
scheduledNotes.push(note);
|
|
1407
1434
|
if (this.isDrumNoteOffException(channel, noteNumber)) {
|
|
1408
1435
|
const stopTime = startTime + note.bufferSource.buffer.duration;
|
|
1409
|
-
const index = noteList.length - 1;
|
|
1410
1436
|
const promise = new Promise((resolve) => {
|
|
1411
1437
|
note.bufferSource.onended = () => {
|
|
1412
|
-
|
|
1438
|
+
scheduledNotes[note.index] = undefined;
|
|
1413
1439
|
this.disconnectNote(note);
|
|
1414
1440
|
resolve();
|
|
1415
1441
|
};
|
|
@@ -1417,10 +1443,23 @@ class MidyGM2 {
|
|
|
1417
1443
|
});
|
|
1418
1444
|
this.notePromises.push(promise);
|
|
1419
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
|
+
}
|
|
1420
1459
|
}
|
|
1421
1460
|
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1422
1461
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1423
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime,
|
|
1462
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
|
|
1424
1463
|
}
|
|
1425
1464
|
disconnectNote(note) {
|
|
1426
1465
|
note.bufferSource.disconnect();
|
|
@@ -1445,8 +1484,7 @@ class MidyGM2 {
|
|
|
1445
1484
|
note.chorusEffectsSend.disconnect();
|
|
1446
1485
|
}
|
|
1447
1486
|
}
|
|
1448
|
-
stopNote(
|
|
1449
|
-
const note = noteList[index];
|
|
1487
|
+
stopNote(channel, note, endTime, stopTime) {
|
|
1450
1488
|
note.volumeEnvelopeNode.gain
|
|
1451
1489
|
.cancelScheduledValues(endTime)
|
|
1452
1490
|
.linearRampToValueAtTime(0, stopTime);
|
|
@@ -1456,73 +1494,57 @@ class MidyGM2 {
|
|
|
1456
1494
|
}, stopTime);
|
|
1457
1495
|
return new Promise((resolve) => {
|
|
1458
1496
|
note.bufferSource.onended = () => {
|
|
1459
|
-
|
|
1497
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
1460
1498
|
this.disconnectNote(note);
|
|
1461
1499
|
resolve();
|
|
1462
1500
|
};
|
|
1463
1501
|
note.bufferSource.stop(stopTime);
|
|
1464
1502
|
});
|
|
1465
1503
|
}
|
|
1466
|
-
|
|
1467
|
-
for (let i = 0; i < noteList.length; i++) {
|
|
1468
|
-
const note = noteList[i];
|
|
1469
|
-
if (!note)
|
|
1470
|
-
continue;
|
|
1471
|
-
if (note.ending)
|
|
1472
|
-
continue;
|
|
1473
|
-
return [note, i];
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
|
|
1504
|
+
scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
|
|
1477
1505
|
const channel = this.channels[channelNumber];
|
|
1478
|
-
if (this.isDrumNoteOffException(channel, noteNumber))
|
|
1506
|
+
if (this.isDrumNoteOffException(channel, note.noteNumber))
|
|
1479
1507
|
return;
|
|
1480
1508
|
const state = channel.state;
|
|
1481
1509
|
if (!force) {
|
|
1482
1510
|
if (0.5 <= state.sustainPedal)
|
|
1483
1511
|
return;
|
|
1484
|
-
if (channel.
|
|
1512
|
+
if (0.5 <= channel.state.sostenutoPedal)
|
|
1485
1513
|
return;
|
|
1486
1514
|
}
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
const
|
|
1499
|
-
note
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
note.voiceParams.volRelease * state.releaseTime * 2;
|
|
1507
|
-
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1508
|
-
note.filterNode.frequency
|
|
1509
|
-
.cancelScheduledValues(endTime)
|
|
1510
|
-
.linearRampToValueAtTime(0, modRelease);
|
|
1511
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1512
|
-
return this.stopNote(endTime, stopTime, noteList, i);
|
|
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;
|
|
1525
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
1526
|
+
const note = scheduledNotes[i];
|
|
1527
|
+
if (!note)
|
|
1528
|
+
continue;
|
|
1529
|
+
if (note.ending)
|
|
1530
|
+
continue;
|
|
1531
|
+
if (note.noteNumber !== noteNumber)
|
|
1532
|
+
continue;
|
|
1533
|
+
return note;
|
|
1513
1534
|
}
|
|
1514
1535
|
}
|
|
1515
1536
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1516
1537
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1517
|
-
|
|
1518
|
-
|
|
1538
|
+
const channel = this.channels[channelNumber];
|
|
1539
|
+
const note = this.findNoteOffTarget(channel, noteNumber);
|
|
1540
|
+
return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
|
|
1519
1541
|
}
|
|
1520
1542
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
1521
1543
|
const velocity = halfVelocity * 2;
|
|
1522
1544
|
const channel = this.channels[channelNumber];
|
|
1523
1545
|
const promises = [];
|
|
1524
1546
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1525
|
-
const promise = this.
|
|
1547
|
+
const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
|
|
1526
1548
|
promises.push(promise);
|
|
1527
1549
|
}
|
|
1528
1550
|
channel.sustainNotes = [];
|
|
@@ -1532,12 +1554,14 @@ class MidyGM2 {
|
|
|
1532
1554
|
const velocity = halfVelocity * 2;
|
|
1533
1555
|
const channel = this.channels[channelNumber];
|
|
1534
1556
|
const promises = [];
|
|
1557
|
+
const sostenutoNotes = channel.sostenutoNotes;
|
|
1535
1558
|
channel.state.sostenutoPedal = 0;
|
|
1536
|
-
|
|
1537
|
-
const
|
|
1559
|
+
for (let i = 0; i < sostenutoNotes.length; i++) {
|
|
1560
|
+
const note = sostenutoNotes[i];
|
|
1561
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
|
|
1538
1562
|
promises.push(promise);
|
|
1539
|
-
}
|
|
1540
|
-
channel.sostenutoNotes
|
|
1563
|
+
}
|
|
1564
|
+
channel.sostenutoNotes = [];
|
|
1541
1565
|
return promises;
|
|
1542
1566
|
}
|
|
1543
1567
|
handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
|
|
@@ -1587,7 +1611,7 @@ class MidyGM2 {
|
|
|
1587
1611
|
channel.detune += pressureDepth * (next - prev);
|
|
1588
1612
|
}
|
|
1589
1613
|
const table = channel.channelPressureTable;
|
|
1590
|
-
this.
|
|
1614
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1591
1615
|
this.setControllerParameters(channel, note, table);
|
|
1592
1616
|
});
|
|
1593
1617
|
this.applyVoiceParams(channel, 13);
|
|
@@ -1794,8 +1818,8 @@ class MidyGM2 {
|
|
|
1794
1818
|
if (key in voiceParams)
|
|
1795
1819
|
noteVoiceParams[key] = voiceParams[key];
|
|
1796
1820
|
}
|
|
1797
|
-
if (0.5 <= channel.state.portamento && note.
|
|
1798
|
-
this.
|
|
1821
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1822
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1799
1823
|
}
|
|
1800
1824
|
else {
|
|
1801
1825
|
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
@@ -1818,32 +1842,32 @@ class MidyGM2 {
|
|
|
1818
1842
|
});
|
|
1819
1843
|
}
|
|
1820
1844
|
createControlChangeHandlers() {
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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;
|
|
1847
1871
|
}
|
|
1848
1872
|
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1849
1873
|
const handler = this.controlChangeHandlers[controllerType];
|
|
@@ -1880,9 +1904,33 @@ class MidyGM2 {
|
|
|
1880
1904
|
channel.state.modulationDepth = modulation / 127;
|
|
1881
1905
|
this.updateModulation(channel, scheduleTime);
|
|
1882
1906
|
}
|
|
1883
|
-
|
|
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) {
|
|
1884
1928
|
const channel = this.channels[channelNumber];
|
|
1929
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1885
1930
|
channel.state.portamentoTime = portamentoTime / 127;
|
|
1931
|
+
if (channel.isDrum)
|
|
1932
|
+
return;
|
|
1933
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1886
1934
|
}
|
|
1887
1935
|
setKeyBasedVolume(channel, scheduleTime) {
|
|
1888
1936
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1968,11 +2016,13 @@ class MidyGM2 {
|
|
|
1968
2016
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
1969
2017
|
}
|
|
1970
2018
|
}
|
|
1971
|
-
setPortamento(channelNumber, value) {
|
|
2019
|
+
setPortamento(channelNumber, value, scheduleTime) {
|
|
1972
2020
|
const channel = this.channels[channelNumber];
|
|
1973
2021
|
if (channel.isDrum)
|
|
1974
2022
|
return;
|
|
2023
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1975
2024
|
channel.state.portamento = value / 127;
|
|
2025
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1976
2026
|
}
|
|
1977
2027
|
setSostenutoPedal(channelNumber, value, scheduleTime) {
|
|
1978
2028
|
const channel = this.channels[channelNumber];
|
|
@@ -1981,7 +2031,11 @@ class MidyGM2 {
|
|
|
1981
2031
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1982
2032
|
channel.state.sostenutoPedal = value / 127;
|
|
1983
2033
|
if (64 <= value) {
|
|
1984
|
-
|
|
2034
|
+
const sostenutoNotes = [];
|
|
2035
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2036
|
+
sostenutoNotes.push(note);
|
|
2037
|
+
});
|
|
2038
|
+
channel.sostenutoNotes = sostenutoNotes;
|
|
1985
2039
|
}
|
|
1986
2040
|
else {
|
|
1987
2041
|
this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
|
|
@@ -1995,9 +2049,9 @@ class MidyGM2 {
|
|
|
1995
2049
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1996
2050
|
state.softPedal = softPedal / 127;
|
|
1997
2051
|
this.processScheduledNotes(channel, (note) => {
|
|
1998
|
-
if (0.5 <= state.portamento && note.
|
|
1999
|
-
this.
|
|
2000
|
-
this.
|
|
2052
|
+
if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2053
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2054
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2001
2055
|
}
|
|
2002
2056
|
else {
|
|
2003
2057
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
@@ -2197,21 +2251,29 @@ class MidyGM2 {
|
|
|
2197
2251
|
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
2198
2252
|
}
|
|
2199
2253
|
resetAllStates(channelNumber) {
|
|
2254
|
+
const scheduleTime = this.audioContext.currentTime;
|
|
2200
2255
|
const channel = this.channels[channelNumber];
|
|
2201
2256
|
const state = channel.state;
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
+
}
|
|
2204
2265
|
}
|
|
2205
|
-
for (const
|
|
2206
|
-
channel[
|
|
2266
|
+
for (const key of Object.keys(this.constructor.channelSettings)) {
|
|
2267
|
+
channel[key] = this.constructor.channelSettings[key];
|
|
2207
2268
|
}
|
|
2269
|
+
this.resetChannelTable(channel);
|
|
2208
2270
|
this.mode = "GM2";
|
|
2209
2271
|
this.masterFineTuning = 0; // cb
|
|
2210
2272
|
this.masterCoarseTuning = 0; // cb
|
|
2211
2273
|
}
|
|
2212
2274
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
|
|
2213
|
-
resetAllControllers(channelNumber) {
|
|
2214
|
-
const
|
|
2275
|
+
resetAllControllers(channelNumber, _value, scheduleTime) {
|
|
2276
|
+
const keys = [
|
|
2215
2277
|
"channelPressure",
|
|
2216
2278
|
"pitchWheel",
|
|
2217
2279
|
"expression",
|
|
@@ -2223,10 +2285,17 @@ class MidyGM2 {
|
|
|
2223
2285
|
];
|
|
2224
2286
|
const channel = this.channels[channelNumber];
|
|
2225
2287
|
const state = channel.state;
|
|
2226
|
-
for (let i = 0; i <
|
|
2227
|
-
const
|
|
2228
|
-
|
|
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
|
+
}
|
|
2229
2297
|
}
|
|
2298
|
+
this.setPitchBend(channelNumber, 8192, scheduleTime);
|
|
2230
2299
|
const settingTypes = [
|
|
2231
2300
|
"rpnMSB",
|
|
2232
2301
|
"rpnLSB",
|
|
@@ -2455,7 +2524,7 @@ class MidyGM2 {
|
|
|
2455
2524
|
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2456
2525
|
}
|
|
2457
2526
|
getReverbTime(value) {
|
|
2458
|
-
return Math.
|
|
2527
|
+
return Math.exp((value - 40) * 0.025);
|
|
2459
2528
|
}
|
|
2460
2529
|
// mean free path equation
|
|
2461
2530
|
// https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
|
|
@@ -2612,6 +2681,8 @@ class MidyGM2 {
|
|
|
2612
2681
|
if (!channelBitmap[i])
|
|
2613
2682
|
continue;
|
|
2614
2683
|
const channel = this.channels[i];
|
|
2684
|
+
if (channel.isDrum)
|
|
2685
|
+
continue;
|
|
2615
2686
|
for (let j = 0; j < 12; j++) {
|
|
2616
2687
|
const centValue = data[j + 7] - 64;
|
|
2617
2688
|
channel.scaleOctaveTuningTable[j] = centValue;
|
|
@@ -2648,7 +2719,13 @@ class MidyGM2 {
|
|
|
2648
2719
|
setControllerParameters(channel, note, table) {
|
|
2649
2720
|
if (table[0] !== 64)
|
|
2650
2721
|
this.updateDetune(channel, note);
|
|
2651
|
-
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 {
|
|
2652
2729
|
if (table[1] !== 64)
|
|
2653
2730
|
this.setFilterEnvelope(channel, note);
|
|
2654
2731
|
if (table[2] !== 64)
|
|
@@ -2663,7 +2740,10 @@ class MidyGM2 {
|
|
|
2663
2740
|
}
|
|
2664
2741
|
handlePressureSysEx(data, tableName) {
|
|
2665
2742
|
const channelNumber = data[4];
|
|
2666
|
-
const
|
|
2743
|
+
const channel = this.channels[channelNumber];
|
|
2744
|
+
if (channel.isDrum)
|
|
2745
|
+
return;
|
|
2746
|
+
const table = channel[tableName];
|
|
2667
2747
|
for (let i = 5; i < data.length - 1; i += 2) {
|
|
2668
2748
|
const pp = data[i];
|
|
2669
2749
|
const rr = data[i + 1];
|
|
@@ -2673,8 +2753,13 @@ class MidyGM2 {
|
|
|
2673
2753
|
initControlTable() {
|
|
2674
2754
|
const channelCount = 128;
|
|
2675
2755
|
const slotSize = 6;
|
|
2676
|
-
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2677
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];
|
|
2678
2763
|
for (let ch = 0; ch < channelCount; ch++) {
|
|
2679
2764
|
const offset = ch * slotSize;
|
|
2680
2765
|
table.set(defaultValues, offset);
|
|
@@ -2691,8 +2776,11 @@ class MidyGM2 {
|
|
|
2691
2776
|
}
|
|
2692
2777
|
handleControlChangeSysEx(data) {
|
|
2693
2778
|
const channelNumber = data[4];
|
|
2779
|
+
const channel = this.channels[channelNumber];
|
|
2780
|
+
if (channel.isDrum)
|
|
2781
|
+
return;
|
|
2694
2782
|
const controllerType = data[5];
|
|
2695
|
-
const table =
|
|
2783
|
+
const table = channel.controlTable[controllerType];
|
|
2696
2784
|
for (let i = 6; i < data.length - 1; i += 2) {
|
|
2697
2785
|
const pp = data[i];
|
|
2698
2786
|
const rr = data[i + 1];
|
|
@@ -2706,8 +2794,11 @@ class MidyGM2 {
|
|
|
2706
2794
|
}
|
|
2707
2795
|
handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
|
|
2708
2796
|
const channelNumber = data[4];
|
|
2797
|
+
const channel = this.channels[channelNumber];
|
|
2798
|
+
if (channel.isDrum)
|
|
2799
|
+
return;
|
|
2709
2800
|
const keyNumber = data[5];
|
|
2710
|
-
const table =
|
|
2801
|
+
const table = channel.keyBasedInstrumentControlTable;
|
|
2711
2802
|
for (let i = 6; i < data.length - 1; i += 2) {
|
|
2712
2803
|
const controllerType = data[i];
|
|
2713
2804
|
const value = data[i + 1];
|