@marmooo/midy 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/midy-GM1.d.ts +11 -50
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +150 -169
- package/esm/midy-GM2.d.ts +25 -83
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +376 -285
- package/esm/midy-GMLite.d.ts +10 -49
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +155 -171
- package/esm/midy.d.ts +28 -106
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +405 -313
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +11 -50
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +150 -169
- package/script/midy-GM2.d.ts +25 -83
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +376 -285
- package/script/midy-GMLite.d.ts +10 -49
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +155 -171
- package/script/midy.d.ts +28 -106
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +405 -313
package/script/midy.js
CHANGED
|
@@ -3,60 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Midy = void 0;
|
|
4
4
|
const midi_file_1 = require("midi-file");
|
|
5
5
|
const soundfont_parser_1 = require("@marmooo/soundfont-parser");
|
|
6
|
-
// 2-3 times faster than Map
|
|
7
|
-
class SparseMap {
|
|
8
|
-
constructor(size) {
|
|
9
|
-
this.data = new Array(size);
|
|
10
|
-
this.activeIndices = [];
|
|
11
|
-
}
|
|
12
|
-
set(key, value) {
|
|
13
|
-
if (this.data[key] === undefined) {
|
|
14
|
-
this.activeIndices.push(key);
|
|
15
|
-
}
|
|
16
|
-
this.data[key] = value;
|
|
17
|
-
}
|
|
18
|
-
get(key) {
|
|
19
|
-
return this.data[key];
|
|
20
|
-
}
|
|
21
|
-
delete(key) {
|
|
22
|
-
if (this.data[key] !== undefined) {
|
|
23
|
-
this.data[key] = undefined;
|
|
24
|
-
const index = this.activeIndices.indexOf(key);
|
|
25
|
-
if (index !== -1) {
|
|
26
|
-
this.activeIndices.splice(index, 1);
|
|
27
|
-
}
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
has(key) {
|
|
33
|
-
return this.data[key] !== undefined;
|
|
34
|
-
}
|
|
35
|
-
get size() {
|
|
36
|
-
return this.activeIndices.length;
|
|
37
|
-
}
|
|
38
|
-
clear() {
|
|
39
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
40
|
-
const key = this.activeIndices[i];
|
|
41
|
-
this.data[key] = undefined;
|
|
42
|
-
}
|
|
43
|
-
this.activeIndices = [];
|
|
44
|
-
}
|
|
45
|
-
*[Symbol.iterator]() {
|
|
46
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
47
|
-
const key = this.activeIndices[i];
|
|
48
|
-
yield [key, this.data[key]];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
forEach(callback) {
|
|
52
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
53
|
-
const key = this.activeIndices[i];
|
|
54
|
-
callback(this.data[key], key, this);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
6
|
class Note {
|
|
59
7
|
constructor(noteNumber, velocity, startTime, voice, voiceParams) {
|
|
8
|
+
Object.defineProperty(this, "index", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: -1
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "noteOffEvent", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: void 0
|
|
19
|
+
});
|
|
60
20
|
Object.defineProperty(this, "bufferSource", {
|
|
61
21
|
enumerable: true,
|
|
62
22
|
configurable: true,
|
|
@@ -141,11 +101,11 @@ class Note {
|
|
|
141
101
|
writable: true,
|
|
142
102
|
value: void 0
|
|
143
103
|
});
|
|
144
|
-
Object.defineProperty(this, "
|
|
104
|
+
Object.defineProperty(this, "portamentoNoteNumber", {
|
|
145
105
|
enumerable: true,
|
|
146
106
|
configurable: true,
|
|
147
107
|
writable: true,
|
|
148
|
-
value:
|
|
108
|
+
value: -1
|
|
149
109
|
});
|
|
150
110
|
Object.defineProperty(this, "pressure", {
|
|
151
111
|
enumerable: true,
|
|
@@ -207,7 +167,7 @@ const defaultControllerState = {
|
|
|
207
167
|
portamentoTime: { type: 128 + 5, defaultValue: 0 },
|
|
208
168
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
209
169
|
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
210
|
-
pan: { type: 128 + 10, defaultValue:
|
|
170
|
+
pan: { type: 128 + 10, defaultValue: 64 / 127 },
|
|
211
171
|
expression: { type: 128 + 11, defaultValue: 1 },
|
|
212
172
|
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
213
173
|
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
@@ -215,14 +175,14 @@ const defaultControllerState = {
|
|
|
215
175
|
portamento: { type: 128 + 65, defaultValue: 0 },
|
|
216
176
|
sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
|
|
217
177
|
softPedal: { type: 128 + 67, defaultValue: 0 },
|
|
218
|
-
filterResonance: { type: 128 + 71, defaultValue:
|
|
219
|
-
releaseTime: { type: 128 + 72, defaultValue:
|
|
220
|
-
attackTime: { type: 128 + 73, defaultValue:
|
|
221
|
-
brightness: { type: 128 + 74, defaultValue:
|
|
222
|
-
decayTime: { type: 128 + 75, defaultValue:
|
|
223
|
-
vibratoRate: { type: 128 + 76, defaultValue:
|
|
224
|
-
vibratoDepth: { type: 128 + 77, defaultValue:
|
|
225
|
-
vibratoDelay: { type: 128 + 78, defaultValue:
|
|
178
|
+
filterResonance: { type: 128 + 71, defaultValue: 64 / 127 },
|
|
179
|
+
releaseTime: { type: 128 + 72, defaultValue: 64 / 127 },
|
|
180
|
+
attackTime: { type: 128 + 73, defaultValue: 64 / 127 },
|
|
181
|
+
brightness: { type: 128 + 74, defaultValue: 64 / 127 },
|
|
182
|
+
decayTime: { type: 128 + 75, defaultValue: 64 / 127 },
|
|
183
|
+
vibratoRate: { type: 128 + 76, defaultValue: 64 / 127 },
|
|
184
|
+
vibratoDepth: { type: 128 + 77, defaultValue: 64 / 127 },
|
|
185
|
+
vibratoDelay: { type: 128 + 78, defaultValue: 64 / 127 },
|
|
226
186
|
reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
|
|
227
187
|
chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
|
|
228
188
|
// dataIncrement: { type: 128 + 96, defaultValue: 0 },
|
|
@@ -497,7 +457,7 @@ class Midy {
|
|
|
497
457
|
initSoundFontTable() {
|
|
498
458
|
const table = new Array(128);
|
|
499
459
|
for (let i = 0; i < 128; i++) {
|
|
500
|
-
table[i] = new
|
|
460
|
+
table[i] = new Map();
|
|
501
461
|
}
|
|
502
462
|
return table;
|
|
503
463
|
}
|
|
@@ -544,18 +504,25 @@ class Midy {
|
|
|
544
504
|
merger,
|
|
545
505
|
};
|
|
546
506
|
}
|
|
507
|
+
resetChannelTable(channel) {
|
|
508
|
+
this.resetControlTable(channel.controlTable);
|
|
509
|
+
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
510
|
+
channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
|
|
511
|
+
channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
|
|
512
|
+
channel.keyBasedInstrumentControlTable.fill(0); // [-64, 63]
|
|
513
|
+
}
|
|
547
514
|
createChannels(audioContext) {
|
|
548
515
|
const channels = Array.from({ length: this.numChannels }, () => {
|
|
549
516
|
return {
|
|
550
517
|
currentBufferSource: null,
|
|
551
518
|
isDrum: false,
|
|
552
|
-
...this.constructor.channelSettings,
|
|
553
519
|
state: new ControllerState(),
|
|
554
|
-
|
|
520
|
+
...this.constructor.channelSettings,
|
|
555
521
|
...this.setChannelAudioNodes(audioContext),
|
|
556
|
-
scheduledNotes:
|
|
522
|
+
scheduledNotes: [],
|
|
557
523
|
sustainNotes: [],
|
|
558
|
-
sostenutoNotes:
|
|
524
|
+
sostenutoNotes: [],
|
|
525
|
+
controlTable: this.initControlTable(),
|
|
559
526
|
scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
|
|
560
527
|
channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
|
|
561
528
|
polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
|
|
@@ -603,46 +570,20 @@ class Midy {
|
|
|
603
570
|
}
|
|
604
571
|
return bufferSource;
|
|
605
572
|
}
|
|
606
|
-
|
|
607
|
-
const endEvent = this.timeline[queueIndex];
|
|
608
|
-
if (!this.channels[endEvent.channel].portamento)
|
|
609
|
-
return;
|
|
610
|
-
const endTime = endEvent.startTime;
|
|
611
|
-
let target;
|
|
612
|
-
while (++queueIndex < this.timeline.length) {
|
|
613
|
-
const event = this.timeline[queueIndex];
|
|
614
|
-
if (endTime !== event.startTime)
|
|
615
|
-
break;
|
|
616
|
-
if (event.type !== "noteOn")
|
|
617
|
-
continue;
|
|
618
|
-
if (!target || event.noteNumber < target.noteNumber) {
|
|
619
|
-
target = event;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
return target;
|
|
623
|
-
}
|
|
624
|
-
async scheduleTimelineEvents(t, offset, queueIndex) {
|
|
573
|
+
async scheduleTimelineEvents(t, resumeTime, queueIndex) {
|
|
625
574
|
while (queueIndex < this.timeline.length) {
|
|
626
575
|
const event = this.timeline[queueIndex];
|
|
627
576
|
if (event.startTime > t + this.lookAhead)
|
|
628
577
|
break;
|
|
629
|
-
const
|
|
578
|
+
const delay = this.startDelay - resumeTime;
|
|
579
|
+
const startTime = event.startTime + delay;
|
|
630
580
|
switch (event.type) {
|
|
631
|
-
case "noteOn":
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
case "noteOff": {
|
|
638
|
-
const portamentoTarget = this.findPortamentoTarget(queueIndex);
|
|
639
|
-
if (portamentoTarget)
|
|
640
|
-
portamentoTarget.portamento = true;
|
|
641
|
-
const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
|
|
642
|
-
portamentoTarget?.noteNumber);
|
|
643
|
-
if (notePromise) {
|
|
644
|
-
this.notePromises.push(notePromise);
|
|
645
|
-
}
|
|
581
|
+
case "noteOn": {
|
|
582
|
+
const noteOffEvent = {
|
|
583
|
+
...event.noteOffEvent,
|
|
584
|
+
startTime: event.noteOffEvent.startTime + delay,
|
|
585
|
+
};
|
|
586
|
+
await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
|
|
646
587
|
break;
|
|
647
588
|
}
|
|
648
589
|
case "noteAftertouch":
|
|
@@ -681,7 +622,7 @@ class Midy {
|
|
|
681
622
|
this.isPaused = false;
|
|
682
623
|
this.startTime = this.audioContext.currentTime;
|
|
683
624
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
684
|
-
let
|
|
625
|
+
let resumeTime = this.resumeTime - this.startTime;
|
|
685
626
|
this.notePromises = [];
|
|
686
627
|
const schedulePlayback = async () => {
|
|
687
628
|
if (queueIndex >= this.timeline.length) {
|
|
@@ -690,18 +631,21 @@ class Midy {
|
|
|
690
631
|
this.exclusiveClassNotes.fill(undefined);
|
|
691
632
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
692
633
|
this.audioBufferCache.clear();
|
|
634
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
635
|
+
this.resetAllStates(i);
|
|
636
|
+
}
|
|
693
637
|
resolve();
|
|
694
638
|
return;
|
|
695
639
|
}
|
|
696
640
|
const now = this.audioContext.currentTime;
|
|
697
|
-
const t = now +
|
|
698
|
-
queueIndex = await this.scheduleTimelineEvents(t,
|
|
641
|
+
const t = now + resumeTime;
|
|
642
|
+
queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
|
|
699
643
|
if (this.isPausing) {
|
|
700
644
|
await this.stopNotes(0, true, now);
|
|
701
645
|
this.notePromises = [];
|
|
702
|
-
resolve();
|
|
703
646
|
this.isPausing = false;
|
|
704
647
|
this.isPaused = true;
|
|
648
|
+
resolve();
|
|
705
649
|
return;
|
|
706
650
|
}
|
|
707
651
|
else if (this.isStopping) {
|
|
@@ -710,9 +654,12 @@ class Midy {
|
|
|
710
654
|
this.exclusiveClassNotes.fill(undefined);
|
|
711
655
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
712
656
|
this.audioBufferCache.clear();
|
|
713
|
-
|
|
657
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
658
|
+
this.resetAllStates(i);
|
|
659
|
+
}
|
|
714
660
|
this.isStopping = false;
|
|
715
661
|
this.isPaused = false;
|
|
662
|
+
resolve();
|
|
716
663
|
return;
|
|
717
664
|
}
|
|
718
665
|
else if (this.isSeeking) {
|
|
@@ -721,7 +668,7 @@ class Midy {
|
|
|
721
668
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
722
669
|
this.startTime = this.audioContext.currentTime;
|
|
723
670
|
queueIndex = this.getQueueIndex(this.resumeTime);
|
|
724
|
-
|
|
671
|
+
resumeTime = this.resumeTime - this.startTime;
|
|
725
672
|
this.isSeeking = false;
|
|
726
673
|
await schedulePlayback();
|
|
727
674
|
}
|
|
@@ -843,13 +790,37 @@ class Midy {
|
|
|
843
790
|
prevTempoTicks = event.ticks;
|
|
844
791
|
}
|
|
845
792
|
}
|
|
793
|
+
const activeNotes = new Array(this.channels.length * 128);
|
|
794
|
+
for (let i = 0; i < activeNotes.length; i++) {
|
|
795
|
+
activeNotes[i] = [];
|
|
796
|
+
}
|
|
797
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
798
|
+
const event = timeline[i];
|
|
799
|
+
switch (event.type) {
|
|
800
|
+
case "noteOn": {
|
|
801
|
+
const index = event.channel * 128 + event.noteNumber;
|
|
802
|
+
activeNotes[index].push(event);
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
case "noteOff": {
|
|
806
|
+
const index = event.channel * 128 + event.noteNumber;
|
|
807
|
+
const noteOn = activeNotes[index].pop();
|
|
808
|
+
if (noteOn) {
|
|
809
|
+
noteOn.noteOffEvent = event;
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
const eventString = JSON.stringify(event, null, 2);
|
|
813
|
+
console.warn(`noteOff without matching noteOn: ${eventString}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
846
818
|
return { instruments, timeline };
|
|
847
819
|
}
|
|
848
820
|
stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
|
|
849
821
|
const channel = this.channels[channelNumber];
|
|
850
822
|
const promises = [];
|
|
851
|
-
|
|
852
|
-
activeNotes.forEach((note) => {
|
|
823
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
853
824
|
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
|
|
854
825
|
this.notePromises.push(promise);
|
|
855
826
|
promises.push(promise);
|
|
@@ -860,11 +831,11 @@ class Midy {
|
|
|
860
831
|
const channel = this.channels[channelNumber];
|
|
861
832
|
const promises = [];
|
|
862
833
|
this.processScheduledNotes(channel, (note) => {
|
|
863
|
-
const promise = this.scheduleNoteOff(channelNumber, note
|
|
834
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
|
|
864
835
|
this.notePromises.push(promise);
|
|
865
836
|
promises.push(promise);
|
|
866
837
|
});
|
|
867
|
-
channel.scheduledNotes
|
|
838
|
+
channel.scheduledNotes = [];
|
|
868
839
|
return Promise.all(promises);
|
|
869
840
|
}
|
|
870
841
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -885,9 +856,6 @@ class Midy {
|
|
|
885
856
|
if (!this.isPlaying)
|
|
886
857
|
return;
|
|
887
858
|
this.isStopping = true;
|
|
888
|
-
for (let i = 0; i < this.channels.length; i++) {
|
|
889
|
-
this.resetAllStates(i);
|
|
890
|
-
}
|
|
891
859
|
}
|
|
892
860
|
pause() {
|
|
893
861
|
if (!this.isPlaying || this.isPaused)
|
|
@@ -922,37 +890,31 @@ class Midy {
|
|
|
922
890
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
923
891
|
}
|
|
924
892
|
processScheduledNotes(channel, callback) {
|
|
925
|
-
channel.scheduledNotes
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
getActiveNotes(channel, scheduleTime) {
|
|
937
|
-
const activeNotes = new SparseMap(128);
|
|
938
|
-
channel.scheduledNotes.forEach((noteList) => {
|
|
939
|
-
const activeNote = this.getActiveNote(noteList, scheduleTime);
|
|
940
|
-
if (activeNote) {
|
|
941
|
-
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
return activeNotes;
|
|
893
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
894
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
895
|
+
const note = scheduledNotes[i];
|
|
896
|
+
if (!note)
|
|
897
|
+
continue;
|
|
898
|
+
if (note.ending)
|
|
899
|
+
continue;
|
|
900
|
+
callback(note);
|
|
901
|
+
}
|
|
945
902
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
903
|
+
processActiveNotes(channel, scheduleTime, callback) {
|
|
904
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
905
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
906
|
+
const note = scheduledNotes[i];
|
|
949
907
|
if (!note)
|
|
950
|
-
|
|
908
|
+
continue;
|
|
909
|
+
if (note.ending)
|
|
910
|
+
continue;
|
|
911
|
+
const noteOffEvent = note.noteOffEvent;
|
|
912
|
+
if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
|
|
913
|
+
continue;
|
|
951
914
|
if (scheduleTime < note.startTime)
|
|
952
915
|
continue;
|
|
953
|
-
|
|
916
|
+
callback(note);
|
|
954
917
|
}
|
|
955
|
-
return noteList[0];
|
|
956
918
|
}
|
|
957
919
|
createConvolutionReverbImpulse(audioContext, decay, preDecay) {
|
|
958
920
|
const sampleRate = audioContext.sampleRate;
|
|
@@ -1119,24 +1081,94 @@ class Midy {
|
|
|
1119
1081
|
const noteDetune = this.calcNoteDetune(channel, note);
|
|
1120
1082
|
const pitchControl = this.getPitchControl(channel, note);
|
|
1121
1083
|
const detune = channel.detune + noteDetune + pitchControl;
|
|
1122
|
-
note.
|
|
1123
|
-
.
|
|
1124
|
-
.
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1084
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1085
|
+
const startTime = note.startTime;
|
|
1086
|
+
const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
|
|
1087
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1088
|
+
note.bufferSource.detune
|
|
1089
|
+
.cancelScheduledValues(scheduleTime)
|
|
1090
|
+
.setValueAtTime(detune - deltaCent, scheduleTime)
|
|
1091
|
+
.linearRampToValueAtTime(detune, portamentoTime);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
note.bufferSource.detune
|
|
1095
|
+
.cancelScheduledValues(scheduleTime)
|
|
1096
|
+
.setValueAtTime(detune, scheduleTime);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
getPortamentoTime(channel, note) {
|
|
1100
|
+
const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
|
|
1101
|
+
const value = Math.ceil(channel.state.portamentoTime * 127);
|
|
1102
|
+
return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
|
|
1103
|
+
}
|
|
1104
|
+
getPitchIncrementSpeed(value) {
|
|
1105
|
+
const points = [
|
|
1106
|
+
[0, 1000],
|
|
1107
|
+
[6, 100],
|
|
1108
|
+
[16, 20],
|
|
1109
|
+
[32, 10],
|
|
1110
|
+
[48, 5],
|
|
1111
|
+
[64, 2.5],
|
|
1112
|
+
[80, 1],
|
|
1113
|
+
[96, 0.4],
|
|
1114
|
+
[112, 0.15],
|
|
1115
|
+
[127, 0.01],
|
|
1116
|
+
];
|
|
1117
|
+
const logPoints = new Array(points.length);
|
|
1118
|
+
for (let i = 0; i < points.length; i++) {
|
|
1119
|
+
const [x, y] = points[i];
|
|
1120
|
+
if (value === x)
|
|
1121
|
+
return y;
|
|
1122
|
+
logPoints[i] = [x, Math.log(y)];
|
|
1123
|
+
}
|
|
1124
|
+
let startIndex = 0;
|
|
1125
|
+
for (let i = 1; i < logPoints.length; i++) {
|
|
1126
|
+
if (value <= logPoints[i][0]) {
|
|
1127
|
+
startIndex = i - 1;
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const [x0, y0] = logPoints[startIndex];
|
|
1132
|
+
const [x1, y1] = logPoints[startIndex + 1];
|
|
1133
|
+
const h = x1 - x0;
|
|
1134
|
+
const t = (value - x0) / h;
|
|
1135
|
+
let m0, m1;
|
|
1136
|
+
if (startIndex === 0) {
|
|
1137
|
+
m0 = (y1 - y0) / h;
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
const [xPrev, yPrev] = logPoints[startIndex - 1];
|
|
1141
|
+
m0 = (y1 - yPrev) / (x1 - xPrev);
|
|
1142
|
+
}
|
|
1143
|
+
if (startIndex === logPoints.length - 2) {
|
|
1144
|
+
m1 = (y1 - y0) / h;
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
const [xNext, yNext] = logPoints[startIndex + 2];
|
|
1148
|
+
m1 = (yNext - y0) / (xNext - x0);
|
|
1149
|
+
}
|
|
1150
|
+
// Cubic Hermite Spline
|
|
1151
|
+
const t2 = t * t;
|
|
1152
|
+
const t3 = t2 * t;
|
|
1153
|
+
const h00 = 2 * t3 - 3 * t2 + 1;
|
|
1154
|
+
const h10 = t3 - 2 * t2 + t;
|
|
1155
|
+
const h01 = -2 * t3 + 3 * t2;
|
|
1156
|
+
const h11 = t3 - t2;
|
|
1157
|
+
const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
|
|
1158
|
+
return Math.exp(y);
|
|
1159
|
+
}
|
|
1160
|
+
setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
|
|
1161
|
+
const state = channel.state;
|
|
1131
1162
|
const { voiceParams, startTime } = note;
|
|
1132
|
-
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation)
|
|
1163
|
+
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
|
|
1164
|
+
(1 + this.getAmplitudeControl(channel, note));
|
|
1133
1165
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1134
1166
|
const volDelay = startTime + voiceParams.volDelay;
|
|
1135
|
-
const
|
|
1167
|
+
const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
|
|
1168
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
1136
1169
|
note.volumeEnvelopeNode.gain
|
|
1137
1170
|
.cancelScheduledValues(scheduleTime)
|
|
1138
|
-
.setValueAtTime(
|
|
1139
|
-
.linearRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1171
|
+
.setValueAtTime(sustainVolume, volHold);
|
|
1140
1172
|
}
|
|
1141
1173
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
1142
1174
|
const state = channel.state;
|
|
@@ -1156,6 +1188,12 @@ class Midy {
|
|
|
1156
1188
|
.setValueAtTime(attackVolume, volHold)
|
|
1157
1189
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
1158
1190
|
}
|
|
1191
|
+
setPortamentoPitchEnvelope(note, scheduleTime) {
|
|
1192
|
+
const baseRate = note.voiceParams.playbackRate;
|
|
1193
|
+
note.bufferSource.playbackRate
|
|
1194
|
+
.cancelScheduledValues(scheduleTime)
|
|
1195
|
+
.setValueAtTime(baseRate, scheduleTime);
|
|
1196
|
+
}
|
|
1159
1197
|
setPitchEnvelope(note, scheduleTime) {
|
|
1160
1198
|
const { voiceParams } = note;
|
|
1161
1199
|
const baseRate = voiceParams.playbackRate;
|
|
@@ -1183,20 +1221,21 @@ class Midy {
|
|
|
1183
1221
|
const maxFrequency = 20000; // max Hz of initialFilterFc
|
|
1184
1222
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
1185
1223
|
}
|
|
1186
|
-
|
|
1224
|
+
setPortamentoFilterEnvelope(channel, note, scheduleTime) {
|
|
1187
1225
|
const state = channel.state;
|
|
1188
1226
|
const { voiceParams, noteNumber, startTime } = note;
|
|
1189
1227
|
const softPedalFactor = 1 -
|
|
1190
1228
|
(0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1229
|
+
const baseCent = voiceParams.initialFilterFc +
|
|
1230
|
+
this.getFilterCutoffControl(channel, note);
|
|
1231
|
+
const baseFreq = this.centToHz(baseCent) * softPedalFactor *
|
|
1193
1232
|
state.brightness * 2;
|
|
1194
1233
|
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
|
|
1195
1234
|
const sustainFreq = baseFreq +
|
|
1196
1235
|
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
1197
1236
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
1198
1237
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
1199
|
-
const portamentoTime = startTime + this.getPortamentoTime(channel);
|
|
1238
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1200
1239
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1201
1240
|
note.filterNode.frequency
|
|
1202
1241
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1282,7 +1321,7 @@ class Midy {
|
|
|
1282
1321
|
return audioBuffer;
|
|
1283
1322
|
}
|
|
1284
1323
|
}
|
|
1285
|
-
async createNote(channel, voice, noteNumber, velocity, startTime,
|
|
1324
|
+
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
1286
1325
|
const now = this.audioContext.currentTime;
|
|
1287
1326
|
const state = channel.state;
|
|
1288
1327
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
@@ -1298,20 +1337,24 @@ class Midy {
|
|
|
1298
1337
|
type: "lowpass",
|
|
1299
1338
|
Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
|
|
1300
1339
|
});
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1340
|
+
const prevNote = channel.scheduledNotes.at(-1);
|
|
1341
|
+
if (prevNote && prevNote.noteNumber !== noteNumber) {
|
|
1342
|
+
note.portamentoNoteNumber = prevNote.noteNumber;
|
|
1343
|
+
}
|
|
1344
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1345
|
+
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
1346
|
+
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
1347
|
+
this.setPortamentoPitchEnvelope(note, now);
|
|
1305
1348
|
}
|
|
1306
1349
|
else {
|
|
1307
|
-
note.portamento = false;
|
|
1308
1350
|
this.setVolumeEnvelope(channel, note, now);
|
|
1309
1351
|
this.setFilterEnvelope(channel, note, now);
|
|
1352
|
+
this.setPitchEnvelope(note, now);
|
|
1310
1353
|
}
|
|
1354
|
+
this.updateDetune(channel, note, now);
|
|
1311
1355
|
if (0 < state.vibratoDepth) {
|
|
1312
1356
|
this.startVibrato(channel, note, now);
|
|
1313
1357
|
}
|
|
1314
|
-
this.setPitchEnvelope(note, now);
|
|
1315
1358
|
if (0 < state.modulationDepth) {
|
|
1316
1359
|
this.startModulation(channel, note, now);
|
|
1317
1360
|
}
|
|
@@ -1357,9 +1400,8 @@ class Midy {
|
|
|
1357
1400
|
if (prev) {
|
|
1358
1401
|
const [prevNote, prevChannelNumber] = prev;
|
|
1359
1402
|
if (prevNote && !prevNote.ending) {
|
|
1360
|
-
this.scheduleNoteOff(prevChannelNumber, prevNote
|
|
1361
|
-
startTime, true
|
|
1362
|
-
undefined);
|
|
1403
|
+
this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
|
|
1404
|
+
startTime, true);
|
|
1363
1405
|
}
|
|
1364
1406
|
}
|
|
1365
1407
|
this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
|
|
@@ -1378,9 +1420,8 @@ class Midy {
|
|
|
1378
1420
|
channelNumber;
|
|
1379
1421
|
const prevNote = this.drumExclusiveClassNotes[index];
|
|
1380
1422
|
if (prevNote && !prevNote.ending) {
|
|
1381
|
-
this.scheduleNoteOff(channelNumber, prevNote
|
|
1382
|
-
startTime, true
|
|
1383
|
-
undefined);
|
|
1423
|
+
this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
|
|
1424
|
+
startTime, true);
|
|
1384
1425
|
}
|
|
1385
1426
|
this.drumExclusiveClassNotes[index] = note;
|
|
1386
1427
|
}
|
|
@@ -1391,7 +1432,7 @@ class Midy {
|
|
|
1391
1432
|
return !((programNumber === 48 && noteNumber === 88) ||
|
|
1392
1433
|
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
1393
1434
|
}
|
|
1394
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime,
|
|
1435
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
1395
1436
|
const channel = this.channels[channelNumber];
|
|
1396
1437
|
const bankNumber = this.calcBank(channel, channelNumber);
|
|
1397
1438
|
const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
|
|
@@ -1402,7 +1443,8 @@ class Midy {
|
|
|
1402
1443
|
if (!voice)
|
|
1403
1444
|
return;
|
|
1404
1445
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
1405
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime,
|
|
1446
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
1447
|
+
note.noteOffEvent = noteOffEvent;
|
|
1406
1448
|
note.gainL.connect(channel.gainL);
|
|
1407
1449
|
note.gainR.connect(channel.gainR);
|
|
1408
1450
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -1411,20 +1453,13 @@ class Midy {
|
|
|
1411
1453
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1412
1454
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1413
1455
|
const scheduledNotes = channel.scheduledNotes;
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
noteList.push(note);
|
|
1417
|
-
}
|
|
1418
|
-
else {
|
|
1419
|
-
noteList = [note];
|
|
1420
|
-
scheduledNotes.set(noteNumber, noteList);
|
|
1421
|
-
}
|
|
1456
|
+
note.index = scheduledNotes.length;
|
|
1457
|
+
scheduledNotes.push(note);
|
|
1422
1458
|
if (this.isDrumNoteOffException(channel, noteNumber)) {
|
|
1423
1459
|
const stopTime = startTime + note.bufferSource.buffer.duration;
|
|
1424
|
-
const index = noteList.length - 1;
|
|
1425
1460
|
const promise = new Promise((resolve) => {
|
|
1426
1461
|
note.bufferSource.onended = () => {
|
|
1427
|
-
|
|
1462
|
+
scheduledNotes[note.index] = undefined;
|
|
1428
1463
|
this.disconnectNote(note);
|
|
1429
1464
|
resolve();
|
|
1430
1465
|
};
|
|
@@ -1432,10 +1467,23 @@ class Midy {
|
|
|
1432
1467
|
});
|
|
1433
1468
|
this.notePromises.push(promise);
|
|
1434
1469
|
}
|
|
1470
|
+
else if (noteOffEvent) {
|
|
1471
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1472
|
+
const portamentoTime = this.getPortamentoTime(channel, note);
|
|
1473
|
+
const portamentoEndTime = startTime + portamentoTime;
|
|
1474
|
+
const notePromise = this.scheduleNoteOff(channelNumber, note, 0, // velocity
|
|
1475
|
+
Math.max(noteOffEvent.startTime, portamentoEndTime), false);
|
|
1476
|
+
this.notePromises.push(notePromise);
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
|
|
1480
|
+
this.notePromises.push(notePromise);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1435
1483
|
}
|
|
1436
1484
|
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1437
1485
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1438
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime,
|
|
1486
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
|
|
1439
1487
|
}
|
|
1440
1488
|
disconnectNote(note) {
|
|
1441
1489
|
note.bufferSource.disconnect();
|
|
@@ -1460,8 +1508,7 @@ class Midy {
|
|
|
1460
1508
|
note.chorusEffectsSend.disconnect();
|
|
1461
1509
|
}
|
|
1462
1510
|
}
|
|
1463
|
-
stopNote(
|
|
1464
|
-
const note = noteList[index];
|
|
1511
|
+
stopNote(channel, note, endTime, stopTime) {
|
|
1465
1512
|
note.volumeEnvelopeNode.gain
|
|
1466
1513
|
.cancelScheduledValues(endTime)
|
|
1467
1514
|
.linearRampToValueAtTime(0, stopTime);
|
|
@@ -1471,73 +1518,58 @@ class Midy {
|
|
|
1471
1518
|
}, stopTime);
|
|
1472
1519
|
return new Promise((resolve) => {
|
|
1473
1520
|
note.bufferSource.onended = () => {
|
|
1474
|
-
|
|
1521
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
1475
1522
|
this.disconnectNote(note);
|
|
1476
1523
|
resolve();
|
|
1477
1524
|
};
|
|
1478
1525
|
note.bufferSource.stop(stopTime);
|
|
1479
1526
|
});
|
|
1480
1527
|
}
|
|
1481
|
-
|
|
1482
|
-
for (let i = 0; i < noteList.length; i++) {
|
|
1483
|
-
const note = noteList[i];
|
|
1484
|
-
if (!note)
|
|
1485
|
-
continue;
|
|
1486
|
-
if (note.ending)
|
|
1487
|
-
continue;
|
|
1488
|
-
return [note, i];
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
|
|
1528
|
+
scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
|
|
1492
1529
|
const channel = this.channels[channelNumber];
|
|
1493
|
-
if (this.isDrumNoteOffException(channel, noteNumber))
|
|
1530
|
+
if (this.isDrumNoteOffException(channel, note.noteNumber))
|
|
1494
1531
|
return;
|
|
1495
1532
|
const state = channel.state;
|
|
1496
1533
|
if (!force) {
|
|
1497
1534
|
if (0.5 <= state.sustainPedal)
|
|
1498
1535
|
return;
|
|
1499
|
-
if (channel.
|
|
1536
|
+
if (0.5 <= channel.state.sostenutoPedal)
|
|
1500
1537
|
return;
|
|
1501
1538
|
}
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
note
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1523
|
-
note.filterNode.frequency
|
|
1524
|
-
.cancelScheduledValues(endTime)
|
|
1525
|
-
.linearRampToValueAtTime(0, modRelease);
|
|
1526
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1527
|
-
return this.stopNote(endTime, stopTime, noteList, i);
|
|
1539
|
+
const volRelease = endTime +
|
|
1540
|
+
note.voiceParams.volRelease * channel.state.releaseTime * 2;
|
|
1541
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1542
|
+
note.filterNode.frequency
|
|
1543
|
+
.cancelScheduledValues(endTime)
|
|
1544
|
+
.linearRampToValueAtTime(0, modRelease);
|
|
1545
|
+
const stopTime = Math.min(volRelease, modRelease);
|
|
1546
|
+
return this.stopNote(channel, note, endTime, stopTime);
|
|
1547
|
+
}
|
|
1548
|
+
findNoteOffTarget(channel, noteNumber) {
|
|
1549
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
1550
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
1551
|
+
const note = scheduledNotes[i];
|
|
1552
|
+
if (!note)
|
|
1553
|
+
continue;
|
|
1554
|
+
if (note.ending)
|
|
1555
|
+
continue;
|
|
1556
|
+
if (note.noteNumber !== noteNumber)
|
|
1557
|
+
continue;
|
|
1558
|
+
return note;
|
|
1528
1559
|
}
|
|
1529
1560
|
}
|
|
1530
1561
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1531
1562
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1532
|
-
|
|
1533
|
-
|
|
1563
|
+
const channel = this.channels[channelNumber];
|
|
1564
|
+
const note = this.findNoteOffTarget(channel, noteNumber);
|
|
1565
|
+
return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
|
|
1534
1566
|
}
|
|
1535
1567
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
1536
1568
|
const velocity = halfVelocity * 2;
|
|
1537
1569
|
const channel = this.channels[channelNumber];
|
|
1538
1570
|
const promises = [];
|
|
1539
1571
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1540
|
-
const promise = this.
|
|
1572
|
+
const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
|
|
1541
1573
|
promises.push(promise);
|
|
1542
1574
|
}
|
|
1543
1575
|
channel.sustainNotes = [];
|
|
@@ -1547,12 +1579,14 @@ class Midy {
|
|
|
1547
1579
|
const velocity = halfVelocity * 2;
|
|
1548
1580
|
const channel = this.channels[channelNumber];
|
|
1549
1581
|
const promises = [];
|
|
1582
|
+
const sostenutoNotes = channel.sostenutoNotes;
|
|
1550
1583
|
channel.state.sostenutoPedal = 0;
|
|
1551
|
-
|
|
1552
|
-
const
|
|
1584
|
+
for (let i = 0; i < sostenutoNotes.length; i++) {
|
|
1585
|
+
const note = sostenutoNotes[i];
|
|
1586
|
+
const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
|
|
1553
1587
|
promises.push(promise);
|
|
1554
|
-
}
|
|
1555
|
-
channel.sostenutoNotes
|
|
1588
|
+
}
|
|
1589
|
+
channel.sostenutoNotes = [];
|
|
1556
1590
|
return promises;
|
|
1557
1591
|
}
|
|
1558
1592
|
handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
|
|
@@ -1581,11 +1615,11 @@ class Midy {
|
|
|
1581
1615
|
const channel = this.channels[channelNumber];
|
|
1582
1616
|
channel.state.polyphonicKeyPressure = pressure / 127;
|
|
1583
1617
|
const table = channel.polyphonicKeyPressureTable;
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1618
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1619
|
+
if (note.noteNumber === noteNumber) {
|
|
1620
|
+
this.setControllerParameters(channel, note, table);
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1589
1623
|
this.applyVoiceParams(channel, 10);
|
|
1590
1624
|
}
|
|
1591
1625
|
handleProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
@@ -1615,7 +1649,7 @@ class Midy {
|
|
|
1615
1649
|
channel.detune += pressureDepth * (next - prev);
|
|
1616
1650
|
}
|
|
1617
1651
|
const table = channel.channelPressureTable;
|
|
1618
|
-
this.
|
|
1652
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1619
1653
|
this.setControllerParameters(channel, note, table);
|
|
1620
1654
|
});
|
|
1621
1655
|
this.applyVoiceParams(channel, 13);
|
|
@@ -1823,8 +1857,8 @@ class Midy {
|
|
|
1823
1857
|
if (key in voiceParams)
|
|
1824
1858
|
noteVoiceParams[key] = voiceParams[key];
|
|
1825
1859
|
}
|
|
1826
|
-
if (0.5 <= channel.state.portamento && note.
|
|
1827
|
-
this.
|
|
1860
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1861
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1828
1862
|
}
|
|
1829
1863
|
else {
|
|
1830
1864
|
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
@@ -1847,42 +1881,42 @@ class Midy {
|
|
|
1847
1881
|
});
|
|
1848
1882
|
}
|
|
1849
1883
|
createControlChangeHandlers() {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1884
|
+
const handlers = new Array(128);
|
|
1885
|
+
handlers[0] = this.setBankMSB;
|
|
1886
|
+
handlers[1] = this.setModulationDepth;
|
|
1887
|
+
handlers[5] = this.setPortamentoTime;
|
|
1888
|
+
handlers[6] = this.dataEntryMSB;
|
|
1889
|
+
handlers[7] = this.setVolume;
|
|
1890
|
+
handlers[10] = this.setPan;
|
|
1891
|
+
handlers[11] = this.setExpression;
|
|
1892
|
+
handlers[32] = this.setBankLSB;
|
|
1893
|
+
handlers[38] = this.dataEntryLSB;
|
|
1894
|
+
handlers[64] = this.setSustainPedal;
|
|
1895
|
+
handlers[65] = this.setPortamento;
|
|
1896
|
+
handlers[66] = this.setSostenutoPedal;
|
|
1897
|
+
handlers[67] = this.setSoftPedal;
|
|
1898
|
+
handlers[71] = this.setFilterResonance;
|
|
1899
|
+
handlers[72] = this.setReleaseTime;
|
|
1900
|
+
handlers[73] = this.setAttackTime;
|
|
1901
|
+
handlers[74] = this.setBrightness;
|
|
1902
|
+
handlers[75] = this.setDecayTime;
|
|
1903
|
+
handlers[76] = this.setVibratoRate;
|
|
1904
|
+
handlers[77] = this.setVibratoDepth;
|
|
1905
|
+
handlers[78] = this.setVibratoDelay;
|
|
1906
|
+
handlers[91] = this.setReverbSendLevel;
|
|
1907
|
+
handlers[93] = this.setChorusSendLevel;
|
|
1908
|
+
handlers[96] = this.dataIncrement;
|
|
1909
|
+
handlers[97] = this.dataDecrement;
|
|
1910
|
+
handlers[100] = this.setRPNLSB;
|
|
1911
|
+
handlers[101] = this.setRPNMSB;
|
|
1912
|
+
handlers[120] = this.allSoundOff;
|
|
1913
|
+
handlers[121] = this.resetAllControllers;
|
|
1914
|
+
handlers[123] = this.allNotesOff;
|
|
1915
|
+
handlers[124] = this.omniOff;
|
|
1916
|
+
handlers[125] = this.omniOn;
|
|
1917
|
+
handlers[126] = this.monoOn;
|
|
1918
|
+
handlers[127] = this.polyOn;
|
|
1919
|
+
return handlers;
|
|
1886
1920
|
}
|
|
1887
1921
|
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1888
1922
|
const handler = this.controlChangeHandlers[controllerType];
|
|
@@ -1919,9 +1953,33 @@ class Midy {
|
|
|
1919
1953
|
channel.state.modulationDepth = modulation / 127;
|
|
1920
1954
|
this.updateModulation(channel, scheduleTime);
|
|
1921
1955
|
}
|
|
1922
|
-
|
|
1956
|
+
updatePortamento(channel, scheduleTime) {
|
|
1957
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1958
|
+
if (0.5 <= channel.state.portamento) {
|
|
1959
|
+
if (0 <= note.portamentoNoteNumber) {
|
|
1960
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
1961
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1962
|
+
this.setPortamentoPitchEnvelope(note, scheduleTime);
|
|
1963
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
else {
|
|
1967
|
+
if (0 <= note.portamentoNoteNumber) {
|
|
1968
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1969
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
1970
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1971
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
|
|
1923
1977
|
const channel = this.channels[channelNumber];
|
|
1978
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1924
1979
|
channel.state.portamentoTime = portamentoTime / 127;
|
|
1980
|
+
if (channel.isDrum)
|
|
1981
|
+
return;
|
|
1982
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1925
1983
|
}
|
|
1926
1984
|
setKeyBasedVolume(channel, scheduleTime) {
|
|
1927
1985
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1979,7 +2037,7 @@ class Midy {
|
|
|
1979
2037
|
}
|
|
1980
2038
|
dataEntryLSB(channelNumber, value, scheduleTime) {
|
|
1981
2039
|
this.channels[channelNumber].dataLSB = value;
|
|
1982
|
-
this.handleRPN(channelNumber, scheduleTime);
|
|
2040
|
+
this.handleRPN(channelNumber, 0, scheduleTime);
|
|
1983
2041
|
}
|
|
1984
2042
|
updateChannelVolume(channel, scheduleTime) {
|
|
1985
2043
|
const state = channel.state;
|
|
@@ -2007,11 +2065,13 @@ class Midy {
|
|
|
2007
2065
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
2008
2066
|
}
|
|
2009
2067
|
}
|
|
2010
|
-
setPortamento(channelNumber, value) {
|
|
2068
|
+
setPortamento(channelNumber, value, scheduleTime) {
|
|
2011
2069
|
const channel = this.channels[channelNumber];
|
|
2012
2070
|
if (channel.isDrum)
|
|
2013
2071
|
return;
|
|
2072
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
2014
2073
|
channel.state.portamento = value / 127;
|
|
2074
|
+
this.updatePortamento(channel, scheduleTime);
|
|
2015
2075
|
}
|
|
2016
2076
|
setSostenutoPedal(channelNumber, value, scheduleTime) {
|
|
2017
2077
|
const channel = this.channels[channelNumber];
|
|
@@ -2020,7 +2080,11 @@ class Midy {
|
|
|
2020
2080
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2021
2081
|
channel.state.sostenutoPedal = value / 127;
|
|
2022
2082
|
if (64 <= value) {
|
|
2023
|
-
|
|
2083
|
+
const sostenutoNotes = [];
|
|
2084
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2085
|
+
sostenutoNotes.push(note);
|
|
2086
|
+
});
|
|
2087
|
+
channel.sostenutoNotes = sostenutoNotes;
|
|
2024
2088
|
}
|
|
2025
2089
|
else {
|
|
2026
2090
|
this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
|
|
@@ -2034,9 +2098,9 @@ class Midy {
|
|
|
2034
2098
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2035
2099
|
state.softPedal = softPedal / 127;
|
|
2036
2100
|
this.processScheduledNotes(channel, (note) => {
|
|
2037
|
-
if (0.5 <= state.portamento && note.
|
|
2038
|
-
this.
|
|
2039
|
-
this.
|
|
2101
|
+
if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2102
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2103
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2040
2104
|
}
|
|
2041
2105
|
else {
|
|
2042
2106
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
@@ -2050,7 +2114,7 @@ class Midy {
|
|
|
2050
2114
|
return;
|
|
2051
2115
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2052
2116
|
const state = channel.state;
|
|
2053
|
-
state.filterResonance = filterResonance /
|
|
2117
|
+
state.filterResonance = filterResonance / 127;
|
|
2054
2118
|
this.processScheduledNotes(channel, (note) => {
|
|
2055
2119
|
const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
|
|
2056
2120
|
note.filterNode.Q.setValueAtTime(Q, scheduleTime);
|
|
@@ -2061,14 +2125,14 @@ class Midy {
|
|
|
2061
2125
|
if (channel.isDrum)
|
|
2062
2126
|
return;
|
|
2063
2127
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2064
|
-
channel.state.releaseTime = releaseTime /
|
|
2128
|
+
channel.state.releaseTime = releaseTime / 127;
|
|
2065
2129
|
}
|
|
2066
2130
|
setAttackTime(channelNumber, attackTime, scheduleTime) {
|
|
2067
2131
|
const channel = this.channels[channelNumber];
|
|
2068
2132
|
if (channel.isDrum)
|
|
2069
2133
|
return;
|
|
2070
2134
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2071
|
-
channel.state.attackTime = attackTime /
|
|
2135
|
+
channel.state.attackTime = attackTime / 127;
|
|
2072
2136
|
this.processScheduledNotes(channel, (note) => {
|
|
2073
2137
|
if (note.startTime < scheduleTime)
|
|
2074
2138
|
return false;
|
|
@@ -2081,10 +2145,10 @@ class Midy {
|
|
|
2081
2145
|
return;
|
|
2082
2146
|
const state = channel.state;
|
|
2083
2147
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2084
|
-
state.brightness = brightness /
|
|
2148
|
+
state.brightness = brightness / 127;
|
|
2085
2149
|
this.processScheduledNotes(channel, (note) => {
|
|
2086
|
-
if (0.5 <= state.portamento && note.
|
|
2087
|
-
this.
|
|
2150
|
+
if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2151
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2088
2152
|
}
|
|
2089
2153
|
else {
|
|
2090
2154
|
this.setFilterEnvelope(channel, note);
|
|
@@ -2096,7 +2160,7 @@ class Midy {
|
|
|
2096
2160
|
if (channel.isDrum)
|
|
2097
2161
|
return;
|
|
2098
2162
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2099
|
-
channel.state.decayTime = dacayTime /
|
|
2163
|
+
channel.state.decayTime = dacayTime / 127;
|
|
2100
2164
|
this.processScheduledNotes(channel, (note) => {
|
|
2101
2165
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
2102
2166
|
});
|
|
@@ -2106,7 +2170,7 @@ class Midy {
|
|
|
2106
2170
|
if (channel.isDrum)
|
|
2107
2171
|
return;
|
|
2108
2172
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2109
|
-
channel.state.vibratoRate = vibratoRate /
|
|
2173
|
+
channel.state.vibratoRate = vibratoRate / 127;
|
|
2110
2174
|
if (channel.vibratoDepth <= 0)
|
|
2111
2175
|
return;
|
|
2112
2176
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -2119,7 +2183,7 @@ class Midy {
|
|
|
2119
2183
|
return;
|
|
2120
2184
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2121
2185
|
const prev = channel.state.vibratoDepth;
|
|
2122
|
-
channel.state.vibratoDepth = vibratoDepth /
|
|
2186
|
+
channel.state.vibratoDepth = vibratoDepth / 127;
|
|
2123
2187
|
if (0 < prev) {
|
|
2124
2188
|
this.processScheduledNotes(channel, (note) => {
|
|
2125
2189
|
this.setFreqVibLFO(channel, note, scheduleTime);
|
|
@@ -2131,12 +2195,12 @@ class Midy {
|
|
|
2131
2195
|
});
|
|
2132
2196
|
}
|
|
2133
2197
|
}
|
|
2134
|
-
setVibratoDelay(channelNumber, vibratoDelay) {
|
|
2198
|
+
setVibratoDelay(channelNumber, vibratoDelay, scheduleTime) {
|
|
2135
2199
|
const channel = this.channels[channelNumber];
|
|
2136
2200
|
if (channel.isDrum)
|
|
2137
2201
|
return;
|
|
2138
2202
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2139
|
-
channel.state.vibratoDelay = vibratoDelay /
|
|
2203
|
+
channel.state.vibratoDelay = vibratoDelay / 127;
|
|
2140
2204
|
if (0 < channel.state.vibratoDepth) {
|
|
2141
2205
|
this.processScheduledNotes(channel, (note) => {
|
|
2142
2206
|
this.startVibrato(channel, note, scheduleTime);
|
|
@@ -2258,12 +2322,14 @@ class Midy {
|
|
|
2258
2322
|
}
|
|
2259
2323
|
}
|
|
2260
2324
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
|
|
2261
|
-
dataIncrement(channelNumber) {
|
|
2262
|
-
this.
|
|
2325
|
+
dataIncrement(channelNumber, scheduleTime) {
|
|
2326
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
2327
|
+
this.handleRPN(channelNumber, 1, scheduleTime);
|
|
2263
2328
|
}
|
|
2264
2329
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
|
|
2265
|
-
dataDecrement(channelNumber) {
|
|
2266
|
-
this.
|
|
2330
|
+
dataDecrement(channelNumber, scheduleTime) {
|
|
2331
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
2332
|
+
this.handleRPN(channelNumber, -1, scheduleTime);
|
|
2267
2333
|
}
|
|
2268
2334
|
setRPNMSB(channelNumber, value) {
|
|
2269
2335
|
this.channels[channelNumber].rpnMSB = value;
|
|
@@ -2273,7 +2339,7 @@ class Midy {
|
|
|
2273
2339
|
}
|
|
2274
2340
|
dataEntryMSB(channelNumber, value, scheduleTime) {
|
|
2275
2341
|
this.channels[channelNumber].dataMSB = value;
|
|
2276
|
-
this.handleRPN(channelNumber, scheduleTime);
|
|
2342
|
+
this.handleRPN(channelNumber, 0, scheduleTime);
|
|
2277
2343
|
}
|
|
2278
2344
|
handlePitchBendRangeRPN(channelNumber, scheduleTime) {
|
|
2279
2345
|
const channel = this.channels[channelNumber];
|
|
@@ -2347,21 +2413,29 @@ class Midy {
|
|
|
2347
2413
|
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
2348
2414
|
}
|
|
2349
2415
|
resetAllStates(channelNumber) {
|
|
2416
|
+
const scheduleTime = this.audioContext.currentTime;
|
|
2350
2417
|
const channel = this.channels[channelNumber];
|
|
2351
2418
|
const state = channel.state;
|
|
2352
|
-
|
|
2353
|
-
|
|
2419
|
+
const entries = Object.entries(defaultControllerState);
|
|
2420
|
+
for (const [key, { type, defaultValue }] of entries) {
|
|
2421
|
+
if (128 <= type) {
|
|
2422
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2423
|
+
}
|
|
2424
|
+
else {
|
|
2425
|
+
state[key] = defaultValue;
|
|
2426
|
+
}
|
|
2354
2427
|
}
|
|
2355
|
-
for (const
|
|
2356
|
-
channel[
|
|
2428
|
+
for (const key of Object.keys(this.constructor.channelSettings)) {
|
|
2429
|
+
channel[key] = this.constructor.channelSettings[key];
|
|
2357
2430
|
}
|
|
2431
|
+
this.resetChannelTable(channel);
|
|
2358
2432
|
this.mode = "GM2";
|
|
2359
2433
|
this.masterFineTuning = 0; // cb
|
|
2360
2434
|
this.masterCoarseTuning = 0; // cb
|
|
2361
2435
|
}
|
|
2362
2436
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
|
|
2363
|
-
resetAllControllers(channelNumber) {
|
|
2364
|
-
const
|
|
2437
|
+
resetAllControllers(channelNumber, _value, scheduleTime) {
|
|
2438
|
+
const keys = [
|
|
2365
2439
|
"polyphonicKeyPressure",
|
|
2366
2440
|
"channelPressure",
|
|
2367
2441
|
"pitchWheel",
|
|
@@ -2374,10 +2448,17 @@ class Midy {
|
|
|
2374
2448
|
];
|
|
2375
2449
|
const channel = this.channels[channelNumber];
|
|
2376
2450
|
const state = channel.state;
|
|
2377
|
-
for (let i = 0; i <
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2451
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2452
|
+
const key = keys[i];
|
|
2453
|
+
const { type, defaultValue } = defaultControllerState[key];
|
|
2454
|
+
if (128 <= type) {
|
|
2455
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2456
|
+
}
|
|
2457
|
+
else {
|
|
2458
|
+
state[key] = defaultValue;
|
|
2459
|
+
}
|
|
2380
2460
|
}
|
|
2461
|
+
this.setPitchBend(channelNumber, 8192, scheduleTime);
|
|
2381
2462
|
const settingTypes = [
|
|
2382
2463
|
"rpnMSB",
|
|
2383
2464
|
"rpnLSB",
|
|
@@ -2622,7 +2703,7 @@ class Midy {
|
|
|
2622
2703
|
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2623
2704
|
}
|
|
2624
2705
|
getReverbTime(value) {
|
|
2625
|
-
return Math.
|
|
2706
|
+
return Math.exp((value - 40) * 0.025);
|
|
2626
2707
|
}
|
|
2627
2708
|
// mean free path equation
|
|
2628
2709
|
// https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
|
|
@@ -2856,7 +2937,13 @@ class Midy {
|
|
|
2856
2937
|
setControllerParameters(channel, note, table) {
|
|
2857
2938
|
if (table[0] !== 64)
|
|
2858
2939
|
this.updateDetune(channel, note);
|
|
2859
|
-
if (
|
|
2940
|
+
if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
|
|
2941
|
+
if (table[1] !== 64)
|
|
2942
|
+
this.setPortamentoFilterEnvelope(channel, note);
|
|
2943
|
+
if (table[2] !== 64)
|
|
2944
|
+
this.setPortamentoVolumeEnvelope(channel, note);
|
|
2945
|
+
}
|
|
2946
|
+
else {
|
|
2860
2947
|
if (table[1] !== 64)
|
|
2861
2948
|
this.setFilterEnvelope(channel, note);
|
|
2862
2949
|
if (table[2] !== 64)
|
|
@@ -2884,8 +2971,13 @@ class Midy {
|
|
|
2884
2971
|
initControlTable() {
|
|
2885
2972
|
const channelCount = 128;
|
|
2886
2973
|
const slotSize = 6;
|
|
2887
|
-
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2888
2974
|
const table = new Uint8Array(channelCount * slotSize);
|
|
2975
|
+
return this.resetControlTable(table);
|
|
2976
|
+
}
|
|
2977
|
+
resetControlTable(table) {
|
|
2978
|
+
const channelCount = 128;
|
|
2979
|
+
const slotSize = 6;
|
|
2980
|
+
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2889
2981
|
for (let ch = 0; ch < channelCount; ch++) {
|
|
2890
2982
|
const offset = ch * slotSize;
|
|
2891
2983
|
table.set(defaultValues, offset);
|