@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/esm/midy-GMLite.js
CHANGED
|
@@ -1,59 +1,19 @@
|
|
|
1
1
|
import { parseMidi } from "midi-file";
|
|
2
2
|
import { parse, SoundFont } from "@marmooo/soundfont-parser";
|
|
3
|
-
// 2-3 times faster than Map
|
|
4
|
-
class SparseMap {
|
|
5
|
-
constructor(size) {
|
|
6
|
-
this.data = new Array(size);
|
|
7
|
-
this.activeIndices = [];
|
|
8
|
-
}
|
|
9
|
-
set(key, value) {
|
|
10
|
-
if (this.data[key] === undefined) {
|
|
11
|
-
this.activeIndices.push(key);
|
|
12
|
-
}
|
|
13
|
-
this.data[key] = value;
|
|
14
|
-
}
|
|
15
|
-
get(key) {
|
|
16
|
-
return this.data[key];
|
|
17
|
-
}
|
|
18
|
-
delete(key) {
|
|
19
|
-
if (this.data[key] !== undefined) {
|
|
20
|
-
this.data[key] = undefined;
|
|
21
|
-
const index = this.activeIndices.indexOf(key);
|
|
22
|
-
if (index !== -1) {
|
|
23
|
-
this.activeIndices.splice(index, 1);
|
|
24
|
-
}
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
has(key) {
|
|
30
|
-
return this.data[key] !== undefined;
|
|
31
|
-
}
|
|
32
|
-
get size() {
|
|
33
|
-
return this.activeIndices.length;
|
|
34
|
-
}
|
|
35
|
-
clear() {
|
|
36
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
37
|
-
const key = this.activeIndices[i];
|
|
38
|
-
this.data[key] = undefined;
|
|
39
|
-
}
|
|
40
|
-
this.activeIndices = [];
|
|
41
|
-
}
|
|
42
|
-
*[Symbol.iterator]() {
|
|
43
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
44
|
-
const key = this.activeIndices[i];
|
|
45
|
-
yield [key, this.data[key]];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
forEach(callback) {
|
|
49
|
-
for (let i = 0; i < this.activeIndices.length; i++) {
|
|
50
|
-
const key = this.activeIndices[i];
|
|
51
|
-
callback(this.data[key], key, this);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
3
|
class Note {
|
|
56
4
|
constructor(noteNumber, velocity, startTime, voice, voiceParams) {
|
|
5
|
+
Object.defineProperty(this, "index", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: -1
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "ending", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: false
|
|
16
|
+
});
|
|
57
17
|
Object.defineProperty(this, "bufferSource", {
|
|
58
18
|
enumerable: true,
|
|
59
19
|
configurable: true,
|
|
@@ -127,7 +87,7 @@ const defaultControllerState = {
|
|
|
127
87
|
modulationDepth: { type: 128 + 1, defaultValue: 0 },
|
|
128
88
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
129
89
|
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
130
|
-
pan: { type: 128 + 10, defaultValue:
|
|
90
|
+
pan: { type: 128 + 10, defaultValue: 64 / 127 },
|
|
131
91
|
expression: { type: 128 + 11, defaultValue: 1 },
|
|
132
92
|
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
133
93
|
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
@@ -338,7 +298,7 @@ export class MidyGMLite {
|
|
|
338
298
|
initSoundFontTable() {
|
|
339
299
|
const table = new Array(128);
|
|
340
300
|
for (let i = 0; i < 128; i++) {
|
|
341
|
-
table[i] = new
|
|
301
|
+
table[i] = new Map();
|
|
342
302
|
}
|
|
343
303
|
return table;
|
|
344
304
|
}
|
|
@@ -354,17 +314,37 @@ export class MidyGMLite {
|
|
|
354
314
|
}
|
|
355
315
|
}
|
|
356
316
|
}
|
|
357
|
-
async loadSoundFont(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
317
|
+
async loadSoundFont(input) {
|
|
318
|
+
let uint8Array;
|
|
319
|
+
if (typeof input === "string") {
|
|
320
|
+
const response = await fetch(input);
|
|
321
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
322
|
+
uint8Array = new Uint8Array(arrayBuffer);
|
|
323
|
+
}
|
|
324
|
+
else if (input instanceof Uint8Array) {
|
|
325
|
+
uint8Array = input;
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
throw new TypeError("input must be a URL string or Uint8Array");
|
|
329
|
+
}
|
|
330
|
+
const parsed = parse(uint8Array);
|
|
361
331
|
const soundFont = new SoundFont(parsed);
|
|
362
332
|
this.addSoundFont(soundFont);
|
|
363
333
|
}
|
|
364
|
-
async loadMIDI(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
334
|
+
async loadMIDI(input) {
|
|
335
|
+
let uint8Array;
|
|
336
|
+
if (typeof input === "string") {
|
|
337
|
+
const response = await fetch(input);
|
|
338
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
339
|
+
uint8Array = new Uint8Array(arrayBuffer);
|
|
340
|
+
}
|
|
341
|
+
else if (input instanceof Uint8Array) {
|
|
342
|
+
uint8Array = input;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
throw new TypeError("input must be a URL string or Uint8Array");
|
|
346
|
+
}
|
|
347
|
+
const midi = parseMidi(uint8Array);
|
|
368
348
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
369
349
|
const midiData = this.extractMidiData(midi);
|
|
370
350
|
this.instruments = midiData.instruments;
|
|
@@ -390,10 +370,10 @@ export class MidyGMLite {
|
|
|
390
370
|
return {
|
|
391
371
|
currentBufferSource: null,
|
|
392
372
|
isDrum: false,
|
|
393
|
-
...this.constructor.channelSettings,
|
|
394
373
|
state: new ControllerState(),
|
|
374
|
+
...this.constructor.channelSettings,
|
|
395
375
|
...this.setChannelAudioNodes(audioContext),
|
|
396
|
-
scheduledNotes:
|
|
376
|
+
scheduledNotes: [],
|
|
397
377
|
sustainNotes: [],
|
|
398
378
|
};
|
|
399
379
|
});
|
|
@@ -428,34 +408,33 @@ export class MidyGMLite {
|
|
|
428
408
|
return audioBuffer;
|
|
429
409
|
}
|
|
430
410
|
}
|
|
431
|
-
createBufferSource(voiceParams, audioBuffer) {
|
|
411
|
+
createBufferSource(channel, voiceParams, audioBuffer) {
|
|
432
412
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
433
413
|
bufferSource.buffer = audioBuffer;
|
|
434
414
|
bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
|
|
415
|
+
if (channel.isDrum)
|
|
416
|
+
bufferSource.loop = false;
|
|
435
417
|
if (bufferSource.loop) {
|
|
436
418
|
bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
437
419
|
bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
|
|
438
420
|
}
|
|
439
421
|
return bufferSource;
|
|
440
422
|
}
|
|
441
|
-
async scheduleTimelineEvents(t,
|
|
423
|
+
async scheduleTimelineEvents(t, resumeTime, queueIndex) {
|
|
442
424
|
while (queueIndex < this.timeline.length) {
|
|
443
425
|
const event = this.timeline[queueIndex];
|
|
444
426
|
if (event.startTime > t + this.lookAhead)
|
|
445
427
|
break;
|
|
446
|
-
const
|
|
428
|
+
const delay = this.startDelay - resumeTime;
|
|
429
|
+
const startTime = event.startTime + delay;
|
|
447
430
|
switch (event.type) {
|
|
448
431
|
case "noteOn":
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
/* falls through */
|
|
432
|
+
await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
433
|
+
break;
|
|
454
434
|
case "noteOff": {
|
|
455
435
|
const notePromise = this.scheduleNoteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
456
|
-
if (notePromise)
|
|
436
|
+
if (notePromise)
|
|
457
437
|
this.notePromises.push(notePromise);
|
|
458
|
-
}
|
|
459
438
|
break;
|
|
460
439
|
}
|
|
461
440
|
case "controller":
|
|
@@ -488,44 +467,53 @@ export class MidyGMLite {
|
|
|
488
467
|
this.isPaused = false;
|
|
489
468
|
this.startTime = this.audioContext.currentTime;
|
|
490
469
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
491
|
-
let
|
|
470
|
+
let resumeTime = this.resumeTime - this.startTime;
|
|
492
471
|
this.notePromises = [];
|
|
493
472
|
const schedulePlayback = async () => {
|
|
494
473
|
if (queueIndex >= this.timeline.length) {
|
|
495
474
|
await Promise.all(this.notePromises);
|
|
496
475
|
this.notePromises = [];
|
|
497
476
|
this.exclusiveClassNotes.fill(undefined);
|
|
477
|
+
this.drumExclusiveClassNotes.fill(undefined);
|
|
498
478
|
this.audioBufferCache.clear();
|
|
479
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
480
|
+
this.resetAllStates(i);
|
|
481
|
+
}
|
|
499
482
|
resolve();
|
|
500
483
|
return;
|
|
501
484
|
}
|
|
502
485
|
const now = this.audioContext.currentTime;
|
|
503
|
-
const t = now +
|
|
504
|
-
queueIndex = await this.scheduleTimelineEvents(t,
|
|
486
|
+
const t = now + resumeTime;
|
|
487
|
+
queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
|
|
505
488
|
if (this.isPausing) {
|
|
506
489
|
await this.stopNotes(0, true, now);
|
|
507
490
|
this.notePromises = [];
|
|
508
|
-
resolve();
|
|
509
491
|
this.isPausing = false;
|
|
510
492
|
this.isPaused = true;
|
|
493
|
+
resolve();
|
|
511
494
|
return;
|
|
512
495
|
}
|
|
513
496
|
else if (this.isStopping) {
|
|
514
497
|
await this.stopNotes(0, true, now);
|
|
515
498
|
this.notePromises = [];
|
|
516
499
|
this.exclusiveClassNotes.fill(undefined);
|
|
500
|
+
this.drumExclusiveClassNotes.fill(undefined);
|
|
517
501
|
this.audioBufferCache.clear();
|
|
518
|
-
|
|
502
|
+
for (let i = 0; i < this.channels.length; i++) {
|
|
503
|
+
this.resetAllStates(i);
|
|
504
|
+
}
|
|
519
505
|
this.isStopping = false;
|
|
520
506
|
this.isPaused = false;
|
|
507
|
+
resolve();
|
|
521
508
|
return;
|
|
522
509
|
}
|
|
523
510
|
else if (this.isSeeking) {
|
|
524
511
|
this.stopNotes(0, true, now);
|
|
525
512
|
this.exclusiveClassNotes.fill(undefined);
|
|
513
|
+
this.drumExclusiveClassNotes.fill(undefined);
|
|
526
514
|
this.startTime = this.audioContext.currentTime;
|
|
527
515
|
queueIndex = this.getQueueIndex(this.resumeTime);
|
|
528
|
-
|
|
516
|
+
resumeTime = this.resumeTime - this.startTime;
|
|
529
517
|
this.isSeeking = false;
|
|
530
518
|
await schedulePlayback();
|
|
531
519
|
}
|
|
@@ -548,6 +536,7 @@ export class MidyGMLite {
|
|
|
548
536
|
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
549
537
|
}
|
|
550
538
|
extractMidiData(midi) {
|
|
539
|
+
this.audioBufferCounter.clear();
|
|
551
540
|
const instruments = new Set();
|
|
552
541
|
const timeline = [];
|
|
553
542
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -616,9 +605,8 @@ export class MidyGMLite {
|
|
|
616
605
|
stopActiveNotes(channelNumber, velocity, force, scheduleTime) {
|
|
617
606
|
const channel = this.channels[channelNumber];
|
|
618
607
|
const promises = [];
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force, undefined);
|
|
608
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
609
|
+
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
622
610
|
this.notePromises.push(promise);
|
|
623
611
|
promises.push(promise);
|
|
624
612
|
});
|
|
@@ -632,7 +620,7 @@ export class MidyGMLite {
|
|
|
632
620
|
this.notePromises.push(promise);
|
|
633
621
|
promises.push(promise);
|
|
634
622
|
});
|
|
635
|
-
channel.scheduledNotes
|
|
623
|
+
channel.scheduledNotes = [];
|
|
636
624
|
return Promise.all(promises);
|
|
637
625
|
}
|
|
638
626
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -653,9 +641,6 @@ export class MidyGMLite {
|
|
|
653
641
|
if (!this.isPlaying)
|
|
654
642
|
return;
|
|
655
643
|
this.isStopping = true;
|
|
656
|
-
for (let i = 0; i < this.channels.length; i++) {
|
|
657
|
-
this.resetAllStates(i);
|
|
658
|
-
}
|
|
659
644
|
}
|
|
660
645
|
pause() {
|
|
661
646
|
if (!this.isPlaying || this.isPaused)
|
|
@@ -690,37 +675,28 @@ export class MidyGMLite {
|
|
|
690
675
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
691
676
|
}
|
|
692
677
|
processScheduledNotes(channel, callback) {
|
|
693
|
-
channel.scheduledNotes
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
getActiveNotes(channel, scheduleTime) {
|
|
705
|
-
const activeNotes = new SparseMap(128);
|
|
706
|
-
channel.scheduledNotes.forEach((noteList) => {
|
|
707
|
-
const activeNote = this.getActiveNote(noteList, scheduleTime);
|
|
708
|
-
if (activeNote) {
|
|
709
|
-
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
return activeNotes;
|
|
678
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
679
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
680
|
+
const note = scheduledNotes[i];
|
|
681
|
+
if (!note)
|
|
682
|
+
continue;
|
|
683
|
+
if (note.ending)
|
|
684
|
+
continue;
|
|
685
|
+
callback(note);
|
|
686
|
+
}
|
|
713
687
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
688
|
+
processActiveNotes(channel, scheduleTime, callback) {
|
|
689
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
690
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
691
|
+
const note = scheduledNotes[i];
|
|
717
692
|
if (!note)
|
|
718
|
-
|
|
693
|
+
continue;
|
|
694
|
+
if (note.ending)
|
|
695
|
+
continue;
|
|
719
696
|
if (scheduleTime < note.startTime)
|
|
720
697
|
continue;
|
|
721
|
-
|
|
698
|
+
callback(note);
|
|
722
699
|
}
|
|
723
|
-
return noteList[0];
|
|
724
700
|
}
|
|
725
701
|
cbToRatio(cb) {
|
|
726
702
|
return Math.pow(10, cb / 200);
|
|
@@ -858,7 +834,7 @@ export class MidyGMLite {
|
|
|
858
834
|
const voiceParams = voice.getAllParams(controllerState);
|
|
859
835
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
860
836
|
const audioBuffer = await this.getAudioBuffer(channel.programNumber, noteNumber, velocity, voiceParams, isSF3);
|
|
861
|
-
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
837
|
+
note.bufferSource = this.createBufferSource(channel, voiceParams, audioBuffer);
|
|
862
838
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
863
839
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
864
840
|
type: "lowpass",
|
|
@@ -867,6 +843,7 @@ export class MidyGMLite {
|
|
|
867
843
|
this.setVolumeEnvelope(note, now);
|
|
868
844
|
this.setFilterEnvelope(note, now);
|
|
869
845
|
this.setPitchEnvelope(note, now);
|
|
846
|
+
this.updateDetune(channel, note, now);
|
|
870
847
|
if (0 < state.modulationDepth) {
|
|
871
848
|
this.startModulation(channel, note, now);
|
|
872
849
|
}
|
|
@@ -904,7 +881,7 @@ export class MidyGMLite {
|
|
|
904
881
|
}
|
|
905
882
|
this.drumExclusiveClassNotes[index] = note;
|
|
906
883
|
}
|
|
907
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
884
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime, noteOffEvent) {
|
|
908
885
|
const channel = this.channels[channelNumber];
|
|
909
886
|
const bankNumber = channel.bank;
|
|
910
887
|
const soundFontIndex = this.soundFontTable[channel.programNumber].get(bankNumber);
|
|
@@ -916,6 +893,7 @@ export class MidyGMLite {
|
|
|
916
893
|
return;
|
|
917
894
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
918
895
|
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
896
|
+
note.noteOffEvent = noteOffEvent;
|
|
919
897
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
920
898
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
921
899
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -924,31 +902,12 @@ export class MidyGMLite {
|
|
|
924
902
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
925
903
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
926
904
|
const scheduledNotes = channel.scheduledNotes;
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
noteList.push(note);
|
|
930
|
-
}
|
|
931
|
-
else {
|
|
932
|
-
noteList = [note];
|
|
933
|
-
scheduledNotes.set(noteNumber, noteList);
|
|
934
|
-
}
|
|
935
|
-
if (channel.isDrum) {
|
|
936
|
-
const stopTime = startTime + note.bufferSource.buffer.duration;
|
|
937
|
-
const index = noteList.length - 1;
|
|
938
|
-
const promise = new Promise((resolve) => {
|
|
939
|
-
note.bufferSource.onended = () => {
|
|
940
|
-
noteList[index] = undefined;
|
|
941
|
-
this.disconnectNote(note);
|
|
942
|
-
resolve();
|
|
943
|
-
};
|
|
944
|
-
note.bufferSource.stop(stopTime);
|
|
945
|
-
});
|
|
946
|
-
this.notePromises.push(promise);
|
|
947
|
-
}
|
|
905
|
+
note.index = scheduledNotes.length;
|
|
906
|
+
scheduledNotes.push(note);
|
|
948
907
|
}
|
|
949
908
|
noteOn(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
950
909
|
scheduleTime ??= this.audioContext.currentTime;
|
|
951
|
-
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime);
|
|
910
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, scheduleTime, undefined);
|
|
952
911
|
}
|
|
953
912
|
disconnectNote(note) {
|
|
954
913
|
note.bufferSource.disconnect();
|
|
@@ -960,57 +919,54 @@ export class MidyGMLite {
|
|
|
960
919
|
note.modulationLFO.stop();
|
|
961
920
|
}
|
|
962
921
|
}
|
|
963
|
-
|
|
964
|
-
const
|
|
922
|
+
releaseNote(channel, note, endTime) {
|
|
923
|
+
const volRelease = endTime + note.voiceParams.volRelease;
|
|
924
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
925
|
+
const stopTime = Math.min(volRelease, modRelease);
|
|
926
|
+
note.filterNode.frequency
|
|
927
|
+
.cancelScheduledValues(endTime)
|
|
928
|
+
.linearRampToValueAtTime(0, modRelease);
|
|
965
929
|
note.volumeEnvelopeNode.gain
|
|
966
930
|
.cancelScheduledValues(endTime)
|
|
967
|
-
.linearRampToValueAtTime(0,
|
|
968
|
-
note.ending = true;
|
|
969
|
-
this.scheduleTask(() => {
|
|
970
|
-
note.bufferSource.loop = false;
|
|
971
|
-
}, stopTime);
|
|
931
|
+
.linearRampToValueAtTime(0, volRelease);
|
|
972
932
|
return new Promise((resolve) => {
|
|
973
|
-
|
|
974
|
-
|
|
933
|
+
this.scheduleTask(() => {
|
|
934
|
+
const bufferSource = note.bufferSource;
|
|
935
|
+
bufferSource.loop = false;
|
|
936
|
+
bufferSource.stop(stopTime);
|
|
975
937
|
this.disconnectNote(note);
|
|
938
|
+
channel.scheduledNotes[note.index] = undefined;
|
|
976
939
|
resolve();
|
|
977
|
-
};
|
|
978
|
-
note.bufferSource.stop(stopTime);
|
|
940
|
+
}, stopTime);
|
|
979
941
|
});
|
|
980
942
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
943
|
+
scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
944
|
+
const channel = this.channels[channelNumber];
|
|
945
|
+
if (!force) {
|
|
946
|
+
if (channel.isDrum)
|
|
947
|
+
return;
|
|
948
|
+
if (0.5 <= channel.state.sustainPedal)
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const note = this.findNoteOffTarget(channel, noteNumber);
|
|
952
|
+
if (!note)
|
|
953
|
+
return;
|
|
954
|
+
note.ending = true;
|
|
955
|
+
this.releaseNote(channel, note, endTime);
|
|
956
|
+
}
|
|
957
|
+
findNoteOffTarget(channel, noteNumber) {
|
|
958
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
959
|
+
for (let i = 0; i < scheduledNotes.length; i++) {
|
|
960
|
+
const note = scheduledNotes[i];
|
|
984
961
|
if (!note)
|
|
985
962
|
continue;
|
|
986
963
|
if (note.ending)
|
|
987
964
|
continue;
|
|
988
|
-
|
|
965
|
+
if (note.noteNumber !== noteNumber)
|
|
966
|
+
continue;
|
|
967
|
+
return note;
|
|
989
968
|
}
|
|
990
969
|
}
|
|
991
|
-
scheduleNoteOff(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
992
|
-
const channel = this.channels[channelNumber];
|
|
993
|
-
if (channel.isDrum)
|
|
994
|
-
return;
|
|
995
|
-
if (!force && 0.5 <= channel.state.sustainPedal)
|
|
996
|
-
return;
|
|
997
|
-
if (!channel.scheduledNotes.has(noteNumber))
|
|
998
|
-
return;
|
|
999
|
-
const noteList = channel.scheduledNotes.get(noteNumber);
|
|
1000
|
-
if (!noteList)
|
|
1001
|
-
return; // be careful with drum channel
|
|
1002
|
-
const noteOffTarget = this.findNoteOffTarget(noteList, endTime);
|
|
1003
|
-
if (!noteOffTarget)
|
|
1004
|
-
return;
|
|
1005
|
-
const [note, i] = noteOffTarget;
|
|
1006
|
-
const volRelease = endTime + note.voiceParams.volRelease;
|
|
1007
|
-
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1008
|
-
note.filterNode.frequency
|
|
1009
|
-
.cancelScheduledValues(endTime)
|
|
1010
|
-
.linearRampToValueAtTime(0, modRelease);
|
|
1011
|
-
const stopTime = Math.min(volRelease, modRelease);
|
|
1012
|
-
return this.stopNote(endTime, stopTime, noteList, i);
|
|
1013
|
-
}
|
|
1014
970
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1015
971
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1016
972
|
return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
|
|
@@ -1020,7 +976,7 @@ export class MidyGMLite {
|
|
|
1020
976
|
const channel = this.channels[channelNumber];
|
|
1021
977
|
const promises = [];
|
|
1022
978
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1023
|
-
const promise = this.
|
|
979
|
+
const promise = this.scheduleNoteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
|
|
1024
980
|
promises.push(promise);
|
|
1025
981
|
}
|
|
1026
982
|
channel.sustainNotes = [];
|
|
@@ -1176,20 +1132,20 @@ export class MidyGMLite {
|
|
|
1176
1132
|
});
|
|
1177
1133
|
}
|
|
1178
1134
|
createControlChangeHandlers() {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1135
|
+
const handlers = new Array(128);
|
|
1136
|
+
handlers[1] = this.setModulationDepth;
|
|
1137
|
+
handlers[6] = this.dataEntryMSB;
|
|
1138
|
+
handlers[7] = this.setVolume;
|
|
1139
|
+
handlers[10] = this.setPan;
|
|
1140
|
+
handlers[11] = this.setExpression;
|
|
1141
|
+
handlers[38] = this.dataEntryLSB;
|
|
1142
|
+
handlers[64] = this.setSustainPedal;
|
|
1143
|
+
handlers[100] = this.setRPNLSB;
|
|
1144
|
+
handlers[101] = this.setRPNMSB;
|
|
1145
|
+
handlers[120] = this.allSoundOff;
|
|
1146
|
+
handlers[121] = this.resetAllControllers;
|
|
1147
|
+
handlers[123] = this.allNotesOff;
|
|
1148
|
+
return handlers;
|
|
1193
1149
|
}
|
|
1194
1150
|
handleControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1195
1151
|
const handler = this.controlChangeHandlers[controllerType];
|
|
@@ -1334,19 +1290,26 @@ export class MidyGMLite {
|
|
|
1334
1290
|
return this.stopActiveNotes(channelNumber, 0, true, scheduleTime);
|
|
1335
1291
|
}
|
|
1336
1292
|
resetAllStates(channelNumber) {
|
|
1293
|
+
const scheduleTime = this.audioContext.currentTime;
|
|
1337
1294
|
const channel = this.channels[channelNumber];
|
|
1338
1295
|
const state = channel.state;
|
|
1339
|
-
|
|
1340
|
-
|
|
1296
|
+
const entries = Object.entries(defaultControllerState);
|
|
1297
|
+
for (const [key, { type, defaultValue }] of entries) {
|
|
1298
|
+
if (128 <= type) {
|
|
1299
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
state[key] = defaultValue;
|
|
1303
|
+
}
|
|
1341
1304
|
}
|
|
1342
|
-
for (const
|
|
1343
|
-
channel[
|
|
1305
|
+
for (const key of Object.keys(this.constructor.channelSettings)) {
|
|
1306
|
+
channel[key] = this.constructor.channelSettings[key];
|
|
1344
1307
|
}
|
|
1345
1308
|
this.mode = "GM1";
|
|
1346
1309
|
}
|
|
1347
1310
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
|
|
1348
|
-
resetAllControllers(channelNumber) {
|
|
1349
|
-
const
|
|
1311
|
+
resetAllControllers(channelNumber, _value, scheduleTime) {
|
|
1312
|
+
const keys = [
|
|
1350
1313
|
"pitchWheel",
|
|
1351
1314
|
"expression",
|
|
1352
1315
|
"modulationDepth",
|
|
@@ -1354,10 +1317,17 @@ export class MidyGMLite {
|
|
|
1354
1317
|
];
|
|
1355
1318
|
const channel = this.channels[channelNumber];
|
|
1356
1319
|
const state = channel.state;
|
|
1357
|
-
for (let i = 0; i <
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1320
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1321
|
+
const key = keys[i];
|
|
1322
|
+
const { type, defaultValue } = defaultControllerState[key];
|
|
1323
|
+
if (128 <= type) {
|
|
1324
|
+
this.handleControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
state[key] = defaultValue;
|
|
1328
|
+
}
|
|
1360
1329
|
}
|
|
1330
|
+
this.setPitchBend(channelNumber, 8192, scheduleTime);
|
|
1361
1331
|
const settingTypes = [
|
|
1362
1332
|
"rpnMSB",
|
|
1363
1333
|
"rpnLSB",
|