@marmooo/midy 0.3.8 → 0.4.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
@@ -1,7 +1,19 @@
1
1
  import { parseMidi } from "midi-file";
2
2
  import { parse, SoundFont } from "@marmooo/soundfont-parser";
3
3
  class Note {
4
- constructor(noteNumber, velocity, startTime, voice, voiceParams) {
4
+ constructor(noteNumber, velocity, startTime) {
5
+ Object.defineProperty(this, "voice", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: void 0
10
+ });
11
+ Object.defineProperty(this, "voiceParams", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: void 0
16
+ });
5
17
  Object.defineProperty(this, "index", {
6
18
  enumerable: true,
7
19
  configurable: true,
@@ -14,6 +26,12 @@ class Note {
14
26
  writable: true,
15
27
  value: false
16
28
  });
29
+ Object.defineProperty(this, "pending", {
30
+ enumerable: true,
31
+ configurable: true,
32
+ writable: true,
33
+ value: true
34
+ });
17
35
  Object.defineProperty(this, "bufferSource", {
18
36
  enumerable: true,
19
37
  configurable: true,
@@ -95,8 +113,6 @@ class Note {
95
113
  this.noteNumber = noteNumber;
96
114
  this.velocity = velocity;
97
115
  this.startTime = startTime;
98
- this.voice = voice;
99
- this.voiceParams = voiceParams;
100
116
  }
101
117
  }
102
118
  const drumExclusiveClassesByKit = new Array(57);
@@ -142,14 +158,19 @@ const defaultControllerState = {
142
158
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
143
159
  link: { type: 127, defaultValue: 0 },
144
160
  // bankMSB: { type: 128 + 0, defaultValue: 121, },
145
- modulationDepth: { type: 128 + 1, defaultValue: 0 },
146
- portamentoTime: { type: 128 + 5, defaultValue: 0 },
161
+ modulationDepthMSB: { type: 128 + 1, defaultValue: 0 },
162
+ portamentoTimeMSB: { type: 128 + 5, defaultValue: 0 },
147
163
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
148
- volume: { type: 128 + 7, defaultValue: 100 / 127 },
149
- pan: { type: 128 + 10, defaultValue: 64 / 127 },
150
- expression: { type: 128 + 11, defaultValue: 1 },
164
+ volumeMSB: { type: 128 + 7, defaultValue: 100 / 127 },
165
+ panMSB: { type: 128 + 10, defaultValue: 64 / 127 },
166
+ expressionMSB: { type: 128 + 11, defaultValue: 1 },
151
167
  // bankLSB: { type: 128 + 32, defaultValue: 0, },
168
+ modulationDepthLSB: { type: 128 + 33, defaultValue: 0 },
169
+ portamentoTimeLSB: { type: 128 + 37, defaultValue: 0 },
152
170
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
171
+ volumeLSB: { type: 128 + 39, defaultValue: 0 },
172
+ panLSB: { type: 128 + 42, defaultValue: 0 },
173
+ expressionLSB: { type: 128 + 43, defaultValue: 0 },
153
174
  sustainPedal: { type: 128 + 64, defaultValue: 0 },
154
175
  portamento: { type: 128 + 65, defaultValue: 0 },
155
176
  sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
@@ -162,6 +183,7 @@ const defaultControllerState = {
162
183
  vibratoRate: { type: 128 + 76, defaultValue: 64 / 127 },
163
184
  vibratoDepth: { type: 128 + 77, defaultValue: 64 / 127 },
164
185
  vibratoDelay: { type: 128 + 78, defaultValue: 64 / 127 },
186
+ portamentoNoteNumber: { type: 128 + 84, defaultValue: 0 },
165
187
  reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
166
188
  chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
167
189
  // dataIncrement: { type: 128 + 96, defaultValue: 0 },
@@ -353,6 +375,12 @@ export class Midy {
353
375
  writable: true,
354
376
  value: new Map()
355
377
  });
378
+ Object.defineProperty(this, "realtimeVoiceCache", {
379
+ enumerable: true,
380
+ configurable: true,
381
+ writable: true,
382
+ value: new Map()
383
+ });
356
384
  Object.defineProperty(this, "isPlaying", {
357
385
  enumerable: true,
358
386
  configurable: true,
@@ -539,7 +567,7 @@ export class Midy {
539
567
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
540
568
  }
541
569
  createChannelAudioNodes(audioContext) {
542
- const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
570
+ const { gainLeft, gainRight } = this.panToGain(defaultControllerState.panMSB.defaultValue);
543
571
  const gainL = new GainNode(audioContext, { gain: gainLeft });
544
572
  const gainR = new GainNode(audioContext, { gain: gainRight });
545
573
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
@@ -582,10 +610,9 @@ export class Midy {
582
610
  return channels;
583
611
  }
584
612
  async createAudioBuffer(voiceParams) {
585
- const sample = voiceParams.sample;
586
- const sampleStart = voiceParams.start;
587
- const sampleEnd = sample.data.length + voiceParams.end;
588
- const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
613
+ const { sample, start, end } = voiceParams;
614
+ const sampleEnd = sample.data.length + end;
615
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, start, sampleEnd);
589
616
  return audioBuffer;
590
617
  }
591
618
  isLoopDrum(channel, noteNumber) {
@@ -617,12 +644,10 @@ export class Midy {
617
644
  const startTime = event.startTime + schedulingOffset;
618
645
  switch (event.type) {
619
646
  case "noteOn":
620
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
647
+ await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
621
648
  break;
622
649
  case "noteOff": {
623
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
624
- if (notePromise)
625
- this.notePromises.push(notePromise);
650
+ this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
626
651
  break;
627
652
  }
628
653
  case "noteAftertouch":
@@ -659,6 +684,7 @@ export class Midy {
659
684
  this.exclusiveClassNotes.fill(undefined);
660
685
  this.drumExclusiveClassNotes.fill(undefined);
661
686
  this.voiceCache.clear();
687
+ this.realtimeVoiceCache.clear();
662
688
  for (let i = 0; i < this.channels.length; i++) {
663
689
  this.channels[i].scheduledNotes = [];
664
690
  this.resetChannelStates(i);
@@ -703,7 +729,6 @@ export class Midy {
703
729
  finished = true;
704
730
  break;
705
731
  }
706
- queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
707
732
  if (this.isPausing) {
708
733
  await this.stopNotes(0, true, now);
709
734
  await this.audioContext.suspend();
@@ -725,9 +750,16 @@ export class Midy {
725
750
  this.isSeeking = false;
726
751
  continue;
727
752
  }
753
+ queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
728
754
  const waitTime = now + this.noteCheckInterval;
729
755
  await this.scheduleTask(() => { }, waitTime);
730
756
  }
757
+ if (this.timeline.length <= queueIndex) {
758
+ const now = this.audioContext.currentTime;
759
+ await this.stopNotes(0, true, now);
760
+ await this.audioContext.suspend();
761
+ finished = true;
762
+ }
731
763
  if (finished) {
732
764
  this.notePromises = [];
733
765
  this.resetAllStates();
@@ -833,7 +865,7 @@ export class Midy {
833
865
  const channel = this.channels[channelNumber];
834
866
  const promises = [];
835
867
  this.processActiveNotes(channel, scheduleTime, (note) => {
836
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
868
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
837
869
  this.notePromises.push(promise);
838
870
  promises.push(promise);
839
871
  });
@@ -843,7 +875,7 @@ export class Midy {
843
875
  const channel = this.channels[channelNumber];
844
876
  const promises = [];
845
877
  this.processScheduledNotes(channel, (note) => {
846
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
878
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
847
879
  this.notePromises.push(promise);
848
880
  promises.push(promise);
849
881
  });
@@ -876,7 +908,7 @@ export class Midy {
876
908
  if (!this.isPlaying || this.isPaused)
877
909
  return;
878
910
  const now = this.audioContext.currentTime;
879
- this.resumeTime += now - this.startTime - this.startDelay;
911
+ this.resumeTime = now - this.startTime - this.startDelay;
880
912
  this.isPausing = true;
881
913
  await this.playPromise;
882
914
  this.isPausing = false;
@@ -902,11 +934,13 @@ export class Midy {
902
934
  if (totalTime < event.startTime)
903
935
  totalTime = event.startTime;
904
936
  }
905
- return totalTime;
937
+ return totalTime + this.startDelay;
906
938
  }
907
939
  currentTime() {
940
+ if (!this.isPlaying)
941
+ return this.resumeTime;
908
942
  const now = this.audioContext.currentTime;
909
- return this.resumeTime + now - this.startTime - this.startDelay;
943
+ return now + this.resumeTime - this.startTime;
910
944
  }
911
945
  processScheduledNotes(channel, callback) {
912
946
  const scheduledNotes = channel.scheduledNotes;
@@ -1118,6 +1152,13 @@ export class Midy {
1118
1152
  const noteDetune = this.calcNoteDetune(channel, note);
1119
1153
  const pitchControl = this.getPitchControl(channel, note);
1120
1154
  const detune = channel.detune + noteDetune + pitchControl;
1155
+ if (channel.portamentoControl) {
1156
+ const state = channel.state;
1157
+ const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
1158
+ note.portamentoNoteNumber = portamentoNoteNumber;
1159
+ channel.portamentoControl = false;
1160
+ state.portamentoNoteNumber = 0;
1161
+ }
1121
1162
  if (this.isPortamento(channel, note)) {
1122
1163
  const startTime = note.startTime;
1123
1164
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
@@ -1134,8 +1175,10 @@ export class Midy {
1134
1175
  }
1135
1176
  }
1136
1177
  getPortamentoTime(channel, note) {
1178
+ const { portamentoTimeMSB, portamentoTimeLSB } = channel.state;
1179
+ const portamentoTime = portamentoTimeMSB + portamentoTimeLSB / 128;
1137
1180
  const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1138
- const value = Math.ceil(channel.state.portamentoTime * 127);
1181
+ const value = Math.ceil(portamentoTime * 128);
1139
1182
  return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1140
1183
  }
1141
1184
  getPitchIncrementSpeed(value) {
@@ -1338,31 +1381,42 @@ export class Midy {
1338
1381
  note.vibratoLFO.connect(note.vibratoDepth);
1339
1382
  note.vibratoDepth.connect(note.bufferSource.detune);
1340
1383
  }
1341
- async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1384
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
1342
1385
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1343
- const cache = this.voiceCache.get(audioBufferId);
1344
- if (cache) {
1345
- cache.counter += 1;
1346
- if (cache.maxCount <= cache.counter) {
1347
- this.voiceCache.delete(audioBufferId);
1348
- }
1349
- return cache.audioBuffer;
1350
- }
1351
- else {
1352
- const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1386
+ if (realtime) {
1387
+ const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
1388
+ if (cachedAudioBuffer)
1389
+ return cachedAudioBuffer;
1353
1390
  const audioBuffer = await this.createAudioBuffer(voiceParams);
1354
- const cache = { audioBuffer, maxCount, counter: 1 };
1355
- this.voiceCache.set(audioBufferId, cache);
1391
+ this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
1356
1392
  return audioBuffer;
1357
1393
  }
1394
+ else {
1395
+ const cache = this.voiceCache.get(audioBufferId);
1396
+ if (cache) {
1397
+ cache.counter += 1;
1398
+ if (cache.maxCount <= cache.counter) {
1399
+ this.voiceCache.delete(audioBufferId);
1400
+ }
1401
+ return cache.audioBuffer;
1402
+ }
1403
+ else {
1404
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1405
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1406
+ const cache = { audioBuffer, maxCount, counter: 1 };
1407
+ this.voiceCache.set(audioBufferId, cache);
1408
+ return audioBuffer;
1409
+ }
1410
+ }
1358
1411
  }
1359
- async createNote(channel, voice, noteNumber, velocity, startTime) {
1412
+ async setNoteAudioNode(channel, note, realtime) {
1360
1413
  const now = this.audioContext.currentTime;
1414
+ const { noteNumber, velocity, startTime } = note;
1361
1415
  const state = channel.state;
1362
1416
  const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1363
- const voiceParams = voice.getAllParams(controllerState);
1364
- const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1365
- const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1417
+ const voiceParams = note.voice.getAllParams(controllerState);
1418
+ note.voiceParams = voiceParams;
1419
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1366
1420
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1367
1421
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1368
1422
  const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
@@ -1388,7 +1442,7 @@ export class Midy {
1388
1442
  if (0 < state.vibratoDepth) {
1389
1443
  this.startVibrato(channel, note, now);
1390
1444
  }
1391
- if (0 < state.modulationDepth) {
1445
+ if (0 < state.modulationDepthMSB + state.modulationDepthLSB) {
1392
1446
  this.startModulation(channel, note, now);
1393
1447
  }
1394
1448
  if (channel.mono && channel.currentBufferSource) {
@@ -1399,7 +1453,13 @@ export class Midy {
1399
1453
  note.filterNode.connect(note.volumeEnvelopeNode);
1400
1454
  this.setChorusSend(channel, note, now);
1401
1455
  this.setReverbSend(channel, note, now);
1402
- note.bufferSource.start(startTime);
1456
+ if (voiceParams.sample.type === "compressed") {
1457
+ const offset = voiceParams.start / audioBuffer.sampleRate;
1458
+ note.bufferSource.start(startTime, offset);
1459
+ }
1460
+ else {
1461
+ note.bufferSource.start(startTime);
1462
+ }
1403
1463
  return note;
1404
1464
  }
1405
1465
  handleExclusiveClass(note, channelNumber, startTime) {
@@ -1410,7 +1470,7 @@ export class Midy {
1410
1470
  if (prev) {
1411
1471
  const [prevNote, prevChannelNumber] = prev;
1412
1472
  if (prevNote && !prevNote.ending) {
1413
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1473
+ this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1414
1474
  startTime, true);
1415
1475
  }
1416
1476
  }
@@ -1430,27 +1490,14 @@ export class Midy {
1430
1490
  channelNumber;
1431
1491
  const prevNote = this.drumExclusiveClassNotes[index];
1432
1492
  if (prevNote && !prevNote.ending) {
1433
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1493
+ this.noteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1434
1494
  startTime, true);
1435
1495
  }
1436
1496
  this.drumExclusiveClassNotes[index] = note;
1437
1497
  }
1438
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1498
+ setNoteRouting(channelNumber, note, startTime) {
1439
1499
  const channel = this.channels[channelNumber];
1440
- const programNumber = channel.programNumber;
1441
- const bankTable = this.soundFontTable[programNumber];
1442
- if (!bankTable)
1443
- return;
1444
- const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1445
- const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1446
- const soundFontIndex = bankTable[bank];
1447
- if (soundFontIndex === undefined)
1448
- return;
1449
- const soundFont = this.soundFonts[soundFontIndex];
1450
- const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1451
- if (!voice)
1452
- return;
1453
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1500
+ const { noteNumber, volumeEnvelopeNode } = note;
1454
1501
  if (channel.isDrum) {
1455
1502
  const { keyBasedGainLs, keyBasedGainRs } = channel;
1456
1503
  let gainL = keyBasedGainLs[noteNumber];
@@ -1460,25 +1507,48 @@ export class Midy {
1460
1507
  gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1461
1508
  gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1462
1509
  }
1463
- note.volumeEnvelopeNode.connect(gainL);
1464
- note.volumeEnvelopeNode.connect(gainR);
1510
+ volumeEnvelopeNode.connect(gainL);
1511
+ volumeEnvelopeNode.connect(gainR);
1465
1512
  }
1466
1513
  else {
1467
- note.volumeEnvelopeNode.connect(channel.gainL);
1468
- note.volumeEnvelopeNode.connect(channel.gainR);
1514
+ volumeEnvelopeNode.connect(channel.gainL);
1515
+ volumeEnvelopeNode.connect(channel.gainR);
1469
1516
  }
1470
1517
  if (0.5 <= channel.state.sustainPedal) {
1471
1518
  channel.sustainNotes.push(note);
1472
1519
  }
1473
1520
  this.handleExclusiveClass(note, channelNumber, startTime);
1474
1521
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1522
+ }
1523
+ async noteOn(channelNumber, noteNumber, velocity, startTime) {
1524
+ const channel = this.channels[channelNumber];
1525
+ const realtime = startTime === undefined;
1526
+ if (realtime)
1527
+ startTime = this.audioContext.currentTime;
1528
+ const note = new Note(noteNumber, velocity, startTime);
1475
1529
  const scheduledNotes = channel.scheduledNotes;
1476
1530
  note.index = scheduledNotes.length;
1477
1531
  scheduledNotes.push(note);
1478
- }
1479
- noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1480
- scheduleTime ??= this.audioContext.currentTime;
1481
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1532
+ const programNumber = channel.programNumber;
1533
+ const bankTable = this.soundFontTable[programNumber];
1534
+ if (!bankTable)
1535
+ return;
1536
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1537
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1538
+ const soundFontIndex = bankTable[bank];
1539
+ if (soundFontIndex === undefined)
1540
+ return;
1541
+ const soundFont = this.soundFonts[soundFontIndex];
1542
+ note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1543
+ if (!note.voice)
1544
+ return;
1545
+ await this.setNoteAudioNode(channel, note, realtime);
1546
+ this.setNoteRouting(channelNumber, note, startTime);
1547
+ note.pending = false;
1548
+ const off = note.offEvent;
1549
+ if (off) {
1550
+ this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1551
+ }
1482
1552
  }
1483
1553
  disconnectNote(note) {
1484
1554
  note.bufferSource.disconnect();
@@ -1501,6 +1571,7 @@ export class Midy {
1501
1571
  }
1502
1572
  }
1503
1573
  releaseNote(channel, note, endTime) {
1574
+ endTime ??= this.audioContext.currentTime;
1504
1575
  const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1505
1576
  const volRelease = endTime + note.voiceParams.volRelease * releaseTime;
1506
1577
  const modRelease = endTime + note.voiceParams.modRelease;
@@ -1522,7 +1593,7 @@ export class Midy {
1522
1593
  }, stopTime);
1523
1594
  });
1524
1595
  }
1525
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1596
+ noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1526
1597
  const channel = this.channels[channelNumber];
1527
1598
  const state = channel.state;
1528
1599
  if (!force) {
@@ -1541,9 +1612,15 @@ export class Midy {
1541
1612
  if (index < 0)
1542
1613
  return;
1543
1614
  const note = channel.scheduledNotes[index];
1615
+ if (note.pending) {
1616
+ note.offEvent = { velocity, startTime: endTime };
1617
+ return;
1618
+ }
1544
1619
  note.ending = true;
1545
1620
  this.setNoteIndex(channel, index);
1546
- this.releaseNote(channel, note, endTime);
1621
+ const promise = this.releaseNote(channel, note, endTime);
1622
+ this.notePromises.push(promise);
1623
+ return promise;
1547
1624
  }
1548
1625
  setNoteIndex(channel, index) {
1549
1626
  let allEnds = true;
@@ -1571,16 +1648,12 @@ export class Midy {
1571
1648
  }
1572
1649
  return -1;
1573
1650
  }
1574
- noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1575
- scheduleTime ??= this.audioContext.currentTime;
1576
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1577
- }
1578
1651
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1579
1652
  const velocity = halfVelocity * 2;
1580
1653
  const channel = this.channels[channelNumber];
1581
1654
  const promises = [];
1582
1655
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1583
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1656
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1584
1657
  promises.push(promise);
1585
1658
  }
1586
1659
  channel.sustainNotes = [];
@@ -1594,7 +1667,7 @@ export class Midy {
1594
1667
  channel.state.sostenutoPedal = 0;
1595
1668
  for (let i = 0; i < sostenutoNotes.length; i++) {
1596
1669
  const note = sostenutoNotes[i];
1597
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1670
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1598
1671
  promises.push(promise);
1599
1672
  }
1600
1673
  channel.sostenutoNotes = [];
@@ -1700,9 +1773,11 @@ export class Midy {
1700
1773
  }
1701
1774
  setModLfoToPitch(channel, note, scheduleTime) {
1702
1775
  if (note.modulationDepth) {
1776
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1777
+ const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
1703
1778
  const modLfoToPitch = note.voiceParams.modLfoToPitch +
1704
1779
  this.getLFOPitchDepth(channel, note);
1705
- const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1780
+ const baseDepth = Math.abs(modLfoToPitch) + modulationDepth;
1706
1781
  const depth = baseDepth * Math.sign(modLfoToPitch);
1707
1782
  note.modulationDepth.gain
1708
1783
  .cancelScheduledValues(scheduleTime)
@@ -1835,7 +1910,8 @@ export class Midy {
1835
1910
  createVoiceParamsHandlers() {
1836
1911
  return {
1837
1912
  modLfoToPitch: (channel, note, scheduleTime) => {
1838
- if (0 < channel.state.modulationDepth) {
1913
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1914
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1839
1915
  this.setModLfoToPitch(channel, note, scheduleTime);
1840
1916
  }
1841
1917
  },
@@ -1845,12 +1921,14 @@ export class Midy {
1845
1921
  }
1846
1922
  },
1847
1923
  modLfoToFilterFc: (channel, note, scheduleTime) => {
1848
- if (0 < channel.state.modulationDepth) {
1924
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1925
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1849
1926
  this.setModLfoToFilterFc(channel, note, scheduleTime);
1850
1927
  }
1851
1928
  },
1852
1929
  modLfoToVolume: (channel, note, scheduleTime) => {
1853
- if (0 < channel.state.modulationDepth) {
1930
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1931
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1854
1932
  this.setModLfoToVolume(channel, note, scheduleTime);
1855
1933
  }
1856
1934
  },
@@ -1861,12 +1939,14 @@ export class Midy {
1861
1939
  this.setReverbSend(channel, note, scheduleTime);
1862
1940
  },
1863
1941
  delayModLFO: (_channel, note, _scheduleTime) => {
1864
- if (0 < channel.state.modulationDepth) {
1942
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1943
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1865
1944
  this.setDelayModLFO(note);
1866
1945
  }
1867
1946
  },
1868
1947
  freqModLFO: (_channel, note, scheduleTime) => {
1869
- if (0 < channel.state.modulationDepth) {
1948
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1949
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1870
1950
  this.setFreqModLFO(note, scheduleTime);
1871
1951
  }
1872
1952
  },
@@ -1935,7 +2015,12 @@ export class Midy {
1935
2015
  handlers[10] = this.setPan;
1936
2016
  handlers[11] = this.setExpression;
1937
2017
  handlers[32] = this.setBankLSB;
2018
+ handlers[33] = this.setModulationDepth;
2019
+ handlers[37] = this.setPortamentoTime;
1938
2020
  handlers[38] = this.dataEntryLSB;
2021
+ handlers[39] = this.setVolume;
2022
+ handlers[42] = this.setPan;
2023
+ handlers[43] = this.setExpression;
1939
2024
  handlers[64] = this.setSustainPedal;
1940
2025
  handlers[65] = this.setPortamento;
1941
2026
  handlers[66] = this.setSostenutoPedal;
@@ -1948,6 +2033,7 @@ export class Midy {
1948
2033
  handlers[76] = this.setVibratoRate;
1949
2034
  handlers[77] = this.setVibratoDepth;
1950
2035
  handlers[78] = this.setVibratoDelay;
2036
+ handlers[84] = this.setPortamentoNoteNumber;
1951
2037
  handlers[91] = this.setReverbSendLevel;
1952
2038
  handlers[93] = this.setChorusSendLevel;
1953
2039
  handlers[96] = this.dataIncrement;
@@ -1979,7 +2065,9 @@ export class Midy {
1979
2065
  this.channels[channelNumber].bankMSB = msb;
1980
2066
  }
1981
2067
  updateModulation(channel, scheduleTime) {
1982
- const depth = channel.state.modulationDepth * channel.modulationDepthRange;
2068
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
2069
+ const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
2070
+ const depth = modulationDepth * channel.modulationDepthRange;
1983
2071
  this.processScheduledNotes(channel, (note) => {
1984
2072
  if (note.modulationDepth) {
1985
2073
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
@@ -1989,12 +2077,15 @@ export class Midy {
1989
2077
  }
1990
2078
  });
1991
2079
  }
1992
- setModulationDepth(channelNumber, modulation, scheduleTime) {
2080
+ setModulationDepth(channelNumber, value, scheduleTime) {
1993
2081
  const channel = this.channels[channelNumber];
1994
2082
  if (channel.isDrum)
1995
2083
  return;
1996
2084
  scheduleTime ??= this.audioContext.currentTime;
1997
- channel.state.modulationDepth = modulation / 127;
2085
+ const state = channel.state;
2086
+ const intPart = Math.trunc(value);
2087
+ state.modulationDepthMSB = intPart / 127;
2088
+ state.modulationDepthLSB = value - intPart;
1998
2089
  this.updateModulation(channel, scheduleTime);
1999
2090
  }
2000
2091
  updatePortamento(channel, scheduleTime) {
@@ -2015,18 +2106,24 @@ export class Midy {
2015
2106
  }
2016
2107
  });
2017
2108
  }
2018
- setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
2019
- const channel = this.channels[channelNumber];
2109
+ setPortamentoTime(channelNumber, value, scheduleTime) {
2020
2110
  scheduleTime ??= this.audioContext.currentTime;
2021
- channel.state.portamentoTime = portamentoTime / 127;
2111
+ const channel = this.channels[channelNumber];
2112
+ const state = channel.state;
2113
+ const intPart = Math.trunc(value);
2114
+ state.portamentoTimeMSB = intPart / 127;
2115
+ state.portamentoTimeLSB = value - 127;
2022
2116
  if (channel.isDrum)
2023
2117
  return;
2024
2118
  this.updatePortamento(channel, scheduleTime);
2025
2119
  }
2026
- setVolume(channelNumber, volume, scheduleTime) {
2120
+ setVolume(channelNumber, value, scheduleTime) {
2027
2121
  scheduleTime ??= this.audioContext.currentTime;
2028
2122
  const channel = this.channels[channelNumber];
2029
- channel.state.volume = volume / 127;
2123
+ const state = channel.state;
2124
+ const intPart = Math.trunc(value);
2125
+ state.volumeMSB = intPart / 127;
2126
+ state.volumeLSB = value - intPart;
2030
2127
  if (channel.isDrum) {
2031
2128
  for (let i = 0; i < 128; i++) {
2032
2129
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2037,16 +2134,19 @@ export class Midy {
2037
2134
  }
2038
2135
  }
2039
2136
  panToGain(pan) {
2040
- const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
2137
+ const theta = Math.PI / 2 * Math.max(pan * 127 - 1) / 126;
2041
2138
  return {
2042
2139
  gainLeft: Math.cos(theta),
2043
2140
  gainRight: Math.sin(theta),
2044
2141
  };
2045
2142
  }
2046
- setPan(channelNumber, pan, scheduleTime) {
2143
+ setPan(channelNumber, value, scheduleTime) {
2047
2144
  scheduleTime ??= this.audioContext.currentTime;
2048
2145
  const channel = this.channels[channelNumber];
2049
- channel.state.pan = pan / 127;
2146
+ const state = channel.state;
2147
+ const intPart = Math.trunc(value);
2148
+ state.panMSB = intPart / 127;
2149
+ state.panLSB = value - intPart;
2050
2150
  if (channel.isDrum) {
2051
2151
  for (let i = 0; i < 128; i++) {
2052
2152
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2056,10 +2156,13 @@ export class Midy {
2056
2156
  this.updateChannelVolume(channel, scheduleTime);
2057
2157
  }
2058
2158
  }
2059
- setExpression(channelNumber, expression, scheduleTime) {
2159
+ setExpression(channelNumber, value, scheduleTime) {
2060
2160
  scheduleTime ??= this.audioContext.currentTime;
2061
2161
  const channel = this.channels[channelNumber];
2062
- channel.state.expression = expression / 127;
2162
+ const state = channel.state;
2163
+ const intPart = Math.trunc(value);
2164
+ state.expressionMSB = intPart / 127;
2165
+ state.expressionLSB = value - intPart;
2063
2166
  this.updateChannelVolume(channel, scheduleTime);
2064
2167
  }
2065
2168
  setBankLSB(channelNumber, lsb) {
@@ -2070,37 +2173,42 @@ export class Midy {
2070
2173
  this.handleRPN(channelNumber, 0, scheduleTime);
2071
2174
  }
2072
2175
  updateChannelVolume(channel, scheduleTime) {
2073
- const state = channel.state;
2074
- const volume = state.volume * state.expression;
2075
- const { gainLeft, gainRight } = this.panToGain(state.pan);
2176
+ const { expressionMSB, expressionLSB, volumeMSB, volumeLSB, panMSB, panLSB, } = channel.state;
2177
+ const volume = volumeMSB + volumeLSB / 128;
2178
+ const expression = expressionMSB + expressionLSB / 128;
2179
+ const pan = panMSB + panLSB / 128;
2180
+ const gain = volume * expression;
2181
+ const { gainLeft, gainRight } = this.panToGain(pan);
2076
2182
  channel.gainL.gain
2077
2183
  .cancelScheduledValues(scheduleTime)
2078
- .setValueAtTime(volume * gainLeft, scheduleTime);
2184
+ .setValueAtTime(gain * gainLeft, scheduleTime);
2079
2185
  channel.gainR.gain
2080
2186
  .cancelScheduledValues(scheduleTime)
2081
- .setValueAtTime(volume * gainRight, scheduleTime);
2187
+ .setValueAtTime(gain * gainRight, scheduleTime);
2082
2188
  }
2083
2189
  updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
2084
2190
  const gainL = channel.keyBasedGainLs[keyNumber];
2085
2191
  if (!gainL)
2086
2192
  return;
2087
2193
  const gainR = channel.keyBasedGainRs[keyNumber];
2088
- const state = channel.state;
2089
- const defaultVolume = state.volume * state.expression;
2090
- const defaultPan = state.pan;
2194
+ const { expressionMSB, expressionLSB, volumeMSB, volumeLSB, panMSB, panLSB, } = channel.state;
2195
+ const volume = volumeMSB + volumeLSB / 128;
2196
+ const expression = expressionMSB + expressionLSB / 128;
2197
+ const defaultGain = volume * expression;
2198
+ const defaultPan = panMSB + panLSB / 128;
2091
2199
  const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
2092
- const volume = (0 <= keyBasedVolume)
2093
- ? defaultVolume * keyBasedVolume / 64
2094
- : defaultVolume;
2200
+ const gain = (0 <= keyBasedVolume)
2201
+ ? defaultGain * keyBasedVolume / 64
2202
+ : defaultGain;
2095
2203
  const keyBasedPan = this.getKeyBasedValue(channel, keyNumber, 10);
2096
2204
  const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2097
2205
  const { gainLeft, gainRight } = this.panToGain(pan);
2098
2206
  gainL.gain
2099
2207
  .cancelScheduledValues(scheduleTime)
2100
- .setValueAtTime(volume * gainLeft, scheduleTime);
2208
+ .setValueAtTime(gain * gainLeft, scheduleTime);
2101
2209
  gainR.gain
2102
2210
  .cancelScheduledValues(scheduleTime)
2103
- .setValueAtTime(volume * gainRight, scheduleTime);
2211
+ .setValueAtTime(gain * gainRight, scheduleTime);
2104
2212
  }
2105
2213
  setSustainPedal(channelNumber, value, scheduleTime) {
2106
2214
  const channel = this.channels[channelNumber];
@@ -2173,8 +2281,8 @@ export class Midy {
2173
2281
  scheduleTime ??= this.audioContext.currentTime;
2174
2282
  const state = channel.state;
2175
2283
  state.filterResonance = ccValue / 127;
2176
- const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2177
2284
  this.processScheduledNotes(channel, (note) => {
2285
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2178
2286
  const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
2179
2287
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2180
2288
  });
@@ -2274,6 +2382,12 @@ export class Midy {
2274
2382
  });
2275
2383
  }
2276
2384
  }
2385
+ setPortamentoNoteNumber(channelNumber, value, scheduleTime) {
2386
+ scheduleTime ??= this.audioContext.currentTime;
2387
+ const channel = this.channels[channelNumber];
2388
+ channel.portamentoControl = true;
2389
+ channel.state.portamentoNoteNumber = value / 127;
2390
+ }
2277
2391
  setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
2278
2392
  scheduleTime ??= this.audioContext.currentTime;
2279
2393
  const channel = this.channels[channelNumber];
@@ -2461,8 +2575,10 @@ export class Midy {
2461
2575
  "polyphonicKeyPressure",
2462
2576
  "channelPressure",
2463
2577
  "pitchWheel",
2464
- "expression",
2465
- "modulationDepth",
2578
+ "expressionMSB",
2579
+ "expressionLSB",
2580
+ "modulationDepthMSB",
2581
+ "modulationDepthLSB",
2466
2582
  "sustainPedal",
2467
2583
  "portamento",
2468
2584
  "sostenutoPedal",
@@ -3171,5 +3287,6 @@ Object.defineProperty(Midy, "channelSettings", {
3171
3287
  modulationDepthRange: 50, // cent
3172
3288
  fineTuning: 0, // cent
3173
3289
  coarseTuning: 0, // cent
3290
+ portamentoControl: false,
3174
3291
  }
3175
3292
  });