@marmooo/midy 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/midy.js CHANGED
@@ -1,59 +1,19 @@
1
1
  import { parseMidi } from "midi-file";
2
2
  import { parse, SoundFont } from "@marmooo/soundfont-parser";
3
- // 2-3 times faster than Map
4
- class SparseMap {
5
- constructor(size) {
6
- this.data = new Array(size);
7
- this.activeIndices = [];
8
- }
9
- set(key, value) {
10
- if (this.data[key] === undefined) {
11
- this.activeIndices.push(key);
12
- }
13
- this.data[key] = value;
14
- }
15
- get(key) {
16
- return this.data[key];
17
- }
18
- delete(key) {
19
- if (this.data[key] !== undefined) {
20
- this.data[key] = undefined;
21
- const index = this.activeIndices.indexOf(key);
22
- if (index !== -1) {
23
- this.activeIndices.splice(index, 1);
24
- }
25
- return true;
26
- }
27
- return false;
28
- }
29
- has(key) {
30
- return this.data[key] !== undefined;
31
- }
32
- get size() {
33
- return this.activeIndices.length;
34
- }
35
- clear() {
36
- for (let i = 0; i < this.activeIndices.length; i++) {
37
- const key = this.activeIndices[i];
38
- this.data[key] = undefined;
39
- }
40
- this.activeIndices = [];
41
- }
42
- *[Symbol.iterator]() {
43
- for (let i = 0; i < this.activeIndices.length; i++) {
44
- const key = this.activeIndices[i];
45
- yield [key, this.data[key]];
46
- }
47
- }
48
- forEach(callback) {
49
- for (let i = 0; i < this.activeIndices.length; i++) {
50
- const key = this.activeIndices[i];
51
- callback(this.data[key], key, this);
52
- }
53
- }
54
- }
55
3
  class Note {
56
4
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
5
+ Object.defineProperty(this, "index", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: -1
10
+ });
11
+ Object.defineProperty(this, "noteOffEvent", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: void 0
16
+ });
57
17
  Object.defineProperty(this, "bufferSource", {
58
18
  enumerable: true,
59
19
  configurable: true,
@@ -138,11 +98,11 @@ class Note {
138
98
  writable: true,
139
99
  value: void 0
140
100
  });
141
- Object.defineProperty(this, "portamento", {
101
+ Object.defineProperty(this, "portamentoNoteNumber", {
142
102
  enumerable: true,
143
103
  configurable: true,
144
104
  writable: true,
145
- value: void 0
105
+ value: -1
146
106
  });
147
107
  Object.defineProperty(this, "pressure", {
148
108
  enumerable: true,
@@ -204,7 +164,7 @@ const defaultControllerState = {
204
164
  portamentoTime: { type: 128 + 5, defaultValue: 0 },
205
165
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
206
166
  volume: { type: 128 + 7, defaultValue: 100 / 127 },
207
- pan: { type: 128 + 10, defaultValue: 0.5 },
167
+ pan: { type: 128 + 10, defaultValue: 64 / 127 },
208
168
  expression: { type: 128 + 11, defaultValue: 1 },
209
169
  // bankLSB: { type: 128 + 32, defaultValue: 0, },
210
170
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
@@ -212,14 +172,14 @@ const defaultControllerState = {
212
172
  portamento: { type: 128 + 65, defaultValue: 0 },
213
173
  sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
214
174
  softPedal: { type: 128 + 67, defaultValue: 0 },
215
- filterResonance: { type: 128 + 71, defaultValue: 0.5 },
216
- releaseTime: { type: 128 + 72, defaultValue: 0.5 },
217
- attackTime: { type: 128 + 73, defaultValue: 0.5 },
218
- brightness: { type: 128 + 74, defaultValue: 0.5 },
219
- decayTime: { type: 128 + 75, defaultValue: 0.5 },
220
- vibratoRate: { type: 128 + 76, defaultValue: 0.5 },
221
- vibratoDepth: { type: 128 + 77, defaultValue: 0.5 },
222
- vibratoDelay: { type: 128 + 78, defaultValue: 0.5 },
175
+ filterResonance: { type: 128 + 71, defaultValue: 64 / 127 },
176
+ releaseTime: { type: 128 + 72, defaultValue: 64 / 127 },
177
+ attackTime: { type: 128 + 73, defaultValue: 64 / 127 },
178
+ brightness: { type: 128 + 74, defaultValue: 64 / 127 },
179
+ decayTime: { type: 128 + 75, defaultValue: 64 / 127 },
180
+ vibratoRate: { type: 128 + 76, defaultValue: 64 / 127 },
181
+ vibratoDepth: { type: 128 + 77, defaultValue: 64 / 127 },
182
+ vibratoDelay: { type: 128 + 78, defaultValue: 64 / 127 },
223
183
  reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
224
184
  chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
225
185
  // dataIncrement: { type: 128 + 96, defaultValue: 0 },
@@ -494,7 +454,7 @@ export class Midy {
494
454
  initSoundFontTable() {
495
455
  const table = new Array(128);
496
456
  for (let i = 0; i < 128; i++) {
497
- table[i] = new SparseMap(128);
457
+ table[i] = new Map();
498
458
  }
499
459
  return table;
500
460
  }
@@ -541,18 +501,25 @@ export class Midy {
541
501
  merger,
542
502
  };
543
503
  }
504
+ resetChannelTable(channel) {
505
+ this.resetControlTable(channel.controlTable);
506
+ channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
507
+ channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
508
+ channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
509
+ channel.keyBasedInstrumentControlTable.fill(0); // [-64, 63]
510
+ }
544
511
  createChannels(audioContext) {
545
512
  const channels = Array.from({ length: this.numChannels }, () => {
546
513
  return {
547
514
  currentBufferSource: null,
548
515
  isDrum: false,
549
- ...this.constructor.channelSettings,
550
516
  state: new ControllerState(),
551
- controlTable: this.initControlTable(),
517
+ ...this.constructor.channelSettings,
552
518
  ...this.setChannelAudioNodes(audioContext),
553
- scheduledNotes: new SparseMap(128),
519
+ scheduledNotes: [],
554
520
  sustainNotes: [],
555
- sostenutoNotes: new SparseMap(128),
521
+ sostenutoNotes: [],
522
+ controlTable: this.initControlTable(),
556
523
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
557
524
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
558
525
  polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
@@ -600,46 +567,20 @@ export class Midy {
600
567
  }
601
568
  return bufferSource;
602
569
  }
603
- findPortamentoTarget(queueIndex) {
604
- const endEvent = this.timeline[queueIndex];
605
- if (!this.channels[endEvent.channel].portamento)
606
- return;
607
- const endTime = endEvent.startTime;
608
- let target;
609
- while (++queueIndex < this.timeline.length) {
610
- const event = this.timeline[queueIndex];
611
- if (endTime !== event.startTime)
612
- break;
613
- if (event.type !== "noteOn")
614
- continue;
615
- if (!target || event.noteNumber < target.noteNumber) {
616
- target = event;
617
- }
618
- }
619
- return target;
620
- }
621
- async scheduleTimelineEvents(t, offset, queueIndex) {
570
+ async scheduleTimelineEvents(t, resumeTime, queueIndex) {
622
571
  while (queueIndex < this.timeline.length) {
623
572
  const event = this.timeline[queueIndex];
624
573
  if (event.startTime > t + this.lookAhead)
625
574
  break;
626
- const startTime = event.startTime + this.startDelay - offset;
575
+ const delay = this.startDelay - resumeTime;
576
+ const startTime = event.startTime + delay;
627
577
  switch (event.type) {
628
- case "noteOn":
629
- if (0 < event.velocity) {
630
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
631
- break;
632
- }
633
- /* falls through */
634
- case "noteOff": {
635
- const portamentoTarget = this.findPortamentoTarget(queueIndex);
636
- if (portamentoTarget)
637
- portamentoTarget.portamento = true;
638
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
639
- portamentoTarget?.noteNumber);
640
- if (notePromise) {
641
- this.notePromises.push(notePromise);
642
- }
578
+ case "noteOn": {
579
+ const noteOffEvent = {
580
+ ...event.noteOffEvent,
581
+ startTime: event.noteOffEvent.startTime + delay,
582
+ };
583
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, noteOffEvent);
643
584
  break;
644
585
  }
645
586
  case "noteAftertouch":
@@ -678,7 +619,7 @@ export class Midy {
678
619
  this.isPaused = false;
679
620
  this.startTime = this.audioContext.currentTime;
680
621
  let queueIndex = this.getQueueIndex(this.resumeTime);
681
- let offset = this.resumeTime - this.startTime;
622
+ let resumeTime = this.resumeTime - this.startTime;
682
623
  this.notePromises = [];
683
624
  const schedulePlayback = async () => {
684
625
  if (queueIndex >= this.timeline.length) {
@@ -687,18 +628,21 @@ export class Midy {
687
628
  this.exclusiveClassNotes.fill(undefined);
688
629
  this.drumExclusiveClassNotes.fill(undefined);
689
630
  this.audioBufferCache.clear();
631
+ for (let i = 0; i < this.channels.length; i++) {
632
+ this.resetAllStates(i);
633
+ }
690
634
  resolve();
691
635
  return;
692
636
  }
693
637
  const now = this.audioContext.currentTime;
694
- const t = now + offset;
695
- queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
638
+ const t = now + resumeTime;
639
+ queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
696
640
  if (this.isPausing) {
697
641
  await this.stopNotes(0, true, now);
698
642
  this.notePromises = [];
699
- resolve();
700
643
  this.isPausing = false;
701
644
  this.isPaused = true;
645
+ resolve();
702
646
  return;
703
647
  }
704
648
  else if (this.isStopping) {
@@ -707,9 +651,12 @@ export class Midy {
707
651
  this.exclusiveClassNotes.fill(undefined);
708
652
  this.drumExclusiveClassNotes.fill(undefined);
709
653
  this.audioBufferCache.clear();
710
- resolve();
654
+ for (let i = 0; i < this.channels.length; i++) {
655
+ this.resetAllStates(i);
656
+ }
711
657
  this.isStopping = false;
712
658
  this.isPaused = false;
659
+ resolve();
713
660
  return;
714
661
  }
715
662
  else if (this.isSeeking) {
@@ -718,7 +665,7 @@ export class Midy {
718
665
  this.drumExclusiveClassNotes.fill(undefined);
719
666
  this.startTime = this.audioContext.currentTime;
720
667
  queueIndex = this.getQueueIndex(this.resumeTime);
721
- offset = this.resumeTime - this.startTime;
668
+ resumeTime = this.resumeTime - this.startTime;
722
669
  this.isSeeking = false;
723
670
  await schedulePlayback();
724
671
  }
@@ -840,13 +787,37 @@ export class Midy {
840
787
  prevTempoTicks = event.ticks;
841
788
  }
842
789
  }
790
+ const activeNotes = new Array(this.channels.length * 128);
791
+ for (let i = 0; i < activeNotes.length; i++) {
792
+ activeNotes[i] = [];
793
+ }
794
+ for (let i = 0; i < timeline.length; i++) {
795
+ const event = timeline[i];
796
+ switch (event.type) {
797
+ case "noteOn": {
798
+ const index = event.channel * 128 + event.noteNumber;
799
+ activeNotes[index].push(event);
800
+ break;
801
+ }
802
+ case "noteOff": {
803
+ const index = event.channel * 128 + event.noteNumber;
804
+ const noteOn = activeNotes[index].pop();
805
+ if (noteOn) {
806
+ noteOn.noteOffEvent = event;
807
+ }
808
+ else {
809
+ const eventString = JSON.stringify(event, null, 2);
810
+ console.warn(`noteOff without matching noteOn: ${eventString}`);
811
+ }
812
+ }
813
+ }
814
+ }
843
815
  return { instruments, timeline };
844
816
  }
845
817
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
846
818
  const channel = this.channels[channelNumber];
847
819
  const promises = [];
848
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
849
- activeNotes.forEach((note) => {
820
+ this.processActiveNotes(channel, scheduleTime, (note) => {
850
821
  const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
851
822
  this.notePromises.push(promise);
852
823
  promises.push(promise);
@@ -857,11 +828,11 @@ export class Midy {
857
828
  const channel = this.channels[channelNumber];
858
829
  const promises = [];
859
830
  this.processScheduledNotes(channel, (note) => {
860
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
831
+ const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
861
832
  this.notePromises.push(promise);
862
833
  promises.push(promise);
863
834
  });
864
- channel.scheduledNotes.clear();
835
+ channel.scheduledNotes = [];
865
836
  return Promise.all(promises);
866
837
  }
867
838
  stopNotes(velocity, force, scheduleTime) {
@@ -882,9 +853,6 @@ export class Midy {
882
853
  if (!this.isPlaying)
883
854
  return;
884
855
  this.isStopping = true;
885
- for (let i = 0; i < this.channels.length; i++) {
886
- this.resetAllStates(i);
887
- }
888
856
  }
889
857
  pause() {
890
858
  if (!this.isPlaying || this.isPaused)
@@ -919,37 +887,31 @@ export class Midy {
919
887
  return this.resumeTime + now - this.startTime - this.startDelay;
920
888
  }
921
889
  processScheduledNotes(channel, callback) {
922
- channel.scheduledNotes.forEach((noteList) => {
923
- for (let i = 0; i < noteList.length; i++) {
924
- const note = noteList[i];
925
- if (!note)
926
- continue;
927
- if (note.ending)
928
- continue;
929
- callback(note);
930
- }
931
- });
932
- }
933
- getActiveNotes(channel, scheduleTime) {
934
- const activeNotes = new SparseMap(128);
935
- channel.scheduledNotes.forEach((noteList) => {
936
- const activeNote = this.getActiveNote(noteList, scheduleTime);
937
- if (activeNote) {
938
- activeNotes.set(activeNote.noteNumber, activeNote);
939
- }
940
- });
941
- return activeNotes;
890
+ const scheduledNotes = channel.scheduledNotes;
891
+ for (let i = 0; i < scheduledNotes.length; i++) {
892
+ const note = scheduledNotes[i];
893
+ if (!note)
894
+ continue;
895
+ if (note.ending)
896
+ continue;
897
+ callback(note);
898
+ }
942
899
  }
943
- getActiveNote(noteList, scheduleTime) {
944
- for (let i = noteList.length - 1; i >= 0; i--) {
945
- const note = noteList[i];
900
+ processActiveNotes(channel, scheduleTime, callback) {
901
+ const scheduledNotes = channel.scheduledNotes;
902
+ for (let i = 0; i < scheduledNotes.length; i++) {
903
+ const note = scheduledNotes[i];
946
904
  if (!note)
947
- return;
905
+ continue;
906
+ if (note.ending)
907
+ continue;
908
+ const noteOffEvent = note.noteOffEvent;
909
+ if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
910
+ continue;
948
911
  if (scheduleTime < note.startTime)
949
912
  continue;
950
- return (note.ending) ? null : note;
913
+ callback(note);
951
914
  }
952
- return noteList[0];
953
915
  }
954
916
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
955
917
  const sampleRate = audioContext.sampleRate;
@@ -1116,24 +1078,94 @@ export class Midy {
1116
1078
  const noteDetune = this.calcNoteDetune(channel, note);
1117
1079
  const pitchControl = this.getPitchControl(channel, note);
1118
1080
  const detune = channel.detune + noteDetune + pitchControl;
1119
- note.bufferSource.detune
1120
- .cancelScheduledValues(scheduleTime)
1121
- .setValueAtTime(detune, scheduleTime);
1122
- }
1123
- getPortamentoTime(channel) {
1124
- const factor = 5 * Math.log(10) * 127;
1125
- return channel.state.portamentoTime * factor;
1126
- }
1127
- setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1081
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1082
+ const startTime = note.startTime;
1083
+ const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1084
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1085
+ note.bufferSource.detune
1086
+ .cancelScheduledValues(scheduleTime)
1087
+ .setValueAtTime(detune - deltaCent, scheduleTime)
1088
+ .linearRampToValueAtTime(detune, portamentoTime);
1089
+ }
1090
+ else {
1091
+ note.bufferSource.detune
1092
+ .cancelScheduledValues(scheduleTime)
1093
+ .setValueAtTime(detune, scheduleTime);
1094
+ }
1095
+ }
1096
+ getPortamentoTime(channel, note) {
1097
+ const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1098
+ const value = Math.ceil(channel.state.portamentoTime * 127);
1099
+ return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1100
+ }
1101
+ getPitchIncrementSpeed(value) {
1102
+ const points = [
1103
+ [0, 1000],
1104
+ [6, 100],
1105
+ [16, 20],
1106
+ [32, 10],
1107
+ [48, 5],
1108
+ [64, 2.5],
1109
+ [80, 1],
1110
+ [96, 0.4],
1111
+ [112, 0.15],
1112
+ [127, 0.01],
1113
+ ];
1114
+ const logPoints = new Array(points.length);
1115
+ for (let i = 0; i < points.length; i++) {
1116
+ const [x, y] = points[i];
1117
+ if (value === x)
1118
+ return y;
1119
+ logPoints[i] = [x, Math.log(y)];
1120
+ }
1121
+ let startIndex = 0;
1122
+ for (let i = 1; i < logPoints.length; i++) {
1123
+ if (value <= logPoints[i][0]) {
1124
+ startIndex = i - 1;
1125
+ break;
1126
+ }
1127
+ }
1128
+ const [x0, y0] = logPoints[startIndex];
1129
+ const [x1, y1] = logPoints[startIndex + 1];
1130
+ const h = x1 - x0;
1131
+ const t = (value - x0) / h;
1132
+ let m0, m1;
1133
+ if (startIndex === 0) {
1134
+ m0 = (y1 - y0) / h;
1135
+ }
1136
+ else {
1137
+ const [xPrev, yPrev] = logPoints[startIndex - 1];
1138
+ m0 = (y1 - yPrev) / (x1 - xPrev);
1139
+ }
1140
+ if (startIndex === logPoints.length - 2) {
1141
+ m1 = (y1 - y0) / h;
1142
+ }
1143
+ else {
1144
+ const [xNext, yNext] = logPoints[startIndex + 2];
1145
+ m1 = (yNext - y0) / (xNext - x0);
1146
+ }
1147
+ // Cubic Hermite Spline
1148
+ const t2 = t * t;
1149
+ const t3 = t2 * t;
1150
+ const h00 = 2 * t3 - 3 * t2 + 1;
1151
+ const h10 = t3 - 2 * t2 + t;
1152
+ const h01 = -2 * t3 + 3 * t2;
1153
+ const h11 = t3 - t2;
1154
+ const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
1155
+ return Math.exp(y);
1156
+ }
1157
+ setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1158
+ const state = channel.state;
1128
1159
  const { voiceParams, startTime } = note;
1129
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1160
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1161
+ (1 + this.getAmplitudeControl(channel, note));
1130
1162
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1131
1163
  const volDelay = startTime + voiceParams.volDelay;
1132
- const portamentoTime = volDelay + this.getPortamentoTime(channel);
1164
+ const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1165
+ const volHold = volAttack + voiceParams.volHold;
1133
1166
  note.volumeEnvelopeNode.gain
1134
1167
  .cancelScheduledValues(scheduleTime)
1135
- .setValueAtTime(0, volDelay)
1136
- .linearRampToValueAtTime(sustainVolume, portamentoTime);
1168
+ .setValueAtTime(sustainVolume, volHold);
1137
1169
  }
1138
1170
  setVolumeEnvelope(channel, note, scheduleTime) {
1139
1171
  const state = channel.state;
@@ -1153,6 +1185,12 @@ export class Midy {
1153
1185
  .setValueAtTime(attackVolume, volHold)
1154
1186
  .linearRampToValueAtTime(sustainVolume, volDecay);
1155
1187
  }
1188
+ setPortamentoPitchEnvelope(note, scheduleTime) {
1189
+ const baseRate = note.voiceParams.playbackRate;
1190
+ note.bufferSource.playbackRate
1191
+ .cancelScheduledValues(scheduleTime)
1192
+ .setValueAtTime(baseRate, scheduleTime);
1193
+ }
1156
1194
  setPitchEnvelope(note, scheduleTime) {
1157
1195
  const { voiceParams } = note;
1158
1196
  const baseRate = voiceParams.playbackRate;
@@ -1180,20 +1218,21 @@ export class Midy {
1180
1218
  const maxFrequency = 20000; // max Hz of initialFilterFc
1181
1219
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1182
1220
  }
1183
- setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1221
+ setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1184
1222
  const state = channel.state;
1185
1223
  const { voiceParams, noteNumber, startTime } = note;
1186
1224
  const softPedalFactor = 1 -
1187
1225
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1188
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1189
- softPedalFactor *
1226
+ const baseCent = voiceParams.initialFilterFc +
1227
+ this.getFilterCutoffControl(channel, note);
1228
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1190
1229
  state.brightness * 2;
1191
1230
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1192
1231
  const sustainFreq = baseFreq +
1193
1232
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1194
1233
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1195
1234
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1196
- const portamentoTime = startTime + this.getPortamentoTime(channel);
1235
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1197
1236
  const modDelay = startTime + voiceParams.modDelay;
1198
1237
  note.filterNode.frequency
1199
1238
  .cancelScheduledValues(scheduleTime)
@@ -1279,7 +1318,7 @@ export class Midy {
1279
1318
  return audioBuffer;
1280
1319
  }
1281
1320
  }
1282
- async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1321
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1283
1322
  const now = this.audioContext.currentTime;
1284
1323
  const state = channel.state;
1285
1324
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
@@ -1295,20 +1334,24 @@ export class Midy {
1295
1334
  type: "lowpass",
1296
1335
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1297
1336
  });
1298
- if (0.5 <= state.portamento && portamento) {
1299
- note.portamento = true;
1300
- this.setPortamentoStartVolumeEnvelope(channel, note, now);
1301
- this.setPortamentoStartFilterEnvelope(channel, note, now);
1337
+ const prevNote = channel.scheduledNotes.at(-1);
1338
+ if (prevNote && prevNote.noteNumber !== noteNumber) {
1339
+ note.portamentoNoteNumber = prevNote.noteNumber;
1340
+ }
1341
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1342
+ this.setPortamentoVolumeEnvelope(channel, note, now);
1343
+ this.setPortamentoFilterEnvelope(channel, note, now);
1344
+ this.setPortamentoPitchEnvelope(note, now);
1302
1345
  }
1303
1346
  else {
1304
- note.portamento = false;
1305
1347
  this.setVolumeEnvelope(channel, note, now);
1306
1348
  this.setFilterEnvelope(channel, note, now);
1349
+ this.setPitchEnvelope(note, now);
1307
1350
  }
1351
+ this.updateDetune(channel, note, now);
1308
1352
  if (0 < state.vibratoDepth) {
1309
1353
  this.startVibrato(channel, note, now);
1310
1354
  }
1311
- this.setPitchEnvelope(note, now);
1312
1355
  if (0 < state.modulationDepth) {
1313
1356
  this.startModulation(channel, note, now);
1314
1357
  }
@@ -1354,9 +1397,8 @@ export class Midy {
1354
1397
  if (prev) {
1355
1398
  const [prevNote, prevChannelNumber] = prev;
1356
1399
  if (prevNote && !prevNote.ending) {
1357
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1358
- startTime, true, // force
1359
- undefined);
1400
+ this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
1401
+ startTime, true);
1360
1402
  }
1361
1403
  }
1362
1404
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
@@ -1375,9 +1417,8 @@ export class Midy {
1375
1417
  channelNumber;
1376
1418
  const prevNote = this.drumExclusiveClassNotes[index];
1377
1419
  if (prevNote && !prevNote.ending) {
1378
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1379
- startTime, true, // force
1380
- undefined);
1420
+ this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
1421
+ startTime, true);
1381
1422
  }
1382
1423
  this.drumExclusiveClassNotes[index] = note;
1383
1424
  }
@@ -1388,7 +1429,7 @@ export class Midy {
1388
1429
  return !((programNumber === 48 && noteNumber === 88) ||
1389
1430
  (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1390
1431
  }
1391
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1432
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1392
1433
  const channel = this.channels[channelNumber];
1393
1434
  const bankNumber = this.calcBank(channel, channelNumber);
1394
1435
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1399,7 +1440,8 @@ export class Midy {
1399
1440
  if (!voice)
1400
1441
  return;
1401
1442
  const isSF3 = soundFont.parsed.info.version.major === 3;
1402
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1443
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1444
+ note.noteOffEvent = noteOffEvent;
1403
1445
  note.gainL.connect(channel.gainL);
1404
1446
  note.gainR.connect(channel.gainR);
1405
1447
  if (0.5 <= channel.state.sustainPedal) {
@@ -1408,20 +1450,13 @@ export class Midy {
1408
1450
  this.handleExclusiveClass(note, channelNumber, startTime);
1409
1451
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1410
1452
  const scheduledNotes = channel.scheduledNotes;
1411
- let noteList = scheduledNotes.get(noteNumber);
1412
- if (noteList) {
1413
- noteList.push(note);
1414
- }
1415
- else {
1416
- noteList = [note];
1417
- scheduledNotes.set(noteNumber, noteList);
1418
- }
1453
+ note.index = scheduledNotes.length;
1454
+ scheduledNotes.push(note);
1419
1455
  if (this.isDrumNoteOffException(channel, noteNumber)) {
1420
1456
  const stopTime = startTime + note.bufferSource.buffer.duration;
1421
- const index = noteList.length - 1;
1422
1457
  const promise = new Promise((resolve) => {
1423
1458
  note.bufferSource.onended = () => {
1424
- noteList[index] = undefined;
1459
+ scheduledNotes[note.index] = undefined;
1425
1460
  this.disconnectNote(note);
1426
1461
  resolve();
1427
1462
  };
@@ -1429,10 +1464,23 @@ export class Midy {
1429
1464
  });
1430
1465
  this.notePromises.push(promise);
1431
1466
  }
1467
+ else if (noteOffEvent) {
1468
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1469
+ const portamentoTime = this.getPortamentoTime(channel, note);
1470
+ const portamentoEndTime = startTime + portamentoTime;
1471
+ const notePromise = this.scheduleNoteOff(channelNumber, note, 0, // velocity
1472
+ Math.max(noteOffEvent.startTime, portamentoEndTime), false);
1473
+ this.notePromises.push(notePromise);
1474
+ }
1475
+ else {
1476
+ const notePromise = this.scheduleNoteOff(channelNumber, note, noteOffEvent.velocity, noteOffEvent.startTime, false);
1477
+ this.notePromises.push(notePromise);
1478
+ }
1479
+ }
1432
1480
  }
1433
1481
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1434
1482
  scheduleTime ??= this.audioContext.currentTime;
1435
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1483
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1436
1484
  }
1437
1485
  disconnectNote(note) {
1438
1486
  note.bufferSource.disconnect();
@@ -1457,8 +1505,7 @@ export class Midy {
1457
1505
  note.chorusEffectsSend.disconnect();
1458
1506
  }
1459
1507
  }
1460
- stopNote(endTime, stopTime, noteList, index) {
1461
- const note = noteList[index];
1508
+ stopNote(channel, note, endTime, stopTime) {
1462
1509
  note.volumeEnvelopeNode.gain
1463
1510
  .cancelScheduledValues(endTime)
1464
1511
  .linearRampToValueAtTime(0, stopTime);
@@ -1468,73 +1515,58 @@ export class Midy {
1468
1515
  }, stopTime);
1469
1516
  return new Promise((resolve) => {
1470
1517
  note.bufferSource.onended = () => {
1471
- noteList[index] = undefined;
1518
+ channel.scheduledNotes[note.index] = undefined;
1472
1519
  this.disconnectNote(note);
1473
1520
  resolve();
1474
1521
  };
1475
1522
  note.bufferSource.stop(stopTime);
1476
1523
  });
1477
1524
  }
1478
- findNoteOffTarget(noteList) {
1479
- for (let i = 0; i < noteList.length; i++) {
1480
- const note = noteList[i];
1481
- if (!note)
1482
- continue;
1483
- if (note.ending)
1484
- continue;
1485
- return [note, i];
1486
- }
1487
- }
1488
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1525
+ scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
1489
1526
  const channel = this.channels[channelNumber];
1490
- if (this.isDrumNoteOffException(channel, noteNumber))
1527
+ if (this.isDrumNoteOffException(channel, note.noteNumber))
1491
1528
  return;
1492
1529
  const state = channel.state;
1493
1530
  if (!force) {
1494
1531
  if (0.5 <= state.sustainPedal)
1495
1532
  return;
1496
- if (channel.sostenutoNotes.has(noteNumber))
1533
+ if (0.5 <= channel.state.sostenutoPedal)
1497
1534
  return;
1498
1535
  }
1499
- const noteList = channel.scheduledNotes.get(noteNumber);
1500
- if (!noteList)
1501
- return; // be careful with drum channel
1502
- const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
1503
- if (!noteOffTarget)
1504
- return;
1505
- const [note, i] = noteOffTarget;
1506
- if (0.5 <= state.portamento && portamentoNoteNumber !== undefined) {
1507
- const portamentoTime = endTime + this.getPortamentoTime(channel);
1508
- const deltaNote = portamentoNoteNumber - noteNumber;
1509
- const baseRate = note.voiceParams.playbackRate;
1510
- const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1511
- note.bufferSource.playbackRate
1512
- .cancelScheduledValues(endTime)
1513
- .linearRampToValueAtTime(targetRate, portamentoTime);
1514
- return this.stopNote(endTime, portamentoTime, noteList, i);
1515
- }
1516
- else {
1517
- const volRelease = endTime +
1518
- note.voiceParams.volRelease * state.releaseTime * 2;
1519
- const modRelease = endTime + note.voiceParams.modRelease;
1520
- note.filterNode.frequency
1521
- .cancelScheduledValues(endTime)
1522
- .linearRampToValueAtTime(0, modRelease);
1523
- const stopTime = Math.min(volRelease, modRelease);
1524
- return this.stopNote(endTime, stopTime, noteList, i);
1536
+ const volRelease = endTime +
1537
+ note.voiceParams.volRelease * channel.state.releaseTime * 2;
1538
+ const modRelease = endTime + note.voiceParams.modRelease;
1539
+ note.filterNode.frequency
1540
+ .cancelScheduledValues(endTime)
1541
+ .linearRampToValueAtTime(0, modRelease);
1542
+ const stopTime = Math.min(volRelease, modRelease);
1543
+ return this.stopNote(channel, note, endTime, stopTime);
1544
+ }
1545
+ findNoteOffTarget(channel, noteNumber) {
1546
+ const scheduledNotes = channel.scheduledNotes;
1547
+ for (let i = 0; i < scheduledNotes.length; i++) {
1548
+ const note = scheduledNotes[i];
1549
+ if (!note)
1550
+ continue;
1551
+ if (note.ending)
1552
+ continue;
1553
+ if (note.noteNumber !== noteNumber)
1554
+ continue;
1555
+ return note;
1525
1556
  }
1526
1557
  }
1527
1558
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1528
1559
  scheduleTime ??= this.audioContext.currentTime;
1529
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1530
- undefined);
1560
+ const channel = this.channels[channelNumber];
1561
+ const note = this.findNoteOffTarget(channel, noteNumber);
1562
+ return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
1531
1563
  }
1532
1564
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1533
1565
  const velocity = halfVelocity * 2;
1534
1566
  const channel = this.channels[channelNumber];
1535
1567
  const promises = [];
1536
1568
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1537
- const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1569
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
1538
1570
  promises.push(promise);
1539
1571
  }
1540
1572
  channel.sustainNotes = [];
@@ -1544,12 +1576,14 @@ export class Midy {
1544
1576
  const velocity = halfVelocity * 2;
1545
1577
  const channel = this.channels[channelNumber];
1546
1578
  const promises = [];
1579
+ const sostenutoNotes = channel.sostenutoNotes;
1547
1580
  channel.state.sostenutoPedal = 0;
1548
- channel.sostenutoNotes.forEach((note) => {
1549
- const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1581
+ for (let i = 0; i < sostenutoNotes.length; i++) {
1582
+ const note = sostenutoNotes[i];
1583
+ const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime);
1550
1584
  promises.push(promise);
1551
- });
1552
- channel.sostenutoNotes.clear();
1585
+ }
1586
+ channel.sostenutoNotes = [];
1553
1587
  return promises;
1554
1588
  }
1555
1589
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
@@ -1578,11 +1612,11 @@ export class Midy {
1578
1612
  const channel = this.channels[channelNumber];
1579
1613
  channel.state.polyphonicKeyPressure = pressure / 127;
1580
1614
  const table = channel.polyphonicKeyPressureTable;
1581
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
1582
- if (activeNotes.has(noteNumber)) {
1583
- const note = activeNotes.get(noteNumber);
1584
- this.setControllerParameters(channel, note, table);
1585
- }
1615
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1616
+ if (note.noteNumber === noteNumber) {
1617
+ this.setControllerParameters(channel, note, table);
1618
+ }
1619
+ });
1586
1620
  this.applyVoiceParams(channel, 10);
1587
1621
  }
1588
1622
  handleProgramChange(channelNumber, programNumber, _scheduleTime) {
@@ -1612,7 +1646,7 @@ export class Midy {
1612
1646
  channel.detune += pressureDepth * (next - prev);
1613
1647
  }
1614
1648
  const table = channel.channelPressureTable;
1615
- this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1649
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1616
1650
  this.setControllerParameters(channel, note, table);
1617
1651
  });
1618
1652
  this.applyVoiceParams(channel, 13);
@@ -1820,8 +1854,8 @@ export class Midy {
1820
1854
  if (key in voiceParams)
1821
1855
  noteVoiceParams[key] = voiceParams[key];
1822
1856
  }
1823
- if (0.5 <= channel.state.portamento && note.portamento) {
1824
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1857
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1858
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1825
1859
  }
1826
1860
  else {
1827
1861
  this.setFilterEnvelope(channel, note, scheduleTime);
@@ -1844,42 +1878,42 @@ export class Midy {
1844
1878
  });
1845
1879
  }
1846
1880
  createControlChangeHandlers() {
1847
- return {
1848
- 0: this.setBankMSB,
1849
- 1: this.setModulationDepth,
1850
- 5: this.setPortamentoTime,
1851
- 6: this.dataEntryMSB,
1852
- 7: this.setVolume,
1853
- 10: this.setPan,
1854
- 11: this.setExpression,
1855
- 32: this.setBankLSB,
1856
- 38: this.dataEntryLSB,
1857
- 64: this.setSustainPedal,
1858
- 65: this.setPortamento,
1859
- 66: this.setSostenutoPedal,
1860
- 67: this.setSoftPedal,
1861
- 71: this.setFilterResonance,
1862
- 72: this.setReleaseTime,
1863
- 73: this.setAttackTime,
1864
- 74: this.setBrightness,
1865
- 75: this.setDecayTime,
1866
- 76: this.setVibratoRate,
1867
- 77: this.setVibratoDepth,
1868
- 78: this.setVibratoDelay,
1869
- 91: this.setReverbSendLevel,
1870
- 93: this.setChorusSendLevel,
1871
- 96: this.dataIncrement,
1872
- 97: this.dataDecrement,
1873
- 100: this.setRPNLSB,
1874
- 101: this.setRPNMSB,
1875
- 120: this.allSoundOff,
1876
- 121: this.resetAllControllers,
1877
- 123: this.allNotesOff,
1878
- 124: this.omniOff,
1879
- 125: this.omniOn,
1880
- 126: this.monoOn,
1881
- 127: this.polyOn,
1882
- };
1881
+ const handlers = new Array(128);
1882
+ handlers[0] = this.setBankMSB;
1883
+ handlers[1] = this.setModulationDepth;
1884
+ handlers[5] = this.setPortamentoTime;
1885
+ handlers[6] = this.dataEntryMSB;
1886
+ handlers[7] = this.setVolume;
1887
+ handlers[10] = this.setPan;
1888
+ handlers[11] = this.setExpression;
1889
+ handlers[32] = this.setBankLSB;
1890
+ handlers[38] = this.dataEntryLSB;
1891
+ handlers[64] = this.setSustainPedal;
1892
+ handlers[65] = this.setPortamento;
1893
+ handlers[66] = this.setSostenutoPedal;
1894
+ handlers[67] = this.setSoftPedal;
1895
+ handlers[71] = this.setFilterResonance;
1896
+ handlers[72] = this.setReleaseTime;
1897
+ handlers[73] = this.setAttackTime;
1898
+ handlers[74] = this.setBrightness;
1899
+ handlers[75] = this.setDecayTime;
1900
+ handlers[76] = this.setVibratoRate;
1901
+ handlers[77] = this.setVibratoDepth;
1902
+ handlers[78] = this.setVibratoDelay;
1903
+ handlers[91] = this.setReverbSendLevel;
1904
+ handlers[93] = this.setChorusSendLevel;
1905
+ handlers[96] = this.dataIncrement;
1906
+ handlers[97] = this.dataDecrement;
1907
+ handlers[100] = this.setRPNLSB;
1908
+ handlers[101] = this.setRPNMSB;
1909
+ handlers[120] = this.allSoundOff;
1910
+ handlers[121] = this.resetAllControllers;
1911
+ handlers[123] = this.allNotesOff;
1912
+ handlers[124] = this.omniOff;
1913
+ handlers[125] = this.omniOn;
1914
+ handlers[126] = this.monoOn;
1915
+ handlers[127] = this.polyOn;
1916
+ return handlers;
1883
1917
  }
1884
1918
  handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1885
1919
  const handler = this.controlChangeHandlers[controllerType];
@@ -1916,9 +1950,33 @@ export class Midy {
1916
1950
  channel.state.modulationDepth = modulation / 127;
1917
1951
  this.updateModulation(channel, scheduleTime);
1918
1952
  }
1919
- setPortamentoTime(channelNumber, portamentoTime) {
1953
+ updatePortamento(channel, scheduleTime) {
1954
+ this.processScheduledNotes(channel, (note) => {
1955
+ if (0.5 <= channel.state.portamento) {
1956
+ if (0 <= note.portamentoNoteNumber) {
1957
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1958
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1959
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
1960
+ this.updateDetune(channel, note, scheduleTime);
1961
+ }
1962
+ }
1963
+ else {
1964
+ if (0 <= note.portamentoNoteNumber) {
1965
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1966
+ this.setFilterEnvelope(channel, note, scheduleTime);
1967
+ this.setPitchEnvelope(note, scheduleTime);
1968
+ this.updateDetune(channel, note, scheduleTime);
1969
+ }
1970
+ }
1971
+ });
1972
+ }
1973
+ setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
1920
1974
  const channel = this.channels[channelNumber];
1975
+ scheduleTime ??= this.audioContext.currentTime;
1921
1976
  channel.state.portamentoTime = portamentoTime / 127;
1977
+ if (channel.isDrum)
1978
+ return;
1979
+ this.updatePortamento(channel, scheduleTime);
1922
1980
  }
1923
1981
  setKeyBasedVolume(channel, scheduleTime) {
1924
1982
  this.processScheduledNotes(channel, (note) => {
@@ -1976,7 +2034,7 @@ export class Midy {
1976
2034
  }
1977
2035
  dataEntryLSB(channelNumber, value, scheduleTime) {
1978
2036
  this.channels[channelNumber].dataLSB = value;
1979
- this.handleRPN(channelNumber, scheduleTime);
2037
+ this.handleRPN(channelNumber, 0, scheduleTime);
1980
2038
  }
1981
2039
  updateChannelVolume(channel, scheduleTime) {
1982
2040
  const state = channel.state;
@@ -2004,11 +2062,13 @@ export class Midy {
2004
2062
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
2005
2063
  }
2006
2064
  }
2007
- setPortamento(channelNumber, value) {
2065
+ setPortamento(channelNumber, value, scheduleTime) {
2008
2066
  const channel = this.channels[channelNumber];
2009
2067
  if (channel.isDrum)
2010
2068
  return;
2069
+ scheduleTime ??= this.audioContext.currentTime;
2011
2070
  channel.state.portamento = value / 127;
2071
+ this.updatePortamento(channel, scheduleTime);
2012
2072
  }
2013
2073
  setSostenutoPedal(channelNumber, value, scheduleTime) {
2014
2074
  const channel = this.channels[channelNumber];
@@ -2017,7 +2077,11 @@ export class Midy {
2017
2077
  scheduleTime ??= this.audioContext.currentTime;
2018
2078
  channel.state.sostenutoPedal = value / 127;
2019
2079
  if (64 <= value) {
2020
- channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
2080
+ const sostenutoNotes = [];
2081
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2082
+ sostenutoNotes.push(note);
2083
+ });
2084
+ channel.sostenutoNotes = sostenutoNotes;
2021
2085
  }
2022
2086
  else {
2023
2087
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
@@ -2031,9 +2095,9 @@ export class Midy {
2031
2095
  scheduleTime ??= this.audioContext.currentTime;
2032
2096
  state.softPedal = softPedal / 127;
2033
2097
  this.processScheduledNotes(channel, (note) => {
2034
- if (0.5 <= state.portamento && note.portamento) {
2035
- this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
2036
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2098
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2099
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2100
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2037
2101
  }
2038
2102
  else {
2039
2103
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2047,7 +2111,7 @@ export class Midy {
2047
2111
  return;
2048
2112
  scheduleTime ??= this.audioContext.currentTime;
2049
2113
  const state = channel.state;
2050
- state.filterResonance = filterResonance / 64;
2114
+ state.filterResonance = filterResonance / 127;
2051
2115
  this.processScheduledNotes(channel, (note) => {
2052
2116
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2053
2117
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
@@ -2058,14 +2122,14 @@ export class Midy {
2058
2122
  if (channel.isDrum)
2059
2123
  return;
2060
2124
  scheduleTime ??= this.audioContext.currentTime;
2061
- channel.state.releaseTime = releaseTime / 64;
2125
+ channel.state.releaseTime = releaseTime / 127;
2062
2126
  }
2063
2127
  setAttackTime(channelNumber, attackTime, scheduleTime) {
2064
2128
  const channel = this.channels[channelNumber];
2065
2129
  if (channel.isDrum)
2066
2130
  return;
2067
2131
  scheduleTime ??= this.audioContext.currentTime;
2068
- channel.state.attackTime = attackTime / 64;
2132
+ channel.state.attackTime = attackTime / 127;
2069
2133
  this.processScheduledNotes(channel, (note) => {
2070
2134
  if (note.startTime < scheduleTime)
2071
2135
  return false;
@@ -2078,10 +2142,10 @@ export class Midy {
2078
2142
  return;
2079
2143
  const state = channel.state;
2080
2144
  scheduleTime ??= this.audioContext.currentTime;
2081
- state.brightness = brightness / 64;
2145
+ state.brightness = brightness / 127;
2082
2146
  this.processScheduledNotes(channel, (note) => {
2083
- if (0.5 <= state.portamento && note.portamento) {
2084
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2147
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2148
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2085
2149
  }
2086
2150
  else {
2087
2151
  this.setFilterEnvelope(channel, note);
@@ -2093,7 +2157,7 @@ export class Midy {
2093
2157
  if (channel.isDrum)
2094
2158
  return;
2095
2159
  scheduleTime ??= this.audioContext.currentTime;
2096
- channel.state.decayTime = dacayTime / 64;
2160
+ channel.state.decayTime = dacayTime / 127;
2097
2161
  this.processScheduledNotes(channel, (note) => {
2098
2162
  this.setVolumeEnvelope(channel, note, scheduleTime);
2099
2163
  });
@@ -2103,7 +2167,7 @@ export class Midy {
2103
2167
  if (channel.isDrum)
2104
2168
  return;
2105
2169
  scheduleTime ??= this.audioContext.currentTime;
2106
- channel.state.vibratoRate = vibratoRate / 64;
2170
+ channel.state.vibratoRate = vibratoRate / 127;
2107
2171
  if (channel.vibratoDepth <= 0)
2108
2172
  return;
2109
2173
  this.processScheduledNotes(channel, (note) => {
@@ -2116,7 +2180,7 @@ export class Midy {
2116
2180
  return;
2117
2181
  scheduleTime ??= this.audioContext.currentTime;
2118
2182
  const prev = channel.state.vibratoDepth;
2119
- channel.state.vibratoDepth = vibratoDepth / 64;
2183
+ channel.state.vibratoDepth = vibratoDepth / 127;
2120
2184
  if (0 < prev) {
2121
2185
  this.processScheduledNotes(channel, (note) => {
2122
2186
  this.setFreqVibLFO(channel, note, scheduleTime);
@@ -2128,12 +2192,12 @@ export class Midy {
2128
2192
  });
2129
2193
  }
2130
2194
  }
2131
- setVibratoDelay(channelNumber, vibratoDelay) {
2195
+ setVibratoDelay(channelNumber, vibratoDelay, scheduleTime) {
2132
2196
  const channel = this.channels[channelNumber];
2133
2197
  if (channel.isDrum)
2134
2198
  return;
2135
2199
  scheduleTime ??= this.audioContext.currentTime;
2136
- channel.state.vibratoDelay = vibratoDelay / 64;
2200
+ channel.state.vibratoDelay = vibratoDelay / 127;
2137
2201
  if (0 < channel.state.vibratoDepth) {
2138
2202
  this.processScheduledNotes(channel, (note) => {
2139
2203
  this.startVibrato(channel, note, scheduleTime);
@@ -2255,12 +2319,14 @@ export class Midy {
2255
2319
  }
2256
2320
  }
2257
2321
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2258
- dataIncrement(channelNumber) {
2259
- this.handleRPN(channelNumber, 1);
2322
+ dataIncrement(channelNumber, scheduleTime) {
2323
+ scheduleTime ??= this.audioContext.currentTime;
2324
+ this.handleRPN(channelNumber, 1, scheduleTime);
2260
2325
  }
2261
2326
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2262
- dataDecrement(channelNumber) {
2263
- this.handleRPN(channelNumber, -1);
2327
+ dataDecrement(channelNumber, scheduleTime) {
2328
+ scheduleTime ??= this.audioContext.currentTime;
2329
+ this.handleRPN(channelNumber, -1, scheduleTime);
2264
2330
  }
2265
2331
  setRPNMSB(channelNumber, value) {
2266
2332
  this.channels[channelNumber].rpnMSB = value;
@@ -2270,7 +2336,7 @@ export class Midy {
2270
2336
  }
2271
2337
  dataEntryMSB(channelNumber, value, scheduleTime) {
2272
2338
  this.channels[channelNumber].dataMSB = value;
2273
- this.handleRPN(channelNumber, scheduleTime);
2339
+ this.handleRPN(channelNumber, 0, scheduleTime);
2274
2340
  }
2275
2341
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2276
2342
  const channel = this.channels[channelNumber];
@@ -2344,21 +2410,29 @@ export class Midy {
2344
2410
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2345
2411
  }
2346
2412
  resetAllStates(channelNumber) {
2413
+ const scheduleTime = this.audioContext.currentTime;
2347
2414
  const channel = this.channels[channelNumber];
2348
2415
  const state = channel.state;
2349
- for (const type of Object.keys(defaultControllerState)) {
2350
- state[type] = defaultControllerState[type].defaultValue;
2416
+ const entries = Object.entries(defaultControllerState);
2417
+ for (const [key, { type, defaultValue }] of entries) {
2418
+ if (128 <= type) {
2419
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2420
+ }
2421
+ else {
2422
+ state[key] = defaultValue;
2423
+ }
2351
2424
  }
2352
- for (const type of Object.keys(this.constructor.channelSettings)) {
2353
- channel[type] = this.constructor.channelSettings[type];
2425
+ for (const key of Object.keys(this.constructor.channelSettings)) {
2426
+ channel[key] = this.constructor.channelSettings[key];
2354
2427
  }
2428
+ this.resetChannelTable(channel);
2355
2429
  this.mode = "GM2";
2356
2430
  this.masterFineTuning = 0; // cb
2357
2431
  this.masterCoarseTuning = 0; // cb
2358
2432
  }
2359
2433
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2360
- resetAllControllers(channelNumber) {
2361
- const stateTypes = [
2434
+ resetAllControllers(channelNumber, _value, scheduleTime) {
2435
+ const keys = [
2362
2436
  "polyphonicKeyPressure",
2363
2437
  "channelPressure",
2364
2438
  "pitchWheel",
@@ -2371,10 +2445,17 @@ export class Midy {
2371
2445
  ];
2372
2446
  const channel = this.channels[channelNumber];
2373
2447
  const state = channel.state;
2374
- for (let i = 0; i < stateTypes.length; i++) {
2375
- const type = stateTypes[i];
2376
- state[type] = defaultControllerState[type].defaultValue;
2448
+ for (let i = 0; i < keys.length; i++) {
2449
+ const key = keys[i];
2450
+ const { type, defaultValue } = defaultControllerState[key];
2451
+ if (128 <= type) {
2452
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2453
+ }
2454
+ else {
2455
+ state[key] = defaultValue;
2456
+ }
2377
2457
  }
2458
+ this.setPitchBend(channelNumber, 8192, scheduleTime);
2378
2459
  const settingTypes = [
2379
2460
  "rpnMSB",
2380
2461
  "rpnLSB",
@@ -2619,7 +2700,7 @@ export class Midy {
2619
2700
  this.reverbEffect = options.reverbAlgorithm(audioContext);
2620
2701
  }
2621
2702
  getReverbTime(value) {
2622
- return Math.pow(Math.E, (value - 40) * 0.025);
2703
+ return Math.exp((value - 40) * 0.025);
2623
2704
  }
2624
2705
  // mean free path equation
2625
2706
  // https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
@@ -2853,7 +2934,13 @@ export class Midy {
2853
2934
  setControllerParameters(channel, note, table) {
2854
2935
  if (table[0] !== 64)
2855
2936
  this.updateDetune(channel, note);
2856
- if (!note.portamento) {
2937
+ if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2938
+ if (table[1] !== 64)
2939
+ this.setPortamentoFilterEnvelope(channel, note);
2940
+ if (table[2] !== 64)
2941
+ this.setPortamentoVolumeEnvelope(channel, note);
2942
+ }
2943
+ else {
2857
2944
  if (table[1] !== 64)
2858
2945
  this.setFilterEnvelope(channel, note);
2859
2946
  if (table[2] !== 64)
@@ -2881,8 +2968,13 @@ export class Midy {
2881
2968
  initControlTable() {
2882
2969
  const channelCount = 128;
2883
2970
  const slotSize = 6;
2884
- const defaultValues = [64, 64, 64, 0, 0, 0];
2885
2971
  const table = new Uint8Array(channelCount * slotSize);
2972
+ return this.resetControlTable(table);
2973
+ }
2974
+ resetControlTable(table) {
2975
+ const channelCount = 128;
2976
+ const slotSize = 6;
2977
+ const defaultValues = [64, 64, 64, 0, 0, 0];
2886
2978
  for (let ch = 0; ch < channelCount; ch++) {
2887
2979
  const offset = ch * slotSize;
2888
2980
  table.set(defaultValues, offset);