@marmooo/midy 0.5.0 → 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,7 @@ 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");
7
8
  // Cache mode
8
9
  // - "none" for full real-time control (dynamic CC, LFO, pitch)
9
10
  // - "ads" for real-time playback with higher cache hit rate
@@ -205,7 +206,13 @@ class Note {
205
206
  }
206
207
  }
207
208
  class Channel {
208
- 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
+ });
209
216
  Object.defineProperty(this, "isDrum", {
210
217
  enumerable: true,
211
218
  configurable: true,
@@ -356,6 +363,7 @@ class Channel {
356
363
  writable: true,
357
364
  value: null
358
365
  });
366
+ this.channelNumber = channelNumber;
359
367
  Object.assign(this, audioNodes);
360
368
  Object.assign(this, settings);
361
369
  this.state = new ControllerState();
@@ -621,7 +629,7 @@ class Midy extends EventTarget {
621
629
  configurable: true,
622
630
  writable: true,
623
631
  value: {
624
- algorithm: "SchroederReverb",
632
+ algorithm: "Schroeder",
625
633
  time: this.getReverbTime(64),
626
634
  feedback: 0.8,
627
635
  }
@@ -869,6 +877,7 @@ class Midy extends EventTarget {
869
877
  writable: true,
870
878
  value: null
871
879
  });
880
+ // MPE
872
881
  Object.defineProperty(this, "mpeEnabled", {
873
882
  enumerable: true,
874
883
  configurable: true,
@@ -909,9 +918,9 @@ class Midy extends EventTarget {
909
918
  this.controlChangeHandlers = this.createControlChangeHandlers();
910
919
  this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
911
920
  this.effectHandlers = this.createEffectHandlers();
912
- this.channels = this.createChannels(audioContext);
913
- this.reverbEffect = this.createReverbEffect(audioContext);
914
- this.chorusEffect = this.createChorusEffect(audioContext);
921
+ this.channels = this.createChannels();
922
+ this.reverbEffect = this.createReverbEffect(this.reverb.algorithm);
923
+ this.chorusEffect = this.createChorusEffect();
915
924
  this.chorusEffect.output.connect(this.masterVolume);
916
925
  this.reverbEffect.output.connect(this.masterVolume);
917
926
  this.masterVolume.connect(audioContext.destination);
@@ -1190,6 +1199,8 @@ class Midy extends EventTarget {
1190
1199
  return;
1191
1200
  const soundFont = this.soundFonts[soundFontIndex];
1192
1201
  const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1202
+ if (!voice)
1203
+ return;
1193
1204
  const { instrument, sampleID } = voice.generators;
1194
1205
  return soundFontIndex * (2 ** 31) + instrument * (2 ** 24) +
1195
1206
  (sampleID << 8);
@@ -1204,9 +1215,10 @@ class Midy extends EventTarget {
1204
1215
  merger.connect(this.masterVolume);
1205
1216
  return { gainL, gainR, merger };
1206
1217
  }
1207
- createChannels(audioContext) {
1218
+ createChannels() {
1208
1219
  const settings = this.constructor.channelSettings;
1209
- 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));
1210
1222
  }
1211
1223
  decodeOggVorbis(sample) {
1212
1224
  const task = decoderQueue.then(async () => {
@@ -1742,6 +1754,7 @@ class Midy extends EventTarget {
1742
1754
  const soundFont = this.soundFonts[soundFontIndex];
1743
1755
  const pressure = renderNoteAftertouch[ch * 128 + noteNumber];
1744
1756
  const fakeChannel = {
1757
+ channelNumber: ch,
1745
1758
  state: { array: renderControllerStates[ch].slice() },
1746
1759
  programNumber,
1747
1760
  isDrum,
@@ -1993,62 +2006,6 @@ class Midy extends EventTarget {
1993
2006
  }
1994
2007
  }
1995
2008
  }
1996
- createConvolutionReverbImpulse(audioContext, decay, preDecay) {
1997
- const sampleRate = audioContext.sampleRate;
1998
- const length = sampleRate * decay;
1999
- const impulse = new AudioBuffer({
2000
- numberOfChannels: 2,
2001
- length,
2002
- sampleRate,
2003
- });
2004
- const preDecayLength = Math.min(sampleRate * preDecay, length);
2005
- for (let channel = 0; channel < impulse.numberOfChannels; channel++) {
2006
- const channelData = impulse.getChannelData(channel);
2007
- for (let i = 0; i < preDecayLength; i++) {
2008
- channelData[i] = Math.random() * 2 - 1;
2009
- }
2010
- const attenuationFactor = 1 / (sampleRate * decay);
2011
- for (let i = preDecayLength; i < length; i++) {
2012
- const attenuation = Math.exp(-(i - preDecayLength) * attenuationFactor);
2013
- channelData[i] = (Math.random() * 2 - 1) * attenuation;
2014
- }
2015
- }
2016
- return impulse;
2017
- }
2018
- createConvolutionReverb(audioContext, impulse) {
2019
- const convolverNode = new ConvolverNode(audioContext, {
2020
- buffer: impulse,
2021
- });
2022
- return {
2023
- input: convolverNode,
2024
- output: convolverNode,
2025
- convolverNode,
2026
- };
2027
- }
2028
- createCombFilter(audioContext, input, delay, feedback) {
2029
- const delayNode = new DelayNode(audioContext, {
2030
- maxDelayTime: delay,
2031
- delayTime: delay,
2032
- });
2033
- const feedbackGain = new GainNode(audioContext, { gain: feedback });
2034
- input.connect(delayNode);
2035
- delayNode.connect(feedbackGain);
2036
- feedbackGain.connect(delayNode);
2037
- return delayNode;
2038
- }
2039
- createAllpassFilter(audioContext, input, delay, feedback) {
2040
- const delayNode = new DelayNode(audioContext, {
2041
- maxDelayTime: delay,
2042
- delayTime: delay,
2043
- });
2044
- const feedbackGain = new GainNode(audioContext, { gain: feedback });
2045
- const passGain = new GainNode(audioContext, { gain: 1 - feedback });
2046
- input.connect(delayNode);
2047
- delayNode.connect(feedbackGain);
2048
- feedbackGain.connect(delayNode);
2049
- delayNode.connect(passGain);
2050
- return passGain;
2051
- }
2052
2009
  generateDistributedArray(center, count, varianceRatio = 0.1, randomness = 0.05) {
2053
2010
  const variance = center * varianceRatio;
2054
2011
  const array = new Array(count);
@@ -2059,40 +2016,60 @@ class Midy extends EventTarget {
2059
2016
  }
2060
2017
  return array;
2061
2018
  }
2062
- // https://hajim.rochester.edu/ece/sites/zduan/teaching/ece472/reading/Schroeder_1962.pdf
2063
- // M.R.Schroeder, "Natural Sounding Artificial Reverberation", J.Audio Eng. Soc., vol.10, p.219, 1962
2064
- createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays) {
2065
- const input = new GainNode(audioContext);
2066
- const mergerGain = new GainNode(audioContext);
2067
- for (let i = 0; i < combDelays.length; i++) {
2068
- const comb = this.createCombFilter(audioContext, input, combDelays[i], combFeedbacks[i]);
2069
- comb.connect(mergerGain);
2070
- }
2071
- const allpasses = [];
2072
- for (let i = 0; i < allpassDelays.length; i++) {
2073
- const allpass = this.createAllpassFilter(audioContext, (i === 0) ? mergerGain : allpasses.at(-1), allpassDelays[i], allpassFeedbacks[i]);
2074
- allpasses.push(allpass);
2075
- }
2076
- const output = allpasses.at(-1);
2077
- 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;
2078
2024
  }
2079
- createReverbEffect(audioContext) {
2080
- const { algorithm, time: rt60, feedback } = this.reverb;
2025
+ createReverbEffect(algorithm) {
2026
+ const { audioContext, reverb } = this;
2027
+ const { time: rt60, feedback } = reverb;
2081
2028
  switch (algorithm) {
2082
- case "ConvolutionReverb": {
2083
- const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
2084
- 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);
2085
2032
  }
2086
- case "SchroederReverb": {
2033
+ case "Schroeder": {
2087
2034
  const combFeedbacks = this.generateDistributedArray(feedback, 4);
2088
- const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
2035
+ const combDelays = combFeedbacks.map((fb) => this.calcDelay(rt60, fb));
2089
2036
  const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
2090
- const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
2091
- 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
+ });
2092
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 };
2064
+ }
2065
+ case "VelvetNoise":
2066
+ return (0, reverb_js_1.createVelvetNoiseReverb)(audioContext, rt60);
2067
+ default:
2068
+ throw new Error(`Unknown reverb algorithm: ${algorithm}`);
2093
2069
  }
2094
2070
  }
2095
- createChorusEffect(audioContext) {
2071
+ createChorusEffect() {
2072
+ const audioContext = this.audioContext;
2096
2073
  const input = new GainNode(audioContext);
2097
2074
  const output = new GainNode(audioContext);
2098
2075
  const sendGain = new GainNode(audioContext);
@@ -2275,9 +2252,10 @@ class Midy extends EventTarget {
2275
2252
  }
2276
2253
  setVolumeNode(channel, note, scheduleTime) {
2277
2254
  const depth = 1 + this.getNoteAmplitudeControl(channel, note);
2255
+ const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
2278
2256
  note.volumeNode.gain
2279
- .cancelScheduledValues(scheduleTime)
2280
- .setValueAtTime(depth, scheduleTime);
2257
+ .cancelAndHoldAtTime(scheduleTime)
2258
+ .setTargetAtTime(depth, scheduleTime, timeConstant);
2281
2259
  }
2282
2260
  setPortamentoDetune(channel, note, scheduleTime) {
2283
2261
  if (channel.portamentoControl) {
@@ -2417,15 +2395,16 @@ class Midy extends EventTarget {
2417
2395
  note.modLfoToVolume.connect(volumeTarget.gain);
2418
2396
  }
2419
2397
  startVibrato(channel, note, scheduleTime) {
2398
+ const audioContext = this.audioContext;
2420
2399
  const { voiceParams, noteNumber } = note;
2421
2400
  const vibratoRate = this.getRelativeKeyBasedValue(channel, noteNumber, 76) *
2422
2401
  2;
2423
2402
  const vibratoDelay = this.getRelativeKeyBasedValue(channel, noteNumber, 78) * 2;
2424
- note.vibLfo = new OscillatorNode(this.audioContext, {
2403
+ note.vibLfo = new OscillatorNode(audioContext, {
2425
2404
  frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
2426
2405
  });
2427
2406
  note.vibLfo.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
2428
- note.vibLfoToPitch = new GainNode(this.audioContext);
2407
+ note.vibLfoToPitch = new GainNode(audioContext);
2429
2408
  this.setVibLfoToPitch(channel, note, scheduleTime);
2430
2409
  note.vibLfo.connect(note.vibLfoToPitch);
2431
2410
  note.vibLfoToPitch.connect(note.bufferSource.detune);
@@ -2436,25 +2415,29 @@ class Midy extends EventTarget {
2436
2415
  const volHold = volAttack + voiceParams.volHold;
2437
2416
  const decayDuration = voiceParams.volDecay;
2438
2417
  const adsDuration = volHold + decayDuration * decayCurve * 5;
2439
- const loopStartTime = voiceParams.loopStart / voiceParams.sampleRate;
2440
- const loopDuration = isLoop
2418
+ const sampleLoopStart = voiceParams.loopStart / voiceParams.sampleRate;
2419
+ const sampleLoopDuration = isLoop
2441
2420
  ? (voiceParams.loopEnd - voiceParams.loopStart) / voiceParams.sampleRate
2442
2421
  : 0;
2443
- const loopCount = isLoop && adsDuration > loopStartTime
2444
- ? Math.ceil((adsDuration - loopStartTime) / loopDuration)
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)
2445
2427
  : 0;
2446
- const alignedLoopStart = loopStartTime + loopCount * loopDuration;
2428
+ const alignedLoopStart = outputLoopStart + loopCount * outputLoopDuration;
2447
2429
  const renderDuration = isLoop
2448
- ? alignedLoopStart + loopDuration
2449
- : audioBuffer.duration;
2450
- const offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, Math.ceil(renderDuration * this.audioContext.sampleRate), this.audioContext.sampleRate);
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);
2451
2434
  const bufferSource = new AudioBufferSourceNode(offlineContext);
2452
2435
  bufferSource.buffer = audioBuffer;
2453
- bufferSource.playbackRate.value = voiceParams.playbackRate;
2436
+ bufferSource.playbackRate.value = playbackRate;
2454
2437
  bufferSource.loop = isLoop;
2455
2438
  if (isLoop) {
2456
- bufferSource.loopStart = loopStartTime;
2457
- bufferSource.loopEnd = loopStartTime + loopDuration;
2439
+ bufferSource.loopStart = sampleLoopStart;
2440
+ bufferSource.loopEnd = sampleLoopStart + sampleLoopDuration;
2458
2441
  }
2459
2442
  const initialFreq = this.clampCutoffFrequency(this.centToHz(voiceParams.initialFilterFc));
2460
2443
  const filterEnvelopeNode = new BiquadFilterNode(offlineContext, {
@@ -2486,7 +2469,7 @@ class Midy extends EventTarget {
2486
2469
  isLoop,
2487
2470
  adsDuration,
2488
2471
  loopStart: alignedLoopStart,
2489
- loopDuration,
2472
+ loopDuration: outputLoopDuration,
2490
2473
  });
2491
2474
  }
2492
2475
  async createAdsrRenderedBuffer(channel, note, voiceParams, audioBuffer, noteDuration) {
@@ -2584,7 +2567,7 @@ class Midy extends EventTarget {
2584
2567
  }
2585
2568
  async createFullRenderedBuffer(channel, note, voiceParams, noteDuration, noteEvent = {}) {
2586
2569
  const { startTime: noteStartTime = 0, events: noteEvents = [] } = noteEvent;
2587
- const ch = note.channel ?? 0;
2570
+ const ch = channel.channelNumber;
2588
2571
  const releaseEndDuration = voiceParams.volRelease * releaseCurve * 5;
2589
2572
  const totalDuration = noteDuration + releaseEndDuration;
2590
2573
  const sampleRate = this.audioContext.sampleRate;
@@ -2639,7 +2622,7 @@ class Midy extends EventTarget {
2639
2622
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
2640
2623
  if (!realtime) {
2641
2624
  if (cacheMode === "note") {
2642
- return await this.getFullCachedBuffer(note, audioBufferId);
2625
+ return await this.getFullCachedBuffer(channel, note, audioBufferId);
2643
2626
  }
2644
2627
  else if (cacheMode === "adsr") {
2645
2628
  return await this.getAdsrCachedBuffer(channel, note, audioBufferId);
@@ -2730,7 +2713,7 @@ class Midy extends EventTarget {
2730
2713
  durationMap.set(cacheKey, renderPromise);
2731
2714
  return await renderPromise;
2732
2715
  }
2733
- async getFullCachedBuffer(note, audioBufferId) {
2716
+ async getFullCachedBuffer(channel, note, audioBufferId) {
2734
2717
  const voiceParams = note.voiceParams;
2735
2718
  const timelineIndex = note.timelineIndex;
2736
2719
  const noteEvent = this.noteOnEvents.get(timelineIndex);
@@ -2755,8 +2738,7 @@ class Midy extends EventTarget {
2755
2738
  }
2756
2739
  const renderPromise = (async () => {
2757
2740
  try {
2758
- const rawBuffer = await this.createAudioBuffer(voiceParams);
2759
- const rendered = await this.createFullRenderedBuffer(note, voiceParams, rawBuffer, noteDuration, noteEvent);
2741
+ const rendered = await this.createFullRenderedBuffer(channel, note, voiceParams, noteDuration, noteEvent);
2760
2742
  durationMap.set(cacheKey, rendered);
2761
2743
  return rendered;
2762
2744
  }
@@ -2783,7 +2765,6 @@ class Midy extends EventTarget {
2783
2765
  note.renderedBuffer = isRendered ? audioBuffer : null;
2784
2766
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
2785
2767
  note.volumeNode = new GainNode(audioContext);
2786
- note.volumeNode.gain.setValueAtTime(1, now);
2787
2768
  const cacheMode = this.cacheMode;
2788
2769
  const isFullCached = isRendered && audioBuffer.isFull === true;
2789
2770
  if (cacheMode === "none") {
@@ -2929,9 +2910,6 @@ class Midy extends EventTarget {
2929
2910
  startTime = this.audioContext.currentTime;
2930
2911
  const note = new Note(noteNumber, velocity, startTime);
2931
2912
  note.channel = channelNumber;
2932
- const channel = this.channels[channelNumber];
2933
- note.index = channel.scheduledNotes.length;
2934
- channel.scheduledNotes.push(note);
2935
2913
  return note;
2936
2914
  }
2937
2915
  async setupNote(channelNumber, note, startTime) {
@@ -2954,6 +2932,8 @@ class Midy extends EventTarget {
2954
2932
  note.voice = soundFont.getVoice(bank, programNumber, note.noteNumber, note.velocity);
2955
2933
  if (!note.voice)
2956
2934
  return;
2935
+ note.index = channel.scheduledNotes.length;
2936
+ channel.scheduledNotes.push(note);
2957
2937
  await this.setNoteAudioNode(channel, note, realtime);
2958
2938
  this.setNoteRouting(channelNumber, note, startTime);
2959
2939
  note.resolveReady();
@@ -3013,18 +2993,8 @@ class Midy extends EventTarget {
3013
2993
  const volRelease = endTime + volDuration;
3014
2994
  note.volumeNode.gain
3015
2995
  .cancelScheduledValues(endTime)
3016
- .setValueAtTime(1, endTime)
3017
2996
  .setTargetAtTime(0, endTime, volDuration * releaseCurve);
3018
- return new Promise((resolve) => {
3019
- this.scheduleTask(() => {
3020
- note.bufferSource.loop = false;
3021
- note.bufferSource.stop(volRelease);
3022
- this.disconnectNote(note);
3023
- channel.scheduledNotes[note.index] = undefined;
3024
- this.releaseFullCache(note);
3025
- resolve();
3026
- }, volRelease);
3027
- });
2997
+ note.bufferSource.stop(volRelease);
3028
2998
  }
3029
2999
  else {
3030
3000
  const now = this.audioContext.currentTime;
@@ -3034,15 +3004,16 @@ class Midy extends EventTarget {
3034
3004
  this.releaseFullCache(note);
3035
3005
  return Promise.resolve();
3036
3006
  }
3037
- return new Promise((resolve) => {
3038
- this.scheduleTask(() => {
3039
- this.disconnectNote(note);
3040
- channel.scheduledNotes[note.index] = undefined;
3041
- this.releaseFullCache(note);
3042
- resolve();
3043
- }, naturalEndTime);
3044
- });
3007
+ note.bufferSource.stop(naturalEndTime);
3045
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
+ });
3046
3017
  }
3047
3018
  const releaseTime = this.getRelativeKeyBasedValue(channel, note.noteNumber, 72) * 2;
3048
3019
  const volDuration = note.voiceParams.volRelease * releaseTime;
@@ -3064,45 +3035,33 @@ class Midy extends EventTarget {
3064
3035
  const noteOffTime = note.startTime + (rb.noteDuration ?? 0);
3065
3036
  const isEarlyCut = endTime < noteOffTime;
3066
3037
  if (isEarlyCut) {
3067
- const volRelease = endTime + volDuration;
3068
3038
  note.volumeNode.gain
3069
3039
  .cancelScheduledValues(endTime)
3070
- .setValueAtTime(1, endTime)
3071
3040
  .setTargetAtTime(0, endTime, volDuration * releaseCurve);
3072
- return new Promise((resolve) => {
3073
- this.scheduleTask(() => {
3074
- note.bufferSource.stop(volRelease);
3075
- this.disconnectNote(note);
3076
- channel.scheduledNotes[note.index] = undefined;
3077
- resolve();
3078
- }, volRelease);
3079
- });
3041
+ note.bufferSource.stop(volRelease);
3080
3042
  }
3081
3043
  else {
3082
- return new Promise((resolve) => {
3083
- this.scheduleTask(() => {
3084
- note.bufferSource.stop();
3085
- this.disconnectNote(note);
3086
- channel.scheduledNotes[note.index] = undefined;
3087
- resolve();
3088
- }, naturalEndTime);
3089
- });
3044
+ note.bufferSource.stop(naturalEndTime);
3090
3045
  }
3046
+ return new Promise((resolve) => {
3047
+ note.bufferSource.onended = () => {
3048
+ this.disconnectNote(note);
3049
+ channel.scheduledNotes[note.index] = undefined;
3050
+ resolve();
3051
+ };
3052
+ });
3091
3053
  }
3092
3054
  note.volumeNode.gain
3093
3055
  .cancelScheduledValues(endTime)
3094
- .setValueAtTime(1, endTime)
3095
3056
  .setTargetAtTime(0, endTime, volDuration * releaseCurve);
3096
3057
  }
3058
+ note.bufferSource.stop(volRelease);
3097
3059
  return new Promise((resolve) => {
3098
- this.scheduleTask(() => {
3099
- const bufferSource = note.bufferSource;
3100
- bufferSource.loop = false;
3101
- bufferSource.stop(volRelease);
3060
+ note.bufferSource.onended = () => {
3102
3061
  this.disconnectNote(note);
3103
3062
  channel.scheduledNotes[note.index] = undefined;
3104
3063
  resolve();
3105
- }, volRelease);
3064
+ };
3106
3065
  });
3107
3066
  }
3108
3067
  noteOff(channelNumber, noteNumber, velocity, endTime, force) {
@@ -4404,9 +4363,10 @@ class Midy extends EventTarget {
4404
4363
  setMasterVolume(value, scheduleTime) {
4405
4364
  if (!(0 <= scheduleTime))
4406
4365
  scheduleTime = this.audioContext.currentTime;
4366
+ const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
4407
4367
  this.masterVolume.gain
4408
- .cancelScheduledValues(scheduleTime)
4409
- .setValueAtTime(value * value, scheduleTime);
4368
+ .cancelAndHoldAtTime(scheduleTime)
4369
+ .setTargetAtTime(value * value, scheduleTime, timeConstant);
4410
4370
  }
4411
4371
  handleMasterFineTuningSysEx(data, scheduleTime) {
4412
4372
  const value = (data[5] * 128 + data[4]) / 16383;
@@ -4471,7 +4431,7 @@ class Midy extends EventTarget {
4471
4431
  setReverbType(type) {
4472
4432
  this.reverb.time = this.getReverbTimeFromType(type);
4473
4433
  this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
4474
- this.reverbEffect = this.createReverbEffect(this.audioContext);
4434
+ this.reverbEffect = this.setReverbEffect(this.reverb.algorithm);
4475
4435
  }
4476
4436
  getReverbTimeFromType(type) {
4477
4437
  switch (type) {
@@ -4493,7 +4453,7 @@ class Midy extends EventTarget {
4493
4453
  }
4494
4454
  setReverbTime(value) {
4495
4455
  this.reverb.time = this.getReverbTime(value);
4496
- this.reverbEffect = this.createReverbEffect(this.audioContext);
4456
+ this.reverbEffect = this.setReverbEffect(this.reverb.algorithm);
4497
4457
  }
4498
4458
  getReverbTime(value) {
4499
4459
  return Math.exp((value - 40) * 0.025);
@@ -0,0 +1,58 @@
1
+ export function createConvolutionReverbImpulse(audioContext: any, decay: any, preDecay: any): any;
2
+ export function createConvolutionReverb(audioContext: any, impulse: any): {
3
+ input: any;
4
+ output: any;
5
+ };
6
+ export function createCombFilter(audioContext: any, input: any, delay: any, feedback: any): any;
7
+ export function createAllpassFilter(audioContext: any, input: any, delay: any, feedback: any): any;
8
+ export function createLPFCombFilter(audioContext: any, input: any, delayTime: any, feedback: any, damping: any): any;
9
+ export function createSchroederReverb(audioContext: any, combFeedbacks: any, combDelays: any, allpassFeedbacks: any, allpassDelays: any): {
10
+ input: any;
11
+ output: any;
12
+ };
13
+ export function createMoorerReverb(audioContext: any, earlyTaps: any, earlyGains: any, combDelays: any, combFeedbacks: any, damping: any, allpassDelays: any, allpassFeedbacks: any): {
14
+ input: any;
15
+ output: any;
16
+ };
17
+ export function createMoorerReverbDefault(audioContext: any, { rt60, damping, }?: {
18
+ rt60?: number | undefined;
19
+ damping?: number | undefined;
20
+ }): {
21
+ input: any;
22
+ output: any;
23
+ };
24
+ export function createFDN(audioContext: any, delayTimes: any, gains: any, damping?: number, modulation?: number): {
25
+ input: any;
26
+ output: any;
27
+ };
28
+ export function createFDNDefault(audioContext: any, { rt60, damping, modulation }?: {
29
+ rt60?: number | undefined;
30
+ damping?: number | undefined;
31
+ modulation?: number | undefined;
32
+ }): {
33
+ input: any;
34
+ output: any;
35
+ };
36
+ export function createDattorroReverb(audioContext: any, { decay, damping, bandwidth, }?: {
37
+ decay?: number | undefined;
38
+ damping?: number | undefined;
39
+ bandwidth?: number | undefined;
40
+ }): {
41
+ input: any;
42
+ output: any;
43
+ };
44
+ export function createFreeverb(audioContext: any, { roomSize, damping }?: {
45
+ roomSize?: number | undefined;
46
+ damping?: number | undefined;
47
+ }): {
48
+ inputL: any;
49
+ inputR: any;
50
+ outputL: any;
51
+ outputR: any;
52
+ };
53
+ export function createVelvetNoiseImpulse(audioContext: any, decay: any, density?: number): any;
54
+ export function createVelvetNoiseReverb(audioContext: any, decay: any, density: any): {
55
+ input: any;
56
+ output: any;
57
+ };
58
+ //# sourceMappingURL=reverb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reverb.d.ts","sourceRoot":"","sources":["../src/reverb.js"],"names":[],"mappings":"AAWA,kGAiBC;AAED;;;EAGC;AAED,gGAUC;AAMD,mGAYC;AAKD,qHAuBC;AAOD;;;EA8BC;AAWD;;;EAmDC;AAGD;;;;;;EA0BC;AAcD;;;EA+EC;AAGD;;;;;;;EAWC;AAcD;;;;;;;EAoFC;AAoBD;;;;;;;;EA2CC;AAOD,+FAkBC;AAED;;;EAGC"}