@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/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 (0 < event.velocity) {
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,13 +790,37 @@ 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
  }
848
820
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
849
821
  const channel = this.channels[channelNumber];
850
822
  const promises = [];
851
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
852
- activeNotes.forEach((note) => {
823
+ this.processActiveNotes(channel, scheduleTime, (note) => {
853
824
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
854
825
  this.notePromises.push(promise);
855
826
  promises.push(promise);
@@ -860,11 +831,11 @@ class Midy {
860
831
  const channel = this.channels[channelNumber];
861
832
  const promises = [];
862
833
  this.processScheduledNotes(channel, (note) => {
863
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
834
+ const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
864
835
  this.notePromises.push(promise);
865
836
  promises.push(promise);
866
837
  });
867
- channel.scheduledNotes.clear();
838
+ channel.scheduledNotes = [];
868
839
  return Promise.all(promises);
869
840
  }
870
841
  stopNotes(velocity, force, scheduleTime) {
@@ -885,9 +856,6 @@ class Midy {
885
856
  if (!this.isPlaying)
886
857
  return;
887
858
  this.isStopping = true;
888
- for (let i = 0; i < this.channels.length; i++) {
889
- this.resetAllStates(i);
890
- }
891
859
  }
892
860
  pause() {
893
861
  if (!this.isPlaying || this.isPaused)
@@ -922,37 +890,31 @@ class Midy {
922
890
  return this.resumeTime + now - this.startTime - this.startDelay;
923
891
  }
924
892
  processScheduledNotes(channel, callback) {
925
- channel.scheduledNotes.forEach((noteList) => {
926
- for (let i = 0; i < noteList.length; i++) {
927
- const note = noteList[i];
928
- if (!note)
929
- continue;
930
- if (note.ending)
931
- continue;
932
- callback(note);
933
- }
934
- });
935
- }
936
- getActiveNotes(channel, scheduleTime) {
937
- const activeNotes = new SparseMap(128);
938
- channel.scheduledNotes.forEach((noteList) => {
939
- const activeNote = this.getActiveNote(noteList, scheduleTime);
940
- if (activeNote) {
941
- activeNotes.set(activeNote.noteNumber, activeNote);
942
- }
943
- });
944
- 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
+ }
945
902
  }
946
- getActiveNote(noteList, scheduleTime) {
947
- for (let i = noteList.length - 1; i >= 0; i--) {
948
- 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];
949
907
  if (!note)
950
- return;
908
+ continue;
909
+ if (note.ending)
910
+ continue;
911
+ const noteOffEvent = note.noteOffEvent;
912
+ if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
913
+ continue;
951
914
  if (scheduleTime < note.startTime)
952
915
  continue;
953
- return (note.ending) ? null : note;
916
+ callback(note);
954
917
  }
955
- return noteList[0];
956
918
  }
957
919
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
958
920
  const sampleRate = audioContext.sampleRate;
@@ -1119,24 +1081,94 @@ class Midy {
1119
1081
  const noteDetune = this.calcNoteDetune(channel, note);
1120
1082
  const pitchControl = this.getPitchControl(channel, note);
1121
1083
  const detune = channel.detune + noteDetune + pitchControl;
1122
- note.bufferSource.detune
1123
- .cancelScheduledValues(scheduleTime)
1124
- .setValueAtTime(detune, scheduleTime);
1125
- }
1126
- getPortamentoTime(channel) {
1127
- const factor = 5 * Math.log(10) * 127;
1128
- return channel.state.portamentoTime * factor;
1129
- }
1130
- 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;
1131
1162
  const { voiceParams, startTime } = note;
1132
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1163
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1164
+ (1 + this.getAmplitudeControl(channel, note));
1133
1165
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1134
1166
  const volDelay = startTime + voiceParams.volDelay;
1135
- const portamentoTime = volDelay + this.getPortamentoTime(channel);
1167
+ const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1168
+ const volHold = volAttack + voiceParams.volHold;
1136
1169
  note.volumeEnvelopeNode.gain
1137
1170
  .cancelScheduledValues(scheduleTime)
1138
- .setValueAtTime(0, volDelay)
1139
- .linearRampToValueAtTime(sustainVolume, portamentoTime);
1171
+ .setValueAtTime(sustainVolume, volHold);
1140
1172
  }
1141
1173
  setVolumeEnvelope(channel, note, scheduleTime) {
1142
1174
  const state = channel.state;
@@ -1156,6 +1188,12 @@ class Midy {
1156
1188
  .setValueAtTime(attackVolume, volHold)
1157
1189
  .linearRampToValueAtTime(sustainVolume, volDecay);
1158
1190
  }
1191
+ setPortamentoPitchEnvelope(note, scheduleTime) {
1192
+ const baseRate = note.voiceParams.playbackRate;
1193
+ note.bufferSource.playbackRate
1194
+ .cancelScheduledValues(scheduleTime)
1195
+ .setValueAtTime(baseRate, scheduleTime);
1196
+ }
1159
1197
  setPitchEnvelope(note, scheduleTime) {
1160
1198
  const { voiceParams } = note;
1161
1199
  const baseRate = voiceParams.playbackRate;
@@ -1183,20 +1221,21 @@ class Midy {
1183
1221
  const maxFrequency = 20000; // max Hz of initialFilterFc
1184
1222
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1185
1223
  }
1186
- setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1224
+ setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1187
1225
  const state = channel.state;
1188
1226
  const { voiceParams, noteNumber, startTime } = note;
1189
1227
  const softPedalFactor = 1 -
1190
1228
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1191
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1192
- softPedalFactor *
1229
+ const baseCent = voiceParams.initialFilterFc +
1230
+ this.getFilterCutoffControl(channel, note);
1231
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1193
1232
  state.brightness * 2;
1194
1233
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1195
1234
  const sustainFreq = baseFreq +
1196
1235
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1197
1236
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1198
1237
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1199
- const portamentoTime = startTime + this.getPortamentoTime(channel);
1238
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1200
1239
  const modDelay = startTime + voiceParams.modDelay;
1201
1240
  note.filterNode.frequency
1202
1241
  .cancelScheduledValues(scheduleTime)
@@ -1282,7 +1321,7 @@ class Midy {
1282
1321
  return audioBuffer;
1283
1322
  }
1284
1323
  }
1285
- async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1324
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1286
1325
  const now = this.audioContext.currentTime;
1287
1326
  const state = channel.state;
1288
1327
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
@@ -1298,20 +1337,24 @@ class Midy {
1298
1337
  type: "lowpass",
1299
1338
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1300
1339
  });
1301
- if (0.5 <= state.portamento && portamento) {
1302
- note.portamento = true;
1303
- this.setPortamentoStartVolumeEnvelope(channel, note, now);
1304
- 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);
1305
1348
  }
1306
1349
  else {
1307
- note.portamento = false;
1308
1350
  this.setVolumeEnvelope(channel, note, now);
1309
1351
  this.setFilterEnvelope(channel, note, now);
1352
+ this.setPitchEnvelope(note, now);
1310
1353
  }
1354
+ this.updateDetune(channel, note, now);
1311
1355
  if (0 < state.vibratoDepth) {
1312
1356
  this.startVibrato(channel, note, now);
1313
1357
  }
1314
- this.setPitchEnvelope(note, now);
1315
1358
  if (0 < state.modulationDepth) {
1316
1359
  this.startModulation(channel, note, now);
1317
1360
  }
@@ -1357,9 +1400,8 @@ class Midy {
1357
1400
  if (prev) {
1358
1401
  const [prevNote, prevChannelNumber] = prev;
1359
1402
  if (prevNote && !prevNote.ending) {
1360
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1361
- startTime, true, // force
1362
- undefined);
1403
+ this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
1404
+ startTime, true);
1363
1405
  }
1364
1406
  }
1365
1407
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
@@ -1378,9 +1420,8 @@ class Midy {
1378
1420
  channelNumber;
1379
1421
  const prevNote = this.drumExclusiveClassNotes[index];
1380
1422
  if (prevNote && !prevNote.ending) {
1381
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1382
- startTime, true, // force
1383
- undefined);
1423
+ this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
1424
+ startTime, true);
1384
1425
  }
1385
1426
  this.drumExclusiveClassNotes[index] = note;
1386
1427
  }
@@ -1391,7 +1432,7 @@ class Midy {
1391
1432
  return !((programNumber === 48 && noteNumber === 88) ||
1392
1433
  (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1393
1434
  }
1394
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1435
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1395
1436
  const channel = this.channels[channelNumber];
1396
1437
  const bankNumber = this.calcBank(channel, channelNumber);
1397
1438
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1402,7 +1443,8 @@ class Midy {
1402
1443
  if (!voice)
1403
1444
  return;
1404
1445
  const isSF3 = soundFont.parsed.info.version.major === 3;
1405
- 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;
1406
1448
  note.gainL.connect(channel.gainL);
1407
1449
  note.gainR.connect(channel.gainR);
1408
1450
  if (0.5 <= channel.state.sustainPedal) {
@@ -1411,20 +1453,13 @@ class Midy {
1411
1453
  this.handleExclusiveClass(note, channelNumber, startTime);
1412
1454
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1413
1455
  const scheduledNotes = channel.scheduledNotes;
1414
- let noteList = scheduledNotes.get(noteNumber);
1415
- if (noteList) {
1416
- noteList.push(note);
1417
- }
1418
- else {
1419
- noteList = [note];
1420
- scheduledNotes.set(noteNumber, noteList);
1421
- }
1456
+ note.index = scheduledNotes.length;
1457
+ scheduledNotes.push(note);
1422
1458
  if (this.isDrumNoteOffException(channel, noteNumber)) {
1423
1459
  const stopTime = startTime + note.bufferSource.buffer.duration;
1424
- const index = noteList.length - 1;
1425
1460
  const promise = new Promise((resolve) => {
1426
1461
  note.bufferSource.onended = () => {
1427
- noteList[index] = undefined;
1462
+ scheduledNotes[note.index] = undefined;
1428
1463
  this.disconnectNote(note);
1429
1464
  resolve();
1430
1465
  };
@@ -1432,10 +1467,23 @@ class Midy {
1432
1467
  });
1433
1468
  this.notePromises.push(promise);
1434
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
+ }
1435
1483
  }
1436
1484
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1437
1485
  scheduleTime ??= this.audioContext.currentTime;
1438
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1486
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1439
1487
  }
1440
1488
  disconnectNote(note) {
1441
1489
  note.bufferSource.disconnect();
@@ -1460,8 +1508,7 @@ class Midy {
1460
1508
  note.chorusEffectsSend.disconnect();
1461
1509
  }
1462
1510
  }
1463
- stopNote(endTime, stopTime, noteList, index) {
1464
- const note = noteList[index];
1511
+ stopNote(channel, note, endTime, stopTime) {
1465
1512
  note.volumeEnvelopeNode.gain
1466
1513
  .cancelScheduledValues(endTime)
1467
1514
  .linearRampToValueAtTime(0, stopTime);
@@ -1471,73 +1518,58 @@ class Midy {
1471
1518
  }, stopTime);
1472
1519
  return new Promise((resolve) => {
1473
1520
  note.bufferSource.onended = () => {
1474
- noteList[index] = undefined;
1521
+ channel.scheduledNotes[note.index] = undefined;
1475
1522
  this.disconnectNote(note);
1476
1523
  resolve();
1477
1524
  };
1478
1525
  note.bufferSource.stop(stopTime);
1479
1526
  });
1480
1527
  }
1481
- findNoteOffTarget(noteList) {
1482
- for (let i = 0; i < noteList.length; i++) {
1483
- const note = noteList[i];
1484
- if (!note)
1485
- continue;
1486
- if (note.ending)
1487
- continue;
1488
- return [note, i];
1489
- }
1490
- }
1491
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1528
+ scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
1492
1529
  const channel = this.channels[channelNumber];
1493
- if (this.isDrumNoteOffException(channel, noteNumber))
1530
+ if (this.isDrumNoteOffException(channel, note.noteNumber))
1494
1531
  return;
1495
1532
  const state = channel.state;
1496
1533
  if (!force) {
1497
1534
  if (0.5 <= state.sustainPedal)
1498
1535
  return;
1499
- if (channel.sostenutoNotes.has(noteNumber))
1536
+ if (0.5 <= channel.state.sostenutoPedal)
1500
1537
  return;
1501
1538
  }
1502
- const noteList = channel.scheduledNotes.get(noteNumber);
1503
- if (!noteList)
1504
- return; // be careful with drum channel
1505
- const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
1506
- if (!noteOffTarget)
1507
- return;
1508
- const [note, i] = noteOffTarget;
1509
- if (0.5 <= state.portamento && portamentoNoteNumber !== undefined) {
1510
- const portamentoTime = endTime + this.getPortamentoTime(channel);
1511
- const deltaNote = portamentoNoteNumber - noteNumber;
1512
- const baseRate = note.voiceParams.playbackRate;
1513
- const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1514
- note.bufferSource.playbackRate
1515
- .cancelScheduledValues(endTime)
1516
- .linearRampToValueAtTime(targetRate, portamentoTime);
1517
- return this.stopNote(endTime, portamentoTime, noteList, i);
1518
- }
1519
- else {
1520
- const volRelease = endTime +
1521
- note.voiceParams.volRelease * state.releaseTime * 2;
1522
- const modRelease = endTime + note.voiceParams.modRelease;
1523
- note.filterNode.frequency
1524
- .cancelScheduledValues(endTime)
1525
- .linearRampToValueAtTime(0, modRelease);
1526
- const stopTime = Math.min(volRelease, modRelease);
1527
- return this.stopNote(endTime, stopTime, noteList, i);
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;
1550
+ for (let i = 0; i < scheduledNotes.length; i++) {
1551
+ const note = scheduledNotes[i];
1552
+ if (!note)
1553
+ continue;
1554
+ if (note.ending)
1555
+ continue;
1556
+ if (note.noteNumber !== noteNumber)
1557
+ continue;
1558
+ return note;
1528
1559
  }
1529
1560
  }
1530
1561
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1531
1562
  scheduleTime ??= this.audioContext.currentTime;
1532
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1533
- undefined);
1563
+ const channel = this.channels[channelNumber];
1564
+ const note = this.findNoteOffTarget(channel, noteNumber);
1565
+ return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
1534
1566
  }
1535
1567
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1536
1568
  const velocity = halfVelocity * 2;
1537
1569
  const channel = this.channels[channelNumber];
1538
1570
  const promises = [];
1539
1571
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1540
- const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1572
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
1541
1573
  promises.push(promise);
1542
1574
  }
1543
1575
  channel.sustainNotes = [];
@@ -1547,12 +1579,14 @@ class Midy {
1547
1579
  const velocity = halfVelocity * 2;
1548
1580
  const channel = this.channels[channelNumber];
1549
1581
  const promises = [];
1582
+ const sostenutoNotes = channel.sostenutoNotes;
1550
1583
  channel.state.sostenutoPedal = 0;
1551
- channel.sostenutoNotes.forEach((note) => {
1552
- 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);
1553
1587
  promises.push(promise);
1554
- });
1555
- channel.sostenutoNotes.clear();
1588
+ }
1589
+ channel.sostenutoNotes = [];
1556
1590
  return promises;
1557
1591
  }
1558
1592
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
@@ -1581,11 +1615,11 @@ class Midy {
1581
1615
  const channel = this.channels[channelNumber];
1582
1616
  channel.state.polyphonicKeyPressure = pressure / 127;
1583
1617
  const table = channel.polyphonicKeyPressureTable;
1584
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
1585
- if (activeNotes.has(noteNumber)) {
1586
- const note = activeNotes.get(noteNumber);
1587
- this.setControllerParameters(channel, note, table);
1588
- }
1618
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1619
+ if (note.noteNumber === noteNumber) {
1620
+ this.setControllerParameters(channel, note, table);
1621
+ }
1622
+ });
1589
1623
  this.applyVoiceParams(channel, 10);
1590
1624
  }
1591
1625
  handleProgramChange(channelNumber, programNumber, _scheduleTime) {
@@ -1615,7 +1649,7 @@ class Midy {
1615
1649
  channel.detune += pressureDepth * (next - prev);
1616
1650
  }
1617
1651
  const table = channel.channelPressureTable;
1618
- this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1652
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1619
1653
  this.setControllerParameters(channel, note, table);
1620
1654
  });
1621
1655
  this.applyVoiceParams(channel, 13);
@@ -1823,8 +1857,8 @@ class Midy {
1823
1857
  if (key in voiceParams)
1824
1858
  noteVoiceParams[key] = voiceParams[key];
1825
1859
  }
1826
- if (0.5 <= channel.state.portamento && note.portamento) {
1827
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1860
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1861
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1828
1862
  }
1829
1863
  else {
1830
1864
  this.setFilterEnvelope(channel, note, scheduleTime);
@@ -1847,42 +1881,42 @@ class Midy {
1847
1881
  });
1848
1882
  }
1849
1883
  createControlChangeHandlers() {
1850
- return {
1851
- 0: this.setBankMSB,
1852
- 1: this.setModulationDepth,
1853
- 5: this.setPortamentoTime,
1854
- 6: this.dataEntryMSB,
1855
- 7: this.setVolume,
1856
- 10: this.setPan,
1857
- 11: this.setExpression,
1858
- 32: this.setBankLSB,
1859
- 38: this.dataEntryLSB,
1860
- 64: this.setSustainPedal,
1861
- 65: this.setPortamento,
1862
- 66: this.setSostenutoPedal,
1863
- 67: this.setSoftPedal,
1864
- 71: this.setFilterResonance,
1865
- 72: this.setReleaseTime,
1866
- 73: this.setAttackTime,
1867
- 74: this.setBrightness,
1868
- 75: this.setDecayTime,
1869
- 76: this.setVibratoRate,
1870
- 77: this.setVibratoDepth,
1871
- 78: this.setVibratoDelay,
1872
- 91: this.setReverbSendLevel,
1873
- 93: this.setChorusSendLevel,
1874
- 96: this.dataIncrement,
1875
- 97: this.dataDecrement,
1876
- 100: this.setRPNLSB,
1877
- 101: this.setRPNMSB,
1878
- 120: this.allSoundOff,
1879
- 121: this.resetAllControllers,
1880
- 123: this.allNotesOff,
1881
- 124: this.omniOff,
1882
- 125: this.omniOn,
1883
- 126: this.monoOn,
1884
- 127: this.polyOn,
1885
- };
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;
1886
1920
  }
1887
1921
  handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1888
1922
  const handler = this.controlChangeHandlers[controllerType];
@@ -1919,9 +1953,33 @@ class Midy {
1919
1953
  channel.state.modulationDepth = modulation / 127;
1920
1954
  this.updateModulation(channel, scheduleTime);
1921
1955
  }
1922
- 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) {
1923
1977
  const channel = this.channels[channelNumber];
1978
+ scheduleTime ??= this.audioContext.currentTime;
1924
1979
  channel.state.portamentoTime = portamentoTime / 127;
1980
+ if (channel.isDrum)
1981
+ return;
1982
+ this.updatePortamento(channel, scheduleTime);
1925
1983
  }
1926
1984
  setKeyBasedVolume(channel, scheduleTime) {
1927
1985
  this.processScheduledNotes(channel, (note) => {
@@ -1979,7 +2037,7 @@ class Midy {
1979
2037
  }
1980
2038
  dataEntryLSB(channelNumber, value, scheduleTime) {
1981
2039
  this.channels[channelNumber].dataLSB = value;
1982
- this.handleRPN(channelNumber, scheduleTime);
2040
+ this.handleRPN(channelNumber, 0, scheduleTime);
1983
2041
  }
1984
2042
  updateChannelVolume(channel, scheduleTime) {
1985
2043
  const state = channel.state;
@@ -2007,11 +2065,13 @@ class Midy {
2007
2065
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
2008
2066
  }
2009
2067
  }
2010
- setPortamento(channelNumber, value) {
2068
+ setPortamento(channelNumber, value, scheduleTime) {
2011
2069
  const channel = this.channels[channelNumber];
2012
2070
  if (channel.isDrum)
2013
2071
  return;
2072
+ scheduleTime ??= this.audioContext.currentTime;
2014
2073
  channel.state.portamento = value / 127;
2074
+ this.updatePortamento(channel, scheduleTime);
2015
2075
  }
2016
2076
  setSostenutoPedal(channelNumber, value, scheduleTime) {
2017
2077
  const channel = this.channels[channelNumber];
@@ -2020,7 +2080,11 @@ class Midy {
2020
2080
  scheduleTime ??= this.audioContext.currentTime;
2021
2081
  channel.state.sostenutoPedal = value / 127;
2022
2082
  if (64 <= value) {
2023
- channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
2083
+ const sostenutoNotes = [];
2084
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2085
+ sostenutoNotes.push(note);
2086
+ });
2087
+ channel.sostenutoNotes = sostenutoNotes;
2024
2088
  }
2025
2089
  else {
2026
2090
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
@@ -2034,9 +2098,9 @@ class Midy {
2034
2098
  scheduleTime ??= this.audioContext.currentTime;
2035
2099
  state.softPedal = softPedal / 127;
2036
2100
  this.processScheduledNotes(channel, (note) => {
2037
- if (0.5 <= state.portamento && note.portamento) {
2038
- this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
2039
- 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);
2040
2104
  }
2041
2105
  else {
2042
2106
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2050,7 +2114,7 @@ class Midy {
2050
2114
  return;
2051
2115
  scheduleTime ??= this.audioContext.currentTime;
2052
2116
  const state = channel.state;
2053
- state.filterResonance = filterResonance / 64;
2117
+ state.filterResonance = filterResonance / 127;
2054
2118
  this.processScheduledNotes(channel, (note) => {
2055
2119
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2056
2120
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
@@ -2061,14 +2125,14 @@ class Midy {
2061
2125
  if (channel.isDrum)
2062
2126
  return;
2063
2127
  scheduleTime ??= this.audioContext.currentTime;
2064
- channel.state.releaseTime = releaseTime / 64;
2128
+ channel.state.releaseTime = releaseTime / 127;
2065
2129
  }
2066
2130
  setAttackTime(channelNumber, attackTime, scheduleTime) {
2067
2131
  const channel = this.channels[channelNumber];
2068
2132
  if (channel.isDrum)
2069
2133
  return;
2070
2134
  scheduleTime ??= this.audioContext.currentTime;
2071
- channel.state.attackTime = attackTime / 64;
2135
+ channel.state.attackTime = attackTime / 127;
2072
2136
  this.processScheduledNotes(channel, (note) => {
2073
2137
  if (note.startTime < scheduleTime)
2074
2138
  return false;
@@ -2081,10 +2145,10 @@ class Midy {
2081
2145
  return;
2082
2146
  const state = channel.state;
2083
2147
  scheduleTime ??= this.audioContext.currentTime;
2084
- state.brightness = brightness / 64;
2148
+ state.brightness = brightness / 127;
2085
2149
  this.processScheduledNotes(channel, (note) => {
2086
- if (0.5 <= state.portamento && note.portamento) {
2087
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2150
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2151
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2088
2152
  }
2089
2153
  else {
2090
2154
  this.setFilterEnvelope(channel, note);
@@ -2096,7 +2160,7 @@ class Midy {
2096
2160
  if (channel.isDrum)
2097
2161
  return;
2098
2162
  scheduleTime ??= this.audioContext.currentTime;
2099
- channel.state.decayTime = dacayTime / 64;
2163
+ channel.state.decayTime = dacayTime / 127;
2100
2164
  this.processScheduledNotes(channel, (note) => {
2101
2165
  this.setVolumeEnvelope(channel, note, scheduleTime);
2102
2166
  });
@@ -2106,7 +2170,7 @@ class Midy {
2106
2170
  if (channel.isDrum)
2107
2171
  return;
2108
2172
  scheduleTime ??= this.audioContext.currentTime;
2109
- channel.state.vibratoRate = vibratoRate / 64;
2173
+ channel.state.vibratoRate = vibratoRate / 127;
2110
2174
  if (channel.vibratoDepth <= 0)
2111
2175
  return;
2112
2176
  this.processScheduledNotes(channel, (note) => {
@@ -2119,7 +2183,7 @@ class Midy {
2119
2183
  return;
2120
2184
  scheduleTime ??= this.audioContext.currentTime;
2121
2185
  const prev = channel.state.vibratoDepth;
2122
- channel.state.vibratoDepth = vibratoDepth / 64;
2186
+ channel.state.vibratoDepth = vibratoDepth / 127;
2123
2187
  if (0 < prev) {
2124
2188
  this.processScheduledNotes(channel, (note) => {
2125
2189
  this.setFreqVibLFO(channel, note, scheduleTime);
@@ -2131,12 +2195,12 @@ class Midy {
2131
2195
  });
2132
2196
  }
2133
2197
  }
2134
- setVibratoDelay(channelNumber, vibratoDelay) {
2198
+ setVibratoDelay(channelNumber, vibratoDelay, scheduleTime) {
2135
2199
  const channel = this.channels[channelNumber];
2136
2200
  if (channel.isDrum)
2137
2201
  return;
2138
2202
  scheduleTime ??= this.audioContext.currentTime;
2139
- channel.state.vibratoDelay = vibratoDelay / 64;
2203
+ channel.state.vibratoDelay = vibratoDelay / 127;
2140
2204
  if (0 < channel.state.vibratoDepth) {
2141
2205
  this.processScheduledNotes(channel, (note) => {
2142
2206
  this.startVibrato(channel, note, scheduleTime);
@@ -2258,12 +2322,14 @@ class Midy {
2258
2322
  }
2259
2323
  }
2260
2324
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2261
- dataIncrement(channelNumber) {
2262
- this.handleRPN(channelNumber, 1);
2325
+ dataIncrement(channelNumber, scheduleTime) {
2326
+ scheduleTime ??= this.audioContext.currentTime;
2327
+ this.handleRPN(channelNumber, 1, scheduleTime);
2263
2328
  }
2264
2329
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2265
- dataDecrement(channelNumber) {
2266
- this.handleRPN(channelNumber, -1);
2330
+ dataDecrement(channelNumber, scheduleTime) {
2331
+ scheduleTime ??= this.audioContext.currentTime;
2332
+ this.handleRPN(channelNumber, -1, scheduleTime);
2267
2333
  }
2268
2334
  setRPNMSB(channelNumber, value) {
2269
2335
  this.channels[channelNumber].rpnMSB = value;
@@ -2273,7 +2339,7 @@ class Midy {
2273
2339
  }
2274
2340
  dataEntryMSB(channelNumber, value, scheduleTime) {
2275
2341
  this.channels[channelNumber].dataMSB = value;
2276
- this.handleRPN(channelNumber, scheduleTime);
2342
+ this.handleRPN(channelNumber, 0, scheduleTime);
2277
2343
  }
2278
2344
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2279
2345
  const channel = this.channels[channelNumber];
@@ -2347,21 +2413,29 @@ class Midy {
2347
2413
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2348
2414
  }
2349
2415
  resetAllStates(channelNumber) {
2416
+ const scheduleTime = this.audioContext.currentTime;
2350
2417
  const channel = this.channels[channelNumber];
2351
2418
  const state = channel.state;
2352
- for (const type of Object.keys(defaultControllerState)) {
2353
- 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
+ }
2354
2427
  }
2355
- for (const type of Object.keys(this.constructor.channelSettings)) {
2356
- channel[type] = this.constructor.channelSettings[type];
2428
+ for (const key of Object.keys(this.constructor.channelSettings)) {
2429
+ channel[key] = this.constructor.channelSettings[key];
2357
2430
  }
2431
+ this.resetChannelTable(channel);
2358
2432
  this.mode = "GM2";
2359
2433
  this.masterFineTuning = 0; // cb
2360
2434
  this.masterCoarseTuning = 0; // cb
2361
2435
  }
2362
2436
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2363
- resetAllControllers(channelNumber) {
2364
- const stateTypes = [
2437
+ resetAllControllers(channelNumber, _value, scheduleTime) {
2438
+ const keys = [
2365
2439
  "polyphonicKeyPressure",
2366
2440
  "channelPressure",
2367
2441
  "pitchWheel",
@@ -2374,10 +2448,17 @@ class Midy {
2374
2448
  ];
2375
2449
  const channel = this.channels[channelNumber];
2376
2450
  const state = channel.state;
2377
- for (let i = 0; i < stateTypes.length; i++) {
2378
- const type = stateTypes[i];
2379
- 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
+ }
2380
2460
  }
2461
+ this.setPitchBend(channelNumber, 8192, scheduleTime);
2381
2462
  const settingTypes = [
2382
2463
  "rpnMSB",
2383
2464
  "rpnLSB",
@@ -2622,7 +2703,7 @@ class Midy {
2622
2703
  this.reverbEffect = options.reverbAlgorithm(audioContext);
2623
2704
  }
2624
2705
  getReverbTime(value) {
2625
- return Math.pow(Math.E, (value - 40) * 0.025);
2706
+ return Math.exp((value - 40) * 0.025);
2626
2707
  }
2627
2708
  // mean free path equation
2628
2709
  // https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
@@ -2856,7 +2937,13 @@ class Midy {
2856
2937
  setControllerParameters(channel, note, table) {
2857
2938
  if (table[0] !== 64)
2858
2939
  this.updateDetune(channel, note);
2859
- 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 {
2860
2947
  if (table[1] !== 64)
2861
2948
  this.setFilterEnvelope(channel, note);
2862
2949
  if (table[2] !== 64)
@@ -2884,8 +2971,13 @@ class Midy {
2884
2971
  initControlTable() {
2885
2972
  const channelCount = 128;
2886
2973
  const slotSize = 6;
2887
- const defaultValues = [64, 64, 64, 0, 0, 0];
2888
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];
2889
2981
  for (let ch = 0; ch < channelCount; ch++) {
2890
2982
  const offset = ch * slotSize;
2891
2983
  table.set(defaultValues, offset);