@marmooo/midy 0.3.0 → 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/script/midy.js CHANGED
@@ -3,60 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Midy = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
- // 2-3 times faster than Map
7
- class SparseMap {
8
- constructor(size) {
9
- this.data = new Array(size);
10
- this.activeIndices = [];
11
- }
12
- set(key, value) {
13
- if (this.data[key] === undefined) {
14
- this.activeIndices.push(key);
15
- }
16
- this.data[key] = value;
17
- }
18
- get(key) {
19
- return this.data[key];
20
- }
21
- delete(key) {
22
- if (this.data[key] !== undefined) {
23
- this.data[key] = undefined;
24
- const index = this.activeIndices.indexOf(key);
25
- if (index !== -1) {
26
- this.activeIndices.splice(index, 1);
27
- }
28
- return true;
29
- }
30
- return false;
31
- }
32
- has(key) {
33
- return this.data[key] !== undefined;
34
- }
35
- get size() {
36
- return this.activeIndices.length;
37
- }
38
- clear() {
39
- for (let i = 0; i < this.activeIndices.length; i++) {
40
- const key = this.activeIndices[i];
41
- this.data[key] = undefined;
42
- }
43
- this.activeIndices = [];
44
- }
45
- *[Symbol.iterator]() {
46
- for (let i = 0; i < this.activeIndices.length; i++) {
47
- const key = this.activeIndices[i];
48
- yield [key, this.data[key]];
49
- }
50
- }
51
- forEach(callback) {
52
- for (let i = 0; i < this.activeIndices.length; i++) {
53
- const key = this.activeIndices[i];
54
- callback(this.data[key], key, this);
55
- }
56
- }
57
- }
58
6
  class Note {
59
7
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
8
+ Object.defineProperty(this, "index", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: -1
13
+ });
14
+ Object.defineProperty(this, "noteOffEvent", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: void 0
19
+ });
60
20
  Object.defineProperty(this, "bufferSource", {
61
21
  enumerable: true,
62
22
  configurable: true,
@@ -141,11 +101,11 @@ class Note {
141
101
  writable: true,
142
102
  value: void 0
143
103
  });
144
- Object.defineProperty(this, "portamento", {
104
+ Object.defineProperty(this, "portamentoNoteNumber", {
145
105
  enumerable: true,
146
106
  configurable: true,
147
107
  writable: true,
148
- value: void 0
108
+ value: -1
149
109
  });
150
110
  Object.defineProperty(this, "pressure", {
151
111
  enumerable: true,
@@ -207,7 +167,7 @@ const defaultControllerState = {
207
167
  portamentoTime: { type: 128 + 5, defaultValue: 0 },
208
168
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
209
169
  volume: { type: 128 + 7, defaultValue: 100 / 127 },
210
- pan: { type: 128 + 10, defaultValue: 0.5 },
170
+ pan: { type: 128 + 10, defaultValue: 64 / 127 },
211
171
  expression: { type: 128 + 11, defaultValue: 1 },
212
172
  // bankLSB: { type: 128 + 32, defaultValue: 0, },
213
173
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
@@ -215,14 +175,14 @@ const defaultControllerState = {
215
175
  portamento: { type: 128 + 65, defaultValue: 0 },
216
176
  sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
217
177
  softPedal: { type: 128 + 67, defaultValue: 0 },
218
- filterResonance: { type: 128 + 71, defaultValue: 0.5 },
219
- releaseTime: { type: 128 + 72, defaultValue: 0.5 },
220
- attackTime: { type: 128 + 73, defaultValue: 0.5 },
221
- brightness: { type: 128 + 74, defaultValue: 0.5 },
222
- decayTime: { type: 128 + 75, defaultValue: 0.5 },
223
- vibratoRate: { type: 128 + 76, defaultValue: 0.5 },
224
- vibratoDepth: { type: 128 + 77, defaultValue: 0.5 },
225
- vibratoDelay: { type: 128 + 78, defaultValue: 0.5 },
178
+ filterResonance: { type: 128 + 71, defaultValue: 64 / 127 },
179
+ releaseTime: { type: 128 + 72, defaultValue: 64 / 127 },
180
+ attackTime: { type: 128 + 73, defaultValue: 64 / 127 },
181
+ brightness: { type: 128 + 74, defaultValue: 64 / 127 },
182
+ decayTime: { type: 128 + 75, defaultValue: 64 / 127 },
183
+ vibratoRate: { type: 128 + 76, defaultValue: 64 / 127 },
184
+ vibratoDepth: { type: 128 + 77, defaultValue: 64 / 127 },
185
+ vibratoDelay: { type: 128 + 78, defaultValue: 64 / 127 },
226
186
  reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
227
187
  chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
228
188
  // dataIncrement: { type: 128 + 96, defaultValue: 0 },
@@ -497,7 +457,7 @@ class Midy {
497
457
  initSoundFontTable() {
498
458
  const table = new Array(128);
499
459
  for (let i = 0; i < 128; i++) {
500
- table[i] = new SparseMap(128);
460
+ table[i] = new Map();
501
461
  }
502
462
  return table;
503
463
  }
@@ -544,18 +504,25 @@ class Midy {
544
504
  merger,
545
505
  };
546
506
  }
507
+ resetChannelTable(channel) {
508
+ this.resetControlTable(channel.controlTable);
509
+ channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
510
+ channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
511
+ channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
512
+ channel.keyBasedInstrumentControlTable.fill(0); // [-64, 63]
513
+ }
547
514
  createChannels(audioContext) {
548
515
  const channels = Array.from({ length: this.numChannels }, () => {
549
516
  return {
550
517
  currentBufferSource: null,
551
518
  isDrum: false,
552
- ...this.constructor.channelSettings,
553
519
  state: new ControllerState(),
554
- controlTable: this.initControlTable(),
520
+ ...this.constructor.channelSettings,
555
521
  ...this.setChannelAudioNodes(audioContext),
556
- scheduledNotes: new SparseMap(128),
522
+ scheduledNotes: [],
557
523
  sustainNotes: [],
558
- sostenutoNotes: new SparseMap(128),
524
+ sostenutoNotes: [],
525
+ controlTable: this.initControlTable(),
559
526
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
560
527
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
561
528
  polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
@@ -603,46 +570,20 @@ class Midy {
603
570
  }
604
571
  return bufferSource;
605
572
  }
606
- findPortamentoTarget(queueIndex) {
607
- const endEvent = this.timeline[queueIndex];
608
- if (!this.channels[endEvent.channel].portamento)
609
- return;
610
- const endTime = endEvent.startTime;
611
- let target;
612
- while (++queueIndex < this.timeline.length) {
613
- const event = this.timeline[queueIndex];
614
- if (endTime !== event.startTime)
615
- break;
616
- if (event.type !== "noteOn")
617
- continue;
618
- if (!target || event.noteNumber < target.noteNumber) {
619
- target = event;
620
- }
621
- }
622
- return target;
623
- }
624
- async scheduleTimelineEvents(t, offset, queueIndex) {
573
+ async scheduleTimelineEvents(t, resumeTime, queueIndex) {
625
574
  while (queueIndex < this.timeline.length) {
626
575
  const event = this.timeline[queueIndex];
627
576
  if (event.startTime > t + this.lookAhead)
628
577
  break;
629
- const startTime = event.startTime + this.startDelay - offset;
578
+ const delay = this.startDelay - resumeTime;
579
+ const startTime = event.startTime + delay;
630
580
  switch (event.type) {
631
- case "noteOn":
632
- if (event.velocity !== 0) {
633
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
634
- break;
635
- }
636
- /* falls through */
637
- case "noteOff": {
638
- const portamentoTarget = this.findPortamentoTarget(queueIndex);
639
- if (portamentoTarget)
640
- portamentoTarget.portamento = true;
641
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
642
- portamentoTarget?.noteNumber);
643
- if (notePromise) {
644
- this.notePromises.push(notePromise);
645
- }
581
+ case "noteOn": {
582
+ const noteOffEvent = {
583
+ ...event.noteOffEvent,
584
+ startTime: event.noteOffEvent.startTime + delay,
585
+ };
586
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
646
587
  break;
647
588
  }
648
589
  case "noteAftertouch":
@@ -681,7 +622,7 @@ class Midy {
681
622
  this.isPaused = false;
682
623
  this.startTime = this.audioContext.currentTime;
683
624
  let queueIndex = this.getQueueIndex(this.resumeTime);
684
- let offset = this.resumeTime - this.startTime;
625
+ let resumeTime = this.resumeTime - this.startTime;
685
626
  this.notePromises = [];
686
627
  const schedulePlayback = async () => {
687
628
  if (queueIndex >= this.timeline.length) {
@@ -690,18 +631,21 @@ class Midy {
690
631
  this.exclusiveClassNotes.fill(undefined);
691
632
  this.drumExclusiveClassNotes.fill(undefined);
692
633
  this.audioBufferCache.clear();
634
+ for (let i = 0; i < this.channels.length; i++) {
635
+ this.resetAllStates(i);
636
+ }
693
637
  resolve();
694
638
  return;
695
639
  }
696
640
  const now = this.audioContext.currentTime;
697
- const t = now + offset;
698
- queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
641
+ const t = now + resumeTime;
642
+ queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
699
643
  if (this.isPausing) {
700
644
  await this.stopNotes(0, true, now);
701
645
  this.notePromises = [];
702
- resolve();
703
646
  this.isPausing = false;
704
647
  this.isPaused = true;
648
+ resolve();
705
649
  return;
706
650
  }
707
651
  else if (this.isStopping) {
@@ -710,9 +654,12 @@ class Midy {
710
654
  this.exclusiveClassNotes.fill(undefined);
711
655
  this.drumExclusiveClassNotes.fill(undefined);
712
656
  this.audioBufferCache.clear();
713
- resolve();
657
+ for (let i = 0; i < this.channels.length; i++) {
658
+ this.resetAllStates(i);
659
+ }
714
660
  this.isStopping = false;
715
661
  this.isPaused = false;
662
+ resolve();
716
663
  return;
717
664
  }
718
665
  else if (this.isSeeking) {
@@ -721,7 +668,7 @@ class Midy {
721
668
  this.drumExclusiveClassNotes.fill(undefined);
722
669
  this.startTime = this.audioContext.currentTime;
723
670
  queueIndex = this.getQueueIndex(this.resumeTime);
724
- offset = this.resumeTime - this.startTime;
671
+ resumeTime = this.resumeTime - this.startTime;
725
672
  this.isSeeking = false;
726
673
  await schedulePlayback();
727
674
  }
@@ -843,17 +790,52 @@ class Midy {
843
790
  prevTempoTicks = event.ticks;
844
791
  }
845
792
  }
793
+ const activeNotes = new Array(this.channels.length * 128);
794
+ for (let i = 0; i < activeNotes.length; i++) {
795
+ activeNotes[i] = [];
796
+ }
797
+ for (let i = 0; i < timeline.length; i++) {
798
+ const event = timeline[i];
799
+ switch (event.type) {
800
+ case "noteOn": {
801
+ const index = event.channel * 128 + event.noteNumber;
802
+ activeNotes[index].push(event);
803
+ break;
804
+ }
805
+ case "noteOff": {
806
+ const index = event.channel * 128 + event.noteNumber;
807
+ const noteOn = activeNotes[index].pop();
808
+ if (noteOn) {
809
+ noteOn.noteOffEvent = event;
810
+ }
811
+ else {
812
+ const eventString = JSON.stringify(event, null, 2);
813
+ console.warn(`noteOff without matching noteOn: ${eventString}`);
814
+ }
815
+ }
816
+ }
817
+ }
846
818
  return { instruments, timeline };
847
819
  }
820
+ stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
821
+ const channel = this.channels[channelNumber];
822
+ const promises = [];
823
+ this.processActiveNotes(channel, scheduleTime, (note) => {
824
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
825
+ this.notePromises.push(promise);
826
+ promises.push(promise);
827
+ });
828
+ return Promise.all(promises);
829
+ }
848
830
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
849
831
  const channel = this.channels[channelNumber];
850
832
  const promises = [];
851
833
  this.processScheduledNotes(channel, (note) => {
852
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
834
+ const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
853
835
  this.notePromises.push(promise);
854
836
  promises.push(promise);
855
837
  });
856
- channel.scheduledNotes.clear();
838
+ channel.scheduledNotes = [];
857
839
  return Promise.all(promises);
858
840
  }
859
841
  stopNotes(velocity, force, scheduleTime) {
@@ -874,9 +856,6 @@ class Midy {
874
856
  if (!this.isPlaying)
875
857
  return;
876
858
  this.isStopping = true;
877
- for (let i = 0; i < this.channels.length; i++) {
878
- this.resetAllStates(i);
879
- }
880
859
  }
881
860
  pause() {
882
861
  if (!this.isPlaying || this.isPaused)
@@ -911,35 +890,31 @@ class Midy {
911
890
  return this.resumeTime + now - this.startTime - this.startDelay;
912
891
  }
913
892
  processScheduledNotes(channel, callback) {
914
- channel.scheduledNotes.forEach((noteList) => {
915
- for (let i = 0; i < noteList.length; i++) {
916
- const note = noteList[i];
917
- if (!note)
918
- continue;
919
- callback(note);
920
- }
921
- });
922
- }
923
- getActiveNotes(channel, scheduleTime) {
924
- const activeNotes = new SparseMap(128);
925
- channel.scheduledNotes.forEach((noteList) => {
926
- const activeNote = this.getActiveNote(noteList, scheduleTime);
927
- if (activeNote) {
928
- activeNotes.set(activeNote.noteNumber, activeNote);
929
- }
930
- });
931
- return activeNotes;
893
+ const scheduledNotes = channel.scheduledNotes;
894
+ for (let i = 0; i < scheduledNotes.length; i++) {
895
+ const note = scheduledNotes[i];
896
+ if (!note)
897
+ continue;
898
+ if (note.ending)
899
+ continue;
900
+ callback(note);
901
+ }
932
902
  }
933
- getActiveNote(noteList, scheduleTime) {
934
- for (let i = noteList.length - 1; i >= 0; i--) {
935
- const note = noteList[i];
903
+ processActiveNotes(channel, scheduleTime, callback) {
904
+ const scheduledNotes = channel.scheduledNotes;
905
+ for (let i = 0; i < scheduledNotes.length; i++) {
906
+ const note = scheduledNotes[i];
936
907
  if (!note)
937
- return;
908
+ continue;
909
+ if (note.ending)
910
+ continue;
911
+ const noteOffEvent = note.noteOffEvent;
912
+ if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
913
+ continue;
938
914
  if (scheduleTime < note.startTime)
939
915
  continue;
940
- return (note.ending) ? null : note;
916
+ callback(note);
941
917
  }
942
- return noteList[0];
943
918
  }
944
919
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
945
920
  const sampleRate = audioContext.sampleRate;
@@ -1106,24 +1081,94 @@ class Midy {
1106
1081
  const noteDetune = this.calcNoteDetune(channel, note);
1107
1082
  const pitchControl = this.getPitchControl(channel, note);
1108
1083
  const detune = channel.detune + noteDetune + pitchControl;
1109
- note.bufferSource.detune
1110
- .cancelScheduledValues(scheduleTime)
1111
- .setValueAtTime(detune, scheduleTime);
1112
- }
1113
- getPortamentoTime(channel) {
1114
- const factor = 5 * Math.log(10) * 127;
1115
- return channel.state.portamentoTime * factor;
1116
- }
1117
- setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1084
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1085
+ const startTime = note.startTime;
1086
+ const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1087
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1088
+ note.bufferSource.detune
1089
+ .cancelScheduledValues(scheduleTime)
1090
+ .setValueAtTime(detune - deltaCent, scheduleTime)
1091
+ .linearRampToValueAtTime(detune, portamentoTime);
1092
+ }
1093
+ else {
1094
+ note.bufferSource.detune
1095
+ .cancelScheduledValues(scheduleTime)
1096
+ .setValueAtTime(detune, scheduleTime);
1097
+ }
1098
+ }
1099
+ getPortamentoTime(channel, note) {
1100
+ const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1101
+ const value = Math.ceil(channel.state.portamentoTime * 127);
1102
+ return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1103
+ }
1104
+ getPitchIncrementSpeed(value) {
1105
+ const points = [
1106
+ [0, 1000],
1107
+ [6, 100],
1108
+ [16, 20],
1109
+ [32, 10],
1110
+ [48, 5],
1111
+ [64, 2.5],
1112
+ [80, 1],
1113
+ [96, 0.4],
1114
+ [112, 0.15],
1115
+ [127, 0.01],
1116
+ ];
1117
+ const logPoints = new Array(points.length);
1118
+ for (let i = 0; i < points.length; i++) {
1119
+ const [x, y] = points[i];
1120
+ if (value === x)
1121
+ return y;
1122
+ logPoints[i] = [x, Math.log(y)];
1123
+ }
1124
+ let startIndex = 0;
1125
+ for (let i = 1; i < logPoints.length; i++) {
1126
+ if (value <= logPoints[i][0]) {
1127
+ startIndex = i - 1;
1128
+ break;
1129
+ }
1130
+ }
1131
+ const [x0, y0] = logPoints[startIndex];
1132
+ const [x1, y1] = logPoints[startIndex + 1];
1133
+ const h = x1 - x0;
1134
+ const t = (value - x0) / h;
1135
+ let m0, m1;
1136
+ if (startIndex === 0) {
1137
+ m0 = (y1 - y0) / h;
1138
+ }
1139
+ else {
1140
+ const [xPrev, yPrev] = logPoints[startIndex - 1];
1141
+ m0 = (y1 - yPrev) / (x1 - xPrev);
1142
+ }
1143
+ if (startIndex === logPoints.length - 2) {
1144
+ m1 = (y1 - y0) / h;
1145
+ }
1146
+ else {
1147
+ const [xNext, yNext] = logPoints[startIndex + 2];
1148
+ m1 = (yNext - y0) / (xNext - x0);
1149
+ }
1150
+ // Cubic Hermite Spline
1151
+ const t2 = t * t;
1152
+ const t3 = t2 * t;
1153
+ const h00 = 2 * t3 - 3 * t2 + 1;
1154
+ const h10 = t3 - 2 * t2 + t;
1155
+ const h01 = -2 * t3 + 3 * t2;
1156
+ const h11 = t3 - t2;
1157
+ const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
1158
+ return Math.exp(y);
1159
+ }
1160
+ setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1161
+ const state = channel.state;
1118
1162
  const { voiceParams, startTime } = note;
1119
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1163
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1164
+ (1 + this.getAmplitudeControl(channel, note));
1120
1165
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1121
1166
  const volDelay = startTime + voiceParams.volDelay;
1122
- const portamentoTime = volDelay + this.getPortamentoTime(channel);
1167
+ const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1168
+ const volHold = volAttack + voiceParams.volHold;
1123
1169
  note.volumeEnvelopeNode.gain
1124
1170
  .cancelScheduledValues(scheduleTime)
1125
- .setValueAtTime(0, volDelay)
1126
- .linearRampToValueAtTime(sustainVolume, portamentoTime);
1171
+ .setValueAtTime(sustainVolume, volHold);
1127
1172
  }
1128
1173
  setVolumeEnvelope(channel, note, scheduleTime) {
1129
1174
  const state = channel.state;
@@ -1143,6 +1188,12 @@ class Midy {
1143
1188
  .setValueAtTime(attackVolume, volHold)
1144
1189
  .linearRampToValueAtTime(sustainVolume, volDecay);
1145
1190
  }
1191
+ setPortamentoPitchEnvelope(note, scheduleTime) {
1192
+ const baseRate = note.voiceParams.playbackRate;
1193
+ note.bufferSource.playbackRate
1194
+ .cancelScheduledValues(scheduleTime)
1195
+ .setValueAtTime(baseRate, scheduleTime);
1196
+ }
1146
1197
  setPitchEnvelope(note, scheduleTime) {
1147
1198
  const { voiceParams } = note;
1148
1199
  const baseRate = voiceParams.playbackRate;
@@ -1170,20 +1221,21 @@ class Midy {
1170
1221
  const maxFrequency = 20000; // max Hz of initialFilterFc
1171
1222
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1172
1223
  }
1173
- setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1224
+ setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1174
1225
  const state = channel.state;
1175
1226
  const { voiceParams, noteNumber, startTime } = note;
1176
1227
  const softPedalFactor = 1 -
1177
1228
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1178
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1179
- softPedalFactor *
1229
+ const baseCent = voiceParams.initialFilterFc +
1230
+ this.getFilterCutoffControl(channel, note);
1231
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1180
1232
  state.brightness * 2;
1181
1233
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1182
1234
  const sustainFreq = baseFreq +
1183
1235
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1184
1236
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1185
1237
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1186
- const portamentoTime = startTime + this.getPortamentoTime(channel);
1238
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1187
1239
  const modDelay = startTime + voiceParams.modDelay;
1188
1240
  note.filterNode.frequency
1189
1241
  .cancelScheduledValues(scheduleTime)
@@ -1269,7 +1321,7 @@ class Midy {
1269
1321
  return audioBuffer;
1270
1322
  }
1271
1323
  }
1272
- async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1324
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1273
1325
  const now = this.audioContext.currentTime;
1274
1326
  const state = channel.state;
1275
1327
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
@@ -1285,20 +1337,24 @@ class Midy {
1285
1337
  type: "lowpass",
1286
1338
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1287
1339
  });
1288
- if (portamento) {
1289
- note.portamento = true;
1290
- this.setPortamentoStartVolumeEnvelope(channel, note, now);
1291
- this.setPortamentoStartFilterEnvelope(channel, note, now);
1340
+ const prevNote = channel.scheduledNotes.at(-1);
1341
+ if (prevNote && prevNote.noteNumber !== noteNumber) {
1342
+ note.portamentoNoteNumber = prevNote.noteNumber;
1343
+ }
1344
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1345
+ this.setPortamentoVolumeEnvelope(channel, note, now);
1346
+ this.setPortamentoFilterEnvelope(channel, note, now);
1347
+ this.setPortamentoPitchEnvelope(note, now);
1292
1348
  }
1293
1349
  else {
1294
- note.portamento = false;
1295
1350
  this.setVolumeEnvelope(channel, note, now);
1296
1351
  this.setFilterEnvelope(channel, note, now);
1352
+ this.setPitchEnvelope(note, now);
1297
1353
  }
1354
+ this.updateDetune(channel, note, now);
1298
1355
  if (0 < state.vibratoDepth) {
1299
1356
  this.startVibrato(channel, note, now);
1300
1357
  }
1301
- this.setPitchEnvelope(note, now);
1302
1358
  if (0 < state.modulationDepth) {
1303
1359
  this.startModulation(channel, note, now);
1304
1360
  }
@@ -1344,9 +1400,8 @@ class Midy {
1344
1400
  if (prev) {
1345
1401
  const [prevNote, prevChannelNumber] = prev;
1346
1402
  if (prevNote && !prevNote.ending) {
1347
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1348
- startTime, true, // force
1349
- undefined);
1403
+ this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
1404
+ startTime, true);
1350
1405
  }
1351
1406
  }
1352
1407
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
@@ -1365,9 +1420,8 @@ class Midy {
1365
1420
  channelNumber;
1366
1421
  const prevNote = this.drumExclusiveClassNotes[index];
1367
1422
  if (prevNote && !prevNote.ending) {
1368
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1369
- startTime, true, // force
1370
- undefined);
1423
+ this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
1424
+ startTime, true);
1371
1425
  }
1372
1426
  this.drumExclusiveClassNotes[index] = note;
1373
1427
  }
@@ -1375,10 +1429,10 @@ class Midy {
1375
1429
  if (!channel.isDrum)
1376
1430
  return false;
1377
1431
  const programNumber = channel.programNumber;
1378
- return (programNumber === 48 && noteNumber === 88) ||
1379
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84);
1432
+ return !((programNumber === 48 && noteNumber === 88) ||
1433
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1380
1434
  }
1381
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1435
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1382
1436
  const channel = this.channels[channelNumber];
1383
1437
  const bankNumber = this.calcBank(channel, channelNumber);
1384
1438
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1389,7 +1443,8 @@ class Midy {
1389
1443
  if (!voice)
1390
1444
  return;
1391
1445
  const isSF3 = soundFont.parsed.info.version.major === 3;
1392
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1446
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1447
+ note.noteOffEvent = noteOffEvent;
1393
1448
  note.gainL.connect(channel.gainL);
1394
1449
  note.gainR.connect(channel.gainR);
1395
1450
  if (0.5 <= channel.state.sustainPedal) {
@@ -1398,33 +1453,39 @@ class Midy {
1398
1453
  this.handleExclusiveClass(note, channelNumber, startTime);
1399
1454
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1400
1455
  const scheduledNotes = channel.scheduledNotes;
1401
- let notes = scheduledNotes.get(noteNumber);
1402
- if (notes) {
1403
- notes.push(note);
1404
- }
1405
- else {
1406
- notes = [note];
1407
- scheduledNotes.set(noteNumber, notes);
1408
- }
1456
+ note.index = scheduledNotes.length;
1457
+ scheduledNotes.push(note);
1409
1458
  if (this.isDrumNoteOffException(channel, noteNumber)) {
1410
1459
  const stopTime = startTime + note.bufferSource.buffer.duration;
1411
- const index = notes.length - 1;
1412
1460
  const promise = new Promise((resolve) => {
1413
1461
  note.bufferSource.onended = () => {
1414
- this.disconnectNote(note, scheduledNotes, index);
1462
+ scheduledNotes[note.index] = undefined;
1463
+ this.disconnectNote(note);
1415
1464
  resolve();
1416
1465
  };
1417
1466
  note.bufferSource.stop(stopTime);
1418
1467
  });
1419
1468
  this.notePromises.push(promise);
1420
1469
  }
1470
+ else if (noteOffEvent) {
1471
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1472
+ const portamentoTime = this.getPortamentoTime(channel, note);
1473
+ const portamentoEndTime = startTime + portamentoTime;
1474
+ const notePromise = this.scheduleNoteOff(channelNumber, note, 0, // velocity
1475
+ Math.max(noteOffEvent.startTime, portamentoEndTime), false);
1476
+ this.notePromises.push(notePromise);
1477
+ }
1478
+ else {
1479
+ const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
1480
+ this.notePromises.push(notePromise);
1481
+ }
1482
+ }
1421
1483
  }
1422
1484
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1423
1485
  scheduleTime ??= this.audioContext.currentTime;
1424
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1486
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1425
1487
  }
1426
- disconnectNote(note, scheduledNotes, index) {
1427
- scheduledNotes[index] = null;
1488
+ disconnectNote(note) {
1428
1489
  note.bufferSource.disconnect();
1429
1490
  note.filterNode.disconnect();
1430
1491
  note.volumeEnvelopeNode.disconnect();
@@ -1447,8 +1508,7 @@ class Midy {
1447
1508
  note.chorusEffectsSend.disconnect();
1448
1509
  }
1449
1510
  }
1450
- stopNote(endTime, stopTime, scheduledNotes, index) {
1451
- const note = scheduledNotes[index];
1511
+ stopNote(channel, note, endTime, stopTime) {
1452
1512
  note.volumeEnvelopeNode.gain
1453
1513
  .cancelScheduledValues(endTime)
1454
1514
  .linearRampToValueAtTime(0, stopTime);
@@ -1458,65 +1518,58 @@ class Midy {
1458
1518
  }, stopTime);
1459
1519
  return new Promise((resolve) => {
1460
1520
  note.bufferSource.onended = () => {
1461
- this.disconnectNote(note, scheduledNotes, index);
1521
+ channel.scheduledNotes[note.index] = undefined;
1522
+ this.disconnectNote(note);
1462
1523
  resolve();
1463
1524
  };
1464
1525
  note.bufferSource.stop(stopTime);
1465
1526
  });
1466
1527
  }
1467
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1528
+ scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
1468
1529
  const channel = this.channels[channelNumber];
1469
- if (this.isDrumNoteOffException(channel, noteNumber))
1530
+ if (this.isDrumNoteOffException(channel, note.noteNumber))
1470
1531
  return;
1471
1532
  const state = channel.state;
1472
1533
  if (!force) {
1473
1534
  if (0.5 <= state.sustainPedal)
1474
1535
  return;
1475
- if (channel.sostenutoNotes.has(noteNumber))
1536
+ if (0.5 <= channel.state.sostenutoPedal)
1476
1537
  return;
1477
1538
  }
1478
- if (!channel.scheduledNotes.has(noteNumber))
1479
- return;
1480
- const scheduledNotes = channel.scheduledNotes.get(noteNumber);
1539
+ const volRelease = endTime +
1540
+ note.voiceParams.volRelease * channel.state.releaseTime * 2;
1541
+ const modRelease = endTime + note.voiceParams.modRelease;
1542
+ note.filterNode.frequency
1543
+ .cancelScheduledValues(endTime)
1544
+ .linearRampToValueAtTime(0, modRelease);
1545
+ const stopTime = Math.min(volRelease, modRelease);
1546
+ return this.stopNote(channel, note, endTime, stopTime);
1547
+ }
1548
+ findNoteOffTarget(channel, noteNumber) {
1549
+ const scheduledNotes = channel.scheduledNotes;
1481
1550
  for (let i = 0; i < scheduledNotes.length; i++) {
1482
1551
  const note = scheduledNotes[i];
1483
1552
  if (!note)
1484
1553
  continue;
1485
1554
  if (note.ending)
1486
1555
  continue;
1487
- if (portamentoNoteNumber === undefined) {
1488
- const volRelease = endTime +
1489
- note.voiceParams.volRelease * state.releaseTime * 2;
1490
- const modRelease = endTime + note.voiceParams.modRelease;
1491
- note.filterNode.frequency
1492
- .cancelScheduledValues(endTime)
1493
- .linearRampToValueAtTime(0, modRelease);
1494
- const stopTime = Math.min(volRelease, modRelease);
1495
- return this.stopNote(endTime, stopTime, scheduledNotes, i);
1496
- }
1497
- else {
1498
- const portamentoTime = endTime + this.getPortamentoTime(channel);
1499
- const deltaNote = portamentoNoteNumber - noteNumber;
1500
- const baseRate = note.voiceParams.playbackRate;
1501
- const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1502
- note.bufferSource.playbackRate
1503
- .cancelScheduledValues(endTime)
1504
- .linearRampToValueAtTime(targetRate, portamentoTime);
1505
- return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1506
- }
1556
+ if (note.noteNumber !== noteNumber)
1557
+ continue;
1558
+ return note;
1507
1559
  }
1508
1560
  }
1509
1561
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1510
1562
  scheduleTime ??= this.audioContext.currentTime;
1511
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1512
- undefined);
1563
+ const channel = this.channels[channelNumber];
1564
+ const note = this.findNoteOffTarget(channel, noteNumber);
1565
+ return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
1513
1566
  }
1514
1567
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1515
1568
  const velocity = halfVelocity * 2;
1516
1569
  const channel = this.channels[channelNumber];
1517
1570
  const promises = [];
1518
1571
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1519
- const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1572
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
1520
1573
  promises.push(promise);
1521
1574
  }
1522
1575
  channel.sustainNotes = [];
@@ -1526,12 +1579,14 @@ class Midy {
1526
1579
  const velocity = halfVelocity * 2;
1527
1580
  const channel = this.channels[channelNumber];
1528
1581
  const promises = [];
1582
+ const sostenutoNotes = channel.sostenutoNotes;
1529
1583
  channel.state.sostenutoPedal = 0;
1530
- channel.sostenutoNotes.forEach((note) => {
1531
- const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1584
+ for (let i = 0; i < sostenutoNotes.length; i++) {
1585
+ const note = sostenutoNotes[i];
1586
+ const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
1532
1587
  promises.push(promise);
1533
- });
1534
- channel.sostenutoNotes.clear();
1588
+ }
1589
+ channel.sostenutoNotes = [];
1535
1590
  return promises;
1536
1591
  }
1537
1592
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
@@ -1560,12 +1615,12 @@ class Midy {
1560
1615
  const channel = this.channels[channelNumber];
1561
1616
  channel.state.polyphonicKeyPressure = pressure / 127;
1562
1617
  const table = channel.polyphonicKeyPressureTable;
1563
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
1564
- if (activeNotes.has(noteNumber)) {
1565
- const note = activeNotes.get(noteNumber);
1566
- this.setControllerParameters(channel, note, table);
1567
- }
1568
- // this.applyVoiceParams(channel, 10);
1618
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1619
+ if (note.noteNumber === noteNumber) {
1620
+ this.setControllerParameters(channel, note, table);
1621
+ }
1622
+ });
1623
+ this.applyVoiceParams(channel, 10);
1569
1624
  }
1570
1625
  handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1571
1626
  const channel = this.channels[channelNumber];
@@ -1594,10 +1649,10 @@ class Midy {
1594
1649
  channel.detune += pressureDepth * (next - prev);
1595
1650
  }
1596
1651
  const table = channel.channelPressureTable;
1597
- this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1652
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1598
1653
  this.setControllerParameters(channel, note, table);
1599
1654
  });
1600
- // this.applyVoiceParams(channel, 13);
1655
+ this.applyVoiceParams(channel, 13);
1601
1656
  }
1602
1657
  handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1603
1658
  const pitchBend = msb * 128 + lsb;
@@ -1774,6 +1829,8 @@ class Midy {
1774
1829
  state.set(channel.state.array);
1775
1830
  state[2] = velocity / 127;
1776
1831
  state[3] = noteNumber / 127;
1832
+ state[10] = state.polyphonicKeyPressure / 127;
1833
+ state[13] = state.channelPressure / 127;
1777
1834
  return state;
1778
1835
  }
1779
1836
  applyVoiceParams(channel, controllerType, scheduleTime) {
@@ -1800,8 +1857,8 @@ class Midy {
1800
1857
  if (key in voiceParams)
1801
1858
  noteVoiceParams[key] = voiceParams[key];
1802
1859
  }
1803
- if (note.portamento) {
1804
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1860
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1861
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1805
1862
  }
1806
1863
  else {
1807
1864
  this.setFilterEnvelope(channel, note, scheduleTime);
@@ -1824,42 +1881,42 @@ class Midy {
1824
1881
  });
1825
1882
  }
1826
1883
  createControlChangeHandlers() {
1827
- return {
1828
- 0: this.setBankMSB,
1829
- 1: this.setModulationDepth,
1830
- 5: this.setPortamentoTime,
1831
- 6: this.dataEntryMSB,
1832
- 7: this.setVolume,
1833
- 10: this.setPan,
1834
- 11: this.setExpression,
1835
- 32: this.setBankLSB,
1836
- 38: this.dataEntryLSB,
1837
- 64: this.setSustainPedal,
1838
- 65: this.setPortamento,
1839
- 66: this.setSostenutoPedal,
1840
- 67: this.setSoftPedal,
1841
- 71: this.setFilterResonance,
1842
- 72: this.setReleaseTime,
1843
- 73: this.setAttackTime,
1844
- 74: this.setBrightness,
1845
- 75: this.setDecayTime,
1846
- 76: this.setVibratoRate,
1847
- 77: this.setVibratoDepth,
1848
- 78: this.setVibratoDelay,
1849
- 91: this.setReverbSendLevel,
1850
- 93: this.setChorusSendLevel,
1851
- 96: this.dataIncrement,
1852
- 97: this.dataDecrement,
1853
- 100: this.setRPNLSB,
1854
- 101: this.setRPNMSB,
1855
- 120: this.allSoundOff,
1856
- 121: this.resetAllControllers,
1857
- 123: this.allNotesOff,
1858
- 124: this.omniOff,
1859
- 125: this.omniOn,
1860
- 126: this.monoOn,
1861
- 127: this.polyOn,
1862
- };
1884
+ const handlers = new Array(128);
1885
+ handlers[0] = this.setBankMSB;
1886
+ handlers[1] = this.setModulationDepth;
1887
+ handlers[5] = this.setPortamentoTime;
1888
+ handlers[6] = this.dataEntryMSB;
1889
+ handlers[7] = this.setVolume;
1890
+ handlers[10] = this.setPan;
1891
+ handlers[11] = this.setExpression;
1892
+ handlers[32] = this.setBankLSB;
1893
+ handlers[38] = this.dataEntryLSB;
1894
+ handlers[64] = this.setSustainPedal;
1895
+ handlers[65] = this.setPortamento;
1896
+ handlers[66] = this.setSostenutoPedal;
1897
+ handlers[67] = this.setSoftPedal;
1898
+ handlers[71] = this.setFilterResonance;
1899
+ handlers[72] = this.setReleaseTime;
1900
+ handlers[73] = this.setAttackTime;
1901
+ handlers[74] = this.setBrightness;
1902
+ handlers[75] = this.setDecayTime;
1903
+ handlers[76] = this.setVibratoRate;
1904
+ handlers[77] = this.setVibratoDepth;
1905
+ handlers[78] = this.setVibratoDelay;
1906
+ handlers[91] = this.setReverbSendLevel;
1907
+ handlers[93] = this.setChorusSendLevel;
1908
+ handlers[96] = this.dataIncrement;
1909
+ handlers[97] = this.dataDecrement;
1910
+ handlers[100] = this.setRPNLSB;
1911
+ handlers[101] = this.setRPNMSB;
1912
+ handlers[120] = this.allSoundOff;
1913
+ handlers[121] = this.resetAllControllers;
1914
+ handlers[123] = this.allNotesOff;
1915
+ handlers[124] = this.omniOff;
1916
+ handlers[125] = this.omniOn;
1917
+ handlers[126] = this.monoOn;
1918
+ handlers[127] = this.polyOn;
1919
+ return handlers;
1863
1920
  }
1864
1921
  handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1865
1922
  const handler = this.controlChangeHandlers[controllerType];
@@ -1896,9 +1953,33 @@ class Midy {
1896
1953
  channel.state.modulationDepth = modulation / 127;
1897
1954
  this.updateModulation(channel, scheduleTime);
1898
1955
  }
1899
- setPortamentoTime(channelNumber, portamentoTime) {
1956
+ updatePortamento(channel, scheduleTime) {
1957
+ this.processScheduledNotes(channel, (note) => {
1958
+ if (0.5 <= channel.state.portamento) {
1959
+ if (0 <= note.portamentoNoteNumber) {
1960
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1961
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1962
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
1963
+ this.updateDetune(channel, note, scheduleTime);
1964
+ }
1965
+ }
1966
+ else {
1967
+ if (0 <= note.portamentoNoteNumber) {
1968
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1969
+ this.setFilterEnvelope(channel, note, scheduleTime);
1970
+ this.setPitchEnvelope(note, scheduleTime);
1971
+ this.updateDetune(channel, note, scheduleTime);
1972
+ }
1973
+ }
1974
+ });
1975
+ }
1976
+ setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
1900
1977
  const channel = this.channels[channelNumber];
1978
+ scheduleTime ??= this.audioContext.currentTime;
1901
1979
  channel.state.portamentoTime = portamentoTime / 127;
1980
+ if (channel.isDrum)
1981
+ return;
1982
+ this.updatePortamento(channel, scheduleTime);
1902
1983
  }
1903
1984
  setKeyBasedVolume(channel, scheduleTime) {
1904
1985
  this.processScheduledNotes(channel, (note) => {
@@ -1956,7 +2037,7 @@ class Midy {
1956
2037
  }
1957
2038
  dataEntryLSB(channelNumber, value, scheduleTime) {
1958
2039
  this.channels[channelNumber].dataLSB = value;
1959
- this.handleRPN(channelNumber, scheduleTime);
2040
+ this.handleRPN(channelNumber, 0, scheduleTime);
1960
2041
  }
1961
2042
  updateChannelVolume(channel, scheduleTime) {
1962
2043
  const state = channel.state;
@@ -1984,11 +2065,13 @@ class Midy {
1984
2065
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
1985
2066
  }
1986
2067
  }
1987
- setPortamento(channelNumber, value) {
2068
+ setPortamento(channelNumber, value, scheduleTime) {
1988
2069
  const channel = this.channels[channelNumber];
1989
2070
  if (channel.isDrum)
1990
2071
  return;
2072
+ scheduleTime ??= this.audioContext.currentTime;
1991
2073
  channel.state.portamento = value / 127;
2074
+ this.updatePortamento(channel, scheduleTime);
1992
2075
  }
1993
2076
  setSostenutoPedal(channelNumber, value, scheduleTime) {
1994
2077
  const channel = this.channels[channelNumber];
@@ -1997,7 +2080,11 @@ class Midy {
1997
2080
  scheduleTime ??= this.audioContext.currentTime;
1998
2081
  channel.state.sostenutoPedal = value / 127;
1999
2082
  if (64 <= value) {
2000
- channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
2083
+ const sostenutoNotes = [];
2084
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2085
+ sostenutoNotes.push(note);
2086
+ });
2087
+ channel.sostenutoNotes = sostenutoNotes;
2001
2088
  }
2002
2089
  else {
2003
2090
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
@@ -2007,12 +2094,13 @@ class Midy {
2007
2094
  const channel = this.channels[channelNumber];
2008
2095
  if (channel.isDrum)
2009
2096
  return;
2097
+ const state = channel.state;
2010
2098
  scheduleTime ??= this.audioContext.currentTime;
2011
- channel.state.softPedal = softPedal / 127;
2099
+ state.softPedal = softPedal / 127;
2012
2100
  this.processScheduledNotes(channel, (note) => {
2013
- if (note.portamento) {
2014
- this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
2015
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2101
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2102
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2103
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2016
2104
  }
2017
2105
  else {
2018
2106
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2026,25 +2114,25 @@ class Midy {
2026
2114
  return;
2027
2115
  scheduleTime ??= this.audioContext.currentTime;
2028
2116
  const state = channel.state;
2029
- state.filterResonance = filterResonance / 64;
2117
+ state.filterResonance = filterResonance / 127;
2030
2118
  this.processScheduledNotes(channel, (note) => {
2031
2119
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2032
2120
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2033
2121
  });
2034
2122
  }
2035
- setReleaseTime(channelNumber, releaseTime, _scheduleTime) {
2123
+ setReleaseTime(channelNumber, releaseTime, scheduleTime) {
2036
2124
  const channel = this.channels[channelNumber];
2037
2125
  if (channel.isDrum)
2038
2126
  return;
2039
2127
  scheduleTime ??= this.audioContext.currentTime;
2040
- channel.state.releaseTime = releaseTime / 64;
2128
+ channel.state.releaseTime = releaseTime / 127;
2041
2129
  }
2042
2130
  setAttackTime(channelNumber, attackTime, scheduleTime) {
2043
2131
  const channel = this.channels[channelNumber];
2044
2132
  if (channel.isDrum)
2045
2133
  return;
2046
2134
  scheduleTime ??= this.audioContext.currentTime;
2047
- channel.state.attackTime = attackTime / 64;
2135
+ channel.state.attackTime = attackTime / 127;
2048
2136
  this.processScheduledNotes(channel, (note) => {
2049
2137
  if (note.startTime < scheduleTime)
2050
2138
  return false;
@@ -2055,11 +2143,12 @@ class Midy {
2055
2143
  const channel = this.channels[channelNumber];
2056
2144
  if (channel.isDrum)
2057
2145
  return;
2146
+ const state = channel.state;
2058
2147
  scheduleTime ??= this.audioContext.currentTime;
2059
- channel.state.brightness = brightness / 64;
2148
+ state.brightness = brightness / 127;
2060
2149
  this.processScheduledNotes(channel, (note) => {
2061
- if (note.portamento) {
2062
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2150
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2151
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2063
2152
  }
2064
2153
  else {
2065
2154
  this.setFilterEnvelope(channel, note);
@@ -2071,7 +2160,7 @@ class Midy {
2071
2160
  if (channel.isDrum)
2072
2161
  return;
2073
2162
  scheduleTime ??= this.audioContext.currentTime;
2074
- channel.state.decayTime = dacayTime / 64;
2163
+ channel.state.decayTime = dacayTime / 127;
2075
2164
  this.processScheduledNotes(channel, (note) => {
2076
2165
  this.setVolumeEnvelope(channel, note, scheduleTime);
2077
2166
  });
@@ -2081,7 +2170,7 @@ class Midy {
2081
2170
  if (channel.isDrum)
2082
2171
  return;
2083
2172
  scheduleTime ??= this.audioContext.currentTime;
2084
- channel.state.vibratoRate = vibratoRate / 64;
2173
+ channel.state.vibratoRate = vibratoRate / 127;
2085
2174
  if (channel.vibratoDepth <= 0)
2086
2175
  return;
2087
2176
  this.processScheduledNotes(channel, (note) => {
@@ -2094,7 +2183,7 @@ class Midy {
2094
2183
  return;
2095
2184
  scheduleTime ??= this.audioContext.currentTime;
2096
2185
  const prev = channel.state.vibratoDepth;
2097
- channel.state.vibratoDepth = vibratoDepth / 64;
2186
+ channel.state.vibratoDepth = vibratoDepth / 127;
2098
2187
  if (0 < prev) {
2099
2188
  this.processScheduledNotes(channel, (note) => {
2100
2189
  this.setFreqVibLFO(channel, note, scheduleTime);
@@ -2106,12 +2195,12 @@ class Midy {
2106
2195
  });
2107
2196
  }
2108
2197
  }
2109
- setVibratoDelay(channelNumber, vibratoDelay) {
2198
+ setVibratoDelay(channelNumber, vibratoDelay, scheduleTime) {
2110
2199
  const channel = this.channels[channelNumber];
2111
2200
  if (channel.isDrum)
2112
2201
  return;
2113
2202
  scheduleTime ??= this.audioContext.currentTime;
2114
- channel.state.vibratoDelay = vibratoDelay / 64;
2203
+ channel.state.vibratoDelay = vibratoDelay / 127;
2115
2204
  if (0 < channel.state.vibratoDepth) {
2116
2205
  this.processScheduledNotes(channel, (note) => {
2117
2206
  this.startVibrato(channel, note, scheduleTime);
@@ -2233,12 +2322,14 @@ class Midy {
2233
2322
  }
2234
2323
  }
2235
2324
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2236
- dataIncrement(channelNumber) {
2237
- this.handleRPN(channelNumber, 1);
2325
+ dataIncrement(channelNumber, scheduleTime) {
2326
+ scheduleTime ??= this.audioContext.currentTime;
2327
+ this.handleRPN(channelNumber, 1, scheduleTime);
2238
2328
  }
2239
2329
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2240
- dataDecrement(channelNumber) {
2241
- this.handleRPN(channelNumber, -1);
2330
+ dataDecrement(channelNumber, scheduleTime) {
2331
+ scheduleTime ??= this.audioContext.currentTime;
2332
+ this.handleRPN(channelNumber, -1, scheduleTime);
2242
2333
  }
2243
2334
  setRPNMSB(channelNumber, value) {
2244
2335
  this.channels[channelNumber].rpnMSB = value;
@@ -2248,7 +2339,7 @@ class Midy {
2248
2339
  }
2249
2340
  dataEntryMSB(channelNumber, value, scheduleTime) {
2250
2341
  this.channels[channelNumber].dataMSB = value;
2251
- this.handleRPN(channelNumber, scheduleTime);
2342
+ this.handleRPN(channelNumber, 0, scheduleTime);
2252
2343
  }
2253
2344
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2254
2345
  const channel = this.channels[channelNumber];
@@ -2319,24 +2410,32 @@ class Midy {
2319
2410
  }
2320
2411
  allSoundOff(channelNumber, _value, scheduleTime) {
2321
2412
  scheduleTime ??= this.audioContext.currentTime;
2322
- return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2413
+ return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2323
2414
  }
2324
2415
  resetAllStates(channelNumber) {
2416
+ const scheduleTime = this.audioContext.currentTime;
2325
2417
  const channel = this.channels[channelNumber];
2326
2418
  const state = channel.state;
2327
- for (const type of Object.keys(defaultControllerState)) {
2328
- state[type] = defaultControllerState[type].defaultValue;
2419
+ const entries = Object.entries(defaultControllerState);
2420
+ for (const [key, { type, defaultValue }] of entries) {
2421
+ if (128 <= type) {
2422
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2423
+ }
2424
+ else {
2425
+ state[key] = defaultValue;
2426
+ }
2329
2427
  }
2330
- for (const type of Object.keys(this.constructor.channelSettings)) {
2331
- channel[type] = this.constructor.channelSettings[type];
2428
+ for (const key of Object.keys(this.constructor.channelSettings)) {
2429
+ channel[key] = this.constructor.channelSettings[key];
2332
2430
  }
2431
+ this.resetChannelTable(channel);
2333
2432
  this.mode = "GM2";
2334
2433
  this.masterFineTuning = 0; // cb
2335
2434
  this.masterCoarseTuning = 0; // cb
2336
2435
  }
2337
2436
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2338
- resetAllControllers(channelNumber) {
2339
- const stateTypes = [
2437
+ resetAllControllers(channelNumber, _value, scheduleTime) {
2438
+ const keys = [
2340
2439
  "polyphonicKeyPressure",
2341
2440
  "channelPressure",
2342
2441
  "pitchWheel",
@@ -2349,10 +2448,17 @@ class Midy {
2349
2448
  ];
2350
2449
  const channel = this.channels[channelNumber];
2351
2450
  const state = channel.state;
2352
- for (let i = 0; i < stateTypes.length; i++) {
2353
- const type = stateTypes[i];
2354
- state[type] = defaultControllerState[type].defaultValue;
2451
+ for (let i = 0; i < keys.length; i++) {
2452
+ const key = keys[i];
2453
+ const { type, defaultValue } = defaultControllerState[key];
2454
+ if (128 <= type) {
2455
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2456
+ }
2457
+ else {
2458
+ state[key] = defaultValue;
2459
+ }
2355
2460
  }
2461
+ this.setPitchBend(channelNumber, 8192, scheduleTime);
2356
2462
  const settingTypes = [
2357
2463
  "rpnMSB",
2358
2464
  "rpnLSB",
@@ -2364,7 +2470,7 @@ class Midy {
2364
2470
  }
2365
2471
  allNotesOff(channelNumber, _value, scheduleTime) {
2366
2472
  scheduleTime ??= this.audioContext.currentTime;
2367
- return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
2473
+ return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
2368
2474
  }
2369
2475
  omniOff(channelNumber, value, scheduleTime) {
2370
2476
  this.allNotesOff(channelNumber, value, scheduleTime);
@@ -2597,7 +2703,7 @@ class Midy {
2597
2703
  this.reverbEffect = options.reverbAlgorithm(audioContext);
2598
2704
  }
2599
2705
  getReverbTime(value) {
2600
- return Math.pow(Math.E, (value - 40) * 0.025);
2706
+ return Math.exp((value - 40) * 0.025);
2601
2707
  }
2602
2708
  // mean free path equation
2603
2709
  // https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
@@ -2831,7 +2937,13 @@ class Midy {
2831
2937
  setControllerParameters(channel, note, table) {
2832
2938
  if (table[0] !== 64)
2833
2939
  this.updateDetune(channel, note);
2834
- if (!note.portamento) {
2940
+ if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2941
+ if (table[1] !== 64)
2942
+ this.setPortamentoFilterEnvelope(channel, note);
2943
+ if (table[2] !== 64)
2944
+ this.setPortamentoVolumeEnvelope(channel, note);
2945
+ }
2946
+ else {
2835
2947
  if (table[1] !== 64)
2836
2948
  this.setFilterEnvelope(channel, note);
2837
2949
  if (table[2] !== 64)
@@ -2859,8 +2971,13 @@ class Midy {
2859
2971
  initControlTable() {
2860
2972
  const channelCount = 128;
2861
2973
  const slotSize = 6;
2862
- const defaultValues = [64, 64, 64, 0, 0, 0];
2863
2974
  const table = new Uint8Array(channelCount * slotSize);
2975
+ return this.resetControlTable(table);
2976
+ }
2977
+ resetControlTable(table) {
2978
+ const channelCount = 128;
2979
+ const slotSize = 6;
2980
+ const defaultValues = [64, 64, 64, 0, 0, 0];
2864
2981
  for (let ch = 0; ch < channelCount; ch++) {
2865
2982
  const offset = ch * slotSize;
2866
2983
  table.set(defaultValues, offset);