@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.
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 },
@@ -252,18 +285,6 @@ export class Midy {
252
285
  writable: true,
253
286
  value: "GM2"
254
287
  });
255
- Object.defineProperty(this, "ticksPerBeat", {
256
- enumerable: true,
257
- configurable: true,
258
- writable: true,
259
- value: 120
260
- });
261
- Object.defineProperty(this, "totalTime", {
262
- enumerable: true,
263
- configurable: true,
264
- writable: true,
265
- value: 0
266
- });
267
288
  Object.defineProperty(this, "masterFineTuning", {
268
289
  enumerable: true,
269
290
  configurable: true,
@@ -297,6 +318,24 @@ export class Midy {
297
318
  delayTimes: this.generateDistributedArray(0.02, 2, 0.5),
298
319
  }
299
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
+ });
300
339
  Object.defineProperty(this, "noteCheckInterval", {
301
340
  enumerable: true,
302
341
  configurable: true,
@@ -399,11 +438,17 @@ export class Midy {
399
438
  writable: true,
400
439
  value: []
401
440
  });
402
- Object.defineProperty(this, "exclusiveClassMap", {
441
+ Object.defineProperty(this, "exclusiveClassNotes", {
403
442
  enumerable: true,
404
443
  configurable: true,
405
444
  writable: true,
406
- value: new SparseMap(128)
445
+ value: new Array(128)
446
+ });
447
+ Object.defineProperty(this, "drumExclusiveClassNotes", {
448
+ enumerable: true,
449
+ configurable: true,
450
+ writable: true,
451
+ value: new Array(this.numChannels * drumExclusiveClassCount)
407
452
  });
408
453
  Object.defineProperty(this, "defaultOptions", {
409
454
  enumerable: true,
@@ -497,8 +542,10 @@ export class Midy {
497
542
  };
498
543
  }
499
544
  createChannels(audioContext) {
500
- const channels = Array.from({ length: 16 }, () => {
545
+ const channels = Array.from({ length: this.numChannels }, () => {
501
546
  return {
547
+ currentBufferSource: null,
548
+ isDrum: false,
502
549
  ...this.constructor.channelSettings,
503
550
  state: new ControllerState(),
504
551
  controlTable: this.initControlTable(),
@@ -543,24 +590,10 @@ export class Midy {
543
590
  return audioBuffer;
544
591
  }
545
592
  }
546
- calcLoopMode(channel, note, voiceParams) {
547
- if (channel.isDrum) {
548
- const noteNumber = note.noteNumber;
549
- if (noteNumber === 88 || 47 <= noteNumber && noteNumber <= 84) {
550
- return true;
551
- }
552
- else {
553
- return false;
554
- }
555
- }
556
- else {
557
- return voiceParams.sampleModes % 2 !== 0;
558
- }
559
- }
560
- createBufferSource(channel, note, voiceParams, audioBuffer) {
593
+ createBufferSource(voiceParams, audioBuffer) {
561
594
  const bufferSource = new AudioBufferSourceNode(this.audioContext);
562
595
  bufferSource.buffer = audioBuffer;
563
- bufferSource.loop = this.calcLoopMode(channel, note, voiceParams);
596
+ bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
564
597
  if (bufferSource.loop) {
565
598
  bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
566
599
  bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
@@ -593,7 +626,7 @@ export class Midy {
593
626
  const startTime = event.startTime + this.startDelay - offset;
594
627
  switch (event.type) {
595
628
  case "noteOn":
596
- if (event.velocity !== 0) {
629
+ if (0 < event.velocity) {
597
630
  await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime, event.portamento);
598
631
  break;
599
632
  }
@@ -651,7 +684,8 @@ export class Midy {
651
684
  if (queueIndex >= this.timeline.length) {
652
685
  await Promise.all(this.notePromises);
653
686
  this.notePromises = [];
654
- this.exclusiveClassMap.clear();
687
+ this.exclusiveClassNotes.fill(undefined);
688
+ this.drumExclusiveClassNotes.fill(undefined);
655
689
  this.audioBufferCache.clear();
656
690
  resolve();
657
691
  return;
@@ -670,7 +704,8 @@ export class Midy {
670
704
  else if (this.isStopping) {
671
705
  await this.stopNotes(0, true, now);
672
706
  this.notePromises = [];
673
- this.exclusiveClassMap.clear();
707
+ this.exclusiveClassNotes.fill(undefined);
708
+ this.drumExclusiveClassNotes.fill(undefined);
674
709
  this.audioBufferCache.clear();
675
710
  resolve();
676
711
  this.isStopping = false;
@@ -679,7 +714,8 @@ export class Midy {
679
714
  }
680
715
  else if (this.isSeeking) {
681
716
  this.stopNotes(0, true, now);
682
- this.exclusiveClassMap.clear();
717
+ this.exclusiveClassNotes.fill(undefined);
718
+ this.drumExclusiveClassNotes.fill(undefined);
683
719
  this.startTime = this.audioContext.currentTime;
684
720
  queueIndex = this.getQueueIndex(this.resumeTime);
685
721
  offset = this.resumeTime - this.startTime;
@@ -707,7 +743,7 @@ export class Midy {
707
743
  extractMidiData(midi) {
708
744
  const instruments = new Set();
709
745
  const timeline = [];
710
- const tmpChannels = new Array(16);
746
+ const tmpChannels = new Array(this.channels.length);
711
747
  for (let i = 0; i < tmpChannels.length; i++) {
712
748
  tmpChannels[i] = {
713
749
  programNumber: -1,
@@ -806,6 +842,17 @@ export class Midy {
806
842
  }
807
843
  return { instruments, timeline };
808
844
  }
845
+ stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
846
+ const channel = this.channels[channelNumber];
847
+ const promises = [];
848
+ const activeNotes = this.getActiveNotes(channel, scheduleTime);
849
+ activeNotes.forEach((note) => {
850
+ const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
851
+ this.notePromises.push(promise);
852
+ promises.push(promise);
853
+ });
854
+ return Promise.all(promises);
855
+ }
809
856
  stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
810
857
  const channel = this.channels[channelNumber];
811
858
  const promises = [];
@@ -835,6 +882,9 @@ export class Midy {
835
882
  if (!this.isPlaying)
836
883
  return;
837
884
  this.isStopping = true;
885
+ for (let i = 0; i < this.channels.length; i++) {
886
+ this.resetAllStates(i);
887
+ }
838
888
  }
839
889
  pause() {
840
890
  if (!this.isPlaying || this.isPaused)
@@ -874,6 +924,8 @@ export class Midy {
874
924
  const note = noteList[i];
875
925
  if (!note)
876
926
  continue;
927
+ if (note.ending)
928
+ continue;
877
929
  callback(note);
878
930
  }
879
931
  });
@@ -1209,8 +1261,8 @@ export class Midy {
1209
1261
  note.vibratoLFO.connect(note.vibratoDepth);
1210
1262
  note.vibratoDepth.connect(note.bufferSource.detune);
1211
1263
  }
1212
- async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
1213
- const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
1264
+ async getAudioBuffer(programNumber, noteNumber, velocity, voiceParams, isSF3) {
1265
+ const audioBufferId = this.getAudioBufferId(programNumber, noteNumber, velocity);
1214
1266
  const cache = this.audioBufferCache.get(audioBufferId);
1215
1267
  if (cache) {
1216
1268
  cache.counter += 1;
@@ -1233,8 +1285,8 @@ export class Midy {
1233
1285
  const controllerState = this.getControllerState(channel, noteNumber, velocity);
1234
1286
  const voiceParams = voice.getAllParams(controllerState);
1235
1287
  const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1236
- const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
1237
- note.bufferSource = this.createBufferSource(channel, note, voiceParams, audioBuffer);
1288
+ const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
1289
+ note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
1238
1290
  note.volumeNode = new GainNode(this.audioContext);
1239
1291
  note.gainL = new GainNode(this.audioContext);
1240
1292
  note.gainR = new GainNode(this.audioContext);
@@ -1243,7 +1295,7 @@ export class Midy {
1243
1295
  type: "lowpass",
1244
1296
  Q: voiceParams.initialFilterQ / 5 * state.filterResonance, // dB
1245
1297
  });
1246
- if (portamento) {
1298
+ if (0.5 <= state.portamento && portamento) {
1247
1299
  note.portamento = true;
1248
1300
  this.setPortamentoStartVolumeEnvelope(channel, note, now);
1249
1301
  this.setPortamentoStartFilterEnvelope(channel, note, now);
@@ -1294,14 +1346,56 @@ export class Midy {
1294
1346
  return channel.bank;
1295
1347
  }
1296
1348
  }
1349
+ handleExclusiveClass(note, channelNumber, startTime) {
1350
+ const exclusiveClass = note.voiceParams.exclusiveClass;
1351
+ if (exclusiveClass === 0)
1352
+ return;
1353
+ const prev = this.exclusiveClassNotes[exclusiveClass];
1354
+ if (prev) {
1355
+ const [prevNote, prevChannelNumber] = prev;
1356
+ if (prevNote && !prevNote.ending) {
1357
+ this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1358
+ startTime, true, // force
1359
+ undefined);
1360
+ }
1361
+ }
1362
+ this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
1363
+ }
1364
+ handleDrumExclusiveClass(note, channelNumber, startTime) {
1365
+ const channel = this.channels[channelNumber];
1366
+ if (!channel.isDrum)
1367
+ return;
1368
+ const kitTable = drumExclusiveClassesByKit[channel.programNumber];
1369
+ if (!kitTable)
1370
+ return;
1371
+ const drumExclusiveClass = kitTable[note.noteNumber];
1372
+ if (drumExclusiveClass === 0)
1373
+ return;
1374
+ const index = (drumExclusiveClass - 1) * this.channels.length +
1375
+ channelNumber;
1376
+ const prevNote = this.drumExclusiveClassNotes[index];
1377
+ if (prevNote && !prevNote.ending) {
1378
+ this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1379
+ startTime, true, // force
1380
+ undefined);
1381
+ }
1382
+ this.drumExclusiveClassNotes[index] = note;
1383
+ }
1384
+ isDrumNoteOffException(channel, noteNumber) {
1385
+ if (!channel.isDrum)
1386
+ return false;
1387
+ const programNumber = channel.programNumber;
1388
+ return !((programNumber === 48 && noteNumber === 88) ||
1389
+ (programNumber === 56 && 47 <= noteNumber && noteNumber <= 84));
1390
+ }
1297
1391
  async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, portamento) {
1298
1392
  const channel = this.channels[channelNumber];
1299
1393
  const bankNumber = this.calcBank(channel, channelNumber);
1300
- const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
1394
+ const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
1301
1395
  if (soundFontIndex === undefined)
1302
1396
  return;
1303
1397
  const soundFont = this.soundFonts[soundFontIndex];
1304
- const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
1398
+ const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
1305
1399
  if (!voice)
1306
1400
  return;
1307
1401
  const isSF3 = soundFont.parsed.info.version.major === 3;
@@ -1311,33 +1405,60 @@ export class Midy {
1311
1405
  if (0.5 <= channel.state.sustainPedal) {
1312
1406
  channel.sustainNotes.push(note);
1313
1407
  }
1314
- const exclusiveClass = note.voiceParams.exclusiveClass;
1315
- if (exclusiveClass !== 0) {
1316
- if (this.exclusiveClassMap.has(exclusiveClass)) {
1317
- const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
1318
- const [prevNote, prevChannelNumber] = prevEntry;
1319
- if (prevNote && !prevNote.ending) {
1320
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1321
- startTime, true, // force
1322
- undefined);
1323
- }
1324
- }
1325
- this.exclusiveClassMap.set(exclusiveClass, [note, channelNumber]);
1326
- }
1408
+ this.handleExclusiveClass(note, channelNumber, startTime);
1409
+ this.handleDrumExclusiveClass(note, channelNumber, startTime);
1327
1410
  const scheduledNotes = channel.scheduledNotes;
1328
- if (scheduledNotes.has(noteNumber)) {
1329
- scheduledNotes.get(noteNumber).push(note);
1411
+ let noteList = scheduledNotes.get(noteNumber);
1412
+ if (noteList) {
1413
+ noteList.push(note);
1330
1414
  }
1331
1415
  else {
1332
- scheduledNotes.set(noteNumber, [note]);
1416
+ noteList = [note];
1417
+ scheduledNotes.set(noteNumber, noteList);
1418
+ }
1419
+ if (this.isDrumNoteOffException(channel, noteNumber)) {
1420
+ const stopTime = startTime + note.bufferSource.buffer.duration;
1421
+ const index = noteList.length - 1;
1422
+ const promise = new Promise((resolve) => {
1423
+ note.bufferSource.onended = () => {
1424
+ noteList[index] = undefined;
1425
+ this.disconnectNote(note);
1426
+ resolve();
1427
+ };
1428
+ note.bufferSource.stop(stopTime);
1429
+ });
1430
+ this.notePromises.push(promise);
1333
1431
  }
1334
1432
  }
1335
1433
  noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1336
1434
  scheduleTime ??= this.audioContext.currentTime;
1337
1435
  return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, false);
1338
1436
  }
1339
- stopNote(endTime, stopTime, scheduledNotes, index) {
1340
- const note = scheduledNotes[index];
1437
+ disconnectNote(note) {
1438
+ note.bufferSource.disconnect();
1439
+ note.filterNode.disconnect();
1440
+ note.volumeEnvelopeNode.disconnect();
1441
+ note.volumeNode.disconnect();
1442
+ note.gainL.disconnect();
1443
+ note.gainR.disconnect();
1444
+ if (note.modulationDepth) {
1445
+ note.volumeDepth.disconnect();
1446
+ note.modulationDepth.disconnect();
1447
+ note.modulationLFO.stop();
1448
+ }
1449
+ if (note.vibratoDepth) {
1450
+ note.vibratoDepth.disconnect();
1451
+ note.vibratoLFO.stop();
1452
+ }
1453
+ if (note.reverbEffectsSend) {
1454
+ note.reverbEffectsSend.disconnect();
1455
+ }
1456
+ if (note.chorusEffectsSend) {
1457
+ note.chorusEffectsSend.disconnect();
1458
+ }
1459
+ }
1460
+ stopNote(endTime, stopTime, noteList, index) {
1461
+ const note = noteList[index];
1341
1462
  note.volumeEnvelopeNode.gain
1342
1463
  .cancelScheduledValues(endTime)
1343
1464
  .linearRampToValueAtTime(0, stopTime);
@@ -1347,35 +1468,27 @@ export class Midy {
1347
1468
  }, stopTime);
1348
1469
  return new Promise((resolve) => {
1349
1470
  note.bufferSource.onended = () => {
1350
- scheduledNotes[index] = null;
1351
- note.bufferSource.disconnect();
1352
- note.filterNode.disconnect();
1353
- note.volumeEnvelopeNode.disconnect();
1354
- note.volumeNode.disconnect();
1355
- note.gainL.disconnect();
1356
- note.gainR.disconnect();
1357
- if (note.modulationDepth) {
1358
- note.volumeDepth.disconnect();
1359
- note.modulationDepth.disconnect();
1360
- note.modulationLFO.stop();
1361
- }
1362
- if (note.vibratoDepth) {
1363
- note.vibratoDepth.disconnect();
1364
- note.vibratoLFO.stop();
1365
- }
1366
- if (note.reverbEffectsSend) {
1367
- note.reverbEffectsSend.disconnect();
1368
- }
1369
- if (note.chorusEffectsSend) {
1370
- note.chorusEffectsSend.disconnect();
1371
- }
1471
+ noteList[index] = undefined;
1472
+ this.disconnectNote(note);
1372
1473
  resolve();
1373
1474
  };
1374
1475
  note.bufferSource.stop(stopTime);
1375
1476
  });
1376
1477
  }
1478
+ findNoteOffTarget(noteList) {
1479
+ for (let i = 0; i < noteList.length; i++) {
1480
+ const note = noteList[i];
1481
+ if (!note)
1482
+ continue;
1483
+ if (note.ending)
1484
+ continue;
1485
+ return [note, i];
1486
+ }
1487
+ }
1377
1488
  scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force, portamentoNoteNumber) {
1378
1489
  const channel = this.channels[channelNumber];
1490
+ if (this.isDrumNoteOffException(channel, noteNumber))
1491
+ return;
1379
1492
  const state = channel.state;
1380
1493
  if (!force) {
1381
1494
  if (0.5 <= state.sustainPedal)
@@ -1383,35 +1496,32 @@ export class Midy {
1383
1496
  if (channel.sostenutoNotes.has(noteNumber))
1384
1497
  return;
1385
1498
  }
1386
- if (!channel.scheduledNotes.has(noteNumber))
1499
+ const noteList = channel.scheduledNotes.get(noteNumber);
1500
+ if (!noteList)
1501
+ return; // be careful with drum channel
1502
+ const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
1503
+ if (!noteOffTarget)
1387
1504
  return;
1388
- const scheduledNotes = channel.scheduledNotes.get(noteNumber);
1389
- for (let i = 0; i < scheduledNotes.length; i++) {
1390
- const note = scheduledNotes[i];
1391
- if (!note)
1392
- continue;
1393
- if (note.ending)
1394
- continue;
1395
- if (portamentoNoteNumber === undefined) {
1396
- const volRelease = endTime +
1397
- note.voiceParams.volRelease * state.releaseTime * 2;
1398
- const modRelease = endTime + note.voiceParams.modRelease;
1399
- note.filterNode.frequency
1400
- .cancelScheduledValues(endTime)
1401
- .linearRampToValueAtTime(0, modRelease);
1402
- const stopTime = Math.min(volRelease, modRelease);
1403
- return this.stopNote(endTime, stopTime, scheduledNotes, i);
1404
- }
1405
- else {
1406
- const portamentoTime = endTime + this.getPortamentoTime(channel);
1407
- const deltaNote = portamentoNoteNumber - noteNumber;
1408
- const baseRate = note.voiceParams.playbackRate;
1409
- const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1410
- note.bufferSource.playbackRate
1411
- .cancelScheduledValues(endTime)
1412
- .linearRampToValueAtTime(targetRate, portamentoTime);
1413
- return this.stopNote(endTime, portamentoTime, scheduledNotes, i);
1414
- }
1505
+ const [note, i] = noteOffTarget;
1506
+ if (0.5 <= state.portamento && portamentoNoteNumber !== undefined) {
1507
+ const portamentoTime = endTime + this.getPortamentoTime(channel);
1508
+ const deltaNote = portamentoNoteNumber - noteNumber;
1509
+ const baseRate = note.voiceParams.playbackRate;
1510
+ const targetRate = baseRate * Math.pow(2, deltaNote / 12);
1511
+ note.bufferSource.playbackRate
1512
+ .cancelScheduledValues(endTime)
1513
+ .linearRampToValueAtTime(targetRate, portamentoTime);
1514
+ return this.stopNote(endTime, portamentoTime, noteList, i);
1515
+ }
1516
+ else {
1517
+ const volRelease = endTime +
1518
+ note.voiceParams.volRelease * state.releaseTime * 2;
1519
+ const modRelease = endTime + note.voiceParams.modRelease;
1520
+ note.filterNode.frequency
1521
+ .cancelScheduledValues(endTime)
1522
+ .linearRampToValueAtTime(0, modRelease);
1523
+ const stopTime = Math.min(volRelease, modRelease);
1524
+ return this.stopNote(endTime, stopTime, noteList, i);
1415
1525
  }
1416
1526
  }
1417
1527
  noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
@@ -1473,12 +1583,12 @@ export class Midy {
1473
1583
  const note = activeNotes.get(noteNumber);
1474
1584
  this.setControllerParameters(channel, note, table);
1475
1585
  }
1476
- // this.applyVoiceParams(channel, 10);
1586
+ this.applyVoiceParams(channel, 10);
1477
1587
  }
1478
- handleProgramChange(channelNumber, program, _scheduleTime) {
1588
+ handleProgramChange(channelNumber, programNumber, _scheduleTime) {
1479
1589
  const channel = this.channels[channelNumber];
1480
1590
  channel.bank = channel.bankMSB * 128 + channel.bankLSB;
1481
- channel.program = program;
1591
+ channel.programNumber = programNumber;
1482
1592
  if (this.mode === "GM2") {
1483
1593
  switch (channel.bankMSB) {
1484
1594
  case 120:
@@ -1505,7 +1615,7 @@ export class Midy {
1505
1615
  this.getActiveNotes(channel, scheduleTime).forEach((note) => {
1506
1616
  this.setControllerParameters(channel, note, table);
1507
1617
  });
1508
- // this.applyVoiceParams(channel, 13);
1618
+ this.applyVoiceParams(channel, 13);
1509
1619
  }
1510
1620
  handlePitchBendMessage(channelNumber, lsb, msb, scheduleTime) {
1511
1621
  const pitchBend = msb * 128 + lsb;
@@ -1682,6 +1792,8 @@ export class Midy {
1682
1792
  state.set(channel.state.array);
1683
1793
  state[2] = velocity / 127;
1684
1794
  state[3] = noteNumber / 127;
1795
+ state[10] = state.polyphonicKeyPressure / 127;
1796
+ state[13] = state.channelPressure / 127;
1685
1797
  return state;
1686
1798
  }
1687
1799
  applyVoiceParams(channel, controllerType, scheduleTime) {
@@ -1708,7 +1820,7 @@ export class Midy {
1708
1820
  if (key in voiceParams)
1709
1821
  noteVoiceParams[key] = voiceParams[key];
1710
1822
  }
1711
- if (note.portamento) {
1823
+ if (0.5 <= channel.state.portamento && note.portamento) {
1712
1824
  this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1713
1825
  }
1714
1826
  else {
@@ -1915,10 +2027,11 @@ export class Midy {
1915
2027
  const channel = this.channels[channelNumber];
1916
2028
  if (channel.isDrum)
1917
2029
  return;
2030
+ const state = channel.state;
1918
2031
  scheduleTime ??= this.audioContext.currentTime;
1919
- channel.state.softPedal = softPedal / 127;
2032
+ state.softPedal = softPedal / 127;
1920
2033
  this.processScheduledNotes(channel, (note) => {
1921
- if (note.portamento) {
2034
+ if (0.5 <= state.portamento && note.portamento) {
1922
2035
  this.setPortamentoStartVolumeEnvelope(channel, note, scheduleTime);
1923
2036
  this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1924
2037
  }
@@ -1940,7 +2053,7 @@ export class Midy {
1940
2053
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
1941
2054
  });
1942
2055
  }
1943
- setReleaseTime(channelNumber, releaseTime, _scheduleTime) {
2056
+ setReleaseTime(channelNumber, releaseTime, scheduleTime) {
1944
2057
  const channel = this.channels[channelNumber];
1945
2058
  if (channel.isDrum)
1946
2059
  return;
@@ -1963,10 +2076,11 @@ export class Midy {
1963
2076
  const channel = this.channels[channelNumber];
1964
2077
  if (channel.isDrum)
1965
2078
  return;
2079
+ const state = channel.state;
1966
2080
  scheduleTime ??= this.audioContext.currentTime;
1967
- channel.state.brightness = brightness / 64;
2081
+ state.brightness = brightness / 64;
1968
2082
  this.processScheduledNotes(channel, (note) => {
1969
- if (note.portamento) {
2083
+ if (0.5 <= state.portamento && note.portamento) {
1970
2084
  this.setPortamentoStartFilterEnvelope(channel, note, scheduleTime);
1971
2085
  }
1972
2086
  else {
@@ -2227,18 +2341,33 @@ export class Midy {
2227
2341
  }
2228
2342
  allSoundOff(channelNumber, _value, scheduleTime) {
2229
2343
  scheduleTime ??= this.audioContext.currentTime;
2230
- return this.stopChannelNotes(channelNumber, 0, true, scheduleTime);
2344
+ return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
2231
2345
  }
2346
+ resetAllStates(channelNumber) {
2347
+ const channel = this.channels[channelNumber];
2348
+ const state = channel.state;
2349
+ for (const type of Object.keys(defaultControllerState)) {
2350
+ state[type] = defaultControllerState[type].defaultValue;
2351
+ }
2352
+ for (const type of Object.keys(this.constructor.channelSettings)) {
2353
+ channel[type] = this.constructor.channelSettings[type];
2354
+ }
2355
+ this.mode = "GM2";
2356
+ this.masterFineTuning = 0; // cb
2357
+ this.masterCoarseTuning = 0; // cb
2358
+ }
2359
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
2232
2360
  resetAllControllers(channelNumber) {
2233
2361
  const stateTypes = [
2362
+ "polyphonicKeyPressure",
2363
+ "channelPressure",
2364
+ "pitchWheel",
2234
2365
  "expression",
2235
2366
  "modulationDepth",
2236
2367
  "sustainPedal",
2237
2368
  "portamento",
2238
2369
  "sostenutoPedal",
2239
2370
  "softPedal",
2240
- "channelPressure",
2241
- "pitchWheelSensitivity",
2242
2371
  ];
2243
2372
  const channel = this.channels[channelNumber];
2244
2373
  const state = channel.state;
@@ -2257,7 +2386,7 @@ export class Midy {
2257
2386
  }
2258
2387
  allNotesOff(channelNumber, _value, scheduleTime) {
2259
2388
  scheduleTime ??= this.audioContext.currentTime;
2260
- return this.stopChannelNotes(channelNumber, 0, false, scheduleTime);
2389
+ return this.stopActiveNotes(channelNumber, 0, false, scheduleTime);
2261
2390
  }
2262
2391
  omniOff(channelNumber, value, scheduleTime) {
2263
2392
  this.allNotesOff(channelNumber, value, scheduleTime);
@@ -2619,7 +2748,7 @@ export class Midy {
2619
2748
  return value * 0.00787;
2620
2749
  }
2621
2750
  getChannelBitmap(data) {
2622
- const bitmap = new Array(16).fill(false);
2751
+ const bitmap = new Array(this.channels.length).fill(false);
2623
2752
  const ff = data[4] & 0b11;
2624
2753
  const gg = data[5] & 0x7F;
2625
2754
  const hh = data[6] & 0x7F;
@@ -2811,6 +2940,7 @@ export class Midy {
2811
2940
  console.warn(`Unsupported Exclusive Message: ${data}`);
2812
2941
  }
2813
2942
  }
2943
+ // https://github.com/marmooo/js-timer-benchmark
2814
2944
  scheduleTask(callback, scheduleTime) {
2815
2945
  return new Promise((resolve) => {
2816
2946
  const bufferSource = new AudioBufferSourceNode(this.audioContext, {
@@ -2835,10 +2965,8 @@ Object.defineProperty(Midy, "channelSettings", {
2835
2965
  configurable: true,
2836
2966
  writable: true,
2837
2967
  value: {
2838
- currentBufferSource: null,
2839
- isDrum: false,
2840
2968
  detune: 0,
2841
- program: 0,
2969
+ programNumber: 0,
2842
2970
  bank: 121 * 128,
2843
2971
  bankMSB: 121,
2844
2972
  bankLSB: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marmooo/midy",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
4
4
  "description": "A MIDI player/synthesizer written in JavaScript that supports GM-Lite/GM1 and SF2/SF3.",
5
5
  "repository": {
6
6
  "type": "git",