@marmooo/midy 0.2.7 → 0.2.9

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.
@@ -242,6 +242,12 @@ const volumeEnvelopeKeys = [
242
242
  const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
243
243
  class MidyGM2 {
244
244
  constructor(audioContext, options = this.defaultOptions) {
245
+ Object.defineProperty(this, "mode", {
246
+ enumerable: true,
247
+ configurable: true,
248
+ writable: true,
249
+ value: "GM2"
250
+ });
245
251
  Object.defineProperty(this, "ticksPerBeat", {
246
252
  enumerable: true,
247
253
  configurable: true,
@@ -287,18 +293,6 @@ class MidyGM2 {
287
293
  delayTimes: this.generateDistributedArray(0.02, 2, 0.5),
288
294
  }
289
295
  });
290
- Object.defineProperty(this, "mono", {
291
- enumerable: true,
292
- configurable: true,
293
- writable: true,
294
- value: false
295
- }); // CC#124, CC#125
296
- Object.defineProperty(this, "omni", {
297
- enumerable: true,
298
- configurable: true,
299
- writable: true,
300
- value: false
301
- }); // CC#126, CC#127
302
296
  Object.defineProperty(this, "noteCheckInterval", {
303
297
  enumerable: true,
304
298
  configurable: true,
@@ -432,6 +426,11 @@ class MidyGM2 {
432
426
  this.audioContext = audioContext;
433
427
  this.options = { ...this.defaultOptions, ...options };
434
428
  this.masterVolume = new GainNode(audioContext);
429
+ this.scheduler = new GainNode(audioContext, { gain: 0 });
430
+ this.schedulerBuffer = new AudioBuffer({
431
+ length: 1,
432
+ sampleRate: audioContext.sampleRate,
433
+ });
435
434
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
436
435
  this.controlChangeHandlers = this.createControlChangeHandlers();
437
436
  this.channels = this.createChannels(audioContext);
@@ -440,6 +439,7 @@ class MidyGM2 {
440
439
  this.chorusEffect.output.connect(this.masterVolume);
441
440
  this.reverbEffect.output.connect(this.masterVolume);
442
441
  this.masterVolume.connect(audioContext.destination);
442
+ this.scheduler.connect(audioContext.destination);
443
443
  this.GM2SystemOn();
444
444
  }
445
445
  initSoundFontTable() {
@@ -500,6 +500,7 @@ class MidyGM2 {
500
500
  controlTable: this.initControlTable(),
501
501
  ...this.setChannelAudioNodes(audioContext),
502
502
  scheduledNotes: new SparseMap(128),
503
+ sustainNotes: [],
503
504
  sostenutoNotes: new SparseMap(128),
504
505
  scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
505
506
  channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
@@ -537,10 +538,24 @@ class MidyGM2 {
537
538
  return audioBuffer;
538
539
  }
539
540
  }
540
- createNoteBufferNode(audioBuffer, voiceParams) {
541
+ calcLoopMode(channel, note, voiceParams) {
542
+ if (channel.isDrum) {
543
+ const noteNumber = note.noteNumber;
544
+ if (noteNumber === 88 || 47 <= noteNumber && noteNumber <= 84) {
545
+ return true;
546
+ }
547
+ else {
548
+ return false;
549
+ }
550
+ }
551
+ else {
552
+ return voiceParams.sampleModes % 2 !== 0;
553
+ }
554
+ }
555
+ createBufferSource(channel, note, voiceParams, audioBuffer) {
541
556
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
542
557
  bufferSource.buffer = audioBuffer;
543
- bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
558
+ bufferSource.loop = this.calcLoopMode(channel, note, voiceParams);
544
559
  if (bufferSource.loop) {
545
560
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
546
561
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -582,7 +597,7 @@ class MidyGM2 {
582
597
  const portamentoTarget = this.findPortamentoTarget(queueIndex);
583
598
  if (portamentoTarget)
584
599
  portamentoTarget.portamento = true;
585
- const notePromise = this.scheduleNoteOff(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, startTime, false, // force
600
+ const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false, // force
586
601
  portamentoTarget?.noteNumber);
587
602
  if (notePromise) {
588
603
  this.notePromises.push(notePromise);
@@ -590,7 +605,7 @@ class MidyGM2 {
590
605
  break;
591
606
  }
592
607
  case "controller":
593
- this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value, startTime);
608
+ this.handleControlChange(event.channel, event.controllerType, event.value, startTime);
594
609
  break;
595
610
  case "programChange":
596
611
  this.handleProgramChange(event.channel, event.programNumber, startTime);
@@ -786,15 +801,10 @@ class MidyGM2 {
786
801
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
787
802
  const channel = this.channels[channelNumber];
788
803
  const promises = [];
789
- channel.scheduledNotes.forEach((noteList) => {
790
- for (let i = 0; i < noteList.length; i++) {
791
- const note = noteList[i];
792
- if (!note)
793
- continue;
794
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
795
- this.notePromises.push(promise);
796
- promises.push(promise);
797
- }
804
+ this.processScheduledNotes(channel, (note) => {
805
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
806
+ this.notePromises.push(promise);
807
+ promises.push(promise);
798
808
  });
799
809
  channel.scheduledNotes.clear();
800
810
  return Promise.all(promises);
@@ -850,14 +860,12 @@ class MidyGM2 {
850
860
  const now = this.audioContext.currentTime;
851
861
  return this.resumeTime + now - this.startTime - this.startDelay;
852
862
  }
853
- processScheduledNotes(channel, scheduleTime, callback) {
863
+ processScheduledNotes(channel, callback) {
854
864
  channel.scheduledNotes.forEach((noteList) => {
855
865
  for (let i = 0; i < noteList.length; i++) {
856
866
  const note = noteList[i];
857
867
  if (!note)
858
868
  continue;
859
- if (scheduleTime < note.startTime)
860
- continue;
861
869
  callback(note);
862
870
  }
863
871
  });
@@ -1024,7 +1032,9 @@ class MidyGM2 {
1024
1032
  return 8.176 * this.centToRate(cent);
1025
1033
  }
1026
1034
  calcChannelDetune(channel) {
1027
- const masterTuning = this.masterCoarseTuning + this.masterFineTuning;
1035
+ const masterTuning = channel.isDrum
1036
+ ? 0
1037
+ : this.masterCoarseTuning + this.masterFineTuning;
1028
1038
  const channelTuning = channel.coarseTuning + channel.fineTuning;
1029
1039
  const tuning = masterTuning + channelTuning;
1030
1040
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
@@ -1038,7 +1048,7 @@ class MidyGM2 {
1038
1048
  return channel.scaleOctaveTuningTable[note.noteNumber % 12];
1039
1049
  }
1040
1050
  updateChannelDetune(channel, scheduleTime) {
1041
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1051
+ this.processScheduledNotes(channel, (note) => {
1042
1052
  this.updateDetune(channel, note, scheduleTime);
1043
1053
  });
1044
1054
  }
@@ -1050,9 +1060,8 @@ class MidyGM2 {
1050
1060
  .setValueAtTime(detune, scheduleTime);
1051
1061
  }
1052
1062
  getPortamentoTime(channel) {
1053
- const factor = 5 * Math.log(10) / 127;
1054
- const time = channel.state.portamentoTime;
1055
- return Math.log(time) / factor;
1063
+ const factor = 5 * Math.log(10) * 127;
1064
+ return channel.state.portamentoTime * factor;
1056
1065
  }
1057
1066
  setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1058
1067
  const { voiceParams, startTime } = note;
@@ -1213,7 +1222,7 @@ class MidyGM2 {
1213
1222
  const voiceParams = voice.getAllParams(controllerState);
1214
1223
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1215
1224
  const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1216
- note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1225
+ note.bufferSource = this.createBufferSource(channel, note, voiceParams, audioBuffer);
1217
1226
  note.volumeNode = new GainNode(this.audioContext);
1218
1227
  note.gainL = new GainNode(this.audioContext);
1219
1228
  note.gainR = new GainNode(this.audioContext);
@@ -1239,7 +1248,7 @@ class MidyGM2 {
1239
1248
  if (0 < state.modulationDepth) {
1240
1249
  this.startModulation(channel, note, now);
1241
1250
  }
1242
- if (this.mono && channel.currentBufferSource) {
1251
+ if (channel.mono && channel.currentBufferSource) {
1243
1252
  channel.currentBufferSource.stop(startTime);
1244
1253
  channel.currentBufferSource = note.bufferSource;
1245
1254
  }
@@ -1257,14 +1266,21 @@ class MidyGM2 {
1257
1266
  note.bufferSource.start(startTime);
1258
1267
  return note;
1259
1268
  }
1260
- calcBank(channel, channelNumber) {
1261
- if (channel.bankMSB === 121) {
1262
- return 0;
1263
- }
1264
- if (channelNumber % 9 <= 1 && channel.bankMSB === 120) {
1265
- return 128;
1269
+ calcBank(channel) {
1270
+ switch (this.mode) {
1271
+ case "GM1":
1272
+ if (channel.isDrum)
1273
+ return 128;
1274
+ return 0;
1275
+ case "GM2":
1276
+ if (channel.bankMSB === 121)
1277
+ return 0;
1278
+ if (channel.isDrum)
1279
+ return 128;
1280
+ return channel.bank;
1281
+ default:
1282
+ return channel.bank;
1266
1283
  }
1267
- return channel.bank;
1268
1284
  }
1269
1285
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1270
1286
  const channel = this.channels[channelNumber];
@@ -1280,15 +1296,15 @@ class MidyGM2 {
1280
1296
  const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
1281
1297
  note.gainL.connect(channel.gainL);
1282
1298
  note.gainR.connect(channel.gainR);
1283
- if (channel.state.sostenutoPedal) {
1284
- channel.sostenutoNotes.set(noteNumber, note);
1299
+ if (0.5 <= channel.state.sustainPedal) {
1300
+ channel.sustainNotes.push(note);
1285
1301
  }
1286
1302
  const exclusiveClass = note.voiceParams.exclusiveClass;
1287
1303
  if (exclusiveClass !== 0) {
1288
1304
  if (this.exclusiveClassMap.has(exclusiveClass)) {
1289
1305
  const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1290
1306
  const [prevNote, prevChannelNumber] = prevEntry;
1291
- if (!prevNote.ending) {
1307
+ if (prevNote && !prevNote.ending) {
1292
1308
  this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1293
1309
  startTime, true, // force
1294
1310
  undefined);
@@ -1350,7 +1366,7 @@ class MidyGM2 {
1350
1366
  const channel = this.channels[channelNumber];
1351
1367
  const state = channel.state;
1352
1368
  if (!force) {
1353
- if (0.5 < state.sustainPedal)
1369
+ if (0.5 <= state.sustainPedal)
1354
1370
  return;
1355
1371
  if (channel.sostenutoNotes.has(noteNumber))
1356
1372
  return;
@@ -1394,28 +1410,27 @@ class MidyGM2 {
1394
1410
  const velocity = halfVelocity * 2;
1395
1411
  const channel = this.channels[channelNumber];
1396
1412
  const promises = [];
1397
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1398
- const { noteNumber } = note;
1399
- const promise = this.noteOff(channelNumber, noteNumber, velocity);
1413
+ for (let i = 0; i < channel.sustainNotes.length; i++) {
1414
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1400
1415
  promises.push(promise);
1401
- });
1416
+ }
1417
+ channel.sustainNotes = [];
1402
1418
  return promises;
1403
1419
  }
1404
- releaseSostenutoPedal(channelNumber, halfVelocity) {
1420
+ releaseSostenutoPedal(channelNumber, halfVelocity, scheduleTime) {
1405
1421
  const velocity = halfVelocity * 2;
1406
1422
  const channel = this.channels[channelNumber];
1407
1423
  const promises = [];
1408
1424
  channel.state.sostenutoPedal = 0;
1409
- channel.sostenutoNotes.forEach((activeNote) => {
1410
- const { noteNumber } = activeNote;
1411
- const promise = this.noteOff(channelNumber, noteNumber, velocity);
1425
+ channel.sostenutoNotes.forEach((note) => {
1426
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1412
1427
  promises.push(promise);
1413
1428
  });
1414
1429
  channel.sostenutoNotes.clear();
1415
1430
  return promises;
1416
1431
  }
1417
1432
  handleMIDIMessage(statusByte, data1, data2, scheduleTime) {
1418
- const channelNumber = omni ? 0 : statusByte & 0x0F;
1433
+ const channelNumber = statusByte & 0x0F;
1419
1434
  const messageType = statusByte & 0xF0;
1420
1435
  switch (messageType) {
1421
1436
  case 0x80:
@@ -1438,9 +1453,21 @@ class MidyGM2 {
1438
1453
  const channel = this.channels[channelNumber];
1439
1454
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1440
1455
  channel.program = program;
1456
+ if (this.mode === "GM2") {
1457
+ switch (channel.bankMSB) {
1458
+ case 120:
1459
+ channel.isDrum = true;
1460
+ break;
1461
+ case 121:
1462
+ channel.isDrum = false;
1463
+ break;
1464
+ }
1465
+ }
1441
1466
  }
1442
1467
  handleChannelPressure(channelNumber, value, scheduleTime) {
1443
1468
  const channel = this.channels[channelNumber];
1469
+ if (channel.isDrum)
1470
+ return;
1444
1471
  const prev = channel.state.channelPressure;
1445
1472
  const next = value / 127;
1446
1473
  channel.state.channelPressure = next;
@@ -1459,8 +1486,10 @@ class MidyGM2 {
1459
1486
  this.setPitchBend(channelNumber, pitchBend, scheduleTime);
1460
1487
  }
1461
1488
  setPitchBend(channelNumber, value, scheduleTime) {
1462
- scheduleTime ??= this.audioContext.currentTime;
1463
1489
  const channel = this.channels[channelNumber];
1490
+ if (channel.isDrum)
1491
+ return;
1492
+ scheduleTime ??= this.audioContext.currentTime;
1464
1493
  const state = channel.state;
1465
1494
  const prev = state.pitchWheel * 2 - 1;
1466
1495
  const next = (value - 8192) / 8192;
@@ -1630,53 +1659,48 @@ class MidyGM2 {
1630
1659
  return state;
1631
1660
  }
1632
1661
  applyVoiceParams(channel, controllerType, scheduleTime) {
1633
- channel.scheduledNotes.forEach((noteList) => {
1634
- for (let i = 0; i < noteList.length; i++) {
1635
- const note = noteList[i];
1636
- if (!note)
1662
+ this.processScheduledNotes(channel, (note) => {
1663
+ const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1664
+ const voiceParams = note.voice.getParams(controllerType, controllerState);
1665
+ let appliedFilterEnvelope = false;
1666
+ let appliedVolumeEnvelope = false;
1667
+ for (const [key, value] of Object.entries(voiceParams)) {
1668
+ const prevValue = note.voiceParams[key];
1669
+ if (value === prevValue)
1637
1670
  continue;
1638
- const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
1639
- const voiceParams = note.voice.getParams(controllerType, controllerState);
1640
- let appliedFilterEnvelope = false;
1641
- let appliedVolumeEnvelope = false;
1642
- for (const [key, value] of Object.entries(voiceParams)) {
1643
- const prevValue = note.voiceParams[key];
1644
- if (value === prevValue)
1671
+ note.voiceParams[key] = value;
1672
+ if (key in this.voiceParamsHandlers) {
1673
+ this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1674
+ }
1675
+ else if (filterEnvelopeKeySet.has(key)) {
1676
+ if (appliedFilterEnvelope)
1645
1677
  continue;
1646
- note.voiceParams[key] = value;
1647
- if (key in this.voiceParamsHandlers) {
1648
- this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
1678
+ appliedFilterEnvelope = true;
1679
+ const noteVoiceParams = note.voiceParams;
1680
+ for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1681
+ const key = filterEnvelopeKeys[i];
1682
+ if (key in voiceParams)
1683
+ noteVoiceParams[key] = voiceParams[key];
1649
1684
  }
1650
- else if (filterEnvelopeKeySet.has(key)) {
1651
- if (appliedFilterEnvelope)
1652
- continue;
1653
- appliedFilterEnvelope = true;
1654
- const noteVoiceParams = note.voiceParams;
1655
- for (let i = 0; i < filterEnvelopeKeys.length; i++) {
1656
- const key = filterEnvelopeKeys[i];
1657
- if (key in voiceParams)
1658
- noteVoiceParams[key] = voiceParams[key];
1659
- }
1660
- if (note.portamento) {
1661
- this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1662
- }
1663
- else {
1664
- this.setFilterEnvelope(channel, note, scheduleTime);
1665
- }
1666
- this.setPitchEnvelope(note, scheduleTime);
1685
+ if (note.portamento) {
1686
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1667
1687
  }
1668
- else if (volumeEnvelopeKeySet.has(key)) {
1669
- if (appliedVolumeEnvelope)
1670
- continue;
1671
- appliedVolumeEnvelope = true;
1672
- const noteVoiceParams = note.voiceParams;
1673
- for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1674
- const key = volumeEnvelopeKeys[i];
1675
- if (key in voiceParams)
1676
- noteVoiceParams[key] = voiceParams[key];
1677
- }
1678
- this.setVolumeEnvelope(channel, note, scheduleTime);
1688
+ else {
1689
+ this.setFilterEnvelope(channel, note, scheduleTime);
1690
+ }
1691
+ this.setPitchEnvelope(note, scheduleTime);
1692
+ }
1693
+ else if (volumeEnvelopeKeySet.has(key)) {
1694
+ if (appliedVolumeEnvelope)
1695
+ continue;
1696
+ appliedVolumeEnvelope = true;
1697
+ const noteVoiceParams = note.voiceParams;
1698
+ for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
1699
+ const key = volumeEnvelopeKeys[i];
1700
+ if (key in voiceParams)
1701
+ noteVoiceParams[key] = voiceParams[key];
1679
1702
  }
1703
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1680
1704
  }
1681
1705
  }
1682
1706
  });
@@ -1725,9 +1749,8 @@ class MidyGM2 {
1725
1749
  this.channels[channelNumber].bankMSB = msb;
1726
1750
  }
1727
1751
  updateModulation(channel, scheduleTime) {
1728
- scheduleTime ??= this.audioContext.currentTime;
1729
1752
  const depth = channel.state.modulationDepth * channel.modulationDepthRange;
1730
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1753
+ this.processScheduledNotes(channel, (note) => {
1731
1754
  if (note.modulationDepth) {
1732
1755
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
1733
1756
  }
@@ -1739,17 +1762,18 @@ class MidyGM2 {
1739
1762
  }
1740
1763
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1741
1764
  const channel = this.channels[channelNumber];
1765
+ if (channel.isDrum)
1766
+ return;
1767
+ scheduleTime ??= this.audioContext.currentTime;
1742
1768
  channel.state.modulationDepth = modulation / 127;
1743
1769
  this.updateModulation(channel, scheduleTime);
1744
1770
  }
1745
1771
  setPortamentoTime(channelNumber, portamentoTime) {
1746
1772
  const channel = this.channels[channelNumber];
1747
- const factor = 5 * Math.log(10) / 127;
1748
- channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1773
+ channel.state.portamentoTime = portamentoTime / 127;
1749
1774
  }
1750
1775
  setKeyBasedVolume(channel, scheduleTime) {
1751
- scheduleTime ??= this.audioContext.currentTime;
1752
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1776
+ this.processScheduledNotes(channel, (note) => {
1753
1777
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 7);
1754
1778
  if (keyBasedValue !== 0) {
1755
1779
  note.volumeNode.gain
@@ -1759,6 +1783,7 @@ class MidyGM2 {
1759
1783
  });
1760
1784
  }
1761
1785
  setVolume(channelNumber, volume, scheduleTime) {
1786
+ scheduleTime ??= this.audioContext.currentTime;
1762
1787
  const channel = this.channels[channelNumber];
1763
1788
  channel.state.volume = volume / 127;
1764
1789
  this.updateChannelVolume(channel, scheduleTime);
@@ -1772,8 +1797,7 @@ class MidyGM2 {
1772
1797
  };
1773
1798
  }
1774
1799
  setKeyBasedPan(channel, scheduleTime) {
1775
- scheduleTime ??= this.audioContext.currentTime;
1776
- this.processScheduledNotes(channel, scheduleTime, (note) => {
1800
+ this.processScheduledNotes(channel, (note) => {
1777
1801
  const keyBasedValue = this.getKeyBasedInstrumentControlValue(channel, note.noteNumber, 10);
1778
1802
  if (keyBasedValue !== 0) {
1779
1803
  const { gainLeft, gainRight } = this.panToGain((keyBasedValue + 1) / 2);
@@ -1787,12 +1811,14 @@ class MidyGM2 {
1787
1811
  });
1788
1812
  }
1789
1813
  setPan(channelNumber, pan, scheduleTime) {
1814
+ scheduleTime ??= this.audioContext.currentTime;
1790
1815
  const channel = this.channels[channelNumber];
1791
1816
  channel.state.pan = pan / 127;
1792
1817
  this.updateChannelVolume(channel, scheduleTime);
1793
1818
  this.setKeyBasedPan(channel, scheduleTime);
1794
1819
  }
1795
1820
  setExpression(channelNumber, expression, scheduleTime) {
1821
+ scheduleTime ??= this.audioContext.currentTime;
1796
1822
  const channel = this.channels[channelNumber];
1797
1823
  channel.state.expression = expression / 127;
1798
1824
  this.updateChannelVolume(channel, scheduleTime);
@@ -1816,30 +1842,58 @@ class MidyGM2 {
1816
1842
  .setValueAtTime(volume * gainRight, scheduleTime);
1817
1843
  }
1818
1844
  setSustainPedal(channelNumber, value, scheduleTime) {
1845
+ const channel = this.channels[channelNumber];
1846
+ if (channel.isDrum)
1847
+ return;
1819
1848
  scheduleTime ??= this.audioContext.currentTime;
1820
- this.channels[channelNumber].state.sustainPedal = value / 127;
1821
- if (value < 64) {
1849
+ channel.state.sustainPedal = value / 127;
1850
+ if (64 <= value) {
1851
+ this.processScheduledNotes(channel, (note) => {
1852
+ channel.sustainNotes.push(note);
1853
+ });
1854
+ }
1855
+ else {
1822
1856
  this.releaseSustainPedal(channelNumber, value, scheduleTime);
1823
1857
  }
1824
1858
  }
1825
1859
  setPortamento(channelNumber, value) {
1826
- this.channels[channelNumber].state.portamento = value / 127;
1860
+ const channel = this.channels[channelNumber];
1861
+ if (channel.isDrum)
1862
+ return;
1863
+ channel.state.portamento = value / 127;
1827
1864
  }
1828
1865
  setSostenutoPedal(channelNumber, value, scheduleTime) {
1829
1866
  const channel = this.channels[channelNumber];
1867
+ if (channel.isDrum)
1868
+ return;
1869
+ scheduleTime ??= this.audioContext.currentTime;
1830
1870
  channel.state.sostenutoPedal = value / 127;
1831
1871
  if (64 <= value) {
1832
1872
  channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
1833
1873
  }
1834
1874
  else {
1835
- this.releaseSostenutoPedal(channelNumber, value);
1875
+ this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
1836
1876
  }
1837
1877
  }
1838
- setSoftPedal(channelNumber, softPedal, _scheduleTime) {
1878
+ setSoftPedal(channelNumber, softPedal, scheduleTime) {
1839
1879
  const channel = this.channels[channelNumber];
1880
+ if (channel.isDrum)
1881
+ return;
1882
+ scheduleTime ??= this.audioContext.currentTime;
1840
1883
  channel.state.softPedal = softPedal / 127;
1884
+ this.processScheduledNotes(channel, (note) => {
1885
+ if (note.portamento) {
1886
+ this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
1887
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1888
+ }
1889
+ else {
1890
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1891
+ this.setFilterEnvelope(channel, note, scheduleTime);
1892
+ }
1893
+ });
1841
1894
  }
1842
1895
  setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
1896
+ scheduleTime ??= this.audioContext.currentTime;
1843
1897
  const channel = this.channels[channelNumber];
1844
1898
  const state = channel.state;
1845
1899
  const reverbEffect = this.reverbEffect;
@@ -1851,27 +1905,17 @@ class MidyGM2 {
1851
1905
  .setValueAtTime(state.reverbSendLevel, scheduleTime);
1852
1906
  }
1853
1907
  else {
1854
- channel.scheduledNotes.forEach((noteList) => {
1855
- for (let i = 0; i < noteList.length; i++) {
1856
- const note = noteList[i];
1857
- if (!note)
1858
- continue;
1859
- if (note.voiceParams.reverbEffectsSend <= 0)
1860
- continue;
1861
- note.reverbEffectsSend.disconnect();
1862
- }
1908
+ this.processScheduledNotes(channel, (note) => {
1909
+ if (note.voiceParams.reverbEffectsSend <= 0)
1910
+ return false;
1911
+ note.reverbEffectsSend.disconnect();
1863
1912
  });
1864
1913
  }
1865
1914
  }
1866
1915
  else {
1867
1916
  if (0 < reverbSendLevel) {
1868
- channel.scheduledNotes.forEach((noteList) => {
1869
- for (let i = 0; i < noteList.length; i++) {
1870
- const note = noteList[i];
1871
- if (!note)
1872
- continue;
1873
- this.setReverbEffectsSend(channel, note, 0, scheduleTime);
1874
- }
1917
+ this.processScheduledNotes(channel, (note) => {
1918
+ this.setReverbEffectsSend(channel, note, 0, scheduleTime);
1875
1919
  });
1876
1920
  state.reverbSendLevel = reverbSendLevel / 127;
1877
1921
  reverbEffect.input.gain
@@ -1881,6 +1925,7 @@ class MidyGM2 {
1881
1925
  }
1882
1926
  }
1883
1927
  setChorusSendLevel(channelNumber, chorusSendLevel, scheduleTime) {
1928
+ scheduleTime ??= this.audioContext.currentTime;
1884
1929
  const channel = this.channels[channelNumber];
1885
1930
  const state = channel.state;
1886
1931
  const chorusEffect = this.chorusEffect;
@@ -1892,27 +1937,17 @@ class MidyGM2 {
1892
1937
  .setValueAtTime(state.chorusSendLevel, scheduleTime);
1893
1938
  }
1894
1939
  else {
1895
- channel.scheduledNotes.forEach((noteList) => {
1896
- for (let i = 0; i < noteList.length; i++) {
1897
- const note = noteList[i];
1898
- if (!note)
1899
- continue;
1900
- if (note.voiceParams.chorusEffectsSend <= 0)
1901
- continue;
1902
- note.chorusEffectsSend.disconnect();
1903
- }
1940
+ this.processScheduledNotes(channel, (note) => {
1941
+ if (note.voiceParams.chorusEffectsSend <= 0)
1942
+ return false;
1943
+ note.chorusEffectsSend.disconnect();
1904
1944
  });
1905
1945
  }
1906
1946
  }
1907
1947
  else {
1908
1948
  if (0 < chorusSendLevel) {
1909
- channel.scheduledNotes.forEach((noteList) => {
1910
- for (let i = 0; i < noteList.length; i++) {
1911
- const note = noteList[i];
1912
- if (!note)
1913
- continue;
1914
- this.setChorusEffectsSend(channel, note, 0, scheduleTime);
1915
- }
1949
+ this.processScheduledNotes(channel, (note) => {
1950
+ this.setChorusEffectsSend(channel, note, 0, scheduleTime);
1916
1951
  });
1917
1952
  state.chorusSendLevel = chorusSendLevel / 127;
1918
1953
  chorusEffect.input.gain
@@ -1955,13 +1990,13 @@ class MidyGM2 {
1955
1990
  this.handlePitchBendRangeRPN(channelNumber, scheduleTime);
1956
1991
  break;
1957
1992
  case 1:
1958
- this.handleFineTuningRPN(channelNumber);
1993
+ this.handleFineTuningRPN(channelNumber, scheduleTime);
1959
1994
  break;
1960
1995
  case 2:
1961
- this.handleCoarseTuningRPN(channelNumber);
1996
+ this.handleCoarseTuningRPN(channelNumber, scheduleTime);
1962
1997
  break;
1963
1998
  case 5:
1964
- this.handleModulationDepthRangeRPN(channelNumber);
1999
+ this.handleModulationDepthRangeRPN(channelNumber, scheduleTime);
1965
2000
  break;
1966
2001
  default:
1967
2002
  console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
@@ -1984,8 +2019,10 @@ class MidyGM2 {
1984
2019
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1985
2020
  }
1986
2021
  setPitchBendRange(channelNumber, value, scheduleTime) {
1987
- scheduleTime ??= this.audioContext.currentTime;
1988
2022
  const channel = this.channels[channelNumber];
2023
+ if (channel.isDrum)
2024
+ return;
2025
+ scheduleTime ??= this.audioContext.currentTime;
1989
2026
  const state = channel.state;
1990
2027
  const prev = state.pitchWheelSensitivity;
1991
2028
  const next = value / 128;
@@ -1994,44 +2031,53 @@ class MidyGM2 {
1994
2031
  this.updateChannelDetune(channel, scheduleTime);
1995
2032
  this.applyVoiceParams(channel, 16, scheduleTime);
1996
2033
  }
1997
- handleFineTuningRPN(channelNumber) {
2034
+ handleFineTuningRPN(channelNumber, scheduleTime) {
1998
2035
  const channel = this.channels[channelNumber];
1999
2036
  this.limitData(channel, 0, 127, 0, 127);
2000
2037
  const fineTuning = channel.dataMSB * 128 + channel.dataLSB;
2001
- this.setFineTuning(channelNumber, fineTuning);
2038
+ this.setFineTuning(channelNumber, fineTuning, scheduleTime);
2002
2039
  }
2003
- setFineTuning(channelNumber, value) {
2040
+ setFineTuning(channelNumber, value, scheduleTime) {
2004
2041
  const channel = this.channels[channelNumber];
2042
+ if (channel.isDrum)
2043
+ return;
2044
+ scheduleTime ??= this.audioContext.currentTime;
2005
2045
  const prev = channel.fineTuning;
2006
2046
  const next = (value - 8192) / 8.192; // cent
2007
2047
  channel.fineTuning = next;
2008
2048
  channel.detune += next - prev;
2009
- this.updateChannelDetune(channel);
2049
+ this.updateChannelDetune(channel, scheduleTime);
2010
2050
  }
2011
- handleCoarseTuningRPN(channelNumber) {
2051
+ handleCoarseTuningRPN(channelNumber, scheduleTime) {
2012
2052
  const channel = this.channels[channelNumber];
2013
2053
  this.limitDataMSB(channel, 0, 127);
2014
2054
  const coarseTuning = channel.dataMSB;
2015
- this.setCoarseTuning(channelNumber, coarseTuning);
2055
+ this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
2016
2056
  }
2017
- setCoarseTuning(channelNumber, value) {
2057
+ setCoarseTuning(channelNumber, value, scheduleTime) {
2018
2058
  const channel = this.channels[channelNumber];
2059
+ if (channel.isDrum)
2060
+ return;
2061
+ scheduleTime ??= this.audioContext.currentTime;
2019
2062
  const prev = channel.coarseTuning;
2020
2063
  const next = (value - 64) * 100; // cent
2021
2064
  channel.coarseTuning = next;
2022
2065
  channel.detune += next - prev;
2023
- this.updateChannelDetune(channel);
2066
+ this.updateChannelDetune(channel, scheduleTime);
2024
2067
  }
2025
- handleModulationDepthRangeRPN(channelNumber) {
2068
+ handleModulationDepthRangeRPN(channelNumber, scheduleTime) {
2026
2069
  const channel = this.channels[channelNumber];
2027
2070
  this.limitData(channel, 0, 127, 0, 127);
2028
2071
  const modulationDepthRange = (dataMSB + dataLSB / 128) * 100;
2029
- this.setModulationDepthRange(channelNumber, modulationDepthRange);
2072
+ this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
2030
2073
  }
2031
- setModulationDepthRange(channelNumber, modulationDepthRange) {
2074
+ setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
2032
2075
  const channel = this.channels[channelNumber];
2076
+ if (channel.isDrum)
2077
+ return;
2078
+ scheduleTime ??= this.audioContext.currentTime;
2033
2079
  channel.modulationDepthRange = modulationDepthRange;
2034
- this.updateModulation(channel);
2080
+ this.updateModulation(channel, scheduleTime);
2035
2081
  }
2036
2082
  allSoundOff(channelNumber, _value, scheduleTime) {
2037
2083
  scheduleTime ??= this.audioContext.currentTime;
@@ -2067,17 +2113,21 @@ class MidyGM2 {
2067
2113
  scheduleTime ??= this.audioContext.currentTime;
2068
2114
  return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
2069
2115
  }
2070
- omniOff() {
2071
- this.omni = false;
2116
+ omniOff(channelNumber, value, scheduleTime) {
2117
+ this.allNotesOff(channelNumber, value, scheduleTime);
2072
2118
  }
2073
- omniOn() {
2074
- this.omni = true;
2119
+ omniOn(channelNumber, value, scheduleTime) {
2120
+ this.allNotesOff(channelNumber, value, scheduleTime);
2075
2121
  }
2076
- monoOn() {
2077
- this.mono = true;
2122
+ monoOn(channelNumber, value, scheduleTime) {
2123
+ const channel = this.channels[channelNumber];
2124
+ this.allNotesOff(channelNumber, value, scheduleTime);
2125
+ channel.mono = true;
2078
2126
  }
2079
- polyOn() {
2080
- this.mono = false;
2127
+ polyOn(channelNumber, value, scheduleTime) {
2128
+ const channel = this.channels[channelNumber];
2129
+ this.allNotesOff(channelNumber, value, scheduleTime);
2130
+ channel.mono = false;
2081
2131
  }
2082
2132
  handleUniversalNonRealTimeExclusiveMessage(data, scheduleTime) {
2083
2133
  switch (data[2]) {
@@ -2093,12 +2143,12 @@ class MidyGM2 {
2093
2143
  case 9:
2094
2144
  switch (data[3]) {
2095
2145
  case 1:
2096
- this.GM1SystemOn();
2146
+ this.GM1SystemOn(scheduleTime);
2097
2147
  break;
2098
2148
  case 2: // GM System Off
2099
2149
  break;
2100
2150
  case 3:
2101
- this.GM2SystemOn();
2151
+ this.GM2SystemOn(scheduleTime);
2102
2152
  break;
2103
2153
  default:
2104
2154
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2108,25 +2158,35 @@ class MidyGM2 {
2108
2158
  console.warn(`Unsupported Exclusive Message: ${data}`);
2109
2159
  }
2110
2160
  }
2111
- GM1SystemOn() {
2161
+ GM1SystemOn(scheduleTime) {
2162
+ scheduleTime ??= this.audioContext.currentTime;
2163
+ this.mode = "GM1";
2112
2164
  for (let i = 0; i < this.channels.length; i++) {
2165
+ this.allSoundOff(i, 0, scheduleTime);
2113
2166
  const channel = this.channels[i];
2114
2167
  channel.bankMSB = 0;
2115
2168
  channel.bankLSB = 0;
2116
2169
  channel.bank = 0;
2170
+ channel.isDrum = false;
2117
2171
  }
2118
2172
  this.channels[9].bankMSB = 1;
2119
2173
  this.channels[9].bank = 128;
2174
+ this.channels[9].isDrum = true;
2120
2175
  }
2121
- GM2SystemOn() {
2176
+ GM2SystemOn(scheduleTime) {
2177
+ scheduleTime ??= this.audioContext.currentTime;
2178
+ this.mode = "GM2";
2122
2179
  for (let i = 0; i < this.channels.length; i++) {
2180
+ this.allSoundOff(i, 0, scheduleTime);
2123
2181
  const channel = this.channels[i];
2124
2182
  channel.bankMSB = 121;
2125
2183
  channel.bankLSB = 0;
2126
2184
  channel.bank = 121 * 128;
2185
+ channel.isDrum = false;
2127
2186
  }
2128
2187
  this.channels[9].bankMSB = 120;
2129
2188
  this.channels[9].bank = 120 * 128;
2189
+ this.channels[9].isDrum = true;
2130
2190
  }
2131
2191
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2132
2192
  switch (data[2]) {
@@ -2189,8 +2249,14 @@ class MidyGM2 {
2189
2249
  const prev = this.masterFineTuning;
2190
2250
  const next = (value - 8192) / 8.192; // cent
2191
2251
  this.masterFineTuning = next;
2192
- channel.detune += next - prev;
2193
- this.updateChannelDetune(channel, scheduleTime);
2252
+ const detuneChange = next - prev;
2253
+ for (let i = 0; i < this.channels.length; i++) {
2254
+ const channel = this.channels[i];
2255
+ if (channel.isDrum)
2256
+ continue;
2257
+ channel.detune += detuneChange;
2258
+ this.updateChannelDetune(channel, scheduleTime);
2259
+ }
2194
2260
  }
2195
2261
  handleMasterCoarseTuningSysEx(data, scheduleTime) {
2196
2262
  const coarseTuning = data[4];
@@ -2200,8 +2266,14 @@ class MidyGM2 {
2200
2266
  const prev = this.masterCoarseTuning;
2201
2267
  const next = (value - 64) * 100; // cent
2202
2268
  this.masterCoarseTuning = next;
2203
- channel.detune += next - prev;
2204
- this.updateChannelDetune(channel, scheduleTime);
2269
+ const detuneChange = next - prev;
2270
+ for (let i = 0; i < this.channels.length; i++) {
2271
+ const channel = this.channels[i];
2272
+ if (channel.isDrum)
2273
+ continue;
2274
+ channel.detune += detuneChange;
2275
+ this.updateChannelDetune(channel, scheduleTime);
2276
+ }
2205
2277
  }
2206
2278
  handleGlobalParameterControlSysEx(data, scheduleTime) {
2207
2279
  if (data[7] === 1) {
@@ -2486,13 +2558,8 @@ class MidyGM2 {
2486
2558
  const slotSize = 6;
2487
2559
  const offset = controllerType * slotSize;
2488
2560
  const table = channel.controlTable.subarray(offset, offset + slotSize);
2489
- channel.scheduledNotes.forEach((noteList) => {
2490
- for (let i = 0; i < noteList.length; i++) {
2491
- const note = noteList[i];
2492
- if (!note)
2493
- continue;
2494
- this.setControllerParameters(channel, note, table);
2495
- }
2561
+ this.processScheduledNotes(channel, (note) => {
2562
+ this.setControllerParameters(channel, note, table);
2496
2563
  });
2497
2564
  }
2498
2565
  handleControlChangeSysEx(data) {
@@ -2534,13 +2601,20 @@ class MidyGM2 {
2534
2601
  }
2535
2602
  scheduleTask(callback, scheduleTime) {
2536
2603
  return new Promise((resolve) => {
2537
- const bufferSource = new AudioBufferSourceNode(this.audioContext);
2604
+ const bufferSource = new AudioBufferSourceNode(this.audioContext, {
2605
+ buffer: this.schedulerBuffer,
2606
+ });
2607
+ bufferSource.connect(this.scheduler);
2538
2608
  bufferSource.onended = () => {
2539
- callback();
2540
- resolve();
2609
+ try {
2610
+ callback();
2611
+ }
2612
+ finally {
2613
+ bufferSource.disconnect();
2614
+ resolve();
2615
+ }
2541
2616
  };
2542
2617
  bufferSource.start(scheduleTime);
2543
- bufferSource.stop(scheduleTime);
2544
2618
  });
2545
2619
  }
2546
2620
  }
@@ -2551,6 +2625,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2551
2625
  writable: true,
2552
2626
  value: {
2553
2627
  currentBufferSource: null,
2628
+ isDrum: false,
2554
2629
  detune: 0,
2555
2630
  program: 0,
2556
2631
  bank: 121 * 128,
@@ -2560,8 +2635,9 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2560
2635
  dataLSB: 0,
2561
2636
  rpnMSB: 127,
2562
2637
  rpnLSB: 127,
2638
+ mono: false, // CC#124, CC#125
2639
+ modulationDepthRange: 50, // cent
2563
2640
  fineTuning: 0, // cb
2564
2641
  coarseTuning: 0, // cb
2565
- modulationDepthRange: 50, // cent
2566
2642
  }
2567
2643
  });