@marmooo/midy 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,12 @@ class Note {
17
17
  writable: true,
18
18
  value: void 0
19
19
  });
20
+ Object.defineProperty(this, "adjustedBaseFreq", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: 20000
25
+ });
20
26
  Object.defineProperty(this, "index", {
21
27
  enumerable: true,
22
28
  configurable: true,
@@ -228,6 +234,12 @@ const pitchEnvelopeKeys = [
228
234
  "playbackRate",
229
235
  ];
230
236
  const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
237
+ const defaultPressureValues = new Int8Array([64, 64, 64, 0, 0, 0]);
238
+ function cbToRatio(cb) {
239
+ return Math.pow(10, cb / 200);
240
+ }
241
+ const decayCurve = 1 / (-Math.log(cbToRatio(-1000)));
242
+ const releaseCurve = 1 / (-Math.log(cbToRatio(-600)));
231
243
  class MidyGM2 extends EventTarget {
232
244
  constructor(audioContext) {
233
245
  super();
@@ -391,6 +403,20 @@ class MidyGM2 extends EventTarget {
391
403
  writable: true,
392
404
  value: false
393
405
  });
406
+ Object.defineProperty(this, "totalTimeEventTypes", {
407
+ enumerable: true,
408
+ configurable: true,
409
+ writable: true,
410
+ value: new Set([
411
+ "noteOff",
412
+ ])
413
+ });
414
+ Object.defineProperty(this, "tempo", {
415
+ enumerable: true,
416
+ configurable: true,
417
+ writable: true,
418
+ value: 1
419
+ });
394
420
  Object.defineProperty(this, "loop", {
395
421
  enumerable: true,
396
422
  configurable: true,
@@ -510,13 +536,13 @@ class MidyGM2 extends EventTarget {
510
536
  this.totalTime = this.calcTotalTime();
511
537
  }
512
538
  cacheVoiceIds() {
513
- const timeline = this.timeline;
539
+ const { channels, timeline, voiceCounter } = this;
514
540
  for (let i = 0; i < timeline.length; i++) {
515
541
  const event = timeline[i];
516
542
  switch (event.type) {
517
543
  case "noteOn": {
518
- const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
519
- this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
544
+ const audioBufferId = this.getVoiceId(channels[event.channel], event.noteNumber, event.velocity);
545
+ voiceCounter.set(audioBufferId, (voiceCounter.get(audioBufferId) ?? 0) + 1);
520
546
  break;
521
547
  }
522
548
  case "controller":
@@ -531,9 +557,9 @@ class MidyGM2 extends EventTarget {
531
557
  this.setProgramChange(event.channel, event.programNumber, event.startTime);
532
558
  }
533
559
  }
534
- for (const [audioBufferId, count] of this.voiceCounter) {
560
+ for (const [audioBufferId, count] of voiceCounter) {
535
561
  if (count === 1)
536
- this.voiceCounter.delete(audioBufferId);
562
+ voiceCounter.delete(audioBufferId);
537
563
  }
538
564
  this.GM2SystemOn();
539
565
  }
@@ -542,8 +568,12 @@ class MidyGM2 extends EventTarget {
542
568
  const bankTable = this.soundFontTable[programNumber];
543
569
  if (!bankTable)
544
570
  return;
545
- const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
546
- const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
571
+ let bank = channel.isDrum ? 128 : channel.bankLSB;
572
+ if (bankTable[bank] === undefined) {
573
+ if (channel.isDrum)
574
+ return;
575
+ bank = 0;
576
+ }
547
577
  const soundFontIndex = bankTable[bank];
548
578
  if (soundFontIndex === undefined)
549
579
  return;
@@ -569,7 +599,7 @@ class MidyGM2 extends EventTarget {
569
599
  resetChannelTable(channel) {
570
600
  channel.controlTable.fill(-1);
571
601
  channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
572
- channel.channelPressureTable.fill(-1);
602
+ channel.channelPressureTable.set(defaultPressureValues);
573
603
  channel.keyBasedTable.fill(-1);
574
604
  }
575
605
  createChannels(audioContext) {
@@ -585,7 +615,7 @@ class MidyGM2 extends EventTarget {
585
615
  sostenutoNotes: [],
586
616
  controlTable: this.initControlTable(),
587
617
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
588
- channelPressureTable: new Int8Array(6).fill(-1),
618
+ channelPressureTable: new Int8Array(defaultPressureValues),
589
619
  keyBasedTable: new Int8Array(128 * 128).fill(-1),
590
620
  keyBasedGainLs: new Array(128),
591
621
  keyBasedGainRs: new Array(128),
@@ -621,11 +651,13 @@ class MidyGM2 extends EventTarget {
621
651
  const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
622
652
  const schedulingOffset = this.startDelay - timeOffset;
623
653
  const timeline = this.timeline;
654
+ const inverseTempo = 1 / this.tempo;
624
655
  while (queueIndex < timeline.length) {
625
656
  const event = timeline[queueIndex];
626
- if (lookAheadCheckTime < event.startTime)
657
+ const t = event.startTime * inverseTempo;
658
+ if (lookAheadCheckTime < t)
627
659
  break;
628
- const startTime = event.startTime + schedulingOffset;
660
+ const startTime = t + schedulingOffset;
629
661
  switch (event.type) {
630
662
  case "noteOn":
631
663
  this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
@@ -654,8 +686,10 @@ class MidyGM2 extends EventTarget {
654
686
  return queueIndex;
655
687
  }
656
688
  getQueueIndex(second) {
657
- for (let i = 0; i < this.timeline.length; i++) {
658
- if (second <= this.timeline[i].startTime) {
689
+ const timeline = this.timeline;
690
+ const inverseTempo = 1 / this.tempo;
691
+ for (let i = 0; i < timeline.length; i++) {
692
+ if (second <= timeline[i].startTime * inverseTempo) {
659
693
  return i;
660
694
  }
661
695
  }
@@ -666,40 +700,44 @@ class MidyGM2 extends EventTarget {
666
700
  this.drumExclusiveClassNotes.fill(undefined);
667
701
  this.voiceCache.clear();
668
702
  this.realtimeVoiceCache.clear();
669
- for (let i = 0; i < this.channels.length; i++) {
670
- this.channels[i].scheduledNotes = [];
703
+ const channels = this.channels;
704
+ for (let i = 0; i < channels.length; i++) {
705
+ channels[i].scheduledNotes = [];
671
706
  this.resetChannelStates(i);
672
707
  }
673
708
  }
674
709
  updateStates(queueIndex, nextQueueIndex) {
710
+ const { timeline, resumeTime } = this;
711
+ const inverseTempo = 1 / this.tempo;
675
712
  const now = this.audioContext.currentTime;
676
713
  if (nextQueueIndex < queueIndex)
677
714
  queueIndex = 0;
678
715
  for (let i = queueIndex; i < nextQueueIndex; i++) {
679
- const event = this.timeline[i];
716
+ const event = timeline[i];
680
717
  switch (event.type) {
681
718
  case "controller":
682
- this.setControlChange(event.channel, event.controllerType, event.value, now - this.resumeTime + event.startTime);
719
+ this.setControlChange(event.channel, event.controllerType, event.value, now - resumeTime + event.startTime * inverseTempo);
683
720
  break;
684
721
  case "programChange":
685
- this.setProgramChange(event.channel, event.programNumber, now - this.resumeTime + event.startTime);
722
+ this.setProgramChange(event.channel, event.programNumber, now - resumeTime + event.startTime * inverseTempo);
686
723
  break;
687
724
  case "pitchBend":
688
- this.setPitchBend(event.channel, event.value + 8192, now - this.resumeTime + event.startTime);
725
+ this.setPitchBend(event.channel, event.value + 8192, now - resumeTime + event.startTime * inverseTempo);
689
726
  break;
690
727
  case "sysEx":
691
- this.handleSysEx(event.data, now - this.resumeTime + event.startTime);
728
+ this.handleSysEx(event.data, now - resumeTime + event.startTime * inverseTempo);
692
729
  }
693
730
  }
694
731
  }
695
732
  async playNotes() {
696
- if (this.audioContext.state === "suspended") {
697
- await this.audioContext.resume();
733
+ const audioContext = this.audioContext;
734
+ if (audioContext.state === "suspended") {
735
+ await audioContext.resume();
698
736
  }
699
737
  const paused = this.isPaused;
700
738
  this.isPlaying = true;
701
739
  this.isPaused = false;
702
- this.startTime = this.audioContext.currentTime;
740
+ this.startTime = audioContext.currentTime;
703
741
  if (paused) {
704
742
  this.dispatchEvent(new Event("resumed"));
705
743
  }
@@ -710,49 +748,48 @@ class MidyGM2 extends EventTarget {
710
748
  let exitReason;
711
749
  this.notePromises = [];
712
750
  while (true) {
713
- const now = this.audioContext.currentTime;
751
+ const now = audioContext.currentTime;
714
752
  if (0 < this.lastActiveSensing &&
715
753
  this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
716
754
  await this.stopNotes(0, true, now);
717
- await this.audioContext.suspend();
755
+ await audioContext.suspend();
718
756
  exitReason = "aborted";
719
757
  break;
720
758
  }
721
- if (this.timeline.length <= queueIndex) {
759
+ if (this.totalTime < this.currentTime() ||
760
+ this.timeline.length <= queueIndex) {
722
761
  await this.stopNotes(0, true, now);
723
762
  if (this.loop) {
724
- this.notePromises = [];
725
763
  this.resetAllStates();
726
- this.startTime = this.audioContext.currentTime;
764
+ this.startTime = audioContext.currentTime;
727
765
  this.resumeTime = 0;
728
766
  queueIndex = 0;
729
767
  this.dispatchEvent(new Event("looped"));
730
768
  continue;
731
769
  }
732
770
  else {
733
- await this.audioContext.suspend();
771
+ await audioContext.suspend();
734
772
  exitReason = "ended";
735
773
  break;
736
774
  }
737
775
  }
738
776
  if (this.isPausing) {
739
777
  await this.stopNotes(0, true, now);
740
- await this.audioContext.suspend();
741
- this.notePromises = [];
778
+ await audioContext.suspend();
742
779
  this.isPausing = false;
743
780
  exitReason = "paused";
744
781
  break;
745
782
  }
746
783
  else if (this.isStopping) {
747
784
  await this.stopNotes(0, true, now);
748
- await this.audioContext.suspend();
785
+ await audioContext.suspend();
749
786
  this.isStopping = false;
750
787
  exitReason = "stopped";
751
788
  break;
752
789
  }
753
790
  else if (this.isSeeking) {
754
791
  this.stopNotes(0, true, now);
755
- this.startTime = this.audioContext.currentTime;
792
+ this.startTime = audioContext.currentTime;
756
793
  const nextQueueIndex = this.getQueueIndex(this.resumeTime);
757
794
  this.updateStates(queueIndex, nextQueueIndex);
758
795
  queueIndex = nextQueueIndex;
@@ -765,7 +802,6 @@ class MidyGM2 extends EventTarget {
765
802
  await this.scheduleTask(() => { }, waitTime);
766
803
  }
767
804
  if (exitReason !== "paused") {
768
- this.notePromises = [];
769
805
  this.resetAllStates();
770
806
  this.lastActiveSensing = 0;
771
807
  }
@@ -894,11 +930,13 @@ class MidyGM2 extends EventTarget {
894
930
  return Promise.all(promises);
895
931
  }
896
932
  stopNotes(velocity, force, scheduleTime) {
897
- const promises = [];
898
- for (let i = 0; i < this.channels.length; i++) {
899
- promises.push(this.stopChannelNotes(i, velocity, force, scheduleTime));
933
+ const channels = this.channels;
934
+ for (let i = 0; i < channels.length; i++) {
935
+ this.stopChannelNotes(i, velocity, force, scheduleTime);
900
936
  }
901
- return Promise.all(this.notePromises);
937
+ const stopPromise = Promise.all(this.notePromises);
938
+ this.notePromises = [];
939
+ return stopPromise;
902
940
  }
903
941
  async start() {
904
942
  if (this.isPlaying || this.isPaused)
@@ -936,11 +974,17 @@ class MidyGM2 extends EventTarget {
936
974
  }
937
975
  }
938
976
  calcTotalTime() {
977
+ const totalTimeEventTypes = this.totalTimeEventTypes;
978
+ const timeline = this.timeline;
979
+ const inverseTempo = 1 / this.tempo;
939
980
  let totalTime = 0;
940
- for (let i = 0; i < this.timeline.length; i++) {
941
- const event = this.timeline[i];
942
- if (totalTime < event.startTime)
943
- totalTime = event.startTime;
981
+ for (let i = 0; i < timeline.length; i++) {
982
+ const event = timeline[i];
983
+ if (!totalTimeEventTypes.has(event.type))
984
+ continue;
985
+ const t = event.startTime * inverseTempo;
986
+ if (totalTime < t)
987
+ totalTime = t;
944
988
  }
945
989
  return totalTime + this.startDelay;
946
990
  }
@@ -1122,9 +1166,6 @@ class MidyGM2 extends EventTarget {
1122
1166
  feedbackGains,
1123
1167
  };
1124
1168
  }
1125
- cbToRatio(cb) {
1126
- return Math.pow(10, cb / 200);
1127
- }
1128
1169
  rateToCent(rate) {
1129
1170
  return 1200 * Math.log2(rate);
1130
1171
  }
@@ -1243,45 +1284,45 @@ class MidyGM2 extends EventTarget {
1243
1284
  }
1244
1285
  setPortamentoVolumeEnvelope(channel, note, scheduleTime) {
1245
1286
  const { voiceParams, startTime } = note;
1246
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1287
+ const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
1247
1288
  (1 + this.getAmplitudeControl(channel));
1248
1289
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1249
- const volDelay = startTime + voiceParams.volDelay;
1250
- const volAttack = volDelay + voiceParams.volAttack;
1251
- const volHold = volAttack + voiceParams.volHold;
1290
+ const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1252
1291
  note.volumeEnvelopeNode.gain
1253
1292
  .cancelScheduledValues(scheduleTime)
1254
- .setValueAtTime(sustainVolume, volHold);
1293
+ .linearRampToValueAtTime(sustainVolume, portamentoTime);
1255
1294
  }
1256
1295
  setVolumeEnvelope(channel, note, scheduleTime) {
1257
1296
  const { voiceParams, startTime } = note;
1258
- const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation) *
1297
+ const attackVolume = cbToRatio(-voiceParams.initialAttenuation) *
1259
1298
  (1 + this.getAmplitudeControl(channel));
1260
1299
  const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
1261
1300
  const volDelay = startTime + voiceParams.volDelay;
1262
1301
  const volAttack = volDelay + voiceParams.volAttack;
1263
1302
  const volHold = volAttack + voiceParams.volHold;
1264
- const volDecay = volHold + voiceParams.volDecay;
1303
+ const decayDuration = voiceParams.volDecay;
1265
1304
  note.volumeEnvelopeNode.gain
1266
1305
  .cancelScheduledValues(scheduleTime)
1267
1306
  .setValueAtTime(0, startTime)
1268
- .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
1269
- .exponentialRampToValueAtTime(attackVolume, volAttack)
1307
+ .setValueAtTime(0, volDelay)
1308
+ .linearRampToValueAtTime(attackVolume, volAttack)
1270
1309
  .setValueAtTime(attackVolume, volHold)
1271
- .linearRampToValueAtTime(sustainVolume, volDecay);
1310
+ .setTargetAtTime(sustainVolume, volHold, decayDuration * decayCurve);
1272
1311
  }
1273
1312
  setPortamentoPitchEnvelope(note, scheduleTime) {
1274
1313
  const baseRate = note.voiceParams.playbackRate;
1314
+ const portamentoTime = note.startTime +
1315
+ this.getPortamentoTime(channel, note);
1275
1316
  note.bufferSource.playbackRate
1276
1317
  .cancelScheduledValues(scheduleTime)
1277
- .setValueAtTime(baseRate, scheduleTime);
1318
+ .linearRampToValueAtTime(baseRate, portamentoTime);
1278
1319
  }
1279
1320
  setPitchEnvelope(note, scheduleTime) {
1280
1321
  const { voiceParams } = note;
1281
1322
  const baseRate = voiceParams.playbackRate;
1282
1323
  note.bufferSource.playbackRate
1283
1324
  .cancelScheduledValues(scheduleTime)
1284
- .setValueAtTime(baseRate, scheduleTime);
1325
+ .setValueAtTime(baseRate, note.startTime);
1285
1326
  const modEnvToPitch = voiceParams.modEnvToPitch;
1286
1327
  if (modEnvToPitch === 0)
1287
1328
  return;
@@ -1291,12 +1332,12 @@ class MidyGM2 extends EventTarget {
1291
1332
  const modDelay = note.startTime + voiceParams.modDelay;
1292
1333
  const modAttack = modDelay + voiceParams.modAttack;
1293
1334
  const modHold = modAttack + voiceParams.modHold;
1294
- const modDecay = modHold + voiceParams.modDecay;
1335
+ const decayDuration = voiceParams.modDecay;
1295
1336
  note.bufferSource.playbackRate
1296
1337
  .setValueAtTime(baseRate, modDelay)
1297
- .exponentialRampToValueAtTime(peekRate, modAttack)
1338
+ .linearRampToValueAtTime(peekRate, modAttack)
1298
1339
  .setValueAtTime(peekRate, modHold)
1299
- .linearRampToValueAtTime(baseRate, modDecay);
1340
+ .setTargetAtTime(baseRate, modHold, decayDuration * decayCurve);
1300
1341
  }
1301
1342
  clampCutoffFrequency(frequency) {
1302
1343
  const minFrequency = 20; // min Hz of initialFilterFc
@@ -1305,17 +1346,18 @@ class MidyGM2 extends EventTarget {
1305
1346
  }
1306
1347
  setPortamentoFilterEnvelope(channel, note, scheduleTime) {
1307
1348
  const { voiceParams, startTime } = note;
1308
- const softPedalFactor = this.getSoftPedalFactor(channel, note);
1349
+ const scale = this.getSoftPedalFactor(channel, note);
1309
1350
  const baseCent = voiceParams.initialFilterFc +
1310
1351
  this.getFilterCutoffControl(channel);
1311
- const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1312
- const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc) * softPedalFactor;
1313
- const sustainFreq = baseFreq +
1314
- (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1315
- const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1316
- const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1352
+ const sustainCent = baseCent +
1353
+ voiceParams.modEnvToFilterFc * (1 - voiceParams.modSustain);
1354
+ const baseFreq = this.centToHz(baseCent);
1355
+ const sustainFreq = this.centToHz(sustainCent);
1356
+ const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq * scale);
1357
+ const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq * scale);
1317
1358
  const portamentoTime = startTime + this.getPortamentoTime(channel, note);
1318
1359
  const modDelay = startTime + voiceParams.modDelay;
1360
+ note.adjustedBaseFreq = adjustedSustainFreq;
1319
1361
  note.filterNode.frequency
1320
1362
  .cancelScheduledValues(scheduleTime)
1321
1363
  .setValueAtTime(adjustedBaseFreq, startTime)
@@ -1324,40 +1366,44 @@ class MidyGM2 extends EventTarget {
1324
1366
  }
1325
1367
  setFilterEnvelope(channel, note, scheduleTime) {
1326
1368
  const { voiceParams, startTime } = note;
1327
- const softPedalFactor = this.getSoftPedalFactor(channel, note);
1369
+ const modEnvToFilterFc = voiceParams.modEnvToFilterFc;
1328
1370
  const baseCent = voiceParams.initialFilterFc +
1329
1371
  this.getFilterCutoffControl(channel);
1372
+ const peekCent = baseCent + modEnvToFilterFc;
1373
+ const sustainCent = baseCent +
1374
+ modEnvToFilterFc * (1 - voiceParams.modSustain);
1375
+ const softPedalFactor = this.getSoftPedalFactor(channel, note);
1330
1376
  const baseFreq = this.centToHz(baseCent) * softPedalFactor;
1331
- const peekFreq = this.centToHz(baseCent + voiceParams.modEnvToFilterFc) *
1332
- softPedalFactor;
1333
- const sustainFreq = baseFreq +
1334
- (peekFreq - baseFreq) * (1 - voiceParams.modSustain);
1377
+ const peekFreq = this.centToHz(peekCent) * softPedalFactor;
1378
+ const sustainFreq = this.centToHz(sustainCent) * softPedalFactor;
1335
1379
  const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
1336
1380
  const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
1337
1381
  const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
1338
1382
  const modDelay = startTime + voiceParams.modDelay;
1339
1383
  const modAttack = modDelay + voiceParams.modAttack;
1340
1384
  const modHold = modAttack + voiceParams.modHold;
1341
- const modDecay = modHold + voiceParams.modDecay;
1385
+ const decayDuration = voiceParams.modDecay;
1386
+ note.adjustedBaseFreq = adjustedBaseFreq;
1342
1387
  note.filterNode.frequency
1343
1388
  .cancelScheduledValues(scheduleTime)
1344
1389
  .setValueAtTime(adjustedBaseFreq, startTime)
1345
1390
  .setValueAtTime(adjustedBaseFreq, modDelay)
1346
- .exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
1391
+ .linearRampToValueAtTime(adjustedPeekFreq, modAttack)
1347
1392
  .setValueAtTime(adjustedPeekFreq, modHold)
1348
- .linearRampToValueAtTime(adjustedSustainFreq, modDecay);
1393
+ .setTargetAtTime(adjustedSustainFreq, modHold, decayDuration * decayCurve);
1349
1394
  }
1350
1395
  startModulation(channel, note, scheduleTime) {
1396
+ const audioContext = this.audioContext;
1351
1397
  const { voiceParams } = note;
1352
- note.modulationLFO = new OscillatorNode(this.audioContext, {
1398
+ note.modulationLFO = new OscillatorNode(audioContext, {
1353
1399
  frequency: this.centToHz(voiceParams.freqModLFO),
1354
1400
  });
1355
- note.filterDepth = new GainNode(this.audioContext, {
1401
+ note.filterDepth = new GainNode(audioContext, {
1356
1402
  gain: voiceParams.modLfoToFilterFc,
1357
1403
  });
1358
- note.modulationDepth = new GainNode(this.audioContext);
1404
+ note.modulationDepth = new GainNode(audioContext);
1359
1405
  this.setModLfoToPitch(channel, note, scheduleTime);
1360
- note.volumeDepth = new GainNode(this.audioContext);
1406
+ note.volumeDepth = new GainNode(audioContext);
1361
1407
  this.setModLfoToVolume(note, scheduleTime);
1362
1408
  note.modulationLFO.start(note.startTime + voiceParams.delayModLFO);
1363
1409
  note.modulationLFO.connect(note.filterDepth);
@@ -1410,7 +1456,8 @@ class MidyGM2 extends EventTarget {
1410
1456
  }
1411
1457
  }
1412
1458
  async setNoteAudioNode(channel, note, realtime) {
1413
- const now = this.audioContext.currentTime;
1459
+ const audioContext = this.audioContext;
1460
+ const now = audioContext.currentTime;
1414
1461
  const { noteNumber, velocity, startTime } = note;
1415
1462
  const state = channel.state;
1416
1463
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
@@ -1418,8 +1465,8 @@ class MidyGM2 extends EventTarget {
1418
1465
  note.voiceParams = voiceParams;
1419
1466
  const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1420
1467
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1421
- note.volumeEnvelopeNode = new GainNode(this.audioContext);
1422
- note.filterNode = new BiquadFilterNode(this.audioContext, {
1468
+ note.volumeEnvelopeNode = new GainNode(audioContext);
1469
+ note.filterNode = new BiquadFilterNode(audioContext, {
1423
1470
  type: "lowpass",
1424
1471
  Q: voiceParams.initialFilterQ / 10, // dB
1425
1472
  });
@@ -1532,8 +1579,12 @@ class MidyGM2 extends EventTarget {
1532
1579
  const bankTable = this.soundFontTable[programNumber];
1533
1580
  if (!bankTable)
1534
1581
  return;
1535
- const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1536
- const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1582
+ let bank = channel.isDrum ? 128 : channel.bankLSB;
1583
+ if (bankTable[bank] === undefined) {
1584
+ if (channel.isDrum)
1585
+ return;
1586
+ bank = 0;
1587
+ }
1537
1588
  const soundFontIndex = bankTable[bank];
1538
1589
  if (soundFontIndex === undefined)
1539
1590
  return;
@@ -1567,24 +1618,24 @@ class MidyGM2 extends EventTarget {
1567
1618
  }
1568
1619
  releaseNote(channel, note, endTime) {
1569
1620
  endTime ??= this.audioContext.currentTime;
1570
- const volRelease = endTime + note.voiceParams.volRelease;
1621
+ const duration = note.voiceParams.volRelease;
1622
+ const volRelease = endTime + duration;
1571
1623
  const modRelease = endTime + note.voiceParams.modRelease;
1572
- const stopTime = Math.min(volRelease, modRelease);
1573
1624
  note.filterNode.frequency
1574
1625
  .cancelScheduledValues(endTime)
1575
- .linearRampToValueAtTime(0, modRelease);
1626
+ .linearRampToValueAtTime(note.adjustedBaseFreq, modRelease);
1576
1627
  note.volumeEnvelopeNode.gain
1577
1628
  .cancelScheduledValues(endTime)
1578
- .linearRampToValueAtTime(0, volRelease);
1629
+ .setTargetAtTime(0, endTime, duration * releaseCurve);
1579
1630
  return new Promise((resolve) => {
1580
1631
  this.scheduleTask(() => {
1581
1632
  const bufferSource = note.bufferSource;
1582
1633
  bufferSource.loop = false;
1583
- bufferSource.stop(stopTime);
1634
+ bufferSource.stop(volRelease);
1584
1635
  this.disconnectNote(note);
1585
1636
  channel.scheduledNotes[note.index] = undefined;
1586
1637
  resolve();
1587
- }, stopTime);
1638
+ }, volRelease);
1588
1639
  });
1589
1640
  }
1590
1641
  noteOff(channelNumber, noteNumber, _velocity, endTime, force) {
@@ -1706,6 +1757,8 @@ class MidyGM2 extends EventTarget {
1706
1757
  }
1707
1758
  }
1708
1759
  setChannelPressure(channelNumber, value, scheduleTime) {
1760
+ if (!(0 <= scheduleTime))
1761
+ scheduleTime = this.audioContext.currentTime;
1709
1762
  const channel = this.channels[channelNumber];
1710
1763
  if (channel.isDrum)
1711
1764
  return;
@@ -1779,7 +1832,7 @@ class MidyGM2 extends EventTarget {
1779
1832
  }
1780
1833
  setModLfoToVolume(channel, note, scheduleTime) {
1781
1834
  const modLfoToVolume = note.voiceParams.modLfoToVolume;
1782
- const baseDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
1835
+ const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
1783
1836
  const volumeDepth = baseDepth * Math.sign(modLfoToVolume) *
1784
1837
  (1 + this.getLFOAmplitudeDepth(channel));
1785
1838
  note.volumeDepth.gain
@@ -2467,32 +2520,34 @@ class MidyGM2 extends EventTarget {
2467
2520
  }
2468
2521
  }
2469
2522
  GM1SystemOn(scheduleTime) {
2523
+ const channels = this.channels;
2470
2524
  if (!(0 <= scheduleTime))
2471
2525
  scheduleTime = this.audioContext.currentTime;
2472
2526
  this.mode = "GM1";
2473
- for (let i = 0; i < this.channels.length; i++) {
2527
+ for (let i = 0; i < channels.length; i++) {
2474
2528
  this.allSoundOff(i, 0, scheduleTime);
2475
- const channel = this.channels[i];
2529
+ const channel = channels[i];
2476
2530
  channel.bankMSB = 0;
2477
2531
  channel.bankLSB = 0;
2478
2532
  channel.isDrum = false;
2479
2533
  }
2480
- this.channels[9].bankMSB = 1;
2481
- this.channels[9].isDrum = true;
2534
+ channels[9].bankMSB = 1;
2535
+ channels[9].isDrum = true;
2482
2536
  }
2483
2537
  GM2SystemOn(scheduleTime) {
2538
+ const channels = this.channels;
2484
2539
  if (!(0 <= scheduleTime))
2485
2540
  scheduleTime = this.audioContext.currentTime;
2486
2541
  this.mode = "GM2";
2487
- for (let i = 0; i < this.channels.length; i++) {
2542
+ for (let i = 0; i < channels.length; i++) {
2488
2543
  this.allSoundOff(i, 0, scheduleTime);
2489
- const channel = this.channels[i];
2544
+ const channel = channels[i];
2490
2545
  channel.bankMSB = 121;
2491
2546
  channel.bankLSB = 0;
2492
2547
  channel.isDrum = false;
2493
2548
  }
2494
- this.channels[9].bankMSB = 120;
2495
- this.channels[9].isDrum = true;
2549
+ channels[9].bankMSB = 120;
2550
+ channels[9].isDrum = true;
2496
2551
  }
2497
2552
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2498
2553
  switch (data[2]) {
@@ -2806,9 +2861,9 @@ class MidyGM2 extends EventTarget {
2806
2861
  getAmplitudeControl(channel) {
2807
2862
  const channelPressureRaw = channel.channelPressureTable[2];
2808
2863
  const channelPressure = (0 <= channelPressureRaw)
2809
- ? channelPressureRaw * channel.state.channelPressure
2864
+ ? channel.state.channelPressure * 127 / channelPressureRaw
2810
2865
  : 0;
2811
- return channelPressure / 64;
2866
+ return channelPressure;
2812
2867
  }
2813
2868
  getLFOPitchDepth(channel) {
2814
2869
  const channelPressureRaw = channel.channelPressureTable[3];
@@ -2832,27 +2887,27 @@ class MidyGM2 extends EventTarget {
2832
2887
  return channelPressure / 127;
2833
2888
  }
2834
2889
  setEffects(channel, note, table, scheduleTime) {
2835
- if (0 <= table[0])
2890
+ if (0 < table[0])
2836
2891
  this.updateDetune(channel, note, scheduleTime);
2837
2892
  if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
2838
- if (0 <= table[1]) {
2893
+ if (0 < table[1]) {
2839
2894
  this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
2840
2895
  }
2841
- if (0 <= table[2]) {
2896
+ if (0 < table[2]) {
2842
2897
  this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
2843
2898
  }
2844
2899
  }
2845
2900
  else {
2846
- if (0 <= table[1])
2901
+ if (0 < table[1])
2847
2902
  this.setFilterEnvelope(channel, note, scheduleTime);
2848
- if (0 <= table[2])
2903
+ if (0 < table[2])
2849
2904
  this.setVolumeEnvelope(channel, note, scheduleTime);
2850
2905
  }
2851
- if (0 <= table[3])
2906
+ if (0 < table[3])
2852
2907
  this.setModLfoToPitch(channel, note, scheduleTime);
2853
- if (0 <= table[4])
2908
+ if (0 < table[4])
2854
2909
  this.setModLfoToFilterFc(channel, note, scheduleTime);
2855
- if (0 <= table[5])
2910
+ if (0 < table[5])
2856
2911
  this.setModLfoToVolume(channel, note, scheduleTime);
2857
2912
  }
2858
2913
  handlePressureSysEx(data, tableName, scheduleTime) {
@@ -29,6 +29,8 @@ export class MidyGMLite extends EventTarget {
29
29
  isPaused: boolean;
30
30
  isStopping: boolean;
31
31
  isSeeking: boolean;
32
+ totalTimeEventTypes: Set<string>;
33
+ tempo: number;
32
34
  loop: boolean;
33
35
  playPromise: any;
34
36
  timeline: any[];
@@ -93,7 +95,6 @@ export class MidyGMLite extends EventTarget {
93
95
  currentTime(): number;
94
96
  processScheduledNotes(channel: any, callback: any): Promise<void>;
95
97
  processActiveNotes(channel: any, scheduleTime: any, callback: any): Promise<void>;
96
- cbToRatio(cb: any): number;
97
98
  rateToCent(rate: any): number;
98
99
  centToRate(cent: any): number;
99
100
  centToHz(cent: any): number;
@@ -113,13 +114,13 @@ export class MidyGMLite extends EventTarget {
113
114
  noteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
114
115
  disconnectNote(note: any): void;
115
116
  releaseNote(channel: any, note: any, endTime: any): Promise<any>;
116
- noteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): Promise<any>;
117
+ noteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): any;
117
118
  setNoteIndex(channel: any, index: any): void;
118
119
  findNoteOffIndex(channel: any, noteNumber: any): any;
119
- releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): Promise<any>[];
120
+ releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): any[];
120
121
  createMessageHandlers(): any[];
121
122
  handleMessage(data: any, scheduleTime: any): void;
122
- handleChannelMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<any>;
123
+ handleChannelMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): any;
123
124
  setProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
124
125
  handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
125
126
  setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;