@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.js CHANGED
@@ -157,6 +157,39 @@ class Note {
157
157
  this.voiceParams = voiceParams;
158
158
  }
159
159
  }
160
+ const drumExclusiveClassesByKit = new Array(57);
161
+ const drumExclusiveClassCount = 10;
162
+ const standardSet = new Uint8Array(128);
163
+ standardSet[42] = 1;
164
+ standardSet[44] = 1;
165
+ standardSet[46] = 1; // HH
166
+ standardSet[71] = 2;
167
+ standardSet[72] = 2; // Whistle
168
+ standardSet[73] = 3;
169
+ standardSet[74] = 3; // Guiro
170
+ standardSet[78] = 4;
171
+ standardSet[79] = 4; // Cuica
172
+ standardSet[80] = 5;
173
+ standardSet[81] = 5; // Triangle
174
+ standardSet[29] = 6;
175
+ standardSet[30] = 6; // Scratch
176
+ standardSet[86] = 7;
177
+ standardSet[87] = 7; // Surdo
178
+ drumExclusiveClassesByKit[0] = standardSet;
179
+ const analogSet = new Uint8Array(128);
180
+ analogSet[42] = 8;
181
+ analogSet[44] = 8;
182
+ analogSet[46] = 8; // CHH
183
+ drumExclusiveClassesByKit[25] = analogSet;
184
+ const orchestraSet = new Uint8Array(128);
185
+ orchestraSet[27] = 9;
186
+ orchestraSet[28] = 9;
187
+ orchestraSet[29] = 9; // HH
188
+ drumExclusiveClassesByKit[48] = orchestraSet;
189
+ const sfxSet = new Uint8Array(128);
190
+ sfxSet[41] = 10;
191
+ sfxSet[42] = 10; // Scratch
192
+ drumExclusiveClassesByKit[56] = sfxSet;
160
193
  // normalized to 0-1 for use with the SF2 modulator model
161
194
  const defaultControllerState = {
162
195
  noteOnVelocity: { type: 2, defaultValue: 0 },
@@ -246,17 +279,11 @@ const volumeEnvelopeKeys = [
246
279
  const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
247
280
  export class Midy {
248
281
  constructor(audioContext, options = this.defaultOptions) {
249
- Object.defineProperty(this, "ticksPerBeat", {
282
+ Object.defineProperty(this, "mode", {
250
283
  enumerable: true,
251
284
  configurable: true,
252
285
  writable: true,
253
- value: 120
254
- });
255
- Object.defineProperty(this, "totalTime", {
256
- enumerable: true,
257
- configurable: true,
258
- writable: true,
259
- value: 0
286
+ value: "GM2"
260
287
  });
261
288
  Object.defineProperty(this, "masterFineTuning", {
262
289
  enumerable: true,
@@ -291,6 +318,24 @@ export class Midy {
291
318
  delayTimes: this.generateDistributedArray(0.02, 2, 0.5),
292
319
  }
293
320
  });
321
+ Object.defineProperty(this, "numChannels", {
322
+ enumerable: true,
323
+ configurable: true,
324
+ writable: true,
325
+ value: 16
326
+ });
327
+ Object.defineProperty(this, "ticksPerBeat", {
328
+ enumerable: true,
329
+ configurable: true,
330
+ writable: true,
331
+ value: 120
332
+ });
333
+ Object.defineProperty(this, "totalTime", {
334
+ enumerable: true,
335
+ configurable: true,
336
+ writable: true,
337
+ value: 0
338
+ });
294
339
  Object.defineProperty(this, "noteCheckInterval", {
295
340
  enumerable: true,
296
341
  configurable: true,
@@ -393,11 +438,17 @@ export class Midy {
393
438
  writable: true,
394
439
  value: []
395
440
  });
396
- Object.defineProperty(this, "exclusiveClassMap", {
441
+ Object.defineProperty(this, "exclusiveClassNotes", {
442
+ enumerable: true,
443
+ configurable: true,
444
+ writable: true,
445
+ value: new Array(128)
446
+ });
447
+ Object.defineProperty(this, "drumExclusiveClassNotes", {
397
448
  enumerable: true,
398
449
  configurable: true,
399
450
  writable: true,
400
- value: new SparseMap(128)
451
+ value: new Array(this.numChannels * drumExclusiveClassCount)
401
452
  });
402
453
  Object.defineProperty(this, "defaultOptions", {
403
454
  enumerable: true,
@@ -424,6 +475,11 @@ export class Midy {
424
475
  this.audioContext = audioContext;
425
476
  this.options = { ...this.defaultOptions, ...options };
426
477
  this.masterVolume = new GainNode(audioContext);
478
+ this.scheduler = new GainNode(audioContext, { gain: 0 });
479
+ this.schedulerBuffer = new AudioBuffer({
480
+ length: 1,
481
+ sampleRate: audioContext.sampleRate,
482
+ });
427
483
  this.voiceParamsHandlers = this.createVoiceParamsHandlers();
428
484
  this.controlChangeHandlers = this.createControlChangeHandlers();
429
485
  this.channels = this.createChannels(audioContext);
@@ -432,6 +488,7 @@ export class Midy {
432
488
  this.chorusEffect.output.connect(this.masterVolume);
433
489
  this.reverbEffect.output.connect(this.masterVolume);
434
490
  this.masterVolume.connect(audioContext.destination);
491
+ this.scheduler.connect(audioContext.destination);
435
492
  this.GM2SystemOn();
436
493
  }
437
494
  initSoundFontTable() {
@@ -485,8 +542,10 @@ export class Midy {
485
542
  };
486
543
  }
487
544
  createChannels(audioContext) {
488
- const channels = Array.from({ length: 16 }, () => {
545
+ const channels = Array.from({ length: this.numChannels }, () => {
489
546
  return {
547
+ currentBufferSource: null,
548
+ isDrum: false,
490
549
  ...this.constructor.channelSettings,
491
550
  state: new ControllerState(),
492
551
  controlTable: this.initControlTable(),
@@ -531,7 +590,7 @@ export class Midy {
531
590
  return audioBuffer;
532
591
  }
533
592
  }
534
- createNoteBufferNode(audioBuffer, voiceParams) {
593
+ createBufferSource(voiceParams, audioBuffer) {
535
594
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
536
595
  bufferSource.buffer = audioBuffer;
537
596
  bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
@@ -625,7 +684,8 @@ export class Midy {
625
684
  if (queueIndex >= this.timeline.length) {
626
685
  await Promise.all(this.notePromises);
627
686
  this.notePromises = [];
628
- this.exclusiveClassMap.clear();
687
+ this.exclusiveClassNotes.fill(undefined);
688
+ this.drumExclusiveClassNotes.fill(undefined);
629
689
  this.audioBufferCache.clear();
630
690
  resolve();
631
691
  return;
@@ -644,7 +704,8 @@ export class Midy {
644
704
  else if (this.isStopping) {
645
705
  await this.stopNotes(0, true, now);
646
706
  this.notePromises = [];
647
- this.exclusiveClassMap.clear();
707
+ this.exclusiveClassNotes.fill(undefined);
708
+ this.drumExclusiveClassNotes.fill(undefined);
648
709
  this.audioBufferCache.clear();
649
710
  resolve();
650
711
  this.isStopping = false;
@@ -653,7 +714,8 @@ export class Midy {
653
714
  }
654
715
  else if (this.isSeeking) {
655
716
  this.stopNotes(0, true, now);
656
- this.exclusiveClassMap.clear();
717
+ this.exclusiveClassNotes.fill(undefined);
718
+ this.drumExclusiveClassNotes.fill(undefined);
657
719
  this.startTime = this.audioContext.currentTime;
658
720
  queueIndex = this.getQueueIndex(this.resumeTime);
659
721
  offset = this.resumeTime - this.startTime;
@@ -681,7 +743,7 @@ export class Midy {
681
743
  extractMidiData(midi) {
682
744
  const instruments = new Set();
683
745
  const timeline = [];
684
- const tmpChannels = new Array(16);
746
+ const tmpChannels = new Array(this.channels.length);
685
747
  for (let i = 0; i < tmpChannels.length; i++) {
686
748
  tmpChannels[i] = {
687
749
  programNumber: -1,
@@ -809,6 +871,9 @@ export class Midy {
809
871
  if (!this.isPlaying)
810
872
  return;
811
873
  this.isStopping = true;
874
+ for (let i = 0; i < this.channels.length; i++) {
875
+ this.resetAllStates(i);
876
+ }
812
877
  }
813
878
  pause() {
814
879
  if (!this.isPlaying || this.isPaused)
@@ -1014,7 +1079,9 @@ export class Midy {
1014
1079
  return 8.176 * this.centToRate(cent);
1015
1080
  }
1016
1081
  calcChannelDetune(channel) {
1017
- const masterTuning = this.masterCoarseTuning + this.masterFineTuning;
1082
+ const masterTuning = channel.isDrum
1083
+ ? 0
1084
+ : this.masterCoarseTuning + this.masterFineTuning;
1018
1085
  const channelTuning = channel.coarseTuning + channel.fineTuning;
1019
1086
  const tuning = masterTuning + channelTuning;
1020
1087
  const pitchWheel = channel.state.pitchWheel * 2 - 1;
@@ -1041,9 +1108,8 @@ export class Midy {
1041
1108
  .setValueAtTime(detune, scheduleTime);
1042
1109
  }
1043
1110
  getPortamentoTime(channel) {
1044
- const factor = 5 * Math.log(10) / 127;
1045
- const time = channel.state.portamentoTime;
1046
- return Math.log(time) / factor;
1111
+ const factor = 5 * Math.log(10) * 127;
1112
+ return channel.state.portamentoTime * factor;
1047
1113
  }
1048
1114
  setPortamentoStartVolumeEnvelope(channel, note, scheduleTime) {
1049
1115
  const { voiceParams, startTime } = note;
@@ -1182,8 +1248,8 @@ export class Midy {
1182
1248
  note.vibratoLFO.connect(note.vibratoDepth);
1183
1249
  note.vibratoDepth.connect(note.bufferSource.detune);
1184
1250
  }
1185
- async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1186
- const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1251
+ async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1252
+ const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1187
1253
  const cache = this.audioBufferCache.get(audioBufferId);
1188
1254
  if (cache) {
1189
1255
  cache.counter += 1;
@@ -1206,8 +1272,8 @@ export class Midy {
1206
1272
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1207
1273
  const voiceParams = voice.getAllParams(controllerState);
1208
1274
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1209
- const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1210
- note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
1275
+ const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1276
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1211
1277
  note.volumeNode = new GainNode(this.audioContext);
1212
1278
  note.gainL = new GainNode(this.audioContext);
1213
1279
  note.gainR = new GainNode(this.audioContext);
@@ -1251,23 +1317,72 @@ export class Midy {
1251
1317
  note.bufferSource.start(startTime);
1252
1318
  return note;
1253
1319
  }
1254
- calcBank(channel, channelNumber) {
1255
- if (channel.bankMSB === 121) {
1256
- return 0;
1320
+ calcBank(channel) {
1321
+ switch (this.mode) {
1322
+ case "GM1":
1323
+ if (channel.isDrum)
1324
+ return 128;
1325
+ return 0;
1326
+ case "GM2":
1327
+ if (channel.bankMSB === 121)
1328
+ return 0;
1329
+ if (channel.isDrum)
1330
+ return 128;
1331
+ return channel.bank;
1332
+ default:
1333
+ return channel.bank;
1257
1334
  }
1258
- if (channelNumber % 9 <= 1 && channel.bankMSB === 120) {
1259
- return 128;
1335
+ }
1336
+ handleExclusiveClass(note, channelNumber, startTime) {
1337
+ const exclusiveClass = note.voiceParams.exclusiveClass;
1338
+ if (exclusiveClass === 0)
1339
+ return;
1340
+ const prev = this.exclusiveClassNotes[exclusiveClass];
1341
+ if (prev) {
1342
+ const [prevNote, prevChannelNumber] = prev;
1343
+ if (prevNote && !prevNote.ending) {
1344
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1345
+ startTime, true, // force
1346
+ undefined);
1347
+ }
1260
1348
  }
1261
- return channel.bank;
1349
+ this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
1350
+ }
1351
+ handleDrumExclusiveClass(note, channelNumber, startTime) {
1352
+ const channel = this.channels[channelNumber];
1353
+ if (!channel.isDrum)
1354
+ return;
1355
+ const kitTable = drumExclusiveClassesByKit[channel.programNumber];
1356
+ if (!kitTable)
1357
+ return;
1358
+ const drumExclusiveClass = kitTable[note.noteNumber];
1359
+ if (drumExclusiveClass === 0)
1360
+ return;
1361
+ const index = (drumExclusiveClass - 1) * this.channels.length +
1362
+ channelNumber;
1363
+ const prevNote = this.drumExclusiveClassNotes[index];
1364
+ if (prevNote && !prevNote.ending) {
1365
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1366
+ startTime, true, // force
1367
+ undefined);
1368
+ }
1369
+ this.drumExclusiveClassNotes[index] = note;
1370
+ }
1371
+ isDrumNoteOffException(channel, noteNumber) {
1372
+ if (!channel.isDrum)
1373
+ return false;
1374
+ const programNumber = channel.programNumber;
1375
+ return (programNumber === 48 && noteNumber === 88) ||
1376
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84);
1262
1377
  }
1263
1378
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1264
1379
  const channel = this.channels[channelNumber];
1265
1380
  const bankNumber = this.calcBank(channel, channelNumber);
1266
- const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
1381
+ const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1267
1382
  if (soundFontIndex === undefined)
1268
1383
  return;
1269
1384
  const soundFont = this.soundFonts[soundFontIndex];
1270
- const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1385
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1271
1386
  if (!voice)
1272
1387
  return;
1273
1388
  const isSF3 = soundFont.parsed.info.version.major === 3;
@@ -1277,31 +1392,58 @@ export class Midy {
1277
1392
  if (0.5 <= channel.state.sustainPedal) {
1278
1393
  channel.sustainNotes.push(note);
1279
1394
  }
1280
- const exclusiveClass = note.voiceParams.exclusiveClass;
1281
- if (exclusiveClass !== 0) {
1282
- if (this.exclusiveClassMap.has(exclusiveClass)) {
1283
- const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1284
- const [prevNote, prevChannelNumber] = prevEntry;
1285
- if (!prevNote.ending) {
1286
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1287
- startTime, true, // force
1288
- undefined);
1289
- }
1290
- }
1291
- this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1292
- }
1395
+ this.handleExclusiveClass(note, channelNumber, startTime);
1396
+ this.handleDrumExclusiveClass(note, channelNumber, startTime);
1293
1397
  const scheduledNotes = channel.scheduledNotes;
1294
- if (scheduledNotes.has(noteNumber)) {
1295
- scheduledNotes.get(noteNumber).push(note);
1398
+ let notes = scheduledNotes.get(noteNumber);
1399
+ if (notes) {
1400
+ notes.push(note);
1296
1401
  }
1297
1402
  else {
1298
- scheduledNotes.set(noteNumber, [note]);
1403
+ notes = [note];
1404
+ scheduledNotes.set(noteNumber, notes);
1405
+ }
1406
+ if (this.isDrumNoteOffException(channel, noteNumber)) {
1407
+ const stopTime = startTime + note.bufferSource.buffer.duration;
1408
+ const index = notes.length - 1;
1409
+ const promise = new Promise((resolve) => {
1410
+ note.bufferSource.onended = () => {
1411
+ this.disconnectNote(note, scheduledNotes, index);
1412
+ resolve();
1413
+ };
1414
+ note.bufferSource.stop(stopTime);
1415
+ });
1416
+ this.notePromises.push(promise);
1299
1417
  }
1300
1418
  }
1301
1419
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1302
1420
  scheduleTime ??= this.audioContext.currentTime;
1303
1421
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1304
1422
  }
1423
+ disconnectNote(note, scheduledNotes, index) {
1424
+ scheduledNotes[index] = null;
1425
+ note.bufferSource.disconnect();
1426
+ note.filterNode.disconnect();
1427
+ note.volumeEnvelopeNode.disconnect();
1428
+ note.volumeNode.disconnect();
1429
+ note.gainL.disconnect();
1430
+ note.gainR.disconnect();
1431
+ if (note.modulationDepth) {
1432
+ note.volumeDepth.disconnect();
1433
+ note.modulationDepth.disconnect();
1434
+ note.modulationLFO.stop();
1435
+ }
1436
+ if (note.vibratoDepth) {
1437
+ note.vibratoDepth.disconnect();
1438
+ note.vibratoLFO.stop();
1439
+ }
1440
+ if (note.reverbEffectsSend) {
1441
+ note.reverbEffectsSend.disconnect();
1442
+ }
1443
+ if (note.chorusEffectsSend) {
1444
+ note.chorusEffectsSend.disconnect();
1445
+ }
1446
+ }
1305
1447
  stopNote(endTime, stopTime, scheduledNotes, index) {
1306
1448
  const note = scheduledNotes[index];
1307
1449
  note.volumeEnvelopeNode.gain
@@ -1313,28 +1455,7 @@ export class Midy {
1313
1455
  }, stopTime);
1314
1456
  return new Promise((resolve) => {
1315
1457
  note.bufferSource.onended = () => {
1316
- scheduledNotes[index] = null;
1317
- note.bufferSource.disconnect();
1318
- note.filterNode.disconnect();
1319
- note.volumeEnvelopeNode.disconnect();
1320
- note.volumeNode.disconnect();
1321
- note.gainL.disconnect();
1322
- note.gainR.disconnect();
1323
- if (note.modulationDepth) {
1324
- note.volumeDepth.disconnect();
1325
- note.modulationDepth.disconnect();
1326
- note.modulationLFO.stop();
1327
- }
1328
- if (note.vibratoDepth) {
1329
- note.vibratoDepth.disconnect();
1330
- note.vibratoLFO.stop();
1331
- }
1332
- if (note.reverbEffectsSend) {
1333
- note.reverbEffectsSend.disconnect();
1334
- }
1335
- if (note.chorusEffectsSend) {
1336
- note.chorusEffectsSend.disconnect();
1337
- }
1458
+ this.disconnectNote(note, scheduledNotes, index);
1338
1459
  resolve();
1339
1460
  };
1340
1461
  note.bufferSource.stop(stopTime);
@@ -1342,6 +1463,8 @@ export class Midy {
1342
1463
  }
1343
1464
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1344
1465
  const channel = this.channels[channelNumber];
1466
+ if (this.isDrumNoteOffException(channel, noteNumber))
1467
+ return;
1345
1468
  const state = channel.state;
1346
1469
  if (!force) {
1347
1470
  if (0.5 <= state.sustainPedal)
@@ -1441,13 +1564,25 @@ export class Midy {
1441
1564
  }
1442
1565
  // this.applyVoiceParams(channel, 10);
1443
1566
  }
1444
- handleProgramChange(channelNumber, program, _scheduleTime) {
1567
+ handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1445
1568
  const channel = this.channels[channelNumber];
1446
1569
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1447
- channel.program = program;
1570
+ channel.programNumber = programNumber;
1571
+ if (this.mode === "GM2") {
1572
+ switch (channel.bankMSB) {
1573
+ case 120:
1574
+ channel.isDrum = true;
1575
+ break;
1576
+ case 121:
1577
+ channel.isDrum = false;
1578
+ break;
1579
+ }
1580
+ }
1448
1581
  }
1449
1582
  handleChannelPressure(channelNumber, value, scheduleTime) {
1450
1583
  const channel = this.channels[channelNumber];
1584
+ if (channel.isDrum)
1585
+ return;
1451
1586
  const prev = channel.state.channelPressure;
1452
1587
  const next = value / 127;
1453
1588
  channel.state.channelPressure = next;
@@ -1466,8 +1601,10 @@ export class Midy {
1466
1601
  this.setPitchBend(channelNumber, pitchBend, scheduleTime);
1467
1602
  }
1468
1603
  setPitchBend(channelNumber, value, scheduleTime) {
1469
- scheduleTime ??= this.audioContext.currentTime;
1470
1604
  const channel = this.channels[channelNumber];
1605
+ if (channel.isDrum)
1606
+ return;
1607
+ scheduleTime ??= this.audioContext.currentTime;
1471
1608
  const state = channel.state;
1472
1609
  const prev = state.pitchWheel * 2 - 1;
1473
1610
  const next = (value - 8192) / 8192;
@@ -1749,15 +1886,16 @@ export class Midy {
1749
1886
  });
1750
1887
  }
1751
1888
  setModulationDepth(channelNumber, modulation, scheduleTime) {
1752
- scheduleTime ??= this.audioContext.currentTime;
1753
1889
  const channel = this.channels[channelNumber];
1890
+ if (channel.isDrum)
1891
+ return;
1892
+ scheduleTime ??= this.audioContext.currentTime;
1754
1893
  channel.state.modulationDepth = modulation / 127;
1755
1894
  this.updateModulation(channel, scheduleTime);
1756
1895
  }
1757
1896
  setPortamentoTime(channelNumber, portamentoTime) {
1758
1897
  const channel = this.channels[channelNumber];
1759
- const factor = 5 * Math.log(10) / 127;
1760
- channel.state.portamentoTime = Math.exp(factor * portamentoTime);
1898
+ channel.state.portamentoTime = portamentoTime / 127;
1761
1899
  }
1762
1900
  setKeyBasedVolume(channel, scheduleTime) {
1763
1901
  this.processScheduledNotes(channel, (note) => {
@@ -1829,8 +1967,10 @@ export class Midy {
1829
1967
  .setValueAtTime(volume * gainRight, scheduleTime);
1830
1968
  }
1831
1969
  setSustainPedal(channelNumber, value, scheduleTime) {
1832
- scheduleTime ??= this.audioContext.currentTime;
1833
1970
  const channel = this.channels[channelNumber];
1971
+ if (channel.isDrum)
1972
+ return;
1973
+ scheduleTime ??= this.audioContext.currentTime;
1834
1974
  channel.state.sustainPedal = value / 127;
1835
1975
  if (64 <= value) {
1836
1976
  this.processScheduledNotes(channel, (note) => {
@@ -1842,11 +1982,16 @@ export class Midy {
1842
1982
  }
1843
1983
  }
1844
1984
  setPortamento(channelNumber, value) {
1845
- this.channels[channelNumber].state.portamento = value / 127;
1985
+ const channel = this.channels[channelNumber];
1986
+ if (channel.isDrum)
1987
+ return;
1988
+ channel.state.portamento = value / 127;
1846
1989
  }
1847
1990
  setSostenutoPedal(channelNumber, value, scheduleTime) {
1848
- scheduleTime ??= this.audioContext.currentTime;
1849
1991
  const channel = this.channels[channelNumber];
1992
+ if (channel.isDrum)
1993
+ return;
1994
+ scheduleTime ??= this.audioContext.currentTime;
1850
1995
  channel.state.sostenutoPedal = value / 127;
1851
1996
  if (64 <= value) {
1852
1997
  channel.sostenutoNotes = this.getActiveNotes(channel, scheduleTime);
@@ -1855,13 +2000,28 @@ export class Midy {
1855
2000
  this.releaseSostenutoPedal(channelNumber, value, scheduleTime);
1856
2001
  }
1857
2002
  }
1858
- setSoftPedal(channelNumber, softPedal, _scheduleTime) {
2003
+ setSoftPedal(channelNumber, softPedal, scheduleTime) {
1859
2004
  const channel = this.channels[channelNumber];
2005
+ if (channel.isDrum)
2006
+ return;
2007
+ scheduleTime ??= this.audioContext.currentTime;
1860
2008
  channel.state.softPedal = softPedal / 127;
2009
+ this.processScheduledNotes(channel, (note) => {
2010
+ if (note.portamento) {
2011
+ this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
2012
+ this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
2013
+ }
2014
+ else {
2015
+ this.setVolumeEnvelope(channel, note, scheduleTime);
2016
+ this.setFilterEnvelope(channel, note, scheduleTime);
2017
+ }
2018
+ });
1861
2019
  }
1862
2020
  setFilterResonance(channelNumber, filterResonance, scheduleTime) {
1863
- scheduleTime ??= this.audioContext.currentTime;
1864
2021
  const channel = this.channels[channelNumber];
2022
+ if (channel.isDrum)
2023
+ return;
2024
+ scheduleTime ??= this.audioContext.currentTime;
1865
2025
  const state = channel.state;
1866
2026
  state.filterResonance = filterResonance / 64;
1867
2027
  this.processScheduledNotes(channel, (note) => {
@@ -1870,13 +2030,17 @@ export class Midy {
1870
2030
  });
1871
2031
  }
1872
2032
  setReleaseTime(channelNumber, releaseTime, _scheduleTime) {
1873
- scheduleTime ??= this.audioContext.currentTime;
1874
2033
  const channel = this.channels[channelNumber];
2034
+ if (channel.isDrum)
2035
+ return;
2036
+ scheduleTime ??= this.audioContext.currentTime;
1875
2037
  channel.state.releaseTime = releaseTime / 64;
1876
2038
  }
1877
2039
  setAttackTime(channelNumber, attackTime, scheduleTime) {
1878
- scheduleTime ??= this.audioContext.currentTime;
1879
2040
  const channel = this.channels[channelNumber];
2041
+ if (channel.isDrum)
2042
+ return;
2043
+ scheduleTime ??= this.audioContext.currentTime;
1880
2044
  channel.state.attackTime = attackTime / 64;
1881
2045
  this.processScheduledNotes(channel, (note) => {
1882
2046
  if (note.startTime < scheduleTime)
@@ -1885,8 +2049,10 @@ export class Midy {
1885
2049
  });
1886
2050
  }
1887
2051
  setBrightness(channelNumber, brightness, scheduleTime) {
1888
- scheduleTime ??= this.audioContext.currentTime;
1889
2052
  const channel = this.channels[channelNumber];
2053
+ if (channel.isDrum)
2054
+ return;
2055
+ scheduleTime ??= this.audioContext.currentTime;
1890
2056
  channel.state.brightness = brightness / 64;
1891
2057
  this.processScheduledNotes(channel, (note) => {
1892
2058
  if (note.portamento) {
@@ -1898,16 +2064,20 @@ export class Midy {
1898
2064
  });
1899
2065
  }
1900
2066
  setDecayTime(channelNumber, dacayTime, scheduleTime) {
1901
- scheduleTime ??= this.audioContext.currentTime;
1902
2067
  const channel = this.channels[channelNumber];
2068
+ if (channel.isDrum)
2069
+ return;
2070
+ scheduleTime ??= this.audioContext.currentTime;
1903
2071
  channel.state.decayTime = dacayTime / 64;
1904
2072
  this.processScheduledNotes(channel, (note) => {
1905
2073
  this.setVolumeEnvelope(channel, note, scheduleTime);
1906
2074
  });
1907
2075
  }
1908
2076
  setVibratoRate(channelNumber, vibratoRate, scheduleTime) {
1909
- scheduleTime ??= this.audioContext.currentTime;
1910
2077
  const channel = this.channels[channelNumber];
2078
+ if (channel.isDrum)
2079
+ return;
2080
+ scheduleTime ??= this.audioContext.currentTime;
1911
2081
  channel.state.vibratoRate = vibratoRate / 64;
1912
2082
  if (channel.vibratoDepth <= 0)
1913
2083
  return;
@@ -1916,8 +2086,10 @@ export class Midy {
1916
2086
  });
1917
2087
  }
1918
2088
  setVibratoDepth(channelNumber, vibratoDepth, scheduleTime) {
1919
- scheduleTime ??= this.audioContext.currentTime;
1920
2089
  const channel = this.channels[channelNumber];
2090
+ if (channel.isDrum)
2091
+ return;
2092
+ scheduleTime ??= this.audioContext.currentTime;
1921
2093
  const prev = channel.state.vibratoDepth;
1922
2094
  channel.state.vibratoDepth = vibratoDepth / 64;
1923
2095
  if (0 < prev) {
@@ -1932,8 +2104,10 @@ export class Midy {
1932
2104
  }
1933
2105
  }
1934
2106
  setVibratoDelay(channelNumber, vibratoDelay) {
1935
- scheduleTime ??= this.audioContext.currentTime;
1936
2107
  const channel = this.channels[channelNumber];
2108
+ if (channel.isDrum)
2109
+ return;
2110
+ scheduleTime ??= this.audioContext.currentTime;
1937
2111
  channel.state.vibratoDelay = vibratoDelay / 64;
1938
2112
  if (0 < channel.state.vibratoDepth) {
1939
2113
  this.processScheduledNotes(channel, (note) => {
@@ -2080,8 +2254,10 @@ export class Midy {
2080
2254
  this.setPitchBendRange(channelNumber, pitchBendRange, scheduleTime);
2081
2255
  }
2082
2256
  setPitchBendRange(channelNumber, value, scheduleTime) {
2083
- scheduleTime ??= this.audioContext.currentTime;
2084
2257
  const channel = this.channels[channelNumber];
2258
+ if (channel.isDrum)
2259
+ return;
2260
+ scheduleTime ??= this.audioContext.currentTime;
2085
2261
  const state = channel.state;
2086
2262
  const prev = state.pitchWheelSensitivity;
2087
2263
  const next = value / 128;
@@ -2097,8 +2273,10 @@ export class Midy {
2097
2273
  this.setFineTuning(channelNumber, fineTuning, scheduleTime);
2098
2274
  }
2099
2275
  setFineTuning(channelNumber, value, scheduleTime) {
2100
- scheduleTime ??= this.audioContext.currentTime;
2101
2276
  const channel = this.channels[channelNumber];
2277
+ if (channel.isDrum)
2278
+ return;
2279
+ scheduleTime ??= this.audioContext.currentTime;
2102
2280
  const prev = channel.fineTuning;
2103
2281
  const next = (value - 8192) / 8.192; // cent
2104
2282
  channel.fineTuning = next;
@@ -2112,8 +2290,10 @@ export class Midy {
2112
2290
  this.setCoarseTuning(channelNumber, coarseTuning, scheduleTime);
2113
2291
  }
2114
2292
  setCoarseTuning(channelNumber, value, scheduleTime) {
2115
- scheduleTime ??= this.audioContext.currentTime;
2116
2293
  const channel = this.channels[channelNumber];
2294
+ if (channel.isDrum)
2295
+ return;
2296
+ scheduleTime ??= this.audioContext.currentTime;
2117
2297
  const prev = channel.coarseTuning;
2118
2298
  const next = (value - 64) * 100; // cent
2119
2299
  channel.coarseTuning = next;
@@ -2127,8 +2307,10 @@ export class Midy {
2127
2307
  this.setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime);
2128
2308
  }
2129
2309
  setModulationDepthRange(channelNumber, modulationDepthRange, scheduleTime) {
2130
- scheduleTime ??= this.audioContext.currentTime;
2131
2310
  const channel = this.channels[channelNumber];
2311
+ if (channel.isDrum)
2312
+ return;
2313
+ scheduleTime ??= this.audioContext.currentTime;
2132
2314
  channel.modulationDepthRange = modulationDepthRange;
2133
2315
  this.updateModulation(channel, scheduleTime);
2134
2316
  }
@@ -2136,16 +2318,31 @@ export class Midy {
2136
2318
  scheduleTime ??= this.audioContext.currentTime;
2137
2319
  return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2138
2320
  }
2321
+ resetAllStates(channelNumber) {
2322
+ const channel = this.channels[channelNumber];
2323
+ const state = channel.state;
2324
+ for (const type of Object.keys(defaultControllerState)) {
2325
+ state[type] = defaultControllerState[type].defaultValue;
2326
+ }
2327
+ for (const type of Object.keys(this.constructor.channelSettings)) {
2328
+ channel[type] = this.constructor.channelSettings[type];
2329
+ }
2330
+ this.mode = "GM2";
2331
+ this.masterFineTuning = 0; // cb
2332
+ this.masterCoarseTuning = 0; // cb
2333
+ }
2334
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2139
2335
  resetAllControllers(channelNumber) {
2140
2336
  const stateTypes = [
2337
+ "polyphonicKeyPressure",
2338
+ "channelPressure",
2339
+ "pitchWheel",
2141
2340
  "expression",
2142
2341
  "modulationDepth",
2143
2342
  "sustainPedal",
2144
2343
  "portamento",
2145
2344
  "sostenutoPedal",
2146
2345
  "softPedal",
2147
- "channelPressure",
2148
- "pitchWheelSensitivity",
2149
2346
  ];
2150
2347
  const channel = this.channels[channelNumber];
2151
2348
  const state = channel.state;
@@ -2199,12 +2396,12 @@ export class Midy {
2199
2396
  case 9:
2200
2397
  switch (data[3]) {
2201
2398
  case 1:
2202
- this.GM1SystemOn();
2399
+ this.GM1SystemOn(scheduleTime);
2203
2400
  break;
2204
2401
  case 2: // GM System Off
2205
2402
  break;
2206
2403
  case 3:
2207
- this.GM2SystemOn();
2404
+ this.GM2SystemOn(scheduleTime);
2208
2405
  break;
2209
2406
  default:
2210
2407
  console.warn(`Unsupported Exclusive Message: ${data}`);
@@ -2214,25 +2411,35 @@ export class Midy {
2214
2411
  console.warn(`Unsupported Exclusive Message: ${data}`);
2215
2412
  }
2216
2413
  }
2217
- GM1SystemOn() {
2414
+ GM1SystemOn(scheduleTime) {
2415
+ scheduleTime ??= this.audioContext.currentTime;
2416
+ this.mode = "GM1";
2218
2417
  for (let i = 0; i < this.channels.length; i++) {
2418
+ this.allSoundOff(i, 0, scheduleTime);
2219
2419
  const channel = this.channels[i];
2220
2420
  channel.bankMSB = 0;
2221
2421
  channel.bankLSB = 0;
2222
2422
  channel.bank = 0;
2423
+ channel.isDrum = false;
2223
2424
  }
2224
2425
  this.channels[9].bankMSB = 1;
2225
2426
  this.channels[9].bank = 128;
2427
+ this.channels[9].isDrum = true;
2226
2428
  }
2227
- GM2SystemOn() {
2429
+ GM2SystemOn(scheduleTime) {
2430
+ scheduleTime ??= this.audioContext.currentTime;
2431
+ this.mode = "GM2";
2228
2432
  for (let i = 0; i < this.channels.length; i++) {
2433
+ this.allSoundOff(i, 0, scheduleTime);
2229
2434
  const channel = this.channels[i];
2230
2435
  channel.bankMSB = 121;
2231
2436
  channel.bankLSB = 0;
2232
2437
  channel.bank = 121 * 128;
2438
+ channel.isDrum = false;
2233
2439
  }
2234
2440
  this.channels[9].bankMSB = 120;
2235
2441
  this.channels[9].bank = 120 * 128;
2442
+ this.channels[9].isDrum = true;
2236
2443
  }
2237
2444
  handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
2238
2445
  switch (data[2]) {
@@ -2308,8 +2515,14 @@ export class Midy {
2308
2515
  const prev = this.masterFineTuning;
2309
2516
  const next = (value - 8192) / 8.192; // cent
2310
2517
  this.masterFineTuning = next;
2311
- channel.detune += next - prev;
2312
- this.updateChannelDetune(channel, scheduleTime);
2518
+ const detuneChange = next - prev;
2519
+ for (let i = 0; i < this.channels.length; i++) {
2520
+ const channel = this.channels[i];
2521
+ if (channel.isDrum)
2522
+ continue;
2523
+ channel.detune += detuneChange;
2524
+ this.updateChannelDetune(channel, scheduleTime);
2525
+ }
2313
2526
  }
2314
2527
  handleMasterCoarseTuningSysEx(data, scheduleTime) {
2315
2528
  const coarseTuning = data[4];
@@ -2319,8 +2532,14 @@ export class Midy {
2319
2532
  const prev = this.masterCoarseTuning;
2320
2533
  const next = (value - 64) * 100; // cent
2321
2534
  this.masterCoarseTuning = next;
2322
- channel.detune += next - prev;
2323
- this.updateChannelDetune(channel, scheduleTime);
2535
+ const detuneChange = next - prev;
2536
+ for (let i = 0; i < this.channels.length; i++) {
2537
+ const channel = this.channels[i];
2538
+ if (channel.isDrum)
2539
+ continue;
2540
+ channel.detune += detuneChange;
2541
+ this.updateChannelDetune(channel, scheduleTime);
2542
+ }
2324
2543
  }
2325
2544
  handleGlobalParameterControlSysEx(data, scheduleTime) {
2326
2545
  if (data[7] === 1) {
@@ -2504,7 +2723,7 @@ export class Midy {
2504
2723
  return value * 0.00787;
2505
2724
  }
2506
2725
  getChannelBitmap(data) {
2507
- const bitmap = new Array(16).fill(false);
2726
+ const bitmap = new Array(this.channels.length).fill(false);
2508
2727
  const ff = data[4] & 0b11;
2509
2728
  const gg = data[5] & 0x7F;
2510
2729
  const hh = data[6] & 0x7F;
@@ -2532,6 +2751,8 @@ export class Midy {
2532
2751
  if (!channelBitmap[i])
2533
2752
  continue;
2534
2753
  const channel = this.channels[i];
2754
+ if (channel.isDrum)
2755
+ continue;
2535
2756
  for (let j = 0; j < 12; j++) {
2536
2757
  const centValue = data[j + 7] - 64;
2537
2758
  channel.scaleOctaveTuningTable[j] = centValue;
@@ -2550,6 +2771,8 @@ export class Midy {
2550
2771
  if (!channelBitmap[i])
2551
2772
  continue;
2552
2773
  const channel = this.channels[i];
2774
+ if (channel.isDrum)
2775
+ continue;
2553
2776
  for (let j = 0; j < 12; j++) {
2554
2777
  const index = 7 + j * 2;
2555
2778
  const msb = data[index] & 0x7F;
@@ -2620,7 +2843,10 @@ export class Midy {
2620
2843
  }
2621
2844
  handlePressureSysEx(data, tableName) {
2622
2845
  const channelNumber = data[4];
2623
- const table = this.channels[channelNumber][tableName];
2846
+ const channel = this.channels[channelNumber];
2847
+ if (channel.isDrum)
2848
+ return;
2849
+ const table = channel[tableName];
2624
2850
  for (let i = 5; i < data.length - 1; i += 2) {
2625
2851
  const pp = data[i];
2626
2852
  const rr = data[i + 1];
@@ -2648,8 +2874,11 @@ export class Midy {
2648
2874
  }
2649
2875
  handleControlChangeSysEx(data) {
2650
2876
  const channelNumber = data[4];
2877
+ const channel = this.channels[channelNumber];
2878
+ if (channel.isDrum)
2879
+ return;
2651
2880
  const controllerType = data[5];
2652
- const table = this.channels[channelNumber].controlTable[controllerType];
2881
+ const table = channel.controlTable[controllerType];
2653
2882
  for (let i = 6; i < data.length - 1; i += 2) {
2654
2883
  const pp = data[i];
2655
2884
  const rr = data[i + 1];
@@ -2663,8 +2892,11 @@ export class Midy {
2663
2892
  }
2664
2893
  handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
2665
2894
  const channelNumber = data[4];
2895
+ const channel = this.channels[channelNumber];
2896
+ if (channel.isDrum)
2897
+ return;
2666
2898
  const keyNumber = data[5];
2667
- const table = this.channels[channelNumber].keyBasedInstrumentControlTable;
2899
+ const table = channel.keyBasedInstrumentControlTable;
2668
2900
  for (let i = 6; i < data.length - 1; i += 2) {
2669
2901
  const controllerType = data[i];
2670
2902
  const value = data[i + 1];
@@ -2683,15 +2915,23 @@ export class Midy {
2683
2915
  console.warn(`Unsupported Exclusive Message: ${data}`);
2684
2916
  }
2685
2917
  }
2918
+ // https://github.com/marmooo/js-timer-benchmark
2686
2919
  scheduleTask(callback, scheduleTime) {
2687
2920
  return new Promise((resolve) => {
2688
- const bufferSource = new AudioBufferSourceNode(this.audioContext);
2921
+ const bufferSource = new AudioBufferSourceNode(this.audioContext, {
2922
+ buffer: this.schedulerBuffer,
2923
+ });
2924
+ bufferSource.connect(this.scheduler);
2689
2925
  bufferSource.onended = () => {
2690
- callback();
2691
- resolve();
2926
+ try {
2927
+ callback();
2928
+ }
2929
+ finally {
2930
+ bufferSource.disconnect();
2931
+ resolve();
2932
+ }
2692
2933
  };
2693
2934
  bufferSource.start(scheduleTime);
2694
- bufferSource.stop(scheduleTime);
2695
2935
  });
2696
2936
  }
2697
2937
  }
@@ -2700,9 +2940,8 @@ Object.defineProperty(Midy, "channelSettings", {
2700
2940
  configurable: true,
2701
2941
  writable: true,
2702
2942
  value: {
2703
- currentBufferSource: null,
2704
2943
  detune: 0,
2705
- program: 0,
2944
+ programNumber: 0,
2706
2945
  bank: 121 * 128,
2707
2946
  bankMSB: 121,
2708
2947
  bankLSB: 0,
@@ -2711,8 +2950,8 @@ Object.defineProperty(Midy, "channelSettings", {
2711
2950
  rpnMSB: 127,
2712
2951
  rpnLSB: 127,
2713
2952
  mono: false, // CC#124, CC#125
2953
+ modulationDepthRange: 50, // cent
2714
2954
  fineTuning: 0, // cb
2715
2955
  coarseTuning: 0, // cb
2716
- modulationDepthRange: 50, // cent
2717
2956
  }
2718
2957
  });