@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-GM1.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
|
// normalized to 0-1 for use with the SF2 modulator model
|
|
@@ -150,8 +153,14 @@ const pitchEnvelopeKeys = [
|
|
|
150
153
|
"playbackRate",
|
|
151
154
|
];
|
|
152
155
|
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
153
|
-
|
|
156
|
+
function cbToRatio(cb) {
|
|
157
|
+
return Math.pow(10, cb / 200);
|
|
158
|
+
}
|
|
159
|
+
const decayCurve = 1 / (-Math.log(cbToRatio(-1000)));
|
|
160
|
+
const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
|
|
161
|
+
export class MidyGM1 extends EventTarget {
|
|
154
162
|
constructor(audioContext) {
|
|
163
|
+
super();
|
|
155
164
|
Object.defineProperty(this, "mode", {
|
|
156
165
|
enumerable: true,
|
|
157
166
|
configurable: true,
|
|
@@ -266,6 +275,26 @@ export class MidyGM1 {
|
|
|
266
275
|
writable: true,
|
|
267
276
|
value: false
|
|
268
277
|
});
|
|
278
|
+
Object.defineProperty(this, "totalTimeEventTypes", {
|
|
279
|
+
enumerable: true,
|
|
280
|
+
configurable: true,
|
|
281
|
+
writable: true,
|
|
282
|
+
value: new Set([
|
|
283
|
+
"noteOff",
|
|
284
|
+
])
|
|
285
|
+
});
|
|
286
|
+
Object.defineProperty(this, "tempo", {
|
|
287
|
+
enumerable: true,
|
|
288
|
+
configurable: true,
|
|
289
|
+
writable: true,
|
|
290
|
+
value: 1
|
|
291
|
+
});
|
|
292
|
+
Object.defineProperty(this, "loop", {
|
|
293
|
+
enumerable: true,
|
|
294
|
+
configurable: true,
|
|
295
|
+
writable: true,
|
|
296
|
+
value: false
|
|
297
|
+
});
|
|
269
298
|
Object.defineProperty(this, "playPromise", {
|
|
270
299
|
enumerable: true,
|
|
271
300
|
configurable: true,
|
|
@@ -368,13 +397,13 @@ export class MidyGM1 {
|
|
|
368
397
|
this.totalTime = this.calcTotalTime();
|
|
369
398
|
}
|
|
370
399
|
cacheVoiceIds() {
|
|
371
|
-
const timeline = this
|
|
400
|
+
const { channels, timeline, voiceCounter } = this;
|
|
372
401
|
for (let i = 0; i < timeline.length; i++) {
|
|
373
402
|
const event = timeline[i];
|
|
374
403
|
switch (event.type) {
|
|
375
404
|
case "noteOn": {
|
|
376
|
-
const audioBufferId = this.getVoiceId(
|
|
377
|
-
|
|
405
|
+
const audioBufferId = this.getVoiceId(channels[event.channel], event.noteNumber, event.velocity);
|
|
406
|
+
voiceCounter.set(audioBufferId, (voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
378
407
|
break;
|
|
379
408
|
}
|
|
380
409
|
case "controller":
|
|
@@ -389,9 +418,9 @@ export class MidyGM1 {
|
|
|
389
418
|
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
390
419
|
}
|
|
391
420
|
}
|
|
392
|
-
for (const [audioBufferId, count] of
|
|
421
|
+
for (const [audioBufferId, count] of voiceCounter) {
|
|
393
422
|
if (count === 1)
|
|
394
|
-
|
|
423
|
+
voiceCounter.delete(audioBufferId);
|
|
395
424
|
}
|
|
396
425
|
this.GM1SystemOn();
|
|
397
426
|
}
|
|
@@ -400,7 +429,12 @@ export class MidyGM1 {
|
|
|
400
429
|
const bankTable = this.soundFontTable[programNumber];
|
|
401
430
|
if (!bankTable)
|
|
402
431
|
return;
|
|
403
|
-
|
|
432
|
+
let bank = channel.isDrum ? 128 : 0;
|
|
433
|
+
if (bankTable[bank] === undefined) {
|
|
434
|
+
if (channel.isDrum)
|
|
435
|
+
return;
|
|
436
|
+
bank = 0;
|
|
437
|
+
}
|
|
404
438
|
const soundFontIndex = bankTable[bank];
|
|
405
439
|
if (soundFontIndex === undefined)
|
|
406
440
|
return;
|
|
@@ -453,19 +487,21 @@ export class MidyGM1 {
|
|
|
453
487
|
}
|
|
454
488
|
return bufferSource;
|
|
455
489
|
}
|
|
456
|
-
|
|
490
|
+
scheduleTimelineEvents(scheduleTime, queueIndex) {
|
|
457
491
|
const timeOffset = this.resumeTime - this.startTime;
|
|
458
492
|
const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
|
|
459
493
|
const schedulingOffset = this.startDelay - timeOffset;
|
|
460
494
|
const timeline = this.timeline;
|
|
495
|
+
const inverseTempo = 1 / this.tempo;
|
|
461
496
|
while (queueIndex < timeline.length) {
|
|
462
497
|
const event = timeline[queueIndex];
|
|
463
|
-
|
|
498
|
+
const t = event.startTime * inverseTempo;
|
|
499
|
+
if (lookAheadCheckTime < t)
|
|
464
500
|
break;
|
|
465
|
-
const startTime =
|
|
501
|
+
const startTime = t + schedulingOffset;
|
|
466
502
|
switch (event.type) {
|
|
467
503
|
case "noteOn":
|
|
468
|
-
|
|
504
|
+
this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
469
505
|
break;
|
|
470
506
|
case "noteOff": {
|
|
471
507
|
this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
@@ -488,8 +524,10 @@ export class MidyGM1 {
|
|
|
488
524
|
return queueIndex;
|
|
489
525
|
}
|
|
490
526
|
getQueueIndex(second) {
|
|
491
|
-
|
|
492
|
-
|
|
527
|
+
const timeline = this.timeline;
|
|
528
|
+
const inverseTempo = 1 / this.tempo;
|
|
529
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
530
|
+
if (second <= timeline[i].startTime * inverseTempo) {
|
|
493
531
|
return i;
|
|
494
532
|
}
|
|
495
533
|
}
|
|
@@ -500,79 +538,112 @@ export class MidyGM1 {
|
|
|
500
538
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
501
539
|
this.voiceCache.clear();
|
|
502
540
|
this.realtimeVoiceCache.clear();
|
|
503
|
-
|
|
504
|
-
|
|
541
|
+
const channels = this.channels;
|
|
542
|
+
for (let i = 0; i < channels.length; i++) {
|
|
543
|
+
channels[i].scheduledNotes = [];
|
|
505
544
|
this.resetChannelStates(i);
|
|
506
545
|
}
|
|
507
546
|
}
|
|
508
547
|
updateStates(queueIndex, nextQueueIndex) {
|
|
548
|
+
const { timeline, resumeTime } = this;
|
|
549
|
+
const inverseTempo = 1 / this.tempo;
|
|
550
|
+
const now = this.audioContext.currentTime;
|
|
509
551
|
if (nextQueueIndex < queueIndex)
|
|
510
552
|
queueIndex = 0;
|
|
511
553
|
for (let i = queueIndex; i < nextQueueIndex; i++) {
|
|
512
|
-
const event =
|
|
554
|
+
const event = timeline[i];
|
|
513
555
|
switch (event.type) {
|
|
514
556
|
case "controller":
|
|
515
|
-
this.setControlChange(event.channel, event.controllerType, event.value,
|
|
557
|
+
this.setControlChange(event.channel, event.controllerType, event.value, now - resumeTime + event.startTime * inverseTempo);
|
|
516
558
|
break;
|
|
517
559
|
case "programChange":
|
|
518
|
-
this.setProgramChange(event.channel, event.programNumber,
|
|
560
|
+
this.setProgramChange(event.channel, event.programNumber, now - resumeTime + event.startTime * inverseTempo);
|
|
519
561
|
break;
|
|
520
562
|
case "pitchBend":
|
|
521
|
-
this.setPitchBend(event.channel, event.value + 8192,
|
|
563
|
+
this.setPitchBend(event.channel, event.value + 8192, now - resumeTime + event.startTime * inverseTempo);
|
|
522
564
|
break;
|
|
523
565
|
case "sysEx":
|
|
524
|
-
this.handleSysEx(event.data,
|
|
566
|
+
this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
|
|
525
567
|
}
|
|
526
568
|
}
|
|
527
569
|
}
|
|
528
570
|
async playNotes() {
|
|
529
|
-
|
|
530
|
-
|
|
571
|
+
const audioContext = this.audioContext;
|
|
572
|
+
if (audioContext.state === "suspended") {
|
|
573
|
+
await audioContext.resume();
|
|
531
574
|
}
|
|
575
|
+
const paused = this.isPaused;
|
|
532
576
|
this.isPlaying = true;
|
|
533
577
|
this.isPaused = false;
|
|
534
|
-
this.startTime =
|
|
578
|
+
this.startTime = audioContext.currentTime;
|
|
579
|
+
if (paused) {
|
|
580
|
+
this.dispatchEvent(new Event("resumed"));
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
this.dispatchEvent(new Event("started"));
|
|
584
|
+
}
|
|
535
585
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
536
|
-
let
|
|
586
|
+
let exitReason;
|
|
537
587
|
this.notePromises = [];
|
|
538
|
-
while (
|
|
539
|
-
const now =
|
|
588
|
+
while (true) {
|
|
589
|
+
const now = audioContext.currentTime;
|
|
590
|
+
if (this.totalTime < this.currentTime() ||
|
|
591
|
+
this.timeline.length <= queueIndex) {
|
|
592
|
+
await this.stopNotes(0, true, now);
|
|
593
|
+
if (this.loop) {
|
|
594
|
+
this.resetAllStates();
|
|
595
|
+
this.startTime = audioContext.currentTime;
|
|
596
|
+
this.resumeTime = 0;
|
|
597
|
+
queueIndex = 0;
|
|
598
|
+
this.dispatchEvent(new Event("looped"));
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
await audioContext.suspend();
|
|
603
|
+
exitReason = "ended";
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
540
607
|
if (this.isPausing) {
|
|
541
608
|
await this.stopNotes(0, true, now);
|
|
542
|
-
await
|
|
543
|
-
this.
|
|
609
|
+
await audioContext.suspend();
|
|
610
|
+
this.isPausing = false;
|
|
611
|
+
exitReason = "paused";
|
|
544
612
|
break;
|
|
545
613
|
}
|
|
546
614
|
else if (this.isStopping) {
|
|
547
615
|
await this.stopNotes(0, true, now);
|
|
548
|
-
await
|
|
549
|
-
|
|
616
|
+
await audioContext.suspend();
|
|
617
|
+
this.isStopping = false;
|
|
618
|
+
exitReason = "stopped";
|
|
550
619
|
break;
|
|
551
620
|
}
|
|
552
621
|
else if (this.isSeeking) {
|
|
553
|
-
|
|
554
|
-
this.startTime =
|
|
622
|
+
this.stopNotes(0, true, now);
|
|
623
|
+
this.startTime = audioContext.currentTime;
|
|
555
624
|
const nextQueueIndex = this.getQueueIndex(this.resumeTime);
|
|
556
625
|
this.updateStates(queueIndex, nextQueueIndex);
|
|
557
626
|
queueIndex = nextQueueIndex;
|
|
558
627
|
this.isSeeking = false;
|
|
628
|
+
this.dispatchEvent(new Event("seeked"));
|
|
559
629
|
continue;
|
|
560
630
|
}
|
|
561
|
-
queueIndex =
|
|
631
|
+
queueIndex = this.scheduleTimelineEvents(now, queueIndex);
|
|
562
632
|
const waitTime = now + this.noteCheckInterval;
|
|
563
633
|
await this.scheduleTask(() => { }, waitTime);
|
|
564
634
|
}
|
|
565
|
-
if (
|
|
566
|
-
const now = this.audioContext.currentTime;
|
|
567
|
-
await this.stopNotes(0, true, now);
|
|
568
|
-
await this.audioContext.suspend();
|
|
569
|
-
finished = true;
|
|
570
|
-
}
|
|
571
|
-
if (finished) {
|
|
572
|
-
this.notePromises = [];
|
|
635
|
+
if (exitReason !== "paused") {
|
|
573
636
|
this.resetAllStates();
|
|
574
637
|
}
|
|
575
638
|
this.isPlaying = false;
|
|
639
|
+
if (exitReason === "paused") {
|
|
640
|
+
this.isPaused = true;
|
|
641
|
+
this.dispatchEvent(new Event("paused"));
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
this.isPaused = false;
|
|
645
|
+
this.dispatchEvent(new Event(exitReason));
|
|
646
|
+
}
|
|
576
647
|
}
|
|
577
648
|
ticksToSecond(ticks, secondsPerBeat) {
|
|
578
649
|
return ticks * secondsPerBeat / this.ticksPerBeat;
|
|
@@ -659,11 +730,13 @@ export class MidyGM1 {
|
|
|
659
730
|
return Promise.all(promises);
|
|
660
731
|
}
|
|
661
732
|
stopNotes(velocity, force, scheduleTime) {
|
|
662
|
-
const
|
|
663
|
-
for (let i = 0; i <
|
|
664
|
-
|
|
733
|
+
const channels = this.channels;
|
|
734
|
+
for (let i = 0; i < channels.length; i++) {
|
|
735
|
+
this.stopChannelNotes(i, velocity, force, scheduleTime);
|
|
665
736
|
}
|
|
666
|
-
|
|
737
|
+
const stopPromise = Promise.all(this.notePromises);
|
|
738
|
+
this.notePromises = [];
|
|
739
|
+
return stopPromise;
|
|
667
740
|
}
|
|
668
741
|
async start() {
|
|
669
742
|
if (this.isPlaying || this.isPaused)
|
|
@@ -679,24 +752,20 @@ export class MidyGM1 {
|
|
|
679
752
|
return;
|
|
680
753
|
this.isStopping = true;
|
|
681
754
|
await this.playPromise;
|
|
682
|
-
this.isStopping = false;
|
|
683
755
|
}
|
|
684
756
|
async pause() {
|
|
685
757
|
if (!this.isPlaying || this.isPaused)
|
|
686
758
|
return;
|
|
687
759
|
const now = this.audioContext.currentTime;
|
|
688
|
-
this.resumeTime = now
|
|
760
|
+
this.resumeTime = now + this.resumeTime - this.startTime;
|
|
689
761
|
this.isPausing = true;
|
|
690
762
|
await this.playPromise;
|
|
691
|
-
this.isPausing = false;
|
|
692
|
-
this.isPaused = true;
|
|
693
763
|
}
|
|
694
764
|
async resume() {
|
|
695
765
|
if (!this.isPaused)
|
|
696
766
|
return;
|
|
697
767
|
this.playPromise = this.playNotes();
|
|
698
768
|
await this.playPromise;
|
|
699
|
-
this.isPaused = false;
|
|
700
769
|
}
|
|
701
770
|
seekTo(second) {
|
|
702
771
|
this.resumeTime = second;
|
|
@@ -705,11 +774,17 @@ export class MidyGM1 {
|
|
|
705
774
|
}
|
|
706
775
|
}
|
|
707
776
|
calcTotalTime() {
|
|
777
|
+
const totalTimeEventTypes = this.totalTimeEventTypes;
|
|
778
|
+
const timeline = this.timeline;
|
|
779
|
+
const inverseTempo = 1 / this.tempo;
|
|
708
780
|
let totalTime = 0;
|
|
709
|
-
for (let i = 0; i <
|
|
710
|
-
const event =
|
|
711
|
-
if (
|
|
712
|
-
|
|
781
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
782
|
+
const event = timeline[i];
|
|
783
|
+
if (!totalTimeEventTypes.has(event.type))
|
|
784
|
+
continue;
|
|
785
|
+
const t = event.startTime * inverseTempo;
|
|
786
|
+
if (totalTime < t)
|
|
787
|
+
totalTime = t;
|
|
713
788
|
}
|
|
714
789
|
return totalTime + this.startDelay;
|
|
715
790
|
}
|
|
@@ -719,19 +794,23 @@ export class MidyGM1 {
|
|
|
719
794
|
const now = this.audioContext.currentTime;
|
|
720
795
|
return now + this.resumeTime - this.startTime;
|
|
721
796
|
}
|
|
722
|
-
processScheduledNotes(channel, callback) {
|
|
797
|
+
async processScheduledNotes(channel, callback) {
|
|
723
798
|
const scheduledNotes = channel.scheduledNotes;
|
|
799
|
+
const tasks = [];
|
|
724
800
|
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
725
801
|
const note = scheduledNotes[i];
|
|
726
802
|
if (!note)
|
|
727
803
|
continue;
|
|
728
804
|
if (note.ending)
|
|
729
805
|
continue;
|
|
730
|
-
callback(note);
|
|
806
|
+
const task = note.ready.then(() => callback(note));
|
|
807
|
+
tasks.push(task);
|
|
731
808
|
}
|
|
809
|
+
await Promise.all(tasks);
|
|
732
810
|
}
|
|
733
|
-
processActiveNotes(channel, scheduleTime, callback) {
|
|
811
|
+
async processActiveNotes(channel, scheduleTime, callback) {
|
|
734
812
|
const scheduledNotes = channel.scheduledNotes;
|
|
813
|
+
const tasks = [];
|
|
735
814
|
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
736
815
|
const note = scheduledNotes[i];
|
|
737
816
|
if (!note)
|
|
@@ -740,11 +819,10 @@ export class MidyGM1 {
|
|
|
740
819
|
continue;
|
|
741
820
|
if (scheduleTime < note.startTime)
|
|
742
821
|
break;
|
|
743
|
-
callback(note);
|
|
822
|
+
const task = note.ready.then(() => callback(note));
|
|
823
|
+
tasks.push(task);
|
|
744
824
|
}
|
|
745
|
-
|
|
746
|
-
cbToRatio(cb) {
|
|
747
|
-
return Math.pow(10, cb / 200);
|
|
825
|
+
await Promise.all(tasks);
|
|
748
826
|
}
|
|
749
827
|
rateToCent(rate) {
|
|
750
828
|
return 1200 * Math.log2(rate);
|
|
@@ -774,26 +852,26 @@ export class MidyGM1 {
|
|
|
774
852
|
}
|
|
775
853
|
setVolumeEnvelope(note, scheduleTime) {
|
|
776
854
|
const { voiceParams, startTime } = note;
|
|
777
|
-
const attackVolume =
|
|
855
|
+
const attackVolume = cbToRatio(-voiceParams.initialAttenuation);
|
|
778
856
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
779
857
|
const volDelay = startTime + voiceParams.volDelay;
|
|
780
858
|
const volAttack = volDelay + voiceParams.volAttack;
|
|
781
859
|
const volHold = volAttack + voiceParams.volHold;
|
|
782
|
-
const
|
|
860
|
+
const decayDuration = voiceParams.volDecay;
|
|
783
861
|
note.volumeEnvelopeNode.gain
|
|
784
862
|
.cancelScheduledValues(scheduleTime)
|
|
785
863
|
.setValueAtTime(0, startTime)
|
|
786
|
-
.setValueAtTime(
|
|
787
|
-
.
|
|
864
|
+
.setValueAtTime(0, volDelay)
|
|
865
|
+
.linearRampToValueAtTime(attackVolume, volAttack)
|
|
788
866
|
.setValueAtTime(attackVolume, volHold)
|
|
789
|
-
.
|
|
867
|
+
.setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
|
|
790
868
|
}
|
|
791
869
|
setPitchEnvelope(note, scheduleTime) {
|
|
792
870
|
const { voiceParams } = note;
|
|
793
871
|
const baseRate = voiceParams.playbackRate;
|
|
794
872
|
note.bufferSource.playbackRate
|
|
795
873
|
.cancelScheduledValues(scheduleTime)
|
|
796
|
-
.setValueAtTime(baseRate,
|
|
874
|
+
.setValueAtTime(baseRate, note.startTime);
|
|
797
875
|
const modEnvToPitch = voiceParams.modEnvToPitch;
|
|
798
876
|
if (modEnvToPitch === 0)
|
|
799
877
|
return;
|
|
@@ -803,12 +881,12 @@ export class MidyGM1 {
|
|
|
803
881
|
const modDelay = note.startTime + voiceParams.modDelay;
|
|
804
882
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
805
883
|
const modHold = modAttack + voiceParams.modHold;
|
|
806
|
-
const
|
|
884
|
+
const decayDuration = voiceParams.modDecay;
|
|
807
885
|
note.bufferSource.playbackRate
|
|
808
886
|
.setValueAtTime(baseRate, modDelay)
|
|
809
|
-
.
|
|
887
|
+
.linearRampToValueAtTime(peekRate, modAttack)
|
|
810
888
|
.setValueAtTime(peekRate, modHold)
|
|
811
|
-
.
|
|
889
|
+
.setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
|
|
812
890
|
}
|
|
813
891
|
clampCutoffFrequency(frequency) {
|
|
814
892
|
const minFrequency = 20; // min Hz of initialFilterFc
|
|
@@ -817,36 +895,42 @@ export class MidyGM1 {
|
|
|
817
895
|
}
|
|
818
896
|
setFilterEnvelope(note, scheduleTime) {
|
|
819
897
|
const { voiceParams, startTime } = note;
|
|
820
|
-
const
|
|
821
|
-
const
|
|
822
|
-
const
|
|
823
|
-
|
|
898
|
+
const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
|
|
899
|
+
const baseCent = voiceParams.initialFilterFc;
|
|
900
|
+
const peekCent = baseCent + modEnvToFilterFc;
|
|
901
|
+
const sustainCent = baseCent +
|
|
902
|
+
modEnvToFilterFc * (1 - voiceParams.modSustain);
|
|
903
|
+
const baseFreq = this.centToHz(baseCent);
|
|
904
|
+
const peekFreq = this.centToHz(peekCent);
|
|
905
|
+
const sustainFreq = this.centToHz(sustainCent);
|
|
824
906
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
825
907
|
const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
|
|
826
908
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
827
909
|
const modDelay = startTime + voiceParams.modDelay;
|
|
828
910
|
const modAttack = modDelay + voiceParams.modAttack;
|
|
829
911
|
const modHold = modAttack + voiceParams.modHold;
|
|
830
|
-
const
|
|
912
|
+
const decayDuration = voiceParams.modDecay;
|
|
913
|
+
note.adjustedBaseFreq = adjustedBaseFreq;
|
|
831
914
|
note.filterNode.frequency
|
|
832
915
|
.cancelScheduledValues(scheduleTime)
|
|
833
916
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
834
917
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
835
|
-
.
|
|
918
|
+
.linearRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
836
919
|
.setValueAtTime(adjustedPeekFreq, modHold)
|
|
837
|
-
.
|
|
920
|
+
.setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
|
|
838
921
|
}
|
|
839
922
|
startModulation(channel, note, scheduleTime) {
|
|
923
|
+
const audioContext = this.audioContext;
|
|
840
924
|
const { voiceParams } = note;
|
|
841
|
-
note.modulationLFO = new OscillatorNode(
|
|
925
|
+
note.modulationLFO = new OscillatorNode(audioContext, {
|
|
842
926
|
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
843
927
|
});
|
|
844
|
-
note.filterDepth = new GainNode(
|
|
928
|
+
note.filterDepth = new GainNode(audioContext, {
|
|
845
929
|
gain: voiceParams.modLfoToFilterFc,
|
|
846
930
|
});
|
|
847
|
-
note.modulationDepth = new GainNode(
|
|
931
|
+
note.modulationDepth = new GainNode(audioContext);
|
|
848
932
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
849
|
-
note.volumeDepth = new GainNode(
|
|
933
|
+
note.volumeDepth = new GainNode(audioContext);
|
|
850
934
|
this.setModLfoToVolume(note, scheduleTime);
|
|
851
935
|
note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
|
|
852
936
|
note.modulationLFO.connect(note.filterDepth);
|
|
@@ -885,7 +969,8 @@ export class MidyGM1 {
|
|
|
885
969
|
}
|
|
886
970
|
}
|
|
887
971
|
async setNoteAudioNode(channel, note, realtime) {
|
|
888
|
-
const
|
|
972
|
+
const audioContext = this.audioContext;
|
|
973
|
+
const now = audioContext.currentTime;
|
|
889
974
|
const { noteNumber, velocity, startTime } = note;
|
|
890
975
|
const state = channel.state;
|
|
891
976
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
@@ -893,8 +978,8 @@ export class MidyGM1 {
|
|
|
893
978
|
note.voiceParams = voiceParams;
|
|
894
979
|
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
895
980
|
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
896
|
-
note.volumeEnvelopeNode = new GainNode(
|
|
897
|
-
note.filterNode = new BiquadFilterNode(
|
|
981
|
+
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
982
|
+
note.filterNode = new BiquadFilterNode(audioContext, {
|
|
898
983
|
type: "lowpass",
|
|
899
984
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
900
985
|
});
|
|
@@ -953,7 +1038,12 @@ export class MidyGM1 {
|
|
|
953
1038
|
const bankTable = this.soundFontTable[programNumber];
|
|
954
1039
|
if (!bankTable)
|
|
955
1040
|
return;
|
|
956
|
-
|
|
1041
|
+
let bank = channel.isDrum ? 128 : 0;
|
|
1042
|
+
if (bankTable[bank] === undefined) {
|
|
1043
|
+
if (channel.isDrum)
|
|
1044
|
+
return;
|
|
1045
|
+
bank = 0;
|
|
1046
|
+
}
|
|
957
1047
|
const soundFontIndex = bankTable[bank];
|
|
958
1048
|
if (soundFontIndex === undefined)
|
|
959
1049
|
return;
|
|
@@ -963,11 +1053,7 @@ export class MidyGM1 {
|
|
|
963
1053
|
return;
|
|
964
1054
|
await this.setNoteAudioNode(channel, note, realtime);
|
|
965
1055
|
this.setNoteRouting(channelNumber, note, startTime);
|
|
966
|
-
note.
|
|
967
|
-
const off = note.offEvent;
|
|
968
|
-
if (off) {
|
|
969
|
-
this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
|
|
970
|
-
}
|
|
1056
|
+
note.resolveReady();
|
|
971
1057
|
}
|
|
972
1058
|
disconnectNote(note) {
|
|
973
1059
|
note.bufferSource.disconnect();
|
|
@@ -981,27 +1067,27 @@ export class MidyGM1 {
|
|
|
981
1067
|
}
|
|
982
1068
|
releaseNote(channel, note, endTime) {
|
|
983
1069
|
endTime ??= this.audioContext.currentTime;
|
|
984
|
-
const
|
|
1070
|
+
const duration = note.voiceParams.volRelease * releaseTime;
|
|
1071
|
+
const volRelease = endTime + duration;
|
|
985
1072
|
const modRelease = endTime + note.voiceParams.modRelease;
|
|
986
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
987
1073
|
note.filterNode.frequency
|
|
988
1074
|
.cancelScheduledValues(endTime)
|
|
989
|
-
.linearRampToValueAtTime(
|
|
1075
|
+
.linearRampToValueAtTime(note.adjustedBaseFreq, modRelease);
|
|
990
1076
|
note.volumeEnvelopeNode.gain
|
|
991
1077
|
.cancelScheduledValues(endTime)
|
|
992
|
-
.
|
|
1078
|
+
.setTargetAtTime(0, endTime, duration * releaseCurve);
|
|
993
1079
|
return new Promise((resolve) => {
|
|
994
1080
|
this.scheduleTask(() => {
|
|
995
1081
|
const bufferSource = note.bufferSource;
|
|
996
1082
|
bufferSource.loop = false;
|
|
997
|
-
bufferSource.stop(
|
|
1083
|
+
bufferSource.stop(volRelease);
|
|
998
1084
|
this.disconnectNote(note);
|
|
999
1085
|
channel.scheduledNotes[note.index] = undefined;
|
|
1000
1086
|
resolve();
|
|
1001
|
-
},
|
|
1087
|
+
}, volRelease);
|
|
1002
1088
|
});
|
|
1003
1089
|
}
|
|
1004
|
-
noteOff(channelNumber, noteNumber,
|
|
1090
|
+
noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
1005
1091
|
const channel = this.channels[channelNumber];
|
|
1006
1092
|
if (!force && 0.5 <= channel.state.sustainPedal)
|
|
1007
1093
|
return;
|
|
@@ -1009,13 +1095,11 @@ export class MidyGM1 {
|
|
|
1009
1095
|
if (index < 0)
|
|
1010
1096
|
return;
|
|
1011
1097
|
const note = channel.scheduledNotes[index];
|
|
1012
|
-
if (note.pending) {
|
|
1013
|
-
note.offEvent = { velocity, startTime: endTime };
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
1098
|
note.ending = true;
|
|
1017
1099
|
this.setNoteIndex(channel, index);
|
|
1018
|
-
const promise =
|
|
1100
|
+
const promise = note.ready.then(() => {
|
|
1101
|
+
return this.releaseNote(channel, note, endTime);
|
|
1102
|
+
});
|
|
1019
1103
|
this.notePromises.push(promise);
|
|
1020
1104
|
return promise;
|
|
1021
1105
|
}
|
|
@@ -1103,7 +1187,8 @@ export class MidyGM1 {
|
|
|
1103
1187
|
}
|
|
1104
1188
|
setPitchBend(channelNumber, value, scheduleTime) {
|
|
1105
1189
|
const channel = this.channels[channelNumber];
|
|
1106
|
-
|
|
1190
|
+
if (!(0 <= scheduleTime))
|
|
1191
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1107
1192
|
const state = channel.state;
|
|
1108
1193
|
const prev = state.pitchWheel * 2 - 1;
|
|
1109
1194
|
const next = (value - 8192) / 8192;
|
|
@@ -1134,7 +1219,7 @@ export class MidyGM1 {
|
|
|
1134
1219
|
}
|
|
1135
1220
|
setModLfoToVolume(note, scheduleTime) {
|
|
1136
1221
|
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
1137
|
-
const baseDepth =
|
|
1222
|
+
const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
1138
1223
|
const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
|
|
1139
1224
|
note.volumeDepth.gain
|
|
1140
1225
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -1268,12 +1353,14 @@ export class MidyGM1 {
|
|
|
1268
1353
|
}
|
|
1269
1354
|
setModulationDepth(channelNumber, modulation, scheduleTime) {
|
|
1270
1355
|
const channel = this.channels[channelNumber];
|
|
1271
|
-
|
|
1356
|
+
if (!(0 <= scheduleTime))
|
|
1357
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1272
1358
|
channel.state.modulationDepthMSB = modulation / 127;
|
|
1273
1359
|
this.updateModulation(channel, scheduleTime);
|
|
1274
1360
|
}
|
|
1275
1361
|
setVolume(channelNumber, volume, scheduleTime) {
|
|
1276
|
-
|
|
1362
|
+
if (!(0 <= scheduleTime))
|
|
1363
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1277
1364
|
const channel = this.channels[channelNumber];
|
|
1278
1365
|
channel.state.volumeMSB = volume / 127;
|
|
1279
1366
|
this.updateChannelVolume(channel, scheduleTime);
|
|
@@ -1286,13 +1373,15 @@ export class MidyGM1 {
|
|
|
1286
1373
|
};
|
|
1287
1374
|
}
|
|
1288
1375
|
setPan(channelNumber, pan, scheduleTime) {
|
|
1289
|
-
|
|
1376
|
+
if (!(0 <= scheduleTime))
|
|
1377
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1290
1378
|
const channel = this.channels[channelNumber];
|
|
1291
1379
|
channel.state.panMSB = pan / 127;
|
|
1292
1380
|
this.updateChannelVolume(channel, scheduleTime);
|
|
1293
1381
|
}
|
|
1294
1382
|
setExpression(channelNumber, expression, scheduleTime) {
|
|
1295
|
-
|
|
1383
|
+
if (!(0 <= scheduleTime))
|
|
1384
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1296
1385
|
const channel = this.channels[channelNumber];
|
|
1297
1386
|
channel.state.expressionMSB = expression / 127;
|
|
1298
1387
|
this.updateChannelVolume(channel, scheduleTime);
|
|
@@ -1314,7 +1403,8 @@ export class MidyGM1 {
|
|
|
1314
1403
|
}
|
|
1315
1404
|
setSustainPedal(channelNumber, value, scheduleTime) {
|
|
1316
1405
|
const channel = this.channels[channelNumber];
|
|
1317
|
-
|
|
1406
|
+
if (!(0 <= scheduleTime))
|
|
1407
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1318
1408
|
channel.state.sustainPedal = value / 127;
|
|
1319
1409
|
if (64 <= value) {
|
|
1320
1410
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1386,7 +1476,8 @@ export class MidyGM1 {
|
|
|
1386
1476
|
}
|
|
1387
1477
|
setPitchBendRange(channelNumber, value, scheduleTime) {
|
|
1388
1478
|
const channel = this.channels[channelNumber];
|
|
1389
|
-
|
|
1479
|
+
if (!(0 <= scheduleTime))
|
|
1480
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1390
1481
|
const state = channel.state;
|
|
1391
1482
|
const prev = state.pitchWheelSensitivity;
|
|
1392
1483
|
const next = value / 12800;
|
|
@@ -1404,7 +1495,8 @@ export class MidyGM1 {
|
|
|
1404
1495
|
}
|
|
1405
1496
|
setFineTuning(channelNumber, value, scheduleTime) {
|
|
1406
1497
|
const channel = this.channels[channelNumber];
|
|
1407
|
-
|
|
1498
|
+
if (!(0 <= scheduleTime))
|
|
1499
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1408
1500
|
const prev = channel.fineTuning;
|
|
1409
1501
|
const next = value;
|
|
1410
1502
|
channel.fineTuning = next;
|
|
@@ -1419,7 +1511,8 @@ export class MidyGM1 {
|
|
|
1419
1511
|
}
|
|
1420
1512
|
setCoarseTuning(channelNumber, value, scheduleTime) {
|
|
1421
1513
|
const channel = this.channels[channelNumber];
|
|
1422
|
-
|
|
1514
|
+
if (!(0 <= scheduleTime))
|
|
1515
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1423
1516
|
const prev = channel.coarseTuning;
|
|
1424
1517
|
const next = value;
|
|
1425
1518
|
channel.coarseTuning = next;
|
|
@@ -1427,7 +1520,8 @@ export class MidyGM1 {
|
|
|
1427
1520
|
this.updateChannelDetune(channel, scheduleTime);
|
|
1428
1521
|
}
|
|
1429
1522
|
allSoundOff(channelNumber, _value, scheduleTime) {
|
|
1430
|
-
|
|
1523
|
+
if (!(0 <= scheduleTime))
|
|
1524
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1431
1525
|
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
1432
1526
|
}
|
|
1433
1527
|
resetChannelStates(channelNumber) {
|
|
@@ -1479,7 +1573,8 @@ export class MidyGM1 {
|
|
|
1479
1573
|
}
|
|
1480
1574
|
}
|
|
1481
1575
|
allNotesOff(channelNumber, _value, scheduleTime) {
|
|
1482
|
-
|
|
1576
|
+
if (!(0 <= scheduleTime))
|
|
1577
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1483
1578
|
return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
|
|
1484
1579
|
}
|
|
1485
1580
|
handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
|
|
@@ -1500,14 +1595,16 @@ export class MidyGM1 {
|
|
|
1500
1595
|
}
|
|
1501
1596
|
}
|
|
1502
1597
|
GM1SystemOn(scheduleTime) {
|
|
1503
|
-
|
|
1598
|
+
const channels = this.channels;
|
|
1599
|
+
if (!(0 <= scheduleTime))
|
|
1600
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1504
1601
|
this.mode = "GM1";
|
|
1505
|
-
for (let i = 0; i <
|
|
1602
|
+
for (let i = 0; i < channels.length; i++) {
|
|
1506
1603
|
this.allSoundOff(i, 0, scheduleTime);
|
|
1507
|
-
const channel =
|
|
1604
|
+
const channel = channels[i];
|
|
1508
1605
|
channel.isDrum = false;
|
|
1509
1606
|
}
|
|
1510
|
-
|
|
1607
|
+
channels[9].isDrum = true;
|
|
1511
1608
|
}
|
|
1512
1609
|
handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
|
|
1513
1610
|
switch (data[2]) {
|
|
@@ -1528,7 +1625,8 @@ export class MidyGM1 {
|
|
|
1528
1625
|
this.setMasterVolume(volume, scheduleTime);
|
|
1529
1626
|
}
|
|
1530
1627
|
setMasterVolume(value, scheduleTime) {
|
|
1531
|
-
|
|
1628
|
+
if (!(0 <= scheduleTime))
|
|
1629
|
+
scheduleTime = this.audioContext.currentTime;
|
|
1532
1630
|
this.masterVolume.gain
|
|
1533
1631
|
.cancelScheduledValues(scheduleTime)
|
|
1534
1632
|
.setValueAtTime(value * value, scheduleTime);
|