@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.
@@ -154,6 +154,39 @@ class Note {
154
154
  this.voiceParams = voiceParams;
155
155
  }
156
156
  }
157
+ const drumExclusiveClassesByKit = new Array(57);
158
+ const drumExclusiveClassCount = 10;
159
+ const standardSet = new Uint8Array(128);
160
+ standardSet[42] = 1;
161
+ standardSet[44] = 1;
162
+ standardSet[46] = 1; // HH
163
+ standardSet[71] = 2;
164
+ standardSet[72] = 2; // Whistle
165
+ standardSet[73] = 3;
166
+ standardSet[74] = 3; // Guiro
167
+ standardSet[78] = 4;
168
+ standardSet[79] = 4; // Cuica
169
+ standardSet[80] = 5;
170
+ standardSet[81] = 5; // Triangle
171
+ standardSet[29] = 6;
172
+ standardSet[30] = 6; // Scratch
173
+ standardSet[86] = 7;
174
+ standardSet[87] = 7; // Surdo
175
+ drumExclusiveClassesByKit[0] = standardSet;
176
+ const analogSet = new Uint8Array(128);
177
+ analogSet[42] = 8;
178
+ analogSet[44] = 8;
179
+ analogSet[46] = 8; // CHH
180
+ drumExclusiveClassesByKit[25] = analogSet;
181
+ const orchestraSet = new Uint8Array(128);
182
+ orchestraSet[27] = 9;
183
+ orchestraSet[28] = 9;
184
+ orchestraSet[29] = 9; // HH
185
+ drumExclusiveClassesByKit[48] = orchestraSet;
186
+ const sfxSet = new Uint8Array(128);
187
+ sfxSet[41] = 10;
188
+ sfxSet[42] = 10; // Scratch
189
+ drumExclusiveClassesByKit[56] = sfxSet;
157
190
  // normalized to 0-1 for use with the SF2 modulator model
158
191
  const defaultControllerState = {
159
192
  noteOnVelocity: { type: 2, defaultValue: 0 },
@@ -242,17 +275,11 @@ const volumeEnvelopeKeys = [
242
275
  const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
243
276
  class MidyGM2 {
244
277
  constructor(audioContext, options = this.defaultOptions) {
245
- Object.defineProperty(this, "ticksPerBeat", {
246
- enumerable: true,
247
- configurable: true,
248
- writable: true,
249
- value: 120
250
- });
251
- Object.defineProperty(this, "totalTime", {
278
+ Object.defineProperty(this, "mode", {
252
279
  enumerable: true,
253
280
  configurable: true,
254
281
  writable: true,
255
- value: 0
282
+ value: "GM2"
256
283
  });
257
284
  Object.defineProperty(this, "masterFineTuning", {
258
285
  enumerable: true,
@@ -287,6 +314,24 @@ class MidyGM2 {
287
314
  delayTimes: this.generateDistributedArray(0.02, 2, 0.5),
288
315
  }
289
316
  });
317
+ Object.defineProperty(this, "numChannels", {
318
+ enumerable: true,
319
+ configurable: true,
320
+ writable: true,
321
+ value: 16
322
+ });
323
+ Object.defineProperty(this, "ticksPerBeat", {
324
+ enumerable: true,
325
+ configurable: true,
326
+ writable: true,
327
+ value: 120
328
+ });
329
+ Object.defineProperty(this, "totalTime", {
330
+ enumerable: true,
331
+ configurable: true,
332
+ writable: true,
333
+ value: 0
334
+ });
290
335
  Object.defineProperty(this, "noteCheckInterval", {
291
336
  enumerable: true,
292
337
  configurable: true,
@@ -389,11 +434,17 @@ class MidyGM2 {
389
434
  writable: true,
390
435
  value: []
391
436
  });
392
- Object.defineProperty(this, "exclusiveClassMap", {
437
+ Object.defineProperty(this, "exclusiveClassNotes", {
393
438
  enumerable: true,
394
439
  configurable: true,
395
440
  writable: true,
396
- value: new SparseMap(128)
441
+ value: new Array(128)
442
+ });
443
+ Object.defineProperty(this, "drumExclusiveClassNotes", {
444
+ enumerable: true,
445
+ configurable: true,
446
+ writable: true,
447
+ value: new Array(this.numChannels * drumExclusiveClassCount)
397
448
  });
398
449
  Object.defineProperty(this, "defaultOptions", {
399
450
  enumerable: true,
@@ -420,6 +471,11 @@ class MidyGM2 {
420
471
  this.audioContext = audioContext;
421
472
  this.options = { ...this.defaultOptions, ...options };
422
473
  this.masterVolume = new GainNode(audioContext);
474
+ this.scheduler = new GainNode(audioContext, { gain: 0 });
475
+ this.schedulerBuffer = new AudioBuffer({
476
+ length: 1,
477
+ sampleRate: audioContext.sampleRate,
478
+ });
423
479
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
424
480
  this.controlChangeHandlers = this.createControlChangeHandlers();
425
481
  this.channels = this.createChannels(audioContext);
@@ -428,6 +484,7 @@ class MidyGM2 {
428
484
  this.chorusEffect.output.connect(this.masterVolume);
429
485
  this.reverbEffect.output.connect(this.masterVolume);
430
486
  this.masterVolume.connect(audioContext.destination);
487
+ this.scheduler.connect(audioContext.destination);
431
488
  this.GM2SystemOn();
432
489
  }
433
490
  initSoundFontTable() {
@@ -481,8 +538,10 @@ class MidyGM2 {
481
538
  };
482
539
  }
483
540
  createChannels(audioContext) {
484
- const channels = Array.from({ length: 16 }, () => {
541
+ const channels = Array.from({ length: this.numChannels }, () => {
485
542
  return {
543
+ currentBufferSource: null,
544
+ isDrum: false,
486
545
  ...this.constructor.channelSettings,
487
546
  state: new ControllerState(),
488
547
  controlTable: this.initControlTable(),
@@ -526,7 +585,7 @@ class MidyGM2 {
526
585
  return audioBuffer;
527
586
  }
528
587
  }
529
- createNoteBufferNode(audioBuffer, voiceParams) {
588
+ createBufferSource(voiceParams, audioBuffer) {
530
589
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
531
590
  bufferSource.buffer = audioBuffer;
532
591
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
@@ -617,7 +676,8 @@ class MidyGM2 {
617
676
  if (queueIndex >= this.timeline.length) {
618
677
  await Promise.all(this.notePromises);
619
678
  this.notePromises = [];
620
- this.exclusiveClassMap.clear();
679
+ this.exclusiveClassNotes.fill(undefined);
680
+ this.drumExclusiveClassNotes.fill(undefined);
621
681
  this.audioBufferCache.clear();
622
682
  resolve();
623
683
  return;
@@ -636,7 +696,8 @@ class MidyGM2 {
636
696
  else if (this.isStopping) {
637
697
  await this.stopNotes(0, true, now);
638
698
  this.notePromises = [];
639
- this.exclusiveClassMap.clear();
699
+ this.exclusiveClassNotes.fill(undefined);
700
+ this.drumExclusiveClassNotes.fill(undefined);
640
701
  this.audioBufferCache.clear();
641
702
  resolve();
642
703
  this.isStopping = false;
@@ -645,7 +706,8 @@ class MidyGM2 {
645
706
  }
646
707
  else if (this.isSeeking) {
647
708
  this.stopNotes(0, true, now);
648
- this.exclusiveClassMap.clear();
709
+ this.exclusiveClassNotes.fill(undefined);
710
+ this.drumExclusiveClassNotes.fill(undefined);
649
711
  this.startTime = this.audioContext.currentTime;
650
712
  queueIndex = this.getQueueIndex(this.resumeTime);
651
713
  offset = this.resumeTime - this.startTime;
@@ -673,7 +735,7 @@ class MidyGM2 {
673
735
  extractMidiData(midi) {
674
736
  const instruments = new Set();
675
737
  const timeline = [];
676
- const tmpChannels = new Array(16);
738
+ const tmpChannels = new Array(this.channels.length);
677
739
  for (let i = 0; i < tmpChannels.length; i++) {
678
740
  tmpChannels[i] = {
679
741
  programNumber: -1,
@@ -801,6 +863,9 @@ class MidyGM2 {
801
863
  if (!this.isPlaying)
802
864
  return;
803
865
  this.isStopping = true;
866
+ for (let i = 0; i < this.channels.length; i++) {
867
+ this.resetAllStates(i);
868
+ }
804
869
  }
805
870
  pause() {
806
871
  if (!this.isPlaying || this.isPaused)
@@ -1006,7 +1071,9 @@ class MidyGM2 {
1006
1071
  return 8.176 * this.centToRate(cent);
1007
1072
  }
1008
1073
  calcChannelDetune(channel) {
1009
- const masterTuning = this.masterCoarseTuning + this.masterFineTuning;
1074
+ const masterTuning = channel.isDrum
1075
+ ? 0
1076
+ : this.masterCoarseTuning + this.masterFineTuning;
1010
1077
  const channelTuning = channel.coarseTuning + channel.fineTuning;
1011
1078
  const tuning = masterTuning + channelTuning;
1012
1079
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
@@ -1032,9 +1099,8 @@ class MidyGM2 {
1032
1099
  .setValueAtTime(detune, scheduleTime);
1033
1100
  }
1034
1101
  getPortamentoTime(channel) {
1035
- const factor = 5 * Math.log(10) / 127;
1036
- const time = channel.state.portamentoTime;
1037
- return Math.log(time) / factor;
1102
+ const factor = 5 * Math.log(10) * 127;
1103
+ return channel.state.portamentoTime * factor;
1038
1104
  }
1039
1105
  setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1040
1106
  const { voiceParams, startTime } = note;
@@ -1170,8 +1236,8 @@ class MidyGM2 {
1170
1236
  note.vibratoLFO.connect(note.vibratoDepth);
1171
1237
  note.vibratoDepth.connect(note.bufferSource.detune);
1172
1238
  }
1173
- async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1174
- const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1239
+ async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1240
+ const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1175
1241
  const cache = this.audioBufferCache.get(audioBufferId);
1176
1242
  if (cache) {
1177
1243
  cache.counter += 1;
@@ -1194,8 +1260,8 @@ class MidyGM2 {
1194
1260
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1195
1261
  const voiceParams = voice.getAllParams(controllerState);
1196
1262
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1197
- const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1198
- note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1263
+ const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1264
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1199
1265
  note.volumeNode = new GainNode(this.audioContext);
1200
1266
  note.gainL = new GainNode(this.audioContext);
1201
1267
  note.gainR = new GainNode(this.audioContext);
@@ -1239,23 +1305,72 @@ class MidyGM2 {
1239
1305
  note.bufferSource.start(startTime);
1240
1306
  return note;
1241
1307
  }
1242
- calcBank(channel, channelNumber) {
1243
- if (channel.bankMSB === 121) {
1244
- return 0;
1308
+ calcBank(channel) {
1309
+ switch (this.mode) {
1310
+ case "GM1":
1311
+ if (channel.isDrum)
1312
+ return 128;
1313
+ return 0;
1314
+ case "GM2":
1315
+ if (channel.bankMSB === 121)
1316
+ return 0;
1317
+ if (channel.isDrum)
1318
+ return 128;
1319
+ return channel.bank;
1320
+ default:
1321
+ return channel.bank;
1245
1322
  }
1246
- if (channelNumber % 9 <= 1 && channel.bankMSB === 120) {
1247
- return 128;
1323
+ }
1324
+ handleExclusiveClass(note, channelNumber, startTime) {
1325
+ const exclusiveClass = note.voiceParams.exclusiveClass;
1326
+ if (exclusiveClass === 0)
1327
+ return;
1328
+ const prev = this.exclusiveClassNotes[exclusiveClass];
1329
+ if (prev) {
1330
+ const [prevNote, prevChannelNumber] = prev;
1331
+ if (prevNote && !prevNote.ending) {
1332
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1333
+ startTime, true, // force
1334
+ undefined);
1335
+ }
1248
1336
  }
1249
- return channel.bank;
1337
+ this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
1338
+ }
1339
+ handleDrumExclusiveClass(note, channelNumber, startTime) {
1340
+ const channel = this.channels[channelNumber];
1341
+ if (!channel.isDrum)
1342
+ return;
1343
+ const kitTable = drumExclusiveClassesByKit[channel.programNumber];
1344
+ if (!kitTable)
1345
+ return;
1346
+ const drumExclusiveClass = kitTable[note.noteNumber];
1347
+ if (drumExclusiveClass === 0)
1348
+ return;
1349
+ const index = (drumExclusiveClass - 1) * this.channels.length +
1350
+ channelNumber;
1351
+ const prevNote = this.drumExclusiveClassNotes[index];
1352
+ if (prevNote && !prevNote.ending) {
1353
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1354
+ startTime, true, // force
1355
+ undefined);
1356
+ }
1357
+ this.drumExclusiveClassNotes[index] = note;
1358
+ }
1359
+ isDrumNoteOffException(channel, noteNumber) {
1360
+ if (!channel.isDrum)
1361
+ return false;
1362
+ const programNumber = channel.programNumber;
1363
+ return (programNumber === 48 && noteNumber === 88) ||
1364
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84);
1250
1365
  }
1251
1366
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1252
1367
  const channel = this.channels[channelNumber];
1253
1368
  const bankNumber = this.calcBank(channel, channelNumber);
1254
- const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
1369
+ const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1255
1370
  if (soundFontIndex === undefined)
1256
1371
  return;
1257
1372
  const soundFont = this.soundFonts[soundFontIndex];
1258
- const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1373
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1259
1374
  if (!voice)
1260
1375
  return;
1261
1376
  const isSF3 = soundFont.parsed.info.version.major === 3;
@@ -1265,31 +1380,58 @@ class MidyGM2 {
1265
1380
  if (0.5 <= channel.state.sustainPedal) {
1266
1381
  channel.sustainNotes.push(note);
1267
1382
  }
1268
- const exclusiveClass = note.voiceParams.exclusiveClass;
1269
- if (exclusiveClass !== 0) {
1270
- if (this.exclusiveClassMap.has(exclusiveClass)) {
1271
- const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1272
- const [prevNote, prevChannelNumber] = prevEntry;
1273
- if (!prevNote.ending) {
1274
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1275
- startTime, true, // force
1276
- undefined);
1277
- }
1278
- }
1279
- this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1280
- }
1383
+ this.handleExclusiveClass(note, channelNumber, startTime);
1384
+ this.handleDrumExclusiveClass(note, channelNumber, startTime);
1281
1385
  const scheduledNotes = channel.scheduledNotes;
1282
- if (scheduledNotes.has(noteNumber)) {
1283
- scheduledNotes.get(noteNumber).push(note);
1386
+ let notes = scheduledNotes.get(noteNumber);
1387
+ if (notes) {
1388
+ notes.push(note);
1284
1389
  }
1285
1390
  else {
1286
- scheduledNotes.set(noteNumber, [note]);
1391
+ notes = [note];
1392
+ scheduledNotes.set(noteNumber, notes);
1393
+ }
1394
+ if (this.isDrumNoteOffException(channel, noteNumber)) {
1395
+ const stopTime = startTime + note.bufferSource.buffer.duration;
1396
+ const index = notes.length - 1;
1397
+ const promise = new Promise((resolve) => {
1398
+ note.bufferSource.onended = () => {
1399
+ this.disconnectNote(note, scheduledNotes, index);
1400
+ resolve();
1401
+ };
1402
+ note.bufferSource.stop(stopTime);
1403
+ });
1404
+ this.notePromises.push(promise);
1287
1405
  }
1288
1406
  }
1289
1407
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1290
1408
  scheduleTime ??= this.audioContext.currentTime;
1291
1409
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1292
1410
  }
1411
+ disconnectNote(note, scheduledNotes, index) {
1412
+ scheduledNotes[index] = null;
1413
+ note.bufferSource.disconnect();
1414
+ note.filterNode.disconnect();
1415
+ note.volumeEnvelopeNode.disconnect();
1416
+ note.volumeNode.disconnect();
1417
+ note.gainL.disconnect();
1418
+ note.gainR.disconnect();
1419
+ if (note.modulationDepth) {
1420
+ note.volumeDepth.disconnect();
1421
+ note.modulationDepth.disconnect();
1422
+ note.modulationLFO.stop();
1423
+ }
1424
+ if (note.vibratoDepth) {
1425
+ note.vibratoDepth.disconnect();
1426
+ note.vibratoLFO.stop();
1427
+ }
1428
+ if (note.reverbEffectsSend) {
1429
+ note.reverbEffectsSend.disconnect();
1430
+ }
1431
+ if (note.chorusEffectsSend) {
1432
+ note.chorusEffectsSend.disconnect();
1433
+ }
1434
+ }
1293
1435
  stopNote(endTime, stopTime, scheduledNotes, index) {
1294
1436
  const note = scheduledNotes[index];
1295
1437
  note.volumeEnvelopeNode.gain
@@ -1301,28 +1443,7 @@ class MidyGM2 {
1301
1443
  }, stopTime);
1302
1444
  return new Promise((resolve) => {
1303
1445
  note.bufferSource.onended = () => {
1304
- scheduledNotes[index] = null;
1305
- note.bufferSource.disconnect();
1306
- note.filterNode.disconnect();
1307
- note.volumeEnvelopeNode.disconnect();
1308
- note.volumeNode.disconnect();
1309
- note.gainL.disconnect();
1310
- note.gainR.disconnect();
1311
- if (note.modulationDepth) {
1312
- note.volumeDepth.disconnect();
1313
- note.modulationDepth.disconnect();
1314
- note.modulationLFO.stop();
1315
- }
1316
- if (note.vibratoDepth) {
1317
- note.vibratoDepth.disconnect();
1318
- note.vibratoLFO.stop();
1319
- }
1320
- if (note.reverbEffectsSend) {
1321
- note.reverbEffectsSend.disconnect();
1322
- }
1323
- if (note.chorusEffectsSend) {
1324
- note.chorusEffectsSend.disconnect();
1325
- }
1446
+ this.disconnectNote(note, scheduledNotes, index);
1326
1447
  resolve();
1327
1448
  };
1328
1449
  note.bufferSource.stop(stopTime);
@@ -1330,6 +1451,8 @@ class MidyGM2 {
1330
1451
  }
1331
1452
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1332
1453
  const channel = this.channels[channelNumber];
1454
+ if (this.isDrumNoteOffException(channel, noteNumber))
1455
+ return;
1333
1456
  const state = channel.state;
1334
1457
  if (!force) {
1335
1458
  if (0.5 <= state.sustainPedal)
@@ -1415,13 +1538,25 @@ class MidyGM2 {
1415
1538
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1416
1539
  }
1417
1540
  }
1418
- handleProgramChange(channelNumber, program, _scheduleTime) {
1541
+ handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1419
1542
  const channel = this.channels[channelNumber];
1420
1543
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1421
- channel.program = program;
1544
+ channel.programNumber = programNumber;
1545
+ if (this.mode === "GM2") {
1546
+ switch (channel.bankMSB) {
1547
+ case 120:
1548
+ channel.isDrum = true;
1549
+ break;
1550
+ case 121:
1551
+ channel.isDrum = false;
1552
+ break;
1553
+ }
1554
+ }
1422
1555
  }
1423
1556
  handleChannelPressure(channelNumber, value, scheduleTime) {
1424
1557
  const channel = this.channels[channelNumber];
1558
+ if (channel.isDrum)
1559
+ return;
1425
1560
  const prev = channel.state.channelPressure;
1426
1561
  const next = value / 127;
1427
1562
  channel.state.channelPressure = next;
@@ -1440,8 +1575,10 @@ class MidyGM2 {
1440
1575
  this.setPitchBend(channelNumber, pitchBend, scheduleTime);
1441
1576
  }
1442
1577
  setPitchBend(channelNumber, value, scheduleTime) {
1443
- scheduleTime ??= this.audioContext.currentTime;
1444
1578
  const channel = this.channels[channelNumber];
1579
+ if (channel.isDrum)
1580
+ return;
1581
+ scheduleTime ??= this.audioContext.currentTime;
1445
1582
  const state = channel.state;
1446
1583
  const prev = state.pitchWheel * 2 - 1;
1447
1584
  const next = (value - 8192) / 8192;
@@ -1713,15 +1850,16 @@ class MidyGM2 {
1713
1850
  });
1714
1851
  }
1715
1852
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1716
- scheduleTime ??= this.audioContext.currentTime;
1717
1853
  const channel = this.channels[channelNumber];
1854
+ if (channel.isDrum)
1855
+ return;
1856
+ scheduleTime ??= this.audioContext.currentTime;
1718
1857
  channel.state.modulationDepth = modulation / 127;
1719
1858
  this.updateModulation(channel, scheduleTime);
1720
1859
  }
1721
1860
  setPortamentoTime(channelNumber, portamentoTime) {
1722
1861
  const channel = this.channels[channelNumber];
1723
- const factor = 5 * Math.log(10) / 127;
1724
- channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1862
+ channel.state.portamentoTime = portamentoTime / 127;
1725
1863
  }
1726
1864
  setKeyBasedVolume(channel, scheduleTime) {
1727
1865
  this.processScheduledNotes(channel, (note) => {
@@ -1793,8 +1931,10 @@ class MidyGM2 {
1793
1931
  .setValueAtTime(volume * gainRight, scheduleTime);
1794
1932
  }
1795
1933
  setSustainPedal(channelNumber, value, scheduleTime) {
1796
- scheduleTime ??= this.audioContext.currentTime;
1797
1934
  const channel = this.channels[channelNumber];
1935
+ if (channel.isDrum)
1936
+ return;
1937
+ scheduleTime ??= this.audioContext.currentTime;
1798
1938
  channel.state.sustainPedal = value / 127;
1799
1939
  if (64 <= value) {
1800
1940
  this.processScheduledNotes(channel, (note) => {
@@ -1806,11 +1946,16 @@ class MidyGM2 {
1806
1946
  }
1807
1947
  }
1808
1948
  setPortamento(channelNumber, value) {
1809
- this.channels[channelNumber].state.portamento = value / 127;
1949
+ const channel = this.channels[channelNumber];
1950
+ if (channel.isDrum)
1951
+ return;
1952
+ channel.state.portamento = value / 127;
1810
1953
  }
1811
1954
  setSostenutoPedal(channelNumber, value, scheduleTime) {
1812
- scheduleTime ??= this.audioContext.currentTime;
1813
1955
  const channel = this.channels[channelNumber];
1956
+ if (channel.isDrum)
1957
+ return;
1958
+ scheduleTime ??= this.audioContext.currentTime;
1814
1959
  channel.state.sostenutoPedal = value / 127;
1815
1960
  if (64 <= value) {
1816
1961
  channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
@@ -1819,9 +1964,22 @@ class MidyGM2 {
1819
1964
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
1820
1965
  }
1821
1966
  }
1822
- setSoftPedal(channelNumber, softPedal, _scheduleTime) {
1967
+ setSoftPedal(channelNumber, softPedal, scheduleTime) {
1823
1968
  const channel = this.channels[channelNumber];
1969
+ if (channel.isDrum)
1970
+ return;
1971
+ scheduleTime ??= this.audioContext.currentTime;
1824
1972
  channel.state.softPedal = softPedal / 127;
1973
+ this.processScheduledNotes(channel, (note) => {
1974
+ if (note.portamento) {
1975
+ this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
1976
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1977
+ }
1978
+ else {
1979
+ this.setVolumeEnvelope(channel, note, scheduleTime);
1980
+ this.setFilterEnvelope(channel, note, scheduleTime);
1981
+ }
1982
+ });
1825
1983
  }
1826
1984
  setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
1827
1985
  scheduleTime ??= this.audioContext.currentTime;
@@ -1950,8 +2108,10 @@ class MidyGM2 {
1950
2108
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
1951
2109
  }
1952
2110
  setPitchBendRange(channelNumber, value, scheduleTime) {
1953
- scheduleTime ??= this.audioContext.currentTime;
1954
2111
  const channel = this.channels[channelNumber];
2112
+ if (channel.isDrum)
2113
+ return;
2114
+ scheduleTime ??= this.audioContext.currentTime;
1955
2115
  const state = channel.state;
1956
2116
  const prev = state.pitchWheelSensitivity;
1957
2117
  const next = value / 128;
@@ -1967,8 +2127,10 @@ class MidyGM2 {
1967
2127
  this.setFineTuning(channelNumber, fineTuning, scheduleTime);
1968
2128
  }
1969
2129
  setFineTuning(channelNumber, value, scheduleTime) {
1970
- scheduleTime ??= this.audioContext.currentTime;
1971
2130
  const channel = this.channels[channelNumber];
2131
+ if (channel.isDrum)
2132
+ return;
2133
+ scheduleTime ??= this.audioContext.currentTime;
1972
2134
  const prev = channel.fineTuning;
1973
2135
  const next = (value - 8192) / 8.192; // cent
1974
2136
  channel.fineTuning = next;
@@ -1982,8 +2144,10 @@ class MidyGM2 {
1982
2144
  this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
1983
2145
  }
1984
2146
  setCoarseTuning(channelNumber, value, scheduleTime) {
1985
- scheduleTime ??= this.audioContext.currentTime;
1986
2147
  const channel = this.channels[channelNumber];
2148
+ if (channel.isDrum)
2149
+ return;
2150
+ scheduleTime ??= this.audioContext.currentTime;
1987
2151
  const prev = channel.coarseTuning;
1988
2152
  const next = (value - 64) * 100; // cent
1989
2153
  channel.coarseTuning = next;
@@ -1997,8 +2161,10 @@ class MidyGM2 {
1997
2161
  this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
1998
2162
  }
1999
2163
  setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
2000
- scheduleTime ??= this.audioContext.currentTime;
2001
2164
  const channel = this.channels[channelNumber];
2165
+ if (channel.isDrum)
2166
+ return;
2167
+ scheduleTime ??= this.audioContext.currentTime;
2002
2168
  channel.modulationDepthRange = modulationDepthRange;
2003
2169
  this.updateModulation(channel, scheduleTime);
2004
2170
  }
@@ -2006,16 +2172,30 @@ class MidyGM2 {
2006
2172
  scheduleTime ??= this.audioContext.currentTime;
2007
2173
  return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2008
2174
  }
2175
+ resetAllStates(channelNumber) {
2176
+ const channel = this.channels[channelNumber];
2177
+ const state = channel.state;
2178
+ for (const type of Object.keys(defaultControllerState)) {
2179
+ state[type] = defaultControllerState[type].defaultValue;
2180
+ }
2181
+ for (const type of Object.keys(this.constructor.channelSettings)) {
2182
+ channel[type] = this.constructor.channelSettings[type];
2183
+ }
2184
+ this.mode = "GM2";
2185
+ this.masterFineTuning = 0; // cb
2186
+ this.masterCoarseTuning = 0; // cb
2187
+ }
2188
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2009
2189
  resetAllControllers(channelNumber) {
2010
2190
  const stateTypes = [
2191
+ "channelPressure",
2192
+ "pitchWheel",
2011
2193
  "expression",
2012
2194
  "modulationDepth",
2013
2195
  "sustainPedal",
2014
2196
  "portamento",
2015
2197
  "sostenutoPedal",
2016
2198
  "softPedal",
2017
- "channelPressure",
2018
- "pitchWheelSensitivity",
2019
2199
  ];
2020
2200
  const channel = this.channels[channelNumber];
2021
2201
  const state = channel.state;
@@ -2066,12 +2246,12 @@ class MidyGM2 {
2066
2246
  case 9:
2067
2247
  switch (data[3]) {
2068
2248
  case 1:
2069
- this.GM1SystemOn();
2249
+ this.GM1SystemOn(scheduleTime);
2070
2250
  break;
2071
2251
  case 2: // GM System Off
2072
2252
  break;
2073
2253
  case 3:
2074
- this.GM2SystemOn();
2254
+ this.GM2SystemOn(scheduleTime);
2075
2255
  break;
2076
2256
  default:
2077
2257
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2081,25 +2261,35 @@ class MidyGM2 {
2081
2261
  console.warn(`Unsupported Exclusive Message: ${data}`);
2082
2262
  }
2083
2263
  }
2084
- GM1SystemOn() {
2264
+ GM1SystemOn(scheduleTime) {
2265
+ scheduleTime ??= this.audioContext.currentTime;
2266
+ this.mode = "GM1";
2085
2267
  for (let i = 0; i < this.channels.length; i++) {
2268
+ this.allSoundOff(i, 0, scheduleTime);
2086
2269
  const channel = this.channels[i];
2087
2270
  channel.bankMSB = 0;
2088
2271
  channel.bankLSB = 0;
2089
2272
  channel.bank = 0;
2273
+ channel.isDrum = false;
2090
2274
  }
2091
2275
  this.channels[9].bankMSB = 1;
2092
2276
  this.channels[9].bank = 128;
2277
+ this.channels[9].isDrum = true;
2093
2278
  }
2094
- GM2SystemOn() {
2279
+ GM2SystemOn(scheduleTime) {
2280
+ scheduleTime ??= this.audioContext.currentTime;
2281
+ this.mode = "GM2";
2095
2282
  for (let i = 0; i < this.channels.length; i++) {
2283
+ this.allSoundOff(i, 0, scheduleTime);
2096
2284
  const channel = this.channels[i];
2097
2285
  channel.bankMSB = 121;
2098
2286
  channel.bankLSB = 0;
2099
2287
  channel.bank = 121 * 128;
2288
+ channel.isDrum = false;
2100
2289
  }
2101
2290
  this.channels[9].bankMSB = 120;
2102
2291
  this.channels[9].bank = 120 * 128;
2292
+ this.channels[9].isDrum = true;
2103
2293
  }
2104
2294
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2105
2295
  switch (data[2]) {
@@ -2162,8 +2352,14 @@ class MidyGM2 {
2162
2352
  const prev = this.masterFineTuning;
2163
2353
  const next = (value - 8192) / 8.192; // cent
2164
2354
  this.masterFineTuning = next;
2165
- channel.detune += next - prev;
2166
- this.updateChannelDetune(channel, scheduleTime);
2355
+ const detuneChange = next - prev;
2356
+ for (let i = 0; i < this.channels.length; i++) {
2357
+ const channel = this.channels[i];
2358
+ if (channel.isDrum)
2359
+ continue;
2360
+ channel.detune += detuneChange;
2361
+ this.updateChannelDetune(channel, scheduleTime);
2362
+ }
2167
2363
  }
2168
2364
  handleMasterCoarseTuningSysEx(data, scheduleTime) {
2169
2365
  const coarseTuning = data[4];
@@ -2173,8 +2369,14 @@ class MidyGM2 {
2173
2369
  const prev = this.masterCoarseTuning;
2174
2370
  const next = (value - 64) * 100; // cent
2175
2371
  this.masterCoarseTuning = next;
2176
- channel.detune += next - prev;
2177
- this.updateChannelDetune(channel, scheduleTime);
2372
+ const detuneChange = next - prev;
2373
+ for (let i = 0; i < this.channels.length; i++) {
2374
+ const channel = this.channels[i];
2375
+ if (channel.isDrum)
2376
+ continue;
2377
+ channel.detune += detuneChange;
2378
+ this.updateChannelDetune(channel, scheduleTime);
2379
+ }
2178
2380
  }
2179
2381
  handleGlobalParameterControlSysEx(data, scheduleTime) {
2180
2382
  if (data[7] === 1) {
@@ -2358,7 +2560,7 @@ class MidyGM2 {
2358
2560
  return value * 0.00787;
2359
2561
  }
2360
2562
  getChannelBitmap(data) {
2361
- const bitmap = new Array(16).fill(false);
2563
+ const bitmap = new Array(this.channels.length).fill(false);
2362
2564
  const ff = data[4] & 0b11;
2363
2565
  const gg = data[5] & 0x7F;
2364
2566
  const hh = data[6] & 0x7F;
@@ -2500,15 +2702,23 @@ class MidyGM2 {
2500
2702
  console.warn(`Unsupported Exclusive Message: ${data}`);
2501
2703
  }
2502
2704
  }
2705
+ // https://github.com/marmooo/js-timer-benchmark
2503
2706
  scheduleTask(callback, scheduleTime) {
2504
2707
  return new Promise((resolve) => {
2505
- const bufferSource = new AudioBufferSourceNode(this.audioContext);
2708
+ const bufferSource = new AudioBufferSourceNode(this.audioContext, {
2709
+ buffer: this.schedulerBuffer,
2710
+ });
2711
+ bufferSource.connect(this.scheduler);
2506
2712
  bufferSource.onended = () => {
2507
- callback();
2508
- resolve();
2713
+ try {
2714
+ callback();
2715
+ }
2716
+ finally {
2717
+ bufferSource.disconnect();
2718
+ resolve();
2719
+ }
2509
2720
  };
2510
2721
  bufferSource.start(scheduleTime);
2511
- bufferSource.stop(scheduleTime);
2512
2722
  });
2513
2723
  }
2514
2724
  }
@@ -2518,9 +2728,8 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2518
2728
  configurable: true,
2519
2729
  writable: true,
2520
2730
  value: {
2521
- currentBufferSource: null,
2522
2731
  detune: 0,
2523
- program: 0,
2732
+ programNumber: 0,
2524
2733
  bank: 121 * 128,
2525
2734
  bankMSB: 121,
2526
2735
  bankLSB: 0,
@@ -2529,8 +2738,8 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2529
2738
  rpnMSB: 127,
2530
2739
  rpnLSB: 127,
2531
2740
  mono: false, // CC#124, CC#125
2741
+ modulationDepthRange: 50, // cent
2532
2742
  fineTuning: 0, // cb
2533
2743
  coarseTuning: 0, // cb
2534
- modulationDepthRange: 50, // cent
2535
2744
  }
2536
2745
  });