@marmooo/midy 0.2.9 → 0.3.1

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 },
@@ -248,18 +281,6 @@ class MidyGM2 {
248
281
  writable: true,
249
282
  value: "GM2"
250
283
  });
251
- Object.defineProperty(this, "ticksPerBeat", {
252
- enumerable: true,
253
- configurable: true,
254
- writable: true,
255
- value: 120
256
- });
257
- Object.defineProperty(this, "totalTime", {
258
- enumerable: true,
259
- configurable: true,
260
- writable: true,
261
- value: 0
262
- });
263
284
  Object.defineProperty(this, "masterFineTuning", {
264
285
  enumerable: true,
265
286
  configurable: true,
@@ -293,6 +314,24 @@ class MidyGM2 {
293
314
  delayTimes: this.generateDistributedArray(0.02, 2, 0.5),
294
315
  }
295
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
+ });
296
335
  Object.defineProperty(this, "noteCheckInterval", {
297
336
  enumerable: true,
298
337
  configurable: true,
@@ -395,11 +434,17 @@ class MidyGM2 {
395
434
  writable: true,
396
435
  value: []
397
436
  });
398
- Object.defineProperty(this, "exclusiveClassMap", {
437
+ Object.defineProperty(this, "exclusiveClassNotes", {
399
438
  enumerable: true,
400
439
  configurable: true,
401
440
  writable: true,
402
- 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)
403
448
  });
404
449
  Object.defineProperty(this, "defaultOptions", {
405
450
  enumerable: true,
@@ -493,8 +538,10 @@ class MidyGM2 {
493
538
  };
494
539
  }
495
540
  createChannels(audioContext) {
496
- const channels = Array.from({ length: 16 }, () => {
541
+ const channels = Array.from({ length: this.numChannels }, () => {
497
542
  return {
543
+ currentBufferSource: null,
544
+ isDrum: false,
498
545
  ...this.constructor.channelSettings,
499
546
  state: new ControllerState(),
500
547
  controlTable: this.initControlTable(),
@@ -538,24 +585,10 @@ class MidyGM2 {
538
585
  return audioBuffer;
539
586
  }
540
587
  }
541
- calcLoopMode(channel, note, voiceParams) {
542
- if (channel.isDrum) {
543
- const noteNumber = note.noteNumber;
544
- if (noteNumber === 88 || 47 <= noteNumber && noteNumber <= 84) {
545
- return true;
546
- }
547
- else {
548
- return false;
549
- }
550
- }
551
- else {
552
- return voiceParams.sampleModes % 2 !== 0;
553
- }
554
- }
555
- createBufferSource(channel, note, voiceParams, audioBuffer) {
588
+ createBufferSource(voiceParams, audioBuffer) {
556
589
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
557
590
  bufferSource.buffer = audioBuffer;
558
- bufferSource.loop = this.calcLoopMode(channel, note, voiceParams);
591
+ bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
559
592
  if (bufferSource.loop) {
560
593
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
561
594
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -588,7 +621,7 @@ class MidyGM2 {
588
621
  const startTime = event.startTime + this.startDelay - offset;
589
622
  switch (event.type) {
590
623
  case "noteOn":
591
- if (event.velocity !== 0) {
624
+ if (0 < event.velocity) {
592
625
  await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
593
626
  break;
594
627
  }
@@ -643,7 +676,8 @@ class MidyGM2 {
643
676
  if (queueIndex >= this.timeline.length) {
644
677
  await Promise.all(this.notePromises);
645
678
  this.notePromises = [];
646
- this.exclusiveClassMap.clear();
679
+ this.exclusiveClassNotes.fill(undefined);
680
+ this.drumExclusiveClassNotes.fill(undefined);
647
681
  this.audioBufferCache.clear();
648
682
  resolve();
649
683
  return;
@@ -662,7 +696,8 @@ class MidyGM2 {
662
696
  else if (this.isStopping) {
663
697
  await this.stopNotes(0, true, now);
664
698
  this.notePromises = [];
665
- this.exclusiveClassMap.clear();
699
+ this.exclusiveClassNotes.fill(undefined);
700
+ this.drumExclusiveClassNotes.fill(undefined);
666
701
  this.audioBufferCache.clear();
667
702
  resolve();
668
703
  this.isStopping = false;
@@ -671,7 +706,8 @@ class MidyGM2 {
671
706
  }
672
707
  else if (this.isSeeking) {
673
708
  this.stopNotes(0, true, now);
674
- this.exclusiveClassMap.clear();
709
+ this.exclusiveClassNotes.fill(undefined);
710
+ this.drumExclusiveClassNotes.fill(undefined);
675
711
  this.startTime = this.audioContext.currentTime;
676
712
  queueIndex = this.getQueueIndex(this.resumeTime);
677
713
  offset = this.resumeTime - this.startTime;
@@ -699,7 +735,7 @@ class MidyGM2 {
699
735
  extractMidiData(midi) {
700
736
  const instruments = new Set();
701
737
  const timeline = [];
702
- const tmpChannels = new Array(16);
738
+ const tmpChannels = new Array(this.channels.length);
703
739
  for (let i = 0; i < tmpChannels.length; i++) {
704
740
  tmpChannels[i] = {
705
741
  programNumber: -1,
@@ -798,6 +834,17 @@ class MidyGM2 {
798
834
  }
799
835
  return { instruments, timeline };
800
836
  }
837
+ stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
838
+ const channel = this.channels[channelNumber];
839
+ const promises = [];
840
+ const activeNotes = this.getActiveNotes(channel, scheduleTime);
841
+ activeNotes.forEach((note) => {
842
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
843
+ this.notePromises.push(promise);
844
+ promises.push(promise);
845
+ });
846
+ return Promise.all(promises);
847
+ }
801
848
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
802
849
  const channel = this.channels[channelNumber];
803
850
  const promises = [];
@@ -827,6 +874,9 @@ class MidyGM2 {
827
874
  if (!this.isPlaying)
828
875
  return;
829
876
  this.isStopping = true;
877
+ for (let i = 0; i < this.channels.length; i++) {
878
+ this.resetAllStates(i);
879
+ }
830
880
  }
831
881
  pause() {
832
882
  if (!this.isPlaying || this.isPaused)
@@ -866,6 +916,8 @@ class MidyGM2 {
866
916
  const note = noteList[i];
867
917
  if (!note)
868
918
  continue;
919
+ if (note.ending)
920
+ continue;
869
921
  callback(note);
870
922
  }
871
923
  });
@@ -1197,8 +1249,8 @@ class MidyGM2 {
1197
1249
  note.vibratoLFO.connect(note.vibratoDepth);
1198
1250
  note.vibratoDepth.connect(note.bufferSource.detune);
1199
1251
  }
1200
- async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1201
- const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1252
+ async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1253
+ const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1202
1254
  const cache = this.audioBufferCache.get(audioBufferId);
1203
1255
  if (cache) {
1204
1256
  cache.counter += 1;
@@ -1221,8 +1273,8 @@ class MidyGM2 {
1221
1273
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1222
1274
  const voiceParams = voice.getAllParams(controllerState);
1223
1275
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1224
- const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1225
- note.bufferSource = this.createBufferSource(channel, note, voiceParams, audioBuffer);
1276
+ const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1277
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1226
1278
  note.volumeNode = new GainNode(this.audioContext);
1227
1279
  note.gainL = new GainNode(this.audioContext);
1228
1280
  note.gainR = new GainNode(this.audioContext);
@@ -1231,7 +1283,7 @@ class MidyGM2 {
1231
1283
  type: "lowpass",
1232
1284
  Q: voiceParams.initialFilterQ / 10, // dB
1233
1285
  });
1234
- if (portamento) {
1286
+ if (0.5 <= state.portamento && portamento) {
1235
1287
  note.portamento = true;
1236
1288
  this.setPortamentoStartVolumeEnvelope(channel, note, now);
1237
1289
  this.setPortamentoStartFilterEnvelope(channel, note, now);
@@ -1282,14 +1334,56 @@ class MidyGM2 {
1282
1334
  return channel.bank;
1283
1335
  }
1284
1336
  }
1337
+ handleExclusiveClass(note, channelNumber, startTime) {
1338
+ const exclusiveClass = note.voiceParams.exclusiveClass;
1339
+ if (exclusiveClass === 0)
1340
+ return;
1341
+ const prev = this.exclusiveClassNotes[exclusiveClass];
1342
+ if (prev) {
1343
+ const [prevNote, prevChannelNumber] = prev;
1344
+ if (prevNote && !prevNote.ending) {
1345
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1346
+ startTime, true, // force
1347
+ undefined);
1348
+ }
1349
+ }
1350
+ this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
1351
+ }
1352
+ handleDrumExclusiveClass(note, channelNumber, startTime) {
1353
+ const channel = this.channels[channelNumber];
1354
+ if (!channel.isDrum)
1355
+ return;
1356
+ const kitTable = drumExclusiveClassesByKit[channel.programNumber];
1357
+ if (!kitTable)
1358
+ return;
1359
+ const drumExclusiveClass = kitTable[note.noteNumber];
1360
+ if (drumExclusiveClass === 0)
1361
+ return;
1362
+ const index = (drumExclusiveClass - 1) * this.channels.length +
1363
+ channelNumber;
1364
+ const prevNote = this.drumExclusiveClassNotes[index];
1365
+ if (prevNote && !prevNote.ending) {
1366
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1367
+ startTime, true, // force
1368
+ undefined);
1369
+ }
1370
+ this.drumExclusiveClassNotes[index] = note;
1371
+ }
1372
+ isDrumNoteOffException(channel, noteNumber) {
1373
+ if (!channel.isDrum)
1374
+ return false;
1375
+ const programNumber = channel.programNumber;
1376
+ return !((programNumber === 48 && noteNumber === 88) ||
1377
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1378
+ }
1285
1379
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1286
1380
  const channel = this.channels[channelNumber];
1287
1381
  const bankNumber = this.calcBank(channel, channelNumber);
1288
- const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
1382
+ const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1289
1383
  if (soundFontIndex === undefined)
1290
1384
  return;
1291
1385
  const soundFont = this.soundFonts[soundFontIndex];
1292
- const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1386
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1293
1387
  if (!voice)
1294
1388
  return;
1295
1389
  const isSF3 = soundFont.parsed.info.version.major === 3;
@@ -1299,33 +1393,60 @@ class MidyGM2 {
1299
1393
  if (0.5 <= channel.state.sustainPedal) {
1300
1394
  channel.sustainNotes.push(note);
1301
1395
  }
1302
- const exclusiveClass = note.voiceParams.exclusiveClass;
1303
- if (exclusiveClass !== 0) {
1304
- if (this.exclusiveClassMap.has(exclusiveClass)) {
1305
- const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1306
- const [prevNote, prevChannelNumber] = prevEntry;
1307
- if (prevNote && !prevNote.ending) {
1308
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1309
- startTime, true, // force
1310
- undefined);
1311
- }
1312
- }
1313
- this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1314
- }
1396
+ this.handleExclusiveClass(note, channelNumber, startTime);
1397
+ this.handleDrumExclusiveClass(note, channelNumber, startTime);
1315
1398
  const scheduledNotes = channel.scheduledNotes;
1316
- if (scheduledNotes.has(noteNumber)) {
1317
- scheduledNotes.get(noteNumber).push(note);
1399
+ let noteList = scheduledNotes.get(noteNumber);
1400
+ if (noteList) {
1401
+ noteList.push(note);
1318
1402
  }
1319
1403
  else {
1320
- scheduledNotes.set(noteNumber, [note]);
1404
+ noteList = [note];
1405
+ scheduledNotes.set(noteNumber, noteList);
1406
+ }
1407
+ if (this.isDrumNoteOffException(channel, noteNumber)) {
1408
+ const stopTime = startTime + note.bufferSource.buffer.duration;
1409
+ const index = noteList.length - 1;
1410
+ const promise = new Promise((resolve) => {
1411
+ note.bufferSource.onended = () => {
1412
+ noteList[index] = undefined;
1413
+ this.disconnectNote(note);
1414
+ resolve();
1415
+ };
1416
+ note.bufferSource.stop(stopTime);
1417
+ });
1418
+ this.notePromises.push(promise);
1321
1419
  }
1322
1420
  }
1323
1421
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1324
1422
  scheduleTime ??= this.audioContext.currentTime;
1325
1423
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1326
1424
  }
1327
- stopNote(endTime, stopTime, scheduledNotes, index) {
1328
- const note = scheduledNotes[index];
1425
+ disconnectNote(note) {
1426
+ note.bufferSource.disconnect();
1427
+ note.filterNode.disconnect();
1428
+ note.volumeEnvelopeNode.disconnect();
1429
+ note.volumeNode.disconnect();
1430
+ note.gainL.disconnect();
1431
+ note.gainR.disconnect();
1432
+ if (note.modulationDepth) {
1433
+ note.volumeDepth.disconnect();
1434
+ note.modulationDepth.disconnect();
1435
+ note.modulationLFO.stop();
1436
+ }
1437
+ if (note.vibratoDepth) {
1438
+ note.vibratoDepth.disconnect();
1439
+ note.vibratoLFO.stop();
1440
+ }
1441
+ if (note.reverbEffectsSend) {
1442
+ note.reverbEffectsSend.disconnect();
1443
+ }
1444
+ if (note.chorusEffectsSend) {
1445
+ note.chorusEffectsSend.disconnect();
1446
+ }
1447
+ }
1448
+ stopNote(endTime, stopTime, noteList, index) {
1449
+ const note = noteList[index];
1329
1450
  note.volumeEnvelopeNode.gain
1330
1451
  .cancelScheduledValues(endTime)
1331
1452
  .linearRampToValueAtTime(0, stopTime);
@@ -1335,35 +1456,27 @@ class MidyGM2 {
1335
1456
  }, stopTime);
1336
1457
  return new Promise((resolve) => {
1337
1458
  note.bufferSource.onended = () => {
1338
- scheduledNotes[index] = null;
1339
- note.bufferSource.disconnect();
1340
- note.filterNode.disconnect();
1341
- note.volumeEnvelopeNode.disconnect();
1342
- note.volumeNode.disconnect();
1343
- note.gainL.disconnect();
1344
- note.gainR.disconnect();
1345
- if (note.modulationDepth) {
1346
- note.volumeDepth.disconnect();
1347
- note.modulationDepth.disconnect();
1348
- note.modulationLFO.stop();
1349
- }
1350
- if (note.vibratoDepth) {
1351
- note.vibratoDepth.disconnect();
1352
- note.vibratoLFO.stop();
1353
- }
1354
- if (note.reverbEffectsSend) {
1355
- note.reverbEffectsSend.disconnect();
1356
- }
1357
- if (note.chorusEffectsSend) {
1358
- note.chorusEffectsSend.disconnect();
1359
- }
1459
+ noteList[index] = undefined;
1460
+ this.disconnectNote(note);
1360
1461
  resolve();
1361
1462
  };
1362
1463
  note.bufferSource.stop(stopTime);
1363
1464
  });
1364
1465
  }
1466
+ findNoteOffTarget(noteList) {
1467
+ for (let i = 0; i < noteList.length; i++) {
1468
+ const note = noteList[i];
1469
+ if (!note)
1470
+ continue;
1471
+ if (note.ending)
1472
+ continue;
1473
+ return [note, i];
1474
+ }
1475
+ }
1365
1476
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1366
1477
  const channel = this.channels[channelNumber];
1478
+ if (this.isDrumNoteOffException(channel, noteNumber))
1479
+ return;
1367
1480
  const state = channel.state;
1368
1481
  if (!force) {
1369
1482
  if (0.5 <= state.sustainPedal)
@@ -1371,34 +1484,32 @@ class MidyGM2 {
1371
1484
  if (channel.sostenutoNotes.has(noteNumber))
1372
1485
  return;
1373
1486
  }
1374
- if (!channel.scheduledNotes.has(noteNumber))
1487
+ const noteList = channel.scheduledNotes.get(noteNumber);
1488
+ if (!noteList)
1489
+ return; // be careful with drum channel
1490
+ const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
1491
+ if (!noteOffTarget)
1375
1492
  return;
1376
- const scheduledNotes = channel.scheduledNotes.get(noteNumber);
1377
- for (let i = 0; i < scheduledNotes.length; i++) {
1378
- const note = scheduledNotes[i];
1379
- if (!note)
1380
- continue;
1381
- if (note.ending)
1382
- continue;
1383
- if (portamentoNoteNumber === undefined) {
1384
- const volRelease = endTime + note.voiceParams.volRelease;
1385
- const modRelease = endTime + note.voiceParams.modRelease;
1386
- note.filterNode.frequency
1387
- .cancelScheduledValues(endTime)
1388
- .linearRampToValueAtTime(0, modRelease);
1389
- const stopTime = Math.min(volRelease, modRelease);
1390
- return this.stopNote(endTime, stopTime, scheduledNotes, i);
1391
- }
1392
- else {
1393
- const portamentoTime = endTime + this.getPortamentoTime(channel);
1394
- const deltaNote = portamentoNoteNumber - noteNumber;
1395
- const baseRate = note.voiceParams.playbackRate;
1396
- const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1397
- note.bufferSource.playbackRate
1398
- .cancelScheduledValues(endTime)
1399
- .linearRampToValueAtTime(targetRate, portamentoTime);
1400
- return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1401
- }
1493
+ const [note, i] = noteOffTarget;
1494
+ if (0.5 <= state.portamento && portamentoNoteNumber !== undefined) {
1495
+ const portamentoTime = endTime + this.getPortamentoTime(channel);
1496
+ const deltaNote = portamentoNoteNumber - noteNumber;
1497
+ const baseRate = note.voiceParams.playbackRate;
1498
+ const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1499
+ note.bufferSource.playbackRate
1500
+ .cancelScheduledValues(endTime)
1501
+ .linearRampToValueAtTime(targetRate, portamentoTime);
1502
+ return this.stopNote(endTime, portamentoTime, noteList, i);
1503
+ }
1504
+ else {
1505
+ const volRelease = endTime +
1506
+ note.voiceParams.volRelease * state.releaseTime * 2;
1507
+ const modRelease = endTime + note.voiceParams.modRelease;
1508
+ note.filterNode.frequency
1509
+ .cancelScheduledValues(endTime)
1510
+ .linearRampToValueAtTime(0, modRelease);
1511
+ const stopTime = Math.min(volRelease, modRelease);
1512
+ return this.stopNote(endTime, stopTime, noteList, i);
1402
1513
  }
1403
1514
  }
1404
1515
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
@@ -1449,10 +1560,10 @@ class MidyGM2 {
1449
1560
  console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
1450
1561
  }
1451
1562
  }
1452
- handleProgramChange(channelNumber, program, _scheduleTime) {
1563
+ handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1453
1564
  const channel = this.channels[channelNumber];
1454
1565
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1455
- channel.program = program;
1566
+ channel.programNumber = programNumber;
1456
1567
  if (this.mode === "GM2") {
1457
1568
  switch (channel.bankMSB) {
1458
1569
  case 120:
@@ -1479,7 +1590,7 @@ class MidyGM2 {
1479
1590
  this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1480
1591
  this.setControllerParameters(channel, note, table);
1481
1592
  });
1482
- // this.applyVoiceParams(channel, 13);
1593
+ this.applyVoiceParams(channel, 13);
1483
1594
  }
1484
1595
  handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1485
1596
  const pitchBend = msb * 128 + lsb;
@@ -1656,6 +1767,7 @@ class MidyGM2 {
1656
1767
  state.set(channel.state.array);
1657
1768
  state[2] = velocity / 127;
1658
1769
  state[3] = noteNumber / 127;
1770
+ state[13] = state.channelPressure / 127;
1659
1771
  return state;
1660
1772
  }
1661
1773
  applyVoiceParams(channel, controllerType, scheduleTime) {
@@ -1682,7 +1794,7 @@ class MidyGM2 {
1682
1794
  if (key in voiceParams)
1683
1795
  noteVoiceParams[key] = voiceParams[key];
1684
1796
  }
1685
- if (note.portamento) {
1797
+ if (0.5 <= channel.state.portamento && note.portamento) {
1686
1798
  this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1687
1799
  }
1688
1800
  else {
@@ -1879,10 +1991,11 @@ class MidyGM2 {
1879
1991
  const channel = this.channels[channelNumber];
1880
1992
  if (channel.isDrum)
1881
1993
  return;
1994
+ const state = channel.state;
1882
1995
  scheduleTime ??= this.audioContext.currentTime;
1883
- channel.state.softPedal = softPedal / 127;
1996
+ state.softPedal = softPedal / 127;
1884
1997
  this.processScheduledNotes(channel, (note) => {
1885
- if (note.portamento) {
1998
+ if (0.5 <= state.portamento && note.portamento) {
1886
1999
  this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
1887
2000
  this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1888
2001
  }
@@ -2081,18 +2194,32 @@ class MidyGM2 {
2081
2194
  }
2082
2195
  allSoundOff(channelNumber, _value, scheduleTime) {
2083
2196
  scheduleTime ??= this.audioContext.currentTime;
2084
- return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2197
+ return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2085
2198
  }
2199
+ resetAllStates(channelNumber) {
2200
+ const channel = this.channels[channelNumber];
2201
+ const state = channel.state;
2202
+ for (const type of Object.keys(defaultControllerState)) {
2203
+ state[type] = defaultControllerState[type].defaultValue;
2204
+ }
2205
+ for (const type of Object.keys(this.constructor.channelSettings)) {
2206
+ channel[type] = this.constructor.channelSettings[type];
2207
+ }
2208
+ this.mode = "GM2";
2209
+ this.masterFineTuning = 0; // cb
2210
+ this.masterCoarseTuning = 0; // cb
2211
+ }
2212
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2086
2213
  resetAllControllers(channelNumber) {
2087
2214
  const stateTypes = [
2215
+ "channelPressure",
2216
+ "pitchWheel",
2088
2217
  "expression",
2089
2218
  "modulationDepth",
2090
2219
  "sustainPedal",
2091
2220
  "portamento",
2092
2221
  "sostenutoPedal",
2093
2222
  "softPedal",
2094
- "channelPressure",
2095
- "pitchWheelSensitivity",
2096
2223
  ];
2097
2224
  const channel = this.channels[channelNumber];
2098
2225
  const state = channel.state;
@@ -2111,7 +2238,7 @@ class MidyGM2 {
2111
2238
  }
2112
2239
  allNotesOff(channelNumber, _value, scheduleTime) {
2113
2240
  scheduleTime ??= this.audioContext.currentTime;
2114
- return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
2241
+ return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
2115
2242
  }
2116
2243
  omniOff(channelNumber, value, scheduleTime) {
2117
2244
  this.allNotesOff(channelNumber, value, scheduleTime);
@@ -2457,7 +2584,7 @@ class MidyGM2 {
2457
2584
  return value * 0.00787;
2458
2585
  }
2459
2586
  getChannelBitmap(data) {
2460
- const bitmap = new Array(16).fill(false);
2587
+ const bitmap = new Array(this.channels.length).fill(false);
2461
2588
  const ff = data[4] & 0b11;
2462
2589
  const gg = data[5] & 0x7F;
2463
2590
  const hh = data[6] & 0x7F;
@@ -2599,6 +2726,7 @@ class MidyGM2 {
2599
2726
  console.warn(`Unsupported Exclusive Message: ${data}`);
2600
2727
  }
2601
2728
  }
2729
+ // https://github.com/marmooo/js-timer-benchmark
2602
2730
  scheduleTask(callback, scheduleTime) {
2603
2731
  return new Promise((resolve) => {
2604
2732
  const bufferSource = new AudioBufferSourceNode(this.audioContext, {
@@ -2624,10 +2752,8 @@ Object.defineProperty(MidyGM2, "channelSettings", {
2624
2752
  configurable: true,
2625
2753
  writable: true,
2626
2754
  value: {
2627
- currentBufferSource: null,
2628
- isDrum: false,
2629
2755
  detune: 0,
2630
- program: 0,
2756
+ programNumber: 0,
2631
2757
  bank: 121 * 128,
2632
2758
  bankMSB: 121,
2633
2759
  bankLSB: 0,