@marmooo/midy 0.4.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/esm/midy-GM1.d.ts +86 -10
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +1190 -101
- package/esm/midy-GM2.d.ts +103 -10
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +1402 -162
- package/esm/midy-GMLite.d.ts +84 -9
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +1183 -98
- package/esm/midy.d.ts +77 -15
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +1416 -175
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +86 -10
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +1190 -101
- package/script/midy-GM2.d.ts +103 -10
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +1402 -162
- package/script/midy-GMLite.d.ts +84 -9
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +1183 -98
- package/script/midy.d.ts +77 -15
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +1416 -175
package/script/midy-GM2.js
CHANGED
|
@@ -4,6 +4,55 @@ exports.MidyGM2 = 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
|
+
// Cache mode
|
|
8
|
+
// - "none" for full real-time control (dynamic CC, LFO, pitch)
|
|
9
|
+
// - "ads" for real-time playback with higher cache hit rate
|
|
10
|
+
// - "adsr" for real-time playback with accurate release envelope
|
|
11
|
+
// - "note" for efficient playback when note behavior is fixed
|
|
12
|
+
// - "audio" for fully pre-rendered playback (lowest CPU)
|
|
13
|
+
//
|
|
14
|
+
// "none"
|
|
15
|
+
// No caching. Envelope processing is done in real time on every note.
|
|
16
|
+
// Uses Web Audio API nodes directly, so LFO and pitch envelope are
|
|
17
|
+
// fully supported. Higher CPU usage.
|
|
18
|
+
// "ads"
|
|
19
|
+
// Pre-renders the ADS (Attack-Decay-Sustain) phase into an
|
|
20
|
+
// OfflineAudioContext and caches the result. The sustain tail is
|
|
21
|
+
// aligned to the loop boundary as a fixed buffer. Release is
|
|
22
|
+
// handled by fading volumeNode gain to 0 at note-off.
|
|
23
|
+
// LFO effects (modLfoToPitch, modLfoToFilterFc, modLfoToVolume,
|
|
24
|
+
// vibLfoToPitch) are applied in real time after playback starts.
|
|
25
|
+
// "adsr"
|
|
26
|
+
// Pre-renders the full ADSR envelope (Attack-Decay-Sustain-Release)
|
|
27
|
+
// into an OfflineAudioContext. The cache key includes the note
|
|
28
|
+
// duration in ticks (tempo-independent) and the volRelease parameter,
|
|
29
|
+
// so notes with the same duration and release shape share a buffer.
|
|
30
|
+
// LFO effects are applied in real time after playback starts,
|
|
31
|
+
// same as "ads" mode. Higher cache hit rate than "note" mode
|
|
32
|
+
// because LFO variations do not produce separate cache entries.
|
|
33
|
+
// "note"
|
|
34
|
+
// Renders the full noteOn-to-noteOff duration per note in an
|
|
35
|
+
// OfflineAudioContext. All events during the note (volume,
|
|
36
|
+
// expression, pitch bend, LFO, CC#1) are baked into the buffer,
|
|
37
|
+
// so no real-time processing is needed during playback. Greatly
|
|
38
|
+
// reduces CPU load for songs with many simultaneous notes.
|
|
39
|
+
// MIDI file playback only — does not respond to real-time CC changes.
|
|
40
|
+
// "audio"
|
|
41
|
+
// Renders the entire MIDI file into a single AudioBuffer offline.
|
|
42
|
+
// Call render() to complete rendering before calling start().
|
|
43
|
+
// Playback simply streams an AudioBufferSourceNode, so CPU usage
|
|
44
|
+
// is near zero. Seek and tempo changes are handled in real time.
|
|
45
|
+
// A "rendering" event is dispatched when rendering starts, and a
|
|
46
|
+
// "rendered" event is dispatched when rendering completes.
|
|
47
|
+
/** @type {"none"|"ads"|"adsr"|"note"|"audio"} */
|
|
48
|
+
const DEFAULT_CACHE_MODE = "ads";
|
|
49
|
+
const _f64Buf = new ArrayBuffer(8);
|
|
50
|
+
const _f64Array = new Float64Array(_f64Buf);
|
|
51
|
+
const _u64Array = new BigUint64Array(_f64Buf);
|
|
52
|
+
function f64ToBigInt(value) {
|
|
53
|
+
_f64Array[0] = value;
|
|
54
|
+
return _u64Array[0];
|
|
55
|
+
}
|
|
7
56
|
let decoderPromise = null;
|
|
8
57
|
let decoderQueue = Promise.resolve();
|
|
9
58
|
function initDecoder() {
|
|
@@ -51,6 +100,24 @@ class Note {
|
|
|
51
100
|
writable: true,
|
|
52
101
|
value: void 0
|
|
53
102
|
});
|
|
103
|
+
Object.defineProperty(this, "timelineIndex", {
|
|
104
|
+
enumerable: true,
|
|
105
|
+
configurable: true,
|
|
106
|
+
writable: true,
|
|
107
|
+
value: null
|
|
108
|
+
});
|
|
109
|
+
Object.defineProperty(this, "renderedBuffer", {
|
|
110
|
+
enumerable: true,
|
|
111
|
+
configurable: true,
|
|
112
|
+
writable: true,
|
|
113
|
+
value: null
|
|
114
|
+
});
|
|
115
|
+
Object.defineProperty(this, "fullCacheVoiceId", {
|
|
116
|
+
enumerable: true,
|
|
117
|
+
configurable: true,
|
|
118
|
+
writable: true,
|
|
119
|
+
value: null
|
|
120
|
+
});
|
|
54
121
|
Object.defineProperty(this, "filterEnvelopeNode", {
|
|
55
122
|
enumerable: true,
|
|
56
123
|
configurable: true,
|
|
@@ -125,6 +192,166 @@ class Note {
|
|
|
125
192
|
});
|
|
126
193
|
}
|
|
127
194
|
}
|
|
195
|
+
class Channel {
|
|
196
|
+
constructor(audioNodes, settings) {
|
|
197
|
+
Object.defineProperty(this, "isDrum", {
|
|
198
|
+
enumerable: true,
|
|
199
|
+
configurable: true,
|
|
200
|
+
writable: true,
|
|
201
|
+
value: false
|
|
202
|
+
});
|
|
203
|
+
Object.defineProperty(this, "programNumber", {
|
|
204
|
+
enumerable: true,
|
|
205
|
+
configurable: true,
|
|
206
|
+
writable: true,
|
|
207
|
+
value: 0
|
|
208
|
+
});
|
|
209
|
+
Object.defineProperty(this, "scheduleIndex", {
|
|
210
|
+
enumerable: true,
|
|
211
|
+
configurable: true,
|
|
212
|
+
writable: true,
|
|
213
|
+
value: 0
|
|
214
|
+
});
|
|
215
|
+
Object.defineProperty(this, "detune", {
|
|
216
|
+
enumerable: true,
|
|
217
|
+
configurable: true,
|
|
218
|
+
writable: true,
|
|
219
|
+
value: 0
|
|
220
|
+
});
|
|
221
|
+
Object.defineProperty(this, "bankMSB", {
|
|
222
|
+
enumerable: true,
|
|
223
|
+
configurable: true,
|
|
224
|
+
writable: true,
|
|
225
|
+
value: 121
|
|
226
|
+
});
|
|
227
|
+
Object.defineProperty(this, "bankLSB", {
|
|
228
|
+
enumerable: true,
|
|
229
|
+
configurable: true,
|
|
230
|
+
writable: true,
|
|
231
|
+
value: 0
|
|
232
|
+
});
|
|
233
|
+
Object.defineProperty(this, "dataMSB", {
|
|
234
|
+
enumerable: true,
|
|
235
|
+
configurable: true,
|
|
236
|
+
writable: true,
|
|
237
|
+
value: 0
|
|
238
|
+
});
|
|
239
|
+
Object.defineProperty(this, "dataLSB", {
|
|
240
|
+
enumerable: true,
|
|
241
|
+
configurable: true,
|
|
242
|
+
writable: true,
|
|
243
|
+
value: 0
|
|
244
|
+
});
|
|
245
|
+
Object.defineProperty(this, "rpnMSB", {
|
|
246
|
+
enumerable: true,
|
|
247
|
+
configurable: true,
|
|
248
|
+
writable: true,
|
|
249
|
+
value: 127
|
|
250
|
+
});
|
|
251
|
+
Object.defineProperty(this, "rpnLSB", {
|
|
252
|
+
enumerable: true,
|
|
253
|
+
configurable: true,
|
|
254
|
+
writable: true,
|
|
255
|
+
value: 127
|
|
256
|
+
});
|
|
257
|
+
Object.defineProperty(this, "mono", {
|
|
258
|
+
enumerable: true,
|
|
259
|
+
configurable: true,
|
|
260
|
+
writable: true,
|
|
261
|
+
value: false
|
|
262
|
+
}); // CC#124, CC#125
|
|
263
|
+
Object.defineProperty(this, "modulationDepthRange", {
|
|
264
|
+
enumerable: true,
|
|
265
|
+
configurable: true,
|
|
266
|
+
writable: true,
|
|
267
|
+
value: 50
|
|
268
|
+
}); // cent
|
|
269
|
+
Object.defineProperty(this, "fineTuning", {
|
|
270
|
+
enumerable: true,
|
|
271
|
+
configurable: true,
|
|
272
|
+
writable: true,
|
|
273
|
+
value: 0
|
|
274
|
+
}); // cent
|
|
275
|
+
Object.defineProperty(this, "coarseTuning", {
|
|
276
|
+
enumerable: true,
|
|
277
|
+
configurable: true,
|
|
278
|
+
writable: true,
|
|
279
|
+
value: 0
|
|
280
|
+
}); // cent
|
|
281
|
+
Object.defineProperty(this, "scheduledNotes", {
|
|
282
|
+
enumerable: true,
|
|
283
|
+
configurable: true,
|
|
284
|
+
writable: true,
|
|
285
|
+
value: []
|
|
286
|
+
});
|
|
287
|
+
Object.defineProperty(this, "sustainNotes", {
|
|
288
|
+
enumerable: true,
|
|
289
|
+
configurable: true,
|
|
290
|
+
writable: true,
|
|
291
|
+
value: []
|
|
292
|
+
});
|
|
293
|
+
Object.defineProperty(this, "sostenutoNotes", {
|
|
294
|
+
enumerable: true,
|
|
295
|
+
configurable: true,
|
|
296
|
+
writable: true,
|
|
297
|
+
value: []
|
|
298
|
+
});
|
|
299
|
+
Object.defineProperty(this, "controlTable", {
|
|
300
|
+
enumerable: true,
|
|
301
|
+
configurable: true,
|
|
302
|
+
writable: true,
|
|
303
|
+
value: new Int8Array(defaultControlValues)
|
|
304
|
+
});
|
|
305
|
+
Object.defineProperty(this, "scaleOctaveTuningTable", {
|
|
306
|
+
enumerable: true,
|
|
307
|
+
configurable: true,
|
|
308
|
+
writable: true,
|
|
309
|
+
value: new Int8Array(12)
|
|
310
|
+
}); // [-64, 63] cent
|
|
311
|
+
Object.defineProperty(this, "channelPressureTable", {
|
|
312
|
+
enumerable: true,
|
|
313
|
+
configurable: true,
|
|
314
|
+
writable: true,
|
|
315
|
+
value: new Int8Array(defaultPressureValues)
|
|
316
|
+
});
|
|
317
|
+
Object.defineProperty(this, "keyBasedTable", {
|
|
318
|
+
enumerable: true,
|
|
319
|
+
configurable: true,
|
|
320
|
+
writable: true,
|
|
321
|
+
value: new Int8Array(128 * 128).fill(-1)
|
|
322
|
+
});
|
|
323
|
+
Object.defineProperty(this, "keyBasedGainLs", {
|
|
324
|
+
enumerable: true,
|
|
325
|
+
configurable: true,
|
|
326
|
+
writable: true,
|
|
327
|
+
value: new Array(128)
|
|
328
|
+
});
|
|
329
|
+
Object.defineProperty(this, "keyBasedGainRs", {
|
|
330
|
+
enumerable: true,
|
|
331
|
+
configurable: true,
|
|
332
|
+
writable: true,
|
|
333
|
+
value: new Array(128)
|
|
334
|
+
});
|
|
335
|
+
Object.defineProperty(this, "currentBufferSource", {
|
|
336
|
+
enumerable: true,
|
|
337
|
+
configurable: true,
|
|
338
|
+
writable: true,
|
|
339
|
+
value: null
|
|
340
|
+
});
|
|
341
|
+
Object.assign(this, audioNodes);
|
|
342
|
+
Object.assign(this, settings);
|
|
343
|
+
this.state = new ControllerState();
|
|
344
|
+
}
|
|
345
|
+
resetSettings(settings) {
|
|
346
|
+
Object.assign(this, settings);
|
|
347
|
+
}
|
|
348
|
+
resetTable() {
|
|
349
|
+
this.controlTable.set(defaultControlValues);
|
|
350
|
+
this.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
351
|
+
this.channelPressureTable.set(defaultPressureValues);
|
|
352
|
+
this.keyBasedTable.fill(-1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
128
355
|
const drumExclusiveClassesByKit = new Array(57);
|
|
129
356
|
const drumExclusiveClassCount = 10;
|
|
130
357
|
const standardSet = new Uint8Array(128);
|
|
@@ -258,13 +485,73 @@ const defaultControlValues = new Int8Array([
|
|
|
258
485
|
...[-1, -1, -1, -1, -1, -1],
|
|
259
486
|
...defaultPressureValues,
|
|
260
487
|
]);
|
|
488
|
+
class RenderedBuffer {
|
|
489
|
+
constructor(buffer, meta = {}) {
|
|
490
|
+
Object.defineProperty(this, "buffer", {
|
|
491
|
+
enumerable: true,
|
|
492
|
+
configurable: true,
|
|
493
|
+
writable: true,
|
|
494
|
+
value: void 0
|
|
495
|
+
});
|
|
496
|
+
Object.defineProperty(this, "isLoop", {
|
|
497
|
+
enumerable: true,
|
|
498
|
+
configurable: true,
|
|
499
|
+
writable: true,
|
|
500
|
+
value: void 0
|
|
501
|
+
});
|
|
502
|
+
Object.defineProperty(this, "isFull", {
|
|
503
|
+
enumerable: true,
|
|
504
|
+
configurable: true,
|
|
505
|
+
writable: true,
|
|
506
|
+
value: void 0
|
|
507
|
+
});
|
|
508
|
+
Object.defineProperty(this, "adsDuration", {
|
|
509
|
+
enumerable: true,
|
|
510
|
+
configurable: true,
|
|
511
|
+
writable: true,
|
|
512
|
+
value: void 0
|
|
513
|
+
});
|
|
514
|
+
Object.defineProperty(this, "loopStart", {
|
|
515
|
+
enumerable: true,
|
|
516
|
+
configurable: true,
|
|
517
|
+
writable: true,
|
|
518
|
+
value: void 0
|
|
519
|
+
});
|
|
520
|
+
Object.defineProperty(this, "loopDuration", {
|
|
521
|
+
enumerable: true,
|
|
522
|
+
configurable: true,
|
|
523
|
+
writable: true,
|
|
524
|
+
value: void 0
|
|
525
|
+
});
|
|
526
|
+
Object.defineProperty(this, "noteDuration", {
|
|
527
|
+
enumerable: true,
|
|
528
|
+
configurable: true,
|
|
529
|
+
writable: true,
|
|
530
|
+
value: void 0
|
|
531
|
+
});
|
|
532
|
+
Object.defineProperty(this, "releaseDuration", {
|
|
533
|
+
enumerable: true,
|
|
534
|
+
configurable: true,
|
|
535
|
+
writable: true,
|
|
536
|
+
value: void 0
|
|
537
|
+
});
|
|
538
|
+
this.buffer = buffer;
|
|
539
|
+
this.isLoop = meta.isLoop ?? false;
|
|
540
|
+
this.isFull = meta.isFull ?? false;
|
|
541
|
+
this.adsDuration = meta.adsDuration;
|
|
542
|
+
this.loopStart = meta.loopStart;
|
|
543
|
+
this.loopDuration = meta.loopDuration;
|
|
544
|
+
this.noteDuration = meta.noteDuration;
|
|
545
|
+
this.releaseDuration = meta.releaseDuration;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
261
548
|
function cbToRatio(cb) {
|
|
262
549
|
return Math.pow(10, cb / 200);
|
|
263
550
|
}
|
|
264
551
|
const decayCurve = 1 / (-Math.log(cbToRatio(-1000)));
|
|
265
552
|
const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
|
|
266
553
|
class MidyGM2 extends EventTarget {
|
|
267
|
-
constructor(audioContext) {
|
|
554
|
+
constructor(audioContext, options = {}) {
|
|
268
555
|
super();
|
|
269
556
|
// https://pmc.ncbi.nlm.nih.gov/articles/PMC4191557/
|
|
270
557
|
// https://pubmed.ncbi.nlm.nih.gov/12488797/
|
|
@@ -446,9 +733,7 @@ class MidyGM2 extends EventTarget {
|
|
|
446
733
|
enumerable: true,
|
|
447
734
|
configurable: true,
|
|
448
735
|
writable: true,
|
|
449
|
-
value: new Set([
|
|
450
|
-
"noteOff",
|
|
451
|
-
])
|
|
736
|
+
value: new Set(["noteOff"])
|
|
452
737
|
});
|
|
453
738
|
Object.defineProperty(this, "tempo", {
|
|
454
739
|
enumerable: true,
|
|
@@ -498,7 +783,53 @@ class MidyGM2 extends EventTarget {
|
|
|
498
783
|
writable: true,
|
|
499
784
|
value: new Array(this.numChannels * drumExclusiveClassCount)
|
|
500
785
|
});
|
|
786
|
+
// "adsr" mode
|
|
787
|
+
Object.defineProperty(this, "adsrVoiceCache", {
|
|
788
|
+
enumerable: true,
|
|
789
|
+
configurable: true,
|
|
790
|
+
writable: true,
|
|
791
|
+
value: new Map()
|
|
792
|
+
});
|
|
793
|
+
// "note" mode
|
|
794
|
+
Object.defineProperty(this, "noteOnDurations", {
|
|
795
|
+
enumerable: true,
|
|
796
|
+
configurable: true,
|
|
797
|
+
writable: true,
|
|
798
|
+
value: new Map()
|
|
799
|
+
});
|
|
800
|
+
Object.defineProperty(this, "noteOnEvents", {
|
|
801
|
+
enumerable: true,
|
|
802
|
+
configurable: true,
|
|
803
|
+
writable: true,
|
|
804
|
+
value: new Map()
|
|
805
|
+
});
|
|
806
|
+
Object.defineProperty(this, "fullVoiceCache", {
|
|
807
|
+
enumerable: true,
|
|
808
|
+
configurable: true,
|
|
809
|
+
writable: true,
|
|
810
|
+
value: new Map()
|
|
811
|
+
});
|
|
812
|
+
// "audio" mode
|
|
813
|
+
Object.defineProperty(this, "renderedAudioBuffer", {
|
|
814
|
+
enumerable: true,
|
|
815
|
+
configurable: true,
|
|
816
|
+
writable: true,
|
|
817
|
+
value: null
|
|
818
|
+
});
|
|
819
|
+
Object.defineProperty(this, "isRendering", {
|
|
820
|
+
enumerable: true,
|
|
821
|
+
configurable: true,
|
|
822
|
+
writable: true,
|
|
823
|
+
value: false
|
|
824
|
+
});
|
|
825
|
+
Object.defineProperty(this, "audioModeBufferSource", {
|
|
826
|
+
enumerable: true,
|
|
827
|
+
configurable: true,
|
|
828
|
+
writable: true,
|
|
829
|
+
value: null
|
|
830
|
+
});
|
|
501
831
|
this.audioContext = audioContext;
|
|
832
|
+
this.cacheMode = options.cacheMode ?? DEFAULT_CACHE_MODE;
|
|
502
833
|
this.masterVolume = new GainNode(audioContext);
|
|
503
834
|
this.scheduler = new GainNode(audioContext, { gain: 0 });
|
|
504
835
|
this.schedulerBuffer = new AudioBuffer({
|
|
@@ -574,9 +905,177 @@ class MidyGM2 extends EventTarget {
|
|
|
574
905
|
this.instruments = midiData.instruments;
|
|
575
906
|
this.timeline = midiData.timeline;
|
|
576
907
|
this.totalTime = this.calcTotalTime();
|
|
908
|
+
if (this.cacheMode === "audio") {
|
|
909
|
+
await this.render();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
buildNoteOnDurations() {
|
|
913
|
+
const { timeline, totalTime, noteOnDurations, noteOnEvents, numChannels } = this;
|
|
914
|
+
noteOnDurations.clear();
|
|
915
|
+
noteOnEvents.clear();
|
|
916
|
+
const inverseTempo = 1 / this.tempo;
|
|
917
|
+
const sustainPedal = new Uint8Array(numChannels);
|
|
918
|
+
const sostenutoPedal = new Uint8Array(numChannels);
|
|
919
|
+
const sostenutoKeys = new Array(numChannels).fill(null).map(() => new Set());
|
|
920
|
+
const activeNotes = new Map();
|
|
921
|
+
const pendingOff = new Map();
|
|
922
|
+
const finalizeEntry = (entry, endTime, endTicks) => {
|
|
923
|
+
const duration = Math.max(0, endTime - entry.startTime);
|
|
924
|
+
const durationTicks = (endTicks == null || endTicks === Infinity)
|
|
925
|
+
? Infinity
|
|
926
|
+
: Math.max(0, endTicks - entry.startTicks);
|
|
927
|
+
noteOnDurations.set(entry.idx, duration);
|
|
928
|
+
noteOnEvents.set(entry.idx, {
|
|
929
|
+
duration,
|
|
930
|
+
durationTicks,
|
|
931
|
+
startTime: entry.startTime,
|
|
932
|
+
events: entry.events,
|
|
933
|
+
});
|
|
934
|
+
};
|
|
935
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
936
|
+
const event = timeline[i];
|
|
937
|
+
const t = event.startTime * inverseTempo;
|
|
938
|
+
switch (event.type) {
|
|
939
|
+
case "noteOn": {
|
|
940
|
+
const key = event.noteNumber * numChannels + event.channel;
|
|
941
|
+
if (!activeNotes.has(key))
|
|
942
|
+
activeNotes.set(key, []);
|
|
943
|
+
activeNotes.get(key).push({
|
|
944
|
+
idx: i,
|
|
945
|
+
startTime: t,
|
|
946
|
+
startTicks: event.ticks,
|
|
947
|
+
events: [],
|
|
948
|
+
});
|
|
949
|
+
const pendingStack = pendingOff.get(key);
|
|
950
|
+
if (pendingStack && pendingStack.length > 0)
|
|
951
|
+
pendingStack.shift();
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
case "noteOff": {
|
|
955
|
+
const ch = event.channel;
|
|
956
|
+
const key = event.noteNumber * numChannels + ch;
|
|
957
|
+
const isSostenuto = sostenutoKeys[ch].has(key);
|
|
958
|
+
if (sustainPedal[ch] || isSostenuto) {
|
|
959
|
+
if (!pendingOff.has(key))
|
|
960
|
+
pendingOff.set(key, []);
|
|
961
|
+
pendingOff.get(key).push({ t, ticks: event.ticks });
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
const stack = activeNotes.get(key);
|
|
965
|
+
if (stack && stack.length > 0) {
|
|
966
|
+
finalizeEntry(stack.shift(), t, event.ticks);
|
|
967
|
+
if (stack.length === 0)
|
|
968
|
+
activeNotes.delete(key);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
case "controller": {
|
|
974
|
+
const ch = event.channel;
|
|
975
|
+
for (const [key, entries] of activeNotes) {
|
|
976
|
+
if (key % numChannels !== ch)
|
|
977
|
+
continue;
|
|
978
|
+
for (const entry of entries)
|
|
979
|
+
entry.events.push(event);
|
|
980
|
+
}
|
|
981
|
+
switch (event.controllerType) {
|
|
982
|
+
case 64: { // Sustain Pedal
|
|
983
|
+
const on = event.value >= 64;
|
|
984
|
+
sustainPedal[ch] = on ? 1 : 0;
|
|
985
|
+
if (!on) {
|
|
986
|
+
for (const [key, offItems] of pendingOff) {
|
|
987
|
+
if (key % numChannels !== ch)
|
|
988
|
+
continue;
|
|
989
|
+
const activeStack = activeNotes.get(key);
|
|
990
|
+
for (const { t: offTime, ticks: offTicks } of offItems) {
|
|
991
|
+
if (activeStack && activeStack.length > 0) {
|
|
992
|
+
finalizeEntry(activeStack.shift(), offTime, offTicks);
|
|
993
|
+
if (activeStack.length === 0)
|
|
994
|
+
activeNotes.delete(key);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
pendingOff.delete(key);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
case 66: { // Sostenuto Pedal
|
|
1003
|
+
const on = event.value >= 64;
|
|
1004
|
+
if (on && !sostenutoPedal[ch]) {
|
|
1005
|
+
for (const [key] of activeNotes) {
|
|
1006
|
+
if (key % numChannels === ch)
|
|
1007
|
+
sostenutoKeys[ch].add(key);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
else if (!on) {
|
|
1011
|
+
sostenutoKeys[ch].clear();
|
|
1012
|
+
}
|
|
1013
|
+
sostenutoPedal[ch] = on ? 1 : 0;
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
case 121: // Reset All Controllers
|
|
1017
|
+
sustainPedal[ch] = 0;
|
|
1018
|
+
sostenutoPedal[ch] = 0;
|
|
1019
|
+
sostenutoKeys[ch].clear();
|
|
1020
|
+
break;
|
|
1021
|
+
case 120: // All Sound Off
|
|
1022
|
+
case 123: { // All Notes Off
|
|
1023
|
+
for (const [key, stack] of activeNotes) {
|
|
1024
|
+
if (key % numChannels !== ch)
|
|
1025
|
+
continue;
|
|
1026
|
+
for (const entry of stack)
|
|
1027
|
+
finalizeEntry(entry, t, event.ticks);
|
|
1028
|
+
activeNotes.delete(key);
|
|
1029
|
+
}
|
|
1030
|
+
for (const key of pendingOff.keys()) {
|
|
1031
|
+
if (key % numChannels === ch)
|
|
1032
|
+
pendingOff.delete(key);
|
|
1033
|
+
}
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
case "sysEx":
|
|
1040
|
+
if (event.data[0] === 126 && event.data[1] === 9 && event.data[2] === 3) {
|
|
1041
|
+
// GM1 System On / GM2 System On
|
|
1042
|
+
if (event.data[3] === 1 || event.data[3] === 3) {
|
|
1043
|
+
sustainPedal.fill(0);
|
|
1044
|
+
pendingOff.clear();
|
|
1045
|
+
for (const [, stack] of activeNotes) {
|
|
1046
|
+
for (const entry of stack)
|
|
1047
|
+
finalizeEntry(entry, t, event.ticks);
|
|
1048
|
+
}
|
|
1049
|
+
activeNotes.clear();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
for (const [, entries] of activeNotes) {
|
|
1054
|
+
for (const entry of entries)
|
|
1055
|
+
entry.events.push(event);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
break;
|
|
1059
|
+
case "pitchBend":
|
|
1060
|
+
case "programChange":
|
|
1061
|
+
case "channelAftertouch": {
|
|
1062
|
+
const ch = event.channel;
|
|
1063
|
+
for (const [key, entries] of activeNotes) {
|
|
1064
|
+
if (key % numChannels !== ch)
|
|
1065
|
+
continue;
|
|
1066
|
+
for (const entry of entries)
|
|
1067
|
+
entry.events.push(event);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
for (const [, stack] of activeNotes) {
|
|
1073
|
+
for (const entry of stack)
|
|
1074
|
+
finalizeEntry(entry, totalTime, Infinity);
|
|
1075
|
+
}
|
|
577
1076
|
}
|
|
578
1077
|
cacheVoiceIds() {
|
|
579
|
-
const { channels, timeline, voiceCounter } = this;
|
|
1078
|
+
const { channels, timeline, voiceCounter, cacheMode } = this;
|
|
580
1079
|
for (let i = 0; i < timeline.length; i++) {
|
|
581
1080
|
const event = timeline[i];
|
|
582
1081
|
switch (event.type) {
|
|
@@ -602,6 +1101,9 @@ class MidyGM2 extends EventTarget {
|
|
|
602
1101
|
voiceCounter.delete(audioBufferId);
|
|
603
1102
|
}
|
|
604
1103
|
this.GM2SystemOn();
|
|
1104
|
+
if (cacheMode === "adsr" || cacheMode === "note" || cacheMode === "audio") {
|
|
1105
|
+
this.buildNoteOnDurations();
|
|
1106
|
+
}
|
|
605
1107
|
}
|
|
606
1108
|
getVoiceId(channel, noteNumber, velocity) {
|
|
607
1109
|
const programNumber = channel.programNumber;
|
|
@@ -620,7 +1122,8 @@ class MidyGM2 extends EventTarget {
|
|
|
620
1122
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
621
1123
|
const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
622
1124
|
const { instrument, sampleID } = voice.generators;
|
|
623
|
-
return soundFontIndex * (2 **
|
|
1125
|
+
return soundFontIndex * (2 ** 31) + instrument * (2 ** 24) +
|
|
1126
|
+
(sampleID << 8);
|
|
624
1127
|
}
|
|
625
1128
|
createChannelAudioNodes(audioContext) {
|
|
626
1129
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.panMSB.defaultValue);
|
|
@@ -630,38 +1133,11 @@ class MidyGM2 extends EventTarget {
|
|
|
630
1133
|
gainL.connect(merger, 0, 0);
|
|
631
1134
|
gainR.connect(merger, 0, 1);
|
|
632
1135
|
merger.connect(this.masterVolume);
|
|
633
|
-
return {
|
|
634
|
-
gainL,
|
|
635
|
-
gainR,
|
|
636
|
-
merger,
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
resetChannelTable(channel) {
|
|
640
|
-
channel.controlTable.set(defaultControlValues);
|
|
641
|
-
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
642
|
-
channel.channelPressureTable.set(defaultPressureValues);
|
|
643
|
-
channel.keyBasedTable.fill(-1);
|
|
1136
|
+
return { gainL, gainR, merger };
|
|
644
1137
|
}
|
|
645
1138
|
createChannels(audioContext) {
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
currentBufferSource: null,
|
|
649
|
-
isDrum: false,
|
|
650
|
-
state: new ControllerState(),
|
|
651
|
-
...this.constructor.channelSettings,
|
|
652
|
-
...this.createChannelAudioNodes(audioContext),
|
|
653
|
-
scheduledNotes: [],
|
|
654
|
-
sustainNotes: [],
|
|
655
|
-
sostenutoNotes: [],
|
|
656
|
-
controlTable: new Int8Array(defaultControlValues),
|
|
657
|
-
scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
|
|
658
|
-
channelPressureTable: new Int8Array(defaultPressureValues),
|
|
659
|
-
keyBasedTable: new Int8Array(128 * 128).fill(-1),
|
|
660
|
-
keyBasedGainLs: new Array(128),
|
|
661
|
-
keyBasedGainRs: new Array(128),
|
|
662
|
-
};
|
|
663
|
-
});
|
|
664
|
-
return channels;
|
|
1139
|
+
const settings = this.constructor.channelSettings;
|
|
1140
|
+
return Array.from({ length: this.numChannels }, () => new Channel(this.createChannelAudioNodes(audioContext), settings));
|
|
665
1141
|
}
|
|
666
1142
|
decodeOggVorbis(sample) {
|
|
667
1143
|
const task = decoderQueue.then(async () => {
|
|
@@ -720,15 +1196,26 @@ class MidyGM2 extends EventTarget {
|
|
|
720
1196
|
return ((programNumber === 48 && noteNumber === 88) ||
|
|
721
1197
|
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
722
1198
|
}
|
|
723
|
-
createBufferSource(channel, noteNumber, voiceParams,
|
|
1199
|
+
createBufferSource(channel, noteNumber, voiceParams, renderedOrRaw) {
|
|
1200
|
+
const isRendered = renderedOrRaw instanceof RenderedBuffer;
|
|
1201
|
+
const audioBuffer = isRendered ? renderedOrRaw.buffer : renderedOrRaw;
|
|
724
1202
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
725
1203
|
bufferSource.buffer = audioBuffer;
|
|
726
|
-
|
|
1204
|
+
const isDrumLoop = channel.isDrum
|
|
727
1205
|
? this.isLoopDrum(channel, noteNumber)
|
|
728
|
-
:
|
|
1206
|
+
: voiceParams.sampleModes % 2 !== 0;
|
|
1207
|
+
const isLoop = isRendered ? renderedOrRaw.isLoop : isDrumLoop;
|
|
1208
|
+
bufferSource.loop = isLoop;
|
|
729
1209
|
if (bufferSource.loop) {
|
|
730
|
-
|
|
731
|
-
|
|
1210
|
+
if (isRendered && renderedOrRaw.adsDuration != null) {
|
|
1211
|
+
bufferSource.loopStart = renderedOrRaw.loopStart;
|
|
1212
|
+
bufferSource.loopEnd = renderedOrRaw.loopStart +
|
|
1213
|
+
renderedOrRaw.loopDuration;
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
1217
|
+
bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
|
|
1218
|
+
}
|
|
732
1219
|
}
|
|
733
1220
|
return bufferSource;
|
|
734
1221
|
}
|
|
@@ -745,27 +1232,29 @@ class MidyGM2 extends EventTarget {
|
|
|
745
1232
|
break;
|
|
746
1233
|
const startTime = t + schedulingOffset;
|
|
747
1234
|
switch (event.type) {
|
|
748
|
-
case "noteOn":
|
|
749
|
-
this.
|
|
1235
|
+
case "noteOn": {
|
|
1236
|
+
const note = this.createNote(event.channel, event.noteNumber, event.velocity, startTime);
|
|
1237
|
+
note.timelineIndex = queueIndex;
|
|
1238
|
+
this.setupNote(event.channel, note, startTime);
|
|
750
1239
|
break;
|
|
751
|
-
|
|
1240
|
+
}
|
|
1241
|
+
case "noteOff":
|
|
752
1242
|
this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
753
1243
|
break;
|
|
754
|
-
}
|
|
755
1244
|
case "controller":
|
|
756
1245
|
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
757
1246
|
break;
|
|
758
1247
|
case "programChange":
|
|
759
1248
|
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
760
1249
|
break;
|
|
761
|
-
case "channelAftertouch":
|
|
762
|
-
this.setChannelPressure(event.channel, event.amount, startTime);
|
|
763
|
-
break;
|
|
764
1250
|
case "pitchBend":
|
|
765
1251
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
766
1252
|
break;
|
|
767
1253
|
case "sysEx":
|
|
768
1254
|
this.handleSysEx(event.data, startTime);
|
|
1255
|
+
break;
|
|
1256
|
+
case "channelAftertouch":
|
|
1257
|
+
this.setChannelPressure(event.channel, event.amount, startTime);
|
|
769
1258
|
}
|
|
770
1259
|
queueIndex++;
|
|
771
1260
|
}
|
|
@@ -786,6 +1275,7 @@ class MidyGM2 extends EventTarget {
|
|
|
786
1275
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
787
1276
|
this.voiceCache.clear();
|
|
788
1277
|
this.realtimeVoiceCache.clear();
|
|
1278
|
+
this.adsrVoiceCache.clear();
|
|
789
1279
|
const channels = this.channels;
|
|
790
1280
|
for (let ch = 0; ch < channels.length; ch++) {
|
|
791
1281
|
channels[ch].scheduledNotes = [];
|
|
@@ -812,14 +1302,101 @@ class MidyGM2 extends EventTarget {
|
|
|
812
1302
|
break;
|
|
813
1303
|
case "sysEx":
|
|
814
1304
|
this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
|
|
1305
|
+
break;
|
|
1306
|
+
case "channelAftertouch":
|
|
1307
|
+
this.setChannelPressure(event.channel, event.amount, now - resumeTime + event.startTime * inverseTempo);
|
|
815
1308
|
}
|
|
816
1309
|
}
|
|
817
1310
|
}
|
|
1311
|
+
async playAudioBuffer() {
|
|
1312
|
+
const audioContext = this.audioContext;
|
|
1313
|
+
const paused = this.isPaused;
|
|
1314
|
+
this.isPlaying = true;
|
|
1315
|
+
this.isPaused = false;
|
|
1316
|
+
this.startTime = audioContext.currentTime;
|
|
1317
|
+
if (paused) {
|
|
1318
|
+
this.dispatchEvent(new Event("resumed"));
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
this.dispatchEvent(new Event("started"));
|
|
1322
|
+
}
|
|
1323
|
+
let exitReason;
|
|
1324
|
+
outer: while (true) {
|
|
1325
|
+
const buffer = this.renderedAudioBuffer;
|
|
1326
|
+
const bufferSource = new AudioBufferSourceNode(audioContext, { buffer });
|
|
1327
|
+
bufferSource.playbackRate.value = this.tempo;
|
|
1328
|
+
bufferSource.connect(this.masterVolume);
|
|
1329
|
+
const offset = Math.min(Math.max(this.resumeTime, 0), buffer.duration);
|
|
1330
|
+
bufferSource.start(audioContext.currentTime, offset);
|
|
1331
|
+
this.audioModeBufferSource = bufferSource;
|
|
1332
|
+
let naturalEnded = false;
|
|
1333
|
+
bufferSource.onended = () => {
|
|
1334
|
+
naturalEnded = true;
|
|
1335
|
+
};
|
|
1336
|
+
while (true) {
|
|
1337
|
+
const now = audioContext.currentTime;
|
|
1338
|
+
await this.scheduleTask(() => { }, now + this.noteCheckInterval);
|
|
1339
|
+
if (naturalEnded || this.currentTime() >= this.totalTime) {
|
|
1340
|
+
bufferSource.disconnect();
|
|
1341
|
+
this.audioModeBufferSource = null;
|
|
1342
|
+
if (this.loop) {
|
|
1343
|
+
this.resumeTime = 0;
|
|
1344
|
+
this.startTime = audioContext.currentTime;
|
|
1345
|
+
this.dispatchEvent(new Event("looped"));
|
|
1346
|
+
continue outer;
|
|
1347
|
+
}
|
|
1348
|
+
await audioContext.suspend();
|
|
1349
|
+
exitReason = "ended";
|
|
1350
|
+
break outer;
|
|
1351
|
+
}
|
|
1352
|
+
if (this.isPausing) {
|
|
1353
|
+
this.resumeTime = this.currentTime();
|
|
1354
|
+
bufferSource.stop();
|
|
1355
|
+
bufferSource.disconnect();
|
|
1356
|
+
this.audioModeBufferSource = null;
|
|
1357
|
+
await audioContext.suspend();
|
|
1358
|
+
this.isPausing = false;
|
|
1359
|
+
exitReason = "paused";
|
|
1360
|
+
break outer;
|
|
1361
|
+
}
|
|
1362
|
+
else if (this.isStopping) {
|
|
1363
|
+
bufferSource.stop();
|
|
1364
|
+
bufferSource.disconnect();
|
|
1365
|
+
this.audioModeBufferSource = null;
|
|
1366
|
+
await audioContext.suspend();
|
|
1367
|
+
this.isStopping = false;
|
|
1368
|
+
exitReason = "stopped";
|
|
1369
|
+
break outer;
|
|
1370
|
+
}
|
|
1371
|
+
else if (this.isSeeking) {
|
|
1372
|
+
bufferSource.stop();
|
|
1373
|
+
bufferSource.disconnect();
|
|
1374
|
+
this.audioModeBufferSource = null;
|
|
1375
|
+
this.startTime = audioContext.currentTime;
|
|
1376
|
+
this.isSeeking = false;
|
|
1377
|
+
this.dispatchEvent(new Event("seeked"));
|
|
1378
|
+
continue outer;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
this.isPlaying = false;
|
|
1383
|
+
if (exitReason === "paused") {
|
|
1384
|
+
this.isPaused = true;
|
|
1385
|
+
this.dispatchEvent(new Event("paused"));
|
|
1386
|
+
}
|
|
1387
|
+
else if (exitReason !== undefined) {
|
|
1388
|
+
this.isPaused = false;
|
|
1389
|
+
this.dispatchEvent(new Event(exitReason));
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
818
1392
|
async playNotes() {
|
|
819
1393
|
const audioContext = this.audioContext;
|
|
820
1394
|
if (audioContext.state === "suspended") {
|
|
821
1395
|
await audioContext.resume();
|
|
822
1396
|
}
|
|
1397
|
+
if (this.cacheMode === "audio" && this.renderedAudioBuffer) {
|
|
1398
|
+
return await this.playAudioBuffer();
|
|
1399
|
+
}
|
|
823
1400
|
const paused = this.isPaused;
|
|
824
1401
|
this.isPlaying = true;
|
|
825
1402
|
this.isPaused = false;
|
|
@@ -952,12 +1529,12 @@ class MidyGM2 extends EventTarget {
|
|
|
952
1529
|
if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
|
|
953
1530
|
switch (data[3]) {
|
|
954
1531
|
case 1:
|
|
955
|
-
this.GM1SystemOn(
|
|
1532
|
+
this.GM1SystemOn();
|
|
956
1533
|
break;
|
|
957
1534
|
case 2: // GM System Off
|
|
958
1535
|
break;
|
|
959
1536
|
case 3:
|
|
960
|
-
this.GM2SystemOn(
|
|
1537
|
+
this.GM2SystemOn();
|
|
961
1538
|
break;
|
|
962
1539
|
default:
|
|
963
1540
|
console.warn(`Unsupported Exclusive Message: ${data}`);
|
|
@@ -1024,6 +1601,186 @@ class MidyGM2 extends EventTarget {
|
|
|
1024
1601
|
this.notePromises = [];
|
|
1025
1602
|
return stopPromise;
|
|
1026
1603
|
}
|
|
1604
|
+
async render() {
|
|
1605
|
+
if (this.isRendering)
|
|
1606
|
+
return;
|
|
1607
|
+
if (this.timeline.length === 0)
|
|
1608
|
+
return;
|
|
1609
|
+
if (this.voiceCounter.size === 0)
|
|
1610
|
+
this.cacheVoiceIds();
|
|
1611
|
+
this.isRendering = true;
|
|
1612
|
+
this.renderedAudioBuffer = null;
|
|
1613
|
+
this.dispatchEvent(new Event("rendering"));
|
|
1614
|
+
const sampleRate = this.audioContext.sampleRate;
|
|
1615
|
+
const totalSamples = Math.ceil((this.totalTime + this.startDelay) * sampleRate);
|
|
1616
|
+
const renderBankMSB = new Uint8Array(this.numChannels);
|
|
1617
|
+
const renderBankLSB = new Uint8Array(this.numChannels);
|
|
1618
|
+
const renderProgramNumber = new Uint8Array(this.numChannels);
|
|
1619
|
+
const renderIsDrum = new Uint8Array(this.numChannels);
|
|
1620
|
+
renderBankMSB.fill(121);
|
|
1621
|
+
renderIsDrum[9] = 1;
|
|
1622
|
+
const renderControllerStates = Array.from({ length: this.numChannels }, () => {
|
|
1623
|
+
const state = new Float32Array(256);
|
|
1624
|
+
for (const { type, defaultValue } of Object.values(defaultControllerState)) {
|
|
1625
|
+
state[type] = defaultValue;
|
|
1626
|
+
}
|
|
1627
|
+
return state;
|
|
1628
|
+
});
|
|
1629
|
+
const tasks = [];
|
|
1630
|
+
const timeline = this.timeline;
|
|
1631
|
+
const inverseTempo = 1 / this.tempo;
|
|
1632
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
1633
|
+
const event = timeline[i];
|
|
1634
|
+
const ch = event.channel;
|
|
1635
|
+
switch (event.type) {
|
|
1636
|
+
case "noteOn": {
|
|
1637
|
+
const noteEvent = this.noteOnEvents.get(i);
|
|
1638
|
+
const noteDuration = noteEvent?.duration ??
|
|
1639
|
+
this.noteOnDurations.get(i) ??
|
|
1640
|
+
0;
|
|
1641
|
+
if (noteDuration <= 0)
|
|
1642
|
+
continue;
|
|
1643
|
+
const { noteNumber, velocity } = event;
|
|
1644
|
+
const isDrum = renderIsDrum[ch] === 1;
|
|
1645
|
+
const programNumber = renderProgramNumber[ch];
|
|
1646
|
+
const bankTable = this.soundFontTable[programNumber];
|
|
1647
|
+
if (!bankTable)
|
|
1648
|
+
continue;
|
|
1649
|
+
let bank = isDrum ? 128 : renderBankLSB[ch];
|
|
1650
|
+
if (bankTable[bank] === undefined) {
|
|
1651
|
+
if (isDrum)
|
|
1652
|
+
continue;
|
|
1653
|
+
bank = 0;
|
|
1654
|
+
}
|
|
1655
|
+
const soundFontIndex = bankTable[bank];
|
|
1656
|
+
if (soundFontIndex === undefined)
|
|
1657
|
+
continue;
|
|
1658
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
1659
|
+
const fakeChannel = {
|
|
1660
|
+
state: { array: renderControllerStates[ch].slice() },
|
|
1661
|
+
programNumber,
|
|
1662
|
+
isDrum,
|
|
1663
|
+
modulationDepthRange: 50,
|
|
1664
|
+
detune: 0,
|
|
1665
|
+
};
|
|
1666
|
+
const controllerState = this.getControllerState(fakeChannel, noteNumber, velocity);
|
|
1667
|
+
const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
1668
|
+
if (!voice)
|
|
1669
|
+
continue;
|
|
1670
|
+
const voiceParams = voice.getAllParams(controllerState);
|
|
1671
|
+
const t = event.startTime * inverseTempo + this.startDelay;
|
|
1672
|
+
const fakeNote = { voiceParams, channel: ch, noteNumber, velocity };
|
|
1673
|
+
const promise = (async () => {
|
|
1674
|
+
try {
|
|
1675
|
+
return await this.createFullRenderedBuffer(fakeChannel, fakeNote, voiceParams, noteDuration, noteEvent);
|
|
1676
|
+
}
|
|
1677
|
+
catch (err) {
|
|
1678
|
+
console.warn("render: note render failed", err);
|
|
1679
|
+
return null;
|
|
1680
|
+
}
|
|
1681
|
+
})();
|
|
1682
|
+
tasks.push({ t, promise, fakeChannel });
|
|
1683
|
+
break;
|
|
1684
|
+
}
|
|
1685
|
+
case "controller": {
|
|
1686
|
+
const { controllerType, value } = event;
|
|
1687
|
+
switch (controllerType) {
|
|
1688
|
+
case 0: // bankMSB
|
|
1689
|
+
renderBankMSB[ch] = value;
|
|
1690
|
+
if (this.mode === "GM2") {
|
|
1691
|
+
if (value === 120) {
|
|
1692
|
+
renderIsDrum[ch] = 1;
|
|
1693
|
+
}
|
|
1694
|
+
else if (value === 121) {
|
|
1695
|
+
renderIsDrum[ch] = 0;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
break;
|
|
1699
|
+
case 32: // bankLSB
|
|
1700
|
+
renderBankLSB[ch] = value;
|
|
1701
|
+
break;
|
|
1702
|
+
default: {
|
|
1703
|
+
const stateIndex = 128 + controllerType;
|
|
1704
|
+
if (stateIndex < 256) {
|
|
1705
|
+
renderControllerStates[ch][stateIndex] = value / 127;
|
|
1706
|
+
}
|
|
1707
|
+
break;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
break;
|
|
1711
|
+
}
|
|
1712
|
+
case "pitchBend":
|
|
1713
|
+
renderControllerStates[ch][14] = (event.value + 8192) / 16383;
|
|
1714
|
+
break;
|
|
1715
|
+
case "programChange":
|
|
1716
|
+
renderProgramNumber[ch] = event.programNumber;
|
|
1717
|
+
if (this.mode === "GM2") {
|
|
1718
|
+
if (renderBankMSB[ch] === 120) {
|
|
1719
|
+
renderIsDrum[ch] = 1;
|
|
1720
|
+
}
|
|
1721
|
+
else if (renderBankMSB[ch] === 121) {
|
|
1722
|
+
renderIsDrum[ch] = 0;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
break;
|
|
1726
|
+
case "sysEx": {
|
|
1727
|
+
const data = event.data;
|
|
1728
|
+
if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
|
|
1729
|
+
if (data[3] === 1) { // GM1 System On
|
|
1730
|
+
renderBankMSB.fill(0);
|
|
1731
|
+
renderBankLSB.fill(0);
|
|
1732
|
+
renderProgramNumber.fill(0);
|
|
1733
|
+
renderIsDrum.fill(0);
|
|
1734
|
+
renderIsDrum[9] = 1;
|
|
1735
|
+
renderBankMSB[9] = 1;
|
|
1736
|
+
for (let c = 0; c < this.numChannels; c++) {
|
|
1737
|
+
for (const { type, defaultValue } of Object.values(defaultControllerState)) {
|
|
1738
|
+
renderControllerStates[c][type] = defaultValue;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
renderNoteAftertouch.fill(0);
|
|
1742
|
+
}
|
|
1743
|
+
else if (data[3] === 3) { // GM2 System On
|
|
1744
|
+
renderBankMSB.fill(121);
|
|
1745
|
+
renderBankLSB.fill(0);
|
|
1746
|
+
renderProgramNumber.fill(0);
|
|
1747
|
+
renderIsDrum.fill(0);
|
|
1748
|
+
renderIsDrum[9] = 1;
|
|
1749
|
+
renderBankMSB[9] = 120;
|
|
1750
|
+
for (let c = 0; c < this.numChannels; c++) {
|
|
1751
|
+
for (const { type, defaultValue } of Object.values(defaultControllerState)) {
|
|
1752
|
+
renderControllerStates[c][type] = defaultValue;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
renderNoteAftertouch.fill(0);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
break;
|
|
1759
|
+
}
|
|
1760
|
+
case "channelAftertouch":
|
|
1761
|
+
renderControllerStates[ch][13] = event.amount / 127;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const offlineContext = new OfflineAudioContext(2, totalSamples, sampleRate);
|
|
1765
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
1766
|
+
const { t, promise } = tasks[i];
|
|
1767
|
+
const noteBuffer = await promise;
|
|
1768
|
+
if (!noteBuffer)
|
|
1769
|
+
continue;
|
|
1770
|
+
const audioBuffer = noteBuffer instanceof RenderedBuffer
|
|
1771
|
+
? noteBuffer.buffer
|
|
1772
|
+
: noteBuffer;
|
|
1773
|
+
const bufferSource = new AudioBufferSourceNode(offlineContext, {
|
|
1774
|
+
buffer: audioBuffer,
|
|
1775
|
+
});
|
|
1776
|
+
bufferSource.connect(offlineContext.destination);
|
|
1777
|
+
bufferSource.start(t);
|
|
1778
|
+
}
|
|
1779
|
+
this.renderedAudioBuffer = await offlineContext.startRendering();
|
|
1780
|
+
this.isRendering = false;
|
|
1781
|
+
this.dispatchEvent(new Event("rendered"));
|
|
1782
|
+
return this.renderedAudioBuffer;
|
|
1783
|
+
}
|
|
1027
1784
|
async start() {
|
|
1028
1785
|
if (this.isPlaying || this.isPaused)
|
|
1029
1786
|
return;
|
|
@@ -1060,11 +1817,22 @@ class MidyGM2 extends EventTarget {
|
|
|
1060
1817
|
}
|
|
1061
1818
|
}
|
|
1062
1819
|
tempoChange(tempo) {
|
|
1820
|
+
const cacheMode = this.cacheMode;
|
|
1063
1821
|
const timeScale = this.tempo / tempo;
|
|
1064
1822
|
this.resumeTime = this.resumeTime * timeScale;
|
|
1065
1823
|
this.tempo = tempo;
|
|
1066
1824
|
this.totalTime = this.calcTotalTime();
|
|
1067
1825
|
this.seekTo(this.currentTime() * timeScale);
|
|
1826
|
+
if (cacheMode === "adsr" || cacheMode === "note" || cacheMode === "audio") {
|
|
1827
|
+
this.buildNoteOnDurations();
|
|
1828
|
+
this.fullVoiceCache.clear();
|
|
1829
|
+
this.adsrVoiceCache.clear();
|
|
1830
|
+
}
|
|
1831
|
+
if (cacheMode === "audio") {
|
|
1832
|
+
if (this.audioModeBufferSource) {
|
|
1833
|
+
this.audioModeBufferSource.playbackRate.setValueAtTime(this.tempo, this.audioContext.currentTime);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1068
1836
|
}
|
|
1069
1837
|
calcTotalTime() {
|
|
1070
1838
|
const totalTimeEventTypes = this.totalTimeEventTypes;
|
|
@@ -1085,6 +1853,9 @@ class MidyGM2 extends EventTarget {
|
|
|
1085
1853
|
if (!this.isPlaying)
|
|
1086
1854
|
return this.resumeTime;
|
|
1087
1855
|
const now = this.audioContext.currentTime;
|
|
1856
|
+
if (this.cacheMode === "audio") {
|
|
1857
|
+
return this.resumeTime + (now - this.startTime) * this.tempo;
|
|
1858
|
+
}
|
|
1088
1859
|
return now + this.resumeTime - this.startTime;
|
|
1089
1860
|
}
|
|
1090
1861
|
async processScheduledNotes(channel, callback) {
|
|
@@ -1282,6 +2053,8 @@ class MidyGM2 extends EventTarget {
|
|
|
1282
2053
|
}
|
|
1283
2054
|
updateChannelDetune(channel, scheduleTime) {
|
|
1284
2055
|
this.processScheduledNotes(channel, (note) => {
|
|
2056
|
+
if (note.renderedBuffer?.isFull)
|
|
2057
|
+
return;
|
|
1285
2058
|
if (this.isPortamento(channel, note)) {
|
|
1286
2059
|
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
1287
2060
|
}
|
|
@@ -1370,6 +2143,8 @@ class MidyGM2 extends EventTarget {
|
|
|
1370
2143
|
.exponentialRampToValueAtTime(sustainVolume, portamentoTime);
|
|
1371
2144
|
}
|
|
1372
2145
|
setVolumeEnvelope(channel, note, scheduleTime) {
|
|
2146
|
+
if (!note.volumeEnvelopeNode)
|
|
2147
|
+
return;
|
|
1373
2148
|
const { voiceParams, startTime } = note;
|
|
1374
2149
|
const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
|
|
1375
2150
|
(1 + this.getAmplitudeControl(channel));
|
|
@@ -1398,9 +2173,6 @@ class MidyGM2 extends EventTarget {
|
|
|
1398
2173
|
}
|
|
1399
2174
|
setDetune(channel, note, scheduleTime) {
|
|
1400
2175
|
const detune = this.calcNoteDetune(channel, note);
|
|
1401
|
-
note.bufferSource.detune
|
|
1402
|
-
.cancelScheduledValues(scheduleTime)
|
|
1403
|
-
.setValueAtTime(detune, scheduleTime);
|
|
1404
2176
|
const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
|
|
1405
2177
|
note.bufferSource.detune
|
|
1406
2178
|
.cancelAndHoldAtTime(scheduleTime)
|
|
@@ -1460,6 +2232,8 @@ class MidyGM2 extends EventTarget {
|
|
|
1460
2232
|
.exponentialRampToValueAtTime(adjustedSustainFreq, portamentoTime);
|
|
1461
2233
|
}
|
|
1462
2234
|
setFilterEnvelope(channel, note, scheduleTime) {
|
|
2235
|
+
if (!note.filterEnvelopeNode)
|
|
2236
|
+
return;
|
|
1463
2237
|
const { voiceParams, startTime } = note;
|
|
1464
2238
|
const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
|
|
1465
2239
|
const baseCent = voiceParams.initialFilterFc +
|
|
@@ -1499,14 +2273,17 @@ class MidyGM2 extends EventTarget {
|
|
|
1499
2273
|
note.modLfoToPitch = new GainNode(audioContext);
|
|
1500
2274
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
1501
2275
|
note.modLfoToVolume = new GainNode(audioContext);
|
|
1502
|
-
this.setModLfoToVolume(note, scheduleTime);
|
|
2276
|
+
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
1503
2277
|
note.modLfo.start(note.startTime + voiceParams.delayModLFO);
|
|
1504
2278
|
note.modLfo.connect(note.modLfoToFilterFc);
|
|
1505
|
-
|
|
2279
|
+
if (note.filterEnvelopeNode) {
|
|
2280
|
+
note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
|
|
2281
|
+
}
|
|
1506
2282
|
note.modLfo.connect(note.modLfoToPitch);
|
|
1507
2283
|
note.modLfoToPitch.connect(note.bufferSource.detune);
|
|
1508
2284
|
note.modLfo.connect(note.modLfoToVolume);
|
|
1509
|
-
note.
|
|
2285
|
+
const volumeTarget = note.volumeEnvelopeNode ?? note.volumeNode;
|
|
2286
|
+
note.modLfoToVolume.connect(volumeTarget.gain);
|
|
1510
2287
|
}
|
|
1511
2288
|
startVibrato(channel, note, scheduleTime) {
|
|
1512
2289
|
const { voiceParams } = note;
|
|
@@ -1522,34 +2299,342 @@ class MidyGM2 extends EventTarget {
|
|
|
1522
2299
|
note.vibLfo.connect(note.vibLfoToPitch);
|
|
1523
2300
|
note.vibLfoToPitch.connect(note.bufferSource.detune);
|
|
1524
2301
|
}
|
|
1525
|
-
async
|
|
2302
|
+
async createAdsRenderedBuffer(channel, note, voiceParams, audioBuffer, isDrum = false) {
|
|
2303
|
+
const isLoop = isDrum ? false : (voiceParams.sampleModes % 2 !== 0);
|
|
2304
|
+
const volAttack = voiceParams.volDelay + voiceParams.volAttack;
|
|
2305
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
2306
|
+
const decayDuration = voiceParams.volDecay;
|
|
2307
|
+
const adsDuration = volHold + decayDuration * decayCurve * 5;
|
|
2308
|
+
const loopStartTime = voiceParams.loopStart / voiceParams.sampleRate;
|
|
2309
|
+
const loopDuration = isLoop
|
|
2310
|
+
? (voiceParams.loopEnd - voiceParams.loopStart) / voiceParams.sampleRate
|
|
2311
|
+
: 0;
|
|
2312
|
+
const loopCount = isLoop && adsDuration > loopStartTime
|
|
2313
|
+
? Math.ceil((adsDuration - loopStartTime) / loopDuration)
|
|
2314
|
+
: 0;
|
|
2315
|
+
const alignedLoopStart = loopStartTime + loopCount * loopDuration;
|
|
2316
|
+
const renderDuration = isLoop
|
|
2317
|
+
? alignedLoopStart + loopDuration
|
|
2318
|
+
: audioBuffer.duration;
|
|
2319
|
+
const offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, Math.ceil(renderDuration * this.audioContext.sampleRate), this.audioContext.sampleRate);
|
|
2320
|
+
const bufferSource = new AudioBufferSourceNode(offlineContext);
|
|
2321
|
+
bufferSource.buffer = audioBuffer;
|
|
2322
|
+
bufferSource.playbackRate.value = voiceParams.playbackRate;
|
|
2323
|
+
bufferSource.loop = isLoop;
|
|
2324
|
+
if (isLoop) {
|
|
2325
|
+
bufferSource.loopStart = loopStartTime;
|
|
2326
|
+
bufferSource.loopEnd = loopStartTime + loopDuration;
|
|
2327
|
+
}
|
|
2328
|
+
const initialFreq = this.clampCutoffFrequency(this.centToHz(voiceParams.initialFilterFc));
|
|
2329
|
+
const filterEnvelopeNode = new BiquadFilterNode(offlineContext, {
|
|
2330
|
+
type: "lowpass",
|
|
2331
|
+
Q: voiceParams.initialFilterQ / 10, // dB
|
|
2332
|
+
frequency: initialFreq,
|
|
2333
|
+
});
|
|
2334
|
+
const volumeEnvelopeNode = new GainNode(offlineContext);
|
|
2335
|
+
const offlineNote = {
|
|
2336
|
+
...note,
|
|
2337
|
+
startTime: 0,
|
|
2338
|
+
bufferSource,
|
|
2339
|
+
filterEnvelopeNode,
|
|
2340
|
+
volumeEnvelopeNode,
|
|
2341
|
+
};
|
|
2342
|
+
this.setVolumeEnvelope(channel, offlineNote, 0);
|
|
2343
|
+
this.setFilterEnvelope(channel, offlineNote, 0);
|
|
2344
|
+
bufferSource.connect(filterEnvelopeNode);
|
|
2345
|
+
filterEnvelopeNode.connect(volumeEnvelopeNode);
|
|
2346
|
+
volumeEnvelopeNode.connect(offlineContext.destination);
|
|
2347
|
+
if (voiceParams.sample.type === "compressed") {
|
|
2348
|
+
bufferSource.start(0, voiceParams.start / audioBuffer.sampleRate);
|
|
2349
|
+
}
|
|
2350
|
+
else {
|
|
2351
|
+
bufferSource.start(0);
|
|
2352
|
+
}
|
|
2353
|
+
const buffer = await offlineContext.startRendering();
|
|
2354
|
+
return new RenderedBuffer(buffer, {
|
|
2355
|
+
isLoop,
|
|
2356
|
+
adsDuration,
|
|
2357
|
+
loopStart: alignedLoopStart,
|
|
2358
|
+
loopDuration,
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
async createAdsrRenderedBuffer(channel, note, voiceParams, audioBuffer, noteDuration) {
|
|
2362
|
+
const isLoop = voiceParams.sampleModes % 2 !== 0;
|
|
2363
|
+
const volAttack = voiceParams.volDelay + voiceParams.volAttack;
|
|
2364
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
2365
|
+
const decayDuration = voiceParams.volDecay;
|
|
2366
|
+
const adsDuration = volHold + decayDuration * decayCurve * 5;
|
|
2367
|
+
const releaseDuration = voiceParams.volRelease;
|
|
2368
|
+
const loopStartTime = voiceParams.loopStart / voiceParams.sampleRate;
|
|
2369
|
+
const loopDuration = isLoop
|
|
2370
|
+
? (voiceParams.loopEnd - voiceParams.loopStart) / voiceParams.sampleRate
|
|
2371
|
+
: 0;
|
|
2372
|
+
const noteLoopCount = isLoop && noteDuration > loopStartTime
|
|
2373
|
+
? Math.ceil((noteDuration - loopStartTime) / loopDuration)
|
|
2374
|
+
: 0;
|
|
2375
|
+
const alignedNoteEnd = isLoop
|
|
2376
|
+
? loopStartTime + noteLoopCount * loopDuration
|
|
2377
|
+
: noteDuration;
|
|
2378
|
+
const noteOffTime = alignedNoteEnd;
|
|
2379
|
+
const totalDuration = noteOffTime + releaseDuration;
|
|
2380
|
+
const sampleRate = this.audioContext.sampleRate;
|
|
2381
|
+
const offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, Math.ceil(totalDuration * sampleRate), sampleRate);
|
|
2382
|
+
const bufferSource = new AudioBufferSourceNode(offlineContext);
|
|
2383
|
+
bufferSource.buffer = audioBuffer;
|
|
2384
|
+
bufferSource.playbackRate.value = voiceParams.playbackRate;
|
|
2385
|
+
bufferSource.loop = isLoop;
|
|
2386
|
+
if (isLoop) {
|
|
2387
|
+
bufferSource.loopStart = loopStartTime;
|
|
2388
|
+
bufferSource.loopEnd = loopStartTime + loopDuration;
|
|
2389
|
+
}
|
|
2390
|
+
const initialFreq = this.clampCutoffFrequency(this.centToHz(voiceParams.initialFilterFc));
|
|
2391
|
+
const filterEnvelopeNode = new BiquadFilterNode(offlineContext, {
|
|
2392
|
+
type: "lowpass",
|
|
2393
|
+
Q: voiceParams.initialFilterQ / 10, // dB
|
|
2394
|
+
frequency: initialFreq,
|
|
2395
|
+
});
|
|
2396
|
+
const volumeEnvelopeNode = new GainNode(offlineContext);
|
|
2397
|
+
const offlineNote = {
|
|
2398
|
+
...note,
|
|
2399
|
+
startTime: 0,
|
|
2400
|
+
bufferSource,
|
|
2401
|
+
filterEnvelopeNode,
|
|
2402
|
+
volumeEnvelopeNode,
|
|
2403
|
+
};
|
|
2404
|
+
this.setVolumeEnvelope(channel, offlineNote, 0);
|
|
2405
|
+
this.setFilterEnvelope(channel, offlineNote, 0);
|
|
2406
|
+
const attackVolume = cbToRatio(-voiceParams.initialAttenuation);
|
|
2407
|
+
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
2408
|
+
const volDelayTime = voiceParams.volDelay;
|
|
2409
|
+
const volAttackTime = volDelayTime + voiceParams.volAttack;
|
|
2410
|
+
const volHoldTime = volAttackTime + voiceParams.volHold;
|
|
2411
|
+
let gainAtNoteOff;
|
|
2412
|
+
if (noteOffTime <= volDelayTime) {
|
|
2413
|
+
gainAtNoteOff = 0;
|
|
2414
|
+
}
|
|
2415
|
+
else if (noteOffTime <= volAttackTime) {
|
|
2416
|
+
gainAtNoteOff = 1e-6 + (attackVolume - 1e-6) *
|
|
2417
|
+
(noteOffTime - volDelayTime) / voiceParams.volAttack;
|
|
2418
|
+
}
|
|
2419
|
+
else if (noteOffTime <= volHoldTime) {
|
|
2420
|
+
gainAtNoteOff = attackVolume;
|
|
2421
|
+
}
|
|
2422
|
+
else {
|
|
2423
|
+
const decayElapsed = noteOffTime - volHoldTime;
|
|
2424
|
+
gainAtNoteOff = sustainVolume +
|
|
2425
|
+
(attackVolume - sustainVolume) *
|
|
2426
|
+
Math.exp(-decayElapsed / (decayCurve * voiceParams.volDecay));
|
|
2427
|
+
}
|
|
2428
|
+
volumeEnvelopeNode.gain
|
|
2429
|
+
.cancelScheduledValues(noteOffTime)
|
|
2430
|
+
.setValueAtTime(gainAtNoteOff, noteOffTime)
|
|
2431
|
+
.setTargetAtTime(0, noteOffTime, releaseDuration * releaseCurve);
|
|
2432
|
+
filterEnvelopeNode.frequency
|
|
2433
|
+
.cancelScheduledValues(noteOffTime)
|
|
2434
|
+
.setValueAtTime(initialFreq, noteOffTime)
|
|
2435
|
+
.setTargetAtTime(initialFreq, noteOffTime, voiceParams.modRelease * releaseCurve);
|
|
2436
|
+
bufferSource.connect(filterEnvelopeNode);
|
|
2437
|
+
filterEnvelopeNode.connect(volumeEnvelopeNode);
|
|
2438
|
+
volumeEnvelopeNode.connect(offlineContext.destination);
|
|
2439
|
+
if (isLoop) {
|
|
2440
|
+
bufferSource.start(0, voiceParams.start / audioBuffer.sampleRate);
|
|
2441
|
+
}
|
|
2442
|
+
else {
|
|
2443
|
+
bufferSource.start(0);
|
|
2444
|
+
}
|
|
2445
|
+
const buffer = await offlineContext.startRendering();
|
|
2446
|
+
return new RenderedBuffer(buffer, {
|
|
2447
|
+
isLoop: false,
|
|
2448
|
+
isFull: false,
|
|
2449
|
+
adsDuration,
|
|
2450
|
+
noteDuration: noteOffTime,
|
|
2451
|
+
releaseDuration,
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
async createFullRenderedBuffer(channel, note, voiceParams, noteDuration, noteEvent = {}) {
|
|
2455
|
+
const { startTime: noteStartTime = 0, events: noteEvents = [] } = noteEvent;
|
|
2456
|
+
const ch = note.channel ?? 0;
|
|
2457
|
+
const releaseEndDuration = voiceParams.volRelease * releaseCurve * 5;
|
|
2458
|
+
const totalDuration = noteDuration + releaseEndDuration;
|
|
2459
|
+
const sampleRate = this.audioContext.sampleRate;
|
|
2460
|
+
const offlineContext = new OfflineAudioContext(2, Math.ceil(totalDuration * sampleRate), sampleRate);
|
|
2461
|
+
const offlinePlayer = new this.constructor(offlineContext, {
|
|
2462
|
+
cacheMode: "none",
|
|
2463
|
+
});
|
|
2464
|
+
offlineContext.suspend = () => Promise.resolve();
|
|
2465
|
+
offlineContext.resume = () => Promise.resolve();
|
|
2466
|
+
offlinePlayer.soundFonts = this.soundFonts;
|
|
2467
|
+
offlinePlayer.soundFontTable = this.soundFontTable;
|
|
2468
|
+
const dstChannel = offlinePlayer.channels[ch];
|
|
2469
|
+
dstChannel.state.array.set(channel.state.array);
|
|
2470
|
+
dstChannel.isDrum = channel.isDrum;
|
|
2471
|
+
dstChannel.programNumber = channel.programNumber;
|
|
2472
|
+
dstChannel.modulationDepthRange = channel.modulationDepthRange;
|
|
2473
|
+
dstChannel.detune = this.calcChannelDetune(dstChannel);
|
|
2474
|
+
await offlinePlayer.noteOn(ch, note.noteNumber, note.velocity, 0);
|
|
2475
|
+
for (const event of noteEvents) {
|
|
2476
|
+
const t = event.startTime / this.tempo - noteStartTime;
|
|
2477
|
+
if (t < 0 || t > noteDuration)
|
|
2478
|
+
continue;
|
|
2479
|
+
switch (event.type) {
|
|
2480
|
+
case "controller":
|
|
2481
|
+
offlinePlayer.setControlChange(ch, event.controllerType, event.value, t);
|
|
2482
|
+
break;
|
|
2483
|
+
case "pitchBend":
|
|
2484
|
+
offlinePlayer.setPitchBend(ch, event.value + 8192, t);
|
|
2485
|
+
break;
|
|
2486
|
+
case "sysEx":
|
|
2487
|
+
offlinePlayer.handleSysEx(event.data, t);
|
|
2488
|
+
break;
|
|
2489
|
+
case "channelAftertouch":
|
|
2490
|
+
offlinePlayer.setChannelPressure(ch, event.amount, t);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
offlinePlayer.noteOff(ch, note.noteNumber, 0, noteDuration, true);
|
|
2494
|
+
const buffer = await offlineContext.startRendering();
|
|
2495
|
+
return new RenderedBuffer(buffer, {
|
|
2496
|
+
isLoop: false,
|
|
2497
|
+
isFull: true,
|
|
2498
|
+
noteDuration: noteDuration,
|
|
2499
|
+
releaseDuration: releaseEndDuration,
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
async getAudioBuffer(channel, note, realtime) {
|
|
2503
|
+
const cacheMode = this.cacheMode;
|
|
2504
|
+
const { noteNumber, velocity } = note;
|
|
1526
2505
|
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
2506
|
+
if (!realtime) {
|
|
2507
|
+
if (cacheMode === "note") {
|
|
2508
|
+
return await this.getFullCachedBuffer(note, audioBufferId);
|
|
2509
|
+
}
|
|
2510
|
+
else if (cacheMode === "adsr") {
|
|
2511
|
+
return await this.getAdsrCachedBuffer(channel, note, audioBufferId);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
if (cacheMode === "none") {
|
|
2515
|
+
return await this.createAudioBuffer(note.voiceParams);
|
|
2516
|
+
}
|
|
2517
|
+
// fallback to ADS cache:
|
|
2518
|
+
// - "ads" (realtime or not)
|
|
2519
|
+
// - "adsr" + realtime
|
|
2520
|
+
// - "note" + realtime
|
|
2521
|
+
return await this.getAdsCachedBuffer(channel, note, audioBufferId, realtime);
|
|
2522
|
+
}
|
|
2523
|
+
async getAdsCachedBuffer(channel, note, audioBufferId, realtime) {
|
|
2524
|
+
const cacheKey = audioBufferId + (note.noteNumber << 1) + 1;
|
|
2525
|
+
const voiceParams = note.voiceParams;
|
|
1527
2526
|
if (realtime) {
|
|
1528
|
-
const
|
|
1529
|
-
if (
|
|
1530
|
-
return
|
|
1531
|
-
const
|
|
1532
|
-
this.
|
|
1533
|
-
|
|
2527
|
+
const cached = this.realtimeVoiceCache.get(cacheKey);
|
|
2528
|
+
if (cached)
|
|
2529
|
+
return cached;
|
|
2530
|
+
const rawBuffer = await this.createAudioBuffer(voiceParams);
|
|
2531
|
+
const rendered = await this.createAdsRenderedBuffer(channel, note, voiceParams, rawBuffer, channel.isDrum);
|
|
2532
|
+
this.realtimeVoiceCache.set(cacheKey, rendered);
|
|
2533
|
+
return rendered;
|
|
1534
2534
|
}
|
|
1535
2535
|
else {
|
|
1536
|
-
const cache = this.voiceCache.get(
|
|
2536
|
+
const cache = this.voiceCache.get(cacheKey);
|
|
1537
2537
|
if (cache) {
|
|
1538
2538
|
cache.counter += 1;
|
|
1539
2539
|
if (cache.maxCount <= cache.counter) {
|
|
1540
|
-
this.voiceCache.delete(
|
|
2540
|
+
this.voiceCache.delete(cacheKey);
|
|
1541
2541
|
}
|
|
1542
2542
|
return cache.audioBuffer;
|
|
1543
2543
|
}
|
|
1544
2544
|
else {
|
|
1545
|
-
const maxCount = this.voiceCounter.get(
|
|
1546
|
-
const
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
|
|
2545
|
+
const maxCount = this.voiceCounter.get(cacheKey) ?? 0;
|
|
2546
|
+
const rawBuffer = await this.createAudioBuffer(voiceParams);
|
|
2547
|
+
const rendered = await this.createAdsRenderedBuffer(channel, note, voiceParams, rawBuffer, channel.isDrum);
|
|
2548
|
+
const cache = { audioBuffer: rendered, maxCount, counter: 1 };
|
|
2549
|
+
this.voiceCache.set(cacheKey, cache);
|
|
2550
|
+
return rendered;
|
|
1550
2551
|
}
|
|
1551
2552
|
}
|
|
1552
2553
|
}
|
|
2554
|
+
async getAdsrCachedBuffer(channel, note, audioBufferId) {
|
|
2555
|
+
const voiceParams = note.voiceParams;
|
|
2556
|
+
const timelineIndex = note.timelineIndex;
|
|
2557
|
+
const noteEvent = this.noteOnEvents.get(timelineIndex);
|
|
2558
|
+
const noteDurationTicks = noteEvent?.durationTicks ?? 0;
|
|
2559
|
+
const safeTicks = noteDurationTicks === Infinity
|
|
2560
|
+
? 0xffffffffn
|
|
2561
|
+
: BigInt(noteDurationTicks);
|
|
2562
|
+
const volReleaseBits = f64ToBigInt(voiceParams.volRelease);
|
|
2563
|
+
const playbackRateBits = f64ToBigInt(voiceParams.playbackRate);
|
|
2564
|
+
const cacheKey = (BigInt(audioBufferId) << 160n) |
|
|
2565
|
+
(playbackRateBits << 96n) |
|
|
2566
|
+
(safeTicks << 64n) |
|
|
2567
|
+
volReleaseBits;
|
|
2568
|
+
let durationMap = this.adsrVoiceCache.get(audioBufferId);
|
|
2569
|
+
if (!durationMap) {
|
|
2570
|
+
durationMap = new Map();
|
|
2571
|
+
this.adsrVoiceCache.set(audioBufferId, durationMap);
|
|
2572
|
+
}
|
|
2573
|
+
const cached = durationMap.get(cacheKey);
|
|
2574
|
+
if (cached instanceof RenderedBuffer) {
|
|
2575
|
+
return cached;
|
|
2576
|
+
}
|
|
2577
|
+
if (cached instanceof Promise) {
|
|
2578
|
+
const buf = await cached;
|
|
2579
|
+
if (buf == null)
|
|
2580
|
+
return await this.createAudioBuffer(voiceParams);
|
|
2581
|
+
return buf;
|
|
2582
|
+
}
|
|
2583
|
+
const noteDuration = noteEvent?.duration ?? 0;
|
|
2584
|
+
const renderPromise = (async () => {
|
|
2585
|
+
try {
|
|
2586
|
+
const rawBuffer = await this.createAudioBuffer(voiceParams);
|
|
2587
|
+
const rendered = await this.createAdsrRenderedBuffer(channel, note, voiceParams, rawBuffer, noteDuration);
|
|
2588
|
+
durationMap.set(cacheKey, rendered);
|
|
2589
|
+
return rendered;
|
|
2590
|
+
}
|
|
2591
|
+
catch (err) {
|
|
2592
|
+
durationMap.delete(cacheKey);
|
|
2593
|
+
throw err;
|
|
2594
|
+
}
|
|
2595
|
+
})();
|
|
2596
|
+
durationMap.set(cacheKey, renderPromise);
|
|
2597
|
+
return await renderPromise;
|
|
2598
|
+
}
|
|
2599
|
+
async getFullCachedBuffer(note, audioBufferId) {
|
|
2600
|
+
const voiceParams = note.voiceParams;
|
|
2601
|
+
const timelineIndex = note.timelineIndex;
|
|
2602
|
+
const noteEvent = this.noteOnEvents.get(timelineIndex);
|
|
2603
|
+
const noteDuration = noteEvent?.duration ?? 0;
|
|
2604
|
+
const cacheKey = timelineIndex;
|
|
2605
|
+
let durationMap = this.fullVoiceCache.get(audioBufferId);
|
|
2606
|
+
if (!durationMap) {
|
|
2607
|
+
durationMap = new Map();
|
|
2608
|
+
this.fullVoiceCache.set(audioBufferId, durationMap);
|
|
2609
|
+
}
|
|
2610
|
+
const cached = durationMap.get(cacheKey);
|
|
2611
|
+
if (cached instanceof RenderedBuffer) {
|
|
2612
|
+
note.fullCacheVoiceId = audioBufferId;
|
|
2613
|
+
return cached;
|
|
2614
|
+
}
|
|
2615
|
+
if (cached instanceof Promise) {
|
|
2616
|
+
const buf = await cached;
|
|
2617
|
+
if (buf == null)
|
|
2618
|
+
return await this.createAudioBuffer(voiceParams);
|
|
2619
|
+
note.fullCacheVoiceId = audioBufferId;
|
|
2620
|
+
return buf;
|
|
2621
|
+
}
|
|
2622
|
+
const renderPromise = (async () => {
|
|
2623
|
+
try {
|
|
2624
|
+
const rendered = await this.createFullRenderedBuffer(this.channels[note.channel], note, voiceParams, noteDuration, noteEvent);
|
|
2625
|
+
durationMap.set(cacheKey, rendered);
|
|
2626
|
+
return rendered;
|
|
2627
|
+
}
|
|
2628
|
+
catch (err) {
|
|
2629
|
+
durationMap.delete(cacheKey);
|
|
2630
|
+
throw err;
|
|
2631
|
+
}
|
|
2632
|
+
})();
|
|
2633
|
+
durationMap.set(cacheKey, renderPromise);
|
|
2634
|
+
const rendered = await renderPromise;
|
|
2635
|
+
note.fullCacheVoiceId = audioBufferId;
|
|
2636
|
+
return rendered;
|
|
2637
|
+
}
|
|
1553
2638
|
async setNoteAudioNode(channel, note, realtime) {
|
|
1554
2639
|
const audioContext = this.audioContext;
|
|
1555
2640
|
const now = audioContext.currentTime;
|
|
@@ -1558,46 +2643,72 @@ class MidyGM2 extends EventTarget {
|
|
|
1558
2643
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
1559
2644
|
const voiceParams = note.voice.getAllParams(controllerState);
|
|
1560
2645
|
note.voiceParams = voiceParams;
|
|
1561
|
-
const audioBuffer = await this.getAudioBuffer(channel,
|
|
2646
|
+
const audioBuffer = await this.getAudioBuffer(channel, note, realtime);
|
|
2647
|
+
const isRendered = audioBuffer instanceof RenderedBuffer;
|
|
2648
|
+
note.renderedBuffer = isRendered ? audioBuffer : null;
|
|
1562
2649
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1563
|
-
note.
|
|
1564
|
-
note.
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2650
|
+
note.volumeNode = new GainNode(audioContext);
|
|
2651
|
+
note.volumeNode.gain.setValueAtTime(1, now);
|
|
2652
|
+
const cacheMode = this.cacheMode;
|
|
2653
|
+
const isFullCached = isRendered && audioBuffer.isFull === true;
|
|
2654
|
+
if (cacheMode === "none") {
|
|
2655
|
+
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
2656
|
+
note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
|
|
2657
|
+
type: "lowpass",
|
|
2658
|
+
Q: voiceParams.initialFilterQ / 10, // dB
|
|
2659
|
+
});
|
|
2660
|
+
const prevNote = channel.scheduledNotes.at(-1);
|
|
2661
|
+
if (prevNote && prevNote.noteNumber !== noteNumber) {
|
|
2662
|
+
note.portamentoNoteNumber = prevNote.noteNumber;
|
|
2663
|
+
}
|
|
2664
|
+
if (!channel.isDrum && this.isPortamento(channel, note)) {
|
|
2665
|
+
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
2666
|
+
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
2667
|
+
this.setPortamentoPitchEnvelope(channel, note, now);
|
|
2668
|
+
this.setPortamentoDetune(channel, note, now);
|
|
2669
|
+
}
|
|
2670
|
+
else {
|
|
2671
|
+
this.setVolumeEnvelope(channel, note, now);
|
|
2672
|
+
this.setFilterEnvelope(channel, note, now);
|
|
2673
|
+
this.setPitchEnvelope(note, now);
|
|
2674
|
+
this.setDetune(channel, note, now);
|
|
2675
|
+
}
|
|
2676
|
+
if (0 < state.vibratoDepth) {
|
|
2677
|
+
this.startVibrato(channel, note, now);
|
|
2678
|
+
}
|
|
2679
|
+
if (0 < state.modulationDepthMSB) {
|
|
2680
|
+
this.startModulation(channel, note, now);
|
|
2681
|
+
}
|
|
2682
|
+
if (channel.mono && channel.currentBufferSource) {
|
|
2683
|
+
channel.currentBufferSource.stop(startTime);
|
|
2684
|
+
channel.currentBufferSource = note.bufferSource;
|
|
2685
|
+
}
|
|
2686
|
+
note.bufferSource.connect(note.filterEnvelopeNode);
|
|
2687
|
+
note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
|
|
2688
|
+
note.volumeEnvelopeNode.connect(note.volumeNode);
|
|
2689
|
+
this.setChorusSend(channel, note, now);
|
|
2690
|
+
this.setReverbSend(channel, note, now);
|
|
2691
|
+
}
|
|
2692
|
+
else if (isFullCached) { // "note" mode
|
|
2693
|
+
note.volumeEnvelopeNode = null;
|
|
2694
|
+
note.filterEnvelopeNode = null;
|
|
2695
|
+
note.bufferSource.connect(note.volumeNode);
|
|
2696
|
+
this.setChorusSend(channel, note, now);
|
|
2697
|
+
this.setReverbSend(channel, note, now);
|
|
2698
|
+
}
|
|
2699
|
+
else { // "ads" / "asdr" mode
|
|
2700
|
+
note.volumeEnvelopeNode = null;
|
|
2701
|
+
note.filterEnvelopeNode = null;
|
|
1582
2702
|
this.setDetune(channel, note, now);
|
|
2703
|
+
if (0 < state.modulationDepthMSB) {
|
|
2704
|
+
this.startModulation(channel, note, now);
|
|
2705
|
+
}
|
|
2706
|
+
note.bufferSource.connect(note.volumeNode);
|
|
2707
|
+
this.setChorusSend(channel, note, now);
|
|
2708
|
+
this.setReverbSend(channel, note, now);
|
|
1583
2709
|
}
|
|
1584
|
-
if (0 < state.vibratoDepth) {
|
|
1585
|
-
this.startVibrato(channel, note, now);
|
|
1586
|
-
}
|
|
1587
|
-
if (0 < state.modulationDepthMSB) {
|
|
1588
|
-
this.startModulation(channel, note, now);
|
|
1589
|
-
}
|
|
1590
|
-
if (channel.mono && channel.currentBufferSource) {
|
|
1591
|
-
channel.currentBufferSource.stop(startTime);
|
|
1592
|
-
channel.currentBufferSource = note.bufferSource;
|
|
1593
|
-
}
|
|
1594
|
-
note.bufferSource.connect(note.filterEnvelopeNode);
|
|
1595
|
-
note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
|
|
1596
|
-
this.setChorusSend(channel, note, now);
|
|
1597
|
-
this.setReverbSend(channel, note, now);
|
|
1598
2710
|
if (voiceParams.sample.type === "compressed") {
|
|
1599
|
-
|
|
1600
|
-
note.bufferSource.start(startTime, offset);
|
|
2711
|
+
note.bufferSource.start(startTime);
|
|
1601
2712
|
}
|
|
1602
2713
|
else {
|
|
1603
2714
|
note.bufferSource.start(startTime);
|
|
@@ -1639,40 +2750,53 @@ class MidyGM2 extends EventTarget {
|
|
|
1639
2750
|
}
|
|
1640
2751
|
setNoteRouting(channelNumber, note, startTime) {
|
|
1641
2752
|
const channel = this.channels[channelNumber];
|
|
1642
|
-
const {
|
|
1643
|
-
if (
|
|
1644
|
-
|
|
1645
|
-
let gainL = keyBasedGainLs[noteNumber];
|
|
1646
|
-
let gainR = keyBasedGainRs[noteNumber];
|
|
1647
|
-
if (!gainL) {
|
|
1648
|
-
const audioNodes = this.createChannelAudioNodes(this.audioContext);
|
|
1649
|
-
gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
|
|
1650
|
-
gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
|
|
1651
|
-
}
|
|
1652
|
-
volumeEnvelopeNode.connect(gainL);
|
|
1653
|
-
volumeEnvelopeNode.connect(gainR);
|
|
2753
|
+
const { volumeNode } = note;
|
|
2754
|
+
if (note.renderedBuffer?.isFull) {
|
|
2755
|
+
volumeNode.connect(this.masterVolume);
|
|
1654
2756
|
}
|
|
1655
2757
|
else {
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
2758
|
+
if (channel.isDrum) {
|
|
2759
|
+
const noteNumber = note.noteNumber;
|
|
2760
|
+
const { keyBasedGainLs, keyBasedGainRs } = channel;
|
|
2761
|
+
let gainL = keyBasedGainLs[noteNumber];
|
|
2762
|
+
let gainR = keyBasedGainRs[noteNumber];
|
|
2763
|
+
if (!gainL) {
|
|
2764
|
+
const audioNodes = this.createChannelAudioNodes(this.audioContext);
|
|
2765
|
+
gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
|
|
2766
|
+
gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
|
|
2767
|
+
}
|
|
2768
|
+
volumeNode.connect(gainL);
|
|
2769
|
+
volumeNode.connect(gainR);
|
|
2770
|
+
}
|
|
2771
|
+
else {
|
|
2772
|
+
volumeNode.connect(channel.gainL);
|
|
2773
|
+
volumeNode.connect(channel.gainR);
|
|
2774
|
+
}
|
|
1661
2775
|
}
|
|
1662
2776
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1663
2777
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1664
2778
|
}
|
|
1665
2779
|
async noteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1668
|
-
|
|
2780
|
+
const note = this.createNote(channelNumber, noteNumber, velocity, startTime);
|
|
2781
|
+
return await this.setupNote(channelNumber, note, startTime);
|
|
2782
|
+
}
|
|
2783
|
+
createNote(channelNumber, noteNumber, velocity, startTime) {
|
|
2784
|
+
if (!(0 <= startTime))
|
|
1669
2785
|
startTime = this.audioContext.currentTime;
|
|
1670
2786
|
const note = new Note(noteNumber, velocity, startTime);
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
scheduledNotes.
|
|
2787
|
+
note.channel = channelNumber;
|
|
2788
|
+
const channel = this.channels[channelNumber];
|
|
2789
|
+
note.index = channel.scheduledNotes.length;
|
|
2790
|
+
channel.scheduledNotes.push(note);
|
|
2791
|
+
return note;
|
|
2792
|
+
}
|
|
2793
|
+
async setupNote(channelNumber, note, startTime) {
|
|
2794
|
+
const realtime = startTime === undefined;
|
|
2795
|
+
const channel = this.channels[channelNumber];
|
|
1674
2796
|
const programNumber = channel.programNumber;
|
|
1675
2797
|
const bankTable = this.soundFontTable[programNumber];
|
|
2798
|
+
if (!bankTable)
|
|
2799
|
+
return;
|
|
1676
2800
|
let bank = channel.isDrum ? 128 : channel.bankLSB;
|
|
1677
2801
|
if (bankTable[bank] === undefined) {
|
|
1678
2802
|
if (channel.isDrum)
|
|
@@ -1683,17 +2807,25 @@ class MidyGM2 extends EventTarget {
|
|
|
1683
2807
|
if (soundFontIndex === undefined)
|
|
1684
2808
|
return;
|
|
1685
2809
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
1686
|
-
note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
2810
|
+
note.voice = soundFont.getVoice(bank, programNumber, note.noteNumber, note.velocity);
|
|
1687
2811
|
if (!note.voice)
|
|
1688
2812
|
return;
|
|
1689
2813
|
await this.setNoteAudioNode(channel, note, realtime);
|
|
1690
2814
|
this.setNoteRouting(channelNumber, note, startTime);
|
|
1691
2815
|
note.resolveReady();
|
|
2816
|
+
if (0.5 <= channel.state.sustainPedal) {
|
|
2817
|
+
channel.sustainNotes.push(note);
|
|
2818
|
+
}
|
|
2819
|
+
if (0.5 <= channel.state.sostenutoPedal) {
|
|
2820
|
+
channel.sostenutoNotes.push(note);
|
|
2821
|
+
}
|
|
2822
|
+
return note;
|
|
1692
2823
|
}
|
|
1693
2824
|
disconnectNote(note) {
|
|
1694
2825
|
note.bufferSource.disconnect();
|
|
1695
|
-
note.filterEnvelopeNode
|
|
1696
|
-
note.volumeEnvelopeNode
|
|
2826
|
+
note.filterEnvelopeNode?.disconnect();
|
|
2827
|
+
note.volumeEnvelopeNode?.disconnect();
|
|
2828
|
+
note.volumeNode.disconnect();
|
|
1697
2829
|
if (note.modLfoToPitch) {
|
|
1698
2830
|
note.modLfoToVolume.disconnect();
|
|
1699
2831
|
note.modLfoToPitch.disconnect();
|
|
@@ -1710,16 +2842,112 @@ class MidyGM2 extends EventTarget {
|
|
|
1710
2842
|
note.chorusSend.disconnect();
|
|
1711
2843
|
}
|
|
1712
2844
|
}
|
|
2845
|
+
releaseFullCache(note) {
|
|
2846
|
+
if (note.timelineIndex == null || note.fullCacheVoiceId == null)
|
|
2847
|
+
return;
|
|
2848
|
+
const durationMap = this.fullVoiceCache.get(note.fullCacheVoiceId);
|
|
2849
|
+
if (!durationMap)
|
|
2850
|
+
return;
|
|
2851
|
+
const entry = durationMap.get(note.timelineIndex);
|
|
2852
|
+
if (entry instanceof RenderedBuffer) {
|
|
2853
|
+
durationMap.delete(note.timelineIndex);
|
|
2854
|
+
if (durationMap.size === 0) {
|
|
2855
|
+
this.fullVoiceCache.delete(note.fullCacheVoiceId);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
1713
2859
|
releaseNote(channel, note, endTime) {
|
|
1714
2860
|
endTime ??= this.audioContext.currentTime;
|
|
2861
|
+
if (note.renderedBuffer?.isFull) {
|
|
2862
|
+
const rb = note.renderedBuffer;
|
|
2863
|
+
const naturalEndTime = note.startTime + rb.buffer.duration;
|
|
2864
|
+
const noteOffTime = note.startTime + (rb.noteDuration ?? 0);
|
|
2865
|
+
const isEarlyCut = endTime < noteOffTime;
|
|
2866
|
+
if (isEarlyCut) {
|
|
2867
|
+
const volDuration = note.voiceParams.volRelease;
|
|
2868
|
+
const volRelease = endTime + volDuration;
|
|
2869
|
+
note.volumeNode.gain
|
|
2870
|
+
.cancelScheduledValues(endTime)
|
|
2871
|
+
.setValueAtTime(1, endTime)
|
|
2872
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
2873
|
+
return new Promise((resolve) => {
|
|
2874
|
+
this.scheduleTask(() => {
|
|
2875
|
+
note.bufferSource.loop = false;
|
|
2876
|
+
note.bufferSource.stop(volRelease);
|
|
2877
|
+
this.disconnectNote(note);
|
|
2878
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
2879
|
+
this.releaseFullCache(note);
|
|
2880
|
+
resolve();
|
|
2881
|
+
}, volRelease);
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
else {
|
|
2885
|
+
const now = this.audioContext.currentTime;
|
|
2886
|
+
if (naturalEndTime <= now) {
|
|
2887
|
+
this.disconnectNote(note);
|
|
2888
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
2889
|
+
this.releaseFullCache(note);
|
|
2890
|
+
return Promise.resolve();
|
|
2891
|
+
}
|
|
2892
|
+
return new Promise((resolve) => {
|
|
2893
|
+
this.scheduleTask(() => {
|
|
2894
|
+
this.disconnectNote(note);
|
|
2895
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
2896
|
+
this.releaseFullCache(note);
|
|
2897
|
+
resolve();
|
|
2898
|
+
}, naturalEndTime);
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
1715
2902
|
const volDuration = note.voiceParams.volRelease;
|
|
1716
2903
|
const volRelease = endTime + volDuration;
|
|
1717
|
-
note.
|
|
1718
|
-
.
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
.
|
|
1722
|
-
|
|
2904
|
+
if (note.volumeEnvelopeNode) { // "none" mode
|
|
2905
|
+
note.filterEnvelopeNode.frequency
|
|
2906
|
+
.cancelScheduledValues(endTime)
|
|
2907
|
+
.setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
|
|
2908
|
+
note.volumeEnvelopeNode.gain
|
|
2909
|
+
.cancelScheduledValues(endTime)
|
|
2910
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
2911
|
+
}
|
|
2912
|
+
else { // "ads" / "adsr" mode
|
|
2913
|
+
const isAdsr = note.renderedBuffer?.releaseDuration != null &&
|
|
2914
|
+
!note.renderedBuffer.isFull;
|
|
2915
|
+
if (isAdsr) {
|
|
2916
|
+
const rb = note.renderedBuffer;
|
|
2917
|
+
const naturalEndTime = note.startTime + rb.buffer.duration;
|
|
2918
|
+
const noteOffTime = note.startTime + (rb.noteDuration ?? 0);
|
|
2919
|
+
const isEarlyCut = endTime < noteOffTime;
|
|
2920
|
+
if (isEarlyCut) {
|
|
2921
|
+
const volRelease = endTime + volDuration;
|
|
2922
|
+
note.volumeNode.gain
|
|
2923
|
+
.cancelScheduledValues(endTime)
|
|
2924
|
+
.setValueAtTime(1, endTime)
|
|
2925
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
2926
|
+
return new Promise((resolve) => {
|
|
2927
|
+
this.scheduleTask(() => {
|
|
2928
|
+
note.bufferSource.stop(volRelease);
|
|
2929
|
+
this.disconnectNote(note);
|
|
2930
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
2931
|
+
resolve();
|
|
2932
|
+
}, volRelease);
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
else {
|
|
2936
|
+
return new Promise((resolve) => {
|
|
2937
|
+
this.scheduleTask(() => {
|
|
2938
|
+
note.bufferSource.stop();
|
|
2939
|
+
this.disconnectNote(note);
|
|
2940
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
2941
|
+
resolve();
|
|
2942
|
+
}, naturalEndTime);
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
note.volumeNode.gain
|
|
2947
|
+
.cancelScheduledValues(endTime)
|
|
2948
|
+
.setValueAtTime(1, endTime)
|
|
2949
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
2950
|
+
}
|
|
1723
2951
|
return new Promise((resolve) => {
|
|
1724
2952
|
this.scheduleTask(() => {
|
|
1725
2953
|
const bufferSource = note.bufferSource;
|
|
@@ -1938,7 +3166,7 @@ class MidyGM2 extends EventTarget {
|
|
|
1938
3166
|
if (!note.reverbSend) {
|
|
1939
3167
|
if (0 < value) {
|
|
1940
3168
|
note.reverbSend = new GainNode(this.audioContext, { gain: value });
|
|
1941
|
-
note.
|
|
3169
|
+
note.volumeNode.connect(note.reverbSend);
|
|
1942
3170
|
note.reverbSend.connect(this.reverbEffect.input);
|
|
1943
3171
|
}
|
|
1944
3172
|
}
|
|
@@ -1947,11 +3175,11 @@ class MidyGM2 extends EventTarget {
|
|
|
1947
3175
|
.cancelScheduledValues(scheduleTime)
|
|
1948
3176
|
.setValueAtTime(value, scheduleTime);
|
|
1949
3177
|
if (0 < value) {
|
|
1950
|
-
note.
|
|
3178
|
+
note.volumeNode.connect(note.reverbSend);
|
|
1951
3179
|
}
|
|
1952
3180
|
else {
|
|
1953
3181
|
try {
|
|
1954
|
-
note.
|
|
3182
|
+
note.volumeNode.disconnect(note.reverbSend);
|
|
1955
3183
|
}
|
|
1956
3184
|
catch { /* empty */ }
|
|
1957
3185
|
}
|
|
@@ -1968,7 +3196,7 @@ class MidyGM2 extends EventTarget {
|
|
|
1968
3196
|
if (!note.chorusSend) {
|
|
1969
3197
|
if (0 < value) {
|
|
1970
3198
|
note.chorusSend = new GainNode(this.audioContext, { gain: value });
|
|
1971
|
-
note.
|
|
3199
|
+
note.volumeNode.connect(note.chorusSend);
|
|
1972
3200
|
note.chorusSend.connect(this.chorusEffect.input);
|
|
1973
3201
|
}
|
|
1974
3202
|
}
|
|
@@ -1977,11 +3205,11 @@ class MidyGM2 extends EventTarget {
|
|
|
1977
3205
|
.cancelScheduledValues(scheduleTime)
|
|
1978
3206
|
.setValueAtTime(value, scheduleTime);
|
|
1979
3207
|
if (0 < value) {
|
|
1980
|
-
note.
|
|
3208
|
+
note.volumeNode.connect(note.chorusSend);
|
|
1981
3209
|
}
|
|
1982
3210
|
else {
|
|
1983
3211
|
try {
|
|
1984
|
-
note.
|
|
3212
|
+
note.volumeNode.disconnect(note.chorusSend);
|
|
1985
3213
|
}
|
|
1986
3214
|
catch { /* empty */ }
|
|
1987
3215
|
}
|
|
@@ -2044,7 +3272,7 @@ class MidyGM2 extends EventTarget {
|
|
|
2044
3272
|
reverbEffectsSend: (channel, note, scheduleTime) => {
|
|
2045
3273
|
this.setReverbSend(channel, note, scheduleTime);
|
|
2046
3274
|
},
|
|
2047
|
-
delayModLFO: (
|
|
3275
|
+
delayModLFO: (channel, note, _scheduleTime) => {
|
|
2048
3276
|
if (0 < channel.state.modulationDepthMSB) {
|
|
2049
3277
|
this.setDelayModLFO(note);
|
|
2050
3278
|
}
|
|
@@ -2079,11 +3307,12 @@ class MidyGM2 extends EventTarget {
|
|
|
2079
3307
|
state.set(channel.state.array);
|
|
2080
3308
|
state[2] = velocity / 127;
|
|
2081
3309
|
state[3] = noteNumber / 127;
|
|
2082
|
-
state[13] = state.channelPressure / 127;
|
|
2083
3310
|
return state;
|
|
2084
3311
|
}
|
|
2085
3312
|
applyVoiceParams(channel, controllerType, scheduleTime) {
|
|
2086
3313
|
this.processScheduledNotes(channel, (note) => {
|
|
3314
|
+
if (note.renderedBuffer?.isFull)
|
|
3315
|
+
return;
|
|
2087
3316
|
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
2088
3317
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
2089
3318
|
let applyVolumeEnvelope = false;
|
|
@@ -2167,6 +3396,8 @@ class MidyGM2 extends EventTarget {
|
|
|
2167
3396
|
const depth = channel.state.modulationDepthMSB *
|
|
2168
3397
|
channel.modulationDepthRange;
|
|
2169
3398
|
this.processScheduledNotes(channel, (note) => {
|
|
3399
|
+
if (note.renderedBuffer?.isFull)
|
|
3400
|
+
return;
|
|
2170
3401
|
if (note.modLfoToPitch) {
|
|
2171
3402
|
note.modLfoToPitch.gain.setValueAtTime(depth, scheduleTime);
|
|
2172
3403
|
}
|
|
@@ -2303,11 +3534,15 @@ class MidyGM2 extends EventTarget {
|
|
|
2303
3534
|
return;
|
|
2304
3535
|
if (!(0 <= scheduleTime))
|
|
2305
3536
|
scheduleTime = this.audioContext.currentTime;
|
|
2306
|
-
|
|
3537
|
+
const state = channel.state;
|
|
3538
|
+
const prevValue = state.sustainPedal;
|
|
3539
|
+
state.sustainPedal = value / 127;
|
|
2307
3540
|
if (64 <= value) {
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
3541
|
+
if (prevValue < 0.5) {
|
|
3542
|
+
this.processScheduledNotes(channel, (note) => {
|
|
3543
|
+
channel.sustainNotes.push(note);
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
2311
3546
|
}
|
|
2312
3547
|
else {
|
|
2313
3548
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
@@ -2331,13 +3566,17 @@ class MidyGM2 extends EventTarget {
|
|
|
2331
3566
|
return;
|
|
2332
3567
|
if (!(0 <= scheduleTime))
|
|
2333
3568
|
scheduleTime = this.audioContext.currentTime;
|
|
2334
|
-
|
|
3569
|
+
const state = channel.state;
|
|
3570
|
+
const prevValue = state.sostenutoPedal;
|
|
3571
|
+
state.sostenutoPedal = value / 127;
|
|
2335
3572
|
if (64 <= value) {
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
3573
|
+
if (prevValue < 0.5) {
|
|
3574
|
+
const sostenutoNotes = [];
|
|
3575
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3576
|
+
sostenutoNotes.push(note);
|
|
3577
|
+
});
|
|
3578
|
+
channel.sostenutoNotes = sostenutoNotes;
|
|
3579
|
+
}
|
|
2341
3580
|
}
|
|
2342
3581
|
else {
|
|
2343
3582
|
this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
|
|
@@ -2533,10 +3772,8 @@ class MidyGM2 extends EventTarget {
|
|
|
2533
3772
|
state[key] = defaultValue;
|
|
2534
3773
|
}
|
|
2535
3774
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
}
|
|
2539
|
-
this.resetChannelTable(channel);
|
|
3775
|
+
channel.resetSettings(this.constructor.channelSettings);
|
|
3776
|
+
channel.resetTable();
|
|
2540
3777
|
this.mode = "GM2";
|
|
2541
3778
|
this.masterFineTuning = 0; // cent
|
|
2542
3779
|
this.masterCoarseTuning = 0; // cent
|
|
@@ -2674,7 +3911,7 @@ class MidyGM2 extends EventTarget {
|
|
|
2674
3911
|
case 9:
|
|
2675
3912
|
switch (data[3]) {
|
|
2676
3913
|
case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
2677
|
-
return this.handleChannelPressureSysEx(data,
|
|
3914
|
+
return this.handleChannelPressureSysEx(data, scheduleTime);
|
|
2678
3915
|
case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
2679
3916
|
return this.handleControlChangeSysEx(data, scheduleTime);
|
|
2680
3917
|
default:
|
|
@@ -2999,6 +4236,9 @@ class MidyGM2 extends EventTarget {
|
|
|
2999
4236
|
getChannelAmplitudeControl(channel) {
|
|
3000
4237
|
return this.calcChannelEffectValue(channel, 2);
|
|
3001
4238
|
}
|
|
4239
|
+
getAmplitudeControl(channel) {
|
|
4240
|
+
return this.calcEffectValue(channel, 2);
|
|
4241
|
+
}
|
|
3002
4242
|
getLFOPitchDepth(channel) {
|
|
3003
4243
|
return this.calcEffectValue(channel, 3);
|
|
3004
4244
|
}
|
|
@@ -3026,7 +4266,7 @@ class MidyGM2 extends EventTarget {
|
|
|
3026
4266
|
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
3027
4267
|
}
|
|
3028
4268
|
};
|
|
3029
|
-
handlers[2] = (channel,
|
|
4269
|
+
handlers[2] = (channel, _note, scheduleTime) => this.applyVolume(channel, scheduleTime);
|
|
3030
4270
|
handlers[3] = (channel, note, scheduleTime) => this.setModLfoToPitch(channel, note, scheduleTime);
|
|
3031
4271
|
handlers[4] = (channel, note, scheduleTime) => this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
3032
4272
|
handlers[5] = (channel, note, scheduleTime) => this.setModLfoToVolume(channel, note, scheduleTime);
|