@marmooo/midy 0.2.8 → 0.3.0

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
@@ -151,6 +151,39 @@ class Note {
151
151
  this.voiceParams = voiceParams;
152
152
  }
153
153
  }
154
+ const drumExclusiveClassesByKit = new Array(57);
155
+ const drumExclusiveClassCount = 10;
156
+ const standardSet = new Uint8Array(128);
157
+ standardSet[42] = 1;
158
+ standardSet[44] = 1;
159
+ standardSet[46] = 1; // HH
160
+ standardSet[71] = 2;
161
+ standardSet[72] = 2; // Whistle
162
+ standardSet[73] = 3;
163
+ standardSet[74] = 3; // Guiro
164
+ standardSet[78] = 4;
165
+ standardSet[79] = 4; // Cuica
166
+ standardSet[80] = 5;
167
+ standardSet[81] = 5; // Triangle
168
+ standardSet[29] = 6;
169
+ standardSet[30] = 6; // Scratch
170
+ standardSet[86] = 7;
171
+ standardSet[87] = 7; // Surdo
172
+ drumExclusiveClassesByKit[0] = standardSet;
173
+ const analogSet = new Uint8Array(128);
174
+ analogSet[42] = 8;
175
+ analogSet[44] = 8;
176
+ analogSet[46] = 8; // CHH
177
+ drumExclusiveClassesByKit[25] = analogSet;
178
+ const orchestraSet = new Uint8Array(128);
179
+ orchestraSet[27] = 9;
180
+ orchestraSet[28] = 9;
181
+ orchestraSet[29] = 9; // HH
182
+ drumExclusiveClassesByKit[48] = orchestraSet;
183
+ const sfxSet = new Uint8Array(128);
184
+ sfxSet[41] = 10;
185
+ sfxSet[42] = 10; // Scratch
186
+ drumExclusiveClassesByKit[56] = sfxSet;
154
187
  // normalized to 0-1 for use with the SF2 modulator model
155
188
  const defaultControllerState = {
156
189
  noteOnVelocity: { type: 2, defaultValue: 0 },
@@ -239,17 +272,11 @@ const volumeEnvelopeKeys = [
239
272
  const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
240
273
  export class MidyGM2 {
241
274
  constructor(audioContext, options = this.defaultOptions) {
242
- Object.defineProperty(this, "ticksPerBeat", {
243
- enumerable: true,
244
- configurable: true,
245
- writable: true,
246
- value: 120
247
- });
248
- Object.defineProperty(this, "totalTime", {
275
+ Object.defineProperty(this, "mode", {
249
276
  enumerable: true,
250
277
  configurable: true,
251
278
  writable: true,
252
- value: 0
279
+ value: "GM2"
253
280
  });
254
281
  Object.defineProperty(this, "masterFineTuning", {
255
282
  enumerable: true,
@@ -284,6 +311,24 @@ export class MidyGM2 {
284
311
  delayTimes: this.generateDistributedArray(0.02, 2, 0.5),
285
312
  }
286
313
  });
314
+ Object.defineProperty(this, "numChannels", {
315
+ enumerable: true,
316
+ configurable: true,
317
+ writable: true,
318
+ value: 16
319
+ });
320
+ Object.defineProperty(this, "ticksPerBeat", {
321
+ enumerable: true,
322
+ configurable: true,
323
+ writable: true,
324
+ value: 120
325
+ });
326
+ Object.defineProperty(this, "totalTime", {
327
+ enumerable: true,
328
+ configurable: true,
329
+ writable: true,
330
+ value: 0
331
+ });
287
332
  Object.defineProperty(this, "noteCheckInterval", {
288
333
  enumerable: true,
289
334
  configurable: true,
@@ -386,11 +431,17 @@ export class MidyGM2 {
386
431
  writable: true,
387
432
  value: []
388
433
  });
389
- Object.defineProperty(this, "exclusiveClassMap", {
434
+ Object.defineProperty(this, "exclusiveClassNotes", {
390
435
  enumerable: true,
391
436
  configurable: true,
392
437
  writable: true,
393
- value: new SparseMap(128)
438
+ value: new Array(128)
439
+ });
440
+ Object.defineProperty(this, "drumExclusiveClassNotes", {
441
+ enumerable: true,
442
+ configurable: true,
443
+ writable: true,
444
+ value: new Array(this.numChannels * drumExclusiveClassCount)
394
445
  });
395
446
  Object.defineProperty(this, "defaultOptions", {
396
447
  enumerable: true,
@@ -417,6 +468,11 @@ export class MidyGM2 {
417
468
  this.audioContext = audioContext;
418
469
  this.options = { ...this.defaultOptions, ...options };
419
470
  this.masterVolume = new GainNode(audioContext);
471
+ this.scheduler = new GainNode(audioContext, { gain: 0 });
472
+ this.schedulerBuffer = new AudioBuffer({
473
+ length: 1,
474
+ sampleRate: audioContext.sampleRate,
475
+ });
420
476
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
421
477
  this.controlChangeHandlers = this.createControlChangeHandlers();
422
478
  this.channels = this.createChannels(audioContext);
@@ -425,6 +481,7 @@ export class MidyGM2 {
425
481
  this.chorusEffect.output.connect(this.masterVolume);
426
482
  this.reverbEffect.output.connect(this.masterVolume);
427
483
  this.masterVolume.connect(audioContext.destination);
484
+ this.scheduler.connect(audioContext.destination);
428
485
  this.GM2SystemOn();
429
486
  }
430
487
  initSoundFontTable() {
@@ -478,8 +535,10 @@ export class MidyGM2 {
478
535
  };
479
536
  }
480
537
  createChannels(audioContext) {
481
- const channels = Array.from({ length: 16 }, () => {
538
+ const channels = Array.from({ length: this.numChannels }, () => {
482
539
  return {
540
+ currentBufferSource: null,
541
+ isDrum: false,
483
542
  ...this.constructor.channelSettings,
484
543
  state: new ControllerState(),
485
544
  controlTable: this.initControlTable(),
@@ -523,7 +582,7 @@ export class MidyGM2 {
523
582
  return audioBuffer;
524
583
  }
525
584
  }
526
- createNoteBufferNode(audioBuffer, voiceParams) {
585
+ createBufferSource(voiceParams, audioBuffer) {
527
586
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
528
587
  bufferSource.buffer = audioBuffer;
529
588
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
@@ -614,7 +673,8 @@ export class MidyGM2 {
614
673
  if (queueIndex >= this.timeline.length) {
615
674
  await Promise.all(this.notePromises);
616
675
  this.notePromises = [];
617
- this.exclusiveClassMap.clear();
676
+ this.exclusiveClassNotes.fill(undefined);
677
+ this.drumExclusiveClassNotes.fill(undefined);
618
678
  this.audioBufferCache.clear();
619
679
  resolve();
620
680
  return;
@@ -633,7 +693,8 @@ export class MidyGM2 {
633
693
  else if (this.isStopping) {
634
694
  await this.stopNotes(0, true, now);
635
695
  this.notePromises = [];
636
- this.exclusiveClassMap.clear();
696
+ this.exclusiveClassNotes.fill(undefined);
697
+ this.drumExclusiveClassNotes.fill(undefined);
637
698
  this.audioBufferCache.clear();
638
699
  resolve();
639
700
  this.isStopping = false;
@@ -642,7 +703,8 @@ export class MidyGM2 {
642
703
  }
643
704
  else if (this.isSeeking) {
644
705
  this.stopNotes(0, true, now);
645
- this.exclusiveClassMap.clear();
706
+ this.exclusiveClassNotes.fill(undefined);
707
+ this.drumExclusiveClassNotes.fill(undefined);
646
708
  this.startTime = this.audioContext.currentTime;
647
709
  queueIndex = this.getQueueIndex(this.resumeTime);
648
710
  offset = this.resumeTime - this.startTime;
@@ -670,7 +732,7 @@ export class MidyGM2 {
670
732
  extractMidiData(midi) {
671
733
  const instruments = new Set();
672
734
  const timeline = [];
673
- const tmpChannels = new Array(16);
735
+ const tmpChannels = new Array(this.channels.length);
674
736
  for (let i = 0; i < tmpChannels.length; i++) {
675
737
  tmpChannels[i] = {
676
738
  programNumber: -1,
@@ -798,6 +860,9 @@ export class MidyGM2 {
798
860
  if (!this.isPlaying)
799
861
  return;
800
862
  this.isStopping = true;
863
+ for (let i = 0; i < this.channels.length; i++) {
864
+ this.resetAllStates(i);
865
+ }
801
866
  }
802
867
  pause() {
803
868
  if (!this.isPlaying || this.isPaused)
@@ -1003,7 +1068,9 @@ export class MidyGM2 {
1003
1068
  return 8.176 * this.centToRate(cent);
1004
1069
  }
1005
1070
  calcChannelDetune(channel) {
1006
- const masterTuning = this.masterCoarseTuning + this.masterFineTuning;
1071
+ const masterTuning = channel.isDrum
1072
+ ? 0
1073
+ : this.masterCoarseTuning + this.masterFineTuning;
1007
1074
  const channelTuning = channel.coarseTuning + channel.fineTuning;
1008
1075
  const tuning = masterTuning + channelTuning;
1009
1076
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
@@ -1029,9 +1096,8 @@ export class MidyGM2 {
1029
1096
  .setValueAtTime(detune, scheduleTime);
1030
1097
  }
1031
1098
  getPortamentoTime(channel) {
1032
- const factor = 5 * Math.log(10) / 127;
1033
- const time = channel.state.portamentoTime;
1034
- return Math.log(time) / factor;
1099
+ const factor = 5 * Math.log(10) * 127;
1100
+ return channel.state.portamentoTime * factor;
1035
1101
  }
1036
1102
  setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1037
1103
  const { voiceParams, startTime } = note;
@@ -1167,8 +1233,8 @@ export class MidyGM2 {
1167
1233
  note.vibratoLFO.connect(note.vibratoDepth);
1168
1234
  note.vibratoDepth.connect(note.bufferSource.detune);
1169
1235
  }
1170
- async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1171
- const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1236
+ async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1237
+ const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1172
1238
  const cache = this.audioBufferCache.get(audioBufferId);
1173
1239
  if (cache) {
1174
1240
  cache.counter += 1;
@@ -1191,8 +1257,8 @@ export class MidyGM2 {
1191
1257
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1192
1258
  const voiceParams = voice.getAllParams(controllerState);
1193
1259
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1194
- const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1195
- note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1260
+ const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1261
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1196
1262
  note.volumeNode = new GainNode(this.audioContext);
1197
1263
  note.gainL = new GainNode(this.audioContext);
1198
1264
  note.gainR = new GainNode(this.audioContext);
@@ -1236,23 +1302,72 @@ export class MidyGM2 {
1236
1302
  note.bufferSource.start(startTime);
1237
1303
  return note;
1238
1304
  }
1239
- calcBank(channel, channelNumber) {
1240
- if (channel.bankMSB === 121) {
1241
- return 0;
1305
+ calcBank(channel) {
1306
+ switch (this.mode) {
1307
+ case "GM1":
1308
+ if (channel.isDrum)
1309
+ return 128;
1310
+ return 0;
1311
+ case "GM2":
1312
+ if (channel.bankMSB === 121)
1313
+ return 0;
1314
+ if (channel.isDrum)
1315
+ return 128;
1316
+ return channel.bank;
1317
+ default:
1318
+ return channel.bank;
1242
1319
  }
1243
- if (channelNumber % 9 <= 1 && channel.bankMSB === 120) {
1244
- return 128;
1320
+ }
1321
+ handleExclusiveClass(note, channelNumber, startTime) {
1322
+ const exclusiveClass = note.voiceParams.exclusiveClass;
1323
+ if (exclusiveClass === 0)
1324
+ return;
1325
+ const prev = this.exclusiveClassNotes[exclusiveClass];
1326
+ if (prev) {
1327
+ const [prevNote, prevChannelNumber] = prev;
1328
+ if (prevNote && !prevNote.ending) {
1329
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1330
+ startTime, true, // force
1331
+ undefined);
1332
+ }
1245
1333
  }
1246
- return channel.bank;
1334
+ this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
1335
+ }
1336
+ handleDrumExclusiveClass(note, channelNumber, startTime) {
1337
+ const channel = this.channels[channelNumber];
1338
+ if (!channel.isDrum)
1339
+ return;
1340
+ const kitTable = drumExclusiveClassesByKit[channel.programNumber];
1341
+ if (!kitTable)
1342
+ return;
1343
+ const drumExclusiveClass = kitTable[note.noteNumber];
1344
+ if (drumExclusiveClass === 0)
1345
+ return;
1346
+ const index = (drumExclusiveClass - 1) * this.channels.length +
1347
+ channelNumber;
1348
+ const prevNote = this.drumExclusiveClassNotes[index];
1349
+ if (prevNote && !prevNote.ending) {
1350
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1351
+ startTime, true, // force
1352
+ undefined);
1353
+ }
1354
+ this.drumExclusiveClassNotes[index] = note;
1355
+ }
1356
+ isDrumNoteOffException(channel, noteNumber) {
1357
+ if (!channel.isDrum)
1358
+ return false;
1359
+ const programNumber = channel.programNumber;
1360
+ return (programNumber === 48 && noteNumber === 88) ||
1361
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84);
1247
1362
  }
1248
1363
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1249
1364
  const channel = this.channels[channelNumber];
1250
1365
  const bankNumber = this.calcBank(channel, channelNumber);
1251
- const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
1366
+ const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1252
1367
  if (soundFontIndex === undefined)
1253
1368
  return;
1254
1369
  const soundFont = this.soundFonts[soundFontIndex];
1255
- const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1370
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1256
1371
  if (!voice)
1257
1372
  return;
1258
1373
  const isSF3 = soundFont.parsed.info.version.major === 3;
@@ -1262,31 +1377,58 @@ export class MidyGM2 {
1262
1377
  if (0.5 <= channel.state.sustainPedal) {
1263
1378
  channel.sustainNotes.push(note);
1264
1379
  }
1265
- const exclusiveClass = note.voiceParams.exclusiveClass;
1266
- if (exclusiveClass !== 0) {
1267
- if (this.exclusiveClassMap.has(exclusiveClass)) {
1268
- const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1269
- const [prevNote, prevChannelNumber] = prevEntry;
1270
- if (!prevNote.ending) {
1271
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1272
- startTime, true, // force
1273
- undefined);
1274
- }
1275
- }
1276
- this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1277
- }
1380
+ this.handleExclusiveClass(note, channelNumber, startTime);
1381
+ this.handleDrumExclusiveClass(note, channelNumber, startTime);
1278
1382
  const scheduledNotes = channel.scheduledNotes;
1279
- if (scheduledNotes.has(noteNumber)) {
1280
- scheduledNotes.get(noteNumber).push(note);
1383
+ let notes = scheduledNotes.get(noteNumber);
1384
+ if (notes) {
1385
+ notes.push(note);
1281
1386
  }
1282
1387
  else {
1283
- scheduledNotes.set(noteNumber, [note]);
1388
+ notes = [note];
1389
+ scheduledNotes.set(noteNumber, notes);
1390
+ }
1391
+ if (this.isDrumNoteOffException(channel, noteNumber)) {
1392
+ const stopTime = startTime + note.bufferSource.buffer.duration;
1393
+ const index = notes.length - 1;
1394
+ const promise = new Promise((resolve) => {
1395
+ note.bufferSource.onended = () => {
1396
+ this.disconnectNote(note, scheduledNotes, index);
1397
+ resolve();
1398
+ };
1399
+ note.bufferSource.stop(stopTime);
1400
+ });
1401
+ this.notePromises.push(promise);
1284
1402
  }
1285
1403
  }
1286
1404
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1287
1405
  scheduleTime ??= this.audioContext.currentTime;
1288
1406
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1289
1407
  }
1408
+ disconnectNote(note, scheduledNotes, index) {
1409
+ scheduledNotes[index] = null;
1410
+ note.bufferSource.disconnect();
1411
+ note.filterNode.disconnect();
1412
+ note.volumeEnvelopeNode.disconnect();
1413
+ note.volumeNode.disconnect();
1414
+ note.gainL.disconnect();
1415
+ note.gainR.disconnect();
1416
+ if (note.modulationDepth) {
1417
+ note.volumeDepth.disconnect();
1418
+ note.modulationDepth.disconnect();
1419
+ note.modulationLFO.stop();
1420
+ }
1421
+ if (note.vibratoDepth) {
1422
+ note.vibratoDepth.disconnect();
1423
+ note.vibratoLFO.stop();
1424
+ }
1425
+ if (note.reverbEffectsSend) {
1426
+ note.reverbEffectsSend.disconnect();
1427
+ }
1428
+ if (note.chorusEffectsSend) {
1429
+ note.chorusEffectsSend.disconnect();
1430
+ }
1431
+ }
1290
1432
  stopNote(endTime, stopTime, scheduledNotes, index) {
1291
1433
  const note = scheduledNotes[index];
1292
1434
  note.volumeEnvelopeNode.gain
@@ -1298,28 +1440,7 @@ export class MidyGM2 {
1298
1440
  }, stopTime);
1299
1441
  return new Promise((resolve) => {
1300
1442
  note.bufferSource.onended = () => {
1301
- scheduledNotes[index] = null;
1302
- note.bufferSource.disconnect();
1303
- note.filterNode.disconnect();
1304
- note.volumeEnvelopeNode.disconnect();
1305
- note.volumeNode.disconnect();
1306
- note.gainL.disconnect();
1307
- note.gainR.disconnect();
1308
- if (note.modulationDepth) {
1309
- note.volumeDepth.disconnect();
1310
- note.modulationDepth.disconnect();
1311
- note.modulationLFO.stop();
1312
- }
1313
- if (note.vibratoDepth) {
1314
- note.vibratoDepth.disconnect();
1315
- note.vibratoLFO.stop();
1316
- }
1317
- if (note.reverbEffectsSend) {
1318
- note.reverbEffectsSend.disconnect();
1319
- }
1320
- if (note.chorusEffectsSend) {
1321
- note.chorusEffectsSend.disconnect();
1322
- }
1443
+ this.disconnectNote(note, scheduledNotes, index);
1323
1444
  resolve();
1324
1445
  };
1325
1446
  note.bufferSource.stop(stopTime);
@@ -1327,6 +1448,8 @@ export class MidyGM2 {
1327
1448
  }
1328
1449
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1329
1450
  const channel = this.channels[channelNumber];
1451
+ if (this.isDrumNoteOffException(channel, noteNumber))
1452
+ return;
1330
1453
  const state = channel.state;
1331
1454
  if (!force) {
1332
1455
  if (0.5 <= state.sustainPedal)
@@ -1412,13 +1535,25 @@ export class MidyGM2 {
1412
1535
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1413
1536
  }
1414
1537
  }
1415
- handleProgramChange(channelNumber, program, _scheduleTime) {
1538
+ handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1416
1539
  const channel = this.channels[channelNumber];
1417
1540
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1418
- channel.program = program;
1541
+ channel.programNumber = programNumber;
1542
+ if (this.mode === "GM2") {
1543
+ switch (channel.bankMSB) {
1544
+ case 120:
1545
+ channel.isDrum = true;
1546
+ break;
1547
+ case 121:
1548
+ channel.isDrum = false;
1549
+ break;
1550
+ }
1551
+ }
1419
1552
  }
1420
1553
  handleChannelPressure(channelNumber, value, scheduleTime) {
1421
1554
  const channel = this.channels[channelNumber];
1555
+ if (channel.isDrum)
1556
+ return;
1422
1557
  const prev = channel.state.channelPressure;
1423
1558
  const next = value / 127;
1424
1559
  channel.state.channelPressure = next;
@@ -1437,8 +1572,10 @@ export class MidyGM2 {
1437
1572
  this.setPitchBend(channelNumber, pitchBend, scheduleTime);
1438
1573
  }
1439
1574
  setPitchBend(channelNumber, value, scheduleTime) {
1440
- scheduleTime ??= this.audioContext.currentTime;
1441
1575
  const channel = this.channels[channelNumber];
1576
+ if (channel.isDrum)
1577
+ return;
1578
+ scheduleTime ??= this.audioContext.currentTime;
1442
1579
  const state = channel.state;
1443
1580
  const prev = state.pitchWheel * 2 - 1;
1444
1581
  const next = (value - 8192) / 8192;
@@ -1710,15 +1847,16 @@ export class MidyGM2 {
1710
1847
  });
1711
1848
  }
1712
1849
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1713
- scheduleTime ??= this.audioContext.currentTime;
1714
1850
  const channel = this.channels[channelNumber];
1851
+ if (channel.isDrum)
1852
+ return;
1853
+ scheduleTime ??= this.audioContext.currentTime;
1715
1854
  channel.state.modulationDepth = modulation / 127;
1716
1855
  this.updateModulation(channel, scheduleTime);
1717
1856
  }
1718
1857
  setPortamentoTime(channelNumber, portamentoTime) {
1719
1858
  const channel = this.channels[channelNumber];
1720
- const factor = 5 * Math.log(10) / 127;
1721
- channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1859
+ channel.state.portamentoTime = portamentoTime / 127;
1722
1860
  }
1723
1861
  setKeyBasedVolume(channel, scheduleTime) {
1724
1862
  this.processScheduledNotes(channel, (note) => {
@@ -1790,8 +1928,10 @@ export class MidyGM2 {
1790
1928
  .setValueAtTime(volume * gainRight, scheduleTime);
1791
1929
  }
1792
1930
  setSustainPedal(channelNumber, value, scheduleTime) {
1793
- scheduleTime ??= this.audioContext.currentTime;
1794
1931
  const channel = this.channels[channelNumber];
1932
+ if (channel.isDrum)
1933
+ return;
1934
+ scheduleTime ??= this.audioContext.currentTime;
1795
1935
  channel.state.sustainPedal = value / 127;
1796
1936
  if (64 <= value) {
1797
1937
  this.processScheduledNotes(channel, (note) => {
@@ -1803,11 +1943,16 @@ export class MidyGM2 {
1803
1943
  }
1804
1944
  }
1805
1945
  setPortamento(channelNumber, value) {
1806
- this.channels[channelNumber].state.portamento = value / 127;
1946
+ const channel = this.channels[channelNumber];
1947
+ if (channel.isDrum)
1948
+ return;
1949
+ channel.state.portamento = value / 127;
1807
1950
  }
1808
1951
  setSostenutoPedal(channelNumber, value, scheduleTime) {
1809
- scheduleTime ??= this.audioContext.currentTime;
1810
1952
  const channel = this.channels[channelNumber];
1953
+ if (channel.isDrum)
1954
+ return;
1955
+ scheduleTime ??= this.audioContext.currentTime;
1811
1956
  channel.state.sostenutoPedal = value / 127;
1812
1957
  if (64 <= value) {
1813
1958
  channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
@@ -1816,9 +1961,22 @@ export class MidyGM2 {
1816
1961
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
1817
1962
  }
1818
1963
  }
1819
- setSoftPedal(channelNumber, softPedal, _scheduleTime) {
1964
+ setSoftPedal(channelNumber, softPedal, scheduleTime) {
1820
1965
  const channel = this.channels[channelNumber];
1966
+ if (channel.isDrum)
1967
+ return;
1968
+ scheduleTime ??= this.audioContext.currentTime;
1821
1969
  channel.state.softPedal = softPedal / 127;
1970
+ this.processScheduledNotes(channel, (note) => {
1971
+ if (note.portamento) {
1972
+ this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
1973
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1974
+ }
1975
+ else {
1976
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1977
+ this.setFilterEnvelope(channel, note, scheduleTime);
1978
+ }
1979
+ });
1822
1980
  }
1823
1981
  setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
1824
1982
  scheduleTime ??= this.audioContext.currentTime;
@@ -1947,8 +2105,10 @@ export class MidyGM2 {
1947
2105
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1948
2106
  }
1949
2107
  setPitchBendRange(channelNumber, value, scheduleTime) {
1950
- scheduleTime ??= this.audioContext.currentTime;
1951
2108
  const channel = this.channels[channelNumber];
2109
+ if (channel.isDrum)
2110
+ return;
2111
+ scheduleTime ??= this.audioContext.currentTime;
1952
2112
  const state = channel.state;
1953
2113
  const prev = state.pitchWheelSensitivity;
1954
2114
  const next = value / 128;
@@ -1964,8 +2124,10 @@ export class MidyGM2 {
1964
2124
  this.setFineTuning(channelNumber, fineTuning, scheduleTime);
1965
2125
  }
1966
2126
  setFineTuning(channelNumber, value, scheduleTime) {
1967
- scheduleTime ??= this.audioContext.currentTime;
1968
2127
  const channel = this.channels[channelNumber];
2128
+ if (channel.isDrum)
2129
+ return;
2130
+ scheduleTime ??= this.audioContext.currentTime;
1969
2131
  const prev = channel.fineTuning;
1970
2132
  const next = (value - 8192) / 8.192; // cent
1971
2133
  channel.fineTuning = next;
@@ -1979,8 +2141,10 @@ export class MidyGM2 {
1979
2141
  this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
1980
2142
  }
1981
2143
  setCoarseTuning(channelNumber, value, scheduleTime) {
1982
- scheduleTime ??= this.audioContext.currentTime;
1983
2144
  const channel = this.channels[channelNumber];
2145
+ if (channel.isDrum)
2146
+ return;
2147
+ scheduleTime ??= this.audioContext.currentTime;
1984
2148
  const prev = channel.coarseTuning;
1985
2149
  const next = (value - 64) * 100; // cent
1986
2150
  channel.coarseTuning = next;
@@ -1994,8 +2158,10 @@ export class MidyGM2 {
1994
2158
  this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
1995
2159
  }
1996
2160
  setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
1997
- scheduleTime ??= this.audioContext.currentTime;
1998
2161
  const channel = this.channels[channelNumber];
2162
+ if (channel.isDrum)
2163
+ return;
2164
+ scheduleTime ??= this.audioContext.currentTime;
1999
2165
  channel.modulationDepthRange = modulationDepthRange;
2000
2166
  this.updateModulation(channel, scheduleTime);
2001
2167
  }
@@ -2003,16 +2169,30 @@ export class MidyGM2 {
2003
2169
  scheduleTime ??= this.audioContext.currentTime;
2004
2170
  return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2005
2171
  }
2172
+ resetAllStates(channelNumber) {
2173
+ const channel = this.channels[channelNumber];
2174
+ const state = channel.state;
2175
+ for (const type of Object.keys(defaultControllerState)) {
2176
+ state[type] = defaultControllerState[type].defaultValue;
2177
+ }
2178
+ for (const type of Object.keys(this.constructor.channelSettings)) {
2179
+ channel[type] = this.constructor.channelSettings[type];
2180
+ }
2181
+ this.mode = "GM2";
2182
+ this.masterFineTuning = 0; // cb
2183
+ this.masterCoarseTuning = 0; // cb
2184
+ }
2185
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2006
2186
  resetAllControllers(channelNumber) {
2007
2187
  const stateTypes = [
2188
+ "channelPressure",
2189
+ "pitchWheel",
2008
2190
  "expression",
2009
2191
  "modulationDepth",
2010
2192
  "sustainPedal",
2011
2193
  "portamento",
2012
2194
  "sostenutoPedal",
2013
2195
  "softPedal",
2014
- "channelPressure",
2015
- "pitchWheelSensitivity",
2016
2196
  ];
2017
2197
  const channel = this.channels[channelNumber];
2018
2198
  const state = channel.state;
@@ -2063,12 +2243,12 @@ export class MidyGM2 {
2063
2243
  case 9:
2064
2244
  switch (data[3]) {
2065
2245
  case 1:
2066
- this.GM1SystemOn();
2246
+ this.GM1SystemOn(scheduleTime);
2067
2247
  break;
2068
2248
  case 2: // GM System Off
2069
2249
  break;
2070
2250
  case 3:
2071
- this.GM2SystemOn();
2251
+ this.GM2SystemOn(scheduleTime);
2072
2252
  break;
2073
2253
  default:
2074
2254
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2078,25 +2258,35 @@ export class MidyGM2 {
2078
2258
  console.warn(`Unsupported Exclusive Message: ${data}`);
2079
2259
  }
2080
2260
  }
2081
- GM1SystemOn() {
2261
+ GM1SystemOn(scheduleTime) {
2262
+ scheduleTime ??= this.audioContext.currentTime;
2263
+ this.mode = "GM1";
2082
2264
  for (let i = 0; i < this.channels.length; i++) {
2265
+ this.allSoundOff(i, 0, scheduleTime);
2083
2266
  const channel = this.channels[i];
2084
2267
  channel.bankMSB = 0;
2085
2268
  channel.bankLSB = 0;
2086
2269
  channel.bank = 0;
2270
+ channel.isDrum = false;
2087
2271
  }
2088
2272
  this.channels[9].bankMSB = 1;
2089
2273
  this.channels[9].bank = 128;
2274
+ this.channels[9].isDrum = true;
2090
2275
  }
2091
- GM2SystemOn() {
2276
+ GM2SystemOn(scheduleTime) {
2277
+ scheduleTime ??= this.audioContext.currentTime;
2278
+ this.mode = "GM2";
2092
2279
  for (let i = 0; i < this.channels.length; i++) {
2280
+ this.allSoundOff(i, 0, scheduleTime);
2093
2281
  const channel = this.channels[i];
2094
2282
  channel.bankMSB = 121;
2095
2283
  channel.bankLSB = 0;
2096
2284
  channel.bank = 121 * 128;
2285
+ channel.isDrum = false;
2097
2286
  }
2098
2287
  this.channels[9].bankMSB = 120;
2099
2288
  this.channels[9].bank = 120 * 128;
2289
+ this.channels[9].isDrum = true;
2100
2290
  }
2101
2291
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2102
2292
  switch (data[2]) {
@@ -2159,8 +2349,14 @@ export class MidyGM2 {
2159
2349
  const prev = this.masterFineTuning;
2160
2350
  const next = (value - 8192) / 8.192; // cent
2161
2351
  this.masterFineTuning = next;
2162
- channel.detune += next - prev;
2163
- this.updateChannelDetune(channel, scheduleTime);
2352
+ const detuneChange = next - prev;
2353
+ for (let i = 0; i < this.channels.length; i++) {
2354
+ const channel = this.channels[i];
2355
+ if (channel.isDrum)
2356
+ continue;
2357
+ channel.detune += detuneChange;
2358
+ this.updateChannelDetune(channel, scheduleTime);
2359
+ }
2164
2360
  }
2165
2361
  handleMasterCoarseTuningSysEx(data, scheduleTime) {
2166
2362
  const coarseTuning = data[4];
@@ -2170,8 +2366,14 @@ export class MidyGM2 {
2170
2366
  const prev = this.masterCoarseTuning;
2171
2367
  const next = (value - 64) * 100; // cent
2172
2368
  this.masterCoarseTuning = next;
2173
- channel.detune += next - prev;
2174
- this.updateChannelDetune(channel, scheduleTime);
2369
+ const detuneChange = next - prev;
2370
+ for (let i = 0; i < this.channels.length; i++) {
2371
+ const channel = this.channels[i];
2372
+ if (channel.isDrum)
2373
+ continue;
2374
+ channel.detune += detuneChange;
2375
+ this.updateChannelDetune(channel, scheduleTime);
2376
+ }
2175
2377
  }
2176
2378
  handleGlobalParameterControlSysEx(data, scheduleTime) {
2177
2379
  if (data[7] === 1) {
@@ -2355,7 +2557,7 @@ export class MidyGM2 {
2355
2557
  return value * 0.00787;
2356
2558
  }
2357
2559
  getChannelBitmap(data) {
2358
- const bitmap = new Array(16).fill(false);
2560
+ const bitmap = new Array(this.channels.length).fill(false);
2359
2561
  const ff = data[4] & 0b11;
2360
2562
  const gg = data[5] & 0x7F;
2361
2563
  const hh = data[6] & 0x7F;
@@ -2497,15 +2699,23 @@ export class MidyGM2 {
2497
2699
  console.warn(`Unsupported Exclusive Message: ${data}`);
2498
2700
  }
2499
2701
  }
2702
+ // https://github.com/marmooo/js-timer-benchmark
2500
2703
  scheduleTask(callback, scheduleTime) {
2501
2704
  return new Promise((resolve) => {
2502
- const bufferSource = new AudioBufferSourceNode(this.audioContext);
2705
+ const bufferSource = new AudioBufferSourceNode(this.audioContext, {
2706
+ buffer: this.schedulerBuffer,
2707
+ });
2708
+ bufferSource.connect(this.scheduler);
2503
2709
  bufferSource.onended = () => {
2504
- callback();
2505
- resolve();
2710
+ try {
2711
+ callback();
2712
+ }
2713
+ finally {
2714
+ bufferSource.disconnect();
2715
+ resolve();
2716
+ }
2506
2717
  };
2507
2718
  bufferSource.start(scheduleTime);
2508
- bufferSource.stop(scheduleTime);
2509
2719
  });
2510
2720
  }
2511
2721
  }
@@ -2514,9 +2724,8 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2514
2724
  configurable: true,
2515
2725
  writable: true,
2516
2726
  value: {
2517
- currentBufferSource: null,
2518
2727
  detune: 0,
2519
- program: 0,
2728
+ programNumber: 0,
2520
2729
  bank: 121 * 128,
2521
2730
  bankMSB: 121,
2522
2731
  bankLSB: 0,
@@ -2525,8 +2734,8 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2525
2734
  rpnMSB: 127,
2526
2735
  rpnLSB: 127,
2527
2736
  mono: false, // CC#124, CC#125
2737
+ modulationDepthRange: 50, // cent
2528
2738
  fineTuning: 0, // cb
2529
2739
  coarseTuning: 0, // cb
2530
- modulationDepthRange: 50, // cent
2531
2740
  }
2532
2741
  });