@marmooo/midy 0.3.1 → 0.3.3

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, "ending", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: false
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
  }
@@ -510,17 +470,37 @@ export class Midy {
510
470
  }
511
471
  }
512
472
  }
513
- async loadSoundFont(soundFontUrl) {
514
- const response = await fetch(soundFontUrl);
515
- const arrayBuffer = await response.arrayBuffer();
516
- const parsed = parse(new Uint8Array(arrayBuffer));
473
+ async loadSoundFont(input) {
474
+ let uint8Array;
475
+ if (typeof input === "string") {
476
+ const response = await fetch(input);
477
+ const arrayBuffer = await response.arrayBuffer();
478
+ uint8Array = new Uint8Array(arrayBuffer);
479
+ }
480
+ else if (input instanceof Uint8Array) {
481
+ uint8Array = input;
482
+ }
483
+ else {
484
+ throw new TypeError("input must be a URL string or Uint8Array");
485
+ }
486
+ const parsed = parse(uint8Array);
517
487
  const soundFont = new SoundFont(parsed);
518
488
  this.addSoundFont(soundFont);
519
489
  }
520
- async loadMIDI(midiUrl) {
521
- const response = await fetch(midiUrl);
522
- const arrayBuffer = await response.arrayBuffer();
523
- const midi = parseMidi(new Uint8Array(arrayBuffer));
490
+ async loadMIDI(input) {
491
+ let uint8Array;
492
+ if (typeof input === "string") {
493
+ const response = await fetch(input);
494
+ const arrayBuffer = await response.arrayBuffer();
495
+ uint8Array = new Uint8Array(arrayBuffer);
496
+ }
497
+ else if (input instanceof Uint8Array) {
498
+ uint8Array = input;
499
+ }
500
+ else {
501
+ throw new TypeError("input must be a URL string or Uint8Array");
502
+ }
503
+ const midi = parseMidi(uint8Array);
524
504
  this.ticksPerBeat = midi.header.ticksPerBeat;
525
505
  const midiData = this.extractMidiData(midi);
526
506
  this.instruments = midiData.instruments;
@@ -541,22 +521,29 @@ export class Midy {
541
521
  merger,
542
522
  };
543
523
  }
524
+ resetChannelTable(channel) {
525
+ this.resetControlTable(channel.controlTable);
526
+ channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
527
+ channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
528
+ channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
529
+ channel.keyBasedInstrumentControlTable.fill(-1);
530
+ }
544
531
  createChannels(audioContext) {
545
532
  const channels = Array.from({ length: this.numChannels }, () => {
546
533
  return {
547
534
  currentBufferSource: null,
548
535
  isDrum: false,
549
- ...this.constructor.channelSettings,
550
536
  state: new ControllerState(),
551
- controlTable: this.initControlTable(),
537
+ ...this.constructor.channelSettings,
552
538
  ...this.setChannelAudioNodes(audioContext),
553
- scheduledNotes: new SparseMap(128),
539
+ scheduledNotes: [],
554
540
  sustainNotes: [],
555
- sostenutoNotes: new SparseMap(128),
541
+ sostenutoNotes: [],
542
+ controlTable: this.initControlTable(),
556
543
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
557
544
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
558
545
  polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
559
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
546
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
560
547
  };
561
548
  });
562
549
  return channels;
@@ -590,56 +577,39 @@ export class Midy {
590
577
  return audioBuffer;
591
578
  }
592
579
  }
593
- createBufferSource(voiceParams, audioBuffer) {
580
+ isLoopDrum(channel, noteNumber) {
581
+ const programNumber = channel.programNumber;
582
+ return ((programNumber === 48 && noteNumber === 88) ||
583
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
584
+ }
585
+ createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
594
586
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
595
587
  bufferSource.buffer = audioBuffer;
596
588
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
589
+ if (channel.isDrum) {
590
+ bufferSource.loop = this.isLoopDrum(channel, noteNumber);
591
+ }
597
592
  if (bufferSource.loop) {
598
593
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
599
594
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
600
595
  }
601
596
  return bufferSource;
602
597
  }
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) {
598
+ async scheduleTimelineEvents(t, resumeTime, queueIndex) {
622
599
  while (queueIndex < this.timeline.length) {
623
600
  const event = this.timeline[queueIndex];
624
601
  if (event.startTime > t + this.lookAhead)
625
602
  break;
626
- const startTime = event.startTime + this.startDelay - offset;
603
+ const delay = this.startDelay - resumeTime;
604
+ const startTime = event.startTime + delay;
627
605
  switch (event.type) {
628
606
  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 */
607
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
608
+ break;
634
609
  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) {
610
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
611
+ if (notePromise)
641
612
  this.notePromises.push(notePromise);
642
- }
643
613
  break;
644
614
  }
645
615
  case "noteAftertouch":
@@ -678,7 +648,7 @@ export class Midy {
678
648
  this.isPaused = false;
679
649
  this.startTime = this.audioContext.currentTime;
680
650
  let queueIndex = this.getQueueIndex(this.resumeTime);
681
- let offset = this.resumeTime - this.startTime;
651
+ let resumeTime = this.resumeTime - this.startTime;
682
652
  this.notePromises = [];
683
653
  const schedulePlayback = async () => {
684
654
  if (queueIndex >= this.timeline.length) {
@@ -687,18 +657,21 @@ export class Midy {
687
657
  this.exclusiveClassNotes.fill(undefined);
688
658
  this.drumExclusiveClassNotes.fill(undefined);
689
659
  this.audioBufferCache.clear();
660
+ for (let i = 0; i < this.channels.length; i++) {
661
+ this.resetAllStates(i);
662
+ }
690
663
  resolve();
691
664
  return;
692
665
  }
693
666
  const now = this.audioContext.currentTime;
694
- const t = now + offset;
695
- queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
667
+ const t = now + resumeTime;
668
+ queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
696
669
  if (this.isPausing) {
697
670
  await this.stopNotes(0, true, now);
698
671
  this.notePromises = [];
699
- resolve();
700
672
  this.isPausing = false;
701
673
  this.isPaused = true;
674
+ resolve();
702
675
  return;
703
676
  }
704
677
  else if (this.isStopping) {
@@ -707,9 +680,12 @@ export class Midy {
707
680
  this.exclusiveClassNotes.fill(undefined);
708
681
  this.drumExclusiveClassNotes.fill(undefined);
709
682
  this.audioBufferCache.clear();
710
- resolve();
683
+ for (let i = 0; i < this.channels.length; i++) {
684
+ this.resetAllStates(i);
685
+ }
711
686
  this.isStopping = false;
712
687
  this.isPaused = false;
688
+ resolve();
713
689
  return;
714
690
  }
715
691
  else if (this.isSeeking) {
@@ -718,7 +694,7 @@ export class Midy {
718
694
  this.drumExclusiveClassNotes.fill(undefined);
719
695
  this.startTime = this.audioContext.currentTime;
720
696
  queueIndex = this.getQueueIndex(this.resumeTime);
721
- offset = this.resumeTime - this.startTime;
697
+ resumeTime = this.resumeTime - this.startTime;
722
698
  this.isSeeking = false;
723
699
  await schedulePlayback();
724
700
  }
@@ -741,6 +717,7 @@ export class Midy {
741
717
  return `${programNumber}:${noteNumber}:${velocity}`;
742
718
  }
743
719
  extractMidiData(midi) {
720
+ this.audioBufferCounter.clear();
744
721
  const instruments = new Set();
745
722
  const timeline = [];
746
723
  const tmpChannels = new Array(this.channels.length);
@@ -845,9 +822,8 @@ export class Midy {
845
822
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
846
823
  const channel = this.channels[channelNumber];
847
824
  const promises = [];
848
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
849
- activeNotes.forEach((note) => {
850
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
825
+ this.processActiveNotes(channel, scheduleTime, (note) => {
826
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
851
827
  this.notePromises.push(promise);
852
828
  promises.push(promise);
853
829
  });
@@ -857,11 +833,11 @@ export class Midy {
857
833
  const channel = this.channels[channelNumber];
858
834
  const promises = [];
859
835
  this.processScheduledNotes(channel, (note) => {
860
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
836
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
861
837
  this.notePromises.push(promise);
862
838
  promises.push(promise);
863
839
  });
864
- channel.scheduledNotes.clear();
840
+ channel.scheduledNotes = [];
865
841
  return Promise.all(promises);
866
842
  }
867
843
  stopNotes(velocity, force, scheduleTime) {
@@ -882,9 +858,6 @@ export class Midy {
882
858
  if (!this.isPlaying)
883
859
  return;
884
860
  this.isStopping = true;
885
- for (let i = 0; i < this.channels.length; i++) {
886
- this.resetAllStates(i);
887
- }
888
861
  }
889
862
  pause() {
890
863
  if (!this.isPlaying || this.isPaused)
@@ -919,37 +892,28 @@ export class Midy {
919
892
  return this.resumeTime + now - this.startTime - this.startDelay;
920
893
  }
921
894
  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;
895
+ const scheduledNotes = channel.scheduledNotes;
896
+ for (let i = 0; i < scheduledNotes.length; i++) {
897
+ const note = scheduledNotes[i];
898
+ if (!note)
899
+ continue;
900
+ if (note.ending)
901
+ continue;
902
+ callback(note);
903
+ }
942
904
  }
943
- getActiveNote(noteList, scheduleTime) {
944
- for (let i = noteList.length - 1; i >= 0; i--) {
945
- const note = noteList[i];
905
+ processActiveNotes(channel, scheduleTime, callback) {
906
+ const scheduledNotes = channel.scheduledNotes;
907
+ for (let i = 0; i < scheduledNotes.length; i++) {
908
+ const note = scheduledNotes[i];
946
909
  if (!note)
947
- return;
910
+ continue;
911
+ if (note.ending)
912
+ continue;
948
913
  if (scheduleTime < note.startTime)
949
914
  continue;
950
- return (note.ending) ? null : note;
915
+ callback(note);
951
916
  }
952
- return noteList[0];
953
917
  }
954
918
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
955
919
  const sampleRate = audioContext.sampleRate;
@@ -1116,24 +1080,94 @@ export class Midy {
1116
1080
  const noteDetune = this.calcNoteDetune(channel, note);
1117
1081
  const pitchControl = this.getPitchControl(channel, note);
1118
1082
  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) {
1083
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1084
+ const startTime = note.startTime;
1085
+ const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1086
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1087
+ note.bufferSource.detune
1088
+ .cancelScheduledValues(scheduleTime)
1089
+ .setValueAtTime(detune - deltaCent, scheduleTime)
1090
+ .linearRampToValueAtTime(detune, portamentoTime);
1091
+ }
1092
+ else {
1093
+ note.bufferSource.detune
1094
+ .cancelScheduledValues(scheduleTime)
1095
+ .setValueAtTime(detune, scheduleTime);
1096
+ }
1097
+ }
1098
+ getPortamentoTime(channel, note) {
1099
+ const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1100
+ const value = Math.ceil(channel.state.portamentoTime * 127);
1101
+ return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1102
+ }
1103
+ getPitchIncrementSpeed(value) {
1104
+ const points = [
1105
+ [0, 1000],
1106
+ [6, 100],
1107
+ [16, 20],
1108
+ [32, 10],
1109
+ [48, 5],
1110
+ [64, 2.5],
1111
+ [80, 1],
1112
+ [96, 0.4],
1113
+ [112, 0.15],
1114
+ [127, 0.01],
1115
+ ];
1116
+ const logPoints = new Array(points.length);
1117
+ for (let i = 0; i < points.length; i++) {
1118
+ const [x, y] = points[i];
1119
+ if (value === x)
1120
+ return y;
1121
+ logPoints[i] = [x, Math.log(y)];
1122
+ }
1123
+ let startIndex = 0;
1124
+ for (let i = 1; i < logPoints.length; i++) {
1125
+ if (value <= logPoints[i][0]) {
1126
+ startIndex = i - 1;
1127
+ break;
1128
+ }
1129
+ }
1130
+ const [x0, y0] = logPoints[startIndex];
1131
+ const [x1, y1] = logPoints[startIndex + 1];
1132
+ const h = x1 - x0;
1133
+ const t = (value - x0) / h;
1134
+ let m0, m1;
1135
+ if (startIndex === 0) {
1136
+ m0 = (y1 - y0) / h;
1137
+ }
1138
+ else {
1139
+ const [xPrev, yPrev] = logPoints[startIndex - 1];
1140
+ m0 = (y1 - yPrev) / (x1 - xPrev);
1141
+ }
1142
+ if (startIndex === logPoints.length - 2) {
1143
+ m1 = (y1 - y0) / h;
1144
+ }
1145
+ else {
1146
+ const [xNext, yNext] = logPoints[startIndex + 2];
1147
+ m1 = (yNext - y0) / (xNext - x0);
1148
+ }
1149
+ // Cubic Hermite Spline
1150
+ const t2 = t * t;
1151
+ const t3 = t2 * t;
1152
+ const h00 = 2 * t3 - 3 * t2 + 1;
1153
+ const h10 = t3 - 2 * t2 + t;
1154
+ const h01 = -2 * t3 + 3 * t2;
1155
+ const h11 = t3 - t2;
1156
+ const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
1157
+ return Math.exp(y);
1158
+ }
1159
+ setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1160
+ const state = channel.state;
1128
1161
  const { voiceParams, startTime } = note;
1129
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1162
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1163
+ (1 + this.getAmplitudeControl(channel, note));
1130
1164
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1131
1165
  const volDelay = startTime + voiceParams.volDelay;
1132
- const portamentoTime = volDelay + this.getPortamentoTime(channel);
1166
+ const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1167
+ const volHold = volAttack + voiceParams.volHold;
1133
1168
  note.volumeEnvelopeNode.gain
1134
1169
  .cancelScheduledValues(scheduleTime)
1135
- .setValueAtTime(0, volDelay)
1136
- .linearRampToValueAtTime(sustainVolume, portamentoTime);
1170
+ .setValueAtTime(sustainVolume, volHold);
1137
1171
  }
1138
1172
  setVolumeEnvelope(channel, note, scheduleTime) {
1139
1173
  const state = channel.state;
@@ -1153,6 +1187,12 @@ export class Midy {
1153
1187
  .setValueAtTime(attackVolume, volHold)
1154
1188
  .linearRampToValueAtTime(sustainVolume, volDecay);
1155
1189
  }
1190
+ setPortamentoPitchEnvelope(note, scheduleTime) {
1191
+ const baseRate = note.voiceParams.playbackRate;
1192
+ note.bufferSource.playbackRate
1193
+ .cancelScheduledValues(scheduleTime)
1194
+ .setValueAtTime(baseRate, scheduleTime);
1195
+ }
1156
1196
  setPitchEnvelope(note, scheduleTime) {
1157
1197
  const { voiceParams } = note;
1158
1198
  const baseRate = voiceParams.playbackRate;
@@ -1180,20 +1220,20 @@ export class Midy {
1180
1220
  const maxFrequency = 20000; // max Hz of initialFilterFc
1181
1221
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1182
1222
  }
1183
- setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1223
+ setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1184
1224
  const state = channel.state;
1185
- const { voiceParams, noteNumber, startTime } = note;
1186
- const softPedalFactor = 1 -
1187
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1188
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1189
- softPedalFactor *
1225
+ const { voiceParams, startTime } = note;
1226
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1227
+ const baseCent = voiceParams.initialFilterFc +
1228
+ this.getFilterCutoffControl(channel, note);
1229
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1190
1230
  state.brightness * 2;
1191
1231
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1192
1232
  const sustainFreq = baseFreq +
1193
1233
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1194
1234
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1195
1235
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1196
- const portamentoTime = startTime + this.getPortamentoTime(channel);
1236
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1197
1237
  const modDelay = startTime + voiceParams.modDelay;
1198
1238
  note.filterNode.frequency
1199
1239
  .cancelScheduledValues(scheduleTime)
@@ -1203,9 +1243,8 @@ export class Midy {
1203
1243
  }
1204
1244
  setFilterEnvelope(channel, note, scheduleTime) {
1205
1245
  const state = channel.state;
1206
- const { voiceParams, noteNumber, startTime } = note;
1207
- const softPedalFactor = 1 -
1208
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1246
+ const { voiceParams, startTime } = note;
1247
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1209
1248
  const baseCent = voiceParams.initialFilterFc +
1210
1249
  this.getFilterCutoffControl(channel, note);
1211
1250
  const baseFreq = this.centToHz(baseCent) * softPedalFactor *
@@ -1279,14 +1318,14 @@ 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);
1286
1325
  const voiceParams = voice.getAllParams(controllerState);
1287
1326
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1288
1327
  const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1289
- note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1328
+ note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1290
1329
  note.volumeNode = new GainNode(this.audioContext);
1291
1330
  note.gainL = new GainNode(this.audioContext);
1292
1331
  note.gainR = new GainNode(this.audioContext);
@@ -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
  }
@@ -1321,10 +1364,10 @@ export class Midy {
1321
1364
  note.volumeEnvelopeNode.connect(note.volumeNode);
1322
1365
  note.volumeNode.connect(note.gainL);
1323
1366
  note.volumeNode.connect(note.gainR);
1324
- if (0 < channel.chorusSendLevel) {
1367
+ if (0 < state.chorusSendLevel) {
1325
1368
  this.setChorusEffectsSend(channel, note, 0, now);
1326
1369
  }
1327
- if (0 < channel.reverbSendLevel) {
1370
+ if (0 < state.reverbSendLevel) {
1328
1371
  this.setReverbEffectsSend(channel, note, 0, now);
1329
1372
  }
1330
1373
  note.bufferSource.start(startTime);
@@ -1355,8 +1398,7 @@ export class Midy {
1355
1398
  const [prevNote, prevChannelNumber] = prev;
1356
1399
  if (prevNote && !prevNote.ending) {
1357
1400
  this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1358
- startTime, true, // force
1359
- undefined);
1401
+ startTime, true);
1360
1402
  }
1361
1403
  }
1362
1404
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
@@ -1376,19 +1418,11 @@ export class Midy {
1376
1418
  const prevNote = this.drumExclusiveClassNotes[index];
1377
1419
  if (prevNote && !prevNote.ending) {
1378
1420
  this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1379
- startTime, true, // force
1380
- undefined);
1421
+ startTime, true);
1381
1422
  }
1382
1423
  this.drumExclusiveClassNotes[index] = note;
1383
1424
  }
1384
- isDrumNoteOffException(channel, noteNumber) {
1385
- if (!channel.isDrum)
1386
- return false;
1387
- const programNumber = channel.programNumber;
1388
- return !((programNumber === 48 && noteNumber === 88) ||
1389
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1390
- }
1391
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1425
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1392
1426
  const channel = this.channels[channelNumber];
1393
1427
  const bankNumber = this.calcBank(channel, channelNumber);
1394
1428
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1399,7 +1433,8 @@ export class Midy {
1399
1433
  if (!voice)
1400
1434
  return;
1401
1435
  const isSF3 = soundFont.parsed.info.version.major === 3;
1402
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1436
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1437
+ note.noteOffEvent = noteOffEvent;
1403
1438
  note.gainL.connect(channel.gainL);
1404
1439
  note.gainR.connect(channel.gainR);
1405
1440
  if (0.5 <= channel.state.sustainPedal) {
@@ -1408,31 +1443,12 @@ export class Midy {
1408
1443
  this.handleExclusiveClass(note, channelNumber, startTime);
1409
1444
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1410
1445
  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
- }
1419
- if (this.isDrumNoteOffException(channel, noteNumber)) {
1420
- const stopTime = startTime + note.bufferSource.buffer.duration;
1421
- const index = noteList.length - 1;
1422
- const promise = new Promise((resolve) => {
1423
- note.bufferSource.onended = () => {
1424
- noteList[index] = undefined;
1425
- this.disconnectNote(note);
1426
- resolve();
1427
- };
1428
- note.bufferSource.stop(stopTime);
1429
- });
1430
- this.notePromises.push(promise);
1431
- }
1446
+ note.index = scheduledNotes.length;
1447
+ scheduledNotes.push(note);
1432
1448
  }
1433
1449
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1434
1450
  scheduleTime ??= this.audioContext.currentTime;
1435
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1451
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1436
1452
  }
1437
1453
  disconnectNote(note) {
1438
1454
  note.bufferSource.disconnect();
@@ -1457,84 +1473,72 @@ export class Midy {
1457
1473
  note.chorusEffectsSend.disconnect();
1458
1474
  }
1459
1475
  }
1460
- stopNote(endTime, stopTime, noteList, index) {
1461
- const note = noteList[index];
1476
+ releaseNote(channel, note, endTime) {
1477
+ const volRelease = endTime +
1478
+ note.voiceParams.volRelease * channel.state.releaseTime * 2;
1479
+ const modRelease = endTime + note.voiceParams.modRelease;
1480
+ const stopTime = Math.min(volRelease, modRelease);
1481
+ note.filterNode.frequency
1482
+ .cancelScheduledValues(endTime)
1483
+ .linearRampToValueAtTime(0, modRelease);
1462
1484
  note.volumeEnvelopeNode.gain
1463
1485
  .cancelScheduledValues(endTime)
1464
- .linearRampToValueAtTime(0, stopTime);
1465
- note.ending = true;
1466
- this.scheduleTask(() => {
1467
- note.bufferSource.loop = false;
1468
- }, stopTime);
1486
+ .linearRampToValueAtTime(0, volRelease);
1469
1487
  return new Promise((resolve) => {
1470
- note.bufferSource.onended = () => {
1471
- noteList[index] = undefined;
1488
+ this.scheduleTask(() => {
1489
+ const bufferSource = note.bufferSource;
1490
+ bufferSource.loop = false;
1491
+ bufferSource.stop(stopTime);
1472
1492
  this.disconnectNote(note);
1493
+ channel.scheduledNotes[note.index] = undefined;
1473
1494
  resolve();
1474
- };
1475
- note.bufferSource.stop(stopTime);
1495
+ }, stopTime);
1476
1496
  });
1477
1497
  }
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) {
1498
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1489
1499
  const channel = this.channels[channelNumber];
1490
- if (this.isDrumNoteOffException(channel, noteNumber))
1491
- return;
1492
1500
  const state = channel.state;
1493
1501
  if (!force) {
1494
- if (0.5 <= state.sustainPedal)
1495
- return;
1496
- if (channel.sostenutoNotes.has(noteNumber))
1497
- return;
1498
- }
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);
1502
+ if (channel.isDrum) {
1503
+ if (!this.isLoopDrum(channel, noteNumber))
1504
+ return;
1505
+ }
1506
+ else {
1507
+ if (0.5 <= state.sustainPedal)
1508
+ return;
1509
+ if (0.5 <= state.sostenutoPedal)
1510
+ return;
1511
+ }
1515
1512
  }
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);
1513
+ const note = this.findNoteOffTarget(channel, noteNumber);
1514
+ if (!note)
1515
+ return;
1516
+ note.ending = true;
1517
+ this.releaseNote(channel, note, endTime);
1518
+ }
1519
+ findNoteOffTarget(channel, noteNumber) {
1520
+ const scheduledNotes = channel.scheduledNotes;
1521
+ for (let i = 0; i < scheduledNotes.length; i++) {
1522
+ const note = scheduledNotes[i];
1523
+ if (!note)
1524
+ continue;
1525
+ if (note.ending)
1526
+ continue;
1527
+ if (note.noteNumber !== noteNumber)
1528
+ continue;
1529
+ return note;
1525
1530
  }
1526
1531
  }
1527
1532
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1528
1533
  scheduleTime ??= this.audioContext.currentTime;
1529
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1530
- undefined);
1534
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1531
1535
  }
1532
1536
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1533
1537
  const velocity = halfVelocity * 2;
1534
1538
  const channel = this.channels[channelNumber];
1535
1539
  const promises = [];
1536
1540
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1537
- const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1541
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1538
1542
  promises.push(promise);
1539
1543
  }
1540
1544
  channel.sustainNotes = [];
@@ -1544,12 +1548,14 @@ export class Midy {
1544
1548
  const velocity = halfVelocity * 2;
1545
1549
  const channel = this.channels[channelNumber];
1546
1550
  const promises = [];
1551
+ const sostenutoNotes = channel.sostenutoNotes;
1547
1552
  channel.state.sostenutoPedal = 0;
1548
- channel.sostenutoNotes.forEach((note) => {
1549
- const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1553
+ for (let i = 0; i < sostenutoNotes.length; i++) {
1554
+ const note = sostenutoNotes[i];
1555
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1550
1556
  promises.push(promise);
1551
- });
1552
- channel.sostenutoNotes.clear();
1557
+ }
1558
+ channel.sostenutoNotes = [];
1553
1559
  return promises;
1554
1560
  }
1555
1561
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
@@ -1578,11 +1584,11 @@ export class Midy {
1578
1584
  const channel = this.channels[channelNumber];
1579
1585
  channel.state.polyphonicKeyPressure = pressure / 127;
1580
1586
  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
- }
1587
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1588
+ if (note.noteNumber === noteNumber) {
1589
+ this.setControllerParameters(channel, note, table);
1590
+ }
1591
+ });
1586
1592
  this.applyVoiceParams(channel, 10);
1587
1593
  }
1588
1594
  handleProgramChange(channelNumber, programNumber, _scheduleTime) {
@@ -1612,7 +1618,7 @@ export class Midy {
1612
1618
  channel.detune += pressureDepth * (next - prev);
1613
1619
  }
1614
1620
  const table = channel.channelPressureTable;
1615
- this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1621
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1616
1622
  this.setControllerParameters(channel, note, table);
1617
1623
  });
1618
1624
  this.applyVoiceParams(channel, 13);
@@ -1669,10 +1675,13 @@ export class Midy {
1669
1675
  .setValueAtTime(volumeDepth, scheduleTime);
1670
1676
  }
1671
1677
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1678
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1679
+ let value = note.voiceParams.reverbEffectsSend;
1680
+ if (0 <= keyBasedValue) {
1681
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1682
+ }
1672
1683
  if (0 < prevValue) {
1673
- if (0 < note.voiceParams.reverbEffectsSend) {
1674
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1675
- const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1684
+ if (0 < value) {
1676
1685
  note.reverbEffectsSend.gain
1677
1686
  .cancelScheduledValues(scheduleTime)
1678
1687
  .setValueAtTime(value, scheduleTime);
@@ -1682,10 +1691,10 @@ export class Midy {
1682
1691
  }
1683
1692
  }
1684
1693
  else {
1685
- if (0 < note.voiceParams.reverbEffectsSend) {
1694
+ if (0 < value) {
1686
1695
  if (!note.reverbEffectsSend) {
1687
1696
  note.reverbEffectsSend = new GainNode(this.audioContext, {
1688
- gain: note.voiceParams.reverbEffectsSend,
1697
+ gain: value,
1689
1698
  });
1690
1699
  note.volumeNode.connect(note.reverbEffectsSend);
1691
1700
  }
@@ -1694,10 +1703,13 @@ export class Midy {
1694
1703
  }
1695
1704
  }
1696
1705
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1706
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1707
+ let value = note.voiceParams.chorusEffectsSend;
1708
+ if (0 <= keyBasedValue) {
1709
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1710
+ }
1697
1711
  if (0 < prevValue) {
1698
- if (0 < note.voiceParams.chorusEffectsSend) {
1699
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1700
- const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1712
+ if (0 < vaule) {
1701
1713
  note.chorusEffectsSend.gain
1702
1714
  .cancelScheduledValues(scheduleTime)
1703
1715
  .setValueAtTime(value, scheduleTime);
@@ -1707,10 +1719,10 @@ export class Midy {
1707
1719
  }
1708
1720
  }
1709
1721
  else {
1710
- if (0 < note.voiceParams.chorusEffectsSend) {
1722
+ if (0 < value) {
1711
1723
  if (!note.chorusEffectsSend) {
1712
1724
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1713
- gain: note.voiceParams.chorusEffectsSend,
1725
+ gain: value,
1714
1726
  });
1715
1727
  note.volumeNode.connect(note.chorusEffectsSend);
1716
1728
  }
@@ -1820,8 +1832,8 @@ export class Midy {
1820
1832
  if (key in voiceParams)
1821
1833
  noteVoiceParams[key] = voiceParams[key];
1822
1834
  }
1823
- if (0.5 <= channel.state.portamento && note.portamento) {
1824
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1835
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1836
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1825
1837
  }
1826
1838
  else {
1827
1839
  this.setFilterEnvelope(channel, note, scheduleTime);
@@ -1844,42 +1856,42 @@ export class Midy {
1844
1856
  });
1845
1857
  }
1846
1858
  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
- };
1859
+ const handlers = new Array(128);
1860
+ handlers[0] = this.setBankMSB;
1861
+ handlers[1] = this.setModulationDepth;
1862
+ handlers[5] = this.setPortamentoTime;
1863
+ handlers[6] = this.dataEntryMSB;
1864
+ handlers[7] = this.setVolume;
1865
+ handlers[10] = this.setPan;
1866
+ handlers[11] = this.setExpression;
1867
+ handlers[32] = this.setBankLSB;
1868
+ handlers[38] = this.dataEntryLSB;
1869
+ handlers[64] = this.setSustainPedal;
1870
+ handlers[65] = this.setPortamento;
1871
+ handlers[66] = this.setSostenutoPedal;
1872
+ handlers[67] = this.setSoftPedal;
1873
+ handlers[71] = this.setFilterResonance;
1874
+ handlers[72] = this.setReleaseTime;
1875
+ handlers[73] = this.setAttackTime;
1876
+ handlers[74] = this.setBrightness;
1877
+ handlers[75] = this.setDecayTime;
1878
+ handlers[76] = this.setVibratoRate;
1879
+ handlers[77] = this.setVibratoDepth;
1880
+ handlers[78] = this.setVibratoDelay;
1881
+ handlers[91] = this.setReverbSendLevel;
1882
+ handlers[93] = this.setChorusSendLevel;
1883
+ handlers[96] = this.dataIncrement;
1884
+ handlers[97] = this.dataDecrement;
1885
+ handlers[100] = this.setRPNLSB;
1886
+ handlers[101] = this.setRPNMSB;
1887
+ handlers[120] = this.allSoundOff;
1888
+ handlers[121] = this.resetAllControllers;
1889
+ handlers[123] = this.allNotesOff;
1890
+ handlers[124] = this.omniOff;
1891
+ handlers[125] = this.omniOn;
1892
+ handlers[126] = this.monoOn;
1893
+ handlers[127] = this.polyOn;
1894
+ return handlers;
1883
1895
  }
1884
1896
  handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1885
1897
  const handler = this.controlChangeHandlers[controllerType];
@@ -1916,17 +1928,41 @@ export class Midy {
1916
1928
  channel.state.modulationDepth = modulation / 127;
1917
1929
  this.updateModulation(channel, scheduleTime);
1918
1930
  }
1919
- setPortamentoTime(channelNumber, portamentoTime) {
1931
+ updatePortamento(channel, scheduleTime) {
1932
+ this.processScheduledNotes(channel, (note) => {
1933
+ if (0.5 <= channel.state.portamento) {
1934
+ if (0 <= note.portamentoNoteNumber) {
1935
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1936
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1937
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
1938
+ this.updateDetune(channel, note, scheduleTime);
1939
+ }
1940
+ }
1941
+ else {
1942
+ if (0 <= note.portamentoNoteNumber) {
1943
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1944
+ this.setFilterEnvelope(channel, note, scheduleTime);
1945
+ this.setPitchEnvelope(note, scheduleTime);
1946
+ this.updateDetune(channel, note, scheduleTime);
1947
+ }
1948
+ }
1949
+ });
1950
+ }
1951
+ setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
1920
1952
  const channel = this.channels[channelNumber];
1953
+ scheduleTime ??= this.audioContext.currentTime;
1921
1954
  channel.state.portamentoTime = portamentoTime / 127;
1955
+ if (channel.isDrum)
1956
+ return;
1957
+ this.updatePortamento(channel, scheduleTime);
1922
1958
  }
1923
1959
  setKeyBasedVolume(channel, scheduleTime) {
1924
1960
  this.processScheduledNotes(channel, (note) => {
1925
1961
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1926
- if (keyBasedValue !== 0) {
1962
+ if (0 <= keyBasedValue) {
1927
1963
  note.volumeNode.gain
1928
1964
  .cancelScheduledValues(scheduleTime)
1929
- .setValueAtTime(1 + keyBasedValue, scheduleTime);
1965
+ .setValueAtTime(keyBasedValue / 127, scheduleTime);
1930
1966
  }
1931
1967
  });
1932
1968
  }
@@ -1947,8 +1983,8 @@ export class Midy {
1947
1983
  setKeyBasedPan(channel, scheduleTime) {
1948
1984
  this.processScheduledNotes(channel, (note) => {
1949
1985
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1950
- if (keyBasedValue !== 0) {
1951
- const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1986
+ if (0 <= keyBasedValue) {
1987
+ const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
1952
1988
  note.gainL.gain
1953
1989
  .cancelScheduledValues(scheduleTime)
1954
1990
  .setValueAtTime(gainLeft, scheduleTime);
@@ -1976,7 +2012,7 @@ export class Midy {
1976
2012
  }
1977
2013
  dataEntryLSB(channelNumber, value, scheduleTime) {
1978
2014
  this.channels[channelNumber].dataLSB = value;
1979
- this.handleRPN(channelNumber, scheduleTime);
2015
+ this.handleRPN(channelNumber, 0, scheduleTime);
1980
2016
  }
1981
2017
  updateChannelVolume(channel, scheduleTime) {
1982
2018
  const state = channel.state;
@@ -2004,11 +2040,13 @@ export class Midy {
2004
2040
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
2005
2041
  }
2006
2042
  }
2007
- setPortamento(channelNumber, value) {
2043
+ setPortamento(channelNumber, value, scheduleTime) {
2008
2044
  const channel = this.channels[channelNumber];
2009
2045
  if (channel.isDrum)
2010
2046
  return;
2047
+ scheduleTime ??= this.audioContext.currentTime;
2011
2048
  channel.state.portamento = value / 127;
2049
+ this.updatePortamento(channel, scheduleTime);
2012
2050
  }
2013
2051
  setSostenutoPedal(channelNumber, value, scheduleTime) {
2014
2052
  const channel = this.channels[channelNumber];
@@ -2017,12 +2055,19 @@ export class Midy {
2017
2055
  scheduleTime ??= this.audioContext.currentTime;
2018
2056
  channel.state.sostenutoPedal = value / 127;
2019
2057
  if (64 <= value) {
2020
- channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
2058
+ const sostenutoNotes = [];
2059
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2060
+ sostenutoNotes.push(note);
2061
+ });
2062
+ channel.sostenutoNotes = sostenutoNotes;
2021
2063
  }
2022
2064
  else {
2023
2065
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
2024
2066
  }
2025
2067
  }
2068
+ getSoftPedalFactor(channel, note) {
2069
+ return 1 - (0.1 + (note.noteNumber / 127) * 0.2) * channel.state.softPedal;
2070
+ }
2026
2071
  setSoftPedal(channelNumber, softPedal, scheduleTime) {
2027
2072
  const channel = this.channels[channelNumber];
2028
2073
  if (channel.isDrum)
@@ -2031,9 +2076,9 @@ export class Midy {
2031
2076
  scheduleTime ??= this.audioContext.currentTime;
2032
2077
  state.softPedal = softPedal / 127;
2033
2078
  this.processScheduledNotes(channel, (note) => {
2034
- if (0.5 <= state.portamento && note.portamento) {
2035
- this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
2036
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2079
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2080
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2081
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2037
2082
  }
2038
2083
  else {
2039
2084
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2047,7 +2092,7 @@ export class Midy {
2047
2092
  return;
2048
2093
  scheduleTime ??= this.audioContext.currentTime;
2049
2094
  const state = channel.state;
2050
- state.filterResonance = filterResonance / 64;
2095
+ state.filterResonance = filterResonance / 127;
2051
2096
  this.processScheduledNotes(channel, (note) => {
2052
2097
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2053
2098
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
@@ -2058,14 +2103,14 @@ export class Midy {
2058
2103
  if (channel.isDrum)
2059
2104
  return;
2060
2105
  scheduleTime ??= this.audioContext.currentTime;
2061
- channel.state.releaseTime = releaseTime / 64;
2106
+ channel.state.releaseTime = releaseTime / 127;
2062
2107
  }
2063
2108
  setAttackTime(channelNumber, attackTime, scheduleTime) {
2064
2109
  const channel = this.channels[channelNumber];
2065
2110
  if (channel.isDrum)
2066
2111
  return;
2067
2112
  scheduleTime ??= this.audioContext.currentTime;
2068
- channel.state.attackTime = attackTime / 64;
2113
+ channel.state.attackTime = attackTime / 127;
2069
2114
  this.processScheduledNotes(channel, (note) => {
2070
2115
  if (note.startTime < scheduleTime)
2071
2116
  return false;
@@ -2078,10 +2123,10 @@ export class Midy {
2078
2123
  return;
2079
2124
  const state = channel.state;
2080
2125
  scheduleTime ??= this.audioContext.currentTime;
2081
- state.brightness = brightness / 64;
2126
+ state.brightness = brightness / 127;
2082
2127
  this.processScheduledNotes(channel, (note) => {
2083
- if (0.5 <= state.portamento && note.portamento) {
2084
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2128
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2129
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2085
2130
  }
2086
2131
  else {
2087
2132
  this.setFilterEnvelope(channel, note);
@@ -2093,7 +2138,7 @@ export class Midy {
2093
2138
  if (channel.isDrum)
2094
2139
  return;
2095
2140
  scheduleTime ??= this.audioContext.currentTime;
2096
- channel.state.decayTime = dacayTime / 64;
2141
+ channel.state.decayTime = dacayTime / 127;
2097
2142
  this.processScheduledNotes(channel, (note) => {
2098
2143
  this.setVolumeEnvelope(channel, note, scheduleTime);
2099
2144
  });
@@ -2103,7 +2148,7 @@ export class Midy {
2103
2148
  if (channel.isDrum)
2104
2149
  return;
2105
2150
  scheduleTime ??= this.audioContext.currentTime;
2106
- channel.state.vibratoRate = vibratoRate / 64;
2151
+ channel.state.vibratoRate = vibratoRate / 127;
2107
2152
  if (channel.vibratoDepth <= 0)
2108
2153
  return;
2109
2154
  this.processScheduledNotes(channel, (note) => {
@@ -2116,7 +2161,7 @@ export class Midy {
2116
2161
  return;
2117
2162
  scheduleTime ??= this.audioContext.currentTime;
2118
2163
  const prev = channel.state.vibratoDepth;
2119
- channel.state.vibratoDepth = vibratoDepth / 64;
2164
+ channel.state.vibratoDepth = vibratoDepth / 127;
2120
2165
  if (0 < prev) {
2121
2166
  this.processScheduledNotes(channel, (note) => {
2122
2167
  this.setFreqVibLFO(channel, note, scheduleTime);
@@ -2128,12 +2173,12 @@ export class Midy {
2128
2173
  });
2129
2174
  }
2130
2175
  }
2131
- setVibratoDelay(channelNumber, vibratoDelay) {
2176
+ setVibratoDelay(channelNumber, vibratoDelay, scheduleTime) {
2132
2177
  const channel = this.channels[channelNumber];
2133
2178
  if (channel.isDrum)
2134
2179
  return;
2135
2180
  scheduleTime ??= this.audioContext.currentTime;
2136
- channel.state.vibratoDelay = vibratoDelay / 64;
2181
+ channel.state.vibratoDelay = vibratoDelay / 127;
2137
2182
  if (0 < channel.state.vibratoDepth) {
2138
2183
  this.processScheduledNotes(channel, (note) => {
2139
2184
  this.startVibrato(channel, note, scheduleTime);
@@ -2156,7 +2201,8 @@ export class Midy {
2156
2201
  this.processScheduledNotes(channel, (note) => {
2157
2202
  if (note.voiceParams.reverbEffectsSend <= 0)
2158
2203
  return false;
2159
- note.reverbEffectsSend.disconnect();
2204
+ if (note.reverbEffectsSend)
2205
+ note.reverbEffectsSend.disconnect();
2160
2206
  });
2161
2207
  }
2162
2208
  }
@@ -2188,7 +2234,8 @@ export class Midy {
2188
2234
  this.processScheduledNotes(channel, (note) => {
2189
2235
  if (note.voiceParams.chorusEffectsSend <= 0)
2190
2236
  return false;
2191
- note.chorusEffectsSend.disconnect();
2237
+ if (note.chorusEffectsSend)
2238
+ note.chorusEffectsSend.disconnect();
2192
2239
  });
2193
2240
  }
2194
2241
  }
@@ -2255,12 +2302,14 @@ export class Midy {
2255
2302
  }
2256
2303
  }
2257
2304
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2258
- dataIncrement(channelNumber) {
2259
- this.handleRPN(channelNumber, 1);
2305
+ dataIncrement(channelNumber, scheduleTime) {
2306
+ scheduleTime ??= this.audioContext.currentTime;
2307
+ this.handleRPN(channelNumber, 1, scheduleTime);
2260
2308
  }
2261
2309
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2262
- dataDecrement(channelNumber) {
2263
- this.handleRPN(channelNumber, -1);
2310
+ dataDecrement(channelNumber, scheduleTime) {
2311
+ scheduleTime ??= this.audioContext.currentTime;
2312
+ this.handleRPN(channelNumber, -1, scheduleTime);
2264
2313
  }
2265
2314
  setRPNMSB(channelNumber, value) {
2266
2315
  this.channels[channelNumber].rpnMSB = value;
@@ -2270,7 +2319,7 @@ export class Midy {
2270
2319
  }
2271
2320
  dataEntryMSB(channelNumber, value, scheduleTime) {
2272
2321
  this.channels[channelNumber].dataMSB = value;
2273
- this.handleRPN(channelNumber, scheduleTime);
2322
+ this.handleRPN(channelNumber, 0, scheduleTime);
2274
2323
  }
2275
2324
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2276
2325
  const channel = this.channels[channelNumber];
@@ -2344,21 +2393,29 @@ export class Midy {
2344
2393
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2345
2394
  }
2346
2395
  resetAllStates(channelNumber) {
2396
+ const scheduleTime = this.audioContext.currentTime;
2347
2397
  const channel = this.channels[channelNumber];
2348
2398
  const state = channel.state;
2349
- for (const type of Object.keys(defaultControllerState)) {
2350
- state[type] = defaultControllerState[type].defaultValue;
2399
+ const entries = Object.entries(defaultControllerState);
2400
+ for (const [key, { type, defaultValue }] of entries) {
2401
+ if (128 <= type) {
2402
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2403
+ }
2404
+ else {
2405
+ state[key] = defaultValue;
2406
+ }
2351
2407
  }
2352
- for (const type of Object.keys(this.constructor.channelSettings)) {
2353
- channel[type] = this.constructor.channelSettings[type];
2408
+ for (const key of Object.keys(this.constructor.channelSettings)) {
2409
+ channel[key] = this.constructor.channelSettings[key];
2354
2410
  }
2411
+ this.resetChannelTable(channel);
2355
2412
  this.mode = "GM2";
2356
2413
  this.masterFineTuning = 0; // cb
2357
2414
  this.masterCoarseTuning = 0; // cb
2358
2415
  }
2359
2416
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2360
- resetAllControllers(channelNumber) {
2361
- const stateTypes = [
2417
+ resetAllControllers(channelNumber, _value, scheduleTime) {
2418
+ const keys = [
2362
2419
  "polyphonicKeyPressure",
2363
2420
  "channelPressure",
2364
2421
  "pitchWheel",
@@ -2371,10 +2428,17 @@ export class Midy {
2371
2428
  ];
2372
2429
  const channel = this.channels[channelNumber];
2373
2430
  const state = channel.state;
2374
- for (let i = 0; i < stateTypes.length; i++) {
2375
- const type = stateTypes[i];
2376
- state[type] = defaultControllerState[type].defaultValue;
2431
+ for (let i = 0; i < keys.length; i++) {
2432
+ const key = keys[i];
2433
+ const { type, defaultValue } = defaultControllerState[key];
2434
+ if (128 <= type) {
2435
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2436
+ }
2437
+ else {
2438
+ state[key] = defaultValue;
2439
+ }
2377
2440
  }
2441
+ this.setPitchBend(channelNumber, 8192, scheduleTime);
2378
2442
  const settingTypes = [
2379
2443
  "rpnMSB",
2380
2444
  "rpnLSB",
@@ -2619,7 +2683,7 @@ export class Midy {
2619
2683
  this.reverbEffect = options.reverbAlgorithm(audioContext);
2620
2684
  }
2621
2685
  getReverbTime(value) {
2622
- return Math.pow(Math.E, (value - 40) * 0.025);
2686
+ return Math.exp((value - 40) * 0.025);
2623
2687
  }
2624
2688
  // mean free path equation
2625
2689
  // https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
@@ -2853,7 +2917,13 @@ export class Midy {
2853
2917
  setControllerParameters(channel, note, table) {
2854
2918
  if (table[0] !== 64)
2855
2919
  this.updateDetune(channel, note);
2856
- if (!note.portamento) {
2920
+ if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2921
+ if (table[1] !== 64)
2922
+ this.setPortamentoFilterEnvelope(channel, note);
2923
+ if (table[2] !== 64)
2924
+ this.setPortamentoVolumeEnvelope(channel, note);
2925
+ }
2926
+ else {
2857
2927
  if (table[1] !== 64)
2858
2928
  this.setFilterEnvelope(channel, note);
2859
2929
  if (table[2] !== 64)
@@ -2881,8 +2951,13 @@ export class Midy {
2881
2951
  initControlTable() {
2882
2952
  const channelCount = 128;
2883
2953
  const slotSize = 6;
2884
- const defaultValues = [64, 64, 64, 0, 0, 0];
2885
2954
  const table = new Uint8Array(channelCount * slotSize);
2955
+ return this.resetControlTable(table);
2956
+ }
2957
+ resetControlTable(table) {
2958
+ const channelCount = 128;
2959
+ const slotSize = 6;
2960
+ const defaultValues = [64, 64, 64, 0, 0, 0];
2886
2961
  for (let ch = 0; ch < channelCount; ch++) {
2887
2962
  const offset = ch * slotSize;
2888
2963
  table.set(defaultValues, offset);
@@ -2913,7 +2988,7 @@ export class Midy {
2913
2988
  getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2914
2989
  const index = keyNumber * 128 + controllerType;
2915
2990
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2916
- return (controlValue + 64) / 64;
2991
+ return controlValue;
2917
2992
  }
2918
2993
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2919
2994
  const channelNumber = data[4];
@@ -2926,7 +3001,7 @@ export class Midy {
2926
3001
  const controllerType = data[i];
2927
3002
  const value = data[i + 1];
2928
3003
  const index = keyNumber * 128 + controllerType;
2929
- table[index] = value - 64;
3004
+ table[index] = value;
2930
3005
  }
2931
3006
  this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2932
3007
  }