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