@marmooo/midy 0.4.9 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/esm/midy-GM1.d.ts +63 -9
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +1094 -94
- package/esm/midy-GM2.d.ts +74 -24
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +1298 -234
- package/esm/midy-GMLite.d.ts +63 -8
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +1099 -92
- package/esm/midy.d.ts +49 -30
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +1310 -248
- package/esm/reverb.d.ts +58 -0
- package/esm/reverb.d.ts.map +1 -0
- package/esm/reverb.js +389 -0
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +63 -9
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +1094 -94
- package/script/midy-GM2.d.ts +74 -24
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +1298 -234
- package/script/midy-GMLite.d.ts +63 -8
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +1099 -92
- package/script/midy.d.ts +49 -30
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +1310 -248
- package/script/reverb.d.ts +58 -0
- package/script/reverb.d.ts.map +1 -0
- package/script/reverb.js +405 -0
package/script/midy.js
CHANGED
|
@@ -4,6 +4,56 @@ exports.Midy = void 0;
|
|
|
4
4
|
const midi_file_1 = require("midi-file");
|
|
5
5
|
const soundfont_parser_1 = require("@marmooo/soundfont-parser");
|
|
6
6
|
const ogg_vorbis_1 = require("@wasm-audio-decoders/ogg-vorbis");
|
|
7
|
+
const reverb_js_1 = require("./reverb.js");
|
|
8
|
+
// Cache mode
|
|
9
|
+
// - "none" for full real-time control (dynamic CC, LFO, pitch)
|
|
10
|
+
// - "ads" for real-time playback with higher cache hit rate
|
|
11
|
+
// - "adsr" for real-time playback with accurate release envelope
|
|
12
|
+
// - "note" for efficient playback when note behavior is fixed
|
|
13
|
+
// - "audio" for fully pre-rendered playback (lowest CPU)
|
|
14
|
+
//
|
|
15
|
+
// "none"
|
|
16
|
+
// No caching. Envelope processing is done in real time on every note.
|
|
17
|
+
// Uses Web Audio API nodes directly, so LFO and pitch envelope are
|
|
18
|
+
// fully supported. Higher CPU usage.
|
|
19
|
+
// "ads"
|
|
20
|
+
// Pre-renders the ADS (Attack-Decay-Sustain) phase into an
|
|
21
|
+
// OfflineAudioContext and caches the result. The sustain tail is
|
|
22
|
+
// aligned to the loop boundary as a fixed buffer. Release is
|
|
23
|
+
// handled by fading volumeNode gain to 0 at note-off.
|
|
24
|
+
// LFO effects (modLfoToPitch, modLfoToFilterFc, modLfoToVolume,
|
|
25
|
+
// vibLfoToPitch) are applied in real time after playback starts.
|
|
26
|
+
// "adsr"
|
|
27
|
+
// Pre-renders the full ADSR envelope (Attack-Decay-Sustain-Release)
|
|
28
|
+
// into an OfflineAudioContext. The cache key includes the note
|
|
29
|
+
// duration in ticks (tempo-independent) and the volRelease parameter,
|
|
30
|
+
// so notes with the same duration and release shape share a buffer.
|
|
31
|
+
// LFO effects are applied in real time after playback starts,
|
|
32
|
+
// same as "ads" mode. Higher cache hit rate than "note" mode
|
|
33
|
+
// because LFO variations do not produce separate cache entries.
|
|
34
|
+
// "note"
|
|
35
|
+
// Renders the full noteOn-to-noteOff duration per note in an
|
|
36
|
+
// OfflineAudioContext. All events during the note (volume,
|
|
37
|
+
// expression, pitch bend, LFO, CC#1) are baked into the buffer,
|
|
38
|
+
// so no real-time processing is needed during playback. Greatly
|
|
39
|
+
// reduces CPU load for songs with many simultaneous notes.
|
|
40
|
+
// MIDI file playback only — does not respond to real-time CC changes.
|
|
41
|
+
// "audio"
|
|
42
|
+
// Renders the entire MIDI file into a single AudioBuffer offline.
|
|
43
|
+
// Call render() to complete rendering before calling start().
|
|
44
|
+
// Playback simply streams an AudioBufferSourceNode, so CPU usage
|
|
45
|
+
// is near zero. Seek and tempo changes are handled in real time.
|
|
46
|
+
// A "rendering" event is dispatched when rendering starts, and a
|
|
47
|
+
// "rendered" event is dispatched when rendering completes.
|
|
48
|
+
/** @type {"none"|"ads"|"adsr"|"note"|"audio"} */
|
|
49
|
+
const DEFAULT_CACHE_MODE = "ads";
|
|
50
|
+
const _f64Buf = new ArrayBuffer(8);
|
|
51
|
+
const _f64Array = new Float64Array(_f64Buf);
|
|
52
|
+
const _u64Array = new BigUint64Array(_f64Buf);
|
|
53
|
+
function f64ToBigInt(value) {
|
|
54
|
+
_f64Array[0] = value;
|
|
55
|
+
return _u64Array[0];
|
|
56
|
+
}
|
|
7
57
|
let decoderPromise = null;
|
|
8
58
|
let decoderQueue = Promise.resolve();
|
|
9
59
|
function initDecoder() {
|
|
@@ -51,6 +101,24 @@ class Note {
|
|
|
51
101
|
writable: true,
|
|
52
102
|
value: void 0
|
|
53
103
|
});
|
|
104
|
+
Object.defineProperty(this, "timelineIndex", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
writable: true,
|
|
108
|
+
value: null
|
|
109
|
+
});
|
|
110
|
+
Object.defineProperty(this, "renderedBuffer", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: null
|
|
115
|
+
});
|
|
116
|
+
Object.defineProperty(this, "fullCacheVoiceId", {
|
|
117
|
+
enumerable: true,
|
|
118
|
+
configurable: true,
|
|
119
|
+
writable: true,
|
|
120
|
+
value: null
|
|
121
|
+
});
|
|
54
122
|
Object.defineProperty(this, "filterEnvelopeNode", {
|
|
55
123
|
enumerable: true,
|
|
56
124
|
configurable: true,
|
|
@@ -138,7 +206,13 @@ class Note {
|
|
|
138
206
|
}
|
|
139
207
|
}
|
|
140
208
|
class Channel {
|
|
141
|
-
constructor(audioNodes, settings) {
|
|
209
|
+
constructor(channelNumber, audioNodes, settings) {
|
|
210
|
+
Object.defineProperty(this, "channelNumber", {
|
|
211
|
+
enumerable: true,
|
|
212
|
+
configurable: true,
|
|
213
|
+
writable: true,
|
|
214
|
+
value: 0
|
|
215
|
+
});
|
|
142
216
|
Object.defineProperty(this, "isDrum", {
|
|
143
217
|
enumerable: true,
|
|
144
218
|
configurable: true,
|
|
@@ -289,6 +363,7 @@ class Channel {
|
|
|
289
363
|
writable: true,
|
|
290
364
|
value: null
|
|
291
365
|
});
|
|
366
|
+
this.channelNumber = channelNumber;
|
|
292
367
|
Object.assign(this, audioNodes);
|
|
293
368
|
Object.assign(this, settings);
|
|
294
369
|
this.state = new ControllerState();
|
|
@@ -296,12 +371,12 @@ class Channel {
|
|
|
296
371
|
resetSettings(settings) {
|
|
297
372
|
Object.assign(this, settings);
|
|
298
373
|
}
|
|
299
|
-
resetTable(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
374
|
+
resetTable() {
|
|
375
|
+
this.controlTable.set(defaultControlValues);
|
|
376
|
+
this.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
377
|
+
this.channelPressureTable.set(defaultPressureValues);
|
|
378
|
+
this.polyphonicKeyPressureTable.set(defaultPressureValues);
|
|
379
|
+
this.keyBasedTable.fill(-1);
|
|
305
380
|
}
|
|
306
381
|
}
|
|
307
382
|
const drumExclusiveClassesByKit = new Array(57);
|
|
@@ -453,13 +528,73 @@ const defaultControlValues = new Int8Array([
|
|
|
453
528
|
...[-1, -1, -1, -1, -1, -1],
|
|
454
529
|
...defaultPressureValues,
|
|
455
530
|
]);
|
|
531
|
+
class RenderedBuffer {
|
|
532
|
+
constructor(buffer, meta = {}) {
|
|
533
|
+
Object.defineProperty(this, "buffer", {
|
|
534
|
+
enumerable: true,
|
|
535
|
+
configurable: true,
|
|
536
|
+
writable: true,
|
|
537
|
+
value: void 0
|
|
538
|
+
});
|
|
539
|
+
Object.defineProperty(this, "isLoop", {
|
|
540
|
+
enumerable: true,
|
|
541
|
+
configurable: true,
|
|
542
|
+
writable: true,
|
|
543
|
+
value: void 0
|
|
544
|
+
});
|
|
545
|
+
Object.defineProperty(this, "isFull", {
|
|
546
|
+
enumerable: true,
|
|
547
|
+
configurable: true,
|
|
548
|
+
writable: true,
|
|
549
|
+
value: void 0
|
|
550
|
+
});
|
|
551
|
+
Object.defineProperty(this, "adsDuration", {
|
|
552
|
+
enumerable: true,
|
|
553
|
+
configurable: true,
|
|
554
|
+
writable: true,
|
|
555
|
+
value: void 0
|
|
556
|
+
});
|
|
557
|
+
Object.defineProperty(this, "loopStart", {
|
|
558
|
+
enumerable: true,
|
|
559
|
+
configurable: true,
|
|
560
|
+
writable: true,
|
|
561
|
+
value: void 0
|
|
562
|
+
});
|
|
563
|
+
Object.defineProperty(this, "loopDuration", {
|
|
564
|
+
enumerable: true,
|
|
565
|
+
configurable: true,
|
|
566
|
+
writable: true,
|
|
567
|
+
value: void 0
|
|
568
|
+
});
|
|
569
|
+
Object.defineProperty(this, "noteDuration", {
|
|
570
|
+
enumerable: true,
|
|
571
|
+
configurable: true,
|
|
572
|
+
writable: true,
|
|
573
|
+
value: void 0
|
|
574
|
+
});
|
|
575
|
+
Object.defineProperty(this, "releaseDuration", {
|
|
576
|
+
enumerable: true,
|
|
577
|
+
configurable: true,
|
|
578
|
+
writable: true,
|
|
579
|
+
value: void 0
|
|
580
|
+
});
|
|
581
|
+
this.buffer = buffer;
|
|
582
|
+
this.isLoop = meta.isLoop ?? false;
|
|
583
|
+
this.isFull = meta.isFull ?? false;
|
|
584
|
+
this.adsDuration = meta.adsDuration;
|
|
585
|
+
this.loopStart = meta.loopStart;
|
|
586
|
+
this.loopDuration = meta.loopDuration;
|
|
587
|
+
this.noteDuration = meta.noteDuration;
|
|
588
|
+
this.releaseDuration = meta.releaseDuration;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
456
591
|
function cbToRatio(cb) {
|
|
457
592
|
return Math.pow(10, cb / 200);
|
|
458
593
|
}
|
|
459
594
|
const decayCurve = 1 / (-Math.log(cbToRatio(-1000)));
|
|
460
595
|
const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
|
|
461
596
|
class Midy extends EventTarget {
|
|
462
|
-
constructor(audioContext) {
|
|
597
|
+
constructor(audioContext, options = {}) {
|
|
463
598
|
super();
|
|
464
599
|
// https://pmc.ncbi.nlm.nih.gov/articles/PMC4191557/
|
|
465
600
|
// https://pubmed.ncbi.nlm.nih.gov/12488797/
|
|
@@ -494,7 +629,7 @@ class Midy extends EventTarget {
|
|
|
494
629
|
configurable: true,
|
|
495
630
|
writable: true,
|
|
496
631
|
value: {
|
|
497
|
-
algorithm: "
|
|
632
|
+
algorithm: "Schroeder",
|
|
498
633
|
time: this.getReverbTime(64),
|
|
499
634
|
feedback: 0.8,
|
|
500
635
|
}
|
|
@@ -641,9 +776,7 @@ class Midy extends EventTarget {
|
|
|
641
776
|
enumerable: true,
|
|
642
777
|
configurable: true,
|
|
643
778
|
writable: true,
|
|
644
|
-
value: new Set([
|
|
645
|
-
"noteOff",
|
|
646
|
-
])
|
|
779
|
+
value: new Set(["noteOff"])
|
|
647
780
|
});
|
|
648
781
|
Object.defineProperty(this, "tempo", {
|
|
649
782
|
enumerable: true,
|
|
@@ -699,6 +832,52 @@ class Midy extends EventTarget {
|
|
|
699
832
|
writable: true,
|
|
700
833
|
value: new Array(this.numChannels * drumExclusiveClassCount)
|
|
701
834
|
});
|
|
835
|
+
// "adsr" mode
|
|
836
|
+
Object.defineProperty(this, "adsrVoiceCache", {
|
|
837
|
+
enumerable: true,
|
|
838
|
+
configurable: true,
|
|
839
|
+
writable: true,
|
|
840
|
+
value: new Map()
|
|
841
|
+
});
|
|
842
|
+
// "note" mode
|
|
843
|
+
Object.defineProperty(this, "noteOnDurations", {
|
|
844
|
+
enumerable: true,
|
|
845
|
+
configurable: true,
|
|
846
|
+
writable: true,
|
|
847
|
+
value: new Map()
|
|
848
|
+
});
|
|
849
|
+
Object.defineProperty(this, "noteOnEvents", {
|
|
850
|
+
enumerable: true,
|
|
851
|
+
configurable: true,
|
|
852
|
+
writable: true,
|
|
853
|
+
value: new Map()
|
|
854
|
+
});
|
|
855
|
+
Object.defineProperty(this, "fullVoiceCache", {
|
|
856
|
+
enumerable: true,
|
|
857
|
+
configurable: true,
|
|
858
|
+
writable: true,
|
|
859
|
+
value: new Map()
|
|
860
|
+
});
|
|
861
|
+
// "audio" mode
|
|
862
|
+
Object.defineProperty(this, "renderedAudioBuffer", {
|
|
863
|
+
enumerable: true,
|
|
864
|
+
configurable: true,
|
|
865
|
+
writable: true,
|
|
866
|
+
value: null
|
|
867
|
+
});
|
|
868
|
+
Object.defineProperty(this, "isRendering", {
|
|
869
|
+
enumerable: true,
|
|
870
|
+
configurable: true,
|
|
871
|
+
writable: true,
|
|
872
|
+
value: false
|
|
873
|
+
});
|
|
874
|
+
Object.defineProperty(this, "audioModeBufferSource", {
|
|
875
|
+
enumerable: true,
|
|
876
|
+
configurable: true,
|
|
877
|
+
writable: true,
|
|
878
|
+
value: null
|
|
879
|
+
});
|
|
880
|
+
// MPE
|
|
702
881
|
Object.defineProperty(this, "mpeEnabled", {
|
|
703
882
|
enumerable: true,
|
|
704
883
|
configurable: true,
|
|
@@ -726,10 +905,8 @@ class Midy extends EventTarget {
|
|
|
726
905
|
noteToChannel: new Map(),
|
|
727
906
|
}
|
|
728
907
|
});
|
|
729
|
-
this.decoder = new ogg_vorbis_1.OggVorbisDecoderWebWorker();
|
|
730
|
-
this.decoderReady = this.decoder.ready;
|
|
731
|
-
this.decoderQueue = Promise.resolve();
|
|
732
908
|
this.audioContext = audioContext;
|
|
909
|
+
this.cacheMode = options.cacheMode ?? DEFAULT_CACHE_MODE;
|
|
733
910
|
this.masterVolume = new GainNode(audioContext);
|
|
734
911
|
this.scheduler = new GainNode(audioContext, { gain: 0 });
|
|
735
912
|
this.schedulerBuffer = new AudioBuffer({
|
|
@@ -741,9 +918,9 @@ class Midy extends EventTarget {
|
|
|
741
918
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
742
919
|
this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
|
|
743
920
|
this.effectHandlers = this.createEffectHandlers();
|
|
744
|
-
this.channels = this.createChannels(
|
|
745
|
-
this.reverbEffect = this.createReverbEffect(
|
|
746
|
-
this.chorusEffect = this.createChorusEffect(
|
|
921
|
+
this.channels = this.createChannels();
|
|
922
|
+
this.reverbEffect = this.createReverbEffect(this.reverb.algorithm);
|
|
923
|
+
this.chorusEffect = this.createChorusEffect();
|
|
747
924
|
this.chorusEffect.output.connect(this.masterVolume);
|
|
748
925
|
this.reverbEffect.output.connect(this.masterVolume);
|
|
749
926
|
this.masterVolume.connect(audioContext.destination);
|
|
@@ -805,9 +982,178 @@ class Midy extends EventTarget {
|
|
|
805
982
|
this.instruments = midiData.instruments;
|
|
806
983
|
this.timeline = midiData.timeline;
|
|
807
984
|
this.totalTime = this.calcTotalTime();
|
|
985
|
+
if (this.cacheMode === "audio") {
|
|
986
|
+
await this.render();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
buildNoteOnDurations() {
|
|
990
|
+
const { timeline, totalTime, noteOnDurations, noteOnEvents, numChannels } = this;
|
|
991
|
+
noteOnDurations.clear();
|
|
992
|
+
noteOnEvents.clear();
|
|
993
|
+
const inverseTempo = 1 / this.tempo;
|
|
994
|
+
const sustainPedal = new Uint8Array(numChannels);
|
|
995
|
+
const sostenutoPedal = new Uint8Array(numChannels);
|
|
996
|
+
const sostenutoKeys = new Array(numChannels).fill(null).map(() => new Set());
|
|
997
|
+
const activeNotes = new Map();
|
|
998
|
+
const pendingOff = new Map();
|
|
999
|
+
const finalizeEntry = (entry, endTime, endTicks) => {
|
|
1000
|
+
const duration = Math.max(0, endTime - entry.startTime);
|
|
1001
|
+
const durationTicks = (endTicks == null || endTicks === Infinity)
|
|
1002
|
+
? Infinity
|
|
1003
|
+
: Math.max(0, endTicks - entry.startTicks);
|
|
1004
|
+
noteOnDurations.set(entry.idx, duration);
|
|
1005
|
+
noteOnEvents.set(entry.idx, {
|
|
1006
|
+
duration,
|
|
1007
|
+
durationTicks,
|
|
1008
|
+
startTime: entry.startTime,
|
|
1009
|
+
events: entry.events,
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
1013
|
+
const event = timeline[i];
|
|
1014
|
+
const t = event.startTime * inverseTempo;
|
|
1015
|
+
switch (event.type) {
|
|
1016
|
+
case "noteOn": {
|
|
1017
|
+
const key = event.noteNumber * numChannels + event.channel;
|
|
1018
|
+
if (!activeNotes.has(key))
|
|
1019
|
+
activeNotes.set(key, []);
|
|
1020
|
+
activeNotes.get(key).push({
|
|
1021
|
+
idx: i,
|
|
1022
|
+
startTime: t,
|
|
1023
|
+
startTicks: event.ticks,
|
|
1024
|
+
events: [],
|
|
1025
|
+
});
|
|
1026
|
+
const pendingStack = pendingOff.get(key);
|
|
1027
|
+
if (pendingStack && pendingStack.length > 0)
|
|
1028
|
+
pendingStack.shift();
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
case "noteOff": {
|
|
1032
|
+
const ch = event.channel;
|
|
1033
|
+
const key = event.noteNumber * numChannels + ch;
|
|
1034
|
+
const isSostenuto = sostenutoKeys[ch].has(key);
|
|
1035
|
+
if (sustainPedal[ch] || isSostenuto) {
|
|
1036
|
+
if (!pendingOff.has(key))
|
|
1037
|
+
pendingOff.set(key, []);
|
|
1038
|
+
pendingOff.get(key).push({ t, ticks: event.ticks });
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
const stack = activeNotes.get(key);
|
|
1042
|
+
if (stack && stack.length > 0) {
|
|
1043
|
+
finalizeEntry(stack.shift(), t, event.ticks);
|
|
1044
|
+
if (stack.length === 0)
|
|
1045
|
+
activeNotes.delete(key);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
case "controller": {
|
|
1051
|
+
const ch = event.channel;
|
|
1052
|
+
for (const [key, entries] of activeNotes) {
|
|
1053
|
+
if (key % numChannels !== ch)
|
|
1054
|
+
continue;
|
|
1055
|
+
for (const entry of entries)
|
|
1056
|
+
entry.events.push(event);
|
|
1057
|
+
}
|
|
1058
|
+
switch (event.controllerType) {
|
|
1059
|
+
case 64: { // Sustain Pedal
|
|
1060
|
+
const on = event.value >= 64;
|
|
1061
|
+
sustainPedal[ch] = on ? 1 : 0;
|
|
1062
|
+
if (!on) {
|
|
1063
|
+
for (const [key, offItems] of pendingOff) {
|
|
1064
|
+
if (key % numChannels !== ch)
|
|
1065
|
+
continue;
|
|
1066
|
+
const activeStack = activeNotes.get(key);
|
|
1067
|
+
for (const { t: offTime, ticks: offTicks } of offItems) {
|
|
1068
|
+
if (activeStack && activeStack.length > 0) {
|
|
1069
|
+
finalizeEntry(activeStack.shift(), offTime, offTicks);
|
|
1070
|
+
if (activeStack.length === 0)
|
|
1071
|
+
activeNotes.delete(key);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
pendingOff.delete(key);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
case 66: { // Sostenuto Pedal
|
|
1080
|
+
const on = event.value >= 64;
|
|
1081
|
+
if (on && !sostenutoPedal[ch]) {
|
|
1082
|
+
for (const [key] of activeNotes) {
|
|
1083
|
+
if (key % numChannels === ch)
|
|
1084
|
+
sostenutoKeys[ch].add(key);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
else if (!on) {
|
|
1088
|
+
sostenutoKeys[ch].clear();
|
|
1089
|
+
}
|
|
1090
|
+
sostenutoPedal[ch] = on ? 1 : 0;
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
case 121: // Reset All Controllers
|
|
1094
|
+
sustainPedal[ch] = 0;
|
|
1095
|
+
sostenutoPedal[ch] = 0;
|
|
1096
|
+
sostenutoKeys[ch].clear();
|
|
1097
|
+
break;
|
|
1098
|
+
case 120: // All Sound Off
|
|
1099
|
+
case 123: { // All Notes Off
|
|
1100
|
+
for (const [key, stack] of activeNotes) {
|
|
1101
|
+
if (key % numChannels !== ch)
|
|
1102
|
+
continue;
|
|
1103
|
+
for (const entry of stack)
|
|
1104
|
+
finalizeEntry(entry, t, event.ticks);
|
|
1105
|
+
activeNotes.delete(key);
|
|
1106
|
+
}
|
|
1107
|
+
for (const key of pendingOff.keys()) {
|
|
1108
|
+
if (key % numChannels === ch)
|
|
1109
|
+
pendingOff.delete(key);
|
|
1110
|
+
}
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
case "sysEx":
|
|
1117
|
+
if (event.data[0] === 126 && event.data[1] === 9 && event.data[2] === 3) {
|
|
1118
|
+
// GM1 System On / GM2 System On
|
|
1119
|
+
if (event.data[3] === 1 || event.data[3] === 3) {
|
|
1120
|
+
sustainPedal.fill(0);
|
|
1121
|
+
pendingOff.clear();
|
|
1122
|
+
for (const [, stack] of activeNotes) {
|
|
1123
|
+
for (const entry of stack)
|
|
1124
|
+
finalizeEntry(entry, t, event.ticks);
|
|
1125
|
+
}
|
|
1126
|
+
activeNotes.clear();
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
else {
|
|
1130
|
+
for (const [, entries] of activeNotes) {
|
|
1131
|
+
for (const entry of entries)
|
|
1132
|
+
entry.events.push(event);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
break;
|
|
1136
|
+
case "pitchBend":
|
|
1137
|
+
case "programChange":
|
|
1138
|
+
case "channelAftertouch":
|
|
1139
|
+
case "noteAftertouch": {
|
|
1140
|
+
const ch = event.channel;
|
|
1141
|
+
for (const [key, entries] of activeNotes) {
|
|
1142
|
+
if (key % numChannels !== ch)
|
|
1143
|
+
continue;
|
|
1144
|
+
for (const entry of entries)
|
|
1145
|
+
entry.events.push(event);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
for (const [, stack] of activeNotes) {
|
|
1151
|
+
for (const entry of stack)
|
|
1152
|
+
finalizeEntry(entry, totalTime, Infinity);
|
|
1153
|
+
}
|
|
808
1154
|
}
|
|
809
1155
|
cacheVoiceIds() {
|
|
810
|
-
const { channels, timeline, voiceCounter } = this;
|
|
1156
|
+
const { channels, timeline, voiceCounter, cacheMode } = this;
|
|
811
1157
|
for (let i = 0; i < timeline.length; i++) {
|
|
812
1158
|
const event = timeline[i];
|
|
813
1159
|
switch (event.type) {
|
|
@@ -833,6 +1179,9 @@ class Midy extends EventTarget {
|
|
|
833
1179
|
voiceCounter.delete(audioBufferId);
|
|
834
1180
|
}
|
|
835
1181
|
this.GM2SystemOn();
|
|
1182
|
+
if (cacheMode === "adsr" || cacheMode === "note" || cacheMode === "audio") {
|
|
1183
|
+
this.buildNoteOnDurations();
|
|
1184
|
+
}
|
|
836
1185
|
}
|
|
837
1186
|
getVoiceId(channel, noteNumber, velocity) {
|
|
838
1187
|
const programNumber = channel.programNumber;
|
|
@@ -850,8 +1199,11 @@ class Midy extends EventTarget {
|
|
|
850
1199
|
return;
|
|
851
1200
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
852
1201
|
const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
1202
|
+
if (!voice)
|
|
1203
|
+
return;
|
|
853
1204
|
const { instrument, sampleID } = voice.generators;
|
|
854
|
-
return soundFontIndex * (2 **
|
|
1205
|
+
return soundFontIndex * (2 ** 31) + instrument * (2 ** 24) +
|
|
1206
|
+
(sampleID << 8);
|
|
855
1207
|
}
|
|
856
1208
|
createChannelAudioNodes(audioContext) {
|
|
857
1209
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.panMSB.defaultValue);
|
|
@@ -861,15 +1213,12 @@ class Midy extends EventTarget {
|
|
|
861
1213
|
gainL.connect(merger, 0, 0);
|
|
862
1214
|
gainR.connect(merger, 0, 1);
|
|
863
1215
|
merger.connect(this.masterVolume);
|
|
864
|
-
return {
|
|
865
|
-
gainL,
|
|
866
|
-
gainR,
|
|
867
|
-
merger,
|
|
868
|
-
};
|
|
1216
|
+
return { gainL, gainR, merger };
|
|
869
1217
|
}
|
|
870
|
-
createChannels(
|
|
1218
|
+
createChannels() {
|
|
871
1219
|
const settings = this.constructor.channelSettings;
|
|
872
|
-
|
|
1220
|
+
const audioContext = this.audioContext;
|
|
1221
|
+
return Array.from({ length: this.numChannels }, (_, ch) => new Channel(ch, this.createChannelAudioNodes(audioContext), settings));
|
|
873
1222
|
}
|
|
874
1223
|
decodeOggVorbis(sample) {
|
|
875
1224
|
const task = decoderQueue.then(async () => {
|
|
@@ -928,15 +1277,26 @@ class Midy extends EventTarget {
|
|
|
928
1277
|
return ((programNumber === 48 && noteNumber === 88) ||
|
|
929
1278
|
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
930
1279
|
}
|
|
931
|
-
createBufferSource(channel, noteNumber, voiceParams,
|
|
1280
|
+
createBufferSource(channel, noteNumber, voiceParams, renderedOrRaw) {
|
|
1281
|
+
const isRendered = renderedOrRaw instanceof RenderedBuffer;
|
|
1282
|
+
const audioBuffer = isRendered ? renderedOrRaw.buffer : renderedOrRaw;
|
|
932
1283
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
933
1284
|
bufferSource.buffer = audioBuffer;
|
|
934
|
-
|
|
1285
|
+
const isDrumLoop = channel.isDrum
|
|
935
1286
|
? this.isLoopDrum(channel, noteNumber)
|
|
936
|
-
:
|
|
1287
|
+
: voiceParams.sampleModes % 2 !== 0;
|
|
1288
|
+
const isLoop = isRendered ? renderedOrRaw.isLoop : isDrumLoop;
|
|
1289
|
+
bufferSource.loop = isLoop;
|
|
937
1290
|
if (bufferSource.loop) {
|
|
938
|
-
|
|
939
|
-
|
|
1291
|
+
if (isRendered && renderedOrRaw.adsDuration != null) {
|
|
1292
|
+
bufferSource.loopStart = renderedOrRaw.loopStart;
|
|
1293
|
+
bufferSource.loopEnd = renderedOrRaw.loopStart +
|
|
1294
|
+
renderedOrRaw.loopDuration;
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
1298
|
+
bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
|
|
1299
|
+
}
|
|
940
1300
|
}
|
|
941
1301
|
return bufferSource;
|
|
942
1302
|
}
|
|
@@ -953,15 +1313,14 @@ class Midy extends EventTarget {
|
|
|
953
1313
|
break;
|
|
954
1314
|
const startTime = t + schedulingOffset;
|
|
955
1315
|
switch (event.type) {
|
|
956
|
-
case "noteOn":
|
|
957
|
-
this.
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
1316
|
+
case "noteOn": {
|
|
1317
|
+
const note = this.createNote(event.channel, event.noteNumber, event.velocity, startTime);
|
|
1318
|
+
note.timelineIndex = queueIndex;
|
|
1319
|
+
this.setupNote(event.channel, note, startTime);
|
|
961
1320
|
break;
|
|
962
1321
|
}
|
|
963
|
-
case "
|
|
964
|
-
this.
|
|
1322
|
+
case "noteOff":
|
|
1323
|
+
this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
965
1324
|
break;
|
|
966
1325
|
case "controller":
|
|
967
1326
|
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
@@ -969,14 +1328,17 @@ class Midy extends EventTarget {
|
|
|
969
1328
|
case "programChange":
|
|
970
1329
|
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
971
1330
|
break;
|
|
972
|
-
case "channelAftertouch":
|
|
973
|
-
this.setChannelPressure(event.channel, event.amount, startTime);
|
|
974
|
-
break;
|
|
975
1331
|
case "pitchBend":
|
|
976
1332
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
977
1333
|
break;
|
|
978
1334
|
case "sysEx":
|
|
979
1335
|
this.handleSysEx(event.data, startTime);
|
|
1336
|
+
break;
|
|
1337
|
+
case "channelAftertouch":
|
|
1338
|
+
this.setChannelPressure(event.channel, event.amount, startTime);
|
|
1339
|
+
break;
|
|
1340
|
+
case "noteAftertouch":
|
|
1341
|
+
this.setPolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
|
|
980
1342
|
}
|
|
981
1343
|
queueIndex++;
|
|
982
1344
|
}
|
|
@@ -997,6 +1359,7 @@ class Midy extends EventTarget {
|
|
|
997
1359
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
998
1360
|
this.voiceCache.clear();
|
|
999
1361
|
this.realtimeVoiceCache.clear();
|
|
1362
|
+
this.adsrVoiceCache.clear();
|
|
1000
1363
|
const channels = this.channels;
|
|
1001
1364
|
for (let ch = 0; ch < channels.length; ch++) {
|
|
1002
1365
|
channels[ch].scheduledNotes = [];
|
|
@@ -1023,14 +1386,104 @@ class Midy extends EventTarget {
|
|
|
1023
1386
|
break;
|
|
1024
1387
|
case "sysEx":
|
|
1025
1388
|
this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
|
|
1389
|
+
break;
|
|
1390
|
+
case "channelAftertouch":
|
|
1391
|
+
this.setChannelPressure(event.channel, event.amount, now - resumeTime + event.startTime * inverseTempo);
|
|
1392
|
+
break;
|
|
1393
|
+
case "noteAftertouch":
|
|
1394
|
+
this.setPolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, now - resumeTime + event.startTime * inverseTempo);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
async playAudioBuffer() {
|
|
1399
|
+
const audioContext = this.audioContext;
|
|
1400
|
+
const paused = this.isPaused;
|
|
1401
|
+
this.isPlaying = true;
|
|
1402
|
+
this.isPaused = false;
|
|
1403
|
+
this.startTime = audioContext.currentTime;
|
|
1404
|
+
if (paused) {
|
|
1405
|
+
this.dispatchEvent(new Event("resumed"));
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
this.dispatchEvent(new Event("started"));
|
|
1409
|
+
}
|
|
1410
|
+
let exitReason;
|
|
1411
|
+
outer: while (true) {
|
|
1412
|
+
const buffer = this.renderedAudioBuffer;
|
|
1413
|
+
const bufferSource = new AudioBufferSourceNode(audioContext, { buffer });
|
|
1414
|
+
bufferSource.playbackRate.value = this.tempo;
|
|
1415
|
+
bufferSource.connect(this.masterVolume);
|
|
1416
|
+
const offset = Math.min(Math.max(this.resumeTime, 0), buffer.duration);
|
|
1417
|
+
bufferSource.start(audioContext.currentTime, offset);
|
|
1418
|
+
this.audioModeBufferSource = bufferSource;
|
|
1419
|
+
let naturalEnded = false;
|
|
1420
|
+
bufferSource.onended = () => {
|
|
1421
|
+
naturalEnded = true;
|
|
1422
|
+
};
|
|
1423
|
+
while (true) {
|
|
1424
|
+
const now = audioContext.currentTime;
|
|
1425
|
+
await this.scheduleTask(() => { }, now + this.noteCheckInterval);
|
|
1426
|
+
if (naturalEnded || this.currentTime() >= this.totalTime) {
|
|
1427
|
+
bufferSource.disconnect();
|
|
1428
|
+
this.audioModeBufferSource = null;
|
|
1429
|
+
if (this.loop) {
|
|
1430
|
+
this.resumeTime = 0;
|
|
1431
|
+
this.startTime = audioContext.currentTime;
|
|
1432
|
+
this.dispatchEvent(new Event("looped"));
|
|
1433
|
+
continue outer;
|
|
1434
|
+
}
|
|
1435
|
+
await audioContext.suspend();
|
|
1436
|
+
exitReason = "ended";
|
|
1437
|
+
break outer;
|
|
1438
|
+
}
|
|
1439
|
+
if (this.isPausing) {
|
|
1440
|
+
this.resumeTime = this.currentTime();
|
|
1441
|
+
bufferSource.stop();
|
|
1442
|
+
bufferSource.disconnect();
|
|
1443
|
+
this.audioModeBufferSource = null;
|
|
1444
|
+
await audioContext.suspend();
|
|
1445
|
+
this.isPausing = false;
|
|
1446
|
+
exitReason = "paused";
|
|
1447
|
+
break outer;
|
|
1448
|
+
}
|
|
1449
|
+
else if (this.isStopping) {
|
|
1450
|
+
bufferSource.stop();
|
|
1451
|
+
bufferSource.disconnect();
|
|
1452
|
+
this.audioModeBufferSource = null;
|
|
1453
|
+
await audioContext.suspend();
|
|
1454
|
+
this.isStopping = false;
|
|
1455
|
+
exitReason = "stopped";
|
|
1456
|
+
break outer;
|
|
1457
|
+
}
|
|
1458
|
+
else if (this.isSeeking) {
|
|
1459
|
+
bufferSource.stop();
|
|
1460
|
+
bufferSource.disconnect();
|
|
1461
|
+
this.audioModeBufferSource = null;
|
|
1462
|
+
this.startTime = audioContext.currentTime;
|
|
1463
|
+
this.isSeeking = false;
|
|
1464
|
+
this.dispatchEvent(new Event("seeked"));
|
|
1465
|
+
continue outer;
|
|
1466
|
+
}
|
|
1026
1467
|
}
|
|
1027
1468
|
}
|
|
1469
|
+
this.isPlaying = false;
|
|
1470
|
+
if (exitReason === "paused") {
|
|
1471
|
+
this.isPaused = true;
|
|
1472
|
+
this.dispatchEvent(new Event("paused"));
|
|
1473
|
+
}
|
|
1474
|
+
else if (exitReason !== undefined) {
|
|
1475
|
+
this.isPaused = false;
|
|
1476
|
+
this.dispatchEvent(new Event(exitReason));
|
|
1477
|
+
}
|
|
1028
1478
|
}
|
|
1029
1479
|
async playNotes() {
|
|
1030
1480
|
const audioContext = this.audioContext;
|
|
1031
1481
|
if (audioContext.state === "suspended") {
|
|
1032
1482
|
await audioContext.resume();
|
|
1033
1483
|
}
|
|
1484
|
+
if (this.cacheMode === "audio" && this.renderedAudioBuffer) {
|
|
1485
|
+
return await this.playAudioBuffer();
|
|
1486
|
+
}
|
|
1034
1487
|
const paused = this.isPaused;
|
|
1035
1488
|
this.isPlaying = true;
|
|
1036
1489
|
this.isPaused = false;
|
|
@@ -1170,12 +1623,12 @@ class Midy extends EventTarget {
|
|
|
1170
1623
|
if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
|
|
1171
1624
|
switch (data[3]) {
|
|
1172
1625
|
case 1:
|
|
1173
|
-
this.GM1SystemOn(
|
|
1626
|
+
this.GM1SystemOn();
|
|
1174
1627
|
break;
|
|
1175
1628
|
case 2: // GM System Off
|
|
1176
1629
|
break;
|
|
1177
1630
|
case 3:
|
|
1178
|
-
this.GM2SystemOn(
|
|
1631
|
+
this.GM2SystemOn();
|
|
1179
1632
|
break;
|
|
1180
1633
|
default:
|
|
1181
1634
|
console.warn(`Unsupported Exclusive Message: ${data}`);
|
|
@@ -1242,6 +1695,194 @@ class Midy extends EventTarget {
|
|
|
1242
1695
|
this.notePromises = [];
|
|
1243
1696
|
return stopPromise;
|
|
1244
1697
|
}
|
|
1698
|
+
async render() {
|
|
1699
|
+
if (this.isRendering)
|
|
1700
|
+
return;
|
|
1701
|
+
if (this.timeline.length === 0)
|
|
1702
|
+
return;
|
|
1703
|
+
if (this.voiceCounter.size === 0)
|
|
1704
|
+
this.cacheVoiceIds();
|
|
1705
|
+
this.isRendering = true;
|
|
1706
|
+
this.renderedAudioBuffer = null;
|
|
1707
|
+
this.dispatchEvent(new Event("rendering"));
|
|
1708
|
+
const sampleRate = this.audioContext.sampleRate;
|
|
1709
|
+
const totalSamples = Math.ceil((this.totalTime + this.startDelay) * sampleRate);
|
|
1710
|
+
const renderBankMSB = new Uint8Array(this.numChannels);
|
|
1711
|
+
const renderBankLSB = new Uint8Array(this.numChannels);
|
|
1712
|
+
const renderProgramNumber = new Uint8Array(this.numChannels);
|
|
1713
|
+
const renderIsDrum = new Uint8Array(this.numChannels);
|
|
1714
|
+
const renderNoteAftertouch = new Uint8Array(this.numChannels * 128);
|
|
1715
|
+
renderBankMSB.fill(121);
|
|
1716
|
+
renderIsDrum[9] = 1;
|
|
1717
|
+
renderBankMSB[9] = 120;
|
|
1718
|
+
const renderControllerStates = Array.from({ length: this.numChannels }, () => {
|
|
1719
|
+
const state = new Float32Array(256);
|
|
1720
|
+
for (const { type, defaultValue } of Object.values(defaultControllerState)) {
|
|
1721
|
+
state[type] = defaultValue;
|
|
1722
|
+
}
|
|
1723
|
+
return state;
|
|
1724
|
+
});
|
|
1725
|
+
const tasks = [];
|
|
1726
|
+
const timeline = this.timeline;
|
|
1727
|
+
const inverseTempo = 1 / this.tempo;
|
|
1728
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
1729
|
+
const event = timeline[i];
|
|
1730
|
+
const ch = event.channel;
|
|
1731
|
+
switch (event.type) {
|
|
1732
|
+
case "noteOn": {
|
|
1733
|
+
const noteEvent = this.noteOnEvents.get(i);
|
|
1734
|
+
const noteDuration = noteEvent?.duration ??
|
|
1735
|
+
this.noteOnDurations.get(i) ??
|
|
1736
|
+
0;
|
|
1737
|
+
if (noteDuration <= 0)
|
|
1738
|
+
continue;
|
|
1739
|
+
const { noteNumber, velocity } = event;
|
|
1740
|
+
const isDrum = renderIsDrum[ch] === 1;
|
|
1741
|
+
const programNumber = renderProgramNumber[ch];
|
|
1742
|
+
const bankTable = this.soundFontTable[programNumber];
|
|
1743
|
+
if (!bankTable)
|
|
1744
|
+
continue;
|
|
1745
|
+
let bank = isDrum ? 128 : renderBankLSB[ch];
|
|
1746
|
+
if (bankTable[bank] === undefined) {
|
|
1747
|
+
if (isDrum)
|
|
1748
|
+
continue;
|
|
1749
|
+
bank = 0;
|
|
1750
|
+
}
|
|
1751
|
+
const soundFontIndex = bankTable[bank];
|
|
1752
|
+
if (soundFontIndex === undefined)
|
|
1753
|
+
continue;
|
|
1754
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
1755
|
+
const pressure = renderNoteAftertouch[ch * 128 + noteNumber];
|
|
1756
|
+
const fakeChannel = {
|
|
1757
|
+
channelNumber: ch,
|
|
1758
|
+
state: { array: renderControllerStates[ch].slice() },
|
|
1759
|
+
programNumber,
|
|
1760
|
+
isDrum,
|
|
1761
|
+
modulationDepthRange: 50,
|
|
1762
|
+
detune: 0,
|
|
1763
|
+
};
|
|
1764
|
+
const controllerState = this.getControllerState(fakeChannel, noteNumber, velocity, pressure);
|
|
1765
|
+
const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
1766
|
+
if (!voice)
|
|
1767
|
+
continue;
|
|
1768
|
+
const voiceParams = voice.getAllParams(controllerState);
|
|
1769
|
+
const t = event.startTime * inverseTempo + this.startDelay;
|
|
1770
|
+
const fakeNote = { voiceParams, channel: ch, noteNumber, velocity };
|
|
1771
|
+
const promise = (async () => {
|
|
1772
|
+
try {
|
|
1773
|
+
return await this.createFullRenderedBuffer(fakeChannel, fakeNote, voiceParams, noteDuration, noteEvent);
|
|
1774
|
+
}
|
|
1775
|
+
catch (err) {
|
|
1776
|
+
console.warn("render: note render failed", err);
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
})();
|
|
1780
|
+
tasks.push({ t, promise, fakeChannel });
|
|
1781
|
+
break;
|
|
1782
|
+
}
|
|
1783
|
+
case "controller": {
|
|
1784
|
+
const { controllerType, value } = event;
|
|
1785
|
+
switch (controllerType) {
|
|
1786
|
+
case 0: // bankMSB
|
|
1787
|
+
renderBankMSB[ch] = value;
|
|
1788
|
+
if (this.mode === "GM2") {
|
|
1789
|
+
if (value === 120) {
|
|
1790
|
+
renderIsDrum[ch] = 1;
|
|
1791
|
+
}
|
|
1792
|
+
else if (value === 121) {
|
|
1793
|
+
renderIsDrum[ch] = 0;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
break;
|
|
1797
|
+
case 32: // bankLSB
|
|
1798
|
+
renderBankLSB[ch] = value;
|
|
1799
|
+
break;
|
|
1800
|
+
default: {
|
|
1801
|
+
const stateIndex = 128 + controllerType;
|
|
1802
|
+
if (stateIndex < 256) {
|
|
1803
|
+
renderControllerStates[ch][stateIndex] = value / 127;
|
|
1804
|
+
}
|
|
1805
|
+
break;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
break;
|
|
1809
|
+
}
|
|
1810
|
+
case "pitchBend":
|
|
1811
|
+
renderControllerStates[ch][14] = (event.value + 8192) / 16383;
|
|
1812
|
+
break;
|
|
1813
|
+
case "programChange":
|
|
1814
|
+
renderProgramNumber[ch] = event.programNumber;
|
|
1815
|
+
if (this.mode === "GM2") {
|
|
1816
|
+
if (renderBankMSB[ch] === 120) {
|
|
1817
|
+
renderIsDrum[ch] = 1;
|
|
1818
|
+
}
|
|
1819
|
+
else if (renderBankMSB[ch] === 121) {
|
|
1820
|
+
renderIsDrum[ch] = 0;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
break;
|
|
1824
|
+
case "sysEx": {
|
|
1825
|
+
const data = event.data;
|
|
1826
|
+
if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
|
|
1827
|
+
if (data[3] === 1) { // GM1 System On
|
|
1828
|
+
renderBankMSB.fill(0);
|
|
1829
|
+
renderBankLSB.fill(0);
|
|
1830
|
+
renderProgramNumber.fill(0);
|
|
1831
|
+
renderIsDrum.fill(0);
|
|
1832
|
+
renderIsDrum[9] = 1;
|
|
1833
|
+
renderBankMSB[9] = 1;
|
|
1834
|
+
for (let c = 0; c < this.numChannels; c++) {
|
|
1835
|
+
for (const { type, defaultValue } of Object.values(defaultControllerState)) {
|
|
1836
|
+
renderControllerStates[c][type] = defaultValue;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
renderNoteAftertouch.fill(0);
|
|
1840
|
+
}
|
|
1841
|
+
else if (data[3] === 3) { // GM2 System On
|
|
1842
|
+
renderBankMSB.fill(121);
|
|
1843
|
+
renderBankLSB.fill(0);
|
|
1844
|
+
renderProgramNumber.fill(0);
|
|
1845
|
+
renderIsDrum.fill(0);
|
|
1846
|
+
renderIsDrum[9] = 1;
|
|
1847
|
+
renderBankMSB[9] = 120;
|
|
1848
|
+
for (let c = 0; c < this.numChannels; c++) {
|
|
1849
|
+
for (const { type, defaultValue } of Object.values(defaultControllerState)) {
|
|
1850
|
+
renderControllerStates[c][type] = defaultValue;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
renderNoteAftertouch.fill(0);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
case "channelAftertouch":
|
|
1859
|
+
renderControllerStates[ch][13] = event.amount / 127;
|
|
1860
|
+
break;
|
|
1861
|
+
case "noteAftertouch":
|
|
1862
|
+
renderNoteAftertouch[ch * 128 + event.noteNumber] = event.amount;
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const offlineContext = new OfflineAudioContext(2, totalSamples, sampleRate);
|
|
1867
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
1868
|
+
const { t, promise } = tasks[i];
|
|
1869
|
+
const noteBuffer = await promise;
|
|
1870
|
+
if (!noteBuffer)
|
|
1871
|
+
continue;
|
|
1872
|
+
const audioBuffer = noteBuffer instanceof RenderedBuffer
|
|
1873
|
+
? noteBuffer.buffer
|
|
1874
|
+
: noteBuffer;
|
|
1875
|
+
const bufferSource = new AudioBufferSourceNode(offlineContext, {
|
|
1876
|
+
buffer: audioBuffer,
|
|
1877
|
+
});
|
|
1878
|
+
bufferSource.connect(offlineContext.destination);
|
|
1879
|
+
bufferSource.start(t);
|
|
1880
|
+
}
|
|
1881
|
+
this.renderedAudioBuffer = await offlineContext.startRendering();
|
|
1882
|
+
this.isRendering = false;
|
|
1883
|
+
this.dispatchEvent(new Event("rendered"));
|
|
1884
|
+
return this.renderedAudioBuffer;
|
|
1885
|
+
}
|
|
1245
1886
|
async start() {
|
|
1246
1887
|
if (this.isPlaying || this.isPaused)
|
|
1247
1888
|
return;
|
|
@@ -1278,11 +1919,22 @@ class Midy extends EventTarget {
|
|
|
1278
1919
|
}
|
|
1279
1920
|
}
|
|
1280
1921
|
tempoChange(tempo) {
|
|
1922
|
+
const cacheMode = this.cacheMode;
|
|
1281
1923
|
const timeScale = this.tempo / tempo;
|
|
1282
1924
|
this.resumeTime = this.resumeTime * timeScale;
|
|
1283
1925
|
this.tempo = tempo;
|
|
1284
1926
|
this.totalTime = this.calcTotalTime();
|
|
1285
1927
|
this.seekTo(this.currentTime() * timeScale);
|
|
1928
|
+
if (cacheMode === "adsr" || cacheMode === "note" || cacheMode === "audio") {
|
|
1929
|
+
this.buildNoteOnDurations();
|
|
1930
|
+
this.fullVoiceCache.clear();
|
|
1931
|
+
this.adsrVoiceCache.clear();
|
|
1932
|
+
}
|
|
1933
|
+
if (cacheMode === "audio") {
|
|
1934
|
+
if (this.audioModeBufferSource) {
|
|
1935
|
+
this.audioModeBufferSource.playbackRate.setValueAtTime(this.tempo, this.audioContext.currentTime);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1286
1938
|
}
|
|
1287
1939
|
calcTotalTime() {
|
|
1288
1940
|
const totalTimeEventTypes = this.totalTimeEventTypes;
|
|
@@ -1303,6 +1955,9 @@ class Midy extends EventTarget {
|
|
|
1303
1955
|
if (!this.isPlaying)
|
|
1304
1956
|
return this.resumeTime;
|
|
1305
1957
|
const now = this.audioContext.currentTime;
|
|
1958
|
+
if (this.cacheMode === "audio") {
|
|
1959
|
+
return this.resumeTime + (now - this.startTime) * this.tempo;
|
|
1960
|
+
}
|
|
1306
1961
|
return now + this.resumeTime - this.startTime;
|
|
1307
1962
|
}
|
|
1308
1963
|
async processScheduledNotes(channel, callback) {
|
|
@@ -1351,62 +2006,6 @@ class Midy extends EventTarget {
|
|
|
1351
2006
|
}
|
|
1352
2007
|
}
|
|
1353
2008
|
}
|
|
1354
|
-
createConvolutionReverbImpulse(audioContext, decay, preDecay) {
|
|
1355
|
-
const sampleRate = audioContext.sampleRate;
|
|
1356
|
-
const length = sampleRate * decay;
|
|
1357
|
-
const impulse = new AudioBuffer({
|
|
1358
|
-
numberOfChannels: 2,
|
|
1359
|
-
length,
|
|
1360
|
-
sampleRate,
|
|
1361
|
-
});
|
|
1362
|
-
const preDecayLength = Math.min(sampleRate * preDecay, length);
|
|
1363
|
-
for (let channel = 0; channel < impulse.numberOfChannels; channel++) {
|
|
1364
|
-
const channelData = impulse.getChannelData(channel);
|
|
1365
|
-
for (let i = 0; i < preDecayLength; i++) {
|
|
1366
|
-
channelData[i] = Math.random() * 2 - 1;
|
|
1367
|
-
}
|
|
1368
|
-
const attenuationFactor = 1 / (sampleRate * decay);
|
|
1369
|
-
for (let i = preDecayLength; i < length; i++) {
|
|
1370
|
-
const attenuation = Math.exp(-(i - preDecayLength) * attenuationFactor);
|
|
1371
|
-
channelData[i] = (Math.random() * 2 - 1) * attenuation;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
return impulse;
|
|
1375
|
-
}
|
|
1376
|
-
createConvolutionReverb(audioContext, impulse) {
|
|
1377
|
-
const convolverNode = new ConvolverNode(audioContext, {
|
|
1378
|
-
buffer: impulse,
|
|
1379
|
-
});
|
|
1380
|
-
return {
|
|
1381
|
-
input: convolverNode,
|
|
1382
|
-
output: convolverNode,
|
|
1383
|
-
convolverNode,
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
createCombFilter(audioContext, input, delay, feedback) {
|
|
1387
|
-
const delayNode = new DelayNode(audioContext, {
|
|
1388
|
-
maxDelayTime: delay,
|
|
1389
|
-
delayTime: delay,
|
|
1390
|
-
});
|
|
1391
|
-
const feedbackGain = new GainNode(audioContext, { gain: feedback });
|
|
1392
|
-
input.connect(delayNode);
|
|
1393
|
-
delayNode.connect(feedbackGain);
|
|
1394
|
-
feedbackGain.connect(delayNode);
|
|
1395
|
-
return delayNode;
|
|
1396
|
-
}
|
|
1397
|
-
createAllpassFilter(audioContext, input, delay, feedback) {
|
|
1398
|
-
const delayNode = new DelayNode(audioContext, {
|
|
1399
|
-
maxDelayTime: delay,
|
|
1400
|
-
delayTime: delay,
|
|
1401
|
-
});
|
|
1402
|
-
const feedbackGain = new GainNode(audioContext, { gain: feedback });
|
|
1403
|
-
const passGain = new GainNode(audioContext, { gain: 1 - feedback });
|
|
1404
|
-
input.connect(delayNode);
|
|
1405
|
-
delayNode.connect(feedbackGain);
|
|
1406
|
-
feedbackGain.connect(delayNode);
|
|
1407
|
-
delayNode.connect(passGain);
|
|
1408
|
-
return passGain;
|
|
1409
|
-
}
|
|
1410
2009
|
generateDistributedArray(center, count, varianceRatio = 0.1, randomness = 0.05) {
|
|
1411
2010
|
const variance = center * varianceRatio;
|
|
1412
2011
|
const array = new Array(count);
|
|
@@ -1417,40 +2016,60 @@ class Midy extends EventTarget {
|
|
|
1417
2016
|
}
|
|
1418
2017
|
return array;
|
|
1419
2018
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
for (let i = 0; i < combDelays.length; i++) {
|
|
1426
|
-
const comb = this.createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
|
|
1427
|
-
comb.connect(mergerGain);
|
|
1428
|
-
}
|
|
1429
|
-
const allpasses = [];
|
|
1430
|
-
for (let i = 0; i < allpassDelays.length; i++) {
|
|
1431
|
-
const allpass = this.createAllpassFilter(audioContext, (i === 0) ? mergerGain : allpasses.at(-1), allpassDelays[i], allpassFeedbacks[i]);
|
|
1432
|
-
allpasses.push(allpass);
|
|
1433
|
-
}
|
|
1434
|
-
const output = allpasses.at(-1);
|
|
1435
|
-
return { input, output };
|
|
2019
|
+
setReverbEffect(algorithm) {
|
|
2020
|
+
if (this.reverbEffect)
|
|
2021
|
+
this.reverbEffect.output.disconnect();
|
|
2022
|
+
this.reverbEffect = this.createReverbEffect(algorithm);
|
|
2023
|
+
this.reverb.algorithm = algorithm;
|
|
1436
2024
|
}
|
|
1437
|
-
createReverbEffect(
|
|
1438
|
-
const {
|
|
2025
|
+
createReverbEffect(algorithm) {
|
|
2026
|
+
const { audioContext, reverb } = this;
|
|
2027
|
+
const { time: rt60, feedback } = reverb;
|
|
1439
2028
|
switch (algorithm) {
|
|
1440
|
-
case "
|
|
1441
|
-
const impulse =
|
|
1442
|
-
return
|
|
2029
|
+
case "Convolution": {
|
|
2030
|
+
const impulse = (0, reverb_js_1.createConvolutionReverbImpulse)(audioContext, rt60, this.calcDelay(rt60, feedback));
|
|
2031
|
+
return (0, reverb_js_1.createConvolutionReverb)(audioContext, impulse);
|
|
1443
2032
|
}
|
|
1444
|
-
case "
|
|
2033
|
+
case "Schroeder": {
|
|
1445
2034
|
const combFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
1446
|
-
const combDelays = combFeedbacks.map((
|
|
2035
|
+
const combDelays = combFeedbacks.map((fb) => this.calcDelay(rt60, fb));
|
|
1447
2036
|
const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
1448
|
-
const allpassDelays = allpassFeedbacks.map((
|
|
1449
|
-
return
|
|
2037
|
+
const allpassDelays = allpassFeedbacks.map((fb) => this.calcDelay(rt60, fb));
|
|
2038
|
+
return (0, reverb_js_1.createSchroederReverb)(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
|
|
2039
|
+
}
|
|
2040
|
+
case "Moorer":
|
|
2041
|
+
return (0, reverb_js_1.createMoorerReverbDefault)(audioContext, {
|
|
2042
|
+
rt60,
|
|
2043
|
+
damping: 1 - feedback,
|
|
2044
|
+
});
|
|
2045
|
+
case "FDN":
|
|
2046
|
+
return (0, reverb_js_1.createFDNDefault)(audioContext, { rt60, damping: 1 - feedback });
|
|
2047
|
+
case "Dattorro": {
|
|
2048
|
+
const decay = feedback * 0.28 + 0.7;
|
|
2049
|
+
return (0, reverb_js_1.createDattorroReverb)(audioContext, {
|
|
2050
|
+
decay,
|
|
2051
|
+
damping: 1 - feedback,
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
case "Freeverb": {
|
|
2055
|
+
const damping = 1 - feedback;
|
|
2056
|
+
const { inputL, inputR, outputL, outputR } = (0, reverb_js_1.createFreeverb)(audioContext, { roomSize: feedback, damping });
|
|
2057
|
+
const inputMerger = new GainNode(audioContext);
|
|
2058
|
+
const outputMerger = new GainNode(audioContext, { gain: 0.5 });
|
|
2059
|
+
inputMerger.connect(inputL);
|
|
2060
|
+
inputMerger.connect(inputR);
|
|
2061
|
+
outputL.connect(outputMerger);
|
|
2062
|
+
outputR.connect(outputMerger);
|
|
2063
|
+
return { input: inputMerger, output: outputMerger };
|
|
1450
2064
|
}
|
|
2065
|
+
case "VelvetNoise":
|
|
2066
|
+
return (0, reverb_js_1.createVelvetNoiseReverb)(audioContext, rt60);
|
|
2067
|
+
default:
|
|
2068
|
+
throw new Error(`Unknown reverb algorithm: ${algorithm}`);
|
|
1451
2069
|
}
|
|
1452
2070
|
}
|
|
1453
|
-
createChorusEffect(
|
|
2071
|
+
createChorusEffect() {
|
|
2072
|
+
const audioContext = this.audioContext;
|
|
1454
2073
|
const input = new GainNode(audioContext);
|
|
1455
2074
|
const output = new GainNode(audioContext);
|
|
1456
2075
|
const sendGain = new GainNode(audioContext);
|
|
@@ -1516,6 +2135,8 @@ class Midy extends EventTarget {
|
|
|
1516
2135
|
}
|
|
1517
2136
|
updateChannelDetune(channel, scheduleTime) {
|
|
1518
2137
|
this.processScheduledNotes(channel, (note) => {
|
|
2138
|
+
if (note.renderedBuffer?.isFull)
|
|
2139
|
+
return;
|
|
1519
2140
|
if (this.isPortamento(channel, note)) {
|
|
1520
2141
|
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
1521
2142
|
}
|
|
@@ -1607,6 +2228,8 @@ class Midy extends EventTarget {
|
|
|
1607
2228
|
.exponentialRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1608
2229
|
}
|
|
1609
2230
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
2231
|
+
if (!note.volumeEnvelopeNode)
|
|
2232
|
+
return;
|
|
1610
2233
|
const { voiceParams, startTime, noteNumber } = note;
|
|
1611
2234
|
const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
|
|
1612
2235
|
(1 + this.getChannelAmplitudeControl(channel));
|
|
@@ -1629,9 +2252,10 @@ class Midy extends EventTarget {
|
|
|
1629
2252
|
}
|
|
1630
2253
|
setVolumeNode(channel, note, scheduleTime) {
|
|
1631
2254
|
const depth = 1 + this.getNoteAmplitudeControl(channel, note);
|
|
2255
|
+
const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
|
|
1632
2256
|
note.volumeNode.gain
|
|
1633
|
-
.
|
|
1634
|
-
.
|
|
2257
|
+
.cancelAndHoldAtTime(scheduleTime)
|
|
2258
|
+
.setTargetAtTime(depth, scheduleTime, timeConstant);
|
|
1635
2259
|
}
|
|
1636
2260
|
setPortamentoDetune(channel, note, scheduleTime) {
|
|
1637
2261
|
if (channel.portamentoControl) {
|
|
@@ -1652,9 +2276,6 @@ class Midy extends EventTarget {
|
|
|
1652
2276
|
}
|
|
1653
2277
|
setDetune(channel, note, scheduleTime) {
|
|
1654
2278
|
const detune = this.calcNoteDetune(channel, note);
|
|
1655
|
-
note.bufferSource.detune
|
|
1656
|
-
.cancelScheduledValues(scheduleTime)
|
|
1657
|
-
.setValueAtTime(detune, scheduleTime);
|
|
1658
2279
|
const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
|
|
1659
2280
|
note.bufferSource.detune
|
|
1660
2281
|
.cancelAndHoldAtTime(scheduleTime)
|
|
@@ -1717,6 +2338,8 @@ class Midy extends EventTarget {
|
|
|
1717
2338
|
.exponentialRampToValueAtTime(adjustedSustainFreq, portamentoTime);
|
|
1718
2339
|
}
|
|
1719
2340
|
setFilterEnvelope(channel, note, scheduleTime) {
|
|
2341
|
+
if (!note.filterEnvelopeNode)
|
|
2342
|
+
return;
|
|
1720
2343
|
const { voiceParams, startTime, noteNumber } = note;
|
|
1721
2344
|
const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
|
|
1722
2345
|
const baseCent = voiceParams.initialFilterFc +
|
|
@@ -1762,54 +2385,373 @@ class Midy extends EventTarget {
|
|
|
1762
2385
|
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
1763
2386
|
note.modLfo.start(note.startTime + voiceParams.delayModLFO);
|
|
1764
2387
|
note.modLfo.connect(note.modLfoToFilterFc);
|
|
1765
|
-
|
|
2388
|
+
if (note.filterEnvelopeNode) {
|
|
2389
|
+
note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
|
|
2390
|
+
}
|
|
1766
2391
|
note.modLfo.connect(note.modLfoToPitch);
|
|
1767
2392
|
note.modLfoToPitch.connect(note.bufferSource.detune);
|
|
1768
2393
|
note.modLfo.connect(note.modLfoToVolume);
|
|
1769
|
-
note.
|
|
2394
|
+
const volumeTarget = note.volumeEnvelopeNode ?? note.volumeNode;
|
|
2395
|
+
note.modLfoToVolume.connect(volumeTarget.gain);
|
|
1770
2396
|
}
|
|
1771
2397
|
startVibrato(channel, note, scheduleTime) {
|
|
2398
|
+
const audioContext = this.audioContext;
|
|
1772
2399
|
const { voiceParams, noteNumber } = note;
|
|
1773
2400
|
const vibratoRate = this.getRelativeKeyBasedValue(channel, noteNumber, 76) *
|
|
1774
2401
|
2;
|
|
1775
2402
|
const vibratoDelay = this.getRelativeKeyBasedValue(channel, noteNumber, 78) * 2;
|
|
1776
|
-
note.vibLfo = new OscillatorNode(
|
|
2403
|
+
note.vibLfo = new OscillatorNode(audioContext, {
|
|
1777
2404
|
frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
|
|
1778
2405
|
});
|
|
1779
2406
|
note.vibLfo.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
|
|
1780
|
-
note.vibLfoToPitch = new GainNode(
|
|
2407
|
+
note.vibLfoToPitch = new GainNode(audioContext);
|
|
1781
2408
|
this.setVibLfoToPitch(channel, note, scheduleTime);
|
|
1782
2409
|
note.vibLfo.connect(note.vibLfoToPitch);
|
|
1783
2410
|
note.vibLfoToPitch.connect(note.bufferSource.detune);
|
|
1784
2411
|
}
|
|
1785
|
-
async
|
|
2412
|
+
async createAdsRenderedBuffer(channel, note, voiceParams, audioBuffer, isDrum = false) {
|
|
2413
|
+
const isLoop = isDrum ? false : (voiceParams.sampleModes % 2 !== 0);
|
|
2414
|
+
const volAttack = voiceParams.volDelay + voiceParams.volAttack;
|
|
2415
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
2416
|
+
const decayDuration = voiceParams.volDecay;
|
|
2417
|
+
const adsDuration = volHold + decayDuration * decayCurve * 5;
|
|
2418
|
+
const sampleLoopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
2419
|
+
const sampleLoopDuration = isLoop
|
|
2420
|
+
? (voiceParams.loopEnd - voiceParams.loopStart) / voiceParams.sampleRate
|
|
2421
|
+
: 0;
|
|
2422
|
+
const playbackRate = voiceParams.playbackRate;
|
|
2423
|
+
const outputLoopStart = sampleLoopStart / playbackRate;
|
|
2424
|
+
const outputLoopDuration = sampleLoopDuration / playbackRate;
|
|
2425
|
+
const loopCount = isLoop && adsDuration > outputLoopStart
|
|
2426
|
+
? Math.ceil((adsDuration - outputLoopStart) / outputLoopDuration)
|
|
2427
|
+
: 0;
|
|
2428
|
+
const alignedLoopStart = outputLoopStart + loopCount * outputLoopDuration;
|
|
2429
|
+
const renderDuration = isLoop
|
|
2430
|
+
? alignedLoopStart + outputLoopDuration
|
|
2431
|
+
: audioBuffer.duration / playbackRate;
|
|
2432
|
+
const sampleRate = this.audioContext.sampleRate;
|
|
2433
|
+
const offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, Math.ceil(renderDuration * sampleRate), sampleRate);
|
|
2434
|
+
const bufferSource = new AudioBufferSourceNode(offlineContext);
|
|
2435
|
+
bufferSource.buffer = audioBuffer;
|
|
2436
|
+
bufferSource.playbackRate.value = playbackRate;
|
|
2437
|
+
bufferSource.loop = isLoop;
|
|
2438
|
+
if (isLoop) {
|
|
2439
|
+
bufferSource.loopStart = sampleLoopStart;
|
|
2440
|
+
bufferSource.loopEnd = sampleLoopStart + sampleLoopDuration;
|
|
2441
|
+
}
|
|
2442
|
+
const initialFreq = this.clampCutoffFrequency(this.centToHz(voiceParams.initialFilterFc));
|
|
2443
|
+
const filterEnvelopeNode = new BiquadFilterNode(offlineContext, {
|
|
2444
|
+
type: "lowpass",
|
|
2445
|
+
Q: voiceParams.initialFilterQ / 10, // dB
|
|
2446
|
+
frequency: initialFreq,
|
|
2447
|
+
});
|
|
2448
|
+
const volumeEnvelopeNode = new GainNode(offlineContext);
|
|
2449
|
+
const offlineNote = {
|
|
2450
|
+
...note,
|
|
2451
|
+
startTime: 0,
|
|
2452
|
+
bufferSource,
|
|
2453
|
+
filterEnvelopeNode,
|
|
2454
|
+
volumeEnvelopeNode,
|
|
2455
|
+
};
|
|
2456
|
+
this.setVolumeEnvelope(channel, offlineNote, 0);
|
|
2457
|
+
this.setFilterEnvelope(channel, offlineNote, 0);
|
|
2458
|
+
bufferSource.connect(filterEnvelopeNode);
|
|
2459
|
+
filterEnvelopeNode.connect(volumeEnvelopeNode);
|
|
2460
|
+
volumeEnvelopeNode.connect(offlineContext.destination);
|
|
2461
|
+
if (voiceParams.sample.type === "compressed") {
|
|
2462
|
+
bufferSource.start(0, voiceParams.start / audioBuffer.sampleRate);
|
|
2463
|
+
}
|
|
2464
|
+
else {
|
|
2465
|
+
bufferSource.start(0);
|
|
2466
|
+
}
|
|
2467
|
+
const buffer = await offlineContext.startRendering();
|
|
2468
|
+
return new RenderedBuffer(buffer, {
|
|
2469
|
+
isLoop,
|
|
2470
|
+
adsDuration,
|
|
2471
|
+
loopStart: alignedLoopStart,
|
|
2472
|
+
loopDuration: outputLoopDuration,
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
async createAdsrRenderedBuffer(channel, note, voiceParams, audioBuffer, noteDuration) {
|
|
2476
|
+
const isLoop = voiceParams.sampleModes % 2 !== 0;
|
|
2477
|
+
const volAttack = voiceParams.volDelay + voiceParams.volAttack;
|
|
2478
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
2479
|
+
const decayDuration = voiceParams.volDecay;
|
|
2480
|
+
const adsDuration = volHold + decayDuration * decayCurve * 5;
|
|
2481
|
+
const releaseDuration = voiceParams.volRelease;
|
|
2482
|
+
const loopStartTime = voiceParams.loopStart / voiceParams.sampleRate;
|
|
2483
|
+
const loopDuration = isLoop
|
|
2484
|
+
? (voiceParams.loopEnd - voiceParams.loopStart) / voiceParams.sampleRate
|
|
2485
|
+
: 0;
|
|
2486
|
+
const noteLoopCount = isLoop && noteDuration > loopStartTime
|
|
2487
|
+
? Math.ceil((noteDuration - loopStartTime) / loopDuration)
|
|
2488
|
+
: 0;
|
|
2489
|
+
const alignedNoteEnd = isLoop
|
|
2490
|
+
? loopStartTime + noteLoopCount * loopDuration
|
|
2491
|
+
: noteDuration;
|
|
2492
|
+
const noteOffTime = alignedNoteEnd;
|
|
2493
|
+
const totalDuration = noteOffTime + releaseDuration;
|
|
2494
|
+
const sampleRate = this.audioContext.sampleRate;
|
|
2495
|
+
const offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, Math.ceil(totalDuration * sampleRate), sampleRate);
|
|
2496
|
+
const bufferSource = new AudioBufferSourceNode(offlineContext);
|
|
2497
|
+
bufferSource.buffer = audioBuffer;
|
|
2498
|
+
bufferSource.playbackRate.value = voiceParams.playbackRate;
|
|
2499
|
+
bufferSource.loop = isLoop;
|
|
2500
|
+
if (isLoop) {
|
|
2501
|
+
bufferSource.loopStart = loopStartTime;
|
|
2502
|
+
bufferSource.loopEnd = loopStartTime + loopDuration;
|
|
2503
|
+
}
|
|
2504
|
+
const initialFreq = this.clampCutoffFrequency(this.centToHz(voiceParams.initialFilterFc));
|
|
2505
|
+
const filterEnvelopeNode = new BiquadFilterNode(offlineContext, {
|
|
2506
|
+
type: "lowpass",
|
|
2507
|
+
Q: voiceParams.initialFilterQ / 10, // dB
|
|
2508
|
+
frequency: initialFreq,
|
|
2509
|
+
});
|
|
2510
|
+
const volumeEnvelopeNode = new GainNode(offlineContext);
|
|
2511
|
+
const offlineNote = {
|
|
2512
|
+
...note,
|
|
2513
|
+
startTime: 0,
|
|
2514
|
+
bufferSource,
|
|
2515
|
+
filterEnvelopeNode,
|
|
2516
|
+
volumeEnvelopeNode,
|
|
2517
|
+
};
|
|
2518
|
+
this.setVolumeEnvelope(channel, offlineNote, 0);
|
|
2519
|
+
this.setFilterEnvelope(channel, offlineNote, 0);
|
|
2520
|
+
const attackVolume = cbToRatio(-voiceParams.initialAttenuation);
|
|
2521
|
+
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
2522
|
+
const volDelayTime = voiceParams.volDelay;
|
|
2523
|
+
const volAttackTime = volDelayTime + voiceParams.volAttack;
|
|
2524
|
+
const volHoldTime = volAttackTime + voiceParams.volHold;
|
|
2525
|
+
let gainAtNoteOff;
|
|
2526
|
+
if (noteOffTime <= volDelayTime) {
|
|
2527
|
+
gainAtNoteOff = 0;
|
|
2528
|
+
}
|
|
2529
|
+
else if (noteOffTime <= volAttackTime) {
|
|
2530
|
+
gainAtNoteOff = 1e-6 + (attackVolume - 1e-6) *
|
|
2531
|
+
(noteOffTime - volDelayTime) / voiceParams.volAttack;
|
|
2532
|
+
}
|
|
2533
|
+
else if (noteOffTime <= volHoldTime) {
|
|
2534
|
+
gainAtNoteOff = attackVolume;
|
|
2535
|
+
}
|
|
2536
|
+
else {
|
|
2537
|
+
const decayElapsed = noteOffTime - volHoldTime;
|
|
2538
|
+
gainAtNoteOff = sustainVolume +
|
|
2539
|
+
(attackVolume - sustainVolume) *
|
|
2540
|
+
Math.exp(-decayElapsed / (decayCurve * voiceParams.volDecay));
|
|
2541
|
+
}
|
|
2542
|
+
volumeEnvelopeNode.gain
|
|
2543
|
+
.cancelScheduledValues(noteOffTime)
|
|
2544
|
+
.setValueAtTime(gainAtNoteOff, noteOffTime)
|
|
2545
|
+
.setTargetAtTime(0, noteOffTime, releaseDuration * releaseCurve);
|
|
2546
|
+
filterEnvelopeNode.frequency
|
|
2547
|
+
.cancelScheduledValues(noteOffTime)
|
|
2548
|
+
.setValueAtTime(initialFreq, noteOffTime)
|
|
2549
|
+
.setTargetAtTime(initialFreq, noteOffTime, voiceParams.modRelease * releaseCurve);
|
|
2550
|
+
bufferSource.connect(filterEnvelopeNode);
|
|
2551
|
+
filterEnvelopeNode.connect(volumeEnvelopeNode);
|
|
2552
|
+
volumeEnvelopeNode.connect(offlineContext.destination);
|
|
2553
|
+
if (isLoop) {
|
|
2554
|
+
bufferSource.start(0, voiceParams.start / audioBuffer.sampleRate);
|
|
2555
|
+
}
|
|
2556
|
+
else {
|
|
2557
|
+
bufferSource.start(0);
|
|
2558
|
+
}
|
|
2559
|
+
const buffer = await offlineContext.startRendering();
|
|
2560
|
+
return new RenderedBuffer(buffer, {
|
|
2561
|
+
isLoop: false,
|
|
2562
|
+
isFull: false,
|
|
2563
|
+
adsDuration,
|
|
2564
|
+
noteDuration: noteOffTime,
|
|
2565
|
+
releaseDuration,
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
async createFullRenderedBuffer(channel, note, voiceParams, noteDuration, noteEvent = {}) {
|
|
2569
|
+
const { startTime: noteStartTime = 0, events: noteEvents = [] } = noteEvent;
|
|
2570
|
+
const ch = channel.channelNumber;
|
|
2571
|
+
const releaseEndDuration = voiceParams.volRelease * releaseCurve * 5;
|
|
2572
|
+
const totalDuration = noteDuration + releaseEndDuration;
|
|
2573
|
+
const sampleRate = this.audioContext.sampleRate;
|
|
2574
|
+
const offlineContext = new OfflineAudioContext(2, Math.ceil(totalDuration * sampleRate), sampleRate);
|
|
2575
|
+
const offlinePlayer = new this.constructor(offlineContext, {
|
|
2576
|
+
cacheMode: "none",
|
|
2577
|
+
});
|
|
2578
|
+
offlineContext.suspend = () => Promise.resolve();
|
|
2579
|
+
offlineContext.resume = () => Promise.resolve();
|
|
2580
|
+
offlinePlayer.soundFonts = this.soundFonts;
|
|
2581
|
+
offlinePlayer.soundFontTable = this.soundFontTable;
|
|
2582
|
+
const dstChannel = offlinePlayer.channels[ch];
|
|
2583
|
+
dstChannel.state.array.set(channel.state.array);
|
|
2584
|
+
dstChannel.isDrum = channel.isDrum;
|
|
2585
|
+
dstChannel.programNumber = channel.programNumber;
|
|
2586
|
+
dstChannel.modulationDepthRange = channel.modulationDepthRange;
|
|
2587
|
+
dstChannel.detune = this.calcChannelDetune(dstChannel);
|
|
2588
|
+
await offlinePlayer.noteOn(ch, note.noteNumber, note.velocity, 0);
|
|
2589
|
+
for (const event of noteEvents) {
|
|
2590
|
+
const t = event.startTime / this.tempo - noteStartTime;
|
|
2591
|
+
if (t < 0 || t > noteDuration)
|
|
2592
|
+
continue;
|
|
2593
|
+
switch (event.type) {
|
|
2594
|
+
case "controller":
|
|
2595
|
+
offlinePlayer.setControlChange(ch, event.controllerType, event.value, t);
|
|
2596
|
+
break;
|
|
2597
|
+
case "pitchBend":
|
|
2598
|
+
offlinePlayer.setPitchBend(ch, event.value + 8192, t);
|
|
2599
|
+
break;
|
|
2600
|
+
case "sysEx":
|
|
2601
|
+
offlinePlayer.handleSysEx(event.data, t);
|
|
2602
|
+
break;
|
|
2603
|
+
case "channelAftertouch":
|
|
2604
|
+
offlinePlayer.setChannelPressure(ch, event.amount, t);
|
|
2605
|
+
break;
|
|
2606
|
+
case "noteAftertouch":
|
|
2607
|
+
offlinePlayer.setPolyphonicKeyPressure(ch, event.noteNumber, event.amount, t);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
offlinePlayer.noteOff(ch, note.noteNumber, 0, noteDuration, true);
|
|
2611
|
+
const buffer = await offlineContext.startRendering();
|
|
2612
|
+
return new RenderedBuffer(buffer, {
|
|
2613
|
+
isLoop: false,
|
|
2614
|
+
isFull: true,
|
|
2615
|
+
noteDuration: noteDuration,
|
|
2616
|
+
releaseDuration: releaseEndDuration,
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
async getAudioBuffer(channel, note, realtime) {
|
|
2620
|
+
const cacheMode = this.cacheMode;
|
|
2621
|
+
const { noteNumber, velocity } = note;
|
|
1786
2622
|
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
2623
|
+
if (!realtime) {
|
|
2624
|
+
if (cacheMode === "note") {
|
|
2625
|
+
return await this.getFullCachedBuffer(channel, note, audioBufferId);
|
|
2626
|
+
}
|
|
2627
|
+
else if (cacheMode === "adsr") {
|
|
2628
|
+
return await this.getAdsrCachedBuffer(channel, note, audioBufferId);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
if (cacheMode === "none") {
|
|
2632
|
+
return await this.createAudioBuffer(note.voiceParams);
|
|
2633
|
+
}
|
|
2634
|
+
// fallback to ADS cache:
|
|
2635
|
+
// - "ads" (realtime or not)
|
|
2636
|
+
// - "adsr" + realtime
|
|
2637
|
+
// - "note" + realtime
|
|
2638
|
+
return await this.getAdsCachedBuffer(channel, note, audioBufferId, realtime);
|
|
2639
|
+
}
|
|
2640
|
+
async getAdsCachedBuffer(channel, note, audioBufferId, realtime) {
|
|
2641
|
+
const cacheKey = audioBufferId + (note.noteNumber << 1) + 1;
|
|
2642
|
+
const voiceParams = note.voiceParams;
|
|
1787
2643
|
if (realtime) {
|
|
1788
|
-
const
|
|
1789
|
-
if (
|
|
1790
|
-
return
|
|
1791
|
-
const
|
|
1792
|
-
this.
|
|
1793
|
-
|
|
2644
|
+
const cached = this.realtimeVoiceCache.get(cacheKey);
|
|
2645
|
+
if (cached)
|
|
2646
|
+
return cached;
|
|
2647
|
+
const rawBuffer = await this.createAudioBuffer(voiceParams);
|
|
2648
|
+
const rendered = await this.createAdsRenderedBuffer(channel, note, voiceParams, rawBuffer, channel.isDrum);
|
|
2649
|
+
this.realtimeVoiceCache.set(cacheKey, rendered);
|
|
2650
|
+
return rendered;
|
|
1794
2651
|
}
|
|
1795
2652
|
else {
|
|
1796
|
-
const cache = this.voiceCache.get(
|
|
2653
|
+
const cache = this.voiceCache.get(cacheKey);
|
|
1797
2654
|
if (cache) {
|
|
1798
2655
|
cache.counter += 1;
|
|
1799
2656
|
if (cache.maxCount <= cache.counter) {
|
|
1800
|
-
this.voiceCache.delete(
|
|
2657
|
+
this.voiceCache.delete(cacheKey);
|
|
1801
2658
|
}
|
|
1802
2659
|
return cache.audioBuffer;
|
|
1803
2660
|
}
|
|
1804
2661
|
else {
|
|
1805
|
-
const maxCount = this.voiceCounter.get(
|
|
1806
|
-
const
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
2662
|
+
const maxCount = this.voiceCounter.get(cacheKey) ?? 0;
|
|
2663
|
+
const rawBuffer = await this.createAudioBuffer(voiceParams);
|
|
2664
|
+
const rendered = await this.createAdsRenderedBuffer(channel, note, voiceParams, rawBuffer, channel.isDrum);
|
|
2665
|
+
const cache = { audioBuffer: rendered, maxCount, counter: 1 };
|
|
2666
|
+
this.voiceCache.set(cacheKey, cache);
|
|
2667
|
+
return rendered;
|
|
1810
2668
|
}
|
|
1811
2669
|
}
|
|
1812
2670
|
}
|
|
2671
|
+
async getAdsrCachedBuffer(channel, note, audioBufferId) {
|
|
2672
|
+
const voiceParams = note.voiceParams;
|
|
2673
|
+
const timelineIndex = note.timelineIndex;
|
|
2674
|
+
const noteEvent = this.noteOnEvents.get(timelineIndex);
|
|
2675
|
+
const noteDurationTicks = noteEvent?.durationTicks ?? 0;
|
|
2676
|
+
const safeTicks = noteDurationTicks === Infinity
|
|
2677
|
+
? 0xffffffffn
|
|
2678
|
+
: BigInt(noteDurationTicks);
|
|
2679
|
+
const volReleaseBits = f64ToBigInt(voiceParams.volRelease);
|
|
2680
|
+
const playbackRateBits = f64ToBigInt(voiceParams.playbackRate);
|
|
2681
|
+
const cacheKey = (BigInt(audioBufferId) << 160n) |
|
|
2682
|
+
(playbackRateBits << 96n) |
|
|
2683
|
+
(safeTicks << 64n) |
|
|
2684
|
+
volReleaseBits;
|
|
2685
|
+
let durationMap = this.adsrVoiceCache.get(audioBufferId);
|
|
2686
|
+
if (!durationMap) {
|
|
2687
|
+
durationMap = new Map();
|
|
2688
|
+
this.adsrVoiceCache.set(audioBufferId, durationMap);
|
|
2689
|
+
}
|
|
2690
|
+
const cached = durationMap.get(cacheKey);
|
|
2691
|
+
if (cached instanceof RenderedBuffer) {
|
|
2692
|
+
return cached;
|
|
2693
|
+
}
|
|
2694
|
+
if (cached instanceof Promise) {
|
|
2695
|
+
const buf = await cached;
|
|
2696
|
+
if (buf == null)
|
|
2697
|
+
return await this.createAudioBuffer(voiceParams);
|
|
2698
|
+
return buf;
|
|
2699
|
+
}
|
|
2700
|
+
const noteDuration = noteEvent?.duration ?? 0;
|
|
2701
|
+
const renderPromise = (async () => {
|
|
2702
|
+
try {
|
|
2703
|
+
const rawBuffer = await this.createAudioBuffer(voiceParams);
|
|
2704
|
+
const rendered = await this.createAdsrRenderedBuffer(channel, note, voiceParams, rawBuffer, noteDuration);
|
|
2705
|
+
durationMap.set(cacheKey, rendered);
|
|
2706
|
+
return rendered;
|
|
2707
|
+
}
|
|
2708
|
+
catch (err) {
|
|
2709
|
+
durationMap.delete(cacheKey);
|
|
2710
|
+
throw err;
|
|
2711
|
+
}
|
|
2712
|
+
})();
|
|
2713
|
+
durationMap.set(cacheKey, renderPromise);
|
|
2714
|
+
return await renderPromise;
|
|
2715
|
+
}
|
|
2716
|
+
async getFullCachedBuffer(channel, note, audioBufferId) {
|
|
2717
|
+
const voiceParams = note.voiceParams;
|
|
2718
|
+
const timelineIndex = note.timelineIndex;
|
|
2719
|
+
const noteEvent = this.noteOnEvents.get(timelineIndex);
|
|
2720
|
+
const noteDuration = noteEvent?.duration ?? 0;
|
|
2721
|
+
const cacheKey = timelineIndex;
|
|
2722
|
+
let durationMap = this.fullVoiceCache.get(audioBufferId);
|
|
2723
|
+
if (!durationMap) {
|
|
2724
|
+
durationMap = new Map();
|
|
2725
|
+
this.fullVoiceCache.set(audioBufferId, durationMap);
|
|
2726
|
+
}
|
|
2727
|
+
const cached = durationMap.get(cacheKey);
|
|
2728
|
+
if (cached instanceof RenderedBuffer) {
|
|
2729
|
+
note.fullCacheVoiceId = audioBufferId;
|
|
2730
|
+
return cached;
|
|
2731
|
+
}
|
|
2732
|
+
if (cached instanceof Promise) {
|
|
2733
|
+
const buf = await cached;
|
|
2734
|
+
if (buf == null)
|
|
2735
|
+
return await this.createAudioBuffer(voiceParams);
|
|
2736
|
+
note.fullCacheVoiceId = audioBufferId;
|
|
2737
|
+
return buf;
|
|
2738
|
+
}
|
|
2739
|
+
const renderPromise = (async () => {
|
|
2740
|
+
try {
|
|
2741
|
+
const rendered = await this.createFullRenderedBuffer(channel, note, voiceParams, noteDuration, noteEvent);
|
|
2742
|
+
durationMap.set(cacheKey, rendered);
|
|
2743
|
+
return rendered;
|
|
2744
|
+
}
|
|
2745
|
+
catch (err) {
|
|
2746
|
+
durationMap.delete(cacheKey);
|
|
2747
|
+
throw err;
|
|
2748
|
+
}
|
|
2749
|
+
})();
|
|
2750
|
+
durationMap.set(cacheKey, renderPromise);
|
|
2751
|
+
const rendered = await renderPromise;
|
|
2752
|
+
note.fullCacheVoiceId = audioBufferId;
|
|
2753
|
+
return rendered;
|
|
2754
|
+
}
|
|
1813
2755
|
async setNoteAudioNode(channel, note, realtime) {
|
|
1814
2756
|
const audioContext = this.audioContext;
|
|
1815
2757
|
const now = audioContext.currentTime;
|
|
@@ -1818,50 +2760,71 @@ class Midy extends EventTarget {
|
|
|
1818
2760
|
const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
|
|
1819
2761
|
const voiceParams = note.voice.getAllParams(controllerState);
|
|
1820
2762
|
note.voiceParams = voiceParams;
|
|
1821
|
-
const audioBuffer = await this.getAudioBuffer(channel,
|
|
2763
|
+
const audioBuffer = await this.getAudioBuffer(channel, note, realtime);
|
|
2764
|
+
const isRendered = audioBuffer instanceof RenderedBuffer;
|
|
2765
|
+
note.renderedBuffer = isRendered ? audioBuffer : null;
|
|
1822
2766
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1823
|
-
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
1824
2767
|
note.volumeNode = new GainNode(audioContext);
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
this.
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2768
|
+
const cacheMode = this.cacheMode;
|
|
2769
|
+
const isFullCached = isRendered && audioBuffer.isFull === true;
|
|
2770
|
+
if (cacheMode === "none") {
|
|
2771
|
+
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
2772
|
+
note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
|
|
2773
|
+
type: "lowpass",
|
|
2774
|
+
Q: voiceParams.initialFilterQ / 10, // dB
|
|
2775
|
+
});
|
|
2776
|
+
const prevNote = channel.scheduledNotes.at(-1);
|
|
2777
|
+
if (prevNote && prevNote.noteNumber !== noteNumber) {
|
|
2778
|
+
note.portamentoNoteNumber = prevNote.noteNumber;
|
|
2779
|
+
}
|
|
2780
|
+
if (!channel.isDrum && this.isPortamento(channel, note)) {
|
|
2781
|
+
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
2782
|
+
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
2783
|
+
this.setPortamentoPitchEnvelope(channel, note, now);
|
|
2784
|
+
this.setPortamentoDetune(channel, note, now);
|
|
2785
|
+
}
|
|
2786
|
+
else {
|
|
2787
|
+
this.setVolumeEnvelope(channel, note, now);
|
|
2788
|
+
this.setFilterEnvelope(channel, note, now);
|
|
2789
|
+
this.setPitchEnvelope(note, now);
|
|
2790
|
+
this.setDetune(channel, note, now);
|
|
2791
|
+
}
|
|
2792
|
+
if (0 < state.vibratoDepth) {
|
|
2793
|
+
this.startVibrato(channel, note, now);
|
|
2794
|
+
}
|
|
2795
|
+
if (0 < state.modulationDepthMSB) {
|
|
2796
|
+
this.startModulation(channel, note, now);
|
|
2797
|
+
}
|
|
2798
|
+
if (channel.mono && channel.currentBufferSource) {
|
|
2799
|
+
channel.currentBufferSource.stop(startTime);
|
|
2800
|
+
channel.currentBufferSource = note.bufferSource;
|
|
2801
|
+
}
|
|
2802
|
+
note.bufferSource.connect(note.filterEnvelopeNode);
|
|
2803
|
+
note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
|
|
2804
|
+
note.volumeEnvelopeNode.connect(note.volumeNode);
|
|
2805
|
+
this.setChorusSend(channel, note, now);
|
|
2806
|
+
this.setReverbSend(channel, note, now);
|
|
2807
|
+
}
|
|
2808
|
+
else if (isFullCached) { // "note" mode
|
|
2809
|
+
note.volumeEnvelopeNode = null;
|
|
2810
|
+
note.filterEnvelopeNode = null;
|
|
2811
|
+
note.bufferSource.connect(note.volumeNode);
|
|
2812
|
+
this.setChorusSend(channel, note, now);
|
|
2813
|
+
this.setReverbSend(channel, note, now);
|
|
2814
|
+
}
|
|
2815
|
+
else { // "ads" / "asdr" mode
|
|
2816
|
+
note.volumeEnvelopeNode = null;
|
|
2817
|
+
note.filterEnvelopeNode = null;
|
|
1845
2818
|
this.setDetune(channel, note, now);
|
|
2819
|
+
if (0 < state.modulationDepthMSB) {
|
|
2820
|
+
this.startModulation(channel, note, now);
|
|
2821
|
+
}
|
|
2822
|
+
note.bufferSource.connect(note.volumeNode);
|
|
2823
|
+
this.setChorusSend(channel, note, now);
|
|
2824
|
+
this.setReverbSend(channel, note, now);
|
|
1846
2825
|
}
|
|
1847
|
-
if (0 < state.vibratoDepth) {
|
|
1848
|
-
this.startVibrato(channel, note, now);
|
|
1849
|
-
}
|
|
1850
|
-
if (0 < state.modulationDepthMSB + state.modulationDepthLSB) {
|
|
1851
|
-
this.startModulation(channel, note, now);
|
|
1852
|
-
}
|
|
1853
|
-
if (channel.mono && channel.currentBufferSource) {
|
|
1854
|
-
channel.currentBufferSource.stop(startTime);
|
|
1855
|
-
channel.currentBufferSource = note.bufferSource;
|
|
1856
|
-
}
|
|
1857
|
-
note.bufferSource.connect(note.filterEnvelopeNode);
|
|
1858
|
-
note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
|
|
1859
|
-
note.volumeEnvelopeNode.connect(note.volumeNode);
|
|
1860
|
-
this.setChorusSend(channel, note, now);
|
|
1861
|
-
this.setReverbSend(channel, note, now);
|
|
1862
2826
|
if (voiceParams.sample.type === "compressed") {
|
|
1863
|
-
|
|
1864
|
-
note.bufferSource.start(startTime, offset);
|
|
2827
|
+
note.bufferSource.start(startTime);
|
|
1865
2828
|
}
|
|
1866
2829
|
else {
|
|
1867
2830
|
note.bufferSource.start(startTime);
|
|
@@ -1903,25 +2866,28 @@ class Midy extends EventTarget {
|
|
|
1903
2866
|
}
|
|
1904
2867
|
setNoteRouting(channelNumber, note, startTime) {
|
|
1905
2868
|
const channel = this.channels[channelNumber];
|
|
1906
|
-
const {
|
|
1907
|
-
if (
|
|
1908
|
-
|
|
1909
|
-
let gainL = keyBasedGainLs[noteNumber];
|
|
1910
|
-
let gainR = keyBasedGainRs[noteNumber];
|
|
1911
|
-
if (!gainL) {
|
|
1912
|
-
const audioNodes = this.createChannelAudioNodes(this.audioContext);
|
|
1913
|
-
gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
|
|
1914
|
-
gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
|
|
1915
|
-
}
|
|
1916
|
-
volumeNode.connect(gainL);
|
|
1917
|
-
volumeNode.connect(gainR);
|
|
2869
|
+
const { volumeNode } = note;
|
|
2870
|
+
if (note.renderedBuffer?.isFull) {
|
|
2871
|
+
volumeNode.connect(this.masterVolume);
|
|
1918
2872
|
}
|
|
1919
2873
|
else {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2874
|
+
if (channel.isDrum) {
|
|
2875
|
+
const noteNumber = note.noteNumber;
|
|
2876
|
+
const { keyBasedGainLs, keyBasedGainRs } = channel;
|
|
2877
|
+
let gainL = keyBasedGainLs[noteNumber];
|
|
2878
|
+
let gainR = keyBasedGainRs[noteNumber];
|
|
2879
|
+
if (!gainL) {
|
|
2880
|
+
const audioNodes = this.createChannelAudioNodes(this.audioContext);
|
|
2881
|
+
gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
|
|
2882
|
+
gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
|
|
2883
|
+
}
|
|
2884
|
+
volumeNode.connect(gainL);
|
|
2885
|
+
volumeNode.connect(gainR);
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
volumeNode.connect(channel.gainL);
|
|
2889
|
+
volumeNode.connect(channel.gainR);
|
|
2890
|
+
}
|
|
1925
2891
|
}
|
|
1926
2892
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1927
2893
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
@@ -1936,17 +2902,19 @@ class Midy extends EventTarget {
|
|
|
1936
2902
|
this.mpeState.channelToNotes.get(channelNumber).add(noteIndex);
|
|
1937
2903
|
this.mpeState.noteToChannel.set(noteIndex, channelNumber);
|
|
1938
2904
|
}
|
|
1939
|
-
|
|
2905
|
+
const note = this.createNote(channelNumber, noteNumber, velocity, startTime);
|
|
2906
|
+
return await this.setupNote(channelNumber, note, startTime);
|
|
1940
2907
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
const realtime = startTime === undefined;
|
|
1944
|
-
if (realtime)
|
|
2908
|
+
createNote(channelNumber, noteNumber, velocity, startTime) {
|
|
2909
|
+
if (!(0 <= startTime))
|
|
1945
2910
|
startTime = this.audioContext.currentTime;
|
|
1946
2911
|
const note = new Note(noteNumber, velocity, startTime);
|
|
1947
|
-
|
|
1948
|
-
note
|
|
1949
|
-
|
|
2912
|
+
note.channel = channelNumber;
|
|
2913
|
+
return note;
|
|
2914
|
+
}
|
|
2915
|
+
async setupNote(channelNumber, note, startTime) {
|
|
2916
|
+
const realtime = startTime === undefined;
|
|
2917
|
+
const channel = this.channels[channelNumber];
|
|
1950
2918
|
const programNumber = channel.programNumber;
|
|
1951
2919
|
const bankTable = this.soundFontTable[programNumber];
|
|
1952
2920
|
if (!bankTable)
|
|
@@ -1961,18 +2929,26 @@ class Midy extends EventTarget {
|
|
|
1961
2929
|
if (soundFontIndex === undefined)
|
|
1962
2930
|
return;
|
|
1963
2931
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
1964
|
-
note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
2932
|
+
note.voice = soundFont.getVoice(bank, programNumber, note.noteNumber, note.velocity);
|
|
1965
2933
|
if (!note.voice)
|
|
1966
2934
|
return;
|
|
2935
|
+
note.index = channel.scheduledNotes.length;
|
|
2936
|
+
channel.scheduledNotes.push(note);
|
|
1967
2937
|
await this.setNoteAudioNode(channel, note, realtime);
|
|
1968
2938
|
this.setNoteRouting(channelNumber, note, startTime);
|
|
1969
2939
|
note.resolveReady();
|
|
2940
|
+
if (0.5 <= channel.state.sustainPedal) {
|
|
2941
|
+
channel.sustainNotes.push(note);
|
|
2942
|
+
}
|
|
2943
|
+
if (0.5 <= channel.state.sostenutoPedal) {
|
|
2944
|
+
channel.sostenutoNotes.push(note);
|
|
2945
|
+
}
|
|
1970
2946
|
return note;
|
|
1971
2947
|
}
|
|
1972
2948
|
disconnectNote(note) {
|
|
1973
2949
|
note.bufferSource.disconnect();
|
|
1974
|
-
note.filterEnvelopeNode
|
|
1975
|
-
note.volumeEnvelopeNode
|
|
2950
|
+
note.filterEnvelopeNode?.disconnect();
|
|
2951
|
+
note.volumeEnvelopeNode?.disconnect();
|
|
1976
2952
|
note.volumeNode.disconnect();
|
|
1977
2953
|
if (note.modLfoToPitch) {
|
|
1978
2954
|
note.modLfoToVolume.disconnect();
|
|
@@ -1990,26 +2966,102 @@ class Midy extends EventTarget {
|
|
|
1990
2966
|
note.chorusSend.disconnect();
|
|
1991
2967
|
}
|
|
1992
2968
|
}
|
|
2969
|
+
releaseFullCache(note) {
|
|
2970
|
+
if (note.timelineIndex == null || note.fullCacheVoiceId == null)
|
|
2971
|
+
return;
|
|
2972
|
+
const durationMap = this.fullVoiceCache.get(note.fullCacheVoiceId);
|
|
2973
|
+
if (!durationMap)
|
|
2974
|
+
return;
|
|
2975
|
+
const entry = durationMap.get(note.timelineIndex);
|
|
2976
|
+
if (entry instanceof RenderedBuffer) {
|
|
2977
|
+
durationMap.delete(note.timelineIndex);
|
|
2978
|
+
if (durationMap.size === 0) {
|
|
2979
|
+
this.fullVoiceCache.delete(note.fullCacheVoiceId);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
1993
2983
|
releaseNote(channel, note, endTime) {
|
|
1994
2984
|
endTime ??= this.audioContext.currentTime;
|
|
2985
|
+
if (note.renderedBuffer?.isFull) {
|
|
2986
|
+
const rb = note.renderedBuffer;
|
|
2987
|
+
const naturalEndTime = note.startTime + rb.buffer.duration;
|
|
2988
|
+
const noteOffTime = note.startTime + (rb.noteDuration ?? 0);
|
|
2989
|
+
const isEarlyCut = endTime < noteOffTime;
|
|
2990
|
+
if (isEarlyCut) {
|
|
2991
|
+
const releaseTime = this.getRelativeKeyBasedValue(channel, note.noteNumber, 72) * 2;
|
|
2992
|
+
const volDuration = note.voiceParams.volRelease * releaseTime;
|
|
2993
|
+
const volRelease = endTime + volDuration;
|
|
2994
|
+
note.volumeNode.gain
|
|
2995
|
+
.cancelScheduledValues(endTime)
|
|
2996
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
2997
|
+
note.bufferSource.stop(volRelease);
|
|
2998
|
+
}
|
|
2999
|
+
else {
|
|
3000
|
+
const now = this.audioContext.currentTime;
|
|
3001
|
+
if (naturalEndTime <= now) {
|
|
3002
|
+
this.disconnectNote(note);
|
|
3003
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
3004
|
+
this.releaseFullCache(note);
|
|
3005
|
+
return Promise.resolve();
|
|
3006
|
+
}
|
|
3007
|
+
note.bufferSource.stop(naturalEndTime);
|
|
3008
|
+
}
|
|
3009
|
+
return new Promise((resolve) => {
|
|
3010
|
+
note.bufferSource.onended = () => {
|
|
3011
|
+
this.disconnectNote(note);
|
|
3012
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
3013
|
+
this.releaseFullCache(note);
|
|
3014
|
+
resolve();
|
|
3015
|
+
};
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
1995
3018
|
const releaseTime = this.getRelativeKeyBasedValue(channel, note.noteNumber, 72) * 2;
|
|
1996
3019
|
const volDuration = note.voiceParams.volRelease * releaseTime;
|
|
1997
3020
|
const volRelease = endTime + volDuration;
|
|
1998
|
-
note.
|
|
1999
|
-
.
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
.
|
|
2003
|
-
|
|
3021
|
+
if (note.volumeEnvelopeNode) { // "none" mode
|
|
3022
|
+
note.filterEnvelopeNode.frequency
|
|
3023
|
+
.cancelScheduledValues(endTime)
|
|
3024
|
+
.setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
|
|
3025
|
+
note.volumeEnvelopeNode.gain
|
|
3026
|
+
.cancelScheduledValues(endTime)
|
|
3027
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
3028
|
+
}
|
|
3029
|
+
else { // "ads" / "adsr" mode
|
|
3030
|
+
const isAdsr = note.renderedBuffer?.releaseDuration != null &&
|
|
3031
|
+
!note.renderedBuffer.isFull;
|
|
3032
|
+
if (isAdsr) {
|
|
3033
|
+
const rb = note.renderedBuffer;
|
|
3034
|
+
const naturalEndTime = note.startTime + rb.buffer.duration;
|
|
3035
|
+
const noteOffTime = note.startTime + (rb.noteDuration ?? 0);
|
|
3036
|
+
const isEarlyCut = endTime < noteOffTime;
|
|
3037
|
+
if (isEarlyCut) {
|
|
3038
|
+
note.volumeNode.gain
|
|
3039
|
+
.cancelScheduledValues(endTime)
|
|
3040
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
3041
|
+
note.bufferSource.stop(volRelease);
|
|
3042
|
+
}
|
|
3043
|
+
else {
|
|
3044
|
+
note.bufferSource.stop(naturalEndTime);
|
|
3045
|
+
}
|
|
3046
|
+
return new Promise((resolve) => {
|
|
3047
|
+
note.bufferSource.onended = () => {
|
|
3048
|
+
this.disconnectNote(note);
|
|
3049
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
3050
|
+
resolve();
|
|
3051
|
+
};
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
note.volumeNode.gain
|
|
3055
|
+
.cancelScheduledValues(endTime)
|
|
3056
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
3057
|
+
}
|
|
3058
|
+
note.bufferSource.stop(volRelease);
|
|
2004
3059
|
return new Promise((resolve) => {
|
|
2005
|
-
|
|
2006
|
-
const bufferSource = note.bufferSource;
|
|
2007
|
-
bufferSource.loop = false;
|
|
2008
|
-
bufferSource.stop(volRelease);
|
|
3060
|
+
note.bufferSource.onended = () => {
|
|
2009
3061
|
this.disconnectNote(note);
|
|
2010
3062
|
channel.scheduledNotes[note.index] = undefined;
|
|
2011
3063
|
resolve();
|
|
2012
|
-
}
|
|
3064
|
+
};
|
|
2013
3065
|
});
|
|
2014
3066
|
}
|
|
2015
3067
|
noteOff(channelNumber, noteNumber, velocity, endTime, force) {
|
|
@@ -2237,7 +3289,7 @@ class Midy extends EventTarget {
|
|
|
2237
3289
|
this.applyVoiceParams(channel, 14, scheduleTime);
|
|
2238
3290
|
}
|
|
2239
3291
|
setModLfoToPitch(channel, note, scheduleTime) {
|
|
2240
|
-
if (note.
|
|
3292
|
+
if (note.modLfoToPitch) {
|
|
2241
3293
|
const { modulationDepthMSB, modulationDepthLSB } = channel.state;
|
|
2242
3294
|
const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
|
|
2243
3295
|
const modLfoToPitch = note.voiceParams.modLfoToPitch +
|
|
@@ -2402,7 +3454,7 @@ class Midy extends EventTarget {
|
|
|
2402
3454
|
reverbEffectsSend: (channel, note, scheduleTime) => {
|
|
2403
3455
|
this.setReverbSend(channel, note, scheduleTime);
|
|
2404
3456
|
},
|
|
2405
|
-
delayModLFO: (
|
|
3457
|
+
delayModLFO: (channel, note, _scheduleTime) => {
|
|
2406
3458
|
const { modulationDepthMSB, modulationDepthLSB } = channel.state;
|
|
2407
3459
|
if (0 < modulationDepthMSB + modulationDepthLSB) {
|
|
2408
3460
|
this.setDelayModLFO(note);
|
|
@@ -2440,11 +3492,12 @@ class Midy extends EventTarget {
|
|
|
2440
3492
|
state[2] = velocity / 127;
|
|
2441
3493
|
state[3] = noteNumber / 127;
|
|
2442
3494
|
state[10] = polyphonicKeyPressure / 127;
|
|
2443
|
-
state[13] = state.channelPressure / 127;
|
|
2444
3495
|
return state;
|
|
2445
3496
|
}
|
|
2446
3497
|
applyVoiceParams(channel, controllerType, scheduleTime) {
|
|
2447
3498
|
this.processScheduledNotes(channel, (note) => {
|
|
3499
|
+
if (note.renderedBuffer?.isFull)
|
|
3500
|
+
return;
|
|
2448
3501
|
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity, note.pressure);
|
|
2449
3502
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
2450
3503
|
let applyVolumeEnvelope = false;
|
|
@@ -2551,8 +3604,8 @@ class Midy extends EventTarget {
|
|
|
2551
3604
|
const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
|
|
2552
3605
|
const depth = modulationDepth * channel.modulationDepthRange;
|
|
2553
3606
|
this.processScheduledNotes(channel, (note) => {
|
|
2554
|
-
if (note.
|
|
2555
|
-
note.
|
|
3607
|
+
if (note.modLfoToPitch) {
|
|
3608
|
+
note.modLfoToPitch.gain.setValueAtTime(depth, scheduleTime);
|
|
2556
3609
|
}
|
|
2557
3610
|
else {
|
|
2558
3611
|
this.startModulation(channel, note, scheduleTime);
|
|
@@ -2707,11 +3760,15 @@ class Midy extends EventTarget {
|
|
|
2707
3760
|
return;
|
|
2708
3761
|
if (!(0 <= scheduleTime))
|
|
2709
3762
|
scheduleTime = this.audioContext.currentTime;
|
|
2710
|
-
|
|
3763
|
+
const state = channel.state;
|
|
3764
|
+
const prevValue = state.sustainPedal;
|
|
3765
|
+
state.sustainPedal = value / 127;
|
|
2711
3766
|
if (64 <= value) {
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
3767
|
+
if (prevValue < 0.5) {
|
|
3768
|
+
this.processScheduledNotes(channel, (note) => {
|
|
3769
|
+
channel.sustainNotes.push(note);
|
|
3770
|
+
});
|
|
3771
|
+
}
|
|
2715
3772
|
}
|
|
2716
3773
|
else {
|
|
2717
3774
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
@@ -2735,13 +3792,17 @@ class Midy extends EventTarget {
|
|
|
2735
3792
|
return;
|
|
2736
3793
|
if (!(0 <= scheduleTime))
|
|
2737
3794
|
scheduleTime = this.audioContext.currentTime;
|
|
2738
|
-
|
|
3795
|
+
const state = channel.state;
|
|
3796
|
+
const prevValue = state.sostenutoPedal;
|
|
3797
|
+
state.sostenutoPedal = value / 127;
|
|
2739
3798
|
if (64 <= value) {
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
3799
|
+
if (prevValue < 0.5) {
|
|
3800
|
+
const sostenutoNotes = [];
|
|
3801
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3802
|
+
sostenutoNotes.push(note);
|
|
3803
|
+
});
|
|
3804
|
+
channel.sostenutoNotes = sostenutoNotes;
|
|
3805
|
+
}
|
|
2745
3806
|
}
|
|
2746
3807
|
else {
|
|
2747
3808
|
this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
|
|
@@ -3111,7 +4172,7 @@ class Midy extends EventTarget {
|
|
|
3111
4172
|
}
|
|
3112
4173
|
}
|
|
3113
4174
|
channel.resetSettings(this.constructor.channelSettings);
|
|
3114
|
-
|
|
4175
|
+
channel.resetTable();
|
|
3115
4176
|
this.mode = "GM2";
|
|
3116
4177
|
this.masterFineTuning = 0; // cent
|
|
3117
4178
|
this.masterCoarseTuning = 0; // cent
|
|
@@ -3274,7 +4335,7 @@ class Midy extends EventTarget {
|
|
|
3274
4335
|
case 9:
|
|
3275
4336
|
switch (data[3]) {
|
|
3276
4337
|
case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
3277
|
-
return this.handleChannelPressureSysEx(data,
|
|
4338
|
+
return this.handleChannelPressureSysEx(data, scheduleTime);
|
|
3278
4339
|
case 2: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
3279
4340
|
return this.handlePolyphonicKeyPressureSysEx(data, scheduleTime);
|
|
3280
4341
|
case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
@@ -3302,9 +4363,10 @@ class Midy extends EventTarget {
|
|
|
3302
4363
|
setMasterVolume(value, scheduleTime) {
|
|
3303
4364
|
if (!(0 <= scheduleTime))
|
|
3304
4365
|
scheduleTime = this.audioContext.currentTime;
|
|
4366
|
+
const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
|
|
3305
4367
|
this.masterVolume.gain
|
|
3306
|
-
.
|
|
3307
|
-
.
|
|
4368
|
+
.cancelAndHoldAtTime(scheduleTime)
|
|
4369
|
+
.setTargetAtTime(value * value, scheduleTime, timeConstant);
|
|
3308
4370
|
}
|
|
3309
4371
|
handleMasterFineTuningSysEx(data, scheduleTime) {
|
|
3310
4372
|
const value = (data[5] * 128 + data[4]) / 16383;
|
|
@@ -3369,7 +4431,7 @@ class Midy extends EventTarget {
|
|
|
3369
4431
|
setReverbType(type) {
|
|
3370
4432
|
this.reverb.time = this.getReverbTimeFromType(type);
|
|
3371
4433
|
this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
|
|
3372
|
-
this.reverbEffect = this.
|
|
4434
|
+
this.reverbEffect = this.setReverbEffect(this.reverb.algorithm);
|
|
3373
4435
|
}
|
|
3374
4436
|
getReverbTimeFromType(type) {
|
|
3375
4437
|
switch (type) {
|
|
@@ -3391,7 +4453,7 @@ class Midy extends EventTarget {
|
|
|
3391
4453
|
}
|
|
3392
4454
|
setReverbTime(value) {
|
|
3393
4455
|
this.reverb.time = this.getReverbTime(value);
|
|
3394
|
-
this.reverbEffect = this.
|
|
4456
|
+
this.reverbEffect = this.setReverbEffect(this.reverb.algorithm);
|
|
3395
4457
|
}
|
|
3396
4458
|
getReverbTime(value) {
|
|
3397
4459
|
return Math.exp((value - 40) * 0.025);
|