@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/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(channel) {
300
- channel.controlTable.set(defaultControlValues);
301
- channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
302
- channel.channelPressureTable.set(defaultPressureValues);
303
- channel.polyphonicKeyPressureTable.set(defaultPressureValues);
304
- channel.keyBasedTable.fill(-1);
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: "SchroederReverb",
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(audioContext);
745
- this.reverbEffect = this.createReverbEffect(audioContext);
746
- this.chorusEffect = this.createChorusEffect(audioContext);
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 ** 32) + (instrument << 16) + sampleID;
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(audioContext) {
1218
+ createChannels() {
871
1219
  const settings = this.constructor.channelSettings;
872
- return Array.from({ length: this.numChannels }, () => new Channel(this.createChannelAudioNodes(audioContext), settings));
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, audioBuffer) {
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
- bufferSource.loop = channel.isDrum
1285
+ const isDrumLoop = channel.isDrum
935
1286
  ? this.isLoopDrum(channel, noteNumber)
936
- : (voiceParams.sampleModes % 2 !== 0);
1287
+ : voiceParams.sampleModes % 2 !== 0;
1288
+ const isLoop = isRendered ? renderedOrRaw.isLoop : isDrumLoop;
1289
+ bufferSource.loop = isLoop;
937
1290
  if (bufferSource.loop) {
938
- bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
939
- bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
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.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
958
- break;
959
- case "noteOff": {
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 "noteAftertouch":
964
- this.setPolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
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(scheduleTime);
1626
+ this.GM1SystemOn();
1174
1627
  break;
1175
1628
  case 2: // GM System Off
1176
1629
  break;
1177
1630
  case 3:
1178
- this.GM2SystemOn(scheduleTime);
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
- // https://hajim.rochester.edu/ece/sites/zduan/teaching/ece472/reading/Schroeder_1962.pdf
1421
- // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
1422
- createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays) {
1423
- const input = new GainNode(audioContext);
1424
- const mergerGain = new GainNode(audioContext);
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(audioContext) {
1438
- const { algorithm, time: rt60, feedback } = this.reverb;
2025
+ createReverbEffect(algorithm) {
2026
+ const { audioContext, reverb } = this;
2027
+ const { time: rt60, feedback } = reverb;
1439
2028
  switch (algorithm) {
1440
- case "ConvolutionReverb": {
1441
- const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
1442
- return this.createConvolutionReverb(audioContext, impulse);
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 "SchroederReverb": {
2033
+ case "Schroeder": {
1445
2034
  const combFeedbacks = this.generateDistributedArray(feedback, 4);
1446
- const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
2035
+ const combDelays = combFeedbacks.map((fb) => this.calcDelay(rt60, fb));
1447
2036
  const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
1448
- const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
1449
- return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
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(audioContext) {
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
- .cancelScheduledValues(scheduleTime)
1634
- .setValueAtTime(depth, scheduleTime);
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
- note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
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.modLfoToVolume.connect(note.volumeEnvelopeNode.gain);
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(this.audioContext, {
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(this.audioContext);
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 getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
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 cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
1789
- if (cachedAudioBuffer)
1790
- return cachedAudioBuffer;
1791
- const audioBuffer = await this.createAudioBuffer(voiceParams);
1792
- this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
1793
- return audioBuffer;
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(audioBufferId);
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(audioBufferId);
2657
+ this.voiceCache.delete(cacheKey);
1801
2658
  }
1802
2659
  return cache.audioBuffer;
1803
2660
  }
1804
2661
  else {
1805
- const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1806
- const audioBuffer = await this.createAudioBuffer(voiceParams);
1807
- const cache = { audioBuffer, maxCount, counter: 1 };
1808
- this.voiceCache.set(audioBufferId, cache);
1809
- return audioBuffer;
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, noteNumber, velocity, voiceParams, realtime);
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 filterResonance = this.getRelativeKeyBasedValue(channel, noteNumber, 71);
1826
- note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
1827
- type: "lowpass",
1828
- Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
1829
- });
1830
- const prevNote = channel.scheduledNotes.at(-1);
1831
- if (prevNote && prevNote.noteNumber !== noteNumber) {
1832
- note.portamentoNoteNumber = prevNote.noteNumber;
1833
- }
1834
- this.setVolumeNode(channel, note, now);
1835
- if (!channel.isDrum && this.isPortamento(channel, note)) {
1836
- this.setPortamentoVolumeEnvelope(channel, note, now);
1837
- this.setPortamentoFilterEnvelope(channel, note, now);
1838
- this.setPortamentoPitchEnvelope(channel, note, now);
1839
- this.setPortamentoDetune(channel, note, now);
1840
- }
1841
- else {
1842
- this.setVolumeEnvelope(channel, note, now);
1843
- this.setFilterEnvelope(channel, note, now);
1844
- this.setPitchEnvelope(note, now);
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
- const offset = voiceParams.start / audioBuffer.sampleRate;
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 { noteNumber, volumeNode } = note;
1907
- if (channel.isDrum) {
1908
- const { keyBasedGainLs, keyBasedGainRs } = channel;
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
- volumeNode.connect(channel.gainL);
1921
- volumeNode.connect(channel.gainR);
1922
- }
1923
- if (0.5 <= channel.state.sustainPedal) {
1924
- channel.sustainNotes.push(note);
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
- await this.startNote(channelNumber, noteNumber, velocity, startTime);
2905
+ const note = this.createNote(channelNumber, noteNumber, velocity, startTime);
2906
+ return await this.setupNote(channelNumber, note, startTime);
1940
2907
  }
1941
- async startNote(channelNumber, noteNumber, velocity, startTime) {
1942
- const channel = this.channels[channelNumber];
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
- const scheduledNotes = channel.scheduledNotes;
1948
- note.index = scheduledNotes.length;
1949
- scheduledNotes.push(note);
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.disconnect();
1975
- note.volumeEnvelopeNode.disconnect();
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.filterEnvelopeNode.frequency
1999
- .cancelScheduledValues(endTime)
2000
- .setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
2001
- note.volumeEnvelopeNode.gain
2002
- .cancelScheduledValues(endTime)
2003
- .setTargetAtTime(0, endTime, volDuration * releaseCurve);
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
- this.scheduleTask(() => {
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
- }, volRelease);
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.modulationDepth) {
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: (_channel, note, _scheduleTime) => {
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.modulationDepth) {
2555
- note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
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
- channel.state.sustainPedal = value / 127;
3763
+ const state = channel.state;
3764
+ const prevValue = state.sustainPedal;
3765
+ state.sustainPedal = value / 127;
2711
3766
  if (64 <= value) {
2712
- this.processScheduledNotes(channel, (note) => {
2713
- channel.sustainNotes.push(note);
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
- channel.state.sostenutoPedal = value / 127;
3795
+ const state = channel.state;
3796
+ const prevValue = state.sostenutoPedal;
3797
+ state.sostenutoPedal = value / 127;
2739
3798
  if (64 <= value) {
2740
- const sostenutoNotes = [];
2741
- this.processActiveNotes(channel, scheduleTime, (note) => {
2742
- sostenutoNotes.push(note);
2743
- });
2744
- channel.sostenutoNotes = sostenutoNotes;
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
- this.resetTable(channel);
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, scheduelTime);
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
- .cancelScheduledValues(scheduleTime)
3307
- .setValueAtTime(value * value, scheduleTime);
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.createReverbEffect(this.audioContext);
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.createReverbEffect(this.audioContext);
4456
+ this.reverbEffect = this.setReverbEffect(this.reverb.algorithm);
3395
4457
  }
3396
4458
  getReverbTime(value) {
3397
4459
  return Math.exp((value - 40) * 0.025);