@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/script/midy.js CHANGED
@@ -4,7 +4,19 @@ exports.Midy = void 0;
4
4
  const midi_file_1 = require("midi-file");
5
5
  const soundfont_parser_1 = require("@marmooo/soundfont-parser");
6
6
  class Note {
7
- constructor(noteNumber, velocity, startTime, voice, voiceParams) {
7
+ constructor(noteNumber, velocity, startTime) {
8
+ Object.defineProperty(this, "voice", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: void 0
13
+ });
14
+ Object.defineProperty(this, "voiceParams", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: void 0
19
+ });
8
20
  Object.defineProperty(this, "index", {
9
21
  enumerable: true,
10
22
  configurable: true,
@@ -17,6 +29,12 @@ class Note {
17
29
  writable: true,
18
30
  value: false
19
31
  });
32
+ Object.defineProperty(this, "pending", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: true
37
+ });
20
38
  Object.defineProperty(this, "bufferSource", {
21
39
  enumerable: true,
22
40
  configurable: true,
@@ -98,8 +116,6 @@ class Note {
98
116
  this.noteNumber = noteNumber;
99
117
  this.velocity = velocity;
100
118
  this.startTime = startTime;
101
- this.voice = voice;
102
- this.voiceParams = voiceParams;
103
119
  }
104
120
  }
105
121
  const drumExclusiveClassesByKit = new Array(57);
@@ -145,14 +161,19 @@ const defaultControllerState = {
145
161
  pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
146
162
  link: { type: 127, defaultValue: 0 },
147
163
  // bankMSB: { type: 128 + 0, defaultValue: 121, },
148
- modulationDepth: { type: 128 + 1, defaultValue: 0 },
149
- portamentoTime: { type: 128 + 5, defaultValue: 0 },
164
+ modulationDepthMSB: { type: 128 + 1, defaultValue: 0 },
165
+ portamentoTimeMSB: { type: 128 + 5, defaultValue: 0 },
150
166
  // dataMSB: { type: 128 + 6, defaultValue: 0, },
151
- volume: { type: 128 + 7, defaultValue: 100 / 127 },
152
- pan: { type: 128 + 10, defaultValue: 64 / 127 },
153
- expression: { type: 128 + 11, defaultValue: 1 },
167
+ volumeMSB: { type: 128 + 7, defaultValue: 100 / 127 },
168
+ panMSB: { type: 128 + 10, defaultValue: 64 / 127 },
169
+ expressionMSB: { type: 128 + 11, defaultValue: 1 },
154
170
  // bankLSB: { type: 128 + 32, defaultValue: 0, },
171
+ modulationDepthLSB: { type: 128 + 33, defaultValue: 0 },
172
+ portamentoTimeLSB: { type: 128 + 37, defaultValue: 0 },
155
173
  // dataLSB: { type: 128 + 38, defaultValue: 0, },
174
+ volumeLSB: { type: 128 + 39, defaultValue: 0 },
175
+ panLSB: { type: 128 + 42, defaultValue: 0 },
176
+ expressionLSB: { type: 128 + 43, defaultValue: 0 },
156
177
  sustainPedal: { type: 128 + 64, defaultValue: 0 },
157
178
  portamento: { type: 128 + 65, defaultValue: 0 },
158
179
  sostenutoPedal: { type: 128 + 66, defaultValue: 0 },
@@ -165,6 +186,7 @@ const defaultControllerState = {
165
186
  vibratoRate: { type: 128 + 76, defaultValue: 64 / 127 },
166
187
  vibratoDepth: { type: 128 + 77, defaultValue: 64 / 127 },
167
188
  vibratoDelay: { type: 128 + 78, defaultValue: 64 / 127 },
189
+ portamentoNoteNumber: { type: 128 + 84, defaultValue: 0 },
168
190
  reverbSendLevel: { type: 128 + 91, defaultValue: 0 },
169
191
  chorusSendLevel: { type: 128 + 93, defaultValue: 0 },
170
192
  // dataIncrement: { type: 128 + 96, defaultValue: 0 },
@@ -356,6 +378,12 @@ class Midy {
356
378
  writable: true,
357
379
  value: new Map()
358
380
  });
381
+ Object.defineProperty(this, "realtimeVoiceCache", {
382
+ enumerable: true,
383
+ configurable: true,
384
+ writable: true,
385
+ value: new Map()
386
+ });
359
387
  Object.defineProperty(this, "isPlaying", {
360
388
  enumerable: true,
361
389
  configurable: true,
@@ -542,7 +570,7 @@ class Midy {
542
570
  return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
543
571
  }
544
572
  createChannelAudioNodes(audioContext) {
545
- const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
573
+ const { gainLeft, gainRight } = this.panToGain(defaultControllerState.panMSB.defaultValue);
546
574
  const gainL = new GainNode(audioContext, { gain: gainLeft });
547
575
  const gainR = new GainNode(audioContext, { gain: gainRight });
548
576
  const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
@@ -585,10 +613,9 @@ class Midy {
585
613
  return channels;
586
614
  }
587
615
  async createAudioBuffer(voiceParams) {
588
- const sample = voiceParams.sample;
589
- const sampleStart = voiceParams.start;
590
- const sampleEnd = sample.data.length + voiceParams.end;
591
- const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
616
+ const { sample, start, end } = voiceParams;
617
+ const sampleEnd = sample.data.length + end;
618
+ const audioBuffer = await sample.toAudioBuffer(this.audioContext, start, sampleEnd);
592
619
  return audioBuffer;
593
620
  }
594
621
  isLoopDrum(channel, noteNumber) {
@@ -620,12 +647,10 @@ class Midy {
620
647
  const startTime = event.startTime + schedulingOffset;
621
648
  switch (event.type) {
622
649
  case "noteOn":
623
- await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
650
+ await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
624
651
  break;
625
652
  case "noteOff": {
626
- const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
627
- if (notePromise)
628
- this.notePromises.push(notePromise);
653
+ this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
629
654
  break;
630
655
  }
631
656
  case "noteAftertouch":
@@ -662,6 +687,7 @@ class Midy {
662
687
  this.exclusiveClassNotes.fill(undefined);
663
688
  this.drumExclusiveClassNotes.fill(undefined);
664
689
  this.voiceCache.clear();
690
+ this.realtimeVoiceCache.clear();
665
691
  for (let i = 0; i < this.channels.length; i++) {
666
692
  this.channels[i].scheduledNotes = [];
667
693
  this.resetChannelStates(i);
@@ -706,7 +732,6 @@ class Midy {
706
732
  finished = true;
707
733
  break;
708
734
  }
709
- queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
710
735
  if (this.isPausing) {
711
736
  await this.stopNotes(0, true, now);
712
737
  await this.audioContext.suspend();
@@ -728,9 +753,16 @@ class Midy {
728
753
  this.isSeeking = false;
729
754
  continue;
730
755
  }
756
+ queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
731
757
  const waitTime = now + this.noteCheckInterval;
732
758
  await this.scheduleTask(() => { }, waitTime);
733
759
  }
760
+ if (this.timeline.length <= queueIndex) {
761
+ const now = this.audioContext.currentTime;
762
+ await this.stopNotes(0, true, now);
763
+ await this.audioContext.suspend();
764
+ finished = true;
765
+ }
734
766
  if (finished) {
735
767
  this.notePromises = [];
736
768
  this.resetAllStates();
@@ -836,7 +868,7 @@ class Midy {
836
868
  const channel = this.channels[channelNumber];
837
869
  const promises = [];
838
870
  this.processActiveNotes(channel, scheduleTime, (note) => {
839
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
871
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
840
872
  this.notePromises.push(promise);
841
873
  promises.push(promise);
842
874
  });
@@ -846,7 +878,7 @@ class Midy {
846
878
  const channel = this.channels[channelNumber];
847
879
  const promises = [];
848
880
  this.processScheduledNotes(channel, (note) => {
849
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
881
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
850
882
  this.notePromises.push(promise);
851
883
  promises.push(promise);
852
884
  });
@@ -879,7 +911,7 @@ class Midy {
879
911
  if (!this.isPlaying || this.isPaused)
880
912
  return;
881
913
  const now = this.audioContext.currentTime;
882
- this.resumeTime += now - this.startTime - this.startDelay;
914
+ this.resumeTime = now - this.startTime - this.startDelay;
883
915
  this.isPausing = true;
884
916
  await this.playPromise;
885
917
  this.isPausing = false;
@@ -905,11 +937,13 @@ class Midy {
905
937
  if (totalTime < event.startTime)
906
938
  totalTime = event.startTime;
907
939
  }
908
- return totalTime;
940
+ return totalTime + this.startDelay;
909
941
  }
910
942
  currentTime() {
943
+ if (!this.isPlaying)
944
+ return this.resumeTime;
911
945
  const now = this.audioContext.currentTime;
912
- return this.resumeTime + now - this.startTime - this.startDelay;
946
+ return now + this.resumeTime - this.startTime;
913
947
  }
914
948
  processScheduledNotes(channel, callback) {
915
949
  const scheduledNotes = channel.scheduledNotes;
@@ -1121,6 +1155,13 @@ class Midy {
1121
1155
  const noteDetune = this.calcNoteDetune(channel, note);
1122
1156
  const pitchControl = this.getPitchControl(channel, note);
1123
1157
  const detune = channel.detune + noteDetune + pitchControl;
1158
+ if (channel.portamentoControl) {
1159
+ const state = channel.state;
1160
+ const portamentoNoteNumber = Math.ceil(state.portamentoNoteNumber * 127);
1161
+ note.portamentoNoteNumber = portamentoNoteNumber;
1162
+ channel.portamentoControl = false;
1163
+ state.portamentoNoteNumber = 0;
1164
+ }
1124
1165
  if (this.isPortamento(channel, note)) {
1125
1166
  const startTime = note.startTime;
1126
1167
  const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
@@ -1137,8 +1178,10 @@ class Midy {
1137
1178
  }
1138
1179
  }
1139
1180
  getPortamentoTime(channel, note) {
1181
+ const { portamentoTimeMSB, portamentoTimeLSB } = channel.state;
1182
+ const portamentoTime = portamentoTimeMSB + portamentoTimeLSB / 128;
1140
1183
  const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
1141
- const value = Math.ceil(channel.state.portamentoTime * 127);
1184
+ const value = Math.ceil(portamentoTime * 128);
1142
1185
  return deltaSemitone / this.getPitchIncrementSpeed(value) / 10;
1143
1186
  }
1144
1187
  getPitchIncrementSpeed(value) {
@@ -1341,31 +1384,42 @@ class Midy {
1341
1384
  note.vibratoLFO.connect(note.vibratoDepth);
1342
1385
  note.vibratoDepth.connect(note.bufferSource.detune);
1343
1386
  }
1344
- async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
1387
+ async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
1345
1388
  const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
1346
- const cache = this.voiceCache.get(audioBufferId);
1347
- if (cache) {
1348
- cache.counter += 1;
1349
- if (cache.maxCount <= cache.counter) {
1350
- this.voiceCache.delete(audioBufferId);
1351
- }
1352
- return cache.audioBuffer;
1353
- }
1354
- else {
1355
- const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1389
+ if (realtime) {
1390
+ const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
1391
+ if (cachedAudioBuffer)
1392
+ return cachedAudioBuffer;
1356
1393
  const audioBuffer = await this.createAudioBuffer(voiceParams);
1357
- const cache = { audioBuffer, maxCount, counter: 1 };
1358
- this.voiceCache.set(audioBufferId, cache);
1394
+ this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
1359
1395
  return audioBuffer;
1360
1396
  }
1397
+ else {
1398
+ const cache = this.voiceCache.get(audioBufferId);
1399
+ if (cache) {
1400
+ cache.counter += 1;
1401
+ if (cache.maxCount <= cache.counter) {
1402
+ this.voiceCache.delete(audioBufferId);
1403
+ }
1404
+ return cache.audioBuffer;
1405
+ }
1406
+ else {
1407
+ const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
1408
+ const audioBuffer = await this.createAudioBuffer(voiceParams);
1409
+ const cache = { audioBuffer, maxCount, counter: 1 };
1410
+ this.voiceCache.set(audioBufferId, cache);
1411
+ return audioBuffer;
1412
+ }
1413
+ }
1361
1414
  }
1362
- async createNote(channel, voice, noteNumber, velocity, startTime) {
1415
+ async setNoteAudioNode(channel, note, realtime) {
1363
1416
  const now = this.audioContext.currentTime;
1417
+ const { noteNumber, velocity, startTime } = note;
1364
1418
  const state = channel.state;
1365
1419
  const controllerState = this.getControllerState(channel, noteNumber, velocity, 0);
1366
- const voiceParams = voice.getAllParams(controllerState);
1367
- const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
1368
- const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
1420
+ const voiceParams = note.voice.getAllParams(controllerState);
1421
+ note.voiceParams = voiceParams;
1422
+ const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
1369
1423
  note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
1370
1424
  note.volumeEnvelopeNode = new GainNode(this.audioContext);
1371
1425
  const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
@@ -1391,7 +1445,7 @@ class Midy {
1391
1445
  if (0 < state.vibratoDepth) {
1392
1446
  this.startVibrato(channel, note, now);
1393
1447
  }
1394
- if (0 < state.modulationDepth) {
1448
+ if (0 < state.modulationDepthMSB + state.modulationDepthLSB) {
1395
1449
  this.startModulation(channel, note, now);
1396
1450
  }
1397
1451
  if (channel.mono && channel.currentBufferSource) {
@@ -1402,7 +1456,13 @@ class Midy {
1402
1456
  note.filterNode.connect(note.volumeEnvelopeNode);
1403
1457
  this.setChorusSend(channel, note, now);
1404
1458
  this.setReverbSend(channel, note, now);
1405
- note.bufferSource.start(startTime);
1459
+ if (voiceParams.sample.type === "compressed") {
1460
+ const offset = voiceParams.start / audioBuffer.sampleRate;
1461
+ note.bufferSource.start(startTime, offset);
1462
+ }
1463
+ else {
1464
+ note.bufferSource.start(startTime);
1465
+ }
1406
1466
  return note;
1407
1467
  }
1408
1468
  handleExclusiveClass(note, channelNumber, startTime) {
@@ -1413,7 +1473,7 @@ class Midy {
1413
1473
  if (prev) {
1414
1474
  const [prevNote, prevChannelNumber] = prev;
1415
1475
  if (prevNote && !prevNote.ending) {
1416
- this.scheduleNoteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1476
+ this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
1417
1477
  startTime, true);
1418
1478
  }
1419
1479
  }
@@ -1433,27 +1493,14 @@ class Midy {
1433
1493
  channelNumber;
1434
1494
  const prevNote = this.drumExclusiveClassNotes[index];
1435
1495
  if (prevNote && !prevNote.ending) {
1436
- this.scheduleNoteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1496
+ this.noteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
1437
1497
  startTime, true);
1438
1498
  }
1439
1499
  this.drumExclusiveClassNotes[index] = note;
1440
1500
  }
1441
- async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
1501
+ setNoteRouting(channelNumber, note, startTime) {
1442
1502
  const channel = this.channels[channelNumber];
1443
- const programNumber = channel.programNumber;
1444
- const bankTable = this.soundFontTable[programNumber];
1445
- if (!bankTable)
1446
- return;
1447
- const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1448
- const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1449
- const soundFontIndex = bankTable[bank];
1450
- if (soundFontIndex === undefined)
1451
- return;
1452
- const soundFont = this.soundFonts[soundFontIndex];
1453
- const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1454
- if (!voice)
1455
- return;
1456
- const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
1503
+ const { noteNumber, volumeEnvelopeNode } = note;
1457
1504
  if (channel.isDrum) {
1458
1505
  const { keyBasedGainLs, keyBasedGainRs } = channel;
1459
1506
  let gainL = keyBasedGainLs[noteNumber];
@@ -1463,25 +1510,48 @@ class Midy {
1463
1510
  gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
1464
1511
  gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
1465
1512
  }
1466
- note.volumeEnvelopeNode.connect(gainL);
1467
- note.volumeEnvelopeNode.connect(gainR);
1513
+ volumeEnvelopeNode.connect(gainL);
1514
+ volumeEnvelopeNode.connect(gainR);
1468
1515
  }
1469
1516
  else {
1470
- note.volumeEnvelopeNode.connect(channel.gainL);
1471
- note.volumeEnvelopeNode.connect(channel.gainR);
1517
+ volumeEnvelopeNode.connect(channel.gainL);
1518
+ volumeEnvelopeNode.connect(channel.gainR);
1472
1519
  }
1473
1520
  if (0.5 <= channel.state.sustainPedal) {
1474
1521
  channel.sustainNotes.push(note);
1475
1522
  }
1476
1523
  this.handleExclusiveClass(note, channelNumber, startTime);
1477
1524
  this.handleDrumExclusiveClass(note, channelNumber, startTime);
1525
+ }
1526
+ async noteOn(channelNumber, noteNumber, velocity, startTime) {
1527
+ const channel = this.channels[channelNumber];
1528
+ const realtime = startTime === undefined;
1529
+ if (realtime)
1530
+ startTime = this.audioContext.currentTime;
1531
+ const note = new Note(noteNumber, velocity, startTime);
1478
1532
  const scheduledNotes = channel.scheduledNotes;
1479
1533
  note.index = scheduledNotes.length;
1480
1534
  scheduledNotes.push(note);
1481
- }
1482
- noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
1483
- scheduleTime ??= this.audioContext.currentTime;
1484
- return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
1535
+ const programNumber = channel.programNumber;
1536
+ const bankTable = this.soundFontTable[programNumber];
1537
+ if (!bankTable)
1538
+ return;
1539
+ const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
1540
+ const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
1541
+ const soundFontIndex = bankTable[bank];
1542
+ if (soundFontIndex === undefined)
1543
+ return;
1544
+ const soundFont = this.soundFonts[soundFontIndex];
1545
+ note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
1546
+ if (!note.voice)
1547
+ return;
1548
+ await this.setNoteAudioNode(channel, note, realtime);
1549
+ this.setNoteRouting(channelNumber, note, startTime);
1550
+ note.pending = false;
1551
+ const off = note.offEvent;
1552
+ if (off) {
1553
+ this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
1554
+ }
1485
1555
  }
1486
1556
  disconnectNote(note) {
1487
1557
  note.bufferSource.disconnect();
@@ -1504,6 +1574,7 @@ class Midy {
1504
1574
  }
1505
1575
  }
1506
1576
  releaseNote(channel, note, endTime) {
1577
+ endTime ??= this.audioContext.currentTime;
1507
1578
  const releaseTime = this.getRelativeKeyBasedValue(channel, note, 72) * 2;
1508
1579
  const volRelease = endTime + note.voiceParams.volRelease * releaseTime;
1509
1580
  const modRelease = endTime + note.voiceParams.modRelease;
@@ -1525,7 +1596,7 @@ class Midy {
1525
1596
  }, stopTime);
1526
1597
  });
1527
1598
  }
1528
- scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
1599
+ noteOff(channelNumber, noteNumber, velocity, endTime, force) {
1529
1600
  const channel = this.channels[channelNumber];
1530
1601
  const state = channel.state;
1531
1602
  if (!force) {
@@ -1544,9 +1615,15 @@ class Midy {
1544
1615
  if (index < 0)
1545
1616
  return;
1546
1617
  const note = channel.scheduledNotes[index];
1618
+ if (note.pending) {
1619
+ note.offEvent = { velocity, startTime: endTime };
1620
+ return;
1621
+ }
1547
1622
  note.ending = true;
1548
1623
  this.setNoteIndex(channel, index);
1549
- this.releaseNote(channel, note, endTime);
1624
+ const promise = this.releaseNote(channel, note, endTime);
1625
+ this.notePromises.push(promise);
1626
+ return promise;
1550
1627
  }
1551
1628
  setNoteIndex(channel, index) {
1552
1629
  let allEnds = true;
@@ -1574,16 +1651,12 @@ class Midy {
1574
1651
  }
1575
1652
  return -1;
1576
1653
  }
1577
- noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
1578
- scheduleTime ??= this.audioContext.currentTime;
1579
- return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
1580
- }
1581
1654
  releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
1582
1655
  const velocity = halfVelocity * 2;
1583
1656
  const channel = this.channels[channelNumber];
1584
1657
  const promises = [];
1585
1658
  for (let i = 0; i < channel.sustainNotes.length; i++) {
1586
- const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1659
+ const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
1587
1660
  promises.push(promise);
1588
1661
  }
1589
1662
  channel.sustainNotes = [];
@@ -1597,7 +1670,7 @@ class Midy {
1597
1670
  channel.state.sostenutoPedal = 0;
1598
1671
  for (let i = 0; i < sostenutoNotes.length; i++) {
1599
1672
  const note = sostenutoNotes[i];
1600
- const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1673
+ const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
1601
1674
  promises.push(promise);
1602
1675
  }
1603
1676
  channel.sostenutoNotes = [];
@@ -1703,9 +1776,11 @@ class Midy {
1703
1776
  }
1704
1777
  setModLfoToPitch(channel, note, scheduleTime) {
1705
1778
  if (note.modulationDepth) {
1779
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1780
+ const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
1706
1781
  const modLfoToPitch = note.voiceParams.modLfoToPitch +
1707
1782
  this.getLFOPitchDepth(channel, note);
1708
- const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
1783
+ const baseDepth = Math.abs(modLfoToPitch) + modulationDepth;
1709
1784
  const depth = baseDepth * Math.sign(modLfoToPitch);
1710
1785
  note.modulationDepth.gain
1711
1786
  .cancelScheduledValues(scheduleTime)
@@ -1838,7 +1913,8 @@ class Midy {
1838
1913
  createVoiceParamsHandlers() {
1839
1914
  return {
1840
1915
  modLfoToPitch: (channel, note, scheduleTime) => {
1841
- if (0 < channel.state.modulationDepth) {
1916
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1917
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1842
1918
  this.setModLfoToPitch(channel, note, scheduleTime);
1843
1919
  }
1844
1920
  },
@@ -1848,12 +1924,14 @@ class Midy {
1848
1924
  }
1849
1925
  },
1850
1926
  modLfoToFilterFc: (channel, note, scheduleTime) => {
1851
- if (0 < channel.state.modulationDepth) {
1927
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1928
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1852
1929
  this.setModLfoToFilterFc(channel, note, scheduleTime);
1853
1930
  }
1854
1931
  },
1855
1932
  modLfoToVolume: (channel, note, scheduleTime) => {
1856
- if (0 < channel.state.modulationDepth) {
1933
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1934
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1857
1935
  this.setModLfoToVolume(channel, note, scheduleTime);
1858
1936
  }
1859
1937
  },
@@ -1864,12 +1942,14 @@ class Midy {
1864
1942
  this.setReverbSend(channel, note, scheduleTime);
1865
1943
  },
1866
1944
  delayModLFO: (_channel, note, _scheduleTime) => {
1867
- if (0 < channel.state.modulationDepth) {
1945
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1946
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1868
1947
  this.setDelayModLFO(note);
1869
1948
  }
1870
1949
  },
1871
1950
  freqModLFO: (_channel, note, scheduleTime) => {
1872
- if (0 < channel.state.modulationDepth) {
1951
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
1952
+ if (0 < modulationDepthMSB + modulationDepthLSB) {
1873
1953
  this.setFreqModLFO(note, scheduleTime);
1874
1954
  }
1875
1955
  },
@@ -1938,7 +2018,12 @@ class Midy {
1938
2018
  handlers[10] = this.setPan;
1939
2019
  handlers[11] = this.setExpression;
1940
2020
  handlers[32] = this.setBankLSB;
2021
+ handlers[33] = this.setModulationDepth;
2022
+ handlers[37] = this.setPortamentoTime;
1941
2023
  handlers[38] = this.dataEntryLSB;
2024
+ handlers[39] = this.setVolume;
2025
+ handlers[42] = this.setPan;
2026
+ handlers[43] = this.setExpression;
1942
2027
  handlers[64] = this.setSustainPedal;
1943
2028
  handlers[65] = this.setPortamento;
1944
2029
  handlers[66] = this.setSostenutoPedal;
@@ -1951,6 +2036,7 @@ class Midy {
1951
2036
  handlers[76] = this.setVibratoRate;
1952
2037
  handlers[77] = this.setVibratoDepth;
1953
2038
  handlers[78] = this.setVibratoDelay;
2039
+ handlers[84] = this.setPortamentoNoteNumber;
1954
2040
  handlers[91] = this.setReverbSendLevel;
1955
2041
  handlers[93] = this.setChorusSendLevel;
1956
2042
  handlers[96] = this.dataIncrement;
@@ -1982,7 +2068,9 @@ class Midy {
1982
2068
  this.channels[channelNumber].bankMSB = msb;
1983
2069
  }
1984
2070
  updateModulation(channel, scheduleTime) {
1985
- const depth = channel.state.modulationDepth * channel.modulationDepthRange;
2071
+ const { modulationDepthMSB, modulationDepthLSB } = channel.state;
2072
+ const modulationDepth = modulationDepthMSB + modulationDepthLSB / 128;
2073
+ const depth = modulationDepth * channel.modulationDepthRange;
1986
2074
  this.processScheduledNotes(channel, (note) => {
1987
2075
  if (note.modulationDepth) {
1988
2076
  note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
@@ -1992,12 +2080,15 @@ class Midy {
1992
2080
  }
1993
2081
  });
1994
2082
  }
1995
- setModulationDepth(channelNumber, modulation, scheduleTime) {
2083
+ setModulationDepth(channelNumber, value, scheduleTime) {
1996
2084
  const channel = this.channels[channelNumber];
1997
2085
  if (channel.isDrum)
1998
2086
  return;
1999
2087
  scheduleTime ??= this.audioContext.currentTime;
2000
- channel.state.modulationDepth = modulation / 127;
2088
+ const state = channel.state;
2089
+ const intPart = Math.trunc(value);
2090
+ state.modulationDepthMSB = intPart / 127;
2091
+ state.modulationDepthLSB = value - intPart;
2001
2092
  this.updateModulation(channel, scheduleTime);
2002
2093
  }
2003
2094
  updatePortamento(channel, scheduleTime) {
@@ -2018,18 +2109,24 @@ class Midy {
2018
2109
  }
2019
2110
  });
2020
2111
  }
2021
- setPortamentoTime(channelNumber, portamentoTime, scheduleTime) {
2022
- const channel = this.channels[channelNumber];
2112
+ setPortamentoTime(channelNumber, value, scheduleTime) {
2023
2113
  scheduleTime ??= this.audioContext.currentTime;
2024
- channel.state.portamentoTime = portamentoTime / 127;
2114
+ const channel = this.channels[channelNumber];
2115
+ const state = channel.state;
2116
+ const intPart = Math.trunc(value);
2117
+ state.portamentoTimeMSB = intPart / 127;
2118
+ state.portamentoTimeLSB = value - 127;
2025
2119
  if (channel.isDrum)
2026
2120
  return;
2027
2121
  this.updatePortamento(channel, scheduleTime);
2028
2122
  }
2029
- setVolume(channelNumber, volume, scheduleTime) {
2123
+ setVolume(channelNumber, value, scheduleTime) {
2030
2124
  scheduleTime ??= this.audioContext.currentTime;
2031
2125
  const channel = this.channels[channelNumber];
2032
- channel.state.volume = volume / 127;
2126
+ const state = channel.state;
2127
+ const intPart = Math.trunc(value);
2128
+ state.volumeMSB = intPart / 127;
2129
+ state.volumeLSB = value - intPart;
2033
2130
  if (channel.isDrum) {
2034
2131
  for (let i = 0; i < 128; i++) {
2035
2132
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2040,16 +2137,19 @@ class Midy {
2040
2137
  }
2041
2138
  }
2042
2139
  panToGain(pan) {
2043
- const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
2140
+ const theta = Math.PI / 2 * Math.max(pan * 127 - 1) / 126;
2044
2141
  return {
2045
2142
  gainLeft: Math.cos(theta),
2046
2143
  gainRight: Math.sin(theta),
2047
2144
  };
2048
2145
  }
2049
- setPan(channelNumber, pan, scheduleTime) {
2146
+ setPan(channelNumber, value, scheduleTime) {
2050
2147
  scheduleTime ??= this.audioContext.currentTime;
2051
2148
  const channel = this.channels[channelNumber];
2052
- channel.state.pan = pan / 127;
2149
+ const state = channel.state;
2150
+ const intPart = Math.trunc(value);
2151
+ state.panMSB = intPart / 127;
2152
+ state.panLSB = value - intPart;
2053
2153
  if (channel.isDrum) {
2054
2154
  for (let i = 0; i < 128; i++) {
2055
2155
  this.updateKeyBasedVolume(channel, i, scheduleTime);
@@ -2059,10 +2159,13 @@ class Midy {
2059
2159
  this.updateChannelVolume(channel, scheduleTime);
2060
2160
  }
2061
2161
  }
2062
- setExpression(channelNumber, expression, scheduleTime) {
2162
+ setExpression(channelNumber, value, scheduleTime) {
2063
2163
  scheduleTime ??= this.audioContext.currentTime;
2064
2164
  const channel = this.channels[channelNumber];
2065
- channel.state.expression = expression / 127;
2165
+ const state = channel.state;
2166
+ const intPart = Math.trunc(value);
2167
+ state.expressionMSB = intPart / 127;
2168
+ state.expressionLSB = value - intPart;
2066
2169
  this.updateChannelVolume(channel, scheduleTime);
2067
2170
  }
2068
2171
  setBankLSB(channelNumber, lsb) {
@@ -2073,37 +2176,42 @@ class Midy {
2073
2176
  this.handleRPN(channelNumber, 0, scheduleTime);
2074
2177
  }
2075
2178
  updateChannelVolume(channel, scheduleTime) {
2076
- const state = channel.state;
2077
- const volume = state.volume * state.expression;
2078
- const { gainLeft, gainRight } = this.panToGain(state.pan);
2179
+ const { expressionMSB, expressionLSB, volumeMSB, volumeLSB, panMSB, panLSB, } = channel.state;
2180
+ const volume = volumeMSB + volumeLSB / 128;
2181
+ const expression = expressionMSB + expressionLSB / 128;
2182
+ const pan = panMSB + panLSB / 128;
2183
+ const gain = volume * expression;
2184
+ const { gainLeft, gainRight } = this.panToGain(pan);
2079
2185
  channel.gainL.gain
2080
2186
  .cancelScheduledValues(scheduleTime)
2081
- .setValueAtTime(volume * gainLeft, scheduleTime);
2187
+ .setValueAtTime(gain * gainLeft, scheduleTime);
2082
2188
  channel.gainR.gain
2083
2189
  .cancelScheduledValues(scheduleTime)
2084
- .setValueAtTime(volume * gainRight, scheduleTime);
2190
+ .setValueAtTime(gain * gainRight, scheduleTime);
2085
2191
  }
2086
2192
  updateKeyBasedVolume(channel, keyNumber, scheduleTime) {
2087
2193
  const gainL = channel.keyBasedGainLs[keyNumber];
2088
2194
  if (!gainL)
2089
2195
  return;
2090
2196
  const gainR = channel.keyBasedGainRs[keyNumber];
2091
- const state = channel.state;
2092
- const defaultVolume = state.volume * state.expression;
2093
- const defaultPan = state.pan;
2197
+ const { expressionMSB, expressionLSB, volumeMSB, volumeLSB, panMSB, panLSB, } = channel.state;
2198
+ const volume = volumeMSB + volumeLSB / 128;
2199
+ const expression = expressionMSB + expressionLSB / 128;
2200
+ const defaultGain = volume * expression;
2201
+ const defaultPan = panMSB + panLSB / 128;
2094
2202
  const keyBasedVolume = this.getKeyBasedValue(channel, keyNumber, 7);
2095
- const volume = (0 <= keyBasedVolume)
2096
- ? defaultVolume * keyBasedVolume / 64
2097
- : defaultVolume;
2203
+ const gain = (0 <= keyBasedVolume)
2204
+ ? defaultGain * keyBasedVolume / 64
2205
+ : defaultGain;
2098
2206
  const keyBasedPan = this.getKeyBasedValue(channel, keyNumber, 10);
2099
2207
  const pan = (0 <= keyBasedPan) ? keyBasedPan / 127 : defaultPan;
2100
2208
  const { gainLeft, gainRight } = this.panToGain(pan);
2101
2209
  gainL.gain
2102
2210
  .cancelScheduledValues(scheduleTime)
2103
- .setValueAtTime(volume * gainLeft, scheduleTime);
2211
+ .setValueAtTime(gain * gainLeft, scheduleTime);
2104
2212
  gainR.gain
2105
2213
  .cancelScheduledValues(scheduleTime)
2106
- .setValueAtTime(volume * gainRight, scheduleTime);
2214
+ .setValueAtTime(gain * gainRight, scheduleTime);
2107
2215
  }
2108
2216
  setSustainPedal(channelNumber, value, scheduleTime) {
2109
2217
  const channel = this.channels[channelNumber];
@@ -2176,8 +2284,8 @@ class Midy {
2176
2284
  scheduleTime ??= this.audioContext.currentTime;
2177
2285
  const state = channel.state;
2178
2286
  state.filterResonance = ccValue / 127;
2179
- const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2180
2287
  this.processScheduledNotes(channel, (note) => {
2288
+ const filterResonance = this.getRelativeKeyBasedValue(channel, note, 71);
2181
2289
  const Q = note.voiceParams.initialFilterQ / 5 * filterResonance;
2182
2290
  note.filterNode.Q.setValueAtTime(Q, scheduleTime);
2183
2291
  });
@@ -2277,6 +2385,12 @@ class Midy {
2277
2385
  });
2278
2386
  }
2279
2387
  }
2388
+ setPortamentoNoteNumber(channelNumber, value, scheduleTime) {
2389
+ scheduleTime ??= this.audioContext.currentTime;
2390
+ const channel = this.channels[channelNumber];
2391
+ channel.portamentoControl = true;
2392
+ channel.state.portamentoNoteNumber = value / 127;
2393
+ }
2280
2394
  setReverbSendLevel(channelNumber, reverbSendLevel, scheduleTime) {
2281
2395
  scheduleTime ??= this.audioContext.currentTime;
2282
2396
  const channel = this.channels[channelNumber];
@@ -2464,8 +2578,10 @@ class Midy {
2464
2578
  "polyphonicKeyPressure",
2465
2579
  "channelPressure",
2466
2580
  "pitchWheel",
2467
- "expression",
2468
- "modulationDepth",
2581
+ "expressionMSB",
2582
+ "expressionLSB",
2583
+ "modulationDepthMSB",
2584
+ "modulationDepthLSB",
2469
2585
  "sustainPedal",
2470
2586
  "portamento",
2471
2587
  "sostenutoPedal",
@@ -3175,5 +3291,6 @@ Object.defineProperty(Midy, "channelSettings", {
3175
3291
  modulationDepthRange: 50, // cent
3176
3292
  fineTuning: 0, // cent
3177
3293
  coarseTuning: 0, // cent
3294
+ portamentoControl: false,
3178
3295
  }
3179
3296
  });