@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/script/midy.js CHANGED
@@ -3,60 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Midy = 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
  Object.defineProperty(this, "pressure", {
151
111
  enumerable: true,
@@ -207,7 +167,7 @@ const defaultControllerState = {
207
167
  portamentoTime: { type: 128 + 5, defaultValue: 0 },
208
168
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
209
169
  volume: { type: 128 + 7, defaultValue: 100 / 127 },
210
- pan: { type: 128 + 10, defaultValue: 0.5 },
170
+ pan: { type: 128 + 10, defaultValue: 64 / 127 },
211
171
  expression: { type: 128 + 11, defaultValue: 1 },
212
172
  // bankLSB: { type: 128 + 32, defaultValue: 0, },
213
173
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
@@ -215,14 +175,14 @@ const defaultControllerState = {
215
175
  portamento: { type: 128 + 65, defaultValue: 0 },
216
176
  sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
217
177
  softPedal: { type: 128 + 67, defaultValue: 0 },
218
- filterResonance: { type: 128 + 71, defaultValue: 0.5 },
219
- releaseTime: { type: 128 + 72, defaultValue: 0.5 },
220
- attackTime: { type: 128 + 73, defaultValue: 0.5 },
221
- brightness: { type: 128 + 74, defaultValue: 0.5 },
222
- decayTime: { type: 128 + 75, defaultValue: 0.5 },
223
- vibratoRate: { type: 128 + 76, defaultValue: 0.5 },
224
- vibratoDepth: { type: 128 + 77, defaultValue: 0.5 },
225
- vibratoDelay: { type: 128 + 78, defaultValue: 0.5 },
178
+ filterResonance: { type: 128 + 71, defaultValue: 64 / 127 },
179
+ releaseTime: { type: 128 + 72, defaultValue: 64 / 127 },
180
+ attackTime: { type: 128 + 73, defaultValue: 64 / 127 },
181
+ brightness: { type: 128 + 74, defaultValue: 64 / 127 },
182
+ decayTime: { type: 128 + 75, defaultValue: 64 / 127 },
183
+ vibratoRate: { type: 128 + 76, defaultValue: 64 / 127 },
184
+ vibratoDepth: { type: 128 + 77, defaultValue: 64 / 127 },
185
+ vibratoDelay: { type: 128 + 78, defaultValue: 64 / 127 },
226
186
  reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
227
187
  chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
228
188
  // dataIncrement: { type: 128 + 96, defaultValue: 0 },
@@ -497,7 +457,7 @@ class Midy {
497
457
  initSoundFontTable() {
498
458
  const table = new Array(128);
499
459
  for (let i = 0; i < 128; i++) {
500
- table[i] = new SparseMap(128);
460
+ table[i] = new Map();
501
461
  }
502
462
  return table;
503
463
  }
@@ -513,17 +473,37 @@ class Midy {
513
473
  }
514
474
  }
515
475
  }
516
- async loadSoundFont(soundFontUrl) {
517
- const response = await fetch(soundFontUrl);
518
- const arrayBuffer = await response.arrayBuffer();
519
- const parsed = (0, soundfont_parser_1.parse)(new Uint8Array(arrayBuffer));
476
+ async loadSoundFont(input) {
477
+ let uint8Array;
478
+ if (typeof input === "string") {
479
+ const response = await fetch(input);
480
+ const arrayBuffer = await response.arrayBuffer();
481
+ uint8Array = new Uint8Array(arrayBuffer);
482
+ }
483
+ else if (input instanceof Uint8Array) {
484
+ uint8Array = input;
485
+ }
486
+ else {
487
+ throw new TypeError("input must be a URL string or Uint8Array");
488
+ }
489
+ const parsed = (0, soundfont_parser_1.parse)(uint8Array);
520
490
  const soundFont = new soundfont_parser_1.SoundFont(parsed);
521
491
  this.addSoundFont(soundFont);
522
492
  }
523
- async loadMIDI(midiUrl) {
524
- const response = await fetch(midiUrl);
525
- const arrayBuffer = await response.arrayBuffer();
526
- const midi = (0, midi_file_1.parseMidi)(new Uint8Array(arrayBuffer));
493
+ async loadMIDI(input) {
494
+ let uint8Array;
495
+ if (typeof input === "string") {
496
+ const response = await fetch(input);
497
+ const arrayBuffer = await response.arrayBuffer();
498
+ uint8Array = new Uint8Array(arrayBuffer);
499
+ }
500
+ else if (input instanceof Uint8Array) {
501
+ uint8Array = input;
502
+ }
503
+ else {
504
+ throw new TypeError("input must be a URL string or Uint8Array");
505
+ }
506
+ const midi = (0, midi_file_1.parseMidi)(uint8Array);
527
507
  this.ticksPerBeat = midi.header.ticksPerBeat;
528
508
  const midiData = this.extractMidiData(midi);
529
509
  this.instruments = midiData.instruments;
@@ -544,22 +524,29 @@ class Midy {
544
524
  merger,
545
525
  };
546
526
  }
527
+ resetChannelTable(channel) {
528
+ this.resetControlTable(channel.controlTable);
529
+ channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
530
+ channel.channelPressureTable.set([64, 64, 64, 0, 0, 0]);
531
+ channel.polyphonicKeyPressureTable.set([64, 64, 64, 0, 0, 0]);
532
+ channel.keyBasedInstrumentControlTable.fill(-1);
533
+ }
547
534
  createChannels(audioContext) {
548
535
  const channels = Array.from({ length: this.numChannels }, () => {
549
536
  return {
550
537
  currentBufferSource: null,
551
538
  isDrum: false,
552
- ...this.constructor.channelSettings,
553
539
  state: new ControllerState(),
554
- controlTable: this.initControlTable(),
540
+ ...this.constructor.channelSettings,
555
541
  ...this.setChannelAudioNodes(audioContext),
556
- scheduledNotes: new SparseMap(128),
542
+ scheduledNotes: [],
557
543
  sustainNotes: [],
558
- sostenutoNotes: new SparseMap(128),
544
+ sostenutoNotes: [],
545
+ controlTable: this.initControlTable(),
559
546
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
560
547
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
561
548
  polyphonicKeyPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
562
- keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
549
+ keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
563
550
  };
564
551
  });
565
552
  return channels;
@@ -593,56 +580,39 @@ class Midy {
593
580
  return audioBuffer;
594
581
  }
595
582
  }
596
- createBufferSource(voiceParams, audioBuffer) {
583
+ isLoopDrum(channel, noteNumber) {
584
+ const programNumber = channel.programNumber;
585
+ return ((programNumber === 48 && noteNumber === 88) ||
586
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
587
+ }
588
+ createBufferSource(channel, noteNumber, voiceParams, audioBuffer) {
597
589
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
598
590
  bufferSource.buffer = audioBuffer;
599
591
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
592
+ if (channel.isDrum) {
593
+ bufferSource.loop = this.isLoopDrum(channel, noteNumber);
594
+ }
600
595
  if (bufferSource.loop) {
601
596
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
602
597
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
603
598
  }
604
599
  return bufferSource;
605
600
  }
606
- findPortamentoTarget(queueIndex) {
607
- const endEvent = this.timeline[queueIndex];
608
- if (!this.channels[endEvent.channel].portamento)
609
- return;
610
- const endTime = endEvent.startTime;
611
- let target;
612
- while (++queueIndex < this.timeline.length) {
613
- const event = this.timeline[queueIndex];
614
- if (endTime !== event.startTime)
615
- break;
616
- if (event.type !== "noteOn")
617
- continue;
618
- if (!target || event.noteNumber < target.noteNumber) {
619
- target = event;
620
- }
621
- }
622
- return target;
623
- }
624
- async scheduleTimelineEvents(t, offset, queueIndex) {
601
+ async scheduleTimelineEvents(t, resumeTime, queueIndex) {
625
602
  while (queueIndex < this.timeline.length) {
626
603
  const event = this.timeline[queueIndex];
627
604
  if (event.startTime > t + this.lookAhead)
628
605
  break;
629
- const startTime = event.startTime + this.startDelay - offset;
606
+ const delay = this.startDelay - resumeTime;
607
+ const startTime = event.startTime + delay;
630
608
  switch (event.type) {
631
609
  case "noteOn":
632
- if (0 < event.velocity) {
633
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
634
- break;
635
- }
636
- /* falls through */
610
+ await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
611
+ break;
637
612
  case "noteOff": {
638
- const portamentoTarget = this.findPortamentoTarget(queueIndex);
639
- if (portamentoTarget)
640
- portamentoTarget.portamento = true;
641
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
642
- portamentoTarget?.noteNumber);
643
- if (notePromise) {
613
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
614
+ if (notePromise)
644
615
  this.notePromises.push(notePromise);
645
- }
646
616
  break;
647
617
  }
648
618
  case "noteAftertouch":
@@ -681,7 +651,7 @@ class Midy {
681
651
  this.isPaused = false;
682
652
  this.startTime = this.audioContext.currentTime;
683
653
  let queueIndex = this.getQueueIndex(this.resumeTime);
684
- let offset = this.resumeTime - this.startTime;
654
+ let resumeTime = this.resumeTime - this.startTime;
685
655
  this.notePromises = [];
686
656
  const schedulePlayback = async () => {
687
657
  if (queueIndex >= this.timeline.length) {
@@ -690,18 +660,21 @@ class Midy {
690
660
  this.exclusiveClassNotes.fill(undefined);
691
661
  this.drumExclusiveClassNotes.fill(undefined);
692
662
  this.audioBufferCache.clear();
663
+ for (let i = 0; i < this.channels.length; i++) {
664
+ this.resetAllStates(i);
665
+ }
693
666
  resolve();
694
667
  return;
695
668
  }
696
669
  const now = this.audioContext.currentTime;
697
- const t = now + offset;
698
- queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
670
+ const t = now + resumeTime;
671
+ queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
699
672
  if (this.isPausing) {
700
673
  await this.stopNotes(0, true, now);
701
674
  this.notePromises = [];
702
- resolve();
703
675
  this.isPausing = false;
704
676
  this.isPaused = true;
677
+ resolve();
705
678
  return;
706
679
  }
707
680
  else if (this.isStopping) {
@@ -710,9 +683,12 @@ class Midy {
710
683
  this.exclusiveClassNotes.fill(undefined);
711
684
  this.drumExclusiveClassNotes.fill(undefined);
712
685
  this.audioBufferCache.clear();
713
- resolve();
686
+ for (let i = 0; i < this.channels.length; i++) {
687
+ this.resetAllStates(i);
688
+ }
714
689
  this.isStopping = false;
715
690
  this.isPaused = false;
691
+ resolve();
716
692
  return;
717
693
  }
718
694
  else if (this.isSeeking) {
@@ -721,7 +697,7 @@ class Midy {
721
697
  this.drumExclusiveClassNotes.fill(undefined);
722
698
  this.startTime = this.audioContext.currentTime;
723
699
  queueIndex = this.getQueueIndex(this.resumeTime);
724
- offset = this.resumeTime - this.startTime;
700
+ resumeTime = this.resumeTime - this.startTime;
725
701
  this.isSeeking = false;
726
702
  await schedulePlayback();
727
703
  }
@@ -744,6 +720,7 @@ class Midy {
744
720
  return `${programNumber}:${noteNumber}:${velocity}`;
745
721
  }
746
722
  extractMidiData(midi) {
723
+ this.audioBufferCounter.clear();
747
724
  const instruments = new Set();
748
725
  const timeline = [];
749
726
  const tmpChannels = new Array(this.channels.length);
@@ -848,9 +825,8 @@ class Midy {
848
825
  stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
849
826
  const channel = this.channels[channelNumber];
850
827
  const promises = [];
851
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
852
- activeNotes.forEach((note) => {
853
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
828
+ this.processActiveNotes(channel, scheduleTime, (note) => {
829
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
854
830
  this.notePromises.push(promise);
855
831
  promises.push(promise);
856
832
  });
@@ -860,11 +836,11 @@ class Midy {
860
836
  const channel = this.channels[channelNumber];
861
837
  const promises = [];
862
838
  this.processScheduledNotes(channel, (note) => {
863
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
839
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
864
840
  this.notePromises.push(promise);
865
841
  promises.push(promise);
866
842
  });
867
- channel.scheduledNotes.clear();
843
+ channel.scheduledNotes = [];
868
844
  return Promise.all(promises);
869
845
  }
870
846
  stopNotes(velocity, force, scheduleTime) {
@@ -885,9 +861,6 @@ class Midy {
885
861
  if (!this.isPlaying)
886
862
  return;
887
863
  this.isStopping = true;
888
- for (let i = 0; i < this.channels.length; i++) {
889
- this.resetAllStates(i);
890
- }
891
864
  }
892
865
  pause() {
893
866
  if (!this.isPlaying || this.isPaused)
@@ -922,37 +895,28 @@ class Midy {
922
895
  return this.resumeTime + now - this.startTime - this.startDelay;
923
896
  }
924
897
  processScheduledNotes(channel, callback) {
925
- channel.scheduledNotes.forEach((noteList) => {
926
- for (let i = 0; i < noteList.length; i++) {
927
- const note = noteList[i];
928
- if (!note)
929
- continue;
930
- if (note.ending)
931
- continue;
932
- callback(note);
933
- }
934
- });
935
- }
936
- getActiveNotes(channel, scheduleTime) {
937
- const activeNotes = new SparseMap(128);
938
- channel.scheduledNotes.forEach((noteList) => {
939
- const activeNote = this.getActiveNote(noteList, scheduleTime);
940
- if (activeNote) {
941
- activeNotes.set(activeNote.noteNumber, activeNote);
942
- }
943
- });
944
- return activeNotes;
898
+ const scheduledNotes = channel.scheduledNotes;
899
+ for (let i = 0; i < scheduledNotes.length; i++) {
900
+ const note = scheduledNotes[i];
901
+ if (!note)
902
+ continue;
903
+ if (note.ending)
904
+ continue;
905
+ callback(note);
906
+ }
945
907
  }
946
- getActiveNote(noteList, scheduleTime) {
947
- for (let i = noteList.length - 1; i >= 0; i--) {
948
- const note = noteList[i];
908
+ processActiveNotes(channel, scheduleTime, callback) {
909
+ const scheduledNotes = channel.scheduledNotes;
910
+ for (let i = 0; i < scheduledNotes.length; i++) {
911
+ const note = scheduledNotes[i];
949
912
  if (!note)
950
- return;
913
+ continue;
914
+ if (note.ending)
915
+ continue;
951
916
  if (scheduleTime < note.startTime)
952
917
  continue;
953
- return (note.ending) ? null : note;
918
+ callback(note);
954
919
  }
955
- return noteList[0];
956
920
  }
957
921
  createConvolutionReverbImpulse(audioContext, decay, preDecay) {
958
922
  const sampleRate = audioContext.sampleRate;
@@ -1119,24 +1083,94 @@ class Midy {
1119
1083
  const noteDetune = this.calcNoteDetune(channel, note);
1120
1084
  const pitchControl = this.getPitchControl(channel, note);
1121
1085
  const detune = channel.detune + noteDetune + pitchControl;
1122
- note.bufferSource.detune
1123
- .cancelScheduledValues(scheduleTime)
1124
- .setValueAtTime(detune, scheduleTime);
1125
- }
1126
- getPortamentoTime(channel) {
1127
- const factor = 5 * Math.log(10) * 127;
1128
- return channel.state.portamentoTime * factor;
1129
- }
1130
- setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1086
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1087
+ const startTime = note.startTime;
1088
+ const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1089
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1090
+ note.bufferSource.detune
1091
+ .cancelScheduledValues(scheduleTime)
1092
+ .setValueAtTime(detune - deltaCent, scheduleTime)
1093
+ .linearRampToValueAtTime(detune, portamentoTime);
1094
+ }
1095
+ else {
1096
+ note.bufferSource.detune
1097
+ .cancelScheduledValues(scheduleTime)
1098
+ .setValueAtTime(detune, scheduleTime);
1099
+ }
1100
+ }
1101
+ getPortamentoTime(channel, note) {
1102
+ const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1103
+ const value = Math.ceil(channel.state.portamentoTime * 127);
1104
+ return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1105
+ }
1106
+ getPitchIncrementSpeed(value) {
1107
+ const points = [
1108
+ [0, 1000],
1109
+ [6, 100],
1110
+ [16, 20],
1111
+ [32, 10],
1112
+ [48, 5],
1113
+ [64, 2.5],
1114
+ [80, 1],
1115
+ [96, 0.4],
1116
+ [112, 0.15],
1117
+ [127, 0.01],
1118
+ ];
1119
+ const logPoints = new Array(points.length);
1120
+ for (let i = 0; i < points.length; i++) {
1121
+ const [x, y] = points[i];
1122
+ if (value === x)
1123
+ return y;
1124
+ logPoints[i] = [x, Math.log(y)];
1125
+ }
1126
+ let startIndex = 0;
1127
+ for (let i = 1; i < logPoints.length; i++) {
1128
+ if (value <= logPoints[i][0]) {
1129
+ startIndex = i - 1;
1130
+ break;
1131
+ }
1132
+ }
1133
+ const [x0, y0] = logPoints[startIndex];
1134
+ const [x1, y1] = logPoints[startIndex + 1];
1135
+ const h = x1 - x0;
1136
+ const t = (value - x0) / h;
1137
+ let m0, m1;
1138
+ if (startIndex === 0) {
1139
+ m0 = (y1 - y0) / h;
1140
+ }
1141
+ else {
1142
+ const [xPrev, yPrev] = logPoints[startIndex - 1];
1143
+ m0 = (y1 - yPrev) / (x1 - xPrev);
1144
+ }
1145
+ if (startIndex === logPoints.length - 2) {
1146
+ m1 = (y1 - y0) / h;
1147
+ }
1148
+ else {
1149
+ const [xNext, yNext] = logPoints[startIndex + 2];
1150
+ m1 = (yNext - y0) / (xNext - x0);
1151
+ }
1152
+ // Cubic Hermite Spline
1153
+ const t2 = t * t;
1154
+ const t3 = t2 * t;
1155
+ const h00 = 2 * t3 - 3 * t2 + 1;
1156
+ const h10 = t3 - 2 * t2 + t;
1157
+ const h01 = -2 * t3 + 3 * t2;
1158
+ const h11 = t3 - t2;
1159
+ const y = h00 * y0 + h01 * y1 + h * (h10 * m0 + h11 * m1);
1160
+ return Math.exp(y);
1161
+ }
1162
+ setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1163
+ const state = channel.state;
1131
1164
  const { voiceParams, startTime } = note;
1132
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
1165
+ const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1166
+ (1 + this.getAmplitudeControl(channel, note));
1133
1167
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1134
1168
  const volDelay = startTime + voiceParams.volDelay;
1135
- const portamentoTime = volDelay + this.getPortamentoTime(channel);
1169
+ const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1170
+ const volHold = volAttack + voiceParams.volHold;
1136
1171
  note.volumeEnvelopeNode.gain
1137
1172
  .cancelScheduledValues(scheduleTime)
1138
- .setValueAtTime(0, volDelay)
1139
- .linearRampToValueAtTime(sustainVolume, portamentoTime);
1173
+ .setValueAtTime(sustainVolume, volHold);
1140
1174
  }
1141
1175
  setVolumeEnvelope(channel, note, scheduleTime) {
1142
1176
  const state = channel.state;
@@ -1156,6 +1190,12 @@ class Midy {
1156
1190
  .setValueAtTime(attackVolume, volHold)
1157
1191
  .linearRampToValueAtTime(sustainVolume, volDecay);
1158
1192
  }
1193
+ setPortamentoPitchEnvelope(note, scheduleTime) {
1194
+ const baseRate = note.voiceParams.playbackRate;
1195
+ note.bufferSource.playbackRate
1196
+ .cancelScheduledValues(scheduleTime)
1197
+ .setValueAtTime(baseRate, scheduleTime);
1198
+ }
1159
1199
  setPitchEnvelope(note, scheduleTime) {
1160
1200
  const { voiceParams } = note;
1161
1201
  const baseRate = voiceParams.playbackRate;
@@ -1183,20 +1223,20 @@ class Midy {
1183
1223
  const maxFrequency = 20000; // max Hz of initialFilterFc
1184
1224
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1185
1225
  }
1186
- setPortamentoStartFilterEnvelope(channel, note, scheduleTime) {
1226
+ setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1187
1227
  const state = channel.state;
1188
- const { voiceParams, noteNumber, startTime } = note;
1189
- const softPedalFactor = 1 -
1190
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1191
- const baseFreq = this.centToHz(voiceParams.initialFilterFc) *
1192
- softPedalFactor *
1228
+ const { voiceParams, startTime } = note;
1229
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1230
+ const baseCent = voiceParams.initialFilterFc +
1231
+ this.getFilterCutoffControl(channel, note);
1232
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1193
1233
  state.brightness * 2;
1194
1234
  const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1195
1235
  const sustainFreq = baseFreq +
1196
1236
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1197
1237
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1198
1238
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1199
- const portamentoTime = startTime + this.getPortamentoTime(channel);
1239
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1200
1240
  const modDelay = startTime + voiceParams.modDelay;
1201
1241
  note.filterNode.frequency
1202
1242
  .cancelScheduledValues(scheduleTime)
@@ -1206,9 +1246,8 @@ class Midy {
1206
1246
  }
1207
1247
  setFilterEnvelope(channel, note, scheduleTime) {
1208
1248
  const state = channel.state;
1209
- const { voiceParams, noteNumber, startTime } = note;
1210
- const softPedalFactor = 1 -
1211
- (0.1 + (noteNumber / 127) * 0.2) * state.softPedal;
1249
+ const { voiceParams, startTime } = note;
1250
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1212
1251
  const baseCent = voiceParams.initialFilterFc +
1213
1252
  this.getFilterCutoffControl(channel, note);
1214
1253
  const baseFreq = this.centToHz(baseCent) * softPedalFactor *
@@ -1282,14 +1321,14 @@ class Midy {
1282
1321
  return audioBuffer;
1283
1322
  }
1284
1323
  }
1285
- async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
1324
+ async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
1286
1325
  const now = this.audioContext.currentTime;
1287
1326
  const state = channel.state;
1288
1327
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1289
1328
  const voiceParams = voice.getAllParams(controllerState);
1290
1329
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1291
1330
  const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1292
- note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1331
+ note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1293
1332
  note.volumeNode = new GainNode(this.audioContext);
1294
1333
  note.gainL = new GainNode(this.audioContext);
1295
1334
  note.gainR = new GainNode(this.audioContext);
@@ -1298,20 +1337,24 @@ class Midy {
1298
1337
  type: "lowpass",
1299
1338
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1300
1339
  });
1301
- if (0.5 <= state.portamento && portamento) {
1302
- note.portamento = true;
1303
- this.setPortamentoStartVolumeEnvelope(channel, note, now);
1304
- this.setPortamentoStartFilterEnvelope(channel, note, now);
1340
+ const prevNote = channel.scheduledNotes.at(-1);
1341
+ if (prevNote && prevNote.noteNumber !== noteNumber) {
1342
+ note.portamentoNoteNumber = prevNote.noteNumber;
1343
+ }
1344
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1345
+ this.setPortamentoVolumeEnvelope(channel, note, now);
1346
+ this.setPortamentoFilterEnvelope(channel, note, now);
1347
+ this.setPortamentoPitchEnvelope(note, now);
1305
1348
  }
1306
1349
  else {
1307
- note.portamento = false;
1308
1350
  this.setVolumeEnvelope(channel, note, now);
1309
1351
  this.setFilterEnvelope(channel, note, now);
1352
+ this.setPitchEnvelope(note, now);
1310
1353
  }
1354
+ this.updateDetune(channel, note, now);
1311
1355
  if (0 < state.vibratoDepth) {
1312
1356
  this.startVibrato(channel, note, now);
1313
1357
  }
1314
- this.setPitchEnvelope(note, now);
1315
1358
  if (0 < state.modulationDepth) {
1316
1359
  this.startModulation(channel, note, now);
1317
1360
  }
@@ -1324,10 +1367,10 @@ class Midy {
1324
1367
  note.volumeEnvelopeNode.connect(note.volumeNode);
1325
1368
  note.volumeNode.connect(note.gainL);
1326
1369
  note.volumeNode.connect(note.gainR);
1327
- if (0 < channel.chorusSendLevel) {
1370
+ if (0 < state.chorusSendLevel) {
1328
1371
  this.setChorusEffectsSend(channel, note, 0, now);
1329
1372
  }
1330
- if (0 < channel.reverbSendLevel) {
1373
+ if (0 < state.reverbSendLevel) {
1331
1374
  this.setReverbEffectsSend(channel, note, 0, now);
1332
1375
  }
1333
1376
  note.bufferSource.start(startTime);
@@ -1358,8 +1401,7 @@ class Midy {
1358
1401
  const [prevNote, prevChannelNumber] = prev;
1359
1402
  if (prevNote && !prevNote.ending) {
1360
1403
  this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1361
- startTime, true, // force
1362
- undefined);
1404
+ startTime, true);
1363
1405
  }
1364
1406
  }
1365
1407
  this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
@@ -1379,19 +1421,11 @@ class Midy {
1379
1421
  const prevNote = this.drumExclusiveClassNotes[index];
1380
1422
  if (prevNote && !prevNote.ending) {
1381
1423
  this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1382
- startTime, true, // force
1383
- undefined);
1424
+ startTime, true);
1384
1425
  }
1385
1426
  this.drumExclusiveClassNotes[index] = note;
1386
1427
  }
1387
- isDrumNoteOffException(channel, noteNumber) {
1388
- if (!channel.isDrum)
1389
- return false;
1390
- const programNumber = channel.programNumber;
1391
- return !((programNumber === 48 && noteNumber === 88) ||
1392
- (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1393
- }
1394
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1428
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
1395
1429
  const channel = this.channels[channelNumber];
1396
1430
  const bankNumber = this.calcBank(channel, channelNumber);
1397
1431
  const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
@@ -1402,7 +1436,8 @@ class Midy {
1402
1436
  if (!voice)
1403
1437
  return;
1404
1438
  const isSF3 = soundFont.parsed.info.version.major === 3;
1405
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1439
+ const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
1440
+ note.noteOffEvent = noteOffEvent;
1406
1441
  note.gainL.connect(channel.gainL);
1407
1442
  note.gainR.connect(channel.gainR);
1408
1443
  if (0.5 <= channel.state.sustainPedal) {
@@ -1411,31 +1446,12 @@ class Midy {
1411
1446
  this.handleExclusiveClass(note, channelNumber, startTime);
1412
1447
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1413
1448
  const scheduledNotes = channel.scheduledNotes;
1414
- let noteList = scheduledNotes.get(noteNumber);
1415
- if (noteList) {
1416
- noteList.push(note);
1417
- }
1418
- else {
1419
- noteList = [note];
1420
- scheduledNotes.set(noteNumber, noteList);
1421
- }
1422
- if (this.isDrumNoteOffException(channel, noteNumber)) {
1423
- const stopTime = startTime + note.bufferSource.buffer.duration;
1424
- const index = noteList.length - 1;
1425
- const promise = new Promise((resolve) => {
1426
- note.bufferSource.onended = () => {
1427
- noteList[index] = undefined;
1428
- this.disconnectNote(note);
1429
- resolve();
1430
- };
1431
- note.bufferSource.stop(stopTime);
1432
- });
1433
- this.notePromises.push(promise);
1434
- }
1449
+ note.index = scheduledNotes.length;
1450
+ scheduledNotes.push(note);
1435
1451
  }
1436
1452
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1437
1453
  scheduleTime ??= this.audioContext.currentTime;
1438
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1454
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1439
1455
  }
1440
1456
  disconnectNote(note) {
1441
1457
  note.bufferSource.disconnect();
@@ -1460,84 +1476,72 @@ class Midy {
1460
1476
  note.chorusEffectsSend.disconnect();
1461
1477
  }
1462
1478
  }
1463
- stopNote(endTime, stopTime, noteList, index) {
1464
- const note = noteList[index];
1479
+ releaseNote(channel, note, endTime) {
1480
+ const volRelease = endTime +
1481
+ note.voiceParams.volRelease * channel.state.releaseTime * 2;
1482
+ const modRelease = endTime + note.voiceParams.modRelease;
1483
+ const stopTime = Math.min(volRelease, modRelease);
1484
+ note.filterNode.frequency
1485
+ .cancelScheduledValues(endTime)
1486
+ .linearRampToValueAtTime(0, modRelease);
1465
1487
  note.volumeEnvelopeNode.gain
1466
1488
  .cancelScheduledValues(endTime)
1467
- .linearRampToValueAtTime(0, stopTime);
1468
- note.ending = true;
1469
- this.scheduleTask(() => {
1470
- note.bufferSource.loop = false;
1471
- }, stopTime);
1489
+ .linearRampToValueAtTime(0, volRelease);
1472
1490
  return new Promise((resolve) => {
1473
- note.bufferSource.onended = () => {
1474
- noteList[index] = undefined;
1491
+ this.scheduleTask(() => {
1492
+ const bufferSource = note.bufferSource;
1493
+ bufferSource.loop = false;
1494
+ bufferSource.stop(stopTime);
1475
1495
  this.disconnectNote(note);
1496
+ channel.scheduledNotes[note.index] = undefined;
1476
1497
  resolve();
1477
- };
1478
- note.bufferSource.stop(stopTime);
1498
+ }, stopTime);
1479
1499
  });
1480
1500
  }
1481
- findNoteOffTarget(noteList) {
1482
- for (let i = 0; i < noteList.length; i++) {
1483
- const note = noteList[i];
1484
- if (!note)
1485
- continue;
1486
- if (note.ending)
1487
- continue;
1488
- return [note, i];
1489
- }
1490
- }
1491
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1501
+ scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1492
1502
  const channel = this.channels[channelNumber];
1493
- if (this.isDrumNoteOffException(channel, noteNumber))
1494
- return;
1495
1503
  const state = channel.state;
1496
1504
  if (!force) {
1497
- if (0.5 <= state.sustainPedal)
1498
- return;
1499
- if (channel.sostenutoNotes.has(noteNumber))
1500
- return;
1501
- }
1502
- const noteList = channel.scheduledNotes.get(noteNumber);
1503
- if (!noteList)
1504
- return; // be careful with drum channel
1505
- const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
1506
- if (!noteOffTarget)
1507
- return;
1508
- const [note, i] = noteOffTarget;
1509
- if (0.5 <= state.portamento && portamentoNoteNumber !== undefined) {
1510
- const portamentoTime = endTime + this.getPortamentoTime(channel);
1511
- const deltaNote = portamentoNoteNumber - noteNumber;
1512
- const baseRate = note.voiceParams.playbackRate;
1513
- const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1514
- note.bufferSource.playbackRate
1515
- .cancelScheduledValues(endTime)
1516
- .linearRampToValueAtTime(targetRate, portamentoTime);
1517
- return this.stopNote(endTime, portamentoTime, noteList, i);
1505
+ if (channel.isDrum) {
1506
+ if (!this.isLoopDrum(channel, noteNumber))
1507
+ return;
1508
+ }
1509
+ else {
1510
+ if (0.5 <= state.sustainPedal)
1511
+ return;
1512
+ if (0.5 <= state.sostenutoPedal)
1513
+ return;
1514
+ }
1518
1515
  }
1519
- else {
1520
- const volRelease = endTime +
1521
- note.voiceParams.volRelease * state.releaseTime * 2;
1522
- const modRelease = endTime + note.voiceParams.modRelease;
1523
- note.filterNode.frequency
1524
- .cancelScheduledValues(endTime)
1525
- .linearRampToValueAtTime(0, modRelease);
1526
- const stopTime = Math.min(volRelease, modRelease);
1527
- return this.stopNote(endTime, stopTime, noteList, i);
1516
+ const note = this.findNoteOffTarget(channel, noteNumber);
1517
+ if (!note)
1518
+ return;
1519
+ note.ending = true;
1520
+ this.releaseNote(channel, note, endTime);
1521
+ }
1522
+ findNoteOffTarget(channel, noteNumber) {
1523
+ const scheduledNotes = channel.scheduledNotes;
1524
+ for (let i = 0; i < scheduledNotes.length; i++) {
1525
+ const note = scheduledNotes[i];
1526
+ if (!note)
1527
+ continue;
1528
+ if (note.ending)
1529
+ continue;
1530
+ if (note.noteNumber !== noteNumber)
1531
+ continue;
1532
+ return note;
1528
1533
  }
1529
1534
  }
1530
1535
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1531
1536
  scheduleTime ??= this.audioContext.currentTime;
1532
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false, // force
1533
- undefined);
1537
+ return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1534
1538
  }
1535
1539
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1536
1540
  const velocity = halfVelocity * 2;
1537
1541
  const channel = this.channels[channelNumber];
1538
1542
  const promises = [];
1539
1543
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1540
- const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1544
+ const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1541
1545
  promises.push(promise);
1542
1546
  }
1543
1547
  channel.sustainNotes = [];
@@ -1547,12 +1551,14 @@ class Midy {
1547
1551
  const velocity = halfVelocity * 2;
1548
1552
  const channel = this.channels[channelNumber];
1549
1553
  const promises = [];
1554
+ const sostenutoNotes = channel.sostenutoNotes;
1550
1555
  channel.state.sostenutoPedal = 0;
1551
- channel.sostenutoNotes.forEach((note) => {
1552
- const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1556
+ for (let i = 0; i < sostenutoNotes.length; i++) {
1557
+ const note = sostenutoNotes[i];
1558
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1553
1559
  promises.push(promise);
1554
- });
1555
- channel.sostenutoNotes.clear();
1560
+ }
1561
+ channel.sostenutoNotes = [];
1556
1562
  return promises;
1557
1563
  }
1558
1564
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
@@ -1581,11 +1587,11 @@ class Midy {
1581
1587
  const channel = this.channels[channelNumber];
1582
1588
  channel.state.polyphonicKeyPressure = pressure / 127;
1583
1589
  const table = channel.polyphonicKeyPressureTable;
1584
- const activeNotes = this.getActiveNotes(channel, scheduleTime);
1585
- if (activeNotes.has(noteNumber)) {
1586
- const note = activeNotes.get(noteNumber);
1587
- this.setControllerParameters(channel, note, table);
1588
- }
1590
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1591
+ if (note.noteNumber === noteNumber) {
1592
+ this.setControllerParameters(channel, note, table);
1593
+ }
1594
+ });
1589
1595
  this.applyVoiceParams(channel, 10);
1590
1596
  }
1591
1597
  handleProgramChange(channelNumber, programNumber, _scheduleTime) {
@@ -1615,7 +1621,7 @@ class Midy {
1615
1621
  channel.detune += pressureDepth * (next - prev);
1616
1622
  }
1617
1623
  const table = channel.channelPressureTable;
1618
- this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1624
+ this.processActiveNotes(channel, scheduleTime, (note) => {
1619
1625
  this.setControllerParameters(channel, note, table);
1620
1626
  });
1621
1627
  this.applyVoiceParams(channel, 13);
@@ -1672,10 +1678,13 @@ class Midy {
1672
1678
  .setValueAtTime(volumeDepth, scheduleTime);
1673
1679
  }
1674
1680
  setReverbEffectsSend(channel, note, prevValue, scheduleTime) {
1681
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1682
+ let value = note.voiceParams.reverbEffectsSend;
1683
+ if (0 <= keyBasedValue) {
1684
+ value *= keyBasedValue / 127 / channel.state.reverbSendLevel;
1685
+ }
1675
1686
  if (0 < prevValue) {
1676
- if (0 < note.voiceParams.reverbEffectsSend) {
1677
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 91);
1678
- const value = note.voiceParams.reverbEffectsSend + keyBasedValue;
1687
+ if (0 < value) {
1679
1688
  note.reverbEffectsSend.gain
1680
1689
  .cancelScheduledValues(scheduleTime)
1681
1690
  .setValueAtTime(value, scheduleTime);
@@ -1685,10 +1694,10 @@ class Midy {
1685
1694
  }
1686
1695
  }
1687
1696
  else {
1688
- if (0 < note.voiceParams.reverbEffectsSend) {
1697
+ if (0 < value) {
1689
1698
  if (!note.reverbEffectsSend) {
1690
1699
  note.reverbEffectsSend = new GainNode(this.audioContext, {
1691
- gain: note.voiceParams.reverbEffectsSend,
1700
+ gain: value,
1692
1701
  });
1693
1702
  note.volumeNode.connect(note.reverbEffectsSend);
1694
1703
  }
@@ -1697,10 +1706,13 @@ class Midy {
1697
1706
  }
1698
1707
  }
1699
1708
  setChorusEffectsSend(channel, note, prevValue, scheduleTime) {
1709
+ const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1710
+ let value = note.voiceParams.chorusEffectsSend;
1711
+ if (0 <= keyBasedValue) {
1712
+ value *= keyBasedValue / 127 / channel.state.chorusSendLevel;
1713
+ }
1700
1714
  if (0 < prevValue) {
1701
- if (0 < note.voiceParams.chorusEffectsSend) {
1702
- const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 93);
1703
- const value = note.voiceParams.chorusEffectsSend + keyBasedValue;
1715
+ if (0 < vaule) {
1704
1716
  note.chorusEffectsSend.gain
1705
1717
  .cancelScheduledValues(scheduleTime)
1706
1718
  .setValueAtTime(value, scheduleTime);
@@ -1710,10 +1722,10 @@ class Midy {
1710
1722
  }
1711
1723
  }
1712
1724
  else {
1713
- if (0 < note.voiceParams.chorusEffectsSend) {
1725
+ if (0 < value) {
1714
1726
  if (!note.chorusEffectsSend) {
1715
1727
  note.chorusEffectsSend = new GainNode(this.audioContext, {
1716
- gain: note.voiceParams.chorusEffectsSend,
1728
+ gain: value,
1717
1729
  });
1718
1730
  note.volumeNode.connect(note.chorusEffectsSend);
1719
1731
  }
@@ -1823,8 +1835,8 @@ class Midy {
1823
1835
  if (key in voiceParams)
1824
1836
  noteVoiceParams[key] = voiceParams[key];
1825
1837
  }
1826
- if (0.5 <= channel.state.portamento && note.portamento) {
1827
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1838
+ if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1839
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1828
1840
  }
1829
1841
  else {
1830
1842
  this.setFilterEnvelope(channel, note, scheduleTime);
@@ -1847,42 +1859,42 @@ class Midy {
1847
1859
  });
1848
1860
  }
1849
1861
  createControlChangeHandlers() {
1850
- return {
1851
- 0: this.setBankMSB,
1852
- 1: this.setModulationDepth,
1853
- 5: this.setPortamentoTime,
1854
- 6: this.dataEntryMSB,
1855
- 7: this.setVolume,
1856
- 10: this.setPan,
1857
- 11: this.setExpression,
1858
- 32: this.setBankLSB,
1859
- 38: this.dataEntryLSB,
1860
- 64: this.setSustainPedal,
1861
- 65: this.setPortamento,
1862
- 66: this.setSostenutoPedal,
1863
- 67: this.setSoftPedal,
1864
- 71: this.setFilterResonance,
1865
- 72: this.setReleaseTime,
1866
- 73: this.setAttackTime,
1867
- 74: this.setBrightness,
1868
- 75: this.setDecayTime,
1869
- 76: this.setVibratoRate,
1870
- 77: this.setVibratoDepth,
1871
- 78: this.setVibratoDelay,
1872
- 91: this.setReverbSendLevel,
1873
- 93: this.setChorusSendLevel,
1874
- 96: this.dataIncrement,
1875
- 97: this.dataDecrement,
1876
- 100: this.setRPNLSB,
1877
- 101: this.setRPNMSB,
1878
- 120: this.allSoundOff,
1879
- 121: this.resetAllControllers,
1880
- 123: this.allNotesOff,
1881
- 124: this.omniOff,
1882
- 125: this.omniOn,
1883
- 126: this.monoOn,
1884
- 127: this.polyOn,
1885
- };
1862
+ const handlers = new Array(128);
1863
+ handlers[0] = this.setBankMSB;
1864
+ handlers[1] = this.setModulationDepth;
1865
+ handlers[5] = this.setPortamentoTime;
1866
+ handlers[6] = this.dataEntryMSB;
1867
+ handlers[7] = this.setVolume;
1868
+ handlers[10] = this.setPan;
1869
+ handlers[11] = this.setExpression;
1870
+ handlers[32] = this.setBankLSB;
1871
+ handlers[38] = this.dataEntryLSB;
1872
+ handlers[64] = this.setSustainPedal;
1873
+ handlers[65] = this.setPortamento;
1874
+ handlers[66] = this.setSostenutoPedal;
1875
+ handlers[67] = this.setSoftPedal;
1876
+ handlers[71] = this.setFilterResonance;
1877
+ handlers[72] = this.setReleaseTime;
1878
+ handlers[73] = this.setAttackTime;
1879
+ handlers[74] = this.setBrightness;
1880
+ handlers[75] = this.setDecayTime;
1881
+ handlers[76] = this.setVibratoRate;
1882
+ handlers[77] = this.setVibratoDepth;
1883
+ handlers[78] = this.setVibratoDelay;
1884
+ handlers[91] = this.setReverbSendLevel;
1885
+ handlers[93] = this.setChorusSendLevel;
1886
+ handlers[96] = this.dataIncrement;
1887
+ handlers[97] = this.dataDecrement;
1888
+ handlers[100] = this.setRPNLSB;
1889
+ handlers[101] = this.setRPNMSB;
1890
+ handlers[120] = this.allSoundOff;
1891
+ handlers[121] = this.resetAllControllers;
1892
+ handlers[123] = this.allNotesOff;
1893
+ handlers[124] = this.omniOff;
1894
+ handlers[125] = this.omniOn;
1895
+ handlers[126] = this.monoOn;
1896
+ handlers[127] = this.polyOn;
1897
+ return handlers;
1886
1898
  }
1887
1899
  handleControlChange(channelNumber, controllerType, value, scheduleTime) {
1888
1900
  const handler = this.controlChangeHandlers[controllerType];
@@ -1919,17 +1931,41 @@ class Midy {
1919
1931
  channel.state.modulationDepth = modulation / 127;
1920
1932
  this.updateModulation(channel, scheduleTime);
1921
1933
  }
1922
- setPortamentoTime(channelNumber, portamentoTime) {
1934
+ updatePortamento(channel, scheduleTime) {
1935
+ this.processScheduledNotes(channel, (note) => {
1936
+ if (0.5 <= channel.state.portamento) {
1937
+ if (0 <= note.portamentoNoteNumber) {
1938
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1939
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1940
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
1941
+ this.updateDetune(channel, note, scheduleTime);
1942
+ }
1943
+ }
1944
+ else {
1945
+ if (0 <= note.portamentoNoteNumber) {
1946
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1947
+ this.setFilterEnvelope(channel, note, scheduleTime);
1948
+ this.setPitchEnvelope(note, scheduleTime);
1949
+ this.updateDetune(channel, note, scheduleTime);
1950
+ }
1951
+ }
1952
+ });
1953
+ }
1954
+ setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
1923
1955
  const channel = this.channels[channelNumber];
1956
+ scheduleTime ??= this.audioContext.currentTime;
1924
1957
  channel.state.portamentoTime = portamentoTime / 127;
1958
+ if (channel.isDrum)
1959
+ return;
1960
+ this.updatePortamento(channel, scheduleTime);
1925
1961
  }
1926
1962
  setKeyBasedVolume(channel, scheduleTime) {
1927
1963
  this.processScheduledNotes(channel, (note) => {
1928
1964
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1929
- if (keyBasedValue !== 0) {
1965
+ if (0 <= keyBasedValue) {
1930
1966
  note.volumeNode.gain
1931
1967
  .cancelScheduledValues(scheduleTime)
1932
- .setValueAtTime(1 + keyBasedValue, scheduleTime);
1968
+ .setValueAtTime(keyBasedValue / 127, scheduleTime);
1933
1969
  }
1934
1970
  });
1935
1971
  }
@@ -1950,8 +1986,8 @@ class Midy {
1950
1986
  setKeyBasedPan(channel, scheduleTime) {
1951
1987
  this.processScheduledNotes(channel, (note) => {
1952
1988
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1953
- if (keyBasedValue !== 0) {
1954
- const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
1989
+ if (0 <= keyBasedValue) {
1990
+ const { gainLeft, gainRight } = this.panToGain(keyBasedValue / 127);
1955
1991
  note.gainL.gain
1956
1992
  .cancelScheduledValues(scheduleTime)
1957
1993
  .setValueAtTime(gainLeft, scheduleTime);
@@ -1979,7 +2015,7 @@ class Midy {
1979
2015
  }
1980
2016
  dataEntryLSB(channelNumber, value, scheduleTime) {
1981
2017
  this.channels[channelNumber].dataLSB = value;
1982
- this.handleRPN(channelNumber, scheduleTime);
2018
+ this.handleRPN(channelNumber, 0, scheduleTime);
1983
2019
  }
1984
2020
  updateChannelVolume(channel, scheduleTime) {
1985
2021
  const state = channel.state;
@@ -2007,11 +2043,13 @@ class Midy {
2007
2043
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
2008
2044
  }
2009
2045
  }
2010
- setPortamento(channelNumber, value) {
2046
+ setPortamento(channelNumber, value, scheduleTime) {
2011
2047
  const channel = this.channels[channelNumber];
2012
2048
  if (channel.isDrum)
2013
2049
  return;
2050
+ scheduleTime ??= this.audioContext.currentTime;
2014
2051
  channel.state.portamento = value / 127;
2052
+ this.updatePortamento(channel, scheduleTime);
2015
2053
  }
2016
2054
  setSostenutoPedal(channelNumber, value, scheduleTime) {
2017
2055
  const channel = this.channels[channelNumber];
@@ -2020,12 +2058,19 @@ class Midy {
2020
2058
  scheduleTime ??= this.audioContext.currentTime;
2021
2059
  channel.state.sostenutoPedal = value / 127;
2022
2060
  if (64 <= value) {
2023
- channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
2061
+ const sostenutoNotes = [];
2062
+ this.processActiveNotes(channel, scheduleTime, (note) => {
2063
+ sostenutoNotes.push(note);
2064
+ });
2065
+ channel.sostenutoNotes = sostenutoNotes;
2024
2066
  }
2025
2067
  else {
2026
2068
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
2027
2069
  }
2028
2070
  }
2071
+ getSoftPedalFactor(channel, note) {
2072
+ return 1 - (0.1 + (note.noteNumber / 127) * 0.2) * channel.state.softPedal;
2073
+ }
2029
2074
  setSoftPedal(channelNumber, softPedal, scheduleTime) {
2030
2075
  const channel = this.channels[channelNumber];
2031
2076
  if (channel.isDrum)
@@ -2034,9 +2079,9 @@ class Midy {
2034
2079
  scheduleTime ??= this.audioContext.currentTime;
2035
2080
  state.softPedal = softPedal / 127;
2036
2081
  this.processScheduledNotes(channel, (note) => {
2037
- if (0.5 <= state.portamento && note.portamento) {
2038
- this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
2039
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2082
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2083
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2084
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2040
2085
  }
2041
2086
  else {
2042
2087
  this.setVolumeEnvelope(channel, note, scheduleTime);
@@ -2050,7 +2095,7 @@ class Midy {
2050
2095
  return;
2051
2096
  scheduleTime ??= this.audioContext.currentTime;
2052
2097
  const state = channel.state;
2053
- state.filterResonance = filterResonance / 64;
2098
+ state.filterResonance = filterResonance / 127;
2054
2099
  this.processScheduledNotes(channel, (note) => {
2055
2100
  const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2056
2101
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
@@ -2061,14 +2106,14 @@ class Midy {
2061
2106
  if (channel.isDrum)
2062
2107
  return;
2063
2108
  scheduleTime ??= this.audioContext.currentTime;
2064
- channel.state.releaseTime = releaseTime / 64;
2109
+ channel.state.releaseTime = releaseTime / 127;
2065
2110
  }
2066
2111
  setAttackTime(channelNumber, attackTime, scheduleTime) {
2067
2112
  const channel = this.channels[channelNumber];
2068
2113
  if (channel.isDrum)
2069
2114
  return;
2070
2115
  scheduleTime ??= this.audioContext.currentTime;
2071
- channel.state.attackTime = attackTime / 64;
2116
+ channel.state.attackTime = attackTime / 127;
2072
2117
  this.processScheduledNotes(channel, (note) => {
2073
2118
  if (note.startTime < scheduleTime)
2074
2119
  return false;
@@ -2081,10 +2126,10 @@ class Midy {
2081
2126
  return;
2082
2127
  const state = channel.state;
2083
2128
  scheduleTime ??= this.audioContext.currentTime;
2084
- state.brightness = brightness / 64;
2129
+ state.brightness = brightness / 127;
2085
2130
  this.processScheduledNotes(channel, (note) => {
2086
- if (0.5 <= state.portamento && note.portamento) {
2087
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2131
+ if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2132
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2088
2133
  }
2089
2134
  else {
2090
2135
  this.setFilterEnvelope(channel, note);
@@ -2096,7 +2141,7 @@ class Midy {
2096
2141
  if (channel.isDrum)
2097
2142
  return;
2098
2143
  scheduleTime ??= this.audioContext.currentTime;
2099
- channel.state.decayTime = dacayTime / 64;
2144
+ channel.state.decayTime = dacayTime / 127;
2100
2145
  this.processScheduledNotes(channel, (note) => {
2101
2146
  this.setVolumeEnvelope(channel, note, scheduleTime);
2102
2147
  });
@@ -2106,7 +2151,7 @@ class Midy {
2106
2151
  if (channel.isDrum)
2107
2152
  return;
2108
2153
  scheduleTime ??= this.audioContext.currentTime;
2109
- channel.state.vibratoRate = vibratoRate / 64;
2154
+ channel.state.vibratoRate = vibratoRate / 127;
2110
2155
  if (channel.vibratoDepth <= 0)
2111
2156
  return;
2112
2157
  this.processScheduledNotes(channel, (note) => {
@@ -2119,7 +2164,7 @@ class Midy {
2119
2164
  return;
2120
2165
  scheduleTime ??= this.audioContext.currentTime;
2121
2166
  const prev = channel.state.vibratoDepth;
2122
- channel.state.vibratoDepth = vibratoDepth / 64;
2167
+ channel.state.vibratoDepth = vibratoDepth / 127;
2123
2168
  if (0 < prev) {
2124
2169
  this.processScheduledNotes(channel, (note) => {
2125
2170
  this.setFreqVibLFO(channel, note, scheduleTime);
@@ -2131,12 +2176,12 @@ class Midy {
2131
2176
  });
2132
2177
  }
2133
2178
  }
2134
- setVibratoDelay(channelNumber, vibratoDelay) {
2179
+ setVibratoDelay(channelNumber, vibratoDelay, scheduleTime) {
2135
2180
  const channel = this.channels[channelNumber];
2136
2181
  if (channel.isDrum)
2137
2182
  return;
2138
2183
  scheduleTime ??= this.audioContext.currentTime;
2139
- channel.state.vibratoDelay = vibratoDelay / 64;
2184
+ channel.state.vibratoDelay = vibratoDelay / 127;
2140
2185
  if (0 < channel.state.vibratoDepth) {
2141
2186
  this.processScheduledNotes(channel, (note) => {
2142
2187
  this.startVibrato(channel, note, scheduleTime);
@@ -2159,7 +2204,8 @@ class Midy {
2159
2204
  this.processScheduledNotes(channel, (note) => {
2160
2205
  if (note.voiceParams.reverbEffectsSend <= 0)
2161
2206
  return false;
2162
- note.reverbEffectsSend.disconnect();
2207
+ if (note.reverbEffectsSend)
2208
+ note.reverbEffectsSend.disconnect();
2163
2209
  });
2164
2210
  }
2165
2211
  }
@@ -2191,7 +2237,8 @@ class Midy {
2191
2237
  this.processScheduledNotes(channel, (note) => {
2192
2238
  if (note.voiceParams.chorusEffectsSend <= 0)
2193
2239
  return false;
2194
- note.chorusEffectsSend.disconnect();
2240
+ if (note.chorusEffectsSend)
2241
+ note.chorusEffectsSend.disconnect();
2195
2242
  });
2196
2243
  }
2197
2244
  }
@@ -2258,12 +2305,14 @@ class Midy {
2258
2305
  }
2259
2306
  }
2260
2307
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2261
- dataIncrement(channelNumber) {
2262
- this.handleRPN(channelNumber, 1);
2308
+ dataIncrement(channelNumber, scheduleTime) {
2309
+ scheduleTime ??= this.audioContext.currentTime;
2310
+ this.handleRPN(channelNumber, 1, scheduleTime);
2263
2311
  }
2264
2312
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp18.pdf
2265
- dataDecrement(channelNumber) {
2266
- this.handleRPN(channelNumber, -1);
2313
+ dataDecrement(channelNumber, scheduleTime) {
2314
+ scheduleTime ??= this.audioContext.currentTime;
2315
+ this.handleRPN(channelNumber, -1, scheduleTime);
2267
2316
  }
2268
2317
  setRPNMSB(channelNumber, value) {
2269
2318
  this.channels[channelNumber].rpnMSB = value;
@@ -2273,7 +2322,7 @@ class Midy {
2273
2322
  }
2274
2323
  dataEntryMSB(channelNumber, value, scheduleTime) {
2275
2324
  this.channels[channelNumber].dataMSB = value;
2276
- this.handleRPN(channelNumber, scheduleTime);
2325
+ this.handleRPN(channelNumber, 0, scheduleTime);
2277
2326
  }
2278
2327
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2279
2328
  const channel = this.channels[channelNumber];
@@ -2347,21 +2396,29 @@ class Midy {
2347
2396
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2348
2397
  }
2349
2398
  resetAllStates(channelNumber) {
2399
+ const scheduleTime = this.audioContext.currentTime;
2350
2400
  const channel = this.channels[channelNumber];
2351
2401
  const state = channel.state;
2352
- for (const type of Object.keys(defaultControllerState)) {
2353
- state[type] = defaultControllerState[type].defaultValue;
2402
+ const entries = Object.entries(defaultControllerState);
2403
+ for (const [key, { type, defaultValue }] of entries) {
2404
+ if (128 <= type) {
2405
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2406
+ }
2407
+ else {
2408
+ state[key] = defaultValue;
2409
+ }
2354
2410
  }
2355
- for (const type of Object.keys(this.constructor.channelSettings)) {
2356
- channel[type] = this.constructor.channelSettings[type];
2411
+ for (const key of Object.keys(this.constructor.channelSettings)) {
2412
+ channel[key] = this.constructor.channelSettings[key];
2357
2413
  }
2414
+ this.resetChannelTable(channel);
2358
2415
  this.mode = "GM2";
2359
2416
  this.masterFineTuning = 0; // cb
2360
2417
  this.masterCoarseTuning = 0; // cb
2361
2418
  }
2362
2419
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2363
- resetAllControllers(channelNumber) {
2364
- const stateTypes = [
2420
+ resetAllControllers(channelNumber, _value, scheduleTime) {
2421
+ const keys = [
2365
2422
  "polyphonicKeyPressure",
2366
2423
  "channelPressure",
2367
2424
  "pitchWheel",
@@ -2374,10 +2431,17 @@ class Midy {
2374
2431
  ];
2375
2432
  const channel = this.channels[channelNumber];
2376
2433
  const state = channel.state;
2377
- for (let i = 0; i < stateTypes.length; i++) {
2378
- const type = stateTypes[i];
2379
- state[type] = defaultControllerState[type].defaultValue;
2434
+ for (let i = 0; i < keys.length; i++) {
2435
+ const key = keys[i];
2436
+ const { type, defaultValue } = defaultControllerState[key];
2437
+ if (128 <= type) {
2438
+ this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
2439
+ }
2440
+ else {
2441
+ state[key] = defaultValue;
2442
+ }
2380
2443
  }
2444
+ this.setPitchBend(channelNumber, 8192, scheduleTime);
2381
2445
  const settingTypes = [
2382
2446
  "rpnMSB",
2383
2447
  "rpnLSB",
@@ -2622,7 +2686,7 @@ class Midy {
2622
2686
  this.reverbEffect = options.reverbAlgorithm(audioContext);
2623
2687
  }
2624
2688
  getReverbTime(value) {
2625
- return Math.pow(Math.E, (value - 40) * 0.025);
2689
+ return Math.exp((value - 40) * 0.025);
2626
2690
  }
2627
2691
  // mean free path equation
2628
2692
  // https://repository.dl.itc.u-tokyo.ac.jp/record/8550/files/A31912.pdf
@@ -2856,7 +2920,13 @@ class Midy {
2856
2920
  setControllerParameters(channel, note, table) {
2857
2921
  if (table[0] !== 64)
2858
2922
  this.updateDetune(channel, note);
2859
- if (!note.portamento) {
2923
+ if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2924
+ if (table[1] !== 64)
2925
+ this.setPortamentoFilterEnvelope(channel, note);
2926
+ if (table[2] !== 64)
2927
+ this.setPortamentoVolumeEnvelope(channel, note);
2928
+ }
2929
+ else {
2860
2930
  if (table[1] !== 64)
2861
2931
  this.setFilterEnvelope(channel, note);
2862
2932
  if (table[2] !== 64)
@@ -2884,8 +2954,13 @@ class Midy {
2884
2954
  initControlTable() {
2885
2955
  const channelCount = 128;
2886
2956
  const slotSize = 6;
2887
- const defaultValues = [64, 64, 64, 0, 0, 0];
2888
2957
  const table = new Uint8Array(channelCount * slotSize);
2958
+ return this.resetControlTable(table);
2959
+ }
2960
+ resetControlTable(table) {
2961
+ const channelCount = 128;
2962
+ const slotSize = 6;
2963
+ const defaultValues = [64, 64, 64, 0, 0, 0];
2889
2964
  for (let ch = 0; ch < channelCount; ch++) {
2890
2965
  const offset = ch * slotSize;
2891
2966
  table.set(defaultValues, offset);
@@ -2916,7 +2991,7 @@ class Midy {
2916
2991
  getKeyBasedInstrumentControlValue(channel, keyNumber, controllerType) {
2917
2992
  const index = keyNumber * 128 + controllerType;
2918
2993
  const controlValue = channel.keyBasedInstrumentControlTable[index];
2919
- return (controlValue + 64) / 64;
2994
+ return controlValue;
2920
2995
  }
2921
2996
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2922
2997
  const channelNumber = data[4];
@@ -2929,7 +3004,7 @@ class Midy {
2929
3004
  const controllerType = data[i];
2930
3005
  const value = data[i + 1];
2931
3006
  const index = keyNumber * 128 + controllerType;
2932
- table[index] = value - 64;
3007
+ table[index] = value;
2933
3008
  }
2934
3009
  this.handleChannelPressure(channelNumber, channel.state.channelPressure * 127, scheduleTime);
2935
3010
  }