@marmooo/midy 0.2.6 → 0.2.7
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 +17 -12
- package/esm/midy-GM1.d.ts +68 -69
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +143 -156
- package/esm/midy-GM2.d.ts +104 -105
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +251 -280
- package/esm/midy-GMLite.d.ts +68 -69
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +143 -156
- package/esm/midy.d.ts +127 -128
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +274 -307
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +68 -69
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +143 -156
- package/script/midy-GM2.d.ts +104 -105
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +251 -280
- package/script/midy-GMLite.d.ts +68 -69
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +143 -156
- package/script/midy.d.ts +127 -128
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +274 -307
package/esm/midy-GMLite.js
CHANGED
|
@@ -412,7 +412,7 @@ export class MidyGMLite {
|
|
|
412
412
|
}
|
|
413
413
|
/* falls through */
|
|
414
414
|
case "noteOff": {
|
|
415
|
-
const notePromise = this.
|
|
415
|
+
const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime);
|
|
416
416
|
if (notePromise) {
|
|
417
417
|
this.notePromises.push(notePromise);
|
|
418
418
|
}
|
|
@@ -459,10 +459,11 @@ export class MidyGMLite {
|
|
|
459
459
|
resolve();
|
|
460
460
|
return;
|
|
461
461
|
}
|
|
462
|
-
const
|
|
462
|
+
const now = this.audioContext.currentTime;
|
|
463
|
+
const t = now + offset;
|
|
463
464
|
queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
|
|
464
465
|
if (this.isPausing) {
|
|
465
|
-
await this.stopNotes(0, true);
|
|
466
|
+
await this.stopNotes(0, true, now);
|
|
466
467
|
this.notePromises = [];
|
|
467
468
|
resolve();
|
|
468
469
|
this.isPausing = false;
|
|
@@ -470,7 +471,7 @@ export class MidyGMLite {
|
|
|
470
471
|
return;
|
|
471
472
|
}
|
|
472
473
|
else if (this.isStopping) {
|
|
473
|
-
await this.stopNotes(0, true);
|
|
474
|
+
await this.stopNotes(0, true, now);
|
|
474
475
|
this.notePromises = [];
|
|
475
476
|
this.exclusiveClassMap.clear();
|
|
476
477
|
this.audioBufferCache.clear();
|
|
@@ -480,7 +481,7 @@ export class MidyGMLite {
|
|
|
480
481
|
return;
|
|
481
482
|
}
|
|
482
483
|
else if (this.isSeeking) {
|
|
483
|
-
this.stopNotes(0, true);
|
|
484
|
+
this.stopNotes(0, true, now);
|
|
484
485
|
this.exclusiveClassMap.clear();
|
|
485
486
|
this.startTime = this.audioContext.currentTime;
|
|
486
487
|
queueIndex = this.getQueueIndex(this.resumeTime);
|
|
@@ -489,7 +490,6 @@ export class MidyGMLite {
|
|
|
489
490
|
await schedulePlayback();
|
|
490
491
|
}
|
|
491
492
|
else {
|
|
492
|
-
const now = this.audioContext.currentTime;
|
|
493
493
|
const waitTime = now + this.noteCheckInterval;
|
|
494
494
|
await this.scheduleTask(() => { }, waitTime);
|
|
495
495
|
await schedulePlayback();
|
|
@@ -573,24 +573,26 @@ export class MidyGMLite {
|
|
|
573
573
|
}
|
|
574
574
|
return { instruments, timeline };
|
|
575
575
|
}
|
|
576
|
-
|
|
577
|
-
const now = this.audioContext.currentTime;
|
|
576
|
+
stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
|
|
578
577
|
const channel = this.channels[channelNumber];
|
|
578
|
+
const promises = [];
|
|
579
579
|
channel.scheduledNotes.forEach((noteList) => {
|
|
580
580
|
for (let i = 0; i < noteList.length; i++) {
|
|
581
581
|
const note = noteList[i];
|
|
582
582
|
if (!note)
|
|
583
583
|
continue;
|
|
584
|
-
const promise = this.
|
|
584
|
+
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
585
585
|
this.notePromises.push(promise);
|
|
586
|
+
promises.push(promise);
|
|
586
587
|
}
|
|
587
588
|
});
|
|
588
589
|
channel.scheduledNotes.clear();
|
|
589
|
-
|
|
590
|
+
return Promise.all(promises);
|
|
590
591
|
}
|
|
591
|
-
stopNotes(velocity, force) {
|
|
592
|
+
stopNotes(velocity, force, scheduleTime) {
|
|
593
|
+
const promises = [];
|
|
592
594
|
for (let i = 0; i < this.channels.length; i++) {
|
|
593
|
-
this.stopChannelNotes(i, velocity, force);
|
|
595
|
+
promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
|
|
594
596
|
}
|
|
595
597
|
return Promise.all(this.notePromises);
|
|
596
598
|
}
|
|
@@ -650,22 +652,22 @@ export class MidyGMLite {
|
|
|
650
652
|
}
|
|
651
653
|
});
|
|
652
654
|
}
|
|
653
|
-
getActiveNotes(channel,
|
|
655
|
+
getActiveNotes(channel, scheduleTime) {
|
|
654
656
|
const activeNotes = new SparseMap(128);
|
|
655
657
|
channel.scheduledNotes.forEach((noteList) => {
|
|
656
|
-
const activeNote = this.getActiveNote(noteList,
|
|
658
|
+
const activeNote = this.getActiveNote(noteList, scheduleTime);
|
|
657
659
|
if (activeNote) {
|
|
658
660
|
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
659
661
|
}
|
|
660
662
|
});
|
|
661
663
|
return activeNotes;
|
|
662
664
|
}
|
|
663
|
-
getActiveNote(noteList,
|
|
665
|
+
getActiveNote(noteList, scheduleTime) {
|
|
664
666
|
for (let i = noteList.length - 1; i >= 0; i--) {
|
|
665
667
|
const note = noteList[i];
|
|
666
668
|
if (!note)
|
|
667
669
|
return;
|
|
668
|
-
if (
|
|
670
|
+
if (scheduleTime < note.startTime)
|
|
669
671
|
continue;
|
|
670
672
|
return (note.ending) ? null : note;
|
|
671
673
|
}
|
|
@@ -688,24 +690,17 @@ export class MidyGMLite {
|
|
|
688
690
|
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
|
|
689
691
|
return pitchWheel * pitchWheelSensitivity;
|
|
690
692
|
}
|
|
691
|
-
updateChannelDetune(channel) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const note = noteList[i];
|
|
695
|
-
if (!note)
|
|
696
|
-
continue;
|
|
697
|
-
this.updateDetune(channel, note);
|
|
698
|
-
}
|
|
693
|
+
updateChannelDetune(channel, scheduleTime) {
|
|
694
|
+
this.processScheduledNotes(channel, scheduleTime, (note) => {
|
|
695
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
699
696
|
});
|
|
700
697
|
}
|
|
701
|
-
updateDetune(channel, note) {
|
|
702
|
-
const now = this.audioContext.currentTime;
|
|
698
|
+
updateDetune(channel, note, scheduleTime) {
|
|
703
699
|
note.bufferSource.detune
|
|
704
|
-
.cancelScheduledValues(
|
|
705
|
-
.setValueAtTime(channel.detune,
|
|
700
|
+
.cancelScheduledValues(scheduleTime)
|
|
701
|
+
.setValueAtTime(channel.detune, scheduleTime);
|
|
706
702
|
}
|
|
707
|
-
setVolumeEnvelope(note) {
|
|
708
|
-
const now = this.audioContext.currentTime;
|
|
703
|
+
setVolumeEnvelope(note, scheduleTime) {
|
|
709
704
|
const { voiceParams, startTime } = note;
|
|
710
705
|
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
|
|
711
706
|
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
@@ -714,7 +709,7 @@ export class MidyGMLite {
|
|
|
714
709
|
const volHold = volAttack + voiceParams.volHold;
|
|
715
710
|
const volDecay = volHold + voiceParams.volDecay;
|
|
716
711
|
note.volumeEnvelopeNode.gain
|
|
717
|
-
.cancelScheduledValues(
|
|
712
|
+
.cancelScheduledValues(scheduleTime)
|
|
718
713
|
.setValueAtTime(0, startTime)
|
|
719
714
|
.setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
720
715
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
@@ -722,7 +717,6 @@ export class MidyGMLite {
|
|
|
722
717
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
723
718
|
}
|
|
724
719
|
setPitchEnvelope(note, scheduleTime) {
|
|
725
|
-
scheduleTime ??= this.audioContext.currentTime;
|
|
726
720
|
const { voiceParams } = note;
|
|
727
721
|
const baseRate = voiceParams.playbackRate;
|
|
728
722
|
note.bufferSource.playbackRate
|
|
@@ -749,8 +743,7 @@ export class MidyGMLite {
|
|
|
749
743
|
const maxFrequency = 20000; // max Hz of initialFilterFc
|
|
750
744
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
751
745
|
}
|
|
752
|
-
setFilterEnvelope(note) {
|
|
753
|
-
const now = this.audioContext.currentTime;
|
|
746
|
+
setFilterEnvelope(note, scheduleTime) {
|
|
754
747
|
const { voiceParams, startTime } = note;
|
|
755
748
|
const baseFreq = this.centToHz(voiceParams.initialFilterFc);
|
|
756
749
|
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
|
|
@@ -764,14 +757,14 @@ export class MidyGMLite {
|
|
|
764
757
|
const modHold = modAttack + voiceParams.modHold;
|
|
765
758
|
const modDecay = modHold + voiceParams.modDecay;
|
|
766
759
|
note.filterNode.frequency
|
|
767
|
-
.cancelScheduledValues(
|
|
760
|
+
.cancelScheduledValues(scheduleTime)
|
|
768
761
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
769
762
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
770
763
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
771
764
|
.setValueAtTime(adjustedPeekFreq, modHold)
|
|
772
765
|
.linearRampToValueAtTime(adjustedSustainFreq, modDecay);
|
|
773
766
|
}
|
|
774
|
-
startModulation(channel, note,
|
|
767
|
+
startModulation(channel, note, scheduleTime) {
|
|
775
768
|
const { voiceParams } = note;
|
|
776
769
|
note.modulationLFO = new OscillatorNode(this.audioContext, {
|
|
777
770
|
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
@@ -780,10 +773,10 @@ export class MidyGMLite {
|
|
|
780
773
|
gain: voiceParams.modLfoToFilterFc,
|
|
781
774
|
});
|
|
782
775
|
note.modulationDepth = new GainNode(this.audioContext);
|
|
783
|
-
this.setModLfoToPitch(channel, note);
|
|
776
|
+
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
784
777
|
note.volumeDepth = new GainNode(this.audioContext);
|
|
785
|
-
this.setModLfoToVolume(note);
|
|
786
|
-
note.modulationLFO.start(startTime + voiceParams.delayModLFO);
|
|
778
|
+
this.setModLfoToVolume(note, scheduleTime);
|
|
779
|
+
note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
|
|
787
780
|
note.modulationLFO.connect(note.filterDepth);
|
|
788
781
|
note.filterDepth.connect(note.filterNode.frequency);
|
|
789
782
|
note.modulationLFO.connect(note.modulationDepth);
|
|
@@ -810,6 +803,7 @@ export class MidyGMLite {
|
|
|
810
803
|
}
|
|
811
804
|
}
|
|
812
805
|
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
806
|
+
const now = this.audioContext.currentTime;
|
|
813
807
|
const state = channel.state;
|
|
814
808
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
815
809
|
const voiceParams = voice.getAllParams(controllerState);
|
|
@@ -821,11 +815,11 @@ export class MidyGMLite {
|
|
|
821
815
|
type: "lowpass",
|
|
822
816
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
823
817
|
});
|
|
824
|
-
this.setVolumeEnvelope(note);
|
|
825
|
-
this.setFilterEnvelope(note);
|
|
826
|
-
this.setPitchEnvelope(note);
|
|
818
|
+
this.setVolumeEnvelope(note, now);
|
|
819
|
+
this.setFilterEnvelope(note, now);
|
|
820
|
+
this.setPitchEnvelope(note, now);
|
|
827
821
|
if (0 < state.modulationDepth) {
|
|
828
|
-
this.startModulation(channel, note,
|
|
822
|
+
this.startModulation(channel, note, now);
|
|
829
823
|
}
|
|
830
824
|
note.bufferSource.connect(note.filterNode);
|
|
831
825
|
note.filterNode.connect(note.volumeEnvelopeNode);
|
|
@@ -852,7 +846,7 @@ export class MidyGMLite {
|
|
|
852
846
|
const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
|
|
853
847
|
const [prevNote, prevChannelNumber] = prevEntry;
|
|
854
848
|
if (!prevNote.ending) {
|
|
855
|
-
this.
|
|
849
|
+
this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
|
|
856
850
|
startTime, undefined, // portamentoNoteNumber
|
|
857
851
|
true);
|
|
858
852
|
}
|
|
@@ -867,9 +861,9 @@ export class MidyGMLite {
|
|
|
867
861
|
scheduledNotes.set(noteNumber, [note]);
|
|
868
862
|
}
|
|
869
863
|
}
|
|
870
|
-
noteOn(channelNumber, noteNumber, velocity) {
|
|
871
|
-
|
|
872
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity,
|
|
864
|
+
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
865
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
866
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
|
|
873
867
|
}
|
|
874
868
|
stopNote(endTime, stopTime, scheduledNotes, index) {
|
|
875
869
|
const note = scheduledNotes[index];
|
|
@@ -896,7 +890,7 @@ export class MidyGMLite {
|
|
|
896
890
|
note.bufferSource.stop(stopTime);
|
|
897
891
|
});
|
|
898
892
|
}
|
|
899
|
-
|
|
893
|
+
scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
900
894
|
const channel = this.channels[channelNumber];
|
|
901
895
|
if (!force && 0.5 < channel.state.sustainPedal)
|
|
902
896
|
return;
|
|
@@ -918,127 +912,119 @@ export class MidyGMLite {
|
|
|
918
912
|
return this.stopNote(endTime, stopTime, scheduledNotes, i);
|
|
919
913
|
}
|
|
920
914
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
return this.
|
|
915
|
+
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
916
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
917
|
+
return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
|
|
924
918
|
}
|
|
925
|
-
releaseSustainPedal(channelNumber, halfVelocity) {
|
|
919
|
+
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
926
920
|
const velocity = halfVelocity * 2;
|
|
927
921
|
const channel = this.channels[channelNumber];
|
|
928
922
|
const promises = [];
|
|
929
|
-
channel
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if (!note)
|
|
934
|
-
continue;
|
|
935
|
-
const { noteNumber } = note;
|
|
936
|
-
const promise = this.releaseNote(channelNumber, noteNumber, velocity);
|
|
937
|
-
promises.push(promise);
|
|
938
|
-
}
|
|
923
|
+
this.processScheduledNotes(channel, scheduleTime, (note) => {
|
|
924
|
+
const { noteNumber } = note;
|
|
925
|
+
const promise = this.noteOff(channelNumber, noteNumber, velocity);
|
|
926
|
+
promises.push(promise);
|
|
939
927
|
});
|
|
940
928
|
return promises;
|
|
941
929
|
}
|
|
942
|
-
handleMIDIMessage(statusByte, data1, data2) {
|
|
930
|
+
handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
|
|
943
931
|
const channelNumber = statusByte & 0x0F;
|
|
944
932
|
const messageType = statusByte & 0xF0;
|
|
945
933
|
switch (messageType) {
|
|
946
934
|
case 0x80:
|
|
947
|
-
return this.
|
|
935
|
+
return this.noteOff(channelNumber, data1, data2, scheduleTime);
|
|
948
936
|
case 0x90:
|
|
949
|
-
return this.noteOn(channelNumber, data1, data2);
|
|
937
|
+
return this.noteOn(channelNumber, data1, data2, scheduleTime);
|
|
950
938
|
case 0xB0:
|
|
951
|
-
return this.handleControlChange(channelNumber, data1, data2);
|
|
939
|
+
return this.handleControlChange(channelNumber, data1, data2, scheduleTime);
|
|
952
940
|
case 0xC0:
|
|
953
|
-
return this.handleProgramChange(channelNumber, data1);
|
|
941
|
+
return this.handleProgramChange(channelNumber, data1, scheduleTime);
|
|
954
942
|
case 0xE0:
|
|
955
|
-
return this.handlePitchBendMessage(channelNumber, data1, data2);
|
|
943
|
+
return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
|
|
956
944
|
default:
|
|
957
945
|
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
958
946
|
}
|
|
959
947
|
}
|
|
960
|
-
handleProgramChange(channelNumber, program) {
|
|
948
|
+
handleProgramChange(channelNumber, program, _scheduleTime) {
|
|
961
949
|
const channel = this.channels[channelNumber];
|
|
962
950
|
channel.program = program;
|
|
963
951
|
}
|
|
964
|
-
handlePitchBendMessage(channelNumber, lsb, msb) {
|
|
952
|
+
handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
|
|
965
953
|
const pitchBend = msb * 128 + lsb;
|
|
966
|
-
this.setPitchBend(channelNumber, pitchBend);
|
|
954
|
+
this.setPitchBend(channelNumber, pitchBend, scheduleTime);
|
|
967
955
|
}
|
|
968
|
-
setPitchBend(channelNumber, value) {
|
|
956
|
+
setPitchBend(channelNumber, value, scheduleTime) {
|
|
957
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
969
958
|
const channel = this.channels[channelNumber];
|
|
970
959
|
const state = channel.state;
|
|
971
960
|
const prev = state.pitchWheel * 2 - 1;
|
|
972
961
|
const next = (value - 8192) / 8192;
|
|
973
962
|
state.pitchWheel = value / 16383;
|
|
974
963
|
channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
|
|
975
|
-
this.updateChannelDetune(channel);
|
|
976
|
-
this.applyVoiceParams(channel, 14);
|
|
964
|
+
this.updateChannelDetune(channel, scheduleTime);
|
|
965
|
+
this.applyVoiceParams(channel, 14, scheduleTime);
|
|
977
966
|
}
|
|
978
|
-
setModLfoToPitch(channel, note) {
|
|
979
|
-
const now = this.audioContext.currentTime;
|
|
967
|
+
setModLfoToPitch(channel, note, scheduleTime) {
|
|
980
968
|
const modLfoToPitch = note.voiceParams.modLfoToPitch;
|
|
981
969
|
const baseDepth = Math.abs(modLfoToPitch) +
|
|
982
970
|
channel.state.modulationDepth;
|
|
983
971
|
const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
|
|
984
972
|
note.modulationDepth.gain
|
|
985
|
-
.cancelScheduledValues(
|
|
986
|
-
.setValueAtTime(modulationDepth,
|
|
973
|
+
.cancelScheduledValues(scheduleTime)
|
|
974
|
+
.setValueAtTime(modulationDepth, scheduleTime);
|
|
987
975
|
}
|
|
988
|
-
setModLfoToFilterFc(note) {
|
|
989
|
-
const now = this.audioContext.currentTime;
|
|
976
|
+
setModLfoToFilterFc(note, scheduleTime) {
|
|
990
977
|
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
|
|
991
978
|
note.filterDepth.gain
|
|
992
|
-
.cancelScheduledValues(
|
|
993
|
-
.setValueAtTime(modLfoToFilterFc,
|
|
979
|
+
.cancelScheduledValues(scheduleTime)
|
|
980
|
+
.setValueAtTime(modLfoToFilterFc, scheduleTime);
|
|
994
981
|
}
|
|
995
|
-
setModLfoToVolume(note) {
|
|
996
|
-
const now = this.audioContext.currentTime;
|
|
982
|
+
setModLfoToVolume(note, scheduleTime) {
|
|
997
983
|
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
998
984
|
const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
999
985
|
const volumeDepth = baseDepth * Math.sign(modLfoToVolume);
|
|
1000
986
|
note.volumeDepth.gain
|
|
1001
|
-
.cancelScheduledValues(
|
|
1002
|
-
.setValueAtTime(volumeDepth,
|
|
987
|
+
.cancelScheduledValues(scheduleTime)
|
|
988
|
+
.setValueAtTime(volumeDepth, scheduleTime);
|
|
1003
989
|
}
|
|
1004
|
-
setDelayModLFO(note) {
|
|
1005
|
-
const now = this.audioContext.currentTime;
|
|
990
|
+
setDelayModLFO(note, scheduleTime) {
|
|
1006
991
|
const startTime = note.startTime;
|
|
1007
|
-
if (startTime <
|
|
992
|
+
if (startTime < scheduleTime)
|
|
1008
993
|
return;
|
|
1009
|
-
note.modulationLFO.stop(
|
|
994
|
+
note.modulationLFO.stop(scheduleTime);
|
|
1010
995
|
note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
|
|
1011
996
|
note.modulationLFO.connect(note.filterDepth);
|
|
1012
997
|
}
|
|
1013
|
-
setFreqModLFO(note) {
|
|
1014
|
-
const now = this.audioContext.currentTime;
|
|
998
|
+
setFreqModLFO(note, scheduleTime) {
|
|
1015
999
|
const freqModLFO = note.voiceParams.freqModLFO;
|
|
1016
1000
|
note.modulationLFO.frequency
|
|
1017
|
-
.cancelScheduledValues(
|
|
1018
|
-
.setValueAtTime(freqModLFO,
|
|
1001
|
+
.cancelScheduledValues(scheduleTime)
|
|
1002
|
+
.setValueAtTime(freqModLFO, scheduleTime);
|
|
1019
1003
|
}
|
|
1020
1004
|
createVoiceParamsHandlers() {
|
|
1021
1005
|
return {
|
|
1022
|
-
modLfoToPitch: (channel, note, _prevValue) => {
|
|
1006
|
+
modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
|
|
1023
1007
|
if (0 < channel.state.modulationDepth) {
|
|
1024
|
-
this.setModLfoToPitch(channel, note);
|
|
1008
|
+
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
1025
1009
|
}
|
|
1026
1010
|
},
|
|
1027
|
-
vibLfoToPitch: (_channel, _note, _prevValue) => { },
|
|
1028
|
-
modLfoToFilterFc: (channel, note, _prevValue) => {
|
|
1029
|
-
if (0 < channel.state.modulationDepth)
|
|
1030
|
-
this.setModLfoToFilterFc(note);
|
|
1011
|
+
vibLfoToPitch: (_channel, _note, _prevValue, _scheduleTime) => { },
|
|
1012
|
+
modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
|
|
1013
|
+
if (0 < channel.state.modulationDepth) {
|
|
1014
|
+
this.setModLfoToFilterFc(note, scheduleTime);
|
|
1015
|
+
}
|
|
1031
1016
|
},
|
|
1032
|
-
modLfoToVolume: (channel, note, _prevValue) => {
|
|
1033
|
-
if (0 < channel.state.modulationDepth)
|
|
1034
|
-
this.setModLfoToVolume(note);
|
|
1017
|
+
modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
|
|
1018
|
+
if (0 < channel.state.modulationDepth) {
|
|
1019
|
+
this.setModLfoToVolume(note, scheduleTime);
|
|
1020
|
+
}
|
|
1035
1021
|
},
|
|
1036
|
-
chorusEffectsSend: (_channel, _note, _prevValue) => { },
|
|
1037
|
-
reverbEffectsSend: (_channel, _note, _prevValue) => { },
|
|
1038
|
-
delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
|
|
1039
|
-
freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
|
|
1040
|
-
delayVibLFO: (_channel, _note, _prevValue) => { },
|
|
1041
|
-
freqVibLFO: (_channel, _note, _prevValue) => { },
|
|
1022
|
+
chorusEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
|
|
1023
|
+
reverbEffectsSend: (_channel, _note, _prevValue, _scheduleTime) => { },
|
|
1024
|
+
delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
|
|
1025
|
+
freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
|
|
1026
|
+
delayVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
|
|
1027
|
+
freqVibLFO: (_channel, _note, _prevValue, _scheduleTime) => { },
|
|
1042
1028
|
};
|
|
1043
1029
|
}
|
|
1044
1030
|
getControllerState(channel, noteNumber, velocity) {
|
|
@@ -1048,7 +1034,7 @@ export class MidyGMLite {
|
|
|
1048
1034
|
state[3] = noteNumber / 127;
|
|
1049
1035
|
return state;
|
|
1050
1036
|
}
|
|
1051
|
-
applyVoiceParams(channel, controllerType) {
|
|
1037
|
+
applyVoiceParams(channel, controllerType, scheduleTime) {
|
|
1052
1038
|
channel.scheduledNotes.forEach((noteList) => {
|
|
1053
1039
|
for (let i = 0; i < noteList.length; i++) {
|
|
1054
1040
|
const note = noteList[i];
|
|
@@ -1064,7 +1050,7 @@ export class MidyGMLite {
|
|
|
1064
1050
|
continue;
|
|
1065
1051
|
note.voiceParams[key] = value;
|
|
1066
1052
|
if (key in this.voiceParamsHandlers) {
|
|
1067
|
-
this.voiceParamsHandlers[key](channel, note, prevValue);
|
|
1053
|
+
this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
|
|
1068
1054
|
}
|
|
1069
1055
|
else if (filterEnvelopeKeySet.has(key)) {
|
|
1070
1056
|
if (appliedFilterEnvelope)
|
|
@@ -1076,8 +1062,8 @@ export class MidyGMLite {
|
|
|
1076
1062
|
if (key in voiceParams)
|
|
1077
1063
|
noteVoiceParams[key] = voiceParams[key];
|
|
1078
1064
|
}
|
|
1079
|
-
this.setFilterEnvelope(note);
|
|
1080
|
-
this.setPitchEnvelope(note);
|
|
1065
|
+
this.setFilterEnvelope(note, scheduleTime);
|
|
1066
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1081
1067
|
}
|
|
1082
1068
|
else if (volumeEnvelopeKeySet.has(key)) {
|
|
1083
1069
|
if (appliedVolumeEnvelope)
|
|
@@ -1089,7 +1075,7 @@ export class MidyGMLite {
|
|
|
1089
1075
|
if (key in voiceParams)
|
|
1090
1076
|
noteVoiceParams[key] = voiceParams[key];
|
|
1091
1077
|
}
|
|
1092
|
-
this.setVolumeEnvelope(channel, note);
|
|
1078
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1093
1079
|
}
|
|
1094
1080
|
}
|
|
1095
1081
|
}
|
|
@@ -1111,12 +1097,12 @@ export class MidyGMLite {
|
|
|
1111
1097
|
123: this.allNotesOff,
|
|
1112
1098
|
};
|
|
1113
1099
|
}
|
|
1114
|
-
handleControlChange(channelNumber, controllerType, value,
|
|
1100
|
+
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1115
1101
|
const handler = this.controlChangeHandlers[controllerType];
|
|
1116
1102
|
if (handler) {
|
|
1117
|
-
handler.call(this, channelNumber, value,
|
|
1103
|
+
handler.call(this, channelNumber, value, scheduleTime);
|
|
1118
1104
|
const channel = this.channels[channelNumber];
|
|
1119
|
-
this.applyVoiceParams(channel, controllerType + 128);
|
|
1105
|
+
this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
|
|
1120
1106
|
}
|
|
1121
1107
|
else {
|
|
1122
1108
|
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
@@ -1162,26 +1148,26 @@ export class MidyGMLite {
|
|
|
1162
1148
|
channel.state.expression = expression / 127;
|
|
1163
1149
|
this.updateChannelVolume(channel, scheduleTime);
|
|
1164
1150
|
}
|
|
1165
|
-
dataEntryLSB(channelNumber, value) {
|
|
1151
|
+
dataEntryLSB(channelNumber, value, scheduleTime) {
|
|
1166
1152
|
this.channels[channelNumber].dataLSB = value;
|
|
1167
|
-
this.handleRPN(channelNumber);
|
|
1153
|
+
this.handleRPN(channelNumber, scheduleTime);
|
|
1168
1154
|
}
|
|
1169
1155
|
updateChannelVolume(channel, scheduleTime) {
|
|
1170
|
-
scheduleTime ??= this.audioContext.currentTime;
|
|
1171
1156
|
const state = channel.state;
|
|
1172
1157
|
const volume = state.volume * state.expression;
|
|
1173
1158
|
const { gainLeft, gainRight } = this.panToGain(state.pan);
|
|
1174
1159
|
channel.gainL.gain
|
|
1175
|
-
.cancelScheduledValues(
|
|
1160
|
+
.cancelScheduledValues(scheduleTime)
|
|
1176
1161
|
.setValueAtTime(volume * gainLeft, scheduleTime);
|
|
1177
1162
|
channel.gainR.gain
|
|
1178
|
-
.cancelScheduledValues(
|
|
1163
|
+
.cancelScheduledValues(scheduleTime)
|
|
1179
1164
|
.setValueAtTime(volume * gainRight, scheduleTime);
|
|
1180
1165
|
}
|
|
1181
|
-
setSustainPedal(channelNumber, value) {
|
|
1166
|
+
setSustainPedal(channelNumber, value, scheduleTime) {
|
|
1167
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1182
1168
|
this.channels[channelNumber].state.sustainPedal = value / 127;
|
|
1183
1169
|
if (value < 64) {
|
|
1184
|
-
this.releaseSustainPedal(channelNumber, value);
|
|
1170
|
+
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
1185
1171
|
}
|
|
1186
1172
|
}
|
|
1187
1173
|
limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
|
|
@@ -1202,12 +1188,12 @@ export class MidyGMLite {
|
|
|
1202
1188
|
channel.dataLSB = minLSB;
|
|
1203
1189
|
}
|
|
1204
1190
|
}
|
|
1205
|
-
handleRPN(channelNumber) {
|
|
1191
|
+
handleRPN(channelNumber, scheduleTime) {
|
|
1206
1192
|
const channel = this.channels[channelNumber];
|
|
1207
1193
|
const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
|
|
1208
1194
|
switch (rpn) {
|
|
1209
1195
|
case 0:
|
|
1210
|
-
this.handlePitchBendRangeRPN(channelNumber);
|
|
1196
|
+
this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
|
|
1211
1197
|
break;
|
|
1212
1198
|
default:
|
|
1213
1199
|
console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
|
|
@@ -1219,28 +1205,30 @@ export class MidyGMLite {
|
|
|
1219
1205
|
setRPNLSB(channelNumber, value) {
|
|
1220
1206
|
this.channels[channelNumber].rpnLSB = value;
|
|
1221
1207
|
}
|
|
1222
|
-
dataEntryMSB(channelNumber, value) {
|
|
1208
|
+
dataEntryMSB(channelNumber, value, scheduleTime) {
|
|
1223
1209
|
this.channels[channelNumber].dataMSB = value;
|
|
1224
|
-
this.handleRPN(channelNumber);
|
|
1210
|
+
this.handleRPN(channelNumber, scheduleTime);
|
|
1225
1211
|
}
|
|
1226
|
-
handlePitchBendRangeRPN(channelNumber) {
|
|
1212
|
+
handlePitchBendRangeRPN(channelNumber, scheduleTime) {
|
|
1227
1213
|
const channel = this.channels[channelNumber];
|
|
1228
1214
|
this.limitData(channel, 0, 127, 0, 99);
|
|
1229
1215
|
const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
|
|
1230
|
-
this.setPitchBendRange(channelNumber, pitchBendRange);
|
|
1216
|
+
this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
|
|
1231
1217
|
}
|
|
1232
|
-
setPitchBendRange(channelNumber, value) {
|
|
1218
|
+
setPitchBendRange(channelNumber, value, scheduleTime) {
|
|
1219
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1233
1220
|
const channel = this.channels[channelNumber];
|
|
1234
1221
|
const state = channel.state;
|
|
1235
1222
|
const prev = state.pitchWheelSensitivity;
|
|
1236
1223
|
const next = value / 128;
|
|
1237
1224
|
state.pitchWheelSensitivity = next;
|
|
1238
1225
|
channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
|
|
1239
|
-
this.updateChannelDetune(channel);
|
|
1240
|
-
this.applyVoiceParams(channel, 16);
|
|
1226
|
+
this.updateChannelDetune(channel, scheduleTime);
|
|
1227
|
+
this.applyVoiceParams(channel, 16, scheduleTime);
|
|
1241
1228
|
}
|
|
1242
|
-
allSoundOff(channelNumber) {
|
|
1243
|
-
|
|
1229
|
+
allSoundOff(channelNumber, _value, scheduleTime) {
|
|
1230
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1231
|
+
return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
|
|
1244
1232
|
}
|
|
1245
1233
|
resetAllControllers(channelNumber) {
|
|
1246
1234
|
const stateTypes = [
|
|
@@ -1264,10 +1252,11 @@ export class MidyGMLite {
|
|
|
1264
1252
|
channel[type] = this.constructor.channelSettings[type];
|
|
1265
1253
|
}
|
|
1266
1254
|
}
|
|
1267
|
-
allNotesOff(channelNumber) {
|
|
1268
|
-
|
|
1255
|
+
allNotesOff(channelNumber, _value, scheduleTime) {
|
|
1256
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1257
|
+
return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
|
|
1269
1258
|
}
|
|
1270
|
-
handleUniversalNonRealTimeExclusiveMessage(data) {
|
|
1259
|
+
handleUniversalNonRealTimeExclusiveMessage(data, _scheduleTime) {
|
|
1271
1260
|
switch (data[2]) {
|
|
1272
1261
|
case 9:
|
|
1273
1262
|
switch (data[3]) {
|
|
@@ -1291,12 +1280,12 @@ export class MidyGMLite {
|
|
|
1291
1280
|
}
|
|
1292
1281
|
this.channels[9].bank = 128;
|
|
1293
1282
|
}
|
|
1294
|
-
handleUniversalRealTimeExclusiveMessage(data) {
|
|
1283
|
+
handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
|
|
1295
1284
|
switch (data[2]) {
|
|
1296
1285
|
case 4:
|
|
1297
1286
|
switch (data[3]) {
|
|
1298
1287
|
case 1:
|
|
1299
|
-
return this.handleMasterVolumeSysEx(data);
|
|
1288
|
+
return this.handleMasterVolumeSysEx(data, scheduleTime);
|
|
1300
1289
|
default:
|
|
1301
1290
|
console.warn(`Unsupported Exclusive Message: ${data}`);
|
|
1302
1291
|
}
|
|
@@ -1305,42 +1294,40 @@ export class MidyGMLite {
|
|
|
1305
1294
|
console.warn(`Unsupported Exclusive Message: ${data}`);
|
|
1306
1295
|
}
|
|
1307
1296
|
}
|
|
1308
|
-
handleMasterVolumeSysEx(data) {
|
|
1297
|
+
handleMasterVolumeSysEx(data, scheduleTime) {
|
|
1309
1298
|
const volume = (data[5] * 128 + data[4]) / 16383;
|
|
1310
|
-
this.setMasterVolume(volume);
|
|
1299
|
+
this.setMasterVolume(volume, scheduleTime);
|
|
1311
1300
|
}
|
|
1312
|
-
setMasterVolume(volume) {
|
|
1301
|
+
setMasterVolume(volume, scheduleTime) {
|
|
1302
|
+
scheduleTime ??= this.audioContext.currentTime;
|
|
1313
1303
|
if (volume < 0 && 1 < volume) {
|
|
1314
1304
|
console.error("Master Volume is out of range");
|
|
1315
1305
|
}
|
|
1316
1306
|
else {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1307
|
+
this.masterVolume.gain
|
|
1308
|
+
.cancelScheduledValues(scheduleTime)
|
|
1309
|
+
.setValueAtTime(volume * volume, scheduleTime);
|
|
1320
1310
|
}
|
|
1321
1311
|
}
|
|
1322
|
-
|
|
1323
|
-
console.warn(`Unsupported Exclusive Message: ${data}`);
|
|
1324
|
-
}
|
|
1325
|
-
handleSysEx(data) {
|
|
1312
|
+
handleSysEx(data, scheduleTime) {
|
|
1326
1313
|
switch (data[0]) {
|
|
1327
1314
|
case 126:
|
|
1328
|
-
return this.handleUniversalNonRealTimeExclusiveMessage(data);
|
|
1315
|
+
return this.handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime);
|
|
1329
1316
|
case 127:
|
|
1330
|
-
return this.handleUniversalRealTimeExclusiveMessage(data);
|
|
1317
|
+
return this.handleUniversalRealTimeExclusiveMessage(data, scheduleTime);
|
|
1331
1318
|
default:
|
|
1332
|
-
|
|
1319
|
+
console.warn(`Unsupported Exclusive Message: ${data}`);
|
|
1333
1320
|
}
|
|
1334
1321
|
}
|
|
1335
|
-
scheduleTask(callback,
|
|
1322
|
+
scheduleTask(callback, scheduleTime) {
|
|
1336
1323
|
return new Promise((resolve) => {
|
|
1337
1324
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
1338
1325
|
bufferSource.onended = () => {
|
|
1339
1326
|
callback();
|
|
1340
1327
|
resolve();
|
|
1341
1328
|
};
|
|
1342
|
-
bufferSource.start(
|
|
1343
|
-
bufferSource.stop(
|
|
1329
|
+
bufferSource.start(scheduleTime);
|
|
1330
|
+
bufferSource.stop(scheduleTime);
|
|
1344
1331
|
});
|
|
1345
1332
|
}
|
|
1346
1333
|
}
|