@marmooo/midy 0.3.1 → 0.3.3
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 +17 -55
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +159 -184
- package/esm/midy-GM2.d.ts +34 -90
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +421 -342
- package/esm/midy-GMLite.d.ts +17 -55
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +169 -199
- package/esm/midy.d.ts +37 -114
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +451 -376
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +17 -55
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +159 -184
- package/script/midy-GM2.d.ts +34 -90
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +421 -342
- package/script/midy-GMLite.d.ts +17 -55
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +169 -199
- package/script/midy.d.ts +37 -114
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +451 -376
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, "ending", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: false
|
|
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
|
}
|
|
@@ -510,17 +470,37 @@ export class Midy {
|
|
|
510
470
|
}
|
|
511
471
|
}
|
|
512
472
|
}
|
|
513
|
-
async loadSoundFont(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
473
|
+
async loadSoundFont(input) {
|
|
474
|
+
let uint8Array;
|
|
475
|
+
if (typeof input === "string") {
|
|
476
|
+
const response = await fetch(input);
|
|
477
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
478
|
+
uint8Array = new Uint8Array(arrayBuffer);
|
|
479
|
+
}
|
|
480
|
+
else if (input instanceof Uint8Array) {
|
|
481
|
+
uint8Array = input;
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
throw new TypeError("input must be a URL string or Uint8Array");
|
|
485
|
+
}
|
|
486
|
+
const parsed = parse(uint8Array);
|
|
517
487
|
const soundFont = new SoundFont(parsed);
|
|
518
488
|
this.addSoundFont(soundFont);
|
|
519
489
|
}
|
|
520
|
-
async loadMIDI(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
490
|
+
async loadMIDI(input) {
|
|
491
|
+
let uint8Array;
|
|
492
|
+
if (typeof input === "string") {
|
|
493
|
+
const response = await fetch(input);
|
|
494
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
495
|
+
uint8Array = new Uint8Array(arrayBuffer);
|
|
496
|
+
}
|
|
497
|
+
else if (input instanceof Uint8Array) {
|
|
498
|
+
uint8Array = input;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
throw new TypeError("input must be a URL string or Uint8Array");
|
|
502
|
+
}
|
|
503
|
+
const midi = parseMidi(uint8Array);
|
|
524
504
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
525
505
|
const midiData = this.extractMidiData(midi);
|
|
526
506
|
this.instruments = midiData.instruments;
|
|
@@ -541,22 +521,29 @@ export class Midy {
|
|
|
541
521
|
merger,
|
|
542
522
|
};
|
|
543
523
|
}
|
|
524
|
+
resetChannelTable(channel) {
|
|
525
|
+
this.resetControlTable(channel.controlTable);
|
|
526
|
+
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
527
|
+
channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
|
|
528
|
+
channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
|
|
529
|
+
channel.keyBasedInstrumentControlTable.fill(-1);
|
|
530
|
+
}
|
|
544
531
|
createChannels(audioContext) {
|
|
545
532
|
const channels = Array.from({ length: this.numChannels }, () => {
|
|
546
533
|
return {
|
|
547
534
|
currentBufferSource: null,
|
|
548
535
|
isDrum: false,
|
|
549
|
-
...this.constructor.channelSettings,
|
|
550
536
|
state: new ControllerState(),
|
|
551
|
-
|
|
537
|
+
...this.constructor.channelSettings,
|
|
552
538
|
...this.setChannelAudioNodes(audioContext),
|
|
553
|
-
scheduledNotes:
|
|
539
|
+
scheduledNotes: [],
|
|
554
540
|
sustainNotes: [],
|
|
555
|
-
sostenutoNotes:
|
|
541
|
+
sostenutoNotes: [],
|
|
542
|
+
controlTable: this.initControlTable(),
|
|
556
543
|
scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
|
|
557
544
|
channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
|
|
558
545
|
polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
|
|
559
|
-
keyBasedInstrumentControlTable: new Int8Array(128 * 128)
|
|
546
|
+
keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
|
|
560
547
|
};
|
|
561
548
|
});
|
|
562
549
|
return channels;
|
|
@@ -590,56 +577,39 @@ export class Midy {
|
|
|
590
577
|
return audioBuffer;
|
|
591
578
|
}
|
|
592
579
|
}
|
|
593
|
-
|
|
580
|
+
isLoopDrum(channel, noteNumber) {
|
|
581
|
+
const programNumber = channel.programNumber;
|
|
582
|
+
return ((programNumber === 48 && noteNumber === 88) ||
|
|
583
|
+
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
584
|
+
}
|
|
585
|
+
createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
|
|
594
586
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
595
587
|
bufferSource.buffer = audioBuffer;
|
|
596
588
|
bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
|
|
589
|
+
if (channel.isDrum) {
|
|
590
|
+
bufferSource.loop = this.isLoopDrum(channel, noteNumber);
|
|
591
|
+
}
|
|
597
592
|
if (bufferSource.loop) {
|
|
598
593
|
bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
599
594
|
bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
|
|
600
595
|
}
|
|
601
596
|
return bufferSource;
|
|
602
597
|
}
|
|
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) {
|
|
598
|
+
async scheduleTimelineEvents(t, resumeTime, queueIndex) {
|
|
622
599
|
while (queueIndex < this.timeline.length) {
|
|
623
600
|
const event = this.timeline[queueIndex];
|
|
624
601
|
if (event.startTime > t + this.lookAhead)
|
|
625
602
|
break;
|
|
626
|
-
const
|
|
603
|
+
const delay = this.startDelay - resumeTime;
|
|
604
|
+
const startTime = event.startTime + delay;
|
|
627
605
|
switch (event.type) {
|
|
628
606
|
case "noteOn":
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
break;
|
|
632
|
-
}
|
|
633
|
-
/* falls through */
|
|
607
|
+
await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
608
|
+
break;
|
|
634
609
|
case "noteOff": {
|
|
635
|
-
const
|
|
636
|
-
if (
|
|
637
|
-
portamentoTarget.portamento = true;
|
|
638
|
-
const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
|
|
639
|
-
portamentoTarget?.noteNumber);
|
|
640
|
-
if (notePromise) {
|
|
610
|
+
const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
611
|
+
if (notePromise)
|
|
641
612
|
this.notePromises.push(notePromise);
|
|
642
|
-
}
|
|
643
613
|
break;
|
|
644
614
|
}
|
|
645
615
|
case "noteAftertouch":
|
|
@@ -678,7 +648,7 @@ export class Midy {
|
|
|
678
648
|
this.isPaused = false;
|
|
679
649
|
this.startTime = this.audioContext.currentTime;
|
|
680
650
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
681
|
-
let
|
|
651
|
+
let resumeTime = this.resumeTime - this.startTime;
|
|
682
652
|
this.notePromises = [];
|
|
683
653
|
const schedulePlayback = async () => {
|
|
684
654
|
if (queueIndex >= this.timeline.length) {
|
|
@@ -687,18 +657,21 @@ export class Midy {
|
|
|
687
657
|
this.exclusiveClassNotes.fill(undefined);
|
|
688
658
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
689
659
|
this.audioBufferCache.clear();
|
|
660
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
661
|
+
this.resetAllStates(i);
|
|
662
|
+
}
|
|
690
663
|
resolve();
|
|
691
664
|
return;
|
|
692
665
|
}
|
|
693
666
|
const now = this.audioContext.currentTime;
|
|
694
|
-
const t = now +
|
|
695
|
-
queueIndex = await this.scheduleTimelineEvents(t,
|
|
667
|
+
const t = now + resumeTime;
|
|
668
|
+
queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
|
|
696
669
|
if (this.isPausing) {
|
|
697
670
|
await this.stopNotes(0, true, now);
|
|
698
671
|
this.notePromises = [];
|
|
699
|
-
resolve();
|
|
700
672
|
this.isPausing = false;
|
|
701
673
|
this.isPaused = true;
|
|
674
|
+
resolve();
|
|
702
675
|
return;
|
|
703
676
|
}
|
|
704
677
|
else if (this.isStopping) {
|
|
@@ -707,9 +680,12 @@ export class Midy {
|
|
|
707
680
|
this.exclusiveClassNotes.fill(undefined);
|
|
708
681
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
709
682
|
this.audioBufferCache.clear();
|
|
710
|
-
|
|
683
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
684
|
+
this.resetAllStates(i);
|
|
685
|
+
}
|
|
711
686
|
this.isStopping = false;
|
|
712
687
|
this.isPaused = false;
|
|
688
|
+
resolve();
|
|
713
689
|
return;
|
|
714
690
|
}
|
|
715
691
|
else if (this.isSeeking) {
|
|
@@ -718,7 +694,7 @@ export class Midy {
|
|
|
718
694
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
719
695
|
this.startTime = this.audioContext.currentTime;
|
|
720
696
|
queueIndex = this.getQueueIndex(this.resumeTime);
|
|
721
|
-
|
|
697
|
+
resumeTime = this.resumeTime - this.startTime;
|
|
722
698
|
this.isSeeking = false;
|
|
723
699
|
await schedulePlayback();
|
|
724
700
|
}
|
|
@@ -741,6 +717,7 @@ export class Midy {
|
|
|
741
717
|
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
742
718
|
}
|
|
743
719
|
extractMidiData(midi) {
|
|
720
|
+
this.audioBufferCounter.clear();
|
|
744
721
|
const instruments = new Set();
|
|
745
722
|
const timeline = [];
|
|
746
723
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -845,9 +822,8 @@ export class Midy {
|
|
|
845
822
|
stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
|
|
846
823
|
const channel = this.channels[channelNumber];
|
|
847
824
|
const promises = [];
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
|
|
825
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
826
|
+
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
851
827
|
this.notePromises.push(promise);
|
|
852
828
|
promises.push(promise);
|
|
853
829
|
});
|
|
@@ -857,11 +833,11 @@ export class Midy {
|
|
|
857
833
|
const channel = this.channels[channelNumber];
|
|
858
834
|
const promises = [];
|
|
859
835
|
this.processScheduledNotes(channel, (note) => {
|
|
860
|
-
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force
|
|
836
|
+
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
861
837
|
this.notePromises.push(promise);
|
|
862
838
|
promises.push(promise);
|
|
863
839
|
});
|
|
864
|
-
channel.scheduledNotes
|
|
840
|
+
channel.scheduledNotes = [];
|
|
865
841
|
return Promise.all(promises);
|
|
866
842
|
}
|
|
867
843
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -882,9 +858,6 @@ export class Midy {
|
|
|
882
858
|
if (!this.isPlaying)
|
|
883
859
|
return;
|
|
884
860
|
this.isStopping = true;
|
|
885
|
-
for (let i = 0; i < this.channels.length; i++) {
|
|
886
|
-
this.resetAllStates(i);
|
|
887
|
-
}
|
|
888
861
|
}
|
|
889
862
|
pause() {
|
|
890
863
|
if (!this.isPlaying || this.isPaused)
|
|
@@ -919,37 +892,28 @@ export class Midy {
|
|
|
919
892
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
920
893
|
}
|
|
921
894
|
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;
|
|
895
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
896
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
897
|
+
const note = scheduledNotes[i];
|
|
898
|
+
if (!note)
|
|
899
|
+
continue;
|
|
900
|
+
if (note.ending)
|
|
901
|
+
continue;
|
|
902
|
+
callback(note);
|
|
903
|
+
}
|
|
942
904
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
905
|
+
processActiveNotes(channel, scheduleTime, callback) {
|
|
906
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
907
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
908
|
+
const note = scheduledNotes[i];
|
|
946
909
|
if (!note)
|
|
947
|
-
|
|
910
|
+
continue;
|
|
911
|
+
if (note.ending)
|
|
912
|
+
continue;
|
|
948
913
|
if (scheduleTime < note.startTime)
|
|
949
914
|
continue;
|
|
950
|
-
|
|
915
|
+
callback(note);
|
|
951
916
|
}
|
|
952
|
-
return noteList[0];
|
|
953
917
|
}
|
|
954
918
|
createConvolutionReverbImpulse(audioContext, decay, preDecay) {
|
|
955
919
|
const sampleRate = audioContext.sampleRate;
|
|
@@ -1116,24 +1080,94 @@ export class Midy {
|
|
|
1116
1080
|
const noteDetune = this.calcNoteDetune(channel, note);
|
|
1117
1081
|
const pitchControl = this.getPitchControl(channel, note);
|
|
1118
1082
|
const detune = channel.detune + noteDetune + pitchControl;
|
|
1119
|
-
note.
|
|
1120
|
-
.
|
|
1121
|
-
.
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1083
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1084
|
+
const startTime = note.startTime;
|
|
1085
|
+
const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
|
|
1086
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1087
|
+
note.bufferSource.detune
|
|
1088
|
+
.cancelScheduledValues(scheduleTime)
|
|
1089
|
+
.setValueAtTime(detune - deltaCent, scheduleTime)
|
|
1090
|
+
.linearRampToValueAtTime(detune, portamentoTime);
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
note.bufferSource.detune
|
|
1094
|
+
.cancelScheduledValues(scheduleTime)
|
|
1095
|
+
.setValueAtTime(detune, scheduleTime);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
getPortamentoTime(channel, note) {
|
|
1099
|
+
const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
|
|
1100
|
+
const value = Math.ceil(channel.state.portamentoTime * 127);
|
|
1101
|
+
return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
|
|
1102
|
+
}
|
|
1103
|
+
getPitchIncrementSpeed(value) {
|
|
1104
|
+
const points = [
|
|
1105
|
+
[0, 1000],
|
|
1106
|
+
[6, 100],
|
|
1107
|
+
[16, 20],
|
|
1108
|
+
[32, 10],
|
|
1109
|
+
[48, 5],
|
|
1110
|
+
[64, 2.5],
|
|
1111
|
+
[80, 1],
|
|
1112
|
+
[96, 0.4],
|
|
1113
|
+
[112, 0.15],
|
|
1114
|
+
[127, 0.01],
|
|
1115
|
+
];
|
|
1116
|
+
const logPoints = new Array(points.length);
|
|
1117
|
+
for (let i = 0; i < points.length; i++) {
|
|
1118
|
+
const [x, y] = points[i];
|
|
1119
|
+
if (value === x)
|
|
1120
|
+
return y;
|
|
1121
|
+
logPoints[i] = [x, Math.log(y)];
|
|
1122
|
+
}
|
|
1123
|
+
let startIndex = 0;
|
|
1124
|
+
for (let i = 1; i < logPoints.length; i++) {
|
|
1125
|
+
if (value <= logPoints[i][0]) {
|
|
1126
|
+
startIndex = i - 1;
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const [x0, y0] = logPoints[startIndex];
|
|
1131
|
+
const [x1, y1] = logPoints[startIndex + 1];
|
|
1132
|
+
const h = x1 - x0;
|
|
1133
|
+
const t = (value - x0) / h;
|
|
1134
|
+
let m0, m1;
|
|
1135
|
+
if (startIndex === 0) {
|
|
1136
|
+
m0 = (y1 - y0) / h;
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
const [xPrev, yPrev] = logPoints[startIndex - 1];
|
|
1140
|
+
m0 = (y1 - yPrev) / (x1 - xPrev);
|
|
1141
|
+
}
|
|
1142
|
+
if (startIndex === logPoints.length - 2) {
|
|
1143
|
+
m1 = (y1 - y0) / h;
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
const [xNext, yNext] = logPoints[startIndex + 2];
|
|
1147
|
+
m1 = (yNext - y0) / (xNext - x0);
|
|
1148
|
+
}
|
|
1149
|
+
// Cubic Hermite Spline
|
|
1150
|
+
const t2 = t * t;
|
|
1151
|
+
const t3 = t2 * t;
|
|
1152
|
+
const h00 = 2 * t3 - 3 * t2 + 1;
|
|
1153
|
+
const h10 = t3 - 2 * t2 + t;
|
|
1154
|
+
const h01 = -2 * t3 + 3 * t2;
|
|
1155
|
+
const h11 = t3 - t2;
|
|
1156
|
+
const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
|
|
1157
|
+
return Math.exp(y);
|
|
1158
|
+
}
|
|
1159
|
+
setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
|
|
1160
|
+
const state = channel.state;
|
|
1128
1161
|
const { voiceParams, startTime } = note;
|
|
1129
|
-
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation)
|
|
1162
|
+
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
|
|
1163
|
+
(1 + this.getAmplitudeControl(channel, note));
|
|
1130
1164
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
1131
1165
|
const volDelay = startTime + voiceParams.volDelay;
|
|
1132
|
-
const
|
|
1166
|
+
const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
|
|
1167
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
1133
1168
|
note.volumeEnvelopeNode.gain
|
|
1134
1169
|
.cancelScheduledValues(scheduleTime)
|
|
1135
|
-
.setValueAtTime(
|
|
1136
|
-
.linearRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1170
|
+
.setValueAtTime(sustainVolume, volHold);
|
|
1137
1171
|
}
|
|
1138
1172
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
1139
1173
|
const state = channel.state;
|
|
@@ -1153,6 +1187,12 @@ export class Midy {
|
|
|
1153
1187
|
.setValueAtTime(attackVolume, volHold)
|
|
1154
1188
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
1155
1189
|
}
|
|
1190
|
+
setPortamentoPitchEnvelope(note, scheduleTime) {
|
|
1191
|
+
const baseRate = note.voiceParams.playbackRate;
|
|
1192
|
+
note.bufferSource.playbackRate
|
|
1193
|
+
.cancelScheduledValues(scheduleTime)
|
|
1194
|
+
.setValueAtTime(baseRate, scheduleTime);
|
|
1195
|
+
}
|
|
1156
1196
|
setPitchEnvelope(note, scheduleTime) {
|
|
1157
1197
|
const { voiceParams } = note;
|
|
1158
1198
|
const baseRate = voiceParams.playbackRate;
|
|
@@ -1180,20 +1220,20 @@ export class Midy {
|
|
|
1180
1220
|
const maxFrequency = 20000; // max Hz of initialFilterFc
|
|
1181
1221
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
1182
1222
|
}
|
|
1183
|
-
|
|
1223
|
+
setPortamentoFilterEnvelope(channel, note, scheduleTime) {
|
|
1184
1224
|
const state = channel.state;
|
|
1185
|
-
const { voiceParams,
|
|
1186
|
-
const softPedalFactor =
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1225
|
+
const { voiceParams, startTime } = note;
|
|
1226
|
+
const softPedalFactor = this.getSoftPedalFactor(channel, note);
|
|
1227
|
+
const baseCent = voiceParams.initialFilterFc +
|
|
1228
|
+
this.getFilterCutoffControl(channel, note);
|
|
1229
|
+
const baseFreq = this.centToHz(baseCent) * softPedalFactor *
|
|
1190
1230
|
state.brightness * 2;
|
|
1191
1231
|
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
|
|
1192
1232
|
const sustainFreq = baseFreq +
|
|
1193
1233
|
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
1194
1234
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
1195
1235
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
1196
|
-
const portamentoTime = startTime + this.getPortamentoTime(channel);
|
|
1236
|
+
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1197
1237
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1198
1238
|
note.filterNode.frequency
|
|
1199
1239
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1203,9 +1243,8 @@ export class Midy {
|
|
|
1203
1243
|
}
|
|
1204
1244
|
setFilterEnvelope(channel, note, scheduleTime) {
|
|
1205
1245
|
const state = channel.state;
|
|
1206
|
-
const { voiceParams,
|
|
1207
|
-
const softPedalFactor =
|
|
1208
|
-
(0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
|
|
1246
|
+
const { voiceParams, startTime } = note;
|
|
1247
|
+
const softPedalFactor = this.getSoftPedalFactor(channel, note);
|
|
1209
1248
|
const baseCent = voiceParams.initialFilterFc +
|
|
1210
1249
|
this.getFilterCutoffControl(channel, note);
|
|
1211
1250
|
const baseFreq = this.centToHz(baseCent) * softPedalFactor *
|
|
@@ -1279,14 +1318,14 @@ 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);
|
|
1286
1325
|
const voiceParams = voice.getAllParams(controllerState);
|
|
1287
1326
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
1288
1327
|
const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
|
|
1289
|
-
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
1328
|
+
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1290
1329
|
note.volumeNode = new GainNode(this.audioContext);
|
|
1291
1330
|
note.gainL = new GainNode(this.audioContext);
|
|
1292
1331
|
note.gainR = new GainNode(this.audioContext);
|
|
@@ -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
|
}
|
|
@@ -1321,10 +1364,10 @@ export class Midy {
|
|
|
1321
1364
|
note.volumeEnvelopeNode.connect(note.volumeNode);
|
|
1322
1365
|
note.volumeNode.connect(note.gainL);
|
|
1323
1366
|
note.volumeNode.connect(note.gainR);
|
|
1324
|
-
if (0 <
|
|
1367
|
+
if (0 < state.chorusSendLevel) {
|
|
1325
1368
|
this.setChorusEffectsSend(channel, note, 0, now);
|
|
1326
1369
|
}
|
|
1327
|
-
if (0 <
|
|
1370
|
+
if (0 < state.reverbSendLevel) {
|
|
1328
1371
|
this.setReverbEffectsSend(channel, note, 0, now);
|
|
1329
1372
|
}
|
|
1330
1373
|
note.bufferSource.start(startTime);
|
|
@@ -1355,8 +1398,7 @@ export class Midy {
|
|
|
1355
1398
|
const [prevNote, prevChannelNumber] = prev;
|
|
1356
1399
|
if (prevNote && !prevNote.ending) {
|
|
1357
1400
|
this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
|
|
1358
|
-
startTime, true
|
|
1359
|
-
undefined);
|
|
1401
|
+
startTime, true);
|
|
1360
1402
|
}
|
|
1361
1403
|
}
|
|
1362
1404
|
this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
|
|
@@ -1376,19 +1418,11 @@ export class Midy {
|
|
|
1376
1418
|
const prevNote = this.drumExclusiveClassNotes[index];
|
|
1377
1419
|
if (prevNote && !prevNote.ending) {
|
|
1378
1420
|
this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
|
|
1379
|
-
startTime, true
|
|
1380
|
-
undefined);
|
|
1421
|
+
startTime, true);
|
|
1381
1422
|
}
|
|
1382
1423
|
this.drumExclusiveClassNotes[index] = note;
|
|
1383
1424
|
}
|
|
1384
|
-
|
|
1385
|
-
if (!channel.isDrum)
|
|
1386
|
-
return false;
|
|
1387
|
-
const programNumber = channel.programNumber;
|
|
1388
|
-
return !((programNumber === 48 && noteNumber === 88) ||
|
|
1389
|
-
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
1390
|
-
}
|
|
1391
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
|
|
1425
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
1392
1426
|
const channel = this.channels[channelNumber];
|
|
1393
1427
|
const bankNumber = this.calcBank(channel, channelNumber);
|
|
1394
1428
|
const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
|
|
@@ -1399,7 +1433,8 @@ export class Midy {
|
|
|
1399
1433
|
if (!voice)
|
|
1400
1434
|
return;
|
|
1401
1435
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
1402
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime,
|
|
1436
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
1437
|
+
note.noteOffEvent = noteOffEvent;
|
|
1403
1438
|
note.gainL.connect(channel.gainL);
|
|
1404
1439
|
note.gainR.connect(channel.gainR);
|
|
1405
1440
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -1408,31 +1443,12 @@ export class Midy {
|
|
|
1408
1443
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1409
1444
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1410
1445
|
const scheduledNotes = channel.scheduledNotes;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
noteList.push(note);
|
|
1414
|
-
}
|
|
1415
|
-
else {
|
|
1416
|
-
noteList = [note];
|
|
1417
|
-
scheduledNotes.set(noteNumber, noteList);
|
|
1418
|
-
}
|
|
1419
|
-
if (this.isDrumNoteOffException(channel, noteNumber)) {
|
|
1420
|
-
const stopTime = startTime + note.bufferSource.buffer.duration;
|
|
1421
|
-
const index = noteList.length - 1;
|
|
1422
|
-
const promise = new Promise((resolve) => {
|
|
1423
|
-
note.bufferSource.onended = () => {
|
|
1424
|
-
noteList[index] = undefined;
|
|
1425
|
-
this.disconnectNote(note);
|
|
1426
|
-
resolve();
|
|
1427
|
-
};
|
|
1428
|
-
note.bufferSource.stop(stopTime);
|
|
1429
|
-
});
|
|
1430
|
-
this.notePromises.push(promise);
|
|
1431
|
-
}
|
|
1446
|
+
note.index = scheduledNotes.length;
|
|
1447
|
+
scheduledNotes.push(note);
|
|
1432
1448
|
}
|
|
1433
1449
|
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1434
1450
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1435
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime,
|
|
1451
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
|
|
1436
1452
|
}
|
|
1437
1453
|
disconnectNote(note) {
|
|
1438
1454
|
note.bufferSource.disconnect();
|
|
@@ -1457,84 +1473,72 @@ export class Midy {
|
|
|
1457
1473
|
note.chorusEffectsSend.disconnect();
|
|
1458
1474
|
}
|
|
1459
1475
|
}
|
|
1460
|
-
|
|
1461
|
-
const
|
|
1476
|
+
releaseNote(channel, note, endTime) {
|
|
1477
|
+
const volRelease = endTime +
|
|
1478
|
+
note.voiceParams.volRelease * channel.state.releaseTime * 2;
|
|
1479
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1480
|
+
const stopTime = Math.min(volRelease, modRelease);
|
|
1481
|
+
note.filterNode.frequency
|
|
1482
|
+
.cancelScheduledValues(endTime)
|
|
1483
|
+
.linearRampToValueAtTime(0, modRelease);
|
|
1462
1484
|
note.volumeEnvelopeNode.gain
|
|
1463
1485
|
.cancelScheduledValues(endTime)
|
|
1464
|
-
.linearRampToValueAtTime(0,
|
|
1465
|
-
note.ending = true;
|
|
1466
|
-
this.scheduleTask(() => {
|
|
1467
|
-
note.bufferSource.loop = false;
|
|
1468
|
-
}, stopTime);
|
|
1486
|
+
.linearRampToValueAtTime(0, volRelease);
|
|
1469
1487
|
return new Promise((resolve) => {
|
|
1470
|
-
|
|
1471
|
-
|
|
1488
|
+
this.scheduleTask(() => {
|
|
1489
|
+
const bufferSource = note.bufferSource;
|
|
1490
|
+
bufferSource.loop = false;
|
|
1491
|
+
bufferSource.stop(stopTime);
|
|
1472
1492
|
this.disconnectNote(note);
|
|
1493
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
1473
1494
|
resolve();
|
|
1474
|
-
};
|
|
1475
|
-
note.bufferSource.stop(stopTime);
|
|
1495
|
+
}, stopTime);
|
|
1476
1496
|
});
|
|
1477
1497
|
}
|
|
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) {
|
|
1498
|
+
scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
1489
1499
|
const channel = this.channels[channelNumber];
|
|
1490
|
-
if (this.isDrumNoteOffException(channel, noteNumber))
|
|
1491
|
-
return;
|
|
1492
1500
|
const state = channel.state;
|
|
1493
1501
|
if (!force) {
|
|
1494
|
-
if (
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
return;
|
|
1505
|
-
const [note, i] = noteOffTarget;
|
|
1506
|
-
if (0.5 <= state.portamento && portamentoNoteNumber !== undefined) {
|
|
1507
|
-
const portamentoTime = endTime + this.getPortamentoTime(channel);
|
|
1508
|
-
const deltaNote = portamentoNoteNumber - noteNumber;
|
|
1509
|
-
const baseRate = note.voiceParams.playbackRate;
|
|
1510
|
-
const targetRate = baseRate * Math.pow(2, deltaNote / 12);
|
|
1511
|
-
note.bufferSource.playbackRate
|
|
1512
|
-
.cancelScheduledValues(endTime)
|
|
1513
|
-
.linearRampToValueAtTime(targetRate, portamentoTime);
|
|
1514
|
-
return this.stopNote(endTime, portamentoTime, noteList, i);
|
|
1502
|
+
if (channel.isDrum) {
|
|
1503
|
+
if (!this.isLoopDrum(channel, noteNumber))
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
if (0.5 <= state.sustainPedal)
|
|
1508
|
+
return;
|
|
1509
|
+
if (0.5 <= state.sostenutoPedal)
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1515
1512
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1513
|
+
const note = this.findNoteOffTarget(channel, noteNumber);
|
|
1514
|
+
if (!note)
|
|
1515
|
+
return;
|
|
1516
|
+
note.ending = true;
|
|
1517
|
+
this.releaseNote(channel, note, endTime);
|
|
1518
|
+
}
|
|
1519
|
+
findNoteOffTarget(channel, noteNumber) {
|
|
1520
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
1521
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
1522
|
+
const note = scheduledNotes[i];
|
|
1523
|
+
if (!note)
|
|
1524
|
+
continue;
|
|
1525
|
+
if (note.ending)
|
|
1526
|
+
continue;
|
|
1527
|
+
if (note.noteNumber !== noteNumber)
|
|
1528
|
+
continue;
|
|
1529
|
+
return note;
|
|
1525
1530
|
}
|
|
1526
1531
|
}
|
|
1527
1532
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1528
1533
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1529
|
-
return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false
|
|
1530
|
-
undefined);
|
|
1534
|
+
return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
|
|
1531
1535
|
}
|
|
1532
1536
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
1533
1537
|
const velocity = halfVelocity * 2;
|
|
1534
1538
|
const channel = this.channels[channelNumber];
|
|
1535
1539
|
const promises = [];
|
|
1536
1540
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1537
|
-
const promise = this.
|
|
1541
|
+
const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
|
|
1538
1542
|
promises.push(promise);
|
|
1539
1543
|
}
|
|
1540
1544
|
channel.sustainNotes = [];
|
|
@@ -1544,12 +1548,14 @@ export class Midy {
|
|
|
1544
1548
|
const velocity = halfVelocity * 2;
|
|
1545
1549
|
const channel = this.channels[channelNumber];
|
|
1546
1550
|
const promises = [];
|
|
1551
|
+
const sostenutoNotes = channel.sostenutoNotes;
|
|
1547
1552
|
channel.state.sostenutoPedal = 0;
|
|
1548
|
-
|
|
1549
|
-
const
|
|
1553
|
+
for (let i = 0; i < sostenutoNotes.length; i++) {
|
|
1554
|
+
const note = sostenutoNotes[i];
|
|
1555
|
+
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
|
|
1550
1556
|
promises.push(promise);
|
|
1551
|
-
}
|
|
1552
|
-
channel.sostenutoNotes
|
|
1557
|
+
}
|
|
1558
|
+
channel.sostenutoNotes = [];
|
|
1553
1559
|
return promises;
|
|
1554
1560
|
}
|
|
1555
1561
|
handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
|
|
@@ -1578,11 +1584,11 @@ export class Midy {
|
|
|
1578
1584
|
const channel = this.channels[channelNumber];
|
|
1579
1585
|
channel.state.polyphonicKeyPressure = pressure / 127;
|
|
1580
1586
|
const table = channel.polyphonicKeyPressureTable;
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
}
|
|
1587
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1588
|
+
if (note.noteNumber === noteNumber) {
|
|
1589
|
+
this.setControllerParameters(channel, note, table);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1586
1592
|
this.applyVoiceParams(channel, 10);
|
|
1587
1593
|
}
|
|
1588
1594
|
handleProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
@@ -1612,7 +1618,7 @@ export class Midy {
|
|
|
1612
1618
|
channel.detune += pressureDepth * (next - prev);
|
|
1613
1619
|
}
|
|
1614
1620
|
const table = channel.channelPressureTable;
|
|
1615
|
-
this.
|
|
1621
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1616
1622
|
this.setControllerParameters(channel, note, table);
|
|
1617
1623
|
});
|
|
1618
1624
|
this.applyVoiceParams(channel, 13);
|
|
@@ -1669,10 +1675,13 @@ export class Midy {
|
|
|
1669
1675
|
.setValueAtTime(volumeDepth, scheduleTime);
|
|
1670
1676
|
}
|
|
1671
1677
|
setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
|
|
1678
|
+
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
|
|
1679
|
+
let value = note.voiceParams.reverbEffectsSend;
|
|
1680
|
+
if (0 <= keyBasedValue) {
|
|
1681
|
+
value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
|
|
1682
|
+
}
|
|
1672
1683
|
if (0 < prevValue) {
|
|
1673
|
-
if (0 <
|
|
1674
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
|
|
1675
|
-
const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
|
|
1684
|
+
if (0 < value) {
|
|
1676
1685
|
note.reverbEffectsSend.gain
|
|
1677
1686
|
.cancelScheduledValues(scheduleTime)
|
|
1678
1687
|
.setValueAtTime(value, scheduleTime);
|
|
@@ -1682,10 +1691,10 @@ export class Midy {
|
|
|
1682
1691
|
}
|
|
1683
1692
|
}
|
|
1684
1693
|
else {
|
|
1685
|
-
if (0 <
|
|
1694
|
+
if (0 < value) {
|
|
1686
1695
|
if (!note.reverbEffectsSend) {
|
|
1687
1696
|
note.reverbEffectsSend = new GainNode(this.audioContext, {
|
|
1688
|
-
gain:
|
|
1697
|
+
gain: value,
|
|
1689
1698
|
});
|
|
1690
1699
|
note.volumeNode.connect(note.reverbEffectsSend);
|
|
1691
1700
|
}
|
|
@@ -1694,10 +1703,13 @@ export class Midy {
|
|
|
1694
1703
|
}
|
|
1695
1704
|
}
|
|
1696
1705
|
setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
|
|
1706
|
+
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
|
|
1707
|
+
let value = note.voiceParams.chorusEffectsSend;
|
|
1708
|
+
if (0 <= keyBasedValue) {
|
|
1709
|
+
value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
|
|
1710
|
+
}
|
|
1697
1711
|
if (0 < prevValue) {
|
|
1698
|
-
if (0 <
|
|
1699
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
|
|
1700
|
-
const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
|
|
1712
|
+
if (0 < vaule) {
|
|
1701
1713
|
note.chorusEffectsSend.gain
|
|
1702
1714
|
.cancelScheduledValues(scheduleTime)
|
|
1703
1715
|
.setValueAtTime(value, scheduleTime);
|
|
@@ -1707,10 +1719,10 @@ export class Midy {
|
|
|
1707
1719
|
}
|
|
1708
1720
|
}
|
|
1709
1721
|
else {
|
|
1710
|
-
if (0 <
|
|
1722
|
+
if (0 < value) {
|
|
1711
1723
|
if (!note.chorusEffectsSend) {
|
|
1712
1724
|
note.chorusEffectsSend = new GainNode(this.audioContext, {
|
|
1713
|
-
gain:
|
|
1725
|
+
gain: value,
|
|
1714
1726
|
});
|
|
1715
1727
|
note.volumeNode.connect(note.chorusEffectsSend);
|
|
1716
1728
|
}
|
|
@@ -1820,8 +1832,8 @@ export class Midy {
|
|
|
1820
1832
|
if (key in voiceParams)
|
|
1821
1833
|
noteVoiceParams[key] = voiceParams[key];
|
|
1822
1834
|
}
|
|
1823
|
-
if (0.5 <= channel.state.portamento && note.
|
|
1824
|
-
this.
|
|
1835
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1836
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1825
1837
|
}
|
|
1826
1838
|
else {
|
|
1827
1839
|
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
@@ -1844,42 +1856,42 @@ export class Midy {
|
|
|
1844
1856
|
});
|
|
1845
1857
|
}
|
|
1846
1858
|
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
|
-
|
|
1859
|
+
const handlers = new Array(128);
|
|
1860
|
+
handlers[0] = this.setBankMSB;
|
|
1861
|
+
handlers[1] = this.setModulationDepth;
|
|
1862
|
+
handlers[5] = this.setPortamentoTime;
|
|
1863
|
+
handlers[6] = this.dataEntryMSB;
|
|
1864
|
+
handlers[7] = this.setVolume;
|
|
1865
|
+
handlers[10] = this.setPan;
|
|
1866
|
+
handlers[11] = this.setExpression;
|
|
1867
|
+
handlers[32] = this.setBankLSB;
|
|
1868
|
+
handlers[38] = this.dataEntryLSB;
|
|
1869
|
+
handlers[64] = this.setSustainPedal;
|
|
1870
|
+
handlers[65] = this.setPortamento;
|
|
1871
|
+
handlers[66] = this.setSostenutoPedal;
|
|
1872
|
+
handlers[67] = this.setSoftPedal;
|
|
1873
|
+
handlers[71] = this.setFilterResonance;
|
|
1874
|
+
handlers[72] = this.setReleaseTime;
|
|
1875
|
+
handlers[73] = this.setAttackTime;
|
|
1876
|
+
handlers[74] = this.setBrightness;
|
|
1877
|
+
handlers[75] = this.setDecayTime;
|
|
1878
|
+
handlers[76] = this.setVibratoRate;
|
|
1879
|
+
handlers[77] = this.setVibratoDepth;
|
|
1880
|
+
handlers[78] = this.setVibratoDelay;
|
|
1881
|
+
handlers[91] = this.setReverbSendLevel;
|
|
1882
|
+
handlers[93] = this.setChorusSendLevel;
|
|
1883
|
+
handlers[96] = this.dataIncrement;
|
|
1884
|
+
handlers[97] = this.dataDecrement;
|
|
1885
|
+
handlers[100] = this.setRPNLSB;
|
|
1886
|
+
handlers[101] = this.setRPNMSB;
|
|
1887
|
+
handlers[120] = this.allSoundOff;
|
|
1888
|
+
handlers[121] = this.resetAllControllers;
|
|
1889
|
+
handlers[123] = this.allNotesOff;
|
|
1890
|
+
handlers[124] = this.omniOff;
|
|
1891
|
+
handlers[125] = this.omniOn;
|
|
1892
|
+
handlers[126] = this.monoOn;
|
|
1893
|
+
handlers[127] = this.polyOn;
|
|
1894
|
+
return handlers;
|
|
1883
1895
|
}
|
|
1884
1896
|
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1885
1897
|
const handler = this.controlChangeHandlers[controllerType];
|
|
@@ -1916,17 +1928,41 @@ export class Midy {
|
|
|
1916
1928
|
channel.state.modulationDepth = modulation / 127;
|
|
1917
1929
|
this.updateModulation(channel, scheduleTime);
|
|
1918
1930
|
}
|
|
1919
|
-
|
|
1931
|
+
updatePortamento(channel, scheduleTime) {
|
|
1932
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1933
|
+
if (0.5 <= channel.state.portamento) {
|
|
1934
|
+
if (0 <= note.portamentoNoteNumber) {
|
|
1935
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
1936
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1937
|
+
this.setPortamentoPitchEnvelope(note, scheduleTime);
|
|
1938
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
else {
|
|
1942
|
+
if (0 <= note.portamentoNoteNumber) {
|
|
1943
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1944
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
1945
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1946
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
|
|
1920
1952
|
const channel = this.channels[channelNumber];
|
|
1953
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1921
1954
|
channel.state.portamentoTime = portamentoTime / 127;
|
|
1955
|
+
if (channel.isDrum)
|
|
1956
|
+
return;
|
|
1957
|
+
this.updatePortamento(channel, scheduleTime);
|
|
1922
1958
|
}
|
|
1923
1959
|
setKeyBasedVolume(channel, scheduleTime) {
|
|
1924
1960
|
this.processScheduledNotes(channel, (note) => {
|
|
1925
1961
|
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
|
|
1926
|
-
if (
|
|
1962
|
+
if (0 <= keyBasedValue) {
|
|
1927
1963
|
note.volumeNode.gain
|
|
1928
1964
|
.cancelScheduledValues(scheduleTime)
|
|
1929
|
-
.setValueAtTime(
|
|
1965
|
+
.setValueAtTime(keyBasedValue / 127, scheduleTime);
|
|
1930
1966
|
}
|
|
1931
1967
|
});
|
|
1932
1968
|
}
|
|
@@ -1947,8 +1983,8 @@ export class Midy {
|
|
|
1947
1983
|
setKeyBasedPan(channel, scheduleTime) {
|
|
1948
1984
|
this.processScheduledNotes(channel, (note) => {
|
|
1949
1985
|
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
|
|
1950
|
-
if (
|
|
1951
|
-
const { gainLeft, gainRight } = this.panToGain(
|
|
1986
|
+
if (0 <= keyBasedValue) {
|
|
1987
|
+
const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
|
|
1952
1988
|
note.gainL.gain
|
|
1953
1989
|
.cancelScheduledValues(scheduleTime)
|
|
1954
1990
|
.setValueAtTime(gainLeft, scheduleTime);
|
|
@@ -1976,7 +2012,7 @@ export class Midy {
|
|
|
1976
2012
|
}
|
|
1977
2013
|
dataEntryLSB(channelNumber, value, scheduleTime) {
|
|
1978
2014
|
this.channels[channelNumber].dataLSB = value;
|
|
1979
|
-
this.handleRPN(channelNumber, scheduleTime);
|
|
2015
|
+
this.handleRPN(channelNumber, 0, scheduleTime);
|
|
1980
2016
|
}
|
|
1981
2017
|
updateChannelVolume(channel, scheduleTime) {
|
|
1982
2018
|
const state = channel.state;
|
|
@@ -2004,11 +2040,13 @@ export class Midy {
|
|
|
2004
2040
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
2005
2041
|
}
|
|
2006
2042
|
}
|
|
2007
|
-
setPortamento(channelNumber, value) {
|
|
2043
|
+
setPortamento(channelNumber, value, scheduleTime) {
|
|
2008
2044
|
const channel = this.channels[channelNumber];
|
|
2009
2045
|
if (channel.isDrum)
|
|
2010
2046
|
return;
|
|
2047
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
2011
2048
|
channel.state.portamento = value / 127;
|
|
2049
|
+
this.updatePortamento(channel, scheduleTime);
|
|
2012
2050
|
}
|
|
2013
2051
|
setSostenutoPedal(channelNumber, value, scheduleTime) {
|
|
2014
2052
|
const channel = this.channels[channelNumber];
|
|
@@ -2017,12 +2055,19 @@ export class Midy {
|
|
|
2017
2055
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2018
2056
|
channel.state.sostenutoPedal = value / 127;
|
|
2019
2057
|
if (64 <= value) {
|
|
2020
|
-
|
|
2058
|
+
const sostenutoNotes = [];
|
|
2059
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2060
|
+
sostenutoNotes.push(note);
|
|
2061
|
+
});
|
|
2062
|
+
channel.sostenutoNotes = sostenutoNotes;
|
|
2021
2063
|
}
|
|
2022
2064
|
else {
|
|
2023
2065
|
this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
|
|
2024
2066
|
}
|
|
2025
2067
|
}
|
|
2068
|
+
getSoftPedalFactor(channel, note) {
|
|
2069
|
+
return 1 - (0.1 + (note.noteNumber / 127) * 0.2) * channel.state.softPedal;
|
|
2070
|
+
}
|
|
2026
2071
|
setSoftPedal(channelNumber, softPedal, scheduleTime) {
|
|
2027
2072
|
const channel = this.channels[channelNumber];
|
|
2028
2073
|
if (channel.isDrum)
|
|
@@ -2031,9 +2076,9 @@ export class Midy {
|
|
|
2031
2076
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2032
2077
|
state.softPedal = softPedal / 127;
|
|
2033
2078
|
this.processScheduledNotes(channel, (note) => {
|
|
2034
|
-
if (0.5 <= state.portamento && note.
|
|
2035
|
-
this.
|
|
2036
|
-
this.
|
|
2079
|
+
if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2080
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2081
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2037
2082
|
}
|
|
2038
2083
|
else {
|
|
2039
2084
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
@@ -2047,7 +2092,7 @@ export class Midy {
|
|
|
2047
2092
|
return;
|
|
2048
2093
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2049
2094
|
const state = channel.state;
|
|
2050
|
-
state.filterResonance = filterResonance /
|
|
2095
|
+
state.filterResonance = filterResonance / 127;
|
|
2051
2096
|
this.processScheduledNotes(channel, (note) => {
|
|
2052
2097
|
const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
|
|
2053
2098
|
note.filterNode.Q.setValueAtTime(Q, scheduleTime);
|
|
@@ -2058,14 +2103,14 @@ export class Midy {
|
|
|
2058
2103
|
if (channel.isDrum)
|
|
2059
2104
|
return;
|
|
2060
2105
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2061
|
-
channel.state.releaseTime = releaseTime /
|
|
2106
|
+
channel.state.releaseTime = releaseTime / 127;
|
|
2062
2107
|
}
|
|
2063
2108
|
setAttackTime(channelNumber, attackTime, scheduleTime) {
|
|
2064
2109
|
const channel = this.channels[channelNumber];
|
|
2065
2110
|
if (channel.isDrum)
|
|
2066
2111
|
return;
|
|
2067
2112
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2068
|
-
channel.state.attackTime = attackTime /
|
|
2113
|
+
channel.state.attackTime = attackTime / 127;
|
|
2069
2114
|
this.processScheduledNotes(channel, (note) => {
|
|
2070
2115
|
if (note.startTime < scheduleTime)
|
|
2071
2116
|
return false;
|
|
@@ -2078,10 +2123,10 @@ export class Midy {
|
|
|
2078
2123
|
return;
|
|
2079
2124
|
const state = channel.state;
|
|
2080
2125
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2081
|
-
state.brightness = brightness /
|
|
2126
|
+
state.brightness = brightness / 127;
|
|
2082
2127
|
this.processScheduledNotes(channel, (note) => {
|
|
2083
|
-
if (0.5 <= state.portamento && note.
|
|
2084
|
-
this.
|
|
2128
|
+
if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2129
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2085
2130
|
}
|
|
2086
2131
|
else {
|
|
2087
2132
|
this.setFilterEnvelope(channel, note);
|
|
@@ -2093,7 +2138,7 @@ export class Midy {
|
|
|
2093
2138
|
if (channel.isDrum)
|
|
2094
2139
|
return;
|
|
2095
2140
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2096
|
-
channel.state.decayTime = dacayTime /
|
|
2141
|
+
channel.state.decayTime = dacayTime / 127;
|
|
2097
2142
|
this.processScheduledNotes(channel, (note) => {
|
|
2098
2143
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
2099
2144
|
});
|
|
@@ -2103,7 +2148,7 @@ export class Midy {
|
|
|
2103
2148
|
if (channel.isDrum)
|
|
2104
2149
|
return;
|
|
2105
2150
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2106
|
-
channel.state.vibratoRate = vibratoRate /
|
|
2151
|
+
channel.state.vibratoRate = vibratoRate / 127;
|
|
2107
2152
|
if (channel.vibratoDepth <= 0)
|
|
2108
2153
|
return;
|
|
2109
2154
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -2116,7 +2161,7 @@ export class Midy {
|
|
|
2116
2161
|
return;
|
|
2117
2162
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2118
2163
|
const prev = channel.state.vibratoDepth;
|
|
2119
|
-
channel.state.vibratoDepth = vibratoDepth /
|
|
2164
|
+
channel.state.vibratoDepth = vibratoDepth / 127;
|
|
2120
2165
|
if (0 < prev) {
|
|
2121
2166
|
this.processScheduledNotes(channel, (note) => {
|
|
2122
2167
|
this.setFreqVibLFO(channel, note, scheduleTime);
|
|
@@ -2128,12 +2173,12 @@ export class Midy {
|
|
|
2128
2173
|
});
|
|
2129
2174
|
}
|
|
2130
2175
|
}
|
|
2131
|
-
setVibratoDelay(channelNumber, vibratoDelay) {
|
|
2176
|
+
setVibratoDelay(channelNumber, vibratoDelay, scheduleTime) {
|
|
2132
2177
|
const channel = this.channels[channelNumber];
|
|
2133
2178
|
if (channel.isDrum)
|
|
2134
2179
|
return;
|
|
2135
2180
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2136
|
-
channel.state.vibratoDelay = vibratoDelay /
|
|
2181
|
+
channel.state.vibratoDelay = vibratoDelay / 127;
|
|
2137
2182
|
if (0 < channel.state.vibratoDepth) {
|
|
2138
2183
|
this.processScheduledNotes(channel, (note) => {
|
|
2139
2184
|
this.startVibrato(channel, note, scheduleTime);
|
|
@@ -2156,7 +2201,8 @@ export class Midy {
|
|
|
2156
2201
|
this.processScheduledNotes(channel, (note) => {
|
|
2157
2202
|
if (note.voiceParams.reverbEffectsSend <= 0)
|
|
2158
2203
|
return false;
|
|
2159
|
-
note.reverbEffectsSend
|
|
2204
|
+
if (note.reverbEffectsSend)
|
|
2205
|
+
note.reverbEffectsSend.disconnect();
|
|
2160
2206
|
});
|
|
2161
2207
|
}
|
|
2162
2208
|
}
|
|
@@ -2188,7 +2234,8 @@ export class Midy {
|
|
|
2188
2234
|
this.processScheduledNotes(channel, (note) => {
|
|
2189
2235
|
if (note.voiceParams.chorusEffectsSend <= 0)
|
|
2190
2236
|
return false;
|
|
2191
|
-
note.chorusEffectsSend
|
|
2237
|
+
if (note.chorusEffectsSend)
|
|
2238
|
+
note.chorusEffectsSend.disconnect();
|
|
2192
2239
|
});
|
|
2193
2240
|
}
|
|
2194
2241
|
}
|
|
@@ -2255,12 +2302,14 @@ export class Midy {
|
|
|
2255
2302
|
}
|
|
2256
2303
|
}
|
|
2257
2304
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
|
|
2258
|
-
dataIncrement(channelNumber) {
|
|
2259
|
-
this.
|
|
2305
|
+
dataIncrement(channelNumber, scheduleTime) {
|
|
2306
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
2307
|
+
this.handleRPN(channelNumber, 1, scheduleTime);
|
|
2260
2308
|
}
|
|
2261
2309
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
|
|
2262
|
-
dataDecrement(channelNumber) {
|
|
2263
|
-
this.
|
|
2310
|
+
dataDecrement(channelNumber, scheduleTime) {
|
|
2311
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
2312
|
+
this.handleRPN(channelNumber, -1, scheduleTime);
|
|
2264
2313
|
}
|
|
2265
2314
|
setRPNMSB(channelNumber, value) {
|
|
2266
2315
|
this.channels[channelNumber].rpnMSB = value;
|
|
@@ -2270,7 +2319,7 @@ export class Midy {
|
|
|
2270
2319
|
}
|
|
2271
2320
|
dataEntryMSB(channelNumber, value, scheduleTime) {
|
|
2272
2321
|
this.channels[channelNumber].dataMSB = value;
|
|
2273
|
-
this.handleRPN(channelNumber, scheduleTime);
|
|
2322
|
+
this.handleRPN(channelNumber, 0, scheduleTime);
|
|
2274
2323
|
}
|
|
2275
2324
|
handlePitchBendRangeRPN(channelNumber, scheduleTime) {
|
|
2276
2325
|
const channel = this.channels[channelNumber];
|
|
@@ -2344,21 +2393,29 @@ export class Midy {
|
|
|
2344
2393
|
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
2345
2394
|
}
|
|
2346
2395
|
resetAllStates(channelNumber) {
|
|
2396
|
+
const scheduleTime = this.audioContext.currentTime;
|
|
2347
2397
|
const channel = this.channels[channelNumber];
|
|
2348
2398
|
const state = channel.state;
|
|
2349
|
-
|
|
2350
|
-
|
|
2399
|
+
const entries = Object.entries(defaultControllerState);
|
|
2400
|
+
for (const [key, { type, defaultValue }] of entries) {
|
|
2401
|
+
if (128 <= type) {
|
|
2402
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2403
|
+
}
|
|
2404
|
+
else {
|
|
2405
|
+
state[key] = defaultValue;
|
|
2406
|
+
}
|
|
2351
2407
|
}
|
|
2352
|
-
for (const
|
|
2353
|
-
channel[
|
|
2408
|
+
for (const key of Object.keys(this.constructor.channelSettings)) {
|
|
2409
|
+
channel[key] = this.constructor.channelSettings[key];
|
|
2354
2410
|
}
|
|
2411
|
+
this.resetChannelTable(channel);
|
|
2355
2412
|
this.mode = "GM2";
|
|
2356
2413
|
this.masterFineTuning = 0; // cb
|
|
2357
2414
|
this.masterCoarseTuning = 0; // cb
|
|
2358
2415
|
}
|
|
2359
2416
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
|
|
2360
|
-
resetAllControllers(channelNumber) {
|
|
2361
|
-
const
|
|
2417
|
+
resetAllControllers(channelNumber, _value, scheduleTime) {
|
|
2418
|
+
const keys = [
|
|
2362
2419
|
"polyphonicKeyPressure",
|
|
2363
2420
|
"channelPressure",
|
|
2364
2421
|
"pitchWheel",
|
|
@@ -2371,10 +2428,17 @@ export class Midy {
|
|
|
2371
2428
|
];
|
|
2372
2429
|
const channel = this.channels[channelNumber];
|
|
2373
2430
|
const state = channel.state;
|
|
2374
|
-
for (let i = 0; i <
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2431
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2432
|
+
const key = keys[i];
|
|
2433
|
+
const { type, defaultValue } = defaultControllerState[key];
|
|
2434
|
+
if (128 <= type) {
|
|
2435
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2436
|
+
}
|
|
2437
|
+
else {
|
|
2438
|
+
state[key] = defaultValue;
|
|
2439
|
+
}
|
|
2377
2440
|
}
|
|
2441
|
+
this.setPitchBend(channelNumber, 8192, scheduleTime);
|
|
2378
2442
|
const settingTypes = [
|
|
2379
2443
|
"rpnMSB",
|
|
2380
2444
|
"rpnLSB",
|
|
@@ -2619,7 +2683,7 @@ export class Midy {
|
|
|
2619
2683
|
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2620
2684
|
}
|
|
2621
2685
|
getReverbTime(value) {
|
|
2622
|
-
return Math.
|
|
2686
|
+
return Math.exp((value - 40) * 0.025);
|
|
2623
2687
|
}
|
|
2624
2688
|
// mean free path equation
|
|
2625
2689
|
// https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
|
|
@@ -2853,7 +2917,13 @@ export class Midy {
|
|
|
2853
2917
|
setControllerParameters(channel, note, table) {
|
|
2854
2918
|
if (table[0] !== 64)
|
|
2855
2919
|
this.updateDetune(channel, note);
|
|
2856
|
-
if (
|
|
2920
|
+
if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
|
|
2921
|
+
if (table[1] !== 64)
|
|
2922
|
+
this.setPortamentoFilterEnvelope(channel, note);
|
|
2923
|
+
if (table[2] !== 64)
|
|
2924
|
+
this.setPortamentoVolumeEnvelope(channel, note);
|
|
2925
|
+
}
|
|
2926
|
+
else {
|
|
2857
2927
|
if (table[1] !== 64)
|
|
2858
2928
|
this.setFilterEnvelope(channel, note);
|
|
2859
2929
|
if (table[2] !== 64)
|
|
@@ -2881,8 +2951,13 @@ export class Midy {
|
|
|
2881
2951
|
initControlTable() {
|
|
2882
2952
|
const channelCount = 128;
|
|
2883
2953
|
const slotSize = 6;
|
|
2884
|
-
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2885
2954
|
const table = new Uint8Array(channelCount * slotSize);
|
|
2955
|
+
return this.resetControlTable(table);
|
|
2956
|
+
}
|
|
2957
|
+
resetControlTable(table) {
|
|
2958
|
+
const channelCount = 128;
|
|
2959
|
+
const slotSize = 6;
|
|
2960
|
+
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2886
2961
|
for (let ch = 0; ch < channelCount; ch++) {
|
|
2887
2962
|
const offset = ch * slotSize;
|
|
2888
2963
|
table.set(defaultValues, offset);
|
|
@@ -2913,7 +2988,7 @@ export class Midy {
|
|
|
2913
2988
|
getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
|
|
2914
2989
|
const index = keyNumber * 128 + controllerType;
|
|
2915
2990
|
const controlValue = channel.keyBasedInstrumentControlTable[index];
|
|
2916
|
-
return
|
|
2991
|
+
return controlValue;
|
|
2917
2992
|
}
|
|
2918
2993
|
handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
|
|
2919
2994
|
const channelNumber = data[4];
|
|
@@ -2926,7 +3001,7 @@ export class Midy {
|
|
|
2926
3001
|
const controllerType = data[i];
|
|
2927
3002
|
const value = data[i + 1];
|
|
2928
3003
|
const index = keyNumber * 128 + controllerType;
|
|
2929
|
-
table[index] = value
|
|
3004
|
+
table[index] = value;
|
|
2930
3005
|
}
|
|
2931
3006
|
this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
|
|
2932
3007
|
}
|