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