@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/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(channel) {
297
- channel.controlTable.set(defaultControlValues);
298
- channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
299
- channel.channelPressureTable.set(defaultPressureValues);
300
- channel.polyphonicKeyPressureTable.set(defaultPressureValues);
301
- channel.keyBasedTable.fill(-1);
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: "SchroederReverb",
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(audioContext);
742
- this.reverbEffect = this.createReverbEffect(audioContext);
743
- this.chorusEffect = this.createChorusEffect(audioContext);
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 ** 32) + (instrument << 16) + sampleID;
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(audioContext) {
1215
+ createChannels() {
868
1216
  const settings = this.constructor.channelSettings;
869
- return Array.from({ length: this.numChannels }, () => new Channel(this.createChannelAudioNodes(audioContext), settings));
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, audioBuffer) {
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
- bufferSource.loop = channel.isDrum
1282
+ const isDrumLoop = channel.isDrum
932
1283
  ? this.isLoopDrum(channel, noteNumber)
933
- : (voiceParams.sampleModes % 2 !== 0);
1284
+ : voiceParams.sampleModes % 2 !== 0;
1285
+ const isLoop = isRendered ? renderedOrRaw.isLoop : isDrumLoop;
1286
+ bufferSource.loop = isLoop;
934
1287
  if (bufferSource.loop) {
935
- bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
936
- bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
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.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
955
- break;
956
- case "noteOff": {
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 "noteAftertouch":
961
- this.setPolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
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(scheduleTime);
1623
+ this.GM1SystemOn();
1171
1624
  break;
1172
1625
  case 2: // GM System Off
1173
1626
  break;
1174
1627
  case 3:
1175
- this.GM2SystemOn(scheduleTime);
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
- // https://hajim.rochester.edu/ece/sites/zduan/teaching/ece472/reading/Schroeder_1962.pdf
1418
- // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
1419
- createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays) {
1420
- const input = new GainNode(audioContext);
1421
- const mergerGain = new GainNode(audioContext);
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(audioContext) {
1435
- const { algorithm, time: rt60, feedback } = this.reverb;
2022
+ createReverbEffect(algorithm) {
2023
+ const { audioContext, reverb } = this;
2024
+ const { time: rt60, feedback } = reverb;
1436
2025
  switch (algorithm) {
1437
- case "ConvolutionReverb": {
1438
- const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
1439
- return this.createConvolutionReverb(audioContext, impulse);
2026
+ case "Convolution": {
2027
+ const impulse = createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
2028
+ return createConvolutionReverb(audioContext, impulse);
1440
2029
  }
1441
- case "SchroederReverb": {
2030
+ case "Schroeder": {
1442
2031
  const combFeedbacks = this.generateDistributedArray(feedback, 4);
1443
- const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
2032
+ const combDelays = combFeedbacks.map((fb) => this.calcDelay(rt60, fb));
1444
2033
  const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
1445
- const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
1446
- return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
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(audioContext) {
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
- .cancelScheduledValues(scheduleTime)
1631
- .setValueAtTime(depth, scheduleTime);
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
- note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
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.modLfoToVolume.connect(note.volumeEnvelopeNode.gain);
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(this.audioContext, {
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(this.audioContext);
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 getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
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 cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
1786
- if (cachedAudioBuffer)
1787
- return cachedAudioBuffer;
1788
- const audioBuffer = await this.createAudioBuffer(voiceParams);
1789
- this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
1790
- return audioBuffer;
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(audioBufferId);
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(audioBufferId);
2654
+ this.voiceCache.delete(cacheKey);
1798
2655
  }
1799
2656
  return cache.audioBuffer;
1800
2657
  }
1801
2658
  else {
1802
- const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1803
- const audioBuffer = await this.createAudioBuffer(voiceParams);
1804
- const cache = { audioBuffer, maxCount, counter: 1 };
1805
- this.voiceCache.set(audioBufferId, cache);
1806
- return audioBuffer;
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, noteNumber, velocity, voiceParams, realtime);
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 filterResonance = this.getRelativeKeyBasedValue(channel, noteNumber, 71);
1823
- note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
1824
- type: "lowpass",
1825
- Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
1826
- });
1827
- const prevNote = channel.scheduledNotes.at(-1);
1828
- if (prevNote && prevNote.noteNumber !== noteNumber) {
1829
- note.portamentoNoteNumber = prevNote.noteNumber;
1830
- }
1831
- this.setVolumeNode(channel, note, now);
1832
- if (!channel.isDrum && this.isPortamento(channel, note)) {
1833
- this.setPortamentoVolumeEnvelope(channel, note, now);
1834
- this.setPortamentoFilterEnvelope(channel, note, now);
1835
- this.setPortamentoPitchEnvelope(channel, note, now);
1836
- this.setPortamentoDetune(channel, note, now);
1837
- }
1838
- else {
1839
- this.setVolumeEnvelope(channel, note, now);
1840
- this.setFilterEnvelope(channel, note, now);
1841
- this.setPitchEnvelope(note, now);
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
- const offset = voiceParams.start / audioBuffer.sampleRate;
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 { noteNumber, volumeNode } = note;
1904
- if (channel.isDrum) {
1905
- const { keyBasedGainLs, keyBasedGainRs } = channel;
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
- volumeNode.connect(channel.gainL);
1918
- volumeNode.connect(channel.gainR);
1919
- }
1920
- if (0.5 <= channel.state.sustainPedal) {
1921
- channel.sustainNotes.push(note);
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
- await this.startNote(channelNumber, noteNumber, velocity, startTime);
2902
+ const note = this.createNote(channelNumber, noteNumber, velocity, startTime);
2903
+ return await this.setupNote(channelNumber, note, startTime);
1937
2904
  }
1938
- async startNote(channelNumber, noteNumber, velocity, startTime) {
1939
- const channel = this.channels[channelNumber];
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
- const scheduledNotes = channel.scheduledNotes;
1945
- note.index = scheduledNotes.length;
1946
- scheduledNotes.push(note);
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.disconnect();
1972
- note.volumeEnvelopeNode.disconnect();
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.filterEnvelopeNode.frequency
1996
- .cancelScheduledValues(endTime)
1997
- .setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
1998
- note.volumeEnvelopeNode.gain
1999
- .cancelScheduledValues(endTime)
2000
- .setTargetAtTime(0, endTime, volDuration * releaseCurve);
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
- this.scheduleTask(() => {
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
- }, volRelease);
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.modulationDepth) {
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: (_channel, note, _scheduleTime) => {
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.modulationDepth) {
2552
- note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
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
- channel.state.sustainPedal = value / 127;
3760
+ const state = channel.state;
3761
+ const prevValue = state.sustainPedal;
3762
+ state.sustainPedal = value / 127;
2708
3763
  if (64 <= value) {
2709
- this.processScheduledNotes(channel, (note) => {
2710
- channel.sustainNotes.push(note);
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
- channel.state.sostenutoPedal = value / 127;
3792
+ const state = channel.state;
3793
+ const prevValue = state.sostenutoPedal;
3794
+ state.sostenutoPedal = value / 127;
2736
3795
  if (64 <= value) {
2737
- const sostenutoNotes = [];
2738
- this.processActiveNotes(channel, scheduleTime, (note) => {
2739
- sostenutoNotes.push(note);
2740
- });
2741
- channel.sostenutoNotes = sostenutoNotes;
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
- this.resetTable(channel);
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, scheduelTime);
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
- .cancelScheduledValues(scheduleTime)
3304
- .setValueAtTime(value * value, scheduleTime);
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.createReverbEffect(this.audioContext);
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.createReverbEffect(this.audioContext);
4453
+ this.reverbEffect = this.setReverbEffect(this.reverb.algorithm);
3392
4454
  }
3393
4455
  getReverbTime(value) {
3394
4456
  return Math.exp((value - 40) * 0.025);