@marmooo/midy 0.4.2 → 0.4.4
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 +2 -0
- package/esm/midy-GM1.d.ts +12 -6
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +177 -96
- package/esm/midy-GM2.d.ts +17 -10
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +264 -165
- package/esm/midy-GMLite.d.ts +15 -9
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +181 -99
- package/esm/midy.d.ts +56 -7
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +383 -166
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +12 -6
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +177 -96
- package/script/midy-GM2.d.ts +17 -10
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +264 -165
- package/script/midy-GMLite.d.ts +15 -9
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +181 -99
- package/script/midy.d.ts +56 -7
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +383 -166
package/esm/midy-GMLite.js
CHANGED
|
@@ -14,6 +14,12 @@ class Note {
|
|
|
14
14
|
writable: true,
|
|
15
15
|
value: void 0
|
|
16
16
|
});
|
|
17
|
+
Object.defineProperty(this, "adjustedBaseFreq", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: 20000
|
|
22
|
+
});
|
|
17
23
|
Object.defineProperty(this, "index", {
|
|
18
24
|
enumerable: true,
|
|
19
25
|
configurable: true,
|
|
@@ -160,9 +166,24 @@ const pitchEnvelopeKeys = [
|
|
|
160
166
|
"playbackRate",
|
|
161
167
|
];
|
|
162
168
|
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
169
|
+
function cbToRatio(cb) {
|
|
170
|
+
return Math.pow(10, cb / 200);
|
|
171
|
+
}
|
|
172
|
+
const decayCurve = 1 / (-Math.log(cbToRatio(-1000)));
|
|
173
|
+
const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
|
|
163
174
|
export class MidyGMLite extends EventTarget {
|
|
164
175
|
constructor(audioContext) {
|
|
165
176
|
super();
|
|
177
|
+
// https://pmc.ncbi.nlm.nih.gov/articles/PMC4191557/
|
|
178
|
+
// https://pubmed.ncbi.nlm.nih.gov/12488797/
|
|
179
|
+
// Gap detection studies indicate humans detect temporal discontinuities
|
|
180
|
+
// around 2–3 ms. Smoothing over ~4 ms is perceived as continuous.
|
|
181
|
+
Object.defineProperty(this, "perceptualSmoothingTime", {
|
|
182
|
+
enumerable: true,
|
|
183
|
+
configurable: true,
|
|
184
|
+
writable: true,
|
|
185
|
+
value: 0.004
|
|
186
|
+
});
|
|
166
187
|
Object.defineProperty(this, "mode", {
|
|
167
188
|
enumerable: true,
|
|
168
189
|
configurable: true,
|
|
@@ -277,6 +298,20 @@ export class MidyGMLite extends EventTarget {
|
|
|
277
298
|
writable: true,
|
|
278
299
|
value: false
|
|
279
300
|
});
|
|
301
|
+
Object.defineProperty(this, "totalTimeEventTypes", {
|
|
302
|
+
enumerable: true,
|
|
303
|
+
configurable: true,
|
|
304
|
+
writable: true,
|
|
305
|
+
value: new Set([
|
|
306
|
+
"noteOff",
|
|
307
|
+
])
|
|
308
|
+
});
|
|
309
|
+
Object.defineProperty(this, "tempo", {
|
|
310
|
+
enumerable: true,
|
|
311
|
+
configurable: true,
|
|
312
|
+
writable: true,
|
|
313
|
+
value: 1
|
|
314
|
+
});
|
|
280
315
|
Object.defineProperty(this, "loop", {
|
|
281
316
|
enumerable: true,
|
|
282
317
|
configurable: true,
|
|
@@ -391,13 +426,13 @@ export class MidyGMLite extends EventTarget {
|
|
|
391
426
|
this.totalTime = this.calcTotalTime();
|
|
392
427
|
}
|
|
393
428
|
cacheVoiceIds() {
|
|
394
|
-
const timeline = this
|
|
429
|
+
const { channels, timeline, voiceCounter } = this;
|
|
395
430
|
for (let i = 0; i < timeline.length; i++) {
|
|
396
431
|
const event = timeline[i];
|
|
397
432
|
switch (event.type) {
|
|
398
433
|
case "noteOn": {
|
|
399
|
-
const audioBufferId = this.getVoiceId(
|
|
400
|
-
|
|
434
|
+
const audioBufferId = this.getVoiceId(channels[event.channel], event.noteNumber, event.velocity);
|
|
435
|
+
voiceCounter.set(audioBufferId, (voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
401
436
|
break;
|
|
402
437
|
}
|
|
403
438
|
case "controller":
|
|
@@ -412,9 +447,9 @@ export class MidyGMLite extends EventTarget {
|
|
|
412
447
|
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
413
448
|
}
|
|
414
449
|
}
|
|
415
|
-
for (const [audioBufferId, count] of
|
|
450
|
+
for (const [audioBufferId, count] of voiceCounter) {
|
|
416
451
|
if (count === 1)
|
|
417
|
-
|
|
452
|
+
voiceCounter.delete(audioBufferId);
|
|
418
453
|
}
|
|
419
454
|
this.GM1SystemOn();
|
|
420
455
|
}
|
|
@@ -423,7 +458,12 @@ export class MidyGMLite extends EventTarget {
|
|
|
423
458
|
const bankTable = this.soundFontTable[programNumber];
|
|
424
459
|
if (!bankTable)
|
|
425
460
|
return;
|
|
426
|
-
|
|
461
|
+
let bank = channel.isDrum ? 128 : 0;
|
|
462
|
+
if (bankTable[bank] === undefined) {
|
|
463
|
+
if (channel.isDrum)
|
|
464
|
+
return;
|
|
465
|
+
bank = 0;
|
|
466
|
+
}
|
|
427
467
|
const soundFontIndex = bankTable[bank];
|
|
428
468
|
if (soundFontIndex === undefined)
|
|
429
469
|
return;
|
|
@@ -483,11 +523,13 @@ export class MidyGMLite extends EventTarget {
|
|
|
483
523
|
const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
|
|
484
524
|
const schedulingOffset = this.startDelay - timeOffset;
|
|
485
525
|
const timeline = this.timeline;
|
|
526
|
+
const inverseTempo = 1 / this.tempo;
|
|
486
527
|
while (queueIndex < timeline.length) {
|
|
487
528
|
const event = timeline[queueIndex];
|
|
488
|
-
|
|
529
|
+
const t = event.startTime * inverseTempo;
|
|
530
|
+
if (lookAheadCheckTime < t)
|
|
489
531
|
break;
|
|
490
|
-
const startTime =
|
|
532
|
+
const startTime = t + schedulingOffset;
|
|
491
533
|
switch (event.type) {
|
|
492
534
|
case "noteOn":
|
|
493
535
|
this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
@@ -513,8 +555,10 @@ export class MidyGMLite extends EventTarget {
|
|
|
513
555
|
return queueIndex;
|
|
514
556
|
}
|
|
515
557
|
getQueueIndex(second) {
|
|
516
|
-
|
|
517
|
-
|
|
558
|
+
const timeline = this.timeline;
|
|
559
|
+
const inverseTempo = 1 / this.tempo;
|
|
560
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
561
|
+
if (second <= timeline[i].startTime * inverseTempo) {
|
|
518
562
|
return i;
|
|
519
563
|
}
|
|
520
564
|
}
|
|
@@ -525,40 +569,44 @@ export class MidyGMLite extends EventTarget {
|
|
|
525
569
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
526
570
|
this.voiceCache.clear();
|
|
527
571
|
this.realtimeVoiceCache.clear();
|
|
528
|
-
|
|
529
|
-
|
|
572
|
+
const channels = this.channels;
|
|
573
|
+
for (let i = 0; i < channels.length; i++) {
|
|
574
|
+
channels[i].scheduledNotes = [];
|
|
530
575
|
this.resetChannelStates(i);
|
|
531
576
|
}
|
|
532
577
|
}
|
|
533
578
|
updateStates(queueIndex, nextQueueIndex) {
|
|
579
|
+
const { timeline, resumeTime } = this;
|
|
580
|
+
const inverseTempo = 1 / this.tempo;
|
|
534
581
|
const now = this.audioContext.currentTime;
|
|
535
582
|
if (nextQueueIndex < queueIndex)
|
|
536
583
|
queueIndex = 0;
|
|
537
584
|
for (let i = queueIndex; i < nextQueueIndex; i++) {
|
|
538
|
-
const event =
|
|
585
|
+
const event = timeline[i];
|
|
539
586
|
switch (event.type) {
|
|
540
587
|
case "controller":
|
|
541
|
-
this.setControlChange(event.channel, event.controllerType, event.value, now -
|
|
588
|
+
this.setControlChange(event.channel, event.controllerType, event.value, now - resumeTime + event.startTime * inverseTempo);
|
|
542
589
|
break;
|
|
543
590
|
case "programChange":
|
|
544
|
-
this.setProgramChange(event.channel, event.programNumber, now -
|
|
591
|
+
this.setProgramChange(event.channel, event.programNumber, now - resumeTime + event.startTime * inverseTempo);
|
|
545
592
|
break;
|
|
546
593
|
case "pitchBend":
|
|
547
|
-
this.setPitchBend(event.channel, event.value + 8192, now -
|
|
594
|
+
this.setPitchBend(event.channel, event.value + 8192, now - resumeTime + event.startTime * inverseTempo);
|
|
548
595
|
break;
|
|
549
596
|
case "sysEx":
|
|
550
|
-
this.handleSysEx(event.data, now -
|
|
597
|
+
this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
|
|
551
598
|
}
|
|
552
599
|
}
|
|
553
600
|
}
|
|
554
601
|
async playNotes() {
|
|
555
|
-
|
|
556
|
-
|
|
602
|
+
const audioContext = this.audioContext;
|
|
603
|
+
if (audioContext.state === "suspended") {
|
|
604
|
+
await audioContext.resume();
|
|
557
605
|
}
|
|
558
606
|
const paused = this.isPaused;
|
|
559
607
|
this.isPlaying = true;
|
|
560
608
|
this.isPaused = false;
|
|
561
|
-
this.startTime =
|
|
609
|
+
this.startTime = audioContext.currentTime;
|
|
562
610
|
if (paused) {
|
|
563
611
|
this.dispatchEvent(new Event("resumed"));
|
|
564
612
|
}
|
|
@@ -569,42 +617,41 @@ export class MidyGMLite extends EventTarget {
|
|
|
569
617
|
let exitReason;
|
|
570
618
|
this.notePromises = [];
|
|
571
619
|
while (true) {
|
|
572
|
-
const now =
|
|
573
|
-
if (this.
|
|
620
|
+
const now = audioContext.currentTime;
|
|
621
|
+
if (this.totalTime < this.currentTime() ||
|
|
622
|
+
this.timeline.length <= queueIndex) {
|
|
574
623
|
await this.stopNotes(0, true, now);
|
|
575
624
|
if (this.loop) {
|
|
576
|
-
this.notePromises = [];
|
|
577
625
|
this.resetAllStates();
|
|
578
|
-
this.startTime =
|
|
626
|
+
this.startTime = audioContext.currentTime;
|
|
579
627
|
this.resumeTime = 0;
|
|
580
628
|
queueIndex = 0;
|
|
581
629
|
this.dispatchEvent(new Event("looped"));
|
|
582
630
|
continue;
|
|
583
631
|
}
|
|
584
632
|
else {
|
|
585
|
-
await
|
|
633
|
+
await audioContext.suspend();
|
|
586
634
|
exitReason = "ended";
|
|
587
635
|
break;
|
|
588
636
|
}
|
|
589
637
|
}
|
|
590
638
|
if (this.isPausing) {
|
|
591
639
|
await this.stopNotes(0, true, now);
|
|
592
|
-
await
|
|
593
|
-
this.notePromises = [];
|
|
640
|
+
await audioContext.suspend();
|
|
594
641
|
this.isPausing = false;
|
|
595
642
|
exitReason = "paused";
|
|
596
643
|
break;
|
|
597
644
|
}
|
|
598
645
|
else if (this.isStopping) {
|
|
599
646
|
await this.stopNotes(0, true, now);
|
|
600
|
-
await
|
|
647
|
+
await audioContext.suspend();
|
|
601
648
|
this.isStopping = false;
|
|
602
649
|
exitReason = "stopped";
|
|
603
650
|
break;
|
|
604
651
|
}
|
|
605
652
|
else if (this.isSeeking) {
|
|
606
653
|
this.stopNotes(0, true, now);
|
|
607
|
-
this.startTime =
|
|
654
|
+
this.startTime = audioContext.currentTime;
|
|
608
655
|
const nextQueueIndex = this.getQueueIndex(this.resumeTime);
|
|
609
656
|
this.updateStates(queueIndex, nextQueueIndex);
|
|
610
657
|
queueIndex = nextQueueIndex;
|
|
@@ -617,7 +664,6 @@ export class MidyGMLite extends EventTarget {
|
|
|
617
664
|
await this.scheduleTask(() => { }, waitTime);
|
|
618
665
|
}
|
|
619
666
|
if (exitReason !== "paused") {
|
|
620
|
-
this.notePromises = [];
|
|
621
667
|
this.resetAllStates();
|
|
622
668
|
}
|
|
623
669
|
this.isPlaying = false;
|
|
@@ -715,11 +761,13 @@ export class MidyGMLite extends EventTarget {
|
|
|
715
761
|
return Promise.all(promises);
|
|
716
762
|
}
|
|
717
763
|
stopNotes(velocity, force, scheduleTime) {
|
|
718
|
-
const
|
|
719
|
-
for (let i = 0; i <
|
|
720
|
-
|
|
764
|
+
const channels = this.channels;
|
|
765
|
+
for (let i = 0; i < channels.length; i++) {
|
|
766
|
+
this.stopChannelNotes(i, velocity, force, scheduleTime);
|
|
721
767
|
}
|
|
722
|
-
|
|
768
|
+
const stopPromise = Promise.all(this.notePromises);
|
|
769
|
+
this.notePromises = [];
|
|
770
|
+
return stopPromise;
|
|
723
771
|
}
|
|
724
772
|
async start() {
|
|
725
773
|
if (this.isPlaying || this.isPaused)
|
|
@@ -756,12 +804,25 @@ export class MidyGMLite extends EventTarget {
|
|
|
756
804
|
this.isSeeking = true;
|
|
757
805
|
}
|
|
758
806
|
}
|
|
807
|
+
tempoChange(tempo) {
|
|
808
|
+
const timeScale = this.tempo / tempo;
|
|
809
|
+
this.resumeTime = this.resumeTime * timeScale;
|
|
810
|
+
this.tempo = tempo;
|
|
811
|
+
this.totalTime = this.calcTotalTime();
|
|
812
|
+
this.seekTo(this.currentTime() * timeScale);
|
|
813
|
+
}
|
|
759
814
|
calcTotalTime() {
|
|
815
|
+
const totalTimeEventTypes = this.totalTimeEventTypes;
|
|
816
|
+
const timeline = this.timeline;
|
|
817
|
+
const inverseTempo = 1 / this.tempo;
|
|
760
818
|
let totalTime = 0;
|
|
761
|
-
for (let i = 0; i <
|
|
762
|
-
const event =
|
|
763
|
-
if (
|
|
764
|
-
|
|
819
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
820
|
+
const event = timeline[i];
|
|
821
|
+
if (!totalTimeEventTypes.has(event.type))
|
|
822
|
+
continue;
|
|
823
|
+
const t = event.startTime * inverseTempo;
|
|
824
|
+
if (totalTime < t)
|
|
825
|
+
totalTime = t;
|
|
765
826
|
}
|
|
766
827
|
return totalTime + this.startDelay;
|
|
767
828
|
}
|
|
@@ -801,9 +862,6 @@ export class MidyGMLite extends EventTarget {
|
|
|
801
862
|
}
|
|
802
863
|
await Promise.all(tasks);
|
|
803
864
|
}
|
|
804
|
-
cbToRatio(cb) {
|
|
805
|
-
return Math.pow(10, cb / 200);
|
|
806
|
-
}
|
|
807
865
|
rateToCent(rate) {
|
|
808
866
|
return 1200 * Math.log2(rate);
|
|
809
867
|
}
|
|
@@ -820,51 +878,57 @@ export class MidyGMLite extends EventTarget {
|
|
|
820
878
|
}
|
|
821
879
|
updateChannelDetune(channel, scheduleTime) {
|
|
822
880
|
this.processScheduledNotes(channel, (note) => {
|
|
823
|
-
this.
|
|
881
|
+
this.setDetune(channel, note, scheduleTime);
|
|
824
882
|
});
|
|
825
883
|
}
|
|
826
|
-
|
|
827
|
-
note.
|
|
828
|
-
.cancelScheduledValues(scheduleTime)
|
|
829
|
-
.setValueAtTime(channel.detune, scheduleTime);
|
|
884
|
+
calcNoteDetune(channel, note) {
|
|
885
|
+
return channel.detune + note.voiceParams.detune;
|
|
830
886
|
}
|
|
831
887
|
setVolumeEnvelope(note, scheduleTime) {
|
|
832
888
|
const { voiceParams, startTime } = note;
|
|
833
|
-
const attackVolume =
|
|
889
|
+
const attackVolume = cbToRatio(-voiceParams.initialAttenuation);
|
|
834
890
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
835
891
|
const volDelay = startTime + voiceParams.volDelay;
|
|
836
892
|
const volAttack = volDelay + voiceParams.volAttack;
|
|
837
893
|
const volHold = volAttack + voiceParams.volHold;
|
|
838
|
-
const
|
|
894
|
+
const decayDuration = voiceParams.volDecay;
|
|
839
895
|
note.volumeEnvelopeNode.gain
|
|
840
896
|
.cancelScheduledValues(scheduleTime)
|
|
841
897
|
.setValueAtTime(0, startTime)
|
|
842
|
-
.setValueAtTime(1e-6, volDelay)
|
|
898
|
+
.setValueAtTime(1e-6, volDelay)
|
|
843
899
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
844
900
|
.setValueAtTime(attackVolume, volHold)
|
|
845
|
-
.
|
|
901
|
+
.setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
|
|
902
|
+
}
|
|
903
|
+
setDetune(channel, note, scheduleTime) {
|
|
904
|
+
const detune = this.calcNoteDetune(channel, note);
|
|
905
|
+
note.bufferSource.detune
|
|
906
|
+
.cancelScheduledValues(scheduleTime)
|
|
907
|
+
.setValueAtTime(detune, scheduleTime);
|
|
908
|
+
const timeConstant = this.perceptualSmoothingTime / 5; // 99.3% (5 * tau)
|
|
909
|
+
note.bufferSource.detune
|
|
910
|
+
.cancelAndHoldAtTime(scheduleTime)
|
|
911
|
+
.setTargetAtTime(detune, scheduleTime, timeConstant);
|
|
846
912
|
}
|
|
847
913
|
setPitchEnvelope(note, scheduleTime) {
|
|
848
|
-
const { voiceParams } = note;
|
|
914
|
+
const { bufferSource, voiceParams } = note;
|
|
849
915
|
const baseRate = voiceParams.playbackRate;
|
|
850
|
-
|
|
916
|
+
bufferSource.playbackRate
|
|
851
917
|
.cancelScheduledValues(scheduleTime)
|
|
852
918
|
.setValueAtTime(baseRate, scheduleTime);
|
|
853
919
|
const modEnvToPitch = voiceParams.modEnvToPitch;
|
|
854
920
|
if (modEnvToPitch === 0)
|
|
855
921
|
return;
|
|
856
|
-
const
|
|
857
|
-
const peekPitch = basePitch + modEnvToPitch;
|
|
858
|
-
const peekRate = this.centToRate(peekPitch);
|
|
922
|
+
const peekRate = baseRate * this.centToRate(modEnvToPitch);
|
|
859
923
|
const modDelay = note.startTime + voiceParams.modDelay;
|
|
860
924
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
861
925
|
const modHold = modAttack + voiceParams.modHold;
|
|
862
|
-
const
|
|
863
|
-
|
|
926
|
+
const decayDuration = voiceParams.modDecay;
|
|
927
|
+
bufferSource.playbackRate
|
|
864
928
|
.setValueAtTime(baseRate, modDelay)
|
|
865
929
|
.exponentialRampToValueAtTime(peekRate, modAttack)
|
|
866
930
|
.setValueAtTime(peekRate, modHold)
|
|
867
|
-
.
|
|
931
|
+
.setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
|
|
868
932
|
}
|
|
869
933
|
clampCutoffFrequency(frequency) {
|
|
870
934
|
const minFrequency = 20; // min Hz of initialFilterFc
|
|
@@ -873,36 +937,42 @@ export class MidyGMLite extends EventTarget {
|
|
|
873
937
|
}
|
|
874
938
|
setFilterEnvelope(note, scheduleTime) {
|
|
875
939
|
const { voiceParams, startTime } = note;
|
|
876
|
-
const
|
|
877
|
-
const
|
|
878
|
-
const
|
|
879
|
-
|
|
940
|
+
const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
|
|
941
|
+
const baseCent = voiceParams.initialFilterFc;
|
|
942
|
+
const peekCent = baseCent + modEnvToFilterFc;
|
|
943
|
+
const sustainCent = baseCent +
|
|
944
|
+
modEnvToFilterFc * (1 - voiceParams.modSustain);
|
|
945
|
+
const baseFreq = this.centToHz(baseCent);
|
|
946
|
+
const peekFreq = this.centToHz(peekCent);
|
|
947
|
+
const sustainFreq = this.centToHz(sustainCent);
|
|
880
948
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
881
949
|
const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
|
|
882
950
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
883
951
|
const modDelay = startTime + voiceParams.modDelay;
|
|
884
952
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
885
953
|
const modHold = modAttack + voiceParams.modHold;
|
|
886
|
-
const
|
|
954
|
+
const decayDuration = voiceParams.modDecay;
|
|
955
|
+
note.adjustedBaseFreq = adjustedBaseFreq;
|
|
887
956
|
note.filterNode.frequency
|
|
888
957
|
.cancelScheduledValues(scheduleTime)
|
|
889
958
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
890
959
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
891
960
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
892
961
|
.setValueAtTime(adjustedPeekFreq, modHold)
|
|
893
|
-
.
|
|
962
|
+
.setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
|
|
894
963
|
}
|
|
895
964
|
startModulation(channel, note, scheduleTime) {
|
|
965
|
+
const audioContext = this.audioContext;
|
|
896
966
|
const { voiceParams } = note;
|
|
897
|
-
note.modulationLFO = new OscillatorNode(
|
|
967
|
+
note.modulationLFO = new OscillatorNode(audioContext, {
|
|
898
968
|
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
899
969
|
});
|
|
900
|
-
note.filterDepth = new GainNode(
|
|
970
|
+
note.filterDepth = new GainNode(audioContext, {
|
|
901
971
|
gain: voiceParams.modLfoToFilterFc,
|
|
902
972
|
});
|
|
903
|
-
note.modulationDepth = new GainNode(
|
|
973
|
+
note.modulationDepth = new GainNode(audioContext);
|
|
904
974
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
905
|
-
note.volumeDepth = new GainNode(
|
|
975
|
+
note.volumeDepth = new GainNode(audioContext);
|
|
906
976
|
this.setModLfoToVolume(note, scheduleTime);
|
|
907
977
|
note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
|
|
908
978
|
note.modulationLFO.connect(note.filterDepth);
|
|
@@ -941,7 +1011,8 @@ export class MidyGMLite extends EventTarget {
|
|
|
941
1011
|
}
|
|
942
1012
|
}
|
|
943
1013
|
async setNoteAudioNode(channel, note, realtime) {
|
|
944
|
-
const
|
|
1014
|
+
const audioContext = this.audioContext;
|
|
1015
|
+
const now = audioContext.currentTime;
|
|
945
1016
|
const { noteNumber, velocity, startTime } = note;
|
|
946
1017
|
const state = channel.state;
|
|
947
1018
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
@@ -949,15 +1020,15 @@ export class MidyGMLite extends EventTarget {
|
|
|
949
1020
|
note.voiceParams = voiceParams;
|
|
950
1021
|
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
951
1022
|
note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
|
|
952
|
-
note.volumeEnvelopeNode = new GainNode(
|
|
953
|
-
note.filterNode = new BiquadFilterNode(
|
|
1023
|
+
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
1024
|
+
note.filterNode = new BiquadFilterNode(audioContext, {
|
|
954
1025
|
type: "lowpass",
|
|
955
1026
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
956
1027
|
});
|
|
957
1028
|
this.setVolumeEnvelope(note, now);
|
|
958
1029
|
this.setFilterEnvelope(note, now);
|
|
959
1030
|
this.setPitchEnvelope(note, now);
|
|
960
|
-
this.
|
|
1031
|
+
this.setDetune(channel, note, now);
|
|
961
1032
|
if (0 < state.modulationDepthMSB) {
|
|
962
1033
|
this.startModulation(channel, note, now);
|
|
963
1034
|
}
|
|
@@ -1025,7 +1096,12 @@ export class MidyGMLite extends EventTarget {
|
|
|
1025
1096
|
const bankTable = this.soundFontTable[programNumber];
|
|
1026
1097
|
if (!bankTable)
|
|
1027
1098
|
return;
|
|
1028
|
-
|
|
1099
|
+
let bank = channel.isDrum ? 128 : 0;
|
|
1100
|
+
if (bankTable[bank] === undefined) {
|
|
1101
|
+
if (channel.isDrum)
|
|
1102
|
+
return;
|
|
1103
|
+
bank = 0;
|
|
1104
|
+
}
|
|
1029
1105
|
const soundFontIndex = bankTable[bank];
|
|
1030
1106
|
if (soundFontIndex === undefined)
|
|
1031
1107
|
return;
|
|
@@ -1049,27 +1125,26 @@ export class MidyGMLite extends EventTarget {
|
|
|
1049
1125
|
}
|
|
1050
1126
|
releaseNote(channel, note, endTime) {
|
|
1051
1127
|
endTime ??= this.audioContext.currentTime;
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1128
|
+
const volDuration = note.voiceParams.volRelease;
|
|
1129
|
+
const volRelease = endTime + volDuration;
|
|
1055
1130
|
note.filterNode.frequency
|
|
1056
1131
|
.cancelScheduledValues(endTime)
|
|
1057
|
-
.
|
|
1132
|
+
.setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
|
|
1058
1133
|
note.volumeEnvelopeNode.gain
|
|
1059
1134
|
.cancelScheduledValues(endTime)
|
|
1060
|
-
.
|
|
1135
|
+
.setTargetAtTime(0, endTime, volDuration * releaseCurve);
|
|
1061
1136
|
return new Promise((resolve) => {
|
|
1062
1137
|
this.scheduleTask(() => {
|
|
1063
1138
|
const bufferSource = note.bufferSource;
|
|
1064
1139
|
bufferSource.loop = false;
|
|
1065
|
-
bufferSource.stop(
|
|
1140
|
+
bufferSource.stop(volRelease);
|
|
1066
1141
|
this.disconnectNote(note);
|
|
1067
1142
|
channel.scheduledNotes[note.index] = undefined;
|
|
1068
1143
|
resolve();
|
|
1069
|
-
},
|
|
1144
|
+
}, volRelease);
|
|
1070
1145
|
});
|
|
1071
1146
|
}
|
|
1072
|
-
|
|
1147
|
+
noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
1073
1148
|
const channel = this.channels[channelNumber];
|
|
1074
1149
|
if (!force) {
|
|
1075
1150
|
if (channel.isDrum)
|
|
@@ -1205,7 +1280,7 @@ export class MidyGMLite extends EventTarget {
|
|
|
1205
1280
|
}
|
|
1206
1281
|
setModLfoToVolume(note, scheduleTime) {
|
|
1207
1282
|
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
1208
|
-
const baseDepth =
|
|
1283
|
+
const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
1209
1284
|
const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
|
|
1210
1285
|
note.volumeDepth.gain
|
|
1211
1286
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1251,12 +1326,15 @@ export class MidyGMLite extends EventTarget {
|
|
|
1251
1326
|
}
|
|
1252
1327
|
},
|
|
1253
1328
|
freqModLFO: (_channel, note, scheduleTime) => {
|
|
1254
|
-
if (0 < channel.state.
|
|
1329
|
+
if (0 < channel.state.modulationDepthMSB) {
|
|
1255
1330
|
this.setFreqModLFO(note, scheduleTime);
|
|
1256
1331
|
}
|
|
1257
1332
|
},
|
|
1258
1333
|
delayVibLFO: (_channel, _note, _scheduleTime) => { },
|
|
1259
1334
|
freqVibLFO: (_channel, _note, _scheduleTime) => { },
|
|
1335
|
+
detune: (channel, note, scheduleTime) => {
|
|
1336
|
+
this.setDetune(channel, note, scheduleTime);
|
|
1337
|
+
},
|
|
1260
1338
|
};
|
|
1261
1339
|
}
|
|
1262
1340
|
getControllerState(channel, noteNumber, velocity) {
|
|
@@ -1326,7 +1404,8 @@ export class MidyGMLite extends EventTarget {
|
|
|
1326
1404
|
}
|
|
1327
1405
|
}
|
|
1328
1406
|
updateModulation(channel, scheduleTime) {
|
|
1329
|
-
const depth = channel.state.
|
|
1407
|
+
const depth = channel.state.modulationDepthMSB *
|
|
1408
|
+
channel.modulationDepthRange;
|
|
1330
1409
|
this.processScheduledNotes(channel, (note) => {
|
|
1331
1410
|
if (note.modulationDepth) {
|
|
1332
1411
|
note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
|
|
@@ -1336,18 +1415,18 @@ export class MidyGMLite extends EventTarget {
|
|
|
1336
1415
|
}
|
|
1337
1416
|
});
|
|
1338
1417
|
}
|
|
1339
|
-
setModulationDepth(channelNumber,
|
|
1418
|
+
setModulationDepth(channelNumber, value, scheduleTime) {
|
|
1340
1419
|
const channel = this.channels[channelNumber];
|
|
1341
1420
|
if (!(0 <= scheduleTime))
|
|
1342
1421
|
scheduleTime = this.audioContext.currentTime;
|
|
1343
|
-
channel.state.
|
|
1422
|
+
channel.state.modulationDepthMSB = value / 127;
|
|
1344
1423
|
this.updateModulation(channel, scheduleTime);
|
|
1345
1424
|
}
|
|
1346
|
-
setVolume(channelNumber,
|
|
1425
|
+
setVolume(channelNumber, value, scheduleTime) {
|
|
1347
1426
|
if (!(0 <= scheduleTime))
|
|
1348
1427
|
scheduleTime = this.audioContext.currentTime;
|
|
1349
1428
|
const channel = this.channels[channelNumber];
|
|
1350
|
-
channel.state.
|
|
1429
|
+
channel.state.volumeMSB = value / 127;
|
|
1351
1430
|
this.updateChannelVolume(channel, scheduleTime);
|
|
1352
1431
|
}
|
|
1353
1432
|
panToGain(pan) {
|
|
@@ -1357,18 +1436,18 @@ export class MidyGMLite extends EventTarget {
|
|
|
1357
1436
|
gainRight: Math.sin(theta),
|
|
1358
1437
|
};
|
|
1359
1438
|
}
|
|
1360
|
-
setPan(channelNumber,
|
|
1439
|
+
setPan(channelNumber, value, scheduleTime) {
|
|
1361
1440
|
if (!(0 <= scheduleTime))
|
|
1362
1441
|
scheduleTime = this.audioContext.currentTime;
|
|
1363
1442
|
const channel = this.channels[channelNumber];
|
|
1364
|
-
channel.state.
|
|
1443
|
+
channel.state.panMSB = value / 127;
|
|
1365
1444
|
this.updateChannelVolume(channel, scheduleTime);
|
|
1366
1445
|
}
|
|
1367
|
-
setExpression(channelNumber,
|
|
1446
|
+
setExpression(channelNumber, value, scheduleTime) {
|
|
1368
1447
|
if (!(0 <= scheduleTime))
|
|
1369
1448
|
scheduleTime = this.audioContext.currentTime;
|
|
1370
1449
|
const channel = this.channels[channelNumber];
|
|
1371
|
-
channel.state.
|
|
1450
|
+
channel.state.expressionMSB = value / 127;
|
|
1372
1451
|
this.updateChannelVolume(channel, scheduleTime);
|
|
1373
1452
|
}
|
|
1374
1453
|
dataEntryLSB(channelNumber, value, scheduleTime) {
|
|
@@ -1377,14 +1456,14 @@ export class MidyGMLite extends EventTarget {
|
|
|
1377
1456
|
}
|
|
1378
1457
|
updateChannelVolume(channel, scheduleTime) {
|
|
1379
1458
|
const state = channel.state;
|
|
1380
|
-
const
|
|
1381
|
-
const { gainLeft, gainRight } = this.panToGain(state.
|
|
1459
|
+
const gain = state.volumeMSB * state.expressionMSB;
|
|
1460
|
+
const { gainLeft, gainRight } = this.panToGain(state.panMSB);
|
|
1382
1461
|
channel.gainL.gain
|
|
1383
1462
|
.cancelScheduledValues(scheduleTime)
|
|
1384
|
-
.setValueAtTime(
|
|
1463
|
+
.setValueAtTime(gain * gainLeft, scheduleTime);
|
|
1385
1464
|
channel.gainR.gain
|
|
1386
1465
|
.cancelScheduledValues(scheduleTime)
|
|
1387
|
-
.setValueAtTime(
|
|
1466
|
+
.setValueAtTime(gain * gainRight, scheduleTime);
|
|
1388
1467
|
}
|
|
1389
1468
|
setSustainPedal(channelNumber, value, scheduleTime) {
|
|
1390
1469
|
const channel = this.channels[channelNumber];
|
|
@@ -1425,6 +1504,8 @@ export class MidyGMLite extends EventTarget {
|
|
|
1425
1504
|
case 0:
|
|
1426
1505
|
this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
|
|
1427
1506
|
break;
|
|
1507
|
+
case 16383: // NULL
|
|
1508
|
+
break;
|
|
1428
1509
|
default:
|
|
1429
1510
|
console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
|
|
1430
1511
|
}
|
|
@@ -1533,15 +1614,16 @@ export class MidyGMLite extends EventTarget {
|
|
|
1533
1614
|
}
|
|
1534
1615
|
}
|
|
1535
1616
|
GM1SystemOn(scheduleTime) {
|
|
1617
|
+
const channels = this.channels;
|
|
1536
1618
|
if (!(0 <= scheduleTime))
|
|
1537
1619
|
scheduleTime = this.audioContext.currentTime;
|
|
1538
1620
|
this.mode = "GM1";
|
|
1539
|
-
for (let i = 0; i <
|
|
1621
|
+
for (let i = 0; i < channels.length; i++) {
|
|
1540
1622
|
this.allSoundOff(i, 0, scheduleTime);
|
|
1541
|
-
const channel =
|
|
1623
|
+
const channel = channels[i];
|
|
1542
1624
|
channel.isDrum = false;
|
|
1543
1625
|
}
|
|
1544
|
-
|
|
1626
|
+
channels[9].isDrum = true;
|
|
1545
1627
|
}
|
|
1546
1628
|
handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
|
|
1547
1629
|
switch (data[2]) {
|