@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/script/midy.js CHANGED
@@ -160,6 +160,39 @@ class Note {
160
160
  this.voiceParams = voiceParams;
161
161
  }
162
162
  }
163
+ const drumExclusiveClassesByKit = new Array(57);
164
+ const drumExclusiveClassCount = 10;
165
+ const standardSet = new Uint8Array(128);
166
+ standardSet[42] = 1;
167
+ standardSet[44] = 1;
168
+ standardSet[46] = 1; // HH
169
+ standardSet[71] = 2;
170
+ standardSet[72] = 2; // Whistle
171
+ standardSet[73] = 3;
172
+ standardSet[74] = 3; // Guiro
173
+ standardSet[78] = 4;
174
+ standardSet[79] = 4; // Cuica
175
+ standardSet[80] = 5;
176
+ standardSet[81] = 5; // Triangle
177
+ standardSet[29] = 6;
178
+ standardSet[30] = 6; // Scratch
179
+ standardSet[86] = 7;
180
+ standardSet[87] = 7; // Surdo
181
+ drumExclusiveClassesByKit[0] = standardSet;
182
+ const analogSet = new Uint8Array(128);
183
+ analogSet[42] = 8;
184
+ analogSet[44] = 8;
185
+ analogSet[46] = 8; // CHH
186
+ drumExclusiveClassesByKit[25] = analogSet;
187
+ const orchestraSet = new Uint8Array(128);
188
+ orchestraSet[27] = 9;
189
+ orchestraSet[28] = 9;
190
+ orchestraSet[29] = 9; // HH
191
+ drumExclusiveClassesByKit[48] = orchestraSet;
192
+ const sfxSet = new Uint8Array(128);
193
+ sfxSet[41] = 10;
194
+ sfxSet[42] = 10; // Scratch
195
+ drumExclusiveClassesByKit[56] = sfxSet;
163
196
  // normalized to 0-1 for use with the SF2 modulator model
164
197
  const defaultControllerState = {
165
198
  noteOnVelocity: { type: 2, defaultValue: 0 },
@@ -249,17 +282,11 @@ const volumeEnvelopeKeys = [
249
282
  const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
250
283
  class Midy {
251
284
  constructor(audioContext, options = this.defaultOptions) {
252
- Object.defineProperty(this, "ticksPerBeat", {
285
+ Object.defineProperty(this, "mode", {
253
286
  enumerable: true,
254
287
  configurable: true,
255
288
  writable: true,
256
- value: 120
257
- });
258
- Object.defineProperty(this, "totalTime", {
259
- enumerable: true,
260
- configurable: true,
261
- writable: true,
262
- value: 0
289
+ value: "GM2"
263
290
  });
264
291
  Object.defineProperty(this, "masterFineTuning", {
265
292
  enumerable: true,
@@ -294,6 +321,24 @@ class Midy {
294
321
  delayTimes: this.generateDistributedArray(0.02, 2, 0.5),
295
322
  }
296
323
  });
324
+ Object.defineProperty(this, "numChannels", {
325
+ enumerable: true,
326
+ configurable: true,
327
+ writable: true,
328
+ value: 16
329
+ });
330
+ Object.defineProperty(this, "ticksPerBeat", {
331
+ enumerable: true,
332
+ configurable: true,
333
+ writable: true,
334
+ value: 120
335
+ });
336
+ Object.defineProperty(this, "totalTime", {
337
+ enumerable: true,
338
+ configurable: true,
339
+ writable: true,
340
+ value: 0
341
+ });
297
342
  Object.defineProperty(this, "noteCheckInterval", {
298
343
  enumerable: true,
299
344
  configurable: true,
@@ -396,11 +441,17 @@ class Midy {
396
441
  writable: true,
397
442
  value: []
398
443
  });
399
- Object.defineProperty(this, "exclusiveClassMap", {
444
+ Object.defineProperty(this, "exclusiveClassNotes", {
445
+ enumerable: true,
446
+ configurable: true,
447
+ writable: true,
448
+ value: new Array(128)
449
+ });
450
+ Object.defineProperty(this, "drumExclusiveClassNotes", {
400
451
  enumerable: true,
401
452
  configurable: true,
402
453
  writable: true,
403
- value: new SparseMap(128)
454
+ value: new Array(this.numChannels * drumExclusiveClassCount)
404
455
  });
405
456
  Object.defineProperty(this, "defaultOptions", {
406
457
  enumerable: true,
@@ -427,6 +478,11 @@ class Midy {
427
478
  this.audioContext = audioContext;
428
479
  this.options = { ...this.defaultOptions, ...options };
429
480
  this.masterVolume = new GainNode(audioContext);
481
+ this.scheduler = new GainNode(audioContext, { gain: 0 });
482
+ this.schedulerBuffer = new AudioBuffer({
483
+ length: 1,
484
+ sampleRate: audioContext.sampleRate,
485
+ });
430
486
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
431
487
  this.controlChangeHandlers = this.createControlChangeHandlers();
432
488
  this.channels = this.createChannels(audioContext);
@@ -435,6 +491,7 @@ class Midy {
435
491
  this.chorusEffect.output.connect(this.masterVolume);
436
492
  this.reverbEffect.output.connect(this.masterVolume);
437
493
  this.masterVolume.connect(audioContext.destination);
494
+ this.scheduler.connect(audioContext.destination);
438
495
  this.GM2SystemOn();
439
496
  }
440
497
  initSoundFontTable() {
@@ -488,8 +545,10 @@ class Midy {
488
545
  };
489
546
  }
490
547
  createChannels(audioContext) {
491
- const channels = Array.from({ length: 16 }, () => {
548
+ const channels = Array.from({ length: this.numChannels }, () => {
492
549
  return {
550
+ currentBufferSource: null,
551
+ isDrum: false,
493
552
  ...this.constructor.channelSettings,
494
553
  state: new ControllerState(),
495
554
  controlTable: this.initControlTable(),
@@ -534,7 +593,7 @@ class Midy {
534
593
  return audioBuffer;
535
594
  }
536
595
  }
537
- createNoteBufferNode(audioBuffer, voiceParams) {
596
+ createBufferSource(voiceParams, audioBuffer) {
538
597
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
539
598
  bufferSource.buffer = audioBuffer;
540
599
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
@@ -628,7 +687,8 @@ class Midy {
628
687
  if (queueIndex >= this.timeline.length) {
629
688
  await Promise.all(this.notePromises);
630
689
  this.notePromises = [];
631
- this.exclusiveClassMap.clear();
690
+ this.exclusiveClassNotes.fill(undefined);
691
+ this.drumExclusiveClassNotes.fill(undefined);
632
692
  this.audioBufferCache.clear();
633
693
  resolve();
634
694
  return;
@@ -647,7 +707,8 @@ class Midy {
647
707
  else if (this.isStopping) {
648
708
  await this.stopNotes(0, true, now);
649
709
  this.notePromises = [];
650
- this.exclusiveClassMap.clear();
710
+ this.exclusiveClassNotes.fill(undefined);
711
+ this.drumExclusiveClassNotes.fill(undefined);
651
712
  this.audioBufferCache.clear();
652
713
  resolve();
653
714
  this.isStopping = false;
@@ -656,7 +717,8 @@ class Midy {
656
717
  }
657
718
  else if (this.isSeeking) {
658
719
  this.stopNotes(0, true, now);
659
- this.exclusiveClassMap.clear();
720
+ this.exclusiveClassNotes.fill(undefined);
721
+ this.drumExclusiveClassNotes.fill(undefined);
660
722
  this.startTime = this.audioContext.currentTime;
661
723
  queueIndex = this.getQueueIndex(this.resumeTime);
662
724
  offset = this.resumeTime - this.startTime;
@@ -684,7 +746,7 @@ class Midy {
684
746
  extractMidiData(midi) {
685
747
  const instruments = new Set();
686
748
  const timeline = [];
687
- const tmpChannels = new Array(16);
749
+ const tmpChannels = new Array(this.channels.length);
688
750
  for (let i = 0; i < tmpChannels.length; i++) {
689
751
  tmpChannels[i] = {
690
752
  programNumber: -1,
@@ -812,6 +874,9 @@ class Midy {
812
874
  if (!this.isPlaying)
813
875
  return;
814
876
  this.isStopping = true;
877
+ for (let i = 0; i < this.channels.length; i++) {
878
+ this.resetAllStates(i);
879
+ }
815
880
  }
816
881
  pause() {
817
882
  if (!this.isPlaying || this.isPaused)
@@ -1017,7 +1082,9 @@ class Midy {
1017
1082
  return 8.176 * this.centToRate(cent);
1018
1083
  }
1019
1084
  calcChannelDetune(channel) {
1020
- const masterTuning = this.masterCoarseTuning + this.masterFineTuning;
1085
+ const masterTuning = channel.isDrum
1086
+ ? 0
1087
+ : this.masterCoarseTuning + this.masterFineTuning;
1021
1088
  const channelTuning = channel.coarseTuning + channel.fineTuning;
1022
1089
  const tuning = masterTuning + channelTuning;
1023
1090
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
@@ -1044,9 +1111,8 @@ class Midy {
1044
1111
  .setValueAtTime(detune, scheduleTime);
1045
1112
  }
1046
1113
  getPortamentoTime(channel) {
1047
- const factor = 5 * Math.log(10) / 127;
1048
- const time = channel.state.portamentoTime;
1049
- return Math.log(time) / factor;
1114
+ const factor = 5 * Math.log(10) * 127;
1115
+ return channel.state.portamentoTime * factor;
1050
1116
  }
1051
1117
  setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1052
1118
  const { voiceParams, startTime } = note;
@@ -1185,8 +1251,8 @@ class Midy {
1185
1251
  note.vibratoLFO.connect(note.vibratoDepth);
1186
1252
  note.vibratoDepth.connect(note.bufferSource.detune);
1187
1253
  }
1188
- async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1189
- const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1254
+ async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1255
+ const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1190
1256
  const cache = this.audioBufferCache.get(audioBufferId);
1191
1257
  if (cache) {
1192
1258
  cache.counter += 1;
@@ -1209,8 +1275,8 @@ class Midy {
1209
1275
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1210
1276
  const voiceParams = voice.getAllParams(controllerState);
1211
1277
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1212
- const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1213
- note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1278
+ const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1279
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1214
1280
  note.volumeNode = new GainNode(this.audioContext);
1215
1281
  note.gainL = new GainNode(this.audioContext);
1216
1282
  note.gainR = new GainNode(this.audioContext);
@@ -1254,23 +1320,72 @@ class Midy {
1254
1320
  note.bufferSource.start(startTime);
1255
1321
  return note;
1256
1322
  }
1257
- calcBank(channel, channelNumber) {
1258
- if (channel.bankMSB === 121) {
1259
- return 0;
1323
+ calcBank(channel) {
1324
+ switch (this.mode) {
1325
+ case "GM1":
1326
+ if (channel.isDrum)
1327
+ return 128;
1328
+ return 0;
1329
+ case "GM2":
1330
+ if (channel.bankMSB === 121)
1331
+ return 0;
1332
+ if (channel.isDrum)
1333
+ return 128;
1334
+ return channel.bank;
1335
+ default:
1336
+ return channel.bank;
1260
1337
  }
1261
- if (channelNumber % 9 <= 1 && channel.bankMSB === 120) {
1262
- return 128;
1338
+ }
1339
+ handleExclusiveClass(note, channelNumber, startTime) {
1340
+ const exclusiveClass = note.voiceParams.exclusiveClass;
1341
+ if (exclusiveClass === 0)
1342
+ return;
1343
+ const prev = this.exclusiveClassNotes[exclusiveClass];
1344
+ if (prev) {
1345
+ const [prevNote, prevChannelNumber] = prev;
1346
+ if (prevNote && !prevNote.ending) {
1347
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1348
+ startTime, true, // force
1349
+ undefined);
1350
+ }
1263
1351
  }
1264
- return channel.bank;
1352
+ this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
1353
+ }
1354
+ handleDrumExclusiveClass(note, channelNumber, startTime) {
1355
+ const channel = this.channels[channelNumber];
1356
+ if (!channel.isDrum)
1357
+ return;
1358
+ const kitTable = drumExclusiveClassesByKit[channel.programNumber];
1359
+ if (!kitTable)
1360
+ return;
1361
+ const drumExclusiveClass = kitTable[note.noteNumber];
1362
+ if (drumExclusiveClass === 0)
1363
+ return;
1364
+ const index = (drumExclusiveClass - 1) * this.channels.length +
1365
+ channelNumber;
1366
+ const prevNote = this.drumExclusiveClassNotes[index];
1367
+ if (prevNote && !prevNote.ending) {
1368
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1369
+ startTime, true, // force
1370
+ undefined);
1371
+ }
1372
+ this.drumExclusiveClassNotes[index] = note;
1373
+ }
1374
+ isDrumNoteOffException(channel, noteNumber) {
1375
+ if (!channel.isDrum)
1376
+ return false;
1377
+ const programNumber = channel.programNumber;
1378
+ return (programNumber === 48 && noteNumber === 88) ||
1379
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84);
1265
1380
  }
1266
1381
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1267
1382
  const channel = this.channels[channelNumber];
1268
1383
  const bankNumber = this.calcBank(channel, channelNumber);
1269
- const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
1384
+ const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1270
1385
  if (soundFontIndex === undefined)
1271
1386
  return;
1272
1387
  const soundFont = this.soundFonts[soundFontIndex];
1273
- const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1388
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1274
1389
  if (!voice)
1275
1390
  return;
1276
1391
  const isSF3 = soundFont.parsed.info.version.major === 3;
@@ -1280,31 +1395,58 @@ class Midy {
1280
1395
  if (0.5 <= channel.state.sustainPedal) {
1281
1396
  channel.sustainNotes.push(note);
1282
1397
  }
1283
- const exclusiveClass = note.voiceParams.exclusiveClass;
1284
- if (exclusiveClass !== 0) {
1285
- if (this.exclusiveClassMap.has(exclusiveClass)) {
1286
- const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1287
- const [prevNote, prevChannelNumber] = prevEntry;
1288
- if (!prevNote.ending) {
1289
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1290
- startTime, true, // force
1291
- undefined);
1292
- }
1293
- }
1294
- this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1295
- }
1398
+ this.handleExclusiveClass(note, channelNumber, startTime);
1399
+ this.handleDrumExclusiveClass(note, channelNumber, startTime);
1296
1400
  const scheduledNotes = channel.scheduledNotes;
1297
- if (scheduledNotes.has(noteNumber)) {
1298
- scheduledNotes.get(noteNumber).push(note);
1401
+ let notes = scheduledNotes.get(noteNumber);
1402
+ if (notes) {
1403
+ notes.push(note);
1299
1404
  }
1300
1405
  else {
1301
- scheduledNotes.set(noteNumber, [note]);
1406
+ notes = [note];
1407
+ scheduledNotes.set(noteNumber, notes);
1408
+ }
1409
+ if (this.isDrumNoteOffException(channel, noteNumber)) {
1410
+ const stopTime = startTime + note.bufferSource.buffer.duration;
1411
+ const index = notes.length - 1;
1412
+ const promise = new Promise((resolve) => {
1413
+ note.bufferSource.onended = () => {
1414
+ this.disconnectNote(note, scheduledNotes, index);
1415
+ resolve();
1416
+ };
1417
+ note.bufferSource.stop(stopTime);
1418
+ });
1419
+ this.notePromises.push(promise);
1302
1420
  }
1303
1421
  }
1304
1422
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1305
1423
  scheduleTime ??= this.audioContext.currentTime;
1306
1424
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1307
1425
  }
1426
+ disconnectNote(note, scheduledNotes, index) {
1427
+ scheduledNotes[index] = null;
1428
+ note.bufferSource.disconnect();
1429
+ note.filterNode.disconnect();
1430
+ note.volumeEnvelopeNode.disconnect();
1431
+ note.volumeNode.disconnect();
1432
+ note.gainL.disconnect();
1433
+ note.gainR.disconnect();
1434
+ if (note.modulationDepth) {
1435
+ note.volumeDepth.disconnect();
1436
+ note.modulationDepth.disconnect();
1437
+ note.modulationLFO.stop();
1438
+ }
1439
+ if (note.vibratoDepth) {
1440
+ note.vibratoDepth.disconnect();
1441
+ note.vibratoLFO.stop();
1442
+ }
1443
+ if (note.reverbEffectsSend) {
1444
+ note.reverbEffectsSend.disconnect();
1445
+ }
1446
+ if (note.chorusEffectsSend) {
1447
+ note.chorusEffectsSend.disconnect();
1448
+ }
1449
+ }
1308
1450
  stopNote(endTime, stopTime, scheduledNotes, index) {
1309
1451
  const note = scheduledNotes[index];
1310
1452
  note.volumeEnvelopeNode.gain
@@ -1316,28 +1458,7 @@ class Midy {
1316
1458
  }, stopTime);
1317
1459
  return new Promise((resolve) => {
1318
1460
  note.bufferSource.onended = () => {
1319
- scheduledNotes[index] = null;
1320
- note.bufferSource.disconnect();
1321
- note.filterNode.disconnect();
1322
- note.volumeEnvelopeNode.disconnect();
1323
- note.volumeNode.disconnect();
1324
- note.gainL.disconnect();
1325
- note.gainR.disconnect();
1326
- if (note.modulationDepth) {
1327
- note.volumeDepth.disconnect();
1328
- note.modulationDepth.disconnect();
1329
- note.modulationLFO.stop();
1330
- }
1331
- if (note.vibratoDepth) {
1332
- note.vibratoDepth.disconnect();
1333
- note.vibratoLFO.stop();
1334
- }
1335
- if (note.reverbEffectsSend) {
1336
- note.reverbEffectsSend.disconnect();
1337
- }
1338
- if (note.chorusEffectsSend) {
1339
- note.chorusEffectsSend.disconnect();
1340
- }
1461
+ this.disconnectNote(note, scheduledNotes, index);
1341
1462
  resolve();
1342
1463
  };
1343
1464
  note.bufferSource.stop(stopTime);
@@ -1345,6 +1466,8 @@ class Midy {
1345
1466
  }
1346
1467
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1347
1468
  const channel = this.channels[channelNumber];
1469
+ if (this.isDrumNoteOffException(channel, noteNumber))
1470
+ return;
1348
1471
  const state = channel.state;
1349
1472
  if (!force) {
1350
1473
  if (0.5 <= state.sustainPedal)
@@ -1444,13 +1567,25 @@ class Midy {
1444
1567
  }
1445
1568
  // this.applyVoiceParams(channel, 10);
1446
1569
  }
1447
- handleProgramChange(channelNumber, program, _scheduleTime) {
1570
+ handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1448
1571
  const channel = this.channels[channelNumber];
1449
1572
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1450
- channel.program = program;
1573
+ channel.programNumber = programNumber;
1574
+ if (this.mode === "GM2") {
1575
+ switch (channel.bankMSB) {
1576
+ case 120:
1577
+ channel.isDrum = true;
1578
+ break;
1579
+ case 121:
1580
+ channel.isDrum = false;
1581
+ break;
1582
+ }
1583
+ }
1451
1584
  }
1452
1585
  handleChannelPressure(channelNumber, value, scheduleTime) {
1453
1586
  const channel = this.channels[channelNumber];
1587
+ if (channel.isDrum)
1588
+ return;
1454
1589
  const prev = channel.state.channelPressure;
1455
1590
  const next = value / 127;
1456
1591
  channel.state.channelPressure = next;
@@ -1469,8 +1604,10 @@ class Midy {
1469
1604
  this.setPitchBend(channelNumber, pitchBend, scheduleTime);
1470
1605
  }
1471
1606
  setPitchBend(channelNumber, value, scheduleTime) {
1472
- scheduleTime ??= this.audioContext.currentTime;
1473
1607
  const channel = this.channels[channelNumber];
1608
+ if (channel.isDrum)
1609
+ return;
1610
+ scheduleTime ??= this.audioContext.currentTime;
1474
1611
  const state = channel.state;
1475
1612
  const prev = state.pitchWheel * 2 - 1;
1476
1613
  const next = (value - 8192) / 8192;
@@ -1752,15 +1889,16 @@ class Midy {
1752
1889
  });
1753
1890
  }
1754
1891
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1755
- scheduleTime ??= this.audioContext.currentTime;
1756
1892
  const channel = this.channels[channelNumber];
1893
+ if (channel.isDrum)
1894
+ return;
1895
+ scheduleTime ??= this.audioContext.currentTime;
1757
1896
  channel.state.modulationDepth = modulation / 127;
1758
1897
  this.updateModulation(channel, scheduleTime);
1759
1898
  }
1760
1899
  setPortamentoTime(channelNumber, portamentoTime) {
1761
1900
  const channel = this.channels[channelNumber];
1762
- const factor = 5 * Math.log(10) / 127;
1763
- channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1901
+ channel.state.portamentoTime = portamentoTime / 127;
1764
1902
  }
1765
1903
  setKeyBasedVolume(channel, scheduleTime) {
1766
1904
  this.processScheduledNotes(channel, (note) => {
@@ -1832,8 +1970,10 @@ class Midy {
1832
1970
  .setValueAtTime(volume * gainRight, scheduleTime);
1833
1971
  }
1834
1972
  setSustainPedal(channelNumber, value, scheduleTime) {
1835
- scheduleTime ??= this.audioContext.currentTime;
1836
1973
  const channel = this.channels[channelNumber];
1974
+ if (channel.isDrum)
1975
+ return;
1976
+ scheduleTime ??= this.audioContext.currentTime;
1837
1977
  channel.state.sustainPedal = value / 127;
1838
1978
  if (64 <= value) {
1839
1979
  this.processScheduledNotes(channel, (note) => {
@@ -1845,11 +1985,16 @@ class Midy {
1845
1985
  }
1846
1986
  }
1847
1987
  setPortamento(channelNumber, value) {
1848
- this.channels[channelNumber].state.portamento = value / 127;
1988
+ const channel = this.channels[channelNumber];
1989
+ if (channel.isDrum)
1990
+ return;
1991
+ channel.state.portamento = value / 127;
1849
1992
  }
1850
1993
  setSostenutoPedal(channelNumber, value, scheduleTime) {
1851
- scheduleTime ??= this.audioContext.currentTime;
1852
1994
  const channel = this.channels[channelNumber];
1995
+ if (channel.isDrum)
1996
+ return;
1997
+ scheduleTime ??= this.audioContext.currentTime;
1853
1998
  channel.state.sostenutoPedal = value / 127;
1854
1999
  if (64 <= value) {
1855
2000
  channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
@@ -1858,13 +2003,28 @@ class Midy {
1858
2003
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
1859
2004
  }
1860
2005
  }
1861
- setSoftPedal(channelNumber, softPedal, _scheduleTime) {
2006
+ setSoftPedal(channelNumber, softPedal, scheduleTime) {
1862
2007
  const channel = this.channels[channelNumber];
2008
+ if (channel.isDrum)
2009
+ return;
2010
+ scheduleTime ??= this.audioContext.currentTime;
1863
2011
  channel.state.softPedal = softPedal / 127;
2012
+ this.processScheduledNotes(channel, (note) => {
2013
+ if (note.portamento) {
2014
+ this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
2015
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2016
+ }
2017
+ else {
2018
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2019
+ this.setFilterEnvelope(channel, note, scheduleTime);
2020
+ }
2021
+ });
1864
2022
  }
1865
2023
  setFilterResonance(channelNumber, filterResonance, scheduleTime) {
1866
- scheduleTime ??= this.audioContext.currentTime;
1867
2024
  const channel = this.channels[channelNumber];
2025
+ if (channel.isDrum)
2026
+ return;
2027
+ scheduleTime ??= this.audioContext.currentTime;
1868
2028
  const state = channel.state;
1869
2029
  state.filterResonance = filterResonance / 64;
1870
2030
  this.processScheduledNotes(channel, (note) => {
@@ -1873,13 +2033,17 @@ class Midy {
1873
2033
  });
1874
2034
  }
1875
2035
  setReleaseTime(channelNumber, releaseTime, _scheduleTime) {
1876
- scheduleTime ??= this.audioContext.currentTime;
1877
2036
  const channel = this.channels[channelNumber];
2037
+ if (channel.isDrum)
2038
+ return;
2039
+ scheduleTime ??= this.audioContext.currentTime;
1878
2040
  channel.state.releaseTime = releaseTime / 64;
1879
2041
  }
1880
2042
  setAttackTime(channelNumber, attackTime, scheduleTime) {
1881
- scheduleTime ??= this.audioContext.currentTime;
1882
2043
  const channel = this.channels[channelNumber];
2044
+ if (channel.isDrum)
2045
+ return;
2046
+ scheduleTime ??= this.audioContext.currentTime;
1883
2047
  channel.state.attackTime = attackTime / 64;
1884
2048
  this.processScheduledNotes(channel, (note) => {
1885
2049
  if (note.startTime < scheduleTime)
@@ -1888,8 +2052,10 @@ class Midy {
1888
2052
  });
1889
2053
  }
1890
2054
  setBrightness(channelNumber, brightness, scheduleTime) {
1891
- scheduleTime ??= this.audioContext.currentTime;
1892
2055
  const channel = this.channels[channelNumber];
2056
+ if (channel.isDrum)
2057
+ return;
2058
+ scheduleTime ??= this.audioContext.currentTime;
1893
2059
  channel.state.brightness = brightness / 64;
1894
2060
  this.processScheduledNotes(channel, (note) => {
1895
2061
  if (note.portamento) {
@@ -1901,16 +2067,20 @@ class Midy {
1901
2067
  });
1902
2068
  }
1903
2069
  setDecayTime(channelNumber, dacayTime, scheduleTime) {
1904
- scheduleTime ??= this.audioContext.currentTime;
1905
2070
  const channel = this.channels[channelNumber];
2071
+ if (channel.isDrum)
2072
+ return;
2073
+ scheduleTime ??= this.audioContext.currentTime;
1906
2074
  channel.state.decayTime = dacayTime / 64;
1907
2075
  this.processScheduledNotes(channel, (note) => {
1908
2076
  this.setVolumeEnvelope(channel, note, scheduleTime);
1909
2077
  });
1910
2078
  }
1911
2079
  setVibratoRate(channelNumber, vibratoRate, scheduleTime) {
1912
- scheduleTime ??= this.audioContext.currentTime;
1913
2080
  const channel = this.channels[channelNumber];
2081
+ if (channel.isDrum)
2082
+ return;
2083
+ scheduleTime ??= this.audioContext.currentTime;
1914
2084
  channel.state.vibratoRate = vibratoRate / 64;
1915
2085
  if (channel.vibratoDepth <= 0)
1916
2086
  return;
@@ -1919,8 +2089,10 @@ class Midy {
1919
2089
  });
1920
2090
  }
1921
2091
  setVibratoDepth(channelNumber, vibratoDepth, scheduleTime) {
1922
- scheduleTime ??= this.audioContext.currentTime;
1923
2092
  const channel = this.channels[channelNumber];
2093
+ if (channel.isDrum)
2094
+ return;
2095
+ scheduleTime ??= this.audioContext.currentTime;
1924
2096
  const prev = channel.state.vibratoDepth;
1925
2097
  channel.state.vibratoDepth = vibratoDepth / 64;
1926
2098
  if (0 < prev) {
@@ -1935,8 +2107,10 @@ class Midy {
1935
2107
  }
1936
2108
  }
1937
2109
  setVibratoDelay(channelNumber, vibratoDelay) {
1938
- scheduleTime ??= this.audioContext.currentTime;
1939
2110
  const channel = this.channels[channelNumber];
2111
+ if (channel.isDrum)
2112
+ return;
2113
+ scheduleTime ??= this.audioContext.currentTime;
1940
2114
  channel.state.vibratoDelay = vibratoDelay / 64;
1941
2115
  if (0 < channel.state.vibratoDepth) {
1942
2116
  this.processScheduledNotes(channel, (note) => {
@@ -2083,8 +2257,10 @@ class Midy {
2083
2257
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
2084
2258
  }
2085
2259
  setPitchBendRange(channelNumber, value, scheduleTime) {
2086
- scheduleTime ??= this.audioContext.currentTime;
2087
2260
  const channel = this.channels[channelNumber];
2261
+ if (channel.isDrum)
2262
+ return;
2263
+ scheduleTime ??= this.audioContext.currentTime;
2088
2264
  const state = channel.state;
2089
2265
  const prev = state.pitchWheelSensitivity;
2090
2266
  const next = value / 128;
@@ -2100,8 +2276,10 @@ class Midy {
2100
2276
  this.setFineTuning(channelNumber, fineTuning, scheduleTime);
2101
2277
  }
2102
2278
  setFineTuning(channelNumber, value, scheduleTime) {
2103
- scheduleTime ??= this.audioContext.currentTime;
2104
2279
  const channel = this.channels[channelNumber];
2280
+ if (channel.isDrum)
2281
+ return;
2282
+ scheduleTime ??= this.audioContext.currentTime;
2105
2283
  const prev = channel.fineTuning;
2106
2284
  const next = (value - 8192) / 8.192; // cent
2107
2285
  channel.fineTuning = next;
@@ -2115,8 +2293,10 @@ class Midy {
2115
2293
  this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
2116
2294
  }
2117
2295
  setCoarseTuning(channelNumber, value, scheduleTime) {
2118
- scheduleTime ??= this.audioContext.currentTime;
2119
2296
  const channel = this.channels[channelNumber];
2297
+ if (channel.isDrum)
2298
+ return;
2299
+ scheduleTime ??= this.audioContext.currentTime;
2120
2300
  const prev = channel.coarseTuning;
2121
2301
  const next = (value - 64) * 100; // cent
2122
2302
  channel.coarseTuning = next;
@@ -2130,8 +2310,10 @@ class Midy {
2130
2310
  this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
2131
2311
  }
2132
2312
  setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
2133
- scheduleTime ??= this.audioContext.currentTime;
2134
2313
  const channel = this.channels[channelNumber];
2314
+ if (channel.isDrum)
2315
+ return;
2316
+ scheduleTime ??= this.audioContext.currentTime;
2135
2317
  channel.modulationDepthRange = modulationDepthRange;
2136
2318
  this.updateModulation(channel, scheduleTime);
2137
2319
  }
@@ -2139,16 +2321,31 @@ class Midy {
2139
2321
  scheduleTime ??= this.audioContext.currentTime;
2140
2322
  return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2141
2323
  }
2324
+ resetAllStates(channelNumber) {
2325
+ const channel = this.channels[channelNumber];
2326
+ const state = channel.state;
2327
+ for (const type of Object.keys(defaultControllerState)) {
2328
+ state[type] = defaultControllerState[type].defaultValue;
2329
+ }
2330
+ for (const type of Object.keys(this.constructor.channelSettings)) {
2331
+ channel[type] = this.constructor.channelSettings[type];
2332
+ }
2333
+ this.mode = "GM2";
2334
+ this.masterFineTuning = 0; // cb
2335
+ this.masterCoarseTuning = 0; // cb
2336
+ }
2337
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2142
2338
  resetAllControllers(channelNumber) {
2143
2339
  const stateTypes = [
2340
+ "polyphonicKeyPressure",
2341
+ "channelPressure",
2342
+ "pitchWheel",
2144
2343
  "expression",
2145
2344
  "modulationDepth",
2146
2345
  "sustainPedal",
2147
2346
  "portamento",
2148
2347
  "sostenutoPedal",
2149
2348
  "softPedal",
2150
- "channelPressure",
2151
- "pitchWheelSensitivity",
2152
2349
  ];
2153
2350
  const channel = this.channels[channelNumber];
2154
2351
  const state = channel.state;
@@ -2202,12 +2399,12 @@ class Midy {
2202
2399
  case 9:
2203
2400
  switch (data[3]) {
2204
2401
  case 1:
2205
- this.GM1SystemOn();
2402
+ this.GM1SystemOn(scheduleTime);
2206
2403
  break;
2207
2404
  case 2: // GM System Off
2208
2405
  break;
2209
2406
  case 3:
2210
- this.GM2SystemOn();
2407
+ this.GM2SystemOn(scheduleTime);
2211
2408
  break;
2212
2409
  default:
2213
2410
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2217,25 +2414,35 @@ class Midy {
2217
2414
  console.warn(`Unsupported Exclusive Message: ${data}`);
2218
2415
  }
2219
2416
  }
2220
- GM1SystemOn() {
2417
+ GM1SystemOn(scheduleTime) {
2418
+ scheduleTime ??= this.audioContext.currentTime;
2419
+ this.mode = "GM1";
2221
2420
  for (let i = 0; i < this.channels.length; i++) {
2421
+ this.allSoundOff(i, 0, scheduleTime);
2222
2422
  const channel = this.channels[i];
2223
2423
  channel.bankMSB = 0;
2224
2424
  channel.bankLSB = 0;
2225
2425
  channel.bank = 0;
2426
+ channel.isDrum = false;
2226
2427
  }
2227
2428
  this.channels[9].bankMSB = 1;
2228
2429
  this.channels[9].bank = 128;
2430
+ this.channels[9].isDrum = true;
2229
2431
  }
2230
- GM2SystemOn() {
2432
+ GM2SystemOn(scheduleTime) {
2433
+ scheduleTime ??= this.audioContext.currentTime;
2434
+ this.mode = "GM2";
2231
2435
  for (let i = 0; i < this.channels.length; i++) {
2436
+ this.allSoundOff(i, 0, scheduleTime);
2232
2437
  const channel = this.channels[i];
2233
2438
  channel.bankMSB = 121;
2234
2439
  channel.bankLSB = 0;
2235
2440
  channel.bank = 121 * 128;
2441
+ channel.isDrum = false;
2236
2442
  }
2237
2443
  this.channels[9].bankMSB = 120;
2238
2444
  this.channels[9].bank = 120 * 128;
2445
+ this.channels[9].isDrum = true;
2239
2446
  }
2240
2447
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2241
2448
  switch (data[2]) {
@@ -2311,8 +2518,14 @@ class Midy {
2311
2518
  const prev = this.masterFineTuning;
2312
2519
  const next = (value - 8192) / 8.192; // cent
2313
2520
  this.masterFineTuning = next;
2314
- channel.detune += next - prev;
2315
- this.updateChannelDetune(channel, scheduleTime);
2521
+ const detuneChange = next - prev;
2522
+ for (let i = 0; i < this.channels.length; i++) {
2523
+ const channel = this.channels[i];
2524
+ if (channel.isDrum)
2525
+ continue;
2526
+ channel.detune += detuneChange;
2527
+ this.updateChannelDetune(channel, scheduleTime);
2528
+ }
2316
2529
  }
2317
2530
  handleMasterCoarseTuningSysEx(data, scheduleTime) {
2318
2531
  const coarseTuning = data[4];
@@ -2322,8 +2535,14 @@ class Midy {
2322
2535
  const prev = this.masterCoarseTuning;
2323
2536
  const next = (value - 64) * 100; // cent
2324
2537
  this.masterCoarseTuning = next;
2325
- channel.detune += next - prev;
2326
- this.updateChannelDetune(channel, scheduleTime);
2538
+ const detuneChange = next - prev;
2539
+ for (let i = 0; i < this.channels.length; i++) {
2540
+ const channel = this.channels[i];
2541
+ if (channel.isDrum)
2542
+ continue;
2543
+ channel.detune += detuneChange;
2544
+ this.updateChannelDetune(channel, scheduleTime);
2545
+ }
2327
2546
  }
2328
2547
  handleGlobalParameterControlSysEx(data, scheduleTime) {
2329
2548
  if (data[7] === 1) {
@@ -2507,7 +2726,7 @@ class Midy {
2507
2726
  return value * 0.00787;
2508
2727
  }
2509
2728
  getChannelBitmap(data) {
2510
- const bitmap = new Array(16).fill(false);
2729
+ const bitmap = new Array(this.channels.length).fill(false);
2511
2730
  const ff = data[4] & 0b11;
2512
2731
  const gg = data[5] & 0x7F;
2513
2732
  const hh = data[6] & 0x7F;
@@ -2535,6 +2754,8 @@ class Midy {
2535
2754
  if (!channelBitmap[i])
2536
2755
  continue;
2537
2756
  const channel = this.channels[i];
2757
+ if (channel.isDrum)
2758
+ continue;
2538
2759
  for (let j = 0; j < 12; j++) {
2539
2760
  const centValue = data[j + 7] - 64;
2540
2761
  channel.scaleOctaveTuningTable[j] = centValue;
@@ -2553,6 +2774,8 @@ class Midy {
2553
2774
  if (!channelBitmap[i])
2554
2775
  continue;
2555
2776
  const channel = this.channels[i];
2777
+ if (channel.isDrum)
2778
+ continue;
2556
2779
  for (let j = 0; j < 12; j++) {
2557
2780
  const index = 7 + j * 2;
2558
2781
  const msb = data[index] & 0x7F;
@@ -2623,7 +2846,10 @@ class Midy {
2623
2846
  }
2624
2847
  handlePressureSysEx(data, tableName) {
2625
2848
  const channelNumber = data[4];
2626
- const table = this.channels[channelNumber][tableName];
2849
+ const channel = this.channels[channelNumber];
2850
+ if (channel.isDrum)
2851
+ return;
2852
+ const table = channel[tableName];
2627
2853
  for (let i = 5; i < data.length - 1; i += 2) {
2628
2854
  const pp = data[i];
2629
2855
  const rr = data[i + 1];
@@ -2651,8 +2877,11 @@ class Midy {
2651
2877
  }
2652
2878
  handleControlChangeSysEx(data) {
2653
2879
  const channelNumber = data[4];
2880
+ const channel = this.channels[channelNumber];
2881
+ if (channel.isDrum)
2882
+ return;
2654
2883
  const controllerType = data[5];
2655
- const table = this.channels[channelNumber].controlTable[controllerType];
2884
+ const table = channel.controlTable[controllerType];
2656
2885
  for (let i = 6; i < data.length - 1; i += 2) {
2657
2886
  const pp = data[i];
2658
2887
  const rr = data[i + 1];
@@ -2666,8 +2895,11 @@ class Midy {
2666
2895
  }
2667
2896
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2668
2897
  const channelNumber = data[4];
2898
+ const channel = this.channels[channelNumber];
2899
+ if (channel.isDrum)
2900
+ return;
2669
2901
  const keyNumber = data[5];
2670
- const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2902
+ const table = channel.keyBasedInstrumentControlTable;
2671
2903
  for (let i = 6; i < data.length - 1; i += 2) {
2672
2904
  const controllerType = data[i];
2673
2905
  const value = data[i + 1];
@@ -2686,15 +2918,23 @@ class Midy {
2686
2918
  console.warn(`Unsupported Exclusive Message: ${data}`);
2687
2919
  }
2688
2920
  }
2921
+ // https://github.com/marmooo/js-timer-benchmark
2689
2922
  scheduleTask(callback, scheduleTime) {
2690
2923
  return new Promise((resolve) => {
2691
- const bufferSource = new AudioBufferSourceNode(this.audioContext);
2924
+ const bufferSource = new AudioBufferSourceNode(this.audioContext, {
2925
+ buffer: this.schedulerBuffer,
2926
+ });
2927
+ bufferSource.connect(this.scheduler);
2692
2928
  bufferSource.onended = () => {
2693
- callback();
2694
- resolve();
2929
+ try {
2930
+ callback();
2931
+ }
2932
+ finally {
2933
+ bufferSource.disconnect();
2934
+ resolve();
2935
+ }
2695
2936
  };
2696
2937
  bufferSource.start(scheduleTime);
2697
- bufferSource.stop(scheduleTime);
2698
2938
  });
2699
2939
  }
2700
2940
  }
@@ -2704,9 +2944,8 @@ Object.defineProperty(Midy, "channelSettings", {
2704
2944
  configurable: true,
2705
2945
  writable: true,
2706
2946
  value: {
2707
- currentBufferSource: null,
2708
2947
  detune: 0,
2709
- program: 0,
2948
+ programNumber: 0,
2710
2949
  bank: 121 * 128,
2711
2950
  bankMSB: 121,
2712
2951
  bankLSB: 0,
@@ -2715,8 +2954,8 @@ Object.defineProperty(Midy, "channelSettings", {
2715
2954
  rpnMSB: 127,
2716
2955
  rpnLSB: 127,
2717
2956
  mono: false, // CC#124, CC#125
2957
+ modulationDepthRange: 50, // cent
2718
2958
  fineTuning: 0, // cb
2719
2959
  coarseTuning: 0, // cb
2720
- modulationDepthRange: 50, // cent
2721
2960
  }
2722
2961
  });