@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/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,13 +767,37 @@ 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
|
}
|
|
834
797
|
stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
|
|
835
798
|
const channel = this.channels[channelNumber];
|
|
836
799
|
const promises = [];
|
|
837
|
-
|
|
838
|
-
activeNotes.forEach((note) => {
|
|
800
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
839
801
|
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
|
|
840
802
|
this.notePromises.push(promise);
|
|
841
803
|
promises.push(promise);
|
|
@@ -846,11 +808,11 @@ export class MidyGM2 {
|
|
|
846
808
|
const channel = this.channels[channelNumber];
|
|
847
809
|
const promises = [];
|
|
848
810
|
this.processScheduledNotes(channel, (note) => {
|
|
849
|
-
const promise = this.scheduleNoteOff(channelNumber, note
|
|
811
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
|
|
850
812
|
this.notePromises.push(promise);
|
|
851
813
|
promises.push(promise);
|
|
852
814
|
});
|
|
853
|
-
channel.scheduledNotes
|
|
815
|
+
channel.scheduledNotes = [];
|
|
854
816
|
return Promise.all(promises);
|
|
855
817
|
}
|
|
856
818
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -871,9 +833,6 @@ export class MidyGM2 {
|
|
|
871
833
|
if (!this.isPlaying)
|
|
872
834
|
return;
|
|
873
835
|
this.isStopping = true;
|
|
874
|
-
for (let i = 0; i < this.channels.length; i++) {
|
|
875
|
-
this.resetAllStates(i);
|
|
876
|
-
}
|
|
877
836
|
}
|
|
878
837
|
pause() {
|
|
879
838
|
if (!this.isPlaying || this.isPaused)
|
|
@@ -908,37 +867,31 @@ export class MidyGM2 {
|
|
|
908
867
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
909
868
|
}
|
|
910
869
|
processScheduledNotes(channel, callback) {
|
|
911
|
-
channel.scheduledNotes
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
getActiveNotes(channel, scheduleTime) {
|
|
923
|
-
const activeNotes = new SparseMap(128);
|
|
924
|
-
channel.scheduledNotes.forEach((noteList) => {
|
|
925
|
-
const activeNote = this.getActiveNote(noteList, scheduleTime);
|
|
926
|
-
if (activeNote) {
|
|
927
|
-
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
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
|
+
}
|
|
931
879
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
880
|
+
processActiveNotes(channel, scheduleTime, callback) {
|
|
881
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
882
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
883
|
+
const note = scheduledNotes[i];
|
|
935
884
|
if (!note)
|
|
936
|
-
|
|
885
|
+
continue;
|
|
886
|
+
if (note.ending)
|
|
887
|
+
continue;
|
|
888
|
+
const noteOffEvent = note.noteOffEvent;
|
|
889
|
+
if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
|
|
890
|
+
continue;
|
|
937
891
|
if (scheduleTime < note.startTime)
|
|
938
892
|
continue;
|
|
939
|
-
|
|
893
|
+
callback(note);
|
|
940
894
|
}
|
|
941
|
-
return noteList[0];
|
|
942
895
|
}
|
|
943
896
|
createConvolutionReverbImpulse(audioContext, decay, preDecay) {
|
|
944
897
|
const sampleRate = audioContext.sampleRate;
|
|
@@ -1103,25 +1056,95 @@ export class MidyGM2 {
|
|
|
1103
1056
|
}
|
|
1104
1057
|
updateDetune(channel, note, scheduleTime) {
|
|
1105
1058
|
const noteDetune = this.calcNoteDetune(channel, note);
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
.
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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) {
|
|
1116
1138
|
const { voiceParams, startTime } = note;
|
|
1117
|
-
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation)
|
|
1139
|
+
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
|
|
1140
|
+
(1 + this.getAmplitudeControl(channel));
|
|
1118
1141
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1119
1142
|
const volDelay = startTime + voiceParams.volDelay;
|
|
1120
|
-
const
|
|
1143
|
+
const volAttack = volDelay + voiceParams.volAttack;
|
|
1144
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
1121
1145
|
note.volumeEnvelopeNode.gain
|
|
1122
1146
|
.cancelScheduledValues(scheduleTime)
|
|
1123
|
-
.setValueAtTime(
|
|
1124
|
-
.linearRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1147
|
+
.setValueAtTime(sustainVolume, volHold);
|
|
1125
1148
|
}
|
|
1126
1149
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
1127
1150
|
const { voiceParams, startTime } = note;
|
|
@@ -1140,6 +1163,12 @@ export class MidyGM2 {
|
|
|
1140
1163
|
.setValueAtTime(attackVolume, volHold)
|
|
1141
1164
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
1142
1165
|
}
|
|
1166
|
+
setPortamentoPitchEnvelope(note, scheduleTime) {
|
|
1167
|
+
const baseRate = note.voiceParams.playbackRate;
|
|
1168
|
+
note.bufferSource.playbackRate
|
|
1169
|
+
.cancelScheduledValues(scheduleTime)
|
|
1170
|
+
.setValueAtTime(baseRate, scheduleTime);
|
|
1171
|
+
}
|
|
1143
1172
|
setPitchEnvelope(note, scheduleTime) {
|
|
1144
1173
|
const { voiceParams } = note;
|
|
1145
1174
|
const baseRate = voiceParams.playbackRate;
|
|
@@ -1167,19 +1196,20 @@ export class MidyGM2 {
|
|
|
1167
1196
|
const maxFrequency = 20000; // max Hz of initialFilterFc
|
|
1168
1197
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
1169
1198
|
}
|
|
1170
|
-
|
|
1199
|
+
setPortamentoFilterEnvelope(channel, note, scheduleTime) {
|
|
1171
1200
|
const state = channel.state;
|
|
1172
1201
|
const { voiceParams, noteNumber, startTime } = note;
|
|
1173
1202
|
const softPedalFactor = 1 -
|
|
1174
1203
|
(0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1204
|
+
const baseCent = voiceParams.initialFilterFc +
|
|
1205
|
+
this.getFilterCutoffControl(channel);
|
|
1206
|
+
const baseFreq = this.centToHz(baseCent) * softPedalFactor;
|
|
1177
1207
|
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
|
|
1178
1208
|
const sustainFreq = baseFreq +
|
|
1179
1209
|
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
1180
1210
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
1181
1211
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
1182
|
-
const portamentoTime = startTime + this.getPortamentoTime(channel);
|
|
1212
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1183
1213
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1184
1214
|
note.filterNode.frequency
|
|
1185
1215
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1264,7 +1294,7 @@ export class MidyGM2 {
|
|
|
1264
1294
|
return audioBuffer;
|
|
1265
1295
|
}
|
|
1266
1296
|
}
|
|
1267
|
-
async createNote(channel, voice, noteNumber, velocity, startTime,
|
|
1297
|
+
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
1268
1298
|
const now = this.audioContext.currentTime;
|
|
1269
1299
|
const state = channel.state;
|
|
1270
1300
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
@@ -1280,20 +1310,24 @@ export class MidyGM2 {
|
|
|
1280
1310
|
type: "lowpass",
|
|
1281
1311
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
1282
1312
|
});
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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);
|
|
1287
1321
|
}
|
|
1288
1322
|
else {
|
|
1289
|
-
note.portamento = false;
|
|
1290
1323
|
this.setVolumeEnvelope(channel, note, now);
|
|
1291
1324
|
this.setFilterEnvelope(channel, note, now);
|
|
1325
|
+
this.setPitchEnvelope(note, now);
|
|
1292
1326
|
}
|
|
1327
|
+
this.updateDetune(channel, note, now);
|
|
1293
1328
|
if (0 < state.vibratoDepth) {
|
|
1294
1329
|
this.startVibrato(channel, note, now);
|
|
1295
1330
|
}
|
|
1296
|
-
this.setPitchEnvelope(note, now);
|
|
1297
1331
|
if (0 < state.modulationDepth) {
|
|
1298
1332
|
this.startModulation(channel, note, now);
|
|
1299
1333
|
}
|
|
@@ -1339,9 +1373,8 @@ export class MidyGM2 {
|
|
|
1339
1373
|
if (prev) {
|
|
1340
1374
|
const [prevNote, prevChannelNumber] = prev;
|
|
1341
1375
|
if (prevNote && !prevNote.ending) {
|
|
1342
|
-
this.scheduleNoteOff(prevChannelNumber, prevNote
|
|
1343
|
-
startTime, true
|
|
1344
|
-
undefined);
|
|
1376
|
+
this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
|
|
1377
|
+
startTime, true);
|
|
1345
1378
|
}
|
|
1346
1379
|
}
|
|
1347
1380
|
this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
|
|
@@ -1360,9 +1393,8 @@ export class MidyGM2 {
|
|
|
1360
1393
|
channelNumber;
|
|
1361
1394
|
const prevNote = this.drumExclusiveClassNotes[index];
|
|
1362
1395
|
if (prevNote && !prevNote.ending) {
|
|
1363
|
-
this.scheduleNoteOff(channelNumber, prevNote
|
|
1364
|
-
startTime, true
|
|
1365
|
-
undefined);
|
|
1396
|
+
this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
|
|
1397
|
+
startTime, true);
|
|
1366
1398
|
}
|
|
1367
1399
|
this.drumExclusiveClassNotes[index] = note;
|
|
1368
1400
|
}
|
|
@@ -1373,7 +1405,7 @@ export class MidyGM2 {
|
|
|
1373
1405
|
return !((programNumber === 48 && noteNumber === 88) ||
|
|
1374
1406
|
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
1375
1407
|
}
|
|
1376
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime,
|
|
1408
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
1377
1409
|
const channel = this.channels[channelNumber];
|
|
1378
1410
|
const bankNumber = this.calcBank(channel, channelNumber);
|
|
1379
1411
|
const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
|
|
@@ -1384,7 +1416,8 @@ export class MidyGM2 {
|
|
|
1384
1416
|
if (!voice)
|
|
1385
1417
|
return;
|
|
1386
1418
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
1387
|
-
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;
|
|
1388
1421
|
note.gainL.connect(channel.gainL);
|
|
1389
1422
|
note.gainR.connect(channel.gainR);
|
|
1390
1423
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -1393,20 +1426,13 @@ export class MidyGM2 {
|
|
|
1393
1426
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1394
1427
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1395
1428
|
const scheduledNotes = channel.scheduledNotes;
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
noteList.push(note);
|
|
1399
|
-
}
|
|
1400
|
-
else {
|
|
1401
|
-
noteList = [note];
|
|
1402
|
-
scheduledNotes.set(noteNumber, noteList);
|
|
1403
|
-
}
|
|
1429
|
+
note.index = scheduledNotes.length;
|
|
1430
|
+
scheduledNotes.push(note);
|
|
1404
1431
|
if (this.isDrumNoteOffException(channel, noteNumber)) {
|
|
1405
1432
|
const stopTime = startTime + note.bufferSource.buffer.duration;
|
|
1406
|
-
const index = noteList.length - 1;
|
|
1407
1433
|
const promise = new Promise((resolve) => {
|
|
1408
1434
|
note.bufferSource.onended = () => {
|
|
1409
|
-
|
|
1435
|
+
scheduledNotes[note.index] = undefined;
|
|
1410
1436
|
this.disconnectNote(note);
|
|
1411
1437
|
resolve();
|
|
1412
1438
|
};
|
|
@@ -1414,10 +1440,23 @@ export class MidyGM2 {
|
|
|
1414
1440
|
});
|
|
1415
1441
|
this.notePromises.push(promise);
|
|
1416
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
|
+
}
|
|
1417
1456
|
}
|
|
1418
1457
|
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1419
1458
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1420
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime,
|
|
1459
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
|
|
1421
1460
|
}
|
|
1422
1461
|
disconnectNote(note) {
|
|
1423
1462
|
note.bufferSource.disconnect();
|
|
@@ -1442,8 +1481,7 @@ export class MidyGM2 {
|
|
|
1442
1481
|
note.chorusEffectsSend.disconnect();
|
|
1443
1482
|
}
|
|
1444
1483
|
}
|
|
1445
|
-
stopNote(
|
|
1446
|
-
const note = noteList[index];
|
|
1484
|
+
stopNote(channel, note, endTime, stopTime) {
|
|
1447
1485
|
note.volumeEnvelopeNode.gain
|
|
1448
1486
|
.cancelScheduledValues(endTime)
|
|
1449
1487
|
.linearRampToValueAtTime(0, stopTime);
|
|
@@ -1453,73 +1491,57 @@ export class MidyGM2 {
|
|
|
1453
1491
|
}, stopTime);
|
|
1454
1492
|
return new Promise((resolve) => {
|
|
1455
1493
|
note.bufferSource.onended = () => {
|
|
1456
|
-
|
|
1494
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
1457
1495
|
this.disconnectNote(note);
|
|
1458
1496
|
resolve();
|
|
1459
1497
|
};
|
|
1460
1498
|
note.bufferSource.stop(stopTime);
|
|
1461
1499
|
});
|
|
1462
1500
|
}
|
|
1463
|
-
|
|
1464
|
-
for (let i = 0; i < noteList.length; i++) {
|
|
1465
|
-
const note = noteList[i];
|
|
1466
|
-
if (!note)
|
|
1467
|
-
continue;
|
|
1468
|
-
if (note.ending)
|
|
1469
|
-
continue;
|
|
1470
|
-
return [note, i];
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
|
|
1501
|
+
scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
|
|
1474
1502
|
const channel = this.channels[channelNumber];
|
|
1475
|
-
if (this.isDrumNoteOffException(channel, noteNumber))
|
|
1503
|
+
if (this.isDrumNoteOffException(channel, note.noteNumber))
|
|
1476
1504
|
return;
|
|
1477
1505
|
const state = channel.state;
|
|
1478
1506
|
if (!force) {
|
|
1479
1507
|
if (0.5 <= state.sustainPedal)
|
|
1480
1508
|
return;
|
|
1481
|
-
if (channel.
|
|
1509
|
+
if (0.5 <= channel.state.sostenutoPedal)
|
|
1482
1510
|
return;
|
|
1483
1511
|
}
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
const
|
|
1496
|
-
note
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
note.voiceParams.volRelease * state.releaseTime * 2;
|
|
1504
|
-
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1505
|
-
note.filterNode.frequency
|
|
1506
|
-
.cancelScheduledValues(endTime)
|
|
1507
|
-
.linearRampToValueAtTime(0, modRelease);
|
|
1508
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1509
|
-
return this.stopNote(endTime, stopTime, noteList, i);
|
|
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;
|
|
1522
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
1523
|
+
const note = scheduledNotes[i];
|
|
1524
|
+
if (!note)
|
|
1525
|
+
continue;
|
|
1526
|
+
if (note.ending)
|
|
1527
|
+
continue;
|
|
1528
|
+
if (note.noteNumber !== noteNumber)
|
|
1529
|
+
continue;
|
|
1530
|
+
return note;
|
|
1510
1531
|
}
|
|
1511
1532
|
}
|
|
1512
1533
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1513
1534
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1514
|
-
|
|
1515
|
-
|
|
1535
|
+
const channel = this.channels[channelNumber];
|
|
1536
|
+
const note = this.findNoteOffTarget(channel, noteNumber);
|
|
1537
|
+
return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
|
|
1516
1538
|
}
|
|
1517
1539
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
1518
1540
|
const velocity = halfVelocity * 2;
|
|
1519
1541
|
const channel = this.channels[channelNumber];
|
|
1520
1542
|
const promises = [];
|
|
1521
1543
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1522
|
-
const promise = this.
|
|
1544
|
+
const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
|
|
1523
1545
|
promises.push(promise);
|
|
1524
1546
|
}
|
|
1525
1547
|
channel.sustainNotes = [];
|
|
@@ -1529,12 +1551,14 @@ export class MidyGM2 {
|
|
|
1529
1551
|
const velocity = halfVelocity * 2;
|
|
1530
1552
|
const channel = this.channels[channelNumber];
|
|
1531
1553
|
const promises = [];
|
|
1554
|
+
const sostenutoNotes = channel.sostenutoNotes;
|
|
1532
1555
|
channel.state.sostenutoPedal = 0;
|
|
1533
|
-
|
|
1534
|
-
const
|
|
1556
|
+
for (let i = 0; i < sostenutoNotes.length; i++) {
|
|
1557
|
+
const note = sostenutoNotes[i];
|
|
1558
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
|
|
1535
1559
|
promises.push(promise);
|
|
1536
|
-
}
|
|
1537
|
-
channel.sostenutoNotes
|
|
1560
|
+
}
|
|
1561
|
+
channel.sostenutoNotes = [];
|
|
1538
1562
|
return promises;
|
|
1539
1563
|
}
|
|
1540
1564
|
handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
|
|
@@ -1584,7 +1608,7 @@ export class MidyGM2 {
|
|
|
1584
1608
|
channel.detune += pressureDepth * (next - prev);
|
|
1585
1609
|
}
|
|
1586
1610
|
const table = channel.channelPressureTable;
|
|
1587
|
-
this.
|
|
1611
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1588
1612
|
this.setControllerParameters(channel, note, table);
|
|
1589
1613
|
});
|
|
1590
1614
|
this.applyVoiceParams(channel, 13);
|
|
@@ -1791,8 +1815,8 @@ export class MidyGM2 {
|
|
|
1791
1815
|
if (key in voiceParams)
|
|
1792
1816
|
noteVoiceParams[key] = voiceParams[key];
|
|
1793
1817
|
}
|
|
1794
|
-
if (0.5 <= channel.state.portamento && note.
|
|
1795
|
-
this.
|
|
1818
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1819
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1796
1820
|
}
|
|
1797
1821
|
else {
|
|
1798
1822
|
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
@@ -1815,32 +1839,32 @@ export class MidyGM2 {
|
|
|
1815
1839
|
});
|
|
1816
1840
|
}
|
|
1817
1841
|
createControlChangeHandlers() {
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
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;
|
|
1844
1868
|
}
|
|
1845
1869
|
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1846
1870
|
const handler = this.controlChangeHandlers[controllerType];
|
|
@@ -1877,9 +1901,33 @@ export class MidyGM2 {
|
|
|
1877
1901
|
channel.state.modulationDepth = modulation / 127;
|
|
1878
1902
|
this.updateModulation(channel, scheduleTime);
|
|
1879
1903
|
}
|
|
1880
|
-
|
|
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) {
|
|
1881
1925
|
const channel = this.channels[channelNumber];
|
|
1926
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1882
1927
|
channel.state.portamentoTime = portamentoTime / 127;
|
|
1928
|
+
if (channel.isDrum)
|
|
1929
|
+
return;
|
|
1930
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1883
1931
|
}
|
|
1884
1932
|
setKeyBasedVolume(channel, scheduleTime) {
|
|
1885
1933
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1965,11 +2013,13 @@ export class MidyGM2 {
|
|
|
1965
2013
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
1966
2014
|
}
|
|
1967
2015
|
}
|
|
1968
|
-
setPortamento(channelNumber, value) {
|
|
2016
|
+
setPortamento(channelNumber, value, scheduleTime) {
|
|
1969
2017
|
const channel = this.channels[channelNumber];
|
|
1970
2018
|
if (channel.isDrum)
|
|
1971
2019
|
return;
|
|
2020
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1972
2021
|
channel.state.portamento = value / 127;
|
|
2022
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1973
2023
|
}
|
|
1974
2024
|
setSostenutoPedal(channelNumber, value, scheduleTime) {
|
|
1975
2025
|
const channel = this.channels[channelNumber];
|
|
@@ -1978,7 +2028,11 @@ export class MidyGM2 {
|
|
|
1978
2028
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1979
2029
|
channel.state.sostenutoPedal = value / 127;
|
|
1980
2030
|
if (64 <= value) {
|
|
1981
|
-
|
|
2031
|
+
const sostenutoNotes = [];
|
|
2032
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2033
|
+
sostenutoNotes.push(note);
|
|
2034
|
+
});
|
|
2035
|
+
channel.sostenutoNotes = sostenutoNotes;
|
|
1982
2036
|
}
|
|
1983
2037
|
else {
|
|
1984
2038
|
this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
|
|
@@ -1992,9 +2046,9 @@ export class MidyGM2 {
|
|
|
1992
2046
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1993
2047
|
state.softPedal = softPedal / 127;
|
|
1994
2048
|
this.processScheduledNotes(channel, (note) => {
|
|
1995
|
-
if (0.5 <= state.portamento && note.
|
|
1996
|
-
this.
|
|
1997
|
-
this.
|
|
2049
|
+
if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2050
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2051
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1998
2052
|
}
|
|
1999
2053
|
else {
|
|
2000
2054
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
@@ -2194,21 +2248,29 @@ export class MidyGM2 {
|
|
|
2194
2248
|
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
2195
2249
|
}
|
|
2196
2250
|
resetAllStates(channelNumber) {
|
|
2251
|
+
const scheduleTime = this.audioContext.currentTime;
|
|
2197
2252
|
const channel = this.channels[channelNumber];
|
|
2198
2253
|
const state = channel.state;
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
+
}
|
|
2201
2262
|
}
|
|
2202
|
-
for (const
|
|
2203
|
-
channel[
|
|
2263
|
+
for (const key of Object.keys(this.constructor.channelSettings)) {
|
|
2264
|
+
channel[key] = this.constructor.channelSettings[key];
|
|
2204
2265
|
}
|
|
2266
|
+
this.resetChannelTable(channel);
|
|
2205
2267
|
this.mode = "GM2";
|
|
2206
2268
|
this.masterFineTuning = 0; // cb
|
|
2207
2269
|
this.masterCoarseTuning = 0; // cb
|
|
2208
2270
|
}
|
|
2209
2271
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
|
|
2210
|
-
resetAllControllers(channelNumber) {
|
|
2211
|
-
const
|
|
2272
|
+
resetAllControllers(channelNumber, _value, scheduleTime) {
|
|
2273
|
+
const keys = [
|
|
2212
2274
|
"channelPressure",
|
|
2213
2275
|
"pitchWheel",
|
|
2214
2276
|
"expression",
|
|
@@ -2220,10 +2282,17 @@ export class MidyGM2 {
|
|
|
2220
2282
|
];
|
|
2221
2283
|
const channel = this.channels[channelNumber];
|
|
2222
2284
|
const state = channel.state;
|
|
2223
|
-
for (let i = 0; i <
|
|
2224
|
-
const
|
|
2225
|
-
|
|
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
|
+
}
|
|
2226
2294
|
}
|
|
2295
|
+
this.setPitchBend(channelNumber, 8192, scheduleTime);
|
|
2227
2296
|
const settingTypes = [
|
|
2228
2297
|
"rpnMSB",
|
|
2229
2298
|
"rpnLSB",
|
|
@@ -2452,7 +2521,7 @@ export class MidyGM2 {
|
|
|
2452
2521
|
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2453
2522
|
}
|
|
2454
2523
|
getReverbTime(value) {
|
|
2455
|
-
return Math.
|
|
2524
|
+
return Math.exp((value - 40) * 0.025);
|
|
2456
2525
|
}
|
|
2457
2526
|
// mean free path equation
|
|
2458
2527
|
// https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
|
|
@@ -2609,6 +2678,8 @@ export class MidyGM2 {
|
|
|
2609
2678
|
if (!channelBitmap[i])
|
|
2610
2679
|
continue;
|
|
2611
2680
|
const channel = this.channels[i];
|
|
2681
|
+
if (channel.isDrum)
|
|
2682
|
+
continue;
|
|
2612
2683
|
for (let j = 0; j < 12; j++) {
|
|
2613
2684
|
const centValue = data[j + 7] - 64;
|
|
2614
2685
|
channel.scaleOctaveTuningTable[j] = centValue;
|
|
@@ -2645,7 +2716,13 @@ export class MidyGM2 {
|
|
|
2645
2716
|
setControllerParameters(channel, note, table) {
|
|
2646
2717
|
if (table[0] !== 64)
|
|
2647
2718
|
this.updateDetune(channel, note);
|
|
2648
|
-
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 {
|
|
2649
2726
|
if (table[1] !== 64)
|
|
2650
2727
|
this.setFilterEnvelope(channel, note);
|
|
2651
2728
|
if (table[2] !== 64)
|
|
@@ -2660,7 +2737,10 @@ export class MidyGM2 {
|
|
|
2660
2737
|
}
|
|
2661
2738
|
handlePressureSysEx(data, tableName) {
|
|
2662
2739
|
const channelNumber = data[4];
|
|
2663
|
-
const
|
|
2740
|
+
const channel = this.channels[channelNumber];
|
|
2741
|
+
if (channel.isDrum)
|
|
2742
|
+
return;
|
|
2743
|
+
const table = channel[tableName];
|
|
2664
2744
|
for (let i = 5; i < data.length - 1; i += 2) {
|
|
2665
2745
|
const pp = data[i];
|
|
2666
2746
|
const rr = data[i + 1];
|
|
@@ -2670,8 +2750,13 @@ export class MidyGM2 {
|
|
|
2670
2750
|
initControlTable() {
|
|
2671
2751
|
const channelCount = 128;
|
|
2672
2752
|
const slotSize = 6;
|
|
2673
|
-
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2674
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];
|
|
2675
2760
|
for (let ch = 0; ch < channelCount; ch++) {
|
|
2676
2761
|
const offset = ch * slotSize;
|
|
2677
2762
|
table.set(defaultValues, offset);
|
|
@@ -2688,8 +2773,11 @@ export class MidyGM2 {
|
|
|
2688
2773
|
}
|
|
2689
2774
|
handleControlChangeSysEx(data) {
|
|
2690
2775
|
const channelNumber = data[4];
|
|
2776
|
+
const channel = this.channels[channelNumber];
|
|
2777
|
+
if (channel.isDrum)
|
|
2778
|
+
return;
|
|
2691
2779
|
const controllerType = data[5];
|
|
2692
|
-
const table =
|
|
2780
|
+
const table = channel.controlTable[controllerType];
|
|
2693
2781
|
for (let i = 6; i < data.length - 1; i += 2) {
|
|
2694
2782
|
const pp = data[i];
|
|
2695
2783
|
const rr = data[i + 1];
|
|
@@ -2703,8 +2791,11 @@ export class MidyGM2 {
|
|
|
2703
2791
|
}
|
|
2704
2792
|
handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
|
|
2705
2793
|
const channelNumber = data[4];
|
|
2794
|
+
const channel = this.channels[channelNumber];
|
|
2795
|
+
if (channel.isDrum)
|
|
2796
|
+
return;
|
|
2706
2797
|
const keyNumber = data[5];
|
|
2707
|
-
const table =
|
|
2798
|
+
const table = channel.keyBasedInstrumentControlTable;
|
|
2708
2799
|
for (let i = 6; i < data.length - 1; i += 2) {
|
|
2709
2800
|
const controllerType = data[i];
|
|
2710
2801
|
const value = data[i + 1];
|