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