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