@marmooo/midy 0.4.1 → 0.4.3
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 +13 -0
- package/esm/midy-GM1.d.ts +10 -8
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +223 -125
- package/esm/midy-GM2.d.ts +10 -8
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +276 -169
- package/esm/midy-GMLite.d.ts +10 -8
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +219 -123
- package/esm/midy.d.ts +12 -8
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +327 -189
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +10 -8
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +223 -125
- package/script/midy-GM2.d.ts +10 -8
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +276 -169
- package/script/midy-GMLite.d.ts +10 -8
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +219 -123
- package/script/midy.d.ts +12 -8
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +327 -189
package/esm/midy-GMLite.js
CHANGED
|
@@ -14,23 +14,23 @@ class Note {
|
|
|
14
14
|
writable: true,
|
|
15
15
|
value: void 0
|
|
16
16
|
});
|
|
17
|
-
Object.defineProperty(this, "
|
|
17
|
+
Object.defineProperty(this, "adjustedBaseFreq", {
|
|
18
18
|
enumerable: true,
|
|
19
19
|
configurable: true,
|
|
20
20
|
writable: true,
|
|
21
|
-
value:
|
|
21
|
+
value: 20000
|
|
22
22
|
});
|
|
23
|
-
Object.defineProperty(this, "
|
|
23
|
+
Object.defineProperty(this, "index", {
|
|
24
24
|
enumerable: true,
|
|
25
25
|
configurable: true,
|
|
26
26
|
writable: true,
|
|
27
|
-
value:
|
|
27
|
+
value: -1
|
|
28
28
|
});
|
|
29
|
-
Object.defineProperty(this, "
|
|
29
|
+
Object.defineProperty(this, "ending", {
|
|
30
30
|
enumerable: true,
|
|
31
31
|
configurable: true,
|
|
32
32
|
writable: true,
|
|
33
|
-
value:
|
|
33
|
+
value: false
|
|
34
34
|
});
|
|
35
35
|
Object.defineProperty(this, "bufferSource", {
|
|
36
36
|
enumerable: true,
|
|
@@ -77,6 +77,9 @@ class Note {
|
|
|
77
77
|
this.noteNumber = noteNumber;
|
|
78
78
|
this.velocity = velocity;
|
|
79
79
|
this.startTime = startTime;
|
|
80
|
+
this.ready = new Promise((resolve) => {
|
|
81
|
+
this.resolveReady = resolve;
|
|
82
|
+
});
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
const drumExclusiveClasses = new Uint8Array(128);
|
|
@@ -163,8 +166,14 @@ const pitchEnvelopeKeys = [
|
|
|
163
166
|
"playbackRate",
|
|
164
167
|
];
|
|
165
168
|
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
166
|
-
|
|
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)));
|
|
174
|
+
export class MidyGMLite extends EventTarget {
|
|
167
175
|
constructor(audioContext) {
|
|
176
|
+
super();
|
|
168
177
|
Object.defineProperty(this, "mode", {
|
|
169
178
|
enumerable: true,
|
|
170
179
|
configurable: true,
|
|
@@ -279,6 +288,26 @@ export class MidyGMLite {
|
|
|
279
288
|
writable: true,
|
|
280
289
|
value: false
|
|
281
290
|
});
|
|
291
|
+
Object.defineProperty(this, "totalTimeEventTypes", {
|
|
292
|
+
enumerable: true,
|
|
293
|
+
configurable: true,
|
|
294
|
+
writable: true,
|
|
295
|
+
value: new Set([
|
|
296
|
+
"noteOff",
|
|
297
|
+
])
|
|
298
|
+
});
|
|
299
|
+
Object.defineProperty(this, "tempo", {
|
|
300
|
+
enumerable: true,
|
|
301
|
+
configurable: true,
|
|
302
|
+
writable: true,
|
|
303
|
+
value: 1
|
|
304
|
+
});
|
|
305
|
+
Object.defineProperty(this, "loop", {
|
|
306
|
+
enumerable: true,
|
|
307
|
+
configurable: true,
|
|
308
|
+
writable: true,
|
|
309
|
+
value: false
|
|
310
|
+
});
|
|
282
311
|
Object.defineProperty(this, "playPromise", {
|
|
283
312
|
enumerable: true,
|
|
284
313
|
configurable: true,
|
|
@@ -387,13 +416,13 @@ export class MidyGMLite {
|
|
|
387
416
|
this.totalTime = this.calcTotalTime();
|
|
388
417
|
}
|
|
389
418
|
cacheVoiceIds() {
|
|
390
|
-
const timeline = this
|
|
419
|
+
const { channels, timeline, voiceCounter } = this;
|
|
391
420
|
for (let i = 0; i < timeline.length; i++) {
|
|
392
421
|
const event = timeline[i];
|
|
393
422
|
switch (event.type) {
|
|
394
423
|
case "noteOn": {
|
|
395
|
-
const audioBufferId = this.getVoiceId(
|
|
396
|
-
|
|
424
|
+
const audioBufferId = this.getVoiceId(channels[event.channel], event.noteNumber, event.velocity);
|
|
425
|
+
voiceCounter.set(audioBufferId, (voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
397
426
|
break;
|
|
398
427
|
}
|
|
399
428
|
case "controller":
|
|
@@ -408,9 +437,9 @@ export class MidyGMLite {
|
|
|
408
437
|
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
409
438
|
}
|
|
410
439
|
}
|
|
411
|
-
for (const [audioBufferId, count] of
|
|
440
|
+
for (const [audioBufferId, count] of voiceCounter) {
|
|
412
441
|
if (count === 1)
|
|
413
|
-
|
|
442
|
+
voiceCounter.delete(audioBufferId);
|
|
414
443
|
}
|
|
415
444
|
this.GM1SystemOn();
|
|
416
445
|
}
|
|
@@ -419,7 +448,12 @@ export class MidyGMLite {
|
|
|
419
448
|
const bankTable = this.soundFontTable[programNumber];
|
|
420
449
|
if (!bankTable)
|
|
421
450
|
return;
|
|
422
|
-
|
|
451
|
+
let bank = channel.isDrum ? 128 : 0;
|
|
452
|
+
if (bankTable[bank] === undefined) {
|
|
453
|
+
if (channel.isDrum)
|
|
454
|
+
return;
|
|
455
|
+
bank = 0;
|
|
456
|
+
}
|
|
423
457
|
const soundFontIndex = bankTable[bank];
|
|
424
458
|
if (soundFontIndex === undefined)
|
|
425
459
|
return;
|
|
@@ -474,19 +508,21 @@ export class MidyGMLite {
|
|
|
474
508
|
}
|
|
475
509
|
return bufferSource;
|
|
476
510
|
}
|
|
477
|
-
|
|
511
|
+
scheduleTimelineEvents(scheduleTime, queueIndex) {
|
|
478
512
|
const timeOffset = this.resumeTime - this.startTime;
|
|
479
513
|
const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
|
|
480
514
|
const schedulingOffset = this.startDelay - timeOffset;
|
|
481
515
|
const timeline = this.timeline;
|
|
516
|
+
const inverseTempo = 1 / this.tempo;
|
|
482
517
|
while (queueIndex < timeline.length) {
|
|
483
518
|
const event = timeline[queueIndex];
|
|
484
|
-
|
|
519
|
+
const t = event.startTime * inverseTempo;
|
|
520
|
+
if (lookAheadCheckTime < t)
|
|
485
521
|
break;
|
|
486
|
-
const startTime =
|
|
522
|
+
const startTime = t + schedulingOffset;
|
|
487
523
|
switch (event.type) {
|
|
488
524
|
case "noteOn":
|
|
489
|
-
|
|
525
|
+
this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
490
526
|
break;
|
|
491
527
|
case "noteOff": {
|
|
492
528
|
this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
@@ -509,8 +545,10 @@ export class MidyGMLite {
|
|
|
509
545
|
return queueIndex;
|
|
510
546
|
}
|
|
511
547
|
getQueueIndex(second) {
|
|
512
|
-
|
|
513
|
-
|
|
548
|
+
const timeline = this.timeline;
|
|
549
|
+
const inverseTempo = 1 / this.tempo;
|
|
550
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
551
|
+
if (second <= timeline[i].startTime * inverseTempo) {
|
|
514
552
|
return i;
|
|
515
553
|
}
|
|
516
554
|
}
|
|
@@ -521,79 +559,112 @@ export class MidyGMLite {
|
|
|
521
559
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
522
560
|
this.voiceCache.clear();
|
|
523
561
|
this.realtimeVoiceCache.clear();
|
|
524
|
-
|
|
525
|
-
|
|
562
|
+
const channels = this.channels;
|
|
563
|
+
for (let i = 0; i < channels.length; i++) {
|
|
564
|
+
channels[i].scheduledNotes = [];
|
|
526
565
|
this.resetChannelStates(i);
|
|
527
566
|
}
|
|
528
567
|
}
|
|
529
568
|
updateStates(queueIndex, nextQueueIndex) {
|
|
569
|
+
const { timeline, resumeTime } = this;
|
|
570
|
+
const inverseTempo = 1 / this.tempo;
|
|
571
|
+
const now = this.audioContext.currentTime;
|
|
530
572
|
if (nextQueueIndex < queueIndex)
|
|
531
573
|
queueIndex = 0;
|
|
532
574
|
for (let i = queueIndex; i < nextQueueIndex; i++) {
|
|
533
|
-
const event =
|
|
575
|
+
const event = timeline[i];
|
|
534
576
|
switch (event.type) {
|
|
535
577
|
case "controller":
|
|
536
|
-
this.setControlChange(event.channel, event.controllerType, event.value,
|
|
578
|
+
this.setControlChange(event.channel, event.controllerType, event.value, now - resumeTime + event.startTime * inverseTempo);
|
|
537
579
|
break;
|
|
538
580
|
case "programChange":
|
|
539
|
-
this.setProgramChange(event.channel, event.programNumber,
|
|
581
|
+
this.setProgramChange(event.channel, event.programNumber, now - resumeTime + event.startTime * inverseTempo);
|
|
540
582
|
break;
|
|
541
583
|
case "pitchBend":
|
|
542
|
-
this.setPitchBend(event.channel, event.value + 8192,
|
|
584
|
+
this.setPitchBend(event.channel, event.value + 8192, now - resumeTime + event.startTime * inverseTempo);
|
|
543
585
|
break;
|
|
544
586
|
case "sysEx":
|
|
545
|
-
this.handleSysEx(event.data,
|
|
587
|
+
this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
|
|
546
588
|
}
|
|
547
589
|
}
|
|
548
590
|
}
|
|
549
591
|
async playNotes() {
|
|
550
|
-
|
|
551
|
-
|
|
592
|
+
const audioContext = this.audioContext;
|
|
593
|
+
if (audioContext.state === "suspended") {
|
|
594
|
+
await audioContext.resume();
|
|
552
595
|
}
|
|
596
|
+
const paused = this.isPaused;
|
|
553
597
|
this.isPlaying = true;
|
|
554
598
|
this.isPaused = false;
|
|
555
|
-
this.startTime =
|
|
599
|
+
this.startTime = audioContext.currentTime;
|
|
600
|
+
if (paused) {
|
|
601
|
+
this.dispatchEvent(new Event("resumed"));
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
this.dispatchEvent(new Event("started"));
|
|
605
|
+
}
|
|
556
606
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
557
|
-
let
|
|
607
|
+
let exitReason;
|
|
558
608
|
this.notePromises = [];
|
|
559
|
-
while (
|
|
560
|
-
const now =
|
|
609
|
+
while (true) {
|
|
610
|
+
const now = audioContext.currentTime;
|
|
611
|
+
if (this.totalTime < this.currentTime() ||
|
|
612
|
+
this.timeline.length <= queueIndex) {
|
|
613
|
+
await this.stopNotes(0, true, now);
|
|
614
|
+
if (this.loop) {
|
|
615
|
+
this.resetAllStates();
|
|
616
|
+
this.startTime = audioContext.currentTime;
|
|
617
|
+
this.resumeTime = 0;
|
|
618
|
+
queueIndex = 0;
|
|
619
|
+
this.dispatchEvent(new Event("looped"));
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
await audioContext.suspend();
|
|
624
|
+
exitReason = "ended";
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
561
628
|
if (this.isPausing) {
|
|
562
629
|
await this.stopNotes(0, true, now);
|
|
563
|
-
await
|
|
564
|
-
this.
|
|
630
|
+
await audioContext.suspend();
|
|
631
|
+
this.isPausing = false;
|
|
632
|
+
exitReason = "paused";
|
|
565
633
|
break;
|
|
566
634
|
}
|
|
567
635
|
else if (this.isStopping) {
|
|
568
636
|
await this.stopNotes(0, true, now);
|
|
569
|
-
await
|
|
570
|
-
|
|
637
|
+
await audioContext.suspend();
|
|
638
|
+
this.isStopping = false;
|
|
639
|
+
exitReason = "stopped";
|
|
571
640
|
break;
|
|
572
641
|
}
|
|
573
642
|
else if (this.isSeeking) {
|
|
574
|
-
|
|
575
|
-
this.startTime =
|
|
643
|
+
this.stopNotes(0, true, now);
|
|
644
|
+
this.startTime = audioContext.currentTime;
|
|
576
645
|
const nextQueueIndex = this.getQueueIndex(this.resumeTime);
|
|
577
646
|
this.updateStates(queueIndex, nextQueueIndex);
|
|
578
647
|
queueIndex = nextQueueIndex;
|
|
579
648
|
this.isSeeking = false;
|
|
649
|
+
this.dispatchEvent(new Event("seeked"));
|
|
580
650
|
continue;
|
|
581
651
|
}
|
|
582
|
-
queueIndex =
|
|
652
|
+
queueIndex = this.scheduleTimelineEvents(now, queueIndex);
|
|
583
653
|
const waitTime = now + this.noteCheckInterval;
|
|
584
654
|
await this.scheduleTask(() => { }, waitTime);
|
|
585
655
|
}
|
|
586
|
-
if (
|
|
587
|
-
const now = this.audioContext.currentTime;
|
|
588
|
-
await this.stopNotes(0, true, now);
|
|
589
|
-
await this.audioContext.suspend();
|
|
590
|
-
finished = true;
|
|
591
|
-
}
|
|
592
|
-
if (finished) {
|
|
593
|
-
this.notePromises = [];
|
|
656
|
+
if (exitReason !== "paused") {
|
|
594
657
|
this.resetAllStates();
|
|
595
658
|
}
|
|
596
659
|
this.isPlaying = false;
|
|
660
|
+
if (exitReason === "paused") {
|
|
661
|
+
this.isPaused = true;
|
|
662
|
+
this.dispatchEvent(new Event("paused"));
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
this.isPaused = false;
|
|
666
|
+
this.dispatchEvent(new Event(exitReason));
|
|
667
|
+
}
|
|
597
668
|
}
|
|
598
669
|
ticksToSecond(ticks, secondsPerBeat) {
|
|
599
670
|
return ticks * secondsPerBeat / this.ticksPerBeat;
|
|
@@ -680,11 +751,13 @@ export class MidyGMLite {
|
|
|
680
751
|
return Promise.all(promises);
|
|
681
752
|
}
|
|
682
753
|
stopNotes(velocity, force, scheduleTime) {
|
|
683
|
-
const
|
|
684
|
-
for (let i = 0; i <
|
|
685
|
-
|
|
754
|
+
const channels = this.channels;
|
|
755
|
+
for (let i = 0; i < channels.length; i++) {
|
|
756
|
+
this.stopChannelNotes(i, velocity, force, scheduleTime);
|
|
686
757
|
}
|
|
687
|
-
|
|
758
|
+
const stopPromise = Promise.all(this.notePromises);
|
|
759
|
+
this.notePromises = [];
|
|
760
|
+
return stopPromise;
|
|
688
761
|
}
|
|
689
762
|
async start() {
|
|
690
763
|
if (this.isPlaying || this.isPaused)
|
|
@@ -700,24 +773,20 @@ export class MidyGMLite {
|
|
|
700
773
|
return;
|
|
701
774
|
this.isStopping = true;
|
|
702
775
|
await this.playPromise;
|
|
703
|
-
this.isStopping = false;
|
|
704
776
|
}
|
|
705
777
|
async pause() {
|
|
706
778
|
if (!this.isPlaying || this.isPaused)
|
|
707
779
|
return;
|
|
708
780
|
const now = this.audioContext.currentTime;
|
|
709
|
-
this.resumeTime = now
|
|
781
|
+
this.resumeTime = now + this.resumeTime - this.startTime;
|
|
710
782
|
this.isPausing = true;
|
|
711
783
|
await this.playPromise;
|
|
712
|
-
this.isPausing = false;
|
|
713
|
-
this.isPaused = true;
|
|
714
784
|
}
|
|
715
785
|
async resume() {
|
|
716
786
|
if (!this.isPaused)
|
|
717
787
|
return;
|
|
718
788
|
this.playPromise = this.playNotes();
|
|
719
789
|
await this.playPromise;
|
|
720
|
-
this.isPaused = false;
|
|
721
790
|
}
|
|
722
791
|
seekTo(second) {
|
|
723
792
|
this.resumeTime = second;
|
|
@@ -726,11 +795,17 @@ export class MidyGMLite {
|
|
|
726
795
|
}
|
|
727
796
|
}
|
|
728
797
|
calcTotalTime() {
|
|
798
|
+
const totalTimeEventTypes = this.totalTimeEventTypes;
|
|
799
|
+
const timeline = this.timeline;
|
|
800
|
+
const inverseTempo = 1 / this.tempo;
|
|
729
801
|
let totalTime = 0;
|
|
730
|
-
for (let i = 0; i <
|
|
731
|
-
const event =
|
|
732
|
-
if (
|
|
733
|
-
|
|
802
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
803
|
+
const event = timeline[i];
|
|
804
|
+
if (!totalTimeEventTypes.has(event.type))
|
|
805
|
+
continue;
|
|
806
|
+
const t = event.startTime * inverseTempo;
|
|
807
|
+
if (totalTime < t)
|
|
808
|
+
totalTime = t;
|
|
734
809
|
}
|
|
735
810
|
return totalTime + this.startDelay;
|
|
736
811
|
}
|
|
@@ -740,19 +815,23 @@ export class MidyGMLite {
|
|
|
740
815
|
const now = this.audioContext.currentTime;
|
|
741
816
|
return now + this.resumeTime - this.startTime;
|
|
742
817
|
}
|
|
743
|
-
processScheduledNotes(channel, callback) {
|
|
818
|
+
async processScheduledNotes(channel, callback) {
|
|
744
819
|
const scheduledNotes = channel.scheduledNotes;
|
|
820
|
+
const tasks = [];
|
|
745
821
|
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
746
822
|
const note = scheduledNotes[i];
|
|
747
823
|
if (!note)
|
|
748
824
|
continue;
|
|
749
825
|
if (note.ending)
|
|
750
826
|
continue;
|
|
751
|
-
callback(note);
|
|
827
|
+
const task = note.ready.then(() => callback(note));
|
|
828
|
+
tasks.push(task);
|
|
752
829
|
}
|
|
830
|
+
await Promise.all(tasks);
|
|
753
831
|
}
|
|
754
|
-
processActiveNotes(channel, scheduleTime, callback) {
|
|
832
|
+
async processActiveNotes(channel, scheduleTime, callback) {
|
|
755
833
|
const scheduledNotes = channel.scheduledNotes;
|
|
834
|
+
const tasks = [];
|
|
756
835
|
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
757
836
|
const note = scheduledNotes[i];
|
|
758
837
|
if (!note)
|
|
@@ -761,11 +840,10 @@ export class MidyGMLite {
|
|
|
761
840
|
continue;
|
|
762
841
|
if (scheduleTime < note.startTime)
|
|
763
842
|
break;
|
|
764
|
-
callback(note);
|
|
843
|
+
const task = note.ready.then(() => callback(note));
|
|
844
|
+
tasks.push(task);
|
|
765
845
|
}
|
|
766
|
-
|
|
767
|
-
cbToRatio(cb) {
|
|
768
|
-
return Math.pow(10, cb / 200);
|
|
846
|
+
await Promise.all(tasks);
|
|
769
847
|
}
|
|
770
848
|
rateToCent(rate) {
|
|
771
849
|
return 1200 * Math.log2(rate);
|
|
@@ -793,26 +871,26 @@ export class MidyGMLite {
|
|
|
793
871
|
}
|
|
794
872
|
setVolumeEnvelope(note, scheduleTime) {
|
|
795
873
|
const { voiceParams, startTime } = note;
|
|
796
|
-
const attackVolume =
|
|
874
|
+
const attackVolume = cbToRatio(-voiceParams.initialAttenuation);
|
|
797
875
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
798
876
|
const volDelay = startTime + voiceParams.volDelay;
|
|
799
877
|
const volAttack = volDelay + voiceParams.volAttack;
|
|
800
878
|
const volHold = volAttack + voiceParams.volHold;
|
|
801
|
-
const
|
|
879
|
+
const decayDuration = voiceParams.volDecay;
|
|
802
880
|
note.volumeEnvelopeNode.gain
|
|
803
881
|
.cancelScheduledValues(scheduleTime)
|
|
804
882
|
.setValueAtTime(0, startTime)
|
|
805
|
-
.setValueAtTime(
|
|
806
|
-
.
|
|
883
|
+
.setValueAtTime(0, volDelay)
|
|
884
|
+
.linearRampToValueAtTime(attackVolume, volAttack)
|
|
807
885
|
.setValueAtTime(attackVolume, volHold)
|
|
808
|
-
.
|
|
886
|
+
.setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
|
|
809
887
|
}
|
|
810
888
|
setPitchEnvelope(note, scheduleTime) {
|
|
811
889
|
const { voiceParams } = note;
|
|
812
890
|
const baseRate = voiceParams.playbackRate;
|
|
813
891
|
note.bufferSource.playbackRate
|
|
814
892
|
.cancelScheduledValues(scheduleTime)
|
|
815
|
-
.setValueAtTime(baseRate,
|
|
893
|
+
.setValueAtTime(baseRate, note.startTime);
|
|
816
894
|
const modEnvToPitch = voiceParams.modEnvToPitch;
|
|
817
895
|
if (modEnvToPitch === 0)
|
|
818
896
|
return;
|
|
@@ -822,12 +900,12 @@ export class MidyGMLite {
|
|
|
822
900
|
const modDelay = note.startTime + voiceParams.modDelay;
|
|
823
901
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
824
902
|
const modHold = modAttack + voiceParams.modHold;
|
|
825
|
-
const
|
|
903
|
+
const decayDuration = voiceParams.modDecay;
|
|
826
904
|
note.bufferSource.playbackRate
|
|
827
905
|
.setValueAtTime(baseRate, modDelay)
|
|
828
|
-
.
|
|
906
|
+
.linearRampToValueAtTime(peekRate, modAttack)
|
|
829
907
|
.setValueAtTime(peekRate, modHold)
|
|
830
|
-
.
|
|
908
|
+
.setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
|
|
831
909
|
}
|
|
832
910
|
clampCutoffFrequency(frequency) {
|
|
833
911
|
const minFrequency = 20; // min Hz of initialFilterFc
|
|
@@ -836,36 +914,42 @@ export class MidyGMLite {
|
|
|
836
914
|
}
|
|
837
915
|
setFilterEnvelope(note, scheduleTime) {
|
|
838
916
|
const { voiceParams, startTime } = note;
|
|
839
|
-
const
|
|
840
|
-
const
|
|
841
|
-
const
|
|
842
|
-
|
|
917
|
+
const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
|
|
918
|
+
const baseCent = voiceParams.initialFilterFc;
|
|
919
|
+
const peekCent = baseCent + modEnvToFilterFc;
|
|
920
|
+
const sustainCent = baseCent +
|
|
921
|
+
modEnvToFilterFc * (1 - voiceParams.modSustain);
|
|
922
|
+
const baseFreq = this.centToHz(baseCent);
|
|
923
|
+
const peekFreq = this.centToHz(peekCent);
|
|
924
|
+
const sustainFreq = this.centToHz(sustainCent);
|
|
843
925
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
844
926
|
const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
|
|
845
927
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
846
928
|
const modDelay = startTime + voiceParams.modDelay;
|
|
847
929
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
848
930
|
const modHold = modAttack + voiceParams.modHold;
|
|
849
|
-
const
|
|
931
|
+
const decayDuration = voiceParams.modDecay;
|
|
932
|
+
note.adjustedBaseFreq = adjustedBaseFreq;
|
|
850
933
|
note.filterNode.frequency
|
|
851
934
|
.cancelScheduledValues(scheduleTime)
|
|
852
935
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
853
936
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
854
|
-
.
|
|
937
|
+
.linearRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
855
938
|
.setValueAtTime(adjustedPeekFreq, modHold)
|
|
856
|
-
.
|
|
939
|
+
.setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
|
|
857
940
|
}
|
|
858
941
|
startModulation(channel, note, scheduleTime) {
|
|
942
|
+
const audioContext = this.audioContext;
|
|
859
943
|
const { voiceParams } = note;
|
|
860
|
-
note.modulationLFO = new OscillatorNode(
|
|
944
|
+
note.modulationLFO = new OscillatorNode(audioContext, {
|
|
861
945
|
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
862
946
|
});
|
|
863
|
-
note.filterDepth = new GainNode(
|
|
947
|
+
note.filterDepth = new GainNode(audioContext, {
|
|
864
948
|
gain: voiceParams.modLfoToFilterFc,
|
|
865
949
|
});
|
|
866
|
-
note.modulationDepth = new GainNode(
|
|
950
|
+
note.modulationDepth = new GainNode(audioContext);
|
|
867
951
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
868
|
-
note.volumeDepth = new GainNode(
|
|
952
|
+
note.volumeDepth = new GainNode(audioContext);
|
|
869
953
|
this.setModLfoToVolume(note, scheduleTime);
|
|
870
954
|
note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
|
|
871
955
|
note.modulationLFO.connect(note.filterDepth);
|
|
@@ -904,7 +988,8 @@ export class MidyGMLite {
|
|
|
904
988
|
}
|
|
905
989
|
}
|
|
906
990
|
async setNoteAudioNode(channel, note, realtime) {
|
|
907
|
-
const
|
|
991
|
+
const audioContext = this.audioContext;
|
|
992
|
+
const now = audioContext.currentTime;
|
|
908
993
|
const { noteNumber, velocity, startTime } = note;
|
|
909
994
|
const state = channel.state;
|
|
910
995
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
@@ -912,8 +997,8 @@ export class MidyGMLite {
|
|
|
912
997
|
note.voiceParams = voiceParams;
|
|
913
998
|
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
914
999
|
note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
|
|
915
|
-
note.volumeEnvelopeNode = new GainNode(
|
|
916
|
-
note.filterNode = new BiquadFilterNode(
|
|
1000
|
+
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
1001
|
+
note.filterNode = new BiquadFilterNode(audioContext, {
|
|
917
1002
|
type: "lowpass",
|
|
918
1003
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
919
1004
|
});
|
|
@@ -988,7 +1073,12 @@ export class MidyGMLite {
|
|
|
988
1073
|
const bankTable = this.soundFontTable[programNumber];
|
|
989
1074
|
if (!bankTable)
|
|
990
1075
|
return;
|
|
991
|
-
|
|
1076
|
+
let bank = channel.isDrum ? 128 : 0;
|
|
1077
|
+
if (bankTable[bank] === undefined) {
|
|
1078
|
+
if (channel.isDrum)
|
|
1079
|
+
return;
|
|
1080
|
+
bank = 0;
|
|
1081
|
+
}
|
|
992
1082
|
const soundFontIndex = bankTable[bank];
|
|
993
1083
|
if (soundFontIndex === undefined)
|
|
994
1084
|
return;
|
|
@@ -998,11 +1088,7 @@ export class MidyGMLite {
|
|
|
998
1088
|
return;
|
|
999
1089
|
await this.setNoteAudioNode(channel, note, realtime);
|
|
1000
1090
|
this.setNoteRouting(channelNumber, note, startTime);
|
|
1001
|
-
note.
|
|
1002
|
-
const off = note.offEvent;
|
|
1003
|
-
if (off) {
|
|
1004
|
-
this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
|
|
1005
|
-
}
|
|
1091
|
+
note.resolveReady();
|
|
1006
1092
|
}
|
|
1007
1093
|
disconnectNote(note) {
|
|
1008
1094
|
note.bufferSource.disconnect();
|
|
@@ -1016,27 +1102,27 @@ export class MidyGMLite {
|
|
|
1016
1102
|
}
|
|
1017
1103
|
releaseNote(channel, note, endTime) {
|
|
1018
1104
|
endTime ??= this.audioContext.currentTime;
|
|
1019
|
-
const
|
|
1105
|
+
const duration = note.voiceParams.volRelease;
|
|
1106
|
+
const volRelease = endTime + duration;
|
|
1020
1107
|
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1021
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1022
1108
|
note.filterNode.frequency
|
|
1023
1109
|
.cancelScheduledValues(endTime)
|
|
1024
|
-
.linearRampToValueAtTime(
|
|
1110
|
+
.linearRampToValueAtTime(note.adjustedBaseFreq, modRelease);
|
|
1025
1111
|
note.volumeEnvelopeNode.gain
|
|
1026
1112
|
.cancelScheduledValues(endTime)
|
|
1027
|
-
.
|
|
1113
|
+
.setTargetAtTime(0, endTime, duration * releaseCurve);
|
|
1028
1114
|
return new Promise((resolve) => {
|
|
1029
1115
|
this.scheduleTask(() => {
|
|
1030
1116
|
const bufferSource = note.bufferSource;
|
|
1031
1117
|
bufferSource.loop = false;
|
|
1032
|
-
bufferSource.stop(
|
|
1118
|
+
bufferSource.stop(volRelease);
|
|
1033
1119
|
this.disconnectNote(note);
|
|
1034
1120
|
channel.scheduledNotes[note.index] = undefined;
|
|
1035
1121
|
resolve();
|
|
1036
|
-
},
|
|
1122
|
+
}, volRelease);
|
|
1037
1123
|
});
|
|
1038
1124
|
}
|
|
1039
|
-
noteOff(channelNumber, noteNumber,
|
|
1125
|
+
noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
1040
1126
|
const channel = this.channels[channelNumber];
|
|
1041
1127
|
if (!force) {
|
|
1042
1128
|
if (channel.isDrum)
|
|
@@ -1048,13 +1134,11 @@ export class MidyGMLite {
|
|
|
1048
1134
|
if (index < 0)
|
|
1049
1135
|
return;
|
|
1050
1136
|
const note = channel.scheduledNotes[index];
|
|
1051
|
-
if (note.pending) {
|
|
1052
|
-
note.offEvent = { velocity, startTime: endTime };
|
|
1053
|
-
return;
|
|
1054
|
-
}
|
|
1055
1137
|
note.ending = true;
|
|
1056
1138
|
this.setNoteIndex(channel, index);
|
|
1057
|
-
const promise =
|
|
1139
|
+
const promise = note.ready.then(() => {
|
|
1140
|
+
return this.releaseNote(channel, note, endTime);
|
|
1141
|
+
});
|
|
1058
1142
|
this.notePromises.push(promise);
|
|
1059
1143
|
return promise;
|
|
1060
1144
|
}
|
|
@@ -1142,7 +1226,8 @@ export class MidyGMLite {
|
|
|
1142
1226
|
}
|
|
1143
1227
|
setPitchBend(channelNumber, value, scheduleTime) {
|
|
1144
1228
|
const channel = this.channels[channelNumber];
|
|
1145
|
-
|
|
1229
|
+
if (!(0 <= scheduleTime))
|
|
1230
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1146
1231
|
const state = channel.state;
|
|
1147
1232
|
const prev = state.pitchWheel * 2 - 1;
|
|
1148
1233
|
const next = (value - 8192) / 8192;
|
|
@@ -1173,7 +1258,7 @@ export class MidyGMLite {
|
|
|
1173
1258
|
}
|
|
1174
1259
|
setModLfoToVolume(note, scheduleTime) {
|
|
1175
1260
|
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
1176
|
-
const baseDepth =
|
|
1261
|
+
const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
1177
1262
|
const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
|
|
1178
1263
|
note.volumeDepth.gain
|
|
1179
1264
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1306,12 +1391,14 @@ export class MidyGMLite {
|
|
|
1306
1391
|
}
|
|
1307
1392
|
setModulationDepth(channelNumber, modulation, scheduleTime) {
|
|
1308
1393
|
const channel = this.channels[channelNumber];
|
|
1309
|
-
|
|
1394
|
+
if (!(0 <= scheduleTime))
|
|
1395
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1310
1396
|
channel.state.modulationDepth = modulation / 127;
|
|
1311
1397
|
this.updateModulation(channel, scheduleTime);
|
|
1312
1398
|
}
|
|
1313
1399
|
setVolume(channelNumber, volume, scheduleTime) {
|
|
1314
|
-
|
|
1400
|
+
if (!(0 <= scheduleTime))
|
|
1401
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1315
1402
|
const channel = this.channels[channelNumber];
|
|
1316
1403
|
channel.state.volume = volume / 127;
|
|
1317
1404
|
this.updateChannelVolume(channel, scheduleTime);
|
|
@@ -1324,13 +1411,15 @@ export class MidyGMLite {
|
|
|
1324
1411
|
};
|
|
1325
1412
|
}
|
|
1326
1413
|
setPan(channelNumber, pan, scheduleTime) {
|
|
1327
|
-
|
|
1414
|
+
if (!(0 <= scheduleTime))
|
|
1415
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1328
1416
|
const channel = this.channels[channelNumber];
|
|
1329
1417
|
channel.state.pan = pan / 127;
|
|
1330
1418
|
this.updateChannelVolume(channel, scheduleTime);
|
|
1331
1419
|
}
|
|
1332
1420
|
setExpression(channelNumber, expression, scheduleTime) {
|
|
1333
|
-
|
|
1421
|
+
if (!(0 <= scheduleTime))
|
|
1422
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1334
1423
|
const channel = this.channels[channelNumber];
|
|
1335
1424
|
channel.state.expression = expression / 127;
|
|
1336
1425
|
this.updateChannelVolume(channel, scheduleTime);
|
|
@@ -1352,7 +1441,8 @@ export class MidyGMLite {
|
|
|
1352
1441
|
}
|
|
1353
1442
|
setSustainPedal(channelNumber, value, scheduleTime) {
|
|
1354
1443
|
const channel = this.channels[channelNumber];
|
|
1355
|
-
|
|
1444
|
+
if (!(0 <= scheduleTime))
|
|
1445
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1356
1446
|
channel.state.sustainPedal = value / 127;
|
|
1357
1447
|
if (64 <= value) {
|
|
1358
1448
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1410,7 +1500,8 @@ export class MidyGMLite {
|
|
|
1410
1500
|
}
|
|
1411
1501
|
setPitchBendRange(channelNumber, value, scheduleTime) {
|
|
1412
1502
|
const channel = this.channels[channelNumber];
|
|
1413
|
-
|
|
1503
|
+
if (!(0 <= scheduleTime))
|
|
1504
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1414
1505
|
const state = channel.state;
|
|
1415
1506
|
const prev = state.pitchWheelSensitivity;
|
|
1416
1507
|
const next = value / 12800;
|
|
@@ -1420,7 +1511,8 @@ export class MidyGMLite {
|
|
|
1420
1511
|
this.applyVoiceParams(channel, 16, scheduleTime);
|
|
1421
1512
|
}
|
|
1422
1513
|
allSoundOff(channelNumber, _value, scheduleTime) {
|
|
1423
|
-
|
|
1514
|
+
if (!(0 <= scheduleTime))
|
|
1515
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1424
1516
|
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
1425
1517
|
}
|
|
1426
1518
|
resetChannelStates(channelNumber) {
|
|
@@ -1472,7 +1564,8 @@ export class MidyGMLite {
|
|
|
1472
1564
|
}
|
|
1473
1565
|
}
|
|
1474
1566
|
allNotesOff(channelNumber, _value, scheduleTime) {
|
|
1475
|
-
|
|
1567
|
+
if (!(0 <= scheduleTime))
|
|
1568
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1476
1569
|
return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
|
|
1477
1570
|
}
|
|
1478
1571
|
handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
|
|
@@ -1493,14 +1586,16 @@ export class MidyGMLite {
|
|
|
1493
1586
|
}
|
|
1494
1587
|
}
|
|
1495
1588
|
GM1SystemOn(scheduleTime) {
|
|
1496
|
-
|
|
1589
|
+
const channels = this.channels;
|
|
1590
|
+
if (!(0 <= scheduleTime))
|
|
1591
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1497
1592
|
this.mode = "GM1";
|
|
1498
|
-
for (let i = 0; i <
|
|
1593
|
+
for (let i = 0; i < channels.length; i++) {
|
|
1499
1594
|
this.allSoundOff(i, 0, scheduleTime);
|
|
1500
|
-
const channel =
|
|
1595
|
+
const channel = channels[i];
|
|
1501
1596
|
channel.isDrum = false;
|
|
1502
1597
|
}
|
|
1503
|
-
|
|
1598
|
+
channels[9].isDrum = true;
|
|
1504
1599
|
}
|
|
1505
1600
|
handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
|
|
1506
1601
|
switch (data[2]) {
|
|
@@ -1521,7 +1616,8 @@ export class MidyGMLite {
|
|
|
1521
1616
|
this.setMasterVolume(volume, scheduleTime);
|
|
1522
1617
|
}
|
|
1523
1618
|
setMasterVolume(value, scheduleTime) {
|
|
1524
|
-
|
|
1619
|
+
if (!(0 <= scheduleTime))
|
|
1620
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1525
1621
|
this.masterVolume.gain
|
|
1526
1622
|
.cancelScheduledValues(scheduleTime)
|
|
1527
1623
|
.setValueAtTime(value * value, scheduleTime);
|