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