@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.
@@ -3,60 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MidyGM2 = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
- // 2-3 times faster than Map
7
- class SparseMap {
8
- constructor(size) {
9
- this.data = new Array(size);
10
- this.activeIndices = [];
11
- }
12
- set(key, value) {
13
- if (this.data[key] === undefined) {
14
- this.activeIndices.push(key);
15
- }
16
- this.data[key] = value;
17
- }
18
- get(key) {
19
- return this.data[key];
20
- }
21
- delete(key) {
22
- if (this.data[key] !== undefined) {
23
- this.data[key] = undefined;
24
- const index = this.activeIndices.indexOf(key);
25
- if (index !== -1) {
26
- this.activeIndices.splice(index, 1);
27
- }
28
- return true;
29
- }
30
- return false;
31
- }
32
- has(key) {
33
- return this.data[key] !== undefined;
34
- }
35
- get size() {
36
- return this.activeIndices.length;
37
- }
38
- clear() {
39
- for (let i = 0; i < this.activeIndices.length; i++) {
40
- const key = this.activeIndices[i];
41
- this.data[key] = undefined;
42
- }
43
- this.activeIndices = [];
44
- }
45
- *[Symbol.iterator]() {
46
- for (let i = 0; i < this.activeIndices.length; i++) {
47
- const key = this.activeIndices[i];
48
- yield [key, this.data[key]];
49
- }
50
- }
51
- forEach(callback) {
52
- for (let i = 0; i < this.activeIndices.length; i++) {
53
- const key = this.activeIndices[i];
54
- callback(this.data[key], key, this);
55
- }
56
- }
57
- }
58
6
  class Note {
59
7
  constructor(noteNumber, velocity, startTime, voice, voiceParams) {
8
+ Object.defineProperty(this, "index", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: -1
13
+ });
14
+ Object.defineProperty(this, "ending", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: false
19
+ });
60
20
  Object.defineProperty(this, "bufferSource", {
61
21
  enumerable: true,
62
22
  configurable: true,
@@ -141,11 +101,11 @@ class Note {
141
101
  writable: true,
142
102
  value: void 0
143
103
  });
144
- Object.defineProperty(this, "portamento", {
104
+ Object.defineProperty(this, "portamentoNoteNumber", {
145
105
  enumerable: true,
146
106
  configurable: true,
147
107
  writable: true,
148
- value: void 0
108
+ value: -1
149
109
  });
150
110
  this.noteNumber = noteNumber;
151
111
  this.velocity = velocity;
@@ -200,7 +160,7 @@ const defaultControllerState = {
200
160
  portamentoTime: { type: 128 + 5, defaultValue: 0 },
201
161
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
202
162
  volume: { type: 128 + 7, defaultValue: 100 / 127 },
203
- pan: { type: 128 + 10, defaultValue: 0.5 },
163
+ pan: { type: 128 + 10, defaultValue: 64 / 127 },
204
164
  expression: { type: 128 + 11, defaultValue: 1 },
205
165
  // bankLSB: { type: 128 + 32, defaultValue: 0, },
206
166
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
@@ -208,14 +168,6 @@ const defaultControllerState = {
208
168
  portamento: { type: 128 + 65, defaultValue: 0 },
209
169
  sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
210
170
  softPedal: { type: 128 + 67, defaultValue: 0 },
211
- filterResonance: { type: 128 + 71, defaultValue: 0.5 },
212
- releaseTime: { type: 128 + 72, defaultValue: 0.5 },
213
- attackTime: { type: 128 + 73, defaultValue: 0.5 },
214
- brightness: { type: 128 + 74, defaultValue: 0.5 },
215
- decayTime: { type: 128 + 75, defaultValue: 0.5 },
216
- vibratoRate: { type: 128 + 76, defaultValue: 0.5 },
217
- vibratoDepth: { type: 128 + 77, defaultValue: 0.5 },
218
- vibratoDelay: { type: 128 + 78, defaultValue: 0.5 },
219
171
  reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
220
172
  chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
221
173
  // dataIncrement: { type: 128 + 96, defaultValue: 0 },
@@ -490,7 +442,7 @@ class MidyGM2 {
490
442
  initSoundFontTable() {
491
443
  const table = new Array(128);
492
444
  for (let i = 0; i < 128; i++) {
493
- table[i] = new SparseMap(128);
445
+ table[i] = new Map();
494
446
  }
495
447
  return table;
496
448
  }
@@ -506,17 +458,37 @@ class MidyGM2 {
506
458
  }
507
459
  }
508
460
  }
509
- async loadSoundFont(soundFontUrl) {
510
- const response = await fetch(soundFontUrl);
511
- const arrayBuffer = await response.arrayBuffer();
512
- const parsed = (0, soundfont_parser_1.parse)(new Uint8Array(arrayBuffer));
461
+ async loadSoundFont(input) {
462
+ let uint8Array;
463
+ if (typeof input === "string") {
464
+ const response = await fetch(input);
465
+ const arrayBuffer = await response.arrayBuffer();
466
+ uint8Array = new Uint8Array(arrayBuffer);
467
+ }
468
+ else if (input instanceof Uint8Array) {
469
+ uint8Array = input;
470
+ }
471
+ else {
472
+ throw new TypeError("input must be a URL string or Uint8Array");
473
+ }
474
+ const parsed = (0, soundfont_parser_1.parse)(uint8Array);
513
475
  const soundFont = new soundfont_parser_1.SoundFont(parsed);
514
476
  this.addSoundFont(soundFont);
515
477
  }
516
- async loadMIDI(midiUrl) {
517
- const response = await fetch(midiUrl);
518
- const arrayBuffer = await response.arrayBuffer();
519
- const midi = (0, midi_file_1.parseMidi)(new Uint8Array(arrayBuffer));
478
+ async loadMIDI(input) {
479
+ let uint8Array;
480
+ if (typeof input === "string") {
481
+ const response = await fetch(input);
482
+ const arrayBuffer = await response.arrayBuffer();
483
+ uint8Array = new Uint8Array(arrayBuffer);
484
+ }
485
+ else if (input instanceof Uint8Array) {
486
+ uint8Array = input;
487
+ }
488
+ else {
489
+ throw new TypeError("input must be a URL string or Uint8Array");
490
+ }
491
+ const midi = (0, midi_file_1.parseMidi)(uint8Array);
520
492
  this.ticksPerBeat = midi.header.ticksPerBeat;
521
493
  const midiData = this.extractMidiData(midi);
522
494
  this.instruments = midiData.instruments;
@@ -537,21 +509,28 @@ class MidyGM2 {
537
509
  merger,
538
510
  };
539
511
  }
512
+ resetChannelTable(channel) {
513
+ this.resetControlTable(channel.controlTable);
514
+ channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
515
+ channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
516
+ channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
517
+ channel.keyBasedInstrumentControlTable.fill(-1);
518
+ }
540
519
  createChannels(audioContext) {
541
520
  const channels = Array.from({ length: this.numChannels }, () => {
542
521
  return {
543
522
  currentBufferSource: null,
544
523
  isDrum: false,
545
- ...this.constructor.channelSettings,
546
524
  state: new ControllerState(),
547
- controlTable: this.initControlTable(),
525
+ ...this.constructor.channelSettings,
548
526
  ...this.setChannelAudioNodes(audioContext),
549
- scheduledNotes: new SparseMap(128),
527
+ scheduledNotes: [],
550
528
  sustainNotes: [],
551
- sostenutoNotes: new SparseMap(128),
529
+ sostenutoNotes: [],
530
+ controlTable: this.initControlTable(),
552
531
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
553
532
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
554
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
533
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
555
534
  };
556
535
  });
557
536
  return channels;
@@ -585,56 +564,39 @@ class MidyGM2 {
585
564
  return audioBuffer;
586
565
  }
587
566
  }
588
- createBufferSource(voiceParams, audioBuffer) {
567
+ isLoopDrum(channel, noteNumber) {
568
+ const programNumber = channel.programNumber;
569
+ return ((programNumber === 48 && noteNumber === 88) ||
570
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
571
+ }
572
+ createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
589
573
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
590
574
  bufferSource.buffer = audioBuffer;
591
575
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
576
+ if (channel.isDrum) {
577
+ bufferSource.loop = this.isLoopDrum(channel, noteNumber);
578
+ }
592
579
  if (bufferSource.loop) {
593
580
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
594
581
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
595
582
  }
596
583
  return bufferSource;
597
584
  }
598
- findPortamentoTarget(queueIndex) {
599
- const endEvent = this.timeline[queueIndex];
600
- if (!this.channels[endEvent.channel].portamento)
601
- return;
602
- const endTime = endEvent.startTime;
603
- let target;
604
- while (++queueIndex < this.timeline.length) {
605
- const event = this.timeline[queueIndex];
606
- if (endTime !== event.startTime)
607
- break;
608
- if (event.type !== "noteOn")
609
- continue;
610
- if (!target || event.noteNumber < target.noteNumber) {
611
- target = event;
612
- }
613
- }
614
- return target;
615
- }
616
- async scheduleTimelineEvents(t, offset, queueIndex) {
585
+ async scheduleTimelineEvents(t, resumeTime, queueIndex) {
617
586
  while (queueIndex < this.timeline.length) {
618
587
  const event = this.timeline[queueIndex];
619
588
  if (event.startTime > t + this.lookAhead)
620
589
  break;
621
- const startTime = event.startTime + this.startDelay - offset;
590
+ const delay = this.startDelay - resumeTime;
591
+ const startTime = event.startTime + delay;
622
592
  switch (event.type) {
623
593
  case "noteOn":
624
- if (0 < event.velocity) {
625
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
626
- break;
627
- }
628
- /* falls through */
594
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
595
+ break;
629
596
  case "noteOff": {
630
- const portamentoTarget = this.findPortamentoTarget(queueIndex);
631
- if (portamentoTarget)
632
- portamentoTarget.portamento = true;
633
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
634
- portamentoTarget?.noteNumber);
635
- if (notePromise) {
597
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
598
+ if (notePromise)
636
599
  this.notePromises.push(notePromise);
637
- }
638
600
  break;
639
601
  }
640
602
  case "controller":
@@ -670,7 +632,7 @@ class MidyGM2 {
670
632
  this.isPaused = false;
671
633
  this.startTime = this.audioContext.currentTime;
672
634
  let queueIndex = this.getQueueIndex(this.resumeTime);
673
- let offset = this.resumeTime - this.startTime;
635
+ let resumeTime = this.resumeTime - this.startTime;
674
636
  this.notePromises = [];
675
637
  const schedulePlayback = async () => {
676
638
  if (queueIndex >= this.timeline.length) {
@@ -679,18 +641,21 @@ class MidyGM2 {
679
641
  this.exclusiveClassNotes.fill(undefined);
680
642
  this.drumExclusiveClassNotes.fill(undefined);
681
643
  this.audioBufferCache.clear();
644
+ for (let i = 0; i < this.channels.length; i++) {
645
+ this.resetAllStates(i);
646
+ }
682
647
  resolve();
683
648
  return;
684
649
  }
685
650
  const now = this.audioContext.currentTime;
686
- const t = now + offset;
687
- queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
651
+ const t = now + resumeTime;
652
+ queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
688
653
  if (this.isPausing) {
689
654
  await this.stopNotes(0, true, now);
690
655
  this.notePromises = [];
691
- resolve();
692
656
  this.isPausing = false;
693
657
  this.isPaused = true;
658
+ resolve();
694
659
  return;
695
660
  }
696
661
  else if (this.isStopping) {
@@ -699,9 +664,12 @@ class MidyGM2 {
699
664
  this.exclusiveClassNotes.fill(undefined);
700
665
  this.drumExclusiveClassNotes.fill(undefined);
701
666
  this.audioBufferCache.clear();
702
- resolve();
667
+ for (let i = 0; i < this.channels.length; i++) {
668
+ this.resetAllStates(i);
669
+ }
703
670
  this.isStopping = false;
704
671
  this.isPaused = false;
672
+ resolve();
705
673
  return;
706
674
  }
707
675
  else if (this.isSeeking) {
@@ -710,7 +678,7 @@ class MidyGM2 {
710
678
  this.drumExclusiveClassNotes.fill(undefined);
711
679
  this.startTime = this.audioContext.currentTime;
712
680
  queueIndex = this.getQueueIndex(this.resumeTime);
713
- offset = this.resumeTime - this.startTime;
681
+ resumeTime = this.resumeTime - this.startTime;
714
682
  this.isSeeking = false;
715
683
  await schedulePlayback();
716
684
  }
@@ -733,6 +701,7 @@ class MidyGM2 {
733
701
  return `${programNumber}:${noteNumber}:${velocity}`;
734
702
  }
735
703
  extractMidiData(midi) {
704
+ this.audioBufferCounter.clear();
736
705
  const instruments = new Set();
737
706
  const timeline = [];
738
707
  const tmpChannels = new Array(this.channels.length);
@@ -837,9 +806,8 @@ class MidyGM2 {
837
806
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
838
807
  const channel = this.channels[channelNumber];
839
808
  const promises = [];
840
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
841
- activeNotes.forEach((note) => {
842
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
809
+ this.processActiveNotes(channel, scheduleTime, (note) => {
810
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
843
811
  this.notePromises.push(promise);
844
812
  promises.push(promise);
845
813
  });
@@ -849,11 +817,11 @@ class MidyGM2 {
849
817
  const channel = this.channels[channelNumber];
850
818
  const promises = [];
851
819
  this.processScheduledNotes(channel, (note) => {
852
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
820
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
853
821
  this.notePromises.push(promise);
854
822
  promises.push(promise);
855
823
  });
856
- channel.scheduledNotes.clear();
824
+ channel.scheduledNotes = [];
857
825
  return Promise.all(promises);
858
826
  }
859
827
  stopNotes(velocity, force, scheduleTime) {
@@ -874,9 +842,6 @@ class MidyGM2 {
874
842
  if (!this.isPlaying)
875
843
  return;
876
844
  this.isStopping = true;
877
- for (let i = 0; i < this.channels.length; i++) {
878
- this.resetAllStates(i);
879
- }
880
845
  }
881
846
  pause() {
882
847
  if (!this.isPlaying || this.isPaused)
@@ -911,37 +876,28 @@ class MidyGM2 {
911
876
  return this.resumeTime + now - this.startTime - this.startDelay;
912
877
  }
913
878
  processScheduledNotes(channel, callback) {
914
- channel.scheduledNotes.forEach((noteList) => {
915
- for (let i = 0; i < noteList.length; i++) {
916
- const note = noteList[i];
917
- if (!note)
918
- continue;
919
- if (note.ending)
920
- continue;
921
- callback(note);
922
- }
923
- });
924
- }
925
- getActiveNotes(channel, scheduleTime) {
926
- const activeNotes = new SparseMap(128);
927
- channel.scheduledNotes.forEach((noteList) => {
928
- const activeNote = this.getActiveNote(noteList, scheduleTime);
929
- if (activeNote) {
930
- activeNotes.set(activeNote.noteNumber, activeNote);
931
- }
932
- });
933
- return activeNotes;
879
+ const scheduledNotes = channel.scheduledNotes;
880
+ for (let i = 0; i < scheduledNotes.length; i++) {
881
+ const note = scheduledNotes[i];
882
+ if (!note)
883
+ continue;
884
+ if (note.ending)
885
+ continue;
886
+ callback(note);
887
+ }
934
888
  }
935
- getActiveNote(noteList, scheduleTime) {
936
- for (let i = noteList.length - 1; i >= 0; i--) {
937
- const note = noteList[i];
889
+ processActiveNotes(channel, scheduleTime, callback) {
890
+ const scheduledNotes = channel.scheduledNotes;
891
+ for (let i = 0; i < scheduledNotes.length; i++) {
892
+ const note = scheduledNotes[i];
938
893
  if (!note)
939
- return;
894
+ continue;
895
+ if (note.ending)
896
+ continue;
940
897
  if (scheduleTime < note.startTime)
941
898
  continue;
942
- return (note.ending) ? null : note;
899
+ callback(note);
943
900
  }
944
- return noteList[0];
945
901
  }
946
902
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
947
903
  const sampleRate = audioContext.sampleRate;
@@ -1107,24 +1063,93 @@ class MidyGM2 {
1107
1063
  updateDetune(channel, note, scheduleTime) {
1108
1064
  const noteDetune = this.calcNoteDetune(channel, note);
1109
1065
  const detune = channel.detune + noteDetune;
1110
- note.bufferSource.detune
1111
- .cancelScheduledValues(scheduleTime)
1112
- .setValueAtTime(detune, scheduleTime);
1113
- }
1114
- getPortamentoTime(channel) {
1115
- const factor = 5 * Math.log(10) * 127;
1116
- return channel.state.portamentoTime * factor;
1117
- }
1118
- setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1066
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1067
+ const startTime = note.startTime;
1068
+ const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1069
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1070
+ note.bufferSource.detune
1071
+ .cancelScheduledValues(scheduleTime)
1072
+ .setValueAtTime(detune - deltaCent, scheduleTime)
1073
+ .linearRampToValueAtTime(detune, portamentoTime);
1074
+ }
1075
+ else {
1076
+ note.bufferSource.detune
1077
+ .cancelScheduledValues(scheduleTime)
1078
+ .setValueAtTime(detune, scheduleTime);
1079
+ }
1080
+ }
1081
+ getPortamentoTime(channel, note) {
1082
+ const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1083
+ const value = Math.ceil(channel.state.portamentoTime * 127);
1084
+ return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1085
+ }
1086
+ getPitchIncrementSpeed(value) {
1087
+ const points = [
1088
+ [0, 1000],
1089
+ [6, 100],
1090
+ [16, 20],
1091
+ [32, 10],
1092
+ [48, 5],
1093
+ [64, 2.5],
1094
+ [80, 1],
1095
+ [96, 0.4],
1096
+ [112, 0.15],
1097
+ [127, 0.01],
1098
+ ];
1099
+ const logPoints = new Array(points.length);
1100
+ for (let i = 0; i < points.length; i++) {
1101
+ const [x, y] = points[i];
1102
+ if (value === x)
1103
+ return y;
1104
+ logPoints[i] = [x, Math.log(y)];
1105
+ }
1106
+ let startIndex = 0;
1107
+ for (let i = 1; i < logPoints.length; i++) {
1108
+ if (value <= logPoints[i][0]) {
1109
+ startIndex = i - 1;
1110
+ break;
1111
+ }
1112
+ }
1113
+ const [x0, y0] = logPoints[startIndex];
1114
+ const [x1, y1] = logPoints[startIndex + 1];
1115
+ const h = x1 - x0;
1116
+ const t = (value - x0) / h;
1117
+ let m0, m1;
1118
+ if (startIndex === 0) {
1119
+ m0 = (y1 - y0) / h;
1120
+ }
1121
+ else {
1122
+ const [xPrev, yPrev] = logPoints[startIndex - 1];
1123
+ m0 = (y1 - yPrev) / (x1 - xPrev);
1124
+ }
1125
+ if (startIndex === logPoints.length - 2) {
1126
+ m1 = (y1 - y0) / h;
1127
+ }
1128
+ else {
1129
+ const [xNext, yNext] = logPoints[startIndex + 2];
1130
+ m1 = (yNext - y0) / (xNext - x0);
1131
+ }
1132
+ // Cubic Hermite Spline
1133
+ const t2 = t * t;
1134
+ const t3 = t2 * t;
1135
+ const h00 = 2 * t3 - 3 * t2 + 1;
1136
+ const h10 = t3 - 2 * t2 + t;
1137
+ const h01 = -2 * t3 + 3 * t2;
1138
+ const h11 = t3 - t2;
1139
+ const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
1140
+ return Math.exp(y);
1141
+ }
1142
+ setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1119
1143
  const { voiceParams, startTime } = note;
1120
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1144
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1145
+ (1 + this.getAmplitudeControl(channel));
1121
1146
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1122
1147
  const volDelay = startTime + voiceParams.volDelay;
1123
- const portamentoTime = volDelay + this.getPortamentoTime(channel);
1148
+ const volAttack = volDelay + voiceParams.volAttack;
1149
+ const volHold = volAttack + voiceParams.volHold;
1124
1150
  note.volumeEnvelopeNode.gain
1125
1151
  .cancelScheduledValues(scheduleTime)
1126
- .setValueAtTime(0, volDelay)
1127
- .linearRampToValueAtTime(sustainVolume, portamentoTime);
1152
+ .setValueAtTime(sustainVolume, volHold);
1128
1153
  }
1129
1154
  setVolumeEnvelope(channel, note, scheduleTime) {
1130
1155
  const { voiceParams, startTime } = note;
@@ -1143,6 +1168,12 @@ class MidyGM2 {
1143
1168
  .setValueAtTime(attackVolume, volHold)
1144
1169
  .linearRampToValueAtTime(sustainVolume, volDecay);
1145
1170
  }
1171
+ setPortamentoPitchEnvelope(note, scheduleTime) {
1172
+ const baseRate = note.voiceParams.playbackRate;
1173
+ note.bufferSource.playbackRate
1174
+ .cancelScheduledValues(scheduleTime)
1175
+ .setValueAtTime(baseRate, scheduleTime);
1176
+ }
1146
1177
  setPitchEnvelope(note, scheduleTime) {
1147
1178
  const { voiceParams } = note;
1148
1179
  const baseRate = voiceParams.playbackRate;
@@ -1170,19 +1201,18 @@ class MidyGM2 {
1170
1201
  const maxFrequency = 20000; // max Hz of initialFilterFc
1171
1202
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1172
1203
  }
1173
- setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1174
- const state = channel.state;
1175
- const { voiceParams, noteNumber, startTime } = note;
1176
- const softPedalFactor = 1 -
1177
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1178
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1179
- softPedalFactor;
1204
+ setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1205
+ const { voiceParams, startTime } = note;
1206
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1207
+ const baseCent = voiceParams.initialFilterFc +
1208
+ this.getFilterCutoffControl(channel);
1209
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1180
1210
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
1181
1211
  const sustainFreq = baseFreq +
1182
1212
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1183
1213
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1184
1214
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1185
- const portamentoTime = startTime + this.getPortamentoTime(channel);
1215
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1186
1216
  const modDelay = startTime + voiceParams.modDelay;
1187
1217
  note.filterNode.frequency
1188
1218
  .cancelScheduledValues(scheduleTime)
@@ -1191,10 +1221,8 @@ class MidyGM2 {
1191
1221
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1192
1222
  }
1193
1223
  setFilterEnvelope(channel, note, scheduleTime) {
1194
- const state = channel.state;
1195
- const { voiceParams, noteNumber, startTime } = note;
1196
- const softPedalFactor = 1 -
1197
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1224
+ const { voiceParams, startTime } = note;
1225
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1198
1226
  const baseCent = voiceParams.initialFilterFc +
1199
1227
  this.getFilterCutoffControl(channel);
1200
1228
  const baseFreq = this.centToHz(baseCent) * softPedalFactor;
@@ -1267,14 +1295,14 @@ class MidyGM2 {
1267
1295
  return audioBuffer;
1268
1296
  }
1269
1297
  }
1270
- async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1298
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1271
1299
  const now = this.audioContext.currentTime;
1272
1300
  const state = channel.state;
1273
1301
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1274
1302
  const voiceParams = voice.getAllParams(controllerState);
1275
1303
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1276
1304
  const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1277
- note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1305
+ note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1278
1306
  note.volumeNode = new GainNode(this.audioContext);
1279
1307
  note.gainL = new GainNode(this.audioContext);
1280
1308
  note.gainR = new GainNode(this.audioContext);
@@ -1283,20 +1311,24 @@ class MidyGM2 {
1283
1311
  type: "lowpass",
1284
1312
  Q: voiceParams.initialFilterQ / 10, // dB
1285
1313
  });
1286
- if (0.5 <= state.portamento && portamento) {
1287
- note.portamento = true;
1288
- this.setPortamentoStartVolumeEnvelope(channel, note, now);
1289
- this.setPortamentoStartFilterEnvelope(channel, note, now);
1314
+ const prevNote = channel.scheduledNotes.at(-1);
1315
+ if (prevNote && prevNote.noteNumber !== noteNumber) {
1316
+ note.portamentoNoteNumber = prevNote.noteNumber;
1317
+ }
1318
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1319
+ this.setPortamentoVolumeEnvelope(channel, note, now);
1320
+ this.setPortamentoFilterEnvelope(channel, note, now);
1321
+ this.setPortamentoPitchEnvelope(note, now);
1290
1322
  }
1291
1323
  else {
1292
- note.portamento = false;
1293
1324
  this.setVolumeEnvelope(channel, note, now);
1294
1325
  this.setFilterEnvelope(channel, note, now);
1326
+ this.setPitchEnvelope(note, now);
1295
1327
  }
1328
+ this.updateDetune(channel, note, now);
1296
1329
  if (0 < state.vibratoDepth) {
1297
1330
  this.startVibrato(channel, note, now);
1298
1331
  }
1299
- this.setPitchEnvelope(note, now);
1300
1332
  if (0 < state.modulationDepth) {
1301
1333
  this.startModulation(channel, note, now);
1302
1334
  }
@@ -1309,10 +1341,10 @@ class MidyGM2 {
1309
1341
  note.volumeEnvelopeNode.connect(note.volumeNode);
1310
1342
  note.volumeNode.connect(note.gainL);
1311
1343
  note.volumeNode.connect(note.gainR);
1312
- if (0 < channel.chorusSendLevel) {
1344
+ if (0 < state.chorusSendLevel) {
1313
1345
  this.setChorusEffectsSend(channel, note, 0, now);
1314
1346
  }
1315
- if (0 < channel.reverbSendLevel) {
1347
+ if (0 < state.reverbSendLevel) {
1316
1348
  this.setReverbEffectsSend(channel, note, 0, now);
1317
1349
  }
1318
1350
  note.bufferSource.start(startTime);
@@ -1343,8 +1375,7 @@ class MidyGM2 {
1343
1375
  const [prevNote, prevChannelNumber] = prev;
1344
1376
  if (prevNote && !prevNote.ending) {
1345
1377
  this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1346
- startTime, true, // force
1347
- undefined);
1378
+ startTime, true);
1348
1379
  }
1349
1380
  }
1350
1381
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
@@ -1364,8 +1395,7 @@ class MidyGM2 {
1364
1395
  const prevNote = this.drumExclusiveClassNotes[index];
1365
1396
  if (prevNote && !prevNote.ending) {
1366
1397
  this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1367
- startTime, true, // force
1368
- undefined);
1398
+ startTime, true);
1369
1399
  }
1370
1400
  this.drumExclusiveClassNotes[index] = note;
1371
1401
  }
@@ -1376,7 +1406,7 @@ class MidyGM2 {
1376
1406
  return !((programNumber === 48 && noteNumber === 88) ||
1377
1407
  (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1378
1408
  }
1379
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1409
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1380
1410
  const channel = this.channels[channelNumber];
1381
1411
  const bankNumber = this.calcBank(channel, channelNumber);
1382
1412
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1387,7 +1417,8 @@ class MidyGM2 {
1387
1417
  if (!voice)
1388
1418
  return;
1389
1419
  const isSF3 = soundFont.parsed.info.version.major === 3;
1390
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1420
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1421
+ note.noteOffEvent = noteOffEvent;
1391
1422
  note.gainL.connect(channel.gainL);
1392
1423
  note.gainR.connect(channel.gainR);
1393
1424
  if (0.5 <= channel.state.sustainPedal) {
@@ -1396,31 +1427,12 @@ class MidyGM2 {
1396
1427
  this.handleExclusiveClass(note, channelNumber, startTime);
1397
1428
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1398
1429
  const scheduledNotes = channel.scheduledNotes;
1399
- let noteList = scheduledNotes.get(noteNumber);
1400
- if (noteList) {
1401
- noteList.push(note);
1402
- }
1403
- else {
1404
- noteList = [note];
1405
- scheduledNotes.set(noteNumber, noteList);
1406
- }
1407
- if (this.isDrumNoteOffException(channel, noteNumber)) {
1408
- const stopTime = startTime + note.bufferSource.buffer.duration;
1409
- const index = noteList.length - 1;
1410
- const promise = new Promise((resolve) => {
1411
- note.bufferSource.onended = () => {
1412
- noteList[index] = undefined;
1413
- this.disconnectNote(note);
1414
- resolve();
1415
- };
1416
- note.bufferSource.stop(stopTime);
1417
- });
1418
- this.notePromises.push(promise);
1419
- }
1430
+ note.index = scheduledNotes.length;
1431
+ scheduledNotes.push(note);
1420
1432
  }
1421
1433
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1422
1434
  scheduleTime ??= this.audioContext.currentTime;
1423
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1435
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1424
1436
  }
1425
1437
  disconnectNote(note) {
1426
1438
  note.bufferSource.disconnect();
@@ -1445,84 +1457,71 @@ class MidyGM2 {
1445
1457
  note.chorusEffectsSend.disconnect();
1446
1458
  }
1447
1459
  }
1448
- stopNote(endTime, stopTime, noteList, index) {
1449
- const note = noteList[index];
1460
+ releaseNote(channel, note, endTime) {
1461
+ const volRelease = endTime + note.voiceParams.volRelease;
1462
+ const modRelease = endTime + note.voiceParams.modRelease;
1463
+ const stopTime = Math.min(volRelease, modRelease);
1464
+ note.filterNode.frequency
1465
+ .cancelScheduledValues(endTime)
1466
+ .linearRampToValueAtTime(0, modRelease);
1450
1467
  note.volumeEnvelopeNode.gain
1451
1468
  .cancelScheduledValues(endTime)
1452
- .linearRampToValueAtTime(0, stopTime);
1453
- note.ending = true;
1454
- this.scheduleTask(() => {
1455
- note.bufferSource.loop = false;
1456
- }, stopTime);
1469
+ .linearRampToValueAtTime(0, volRelease);
1457
1470
  return new Promise((resolve) => {
1458
- note.bufferSource.onended = () => {
1459
- noteList[index] = undefined;
1471
+ this.scheduleTask(() => {
1472
+ const bufferSource = note.bufferSource;
1473
+ bufferSource.loop = false;
1474
+ bufferSource.stop(stopTime);
1460
1475
  this.disconnectNote(note);
1476
+ channel.scheduledNotes[note.index] = undefined;
1461
1477
  resolve();
1462
- };
1463
- note.bufferSource.stop(stopTime);
1478
+ }, stopTime);
1464
1479
  });
1465
1480
  }
1466
- findNoteOffTarget(noteList) {
1467
- for (let i = 0; i < noteList.length; i++) {
1468
- const note = noteList[i];
1469
- if (!note)
1470
- continue;
1471
- if (note.ending)
1472
- continue;
1473
- return [note, i];
1474
- }
1475
- }
1476
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1481
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1477
1482
  const channel = this.channels[channelNumber];
1478
- if (this.isDrumNoteOffException(channel, noteNumber))
1479
- return;
1480
1483
  const state = channel.state;
1481
1484
  if (!force) {
1482
- if (0.5 <= state.sustainPedal)
1483
- return;
1484
- if (channel.sostenutoNotes.has(noteNumber))
1485
- return;
1486
- }
1487
- const noteList = channel.scheduledNotes.get(noteNumber);
1488
- if (!noteList)
1489
- return; // be careful with drum channel
1490
- const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
1491
- if (!noteOffTarget)
1492
- return;
1493
- const [note, i] = noteOffTarget;
1494
- if (0.5 <= state.portamento && portamentoNoteNumber !== undefined) {
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, noteList, i);
1485
+ if (channel.isDrum) {
1486
+ if (!this.isLoopDrum(channel, noteNumber))
1487
+ return;
1488
+ }
1489
+ else {
1490
+ if (0.5 <= state.sustainPedal)
1491
+ return;
1492
+ if (0.5 <= state.sostenutoPedal)
1493
+ return;
1494
+ }
1503
1495
  }
1504
- else {
1505
- const volRelease = endTime +
1506
- note.voiceParams.volRelease * state.releaseTime * 2;
1507
- const modRelease = endTime + note.voiceParams.modRelease;
1508
- note.filterNode.frequency
1509
- .cancelScheduledValues(endTime)
1510
- .linearRampToValueAtTime(0, modRelease);
1511
- const stopTime = Math.min(volRelease, modRelease);
1512
- return this.stopNote(endTime, stopTime, noteList, i);
1496
+ const note = this.findNoteOffTarget(channel, noteNumber);
1497
+ if (!note)
1498
+ return;
1499
+ note.ending = true;
1500
+ this.releaseNote(channel, note, endTime);
1501
+ }
1502
+ findNoteOffTarget(channel, noteNumber) {
1503
+ const scheduledNotes = channel.scheduledNotes;
1504
+ for (let i = 0; i < scheduledNotes.length; i++) {
1505
+ const note = scheduledNotes[i];
1506
+ if (!note)
1507
+ continue;
1508
+ if (note.ending)
1509
+ continue;
1510
+ if (note.noteNumber !== noteNumber)
1511
+ continue;
1512
+ return note;
1513
1513
  }
1514
1514
  }
1515
1515
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1516
1516
  scheduleTime ??= this.audioContext.currentTime;
1517
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1518
- undefined);
1517
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1519
1518
  }
1520
1519
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1521
1520
  const velocity = halfVelocity * 2;
1522
1521
  const channel = this.channels[channelNumber];
1523
1522
  const promises = [];
1524
1523
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1525
- const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1524
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1526
1525
  promises.push(promise);
1527
1526
  }
1528
1527
  channel.sustainNotes = [];
@@ -1532,12 +1531,14 @@ class MidyGM2 {
1532
1531
  const velocity = halfVelocity * 2;
1533
1532
  const channel = this.channels[channelNumber];
1534
1533
  const promises = [];
1534
+ const sostenutoNotes = channel.sostenutoNotes;
1535
1535
  channel.state.sostenutoPedal = 0;
1536
- channel.sostenutoNotes.forEach((note) => {
1537
- const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1536
+ for (let i = 0; i < sostenutoNotes.length; i++) {
1537
+ const note = sostenutoNotes[i];
1538
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1538
1539
  promises.push(promise);
1539
- });
1540
- channel.sostenutoNotes.clear();
1540
+ }
1541
+ channel.sostenutoNotes = [];
1541
1542
  return promises;
1542
1543
  }
1543
1544
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
@@ -1587,7 +1588,7 @@ class MidyGM2 {
1587
1588
  channel.detune += pressureDepth * (next - prev);
1588
1589
  }
1589
1590
  const table = channel.channelPressureTable;
1590
- this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1591
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1591
1592
  this.setControllerParameters(channel, note, table);
1592
1593
  });
1593
1594
  this.applyVoiceParams(channel, 13);
@@ -1644,10 +1645,13 @@ class MidyGM2 {
1644
1645
  .setValueAtTime(volumeDepth, scheduleTime);
1645
1646
  }
1646
1647
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1648
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1649
+ let value = note.voiceParams.reverbEffectsSend;
1650
+ if (0 <= keyBasedValue) {
1651
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1652
+ }
1647
1653
  if (0 < prevValue) {
1648
- if (0 < note.voiceParams.reverbEffectsSend) {
1649
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1650
- const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1654
+ if (0 < value) {
1651
1655
  note.reverbEffectsSend.gain
1652
1656
  .cancelScheduledValues(scheduleTime)
1653
1657
  .setValueAtTime(value, scheduleTime);
@@ -1657,10 +1661,10 @@ class MidyGM2 {
1657
1661
  }
1658
1662
  }
1659
1663
  else {
1660
- if (0 < note.voiceParams.reverbEffectsSend) {
1664
+ if (0 < value) {
1661
1665
  if (!note.reverbEffectsSend) {
1662
1666
  note.reverbEffectsSend = new GainNode(this.audioContext, {
1663
- gain: note.voiceParams.reverbEffectsSend,
1667
+ gain: value,
1664
1668
  });
1665
1669
  note.volumeNode.connect(note.reverbEffectsSend);
1666
1670
  }
@@ -1669,10 +1673,13 @@ class MidyGM2 {
1669
1673
  }
1670
1674
  }
1671
1675
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1676
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1677
+ let value = note.voiceParams.chorusEffectsSend;
1678
+ if (0 <= keyBasedValue) {
1679
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1680
+ }
1672
1681
  if (0 < prevValue) {
1673
- if (0 < note.voiceParams.chorusEffectsSend) {
1674
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1675
- const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1682
+ if (0 < vaule) {
1676
1683
  note.chorusEffectsSend.gain
1677
1684
  .cancelScheduledValues(scheduleTime)
1678
1685
  .setValueAtTime(value, scheduleTime);
@@ -1682,10 +1689,10 @@ class MidyGM2 {
1682
1689
  }
1683
1690
  }
1684
1691
  else {
1685
- if (0 < note.voiceParams.chorusEffectsSend) {
1692
+ if (0 < value) {
1686
1693
  if (!note.chorusEffectsSend) {
1687
1694
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1688
- gain: note.voiceParams.chorusEffectsSend,
1695
+ gain: value,
1689
1696
  });
1690
1697
  note.volumeNode.connect(note.chorusEffectsSend);
1691
1698
  }
@@ -1794,8 +1801,8 @@ class MidyGM2 {
1794
1801
  if (key in voiceParams)
1795
1802
  noteVoiceParams[key] = voiceParams[key];
1796
1803
  }
1797
- if (0.5 <= channel.state.portamento && note.portamento) {
1798
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1804
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1805
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1799
1806
  }
1800
1807
  else {
1801
1808
  this.setFilterEnvelope(channel, note, scheduleTime);
@@ -1818,32 +1825,32 @@ class MidyGM2 {
1818
1825
  });
1819
1826
  }
1820
1827
  createControlChangeHandlers() {
1821
- return {
1822
- 0: this.setBankMSB,
1823
- 1: this.setModulationDepth,
1824
- 5: this.setPortamentoTime,
1825
- 6: this.dataEntryMSB,
1826
- 7: this.setVolume,
1827
- 10: this.setPan,
1828
- 11: this.setExpression,
1829
- 32: this.setBankLSB,
1830
- 38: this.dataEntryLSB,
1831
- 64: this.setSustainPedal,
1832
- 65: this.setPortamento,
1833
- 66: this.setSostenutoPedal,
1834
- 67: this.setSoftPedal,
1835
- 91: this.setReverbSendLevel,
1836
- 93: this.setChorusSendLevel,
1837
- 100: this.setRPNLSB,
1838
- 101: this.setRPNMSB,
1839
- 120: this.allSoundOff,
1840
- 121: this.resetAllControllers,
1841
- 123: this.allNotesOff,
1842
- 124: this.omniOff,
1843
- 125: this.omniOn,
1844
- 126: this.monoOn,
1845
- 127: this.polyOn,
1846
- };
1828
+ const handlers = new Array(128);
1829
+ handlers[0] = this.setBankMSB;
1830
+ handlers[1] = this.setModulationDepth;
1831
+ handlers[5] = this.setPortamentoTime;
1832
+ handlers[6] = this.dataEntryMSB;
1833
+ handlers[7] = this.setVolume;
1834
+ handlers[10] = this.setPan;
1835
+ handlers[11] = this.setExpression;
1836
+ handlers[32] = this.setBankLSB;
1837
+ handlers[38] = this.dataEntryLSB;
1838
+ handlers[64] = this.setSustainPedal;
1839
+ handlers[65] = this.setPortamento;
1840
+ handlers[66] = this.setSostenutoPedal;
1841
+ handlers[67] = this.setSoftPedal;
1842
+ handlers[91] = this.setReverbSendLevel;
1843
+ handlers[93] = this.setChorusSendLevel;
1844
+ handlers[100] = this.setRPNLSB;
1845
+ handlers[101] = this.setRPNMSB;
1846
+ handlers[120] = this.allSoundOff;
1847
+ handlers[121] = this.resetAllControllers;
1848
+ handlers[123] = this.allNotesOff;
1849
+ handlers[124] = this.omniOff;
1850
+ handlers[125] = this.omniOn;
1851
+ handlers[126] = this.monoOn;
1852
+ handlers[127] = this.polyOn;
1853
+ return handlers;
1847
1854
  }
1848
1855
  handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1849
1856
  const handler = this.controlChangeHandlers[controllerType];
@@ -1880,17 +1887,41 @@ class MidyGM2 {
1880
1887
  channel.state.modulationDepth = modulation / 127;
1881
1888
  this.updateModulation(channel, scheduleTime);
1882
1889
  }
1883
- setPortamentoTime(channelNumber, portamentoTime) {
1890
+ updatePortamento(channel, scheduleTime) {
1891
+ this.processScheduledNotes(channel, (note) => {
1892
+ if (0.5 <= channel.state.portamento) {
1893
+ if (0 <= note.portamentoNoteNumber) {
1894
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1895
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1896
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
1897
+ this.updateDetune(channel, note, scheduleTime);
1898
+ }
1899
+ }
1900
+ else {
1901
+ if (0 <= note.portamentoNoteNumber) {
1902
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1903
+ this.setFilterEnvelope(channel, note, scheduleTime);
1904
+ this.setPitchEnvelope(note, scheduleTime);
1905
+ this.updateDetune(channel, note, scheduleTime);
1906
+ }
1907
+ }
1908
+ });
1909
+ }
1910
+ setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
1884
1911
  const channel = this.channels[channelNumber];
1912
+ scheduleTime ??= this.audioContext.currentTime;
1885
1913
  channel.state.portamentoTime = portamentoTime / 127;
1914
+ if (channel.isDrum)
1915
+ return;
1916
+ this.updatePortamento(channel, scheduleTime);
1886
1917
  }
1887
1918
  setKeyBasedVolume(channel, scheduleTime) {
1888
1919
  this.processScheduledNotes(channel, (note) => {
1889
1920
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1890
- if (keyBasedValue !== 0) {
1921
+ if (0 <= keyBasedValue) {
1891
1922
  note.volumeNode.gain
1892
1923
  .cancelScheduledValues(scheduleTime)
1893
- .setValueAtTime(1 + keyBasedValue, scheduleTime);
1924
+ .setValueAtTime(keyBasedValue / 127, scheduleTime);
1894
1925
  }
1895
1926
  });
1896
1927
  }
@@ -1911,8 +1942,8 @@ class MidyGM2 {
1911
1942
  setKeyBasedPan(channel, scheduleTime) {
1912
1943
  this.processScheduledNotes(channel, (note) => {
1913
1944
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1914
- if (keyBasedValue !== 0) {
1915
- const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1945
+ if (0 <= keyBasedValue) {
1946
+ const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
1916
1947
  note.gainL.gain
1917
1948
  .cancelScheduledValues(scheduleTime)
1918
1949
  .setValueAtTime(gainLeft, scheduleTime);
@@ -1968,11 +1999,13 @@ class MidyGM2 {
1968
1999
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
1969
2000
  }
1970
2001
  }
1971
- setPortamento(channelNumber, value) {
2002
+ setPortamento(channelNumber, value, scheduleTime) {
1972
2003
  const channel = this.channels[channelNumber];
1973
2004
  if (channel.isDrum)
1974
2005
  return;
2006
+ scheduleTime ??= this.audioContext.currentTime;
1975
2007
  channel.state.portamento = value / 127;
2008
+ this.updatePortamento(channel, scheduleTime);
1976
2009
  }
1977
2010
  setSostenutoPedal(channelNumber, value, scheduleTime) {
1978
2011
  const channel = this.channels[channelNumber];
@@ -1981,12 +2014,19 @@ class MidyGM2 {
1981
2014
  scheduleTime ??= this.audioContext.currentTime;
1982
2015
  channel.state.sostenutoPedal = value / 127;
1983
2016
  if (64 <= value) {
1984
- channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
2017
+ const sostenutoNotes = [];
2018
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2019
+ sostenutoNotes.push(note);
2020
+ });
2021
+ channel.sostenutoNotes = sostenutoNotes;
1985
2022
  }
1986
2023
  else {
1987
2024
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
1988
2025
  }
1989
2026
  }
2027
+ getSoftPedalFactor(channel, note) {
2028
+ return 1 - (0.1 + (note.noteNumber / 127) * 0.2) * channel.state.softPedal;
2029
+ }
1990
2030
  setSoftPedal(channelNumber, softPedal, scheduleTime) {
1991
2031
  const channel = this.channels[channelNumber];
1992
2032
  if (channel.isDrum)
@@ -1995,9 +2035,9 @@ class MidyGM2 {
1995
2035
  scheduleTime ??= this.audioContext.currentTime;
1996
2036
  state.softPedal = softPedal / 127;
1997
2037
  this.processScheduledNotes(channel, (note) => {
1998
- if (0.5 <= state.portamento && note.portamento) {
1999
- this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
2000
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2038
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2039
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2040
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2001
2041
  }
2002
2042
  else {
2003
2043
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2021,7 +2061,8 @@ class MidyGM2 {
2021
2061
  this.processScheduledNotes(channel, (note) => {
2022
2062
  if (note.voiceParams.reverbEffectsSend <= 0)
2023
2063
  return false;
2024
- note.reverbEffectsSend.disconnect();
2064
+ if (note.reverbEffectsSend)
2065
+ note.reverbEffectsSend.disconnect();
2025
2066
  });
2026
2067
  }
2027
2068
  }
@@ -2053,7 +2094,8 @@ class MidyGM2 {
2053
2094
  this.processScheduledNotes(channel, (note) => {
2054
2095
  if (note.voiceParams.chorusEffectsSend <= 0)
2055
2096
  return false;
2056
- note.chorusEffectsSend.disconnect();
2097
+ if (note.chorusEffectsSend)
2098
+ note.chorusEffectsSend.disconnect();
2057
2099
  });
2058
2100
  }
2059
2101
  }
@@ -2197,21 +2239,29 @@ class MidyGM2 {
2197
2239
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2198
2240
  }
2199
2241
  resetAllStates(channelNumber) {
2242
+ const scheduleTime = this.audioContext.currentTime;
2200
2243
  const channel = this.channels[channelNumber];
2201
2244
  const state = channel.state;
2202
- for (const type of Object.keys(defaultControllerState)) {
2203
- state[type] = defaultControllerState[type].defaultValue;
2245
+ const entries = Object.entries(defaultControllerState);
2246
+ for (const [key, { type, defaultValue }] of entries) {
2247
+ if (128 <= type) {
2248
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2249
+ }
2250
+ else {
2251
+ state[key] = defaultValue;
2252
+ }
2204
2253
  }
2205
- for (const type of Object.keys(this.constructor.channelSettings)) {
2206
- channel[type] = this.constructor.channelSettings[type];
2254
+ for (const key of Object.keys(this.constructor.channelSettings)) {
2255
+ channel[key] = this.constructor.channelSettings[key];
2207
2256
  }
2257
+ this.resetChannelTable(channel);
2208
2258
  this.mode = "GM2";
2209
2259
  this.masterFineTuning = 0; // cb
2210
2260
  this.masterCoarseTuning = 0; // cb
2211
2261
  }
2212
2262
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2213
- resetAllControllers(channelNumber) {
2214
- const stateTypes = [
2263
+ resetAllControllers(channelNumber, _value, scheduleTime) {
2264
+ const keys = [
2215
2265
  "channelPressure",
2216
2266
  "pitchWheel",
2217
2267
  "expression",
@@ -2223,10 +2273,17 @@ class MidyGM2 {
2223
2273
  ];
2224
2274
  const channel = this.channels[channelNumber];
2225
2275
  const state = channel.state;
2226
- for (let i = 0; i < stateTypes.length; i++) {
2227
- const type = stateTypes[i];
2228
- state[type] = defaultControllerState[type].defaultValue;
2276
+ for (let i = 0; i < keys.length; i++) {
2277
+ const key = keys[i];
2278
+ const { type, defaultValue } = defaultControllerState[key];
2279
+ if (128 <= type) {
2280
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2281
+ }
2282
+ else {
2283
+ state[key] = defaultValue;
2284
+ }
2229
2285
  }
2286
+ this.setPitchBend(channelNumber, 8192, scheduleTime);
2230
2287
  const settingTypes = [
2231
2288
  "rpnMSB",
2232
2289
  "rpnLSB",
@@ -2455,7 +2512,7 @@ class MidyGM2 {
2455
2512
  this.reverbEffect = options.reverbAlgorithm(audioContext);
2456
2513
  }
2457
2514
  getReverbTime(value) {
2458
- return Math.pow(Math.E, (value - 40) * 0.025);
2515
+ return Math.exp((value - 40) * 0.025);
2459
2516
  }
2460
2517
  // mean free path equation
2461
2518
  // https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
@@ -2612,6 +2669,8 @@ class MidyGM2 {
2612
2669
  if (!channelBitmap[i])
2613
2670
  continue;
2614
2671
  const channel = this.channels[i];
2672
+ if (channel.isDrum)
2673
+ continue;
2615
2674
  for (let j = 0; j < 12; j++) {
2616
2675
  const centValue = data[j + 7] - 64;
2617
2676
  channel.scaleOctaveTuningTable[j] = centValue;
@@ -2648,7 +2707,13 @@ class MidyGM2 {
2648
2707
  setControllerParameters(channel, note, table) {
2649
2708
  if (table[0] !== 64)
2650
2709
  this.updateDetune(channel, note);
2651
- if (!note.portamento) {
2710
+ if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2711
+ if (table[1] !== 64)
2712
+ this.setPortamentoFilterEnvelope(channel, note);
2713
+ if (table[2] !== 64)
2714
+ this.setPortamentoVolumeEnvelope(channel, note);
2715
+ }
2716
+ else {
2652
2717
  if (table[1] !== 64)
2653
2718
  this.setFilterEnvelope(channel, note);
2654
2719
  if (table[2] !== 64)
@@ -2663,7 +2728,10 @@ class MidyGM2 {
2663
2728
  }
2664
2729
  handlePressureSysEx(data, tableName) {
2665
2730
  const channelNumber = data[4];
2666
- const table = this.channels[channelNumber][tableName];
2731
+ const channel = this.channels[channelNumber];
2732
+ if (channel.isDrum)
2733
+ return;
2734
+ const table = channel[tableName];
2667
2735
  for (let i = 5; i < data.length - 1; i += 2) {
2668
2736
  const pp = data[i];
2669
2737
  const rr = data[i + 1];
@@ -2673,8 +2741,13 @@ class MidyGM2 {
2673
2741
  initControlTable() {
2674
2742
  const channelCount = 128;
2675
2743
  const slotSize = 6;
2676
- const defaultValues = [64, 64, 64, 0, 0, 0];
2677
2744
  const table = new Uint8Array(channelCount * slotSize);
2745
+ return this.resetControlTable(table);
2746
+ }
2747
+ resetControlTable(table) {
2748
+ const channelCount = 128;
2749
+ const slotSize = 6;
2750
+ const defaultValues = [64, 64, 64, 0, 0, 0];
2678
2751
  for (let ch = 0; ch < channelCount; ch++) {
2679
2752
  const offset = ch * slotSize;
2680
2753
  table.set(defaultValues, offset);
@@ -2691,8 +2764,11 @@ class MidyGM2 {
2691
2764
  }
2692
2765
  handleControlChangeSysEx(data) {
2693
2766
  const channelNumber = data[4];
2767
+ const channel = this.channels[channelNumber];
2768
+ if (channel.isDrum)
2769
+ return;
2694
2770
  const controllerType = data[5];
2695
- const table = this.channels[channelNumber].controlTable[controllerType];
2771
+ const table = channel.controlTable[controllerType];
2696
2772
  for (let i = 6; i < data.length - 1; i += 2) {
2697
2773
  const pp = data[i];
2698
2774
  const rr = data[i + 1];
@@ -2702,17 +2778,20 @@ class MidyGM2 {
2702
2778
  getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2703
2779
  const index = keyNumber * 128 + controllerType;
2704
2780
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2705
- return (controlValue + 64) / 64;
2781
+ return controlValue;
2706
2782
  }
2707
2783
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2708
2784
  const channelNumber = data[4];
2785
+ const channel = this.channels[channelNumber];
2786
+ if (channel.isDrum)
2787
+ return;
2709
2788
  const keyNumber = data[5];
2710
- const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2789
+ const table = channel.keyBasedInstrumentControlTable;
2711
2790
  for (let i = 6; i < data.length - 1; i += 2) {
2712
2791
  const controllerType = data[i];
2713
2792
  const value = data[i + 1];
2714
2793
  const index = keyNumber * 128 + controllerType;
2715
- table[index] = value - 64;
2794
+ table[index] = value;
2716
2795
  }
2717
2796
  this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2718
2797
  }