@marmooo/midy 0.4.2 → 0.4.4
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 -0
- package/esm/midy-GM1.d.ts +12 -6
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +177 -96
- package/esm/midy-GM2.d.ts +17 -10
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +264 -165
- package/esm/midy-GMLite.d.ts +15 -9
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +181 -99
- package/esm/midy.d.ts +56 -7
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +383 -166
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +12 -6
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +177 -96
- package/script/midy-GM2.d.ts +17 -10
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +264 -165
- package/script/midy-GMLite.d.ts +15 -9
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +181 -99
- package/script/midy.d.ts +56 -7
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +383 -166
package/esm/midy.js
CHANGED
|
@@ -14,6 +14,12 @@ class Note {
|
|
|
14
14
|
writable: true,
|
|
15
15
|
value: void 0
|
|
16
16
|
});
|
|
17
|
+
Object.defineProperty(this, "adjustedBaseFreq", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: 20000
|
|
22
|
+
});
|
|
17
23
|
Object.defineProperty(this, "index", {
|
|
18
24
|
enumerable: true,
|
|
19
25
|
configurable: true,
|
|
@@ -247,9 +253,25 @@ const pitchEnvelopeKeys = [
|
|
|
247
253
|
"playbackRate",
|
|
248
254
|
];
|
|
249
255
|
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
256
|
+
const defaultPressureValues = new Int8Array([64, 64, 64, 0, 0, 0]);
|
|
257
|
+
function cbToRatio(cb) {
|
|
258
|
+
return Math.pow(10, cb / 200);
|
|
259
|
+
}
|
|
260
|
+
const decayCurve = 1 / (-Math.log(cbToRatio(-1000)));
|
|
261
|
+
const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
|
|
250
262
|
export class Midy extends EventTarget {
|
|
251
263
|
constructor(audioContext) {
|
|
252
264
|
super();
|
|
265
|
+
// https://pmc.ncbi.nlm.nih.gov/articles/PMC4191557/
|
|
266
|
+
// https://pubmed.ncbi.nlm.nih.gov/12488797/
|
|
267
|
+
// Gap detection studies indicate humans detect temporal discontinuities
|
|
268
|
+
// around 2–3 ms. Smoothing over ~4 ms is perceived as continuous.
|
|
269
|
+
Object.defineProperty(this, "perceptualSmoothingTime", {
|
|
270
|
+
enumerable: true,
|
|
271
|
+
configurable: true,
|
|
272
|
+
writable: true,
|
|
273
|
+
value: 0.004
|
|
274
|
+
});
|
|
253
275
|
Object.defineProperty(this, "mode", {
|
|
254
276
|
enumerable: true,
|
|
255
277
|
configurable: true,
|
|
@@ -410,11 +432,25 @@ export class Midy extends EventTarget {
|
|
|
410
432
|
writable: true,
|
|
411
433
|
value: false
|
|
412
434
|
});
|
|
435
|
+
Object.defineProperty(this, "totalTimeEventTypes", {
|
|
436
|
+
enumerable: true,
|
|
437
|
+
configurable: true,
|
|
438
|
+
writable: true,
|
|
439
|
+
value: new Set([
|
|
440
|
+
"noteOff",
|
|
441
|
+
])
|
|
442
|
+
});
|
|
443
|
+
Object.defineProperty(this, "tempo", {
|
|
444
|
+
enumerable: true,
|
|
445
|
+
configurable: true,
|
|
446
|
+
writable: true,
|
|
447
|
+
value: 1
|
|
448
|
+
});
|
|
413
449
|
Object.defineProperty(this, "loop", {
|
|
414
450
|
enumerable: true,
|
|
415
451
|
configurable: true,
|
|
416
452
|
writable: true,
|
|
417
|
-
value:
|
|
453
|
+
value: false
|
|
418
454
|
});
|
|
419
455
|
Object.defineProperty(this, "loopStart", {
|
|
420
456
|
enumerable: true,
|
|
@@ -458,6 +494,33 @@ export class Midy extends EventTarget {
|
|
|
458
494
|
writable: true,
|
|
459
495
|
value: new Array(this.numChannels * drumExclusiveClassCount)
|
|
460
496
|
});
|
|
497
|
+
Object.defineProperty(this, "mpeEnabled", {
|
|
498
|
+
enumerable: true,
|
|
499
|
+
configurable: true,
|
|
500
|
+
writable: true,
|
|
501
|
+
value: false
|
|
502
|
+
});
|
|
503
|
+
Object.defineProperty(this, "lowerMPEMembers", {
|
|
504
|
+
enumerable: true,
|
|
505
|
+
configurable: true,
|
|
506
|
+
writable: true,
|
|
507
|
+
value: 0
|
|
508
|
+
});
|
|
509
|
+
Object.defineProperty(this, "upperMPEMembers", {
|
|
510
|
+
enumerable: true,
|
|
511
|
+
configurable: true,
|
|
512
|
+
writable: true,
|
|
513
|
+
value: 0
|
|
514
|
+
});
|
|
515
|
+
Object.defineProperty(this, "mpeState", {
|
|
516
|
+
enumerable: true,
|
|
517
|
+
configurable: true,
|
|
518
|
+
writable: true,
|
|
519
|
+
value: {
|
|
520
|
+
channelToNote: new Map(),
|
|
521
|
+
noteToChannel: new Map(),
|
|
522
|
+
}
|
|
523
|
+
});
|
|
461
524
|
this.audioContext = audioContext;
|
|
462
525
|
this.masterVolume = new GainNode(audioContext);
|
|
463
526
|
this.scheduler = new GainNode(audioContext, { gain: 0 });
|
|
@@ -535,13 +598,13 @@ export class Midy extends EventTarget {
|
|
|
535
598
|
this.totalTime = this.calcTotalTime();
|
|
536
599
|
}
|
|
537
600
|
cacheVoiceIds() {
|
|
538
|
-
const timeline = this
|
|
601
|
+
const { channels, timeline, voiceCounter } = this;
|
|
539
602
|
for (let i = 0; i < timeline.length; i++) {
|
|
540
603
|
const event = timeline[i];
|
|
541
604
|
switch (event.type) {
|
|
542
605
|
case "noteOn": {
|
|
543
|
-
const audioBufferId = this.getVoiceId(
|
|
544
|
-
|
|
606
|
+
const audioBufferId = this.getVoiceId(channels[event.channel], event.noteNumber, event.velocity);
|
|
607
|
+
voiceCounter.set(audioBufferId, (voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
545
608
|
break;
|
|
546
609
|
}
|
|
547
610
|
case "controller":
|
|
@@ -556,9 +619,9 @@ export class Midy extends EventTarget {
|
|
|
556
619
|
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
557
620
|
}
|
|
558
621
|
}
|
|
559
|
-
for (const [audioBufferId, count] of
|
|
622
|
+
for (const [audioBufferId, count] of voiceCounter) {
|
|
560
623
|
if (count === 1)
|
|
561
|
-
|
|
624
|
+
voiceCounter.delete(audioBufferId);
|
|
562
625
|
}
|
|
563
626
|
this.GM2SystemOn();
|
|
564
627
|
}
|
|
@@ -567,8 +630,12 @@ export class Midy extends EventTarget {
|
|
|
567
630
|
const bankTable = this.soundFontTable[programNumber];
|
|
568
631
|
if (!bankTable)
|
|
569
632
|
return;
|
|
570
|
-
|
|
571
|
-
|
|
633
|
+
let bank = channel.isDrum ? 128 : channel.bankLSB;
|
|
634
|
+
if (bankTable[bank] === undefined) {
|
|
635
|
+
if (channel.isDrum)
|
|
636
|
+
return;
|
|
637
|
+
bank = 0;
|
|
638
|
+
}
|
|
572
639
|
const soundFontIndex = bankTable[bank];
|
|
573
640
|
if (soundFontIndex === undefined)
|
|
574
641
|
return;
|
|
@@ -594,8 +661,8 @@ export class Midy extends EventTarget {
|
|
|
594
661
|
resetChannelTable(channel) {
|
|
595
662
|
channel.controlTable.fill(-1);
|
|
596
663
|
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
597
|
-
channel.channelPressureTable.
|
|
598
|
-
channel.polyphonicKeyPressureTable.
|
|
664
|
+
channel.channelPressureTable.set(defaultPressureValues);
|
|
665
|
+
channel.polyphonicKeyPressureTable.set(defaultPressureValues);
|
|
599
666
|
channel.keyBasedTable.fill(-1);
|
|
600
667
|
}
|
|
601
668
|
createChannels(audioContext) {
|
|
@@ -611,8 +678,8 @@ export class Midy extends EventTarget {
|
|
|
611
678
|
sostenutoNotes: [],
|
|
612
679
|
controlTable: this.initControlTable(),
|
|
613
680
|
scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
|
|
614
|
-
channelPressureTable: new Int8Array(
|
|
615
|
-
polyphonicKeyPressureTable: new Int8Array(
|
|
681
|
+
channelPressureTable: new Int8Array(defaultPressureValues),
|
|
682
|
+
polyphonicKeyPressureTable: new Int8Array(defaultPressureValues),
|
|
616
683
|
keyBasedTable: new Int8Array(128 * 128).fill(-1),
|
|
617
684
|
keyBasedGainLs: new Array(128),
|
|
618
685
|
keyBasedGainRs: new Array(128),
|
|
@@ -648,11 +715,13 @@ export class Midy extends EventTarget {
|
|
|
648
715
|
const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
|
|
649
716
|
const schedulingOffset = this.startDelay - timeOffset;
|
|
650
717
|
const timeline = this.timeline;
|
|
718
|
+
const inverseTempo = 1 / this.tempo;
|
|
651
719
|
while (queueIndex < timeline.length) {
|
|
652
720
|
const event = timeline[queueIndex];
|
|
653
|
-
|
|
721
|
+
const t = event.startTime * inverseTempo;
|
|
722
|
+
if (lookAheadCheckTime < t)
|
|
654
723
|
break;
|
|
655
|
-
const startTime =
|
|
724
|
+
const startTime = t + schedulingOffset;
|
|
656
725
|
switch (event.type) {
|
|
657
726
|
case "noteOn":
|
|
658
727
|
this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
@@ -684,8 +753,10 @@ export class Midy extends EventTarget {
|
|
|
684
753
|
return queueIndex;
|
|
685
754
|
}
|
|
686
755
|
getQueueIndex(second) {
|
|
687
|
-
|
|
688
|
-
|
|
756
|
+
const timeline = this.timeline;
|
|
757
|
+
const inverseTempo = 1 / this.tempo;
|
|
758
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
759
|
+
if (second <= timeline[i].startTime * inverseTempo) {
|
|
689
760
|
return i;
|
|
690
761
|
}
|
|
691
762
|
}
|
|
@@ -696,40 +767,44 @@ export class Midy extends EventTarget {
|
|
|
696
767
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
697
768
|
this.voiceCache.clear();
|
|
698
769
|
this.realtimeVoiceCache.clear();
|
|
699
|
-
|
|
700
|
-
|
|
770
|
+
const channels = this.channels;
|
|
771
|
+
for (let i = 0; i < channels.length; i++) {
|
|
772
|
+
channels[i].scheduledNotes = [];
|
|
701
773
|
this.resetChannelStates(i);
|
|
702
774
|
}
|
|
703
775
|
}
|
|
704
776
|
updateStates(queueIndex, nextQueueIndex) {
|
|
777
|
+
const { timeline, resumeTime } = this;
|
|
778
|
+
const inverseTempo = 1 / this.tempo;
|
|
705
779
|
const now = this.audioContext.currentTime;
|
|
706
780
|
if (nextQueueIndex < queueIndex)
|
|
707
781
|
queueIndex = 0;
|
|
708
782
|
for (let i = queueIndex; i < nextQueueIndex; i++) {
|
|
709
|
-
const event =
|
|
783
|
+
const event = timeline[i];
|
|
710
784
|
switch (event.type) {
|
|
711
785
|
case "controller":
|
|
712
|
-
this.setControlChange(event.channel, event.controllerType, event.value, now -
|
|
786
|
+
this.setControlChange(event.channel, event.controllerType, event.value, now - resumeTime + event.startTime * inverseTempo);
|
|
713
787
|
break;
|
|
714
788
|
case "programChange":
|
|
715
|
-
this.setProgramChange(event.channel, event.programNumber, now -
|
|
789
|
+
this.setProgramChange(event.channel, event.programNumber, now - resumeTime + event.startTime * inverseTempo);
|
|
716
790
|
break;
|
|
717
791
|
case "pitchBend":
|
|
718
|
-
this.setPitchBend(event.channel, event.value + 8192, now -
|
|
792
|
+
this.setPitchBend(event.channel, event.value + 8192, now - resumeTime + event.startTime * inverseTempo);
|
|
719
793
|
break;
|
|
720
794
|
case "sysEx":
|
|
721
|
-
this.handleSysEx(event.data, now -
|
|
795
|
+
this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
|
|
722
796
|
}
|
|
723
797
|
}
|
|
724
798
|
}
|
|
725
799
|
async playNotes() {
|
|
726
|
-
|
|
727
|
-
|
|
800
|
+
const audioContext = this.audioContext;
|
|
801
|
+
if (audioContext.state === "suspended") {
|
|
802
|
+
await audioContext.resume();
|
|
728
803
|
}
|
|
729
804
|
const paused = this.isPaused;
|
|
730
805
|
this.isPlaying = true;
|
|
731
806
|
this.isPaused = false;
|
|
732
|
-
this.startTime =
|
|
807
|
+
this.startTime = audioContext.currentTime;
|
|
733
808
|
if (paused) {
|
|
734
809
|
this.dispatchEvent(new Event("resumed"));
|
|
735
810
|
}
|
|
@@ -740,20 +815,20 @@ export class Midy extends EventTarget {
|
|
|
740
815
|
let exitReason;
|
|
741
816
|
this.notePromises = [];
|
|
742
817
|
while (true) {
|
|
743
|
-
const now =
|
|
818
|
+
const now = audioContext.currentTime;
|
|
744
819
|
if (0 < this.lastActiveSensing &&
|
|
745
820
|
this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
|
|
746
821
|
await this.stopNotes(0, true, now);
|
|
747
|
-
await
|
|
822
|
+
await audioContext.suspend();
|
|
748
823
|
exitReason = "aborted";
|
|
749
824
|
break;
|
|
750
825
|
}
|
|
751
|
-
if (this.
|
|
826
|
+
if (this.totalTime < this.currentTime() ||
|
|
827
|
+
this.timeline.length <= queueIndex) {
|
|
752
828
|
await this.stopNotes(0, true, now);
|
|
753
829
|
if (this.loop) {
|
|
754
|
-
this.notePromises = [];
|
|
755
830
|
this.resetAllStates();
|
|
756
|
-
this.startTime =
|
|
831
|
+
this.startTime = audioContext.currentTime;
|
|
757
832
|
this.resumeTime = this.loopStart;
|
|
758
833
|
if (0 < this.loopStart) {
|
|
759
834
|
const nextQueueIndex = this.getQueueIndex(this.resumeTime);
|
|
@@ -767,29 +842,28 @@ export class Midy extends EventTarget {
|
|
|
767
842
|
continue;
|
|
768
843
|
}
|
|
769
844
|
else {
|
|
770
|
-
await
|
|
845
|
+
await audioContext.suspend();
|
|
771
846
|
exitReason = "ended";
|
|
772
847
|
break;
|
|
773
848
|
}
|
|
774
849
|
}
|
|
775
850
|
if (this.isPausing) {
|
|
776
851
|
await this.stopNotes(0, true, now);
|
|
777
|
-
await
|
|
778
|
-
this.notePromises = [];
|
|
852
|
+
await audioContext.suspend();
|
|
779
853
|
this.isPausing = false;
|
|
780
854
|
exitReason = "paused";
|
|
781
855
|
break;
|
|
782
856
|
}
|
|
783
857
|
else if (this.isStopping) {
|
|
784
858
|
await this.stopNotes(0, true, now);
|
|
785
|
-
await
|
|
859
|
+
await audioContext.suspend();
|
|
786
860
|
this.isStopping = false;
|
|
787
861
|
exitReason = "stopped";
|
|
788
862
|
break;
|
|
789
863
|
}
|
|
790
864
|
else if (this.isSeeking) {
|
|
791
865
|
this.stopNotes(0, true, now);
|
|
792
|
-
this.startTime =
|
|
866
|
+
this.startTime = audioContext.currentTime;
|
|
793
867
|
const nextQueueIndex = this.getQueueIndex(this.resumeTime);
|
|
794
868
|
this.updateStates(queueIndex, nextQueueIndex);
|
|
795
869
|
queueIndex = nextQueueIndex;
|
|
@@ -802,7 +876,6 @@ export class Midy extends EventTarget {
|
|
|
802
876
|
await this.scheduleTask(() => { }, waitTime);
|
|
803
877
|
}
|
|
804
878
|
if (exitReason !== "paused") {
|
|
805
|
-
this.notePromises = [];
|
|
806
879
|
this.resetAllStates();
|
|
807
880
|
this.lastActiveSensing = 0;
|
|
808
881
|
}
|
|
@@ -931,11 +1004,13 @@ export class Midy extends EventTarget {
|
|
|
931
1004
|
return Promise.all(promises);
|
|
932
1005
|
}
|
|
933
1006
|
stopNotes(velocity, force, scheduleTime) {
|
|
934
|
-
const
|
|
935
|
-
for (let i = 0; i <
|
|
936
|
-
|
|
1007
|
+
const channels = this.channels;
|
|
1008
|
+
for (let i = 0; i < channels.length; i++) {
|
|
1009
|
+
this.stopChannelNotes(i, velocity, force, scheduleTime);
|
|
937
1010
|
}
|
|
938
|
-
|
|
1011
|
+
const stopPromise = Promise.all(this.notePromises);
|
|
1012
|
+
this.notePromises = [];
|
|
1013
|
+
return stopPromise;
|
|
939
1014
|
}
|
|
940
1015
|
async start() {
|
|
941
1016
|
if (this.isPlaying || this.isPaused)
|
|
@@ -972,12 +1047,25 @@ export class Midy extends EventTarget {
|
|
|
972
1047
|
this.isSeeking = true;
|
|
973
1048
|
}
|
|
974
1049
|
}
|
|
1050
|
+
tempoChange(tempo) {
|
|
1051
|
+
const timeScale = this.tempo / tempo;
|
|
1052
|
+
this.resumeTime = this.resumeTime * timeScale;
|
|
1053
|
+
this.tempo = tempo;
|
|
1054
|
+
this.totalTime = this.calcTotalTime();
|
|
1055
|
+
this.seekTo(this.currentTime() * timeScale);
|
|
1056
|
+
}
|
|
975
1057
|
calcTotalTime() {
|
|
1058
|
+
const totalTimeEventTypes = this.totalTimeEventTypes;
|
|
1059
|
+
const timeline = this.timeline;
|
|
1060
|
+
const inverseTempo = 1 / this.tempo;
|
|
976
1061
|
let totalTime = 0;
|
|
977
|
-
for (let i = 0; i <
|
|
978
|
-
const event =
|
|
979
|
-
if (
|
|
980
|
-
|
|
1062
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
1063
|
+
const event = timeline[i];
|
|
1064
|
+
if (!totalTimeEventTypes.has(event.type))
|
|
1065
|
+
continue;
|
|
1066
|
+
const t = event.startTime * inverseTempo;
|
|
1067
|
+
if (totalTime < t)
|
|
1068
|
+
totalTime = t;
|
|
981
1069
|
}
|
|
982
1070
|
return totalTime + this.startDelay;
|
|
983
1071
|
}
|
|
@@ -1159,9 +1247,6 @@ export class Midy extends EventTarget {
|
|
|
1159
1247
|
feedbackGains,
|
|
1160
1248
|
};
|
|
1161
1249
|
}
|
|
1162
|
-
cbToRatio(cb) {
|
|
1163
|
-
return Math.pow(10, cb / 200);
|
|
1164
|
-
}
|
|
1165
1250
|
rateToCent(rate) {
|
|
1166
1251
|
return 1200 * Math.log2(rate);
|
|
1167
1252
|
}
|
|
@@ -1191,39 +1276,24 @@ export class Midy extends EventTarget {
|
|
|
1191
1276
|
return tuning + pitch;
|
|
1192
1277
|
}
|
|
1193
1278
|
}
|
|
1194
|
-
calcNoteDetune(channel, note) {
|
|
1195
|
-
return channel.scaleOctaveTuningTable[note.noteNumber % 12];
|
|
1196
|
-
}
|
|
1197
1279
|
updateChannelDetune(channel, scheduleTime) {
|
|
1198
1280
|
this.processScheduledNotes(channel, (note) => {
|
|
1199
|
-
this.
|
|
1281
|
+
if (this.isPortamento(channel, note)) {
|
|
1282
|
+
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
this.setDetune(channel, note, scheduleTime);
|
|
1286
|
+
}
|
|
1200
1287
|
});
|
|
1201
1288
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1289
|
+
calcScaleOctaveTuning(channel, note) {
|
|
1290
|
+
return channel.scaleOctaveTuningTable[note.noteNumber % 12];
|
|
1291
|
+
}
|
|
1292
|
+
calcNoteDetune(channel, note) {
|
|
1293
|
+
const noteDetune = note.voiceParams.detune +
|
|
1294
|
+
this.calcScaleOctaveTuning(channel, note);
|
|
1204
1295
|
const pitchControl = this.getPitchControl(channel, note);
|
|
1205
|
-
|
|
1206
|
-
if (channel.portamentoControl) {
|
|
1207
|
-
const state = channel.state;
|
|
1208
|
-
const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
|
|
1209
|
-
note.portamentoNoteNumber = portamentoNoteNumber;
|
|
1210
|
-
channel.portamentoControl = false;
|
|
1211
|
-
state.portamentoNoteNumber = 0;
|
|
1212
|
-
}
|
|
1213
|
-
if (this.isPortamento(channel, note)) {
|
|
1214
|
-
const startTime = note.startTime;
|
|
1215
|
-
const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
|
|
1216
|
-
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1217
|
-
note.bufferSource.detune
|
|
1218
|
-
.cancelScheduledValues(scheduleTime)
|
|
1219
|
-
.setValueAtTime(detune - deltaCent, scheduleTime)
|
|
1220
|
-
.linearRampToValueAtTime(detune, portamentoTime);
|
|
1221
|
-
}
|
|
1222
|
-
else {
|
|
1223
|
-
note.bufferSource.detune
|
|
1224
|
-
.cancelScheduledValues(scheduleTime)
|
|
1225
|
-
.setValueAtTime(detune, scheduleTime);
|
|
1226
|
-
}
|
|
1296
|
+
return channel.detune + noteDetune + pitchControl;
|
|
1227
1297
|
}
|
|
1228
1298
|
getPortamentoTime(channel, note) {
|
|
1229
1299
|
const { portamentoTimeMSB, portamentoTimeLSB } = channel.state;
|
|
@@ -1290,20 +1360,17 @@ export class Midy extends EventTarget {
|
|
|
1290
1360
|
}
|
|
1291
1361
|
setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
|
|
1292
1362
|
const { voiceParams, startTime } = note;
|
|
1293
|
-
const attackVolume =
|
|
1363
|
+
const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
|
|
1294
1364
|
(1 + this.getAmplitudeControl(channel, note));
|
|
1295
1365
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1296
|
-
const
|
|
1297
|
-
const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
|
|
1298
|
-
const volAttack = volDelay + voiceParams.volAttack * attackTime;
|
|
1299
|
-
const volHold = volAttack + voiceParams.volHold;
|
|
1366
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1300
1367
|
note.volumeEnvelopeNode.gain
|
|
1301
1368
|
.cancelScheduledValues(scheduleTime)
|
|
1302
|
-
.
|
|
1369
|
+
.exponentialRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1303
1370
|
}
|
|
1304
1371
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
1305
1372
|
const { voiceParams, startTime } = note;
|
|
1306
|
-
const attackVolume =
|
|
1373
|
+
const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
|
|
1307
1374
|
(1 + this.getAmplitudeControl(channel, note));
|
|
1308
1375
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1309
1376
|
const volDelay = startTime + voiceParams.volDelay;
|
|
@@ -1311,42 +1378,69 @@ export class Midy extends EventTarget {
|
|
|
1311
1378
|
const volAttack = volDelay + voiceParams.volAttack * attackTime;
|
|
1312
1379
|
const volHold = volAttack + voiceParams.volHold;
|
|
1313
1380
|
const decayTime = this.getRelativeKeyBasedValue(channel, note, 75) * 2;
|
|
1314
|
-
const
|
|
1381
|
+
const decayDuration = voiceParams.volDecay * decayTime;
|
|
1315
1382
|
note.volumeEnvelopeNode.gain
|
|
1316
1383
|
.cancelScheduledValues(scheduleTime)
|
|
1317
1384
|
.setValueAtTime(0, startTime)
|
|
1318
|
-
.setValueAtTime(1e-6, volDelay)
|
|
1385
|
+
.setValueAtTime(1e-6, volDelay)
|
|
1319
1386
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
1320
1387
|
.setValueAtTime(attackVolume, volHold)
|
|
1321
|
-
.
|
|
1388
|
+
.setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
|
|
1389
|
+
}
|
|
1390
|
+
setPortamentoDetune(channel, note, scheduleTime) {
|
|
1391
|
+
if (channel.portamentoControl) {
|
|
1392
|
+
const state = channel.state;
|
|
1393
|
+
const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
|
|
1394
|
+
note.portamentoNoteNumber = portamentoNoteNumber;
|
|
1395
|
+
channel.portamentoControl = false;
|
|
1396
|
+
state.portamentoNoteNumber = 0;
|
|
1397
|
+
}
|
|
1398
|
+
const detune = this.calcNoteDetune(channel, note);
|
|
1399
|
+
const startTime = note.startTime;
|
|
1400
|
+
const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
|
|
1401
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1402
|
+
note.bufferSource.detune
|
|
1403
|
+
.cancelScheduledValues(scheduleTime)
|
|
1404
|
+
.setValueAtTime(detune - deltaCent, scheduleTime)
|
|
1405
|
+
.linearRampToValueAtTime(detune, portamentoTime);
|
|
1406
|
+
}
|
|
1407
|
+
setDetune(channel, note, scheduleTime) {
|
|
1408
|
+
const detune = this.calcNoteDetune(channel, note);
|
|
1409
|
+
note.bufferSource.detune
|
|
1410
|
+
.cancelScheduledValues(scheduleTime)
|
|
1411
|
+
.setValueAtTime(detune, scheduleTime);
|
|
1412
|
+
const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
|
|
1413
|
+
note.bufferSource.detune
|
|
1414
|
+
.cancelAndHoldAtTime(scheduleTime)
|
|
1415
|
+
.setTargetAtTime(detune, scheduleTime, timeConstant);
|
|
1322
1416
|
}
|
|
1323
|
-
setPortamentoPitchEnvelope(note, scheduleTime) {
|
|
1417
|
+
setPortamentoPitchEnvelope(channel, note, scheduleTime) {
|
|
1324
1418
|
const baseRate = note.voiceParams.playbackRate;
|
|
1419
|
+
const portamentoTime = note.startTime +
|
|
1420
|
+
this.getPortamentoTime(channel, note);
|
|
1325
1421
|
note.bufferSource.playbackRate
|
|
1326
1422
|
.cancelScheduledValues(scheduleTime)
|
|
1327
|
-
.
|
|
1423
|
+
.exponentialRampToValueAtTime(baseRate, portamentoTime);
|
|
1328
1424
|
}
|
|
1329
1425
|
setPitchEnvelope(note, scheduleTime) {
|
|
1330
|
-
const { voiceParams } = note;
|
|
1426
|
+
const { bufferSource, voiceParams } = note;
|
|
1331
1427
|
const baseRate = voiceParams.playbackRate;
|
|
1332
|
-
|
|
1428
|
+
bufferSource.playbackRate
|
|
1333
1429
|
.cancelScheduledValues(scheduleTime)
|
|
1334
1430
|
.setValueAtTime(baseRate, scheduleTime);
|
|
1335
1431
|
const modEnvToPitch = voiceParams.modEnvToPitch;
|
|
1336
1432
|
if (modEnvToPitch === 0)
|
|
1337
1433
|
return;
|
|
1338
|
-
const
|
|
1339
|
-
const peekPitch = basePitch + modEnvToPitch;
|
|
1340
|
-
const peekRate = this.centToRate(peekPitch);
|
|
1434
|
+
const peekRate = baseRate * this.centToRate(modEnvToPitch);
|
|
1341
1435
|
const modDelay = note.startTime + voiceParams.modDelay;
|
|
1342
1436
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
1343
1437
|
const modHold = modAttack + voiceParams.modHold;
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1438
|
+
const decayDuration = voiceParams.modDecay;
|
|
1439
|
+
bufferSource.playbackRate
|
|
1346
1440
|
.setValueAtTime(baseRate, modDelay)
|
|
1347
1441
|
.exponentialRampToValueAtTime(peekRate, modAttack)
|
|
1348
1442
|
.setValueAtTime(peekRate, modHold)
|
|
1349
|
-
.
|
|
1443
|
+
.setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
|
|
1350
1444
|
}
|
|
1351
1445
|
clampCutoffFrequency(frequency) {
|
|
1352
1446
|
const minFrequency = 20; // min Hz of initialFilterFc
|
|
@@ -1356,60 +1450,67 @@ export class Midy extends EventTarget {
|
|
|
1356
1450
|
setPortamentoFilterEnvelope(channel, note, scheduleTime) {
|
|
1357
1451
|
const { voiceParams, startTime } = note;
|
|
1358
1452
|
const softPedalFactor = this.getSoftPedalFactor(channel, note);
|
|
1453
|
+
const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
|
|
1454
|
+
const scale = softPedalFactor * brightness;
|
|
1359
1455
|
const baseCent = voiceParams.initialFilterFc +
|
|
1360
1456
|
this.getFilterCutoffControl(channel, note);
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
const
|
|
1364
|
-
const sustainFreq =
|
|
1365
|
-
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
1457
|
+
const sustainCent = baseCent +
|
|
1458
|
+
voiceParams.modEnvToFilterFc * (1 - voiceParams.modSustain);
|
|
1459
|
+
const baseFreq = this.centToHz(baseCent) * scale;
|
|
1460
|
+
const sustainFreq = this.centToHz(sustainCent) * scale;
|
|
1366
1461
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
1367
1462
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
1368
1463
|
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1369
1464
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1465
|
+
note.adjustedBaseFreq = adjustedSustainFreq;
|
|
1370
1466
|
note.filterNode.frequency
|
|
1371
1467
|
.cancelScheduledValues(scheduleTime)
|
|
1372
1468
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1373
1469
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
1374
|
-
.
|
|
1470
|
+
.exponentialRampToValueAtTime(adjustedSustainFreq, portamentoTime);
|
|
1375
1471
|
}
|
|
1376
1472
|
setFilterEnvelope(channel, note, scheduleTime) {
|
|
1377
1473
|
const { voiceParams, startTime } = note;
|
|
1378
|
-
const
|
|
1474
|
+
const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
|
|
1379
1475
|
const baseCent = voiceParams.initialFilterFc +
|
|
1380
1476
|
this.getFilterCutoffControl(channel, note);
|
|
1477
|
+
const peekCent = baseCent + modEnvToFilterFc;
|
|
1478
|
+
const sustainCent = baseCent +
|
|
1479
|
+
modEnvToFilterFc * (1 - voiceParams.modSustain);
|
|
1480
|
+
const softPedalFactor = this.getSoftPedalFactor(channel, note);
|
|
1381
1481
|
const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
|
|
1382
|
-
const
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
const sustainFreq =
|
|
1386
|
-
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
1482
|
+
const scale = softPedalFactor * brightness;
|
|
1483
|
+
const baseFreq = this.centToHz(baseCent) * scale;
|
|
1484
|
+
const peekFreq = this.centToHz(peekCent) * scale;
|
|
1485
|
+
const sustainFreq = this.centToHz(sustainCent) * scale;
|
|
1387
1486
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
1388
1487
|
const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
|
|
1389
1488
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
1390
1489
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1391
1490
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
1392
1491
|
const modHold = modAttack + voiceParams.modHold;
|
|
1393
|
-
const
|
|
1492
|
+
const decayDuration = modHold + voiceParams.modDecay;
|
|
1493
|
+
note.adjustedBaseFreq = adjustedBaseFreq;
|
|
1394
1494
|
note.filterNode.frequency
|
|
1395
1495
|
.cancelScheduledValues(scheduleTime)
|
|
1396
1496
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1397
1497
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
1398
1498
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
1399
1499
|
.setValueAtTime(adjustedPeekFreq, modHold)
|
|
1400
|
-
.
|
|
1500
|
+
.setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
|
|
1401
1501
|
}
|
|
1402
1502
|
startModulation(channel, note, scheduleTime) {
|
|
1503
|
+
const audioContext = this.audioContext;
|
|
1403
1504
|
const { voiceParams } = note;
|
|
1404
|
-
note.modulationLFO = new OscillatorNode(
|
|
1505
|
+
note.modulationLFO = new OscillatorNode(audioContext, {
|
|
1405
1506
|
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
1406
1507
|
});
|
|
1407
|
-
note.filterDepth = new GainNode(
|
|
1508
|
+
note.filterDepth = new GainNode(audioContext, {
|
|
1408
1509
|
gain: voiceParams.modLfoToFilterFc,
|
|
1409
1510
|
});
|
|
1410
|
-
note.modulationDepth = new GainNode(
|
|
1511
|
+
note.modulationDepth = new GainNode(audioContext);
|
|
1411
1512
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
1412
|
-
note.volumeDepth = new GainNode(
|
|
1513
|
+
note.volumeDepth = new GainNode(audioContext);
|
|
1413
1514
|
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
1414
1515
|
note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
|
|
1415
1516
|
note.modulationLFO.connect(note.filterDepth);
|
|
@@ -1461,7 +1562,8 @@ export class Midy extends EventTarget {
|
|
|
1461
1562
|
}
|
|
1462
1563
|
}
|
|
1463
1564
|
async setNoteAudioNode(channel, note, realtime) {
|
|
1464
|
-
const
|
|
1565
|
+
const audioContext = this.audioContext;
|
|
1566
|
+
const now = audioContext.currentTime;
|
|
1465
1567
|
const { noteNumber, velocity, startTime } = note;
|
|
1466
1568
|
const state = channel.state;
|
|
1467
1569
|
const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
|
|
@@ -1469,9 +1571,9 @@ export class Midy extends EventTarget {
|
|
|
1469
1571
|
note.voiceParams = voiceParams;
|
|
1470
1572
|
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
1471
1573
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1472
|
-
note.volumeEnvelopeNode = new GainNode(
|
|
1574
|
+
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
1473
1575
|
const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
|
|
1474
|
-
note.filterNode = new BiquadFilterNode(
|
|
1576
|
+
note.filterNode = new BiquadFilterNode(audioContext, {
|
|
1475
1577
|
type: "lowpass",
|
|
1476
1578
|
Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
|
|
1477
1579
|
});
|
|
@@ -1482,14 +1584,15 @@ export class Midy extends EventTarget {
|
|
|
1482
1584
|
if (!channel.isDrum && this.isPortamento(channel, note)) {
|
|
1483
1585
|
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
1484
1586
|
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
1485
|
-
this.setPortamentoPitchEnvelope(note, now);
|
|
1587
|
+
this.setPortamentoPitchEnvelope(channel, note, now);
|
|
1588
|
+
this.setPortamentoDetune(channel, note, now);
|
|
1486
1589
|
}
|
|
1487
1590
|
else {
|
|
1488
1591
|
this.setVolumeEnvelope(channel, note, now);
|
|
1489
1592
|
this.setFilterEnvelope(channel, note, now);
|
|
1490
1593
|
this.setPitchEnvelope(note, now);
|
|
1594
|
+
this.setDetune(channel, note, now);
|
|
1491
1595
|
}
|
|
1492
|
-
this.updateDetune(channel, note, now);
|
|
1493
1596
|
if (0 < state.vibratoDepth) {
|
|
1494
1597
|
this.startVibrato(channel, note, now);
|
|
1495
1598
|
}
|
|
@@ -1572,6 +1675,16 @@ export class Midy extends EventTarget {
|
|
|
1572
1675
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1573
1676
|
}
|
|
1574
1677
|
async noteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
1678
|
+
if (this.mpeEnabled) {
|
|
1679
|
+
const note = await this.startNote(channelNumber, noteNumber, velocity, startTime);
|
|
1680
|
+
this.mpeState.channelToNote.set(channelNumber, note.index);
|
|
1681
|
+
this.mpeState.noteToChannel.set(note.index, channelNumber);
|
|
1682
|
+
}
|
|
1683
|
+
else {
|
|
1684
|
+
await this.startNote(channelNumber, noteNumber, velocity, startTime);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
async startNote(channelNumber, noteNumber, velocity, startTime) {
|
|
1575
1688
|
const channel = this.channels[channelNumber];
|
|
1576
1689
|
const realtime = startTime === undefined;
|
|
1577
1690
|
if (realtime)
|
|
@@ -1582,10 +1695,12 @@ export class Midy extends EventTarget {
|
|
|
1582
1695
|
scheduledNotes.push(note);
|
|
1583
1696
|
const programNumber = channel.programNumber;
|
|
1584
1697
|
const bankTable = this.soundFontTable[programNumber];
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1698
|
+
let bank = channel.isDrum ? 128 : channel.bankLSB;
|
|
1699
|
+
if (bankTable[bank] === undefined) {
|
|
1700
|
+
if (channel.isDrum)
|
|
1701
|
+
return;
|
|
1702
|
+
bank = 0;
|
|
1703
|
+
}
|
|
1589
1704
|
const soundFontIndex = bankTable[bank];
|
|
1590
1705
|
if (soundFontIndex === undefined)
|
|
1591
1706
|
return;
|
|
@@ -1596,6 +1711,7 @@ export class Midy extends EventTarget {
|
|
|
1596
1711
|
await this.setNoteAudioNode(channel, note, realtime);
|
|
1597
1712
|
this.setNoteRouting(channelNumber, note, startTime);
|
|
1598
1713
|
note.resolveReady();
|
|
1714
|
+
return note;
|
|
1599
1715
|
}
|
|
1600
1716
|
disconnectNote(note) {
|
|
1601
1717
|
note.bufferSource.disconnect();
|
|
@@ -1620,27 +1736,45 @@ export class Midy extends EventTarget {
|
|
|
1620
1736
|
releaseNote(channel, note, endTime) {
|
|
1621
1737
|
endTime ??= this.audioContext.currentTime;
|
|
1622
1738
|
const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
|
|
1623
|
-
const
|
|
1624
|
-
const
|
|
1625
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1739
|
+
const volDuration = note.voiceParams.volRelease * releaseTime;
|
|
1740
|
+
const volRelease = endTime + volDuration;
|
|
1626
1741
|
note.filterNode.frequency
|
|
1627
1742
|
.cancelScheduledValues(endTime)
|
|
1628
|
-
.
|
|
1743
|
+
.setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
|
|
1629
1744
|
note.volumeEnvelopeNode.gain
|
|
1630
1745
|
.cancelScheduledValues(endTime)
|
|
1631
|
-
.
|
|
1746
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
1632
1747
|
return new Promise((resolve) => {
|
|
1633
1748
|
this.scheduleTask(() => {
|
|
1634
1749
|
const bufferSource = note.bufferSource;
|
|
1635
1750
|
bufferSource.loop = false;
|
|
1636
|
-
bufferSource.stop(
|
|
1751
|
+
bufferSource.stop(volRelease);
|
|
1637
1752
|
this.disconnectNote(note);
|
|
1638
1753
|
channel.scheduledNotes[note.index] = undefined;
|
|
1639
1754
|
resolve();
|
|
1640
|
-
},
|
|
1755
|
+
}, volRelease);
|
|
1641
1756
|
});
|
|
1642
1757
|
}
|
|
1643
|
-
noteOff(channelNumber, noteNumber,
|
|
1758
|
+
noteOff(channelNumber, noteNumber, velocity, endTime, force) {
|
|
1759
|
+
if (this.mpeEnabled) {
|
|
1760
|
+
const noteIndex = this.mpeState.channelToNote.get(channelNumber);
|
|
1761
|
+
if (noteIndex === undefined)
|
|
1762
|
+
return;
|
|
1763
|
+
const channel = this.channels[channelNumber];
|
|
1764
|
+
const note = channel.scheduledNotes[noteIndex];
|
|
1765
|
+
note.ending = true;
|
|
1766
|
+
const promise = note.ready.then(() => {
|
|
1767
|
+
return this.releaseNote(channel, note, endTime);
|
|
1768
|
+
});
|
|
1769
|
+
this.mpeState.channelToNote.delete(channelNumber);
|
|
1770
|
+
this.mpeState.noteToChannel.delete(noteIndex);
|
|
1771
|
+
return promise;
|
|
1772
|
+
}
|
|
1773
|
+
else {
|
|
1774
|
+
return this.stopNote(channelNumber, noteNumber, velocity, endTime, force);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
stopNote(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
1644
1778
|
const channel = this.channels[channelNumber];
|
|
1645
1779
|
const state = channel.state;
|
|
1646
1780
|
if (!force) {
|
|
@@ -1757,6 +1891,10 @@ export class Midy extends EventTarget {
|
|
|
1757
1891
|
}
|
|
1758
1892
|
setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
|
|
1759
1893
|
const channel = this.channels[channelNumber];
|
|
1894
|
+
if (channel.isMPEMember)
|
|
1895
|
+
return;
|
|
1896
|
+
if (!(0 <= scheduleTime))
|
|
1897
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1760
1898
|
const table = channel.polyphonicKeyPressureTable;
|
|
1761
1899
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1762
1900
|
if (note.noteNumber === noteNumber) {
|
|
@@ -1782,6 +1920,8 @@ export class Midy extends EventTarget {
|
|
|
1782
1920
|
}
|
|
1783
1921
|
}
|
|
1784
1922
|
setChannelPressure(channelNumber, value, scheduleTime) {
|
|
1923
|
+
if (!(0 <= scheduleTime))
|
|
1924
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1785
1925
|
const channel = this.channels[channelNumber];
|
|
1786
1926
|
if (channel.isDrum)
|
|
1787
1927
|
return;
|
|
@@ -1857,7 +1997,7 @@ export class Midy extends EventTarget {
|
|
|
1857
1997
|
}
|
|
1858
1998
|
setModLfoToVolume(channel, note, scheduleTime) {
|
|
1859
1999
|
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
1860
|
-
const baseDepth =
|
|
2000
|
+
const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
1861
2001
|
const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
|
|
1862
2002
|
(1 + this.getLFOAmplitudeDepth(channel, note));
|
|
1863
2003
|
note.volumeDepth.gain
|
|
@@ -1937,13 +2077,6 @@ export class Midy extends EventTarget {
|
|
|
1937
2077
|
.cancelScheduledValues(scheduleTime)
|
|
1938
2078
|
.setValueAtTime(freqModLFO, scheduleTime);
|
|
1939
2079
|
}
|
|
1940
|
-
setFreqVibLFO(channel, note, scheduleTime) {
|
|
1941
|
-
const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
|
|
1942
|
-
const freqVibLFO = note.voiceParams.freqVibLFO;
|
|
1943
|
-
note.vibratoLFO.frequency
|
|
1944
|
-
.cancelScheduledValues(scheduleTime)
|
|
1945
|
-
.setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
|
|
1946
|
-
}
|
|
1947
2080
|
setDelayVibLFO(channel, note) {
|
|
1948
2081
|
const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
|
|
1949
2082
|
const value = note.voiceParams.delayVibLFO;
|
|
@@ -1953,6 +2086,13 @@ export class Midy extends EventTarget {
|
|
|
1953
2086
|
}
|
|
1954
2087
|
catch { /* empty */ }
|
|
1955
2088
|
}
|
|
2089
|
+
setFreqVibLFO(channel, note, scheduleTime) {
|
|
2090
|
+
const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
|
|
2091
|
+
const freqVibLFO = note.voiceParams.freqVibLFO;
|
|
2092
|
+
note.vibratoLFO.frequency
|
|
2093
|
+
.cancelScheduledValues(scheduleTime)
|
|
2094
|
+
.setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
|
|
2095
|
+
}
|
|
1956
2096
|
createVoiceParamsHandlers() {
|
|
1957
2097
|
return {
|
|
1958
2098
|
modLfoToPitch: (channel, note, scheduleTime) => {
|
|
@@ -2006,6 +2146,14 @@ export class Midy extends EventTarget {
|
|
|
2006
2146
|
this.setFreqVibLFO(channel, note, scheduleTime);
|
|
2007
2147
|
}
|
|
2008
2148
|
},
|
|
2149
|
+
detune: (channel, note, scheduleTime) => {
|
|
2150
|
+
if (this.isPortamento(channel, note)) {
|
|
2151
|
+
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
this.setDetune(channel, note, scheduleTime);
|
|
2155
|
+
}
|
|
2156
|
+
},
|
|
2009
2157
|
};
|
|
2010
2158
|
}
|
|
2011
2159
|
getControllerState(channel, noteNumber, velocity, polyphonicKeyPressure) {
|
|
@@ -2097,6 +2245,21 @@ export class Midy extends EventTarget {
|
|
|
2097
2245
|
return handlers;
|
|
2098
2246
|
}
|
|
2099
2247
|
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
2248
|
+
const channel = this.channels[channelNumber];
|
|
2249
|
+
if (channel.isMPEMember) {
|
|
2250
|
+
this.applyControlChange(channelNumber, controllerType, value, scheduleTime);
|
|
2251
|
+
}
|
|
2252
|
+
else if (channel.isMPEManager) {
|
|
2253
|
+
channel.state[controllerType] = value / 127;
|
|
2254
|
+
for (const memberChannel of this.mpeState.channelToNote.keys()) {
|
|
2255
|
+
this.applyControlChange(memberChannel, controllerType, value, scheduleTime);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
else {
|
|
2259
|
+
this.applyControlChange(channelNumber, controllerType, value, scheduleTime);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
applyControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
2100
2263
|
const handler = this.controlChangeHandlers[controllerType];
|
|
2101
2264
|
if (handler) {
|
|
2102
2265
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
@@ -2143,14 +2306,14 @@ export class Midy extends EventTarget {
|
|
|
2143
2306
|
if (this.isPortamento(channel, note)) {
|
|
2144
2307
|
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2145
2308
|
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2146
|
-
this.setPortamentoPitchEnvelope(note, scheduleTime);
|
|
2147
|
-
this.
|
|
2309
|
+
this.setPortamentoPitchEnvelope(channel, note, scheduleTime);
|
|
2310
|
+
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
2148
2311
|
}
|
|
2149
2312
|
else {
|
|
2150
2313
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
2151
2314
|
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
2152
2315
|
this.setPitchEnvelope(note, scheduleTime);
|
|
2153
|
-
this.
|
|
2316
|
+
this.setDetune(channel, note, scheduleTime);
|
|
2154
2317
|
}
|
|
2155
2318
|
});
|
|
2156
2319
|
}
|
|
@@ -2519,6 +2682,12 @@ export class Midy extends EventTarget {
|
|
|
2519
2682
|
channel.dataLSB += value;
|
|
2520
2683
|
this.handleModulationDepthRangeRPN(channelNumber, scheduleTime);
|
|
2521
2684
|
break;
|
|
2685
|
+
case 6: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp053.pdf
|
|
2686
|
+
channel.dataLSB += value;
|
|
2687
|
+
this.handleMIDIPolyphonicExpressionRPN(channelNumber, scheduleTime);
|
|
2688
|
+
break;
|
|
2689
|
+
case 16383: // NULL
|
|
2690
|
+
break;
|
|
2522
2691
|
default:
|
|
2523
2692
|
console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
|
|
2524
2693
|
}
|
|
@@ -2617,11 +2786,43 @@ export class Midy extends EventTarget {
|
|
|
2617
2786
|
channel.modulationDepthRange = value;
|
|
2618
2787
|
this.updateModulation(channel, scheduleTime);
|
|
2619
2788
|
}
|
|
2789
|
+
handleMIDIPolyphonicExpressionRPN(channelNumber, _scheduleTime) {
|
|
2790
|
+
this.setMIDIPolyphonicExpression(channelNumber, channel.dataMSB);
|
|
2791
|
+
}
|
|
2792
|
+
setMIDIPolyphonicExpression(channelNumber, value) {
|
|
2793
|
+
if (channelNumber !== 0 && channelNumber !== 15)
|
|
2794
|
+
return;
|
|
2795
|
+
const members = value & 15;
|
|
2796
|
+
if (channelNumber === 0) {
|
|
2797
|
+
this.lowerMPEMembers = members;
|
|
2798
|
+
}
|
|
2799
|
+
else {
|
|
2800
|
+
this.upperMPEMembers = members;
|
|
2801
|
+
}
|
|
2802
|
+
this.mpeEnabled = this.lowerMPEMembers > 0 || this.upperMPEMembers > 0;
|
|
2803
|
+
const lowerStart = 1;
|
|
2804
|
+
const lowerEnd = this.lowerMPEMembers;
|
|
2805
|
+
const upperStart = 16 - this.upperMPEMembers;
|
|
2806
|
+
const upperEnd = 14;
|
|
2807
|
+
for (let i = 0; i < 16; i++) {
|
|
2808
|
+
const isLower = this.lowerMPEMembers && lowerStart <= i && i <= lowerEnd;
|
|
2809
|
+
const isUpper = this.upperMPEMembers && upperStart <= i && i <= upperEnd;
|
|
2810
|
+
this.channels[i].isMPEMember = this.mpeEnabled && (isLower || isUpper);
|
|
2811
|
+
this.channels[i].isMPEManager = this.mpeEnabled && (i === 0 || i === 15);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2620
2814
|
setRPGMakerLoop(_channelNumber, _value, scheduleTime) {
|
|
2621
2815
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2622
2816
|
this.loopStart = scheduleTime + this.resumeTime - this.startTime;
|
|
2623
2817
|
}
|
|
2624
|
-
allSoundOff(channelNumber,
|
|
2818
|
+
allSoundOff(channelNumber, value, scheduleTime) {
|
|
2819
|
+
if (this.channels[channelNumber].isMPEManager)
|
|
2820
|
+
return;
|
|
2821
|
+
this.applyAllSoundOff(channelNumber, value, scheduleTime);
|
|
2822
|
+
}
|
|
2823
|
+
applyAllSoundOff(channelNumber, _value, scheduleTime) {
|
|
2824
|
+
if (this.channels[channelNumber].isMPEManager)
|
|
2825
|
+
return;
|
|
2625
2826
|
if (!(0 <= scheduleTime))
|
|
2626
2827
|
scheduleTime = this.audioContext.currentTime;
|
|
2627
2828
|
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
@@ -2693,15 +2894,21 @@ export class Midy extends EventTarget {
|
|
|
2693
2894
|
this.allNotesOff(channelNumber, value, scheduleTime);
|
|
2694
2895
|
}
|
|
2695
2896
|
omniOn(channelNumber, value, scheduleTime) {
|
|
2897
|
+
if (this.mpeEnabled)
|
|
2898
|
+
return;
|
|
2696
2899
|
this.allNotesOff(channelNumber, value, scheduleTime);
|
|
2697
2900
|
}
|
|
2698
2901
|
monoOn(channelNumber, value, scheduleTime) {
|
|
2699
2902
|
const channel = this.channels[channelNumber];
|
|
2903
|
+
if (channel.isMPEManager)
|
|
2904
|
+
return;
|
|
2700
2905
|
this.allNotesOff(channelNumber, value, scheduleTime);
|
|
2701
2906
|
channel.mono = true;
|
|
2702
2907
|
}
|
|
2703
2908
|
polyOn(channelNumber, value, scheduleTime) {
|
|
2704
2909
|
const channel = this.channels[channelNumber];
|
|
2910
|
+
if (channel.isMPEManager)
|
|
2911
|
+
return;
|
|
2705
2912
|
this.allNotesOff(channelNumber, value, scheduleTime);
|
|
2706
2913
|
channel.mono = false;
|
|
2707
2914
|
}
|
|
@@ -2738,32 +2945,34 @@ export class Midy extends EventTarget {
|
|
|
2738
2945
|
}
|
|
2739
2946
|
}
|
|
2740
2947
|
GM1SystemOn(scheduleTime) {
|
|
2948
|
+
const channels = this.channels;
|
|
2741
2949
|
if (!(0 <= scheduleTime))
|
|
2742
2950
|
scheduleTime = this.audioContext.currentTime;
|
|
2743
2951
|
this.mode = "GM1";
|
|
2744
|
-
for (let i = 0; i <
|
|
2745
|
-
this.
|
|
2746
|
-
const channel =
|
|
2952
|
+
for (let i = 0; i < channels.length; i++) {
|
|
2953
|
+
this.applyAllSoundOff(i, 0, scheduleTime);
|
|
2954
|
+
const channel = channels[i];
|
|
2747
2955
|
channel.bankMSB = 0;
|
|
2748
2956
|
channel.bankLSB = 0;
|
|
2749
2957
|
channel.isDrum = false;
|
|
2750
2958
|
}
|
|
2751
|
-
|
|
2752
|
-
|
|
2959
|
+
channels[9].bankMSB = 1;
|
|
2960
|
+
channels[9].isDrum = true;
|
|
2753
2961
|
}
|
|
2754
2962
|
GM2SystemOn(scheduleTime) {
|
|
2963
|
+
const channels = this.channels;
|
|
2755
2964
|
if (!(0 <= scheduleTime))
|
|
2756
2965
|
scheduleTime = this.audioContext.currentTime;
|
|
2757
2966
|
this.mode = "GM2";
|
|
2758
|
-
for (let i = 0; i <
|
|
2759
|
-
this.
|
|
2760
|
-
const channel =
|
|
2967
|
+
for (let i = 0; i < channels.length; i++) {
|
|
2968
|
+
this.applyAllSoundOff(i, 0, scheduleTime);
|
|
2969
|
+
const channel = channels[i];
|
|
2761
2970
|
channel.bankMSB = 121;
|
|
2762
2971
|
channel.bankLSB = 0;
|
|
2763
2972
|
channel.isDrum = false;
|
|
2764
2973
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2974
|
+
channels[9].bankMSB = 120;
|
|
2975
|
+
channels[9].isDrum = true;
|
|
2767
2976
|
}
|
|
2768
2977
|
handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
|
|
2769
2978
|
switch (data[2]) {
|
|
@@ -3106,7 +3315,7 @@ export class Midy extends EventTarget {
|
|
|
3106
3315
|
}
|
|
3107
3316
|
getPitchControl(channel, note) {
|
|
3108
3317
|
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[0];
|
|
3109
|
-
if (polyphonicKeyPressureRaw
|
|
3318
|
+
if (polyphonicKeyPressureRaw <= 0)
|
|
3110
3319
|
return 0;
|
|
3111
3320
|
const polyphonicKeyPressure = (polyphonicKeyPressureRaw - 64) *
|
|
3112
3321
|
note.pressure;
|
|
@@ -3126,13 +3335,13 @@ export class Midy extends EventTarget {
|
|
|
3126
3335
|
getAmplitudeControl(channel, note) {
|
|
3127
3336
|
const channelPressureRaw = channel.channelPressureTable[2];
|
|
3128
3337
|
const channelPressure = (0 <= channelPressureRaw)
|
|
3129
|
-
?
|
|
3338
|
+
? channel.state.channelPressure * 127 / channelPressureRaw
|
|
3130
3339
|
: 0;
|
|
3131
3340
|
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
|
|
3132
3341
|
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
3133
|
-
?
|
|
3342
|
+
? note.pressure / polyphonicKeyPressureRaw
|
|
3134
3343
|
: 0;
|
|
3135
|
-
return
|
|
3344
|
+
return channelPressure + polyphonicKeyPressure;
|
|
3136
3345
|
}
|
|
3137
3346
|
getLFOPitchDepth(channel, note) {
|
|
3138
3347
|
const channelPressureRaw = channel.channelPressureTable[3];
|
|
@@ -3168,27 +3377,33 @@ export class Midy extends EventTarget {
|
|
|
3168
3377
|
return (channelPressure + polyphonicKeyPressure) / 254;
|
|
3169
3378
|
}
|
|
3170
3379
|
setEffects(channel, note, table, scheduleTime) {
|
|
3171
|
-
if (0
|
|
3172
|
-
this.
|
|
3380
|
+
if (0 < table[0]) {
|
|
3381
|
+
if (this.isPortamento(channel, note)) {
|
|
3382
|
+
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
3383
|
+
}
|
|
3384
|
+
else {
|
|
3385
|
+
this.setDetune(channel, note, scheduleTime);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3173
3388
|
if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
|
|
3174
|
-
if (0
|
|
3389
|
+
if (0 < table[1]) {
|
|
3175
3390
|
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
3176
3391
|
}
|
|
3177
|
-
if (0
|
|
3392
|
+
if (0 < table[2]) {
|
|
3178
3393
|
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
3179
3394
|
}
|
|
3180
3395
|
}
|
|
3181
3396
|
else {
|
|
3182
|
-
if (0
|
|
3397
|
+
if (0 < table[1])
|
|
3183
3398
|
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
3184
|
-
if (0
|
|
3399
|
+
if (0 < table[2])
|
|
3185
3400
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
3186
3401
|
}
|
|
3187
|
-
if (0
|
|
3402
|
+
if (0 < table[3])
|
|
3188
3403
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
3189
|
-
if (0
|
|
3404
|
+
if (0 < table[4])
|
|
3190
3405
|
this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
3191
|
-
if (0
|
|
3406
|
+
if (0 < table[5])
|
|
3192
3407
|
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
3193
3408
|
}
|
|
3194
3409
|
handlePressureSysEx(data, tableName, scheduleTime) {
|
|
@@ -3370,5 +3585,7 @@ Object.defineProperty(Midy, "channelSettings", {
|
|
|
3370
3585
|
fineTuning: 0, // cent
|
|
3371
3586
|
coarseTuning: 0, // cent
|
|
3372
3587
|
portamentoControl: false,
|
|
3588
|
+
isMPEMember: false,
|
|
3589
|
+
isMPEManager: false,
|
|
3373
3590
|
}
|
|
3374
3591
|
});
|