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