@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/README.md +2 -1
- package/esm/midy-GM1.d.ts +8 -26
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +140 -80
- package/esm/midy-GM2.d.ts +8 -31
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +155 -95
- package/esm/midy-GMLite.d.ts +8 -26
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +134 -75
- package/esm/midy.d.ts +15 -37
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +228 -111
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +8 -26
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +140 -80
- package/script/midy-GM2.d.ts +8 -31
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +155 -95
- package/script/midy-GMLite.d.ts +8 -26
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +134 -75
- package/script/midy.d.ts +15 -37
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +228 -111
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
|
|
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
|
-
|
|
146
|
-
|
|
161
|
+
modulationDepthMSB: { type: 128 + 1, defaultValue: 0 },
|
|
162
|
+
portamentoTimeMSB: { type: 128 + 5, defaultValue: 0 },
|
|
147
163
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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.
|
|
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
|
|
586
|
-
const
|
|
587
|
-
const
|
|
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.
|
|
647
|
+
await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
621
648
|
break;
|
|
622
649
|
case "noteOff": {
|
|
623
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1493
|
+
this.noteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
|
|
1434
1494
|
startTime, true);
|
|
1435
1495
|
}
|
|
1436
1496
|
this.drumExclusiveClassNotes[index] = note;
|
|
1437
1497
|
}
|
|
1438
|
-
|
|
1498
|
+
setNoteRouting(channelNumber, note, startTime) {
|
|
1439
1499
|
const channel = this.channels[channelNumber];
|
|
1440
|
-
const
|
|
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
|
-
|
|
1464
|
-
|
|
1510
|
+
volumeEnvelopeNode.connect(gainL);
|
|
1511
|
+
volumeEnvelopeNode.connect(gainR);
|
|
1465
1512
|
}
|
|
1466
1513
|
else {
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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) +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
2019
|
-
const channel = this.channels[channelNumber];
|
|
2109
|
+
setPortamentoTime(channelNumber, value, scheduleTime) {
|
|
2020
2110
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2021
|
-
channel
|
|
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,
|
|
2120
|
+
setVolume(channelNumber, value, scheduleTime) {
|
|
2027
2121
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2028
2122
|
const channel = this.channels[channelNumber];
|
|
2029
|
-
|
|
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(
|
|
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,
|
|
2143
|
+
setPan(channelNumber, value, scheduleTime) {
|
|
2047
2144
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2048
2145
|
const channel = this.channels[channelNumber];
|
|
2049
|
-
|
|
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,
|
|
2159
|
+
setExpression(channelNumber, value, scheduleTime) {
|
|
2060
2160
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2061
2161
|
const channel = this.channels[channelNumber];
|
|
2062
|
-
|
|
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
|
|
2074
|
-
const volume =
|
|
2075
|
-
const
|
|
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(
|
|
2184
|
+
.setValueAtTime(gain * gainLeft, scheduleTime);
|
|
2079
2185
|
channel.gainR.gain
|
|
2080
2186
|
.cancelScheduledValues(scheduleTime)
|
|
2081
|
-
.setValueAtTime(
|
|
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
|
|
2089
|
-
const
|
|
2090
|
-
const
|
|
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
|
|
2093
|
-
?
|
|
2094
|
-
:
|
|
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(
|
|
2208
|
+
.setValueAtTime(gain * gainLeft, scheduleTime);
|
|
2101
2209
|
gainR.gain
|
|
2102
2210
|
.cancelScheduledValues(scheduleTime)
|
|
2103
|
-
.setValueAtTime(
|
|
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
|
-
"
|
|
2465
|
-
"
|
|
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
|
});
|