@marmooo/midy 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/midy-GM1.d.ts +17 -55
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +159 -184
- package/esm/midy-GM2.d.ts +34 -90
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +421 -342
- package/esm/midy-GMLite.d.ts +17 -55
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +169 -199
- package/esm/midy.d.ts +37 -114
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +451 -376
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +17 -55
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +159 -184
- package/script/midy-GM2.d.ts +34 -90
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +421 -342
- package/script/midy-GMLite.d.ts +17 -55
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +169 -199
- package/script/midy.d.ts +37 -114
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +451 -376
package/script/midy-GMLite.js
CHANGED
|
@@ -3,60 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MidyGMLite = void 0;
|
|
4
4
|
const midi_file_1 = require("midi-file");
|
|
5
5
|
const soundfont_parser_1 = require("@marmooo/soundfont-parser");
|
|
6
|
-
// 2-3 times faster than Map
|
|
7
|
-
class SparseMap {
|
|
8
|
-
constructor(size) {
|
|
9
|
-
this.data = new Array(size);
|
|
10
|
-
this.activeIndices = [];
|
|
11
|
-
}
|
|
12
|
-
set(key, value) {
|
|
13
|
-
if (this.data[key] === undefined) {
|
|
14
|
-
this.activeIndices.push(key);
|
|
15
|
-
}
|
|
16
|
-
this.data[key] = value;
|
|
17
|
-
}
|
|
18
|
-
get(key) {
|
|
19
|
-
return this.data[key];
|
|
20
|
-
}
|
|
21
|
-
delete(key) {
|
|
22
|
-
if (this.data[key] !== undefined) {
|
|
23
|
-
this.data[key] = undefined;
|
|
24
|
-
const index = this.activeIndices.indexOf(key);
|
|
25
|
-
if (index !== -1) {
|
|
26
|
-
this.activeIndices.splice(index, 1);
|
|
27
|
-
}
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
has(key) {
|
|
33
|
-
return this.data[key] !== undefined;
|
|
34
|
-
}
|
|
35
|
-
get size() {
|
|
36
|
-
return this.activeIndices.length;
|
|
37
|
-
}
|
|
38
|
-
clear() {
|
|
39
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
40
|
-
const key = this.activeIndices[i];
|
|
41
|
-
this.data[key] = undefined;
|
|
42
|
-
}
|
|
43
|
-
this.activeIndices = [];
|
|
44
|
-
}
|
|
45
|
-
*[Symbol.iterator]() {
|
|
46
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
47
|
-
const key = this.activeIndices[i];
|
|
48
|
-
yield [key, this.data[key]];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
forEach(callback) {
|
|
52
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
53
|
-
const key = this.activeIndices[i];
|
|
54
|
-
callback(this.data[key], key, this);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
6
|
class Note {
|
|
59
7
|
constructor(noteNumber, velocity, startTime, voice, voiceParams) {
|
|
8
|
+
Object.defineProperty(this, "index", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: -1
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "ending", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: false
|
|
19
|
+
});
|
|
60
20
|
Object.defineProperty(this, "bufferSource", {
|
|
61
21
|
enumerable: true,
|
|
62
22
|
configurable: true,
|
|
@@ -130,7 +90,7 @@ const defaultControllerState = {
|
|
|
130
90
|
modulationDepth: { type: 128 + 1, defaultValue: 0 },
|
|
131
91
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
132
92
|
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
133
|
-
pan: { type: 128 + 10, defaultValue:
|
|
93
|
+
pan: { type: 128 + 10, defaultValue: 64 / 127 },
|
|
134
94
|
expression: { type: 128 + 11, defaultValue: 1 },
|
|
135
95
|
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
136
96
|
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
@@ -341,7 +301,7 @@ class MidyGMLite {
|
|
|
341
301
|
initSoundFontTable() {
|
|
342
302
|
const table = new Array(128);
|
|
343
303
|
for (let i = 0; i < 128; i++) {
|
|
344
|
-
table[i] = new
|
|
304
|
+
table[i] = new Map();
|
|
345
305
|
}
|
|
346
306
|
return table;
|
|
347
307
|
}
|
|
@@ -357,17 +317,37 @@ class MidyGMLite {
|
|
|
357
317
|
}
|
|
358
318
|
}
|
|
359
319
|
}
|
|
360
|
-
async loadSoundFont(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
320
|
+
async loadSoundFont(input) {
|
|
321
|
+
let uint8Array;
|
|
322
|
+
if (typeof input === "string") {
|
|
323
|
+
const response = await fetch(input);
|
|
324
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
325
|
+
uint8Array = new Uint8Array(arrayBuffer);
|
|
326
|
+
}
|
|
327
|
+
else if (input instanceof Uint8Array) {
|
|
328
|
+
uint8Array = input;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
throw new TypeError("input must be a URL string or Uint8Array");
|
|
332
|
+
}
|
|
333
|
+
const parsed = (0, soundfont_parser_1.parse)(uint8Array);
|
|
364
334
|
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
365
335
|
this.addSoundFont(soundFont);
|
|
366
336
|
}
|
|
367
|
-
async loadMIDI(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
337
|
+
async loadMIDI(input) {
|
|
338
|
+
let uint8Array;
|
|
339
|
+
if (typeof input === "string") {
|
|
340
|
+
const response = await fetch(input);
|
|
341
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
342
|
+
uint8Array = new Uint8Array(arrayBuffer);
|
|
343
|
+
}
|
|
344
|
+
else if (input instanceof Uint8Array) {
|
|
345
|
+
uint8Array = input;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
throw new TypeError("input must be a URL string or Uint8Array");
|
|
349
|
+
}
|
|
350
|
+
const midi = (0, midi_file_1.parseMidi)(uint8Array);
|
|
371
351
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
372
352
|
const midiData = this.extractMidiData(midi);
|
|
373
353
|
this.instruments = midiData.instruments;
|
|
@@ -393,10 +373,10 @@ class MidyGMLite {
|
|
|
393
373
|
return {
|
|
394
374
|
currentBufferSource: null,
|
|
395
375
|
isDrum: false,
|
|
396
|
-
...this.constructor.channelSettings,
|
|
397
376
|
state: new ControllerState(),
|
|
377
|
+
...this.constructor.channelSettings,
|
|
398
378
|
...this.setChannelAudioNodes(audioContext),
|
|
399
|
-
scheduledNotes:
|
|
379
|
+
scheduledNotes: [],
|
|
400
380
|
sustainNotes: [],
|
|
401
381
|
};
|
|
402
382
|
});
|
|
@@ -431,34 +411,33 @@ class MidyGMLite {
|
|
|
431
411
|
return audioBuffer;
|
|
432
412
|
}
|
|
433
413
|
}
|
|
434
|
-
createBufferSource(voiceParams, audioBuffer) {
|
|
414
|
+
createBufferSource(channel, voiceParams, audioBuffer) {
|
|
435
415
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
436
416
|
bufferSource.buffer = audioBuffer;
|
|
437
417
|
bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
|
|
418
|
+
if (channel.isDrum)
|
|
419
|
+
bufferSource.loop = false;
|
|
438
420
|
if (bufferSource.loop) {
|
|
439
421
|
bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
440
422
|
bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
|
|
441
423
|
}
|
|
442
424
|
return bufferSource;
|
|
443
425
|
}
|
|
444
|
-
async scheduleTimelineEvents(t,
|
|
426
|
+
async scheduleTimelineEvents(t, resumeTime, queueIndex) {
|
|
445
427
|
while (queueIndex < this.timeline.length) {
|
|
446
428
|
const event = this.timeline[queueIndex];
|
|
447
429
|
if (event.startTime > t + this.lookAhead)
|
|
448
430
|
break;
|
|
449
|
-
const
|
|
431
|
+
const delay = this.startDelay - resumeTime;
|
|
432
|
+
const startTime = event.startTime + delay;
|
|
450
433
|
switch (event.type) {
|
|
451
434
|
case "noteOn":
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
break;
|
|
455
|
-
}
|
|
456
|
-
/* falls through */
|
|
435
|
+
await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
436
|
+
break;
|
|
457
437
|
case "noteOff": {
|
|
458
438
|
const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
459
|
-
if (notePromise)
|
|
439
|
+
if (notePromise)
|
|
460
440
|
this.notePromises.push(notePromise);
|
|
461
|
-
}
|
|
462
441
|
break;
|
|
463
442
|
}
|
|
464
443
|
case "controller":
|
|
@@ -491,44 +470,53 @@ class MidyGMLite {
|
|
|
491
470
|
this.isPaused = false;
|
|
492
471
|
this.startTime = this.audioContext.currentTime;
|
|
493
472
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
494
|
-
let
|
|
473
|
+
let resumeTime = this.resumeTime - this.startTime;
|
|
495
474
|
this.notePromises = [];
|
|
496
475
|
const schedulePlayback = async () => {
|
|
497
476
|
if (queueIndex >= this.timeline.length) {
|
|
498
477
|
await Promise.all(this.notePromises);
|
|
499
478
|
this.notePromises = [];
|
|
500
479
|
this.exclusiveClassNotes.fill(undefined);
|
|
480
|
+
this.drumExclusiveClassNotes.fill(undefined);
|
|
501
481
|
this.audioBufferCache.clear();
|
|
482
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
483
|
+
this.resetAllStates(i);
|
|
484
|
+
}
|
|
502
485
|
resolve();
|
|
503
486
|
return;
|
|
504
487
|
}
|
|
505
488
|
const now = this.audioContext.currentTime;
|
|
506
|
-
const t = now +
|
|
507
|
-
queueIndex = await this.scheduleTimelineEvents(t,
|
|
489
|
+
const t = now + resumeTime;
|
|
490
|
+
queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
|
|
508
491
|
if (this.isPausing) {
|
|
509
492
|
await this.stopNotes(0, true, now);
|
|
510
493
|
this.notePromises = [];
|
|
511
|
-
resolve();
|
|
512
494
|
this.isPausing = false;
|
|
513
495
|
this.isPaused = true;
|
|
496
|
+
resolve();
|
|
514
497
|
return;
|
|
515
498
|
}
|
|
516
499
|
else if (this.isStopping) {
|
|
517
500
|
await this.stopNotes(0, true, now);
|
|
518
501
|
this.notePromises = [];
|
|
519
502
|
this.exclusiveClassNotes.fill(undefined);
|
|
503
|
+
this.drumExclusiveClassNotes.fill(undefined);
|
|
520
504
|
this.audioBufferCache.clear();
|
|
521
|
-
|
|
505
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
506
|
+
this.resetAllStates(i);
|
|
507
|
+
}
|
|
522
508
|
this.isStopping = false;
|
|
523
509
|
this.isPaused = false;
|
|
510
|
+
resolve();
|
|
524
511
|
return;
|
|
525
512
|
}
|
|
526
513
|
else if (this.isSeeking) {
|
|
527
514
|
this.stopNotes(0, true, now);
|
|
528
515
|
this.exclusiveClassNotes.fill(undefined);
|
|
516
|
+
this.drumExclusiveClassNotes.fill(undefined);
|
|
529
517
|
this.startTime = this.audioContext.currentTime;
|
|
530
518
|
queueIndex = this.getQueueIndex(this.resumeTime);
|
|
531
|
-
|
|
519
|
+
resumeTime = this.resumeTime - this.startTime;
|
|
532
520
|
this.isSeeking = false;
|
|
533
521
|
await schedulePlayback();
|
|
534
522
|
}
|
|
@@ -551,6 +539,7 @@ class MidyGMLite {
|
|
|
551
539
|
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
552
540
|
}
|
|
553
541
|
extractMidiData(midi) {
|
|
542
|
+
this.audioBufferCounter.clear();
|
|
554
543
|
const instruments = new Set();
|
|
555
544
|
const timeline = [];
|
|
556
545
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -619,9 +608,8 @@ class MidyGMLite {
|
|
|
619
608
|
stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
|
|
620
609
|
const channel = this.channels[channelNumber];
|
|
621
610
|
const promises = [];
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
|
|
611
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
612
|
+
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
625
613
|
this.notePromises.push(promise);
|
|
626
614
|
promises.push(promise);
|
|
627
615
|
});
|
|
@@ -635,7 +623,7 @@ class MidyGMLite {
|
|
|
635
623
|
this.notePromises.push(promise);
|
|
636
624
|
promises.push(promise);
|
|
637
625
|
});
|
|
638
|
-
channel.scheduledNotes
|
|
626
|
+
channel.scheduledNotes = [];
|
|
639
627
|
return Promise.all(promises);
|
|
640
628
|
}
|
|
641
629
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -656,9 +644,6 @@ class MidyGMLite {
|
|
|
656
644
|
if (!this.isPlaying)
|
|
657
645
|
return;
|
|
658
646
|
this.isStopping = true;
|
|
659
|
-
for (let i = 0; i < this.channels.length; i++) {
|
|
660
|
-
this.resetAllStates(i);
|
|
661
|
-
}
|
|
662
647
|
}
|
|
663
648
|
pause() {
|
|
664
649
|
if (!this.isPlaying || this.isPaused)
|
|
@@ -693,37 +678,28 @@ class MidyGMLite {
|
|
|
693
678
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
694
679
|
}
|
|
695
680
|
processScheduledNotes(channel, callback) {
|
|
696
|
-
channel.scheduledNotes
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
getActiveNotes(channel, scheduleTime) {
|
|
708
|
-
const activeNotes = new SparseMap(128);
|
|
709
|
-
channel.scheduledNotes.forEach((noteList) => {
|
|
710
|
-
const activeNote = this.getActiveNote(noteList, scheduleTime);
|
|
711
|
-
if (activeNote) {
|
|
712
|
-
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
return activeNotes;
|
|
681
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
682
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
683
|
+
const note = scheduledNotes[i];
|
|
684
|
+
if (!note)
|
|
685
|
+
continue;
|
|
686
|
+
if (note.ending)
|
|
687
|
+
continue;
|
|
688
|
+
callback(note);
|
|
689
|
+
}
|
|
716
690
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
691
|
+
processActiveNotes(channel, scheduleTime, callback) {
|
|
692
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
693
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
694
|
+
const note = scheduledNotes[i];
|
|
720
695
|
if (!note)
|
|
721
|
-
|
|
696
|
+
continue;
|
|
697
|
+
if (note.ending)
|
|
698
|
+
continue;
|
|
722
699
|
if (scheduleTime < note.startTime)
|
|
723
700
|
continue;
|
|
724
|
-
|
|
701
|
+
callback(note);
|
|
725
702
|
}
|
|
726
|
-
return noteList[0];
|
|
727
703
|
}
|
|
728
704
|
cbToRatio(cb) {
|
|
729
705
|
return Math.pow(10, cb / 200);
|
|
@@ -861,7 +837,7 @@ class MidyGMLite {
|
|
|
861
837
|
const voiceParams = voice.getAllParams(controllerState);
|
|
862
838
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
863
839
|
const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
|
|
864
|
-
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
840
|
+
note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
|
|
865
841
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
866
842
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
867
843
|
type: "lowpass",
|
|
@@ -870,6 +846,7 @@ class MidyGMLite {
|
|
|
870
846
|
this.setVolumeEnvelope(note, now);
|
|
871
847
|
this.setFilterEnvelope(note, now);
|
|
872
848
|
this.setPitchEnvelope(note, now);
|
|
849
|
+
this.updateDetune(channel, note, now);
|
|
873
850
|
if (0 < state.modulationDepth) {
|
|
874
851
|
this.startModulation(channel, note, now);
|
|
875
852
|
}
|
|
@@ -907,7 +884,7 @@ class MidyGMLite {
|
|
|
907
884
|
}
|
|
908
885
|
this.drumExclusiveClassNotes[index] = note;
|
|
909
886
|
}
|
|
910
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
887
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
911
888
|
const channel = this.channels[channelNumber];
|
|
912
889
|
const bankNumber = channel.bank;
|
|
913
890
|
const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
|
|
@@ -919,6 +896,7 @@ class MidyGMLite {
|
|
|
919
896
|
return;
|
|
920
897
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
921
898
|
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
899
|
+
note.noteOffEvent = noteOffEvent;
|
|
922
900
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
923
901
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
924
902
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -927,31 +905,12 @@ class MidyGMLite {
|
|
|
927
905
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
928
906
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
929
907
|
const scheduledNotes = channel.scheduledNotes;
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
noteList.push(note);
|
|
933
|
-
}
|
|
934
|
-
else {
|
|
935
|
-
noteList = [note];
|
|
936
|
-
scheduledNotes.set(noteNumber, noteList);
|
|
937
|
-
}
|
|
938
|
-
if (channel.isDrum) {
|
|
939
|
-
const stopTime = startTime + note.bufferSource.buffer.duration;
|
|
940
|
-
const index = noteList.length - 1;
|
|
941
|
-
const promise = new Promise((resolve) => {
|
|
942
|
-
note.bufferSource.onended = () => {
|
|
943
|
-
noteList[index] = undefined;
|
|
944
|
-
this.disconnectNote(note);
|
|
945
|
-
resolve();
|
|
946
|
-
};
|
|
947
|
-
note.bufferSource.stop(stopTime);
|
|
948
|
-
});
|
|
949
|
-
this.notePromises.push(promise);
|
|
950
|
-
}
|
|
908
|
+
note.index = scheduledNotes.length;
|
|
909
|
+
scheduledNotes.push(note);
|
|
951
910
|
}
|
|
952
911
|
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
953
912
|
scheduleTime ??= this.audioContext.currentTime;
|
|
954
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
|
|
913
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
|
|
955
914
|
}
|
|
956
915
|
disconnectNote(note) {
|
|
957
916
|
note.bufferSource.disconnect();
|
|
@@ -963,57 +922,54 @@ class MidyGMLite {
|
|
|
963
922
|
note.modulationLFO.stop();
|
|
964
923
|
}
|
|
965
924
|
}
|
|
966
|
-
|
|
967
|
-
const
|
|
925
|
+
releaseNote(channel, note, endTime) {
|
|
926
|
+
const volRelease = endTime + note.voiceParams.volRelease;
|
|
927
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
928
|
+
const stopTime = Math.min(volRelease, modRelease);
|
|
929
|
+
note.filterNode.frequency
|
|
930
|
+
.cancelScheduledValues(endTime)
|
|
931
|
+
.linearRampToValueAtTime(0, modRelease);
|
|
968
932
|
note.volumeEnvelopeNode.gain
|
|
969
933
|
.cancelScheduledValues(endTime)
|
|
970
|
-
.linearRampToValueAtTime(0,
|
|
971
|
-
note.ending = true;
|
|
972
|
-
this.scheduleTask(() => {
|
|
973
|
-
note.bufferSource.loop = false;
|
|
974
|
-
}, stopTime);
|
|
934
|
+
.linearRampToValueAtTime(0, volRelease);
|
|
975
935
|
return new Promise((resolve) => {
|
|
976
|
-
|
|
977
|
-
|
|
936
|
+
this.scheduleTask(() => {
|
|
937
|
+
const bufferSource = note.bufferSource;
|
|
938
|
+
bufferSource.loop = false;
|
|
939
|
+
bufferSource.stop(stopTime);
|
|
978
940
|
this.disconnectNote(note);
|
|
941
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
979
942
|
resolve();
|
|
980
|
-
};
|
|
981
|
-
note.bufferSource.stop(stopTime);
|
|
943
|
+
}, stopTime);
|
|
982
944
|
});
|
|
983
945
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
946
|
+
scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
947
|
+
const channel = this.channels[channelNumber];
|
|
948
|
+
if (!force) {
|
|
949
|
+
if (channel.isDrum)
|
|
950
|
+
return;
|
|
951
|
+
if (0.5 <= channel.state.sustainPedal)
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const note = this.findNoteOffTarget(channel, noteNumber);
|
|
955
|
+
if (!note)
|
|
956
|
+
return;
|
|
957
|
+
note.ending = true;
|
|
958
|
+
this.releaseNote(channel, note, endTime);
|
|
959
|
+
}
|
|
960
|
+
findNoteOffTarget(channel, noteNumber) {
|
|
961
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
962
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
963
|
+
const note = scheduledNotes[i];
|
|
987
964
|
if (!note)
|
|
988
965
|
continue;
|
|
989
966
|
if (note.ending)
|
|
990
967
|
continue;
|
|
991
|
-
|
|
968
|
+
if (note.noteNumber !== noteNumber)
|
|
969
|
+
continue;
|
|
970
|
+
return note;
|
|
992
971
|
}
|
|
993
972
|
}
|
|
994
|
-
scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
995
|
-
const channel = this.channels[channelNumber];
|
|
996
|
-
if (channel.isDrum)
|
|
997
|
-
return;
|
|
998
|
-
if (!force && 0.5 <= channel.state.sustainPedal)
|
|
999
|
-
return;
|
|
1000
|
-
if (!channel.scheduledNotes.has(noteNumber))
|
|
1001
|
-
return;
|
|
1002
|
-
const noteList = channel.scheduledNotes.get(noteNumber);
|
|
1003
|
-
if (!noteList)
|
|
1004
|
-
return; // be careful with drum channel
|
|
1005
|
-
const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
|
|
1006
|
-
if (!noteOffTarget)
|
|
1007
|
-
return;
|
|
1008
|
-
const [note, i] = noteOffTarget;
|
|
1009
|
-
const volRelease = endTime + note.voiceParams.volRelease;
|
|
1010
|
-
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1011
|
-
note.filterNode.frequency
|
|
1012
|
-
.cancelScheduledValues(endTime)
|
|
1013
|
-
.linearRampToValueAtTime(0, modRelease);
|
|
1014
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1015
|
-
return this.stopNote(endTime, stopTime, noteList, i);
|
|
1016
|
-
}
|
|
1017
973
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1018
974
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1019
975
|
return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
|
|
@@ -1023,7 +979,7 @@ class MidyGMLite {
|
|
|
1023
979
|
const channel = this.channels[channelNumber];
|
|
1024
980
|
const promises = [];
|
|
1025
981
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1026
|
-
const promise = this.
|
|
982
|
+
const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
|
|
1027
983
|
promises.push(promise);
|
|
1028
984
|
}
|
|
1029
985
|
channel.sustainNotes = [];
|
|
@@ -1179,20 +1135,20 @@ class MidyGMLite {
|
|
|
1179
1135
|
});
|
|
1180
1136
|
}
|
|
1181
1137
|
createControlChangeHandlers() {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1138
|
+
const handlers = new Array(128);
|
|
1139
|
+
handlers[1] = this.setModulationDepth;
|
|
1140
|
+
handlers[6] = this.dataEntryMSB;
|
|
1141
|
+
handlers[7] = this.setVolume;
|
|
1142
|
+
handlers[10] = this.setPan;
|
|
1143
|
+
handlers[11] = this.setExpression;
|
|
1144
|
+
handlers[38] = this.dataEntryLSB;
|
|
1145
|
+
handlers[64] = this.setSustainPedal;
|
|
1146
|
+
handlers[100] = this.setRPNLSB;
|
|
1147
|
+
handlers[101] = this.setRPNMSB;
|
|
1148
|
+
handlers[120] = this.allSoundOff;
|
|
1149
|
+
handlers[121] = this.resetAllControllers;
|
|
1150
|
+
handlers[123] = this.allNotesOff;
|
|
1151
|
+
return handlers;
|
|
1196
1152
|
}
|
|
1197
1153
|
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1198
1154
|
const handler = this.controlChangeHandlers[controllerType];
|
|
@@ -1337,19 +1293,26 @@ class MidyGMLite {
|
|
|
1337
1293
|
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
1338
1294
|
}
|
|
1339
1295
|
resetAllStates(channelNumber) {
|
|
1296
|
+
const scheduleTime = this.audioContext.currentTime;
|
|
1340
1297
|
const channel = this.channels[channelNumber];
|
|
1341
1298
|
const state = channel.state;
|
|
1342
|
-
|
|
1343
|
-
|
|
1299
|
+
const entries = Object.entries(defaultControllerState);
|
|
1300
|
+
for (const [key, { type, defaultValue }] of entries) {
|
|
1301
|
+
if (128 <= type) {
|
|
1302
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
state[key] = defaultValue;
|
|
1306
|
+
}
|
|
1344
1307
|
}
|
|
1345
|
-
for (const
|
|
1346
|
-
channel[
|
|
1308
|
+
for (const key of Object.keys(this.constructor.channelSettings)) {
|
|
1309
|
+
channel[key] = this.constructor.channelSettings[key];
|
|
1347
1310
|
}
|
|
1348
1311
|
this.mode = "GM1";
|
|
1349
1312
|
}
|
|
1350
1313
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
|
|
1351
|
-
resetAllControllers(channelNumber) {
|
|
1352
|
-
const
|
|
1314
|
+
resetAllControllers(channelNumber, _value, scheduleTime) {
|
|
1315
|
+
const keys = [
|
|
1353
1316
|
"pitchWheel",
|
|
1354
1317
|
"expression",
|
|
1355
1318
|
"modulationDepth",
|
|
@@ -1357,10 +1320,17 @@ class MidyGMLite {
|
|
|
1357
1320
|
];
|
|
1358
1321
|
const channel = this.channels[channelNumber];
|
|
1359
1322
|
const state = channel.state;
|
|
1360
|
-
for (let i = 0; i <
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1323
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1324
|
+
const key = keys[i];
|
|
1325
|
+
const { type, defaultValue } = defaultControllerState[key];
|
|
1326
|
+
if (128 <= type) {
|
|
1327
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
state[key] = defaultValue;
|
|
1331
|
+
}
|
|
1363
1332
|
}
|
|
1333
|
+
this.setPitchBend(channelNumber, 8192, scheduleTime);
|
|
1364
1334
|
const settingTypes = [
|
|
1365
1335
|
"rpnMSB",
|
|
1366
1336
|
"rpnLSB",
|