@marmooo/midy 0.3.1 → 0.3.2

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/esm/midy-GM2.js CHANGED
@@ -1,59 +1,19 @@
1
1
  import { parseMidi } from "midi-file";
2
2
  import { parse, SoundFont } from "@marmooo/soundfont-parser";
3
- // 2-3 times faster than Map
4
- class SparseMap {
5
- constructor(size) {
6
- this.data = new Array(size);
7
- this.activeIndices = [];
8
- }
9
- set(key, value) {
10
- if (this.data[key] === undefined) {
11
- this.activeIndices.push(key);
12
- }
13
- this.data[key] = value;
14
- }
15
- get(key) {
16
- return this.data[key];
17
- }
18
- delete(key) {
19
- if (this.data[key] !== undefined) {
20
- this.data[key] = undefined;
21
- const index = this.activeIndices.indexOf(key);
22
- if (index !== -1) {
23
- this.activeIndices.splice(index, 1);
24
- }
25
- return true;
26
- }
27
- return false;
28
- }
29
- has(key) {
30
- return this.data[key] !== undefined;
31
- }
32
- get size() {
33
- return this.activeIndices.length;
34
- }
35
- clear() {
36
- for (let i = 0; i < this.activeIndices.length; i++) {
37
- const key = this.activeIndices[i];
38
- this.data[key] = undefined;
39
- }
40
- this.activeIndices = [];
41
- }
42
- *[Symbol.iterator]() {
43
- for (let i = 0; i < this.activeIndices.length; i++) {
44
- const key = this.activeIndices[i];
45
- yield [key, this.data[key]];
46
- }
47
- }
48
- forEach(callback) {
49
- for (let i = 0; i < this.activeIndices.length; i++) {
50
- const key = this.activeIndices[i];
51
- callback(this.data[key], key, this);
52
- }
53
- }
54
- }
55
3
  class Note {
56
4
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
5
+ Object.defineProperty(this, "index", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: -1
10
+ });
11
+ Object.defineProperty(this, "noteOffEvent", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: void 0
16
+ });
57
17
  Object.defineProperty(this, "bufferSource", {
58
18
  enumerable: true,
59
19
  configurable: true,
@@ -138,11 +98,11 @@ class Note {
138
98
  writable: true,
139
99
  value: void 0
140
100
  });
141
- Object.defineProperty(this, "portamento", {
101
+ Object.defineProperty(this, "portamentoNoteNumber", {
142
102
  enumerable: true,
143
103
  configurable: true,
144
104
  writable: true,
145
- value: void 0
105
+ value: -1
146
106
  });
147
107
  this.noteNumber = noteNumber;
148
108
  this.velocity = velocity;
@@ -197,7 +157,7 @@ const defaultControllerState = {
197
157
  portamentoTime: { type: 128 + 5, defaultValue: 0 },
198
158
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
199
159
  volume: { type: 128 + 7, defaultValue: 100 / 127 },
200
- pan: { type: 128 + 10, defaultValue: 0.5 },
160
+ pan: { type: 128 + 10, defaultValue: 64 / 127 },
201
161
  expression: { type: 128 + 11, defaultValue: 1 },
202
162
  // bankLSB: { type: 128 + 32, defaultValue: 0, },
203
163
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
@@ -205,14 +165,6 @@ const defaultControllerState = {
205
165
  portamento: { type: 128 + 65, defaultValue: 0 },
206
166
  sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
207
167
  softPedal: { type: 128 + 67, defaultValue: 0 },
208
- filterResonance: { type: 128 + 71, defaultValue: 0.5 },
209
- releaseTime: { type: 128 + 72, defaultValue: 0.5 },
210
- attackTime: { type: 128 + 73, defaultValue: 0.5 },
211
- brightness: { type: 128 + 74, defaultValue: 0.5 },
212
- decayTime: { type: 128 + 75, defaultValue: 0.5 },
213
- vibratoRate: { type: 128 + 76, defaultValue: 0.5 },
214
- vibratoDepth: { type: 128 + 77, defaultValue: 0.5 },
215
- vibratoDelay: { type: 128 + 78, defaultValue: 0.5 },
216
168
  reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
217
169
  chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
218
170
  // dataIncrement: { type: 128 + 96, defaultValue: 0 },
@@ -487,7 +439,7 @@ export class MidyGM2 {
487
439
  initSoundFontTable() {
488
440
  const table = new Array(128);
489
441
  for (let i = 0; i < 128; i++) {
490
- table[i] = new SparseMap(128);
442
+ table[i] = new Map();
491
443
  }
492
444
  return table;
493
445
  }
@@ -534,18 +486,24 @@ export class MidyGM2 {
534
486
  merger,
535
487
  };
536
488
  }
489
+ resetChannelTable(channel) {
490
+ this.resetControlTable(channel.controlTable);
491
+ channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
492
+ channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
493
+ channel.keyBasedInstrumentControlTable.fill(0); // [-64, 63]
494
+ }
537
495
  createChannels(audioContext) {
538
496
  const channels = Array.from({ length: this.numChannels }, () => {
539
497
  return {
540
498
  currentBufferSource: null,
541
499
  isDrum: false,
542
- ...this.constructor.channelSettings,
543
500
  state: new ControllerState(),
544
- controlTable: this.initControlTable(),
501
+ ...this.constructor.channelSettings,
545
502
  ...this.setChannelAudioNodes(audioContext),
546
- scheduledNotes: new SparseMap(128),
503
+ scheduledNotes: [],
547
504
  sustainNotes: [],
548
- sostenutoNotes: new SparseMap(128),
505
+ sostenutoNotes: [],
506
+ controlTable: this.initControlTable(),
549
507
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
550
508
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
551
509
  keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
@@ -592,46 +550,20 @@ export class MidyGM2 {
592
550
  }
593
551
  return bufferSource;
594
552
  }
595
- findPortamentoTarget(queueIndex) {
596
- const endEvent = this.timeline[queueIndex];
597
- if (!this.channels[endEvent.channel].portamento)
598
- return;
599
- const endTime = endEvent.startTime;
600
- let target;
601
- while (++queueIndex < this.timeline.length) {
602
- const event = this.timeline[queueIndex];
603
- if (endTime !== event.startTime)
604
- break;
605
- if (event.type !== "noteOn")
606
- continue;
607
- if (!target || event.noteNumber < target.noteNumber) {
608
- target = event;
609
- }
610
- }
611
- return target;
612
- }
613
- async scheduleTimelineEvents(t, offset, queueIndex) {
553
+ async scheduleTimelineEvents(t, resumeTime, queueIndex) {
614
554
  while (queueIndex < this.timeline.length) {
615
555
  const event = this.timeline[queueIndex];
616
556
  if (event.startTime > t + this.lookAhead)
617
557
  break;
618
- const startTime = event.startTime + this.startDelay - offset;
558
+ const delay = this.startDelay - resumeTime;
559
+ const startTime = event.startTime + delay;
619
560
  switch (event.type) {
620
- case "noteOn":
621
- if (0 < event.velocity) {
622
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
623
- break;
624
- }
625
- /* falls through */
626
- case "noteOff": {
627
- const portamentoTarget = this.findPortamentoTarget(queueIndex);
628
- if (portamentoTarget)
629
- portamentoTarget.portamento = true;
630
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
631
- portamentoTarget?.noteNumber);
632
- if (notePromise) {
633
- this.notePromises.push(notePromise);
634
- }
561
+ case "noteOn": {
562
+ const noteOffEvent = {
563
+ ...event.noteOffEvent,
564
+ startTime: event.noteOffEvent.startTime + delay,
565
+ };
566
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
635
567
  break;
636
568
  }
637
569
  case "controller":
@@ -667,7 +599,7 @@ export class MidyGM2 {
667
599
  this.isPaused = false;
668
600
  this.startTime = this.audioContext.currentTime;
669
601
  let queueIndex = this.getQueueIndex(this.resumeTime);
670
- let offset = this.resumeTime - this.startTime;
602
+ let resumeTime = this.resumeTime - this.startTime;
671
603
  this.notePromises = [];
672
604
  const schedulePlayback = async () => {
673
605
  if (queueIndex >= this.timeline.length) {
@@ -676,18 +608,21 @@ export class MidyGM2 {
676
608
  this.exclusiveClassNotes.fill(undefined);
677
609
  this.drumExclusiveClassNotes.fill(undefined);
678
610
  this.audioBufferCache.clear();
611
+ for (let i = 0; i < this.channels.length; i++) {
612
+ this.resetAllStates(i);
613
+ }
679
614
  resolve();
680
615
  return;
681
616
  }
682
617
  const now = this.audioContext.currentTime;
683
- const t = now + offset;
684
- queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
618
+ const t = now + resumeTime;
619
+ queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
685
620
  if (this.isPausing) {
686
621
  await this.stopNotes(0, true, now);
687
622
  this.notePromises = [];
688
- resolve();
689
623
  this.isPausing = false;
690
624
  this.isPaused = true;
625
+ resolve();
691
626
  return;
692
627
  }
693
628
  else if (this.isStopping) {
@@ -696,9 +631,12 @@ export class MidyGM2 {
696
631
  this.exclusiveClassNotes.fill(undefined);
697
632
  this.drumExclusiveClassNotes.fill(undefined);
698
633
  this.audioBufferCache.clear();
699
- resolve();
634
+ for (let i = 0; i < this.channels.length; i++) {
635
+ this.resetAllStates(i);
636
+ }
700
637
  this.isStopping = false;
701
638
  this.isPaused = false;
639
+ resolve();
702
640
  return;
703
641
  }
704
642
  else if (this.isSeeking) {
@@ -707,7 +645,7 @@ export class MidyGM2 {
707
645
  this.drumExclusiveClassNotes.fill(undefined);
708
646
  this.startTime = this.audioContext.currentTime;
709
647
  queueIndex = this.getQueueIndex(this.resumeTime);
710
- offset = this.resumeTime - this.startTime;
648
+ resumeTime = this.resumeTime - this.startTime;
711
649
  this.isSeeking = false;
712
650
  await schedulePlayback();
713
651
  }
@@ -829,13 +767,37 @@ export class MidyGM2 {
829
767
  prevTempoTicks = event.ticks;
830
768
  }
831
769
  }
770
+ const activeNotes = new Array(this.channels.length * 128);
771
+ for (let i = 0; i < activeNotes.length; i++) {
772
+ activeNotes[i] = [];
773
+ }
774
+ for (let i = 0; i < timeline.length; i++) {
775
+ const event = timeline[i];
776
+ switch (event.type) {
777
+ case "noteOn": {
778
+ const index = event.channel * 128 + event.noteNumber;
779
+ activeNotes[index].push(event);
780
+ break;
781
+ }
782
+ case "noteOff": {
783
+ const index = event.channel * 128 + event.noteNumber;
784
+ const noteOn = activeNotes[index].pop();
785
+ if (noteOn) {
786
+ noteOn.noteOffEvent = event;
787
+ }
788
+ else {
789
+ const eventString = JSON.stringify(event, null, 2);
790
+ console.warn(`noteOff without matching noteOn: ${eventString}`);
791
+ }
792
+ }
793
+ }
794
+ }
832
795
  return { instruments, timeline };
833
796
  }
834
797
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
835
798
  const channel = this.channels[channelNumber];
836
799
  const promises = [];
837
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
838
- activeNotes.forEach((note) => {
800
+ this.processActiveNotes(channel, scheduleTime, (note) => {
839
801
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
840
802
  this.notePromises.push(promise);
841
803
  promises.push(promise);
@@ -846,11 +808,11 @@ export class MidyGM2 {
846
808
  const channel = this.channels[channelNumber];
847
809
  const promises = [];
848
810
  this.processScheduledNotes(channel, (note) => {
849
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
811
+ const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
850
812
  this.notePromises.push(promise);
851
813
  promises.push(promise);
852
814
  });
853
- channel.scheduledNotes.clear();
815
+ channel.scheduledNotes = [];
854
816
  return Promise.all(promises);
855
817
  }
856
818
  stopNotes(velocity, force, scheduleTime) {
@@ -871,9 +833,6 @@ export class MidyGM2 {
871
833
  if (!this.isPlaying)
872
834
  return;
873
835
  this.isStopping = true;
874
- for (let i = 0; i < this.channels.length; i++) {
875
- this.resetAllStates(i);
876
- }
877
836
  }
878
837
  pause() {
879
838
  if (!this.isPlaying || this.isPaused)
@@ -908,37 +867,31 @@ export class MidyGM2 {
908
867
  return this.resumeTime + now - this.startTime - this.startDelay;
909
868
  }
910
869
  processScheduledNotes(channel, callback) {
911
- channel.scheduledNotes.forEach((noteList) => {
912
- for (let i = 0; i < noteList.length; i++) {
913
- const note = noteList[i];
914
- if (!note)
915
- continue;
916
- if (note.ending)
917
- continue;
918
- callback(note);
919
- }
920
- });
921
- }
922
- getActiveNotes(channel, scheduleTime) {
923
- const activeNotes = new SparseMap(128);
924
- channel.scheduledNotes.forEach((noteList) => {
925
- const activeNote = this.getActiveNote(noteList, scheduleTime);
926
- if (activeNote) {
927
- activeNotes.set(activeNote.noteNumber, activeNote);
928
- }
929
- });
930
- return activeNotes;
870
+ const scheduledNotes = channel.scheduledNotes;
871
+ for (let i = 0; i < scheduledNotes.length; i++) {
872
+ const note = scheduledNotes[i];
873
+ if (!note)
874
+ continue;
875
+ if (note.ending)
876
+ continue;
877
+ callback(note);
878
+ }
931
879
  }
932
- getActiveNote(noteList, scheduleTime) {
933
- for (let i = noteList.length - 1; i >= 0; i--) {
934
- const note = noteList[i];
880
+ processActiveNotes(channel, scheduleTime, callback) {
881
+ const scheduledNotes = channel.scheduledNotes;
882
+ for (let i = 0; i < scheduledNotes.length; i++) {
883
+ const note = scheduledNotes[i];
935
884
  if (!note)
936
- return;
885
+ continue;
886
+ if (note.ending)
887
+ continue;
888
+ const noteOffEvent = note.noteOffEvent;
889
+ if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
890
+ continue;
937
891
  if (scheduleTime < note.startTime)
938
892
  continue;
939
- return (note.ending) ? null : note;
893
+ callback(note);
940
894
  }
941
- return noteList[0];
942
895
  }
943
896
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
944
897
  const sampleRate = audioContext.sampleRate;
@@ -1103,25 +1056,95 @@ export class MidyGM2 {
1103
1056
  }
1104
1057
  updateDetune(channel, note, scheduleTime) {
1105
1058
  const noteDetune = this.calcNoteDetune(channel, note);
1106
- const detune = channel.detune + noteDetune;
1107
- note.bufferSource.detune
1108
- .cancelScheduledValues(scheduleTime)
1109
- .setValueAtTime(detune, scheduleTime);
1110
- }
1111
- getPortamentoTime(channel) {
1112
- const factor = 5 * Math.log(10) * 127;
1113
- return channel.state.portamentoTime * factor;
1114
- }
1115
- setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1059
+ const pitchControl = this.getPitchControl(channel, note);
1060
+ const detune = channel.detune + noteDetune + pitchControl;
1061
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1062
+ const startTime = note.startTime;
1063
+ const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1064
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1065
+ note.bufferSource.detune
1066
+ .cancelScheduledValues(scheduleTime)
1067
+ .setValueAtTime(detune - deltaCent, scheduleTime)
1068
+ .linearRampToValueAtTime(detune, portamentoTime);
1069
+ }
1070
+ else {
1071
+ note.bufferSource.detune
1072
+ .cancelScheduledValues(scheduleTime)
1073
+ .setValueAtTime(detune, scheduleTime);
1074
+ }
1075
+ }
1076
+ getPortamentoTime(channel, note) {
1077
+ const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1078
+ const value = Math.ceil(channel.state.portamentoTime * 127);
1079
+ return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1080
+ }
1081
+ getPitchIncrementSpeed(value) {
1082
+ const points = [
1083
+ [0, 1000],
1084
+ [6, 100],
1085
+ [16, 20],
1086
+ [32, 10],
1087
+ [48, 5],
1088
+ [64, 2.5],
1089
+ [80, 1],
1090
+ [96, 0.4],
1091
+ [112, 0.15],
1092
+ [127, 0.01],
1093
+ ];
1094
+ const logPoints = new Array(points.length);
1095
+ for (let i = 0; i < points.length; i++) {
1096
+ const [x, y] = points[i];
1097
+ if (value === x)
1098
+ return y;
1099
+ logPoints[i] = [x, Math.log(y)];
1100
+ }
1101
+ let startIndex = 0;
1102
+ for (let i = 1; i < logPoints.length; i++) {
1103
+ if (value <= logPoints[i][0]) {
1104
+ startIndex = i - 1;
1105
+ break;
1106
+ }
1107
+ }
1108
+ const [x0, y0] = logPoints[startIndex];
1109
+ const [x1, y1] = logPoints[startIndex + 1];
1110
+ const h = x1 - x0;
1111
+ const t = (value - x0) / h;
1112
+ let m0, m1;
1113
+ if (startIndex === 0) {
1114
+ m0 = (y1 - y0) / h;
1115
+ }
1116
+ else {
1117
+ const [xPrev, yPrev] = logPoints[startIndex - 1];
1118
+ m0 = (y1 - yPrev) / (x1 - xPrev);
1119
+ }
1120
+ if (startIndex === logPoints.length - 2) {
1121
+ m1 = (y1 - y0) / h;
1122
+ }
1123
+ else {
1124
+ const [xNext, yNext] = logPoints[startIndex + 2];
1125
+ m1 = (yNext - y0) / (xNext - x0);
1126
+ }
1127
+ // Cubic Hermite Spline
1128
+ const t2 = t * t;
1129
+ const t3 = t2 * t;
1130
+ const h00 = 2 * t3 - 3 * t2 + 1;
1131
+ const h10 = t3 - 2 * t2 + t;
1132
+ const h01 = -2 * t3 + 3 * t2;
1133
+ const h11 = t3 - t2;
1134
+ const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
1135
+ return Math.exp(y);
1136
+ }
1137
+ setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1116
1138
  const { voiceParams, startTime } = note;
1117
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1139
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1140
+ (1 + this.getAmplitudeControl(channel));
1118
1141
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1119
1142
  const volDelay = startTime + voiceParams.volDelay;
1120
- const portamentoTime = volDelay + this.getPortamentoTime(channel);
1143
+ const volAttack = volDelay + voiceParams.volAttack;
1144
+ const volHold = volAttack + voiceParams.volHold;
1121
1145
  note.volumeEnvelopeNode.gain
1122
1146
  .cancelScheduledValues(scheduleTime)
1123
- .setValueAtTime(0, volDelay)
1124
- .linearRampToValueAtTime(sustainVolume, portamentoTime);
1147
+ .setValueAtTime(sustainVolume, volHold);
1125
1148
  }
1126
1149
  setVolumeEnvelope(channel, note, scheduleTime) {
1127
1150
  const { voiceParams, startTime } = note;
@@ -1140,6 +1163,12 @@ export class MidyGM2 {
1140
1163
  .setValueAtTime(attackVolume, volHold)
1141
1164
  .linearRampToValueAtTime(sustainVolume, volDecay);
1142
1165
  }
1166
+ setPortamentoPitchEnvelope(note, scheduleTime) {
1167
+ const baseRate = note.voiceParams.playbackRate;
1168
+ note.bufferSource.playbackRate
1169
+ .cancelScheduledValues(scheduleTime)
1170
+ .setValueAtTime(baseRate, scheduleTime);
1171
+ }
1143
1172
  setPitchEnvelope(note, scheduleTime) {
1144
1173
  const { voiceParams } = note;
1145
1174
  const baseRate = voiceParams.playbackRate;
@@ -1167,19 +1196,20 @@ export class MidyGM2 {
1167
1196
  const maxFrequency = 20000; // max Hz of initialFilterFc
1168
1197
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1169
1198
  }
1170
- setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1199
+ setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1171
1200
  const state = channel.state;
1172
1201
  const { voiceParams, noteNumber, startTime } = note;
1173
1202
  const softPedalFactor = 1 -
1174
1203
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1175
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1176
- softPedalFactor;
1204
+ const baseCent = voiceParams.initialFilterFc +
1205
+ this.getFilterCutoffControl(channel);
1206
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1177
1207
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
1178
1208
  const sustainFreq = baseFreq +
1179
1209
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1180
1210
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1181
1211
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1182
- const portamentoTime = startTime + this.getPortamentoTime(channel);
1212
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1183
1213
  const modDelay = startTime + voiceParams.modDelay;
1184
1214
  note.filterNode.frequency
1185
1215
  .cancelScheduledValues(scheduleTime)
@@ -1264,7 +1294,7 @@ export class MidyGM2 {
1264
1294
  return audioBuffer;
1265
1295
  }
1266
1296
  }
1267
- async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1297
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1268
1298
  const now = this.audioContext.currentTime;
1269
1299
  const state = channel.state;
1270
1300
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
@@ -1280,20 +1310,24 @@ export class MidyGM2 {
1280
1310
  type: "lowpass",
1281
1311
  Q: voiceParams.initialFilterQ / 10, // dB
1282
1312
  });
1283
- if (0.5 <= state.portamento && portamento) {
1284
- note.portamento = true;
1285
- this.setPortamentoStartVolumeEnvelope(channel, note, now);
1286
- this.setPortamentoStartFilterEnvelope(channel, note, now);
1313
+ const prevNote = channel.scheduledNotes.at(-1);
1314
+ if (prevNote && prevNote.noteNumber !== noteNumber) {
1315
+ note.portamentoNoteNumber = prevNote.noteNumber;
1316
+ }
1317
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1318
+ this.setPortamentoVolumeEnvelope(channel, note, now);
1319
+ this.setPortamentoFilterEnvelope(channel, note, now);
1320
+ this.setPortamentoPitchEnvelope(note, now);
1287
1321
  }
1288
1322
  else {
1289
- note.portamento = false;
1290
1323
  this.setVolumeEnvelope(channel, note, now);
1291
1324
  this.setFilterEnvelope(channel, note, now);
1325
+ this.setPitchEnvelope(note, now);
1292
1326
  }
1327
+ this.updateDetune(channel, note, now);
1293
1328
  if (0 < state.vibratoDepth) {
1294
1329
  this.startVibrato(channel, note, now);
1295
1330
  }
1296
- this.setPitchEnvelope(note, now);
1297
1331
  if (0 < state.modulationDepth) {
1298
1332
  this.startModulation(channel, note, now);
1299
1333
  }
@@ -1339,9 +1373,8 @@ export class MidyGM2 {
1339
1373
  if (prev) {
1340
1374
  const [prevNote, prevChannelNumber] = prev;
1341
1375
  if (prevNote && !prevNote.ending) {
1342
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1343
- startTime, true, // force
1344
- undefined);
1376
+ this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
1377
+ startTime, true);
1345
1378
  }
1346
1379
  }
1347
1380
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
@@ -1360,9 +1393,8 @@ export class MidyGM2 {
1360
1393
  channelNumber;
1361
1394
  const prevNote = this.drumExclusiveClassNotes[index];
1362
1395
  if (prevNote && !prevNote.ending) {
1363
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1364
- startTime, true, // force
1365
- undefined);
1396
+ this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
1397
+ startTime, true);
1366
1398
  }
1367
1399
  this.drumExclusiveClassNotes[index] = note;
1368
1400
  }
@@ -1373,7 +1405,7 @@ export class MidyGM2 {
1373
1405
  return !((programNumber === 48 && noteNumber === 88) ||
1374
1406
  (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1375
1407
  }
1376
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1408
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1377
1409
  const channel = this.channels[channelNumber];
1378
1410
  const bankNumber = this.calcBank(channel, channelNumber);
1379
1411
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1384,7 +1416,8 @@ export class MidyGM2 {
1384
1416
  if (!voice)
1385
1417
  return;
1386
1418
  const isSF3 = soundFont.parsed.info.version.major === 3;
1387
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1419
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1420
+ note.noteOffEvent = noteOffEvent;
1388
1421
  note.gainL.connect(channel.gainL);
1389
1422
  note.gainR.connect(channel.gainR);
1390
1423
  if (0.5 <= channel.state.sustainPedal) {
@@ -1393,20 +1426,13 @@ export class MidyGM2 {
1393
1426
  this.handleExclusiveClass(note, channelNumber, startTime);
1394
1427
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1395
1428
  const scheduledNotes = channel.scheduledNotes;
1396
- let noteList = scheduledNotes.get(noteNumber);
1397
- if (noteList) {
1398
- noteList.push(note);
1399
- }
1400
- else {
1401
- noteList = [note];
1402
- scheduledNotes.set(noteNumber, noteList);
1403
- }
1429
+ note.index = scheduledNotes.length;
1430
+ scheduledNotes.push(note);
1404
1431
  if (this.isDrumNoteOffException(channel, noteNumber)) {
1405
1432
  const stopTime = startTime + note.bufferSource.buffer.duration;
1406
- const index = noteList.length - 1;
1407
1433
  const promise = new Promise((resolve) => {
1408
1434
  note.bufferSource.onended = () => {
1409
- noteList[index] = undefined;
1435
+ scheduledNotes[note.index] = undefined;
1410
1436
  this.disconnectNote(note);
1411
1437
  resolve();
1412
1438
  };
@@ -1414,10 +1440,23 @@ export class MidyGM2 {
1414
1440
  });
1415
1441
  this.notePromises.push(promise);
1416
1442
  }
1443
+ else if (noteOffEvent) {
1444
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1445
+ const portamentoTime = this.getPortamentoTime(channel, note);
1446
+ const portamentoEndTime = startTime + portamentoTime;
1447
+ const notePromise = this.scheduleNoteOff(channelNumber, note, 0, // velocity
1448
+ Math.max(noteOffEvent.startTime, portamentoEndTime), false);
1449
+ this.notePromises.push(notePromise);
1450
+ }
1451
+ else {
1452
+ const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
1453
+ this.notePromises.push(notePromise);
1454
+ }
1455
+ }
1417
1456
  }
1418
1457
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1419
1458
  scheduleTime ??= this.audioContext.currentTime;
1420
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1459
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1421
1460
  }
1422
1461
  disconnectNote(note) {
1423
1462
  note.bufferSource.disconnect();
@@ -1442,8 +1481,7 @@ export class MidyGM2 {
1442
1481
  note.chorusEffectsSend.disconnect();
1443
1482
  }
1444
1483
  }
1445
- stopNote(endTime, stopTime, noteList, index) {
1446
- const note = noteList[index];
1484
+ stopNote(channel, note, endTime, stopTime) {
1447
1485
  note.volumeEnvelopeNode.gain
1448
1486
  .cancelScheduledValues(endTime)
1449
1487
  .linearRampToValueAtTime(0, stopTime);
@@ -1453,73 +1491,57 @@ export class MidyGM2 {
1453
1491
  }, stopTime);
1454
1492
  return new Promise((resolve) => {
1455
1493
  note.bufferSource.onended = () => {
1456
- noteList[index] = undefined;
1494
+ channel.scheduledNotes[note.index] = undefined;
1457
1495
  this.disconnectNote(note);
1458
1496
  resolve();
1459
1497
  };
1460
1498
  note.bufferSource.stop(stopTime);
1461
1499
  });
1462
1500
  }
1463
- findNoteOffTarget(noteList) {
1464
- for (let i = 0; i < noteList.length; i++) {
1465
- const note = noteList[i];
1466
- if (!note)
1467
- continue;
1468
- if (note.ending)
1469
- continue;
1470
- return [note, i];
1471
- }
1472
- }
1473
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1501
+ scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
1474
1502
  const channel = this.channels[channelNumber];
1475
- if (this.isDrumNoteOffException(channel, noteNumber))
1503
+ if (this.isDrumNoteOffException(channel, note.noteNumber))
1476
1504
  return;
1477
1505
  const state = channel.state;
1478
1506
  if (!force) {
1479
1507
  if (0.5 <= state.sustainPedal)
1480
1508
  return;
1481
- if (channel.sostenutoNotes.has(noteNumber))
1509
+ if (0.5 <= channel.state.sostenutoPedal)
1482
1510
  return;
1483
1511
  }
1484
- const noteList = channel.scheduledNotes.get(noteNumber);
1485
- if (!noteList)
1486
- return; // be careful with drum channel
1487
- const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
1488
- if (!noteOffTarget)
1489
- return;
1490
- const [note, i] = noteOffTarget;
1491
- if (0.5 <= state.portamento && portamentoNoteNumber !== undefined) {
1492
- const portamentoTime = endTime + this.getPortamentoTime(channel);
1493
- const deltaNote = portamentoNoteNumber - noteNumber;
1494
- const baseRate = note.voiceParams.playbackRate;
1495
- const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1496
- note.bufferSource.playbackRate
1497
- .cancelScheduledValues(endTime)
1498
- .linearRampToValueAtTime(targetRate, portamentoTime);
1499
- return this.stopNote(endTime, portamentoTime, noteList, i);
1500
- }
1501
- else {
1502
- const volRelease = endTime +
1503
- note.voiceParams.volRelease * state.releaseTime * 2;
1504
- const modRelease = endTime + note.voiceParams.modRelease;
1505
- note.filterNode.frequency
1506
- .cancelScheduledValues(endTime)
1507
- .linearRampToValueAtTime(0, modRelease);
1508
- const stopTime = Math.min(volRelease, modRelease);
1509
- return this.stopNote(endTime, stopTime, noteList, i);
1512
+ const volRelease = endTime + note.voiceParams.volRelease;
1513
+ const modRelease = endTime + note.voiceParams.modRelease;
1514
+ note.filterNode.frequency
1515
+ .cancelScheduledValues(endTime)
1516
+ .linearRampToValueAtTime(0, modRelease);
1517
+ const stopTime = Math.min(volRelease, modRelease);
1518
+ return this.stopNote(channel, note, endTime, stopTime);
1519
+ }
1520
+ findNoteOffTarget(channel, noteNumber) {
1521
+ const scheduledNotes = channel.scheduledNotes;
1522
+ for (let i = 0; i < scheduledNotes.length; i++) {
1523
+ const note = scheduledNotes[i];
1524
+ if (!note)
1525
+ continue;
1526
+ if (note.ending)
1527
+ continue;
1528
+ if (note.noteNumber !== noteNumber)
1529
+ continue;
1530
+ return note;
1510
1531
  }
1511
1532
  }
1512
1533
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1513
1534
  scheduleTime ??= this.audioContext.currentTime;
1514
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1515
- undefined);
1535
+ const channel = this.channels[channelNumber];
1536
+ const note = this.findNoteOffTarget(channel, noteNumber);
1537
+ return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
1516
1538
  }
1517
1539
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1518
1540
  const velocity = halfVelocity * 2;
1519
1541
  const channel = this.channels[channelNumber];
1520
1542
  const promises = [];
1521
1543
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1522
- const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1544
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
1523
1545
  promises.push(promise);
1524
1546
  }
1525
1547
  channel.sustainNotes = [];
@@ -1529,12 +1551,14 @@ export class MidyGM2 {
1529
1551
  const velocity = halfVelocity * 2;
1530
1552
  const channel = this.channels[channelNumber];
1531
1553
  const promises = [];
1554
+ const sostenutoNotes = channel.sostenutoNotes;
1532
1555
  channel.state.sostenutoPedal = 0;
1533
- channel.sostenutoNotes.forEach((note) => {
1534
- const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1556
+ for (let i = 0; i < sostenutoNotes.length; i++) {
1557
+ const note = sostenutoNotes[i];
1558
+ const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
1535
1559
  promises.push(promise);
1536
- });
1537
- channel.sostenutoNotes.clear();
1560
+ }
1561
+ channel.sostenutoNotes = [];
1538
1562
  return promises;
1539
1563
  }
1540
1564
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
@@ -1584,7 +1608,7 @@ export class MidyGM2 {
1584
1608
  channel.detune += pressureDepth * (next - prev);
1585
1609
  }
1586
1610
  const table = channel.channelPressureTable;
1587
- this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1611
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1588
1612
  this.setControllerParameters(channel, note, table);
1589
1613
  });
1590
1614
  this.applyVoiceParams(channel, 13);
@@ -1791,8 +1815,8 @@ export class MidyGM2 {
1791
1815
  if (key in voiceParams)
1792
1816
  noteVoiceParams[key] = voiceParams[key];
1793
1817
  }
1794
- if (0.5 <= channel.state.portamento && note.portamento) {
1795
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1818
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1819
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1796
1820
  }
1797
1821
  else {
1798
1822
  this.setFilterEnvelope(channel, note, scheduleTime);
@@ -1815,32 +1839,32 @@ export class MidyGM2 {
1815
1839
  });
1816
1840
  }
1817
1841
  createControlChangeHandlers() {
1818
- return {
1819
- 0: this.setBankMSB,
1820
- 1: this.setModulationDepth,
1821
- 5: this.setPortamentoTime,
1822
- 6: this.dataEntryMSB,
1823
- 7: this.setVolume,
1824
- 10: this.setPan,
1825
- 11: this.setExpression,
1826
- 32: this.setBankLSB,
1827
- 38: this.dataEntryLSB,
1828
- 64: this.setSustainPedal,
1829
- 65: this.setPortamento,
1830
- 66: this.setSostenutoPedal,
1831
- 67: this.setSoftPedal,
1832
- 91: this.setReverbSendLevel,
1833
- 93: this.setChorusSendLevel,
1834
- 100: this.setRPNLSB,
1835
- 101: this.setRPNMSB,
1836
- 120: this.allSoundOff,
1837
- 121: this.resetAllControllers,
1838
- 123: this.allNotesOff,
1839
- 124: this.omniOff,
1840
- 125: this.omniOn,
1841
- 126: this.monoOn,
1842
- 127: this.polyOn,
1843
- };
1842
+ const handlers = new Array(128);
1843
+ handlers[0] = this.setBankMSB;
1844
+ handlers[1] = this.setModulationDepth;
1845
+ handlers[5] = this.setPortamentoTime;
1846
+ handlers[6] = this.dataEntryMSB;
1847
+ handlers[7] = this.setVolume;
1848
+ handlers[10] = this.setPan;
1849
+ handlers[11] = this.setExpression;
1850
+ handlers[32] = this.setBankLSB;
1851
+ handlers[38] = this.dataEntryLSB;
1852
+ handlers[64] = this.setSustainPedal;
1853
+ handlers[65] = this.setPortamento;
1854
+ handlers[66] = this.setSostenutoPedal;
1855
+ handlers[67] = this.setSoftPedal;
1856
+ handlers[91] = this.setReverbSendLevel;
1857
+ handlers[93] = this.setChorusSendLevel;
1858
+ handlers[100] = this.setRPNLSB;
1859
+ handlers[101] = this.setRPNMSB;
1860
+ handlers[120] = this.allSoundOff;
1861
+ handlers[121] = this.resetAllControllers;
1862
+ handlers[123] = this.allNotesOff;
1863
+ handlers[124] = this.omniOff;
1864
+ handlers[125] = this.omniOn;
1865
+ handlers[126] = this.monoOn;
1866
+ handlers[127] = this.polyOn;
1867
+ return handlers;
1844
1868
  }
1845
1869
  handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1846
1870
  const handler = this.controlChangeHandlers[controllerType];
@@ -1877,9 +1901,33 @@ export class MidyGM2 {
1877
1901
  channel.state.modulationDepth = modulation / 127;
1878
1902
  this.updateModulation(channel, scheduleTime);
1879
1903
  }
1880
- setPortamentoTime(channelNumber, portamentoTime) {
1904
+ updatePortamento(channel, scheduleTime) {
1905
+ this.processScheduledNotes(channel, (note) => {
1906
+ if (0.5 <= channel.state.portamento) {
1907
+ if (0 <= note.portamentoNoteNumber) {
1908
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1909
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1910
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
1911
+ this.updateDetune(channel, note, scheduleTime);
1912
+ }
1913
+ }
1914
+ else {
1915
+ if (0 <= note.portamentoNoteNumber) {
1916
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1917
+ this.setFilterEnvelope(channel, note, scheduleTime);
1918
+ this.setPitchEnvelope(note, scheduleTime);
1919
+ this.updateDetune(channel, note, scheduleTime);
1920
+ }
1921
+ }
1922
+ });
1923
+ }
1924
+ setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
1881
1925
  const channel = this.channels[channelNumber];
1926
+ scheduleTime ??= this.audioContext.currentTime;
1882
1927
  channel.state.portamentoTime = portamentoTime / 127;
1928
+ if (channel.isDrum)
1929
+ return;
1930
+ this.updatePortamento(channel, scheduleTime);
1883
1931
  }
1884
1932
  setKeyBasedVolume(channel, scheduleTime) {
1885
1933
  this.processScheduledNotes(channel, (note) => {
@@ -1965,11 +2013,13 @@ export class MidyGM2 {
1965
2013
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
1966
2014
  }
1967
2015
  }
1968
- setPortamento(channelNumber, value) {
2016
+ setPortamento(channelNumber, value, scheduleTime) {
1969
2017
  const channel = this.channels[channelNumber];
1970
2018
  if (channel.isDrum)
1971
2019
  return;
2020
+ scheduleTime ??= this.audioContext.currentTime;
1972
2021
  channel.state.portamento = value / 127;
2022
+ this.updatePortamento(channel, scheduleTime);
1973
2023
  }
1974
2024
  setSostenutoPedal(channelNumber, value, scheduleTime) {
1975
2025
  const channel = this.channels[channelNumber];
@@ -1978,7 +2028,11 @@ export class MidyGM2 {
1978
2028
  scheduleTime ??= this.audioContext.currentTime;
1979
2029
  channel.state.sostenutoPedal = value / 127;
1980
2030
  if (64 <= value) {
1981
- channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
2031
+ const sostenutoNotes = [];
2032
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2033
+ sostenutoNotes.push(note);
2034
+ });
2035
+ channel.sostenutoNotes = sostenutoNotes;
1982
2036
  }
1983
2037
  else {
1984
2038
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
@@ -1992,9 +2046,9 @@ export class MidyGM2 {
1992
2046
  scheduleTime ??= this.audioContext.currentTime;
1993
2047
  state.softPedal = softPedal / 127;
1994
2048
  this.processScheduledNotes(channel, (note) => {
1995
- if (0.5 <= state.portamento && note.portamento) {
1996
- this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
1997
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2049
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2050
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2051
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1998
2052
  }
1999
2053
  else {
2000
2054
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2194,21 +2248,29 @@ export class MidyGM2 {
2194
2248
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2195
2249
  }
2196
2250
  resetAllStates(channelNumber) {
2251
+ const scheduleTime = this.audioContext.currentTime;
2197
2252
  const channel = this.channels[channelNumber];
2198
2253
  const state = channel.state;
2199
- for (const type of Object.keys(defaultControllerState)) {
2200
- state[type] = defaultControllerState[type].defaultValue;
2254
+ const entries = Object.entries(defaultControllerState);
2255
+ for (const [key, { type, defaultValue }] of entries) {
2256
+ if (128 <= type) {
2257
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2258
+ }
2259
+ else {
2260
+ state[key] = defaultValue;
2261
+ }
2201
2262
  }
2202
- for (const type of Object.keys(this.constructor.channelSettings)) {
2203
- channel[type] = this.constructor.channelSettings[type];
2263
+ for (const key of Object.keys(this.constructor.channelSettings)) {
2264
+ channel[key] = this.constructor.channelSettings[key];
2204
2265
  }
2266
+ this.resetChannelTable(channel);
2205
2267
  this.mode = "GM2";
2206
2268
  this.masterFineTuning = 0; // cb
2207
2269
  this.masterCoarseTuning = 0; // cb
2208
2270
  }
2209
2271
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2210
- resetAllControllers(channelNumber) {
2211
- const stateTypes = [
2272
+ resetAllControllers(channelNumber, _value, scheduleTime) {
2273
+ const keys = [
2212
2274
  "channelPressure",
2213
2275
  "pitchWheel",
2214
2276
  "expression",
@@ -2220,10 +2282,17 @@ export class MidyGM2 {
2220
2282
  ];
2221
2283
  const channel = this.channels[channelNumber];
2222
2284
  const state = channel.state;
2223
- for (let i = 0; i < stateTypes.length; i++) {
2224
- const type = stateTypes[i];
2225
- state[type] = defaultControllerState[type].defaultValue;
2285
+ for (let i = 0; i < keys.length; i++) {
2286
+ const key = keys[i];
2287
+ const { type, defaultValue } = defaultControllerState[key];
2288
+ if (128 <= type) {
2289
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2290
+ }
2291
+ else {
2292
+ state[key] = defaultValue;
2293
+ }
2226
2294
  }
2295
+ this.setPitchBend(channelNumber, 8192, scheduleTime);
2227
2296
  const settingTypes = [
2228
2297
  "rpnMSB",
2229
2298
  "rpnLSB",
@@ -2452,7 +2521,7 @@ export class MidyGM2 {
2452
2521
  this.reverbEffect = options.reverbAlgorithm(audioContext);
2453
2522
  }
2454
2523
  getReverbTime(value) {
2455
- return Math.pow(Math.E, (value - 40) * 0.025);
2524
+ return Math.exp((value - 40) * 0.025);
2456
2525
  }
2457
2526
  // mean free path equation
2458
2527
  // https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
@@ -2609,6 +2678,8 @@ export class MidyGM2 {
2609
2678
  if (!channelBitmap[i])
2610
2679
  continue;
2611
2680
  const channel = this.channels[i];
2681
+ if (channel.isDrum)
2682
+ continue;
2612
2683
  for (let j = 0; j < 12; j++) {
2613
2684
  const centValue = data[j + 7] - 64;
2614
2685
  channel.scaleOctaveTuningTable[j] = centValue;
@@ -2645,7 +2716,13 @@ export class MidyGM2 {
2645
2716
  setControllerParameters(channel, note, table) {
2646
2717
  if (table[0] !== 64)
2647
2718
  this.updateDetune(channel, note);
2648
- if (!note.portamento) {
2719
+ if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2720
+ if (table[1] !== 64)
2721
+ this.setPortamentoFilterEnvelope(channel, note);
2722
+ if (table[2] !== 64)
2723
+ this.setPortamentoVolumeEnvelope(channel, note);
2724
+ }
2725
+ else {
2649
2726
  if (table[1] !== 64)
2650
2727
  this.setFilterEnvelope(channel, note);
2651
2728
  if (table[2] !== 64)
@@ -2660,7 +2737,10 @@ export class MidyGM2 {
2660
2737
  }
2661
2738
  handlePressureSysEx(data, tableName) {
2662
2739
  const channelNumber = data[4];
2663
- const table = this.channels[channelNumber][tableName];
2740
+ const channel = this.channels[channelNumber];
2741
+ if (channel.isDrum)
2742
+ return;
2743
+ const table = channel[tableName];
2664
2744
  for (let i = 5; i < data.length - 1; i += 2) {
2665
2745
  const pp = data[i];
2666
2746
  const rr = data[i + 1];
@@ -2670,8 +2750,13 @@ export class MidyGM2 {
2670
2750
  initControlTable() {
2671
2751
  const channelCount = 128;
2672
2752
  const slotSize = 6;
2673
- const defaultValues = [64, 64, 64, 0, 0, 0];
2674
2753
  const table = new Uint8Array(channelCount * slotSize);
2754
+ return this.resetControlTable(table);
2755
+ }
2756
+ resetControlTable(table) {
2757
+ const channelCount = 128;
2758
+ const slotSize = 6;
2759
+ const defaultValues = [64, 64, 64, 0, 0, 0];
2675
2760
  for (let ch = 0; ch < channelCount; ch++) {
2676
2761
  const offset = ch * slotSize;
2677
2762
  table.set(defaultValues, offset);
@@ -2688,8 +2773,11 @@ export class MidyGM2 {
2688
2773
  }
2689
2774
  handleControlChangeSysEx(data) {
2690
2775
  const channelNumber = data[4];
2776
+ const channel = this.channels[channelNumber];
2777
+ if (channel.isDrum)
2778
+ return;
2691
2779
  const controllerType = data[5];
2692
- const table = this.channels[channelNumber].controlTable[controllerType];
2780
+ const table = channel.controlTable[controllerType];
2693
2781
  for (let i = 6; i < data.length - 1; i += 2) {
2694
2782
  const pp = data[i];
2695
2783
  const rr = data[i + 1];
@@ -2703,8 +2791,11 @@ export class MidyGM2 {
2703
2791
  }
2704
2792
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2705
2793
  const channelNumber = data[4];
2794
+ const channel = this.channels[channelNumber];
2795
+ if (channel.isDrum)
2796
+ return;
2706
2797
  const keyNumber = data[5];
2707
- const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2798
+ const table = channel.keyBasedInstrumentControlTable;
2708
2799
  for (let i = 6; i < data.length - 1; i += 2) {
2709
2800
  const controllerType = data[i];
2710
2801
  const value = data[i + 1];