@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.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,
|
|
@@ -217,6 +199,16 @@ class ControllerState {
|
|
|
217
199
|
}
|
|
218
200
|
}
|
|
219
201
|
}
|
|
202
|
+
const volumeEnvelopeKeys = [
|
|
203
|
+
"volDelay",
|
|
204
|
+
"volAttack",
|
|
205
|
+
"volHold",
|
|
206
|
+
"volDecay",
|
|
207
|
+
"volSustain",
|
|
208
|
+
"volRelease",
|
|
209
|
+
"initialAttenuation",
|
|
210
|
+
];
|
|
211
|
+
const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
|
|
220
212
|
const filterEnvelopeKeys = [
|
|
221
213
|
"modEnvToPitch",
|
|
222
214
|
"initialFilterFc",
|
|
@@ -226,22 +218,20 @@ const filterEnvelopeKeys = [
|
|
|
226
218
|
"modHold",
|
|
227
219
|
"modDecay",
|
|
228
220
|
"modSustain",
|
|
229
|
-
"modRelease",
|
|
230
|
-
"playbackRate",
|
|
231
221
|
];
|
|
232
222
|
const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
|
|
233
|
-
const
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"
|
|
223
|
+
const pitchEnvelopeKeys = [
|
|
224
|
+
"modEnvToPitch",
|
|
225
|
+
"modDelay",
|
|
226
|
+
"modAttack",
|
|
227
|
+
"modHold",
|
|
228
|
+
"modDecay",
|
|
229
|
+
"modSustain",
|
|
230
|
+
"playbackRate",
|
|
241
231
|
];
|
|
242
|
-
const
|
|
232
|
+
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
243
233
|
class Midy {
|
|
244
|
-
constructor(audioContext
|
|
234
|
+
constructor(audioContext) {
|
|
245
235
|
Object.defineProperty(this, "mode", {
|
|
246
236
|
enumerable: true,
|
|
247
237
|
configurable: true,
|
|
@@ -265,6 +255,7 @@ class Midy {
|
|
|
265
255
|
configurable: true,
|
|
266
256
|
writable: true,
|
|
267
257
|
value: {
|
|
258
|
+
algorithm: "SchroederReverb",
|
|
268
259
|
time: this.getReverbTime(64),
|
|
269
260
|
feedback: 0.8,
|
|
270
261
|
}
|
|
@@ -341,13 +332,13 @@ class Midy {
|
|
|
341
332
|
writable: true,
|
|
342
333
|
value: this.initSoundFontTable()
|
|
343
334
|
});
|
|
344
|
-
Object.defineProperty(this, "
|
|
335
|
+
Object.defineProperty(this, "voiceCounter", {
|
|
345
336
|
enumerable: true,
|
|
346
337
|
configurable: true,
|
|
347
338
|
writable: true,
|
|
348
339
|
value: new Map()
|
|
349
340
|
});
|
|
350
|
-
Object.defineProperty(this, "
|
|
341
|
+
Object.defineProperty(this, "voiceCache", {
|
|
351
342
|
enumerable: true,
|
|
352
343
|
configurable: true,
|
|
353
344
|
writable: true,
|
|
@@ -413,30 +404,7 @@ class Midy {
|
|
|
413
404
|
writable: true,
|
|
414
405
|
value: new Array(this.numChannels * drumExclusiveClassCount)
|
|
415
406
|
});
|
|
416
|
-
Object.defineProperty(this, "defaultOptions", {
|
|
417
|
-
enumerable: true,
|
|
418
|
-
configurable: true,
|
|
419
|
-
writable: true,
|
|
420
|
-
value: {
|
|
421
|
-
reverbAlgorithm: (audioContext) => {
|
|
422
|
-
const { time: rt60, feedback } = this.reverb;
|
|
423
|
-
// const delay = this.calcDelay(rt60, feedback);
|
|
424
|
-
// const impulse = this.createConvolutionReverbImpulse(
|
|
425
|
-
// audioContext,
|
|
426
|
-
// rt60,
|
|
427
|
-
// delay,
|
|
428
|
-
// );
|
|
429
|
-
// return this.createConvolutionReverb(audioContext, impulse);
|
|
430
|
-
const combFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
431
|
-
const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
|
|
432
|
-
const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
433
|
-
const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
|
|
434
|
-
return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
|
|
435
|
-
},
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
407
|
this.audioContext = audioContext;
|
|
439
|
-
this.options = { ...this.defaultOptions, ...options };
|
|
440
408
|
this.masterVolume = new GainNode(audioContext);
|
|
441
409
|
this.scheduler = new GainNode(audioContext, { gain: 0 });
|
|
442
410
|
this.schedulerBuffer = new AudioBuffer({
|
|
@@ -446,7 +414,7 @@ class Midy {
|
|
|
446
414
|
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
447
415
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
448
416
|
this.channels = this.createChannels(audioContext);
|
|
449
|
-
this.reverbEffect = this.
|
|
417
|
+
this.reverbEffect = this.createReverbEffect(audioContext);
|
|
450
418
|
this.chorusEffect = this.createChorusEffect(audioContext);
|
|
451
419
|
this.chorusEffect.output.connect(this.masterVolume);
|
|
452
420
|
this.reverbEffect.output.connect(this.masterVolume);
|
|
@@ -467,13 +435,11 @@ class Midy {
|
|
|
467
435
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
468
436
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
469
437
|
const presetHeader = presetHeaders[i];
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
banks.set(presetHeader.bank, index);
|
|
473
|
-
}
|
|
438
|
+
const banks = this.soundFontTable[presetHeader.preset];
|
|
439
|
+
banks.set(presetHeader.bank, index);
|
|
474
440
|
}
|
|
475
441
|
}
|
|
476
|
-
async
|
|
442
|
+
async toUint8Array(input) {
|
|
477
443
|
let uint8Array;
|
|
478
444
|
if (typeof input === "string") {
|
|
479
445
|
const response = await fetch(input);
|
|
@@ -486,23 +452,32 @@ class Midy {
|
|
|
486
452
|
else {
|
|
487
453
|
throw new TypeError("input must be a URL string or Uint8Array");
|
|
488
454
|
}
|
|
489
|
-
|
|
490
|
-
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
491
|
-
this.addSoundFont(soundFont);
|
|
455
|
+
return uint8Array;
|
|
492
456
|
}
|
|
493
|
-
async
|
|
494
|
-
|
|
495
|
-
if (
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
457
|
+
async loadSoundFont(input) {
|
|
458
|
+
this.voiceCounter.clear();
|
|
459
|
+
if (Array.isArray(input)) {
|
|
460
|
+
const promises = new Array(input.length);
|
|
461
|
+
for (let i = 0; i < input.length; i++) {
|
|
462
|
+
promises[i] = this.toUint8Array(input[i]);
|
|
463
|
+
}
|
|
464
|
+
const uint8Arrays = await Promise.all(promises);
|
|
465
|
+
for (let i = 0; i < uint8Arrays.length; i++) {
|
|
466
|
+
const parsed = (0, soundfont_parser_1.parse)(uint8Arrays[i]);
|
|
467
|
+
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
468
|
+
this.addSoundFont(soundFont);
|
|
469
|
+
}
|
|
502
470
|
}
|
|
503
471
|
else {
|
|
504
|
-
|
|
472
|
+
const uint8Array = await this.toUint8Array(input);
|
|
473
|
+
const parsed = (0, soundfont_parser_1.parse)(uint8Array);
|
|
474
|
+
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
475
|
+
this.addSoundFont(soundFont);
|
|
505
476
|
}
|
|
477
|
+
}
|
|
478
|
+
async loadMIDI(input) {
|
|
479
|
+
this.voiceCounter.clear();
|
|
480
|
+
const uint8Array = await this.toUint8Array(input);
|
|
506
481
|
const midi = (0, midi_file_1.parseMidi)(uint8Array);
|
|
507
482
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
508
483
|
const midiData = this.extractMidiData(midi);
|
|
@@ -510,7 +485,46 @@ class Midy {
|
|
|
510
485
|
this.timeline = midiData.timeline;
|
|
511
486
|
this.totalTime = this.calcTotalTime();
|
|
512
487
|
}
|
|
513
|
-
|
|
488
|
+
cacheVoiceIds() {
|
|
489
|
+
const timeline = this.timeline;
|
|
490
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
491
|
+
const event = timeline[i];
|
|
492
|
+
switch (event.type) {
|
|
493
|
+
case "noteOn": {
|
|
494
|
+
const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
|
|
495
|
+
this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
case "controller":
|
|
499
|
+
if (event.controllerType === 0) {
|
|
500
|
+
this.setBankMSB(event.channel, event.value);
|
|
501
|
+
}
|
|
502
|
+
else if (event.controllerType === 32) {
|
|
503
|
+
this.setBankLSB(event.channel, event.value);
|
|
504
|
+
}
|
|
505
|
+
break;
|
|
506
|
+
case "programChange":
|
|
507
|
+
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
for (const [audioBufferId, count] of this.voiceCounter) {
|
|
511
|
+
if (count === 1)
|
|
512
|
+
this.voiceCounter.delete(audioBufferId);
|
|
513
|
+
}
|
|
514
|
+
this.GM2SystemOn();
|
|
515
|
+
}
|
|
516
|
+
getVoiceId(channel, noteNumber, velocity) {
|
|
517
|
+
const bankNumber = this.calcBank(channel);
|
|
518
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
519
|
+
.get(bankNumber);
|
|
520
|
+
if (soundFontIndex === undefined)
|
|
521
|
+
return;
|
|
522
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
523
|
+
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
524
|
+
const { instrument, sampleID } = voice.generators;
|
|
525
|
+
return `${soundFontIndex}:${instrument}:${sampleID}`;
|
|
526
|
+
}
|
|
527
|
+
createChannelAudioNodes(audioContext) {
|
|
514
528
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
515
529
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
516
530
|
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
@@ -525,10 +539,10 @@ class Midy {
|
|
|
525
539
|
};
|
|
526
540
|
}
|
|
527
541
|
resetChannelTable(channel) {
|
|
528
|
-
|
|
542
|
+
channel.controlTable.fill(-1);
|
|
529
543
|
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
530
|
-
channel.channelPressureTable.
|
|
531
|
-
channel.polyphonicKeyPressureTable.
|
|
544
|
+
channel.channelPressureTable.fill(-1);
|
|
545
|
+
channel.polyphonicKeyPressureTable.fill(-1);
|
|
532
546
|
channel.keyBasedInstrumentControlTable.fill(-1);
|
|
533
547
|
}
|
|
534
548
|
createChannels(audioContext) {
|
|
@@ -538,47 +552,27 @@ class Midy {
|
|
|
538
552
|
isDrum: false,
|
|
539
553
|
state: new ControllerState(),
|
|
540
554
|
...this.constructor.channelSettings,
|
|
541
|
-
...this.
|
|
555
|
+
...this.createChannelAudioNodes(audioContext),
|
|
542
556
|
scheduledNotes: [],
|
|
543
557
|
sustainNotes: [],
|
|
544
558
|
sostenutoNotes: [],
|
|
545
559
|
controlTable: this.initControlTable(),
|
|
546
560
|
scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
|
|
547
|
-
channelPressureTable: new
|
|
548
|
-
polyphonicKeyPressureTable: new
|
|
561
|
+
channelPressureTable: new Int8Array(6).fill(-1),
|
|
562
|
+
polyphonicKeyPressureTable: new Int8Array(6).fill(-1),
|
|
549
563
|
keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
|
|
564
|
+
keyBasedGainLs: new Array(128),
|
|
565
|
+
keyBasedGainRs: new Array(128),
|
|
550
566
|
};
|
|
551
567
|
});
|
|
552
568
|
return channels;
|
|
553
569
|
}
|
|
554
|
-
async
|
|
570
|
+
async createAudioBuffer(voiceParams) {
|
|
571
|
+
const sample = voiceParams.sample;
|
|
555
572
|
const sampleStart = voiceParams.start;
|
|
556
|
-
const sampleEnd =
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const start = sample.byteOffset + sampleStart;
|
|
560
|
-
const end = sample.byteOffset + sampleEnd;
|
|
561
|
-
const buffer = sample.buffer.slice(start, end);
|
|
562
|
-
const audioBuffer = await this.audioContext.decodeAudioData(buffer);
|
|
563
|
-
return audioBuffer;
|
|
564
|
-
}
|
|
565
|
-
else {
|
|
566
|
-
const sample = voiceParams.sample;
|
|
567
|
-
const start = sample.byteOffset + sampleStart;
|
|
568
|
-
const end = sample.byteOffset + sampleEnd;
|
|
569
|
-
const buffer = sample.buffer.slice(start, end);
|
|
570
|
-
const audioBuffer = new AudioBuffer({
|
|
571
|
-
numberOfChannels: 1,
|
|
572
|
-
length: sample.length,
|
|
573
|
-
sampleRate: voiceParams.sampleRate,
|
|
574
|
-
});
|
|
575
|
-
const channelData = audioBuffer.getChannelData(0);
|
|
576
|
-
const int16Array = new Int16Array(buffer);
|
|
577
|
-
for (let i = 0; i < int16Array.length; i++) {
|
|
578
|
-
channelData[i] = int16Array[i] / 32768;
|
|
579
|
-
}
|
|
580
|
-
return audioBuffer;
|
|
581
|
-
}
|
|
573
|
+
const sampleEnd = sample.data.length + voiceParams.end;
|
|
574
|
+
const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
|
|
575
|
+
return audioBuffer;
|
|
582
576
|
}
|
|
583
577
|
isLoopDrum(channel, noteNumber) {
|
|
584
578
|
const programNumber = channel.programNumber;
|
|
@@ -588,10 +582,9 @@ class Midy {
|
|
|
588
582
|
createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
|
|
589
583
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
590
584
|
bufferSource.buffer = audioBuffer;
|
|
591
|
-
bufferSource.loop =
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
585
|
+
bufferSource.loop = channel.isDrum
|
|
586
|
+
? this.isLoopDrum(channel, noteNumber)
|
|
587
|
+
: (voiceParams.sampleModes % 2 !== 0);
|
|
595
588
|
if (bufferSource.loop) {
|
|
596
589
|
bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
597
590
|
bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
|
|
@@ -616,16 +609,16 @@ class Midy {
|
|
|
616
609
|
break;
|
|
617
610
|
}
|
|
618
611
|
case "noteAftertouch":
|
|
619
|
-
this.
|
|
612
|
+
this.setPolyphonicKeyPressure(event.channel, event.noteNumber, event.amount, startTime);
|
|
620
613
|
break;
|
|
621
614
|
case "controller":
|
|
622
|
-
this.
|
|
615
|
+
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
623
616
|
break;
|
|
624
617
|
case "programChange":
|
|
625
|
-
this.
|
|
618
|
+
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
626
619
|
break;
|
|
627
620
|
case "channelAftertouch":
|
|
628
|
-
this.
|
|
621
|
+
this.setChannelPressure(event.channel, event.amount, startTime);
|
|
629
622
|
break;
|
|
630
623
|
case "pitchBend":
|
|
631
624
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
@@ -659,8 +652,9 @@ class Midy {
|
|
|
659
652
|
this.notePromises = [];
|
|
660
653
|
this.exclusiveClassNotes.fill(undefined);
|
|
661
654
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
662
|
-
this.
|
|
655
|
+
this.voiceCache.clear();
|
|
663
656
|
for (let i = 0; i < this.channels.length; i++) {
|
|
657
|
+
this.channels[i].scheduledNotes = [];
|
|
664
658
|
this.resetAllStates(i);
|
|
665
659
|
}
|
|
666
660
|
resolve();
|
|
@@ -682,8 +676,9 @@ class Midy {
|
|
|
682
676
|
this.notePromises = [];
|
|
683
677
|
this.exclusiveClassNotes.fill(undefined);
|
|
684
678
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
685
|
-
this.
|
|
679
|
+
this.voiceCache.clear();
|
|
686
680
|
for (let i = 0; i < this.channels.length; i++) {
|
|
681
|
+
this.channels[i].scheduledNotes = [];
|
|
687
682
|
this.resetAllStates(i);
|
|
688
683
|
}
|
|
689
684
|
this.isStopping = false;
|
|
@@ -716,11 +711,7 @@ class Midy {
|
|
|
716
711
|
secondToTicks(second, secondsPerBeat) {
|
|
717
712
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
718
713
|
}
|
|
719
|
-
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
720
|
-
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
721
|
-
}
|
|
722
714
|
extractMidiData(midi) {
|
|
723
|
-
this.audioBufferCounter.clear();
|
|
724
715
|
const instruments = new Set();
|
|
725
716
|
const timeline = [];
|
|
726
717
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -741,8 +732,6 @@ class Midy {
|
|
|
741
732
|
switch (event.type) {
|
|
742
733
|
case "noteOn": {
|
|
743
734
|
const channel = tmpChannels[event.channel];
|
|
744
|
-
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
745
|
-
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
746
735
|
if (channel.programNumber < 0) {
|
|
747
736
|
channel.programNumber = event.programNumber;
|
|
748
737
|
switch (channel.bankMSB) {
|
|
@@ -792,10 +781,6 @@ class Midy {
|
|
|
792
781
|
timeline.push(event);
|
|
793
782
|
}
|
|
794
783
|
}
|
|
795
|
-
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
796
|
-
if (count === 1)
|
|
797
|
-
this.audioBufferCounter.delete(audioBufferId);
|
|
798
|
-
}
|
|
799
784
|
const priority = {
|
|
800
785
|
controller: 0,
|
|
801
786
|
sysEx: 1,
|
|
@@ -840,7 +825,6 @@ class Midy {
|
|
|
840
825
|
this.notePromises.push(promise);
|
|
841
826
|
promises.push(promise);
|
|
842
827
|
});
|
|
843
|
-
channel.scheduledNotes = [];
|
|
844
828
|
return Promise.all(promises);
|
|
845
829
|
}
|
|
846
830
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -854,6 +838,8 @@ class Midy {
|
|
|
854
838
|
if (this.isPlaying || this.isPaused)
|
|
855
839
|
return;
|
|
856
840
|
this.resumeTime = 0;
|
|
841
|
+
if (this.voiceCounter.size === 0)
|
|
842
|
+
this.cacheVoiceIds();
|
|
857
843
|
await this.playNotes();
|
|
858
844
|
this.isPlaying = false;
|
|
859
845
|
}
|
|
@@ -896,7 +882,7 @@ class Midy {
|
|
|
896
882
|
}
|
|
897
883
|
processScheduledNotes(channel, callback) {
|
|
898
884
|
const scheduledNotes = channel.scheduledNotes;
|
|
899
|
-
for (let i =
|
|
885
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
900
886
|
const note = scheduledNotes[i];
|
|
901
887
|
if (!note)
|
|
902
888
|
continue;
|
|
@@ -907,14 +893,14 @@ class Midy {
|
|
|
907
893
|
}
|
|
908
894
|
processActiveNotes(channel, scheduleTime, callback) {
|
|
909
895
|
const scheduledNotes = channel.scheduledNotes;
|
|
910
|
-
for (let i =
|
|
896
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
911
897
|
const note = scheduledNotes[i];
|
|
912
898
|
if (!note)
|
|
913
899
|
continue;
|
|
914
900
|
if (note.ending)
|
|
915
901
|
continue;
|
|
916
902
|
if (scheduleTime < note.startTime)
|
|
917
|
-
|
|
903
|
+
break;
|
|
918
904
|
callback(note);
|
|
919
905
|
}
|
|
920
906
|
}
|
|
@@ -1003,6 +989,22 @@ class Midy {
|
|
|
1003
989
|
const output = allpasses.at(-1);
|
|
1004
990
|
return { input, output };
|
|
1005
991
|
}
|
|
992
|
+
createReverbEffect(audioContext) {
|
|
993
|
+
const { algorithm, time: rt60, feedback } = this.reverb;
|
|
994
|
+
switch (algorithm) {
|
|
995
|
+
case "ConvolutionReverb": {
|
|
996
|
+
const impulse = this.createConvolutionReverbImpulse(audioContext, rt60, this.calcDelay(rt60, feedback));
|
|
997
|
+
return this.createConvolutionReverb(audioContext, impulse);
|
|
998
|
+
}
|
|
999
|
+
case "SchroederReverb": {
|
|
1000
|
+
const combFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
1001
|
+
const combDelays = combFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
|
|
1002
|
+
const allpassFeedbacks = this.generateDistributedArray(feedback, 4);
|
|
1003
|
+
const allpassDelays = allpassFeedbacks.map((feedback) => this.calcDelay(rt60, feedback));
|
|
1004
|
+
return this.createSchroederReverb(audioContext, combFeedbacks, combDelays, allpassFeedbacks, allpassDelays);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1006
1008
|
createChorusEffect(audioContext) {
|
|
1007
1009
|
const input = new GainNode(audioContext);
|
|
1008
1010
|
const output = new GainNode(audioContext);
|
|
@@ -1067,9 +1069,16 @@ class Midy {
|
|
|
1067
1069
|
const pitchWheel = channel.state.pitchWheel * 2 - 1;
|
|
1068
1070
|
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
|
|
1069
1071
|
const pitch = pitchWheel * pitchWheelSensitivity;
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1072
|
+
const channelPressureRaw = channel.channelPressureTable[0];
|
|
1073
|
+
if (0 <= channelPressureRaw) {
|
|
1074
|
+
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1075
|
+
const channelPressure = channelPressureDepth *
|
|
1076
|
+
channel.state.channelPressure;
|
|
1077
|
+
return tuning + pitch + channelPressure;
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
return tuning + pitch;
|
|
1081
|
+
}
|
|
1073
1082
|
}
|
|
1074
1083
|
calcNoteDetune(channel, note) {
|
|
1075
1084
|
return channel.scaleOctaveTuningTable[note.noteNumber % 12];
|
|
@@ -1303,35 +1312,32 @@ class Midy {
|
|
|
1303
1312
|
note.vibratoLFO.connect(note.vibratoDepth);
|
|
1304
1313
|
note.vibratoDepth.connect(note.bufferSource.detune);
|
|
1305
1314
|
}
|
|
1306
|
-
async getAudioBuffer(
|
|
1307
|
-
const audioBufferId = this.
|
|
1308
|
-
const cache = this.
|
|
1315
|
+
async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
|
|
1316
|
+
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
1317
|
+
const cache = this.voiceCache.get(audioBufferId);
|
|
1309
1318
|
if (cache) {
|
|
1310
1319
|
cache.counter += 1;
|
|
1311
1320
|
if (cache.maxCount <= cache.counter) {
|
|
1312
|
-
this.
|
|
1321
|
+
this.voiceCache.delete(audioBufferId);
|
|
1313
1322
|
}
|
|
1314
1323
|
return cache.audioBuffer;
|
|
1315
1324
|
}
|
|
1316
1325
|
else {
|
|
1317
|
-
const maxCount = this.
|
|
1318
|
-
const audioBuffer = await this.
|
|
1326
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
1327
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
1319
1328
|
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
1320
|
-
this.
|
|
1329
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
1321
1330
|
return audioBuffer;
|
|
1322
1331
|
}
|
|
1323
1332
|
}
|
|
1324
|
-
async createNote(channel, voice, noteNumber, velocity, startTime
|
|
1333
|
+
async createNote(channel, voice, noteNumber, velocity, startTime) {
|
|
1325
1334
|
const now = this.audioContext.currentTime;
|
|
1326
1335
|
const state = channel.state;
|
|
1327
|
-
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
1336
|
+
const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
|
|
1328
1337
|
const voiceParams = voice.getAllParams(controllerState);
|
|
1329
1338
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
1330
|
-
const audioBuffer = await this.getAudioBuffer(channel
|
|
1339
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
1331
1340
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1332
|
-
note.volumeNode = new GainNode(this.audioContext);
|
|
1333
|
-
note.gainL = new GainNode(this.audioContext);
|
|
1334
|
-
note.gainR = new GainNode(this.audioContext);
|
|
1335
1341
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
1336
1342
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
1337
1343
|
type: "lowpass",
|
|
@@ -1364,9 +1370,6 @@ class Midy {
|
|
|
1364
1370
|
}
|
|
1365
1371
|
note.bufferSource.connect(note.filterNode);
|
|
1366
1372
|
note.filterNode.connect(note.volumeEnvelopeNode);
|
|
1367
|
-
note.volumeEnvelopeNode.connect(note.volumeNode);
|
|
1368
|
-
note.volumeNode.connect(note.gainL);
|
|
1369
|
-
note.volumeNode.connect(note.gainR);
|
|
1370
1373
|
if (0 < state.chorusSendLevel) {
|
|
1371
1374
|
this.setChorusEffectsSend(channel, note, 0, now);
|
|
1372
1375
|
}
|
|
@@ -1425,21 +1428,30 @@ class Midy {
|
|
|
1425
1428
|
}
|
|
1426
1429
|
this.drumExclusiveClassNotes[index] = note;
|
|
1427
1430
|
}
|
|
1428
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime
|
|
1431
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
1429
1432
|
const channel = this.channels[channelNumber];
|
|
1430
1433
|
const bankNumber = this.calcBank(channel, channelNumber);
|
|
1431
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
1434
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
1435
|
+
.get(bankNumber);
|
|
1432
1436
|
if (soundFontIndex === undefined)
|
|
1433
1437
|
return;
|
|
1434
1438
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
1435
1439
|
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
1436
1440
|
if (!voice)
|
|
1437
1441
|
return;
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1442
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
1443
|
+
if (channel.isDrum) {
|
|
1444
|
+
const audioContext = this.audioContext;
|
|
1445
|
+
const { gainL, gainR } = this.createChannelAudioNodes(audioContext);
|
|
1446
|
+
channel.keyBasedGainLs[noteNumber] = gainL;
|
|
1447
|
+
channel.keyBasedGainRs[noteNumber] = gainR;
|
|
1448
|
+
note.volumeEnvelopeNode.connect(gainL);
|
|
1449
|
+
note.volumeEnvelopeNode.connect(gainR);
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
1453
|
+
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
1454
|
+
}
|
|
1443
1455
|
if (0.5 <= channel.state.sustainPedal) {
|
|
1444
1456
|
channel.sustainNotes.push(note);
|
|
1445
1457
|
}
|
|
@@ -1457,9 +1469,6 @@ class Midy {
|
|
|
1457
1469
|
note.bufferSource.disconnect();
|
|
1458
1470
|
note.filterNode.disconnect();
|
|
1459
1471
|
note.volumeEnvelopeNode.disconnect();
|
|
1460
|
-
note.volumeNode.disconnect();
|
|
1461
|
-
note.gainL.disconnect();
|
|
1462
|
-
note.gainR.disconnect();
|
|
1463
1472
|
if (note.modulationDepth) {
|
|
1464
1473
|
note.volumeDepth.disconnect();
|
|
1465
1474
|
note.modulationDepth.disconnect();
|
|
@@ -1513,15 +1522,29 @@ class Midy {
|
|
|
1513
1522
|
return;
|
|
1514
1523
|
}
|
|
1515
1524
|
}
|
|
1516
|
-
const
|
|
1517
|
-
if (
|
|
1525
|
+
const index = this.findNoteOffIndex(channel, noteNumber);
|
|
1526
|
+
if (index < 0)
|
|
1518
1527
|
return;
|
|
1528
|
+
const note = channel.scheduledNotes[index];
|
|
1519
1529
|
note.ending = true;
|
|
1530
|
+
this.setNoteIndex(channel, index);
|
|
1520
1531
|
this.releaseNote(channel, note, endTime);
|
|
1521
1532
|
}
|
|
1522
|
-
|
|
1533
|
+
setNoteIndex(channel, index) {
|
|
1534
|
+
let allEnds = true;
|
|
1535
|
+
for (let i = channel.scheduleIndex; i < index; i++) {
|
|
1536
|
+
const note = channel.scheduledNotes[i];
|
|
1537
|
+
if (note && !note.ending) {
|
|
1538
|
+
allEnds = false;
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (allEnds)
|
|
1543
|
+
channel.scheduleIndex = index + 1;
|
|
1544
|
+
}
|
|
1545
|
+
findNoteOffIndex(channel, noteNumber) {
|
|
1523
1546
|
const scheduledNotes = channel.scheduledNotes;
|
|
1524
|
-
for (let i =
|
|
1547
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
1525
1548
|
const note = scheduledNotes[i];
|
|
1526
1549
|
if (!note)
|
|
1527
1550
|
continue;
|
|
@@ -1529,8 +1552,9 @@ class Midy {
|
|
|
1529
1552
|
continue;
|
|
1530
1553
|
if (note.noteNumber !== noteNumber)
|
|
1531
1554
|
continue;
|
|
1532
|
-
return
|
|
1555
|
+
return i;
|
|
1533
1556
|
}
|
|
1557
|
+
return -1;
|
|
1534
1558
|
}
|
|
1535
1559
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1536
1560
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -1570,31 +1594,31 @@ class Midy {
|
|
|
1570
1594
|
case 0x90:
|
|
1571
1595
|
return this.noteOn(channelNumber, data1, data2, scheduleTime);
|
|
1572
1596
|
case 0xA0:
|
|
1573
|
-
return this.
|
|
1597
|
+
return this.setPolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
|
|
1574
1598
|
case 0xB0:
|
|
1575
|
-
return this.
|
|
1599
|
+
return this.setControlChange(channelNumber, data1, data2, scheduleTime);
|
|
1576
1600
|
case 0xC0:
|
|
1577
|
-
return this.
|
|
1601
|
+
return this.setProgramChange(channelNumber, data1, scheduleTime);
|
|
1578
1602
|
case 0xD0:
|
|
1579
|
-
return this.
|
|
1603
|
+
return this.setChannelPressure(channelNumber, data1, scheduleTime);
|
|
1580
1604
|
case 0xE0:
|
|
1581
1605
|
return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
|
|
1582
1606
|
default:
|
|
1583
1607
|
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
1584
1608
|
}
|
|
1585
1609
|
}
|
|
1586
|
-
|
|
1610
|
+
setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
|
|
1587
1611
|
const channel = this.channels[channelNumber];
|
|
1588
|
-
channel.state.polyphonicKeyPressure = pressure / 127;
|
|
1589
1612
|
const table = channel.polyphonicKeyPressureTable;
|
|
1590
1613
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1591
1614
|
if (note.noteNumber === noteNumber) {
|
|
1592
|
-
|
|
1615
|
+
note.pressure = pressure;
|
|
1616
|
+
this.setControllerParameters(channel, note, table, scheduleTime);
|
|
1593
1617
|
}
|
|
1594
1618
|
});
|
|
1595
1619
|
this.applyVoiceParams(channel, 10);
|
|
1596
1620
|
}
|
|
1597
|
-
|
|
1621
|
+
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
1598
1622
|
const channel = this.channels[channelNumber];
|
|
1599
1623
|
channel.bank = channel.bankMSB * 128 + channel.bankLSB;
|
|
1600
1624
|
channel.programNumber = programNumber;
|
|
@@ -1609,20 +1633,21 @@ class Midy {
|
|
|
1609
1633
|
}
|
|
1610
1634
|
}
|
|
1611
1635
|
}
|
|
1612
|
-
|
|
1636
|
+
setChannelPressure(channelNumber, value, scheduleTime) {
|
|
1613
1637
|
const channel = this.channels[channelNumber];
|
|
1614
1638
|
if (channel.isDrum)
|
|
1615
1639
|
return;
|
|
1616
1640
|
const prev = channel.state.channelPressure;
|
|
1617
1641
|
const next = value / 127;
|
|
1618
1642
|
channel.state.channelPressure = next;
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1643
|
+
const channelPressureRaw = channel.channelPressureTable[0];
|
|
1644
|
+
if (0 <= channelPressureRaw) {
|
|
1645
|
+
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1646
|
+
channel.detune += channelPressureDepth * (next - prev);
|
|
1622
1647
|
}
|
|
1623
1648
|
const table = channel.channelPressureTable;
|
|
1624
1649
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1625
|
-
this.setControllerParameters(channel, note, table);
|
|
1650
|
+
this.setControllerParameters(channel, note, table, scheduleTime);
|
|
1626
1651
|
});
|
|
1627
1652
|
this.applyVoiceParams(channel, 13);
|
|
1628
1653
|
}
|
|
@@ -1678,10 +1703,12 @@ class Midy {
|
|
|
1678
1703
|
.setValueAtTime(volumeDepth, scheduleTime);
|
|
1679
1704
|
}
|
|
1680
1705
|
setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
|
|
1681
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
|
|
1682
1706
|
let value = note.voiceParams.reverbEffectsSend;
|
|
1683
|
-
if (
|
|
1684
|
-
|
|
1707
|
+
if (channel.isDrum) {
|
|
1708
|
+
const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 91);
|
|
1709
|
+
if (0 <= keyBasedValue) {
|
|
1710
|
+
value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
|
|
1711
|
+
}
|
|
1685
1712
|
}
|
|
1686
1713
|
if (0 < prevValue) {
|
|
1687
1714
|
if (0 < value) {
|
|
@@ -1699,20 +1726,22 @@ class Midy {
|
|
|
1699
1726
|
note.reverbEffectsSend = new GainNode(this.audioContext, {
|
|
1700
1727
|
gain: value,
|
|
1701
1728
|
});
|
|
1702
|
-
note.
|
|
1729
|
+
note.volumeEnvelopeNode.connect(note.reverbEffectsSend);
|
|
1703
1730
|
}
|
|
1704
1731
|
note.reverbEffectsSend.connect(this.reverbEffect.input);
|
|
1705
1732
|
}
|
|
1706
1733
|
}
|
|
1707
1734
|
}
|
|
1708
1735
|
setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
|
|
1709
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
|
|
1710
1736
|
let value = note.voiceParams.chorusEffectsSend;
|
|
1711
|
-
if (
|
|
1712
|
-
|
|
1737
|
+
if (channel.isDrum) {
|
|
1738
|
+
const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, 93);
|
|
1739
|
+
if (0 <= keyBasedValue) {
|
|
1740
|
+
value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
|
|
1741
|
+
}
|
|
1713
1742
|
}
|
|
1714
1743
|
if (0 < prevValue) {
|
|
1715
|
-
if (0 <
|
|
1744
|
+
if (0 < value) {
|
|
1716
1745
|
note.chorusEffectsSend.gain
|
|
1717
1746
|
.cancelScheduledValues(scheduleTime)
|
|
1718
1747
|
.setValueAtTime(value, scheduleTime);
|
|
@@ -1727,7 +1756,7 @@ class Midy {
|
|
|
1727
1756
|
note.chorusEffectsSend = new GainNode(this.audioContext, {
|
|
1728
1757
|
gain: value,
|
|
1729
1758
|
});
|
|
1730
|
-
note.
|
|
1759
|
+
note.volumeEnvelopeNode.connect(note.chorusEffectsSend);
|
|
1731
1760
|
}
|
|
1732
1761
|
note.chorusEffectsSend.connect(this.chorusEffect.input);
|
|
1733
1762
|
}
|
|
@@ -1802,21 +1831,22 @@ class Midy {
|
|
|
1802
1831
|
},
|
|
1803
1832
|
};
|
|
1804
1833
|
}
|
|
1805
|
-
getControllerState(channel, noteNumber, velocity) {
|
|
1834
|
+
getControllerState(channel, noteNumber, velocity, polyphonicKeyPressure) {
|
|
1806
1835
|
const state = new Float32Array(channel.state.array.length);
|
|
1807
1836
|
state.set(channel.state.array);
|
|
1808
1837
|
state[2] = velocity / 127;
|
|
1809
1838
|
state[3] = noteNumber / 127;
|
|
1810
|
-
state[10] =
|
|
1839
|
+
state[10] = polyphonicKeyPressure / 127;
|
|
1811
1840
|
state[13] = state.channelPressure / 127;
|
|
1812
1841
|
return state;
|
|
1813
1842
|
}
|
|
1814
1843
|
applyVoiceParams(channel, controllerType, scheduleTime) {
|
|
1815
1844
|
this.processScheduledNotes(channel, (note) => {
|
|
1816
|
-
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
1845
|
+
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity, note.pressure);
|
|
1817
1846
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
1818
|
-
let
|
|
1819
|
-
let
|
|
1847
|
+
let applyVolumeEnvelope = false;
|
|
1848
|
+
let applyFilterEnvelope = false;
|
|
1849
|
+
let applyPitchEnvelope = false;
|
|
1820
1850
|
for (const [key, value] of Object.entries(voiceParams)) {
|
|
1821
1851
|
const prevValue = note.voiceParams[key];
|
|
1822
1852
|
if (value === prevValue)
|
|
@@ -1825,37 +1855,23 @@ class Midy {
|
|
|
1825
1855
|
if (key in this.voiceParamsHandlers) {
|
|
1826
1856
|
this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
|
|
1827
1857
|
}
|
|
1828
|
-
else
|
|
1829
|
-
if (
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
if (key in voiceParams)
|
|
1836
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1837
|
-
}
|
|
1838
|
-
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
1839
|
-
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1840
|
-
}
|
|
1841
|
-
else {
|
|
1842
|
-
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
1843
|
-
}
|
|
1844
|
-
this.setPitchEnvelope(note, scheduleTime);
|
|
1845
|
-
}
|
|
1846
|
-
else if (volumeEnvelopeKeySet.has(key)) {
|
|
1847
|
-
if (appliedVolumeEnvelope)
|
|
1848
|
-
continue;
|
|
1849
|
-
appliedVolumeEnvelope = true;
|
|
1850
|
-
const noteVoiceParams = note.voiceParams;
|
|
1851
|
-
for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
|
|
1852
|
-
const key = volumeEnvelopeKeys[i];
|
|
1853
|
-
if (key in voiceParams)
|
|
1854
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1855
|
-
}
|
|
1856
|
-
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1858
|
+
else {
|
|
1859
|
+
if (volumeEnvelopeKeySet.has(key))
|
|
1860
|
+
applyVolumeEnvelope = true;
|
|
1861
|
+
if (filterEnvelopeKeySet.has(key))
|
|
1862
|
+
applyFilterEnvelope = true;
|
|
1863
|
+
if (pitchEnvelopeKeySet.has(key))
|
|
1864
|
+
applyPitchEnvelope = true;
|
|
1857
1865
|
}
|
|
1858
1866
|
}
|
|
1867
|
+
if (applyVolumeEnvelope) {
|
|
1868
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1869
|
+
}
|
|
1870
|
+
if (applyFilterEnvelope) {
|
|
1871
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
1872
|
+
}
|
|
1873
|
+
if (applyPitchEnvelope)
|
|
1874
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1859
1875
|
});
|
|
1860
1876
|
}
|
|
1861
1877
|
createControlChangeHandlers() {
|
|
@@ -1896,13 +1912,13 @@ class Midy {
|
|
|
1896
1912
|
handlers[127] = this.polyOn;
|
|
1897
1913
|
return handlers;
|
|
1898
1914
|
}
|
|
1899
|
-
|
|
1915
|
+
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1900
1916
|
const handler = this.controlChangeHandlers[controllerType];
|
|
1901
1917
|
if (handler) {
|
|
1902
1918
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
1903
1919
|
const channel = this.channels[channelNumber];
|
|
1904
1920
|
this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
|
|
1905
|
-
this.applyControlTable(channel, controllerType);
|
|
1921
|
+
this.applyControlTable(channel, controllerType, scheduleTime);
|
|
1906
1922
|
}
|
|
1907
1923
|
else {
|
|
1908
1924
|
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
@@ -1959,22 +1975,12 @@ class Midy {
|
|
|
1959
1975
|
return;
|
|
1960
1976
|
this.updatePortamento(channel, scheduleTime);
|
|
1961
1977
|
}
|
|
1962
|
-
setKeyBasedVolume(channel, scheduleTime) {
|
|
1963
|
-
this.processScheduledNotes(channel, (note) => {
|
|
1964
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
|
|
1965
|
-
if (0 <= keyBasedValue) {
|
|
1966
|
-
note.volumeNode.gain
|
|
1967
|
-
.cancelScheduledValues(scheduleTime)
|
|
1968
|
-
.setValueAtTime(keyBasedValue / 127, scheduleTime);
|
|
1969
|
-
}
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
1978
|
setVolume(channelNumber, volume, scheduleTime) {
|
|
1973
1979
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1974
1980
|
const channel = this.channels[channelNumber];
|
|
1975
1981
|
channel.state.volume = volume / 127;
|
|
1976
1982
|
this.updateChannelVolume(channel, scheduleTime);
|
|
1977
|
-
this.
|
|
1983
|
+
this.updateKeyBasedVolume(channel, scheduleTime);
|
|
1978
1984
|
}
|
|
1979
1985
|
panToGain(pan) {
|
|
1980
1986
|
const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
|
|
@@ -1983,26 +1989,12 @@ class Midy {
|
|
|
1983
1989
|
gainRight: Math.sin(theta),
|
|
1984
1990
|
};
|
|
1985
1991
|
}
|
|
1986
|
-
setKeyBasedPan(channel, scheduleTime) {
|
|
1987
|
-
this.processScheduledNotes(channel, (note) => {
|
|
1988
|
-
const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
|
|
1989
|
-
if (0 <= keyBasedValue) {
|
|
1990
|
-
const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
|
|
1991
|
-
note.gainL.gain
|
|
1992
|
-
.cancelScheduledValues(scheduleTime)
|
|
1993
|
-
.setValueAtTime(gainLeft, scheduleTime);
|
|
1994
|
-
note.gainR.gain
|
|
1995
|
-
.cancelScheduledValues(scheduleTime)
|
|
1996
|
-
.setValueAtTime(gainRight, scheduleTime);
|
|
1997
|
-
}
|
|
1998
|
-
});
|
|
1999
|
-
}
|
|
2000
1992
|
setPan(channelNumber, pan, scheduleTime) {
|
|
2001
1993
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2002
1994
|
const channel = this.channels[channelNumber];
|
|
2003
1995
|
channel.state.pan = pan / 127;
|
|
2004
1996
|
this.updateChannelVolume(channel, scheduleTime);
|
|
2005
|
-
this.
|
|
1997
|
+
this.updateKeyBasedVolume(channel, scheduleTime);
|
|
2006
1998
|
}
|
|
2007
1999
|
setExpression(channelNumber, expression, scheduleTime) {
|
|
2008
2000
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -2028,6 +2020,34 @@ class Midy {
|
|
|
2028
2020
|
.cancelScheduledValues(scheduleTime)
|
|
2029
2021
|
.setValueAtTime(volume * gainRight, scheduleTime);
|
|
2030
2022
|
}
|
|
2023
|
+
updateKeyBasedVolume(channel, scheduleTime) {
|
|
2024
|
+
if (!channel.isDrum)
|
|
2025
|
+
return;
|
|
2026
|
+
const state = channel.state;
|
|
2027
|
+
const defaultVolume = state.volume * state.expression;
|
|
2028
|
+
const defaultPan = state.pan;
|
|
2029
|
+
for (let i = 0; i < 128; i++) {
|
|
2030
|
+
const gainL = channel.keyBasedGainLs[i];
|
|
2031
|
+
const gainR = channel.keyBasedGainLs[i];
|
|
2032
|
+
if (!gainL)
|
|
2033
|
+
continue;
|
|
2034
|
+
if (!gainR)
|
|
2035
|
+
continue;
|
|
2036
|
+
const keyBasedVolume = this.getKeyBasedValue(channel, i, 7);
|
|
2037
|
+
const volume = (0 <= keyBasedVolume)
|
|
2038
|
+
? defaultVolume * keyBasedVolume / 64
|
|
2039
|
+
: defaultVolume;
|
|
2040
|
+
const keyBasedPan = this.getKeyBasedValue(channel, i, 10);
|
|
2041
|
+
const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
|
|
2042
|
+
const { gainLeft, gainRight } = this.panToGain(pan);
|
|
2043
|
+
gainL.gain
|
|
2044
|
+
.cancelScheduledValues(scheduleTime)
|
|
2045
|
+
.setValueAtTime(volume * gainLeft, scheduleTime);
|
|
2046
|
+
gainR.gain
|
|
2047
|
+
.cancelScheduledValues(scheduleTime)
|
|
2048
|
+
.setValueAtTime(volume * gainRight, scheduleTime);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2031
2051
|
setSustainPedal(channelNumber, value, scheduleTime) {
|
|
2032
2052
|
const channel = this.channels[channelNumber];
|
|
2033
2053
|
if (channel.isDrum)
|
|
@@ -2117,7 +2137,7 @@ class Midy {
|
|
|
2117
2137
|
this.processScheduledNotes(channel, (note) => {
|
|
2118
2138
|
if (note.startTime < scheduleTime)
|
|
2119
2139
|
return false;
|
|
2120
|
-
this.setVolumeEnvelope(channel, note);
|
|
2140
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
2121
2141
|
});
|
|
2122
2142
|
}
|
|
2123
2143
|
setBrightness(channelNumber, brightness, scheduleTime) {
|
|
@@ -2132,7 +2152,7 @@ class Midy {
|
|
|
2132
2152
|
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2133
2153
|
}
|
|
2134
2154
|
else {
|
|
2135
|
-
this.setFilterEnvelope(channel, note);
|
|
2155
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
2136
2156
|
}
|
|
2137
2157
|
});
|
|
2138
2158
|
}
|
|
@@ -2402,7 +2422,7 @@ class Midy {
|
|
|
2402
2422
|
const entries = Object.entries(defaultControllerState);
|
|
2403
2423
|
for (const [key, { type, defaultValue }] of entries) {
|
|
2404
2424
|
if (128 <= type) {
|
|
2405
|
-
this.
|
|
2425
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2406
2426
|
}
|
|
2407
2427
|
else {
|
|
2408
2428
|
state[key] = defaultValue;
|
|
@@ -2435,7 +2455,7 @@ class Midy {
|
|
|
2435
2455
|
const key = keys[i];
|
|
2436
2456
|
const { type, defaultValue } = defaultControllerState[key];
|
|
2437
2457
|
if (128 <= type) {
|
|
2438
|
-
this.
|
|
2458
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
2439
2459
|
}
|
|
2440
2460
|
else {
|
|
2441
2461
|
state[key] = defaultValue;
|
|
@@ -2659,8 +2679,7 @@ class Midy {
|
|
|
2659
2679
|
setReverbType(type) {
|
|
2660
2680
|
this.reverb.time = this.getReverbTimeFromType(type);
|
|
2661
2681
|
this.reverb.feedback = (type === 8) ? 0.9 : 0.8;
|
|
2662
|
-
|
|
2663
|
-
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2682
|
+
this.reverbEffect = this.createReverbEffect(this.audioContext);
|
|
2664
2683
|
}
|
|
2665
2684
|
getReverbTimeFromType(type) {
|
|
2666
2685
|
switch (type) {
|
|
@@ -2682,8 +2701,7 @@ class Midy {
|
|
|
2682
2701
|
}
|
|
2683
2702
|
setReverbTime(value) {
|
|
2684
2703
|
this.reverb.time = this.getReverbTime(value);
|
|
2685
|
-
|
|
2686
|
-
this.reverbEffect = options.reverbAlgorithm(audioContext);
|
|
2704
|
+
this.reverbEffect = this.createReverbEffect(this.audioContext);
|
|
2687
2705
|
}
|
|
2688
2706
|
getReverbTime(value) {
|
|
2689
2707
|
return Math.exp((value - 40) * 0.025);
|
|
@@ -2878,66 +2896,91 @@ class Midy {
|
|
|
2878
2896
|
}
|
|
2879
2897
|
}
|
|
2880
2898
|
getPitchControl(channel, note) {
|
|
2881
|
-
const
|
|
2899
|
+
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[0];
|
|
2900
|
+
if (polyphonicKeyPressureRaw < 0)
|
|
2901
|
+
return 0;
|
|
2902
|
+
const polyphonicKeyPressure = (polyphonicKeyPressureRaw - 64) *
|
|
2882
2903
|
note.pressure;
|
|
2883
2904
|
return polyphonicKeyPressure * note.pressure / 37.5; // 2400 / 64;
|
|
2884
2905
|
}
|
|
2885
2906
|
getFilterCutoffControl(channel, note) {
|
|
2886
|
-
const
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2907
|
+
const channelPressureRaw = channel.channelPressureTable[1];
|
|
2908
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2909
|
+
? (channelPressureRaw - 64) * channel.state.channelPressure
|
|
2910
|
+
: 0;
|
|
2911
|
+
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[1];
|
|
2912
|
+
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
2913
|
+
? (polyphonicKeyPressureRaw - 64) * note.pressure
|
|
2914
|
+
: 0;
|
|
2890
2915
|
return (channelPressure + polyphonicKeyPressure) * 15;
|
|
2891
2916
|
}
|
|
2892
2917
|
getAmplitudeControl(channel, note) {
|
|
2893
|
-
const
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2918
|
+
const channelPressureRaw = channel.channelPressureTable[2];
|
|
2919
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2920
|
+
? channelPressureRaw * channel.state.channelPressure
|
|
2921
|
+
: 0;
|
|
2922
|
+
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[2];
|
|
2923
|
+
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
2924
|
+
? polyphonicKeyPressureRaw * note.pressure
|
|
2925
|
+
: 0;
|
|
2897
2926
|
return (channelPressure + polyphonicKeyPressure) / 128;
|
|
2898
2927
|
}
|
|
2899
2928
|
getLFOPitchDepth(channel, note) {
|
|
2900
|
-
const
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2929
|
+
const channelPressureRaw = channel.channelPressureTable[3];
|
|
2930
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2931
|
+
? channelPressureRaw * channel.state.channelPressure
|
|
2932
|
+
: 0;
|
|
2933
|
+
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[3];
|
|
2934
|
+
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
2935
|
+
? polyphonicKeyPressureRaw * note.pressure
|
|
2936
|
+
: 0;
|
|
2904
2937
|
return (channelPressure + polyphonicKeyPressure) / 254 * 600;
|
|
2905
2938
|
}
|
|
2906
2939
|
getLFOFilterDepth(channel, note) {
|
|
2907
|
-
const
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2940
|
+
const channelPressureRaw = channel.channelPressureTable[4];
|
|
2941
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2942
|
+
? channelPressureRaw * channel.state.channelPressure
|
|
2943
|
+
: 0;
|
|
2944
|
+
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[4];
|
|
2945
|
+
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
2946
|
+
? polyphonicKeyPressureRaw * note.pressure
|
|
2947
|
+
: 0;
|
|
2911
2948
|
return (channelPressure + polyphonicKeyPressure) / 254 * 2400;
|
|
2912
2949
|
}
|
|
2913
2950
|
getLFOAmplitudeDepth(channel, note) {
|
|
2914
|
-
const
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2951
|
+
const channelPressureRaw = channel.channelPressureTable[5];
|
|
2952
|
+
const channelPressure = (0 <= channelPressureRaw)
|
|
2953
|
+
? channelPressureRaw * channel.state.channelPressure
|
|
2954
|
+
: 0;
|
|
2955
|
+
const polyphonicKeyPressureRaw = channel.polyphonicKeyPressureTable[5];
|
|
2956
|
+
const polyphonicKeyPressure = (0 <= polyphonicKeyPressureRaw)
|
|
2957
|
+
? polyphonicKeyPressureRaw * note.pressure
|
|
2958
|
+
: 0;
|
|
2918
2959
|
return (channelPressure + polyphonicKeyPressure) / 254;
|
|
2919
2960
|
}
|
|
2920
|
-
setControllerParameters(channel, note, table) {
|
|
2921
|
-
if (table[0]
|
|
2922
|
-
this.updateDetune(channel, note);
|
|
2961
|
+
setControllerParameters(channel, note, table, scheduleTime) {
|
|
2962
|
+
if (0 <= table[0])
|
|
2963
|
+
this.updateDetune(channel, note, scueduleTime);
|
|
2923
2964
|
if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
|
|
2924
|
-
if (table[1]
|
|
2925
|
-
this.setPortamentoFilterEnvelope(channel, note);
|
|
2926
|
-
|
|
2927
|
-
|
|
2965
|
+
if (0 <= table[1]) {
|
|
2966
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2967
|
+
}
|
|
2968
|
+
if (0 <= table[2]) {
|
|
2969
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2970
|
+
}
|
|
2928
2971
|
}
|
|
2929
2972
|
else {
|
|
2930
|
-
if (table[1]
|
|
2931
|
-
this.setFilterEnvelope(channel, note);
|
|
2932
|
-
if (table[2]
|
|
2933
|
-
this.setVolumeEnvelope(channel, note);
|
|
2934
|
-
}
|
|
2935
|
-
if (table[3]
|
|
2936
|
-
this.setModLfoToPitch(channel, note);
|
|
2937
|
-
if (table[4]
|
|
2938
|
-
this.setModLfoToFilterFc(channel, note);
|
|
2939
|
-
if (table[5]
|
|
2940
|
-
this.setModLfoToVolume(channel, note);
|
|
2973
|
+
if (0 <= table[1])
|
|
2974
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
2975
|
+
if (0 <= table[2])
|
|
2976
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
2977
|
+
}
|
|
2978
|
+
if (0 <= table[3])
|
|
2979
|
+
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
2980
|
+
if (0 <= table[4])
|
|
2981
|
+
this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
2982
|
+
if (0 <= table[5])
|
|
2983
|
+
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
2941
2984
|
}
|
|
2942
2985
|
handlePressureSysEx(data, tableName) {
|
|
2943
2986
|
const channelNumber = data[4];
|
|
@@ -2952,27 +2995,16 @@ class Midy {
|
|
|
2952
2995
|
}
|
|
2953
2996
|
}
|
|
2954
2997
|
initControlTable() {
|
|
2955
|
-
const
|
|
2998
|
+
const ccCount = 128;
|
|
2956
2999
|
const slotSize = 6;
|
|
2957
|
-
|
|
2958
|
-
return this.resetControlTable(table);
|
|
3000
|
+
return new Int8Array(ccCount * slotSize).fill(-1);
|
|
2959
3001
|
}
|
|
2960
|
-
|
|
2961
|
-
const channelCount = 128;
|
|
2962
|
-
const slotSize = 6;
|
|
2963
|
-
const defaultValues = [64, 64, 64, 0, 0, 0];
|
|
2964
|
-
for (let ch = 0; ch < channelCount; ch++) {
|
|
2965
|
-
const offset = ch * slotSize;
|
|
2966
|
-
table.set(defaultValues, offset);
|
|
2967
|
-
}
|
|
2968
|
-
return table;
|
|
2969
|
-
}
|
|
2970
|
-
applyControlTable(channel, controllerType) {
|
|
3002
|
+
applyControlTable(channel, controllerType, scheduleTime) {
|
|
2971
3003
|
const slotSize = 6;
|
|
2972
3004
|
const offset = controllerType * slotSize;
|
|
2973
3005
|
const table = channel.controlTable.subarray(offset, offset + slotSize);
|
|
2974
3006
|
this.processScheduledNotes(channel, (note) => {
|
|
2975
|
-
this.setControllerParameters(channel, note, table);
|
|
3007
|
+
this.setControllerParameters(channel, note, table, scheduleTime);
|
|
2976
3008
|
});
|
|
2977
3009
|
}
|
|
2978
3010
|
handleControlChangeSysEx(data) {
|
|
@@ -2988,7 +3020,7 @@ class Midy {
|
|
|
2988
3020
|
table[pp] = rr;
|
|
2989
3021
|
}
|
|
2990
3022
|
}
|
|
2991
|
-
|
|
3023
|
+
getKeyBasedValue(channel, keyNumber, controllerType) {
|
|
2992
3024
|
const index = keyNumber * 128 + controllerType;
|
|
2993
3025
|
const controlValue = channel.keyBasedInstrumentControlTable[index];
|
|
2994
3026
|
return controlValue;
|
|
@@ -2996,7 +3028,7 @@ class Midy {
|
|
|
2996
3028
|
handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
|
|
2997
3029
|
const channelNumber = data[4];
|
|
2998
3030
|
const channel = this.channels[channelNumber];
|
|
2999
|
-
if (channel.isDrum)
|
|
3031
|
+
if (!channel.isDrum)
|
|
3000
3032
|
return;
|
|
3001
3033
|
const keyNumber = data[5];
|
|
3002
3034
|
const table = channel.keyBasedInstrumentControlTable;
|
|
@@ -3006,7 +3038,7 @@ class Midy {
|
|
|
3006
3038
|
const index = keyNumber * 128 + controllerType;
|
|
3007
3039
|
table[index] = value;
|
|
3008
3040
|
}
|
|
3009
|
-
this.
|
|
3041
|
+
this.setChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
|
|
3010
3042
|
}
|
|
3011
3043
|
handleSysEx(data, scheduleTime) {
|
|
3012
3044
|
switch (data[0]) {
|
|
@@ -3044,6 +3076,7 @@ Object.defineProperty(Midy, "channelSettings", {
|
|
|
3044
3076
|
configurable: true,
|
|
3045
3077
|
writable: true,
|
|
3046
3078
|
value: {
|
|
3079
|
+
scheduleIndex: 0,
|
|
3047
3080
|
detune: 0,
|
|
3048
3081
|
programNumber: 0,
|
|
3049
3082
|
bank: 121 * 128,
|