@marmooo/midy 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/midy.js CHANGED
@@ -240,13 +240,13 @@ export class Midy {
240
240
  configurable: true,
241
241
  writable: true,
242
242
  value: 0
243
- }); // cb
243
+ }); // cent
244
244
  Object.defineProperty(this, "masterCoarseTuning", {
245
245
  enumerable: true,
246
246
  configurable: true,
247
247
  writable: true,
248
248
  value: 0
249
- }); // cb
249
+ }); // cent
250
250
  Object.defineProperty(this, "reverb", {
251
251
  enumerable: true,
252
252
  configurable: true,
@@ -287,6 +287,18 @@ export class Midy {
287
287
  writable: true,
288
288
  value: 0
289
289
  });
290
+ Object.defineProperty(this, "lastActiveSensing", {
291
+ enumerable: true,
292
+ configurable: true,
293
+ writable: true,
294
+ value: 0
295
+ });
296
+ Object.defineProperty(this, "activeSensingThreshold", {
297
+ enumerable: true,
298
+ configurable: true,
299
+ writable: true,
300
+ value: 0.3
301
+ });
290
302
  Object.defineProperty(this, "noteCheckInterval", {
291
303
  enumerable: true,
292
304
  configurable: true,
@@ -327,7 +339,7 @@ export class Midy {
327
339
  enumerable: true,
328
340
  configurable: true,
329
341
  writable: true,
330
- value: this.initSoundFontTable()
342
+ value: Array.from({ length: 128 }, () => [])
331
343
  });
332
344
  Object.defineProperty(this, "voiceCounter", {
333
345
  enumerable: true,
@@ -371,6 +383,12 @@ export class Midy {
371
383
  writable: true,
372
384
  value: false
373
385
  });
386
+ Object.defineProperty(this, "playPromise", {
387
+ enumerable: true,
388
+ configurable: true,
389
+ writable: true,
390
+ value: void 0
391
+ });
374
392
  Object.defineProperty(this, "timeline", {
375
393
  enumerable: true,
376
394
  configurable: true,
@@ -408,8 +426,10 @@ export class Midy {
408
426
  length: 1,
409
427
  sampleRate: audioContext.sampleRate,
410
428
  });
429
+ this.messageHandlers = this.createMessageHandlers();
411
430
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
412
431
  this.controlChangeHandlers = this.createControlChangeHandlers();
432
+ this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
413
433
  this.channels = this.createChannels(audioContext);
414
434
  this.reverbEffect = this.createReverbEffect(audioContext);
415
435
  this.chorusEffect = this.createChorusEffect(audioContext);
@@ -419,21 +439,14 @@ export class Midy {
419
439
  this.scheduler.connect(audioContext.destination);
420
440
  this.GM2SystemOn();
421
441
  }
422
- initSoundFontTable() {
423
- const table = new Array(128);
424
- for (let i = 0; i < 128; i++) {
425
- table[i] = new Map();
426
- }
427
- return table;
428
- }
429
442
  addSoundFont(soundFont) {
430
443
  const index = this.soundFonts.length;
431
444
  this.soundFonts.push(soundFont);
432
445
  const presetHeaders = soundFont.parsed.presetHeaders;
446
+ const soundFontTable = this.soundFontTable;
433
447
  for (let i = 0; i < presetHeaders.length; i++) {
434
- const presetHeader = presetHeaders[i];
435
- const banks = this.soundFontTable[presetHeader.preset];
436
- banks.set(presetHeader.bank, index);
448
+ const { preset, bank } = presetHeaders[i];
449
+ soundFontTable[preset][bank] = index;
437
450
  }
438
451
  }
439
452
  async toUint8Array(input) {
@@ -511,13 +524,17 @@ export class Midy {
511
524
  this.GM2SystemOn();
512
525
  }
513
526
  getVoiceId(channel, noteNumber, velocity) {
514
- const bankNumber = this.calcBank(channel);
515
- const soundFontIndex = this.soundFontTable[channel.programNumber]
516
- .get(bankNumber);
527
+ const programNumber = channel.programNumber;
528
+ const bankTable = this.soundFontTable[programNumber];
529
+ if (!bankTable)
530
+ return;
531
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
532
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
533
+ const soundFontIndex = bankTable[bank];
517
534
  if (soundFontIndex === undefined)
518
535
  return;
519
536
  const soundFont = this.soundFonts[soundFontIndex];
520
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
537
+ const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
521
538
  const { instrument, sampleID } = voice.generators;
522
539
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
523
540
  }
@@ -540,7 +557,7 @@ export class Midy {
540
557
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
541
558
  channel.channelPressureTable.fill(-1);
542
559
  channel.polyphonicKeyPressureTable.fill(-1);
543
- channel.keyBasedInstrumentControlTable.fill(-1);
560
+ channel.keyBasedTable.fill(-1);
544
561
  }
545
562
  createChannels(audioContext) {
546
563
  const channels = Array.from({ length: this.numChannels }, () => {
@@ -557,7 +574,7 @@ export class Midy {
557
574
  scaleOctaveTuningTable: new Float32Array(12), // [-100, 100] cent
558
575
  channelPressureTable: new Int8Array(6).fill(-1),
559
576
  polyphonicKeyPressureTable: new Int8Array(6).fill(-1),
560
- keyBasedInstrumentControlTable: new Int8Array(128 * 128).fill(-1),
577
+ keyBasedTable: new Int8Array(128 * 128).fill(-1),
561
578
  keyBasedGainLs: new Array(128),
562
579
  keyBasedGainRs: new Array(128),
563
580
  };
@@ -588,13 +605,16 @@ export class Midy {
588
605
  }
589
606
  return bufferSource;
590
607
  }
591
- async scheduleTimelineEvents(t, resumeTime, queueIndex) {
592
- while (queueIndex < this.timeline.length) {
593
- const event = this.timeline[queueIndex];
594
- if (event.startTime > t + this.lookAhead)
608
+ async scheduleTimelineEvents(scheduleTime, queueIndex) {
609
+ const timeOffset = this.resumeTime - this.startTime;
610
+ const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
611
+ const schedulingOffset = this.startDelay - timeOffset;
612
+ const timeline = this.timeline;
613
+ while (queueIndex < timeline.length) {
614
+ const event = timeline[queueIndex];
615
+ if (lookAheadCheckTime < event.startTime)
595
616
  break;
596
- const delay = this.startDelay - resumeTime;
597
- const startTime = event.startTime + delay;
617
+ const startTime = event.startTime + schedulingOffset;
598
618
  switch (event.type) {
599
619
  case "noteOn":
600
620
  await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
@@ -635,72 +655,85 @@ export class Midy {
635
655
  }
636
656
  return 0;
637
657
  }
638
- playNotes() {
639
- return new Promise((resolve) => {
640
- this.isPlaying = true;
641
- this.isPaused = false;
642
- this.startTime = this.audioContext.currentTime;
643
- let queueIndex = this.getQueueIndex(this.resumeTime);
644
- let resumeTime = this.resumeTime - this.startTime;
658
+ resetAllStates() {
659
+ this.exclusiveClassNotes.fill(undefined);
660
+ this.drumExclusiveClassNotes.fill(undefined);
661
+ this.voiceCache.clear();
662
+ for (let i = 0; i < this.channels.length; i++) {
663
+ this.channels[i].scheduledNotes = [];
664
+ this.resetChannelStates(i);
665
+ }
666
+ }
667
+ updateStates(queueIndex, nextQueueIndex) {
668
+ if (nextQueueIndex < queueIndex)
669
+ queueIndex = 0;
670
+ for (let i = queueIndex; i < nextQueueIndex; i++) {
671
+ const event = this.timeline[i];
672
+ switch (event.type) {
673
+ case "controller":
674
+ this.setControlChange(event.channel, event.controllerType, event.value, 0);
675
+ break;
676
+ case "programChange":
677
+ this.setProgramChange(event.channel, event.programNumber, 0);
678
+ break;
679
+ case "pitchBend":
680
+ this.setPitchBend(event.channel, event.value + 8192, 0);
681
+ break;
682
+ case "sysEx":
683
+ this.handleSysEx(event.data, 0);
684
+ }
685
+ }
686
+ }
687
+ async playNotes() {
688
+ if (this.audioContext.state === "suspended") {
689
+ await this.audioContext.resume();
690
+ }
691
+ this.isPlaying = true;
692
+ this.isPaused = false;
693
+ this.startTime = this.audioContext.currentTime;
694
+ let queueIndex = this.getQueueIndex(this.resumeTime);
695
+ let finished = false;
696
+ this.notePromises = [];
697
+ while (queueIndex < this.timeline.length) {
698
+ const now = this.audioContext.currentTime;
699
+ if (0 < this.lastActiveSensing &&
700
+ this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
701
+ await this.stopNotes(0, true, now);
702
+ await this.audioContext.suspend();
703
+ finished = true;
704
+ break;
705
+ }
706
+ queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
707
+ if (this.isPausing) {
708
+ await this.stopNotes(0, true, now);
709
+ await this.audioContext.suspend();
710
+ this.notePromises = [];
711
+ break;
712
+ }
713
+ else if (this.isStopping) {
714
+ await this.stopNotes(0, true, now);
715
+ await this.audioContext.suspend();
716
+ finished = true;
717
+ break;
718
+ }
719
+ else if (this.isSeeking) {
720
+ await this.stopNotes(0, true, now);
721
+ this.startTime = this.audioContext.currentTime;
722
+ const nextQueueIndex = this.getQueueIndex(this.resumeTime);
723
+ this.updateStates(queueIndex, nextQueueIndex);
724
+ queueIndex = nextQueueIndex;
725
+ this.isSeeking = false;
726
+ continue;
727
+ }
728
+ const waitTime = now + this.noteCheckInterval;
729
+ await this.scheduleTask(() => { }, waitTime);
730
+ }
731
+ if (finished) {
645
732
  this.notePromises = [];
646
- const schedulePlayback = async () => {
647
- if (queueIndex >= this.timeline.length) {
648
- await Promise.all(this.notePromises);
649
- this.notePromises = [];
650
- this.exclusiveClassNotes.fill(undefined);
651
- this.drumExclusiveClassNotes.fill(undefined);
652
- this.voiceCache.clear();
653
- for (let i = 0; i < this.channels.length; i++) {
654
- this.channels[i].scheduledNotes = [];
655
- this.resetAllStates(i);
656
- }
657
- resolve();
658
- return;
659
- }
660
- const now = this.audioContext.currentTime;
661
- const t = now + resumeTime;
662
- queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
663
- if (this.isPausing) {
664
- await this.stopNotes(0, true, now);
665
- this.notePromises = [];
666
- this.isPausing = false;
667
- this.isPaused = true;
668
- resolve();
669
- return;
670
- }
671
- else if (this.isStopping) {
672
- await this.stopNotes(0, true, now);
673
- this.notePromises = [];
674
- this.exclusiveClassNotes.fill(undefined);
675
- this.drumExclusiveClassNotes.fill(undefined);
676
- this.voiceCache.clear();
677
- for (let i = 0; i < this.channels.length; i++) {
678
- this.channels[i].scheduledNotes = [];
679
- this.resetAllStates(i);
680
- }
681
- this.isStopping = false;
682
- this.isPaused = false;
683
- resolve();
684
- return;
685
- }
686
- else if (this.isSeeking) {
687
- this.stopNotes(0, true, now);
688
- this.exclusiveClassNotes.fill(undefined);
689
- this.drumExclusiveClassNotes.fill(undefined);
690
- this.startTime = this.audioContext.currentTime;
691
- queueIndex = this.getQueueIndex(this.resumeTime);
692
- resumeTime = this.resumeTime - this.startTime;
693
- this.isSeeking = false;
694
- await schedulePlayback();
695
- }
696
- else {
697
- const waitTime = now + this.noteCheckInterval;
698
- await this.scheduleTask(() => { }, waitTime);
699
- await schedulePlayback();
700
- }
701
- };
702
- schedulePlayback();
703
- });
733
+ this.resetAllStates();
734
+ this.lastActiveSensing = 0;
735
+ }
736
+ this.isPlaying = false;
704
737
  }
705
738
  ticksToSecond(ticks, secondsPerBeat) {
706
739
  return ticks * secondsPerBeat / this.ticksPerBeat;
@@ -708,17 +741,17 @@ export class Midy {
708
741
  secondToTicks(second, secondsPerBeat) {
709
742
  return second * this.ticksPerBeat / secondsPerBeat;
710
743
  }
744
+ getSoundFontId(channel) {
745
+ const programNumber = channel.programNumber;
746
+ const bankNumber = channel.isDrum ? 128 : channel.bankLSB;
747
+ const bank = bankNumber.toString().padStart(3, "0");
748
+ const program = programNumber.toString().padStart(3, "0");
749
+ return `${bank}:${program}`;
750
+ }
711
751
  extractMidiData(midi) {
712
752
  const instruments = new Set();
713
753
  const timeline = [];
714
- const tmpChannels = new Array(this.channels.length);
715
- for (let i = 0; i < tmpChannels.length; i++) {
716
- tmpChannels[i] = {
717
- programNumber: -1,
718
- bankMSB: this.channels[i].bankMSB,
719
- bankLSB: this.channels[i].bankLSB,
720
- };
721
- }
754
+ const channels = this.channels;
722
755
  for (let i = 0; i < midi.tracks.length; i++) {
723
756
  const track = midi.tracks[i];
724
757
  let currentTicks = 0;
@@ -728,48 +761,40 @@ export class Midy {
728
761
  event.ticks = currentTicks;
729
762
  switch (event.type) {
730
763
  case "noteOn": {
731
- const channel = tmpChannels[event.channel];
732
- if (channel.programNumber < 0) {
733
- channel.programNumber = event.programNumber;
734
- switch (channel.bankMSB) {
735
- case 120:
736
- instruments.add(`128:0`);
737
- break;
738
- case 121:
739
- instruments.add(`${channel.bankLSB}:0`);
740
- break;
741
- default: {
742
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
743
- instruments.add(`${bankNumber}:0`);
744
- }
745
- }
746
- channel.programNumber = 0;
747
- }
764
+ const channel = channels[event.channel];
765
+ instruments.add(this.getSoundFontId(channel));
748
766
  break;
749
767
  }
750
768
  case "controller":
751
769
  switch (event.controllerType) {
752
770
  case 0:
753
- tmpChannels[event.channel].bankMSB = event.value;
771
+ this.setBankMSB(event.channel, event.value);
754
772
  break;
755
773
  case 32:
756
- tmpChannels[event.channel].bankLSB = event.value;
774
+ this.setBankLSB(event.channel, event.value);
757
775
  break;
758
776
  }
759
777
  break;
760
778
  case "programChange": {
761
- const channel = tmpChannels[event.channel];
762
- channel.programNumber = event.programNumber;
763
- switch (channel.bankMSB) {
764
- case 120:
765
- instruments.add(`128:${channel.programNumber}`);
766
- break;
767
- case 121:
768
- instruments.add(`${channel.bankLSB}:${channel.programNumber}`);
769
- break;
770
- default: {
771
- const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
772
- instruments.add(`${bankNumber}:${channel.programNumber}`);
779
+ const channel = channels[event.channel];
780
+ this.setProgramChange(event.channel, event.programNumber);
781
+ instruments.add(this.getSoundFontId(channel));
782
+ break;
783
+ }
784
+ case "sysEx": {
785
+ const data = event.data;
786
+ if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
787
+ switch (data[3]) {
788
+ case 1:
789
+ this.GM1SystemOn(scheduleTime);
790
+ break;
791
+ case 2: // GM System Off
792
+ break;
793
+ case 3:
794
+ this.GM2SystemOn(scheduleTime);
795
+ break;
796
+ default:
797
+ console.warn(`Unsupported Exclusive Message: ${data}`);
773
798
  }
774
799
  }
775
800
  }
@@ -837,26 +862,32 @@ export class Midy {
837
862
  this.resumeTime = 0;
838
863
  if (this.voiceCounter.size === 0)
839
864
  this.cacheVoiceIds();
840
- await this.playNotes();
841
- this.isPlaying = false;
865
+ this.playPromise = this.playNotes();
866
+ await this.playPromise;
842
867
  }
843
- stop() {
868
+ async stop() {
844
869
  if (!this.isPlaying)
845
870
  return;
846
871
  this.isStopping = true;
872
+ await this.playPromise;
873
+ this.isStopping = false;
847
874
  }
848
- pause() {
875
+ async pause() {
849
876
  if (!this.isPlaying || this.isPaused)
850
877
  return;
851
878
  const now = this.audioContext.currentTime;
852
879
  this.resumeTime += now - this.startTime - this.startDelay;
853
880
  this.isPausing = true;
881
+ await this.playPromise;
882
+ this.isPausing = false;
883
+ this.isPaused = true;
854
884
  }
855
885
  async resume() {
856
886
  if (!this.isPaused)
857
887
  return;
858
- await this.playNotes();
859
- this.isPlaying = false;
888
+ this.playPromise = this.playNotes();
889
+ await this.playPromise;
890
+ this.isPaused = false;
860
891
  }
861
892
  seekTo(second) {
862
893
  this.resumeTime = second;
@@ -1087,7 +1118,7 @@ export class Midy {
1087
1118
  const noteDetune = this.calcNoteDetune(channel, note);
1088
1119
  const pitchControl = this.getPitchControl(channel, note);
1089
1120
  const detune = channel.detune + noteDetune + pitchControl;
1090
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1121
+ if (this.isPortamento(channel, note)) {
1091
1122
  const startTime = note.startTime;
1092
1123
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
1093
1124
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
@@ -1164,28 +1195,29 @@ export class Midy {
1164
1195
  return Math.exp(y);
1165
1196
  }
1166
1197
  setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1167
- const state = channel.state;
1168
1198
  const { voiceParams, startTime } = note;
1169
1199
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1170
1200
  (1 + this.getAmplitudeControl(channel, note));
1171
1201
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1172
1202
  const volDelay = startTime + voiceParams.volDelay;
1173
- const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1203
+ const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1204
+ const volAttack = volDelay + voiceParams.volAttack * attackTime;
1174
1205
  const volHold = volAttack + voiceParams.volHold;
1175
1206
  note.volumeEnvelopeNode.gain
1176
1207
  .cancelScheduledValues(scheduleTime)
1177
1208
  .setValueAtTime(sustainVolume, volHold);
1178
1209
  }
1179
1210
  setVolumeEnvelope(channel, note, scheduleTime) {
1180
- const state = channel.state;
1181
1211
  const { voiceParams, startTime } = note;
1182
1212
  const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1183
1213
  (1 + this.getAmplitudeControl(channel, note));
1184
1214
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1185
1215
  const volDelay = startTime + voiceParams.volDelay;
1186
- const volAttack = volDelay + voiceParams.volAttack * state.attackTime * 2;
1216
+ const attackTime = this.getRelativeKeyBasedValue(channel, note, 73) * 2;
1217
+ const volAttack = volDelay + voiceParams.volAttack * attackTime;
1187
1218
  const volHold = volAttack + voiceParams.volHold;
1188
- const volDecay = volHold + voiceParams.volDecay * state.decayTime * 2;
1219
+ const decayTime = this.getRelativeKeyBasedValue(channel, note, 75) * 2;
1220
+ const volDecay = volHold + voiceParams.volDecay * decayTime;
1189
1221
  note.volumeEnvelopeNode.gain
1190
1222
  .cancelScheduledValues(scheduleTime)
1191
1223
  .setValueAtTime(0, startTime)
@@ -1228,14 +1260,13 @@ export class Midy {
1228
1260
  return Math.max(minFrequency, Math.min(frequency, maxFrequency));
1229
1261
  }
1230
1262
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1231
- const state = channel.state;
1232
1263
  const { voiceParams, startTime } = note;
1233
1264
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1234
1265
  const baseCent = voiceParams.initialFilterFc +
1235
1266
  this.getFilterCutoffControl(channel, note);
1236
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1237
- state.brightness * 2;
1238
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor * state.brightness * 2;
1267
+ const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1268
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor * brightness;
1269
+ const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * brightness;
1239
1270
  const sustainFreq = baseFreq +
1240
1271
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1241
1272
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1249,15 +1280,14 @@ export class Midy {
1249
1280
  .linearRampToValueAtTime(adjustedSustainFreq, portamentoTime);
1250
1281
  }
1251
1282
  setFilterEnvelope(channel, note, scheduleTime) {
1252
- const state = channel.state;
1253
1283
  const { voiceParams, startTime } = note;
1254
1284
  const softPedalFactor = this.getSoftPedalFactor(channel, note);
1255
1285
  const baseCent = voiceParams.initialFilterFc +
1256
1286
  this.getFilterCutoffControl(channel, note);
1257
- const baseFreq = this.centToHz(baseCent) * softPedalFactor *
1258
- state.brightness * 2;
1287
+ const brightness = this.getRelativeKeyBasedValue(channel, note, 74) * 2;
1288
+ const baseFreq = this.centToHz(baseCent) * softPedalFactor * brightness;
1259
1289
  const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1260
- softPedalFactor * state.brightness * 2;
1290
+ softPedalFactor * brightness;
1261
1291
  const sustainFreq = baseFreq +
1262
1292
  (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1263
1293
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
@@ -1297,11 +1327,12 @@ export class Midy {
1297
1327
  }
1298
1328
  startVibrato(channel, note, scheduleTime) {
1299
1329
  const { voiceParams } = note;
1300
- const state = channel.state;
1330
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1331
+ const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
1301
1332
  note.vibratoLFO = new OscillatorNode(this.audioContext, {
1302
- frequency: this.centToHz(voiceParams.freqVibLFO) * state.vibratoRate * 2,
1333
+ frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
1303
1334
  });
1304
- note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * state.vibratoDelay * 2);
1335
+ note.vibratoLFO.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
1305
1336
  note.vibratoDepth = new GainNode(this.audioContext);
1306
1337
  this.setVibLfoToPitch(channel, note, scheduleTime);
1307
1338
  note.vibratoLFO.connect(note.vibratoDepth);
@@ -1334,15 +1365,16 @@ export class Midy {
1334
1365
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1335
1366
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1336
1367
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1368
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
1337
1369
  note.filterNode = new BiquadFilterNode(this.audioContext, {
1338
1370
  type: "lowpass",
1339
- Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1371
+ Q: voiceParams.initialFilterQ / 5 * filterResonance, // dB
1340
1372
  });
1341
1373
  const prevNote = channel.scheduledNotes.at(-1);
1342
1374
  if (prevNote && prevNote.noteNumber !== noteNumber) {
1343
1375
  note.portamentoNoteNumber = prevNote.noteNumber;
1344
1376
  }
1345
- if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
1377
+ if (!channel.isDrum && this.isPortamento(channel, note)) {
1346
1378
  this.setPortamentoVolumeEnvelope(channel, note, now);
1347
1379
  this.setPortamentoFilterEnvelope(channel, note, now);
1348
1380
  this.setPortamentoPitchEnvelope(note, now);
@@ -1370,22 +1402,6 @@ export class Midy {
1370
1402
  note.bufferSource.start(startTime);
1371
1403
  return note;
1372
1404
  }
1373
- calcBank(channel) {
1374
- switch (this.mode) {
1375
- case "GM1":
1376
- if (channel.isDrum)
1377
- return 128;
1378
- return 0;
1379
- case "GM2":
1380
- if (channel.bankMSB === 121)
1381
- return 0;
1382
- if (channel.isDrum)
1383
- return 128;
1384
- return channel.bank;
1385
- default:
1386
- return channel.bank;
1387
- }
1388
- }
1389
1405
  handleExclusiveClass(note, channelNumber, startTime) {
1390
1406
  const exclusiveClass = note.voiceParams.exclusiveClass;
1391
1407
  if (exclusiveClass === 0)
@@ -1421,13 +1437,17 @@ export class Midy {
1421
1437
  }
1422
1438
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1423
1439
  const channel = this.channels[channelNumber];
1424
- const bankNumber = this.calcBank(channel, channelNumber);
1425
- const soundFontIndex = this.soundFontTable[channel.programNumber]
1426
- .get(bankNumber);
1440
+ const programNumber = channel.programNumber;
1441
+ const bankTable = this.soundFontTable[programNumber];
1442
+ if (!bankTable)
1443
+ return;
1444
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1445
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1446
+ const soundFontIndex = bankTable[bank];
1427
1447
  if (soundFontIndex === undefined)
1428
1448
  return;
1429
1449
  const soundFont = this.soundFonts[soundFontIndex];
1430
- const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1450
+ const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1431
1451
  if (!voice)
1432
1452
  return;
1433
1453
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
@@ -1481,8 +1501,8 @@ export class Midy {
1481
1501
  }
1482
1502
  }
1483
1503
  releaseNote(channel, note, endTime) {
1484
- const volRelease = endTime +
1485
- note.voiceParams.volRelease * channel.state.releaseTime * 2;
1504
+ const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1505
+ const volRelease = endTime + note.voiceParams.volRelease * releaseTime;
1486
1506
  const modRelease = endTime + note.voiceParams.modRelease;
1487
1507
  const stopTime = Math.min(volRelease, modRelease);
1488
1508
  note.filterNode.frequency
@@ -1580,27 +1600,42 @@ export class Midy {
1580
1600
  channel.sostenutoNotes = [];
1581
1601
  return promises;
1582
1602
  }
1583
- handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1584
- const channelNumber = statusByte & 0x0F;
1585
- const messageType = statusByte & 0xF0;
1586
- switch (messageType) {
1587
- case 0x80:
1588
- return this.noteOff(channelNumber, data1, data2, scheduleTime);
1589
- case 0x90:
1590
- return this.noteOn(channelNumber, data1, data2, scheduleTime);
1591
- case 0xA0:
1592
- return this.setPolyphonicKeyPressure(channelNumber, data1, data2, scheduleTime);
1593
- case 0xB0:
1594
- return this.setControlChange(channelNumber, data1, data2, scheduleTime);
1595
- case 0xC0:
1596
- return this.setProgramChange(channelNumber, data1, scheduleTime);
1597
- case 0xD0:
1598
- return this.setChannelPressure(channelNumber, data1, scheduleTime);
1599
- case 0xE0:
1600
- return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
1601
- default:
1602
- console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1603
+ createMessageHandlers() {
1604
+ const handlers = new Array(256);
1605
+ // Channel Message
1606
+ handlers[0x80] = (data, scheduleTime) => this.noteOff(data[0] & 0x0F, data[1], data[2], scheduleTime);
1607
+ handlers[0x90] = (data, scheduleTime) => this.noteOn(data[0] & 0x0F, data[1], data[2], scheduleTime);
1608
+ handlers[0xA0] = (data, scheduleTime) => this.setPolyphonicKeyPressure(data[0] & 0x0F, data[1], data[2], scheduleTime);
1609
+ handlers[0xB0] = (data, scheduleTime) => this.setControlChange(data[0] & 0x0F, data[1], data[2], scheduleTime);
1610
+ handlers[0xC0] = (data, scheduleTime) => this.setProgramChange(data[0] & 0x0F, data[1], scheduleTime);
1611
+ handlers[0xD0] = (data, scheduleTime) => this.setChannelPressure(data[0] & 0x0F, data[1], scheduleTime);
1612
+ handlers[0xE0] = (data, scheduleTime) => this.handlePitchBendMessage(data[0] & 0x0F, data[1], data[2], scheduleTime);
1613
+ // System Common Message
1614
+ // handlers[0xF1] = (_data, _scheduleTime) => {}; // MTC Quarter Frame
1615
+ // handlers[0xF2] = (_data, _scheduleTime) => {}; // Song Position Pointer
1616
+ // handlers[0xF3] = (_data, _scheduleTime) => {}; // Song Select
1617
+ // handlers[0xF6] = (_data, _scheduleTime) => {}; // Tune Request
1618
+ // handlers[0xF7] = (_data, _scheduleTime) => {}; // End of Exclusive (EOX)
1619
+ // System Real Time Message
1620
+ // handlers[0xF8] = (_data, _scheduleTime) => {}; // Timing Clock
1621
+ // handlers[0xFA] = (_data, _scheduleTime) => {}; // Start
1622
+ // handlers[0xFB] = (_data, _scheduleTime) => {}; // Continue
1623
+ // handlers[0xFC] = (_data, _scheduleTime) => {}; // Stop
1624
+ handlers[0xFE] = (_data, _scheduleTime) => this.activeSensing();
1625
+ // handlers[0xFF] = (_data, _scheduleTime) => {}; // Reset
1626
+ return handlers;
1627
+ }
1628
+ handleMessage(data, scheduleTime) {
1629
+ const status = data[0];
1630
+ if (status === 0xF0) {
1631
+ return this.handleSysEx(data.subarray(1), scheduleTime);
1603
1632
  }
1633
+ const handler = this.messageHandlers[status];
1634
+ if (handler)
1635
+ handler(data, scheduleTime);
1636
+ }
1637
+ activeSensing() {
1638
+ this.lastActiveSensing = performance.now();
1604
1639
  }
1605
1640
  setPolyphonicKeyPressure(channelNumber, noteNumber, pressure, scheduleTime) {
1606
1641
  const channel = this.channels[channelNumber];
@@ -1615,12 +1650,12 @@ export class Midy {
1615
1650
  }
1616
1651
  setProgramChange(channelNumber, programNumber, _scheduleTime) {
1617
1652
  const channel = this.channels[channelNumber];
1618
- channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1619
1653
  channel.programNumber = programNumber;
1620
1654
  if (this.mode === "GM2") {
1621
1655
  switch (channel.bankMSB) {
1622
1656
  case 120:
1623
1657
  channel.isDrum = true;
1658
+ channel.keyBasedTable.fill(-1);
1624
1659
  break;
1625
1660
  case 121:
1626
1661
  channel.isDrum = false;
@@ -1668,23 +1703,29 @@ export class Midy {
1668
1703
  const modLfoToPitch = note.voiceParams.modLfoToPitch +
1669
1704
  this.getLFOPitchDepth(channel, note);
1670
1705
  const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1671
- const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
1706
+ const depth = baseDepth * Math.sign(modLfoToPitch);
1672
1707
  note.modulationDepth.gain
1673
1708
  .cancelScheduledValues(scheduleTime)
1674
- .setValueAtTime(modulationDepth, scheduleTime);
1709
+ .setValueAtTime(depth, scheduleTime);
1675
1710
  }
1676
1711
  else {
1677
1712
  this.startModulation(channel, note, scheduleTime);
1678
1713
  }
1679
1714
  }
1680
1715
  setVibLfoToPitch(channel, note, scheduleTime) {
1681
- const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1682
- const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
1683
- 2;
1684
- const vibratoDepthSign = 0 < vibLfoToPitch;
1685
- note.vibratoDepth.gain
1686
- .cancelScheduledValues(scheduleTime)
1687
- .setValueAtTime(vibratoDepth * vibratoDepthSign, scheduleTime);
1716
+ if (note.vibratoDepth) {
1717
+ const vibratoDepth = this.getKeyBasedValue(channel, note.noteNumber, 77) *
1718
+ 2;
1719
+ const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
1720
+ const baseDepth = Math.abs(vibLfoToPitch) * vibratoDepth;
1721
+ const depth = baseDepth * Math.sign(vibLfoToPitch);
1722
+ note.vibratoDepth.gain
1723
+ .cancelScheduledValues(scheduleTime)
1724
+ .setValueAtTime(depth, scheduleTime);
1725
+ }
1726
+ else {
1727
+ this.startVibrato(channel, note, scheduleTime);
1728
+ }
1688
1729
  }
1689
1730
  setModLfoToFilterFc(channel, note, scheduleTime) {
1690
1731
  const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
@@ -1762,13 +1803,12 @@ export class Midy {
1762
1803
  }
1763
1804
  }
1764
1805
  }
1765
- setDelayModLFO(note, scheduleTime) {
1766
- const startTime = note.startTime;
1767
- if (startTime < scheduleTime)
1768
- return;
1769
- note.modulationLFO.stop(scheduleTime);
1770
- note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
1771
- note.modulationLFO.connect(note.filterDepth);
1806
+ setDelayModLFO(note) {
1807
+ const startTime = note.startTime + note.voiceParams.delayModLFO;
1808
+ try {
1809
+ note.modulationLFO.start(startTime);
1810
+ }
1811
+ catch { /* empty */ }
1772
1812
  }
1773
1813
  setFreqModLFO(note, scheduleTime) {
1774
1814
  const freqModLFO = note.voiceParams.freqModLFO;
@@ -1777,54 +1817,65 @@ export class Midy {
1777
1817
  .setValueAtTime(freqModLFO, scheduleTime);
1778
1818
  }
1779
1819
  setFreqVibLFO(channel, note, scheduleTime) {
1820
+ const vibratoRate = this.getRelativeKeyBasedValue(channel, note, 76) * 2;
1780
1821
  const freqVibLFO = note.voiceParams.freqVibLFO;
1781
1822
  note.vibratoLFO.frequency
1782
1823
  .cancelScheduledValues(scheduleTime)
1783
- .setValueAtTime(freqVibLFO * channel.state.vibratoRate * 2, scheduleTime);
1824
+ .setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
1825
+ }
1826
+ setDelayVibLFO(channel, note) {
1827
+ const vibratoDelay = this.getRelativeKeyBasedValue(channel, note, 78) * 2;
1828
+ const value = note.voiceParams.delayVibLFO;
1829
+ const startTime = note.startTime + value * vibratoDelay;
1830
+ try {
1831
+ note.vibratoLFO.start(startTime);
1832
+ }
1833
+ catch { /* empty */ }
1784
1834
  }
1785
1835
  createVoiceParamsHandlers() {
1786
1836
  return {
1787
- modLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1837
+ modLfoToPitch: (channel, note, scheduleTime) => {
1788
1838
  if (0 < channel.state.modulationDepth) {
1789
1839
  this.setModLfoToPitch(channel, note, scheduleTime);
1790
1840
  }
1791
1841
  },
1792
- vibLfoToPitch: (channel, note, _prevValue, scheduleTime) => {
1842
+ vibLfoToPitch: (channel, note, scheduleTime) => {
1793
1843
  if (0 < channel.state.vibratoDepth) {
1794
1844
  this.setVibLfoToPitch(channel, note, scheduleTime);
1795
1845
  }
1796
1846
  },
1797
- modLfoToFilterFc: (channel, note, _prevValue, scheduleTime) => {
1847
+ modLfoToFilterFc: (channel, note, scheduleTime) => {
1798
1848
  if (0 < channel.state.modulationDepth) {
1799
1849
  this.setModLfoToFilterFc(channel, note, scheduleTime);
1800
1850
  }
1801
1851
  },
1802
- modLfoToVolume: (channel, note, _prevValue, scheduleTime) => {
1852
+ modLfoToVolume: (channel, note, scheduleTime) => {
1803
1853
  if (0 < channel.state.modulationDepth) {
1804
1854
  this.setModLfoToVolume(channel, note, scheduleTime);
1805
1855
  }
1806
1856
  },
1807
- chorusEffectsSend: (channel, note, _prevValue, scheduleTime) => {
1857
+ chorusEffectsSend: (channel, note, scheduleTime) => {
1808
1858
  this.setChorusSend(channel, note, scheduleTime);
1809
1859
  },
1810
- reverbEffectsSend: (channel, note, _prevValue, scheduleTime) => {
1860
+ reverbEffectsSend: (channel, note, scheduleTime) => {
1811
1861
  this.setReverbSend(channel, note, scheduleTime);
1812
1862
  },
1813
- delayModLFO: (_channel, note, _prevValue, scheduleTime) => this.setDelayModLFO(note, scheduleTime),
1814
- freqModLFO: (_channel, note, _prevValue, scheduleTime) => this.setFreqModLFO(note, scheduleTime),
1815
- delayVibLFO: (channel, note, prevValue, scheduleTime) => {
1863
+ delayModLFO: (_channel, note, _scheduleTime) => {
1864
+ if (0 < channel.state.modulationDepth) {
1865
+ this.setDelayModLFO(note);
1866
+ }
1867
+ },
1868
+ freqModLFO: (_channel, note, scheduleTime) => {
1869
+ if (0 < channel.state.modulationDepth) {
1870
+ this.setFreqModLFO(note, scheduleTime);
1871
+ }
1872
+ },
1873
+ delayVibLFO: (channel, note, _scheduleTime) => {
1816
1874
  if (0 < channel.state.vibratoDepth) {
1817
- const vibratoDelay = channel.state.vibratoDelay * 2;
1818
- const prevStartTime = note.startTime + prevValue * vibratoDelay;
1819
- if (scheduleTime < prevStartTime)
1820
- return;
1821
- const value = note.voiceParams.delayVibLFO;
1822
- const startTime = note.startTime + value * vibratoDelay;
1823
- note.vibratoLFO.stop(scheduleTime);
1824
- note.vibratoLFO.start(startTime);
1875
+ setDelayVibLFO(channel, note);
1825
1876
  }
1826
1877
  },
1827
- freqVibLFO: (channel, note, _prevValue, scheduleTime) => {
1878
+ freqVibLFO: (channel, note, scheduleTime) => {
1828
1879
  if (0 < channel.state.vibratoDepth) {
1829
1880
  this.setFreqVibLFO(channel, note, scheduleTime);
1830
1881
  }
@@ -1853,7 +1904,7 @@ export class Midy {
1853
1904
  continue;
1854
1905
  note.voiceParams[key] = value;
1855
1906
  if (key in this.voiceParamsHandlers) {
1856
- this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1907
+ this.voiceParamsHandlers[key](channel, note, scheduleTime);
1857
1908
  }
1858
1909
  else {
1859
1910
  if (volumeEnvelopeKeySet.has(key))
@@ -1947,22 +1998,20 @@ export class Midy {
1947
1998
  this.updateModulation(channel, scheduleTime);
1948
1999
  }
1949
2000
  updatePortamento(channel, scheduleTime) {
2001
+ if (channel.isDrum)
2002
+ return;
1950
2003
  this.processScheduledNotes(channel, (note) => {
1951
- if (0.5 <= channel.state.portamento) {
1952
- if (0 <= note.portamentoNoteNumber) {
1953
- this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
1954
- this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
1955
- this.setPortamentoPitchEnvelope(note, scheduleTime);
1956
- this.updateDetune(channel, note, scheduleTime);
1957
- }
2004
+ if (this.isPortamento(channel, note)) {
2005
+ this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2006
+ this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2007
+ this.setPortamentoPitchEnvelope(note, scheduleTime);
2008
+ this.updateDetune(channel, note, scheduleTime);
1958
2009
  }
1959
2010
  else {
1960
- if (0 <= note.portamentoNoteNumber) {
1961
- this.setVolumeEnvelope(channel, note, scheduleTime);
1962
- this.setFilterEnvelope(channel, note, scheduleTime);
1963
- this.setPitchEnvelope(note, scheduleTime);
1964
- this.updateDetune(channel, note, scheduleTime);
1965
- }
2011
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2012
+ this.setFilterEnvelope(channel, note, scheduleTime);
2013
+ this.setPitchEnvelope(note, scheduleTime);
2014
+ this.updateDetune(channel, note, scheduleTime);
1966
2015
  }
1967
2016
  });
1968
2017
  }
@@ -2032,13 +2081,13 @@ export class Midy {
2032
2081
  .setValueAtTime(volume * gainRight, scheduleTime);
2033
2082
  }
2034
2083
  updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
2035
- const state = channel.state;
2036
- const defaultVolume = state.volume * state.expression;
2037
- const defaultPan = state.pan;
2038
2084
  const gainL = channel.keyBasedGainLs[keyNumber];
2039
- const gainR = channel.keyBasedGainRs[keyNumber];
2040
2085
  if (!gainL)
2041
2086
  return;
2087
+ const gainR = channel.keyBasedGainRs[keyNumber];
2088
+ const state = channel.state;
2089
+ const defaultVolume = state.volume * state.expression;
2090
+ const defaultPan = state.pan;
2042
2091
  const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
2043
2092
  const volume = (0 <= keyBasedVolume)
2044
2093
  ? defaultVolume * keyBasedVolume / 64
@@ -2068,6 +2117,9 @@ export class Midy {
2068
2117
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
2069
2118
  }
2070
2119
  }
2120
+ isPortamento(channel, note) {
2121
+ return 0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber;
2122
+ }
2071
2123
  setPortamento(channelNumber, value, scheduleTime) {
2072
2124
  const channel = this.channels[channelNumber];
2073
2125
  if (channel.isDrum)
@@ -2104,7 +2156,7 @@ export class Midy {
2104
2156
  scheduleTime ??= this.audioContext.currentTime;
2105
2157
  state.softPedal = softPedal / 127;
2106
2158
  this.processScheduledNotes(channel, (note) => {
2107
- if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2159
+ if (this.isPortamento(channel, note)) {
2108
2160
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2109
2161
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2110
2162
  }
@@ -2114,18 +2166,27 @@ export class Midy {
2114
2166
  }
2115
2167
  });
2116
2168
  }
2117
- setFilterResonance(channelNumber, filterResonance, scheduleTime) {
2169
+ setFilterResonance(channelNumber, ccValue, scheduleTime) {
2118
2170
  const channel = this.channels[channelNumber];
2119
2171
  if (channel.isDrum)
2120
2172
  return;
2121
2173
  scheduleTime ??= this.audioContext.currentTime;
2122
2174
  const state = channel.state;
2123
- state.filterResonance = filterResonance / 127;
2175
+ state.filterResonance = ccValue / 127;
2176
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2124
2177
  this.processScheduledNotes(channel, (note) => {
2125
- const Q = note.voiceParams.initialFilterQ / 5 * state.filterResonance;
2178
+ const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
2126
2179
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2127
2180
  });
2128
2181
  }
2182
+ getRelativeKeyBasedValue(channel, note, controllerType) {
2183
+ const ccState = channel.state.array[128 + controllerType];
2184
+ const keyBasedValue = this.getKeyBasedValue(channel, note.noteNumber, controllerType);
2185
+ if (keyBasedValue < 0)
2186
+ return ccState;
2187
+ const keyValue = ccState + keyBasedValue / 127 - 0.5;
2188
+ return keyValue < 0 ? keyValue : 0;
2189
+ }
2129
2190
  setReleaseTime(channelNumber, releaseTime, scheduleTime) {
2130
2191
  const channel = this.channels[channelNumber];
2131
2192
  if (channel.isDrum)
@@ -2140,9 +2201,9 @@ export class Midy {
2140
2201
  scheduleTime ??= this.audioContext.currentTime;
2141
2202
  channel.state.attackTime = attackTime / 127;
2142
2203
  this.processScheduledNotes(channel, (note) => {
2143
- if (note.startTime < scheduleTime)
2144
- return false;
2145
- this.setVolumeEnvelope(channel, note, scheduleTime);
2204
+ if (scheduleTime < note.startTime) {
2205
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2206
+ }
2146
2207
  });
2147
2208
  }
2148
2209
  setBrightness(channelNumber, brightness, scheduleTime) {
@@ -2153,7 +2214,7 @@ export class Midy {
2153
2214
  scheduleTime ??= this.audioContext.currentTime;
2154
2215
  state.brightness = brightness / 127;
2155
2216
  this.processScheduledNotes(channel, (note) => {
2156
- if (0.5 <= state.portamento && 0 <= note.portamentoNoteNumber) {
2217
+ if (this.isPortamento(channel, note)) {
2157
2218
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2158
2219
  }
2159
2220
  else {
@@ -2303,8 +2364,8 @@ export class Midy {
2303
2364
  }
2304
2365
  handlePitchBendRangeRPN(channelNumber, scheduleTime) {
2305
2366
  const channel = this.channels[channelNumber];
2306
- this.limitData(channel, 0, 127, 0, 99);
2307
- const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
2367
+ this.limitData(channel, 0, 127, 0, 127);
2368
+ const pitchBendRange = (channel.dataMSB + channel.dataLSB / 128) * 100;
2308
2369
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
2309
2370
  }
2310
2371
  setPitchBendRange(channelNumber, value, scheduleTime) {
@@ -2314,7 +2375,7 @@ export class Midy {
2314
2375
  scheduleTime ??= this.audioContext.currentTime;
2315
2376
  const state = channel.state;
2316
2377
  const prev = state.pitchWheelSensitivity;
2317
- const next = value / 128;
2378
+ const next = value / 12800;
2318
2379
  state.pitchWheelSensitivity = next;
2319
2380
  channel.detune += (state.pitchWheel * 2 - 1) * (next - prev) * 12800;
2320
2381
  this.updateChannelDetune(channel, scheduleTime);
@@ -2323,7 +2384,8 @@ export class Midy {
2323
2384
  handleFineTuningRPN(channelNumber, scheduleTime) {
2324
2385
  const channel = this.channels[channelNumber];
2325
2386
  this.limitData(channel, 0, 127, 0, 127);
2326
- const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
2387
+ const value = channel.dataMSB * 128 + channel.dataLSB;
2388
+ const fineTuning = (value - 8192) / 8192 * 100;
2327
2389
  this.setFineTuning(channelNumber, fineTuning, scheduleTime);
2328
2390
  }
2329
2391
  setFineTuning(channelNumber, value, scheduleTime) {
@@ -2332,7 +2394,7 @@ export class Midy {
2332
2394
  return;
2333
2395
  scheduleTime ??= this.audioContext.currentTime;
2334
2396
  const prev = channel.fineTuning;
2335
- const next = (value - 8192) / 8.192; // cent
2397
+ const next = value;
2336
2398
  channel.fineTuning = next;
2337
2399
  channel.detune += next - prev;
2338
2400
  this.updateChannelDetune(channel, scheduleTime);
@@ -2340,7 +2402,7 @@ export class Midy {
2340
2402
  handleCoarseTuningRPN(channelNumber, scheduleTime) {
2341
2403
  const channel = this.channels[channelNumber];
2342
2404
  this.limitDataMSB(channel, 0, 127);
2343
- const coarseTuning = channel.dataMSB;
2405
+ const coarseTuning = (channel.dataMSB - 64) * 100;
2344
2406
  this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
2345
2407
  }
2346
2408
  setCoarseTuning(channelNumber, value, scheduleTime) {
@@ -2349,7 +2411,7 @@ export class Midy {
2349
2411
  return;
2350
2412
  scheduleTime ??= this.audioContext.currentTime;
2351
2413
  const prev = channel.coarseTuning;
2352
- const next = (value - 64) * 100; // cent
2414
+ const next = value;
2353
2415
  channel.coarseTuning = next;
2354
2416
  channel.detune += next - prev;
2355
2417
  this.updateChannelDetune(channel, scheduleTime);
@@ -2357,22 +2419,22 @@ export class Midy {
2357
2419
  handleModulationDepthRangeRPN(channelNumber, scheduleTime) {
2358
2420
  const channel = this.channels[channelNumber];
2359
2421
  this.limitData(channel, 0, 127, 0, 127);
2360
- const modulationDepthRange = (dataMSB + dataLSB / 128) * 100;
2361
- this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
2422
+ const value = (channel.dataMSB + channel.dataLSB / 128) * 100;
2423
+ this.setModulationDepthRange(channelNumber, value, scheduleTime);
2362
2424
  }
2363
- setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
2425
+ setModulationDepthRange(channelNumber, value, scheduleTime) {
2364
2426
  const channel = this.channels[channelNumber];
2365
2427
  if (channel.isDrum)
2366
2428
  return;
2367
2429
  scheduleTime ??= this.audioContext.currentTime;
2368
- channel.modulationDepthRange = modulationDepthRange;
2430
+ channel.modulationDepthRange = value;
2369
2431
  this.updateModulation(channel, scheduleTime);
2370
2432
  }
2371
2433
  allSoundOff(channelNumber, _value, scheduleTime) {
2372
2434
  scheduleTime ??= this.audioContext.currentTime;
2373
2435
  return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2374
2436
  }
2375
- resetAllStates(channelNumber) {
2437
+ resetChannelStates(channelNumber) {
2376
2438
  const scheduleTime = this.audioContext.currentTime;
2377
2439
  const channel = this.channels[channelNumber];
2378
2440
  const state = channel.state;
@@ -2390,8 +2452,8 @@ export class Midy {
2390
2452
  }
2391
2453
  this.resetChannelTable(channel);
2392
2454
  this.mode = "GM2";
2393
- this.masterFineTuning = 0; // cb
2394
- this.masterCoarseTuning = 0; // cb
2455
+ this.masterFineTuning = 0; // cent
2456
+ this.masterCoarseTuning = 0; // cent
2395
2457
  }
2396
2458
  // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2397
2459
  resetAllControllers(channelNumber, _value, scheduleTime) {
@@ -2488,11 +2550,9 @@ export class Midy {
2488
2550
  const channel = this.channels[i];
2489
2551
  channel.bankMSB = 0;
2490
2552
  channel.bankLSB = 0;
2491
- channel.bank = 0;
2492
2553
  channel.isDrum = false;
2493
2554
  }
2494
2555
  this.channels[9].bankMSB = 1;
2495
- this.channels[9].bank = 128;
2496
2556
  this.channels[9].isDrum = true;
2497
2557
  }
2498
2558
  GM2SystemOn(scheduleTime) {
@@ -2503,11 +2563,9 @@ export class Midy {
2503
2563
  const channel = this.channels[i];
2504
2564
  channel.bankMSB = 121;
2505
2565
  channel.bankLSB = 0;
2506
- channel.bank = 121 * 128;
2507
2566
  channel.isDrum = false;
2508
2567
  }
2509
2568
  this.channels[9].bankMSB = 120;
2510
- this.channels[9].bank = 120 * 128;
2511
2569
  this.channels[9].isDrum = true;
2512
2570
  }
2513
2571
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
@@ -2565,24 +2623,20 @@ export class Midy {
2565
2623
  const volume = (data[5] * 128 + data[4]) / 16383;
2566
2624
  this.setMasterVolume(volume, scheduleTime);
2567
2625
  }
2568
- setMasterVolume(volume, scheduleTime) {
2626
+ setMasterVolume(value, scheduleTime) {
2569
2627
  scheduleTime ??= this.audioContext.currentTime;
2570
- if (volume < 0 && 1 < volume) {
2571
- console.error("Master Volume is out of range");
2572
- }
2573
- else {
2574
- this.masterVolume.gain
2575
- .cancelScheduledValues(scheduleTime)
2576
- .setValueAtTime(volume * volume, scheduleTime);
2577
- }
2628
+ this.masterVolume.gain
2629
+ .cancelScheduledValues(scheduleTime)
2630
+ .setValueAtTime(value * value, scheduleTime);
2578
2631
  }
2579
2632
  handleMasterFineTuningSysEx(data, scheduleTime) {
2580
- const fineTuning = data[5] * 128 + data[4];
2633
+ const value = (data[5] * 128 + data[4]) / 16383;
2634
+ const fineTuning = (value - 8192) / 8192 * 100;
2581
2635
  this.setMasterFineTuning(fineTuning, scheduleTime);
2582
2636
  }
2583
2637
  setMasterFineTuning(value, scheduleTime) {
2584
2638
  const prev = this.masterFineTuning;
2585
- const next = (value - 8192) / 8.192; // cent
2639
+ const next = value;
2586
2640
  this.masterFineTuning = next;
2587
2641
  const detuneChange = next - prev;
2588
2642
  for (let i = 0; i < this.channels.length; i++) {
@@ -2594,12 +2648,12 @@ export class Midy {
2594
2648
  }
2595
2649
  }
2596
2650
  handleMasterCoarseTuningSysEx(data, scheduleTime) {
2597
- const coarseTuning = data[4];
2651
+ const coarseTuning = (data[4] - 64) * 100;
2598
2652
  this.setMasterCoarseTuning(coarseTuning, scheduleTime);
2599
2653
  }
2600
2654
  setMasterCoarseTuning(value, scheduleTime) {
2601
2655
  const prev = this.masterCoarseTuning;
2602
- const next = (value - 64) * 100; // cent
2656
+ const next = value;
2603
2657
  this.masterCoarseTuning = next;
2604
2658
  const detuneChange = next - prev;
2605
2659
  for (let i = 0; i < this.channels.length; i++) {
@@ -2985,29 +3039,88 @@ export class Midy {
2985
3039
  }
2986
3040
  getKeyBasedValue(channel, keyNumber, controllerType) {
2987
3041
  const index = keyNumber * 128 + controllerType;
2988
- const controlValue = channel.keyBasedInstrumentControlTable[index];
3042
+ const controlValue = channel.keyBasedTable[index];
2989
3043
  return controlValue;
2990
3044
  }
3045
+ createKeyBasedControllerHandlers() {
3046
+ const handlers = new Array(128);
3047
+ handlers[7] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3048
+ handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3049
+ handlers[71] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3050
+ if (note.noteNumber === keyNumber) {
3051
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
3052
+ const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
3053
+ note.filterNode.Q.setValueAtTime(Q, scheduleTime);
3054
+ }
3055
+ });
3056
+ handlers[73] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3057
+ if (note.noteNumber === keyNumber) {
3058
+ this.setVolumeEnvelope(channel, note, scheduleTime);
3059
+ }
3060
+ });
3061
+ handlers[74] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3062
+ if (note.noteNumber === keyNumber) {
3063
+ this.setFilterEnvelope(channel, note, scheduleTime);
3064
+ }
3065
+ });
3066
+ handlers[75] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3067
+ if (note.noteNumber === keyNumber) {
3068
+ this.setVolumeEnvelope(channel, note, scheduleTime);
3069
+ }
3070
+ });
3071
+ handlers[76] = (channel, keyNumber, scheduleTime) => {
3072
+ if (channel.state.vibratoDepth <= 0)
3073
+ return;
3074
+ this.processScheduledNotes(channel, (note) => {
3075
+ if (note.noteNumber === keyNumber) {
3076
+ this.setFreqVibLFO(channel, note, scheduleTime);
3077
+ }
3078
+ });
3079
+ };
3080
+ handlers[77] = (channel, keyNumber, scheduleTime) => {
3081
+ if (channel.state.vibratoDepth <= 0)
3082
+ return;
3083
+ this.processScheduledNotes(channel, (note) => {
3084
+ if (note.noteNumber === keyNumber) {
3085
+ this.setVibLfoToPitch(channel, note, scheduleTime);
3086
+ }
3087
+ });
3088
+ };
3089
+ handlers[78] = (channel, keyNumber) => {
3090
+ if (channel.state.vibratoDepth <= 0)
3091
+ return;
3092
+ this.processScheduledNotes(channel, (note) => {
3093
+ if (note.noteNumber === keyNumber)
3094
+ this.setDelayVibLFO(channel, note);
3095
+ });
3096
+ };
3097
+ handlers[91] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3098
+ if (note.noteNumber === keyNumber) {
3099
+ this.setReverbSend(channel, note, scheduleTime);
3100
+ }
3101
+ });
3102
+ handlers[93] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
3103
+ if (note.noteNumber === keyNumber) {
3104
+ this.setChorusSend(channel, note, scheduleTime);
3105
+ }
3106
+ });
3107
+ return handlers;
3108
+ }
2991
3109
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2992
3110
  const channelNumber = data[4];
2993
3111
  const channel = this.channels[channelNumber];
2994
3112
  if (!channel.isDrum)
2995
3113
  return;
2996
3114
  const keyNumber = data[5];
2997
- const table = channel.keyBasedInstrumentControlTable;
3115
+ const table = channel.keyBasedTable;
2998
3116
  for (let i = 6; i < data.length; i += 2) {
2999
3117
  const controllerType = data[i];
3000
3118
  const value = data[i + 1];
3001
3119
  const index = keyNumber * 128 + controllerType;
3002
3120
  table[index] = value;
3003
- switch (controllerType) {
3004
- case 7:
3005
- case 10:
3006
- this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
3007
- break;
3008
- default: // TODO
3009
- this.setControlChange(channelNumber, controllerType, value, scheduleTime);
3010
- }
3121
+ const handler = this.keyBasedControllerHandlers[controllerType];
3122
+ if (handler)
3123
+ handler(channel, keyNumber, scheduleTime);
3011
3124
  }
3012
3125
  }
3013
3126
  handleSysEx(data, scheduleTime) {
@@ -3048,7 +3161,6 @@ Object.defineProperty(Midy, "channelSettings", {
3048
3161
  scheduleIndex: 0,
3049
3162
  detune: 0,
3050
3163
  programNumber: 0,
3051
- bank: 121 * 128,
3052
3164
  bankMSB: 121,
3053
3165
  bankLSB: 0,
3054
3166
  dataMSB: 0,
@@ -3057,7 +3169,7 @@ Object.defineProperty(Midy, "channelSettings", {
3057
3169
  rpnLSB: 127,
3058
3170
  mono: false, // CC#124, CC#125
3059
3171
  modulationDepthRange: 50, // cent
3060
- fineTuning: 0, // cb
3061
- coarseTuning: 0, // cb
3172
+ fineTuning: 0, // cent
3173
+ coarseTuning: 0, // cent
3062
3174
  }
3063
3175
  });