@marmooo/midy 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/esm/midy-GM1.d.ts +13 -50
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +164 -160
- package/esm/midy-GM2.d.ts +27 -83
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +390 -275
- package/esm/midy-GMLite.d.ts +12 -49
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +170 -163
- package/esm/midy.d.ts +31 -107
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +423 -306
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +13 -50
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +164 -160
- package/script/midy-GM2.d.ts +27 -83
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +390 -275
- package/script/midy-GMLite.d.ts +12 -49
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +170 -163
- package/script/midy.d.ts +31 -107
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +423 -306
package/esm/midy-GM2.js
CHANGED
|
@@ -1,59 +1,19 @@
|
|
|
1
1
|
import { parseMidi } from "midi-file";
|
|
2
2
|
import { parse, SoundFont } from "@marmooo/soundfont-parser";
|
|
3
|
-
// 2-3 times faster than Map
|
|
4
|
-
class SparseMap {
|
|
5
|
-
constructor(size) {
|
|
6
|
-
this.data = new Array(size);
|
|
7
|
-
this.activeIndices = [];
|
|
8
|
-
}
|
|
9
|
-
set(key, value) {
|
|
10
|
-
if (this.data[key] === undefined) {
|
|
11
|
-
this.activeIndices.push(key);
|
|
12
|
-
}
|
|
13
|
-
this.data[key] = value;
|
|
14
|
-
}
|
|
15
|
-
get(key) {
|
|
16
|
-
return this.data[key];
|
|
17
|
-
}
|
|
18
|
-
delete(key) {
|
|
19
|
-
if (this.data[key] !== undefined) {
|
|
20
|
-
this.data[key] = undefined;
|
|
21
|
-
const index = this.activeIndices.indexOf(key);
|
|
22
|
-
if (index !== -1) {
|
|
23
|
-
this.activeIndices.splice(index, 1);
|
|
24
|
-
}
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
has(key) {
|
|
30
|
-
return this.data[key] !== undefined;
|
|
31
|
-
}
|
|
32
|
-
get size() {
|
|
33
|
-
return this.activeIndices.length;
|
|
34
|
-
}
|
|
35
|
-
clear() {
|
|
36
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
37
|
-
const key = this.activeIndices[i];
|
|
38
|
-
this.data[key] = undefined;
|
|
39
|
-
}
|
|
40
|
-
this.activeIndices = [];
|
|
41
|
-
}
|
|
42
|
-
*[Symbol.iterator]() {
|
|
43
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
44
|
-
const key = this.activeIndices[i];
|
|
45
|
-
yield [key, this.data[key]];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
forEach(callback) {
|
|
49
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
50
|
-
const key = this.activeIndices[i];
|
|
51
|
-
callback(this.data[key], key, this);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
3
|
class Note {
|
|
56
4
|
constructor(noteNumber, velocity, startTime, voice, voiceParams) {
|
|
5
|
+
Object.defineProperty(this, "index", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: -1
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "noteOffEvent", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
57
17
|
Object.defineProperty(this, "bufferSource", {
|
|
58
18
|
enumerable: true,
|
|
59
19
|
configurable: true,
|
|
@@ -138,11 +98,11 @@ class Note {
|
|
|
138
98
|
writable: true,
|
|
139
99
|
value: void 0
|
|
140
100
|
});
|
|
141
|
-
Object.defineProperty(this, "
|
|
101
|
+
Object.defineProperty(this, "portamentoNoteNumber", {
|
|
142
102
|
enumerable: true,
|
|
143
103
|
configurable: true,
|
|
144
104
|
writable: true,
|
|
145
|
-
value:
|
|
105
|
+
value: -1
|
|
146
106
|
});
|
|
147
107
|
this.noteNumber = noteNumber;
|
|
148
108
|
this.velocity = velocity;
|
|
@@ -197,7 +157,7 @@ const defaultControllerState = {
|
|
|
197
157
|
portamentoTime: { type: 128 + 5, defaultValue: 0 },
|
|
198
158
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
199
159
|
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
200
|
-
pan: { type: 128 + 10, defaultValue:
|
|
160
|
+
pan: { type: 128 + 10, defaultValue: 64 / 127 },
|
|
201
161
|
expression: { type: 128 + 11, defaultValue: 1 },
|
|
202
162
|
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
203
163
|
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
@@ -205,14 +165,6 @@ const defaultControllerState = {
|
|
|
205
165
|
portamento: { type: 128 + 65, defaultValue: 0 },
|
|
206
166
|
sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
|
|
207
167
|
softPedal: { type: 128 + 67, defaultValue: 0 },
|
|
208
|
-
filterResonance: { type: 128 + 71, defaultValue: 0.5 },
|
|
209
|
-
releaseTime: { type: 128 + 72, defaultValue: 0.5 },
|
|
210
|
-
attackTime: { type: 128 + 73, defaultValue: 0.5 },
|
|
211
|
-
brightness: { type: 128 + 74, defaultValue: 0.5 },
|
|
212
|
-
decayTime: { type: 128 + 75, defaultValue: 0.5 },
|
|
213
|
-
vibratoRate: { type: 128 + 76, defaultValue: 0.5 },
|
|
214
|
-
vibratoDepth: { type: 128 + 77, defaultValue: 0.5 },
|
|
215
|
-
vibratoDelay: { type: 128 + 78, defaultValue: 0.5 },
|
|
216
168
|
reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
|
|
217
169
|
chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
|
|
218
170
|
// dataIncrement: { type: 128 + 96, defaultValue: 0 },
|
|
@@ -487,7 +439,7 @@ export class MidyGM2 {
|
|
|
487
439
|
initSoundFontTable() {
|
|
488
440
|
const table = new Array(128);
|
|
489
441
|
for (let i = 0; i < 128; i++) {
|
|
490
|
-
table[i] = new
|
|
442
|
+
table[i] = new Map();
|
|
491
443
|
}
|
|
492
444
|
return table;
|
|
493
445
|
}
|
|
@@ -534,18 +486,24 @@ export class MidyGM2 {
|
|
|
534
486
|
merger,
|
|
535
487
|
};
|
|
536
488
|
}
|
|
489
|
+
resetChannelTable(channel) {
|
|
490
|
+
this.resetControlTable(channel.controlTable);
|
|
491
|
+
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
492
|
+
channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
|
|
493
|
+
channel.keyBasedInstrumentControlTable.fill(0); // [-64, 63]
|
|
494
|
+
}
|
|
537
495
|
createChannels(audioContext) {
|
|
538
496
|
const channels = Array.from({ length: this.numChannels }, () => {
|
|
539
497
|
return {
|
|
540
498
|
currentBufferSource: null,
|
|
541
499
|
isDrum: false,
|
|
542
|
-
...this.constructor.channelSettings,
|
|
543
500
|
state: new ControllerState(),
|
|
544
|
-
|
|
501
|
+
...this.constructor.channelSettings,
|
|
545
502
|
...this.setChannelAudioNodes(audioContext),
|
|
546
|
-
scheduledNotes:
|
|
503
|
+
scheduledNotes: [],
|
|
547
504
|
sustainNotes: [],
|
|
548
|
-
sostenutoNotes:
|
|
505
|
+
sostenutoNotes: [],
|
|
506
|
+
controlTable: this.initControlTable(),
|
|
549
507
|
scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
|
|
550
508
|
channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
|
|
551
509
|
keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
|
|
@@ -592,46 +550,20 @@ export class MidyGM2 {
|
|
|
592
550
|
}
|
|
593
551
|
return bufferSource;
|
|
594
552
|
}
|
|
595
|
-
|
|
596
|
-
const endEvent = this.timeline[queueIndex];
|
|
597
|
-
if (!this.channels[endEvent.channel].portamento)
|
|
598
|
-
return;
|
|
599
|
-
const endTime = endEvent.startTime;
|
|
600
|
-
let target;
|
|
601
|
-
while (++queueIndex < this.timeline.length) {
|
|
602
|
-
const event = this.timeline[queueIndex];
|
|
603
|
-
if (endTime !== event.startTime)
|
|
604
|
-
break;
|
|
605
|
-
if (event.type !== "noteOn")
|
|
606
|
-
continue;
|
|
607
|
-
if (!target || event.noteNumber < target.noteNumber) {
|
|
608
|
-
target = event;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
return target;
|
|
612
|
-
}
|
|
613
|
-
async scheduleTimelineEvents(t, offset, queueIndex) {
|
|
553
|
+
async scheduleTimelineEvents(t, resumeTime, queueIndex) {
|
|
614
554
|
while (queueIndex < this.timeline.length) {
|
|
615
555
|
const event = this.timeline[queueIndex];
|
|
616
556
|
if (event.startTime > t + this.lookAhead)
|
|
617
557
|
break;
|
|
618
|
-
const
|
|
558
|
+
const delay = this.startDelay - resumeTime;
|
|
559
|
+
const startTime = event.startTime + delay;
|
|
619
560
|
switch (event.type) {
|
|
620
|
-
case "noteOn":
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
case "noteOff": {
|
|
627
|
-
const portamentoTarget = this.findPortamentoTarget(queueIndex);
|
|
628
|
-
if (portamentoTarget)
|
|
629
|
-
portamentoTarget.portamento = true;
|
|
630
|
-
const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
|
|
631
|
-
portamentoTarget?.noteNumber);
|
|
632
|
-
if (notePromise) {
|
|
633
|
-
this.notePromises.push(notePromise);
|
|
634
|
-
}
|
|
561
|
+
case "noteOn": {
|
|
562
|
+
const noteOffEvent = {
|
|
563
|
+
...event.noteOffEvent,
|
|
564
|
+
startTime: event.noteOffEvent.startTime + delay,
|
|
565
|
+
};
|
|
566
|
+
await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
|
|
635
567
|
break;
|
|
636
568
|
}
|
|
637
569
|
case "controller":
|
|
@@ -667,7 +599,7 @@ export class MidyGM2 {
|
|
|
667
599
|
this.isPaused = false;
|
|
668
600
|
this.startTime = this.audioContext.currentTime;
|
|
669
601
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
670
|
-
let
|
|
602
|
+
let resumeTime = this.resumeTime - this.startTime;
|
|
671
603
|
this.notePromises = [];
|
|
672
604
|
const schedulePlayback = async () => {
|
|
673
605
|
if (queueIndex >= this.timeline.length) {
|
|
@@ -676,18 +608,21 @@ export class MidyGM2 {
|
|
|
676
608
|
this.exclusiveClassNotes.fill(undefined);
|
|
677
609
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
678
610
|
this.audioBufferCache.clear();
|
|
611
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
612
|
+
this.resetAllStates(i);
|
|
613
|
+
}
|
|
679
614
|
resolve();
|
|
680
615
|
return;
|
|
681
616
|
}
|
|
682
617
|
const now = this.audioContext.currentTime;
|
|
683
|
-
const t = now +
|
|
684
|
-
queueIndex = await this.scheduleTimelineEvents(t,
|
|
618
|
+
const t = now + resumeTime;
|
|
619
|
+
queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
|
|
685
620
|
if (this.isPausing) {
|
|
686
621
|
await this.stopNotes(0, true, now);
|
|
687
622
|
this.notePromises = [];
|
|
688
|
-
resolve();
|
|
689
623
|
this.isPausing = false;
|
|
690
624
|
this.isPaused = true;
|
|
625
|
+
resolve();
|
|
691
626
|
return;
|
|
692
627
|
}
|
|
693
628
|
else if (this.isStopping) {
|
|
@@ -696,9 +631,12 @@ export class MidyGM2 {
|
|
|
696
631
|
this.exclusiveClassNotes.fill(undefined);
|
|
697
632
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
698
633
|
this.audioBufferCache.clear();
|
|
699
|
-
|
|
634
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
635
|
+
this.resetAllStates(i);
|
|
636
|
+
}
|
|
700
637
|
this.isStopping = false;
|
|
701
638
|
this.isPaused = false;
|
|
639
|
+
resolve();
|
|
702
640
|
return;
|
|
703
641
|
}
|
|
704
642
|
else if (this.isSeeking) {
|
|
@@ -707,7 +645,7 @@ export class MidyGM2 {
|
|
|
707
645
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
708
646
|
this.startTime = this.audioContext.currentTime;
|
|
709
647
|
queueIndex = this.getQueueIndex(this.resumeTime);
|
|
710
|
-
|
|
648
|
+
resumeTime = this.resumeTime - this.startTime;
|
|
711
649
|
this.isSeeking = false;
|
|
712
650
|
await schedulePlayback();
|
|
713
651
|
}
|
|
@@ -829,17 +767,52 @@ export class MidyGM2 {
|
|
|
829
767
|
prevTempoTicks = event.ticks;
|
|
830
768
|
}
|
|
831
769
|
}
|
|
770
|
+
const activeNotes = new Array(this.channels.length * 128);
|
|
771
|
+
for (let i = 0; i < activeNotes.length; i++) {
|
|
772
|
+
activeNotes[i] = [];
|
|
773
|
+
}
|
|
774
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
775
|
+
const event = timeline[i];
|
|
776
|
+
switch (event.type) {
|
|
777
|
+
case "noteOn": {
|
|
778
|
+
const index = event.channel * 128 + event.noteNumber;
|
|
779
|
+
activeNotes[index].push(event);
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
case "noteOff": {
|
|
783
|
+
const index = event.channel * 128 + event.noteNumber;
|
|
784
|
+
const noteOn = activeNotes[index].pop();
|
|
785
|
+
if (noteOn) {
|
|
786
|
+
noteOn.noteOffEvent = event;
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
const eventString = JSON.stringify(event, null, 2);
|
|
790
|
+
console.warn(`noteOff without matching noteOn: ${eventString}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
832
795
|
return { instruments, timeline };
|
|
833
796
|
}
|
|
797
|
+
stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
|
|
798
|
+
const channel = this.channels[channelNumber];
|
|
799
|
+
const promises = [];
|
|
800
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
801
|
+
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
|
|
802
|
+
this.notePromises.push(promise);
|
|
803
|
+
promises.push(promise);
|
|
804
|
+
});
|
|
805
|
+
return Promise.all(promises);
|
|
806
|
+
}
|
|
834
807
|
stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
|
|
835
808
|
const channel = this.channels[channelNumber];
|
|
836
809
|
const promises = [];
|
|
837
810
|
this.processScheduledNotes(channel, (note) => {
|
|
838
|
-
const promise = this.scheduleNoteOff(channelNumber, note
|
|
811
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
|
|
839
812
|
this.notePromises.push(promise);
|
|
840
813
|
promises.push(promise);
|
|
841
814
|
});
|
|
842
|
-
channel.scheduledNotes
|
|
815
|
+
channel.scheduledNotes = [];
|
|
843
816
|
return Promise.all(promises);
|
|
844
817
|
}
|
|
845
818
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -860,9 +833,6 @@ export class MidyGM2 {
|
|
|
860
833
|
if (!this.isPlaying)
|
|
861
834
|
return;
|
|
862
835
|
this.isStopping = true;
|
|
863
|
-
for (let i = 0; i < this.channels.length; i++) {
|
|
864
|
-
this.resetAllStates(i);
|
|
865
|
-
}
|
|
866
836
|
}
|
|
867
837
|
pause() {
|
|
868
838
|
if (!this.isPlaying || this.isPaused)
|
|
@@ -897,35 +867,31 @@ export class MidyGM2 {
|
|
|
897
867
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
898
868
|
}
|
|
899
869
|
processScheduledNotes(channel, callback) {
|
|
900
|
-
channel.scheduledNotes
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
getActiveNotes(channel, scheduleTime) {
|
|
910
|
-
const activeNotes = new SparseMap(128);
|
|
911
|
-
channel.scheduledNotes.forEach((noteList) => {
|
|
912
|
-
const activeNote = this.getActiveNote(noteList, scheduleTime);
|
|
913
|
-
if (activeNote) {
|
|
914
|
-
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
915
|
-
}
|
|
916
|
-
});
|
|
917
|
-
return activeNotes;
|
|
870
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
871
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
872
|
+
const note = scheduledNotes[i];
|
|
873
|
+
if (!note)
|
|
874
|
+
continue;
|
|
875
|
+
if (note.ending)
|
|
876
|
+
continue;
|
|
877
|
+
callback(note);
|
|
878
|
+
}
|
|
918
879
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
880
|
+
processActiveNotes(channel, scheduleTime, callback) {
|
|
881
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
882
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
883
|
+
const note = scheduledNotes[i];
|
|
922
884
|
if (!note)
|
|
923
|
-
|
|
885
|
+
continue;
|
|
886
|
+
if (note.ending)
|
|
887
|
+
continue;
|
|
888
|
+
const noteOffEvent = note.noteOffEvent;
|
|
889
|
+
if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
|
|
890
|
+
continue;
|
|
924
891
|
if (scheduleTime < note.startTime)
|
|
925
892
|
continue;
|
|
926
|
-
|
|
893
|
+
callback(note);
|
|
927
894
|
}
|
|
928
|
-
return noteList[0];
|
|
929
895
|
}
|
|
930
896
|
createConvolutionReverbImpulse(audioContext, decay, preDecay) {
|
|
931
897
|
const sampleRate = audioContext.sampleRate;
|
|
@@ -1090,25 +1056,95 @@ export class MidyGM2 {
|
|
|
1090
1056
|
}
|
|
1091
1057
|
updateDetune(channel, note, scheduleTime) {
|
|
1092
1058
|
const noteDetune = this.calcNoteDetune(channel, note);
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
.
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1059
|
+
const pitchControl = this.getPitchControl(channel, note);
|
|
1060
|
+
const detune = channel.detune + noteDetune + pitchControl;
|
|
1061
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1062
|
+
const startTime = note.startTime;
|
|
1063
|
+
const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
|
|
1064
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1065
|
+
note.bufferSource.detune
|
|
1066
|
+
.cancelScheduledValues(scheduleTime)
|
|
1067
|
+
.setValueAtTime(detune - deltaCent, scheduleTime)
|
|
1068
|
+
.linearRampToValueAtTime(detune, portamentoTime);
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
note.bufferSource.detune
|
|
1072
|
+
.cancelScheduledValues(scheduleTime)
|
|
1073
|
+
.setValueAtTime(detune, scheduleTime);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
getPortamentoTime(channel, note) {
|
|
1077
|
+
const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
|
|
1078
|
+
const value = Math.ceil(channel.state.portamentoTime * 127);
|
|
1079
|
+
return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
|
|
1080
|
+
}
|
|
1081
|
+
getPitchIncrementSpeed(value) {
|
|
1082
|
+
const points = [
|
|
1083
|
+
[0, 1000],
|
|
1084
|
+
[6, 100],
|
|
1085
|
+
[16, 20],
|
|
1086
|
+
[32, 10],
|
|
1087
|
+
[48, 5],
|
|
1088
|
+
[64, 2.5],
|
|
1089
|
+
[80, 1],
|
|
1090
|
+
[96, 0.4],
|
|
1091
|
+
[112, 0.15],
|
|
1092
|
+
[127, 0.01],
|
|
1093
|
+
];
|
|
1094
|
+
const logPoints = new Array(points.length);
|
|
1095
|
+
for (let i = 0; i < points.length; i++) {
|
|
1096
|
+
const [x, y] = points[i];
|
|
1097
|
+
if (value === x)
|
|
1098
|
+
return y;
|
|
1099
|
+
logPoints[i] = [x, Math.log(y)];
|
|
1100
|
+
}
|
|
1101
|
+
let startIndex = 0;
|
|
1102
|
+
for (let i = 1; i < logPoints.length; i++) {
|
|
1103
|
+
if (value <= logPoints[i][0]) {
|
|
1104
|
+
startIndex = i - 1;
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const [x0, y0] = logPoints[startIndex];
|
|
1109
|
+
const [x1, y1] = logPoints[startIndex + 1];
|
|
1110
|
+
const h = x1 - x0;
|
|
1111
|
+
const t = (value - x0) / h;
|
|
1112
|
+
let m0, m1;
|
|
1113
|
+
if (startIndex === 0) {
|
|
1114
|
+
m0 = (y1 - y0) / h;
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
const [xPrev, yPrev] = logPoints[startIndex - 1];
|
|
1118
|
+
m0 = (y1 - yPrev) / (x1 - xPrev);
|
|
1119
|
+
}
|
|
1120
|
+
if (startIndex === logPoints.length - 2) {
|
|
1121
|
+
m1 = (y1 - y0) / h;
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
const [xNext, yNext] = logPoints[startIndex + 2];
|
|
1125
|
+
m1 = (yNext - y0) / (xNext - x0);
|
|
1126
|
+
}
|
|
1127
|
+
// Cubic Hermite Spline
|
|
1128
|
+
const t2 = t * t;
|
|
1129
|
+
const t3 = t2 * t;
|
|
1130
|
+
const h00 = 2 * t3 - 3 * t2 + 1;
|
|
1131
|
+
const h10 = t3 - 2 * t2 + t;
|
|
1132
|
+
const h01 = -2 * t3 + 3 * t2;
|
|
1133
|
+
const h11 = t3 - t2;
|
|
1134
|
+
const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
|
|
1135
|
+
return Math.exp(y);
|
|
1136
|
+
}
|
|
1137
|
+
setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
|
|
1103
1138
|
const { voiceParams, startTime } = note;
|
|
1104
|
-
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation)
|
|
1139
|
+
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
|
|
1140
|
+
(1 + this.getAmplitudeControl(channel));
|
|
1105
1141
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1106
1142
|
const volDelay = startTime + voiceParams.volDelay;
|
|
1107
|
-
const
|
|
1143
|
+
const volAttack = volDelay + voiceParams.volAttack;
|
|
1144
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
1108
1145
|
note.volumeEnvelopeNode.gain
|
|
1109
1146
|
.cancelScheduledValues(scheduleTime)
|
|
1110
|
-
.setValueAtTime(
|
|
1111
|
-
.linearRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1147
|
+
.setValueAtTime(sustainVolume, volHold);
|
|
1112
1148
|
}
|
|
1113
1149
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
1114
1150
|
const { voiceParams, startTime } = note;
|
|
@@ -1127,6 +1163,12 @@ export class MidyGM2 {
|
|
|
1127
1163
|
.setValueAtTime(attackVolume, volHold)
|
|
1128
1164
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
1129
1165
|
}
|
|
1166
|
+
setPortamentoPitchEnvelope(note, scheduleTime) {
|
|
1167
|
+
const baseRate = note.voiceParams.playbackRate;
|
|
1168
|
+
note.bufferSource.playbackRate
|
|
1169
|
+
.cancelScheduledValues(scheduleTime)
|
|
1170
|
+
.setValueAtTime(baseRate, scheduleTime);
|
|
1171
|
+
}
|
|
1130
1172
|
setPitchEnvelope(note, scheduleTime) {
|
|
1131
1173
|
const { voiceParams } = note;
|
|
1132
1174
|
const baseRate = voiceParams.playbackRate;
|
|
@@ -1154,19 +1196,20 @@ export class MidyGM2 {
|
|
|
1154
1196
|
const maxFrequency = 20000; // max Hz of initialFilterFc
|
|
1155
1197
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
1156
1198
|
}
|
|
1157
|
-
|
|
1199
|
+
setPortamentoFilterEnvelope(channel, note, scheduleTime) {
|
|
1158
1200
|
const state = channel.state;
|
|
1159
1201
|
const { voiceParams, noteNumber, startTime } = note;
|
|
1160
1202
|
const softPedalFactor = 1 -
|
|
1161
1203
|
(0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1204
|
+
const baseCent = voiceParams.initialFilterFc +
|
|
1205
|
+
this.getFilterCutoffControl(channel);
|
|
1206
|
+
const baseFreq = this.centToHz(baseCent) * softPedalFactor;
|
|
1164
1207
|
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
|
|
1165
1208
|
const sustainFreq = baseFreq +
|
|
1166
1209
|
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
1167
1210
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
1168
1211
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
1169
|
-
const portamentoTime = startTime + this.getPortamentoTime(channel);
|
|
1212
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1170
1213
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1171
1214
|
note.filterNode.frequency
|
|
1172
1215
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1251,7 +1294,7 @@ export class MidyGM2 {
|
|
|
1251
1294
|
return audioBuffer;
|
|
1252
1295
|
}
|
|
1253
1296
|
}
|
|
1254
|
-
async createNote(channel, voice, noteNumber, velocity, startTime,
|
|
1297
|
+
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
1255
1298
|
const now = this.audioContext.currentTime;
|
|
1256
1299
|
const state = channel.state;
|
|
1257
1300
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
@@ -1267,20 +1310,24 @@ export class MidyGM2 {
|
|
|
1267
1310
|
type: "lowpass",
|
|
1268
1311
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
1269
1312
|
});
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1313
|
+
const prevNote = channel.scheduledNotes.at(-1);
|
|
1314
|
+
if (prevNote && prevNote.noteNumber !== noteNumber) {
|
|
1315
|
+
note.portamentoNoteNumber = prevNote.noteNumber;
|
|
1316
|
+
}
|
|
1317
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1318
|
+
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
1319
|
+
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
1320
|
+
this.setPortamentoPitchEnvelope(note, now);
|
|
1274
1321
|
}
|
|
1275
1322
|
else {
|
|
1276
|
-
note.portamento = false;
|
|
1277
1323
|
this.setVolumeEnvelope(channel, note, now);
|
|
1278
1324
|
this.setFilterEnvelope(channel, note, now);
|
|
1325
|
+
this.setPitchEnvelope(note, now);
|
|
1279
1326
|
}
|
|
1327
|
+
this.updateDetune(channel, note, now);
|
|
1280
1328
|
if (0 < state.vibratoDepth) {
|
|
1281
1329
|
this.startVibrato(channel, note, now);
|
|
1282
1330
|
}
|
|
1283
|
-
this.setPitchEnvelope(note, now);
|
|
1284
1331
|
if (0 < state.modulationDepth) {
|
|
1285
1332
|
this.startModulation(channel, note, now);
|
|
1286
1333
|
}
|
|
@@ -1326,9 +1373,8 @@ export class MidyGM2 {
|
|
|
1326
1373
|
if (prev) {
|
|
1327
1374
|
const [prevNote, prevChannelNumber] = prev;
|
|
1328
1375
|
if (prevNote && !prevNote.ending) {
|
|
1329
|
-
this.scheduleNoteOff(prevChannelNumber, prevNote
|
|
1330
|
-
startTime, true
|
|
1331
|
-
undefined);
|
|
1376
|
+
this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
|
|
1377
|
+
startTime, true);
|
|
1332
1378
|
}
|
|
1333
1379
|
}
|
|
1334
1380
|
this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
|
|
@@ -1347,9 +1393,8 @@ export class MidyGM2 {
|
|
|
1347
1393
|
channelNumber;
|
|
1348
1394
|
const prevNote = this.drumExclusiveClassNotes[index];
|
|
1349
1395
|
if (prevNote && !prevNote.ending) {
|
|
1350
|
-
this.scheduleNoteOff(channelNumber, prevNote
|
|
1351
|
-
startTime, true
|
|
1352
|
-
undefined);
|
|
1396
|
+
this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
|
|
1397
|
+
startTime, true);
|
|
1353
1398
|
}
|
|
1354
1399
|
this.drumExclusiveClassNotes[index] = note;
|
|
1355
1400
|
}
|
|
@@ -1357,10 +1402,10 @@ export class MidyGM2 {
|
|
|
1357
1402
|
if (!channel.isDrum)
|
|
1358
1403
|
return false;
|
|
1359
1404
|
const programNumber = channel.programNumber;
|
|
1360
|
-
return (programNumber === 48 && noteNumber === 88) ||
|
|
1361
|
-
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84);
|
|
1405
|
+
return !((programNumber === 48 && noteNumber === 88) ||
|
|
1406
|
+
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
1362
1407
|
}
|
|
1363
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime,
|
|
1408
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
1364
1409
|
const channel = this.channels[channelNumber];
|
|
1365
1410
|
const bankNumber = this.calcBank(channel, channelNumber);
|
|
1366
1411
|
const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
|
|
@@ -1371,7 +1416,8 @@ export class MidyGM2 {
|
|
|
1371
1416
|
if (!voice)
|
|
1372
1417
|
return;
|
|
1373
1418
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
1374
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime,
|
|
1419
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
1420
|
+
note.noteOffEvent = noteOffEvent;
|
|
1375
1421
|
note.gainL.connect(channel.gainL);
|
|
1376
1422
|
note.gainR.connect(channel.gainR);
|
|
1377
1423
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -1380,33 +1426,39 @@ export class MidyGM2 {
|
|
|
1380
1426
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1381
1427
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1382
1428
|
const scheduledNotes = channel.scheduledNotes;
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
notes.push(note);
|
|
1386
|
-
}
|
|
1387
|
-
else {
|
|
1388
|
-
notes = [note];
|
|
1389
|
-
scheduledNotes.set(noteNumber, notes);
|
|
1390
|
-
}
|
|
1429
|
+
note.index = scheduledNotes.length;
|
|
1430
|
+
scheduledNotes.push(note);
|
|
1391
1431
|
if (this.isDrumNoteOffException(channel, noteNumber)) {
|
|
1392
1432
|
const stopTime = startTime + note.bufferSource.buffer.duration;
|
|
1393
|
-
const index = notes.length - 1;
|
|
1394
1433
|
const promise = new Promise((resolve) => {
|
|
1395
1434
|
note.bufferSource.onended = () => {
|
|
1396
|
-
|
|
1435
|
+
scheduledNotes[note.index] = undefined;
|
|
1436
|
+
this.disconnectNote(note);
|
|
1397
1437
|
resolve();
|
|
1398
1438
|
};
|
|
1399
1439
|
note.bufferSource.stop(stopTime);
|
|
1400
1440
|
});
|
|
1401
1441
|
this.notePromises.push(promise);
|
|
1402
1442
|
}
|
|
1443
|
+
else if (noteOffEvent) {
|
|
1444
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1445
|
+
const portamentoTime = this.getPortamentoTime(channel, note);
|
|
1446
|
+
const portamentoEndTime = startTime + portamentoTime;
|
|
1447
|
+
const notePromise = this.scheduleNoteOff(channelNumber, note, 0, // velocity
|
|
1448
|
+
Math.max(noteOffEvent.startTime, portamentoEndTime), false);
|
|
1449
|
+
this.notePromises.push(notePromise);
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
|
|
1453
|
+
this.notePromises.push(notePromise);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1403
1456
|
}
|
|
1404
1457
|
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1405
1458
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1406
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime,
|
|
1459
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
|
|
1407
1460
|
}
|
|
1408
|
-
disconnectNote(note
|
|
1409
|
-
scheduledNotes[index] = null;
|
|
1461
|
+
disconnectNote(note) {
|
|
1410
1462
|
note.bufferSource.disconnect();
|
|
1411
1463
|
note.filterNode.disconnect();
|
|
1412
1464
|
note.volumeEnvelopeNode.disconnect();
|
|
@@ -1429,8 +1481,7 @@ export class MidyGM2 {
|
|
|
1429
1481
|
note.chorusEffectsSend.disconnect();
|
|
1430
1482
|
}
|
|
1431
1483
|
}
|
|
1432
|
-
stopNote(
|
|
1433
|
-
const note = scheduledNotes[index];
|
|
1484
|
+
stopNote(channel, note, endTime, stopTime) {
|
|
1434
1485
|
note.volumeEnvelopeNode.gain
|
|
1435
1486
|
.cancelScheduledValues(endTime)
|
|
1436
1487
|
.linearRampToValueAtTime(0, stopTime);
|
|
@@ -1440,64 +1491,57 @@ export class MidyGM2 {
|
|
|
1440
1491
|
}, stopTime);
|
|
1441
1492
|
return new Promise((resolve) => {
|
|
1442
1493
|
note.bufferSource.onended = () => {
|
|
1443
|
-
|
|
1494
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
1495
|
+
this.disconnectNote(note);
|
|
1444
1496
|
resolve();
|
|
1445
1497
|
};
|
|
1446
1498
|
note.bufferSource.stop(stopTime);
|
|
1447
1499
|
});
|
|
1448
1500
|
}
|
|
1449
|
-
scheduleNoteOff(channelNumber,
|
|
1501
|
+
scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
|
|
1450
1502
|
const channel = this.channels[channelNumber];
|
|
1451
|
-
if (this.isDrumNoteOffException(channel, noteNumber))
|
|
1503
|
+
if (this.isDrumNoteOffException(channel, note.noteNumber))
|
|
1452
1504
|
return;
|
|
1453
1505
|
const state = channel.state;
|
|
1454
1506
|
if (!force) {
|
|
1455
1507
|
if (0.5 <= state.sustainPedal)
|
|
1456
1508
|
return;
|
|
1457
|
-
if (channel.
|
|
1509
|
+
if (0.5 <= channel.state.sostenutoPedal)
|
|
1458
1510
|
return;
|
|
1459
1511
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1512
|
+
const volRelease = endTime + note.voiceParams.volRelease;
|
|
1513
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1514
|
+
note.filterNode.frequency
|
|
1515
|
+
.cancelScheduledValues(endTime)
|
|
1516
|
+
.linearRampToValueAtTime(0, modRelease);
|
|
1517
|
+
const stopTime = Math.min(volRelease, modRelease);
|
|
1518
|
+
return this.stopNote(channel, note, endTime, stopTime);
|
|
1519
|
+
}
|
|
1520
|
+
findNoteOffTarget(channel, noteNumber) {
|
|
1521
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
1463
1522
|
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
1464
1523
|
const note = scheduledNotes[i];
|
|
1465
1524
|
if (!note)
|
|
1466
1525
|
continue;
|
|
1467
1526
|
if (note.ending)
|
|
1468
1527
|
continue;
|
|
1469
|
-
if (
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
note.filterNode.frequency
|
|
1473
|
-
.cancelScheduledValues(endTime)
|
|
1474
|
-
.linearRampToValueAtTime(0, modRelease);
|
|
1475
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1476
|
-
return this.stopNote(endTime, stopTime, scheduledNotes, i);
|
|
1477
|
-
}
|
|
1478
|
-
else {
|
|
1479
|
-
const portamentoTime = endTime + this.getPortamentoTime(channel);
|
|
1480
|
-
const deltaNote = portamentoNoteNumber - noteNumber;
|
|
1481
|
-
const baseRate = note.voiceParams.playbackRate;
|
|
1482
|
-
const targetRate = baseRate * Math.pow(2, deltaNote / 12);
|
|
1483
|
-
note.bufferSource.playbackRate
|
|
1484
|
-
.cancelScheduledValues(endTime)
|
|
1485
|
-
.linearRampToValueAtTime(targetRate, portamentoTime);
|
|
1486
|
-
return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
|
|
1487
|
-
}
|
|
1528
|
+
if (note.noteNumber !== noteNumber)
|
|
1529
|
+
continue;
|
|
1530
|
+
return note;
|
|
1488
1531
|
}
|
|
1489
1532
|
}
|
|
1490
1533
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1491
1534
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1492
|
-
|
|
1493
|
-
|
|
1535
|
+
const channel = this.channels[channelNumber];
|
|
1536
|
+
const note = this.findNoteOffTarget(channel, noteNumber);
|
|
1537
|
+
return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
|
|
1494
1538
|
}
|
|
1495
1539
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
1496
1540
|
const velocity = halfVelocity * 2;
|
|
1497
1541
|
const channel = this.channels[channelNumber];
|
|
1498
1542
|
const promises = [];
|
|
1499
1543
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1500
|
-
const promise = this.
|
|
1544
|
+
const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
|
|
1501
1545
|
promises.push(promise);
|
|
1502
1546
|
}
|
|
1503
1547
|
channel.sustainNotes = [];
|
|
@@ -1507,12 +1551,14 @@ export class MidyGM2 {
|
|
|
1507
1551
|
const velocity = halfVelocity * 2;
|
|
1508
1552
|
const channel = this.channels[channelNumber];
|
|
1509
1553
|
const promises = [];
|
|
1554
|
+
const sostenutoNotes = channel.sostenutoNotes;
|
|
1510
1555
|
channel.state.sostenutoPedal = 0;
|
|
1511
|
-
|
|
1512
|
-
const
|
|
1556
|
+
for (let i = 0; i < sostenutoNotes.length; i++) {
|
|
1557
|
+
const note = sostenutoNotes[i];
|
|
1558
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
|
|
1513
1559
|
promises.push(promise);
|
|
1514
|
-
}
|
|
1515
|
-
channel.sostenutoNotes
|
|
1560
|
+
}
|
|
1561
|
+
channel.sostenutoNotes = [];
|
|
1516
1562
|
return promises;
|
|
1517
1563
|
}
|
|
1518
1564
|
handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
|
|
@@ -1562,10 +1608,10 @@ export class MidyGM2 {
|
|
|
1562
1608
|
channel.detune += pressureDepth * (next - prev);
|
|
1563
1609
|
}
|
|
1564
1610
|
const table = channel.channelPressureTable;
|
|
1565
|
-
this.
|
|
1611
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1566
1612
|
this.setControllerParameters(channel, note, table);
|
|
1567
1613
|
});
|
|
1568
|
-
|
|
1614
|
+
this.applyVoiceParams(channel, 13);
|
|
1569
1615
|
}
|
|
1570
1616
|
handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
|
|
1571
1617
|
const pitchBend = msb * 128 + lsb;
|
|
@@ -1742,6 +1788,7 @@ export class MidyGM2 {
|
|
|
1742
1788
|
state.set(channel.state.array);
|
|
1743
1789
|
state[2] = velocity / 127;
|
|
1744
1790
|
state[3] = noteNumber / 127;
|
|
1791
|
+
state[13] = state.channelPressure / 127;
|
|
1745
1792
|
return state;
|
|
1746
1793
|
}
|
|
1747
1794
|
applyVoiceParams(channel, controllerType, scheduleTime) {
|
|
@@ -1768,8 +1815,8 @@ export class MidyGM2 {
|
|
|
1768
1815
|
if (key in voiceParams)
|
|
1769
1816
|
noteVoiceParams[key] = voiceParams[key];
|
|
1770
1817
|
}
|
|
1771
|
-
if (note.
|
|
1772
|
-
this.
|
|
1818
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1819
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1773
1820
|
}
|
|
1774
1821
|
else {
|
|
1775
1822
|
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
@@ -1792,32 +1839,32 @@ export class MidyGM2 {
|
|
|
1792
1839
|
});
|
|
1793
1840
|
}
|
|
1794
1841
|
createControlChangeHandlers() {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1842
|
+
const handlers = new Array(128);
|
|
1843
|
+
handlers[0] = this.setBankMSB;
|
|
1844
|
+
handlers[1] = this.setModulationDepth;
|
|
1845
|
+
handlers[5] = this.setPortamentoTime;
|
|
1846
|
+
handlers[6] = this.dataEntryMSB;
|
|
1847
|
+
handlers[7] = this.setVolume;
|
|
1848
|
+
handlers[10] = this.setPan;
|
|
1849
|
+
handlers[11] = this.setExpression;
|
|
1850
|
+
handlers[32] = this.setBankLSB;
|
|
1851
|
+
handlers[38] = this.dataEntryLSB;
|
|
1852
|
+
handlers[64] = this.setSustainPedal;
|
|
1853
|
+
handlers[65] = this.setPortamento;
|
|
1854
|
+
handlers[66] = this.setSostenutoPedal;
|
|
1855
|
+
handlers[67] = this.setSoftPedal;
|
|
1856
|
+
handlers[91] = this.setReverbSendLevel;
|
|
1857
|
+
handlers[93] = this.setChorusSendLevel;
|
|
1858
|
+
handlers[100] = this.setRPNLSB;
|
|
1859
|
+
handlers[101] = this.setRPNMSB;
|
|
1860
|
+
handlers[120] = this.allSoundOff;
|
|
1861
|
+
handlers[121] = this.resetAllControllers;
|
|
1862
|
+
handlers[123] = this.allNotesOff;
|
|
1863
|
+
handlers[124] = this.omniOff;
|
|
1864
|
+
handlers[125] = this.omniOn;
|
|
1865
|
+
handlers[126] = this.monoOn;
|
|
1866
|
+
handlers[127] = this.polyOn;
|
|
1867
|
+
return handlers;
|
|
1821
1868
|
}
|
|
1822
1869
|
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1823
1870
|
const handler = this.controlChangeHandlers[controllerType];
|
|
@@ -1854,9 +1901,33 @@ export class MidyGM2 {
|
|
|
1854
1901
|
channel.state.modulationDepth = modulation / 127;
|
|
1855
1902
|
this.updateModulation(channel, scheduleTime);
|
|
1856
1903
|
}
|
|
1857
|
-
|
|
1904
|
+
updatePortamento(channel, scheduleTime) {
|
|
1905
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1906
|
+
if (0.5 <= channel.state.portamento) {
|
|
1907
|
+
if (0 <= note.portamentoNoteNumber) {
|
|
1908
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
1909
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1910
|
+
this.setPortamentoPitchEnvelope(note, scheduleTime);
|
|
1911
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
else {
|
|
1915
|
+
if (0 <= note.portamentoNoteNumber) {
|
|
1916
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1917
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
1918
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1919
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
|
|
1858
1925
|
const channel = this.channels[channelNumber];
|
|
1926
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1859
1927
|
channel.state.portamentoTime = portamentoTime / 127;
|
|
1928
|
+
if (channel.isDrum)
|
|
1929
|
+
return;
|
|
1930
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1860
1931
|
}
|
|
1861
1932
|
setKeyBasedVolume(channel, scheduleTime) {
|
|
1862
1933
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1942,11 +2013,13 @@ export class MidyGM2 {
|
|
|
1942
2013
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
1943
2014
|
}
|
|
1944
2015
|
}
|
|
1945
|
-
setPortamento(channelNumber, value) {
|
|
2016
|
+
setPortamento(channelNumber, value, scheduleTime) {
|
|
1946
2017
|
const channel = this.channels[channelNumber];
|
|
1947
2018
|
if (channel.isDrum)
|
|
1948
2019
|
return;
|
|
2020
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1949
2021
|
channel.state.portamento = value / 127;
|
|
2022
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1950
2023
|
}
|
|
1951
2024
|
setSostenutoPedal(channelNumber, value, scheduleTime) {
|
|
1952
2025
|
const channel = this.channels[channelNumber];
|
|
@@ -1955,7 +2028,11 @@ export class MidyGM2 {
|
|
|
1955
2028
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1956
2029
|
channel.state.sostenutoPedal = value / 127;
|
|
1957
2030
|
if (64 <= value) {
|
|
1958
|
-
|
|
2031
|
+
const sostenutoNotes = [];
|
|
2032
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2033
|
+
sostenutoNotes.push(note);
|
|
2034
|
+
});
|
|
2035
|
+
channel.sostenutoNotes = sostenutoNotes;
|
|
1959
2036
|
}
|
|
1960
2037
|
else {
|
|
1961
2038
|
this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
|
|
@@ -1965,12 +2042,13 @@ export class MidyGM2 {
|
|
|
1965
2042
|
const channel = this.channels[channelNumber];
|
|
1966
2043
|
if (channel.isDrum)
|
|
1967
2044
|
return;
|
|
2045
|
+
const state = channel.state;
|
|
1968
2046
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1969
|
-
|
|
2047
|
+
state.softPedal = softPedal / 127;
|
|
1970
2048
|
this.processScheduledNotes(channel, (note) => {
|
|
1971
|
-
if (note.
|
|
1972
|
-
this.
|
|
1973
|
-
this.
|
|
2049
|
+
if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2050
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2051
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1974
2052
|
}
|
|
1975
2053
|
else {
|
|
1976
2054
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
@@ -2167,24 +2245,32 @@ export class MidyGM2 {
|
|
|
2167
2245
|
}
|
|
2168
2246
|
allSoundOff(channelNumber, _value, scheduleTime) {
|
|
2169
2247
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2170
|
-
return this.
|
|
2248
|
+
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
2171
2249
|
}
|
|
2172
2250
|
resetAllStates(channelNumber) {
|
|
2251
|
+
const scheduleTime = this.audioContext.currentTime;
|
|
2173
2252
|
const channel = this.channels[channelNumber];
|
|
2174
2253
|
const state = channel.state;
|
|
2175
|
-
|
|
2176
|
-
|
|
2254
|
+
const entries = Object.entries(defaultControllerState);
|
|
2255
|
+
for (const [key, { type, defaultValue }] of entries) {
|
|
2256
|
+
if (128 <= type) {
|
|
2257
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2258
|
+
}
|
|
2259
|
+
else {
|
|
2260
|
+
state[key] = defaultValue;
|
|
2261
|
+
}
|
|
2177
2262
|
}
|
|
2178
|
-
for (const
|
|
2179
|
-
channel[
|
|
2263
|
+
for (const key of Object.keys(this.constructor.channelSettings)) {
|
|
2264
|
+
channel[key] = this.constructor.channelSettings[key];
|
|
2180
2265
|
}
|
|
2266
|
+
this.resetChannelTable(channel);
|
|
2181
2267
|
this.mode = "GM2";
|
|
2182
2268
|
this.masterFineTuning = 0; // cb
|
|
2183
2269
|
this.masterCoarseTuning = 0; // cb
|
|
2184
2270
|
}
|
|
2185
2271
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
|
|
2186
|
-
resetAllControllers(channelNumber) {
|
|
2187
|
-
const
|
|
2272
|
+
resetAllControllers(channelNumber, _value, scheduleTime) {
|
|
2273
|
+
const keys = [
|
|
2188
2274
|
"channelPressure",
|
|
2189
2275
|
"pitchWheel",
|
|
2190
2276
|
"expression",
|
|
@@ -2196,10 +2282,17 @@ export class MidyGM2 {
|
|
|
2196
2282
|
];
|
|
2197
2283
|
const channel = this.channels[channelNumber];
|
|
2198
2284
|
const state = channel.state;
|
|
2199
|
-
for (let i = 0; i <
|
|
2200
|
-
const
|
|
2201
|
-
|
|
2285
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2286
|
+
const key = keys[i];
|
|
2287
|
+
const { type, defaultValue } = defaultControllerState[key];
|
|
2288
|
+
if (128 <= type) {
|
|
2289
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2290
|
+
}
|
|
2291
|
+
else {
|
|
2292
|
+
state[key] = defaultValue;
|
|
2293
|
+
}
|
|
2202
2294
|
}
|
|
2295
|
+
this.setPitchBend(channelNumber, 8192, scheduleTime);
|
|
2203
2296
|
const settingTypes = [
|
|
2204
2297
|
"rpnMSB",
|
|
2205
2298
|
"rpnLSB",
|
|
@@ -2211,7 +2304,7 @@ export class MidyGM2 {
|
|
|
2211
2304
|
}
|
|
2212
2305
|
allNotesOff(channelNumber, _value, scheduleTime) {
|
|
2213
2306
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2214
|
-
return this.
|
|
2307
|
+
return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
|
|
2215
2308
|
}
|
|
2216
2309
|
omniOff(channelNumber, value, scheduleTime) {
|
|
2217
2310
|
this.allNotesOff(channelNumber, value, scheduleTime);
|
|
@@ -2428,7 +2521,7 @@ export class MidyGM2 {
|
|
|
2428
2521
|
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2429
2522
|
}
|
|
2430
2523
|
getReverbTime(value) {
|
|
2431
|
-
return Math.
|
|
2524
|
+
return Math.exp((value - 40) * 0.025);
|
|
2432
2525
|
}
|
|
2433
2526
|
// mean free path equation
|
|
2434
2527
|
// https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
|
|
@@ -2585,6 +2678,8 @@ export class MidyGM2 {
|
|
|
2585
2678
|
if (!channelBitmap[i])
|
|
2586
2679
|
continue;
|
|
2587
2680
|
const channel = this.channels[i];
|
|
2681
|
+
if (channel.isDrum)
|
|
2682
|
+
continue;
|
|
2588
2683
|
for (let j = 0; j < 12; j++) {
|
|
2589
2684
|
const centValue = data[j + 7] - 64;
|
|
2590
2685
|
channel.scaleOctaveTuningTable[j] = centValue;
|
|
@@ -2621,7 +2716,13 @@ export class MidyGM2 {
|
|
|
2621
2716
|
setControllerParameters(channel, note, table) {
|
|
2622
2717
|
if (table[0] !== 64)
|
|
2623
2718
|
this.updateDetune(channel, note);
|
|
2624
|
-
if (
|
|
2719
|
+
if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
|
|
2720
|
+
if (table[1] !== 64)
|
|
2721
|
+
this.setPortamentoFilterEnvelope(channel, note);
|
|
2722
|
+
if (table[2] !== 64)
|
|
2723
|
+
this.setPortamentoVolumeEnvelope(channel, note);
|
|
2724
|
+
}
|
|
2725
|
+
else {
|
|
2625
2726
|
if (table[1] !== 64)
|
|
2626
2727
|
this.setFilterEnvelope(channel, note);
|
|
2627
2728
|
if (table[2] !== 64)
|
|
@@ -2636,7 +2737,10 @@ export class MidyGM2 {
|
|
|
2636
2737
|
}
|
|
2637
2738
|
handlePressureSysEx(data, tableName) {
|
|
2638
2739
|
const channelNumber = data[4];
|
|
2639
|
-
const
|
|
2740
|
+
const channel = this.channels[channelNumber];
|
|
2741
|
+
if (channel.isDrum)
|
|
2742
|
+
return;
|
|
2743
|
+
const table = channel[tableName];
|
|
2640
2744
|
for (let i = 5; i < data.length - 1; i += 2) {
|
|
2641
2745
|
const pp = data[i];
|
|
2642
2746
|
const rr = data[i + 1];
|
|
@@ -2646,8 +2750,13 @@ export class MidyGM2 {
|
|
|
2646
2750
|
initControlTable() {
|
|
2647
2751
|
const channelCount = 128;
|
|
2648
2752
|
const slotSize = 6;
|
|
2649
|
-
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2650
2753
|
const table = new Uint8Array(channelCount * slotSize);
|
|
2754
|
+
return this.resetControlTable(table);
|
|
2755
|
+
}
|
|
2756
|
+
resetControlTable(table) {
|
|
2757
|
+
const channelCount = 128;
|
|
2758
|
+
const slotSize = 6;
|
|
2759
|
+
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2651
2760
|
for (let ch = 0; ch < channelCount; ch++) {
|
|
2652
2761
|
const offset = ch * slotSize;
|
|
2653
2762
|
table.set(defaultValues, offset);
|
|
@@ -2664,8 +2773,11 @@ export class MidyGM2 {
|
|
|
2664
2773
|
}
|
|
2665
2774
|
handleControlChangeSysEx(data) {
|
|
2666
2775
|
const channelNumber = data[4];
|
|
2776
|
+
const channel = this.channels[channelNumber];
|
|
2777
|
+
if (channel.isDrum)
|
|
2778
|
+
return;
|
|
2667
2779
|
const controllerType = data[5];
|
|
2668
|
-
const table =
|
|
2780
|
+
const table = channel.controlTable[controllerType];
|
|
2669
2781
|
for (let i = 6; i < data.length - 1; i += 2) {
|
|
2670
2782
|
const pp = data[i];
|
|
2671
2783
|
const rr = data[i + 1];
|
|
@@ -2679,8 +2791,11 @@ export class MidyGM2 {
|
|
|
2679
2791
|
}
|
|
2680
2792
|
handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
|
|
2681
2793
|
const channelNumber = data[4];
|
|
2794
|
+
const channel = this.channels[channelNumber];
|
|
2795
|
+
if (channel.isDrum)
|
|
2796
|
+
return;
|
|
2682
2797
|
const keyNumber = data[5];
|
|
2683
|
-
const table =
|
|
2798
|
+
const table = channel.keyBasedInstrumentControlTable;
|
|
2684
2799
|
for (let i = 6; i < data.length - 1; i += 2) {
|
|
2685
2800
|
const controllerType = data[i];
|
|
2686
2801
|
const value = data[i + 1];
|