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