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