@marmooo/midy 0.2.9 → 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 },
@@ -255,18 +288,6 @@ class Midy {
255
288
  writable: true,
256
289
  value: "GM2"
257
290
  });
258
- Object.defineProperty(this, "ticksPerBeat", {
259
- enumerable: true,
260
- configurable: true,
261
- writable: true,
262
- value: 120
263
- });
264
- Object.defineProperty(this, "totalTime", {
265
- enumerable: true,
266
- configurable: true,
267
- writable: true,
268
- value: 0
269
- });
270
291
  Object.defineProperty(this, "masterFineTuning", {
271
292
  enumerable: true,
272
293
  configurable: true,
@@ -300,6 +321,24 @@ class Midy {
300
321
  delayTimes: this.generateDistributedArray(0.02, 2, 0.5),
301
322
  }
302
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
+ });
303
342
  Object.defineProperty(this, "noteCheckInterval", {
304
343
  enumerable: true,
305
344
  configurable: true,
@@ -402,11 +441,17 @@ class Midy {
402
441
  writable: true,
403
442
  value: []
404
443
  });
405
- 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", {
406
451
  enumerable: true,
407
452
  configurable: true,
408
453
  writable: true,
409
- value: new SparseMap(128)
454
+ value: new Array(this.numChannels * drumExclusiveClassCount)
410
455
  });
411
456
  Object.defineProperty(this, "defaultOptions", {
412
457
  enumerable: true,
@@ -500,8 +545,10 @@ class Midy {
500
545
  };
501
546
  }
502
547
  createChannels(audioContext) {
503
- const channels = Array.from({ length: 16 }, () => {
548
+ const channels = Array.from({ length: this.numChannels }, () => {
504
549
  return {
550
+ currentBufferSource: null,
551
+ isDrum: false,
505
552
  ...this.constructor.channelSettings,
506
553
  state: new ControllerState(),
507
554
  controlTable: this.initControlTable(),
@@ -546,24 +593,10 @@ class Midy {
546
593
  return audioBuffer;
547
594
  }
548
595
  }
549
- calcLoopMode(channel, note, voiceParams) {
550
- if (channel.isDrum) {
551
- const noteNumber = note.noteNumber;
552
- if (noteNumber === 88 || 47 <= noteNumber && noteNumber <= 84) {
553
- return true;
554
- }
555
- else {
556
- return false;
557
- }
558
- }
559
- else {
560
- return voiceParams.sampleModes % 2 !== 0;
561
- }
562
- }
563
- createBufferSource(channel, note, voiceParams, audioBuffer) {
596
+ createBufferSource(voiceParams, audioBuffer) {
564
597
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
565
598
  bufferSource.buffer = audioBuffer;
566
- bufferSource.loop = this.calcLoopMode(channel, note, voiceParams);
599
+ bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
567
600
  if (bufferSource.loop) {
568
601
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
569
602
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -654,7 +687,8 @@ class Midy {
654
687
  if (queueIndex >= this.timeline.length) {
655
688
  await Promise.all(this.notePromises);
656
689
  this.notePromises = [];
657
- this.exclusiveClassMap.clear();
690
+ this.exclusiveClassNotes.fill(undefined);
691
+ this.drumExclusiveClassNotes.fill(undefined);
658
692
  this.audioBufferCache.clear();
659
693
  resolve();
660
694
  return;
@@ -673,7 +707,8 @@ class Midy {
673
707
  else if (this.isStopping) {
674
708
  await this.stopNotes(0, true, now);
675
709
  this.notePromises = [];
676
- this.exclusiveClassMap.clear();
710
+ this.exclusiveClassNotes.fill(undefined);
711
+ this.drumExclusiveClassNotes.fill(undefined);
677
712
  this.audioBufferCache.clear();
678
713
  resolve();
679
714
  this.isStopping = false;
@@ -682,7 +717,8 @@ class Midy {
682
717
  }
683
718
  else if (this.isSeeking) {
684
719
  this.stopNotes(0, true, now);
685
- this.exclusiveClassMap.clear();
720
+ this.exclusiveClassNotes.fill(undefined);
721
+ this.drumExclusiveClassNotes.fill(undefined);
686
722
  this.startTime = this.audioContext.currentTime;
687
723
  queueIndex = this.getQueueIndex(this.resumeTime);
688
724
  offset = this.resumeTime - this.startTime;
@@ -710,7 +746,7 @@ class Midy {
710
746
  extractMidiData(midi) {
711
747
  const instruments = new Set();
712
748
  const timeline = [];
713
- const tmpChannels = new Array(16);
749
+ const tmpChannels = new Array(this.channels.length);
714
750
  for (let i = 0; i < tmpChannels.length; i++) {
715
751
  tmpChannels[i] = {
716
752
  programNumber: -1,
@@ -838,6 +874,9 @@ class Midy {
838
874
  if (!this.isPlaying)
839
875
  return;
840
876
  this.isStopping = true;
877
+ for (let i = 0; i < this.channels.length; i++) {
878
+ this.resetAllStates(i);
879
+ }
841
880
  }
842
881
  pause() {
843
882
  if (!this.isPlaying || this.isPaused)
@@ -1212,8 +1251,8 @@ class Midy {
1212
1251
  note.vibratoLFO.connect(note.vibratoDepth);
1213
1252
  note.vibratoDepth.connect(note.bufferSource.detune);
1214
1253
  }
1215
- async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1216
- const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1254
+ async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1255
+ const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1217
1256
  const cache = this.audioBufferCache.get(audioBufferId);
1218
1257
  if (cache) {
1219
1258
  cache.counter += 1;
@@ -1236,8 +1275,8 @@ class Midy {
1236
1275
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1237
1276
  const voiceParams = voice.getAllParams(controllerState);
1238
1277
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1239
- const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1240
- note.bufferSource = this.createBufferSource(channel, note, voiceParams, audioBuffer);
1278
+ const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1279
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1241
1280
  note.volumeNode = new GainNode(this.audioContext);
1242
1281
  note.gainL = new GainNode(this.audioContext);
1243
1282
  note.gainR = new GainNode(this.audioContext);
@@ -1297,14 +1336,56 @@ class Midy {
1297
1336
  return channel.bank;
1298
1337
  }
1299
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
+ }
1351
+ }
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);
1380
+ }
1300
1381
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1301
1382
  const channel = this.channels[channelNumber];
1302
1383
  const bankNumber = this.calcBank(channel, channelNumber);
1303
- const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
1384
+ const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1304
1385
  if (soundFontIndex === undefined)
1305
1386
  return;
1306
1387
  const soundFont = this.soundFonts[soundFontIndex];
1307
- const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1388
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1308
1389
  if (!voice)
1309
1390
  return;
1310
1391
  const isSF3 = soundFont.parsed.info.version.major === 3;
@@ -1314,31 +1395,58 @@ class Midy {
1314
1395
  if (0.5 <= channel.state.sustainPedal) {
1315
1396
  channel.sustainNotes.push(note);
1316
1397
  }
1317
- const exclusiveClass = note.voiceParams.exclusiveClass;
1318
- if (exclusiveClass !== 0) {
1319
- if (this.exclusiveClassMap.has(exclusiveClass)) {
1320
- const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1321
- const [prevNote, prevChannelNumber] = prevEntry;
1322
- if (prevNote && !prevNote.ending) {
1323
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1324
- startTime, true, // force
1325
- undefined);
1326
- }
1327
- }
1328
- this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1329
- }
1398
+ this.handleExclusiveClass(note, channelNumber, startTime);
1399
+ this.handleDrumExclusiveClass(note, channelNumber, startTime);
1330
1400
  const scheduledNotes = channel.scheduledNotes;
1331
- if (scheduledNotes.has(noteNumber)) {
1332
- scheduledNotes.get(noteNumber).push(note);
1401
+ let notes = scheduledNotes.get(noteNumber);
1402
+ if (notes) {
1403
+ notes.push(note);
1333
1404
  }
1334
1405
  else {
1335
- 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);
1336
1420
  }
1337
1421
  }
1338
1422
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1339
1423
  scheduleTime ??= this.audioContext.currentTime;
1340
1424
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1341
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
+ }
1342
1450
  stopNote(endTime, stopTime, scheduledNotes, index) {
1343
1451
  const note = scheduledNotes[index];
1344
1452
  note.volumeEnvelopeNode.gain
@@ -1350,28 +1458,7 @@ class Midy {
1350
1458
  }, stopTime);
1351
1459
  return new Promise((resolve) => {
1352
1460
  note.bufferSource.onended = () => {
1353
- scheduledNotes[index] = null;
1354
- note.bufferSource.disconnect();
1355
- note.filterNode.disconnect();
1356
- note.volumeEnvelopeNode.disconnect();
1357
- note.volumeNode.disconnect();
1358
- note.gainL.disconnect();
1359
- note.gainR.disconnect();
1360
- if (note.modulationDepth) {
1361
- note.volumeDepth.disconnect();
1362
- note.modulationDepth.disconnect();
1363
- note.modulationLFO.stop();
1364
- }
1365
- if (note.vibratoDepth) {
1366
- note.vibratoDepth.disconnect();
1367
- note.vibratoLFO.stop();
1368
- }
1369
- if (note.reverbEffectsSend) {
1370
- note.reverbEffectsSend.disconnect();
1371
- }
1372
- if (note.chorusEffectsSend) {
1373
- note.chorusEffectsSend.disconnect();
1374
- }
1461
+ this.disconnectNote(note, scheduledNotes, index);
1375
1462
  resolve();
1376
1463
  };
1377
1464
  note.bufferSource.stop(stopTime);
@@ -1379,6 +1466,8 @@ class Midy {
1379
1466
  }
1380
1467
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1381
1468
  const channel = this.channels[channelNumber];
1469
+ if (this.isDrumNoteOffException(channel, noteNumber))
1470
+ return;
1382
1471
  const state = channel.state;
1383
1472
  if (!force) {
1384
1473
  if (0.5 <= state.sustainPedal)
@@ -1478,10 +1567,10 @@ class Midy {
1478
1567
  }
1479
1568
  // this.applyVoiceParams(channel, 10);
1480
1569
  }
1481
- handleProgramChange(channelNumber, program, _scheduleTime) {
1570
+ handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1482
1571
  const channel = this.channels[channelNumber];
1483
1572
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1484
- channel.program = program;
1573
+ channel.programNumber = programNumber;
1485
1574
  if (this.mode === "GM2") {
1486
1575
  switch (channel.bankMSB) {
1487
1576
  case 120:
@@ -2232,16 +2321,31 @@ class Midy {
2232
2321
  scheduleTime ??= this.audioContext.currentTime;
2233
2322
  return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2234
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
2235
2338
  resetAllControllers(channelNumber) {
2236
2339
  const stateTypes = [
2340
+ "polyphonicKeyPressure",
2341
+ "channelPressure",
2342
+ "pitchWheel",
2237
2343
  "expression",
2238
2344
  "modulationDepth",
2239
2345
  "sustainPedal",
2240
2346
  "portamento",
2241
2347
  "sostenutoPedal",
2242
2348
  "softPedal",
2243
- "channelPressure",
2244
- "pitchWheelSensitivity",
2245
2349
  ];
2246
2350
  const channel = this.channels[channelNumber];
2247
2351
  const state = channel.state;
@@ -2622,7 +2726,7 @@ class Midy {
2622
2726
  return value * 0.00787;
2623
2727
  }
2624
2728
  getChannelBitmap(data) {
2625
- const bitmap = new Array(16).fill(false);
2729
+ const bitmap = new Array(this.channels.length).fill(false);
2626
2730
  const ff = data[4] & 0b11;
2627
2731
  const gg = data[5] & 0x7F;
2628
2732
  const hh = data[6] & 0x7F;
@@ -2814,6 +2918,7 @@ class Midy {
2814
2918
  console.warn(`Unsupported Exclusive Message: ${data}`);
2815
2919
  }
2816
2920
  }
2921
+ // https://github.com/marmooo/js-timer-benchmark
2817
2922
  scheduleTask(callback, scheduleTime) {
2818
2923
  return new Promise((resolve) => {
2819
2924
  const bufferSource = new AudioBufferSourceNode(this.audioContext, {
@@ -2839,10 +2944,8 @@ Object.defineProperty(Midy, "channelSettings", {
2839
2944
  configurable: true,
2840
2945
  writable: true,
2841
2946
  value: {
2842
- currentBufferSource: null,
2843
- isDrum: false,
2844
2947
  detune: 0,
2845
- program: 0,
2948
+ programNumber: 0,
2846
2949
  bank: 121 * 128,
2847
2950
  bankMSB: 121,
2848
2951
  bankLSB: 0,