@marmooo/midy 0.3.3 → 0.3.5
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 +19 -11
- package/esm/midy-GM1.d.ts +14 -11
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +159 -131
- package/esm/midy-GM2.d.ts +28 -42
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +309 -297
- package/esm/midy-GMLite.d.ts +15 -11
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +158 -132
- package/esm/midy.d.ts +30 -43
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +348 -315
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +14 -11
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +159 -131
- package/script/midy-GM2.d.ts +28 -42
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +309 -297
- package/script/midy-GMLite.d.ts +15 -11
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +158 -132
- package/script/midy.d.ts +30 -43
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +348 -315
package/script/midy-GM2.js
CHANGED
|
@@ -47,24 +47,6 @@ class Note {
|
|
|
47
47
|
writable: true,
|
|
48
48
|
value: void 0
|
|
49
49
|
});
|
|
50
|
-
Object.defineProperty(this, "volumeNode", {
|
|
51
|
-
enumerable: true,
|
|
52
|
-
configurable: true,
|
|
53
|
-
writable: true,
|
|
54
|
-
value: void 0
|
|
55
|
-
});
|
|
56
|
-
Object.defineProperty(this, "gainL", {
|
|
57
|
-
enumerable: true,
|
|
58
|
-
configurable: true,
|
|
59
|
-
writable: true,
|
|
60
|
-
value: void 0
|
|
61
|
-
});
|
|
62
|
-
Object.defineProperty(this, "gainR", {
|
|
63
|
-
enumerable: true,
|
|
64
|
-
configurable: true,
|
|
65
|
-
writable: true,
|
|
66
|
-
value: void 0
|
|
67
|
-
});
|
|
68
50
|
Object.defineProperty(this, "modulationLFO", {
|
|
69
51
|
enumerable: true,
|
|
70
52
|
configurable: true,
|
|
@@ -202,6 +184,16 @@ class ControllerState {
|
|
|
202
184
|
}
|
|
203
185
|
}
|
|
204
186
|
}
|
|
187
|
+
const volumeEnvelopeKeys = [
|
|
188
|
+
"volDelay",
|
|
189
|
+
"volAttack",
|
|
190
|
+
"volHold",
|
|
191
|
+
"volDecay",
|
|
192
|
+
"volSustain",
|
|
193
|
+
"volRelease",
|
|
194
|
+
"initialAttenuation",
|
|
195
|
+
];
|
|
196
|
+
const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
|
|
205
197
|
const filterEnvelopeKeys = [
|
|
206
198
|
"modEnvToPitch",
|
|
207
199
|
"initialFilterFc",
|
|
@@ -211,22 +203,20 @@ const filterEnvelopeKeys = [
|
|
|
211
203
|
"modHold",
|
|
212
204
|
"modDecay",
|
|
213
205
|
"modSustain",
|
|
214
|
-
"modRelease",
|
|
215
|
-
"playbackRate",
|
|
216
206
|
];
|
|
217
207
|
const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
|
|
218
|
-
const
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
"
|
|
225
|
-
"
|
|
208
|
+
const pitchEnvelopeKeys = [
|
|
209
|
+
"modEnvToPitch",
|
|
210
|
+
"modDelay",
|
|
211
|
+
"modAttack",
|
|
212
|
+
"modHold",
|
|
213
|
+
"modDecay",
|
|
214
|
+
"modSustain",
|
|
215
|
+
"playbackRate",
|
|
226
216
|
];
|
|
227
|
-
const
|
|
217
|
+
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
228
218
|
class MidyGM2 {
|
|
229
|
-
constructor(audioContext
|
|
219
|
+
constructor(audioContext) {
|
|
230
220
|
Object.defineProperty(this, "mode", {
|
|
231
221
|
enumerable: true,
|
|
232
222
|
configurable: true,
|
|
@@ -250,6 +240,7 @@ class MidyGM2 {
|
|
|
250
240
|
configurable: true,
|
|
251
241
|
writable: true,
|
|
252
242
|
value: {
|
|
243
|
+
algorithm: "SchroederReverb",
|
|
253
244
|
time: this.getReverbTime(64),
|
|
254
245
|
feedback: 0.8,
|
|
255
246
|
}
|
|
@@ -326,13 +317,13 @@ class MidyGM2 {
|
|
|
326
317
|
writable: true,
|
|
327
318
|
value: this.initSoundFontTable()
|
|
328
319
|
});
|
|
329
|
-
Object.defineProperty(this, "
|
|
320
|
+
Object.defineProperty(this, "voiceCounter", {
|
|
330
321
|
enumerable: true,
|
|
331
322
|
configurable: true,
|
|
332
323
|
writable: true,
|
|
333
324
|
value: new Map()
|
|
334
325
|
});
|
|
335
|
-
Object.defineProperty(this, "
|
|
326
|
+
Object.defineProperty(this, "voiceCache", {
|
|
336
327
|
enumerable: true,
|
|
337
328
|
configurable: true,
|
|
338
329
|
writable: true,
|
|
@@ -398,30 +389,7 @@ class MidyGM2 {
|
|
|
398
389
|
writable: true,
|
|
399
390
|
value: new Array(this.numChannels * drumExclusiveClassCount)
|
|
400
391
|
});
|
|
401
|
-
Object.defineProperty(this, "defaultOptions", {
|
|
402
|
-
enumerable: true,
|
|
403
|
-
configurable: true,
|
|
404
|
-
writable: true,
|
|
405
|
-
value: {
|
|
406
|
-
reverbAlgorithm: (audioContext) => {
|
|
407
|
-
const { time: rt60, feedback } = this.reverb;
|
|
408
|
-
// const delay = this.calcDelay(rt60, feedback);
|
|
409
|
-
// const impulse = this.createConvolutionReverbImpulse(
|
|
410
|
-
// audioContext,
|
|
411
|
-
// rt60,
|
|
412
|
-
// delay,
|
|
413
|
-
// );
|
|
414
|
-
// return this.createConvolutionReverb(audioContext, impulse);
|
|
415
|
-
const combFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
416
|
-
const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
|
|
417
|
-
const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
418
|
-
const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
|
|
419
|
-
return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
|
|
420
|
-
},
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
392
|
this.audioContext = audioContext;
|
|
424
|
-
this.options = { ...this.defaultOptions, ...options };
|
|
425
393
|
this.masterVolume = new GainNode(audioContext);
|
|
426
394
|
this.scheduler = new GainNode(audioContext, { gain: 0 });
|
|
427
395
|
this.schedulerBuffer = new AudioBuffer({
|
|
@@ -431,7 +399,7 @@ class MidyGM2 {
|
|
|
431
399
|
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
432
400
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
433
401
|
this.channels = this.createChannels(audioContext);
|
|
434
|
-
this.reverbEffect = this.
|
|
402
|
+
this.reverbEffect = this.createReverbEffect(audioContext);
|
|
435
403
|
this.chorusEffect = this.createChorusEffect(audioContext);
|
|
436
404
|
this.chorusEffect.output.connect(this.masterVolume);
|
|
437
405
|
this.reverbEffect.output.connect(this.masterVolume);
|
|
@@ -452,13 +420,11 @@ class MidyGM2 {
|
|
|
452
420
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
453
421
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
454
422
|
const presetHeader = presetHeaders[i];
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
banks.set(presetHeader.bank, index);
|
|
458
|
-
}
|
|
423
|
+
const banks = this.soundFontTable[presetHeader.preset];
|
|
424
|
+
banks.set(presetHeader.bank, index);
|
|
459
425
|
}
|
|
460
426
|
}
|
|
461
|
-
async
|
|
427
|
+
async toUint8Array(input) {
|
|
462
428
|
let uint8Array;
|
|
463
429
|
if (typeof input === "string") {
|
|
464
430
|
const response = await fetch(input);
|
|
@@ -471,23 +437,32 @@ class MidyGM2 {
|
|
|
471
437
|
else {
|
|
472
438
|
throw new TypeError("input must be a URL string or Uint8Array");
|
|
473
439
|
}
|
|
474
|
-
|
|
475
|
-
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
476
|
-
this.addSoundFont(soundFont);
|
|
440
|
+
return uint8Array;
|
|
477
441
|
}
|
|
478
|
-
async
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
442
|
+
async loadSoundFont(input) {
|
|
443
|
+
this.voiceCounter.clear();
|
|
444
|
+
if (Array.isArray(input)) {
|
|
445
|
+
const promises = new Array(input.length);
|
|
446
|
+
for (let i = 0; i < input.length; i++) {
|
|
447
|
+
promises[i] = this.toUint8Array(input[i]);
|
|
448
|
+
}
|
|
449
|
+
const uint8Arrays = await Promise.all(promises);
|
|
450
|
+
for (let i = 0; i < uint8Arrays.length; i++) {
|
|
451
|
+
const parsed = (0, soundfont_parser_1.parse)(uint8Arrays[i]);
|
|
452
|
+
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
453
|
+
this.addSoundFont(soundFont);
|
|
454
|
+
}
|
|
487
455
|
}
|
|
488
456
|
else {
|
|
489
|
-
|
|
457
|
+
const uint8Array = await this.toUint8Array(input);
|
|
458
|
+
const parsed = (0, soundfont_parser_1.parse)(uint8Array);
|
|
459
|
+
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
460
|
+
this.addSoundFont(soundFont);
|
|
490
461
|
}
|
|
462
|
+
}
|
|
463
|
+
async loadMIDI(input) {
|
|
464
|
+
this.voiceCounter.clear();
|
|
465
|
+
const uint8Array = await this.toUint8Array(input);
|
|
491
466
|
const midi = (0, midi_file_1.parseMidi)(uint8Array);
|
|
492
467
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
493
468
|
const midiData = this.extractMidiData(midi);
|
|
@@ -495,7 +470,46 @@ class MidyGM2 {
|
|
|
495
470
|
this.timeline = midiData.timeline;
|
|
496
471
|
this.totalTime = this.calcTotalTime();
|
|
497
472
|
}
|
|
498
|
-
|
|
473
|
+
cacheVoiceIds() {
|
|
474
|
+
const timeline = this.timeline;
|
|
475
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
476
|
+
const event = timeline[i];
|
|
477
|
+
switch (event.type) {
|
|
478
|
+
case "noteOn": {
|
|
479
|
+
const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
|
|
480
|
+
this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
case "controller":
|
|
484
|
+
if (event.controllerType === 0) {
|
|
485
|
+
this.setBankMSB(event.channel, event.value);
|
|
486
|
+
}
|
|
487
|
+
else if (event.controllerType === 32) {
|
|
488
|
+
this.setBankLSB(event.channel, event.value);
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
case "programChange":
|
|
492
|
+
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
for (const [audioBufferId, count] of this.voiceCounter) {
|
|
496
|
+
if (count === 1)
|
|
497
|
+
this.voiceCounter.delete(audioBufferId);
|
|
498
|
+
}
|
|
499
|
+
this.GM2SystemOn();
|
|
500
|
+
}
|
|
501
|
+
getVoiceId(channel, noteNumber, velocity) {
|
|
502
|
+
const bankNumber = this.calcBank(channel);
|
|
503
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
504
|
+
.get(bankNumber);
|
|
505
|
+
if (soundFontIndex === undefined)
|
|
506
|
+
return;
|
|
507
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
508
|
+
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
509
|
+
const { instrument, sampleID } = voice.generators;
|
|
510
|
+
return `${soundFontIndex}:${instrument}:${sampleID}`;
|
|
511
|
+
}
|
|
512
|
+
createChannelAudioNodes(audioContext) {
|
|
499
513
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
500
514
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
501
515
|
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
@@ -510,10 +524,9 @@ class MidyGM2 {
|
|
|
510
524
|
};
|
|
511
525
|
}
|
|
512
526
|
resetChannelTable(channel) {
|
|
513
|
-
|
|
527
|
+
channel.controlTable.fill(-1);
|
|
514
528
|
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
515
|
-
channel.channelPressureTable.
|
|
516
|
-
channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
|
|
529
|
+
channel.channelPressureTable.fill(-1);
|
|
517
530
|
channel.keyBasedInstrumentControlTable.fill(-1);
|
|
518
531
|
}
|
|
519
532
|
createChannels(audioContext) {
|
|
@@ -523,46 +536,26 @@ class MidyGM2 {
|
|
|
523
536
|
isDrum: false,
|
|
524
537
|
state: new ControllerState(),
|
|
525
538
|
...this.constructor.channelSettings,
|
|
526
|
-
...this.
|
|
539
|
+
...this.createChannelAudioNodes(audioContext),
|
|
527
540
|
scheduledNotes: [],
|
|
528
541
|
sustainNotes: [],
|
|
529
542
|
sostenutoNotes: [],
|
|
530
543
|
controlTable: this.initControlTable(),
|
|
531
544
|
scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
|
|
532
|
-
channelPressureTable: new
|
|
545
|
+
channelPressureTable: new Int8Array(6).fill(-1),
|
|
533
546
|
keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
|
|
547
|
+
keyBasedGainLs: new Array(128),
|
|
548
|
+
keyBasedGainRs: new Array(128),
|
|
534
549
|
};
|
|
535
550
|
});
|
|
536
551
|
return channels;
|
|
537
552
|
}
|
|
538
|
-
async
|
|
553
|
+
async createAudioBuffer(voiceParams) {
|
|
554
|
+
const sample = voiceParams.sample;
|
|
539
555
|
const sampleStart = voiceParams.start;
|
|
540
|
-
const sampleEnd =
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const start = sample.byteOffset + sampleStart;
|
|
544
|
-
const end = sample.byteOffset + sampleEnd;
|
|
545
|
-
const buffer = sample.buffer.slice(start, end);
|
|
546
|
-
const audioBuffer = await this.audioContext.decodeAudioData(buffer);
|
|
547
|
-
return audioBuffer;
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
const sample = voiceParams.sample;
|
|
551
|
-
const start = sample.byteOffset + sampleStart;
|
|
552
|
-
const end = sample.byteOffset + sampleEnd;
|
|
553
|
-
const buffer = sample.buffer.slice(start, end);
|
|
554
|
-
const audioBuffer = new AudioBuffer({
|
|
555
|
-
numberOfChannels: 1,
|
|
556
|
-
length: sample.length,
|
|
557
|
-
sampleRate: voiceParams.sampleRate,
|
|
558
|
-
});
|
|
559
|
-
const channelData = audioBuffer.getChannelData(0);
|
|
560
|
-
const int16Array = new Int16Array(buffer);
|
|
561
|
-
for (let i = 0; i < int16Array.length; i++) {
|
|
562
|
-
channelData[i] = int16Array[i] / 32768;
|
|
563
|
-
}
|
|
564
|
-
return audioBuffer;
|
|
565
|
-
}
|
|
556
|
+
const sampleEnd = sample.data.length + voiceParams.end;
|
|
557
|
+
const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
|
|
558
|
+
return audioBuffer;
|
|
566
559
|
}
|
|
567
560
|
isLoopDrum(channel, noteNumber) {
|
|
568
561
|
const programNumber = channel.programNumber;
|
|
@@ -572,10 +565,9 @@ class MidyGM2 {
|
|
|
572
565
|
createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
|
|
573
566
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
574
567
|
bufferSource.buffer = audioBuffer;
|
|
575
|
-
bufferSource.loop =
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
568
|
+
bufferSource.loop = channel.isDrum
|
|
569
|
+
? this.isLoopDrum(channel, noteNumber)
|
|
570
|
+
: (voiceParams.sampleModes % 2 !== 0);
|
|
579
571
|
if (bufferSource.loop) {
|
|
580
572
|
bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
581
573
|
bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
|
|
@@ -600,13 +592,13 @@ class MidyGM2 {
|
|
|
600
592
|
break;
|
|
601
593
|
}
|
|
602
594
|
case "controller":
|
|
603
|
-
this.
|
|
595
|
+
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
604
596
|
break;
|
|
605
597
|
case "programChange":
|
|
606
|
-
this.
|
|
598
|
+
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
607
599
|
break;
|
|
608
600
|
case "channelAftertouch":
|
|
609
|
-
this.
|
|
601
|
+
this.setChannelPressure(event.channel, event.amount, startTime);
|
|
610
602
|
break;
|
|
611
603
|
case "pitchBend":
|
|
612
604
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
@@ -640,8 +632,9 @@ class MidyGM2 {
|
|
|
640
632
|
this.notePromises = [];
|
|
641
633
|
this.exclusiveClassNotes.fill(undefined);
|
|
642
634
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
643
|
-
this.
|
|
635
|
+
this.voiceCache.clear();
|
|
644
636
|
for (let i = 0; i < this.channels.length; i++) {
|
|
637
|
+
this.channels[i].scheduledNotes = [];
|
|
645
638
|
this.resetAllStates(i);
|
|
646
639
|
}
|
|
647
640
|
resolve();
|
|
@@ -663,8 +656,9 @@ class MidyGM2 {
|
|
|
663
656
|
this.notePromises = [];
|
|
664
657
|
this.exclusiveClassNotes.fill(undefined);
|
|
665
658
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
666
|
-
this.
|
|
659
|
+
this.voiceCache.clear();
|
|
667
660
|
for (let i = 0; i < this.channels.length; i++) {
|
|
661
|
+
this.channels[i].scheduledNotes = [];
|
|
668
662
|
this.resetAllStates(i);
|
|
669
663
|
}
|
|
670
664
|
this.isStopping = false;
|
|
@@ -697,11 +691,7 @@ class MidyGM2 {
|
|
|
697
691
|
secondToTicks(second, secondsPerBeat) {
|
|
698
692
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
699
693
|
}
|
|
700
|
-
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
701
|
-
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
702
|
-
}
|
|
703
694
|
extractMidiData(midi) {
|
|
704
|
-
this.audioBufferCounter.clear();
|
|
705
695
|
const instruments = new Set();
|
|
706
696
|
const timeline = [];
|
|
707
697
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -722,8 +712,6 @@ class MidyGM2 {
|
|
|
722
712
|
switch (event.type) {
|
|
723
713
|
case "noteOn": {
|
|
724
714
|
const channel = tmpChannels[event.channel];
|
|
725
|
-
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
726
|
-
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
727
715
|
if (channel.programNumber < 0) {
|
|
728
716
|
channel.programNumber = event.programNumber;
|
|
729
717
|
switch (channel.bankMSB) {
|
|
@@ -773,10 +761,6 @@ class MidyGM2 {
|
|
|
773
761
|
timeline.push(event);
|
|
774
762
|
}
|
|
775
763
|
}
|
|
776
|
-
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
777
|
-
if (count === 1)
|
|
778
|
-
this.audioBufferCounter.delete(audioBufferId);
|
|
779
|
-
}
|
|
780
764
|
const priority = {
|
|
781
765
|
controller: 0,
|
|
782
766
|
sysEx: 1,
|
|
@@ -821,7 +805,6 @@ class MidyGM2 {
|
|
|
821
805
|
this.notePromises.push(promise);
|
|
822
806
|
promises.push(promise);
|
|
823
807
|
});
|
|
824
|
-
channel.scheduledNotes = [];
|
|
825
808
|
return Promise.all(promises);
|
|
826
809
|
}
|
|
827
810
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -835,6 +818,8 @@ class MidyGM2 {
|
|
|
835
818
|
if (this.isPlaying || this.isPaused)
|
|
836
819
|
return;
|
|
837
820
|
this.resumeTime = 0;
|
|
821
|
+
if (this.voiceCounter.size === 0)
|
|
822
|
+
this.cacheVoiceIds();
|
|
838
823
|
await this.playNotes();
|
|
839
824
|
this.isPlaying = false;
|
|
840
825
|
}
|
|
@@ -877,7 +862,7 @@ class MidyGM2 {
|
|
|
877
862
|
}
|
|
878
863
|
processScheduledNotes(channel, callback) {
|
|
879
864
|
const scheduledNotes = channel.scheduledNotes;
|
|
880
|
-
for (let i =
|
|
865
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
881
866
|
const note = scheduledNotes[i];
|
|
882
867
|
if (!note)
|
|
883
868
|
continue;
|
|
@@ -888,14 +873,14 @@ class MidyGM2 {
|
|
|
888
873
|
}
|
|
889
874
|
processActiveNotes(channel, scheduleTime, callback) {
|
|
890
875
|
const scheduledNotes = channel.scheduledNotes;
|
|
891
|
-
for (let i =
|
|
876
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
892
877
|
const note = scheduledNotes[i];
|
|
893
878
|
if (!note)
|
|
894
879
|
continue;
|
|
895
880
|
if (note.ending)
|
|
896
881
|
continue;
|
|
897
882
|
if (scheduleTime < note.startTime)
|
|
898
|
-
|
|
883
|
+
break;
|
|
899
884
|
callback(note);
|
|
900
885
|
}
|
|
901
886
|
}
|
|
@@ -984,6 +969,22 @@ class MidyGM2 {
|
|
|
984
969
|
const output = allpasses.at(-1);
|
|
985
970
|
return { input, output };
|
|
986
971
|
}
|
|
972
|
+
createReverbEffect(audioContext) {
|
|
973
|
+
const { algorithm, time: rt60, feedback } = this.reverb;
|
|
974
|
+
switch (algorithm) {
|
|
975
|
+
case "ConvolutionReverb": {
|
|
976
|
+
const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
|
|
977
|
+
return this.createConvolutionReverb(audioContext, impulse);
|
|
978
|
+
}
|
|
979
|
+
case "SchroederReverb": {
|
|
980
|
+
const combFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
981
|
+
const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
|
|
982
|
+
const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
983
|
+
const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
|
|
984
|
+
return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
987
988
|
createChorusEffect(audioContext) {
|
|
988
989
|
const input = new GainNode(audioContext);
|
|
989
990
|
const output = new GainNode(audioContext);
|
|
@@ -1048,9 +1049,16 @@ class MidyGM2 {
|
|
|
1048
1049
|
const pitchWheel = channel.state.pitchWheel * 2 - 1;
|
|
1049
1050
|
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
|
|
1050
1051
|
const pitch = pitchWheel * pitchWheelSensitivity;
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1052
|
+
const channelPressureRaw = channel.channelPressureTable[0];
|
|
1053
|
+
if (0 <= channelPressureRaw) {
|
|
1054
|
+
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1055
|
+
const channelPressure = channelPressureDepth *
|
|
1056
|
+
channel.state.channelPressure;
|
|
1057
|
+
return tuning + pitch + channelPressure;
|
|
1058
|
+
}
|
|
1059
|
+
else {
|
|
1060
|
+
return tuning + pitch;
|
|
1061
|
+
}
|
|
1054
1062
|
}
|
|
1055
1063
|
calcNoteDetune(channel, note) {
|
|
1056
1064
|
return channel.scaleOctaveTuningTable[note.noteNumber % 12];
|
|
@@ -1277,35 +1285,32 @@ class MidyGM2 {
|
|
|
1277
1285
|
note.vibratoLFO.connect(note.vibratoDepth);
|
|
1278
1286
|
note.vibratoDepth.connect(note.bufferSource.detune);
|
|
1279
1287
|
}
|
|
1280
|
-
async getAudioBuffer(
|
|
1281
|
-
const audioBufferId = this.
|
|
1282
|
-
const cache = this.
|
|
1288
|
+
async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
|
|
1289
|
+
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
1290
|
+
const cache = this.voiceCache.get(audioBufferId);
|
|
1283
1291
|
if (cache) {
|
|
1284
1292
|
cache.counter += 1;
|
|
1285
1293
|
if (cache.maxCount <= cache.counter) {
|
|
1286
|
-
this.
|
|
1294
|
+
this.voiceCache.delete(audioBufferId);
|
|
1287
1295
|
}
|
|
1288
1296
|
return cache.audioBuffer;
|
|
1289
1297
|
}
|
|
1290
1298
|
else {
|
|
1291
|
-
const maxCount = this.
|
|
1292
|
-
const audioBuffer = await this.
|
|
1299
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
1300
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
1293
1301
|
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
1294
|
-
this.
|
|
1302
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
1295
1303
|
return audioBuffer;
|
|
1296
1304
|
}
|
|
1297
1305
|
}
|
|
1298
|
-
async createNote(channel, voice, noteNumber, velocity, startTime
|
|
1306
|
+
async createNote(channel, voice, noteNumber, velocity, startTime) {
|
|
1299
1307
|
const now = this.audioContext.currentTime;
|
|
1300
1308
|
const state = channel.state;
|
|
1301
1309
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
1302
1310
|
const voiceParams = voice.getAllParams(controllerState);
|
|
1303
1311
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
1304
|
-
const audioBuffer = await this.getAudioBuffer(channel
|
|
1312
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
1305
1313
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1306
|
-
note.volumeNode = new GainNode(this.audioContext);
|
|
1307
|
-
note.gainL = new GainNode(this.audioContext);
|
|
1308
|
-
note.gainR = new GainNode(this.audioContext);
|
|
1309
1314
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
1310
1315
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
1311
1316
|
type: "lowpass",
|
|
@@ -1338,9 +1343,6 @@ class MidyGM2 {
|
|
|
1338
1343
|
}
|
|
1339
1344
|
note.bufferSource.connect(note.filterNode);
|
|
1340
1345
|
note.filterNode.connect(note.volumeEnvelopeNode);
|
|
1341
|
-
note.volumeEnvelopeNode.connect(note.volumeNode);
|
|
1342
|
-
note.volumeNode.connect(note.gainL);
|
|
1343
|
-
note.volumeNode.connect(note.gainR);
|
|
1344
1346
|
if (0 < state.chorusSendLevel) {
|
|
1345
1347
|
this.setChorusEffectsSend(channel, note, 0, now);
|
|
1346
1348
|
}
|
|
@@ -1399,28 +1401,30 @@ class MidyGM2 {
|
|
|
1399
1401
|
}
|
|
1400
1402
|
this.drumExclusiveClassNotes[index] = note;
|
|
1401
1403
|
}
|
|
1402
|
-
|
|
1403
|
-
if (!channel.isDrum)
|
|
1404
|
-
return false;
|
|
1405
|
-
const programNumber = channel.programNumber;
|
|
1406
|
-
return !((programNumber === 48 && noteNumber === 88) ||
|
|
1407
|
-
(programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
|
|
1408
|
-
}
|
|
1409
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
1404
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
1410
1405
|
const channel = this.channels[channelNumber];
|
|
1411
1406
|
const bankNumber = this.calcBank(channel, channelNumber);
|
|
1412
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
1407
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
1408
|
+
.get(bankNumber);
|
|
1413
1409
|
if (soundFontIndex === undefined)
|
|
1414
1410
|
return;
|
|
1415
1411
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
1416
1412
|
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
1417
1413
|
if (!voice)
|
|
1418
1414
|
return;
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1415
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
1416
|
+
if (channel.isDrum) {
|
|
1417
|
+
const audioContext = this.audioContext;
|
|
1418
|
+
const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
|
|
1419
|
+
channel.keyBasedGainLs[noteNumber] = gainL;
|
|
1420
|
+
channel.keyBasedGainRs[noteNumber] = gainR;
|
|
1421
|
+
note.volumeEnvelopeNode.connect(gainL);
|
|
1422
|
+
note.volumeEnvelopeNode.connect(gainR);
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
1426
|
+
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
1427
|
+
}
|
|
1424
1428
|
if (0.5 <= channel.state.sustainPedal) {
|
|
1425
1429
|
channel.sustainNotes.push(note);
|
|
1426
1430
|
}
|
|
@@ -1438,9 +1442,6 @@ class MidyGM2 {
|
|
|
1438
1442
|
note.bufferSource.disconnect();
|
|
1439
1443
|
note.filterNode.disconnect();
|
|
1440
1444
|
note.volumeEnvelopeNode.disconnect();
|
|
1441
|
-
note.volumeNode.disconnect();
|
|
1442
|
-
note.gainL.disconnect();
|
|
1443
|
-
note.gainR.disconnect();
|
|
1444
1445
|
if (note.modulationDepth) {
|
|
1445
1446
|
note.volumeDepth.disconnect();
|
|
1446
1447
|
note.modulationDepth.disconnect();
|
|
@@ -1493,15 +1494,29 @@ class MidyGM2 {
|
|
|
1493
1494
|
return;
|
|
1494
1495
|
}
|
|
1495
1496
|
}
|
|
1496
|
-
const
|
|
1497
|
-
if (
|
|
1497
|
+
const index = this.findNoteOffIndex(channel, noteNumber);
|
|
1498
|
+
if (index < 0)
|
|
1498
1499
|
return;
|
|
1500
|
+
const note = channel.scheduledNotes[index];
|
|
1499
1501
|
note.ending = true;
|
|
1502
|
+
this.setNoteIndex(channel, index);
|
|
1500
1503
|
this.releaseNote(channel, note, endTime);
|
|
1501
1504
|
}
|
|
1502
|
-
|
|
1505
|
+
setNoteIndex(channel, index) {
|
|
1506
|
+
let allEnds = true;
|
|
1507
|
+
for (let i = channel.scheduleIndex; i < index; i++) {
|
|
1508
|
+
const note = channel.scheduledNotes[i];
|
|
1509
|
+
if (note && !note.ending) {
|
|
1510
|
+
allEnds = false;
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (allEnds)
|
|
1515
|
+
channel.scheduleIndex = index + 1;
|
|
1516
|
+
}
|
|
1517
|
+
findNoteOffIndex(channel, noteNumber) {
|
|
1503
1518
|
const scheduledNotes = channel.scheduledNotes;
|
|
1504
|
-
for (let i =
|
|
1519
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
1505
1520
|
const note = scheduledNotes[i];
|
|
1506
1521
|
if (!note)
|
|
1507
1522
|
continue;
|
|
@@ -1509,8 +1524,9 @@ class MidyGM2 {
|
|
|
1509
1524
|
continue;
|
|
1510
1525
|
if (note.noteNumber !== noteNumber)
|
|
1511
1526
|
continue;
|
|
1512
|
-
return
|
|
1527
|
+
return i;
|
|
1513
1528
|
}
|
|
1529
|
+
return -1;
|
|
1514
1530
|
}
|
|
1515
1531
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1516
1532
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -1550,18 +1566,18 @@ class MidyGM2 {
|
|
|
1550
1566
|
case 0x90:
|
|
1551
1567
|
return this.noteOn(channelNumber, data1, data2, scheduleTime);
|
|
1552
1568
|
case 0xB0:
|
|
1553
|
-
return this.
|
|
1569
|
+
return this.setControlChange(channelNumber, data1, data2, scheduleTime);
|
|
1554
1570
|
case 0xC0:
|
|
1555
|
-
return this.
|
|
1571
|
+
return this.setProgramChange(channelNumber, data1, scheduleTime);
|
|
1556
1572
|
case 0xD0:
|
|
1557
|
-
return this.
|
|
1573
|
+
return this.setChannelPressure(channelNumber, data1, scheduleTime);
|
|
1558
1574
|
case 0xE0:
|
|
1559
1575
|
return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
|
|
1560
1576
|
default:
|
|
1561
1577
|
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
1562
1578
|
}
|
|
1563
1579
|
}
|
|
1564
|
-
|
|
1580
|
+
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
1565
1581
|
const channel = this.channels[channelNumber];
|
|
1566
1582
|
channel.bank = channel.bankMSB * 128 + channel.bankLSB;
|
|
1567
1583
|
channel.programNumber = programNumber;
|
|
@@ -1576,16 +1592,17 @@ class MidyGM2 {
|
|
|
1576
1592
|
}
|
|
1577
1593
|
}
|
|
1578
1594
|
}
|
|
1579
|
-
|
|
1595
|
+
setChannelPressure(channelNumber, value, scheduleTime) {
|
|
1580
1596
|
const channel = this.channels[channelNumber];
|
|
1581
1597
|
if (channel.isDrum)
|
|
1582
1598
|
return;
|
|
1583
1599
|
const prev = channel.state.channelPressure;
|
|
1584
1600
|
const next = value / 127;
|
|
1585
1601
|
channel.state.channelPressure = next;
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1602
|
+
const channelPressureRaw = channel.channelPressureTable[0];
|
|
1603
|
+
if (0 <= channelPressureRaw) {
|
|
1604
|
+
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1605
|
+
channel.detune += channelPressureDepth * (next - prev);
|
|
1589
1606
|
}
|
|
1590
1607
|
const table = channel.channelPressureTable;
|
|
1591
1608
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
@@ -1645,10 +1662,12 @@ class MidyGM2 {
|
|
|
1645
1662
|
.setValueAtTime(volumeDepth, scheduleTime);
|
|
1646
1663
|
}
|
|
1647
1664
|
setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
|
|
1648
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
|
|
1649
1665
|
let value = note.voiceParams.reverbEffectsSend;
|
|
1650
|
-
if (
|
|
1651
|
-
|
|
1666
|
+
if (channel.isDrum) {
|
|
1667
|
+
const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
|
|
1668
|
+
if (0 <= keyBasedValue) {
|
|
1669
|
+
value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
|
|
1670
|
+
}
|
|
1652
1671
|
}
|
|
1653
1672
|
if (0 < prevValue) {
|
|
1654
1673
|
if (0 < value) {
|
|
@@ -1673,13 +1692,15 @@ class MidyGM2 {
|
|
|
1673
1692
|
}
|
|
1674
1693
|
}
|
|
1675
1694
|
setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
|
|
1676
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
|
|
1677
1695
|
let value = note.voiceParams.chorusEffectsSend;
|
|
1678
|
-
if (
|
|
1679
|
-
|
|
1696
|
+
if (channel.isDrum) {
|
|
1697
|
+
const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
|
|
1698
|
+
if (0 <= keyBasedValue) {
|
|
1699
|
+
value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
|
|
1700
|
+
}
|
|
1680
1701
|
}
|
|
1681
1702
|
if (0 < prevValue) {
|
|
1682
|
-
if (0 <
|
|
1703
|
+
if (0 < value) {
|
|
1683
1704
|
note.chorusEffectsSend.gain
|
|
1684
1705
|
.cancelScheduledValues(scheduleTime)
|
|
1685
1706
|
.setValueAtTime(value, scheduleTime);
|
|
@@ -1694,7 +1715,7 @@ class MidyGM2 {
|
|
|
1694
1715
|
note.chorusEffectsSend = new GainNode(this.audioContext, {
|
|
1695
1716
|
gain: value,
|
|
1696
1717
|
});
|
|
1697
|
-
note.
|
|
1718
|
+
note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
|
|
1698
1719
|
}
|
|
1699
1720
|
note.chorusEffectsSend.connect(this.chorusEffect.input);
|
|
1700
1721
|
}
|
|
@@ -1781,8 +1802,9 @@ class MidyGM2 {
|
|
|
1781
1802
|
this.processScheduledNotes(channel, (note) => {
|
|
1782
1803
|
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
1783
1804
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
1784
|
-
let
|
|
1785
|
-
let
|
|
1805
|
+
let applyVolumeEnvelope = false;
|
|
1806
|
+
let applyFilterEnvelope = false;
|
|
1807
|
+
let applyPitchEnvelope = false;
|
|
1786
1808
|
for (const [key, value] of Object.entries(voiceParams)) {
|
|
1787
1809
|
const prevValue = note.voiceParams[key];
|
|
1788
1810
|
if (value === prevValue)
|
|
@@ -1791,37 +1813,23 @@ class MidyGM2 {
|
|
|
1791
1813
|
if (key in this.voiceParamsHandlers) {
|
|
1792
1814
|
this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
|
|
1793
1815
|
}
|
|
1794
|
-
else
|
|
1795
|
-
if (
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
if (key in voiceParams)
|
|
1802
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1803
|
-
}
|
|
1804
|
-
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1805
|
-
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1806
|
-
}
|
|
1807
|
-
else {
|
|
1808
|
-
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
1809
|
-
}
|
|
1810
|
-
this.setPitchEnvelope(note, scheduleTime);
|
|
1811
|
-
}
|
|
1812
|
-
else if (volumeEnvelopeKeySet.has(key)) {
|
|
1813
|
-
if (appliedVolumeEnvelope)
|
|
1814
|
-
continue;
|
|
1815
|
-
appliedVolumeEnvelope = true;
|
|
1816
|
-
const noteVoiceParams = note.voiceParams;
|
|
1817
|
-
for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
|
|
1818
|
-
const key = volumeEnvelopeKeys[i];
|
|
1819
|
-
if (key in voiceParams)
|
|
1820
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1821
|
-
}
|
|
1822
|
-
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1816
|
+
else {
|
|
1817
|
+
if (volumeEnvelopeKeySet.has(key))
|
|
1818
|
+
applyVolumeEnvelope = true;
|
|
1819
|
+
if (filterEnvelopeKeySet.has(key))
|
|
1820
|
+
applyFilterEnvelope = true;
|
|
1821
|
+
if (pitchEnvelopeKeySet.has(key))
|
|
1822
|
+
applyPitchEnvelope = true;
|
|
1823
1823
|
}
|
|
1824
1824
|
}
|
|
1825
|
+
if (applyVolumeEnvelope) {
|
|
1826
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1827
|
+
}
|
|
1828
|
+
if (applyFilterEnvelope) {
|
|
1829
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
1830
|
+
}
|
|
1831
|
+
if (applyPitchEnvelope)
|
|
1832
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1825
1833
|
});
|
|
1826
1834
|
}
|
|
1827
1835
|
createControlChangeHandlers() {
|
|
@@ -1852,13 +1860,13 @@ class MidyGM2 {
|
|
|
1852
1860
|
handlers[127] = this.polyOn;
|
|
1853
1861
|
return handlers;
|
|
1854
1862
|
}
|
|
1855
|
-
|
|
1863
|
+
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1856
1864
|
const handler = this.controlChangeHandlers[controllerType];
|
|
1857
1865
|
if (handler) {
|
|
1858
1866
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
1859
1867
|
const channel = this.channels[channelNumber];
|
|
1860
1868
|
this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
|
|
1861
|
-
this.applyControlTable(channel, controllerType);
|
|
1869
|
+
this.applyControlTable(channel, controllerType, scheduleTime);
|
|
1862
1870
|
}
|
|
1863
1871
|
else {
|
|
1864
1872
|
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
@@ -1915,22 +1923,12 @@ class MidyGM2 {
|
|
|
1915
1923
|
return;
|
|
1916
1924
|
this.updatePortamento(channel, scheduleTime);
|
|
1917
1925
|
}
|
|
1918
|
-
setKeyBasedVolume(channel, scheduleTime) {
|
|
1919
|
-
this.processScheduledNotes(channel, (note) => {
|
|
1920
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
|
|
1921
|
-
if (0 <= keyBasedValue) {
|
|
1922
|
-
note.volumeNode.gain
|
|
1923
|
-
.cancelScheduledValues(scheduleTime)
|
|
1924
|
-
.setValueAtTime(keyBasedValue / 127, scheduleTime);
|
|
1925
|
-
}
|
|
1926
|
-
});
|
|
1927
|
-
}
|
|
1928
1926
|
setVolume(channelNumber, volume, scheduleTime) {
|
|
1929
1927
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1930
1928
|
const channel = this.channels[channelNumber];
|
|
1931
1929
|
channel.state.volume = volume / 127;
|
|
1932
1930
|
this.updateChannelVolume(channel, scheduleTime);
|
|
1933
|
-
this.
|
|
1931
|
+
this.updateKeyBasedVolume(channel, scheduleTime);
|
|
1934
1932
|
}
|
|
1935
1933
|
panToGain(pan) {
|
|
1936
1934
|
const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
|
|
@@ -1939,26 +1937,12 @@ class MidyGM2 {
|
|
|
1939
1937
|
gainRight: Math.sin(theta),
|
|
1940
1938
|
};
|
|
1941
1939
|
}
|
|
1942
|
-
setKeyBasedPan(channel, scheduleTime) {
|
|
1943
|
-
this.processScheduledNotes(channel, (note) => {
|
|
1944
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
|
|
1945
|
-
if (0 <= keyBasedValue) {
|
|
1946
|
-
const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
|
|
1947
|
-
note.gainL.gain
|
|
1948
|
-
.cancelScheduledValues(scheduleTime)
|
|
1949
|
-
.setValueAtTime(gainLeft, scheduleTime);
|
|
1950
|
-
note.gainR.gain
|
|
1951
|
-
.cancelScheduledValues(scheduleTime)
|
|
1952
|
-
.setValueAtTime(gainRight, scheduleTime);
|
|
1953
|
-
}
|
|
1954
|
-
});
|
|
1955
|
-
}
|
|
1956
1940
|
setPan(channelNumber, pan, scheduleTime) {
|
|
1957
1941
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1958
1942
|
const channel = this.channels[channelNumber];
|
|
1959
1943
|
channel.state.pan = pan / 127;
|
|
1960
1944
|
this.updateChannelVolume(channel, scheduleTime);
|
|
1961
|
-
this.
|
|
1945
|
+
this.updateKeyBasedVolume(channel, scheduleTime);
|
|
1962
1946
|
}
|
|
1963
1947
|
setExpression(channelNumber, expression, scheduleTime) {
|
|
1964
1948
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -1984,6 +1968,34 @@ class MidyGM2 {
|
|
|
1984
1968
|
.cancelScheduledValues(scheduleTime)
|
|
1985
1969
|
.setValueAtTime(volume * gainRight, scheduleTime);
|
|
1986
1970
|
}
|
|
1971
|
+
updateKeyBasedVolume(channel, scheduleTime) {
|
|
1972
|
+
if (!channel.isDrum)
|
|
1973
|
+
return;
|
|
1974
|
+
const state = channel.state;
|
|
1975
|
+
const defaultVolume = state.volume * state.expression;
|
|
1976
|
+
const defaultPan = state.pan;
|
|
1977
|
+
for (let i = 0; i < 128; i++) {
|
|
1978
|
+
const gainL = channel.keyBasedGainLs[i];
|
|
1979
|
+
const gainR = channel.keyBasedGainLs[i];
|
|
1980
|
+
if (!gainL)
|
|
1981
|
+
continue;
|
|
1982
|
+
if (!gainR)
|
|
1983
|
+
continue;
|
|
1984
|
+
const keyBasedVolume = this.getKeyBasedValue(channel, i, 7);
|
|
1985
|
+
const volume = (0 <= keyBasedVolume)
|
|
1986
|
+
? defaultVolume * keyBasedVolume / 64
|
|
1987
|
+
: defaultVolume;
|
|
1988
|
+
const keyBasedPan = this.getKeyBasedValue(channel, i, 10);
|
|
1989
|
+
const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
|
|
1990
|
+
const { gainLeft, gainRight } = this.panToGain(pan);
|
|
1991
|
+
gainL.gain
|
|
1992
|
+
.cancelScheduledValues(scheduleTime)
|
|
1993
|
+
.setValueAtTime(volume * gainLeft, scheduleTime);
|
|
1994
|
+
gainR.gain
|
|
1995
|
+
.cancelScheduledValues(scheduleTime)
|
|
1996
|
+
.setValueAtTime(volume * gainRight, scheduleTime);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1987
1999
|
setSustainPedal(channelNumber, value, scheduleTime) {
|
|
1988
2000
|
const channel = this.channels[channelNumber];
|
|
1989
2001
|
if (channel.isDrum)
|
|
@@ -2245,7 +2257,7 @@ class MidyGM2 {
|
|
|
2245
2257
|
const entries = Object.entries(defaultControllerState);
|
|
2246
2258
|
for (const [key, { type, defaultValue }] of entries) {
|
|
2247
2259
|
if (128 <= type) {
|
|
2248
|
-
this.
|
|
2260
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2249
2261
|
}
|
|
2250
2262
|
else {
|
|
2251
2263
|
state[key] = defaultValue;
|
|
@@ -2277,7 +2289,7 @@ class MidyGM2 {
|
|
|
2277
2289
|
const key = keys[i];
|
|
2278
2290
|
const { type, defaultValue } = defaultControllerState[key];
|
|
2279
2291
|
if (128 <= type) {
|
|
2280
|
-
this.
|
|
2292
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2281
2293
|
}
|
|
2282
2294
|
else {
|
|
2283
2295
|
state[key] = defaultValue;
|
|
@@ -2485,8 +2497,7 @@ class MidyGM2 {
|
|
|
2485
2497
|
setReverbType(type) {
|
|
2486
2498
|
this.reverb.time = this.getReverbTimeFromType(type);
|
|
2487
2499
|
this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
|
|
2488
|
-
|
|
2489
|
-
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2500
|
+
this.reverbEffect = this.createReverbEffect(this.audioContext);
|
|
2490
2501
|
}
|
|
2491
2502
|
getReverbTimeFromType(type) {
|
|
2492
2503
|
switch (type) {
|
|
@@ -2508,8 +2519,7 @@ class MidyGM2 {
|
|
|
2508
2519
|
}
|
|
2509
2520
|
setReverbTime(value) {
|
|
2510
2521
|
this.reverb.time = this.getReverbTime(value);
|
|
2511
|
-
|
|
2512
|
-
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2522
|
+
this.reverbEffect = this.createReverbEffect(this.audioContext);
|
|
2513
2523
|
}
|
|
2514
2524
|
getReverbTime(value) {
|
|
2515
2525
|
return Math.exp((value - 40) * 0.025);
|
|
@@ -2680,51 +2690,63 @@ class MidyGM2 {
|
|
|
2680
2690
|
}
|
|
2681
2691
|
}
|
|
2682
2692
|
getFilterCutoffControl(channel) {
|
|
2683
|
-
const
|
|
2684
|
-
|
|
2693
|
+
const channelPressureRaw = channel.channelPressureTable[1];
|
|
2694
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2695
|
+
? (channelPressureRaw - 64) * channel.state.channelPressure
|
|
2696
|
+
: 0;
|
|
2685
2697
|
return channelPressure * 15;
|
|
2686
2698
|
}
|
|
2687
2699
|
getAmplitudeControl(channel) {
|
|
2688
|
-
const
|
|
2689
|
-
|
|
2700
|
+
const channelPressureRaw = channel.channelPressureTable[2];
|
|
2701
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2702
|
+
? channelPressureRaw * channel.state.channelPressure
|
|
2703
|
+
: 0;
|
|
2690
2704
|
return channelPressure / 64;
|
|
2691
2705
|
}
|
|
2692
2706
|
getLFOPitchDepth(channel) {
|
|
2693
|
-
const
|
|
2694
|
-
|
|
2707
|
+
const channelPressureRaw = channel.channelPressureTable[3];
|
|
2708
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2709
|
+
? channelPressureRaw * channel.state.channelPressure
|
|
2710
|
+
: 0;
|
|
2695
2711
|
return channelPressure / 127 * 600;
|
|
2696
2712
|
}
|
|
2697
2713
|
getLFOFilterDepth(channel) {
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2714
|
+
const channelPressureRaw = channel.channelPressureTable[4];
|
|
2715
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2716
|
+
? channelPressureRaw * channel.state.channelPressure
|
|
2717
|
+
: 0;
|
|
2700
2718
|
return channelPressure / 127 * 2400;
|
|
2701
2719
|
}
|
|
2702
2720
|
getLFOAmplitudeDepth(channel) {
|
|
2703
|
-
const
|
|
2704
|
-
|
|
2721
|
+
const channelPressureRaw = channel.channelPressureTable[5];
|
|
2722
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2723
|
+
? channelPressureRaw * channel.state.channelPressure
|
|
2724
|
+
: 0;
|
|
2705
2725
|
return channelPressure / 127;
|
|
2706
2726
|
}
|
|
2707
|
-
setControllerParameters(channel, note, table) {
|
|
2708
|
-
if (table[0]
|
|
2709
|
-
this.updateDetune(channel, note);
|
|
2727
|
+
setControllerParameters(channel, note, table, scheduleTime) {
|
|
2728
|
+
if (0 <= table[0])
|
|
2729
|
+
this.updateDetune(channel, note, scueduleTime);
|
|
2710
2730
|
if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
|
|
2711
|
-
if (table[1]
|
|
2712
|
-
this.setPortamentoFilterEnvelope(channel, note);
|
|
2713
|
-
|
|
2714
|
-
|
|
2731
|
+
if (0 <= table[1]) {
|
|
2732
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2733
|
+
}
|
|
2734
|
+
if (0 <= table[2]) {
|
|
2735
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2736
|
+
}
|
|
2715
2737
|
}
|
|
2716
2738
|
else {
|
|
2717
|
-
if (table[1]
|
|
2718
|
-
this.setFilterEnvelope(channel, note);
|
|
2719
|
-
if (table[2]
|
|
2720
|
-
this.setVolumeEnvelope(channel, note);
|
|
2721
|
-
}
|
|
2722
|
-
if (table[3]
|
|
2723
|
-
this.setModLfoToPitch(channel, note);
|
|
2724
|
-
if (table[4]
|
|
2725
|
-
this.setModLfoToFilterFc(channel, note);
|
|
2726
|
-
if (table[5]
|
|
2727
|
-
this.setModLfoToVolume(channel, note);
|
|
2739
|
+
if (0 <= table[1])
|
|
2740
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
2741
|
+
if (0 <= table[2])
|
|
2742
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
2743
|
+
}
|
|
2744
|
+
if (0 <= table[3])
|
|
2745
|
+
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
2746
|
+
if (0 <= table[4])
|
|
2747
|
+
this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
2748
|
+
if (0 <= table[5])
|
|
2749
|
+
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
2728
2750
|
}
|
|
2729
2751
|
handlePressureSysEx(data, tableName) {
|
|
2730
2752
|
const channelNumber = data[4];
|
|
@@ -2739,27 +2761,16 @@ class MidyGM2 {
|
|
|
2739
2761
|
}
|
|
2740
2762
|
}
|
|
2741
2763
|
initControlTable() {
|
|
2742
|
-
const
|
|
2764
|
+
const ccCount = 128;
|
|
2743
2765
|
const slotSize = 6;
|
|
2744
|
-
|
|
2745
|
-
return this.resetControlTable(table);
|
|
2766
|
+
return new Int8Array(ccCount * slotSize).fill(-1);
|
|
2746
2767
|
}
|
|
2747
|
-
|
|
2748
|
-
const channelCount = 128;
|
|
2749
|
-
const slotSize = 6;
|
|
2750
|
-
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2751
|
-
for (let ch = 0; ch < channelCount; ch++) {
|
|
2752
|
-
const offset = ch * slotSize;
|
|
2753
|
-
table.set(defaultValues, offset);
|
|
2754
|
-
}
|
|
2755
|
-
return table;
|
|
2756
|
-
}
|
|
2757
|
-
applyControlTable(channel, controllerType) {
|
|
2768
|
+
applyControlTable(channel, controllerType, scheduleTime) {
|
|
2758
2769
|
const slotSize = 6;
|
|
2759
2770
|
const offset = controllerType * slotSize;
|
|
2760
2771
|
const table = channel.controlTable.subarray(offset, offset + slotSize);
|
|
2761
2772
|
this.processScheduledNotes(channel, (note) => {
|
|
2762
|
-
this.setControllerParameters(channel, note, table);
|
|
2773
|
+
this.setControllerParameters(channel, note, table, scheduleTime);
|
|
2763
2774
|
});
|
|
2764
2775
|
}
|
|
2765
2776
|
handleControlChangeSysEx(data) {
|
|
@@ -2775,7 +2786,7 @@ class MidyGM2 {
|
|
|
2775
2786
|
table[pp] = rr;
|
|
2776
2787
|
}
|
|
2777
2788
|
}
|
|
2778
|
-
|
|
2789
|
+
getKeyBasedValue(channel, keyNumber, controllerType) {
|
|
2779
2790
|
const index = keyNumber * 128 + controllerType;
|
|
2780
2791
|
const controlValue = channel.keyBasedInstrumentControlTable[index];
|
|
2781
2792
|
return controlValue;
|
|
@@ -2783,7 +2794,7 @@ class MidyGM2 {
|
|
|
2783
2794
|
handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
|
|
2784
2795
|
const channelNumber = data[4];
|
|
2785
2796
|
const channel = this.channels[channelNumber];
|
|
2786
|
-
if (channel.isDrum)
|
|
2797
|
+
if (!channel.isDrum)
|
|
2787
2798
|
return;
|
|
2788
2799
|
const keyNumber = data[5];
|
|
2789
2800
|
const table = channel.keyBasedInstrumentControlTable;
|
|
@@ -2793,7 +2804,7 @@ class MidyGM2 {
|
|
|
2793
2804
|
const index = keyNumber * 128 + controllerType;
|
|
2794
2805
|
table[index] = value;
|
|
2795
2806
|
}
|
|
2796
|
-
this.
|
|
2807
|
+
this.setChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
|
|
2797
2808
|
}
|
|
2798
2809
|
handleSysEx(data, scheduleTime) {
|
|
2799
2810
|
switch (data[0]) {
|
|
@@ -2831,6 +2842,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
|
|
|
2831
2842
|
configurable: true,
|
|
2832
2843
|
writable: true,
|
|
2833
2844
|
value: {
|
|
2845
|
+
scheduleIndex: 0,
|
|
2834
2846
|
detune: 0,
|
|
2835
2847
|
programNumber: 0,
|
|
2836
2848
|
bank: 121 * 128,
|