@marmooo/midy 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 (event.velocity !== 0) {
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,17 +787,52 @@ 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
  }
817
+ stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
818
+ const channel = this.channels[channelNumber];
819
+ const promises = [];
820
+ this.processActiveNotes(channel, scheduleTime, (note) => {
821
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
822
+ this.notePromises.push(promise);
823
+ promises.push(promise);
824
+ });
825
+ return Promise.all(promises);
826
+ }
845
827
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
846
828
  const channel = this.channels[channelNumber];
847
829
  const promises = [];
848
830
  this.processScheduledNotes(channel, (note) => {
849
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
831
+ const promise = this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, force);
850
832
  this.notePromises.push(promise);
851
833
  promises.push(promise);
852
834
  });
853
- channel.scheduledNotes.clear();
835
+ channel.scheduledNotes = [];
854
836
  return Promise.all(promises);
855
837
  }
856
838
  stopNotes(velocity, force, scheduleTime) {
@@ -871,9 +853,6 @@ export class Midy {
871
853
  if (!this.isPlaying)
872
854
  return;
873
855
  this.isStopping = true;
874
- for (let i = 0; i < this.channels.length; i++) {
875
- this.resetAllStates(i);
876
- }
877
856
  }
878
857
  pause() {
879
858
  if (!this.isPlaying || this.isPaused)
@@ -908,35 +887,31 @@ export class Midy {
908
887
  return this.resumeTime + now - this.startTime - this.startDelay;
909
888
  }
910
889
  processScheduledNotes(channel, callback) {
911
- channel.scheduledNotes.forEach((noteList) => {
912
- for (let i = 0; i < noteList.length; i++) {
913
- const note = noteList[i];
914
- if (!note)
915
- continue;
916
- callback(note);
917
- }
918
- });
919
- }
920
- getActiveNotes(channel, scheduleTime) {
921
- const activeNotes = new SparseMap(128);
922
- channel.scheduledNotes.forEach((noteList) => {
923
- const activeNote = this.getActiveNote(noteList, scheduleTime);
924
- if (activeNote) {
925
- activeNotes.set(activeNote.noteNumber, activeNote);
926
- }
927
- });
928
- 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
+ }
929
899
  }
930
- getActiveNote(noteList, scheduleTime) {
931
- for (let i = noteList.length - 1; i >= 0; i--) {
932
- 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];
933
904
  if (!note)
934
- return;
905
+ continue;
906
+ if (note.ending)
907
+ continue;
908
+ const noteOffEvent = note.noteOffEvent;
909
+ if (noteOffEvent && noteOffEvent.startTime < scheduleTime)
910
+ continue;
935
911
  if (scheduleTime < note.startTime)
936
912
  continue;
937
- return (note.ending) ? null : note;
913
+ callback(note);
938
914
  }
939
- return noteList[0];
940
915
  }
941
916
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
942
917
  const sampleRate = audioContext.sampleRate;
@@ -1103,24 +1078,94 @@ export class Midy {
1103
1078
  const noteDetune = this.calcNoteDetune(channel, note);
1104
1079
  const pitchControl = this.getPitchControl(channel, note);
1105
1080
  const detune = channel.detune + noteDetune + pitchControl;
1106
- note.bufferSource.detune
1107
- .cancelScheduledValues(scheduleTime)
1108
- .setValueAtTime(detune, scheduleTime);
1109
- }
1110
- getPortamentoTime(channel) {
1111
- const factor = 5 * Math.log(10) * 127;
1112
- return channel.state.portamentoTime * factor;
1113
- }
1114
- 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;
1115
1159
  const { voiceParams, startTime } = note;
1116
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1160
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1161
+ (1 + this.getAmplitudeControl(channel, note));
1117
1162
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1118
1163
  const volDelay = startTime + voiceParams.volDelay;
1119
- const portamentoTime = volDelay + this.getPortamentoTime(channel);
1164
+ const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1165
+ const volHold = volAttack + voiceParams.volHold;
1120
1166
  note.volumeEnvelopeNode.gain
1121
1167
  .cancelScheduledValues(scheduleTime)
1122
- .setValueAtTime(0, volDelay)
1123
- .linearRampToValueAtTime(sustainVolume, portamentoTime);
1168
+ .setValueAtTime(sustainVolume, volHold);
1124
1169
  }
1125
1170
  setVolumeEnvelope(channel, note, scheduleTime) {
1126
1171
  const state = channel.state;
@@ -1140,6 +1185,12 @@ export class Midy {
1140
1185
  .setValueAtTime(attackVolume, volHold)
1141
1186
  .linearRampToValueAtTime(sustainVolume, volDecay);
1142
1187
  }
1188
+ setPortamentoPitchEnvelope(note, scheduleTime) {
1189
+ const baseRate = note.voiceParams.playbackRate;
1190
+ note.bufferSource.playbackRate
1191
+ .cancelScheduledValues(scheduleTime)
1192
+ .setValueAtTime(baseRate, scheduleTime);
1193
+ }
1143
1194
  setPitchEnvelope(note, scheduleTime) {
1144
1195
  const { voiceParams } = note;
1145
1196
  const baseRate = voiceParams.playbackRate;
@@ -1167,20 +1218,21 @@ export class Midy {
1167
1218
  const maxFrequency = 20000; // max Hz of initialFilterFc
1168
1219
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1169
1220
  }
1170
- setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1221
+ setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1171
1222
  const state = channel.state;
1172
1223
  const { voiceParams, noteNumber, startTime } = note;
1173
1224
  const softPedalFactor = 1 -
1174
1225
  (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1175
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1176
- softPedalFactor *
1226
+ const baseCent = voiceParams.initialFilterFc +
1227
+ this.getFilterCutoffControl(channel, note);
1228
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1177
1229
  state.brightness * 2;
1178
1230
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1179
1231
  const sustainFreq = baseFreq +
1180
1232
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1181
1233
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1182
1234
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1183
- const portamentoTime = startTime + this.getPortamentoTime(channel);
1235
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1184
1236
  const modDelay = startTime + voiceParams.modDelay;
1185
1237
  note.filterNode.frequency
1186
1238
  .cancelScheduledValues(scheduleTime)
@@ -1266,7 +1318,7 @@ export class Midy {
1266
1318
  return audioBuffer;
1267
1319
  }
1268
1320
  }
1269
- async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1321
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1270
1322
  const now = this.audioContext.currentTime;
1271
1323
  const state = channel.state;
1272
1324
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
@@ -1282,20 +1334,24 @@ export class Midy {
1282
1334
  type: "lowpass",
1283
1335
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1284
1336
  });
1285
- if (portamento) {
1286
- note.portamento = true;
1287
- this.setPortamentoStartVolumeEnvelope(channel, note, now);
1288
- 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);
1289
1345
  }
1290
1346
  else {
1291
- note.portamento = false;
1292
1347
  this.setVolumeEnvelope(channel, note, now);
1293
1348
  this.setFilterEnvelope(channel, note, now);
1349
+ this.setPitchEnvelope(note, now);
1294
1350
  }
1351
+ this.updateDetune(channel, note, now);
1295
1352
  if (0 < state.vibratoDepth) {
1296
1353
  this.startVibrato(channel, note, now);
1297
1354
  }
1298
- this.setPitchEnvelope(note, now);
1299
1355
  if (0 < state.modulationDepth) {
1300
1356
  this.startModulation(channel, note, now);
1301
1357
  }
@@ -1341,9 +1397,8 @@ export class Midy {
1341
1397
  if (prev) {
1342
1398
  const [prevNote, prevChannelNumber] = prev;
1343
1399
  if (prevNote && !prevNote.ending) {
1344
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1345
- startTime, true, // force
1346
- undefined);
1400
+ this.scheduleNoteOff(prevChannelNumber, prevNote, 0, // velocity,
1401
+ startTime, true);
1347
1402
  }
1348
1403
  }
1349
1404
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
@@ -1362,9 +1417,8 @@ export class Midy {
1362
1417
  channelNumber;
1363
1418
  const prevNote = this.drumExclusiveClassNotes[index];
1364
1419
  if (prevNote && !prevNote.ending) {
1365
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1366
- startTime, true, // force
1367
- undefined);
1420
+ this.scheduleNoteOff(channelNumber, prevNote, 0, // velocity,
1421
+ startTime, true);
1368
1422
  }
1369
1423
  this.drumExclusiveClassNotes[index] = note;
1370
1424
  }
@@ -1372,10 +1426,10 @@ export class Midy {
1372
1426
  if (!channel.isDrum)
1373
1427
  return false;
1374
1428
  const programNumber = channel.programNumber;
1375
- return (programNumber === 48 && noteNumber === 88) ||
1376
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84);
1429
+ return !((programNumber === 48 && noteNumber === 88) ||
1430
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1377
1431
  }
1378
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1432
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1379
1433
  const channel = this.channels[channelNumber];
1380
1434
  const bankNumber = this.calcBank(channel, channelNumber);
1381
1435
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1386,7 +1440,8 @@ export class Midy {
1386
1440
  if (!voice)
1387
1441
  return;
1388
1442
  const isSF3 = soundFont.parsed.info.version.major === 3;
1389
- 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;
1390
1445
  note.gainL.connect(channel.gainL);
1391
1446
  note.gainR.connect(channel.gainR);
1392
1447
  if (0.5 <= channel.state.sustainPedal) {
@@ -1395,33 +1450,39 @@ export class Midy {
1395
1450
  this.handleExclusiveClass(note, channelNumber, startTime);
1396
1451
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1397
1452
  const scheduledNotes = channel.scheduledNotes;
1398
- let notes = scheduledNotes.get(noteNumber);
1399
- if (notes) {
1400
- notes.push(note);
1401
- }
1402
- else {
1403
- notes = [note];
1404
- scheduledNotes.set(noteNumber, notes);
1405
- }
1453
+ note.index = scheduledNotes.length;
1454
+ scheduledNotes.push(note);
1406
1455
  if (this.isDrumNoteOffException(channel, noteNumber)) {
1407
1456
  const stopTime = startTime + note.bufferSource.buffer.duration;
1408
- const index = notes.length - 1;
1409
1457
  const promise = new Promise((resolve) => {
1410
1458
  note.bufferSource.onended = () => {
1411
- this.disconnectNote(note, scheduledNotes, index);
1459
+ scheduledNotes[note.index] = undefined;
1460
+ this.disconnectNote(note);
1412
1461
  resolve();
1413
1462
  };
1414
1463
  note.bufferSource.stop(stopTime);
1415
1464
  });
1416
1465
  this.notePromises.push(promise);
1417
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
+ }
1418
1480
  }
1419
1481
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1420
1482
  scheduleTime ??= this.audioContext.currentTime;
1421
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1483
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1422
1484
  }
1423
- disconnectNote(note, scheduledNotes, index) {
1424
- scheduledNotes[index] = null;
1485
+ disconnectNote(note) {
1425
1486
  note.bufferSource.disconnect();
1426
1487
  note.filterNode.disconnect();
1427
1488
  note.volumeEnvelopeNode.disconnect();
@@ -1444,8 +1505,7 @@ export class Midy {
1444
1505
  note.chorusEffectsSend.disconnect();
1445
1506
  }
1446
1507
  }
1447
- stopNote(endTime, stopTime, scheduledNotes, index) {
1448
- const note = scheduledNotes[index];
1508
+ stopNote(channel, note, endTime, stopTime) {
1449
1509
  note.volumeEnvelopeNode.gain
1450
1510
  .cancelScheduledValues(endTime)
1451
1511
  .linearRampToValueAtTime(0, stopTime);
@@ -1455,65 +1515,58 @@ export class Midy {
1455
1515
  }, stopTime);
1456
1516
  return new Promise((resolve) => {
1457
1517
  note.bufferSource.onended = () => {
1458
- this.disconnectNote(note, scheduledNotes, index);
1518
+ channel.scheduledNotes[note.index] = undefined;
1519
+ this.disconnectNote(note);
1459
1520
  resolve();
1460
1521
  };
1461
1522
  note.bufferSource.stop(stopTime);
1462
1523
  });
1463
1524
  }
1464
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1525
+ scheduleNoteOff(channelNumber, note, _velocity, endTime, force) {
1465
1526
  const channel = this.channels[channelNumber];
1466
- if (this.isDrumNoteOffException(channel, noteNumber))
1527
+ if (this.isDrumNoteOffException(channel, note.noteNumber))
1467
1528
  return;
1468
1529
  const state = channel.state;
1469
1530
  if (!force) {
1470
1531
  if (0.5 <= state.sustainPedal)
1471
1532
  return;
1472
- if (channel.sostenutoNotes.has(noteNumber))
1533
+ if (0.5 <= channel.state.sostenutoPedal)
1473
1534
  return;
1474
1535
  }
1475
- if (!channel.scheduledNotes.has(noteNumber))
1476
- return;
1477
- const scheduledNotes = channel.scheduledNotes.get(noteNumber);
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;
1478
1547
  for (let i = 0; i < scheduledNotes.length; i++) {
1479
1548
  const note = scheduledNotes[i];
1480
1549
  if (!note)
1481
1550
  continue;
1482
1551
  if (note.ending)
1483
1552
  continue;
1484
- if (portamentoNoteNumber === undefined) {
1485
- const volRelease = endTime +
1486
- note.voiceParams.volRelease * state.releaseTime * 2;
1487
- const modRelease = endTime + note.voiceParams.modRelease;
1488
- note.filterNode.frequency
1489
- .cancelScheduledValues(endTime)
1490
- .linearRampToValueAtTime(0, modRelease);
1491
- const stopTime = Math.min(volRelease, modRelease);
1492
- return this.stopNote(endTime, stopTime, scheduledNotes, i);
1493
- }
1494
- else {
1495
- const portamentoTime = endTime + this.getPortamentoTime(channel);
1496
- const deltaNote = portamentoNoteNumber - noteNumber;
1497
- const baseRate = note.voiceParams.playbackRate;
1498
- const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1499
- note.bufferSource.playbackRate
1500
- .cancelScheduledValues(endTime)
1501
- .linearRampToValueAtTime(targetRate, portamentoTime);
1502
- return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1503
- }
1553
+ if (note.noteNumber !== noteNumber)
1554
+ continue;
1555
+ return note;
1504
1556
  }
1505
1557
  }
1506
1558
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1507
1559
  scheduleTime ??= this.audioContext.currentTime;
1508
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1509
- undefined);
1560
+ const channel = this.channels[channelNumber];
1561
+ const note = this.findNoteOffTarget(channel, noteNumber);
1562
+ return this.scheduleNoteOff(channelNumber, note, velocity, scheduleTime, false);
1510
1563
  }
1511
1564
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1512
1565
  const velocity = halfVelocity * 2;
1513
1566
  const channel = this.channels[channelNumber];
1514
1567
  const promises = [];
1515
1568
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1516
- const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1569
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i], velocity, scheduleTime);
1517
1570
  promises.push(promise);
1518
1571
  }
1519
1572
  channel.sustainNotes = [];
@@ -1523,12 +1576,14 @@ export class Midy {
1523
1576
  const velocity = halfVelocity * 2;
1524
1577
  const channel = this.channels[channelNumber];
1525
1578
  const promises = [];
1579
+ const sostenutoNotes = channel.sostenutoNotes;
1526
1580
  channel.state.sostenutoPedal = 0;
1527
- channel.sostenutoNotes.forEach((note) => {
1528
- 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);
1529
1584
  promises.push(promise);
1530
- });
1531
- channel.sostenutoNotes.clear();
1585
+ }
1586
+ channel.sostenutoNotes = [];
1532
1587
  return promises;
1533
1588
  }
1534
1589
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
@@ -1557,12 +1612,12 @@ export class Midy {
1557
1612
  const channel = this.channels[channelNumber];
1558
1613
  channel.state.polyphonicKeyPressure = pressure / 127;
1559
1614
  const table = channel.polyphonicKeyPressureTable;
1560
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
1561
- if (activeNotes.has(noteNumber)) {
1562
- const note = activeNotes.get(noteNumber);
1563
- this.setControllerParameters(channel, note, table);
1564
- }
1565
- // this.applyVoiceParams(channel, 10);
1615
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1616
+ if (note.noteNumber === noteNumber) {
1617
+ this.setControllerParameters(channel, note, table);
1618
+ }
1619
+ });
1620
+ this.applyVoiceParams(channel, 10);
1566
1621
  }
1567
1622
  handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1568
1623
  const channel = this.channels[channelNumber];
@@ -1591,10 +1646,10 @@ export class Midy {
1591
1646
  channel.detune += pressureDepth * (next - prev);
1592
1647
  }
1593
1648
  const table = channel.channelPressureTable;
1594
- this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1649
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1595
1650
  this.setControllerParameters(channel, note, table);
1596
1651
  });
1597
- // this.applyVoiceParams(channel, 13);
1652
+ this.applyVoiceParams(channel, 13);
1598
1653
  }
1599
1654
  handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1600
1655
  const pitchBend = msb * 128 + lsb;
@@ -1771,6 +1826,8 @@ export class Midy {
1771
1826
  state.set(channel.state.array);
1772
1827
  state[2] = velocity / 127;
1773
1828
  state[3] = noteNumber / 127;
1829
+ state[10] = state.polyphonicKeyPressure / 127;
1830
+ state[13] = state.channelPressure / 127;
1774
1831
  return state;
1775
1832
  }
1776
1833
  applyVoiceParams(channel, controllerType, scheduleTime) {
@@ -1797,8 +1854,8 @@ export class Midy {
1797
1854
  if (key in voiceParams)
1798
1855
  noteVoiceParams[key] = voiceParams[key];
1799
1856
  }
1800
- if (note.portamento) {
1801
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1857
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1858
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1802
1859
  }
1803
1860
  else {
1804
1861
  this.setFilterEnvelope(channel, note, scheduleTime);
@@ -1821,42 +1878,42 @@ export class Midy {
1821
1878
  });
1822
1879
  }
1823
1880
  createControlChangeHandlers() {
1824
- return {
1825
- 0: this.setBankMSB,
1826
- 1: this.setModulationDepth,
1827
- 5: this.setPortamentoTime,
1828
- 6: this.dataEntryMSB,
1829
- 7: this.setVolume,
1830
- 10: this.setPan,
1831
- 11: this.setExpression,
1832
- 32: this.setBankLSB,
1833
- 38: this.dataEntryLSB,
1834
- 64: this.setSustainPedal,
1835
- 65: this.setPortamento,
1836
- 66: this.setSostenutoPedal,
1837
- 67: this.setSoftPedal,
1838
- 71: this.setFilterResonance,
1839
- 72: this.setReleaseTime,
1840
- 73: this.setAttackTime,
1841
- 74: this.setBrightness,
1842
- 75: this.setDecayTime,
1843
- 76: this.setVibratoRate,
1844
- 77: this.setVibratoDepth,
1845
- 78: this.setVibratoDelay,
1846
- 91: this.setReverbSendLevel,
1847
- 93: this.setChorusSendLevel,
1848
- 96: this.dataIncrement,
1849
- 97: this.dataDecrement,
1850
- 100: this.setRPNLSB,
1851
- 101: this.setRPNMSB,
1852
- 120: this.allSoundOff,
1853
- 121: this.resetAllControllers,
1854
- 123: this.allNotesOff,
1855
- 124: this.omniOff,
1856
- 125: this.omniOn,
1857
- 126: this.monoOn,
1858
- 127: this.polyOn,
1859
- };
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;
1860
1917
  }
1861
1918
  handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1862
1919
  const handler = this.controlChangeHandlers[controllerType];
@@ -1893,9 +1950,33 @@ export class Midy {
1893
1950
  channel.state.modulationDepth = modulation / 127;
1894
1951
  this.updateModulation(channel, scheduleTime);
1895
1952
  }
1896
- 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) {
1897
1974
  const channel = this.channels[channelNumber];
1975
+ scheduleTime ??= this.audioContext.currentTime;
1898
1976
  channel.state.portamentoTime = portamentoTime / 127;
1977
+ if (channel.isDrum)
1978
+ return;
1979
+ this.updatePortamento(channel, scheduleTime);
1899
1980
  }
1900
1981
  setKeyBasedVolume(channel, scheduleTime) {
1901
1982
  this.processScheduledNotes(channel, (note) => {
@@ -1953,7 +2034,7 @@ export class Midy {
1953
2034
  }
1954
2035
  dataEntryLSB(channelNumber, value, scheduleTime) {
1955
2036
  this.channels[channelNumber].dataLSB = value;
1956
- this.handleRPN(channelNumber, scheduleTime);
2037
+ this.handleRPN(channelNumber, 0, scheduleTime);
1957
2038
  }
1958
2039
  updateChannelVolume(channel, scheduleTime) {
1959
2040
  const state = channel.state;
@@ -1981,11 +2062,13 @@ export class Midy {
1981
2062
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
1982
2063
  }
1983
2064
  }
1984
- setPortamento(channelNumber, value) {
2065
+ setPortamento(channelNumber, value, scheduleTime) {
1985
2066
  const channel = this.channels[channelNumber];
1986
2067
  if (channel.isDrum)
1987
2068
  return;
2069
+ scheduleTime ??= this.audioContext.currentTime;
1988
2070
  channel.state.portamento = value / 127;
2071
+ this.updatePortamento(channel, scheduleTime);
1989
2072
  }
1990
2073
  setSostenutoPedal(channelNumber, value, scheduleTime) {
1991
2074
  const channel = this.channels[channelNumber];
@@ -1994,7 +2077,11 @@ export class Midy {
1994
2077
  scheduleTime ??= this.audioContext.currentTime;
1995
2078
  channel.state.sostenutoPedal = value / 127;
1996
2079
  if (64 <= value) {
1997
- channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
2080
+ const sostenutoNotes = [];
2081
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2082
+ sostenutoNotes.push(note);
2083
+ });
2084
+ channel.sostenutoNotes = sostenutoNotes;
1998
2085
  }
1999
2086
  else {
2000
2087
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
@@ -2004,12 +2091,13 @@ export class Midy {
2004
2091
  const channel = this.channels[channelNumber];
2005
2092
  if (channel.isDrum)
2006
2093
  return;
2094
+ const state = channel.state;
2007
2095
  scheduleTime ??= this.audioContext.currentTime;
2008
- channel.state.softPedal = softPedal / 127;
2096
+ state.softPedal = softPedal / 127;
2009
2097
  this.processScheduledNotes(channel, (note) => {
2010
- if (note.portamento) {
2011
- this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
2012
- 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);
2013
2101
  }
2014
2102
  else {
2015
2103
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2023,25 +2111,25 @@ export class Midy {
2023
2111
  return;
2024
2112
  scheduleTime ??= this.audioContext.currentTime;
2025
2113
  const state = channel.state;
2026
- state.filterResonance = filterResonance / 64;
2114
+ state.filterResonance = filterResonance / 127;
2027
2115
  this.processScheduledNotes(channel, (note) => {
2028
2116
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2029
2117
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2030
2118
  });
2031
2119
  }
2032
- setReleaseTime(channelNumber, releaseTime, _scheduleTime) {
2120
+ setReleaseTime(channelNumber, releaseTime, scheduleTime) {
2033
2121
  const channel = this.channels[channelNumber];
2034
2122
  if (channel.isDrum)
2035
2123
  return;
2036
2124
  scheduleTime ??= this.audioContext.currentTime;
2037
- channel.state.releaseTime = releaseTime / 64;
2125
+ channel.state.releaseTime = releaseTime / 127;
2038
2126
  }
2039
2127
  setAttackTime(channelNumber, attackTime, scheduleTime) {
2040
2128
  const channel = this.channels[channelNumber];
2041
2129
  if (channel.isDrum)
2042
2130
  return;
2043
2131
  scheduleTime ??= this.audioContext.currentTime;
2044
- channel.state.attackTime = attackTime / 64;
2132
+ channel.state.attackTime = attackTime / 127;
2045
2133
  this.processScheduledNotes(channel, (note) => {
2046
2134
  if (note.startTime < scheduleTime)
2047
2135
  return false;
@@ -2052,11 +2140,12 @@ export class Midy {
2052
2140
  const channel = this.channels[channelNumber];
2053
2141
  if (channel.isDrum)
2054
2142
  return;
2143
+ const state = channel.state;
2055
2144
  scheduleTime ??= this.audioContext.currentTime;
2056
- channel.state.brightness = brightness / 64;
2145
+ state.brightness = brightness / 127;
2057
2146
  this.processScheduledNotes(channel, (note) => {
2058
- if (note.portamento) {
2059
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2147
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2148
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2060
2149
  }
2061
2150
  else {
2062
2151
  this.setFilterEnvelope(channel, note);
@@ -2068,7 +2157,7 @@ export class Midy {
2068
2157
  if (channel.isDrum)
2069
2158
  return;
2070
2159
  scheduleTime ??= this.audioContext.currentTime;
2071
- channel.state.decayTime = dacayTime / 64;
2160
+ channel.state.decayTime = dacayTime / 127;
2072
2161
  this.processScheduledNotes(channel, (note) => {
2073
2162
  this.setVolumeEnvelope(channel, note, scheduleTime);
2074
2163
  });
@@ -2078,7 +2167,7 @@ export class Midy {
2078
2167
  if (channel.isDrum)
2079
2168
  return;
2080
2169
  scheduleTime ??= this.audioContext.currentTime;
2081
- channel.state.vibratoRate = vibratoRate / 64;
2170
+ channel.state.vibratoRate = vibratoRate / 127;
2082
2171
  if (channel.vibratoDepth <= 0)
2083
2172
  return;
2084
2173
  this.processScheduledNotes(channel, (note) => {
@@ -2091,7 +2180,7 @@ export class Midy {
2091
2180
  return;
2092
2181
  scheduleTime ??= this.audioContext.currentTime;
2093
2182
  const prev = channel.state.vibratoDepth;
2094
- channel.state.vibratoDepth = vibratoDepth / 64;
2183
+ channel.state.vibratoDepth = vibratoDepth / 127;
2095
2184
  if (0 < prev) {
2096
2185
  this.processScheduledNotes(channel, (note) => {
2097
2186
  this.setFreqVibLFO(channel, note, scheduleTime);
@@ -2103,12 +2192,12 @@ export class Midy {
2103
2192
  });
2104
2193
  }
2105
2194
  }
2106
- setVibratoDelay(channelNumber, vibratoDelay) {
2195
+ setVibratoDelay(channelNumber, vibratoDelay, scheduleTime) {
2107
2196
  const channel = this.channels[channelNumber];
2108
2197
  if (channel.isDrum)
2109
2198
  return;
2110
2199
  scheduleTime ??= this.audioContext.currentTime;
2111
- channel.state.vibratoDelay = vibratoDelay / 64;
2200
+ channel.state.vibratoDelay = vibratoDelay / 127;
2112
2201
  if (0 < channel.state.vibratoDepth) {
2113
2202
  this.processScheduledNotes(channel, (note) => {
2114
2203
  this.startVibrato(channel, note, scheduleTime);
@@ -2230,12 +2319,14 @@ export class Midy {
2230
2319
  }
2231
2320
  }
2232
2321
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2233
- dataIncrement(channelNumber) {
2234
- this.handleRPN(channelNumber, 1);
2322
+ dataIncrement(channelNumber, scheduleTime) {
2323
+ scheduleTime ??= this.audioContext.currentTime;
2324
+ this.handleRPN(channelNumber, 1, scheduleTime);
2235
2325
  }
2236
2326
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2237
- dataDecrement(channelNumber) {
2238
- this.handleRPN(channelNumber, -1);
2327
+ dataDecrement(channelNumber, scheduleTime) {
2328
+ scheduleTime ??= this.audioContext.currentTime;
2329
+ this.handleRPN(channelNumber, -1, scheduleTime);
2239
2330
  }
2240
2331
  setRPNMSB(channelNumber, value) {
2241
2332
  this.channels[channelNumber].rpnMSB = value;
@@ -2245,7 +2336,7 @@ export class Midy {
2245
2336
  }
2246
2337
  dataEntryMSB(channelNumber, value, scheduleTime) {
2247
2338
  this.channels[channelNumber].dataMSB = value;
2248
- this.handleRPN(channelNumber, scheduleTime);
2339
+ this.handleRPN(channelNumber, 0, scheduleTime);
2249
2340
  }
2250
2341
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2251
2342
  const channel = this.channels[channelNumber];
@@ -2316,24 +2407,32 @@ export class Midy {
2316
2407
  }
2317
2408
  allSoundOff(channelNumber, _value, scheduleTime) {
2318
2409
  scheduleTime ??= this.audioContext.currentTime;
2319
- return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2410
+ return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2320
2411
  }
2321
2412
  resetAllStates(channelNumber) {
2413
+ const scheduleTime = this.audioContext.currentTime;
2322
2414
  const channel = this.channels[channelNumber];
2323
2415
  const state = channel.state;
2324
- for (const type of Object.keys(defaultControllerState)) {
2325
- 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
+ }
2326
2424
  }
2327
- for (const type of Object.keys(this.constructor.channelSettings)) {
2328
- channel[type] = this.constructor.channelSettings[type];
2425
+ for (const key of Object.keys(this.constructor.channelSettings)) {
2426
+ channel[key] = this.constructor.channelSettings[key];
2329
2427
  }
2428
+ this.resetChannelTable(channel);
2330
2429
  this.mode = "GM2";
2331
2430
  this.masterFineTuning = 0; // cb
2332
2431
  this.masterCoarseTuning = 0; // cb
2333
2432
  }
2334
2433
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2335
- resetAllControllers(channelNumber) {
2336
- const stateTypes = [
2434
+ resetAllControllers(channelNumber, _value, scheduleTime) {
2435
+ const keys = [
2337
2436
  "polyphonicKeyPressure",
2338
2437
  "channelPressure",
2339
2438
  "pitchWheel",
@@ -2346,10 +2445,17 @@ export class Midy {
2346
2445
  ];
2347
2446
  const channel = this.channels[channelNumber];
2348
2447
  const state = channel.state;
2349
- for (let i = 0; i < stateTypes.length; i++) {
2350
- const type = stateTypes[i];
2351
- 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
+ }
2352
2457
  }
2458
+ this.setPitchBend(channelNumber, 8192, scheduleTime);
2353
2459
  const settingTypes = [
2354
2460
  "rpnMSB",
2355
2461
  "rpnLSB",
@@ -2361,7 +2467,7 @@ export class Midy {
2361
2467
  }
2362
2468
  allNotesOff(channelNumber, _value, scheduleTime) {
2363
2469
  scheduleTime ??= this.audioContext.currentTime;
2364
- return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
2470
+ return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
2365
2471
  }
2366
2472
  omniOff(channelNumber, value, scheduleTime) {
2367
2473
  this.allNotesOff(channelNumber, value, scheduleTime);
@@ -2594,7 +2700,7 @@ export class Midy {
2594
2700
  this.reverbEffect = options.reverbAlgorithm(audioContext);
2595
2701
  }
2596
2702
  getReverbTime(value) {
2597
- return Math.pow(Math.E, (value - 40) * 0.025);
2703
+ return Math.exp((value - 40) * 0.025);
2598
2704
  }
2599
2705
  // mean free path equation
2600
2706
  // https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
@@ -2828,7 +2934,13 @@ export class Midy {
2828
2934
  setControllerParameters(channel, note, table) {
2829
2935
  if (table[0] !== 64)
2830
2936
  this.updateDetune(channel, note);
2831
- 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 {
2832
2944
  if (table[1] !== 64)
2833
2945
  this.setFilterEnvelope(channel, note);
2834
2946
  if (table[2] !== 64)
@@ -2856,8 +2968,13 @@ export class Midy {
2856
2968
  initControlTable() {
2857
2969
  const channelCount = 128;
2858
2970
  const slotSize = 6;
2859
- const defaultValues = [64, 64, 64, 0, 0, 0];
2860
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];
2861
2978
  for (let ch = 0; ch < channelCount; ch++) {
2862
2979
  const offset = ch * slotSize;
2863
2980
  table.set(defaultValues, offset);