@marmooo/midy 0.3.7 → 0.4.0
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 +31 -11
- package/esm/midy-GM1.d.ts +15 -30
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +161 -104
- package/esm/midy-GM2.d.ts +19 -36
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +245 -205
- package/esm/midy-GMLite.d.ts +14 -30
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +163 -107
- package/esm/midy.d.ts +19 -37
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +306 -277
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +15 -30
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +161 -104
- package/script/midy-GM2.d.ts +19 -36
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +245 -205
- package/script/midy-GMLite.d.ts +14 -30
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +163 -107
- package/script/midy.d.ts +19 -37
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +306 -277
package/script/midy-GM2.js
CHANGED
|
@@ -4,7 +4,19 @@ exports.MidyGM2 = void 0;
|
|
|
4
4
|
const midi_file_1 = require("midi-file");
|
|
5
5
|
const soundfont_parser_1 = require("@marmooo/soundfont-parser");
|
|
6
6
|
class Note {
|
|
7
|
-
constructor(noteNumber, velocity, startTime
|
|
7
|
+
constructor(noteNumber, velocity, startTime) {
|
|
8
|
+
Object.defineProperty(this, "voice", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: void 0
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "voiceParams", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: void 0
|
|
19
|
+
});
|
|
8
20
|
Object.defineProperty(this, "index", {
|
|
9
21
|
enumerable: true,
|
|
10
22
|
configurable: true,
|
|
@@ -17,6 +29,12 @@ class Note {
|
|
|
17
29
|
writable: true,
|
|
18
30
|
value: false
|
|
19
31
|
});
|
|
32
|
+
Object.defineProperty(this, "pending", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: true
|
|
37
|
+
});
|
|
20
38
|
Object.defineProperty(this, "bufferSource", {
|
|
21
39
|
enumerable: true,
|
|
22
40
|
configurable: true,
|
|
@@ -92,8 +110,6 @@ class Note {
|
|
|
92
110
|
this.noteNumber = noteNumber;
|
|
93
111
|
this.velocity = velocity;
|
|
94
112
|
this.startTime = startTime;
|
|
95
|
-
this.voice = voice;
|
|
96
|
-
this.voiceParams = voiceParams;
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
115
|
const drumExclusiveClassesByKit = new Array(57);
|
|
@@ -275,6 +291,18 @@ class MidyGM2 {
|
|
|
275
291
|
writable: true,
|
|
276
292
|
value: 0
|
|
277
293
|
});
|
|
294
|
+
Object.defineProperty(this, "lastActiveSensing", {
|
|
295
|
+
enumerable: true,
|
|
296
|
+
configurable: true,
|
|
297
|
+
writable: true,
|
|
298
|
+
value: 0
|
|
299
|
+
});
|
|
300
|
+
Object.defineProperty(this, "activeSensingThreshold", {
|
|
301
|
+
enumerable: true,
|
|
302
|
+
configurable: true,
|
|
303
|
+
writable: true,
|
|
304
|
+
value: 0.3
|
|
305
|
+
});
|
|
278
306
|
Object.defineProperty(this, "noteCheckInterval", {
|
|
279
307
|
enumerable: true,
|
|
280
308
|
configurable: true,
|
|
@@ -315,7 +343,7 @@ class MidyGM2 {
|
|
|
315
343
|
enumerable: true,
|
|
316
344
|
configurable: true,
|
|
317
345
|
writable: true,
|
|
318
|
-
value:
|
|
346
|
+
value: Array.from({ length: 128 }, () => [])
|
|
319
347
|
});
|
|
320
348
|
Object.defineProperty(this, "voiceCounter", {
|
|
321
349
|
enumerable: true,
|
|
@@ -329,6 +357,12 @@ class MidyGM2 {
|
|
|
329
357
|
writable: true,
|
|
330
358
|
value: new Map()
|
|
331
359
|
});
|
|
360
|
+
Object.defineProperty(this, "realtimeVoiceCache", {
|
|
361
|
+
enumerable: true,
|
|
362
|
+
configurable: true,
|
|
363
|
+
writable: true,
|
|
364
|
+
value: new Map()
|
|
365
|
+
});
|
|
332
366
|
Object.defineProperty(this, "isPlaying", {
|
|
333
367
|
enumerable: true,
|
|
334
368
|
configurable: true,
|
|
@@ -402,8 +436,10 @@ class MidyGM2 {
|
|
|
402
436
|
length: 1,
|
|
403
437
|
sampleRate: audioContext.sampleRate,
|
|
404
438
|
});
|
|
439
|
+
this.messageHandlers = this.createMessageHandlers();
|
|
405
440
|
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
406
441
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
442
|
+
this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
|
|
407
443
|
this.channels = this.createChannels(audioContext);
|
|
408
444
|
this.reverbEffect = this.createReverbEffect(audioContext);
|
|
409
445
|
this.chorusEffect = this.createChorusEffect(audioContext);
|
|
@@ -413,21 +449,14 @@ class MidyGM2 {
|
|
|
413
449
|
this.scheduler.connect(audioContext.destination);
|
|
414
450
|
this.GM2SystemOn();
|
|
415
451
|
}
|
|
416
|
-
initSoundFontTable() {
|
|
417
|
-
const table = new Array(128);
|
|
418
|
-
for (let i = 0; i < 128; i++) {
|
|
419
|
-
table[i] = new Map();
|
|
420
|
-
}
|
|
421
|
-
return table;
|
|
422
|
-
}
|
|
423
452
|
addSoundFont(soundFont) {
|
|
424
453
|
const index = this.soundFonts.length;
|
|
425
454
|
this.soundFonts.push(soundFont);
|
|
426
455
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
456
|
+
const soundFontTable = this.soundFontTable;
|
|
427
457
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
banks.set(presetHeader.bank, index);
|
|
458
|
+
const { preset, bank } = presetHeaders[i];
|
|
459
|
+
soundFontTable[preset][bank] = index;
|
|
431
460
|
}
|
|
432
461
|
}
|
|
433
462
|
async toUint8Array(input) {
|
|
@@ -505,13 +534,17 @@ class MidyGM2 {
|
|
|
505
534
|
this.GM2SystemOn();
|
|
506
535
|
}
|
|
507
536
|
getVoiceId(channel, noteNumber, velocity) {
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
|
|
537
|
+
const programNumber = channel.programNumber;
|
|
538
|
+
const bankTable = this.soundFontTable[programNumber];
|
|
539
|
+
if (!bankTable)
|
|
540
|
+
return;
|
|
541
|
+
const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
|
|
542
|
+
const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
|
|
543
|
+
const soundFontIndex = bankTable[bank];
|
|
511
544
|
if (soundFontIndex === undefined)
|
|
512
545
|
return;
|
|
513
546
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
514
|
-
const voice = soundFont.getVoice(
|
|
547
|
+
const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
515
548
|
const { instrument, sampleID } = voice.generators;
|
|
516
549
|
return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
|
|
517
550
|
}
|
|
@@ -580,19 +613,22 @@ class MidyGM2 {
|
|
|
580
613
|
}
|
|
581
614
|
return bufferSource;
|
|
582
615
|
}
|
|
583
|
-
async scheduleTimelineEvents(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
616
|
+
async scheduleTimelineEvents(scheduleTime, queueIndex) {
|
|
617
|
+
const timeOffset = this.resumeTime - this.startTime;
|
|
618
|
+
const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
|
|
619
|
+
const schedulingOffset = this.startDelay - timeOffset;
|
|
620
|
+
const timeline = this.timeline;
|
|
621
|
+
while (queueIndex < timeline.length) {
|
|
622
|
+
const event = timeline[queueIndex];
|
|
623
|
+
if (lookAheadCheckTime < event.startTime)
|
|
587
624
|
break;
|
|
588
|
-
const
|
|
589
|
-
const startTime = event.startTime + delay;
|
|
625
|
+
const startTime = event.startTime + schedulingOffset;
|
|
590
626
|
switch (event.type) {
|
|
591
627
|
case "noteOn":
|
|
592
|
-
await this.
|
|
628
|
+
await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
593
629
|
break;
|
|
594
630
|
case "noteOff": {
|
|
595
|
-
const notePromise = this.
|
|
631
|
+
const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
596
632
|
if (notePromise)
|
|
597
633
|
this.notePromises.push(notePromise);
|
|
598
634
|
break;
|
|
@@ -628,6 +664,7 @@ class MidyGM2 {
|
|
|
628
664
|
this.exclusiveClassNotes.fill(undefined);
|
|
629
665
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
630
666
|
this.voiceCache.clear();
|
|
667
|
+
this.realtimeVoiceCache.clear();
|
|
631
668
|
for (let i = 0; i < this.channels.length; i++) {
|
|
632
669
|
this.channels[i].scheduledNotes = [];
|
|
633
670
|
this.resetChannelStates(i);
|
|
@@ -661,13 +698,17 @@ class MidyGM2 {
|
|
|
661
698
|
this.isPaused = false;
|
|
662
699
|
this.startTime = this.audioContext.currentTime;
|
|
663
700
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
664
|
-
let resumeTime = this.resumeTime - this.startTime;
|
|
665
701
|
let finished = false;
|
|
666
702
|
this.notePromises = [];
|
|
667
703
|
while (queueIndex < this.timeline.length) {
|
|
668
704
|
const now = this.audioContext.currentTime;
|
|
669
|
-
|
|
670
|
-
|
|
705
|
+
if (0 < this.lastActiveSensing &&
|
|
706
|
+
this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
|
|
707
|
+
await this.stopNotes(0, true, now);
|
|
708
|
+
await this.audioContext.suspend();
|
|
709
|
+
finished = true;
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
671
712
|
if (this.isPausing) {
|
|
672
713
|
await this.stopNotes(0, true, now);
|
|
673
714
|
await this.audioContext.suspend();
|
|
@@ -686,16 +727,17 @@ class MidyGM2 {
|
|
|
686
727
|
const nextQueueIndex = this.getQueueIndex(this.resumeTime);
|
|
687
728
|
this.updateStates(queueIndex, nextQueueIndex);
|
|
688
729
|
queueIndex = nextQueueIndex;
|
|
689
|
-
resumeTime = this.resumeTime - this.startTime;
|
|
690
730
|
this.isSeeking = false;
|
|
691
731
|
continue;
|
|
692
732
|
}
|
|
733
|
+
queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
|
|
693
734
|
const waitTime = now + this.noteCheckInterval;
|
|
694
735
|
await this.scheduleTask(() => { }, waitTime);
|
|
695
736
|
}
|
|
696
737
|
if (finished) {
|
|
697
738
|
this.notePromises = [];
|
|
698
739
|
this.resetAllStates();
|
|
740
|
+
this.lastActiveSensing = 0;
|
|
699
741
|
}
|
|
700
742
|
this.isPlaying = false;
|
|
701
743
|
}
|
|
@@ -705,17 +747,17 @@ class MidyGM2 {
|
|
|
705
747
|
secondToTicks(second, secondsPerBeat) {
|
|
706
748
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
707
749
|
}
|
|
750
|
+
getSoundFontId(channel) {
|
|
751
|
+
const programNumber = channel.programNumber;
|
|
752
|
+
const bankNumber = channel.isDrum ? 128 : channel.bankLSB;
|
|
753
|
+
const bank = bankNumber.toString().padStart(3, "0");
|
|
754
|
+
const program = programNumber.toString().padStart(3, "0");
|
|
755
|
+
return `${bank}:${program}`;
|
|
756
|
+
}
|
|
708
757
|
extractMidiData(midi) {
|
|
709
758
|
const instruments = new Set();
|
|
710
759
|
const timeline = [];
|
|
711
|
-
const
|
|
712
|
-
for (let i = 0; i < tmpChannels.length; i++) {
|
|
713
|
-
tmpChannels[i] = {
|
|
714
|
-
programNumber: -1,
|
|
715
|
-
bankMSB: this.channels[i].bankMSB,
|
|
716
|
-
bankLSB: this.channels[i].bankLSB,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
760
|
+
const channels = this.channels;
|
|
719
761
|
for (let i = 0; i < midi.tracks.length; i++) {
|
|
720
762
|
const track = midi.tracks[i];
|
|
721
763
|
let currentTicks = 0;
|
|
@@ -725,48 +767,40 @@ class MidyGM2 {
|
|
|
725
767
|
event.ticks = currentTicks;
|
|
726
768
|
switch (event.type) {
|
|
727
769
|
case "noteOn": {
|
|
728
|
-
const channel =
|
|
729
|
-
|
|
730
|
-
channel.programNumber = event.programNumber;
|
|
731
|
-
switch (channel.bankMSB) {
|
|
732
|
-
case 120:
|
|
733
|
-
instruments.add(`128:0`);
|
|
734
|
-
break;
|
|
735
|
-
case 121:
|
|
736
|
-
instruments.add(`${channel.bankLSB}:0`);
|
|
737
|
-
break;
|
|
738
|
-
default: {
|
|
739
|
-
const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
|
|
740
|
-
instruments.add(`${bankNumber}:0`);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
channel.programNumber = 0;
|
|
744
|
-
}
|
|
770
|
+
const channel = channels[event.channel];
|
|
771
|
+
instruments.add(this.getSoundFontId(channel));
|
|
745
772
|
break;
|
|
746
773
|
}
|
|
747
774
|
case "controller":
|
|
748
775
|
switch (event.controllerType) {
|
|
749
776
|
case 0:
|
|
750
|
-
|
|
777
|
+
this.setBankMSB(event.channel, event.value);
|
|
751
778
|
break;
|
|
752
779
|
case 32:
|
|
753
|
-
|
|
780
|
+
this.setBankLSB(event.channel, event.value);
|
|
754
781
|
break;
|
|
755
782
|
}
|
|
756
783
|
break;
|
|
757
784
|
case "programChange": {
|
|
758
|
-
const channel =
|
|
759
|
-
channel
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
785
|
+
const channel = channels[event.channel];
|
|
786
|
+
this.setProgramChange(event.channel, event.programNumber);
|
|
787
|
+
instruments.add(this.getSoundFontId(channel));
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case "sysEx": {
|
|
791
|
+
const data = event.data;
|
|
792
|
+
if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
|
|
793
|
+
switch (data[3]) {
|
|
794
|
+
case 1:
|
|
795
|
+
this.GM1SystemOn(scheduleTime);
|
|
796
|
+
break;
|
|
797
|
+
case 2: // GM System Off
|
|
798
|
+
break;
|
|
799
|
+
case 3:
|
|
800
|
+
this.GM2SystemOn(scheduleTime);
|
|
801
|
+
break;
|
|
802
|
+
default:
|
|
803
|
+
console.warn(`Unsupported Exclusive Message: ${data}`);
|
|
770
804
|
}
|
|
771
805
|
}
|
|
772
806
|
}
|
|
@@ -805,7 +839,7 @@ class MidyGM2 {
|
|
|
805
839
|
const channel = this.channels[channelNumber];
|
|
806
840
|
const promises = [];
|
|
807
841
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
808
|
-
const promise = this.
|
|
842
|
+
const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
809
843
|
this.notePromises.push(promise);
|
|
810
844
|
promises.push(promise);
|
|
811
845
|
});
|
|
@@ -815,7 +849,7 @@ class MidyGM2 {
|
|
|
815
849
|
const channel = this.channels[channelNumber];
|
|
816
850
|
const promises = [];
|
|
817
851
|
this.processScheduledNotes(channel, (note) => {
|
|
818
|
-
const promise = this.
|
|
852
|
+
const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
819
853
|
this.notePromises.push(promise);
|
|
820
854
|
promises.push(promise);
|
|
821
855
|
});
|
|
@@ -848,7 +882,7 @@ class MidyGM2 {
|
|
|
848
882
|
if (!this.isPlaying || this.isPaused)
|
|
849
883
|
return;
|
|
850
884
|
const now = this.audioContext.currentTime;
|
|
851
|
-
this.resumeTime
|
|
885
|
+
this.resumeTime = now - this.startTime - this.startDelay;
|
|
852
886
|
this.isPausing = true;
|
|
853
887
|
await this.playPromise;
|
|
854
888
|
this.isPausing = false;
|
|
@@ -874,11 +908,13 @@ class MidyGM2 {
|
|
|
874
908
|
if (totalTime < event.startTime)
|
|
875
909
|
totalTime = event.startTime;
|
|
876
910
|
}
|
|
877
|
-
return totalTime;
|
|
911
|
+
return totalTime + this.startDelay;
|
|
878
912
|
}
|
|
879
913
|
currentTime() {
|
|
914
|
+
if (!this.isPlaying)
|
|
915
|
+
return this.resumeTime;
|
|
880
916
|
const now = this.audioContext.currentTime;
|
|
881
|
-
return
|
|
917
|
+
return now + this.resumeTime - this.startTime;
|
|
882
918
|
}
|
|
883
919
|
processScheduledNotes(channel, callback) {
|
|
884
920
|
const scheduledNotes = channel.scheduledNotes;
|
|
@@ -1089,7 +1125,7 @@ class MidyGM2 {
|
|
|
1089
1125
|
updateDetune(channel, note, scheduleTime) {
|
|
1090
1126
|
const noteDetune = this.calcNoteDetune(channel, note);
|
|
1091
1127
|
const detune = channel.detune + noteDetune;
|
|
1092
|
-
if (
|
|
1128
|
+
if (this.isPortamento(channel, note)) {
|
|
1093
1129
|
const startTime = note.startTime;
|
|
1094
1130
|
const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
|
|
1095
1131
|
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
@@ -1305,31 +1341,42 @@ class MidyGM2 {
|
|
|
1305
1341
|
note.vibratoLFO.connect(note.vibratoDepth);
|
|
1306
1342
|
note.vibratoDepth.connect(note.bufferSource.detune);
|
|
1307
1343
|
}
|
|
1308
|
-
async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
|
|
1344
|
+
async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
|
|
1309
1345
|
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
this.voiceCache.delete(audioBufferId);
|
|
1315
|
-
}
|
|
1316
|
-
return cache.audioBuffer;
|
|
1317
|
-
}
|
|
1318
|
-
else {
|
|
1319
|
-
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
1346
|
+
if (realtime) {
|
|
1347
|
+
const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
|
|
1348
|
+
if (cachedAudioBuffer)
|
|
1349
|
+
return cachedAudioBuffer;
|
|
1320
1350
|
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
1321
|
-
|
|
1322
|
-
this.voiceCache.set(audioBufferId, cache);
|
|
1351
|
+
this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
|
|
1323
1352
|
return audioBuffer;
|
|
1324
1353
|
}
|
|
1354
|
+
else {
|
|
1355
|
+
const cache = this.voiceCache.get(audioBufferId);
|
|
1356
|
+
if (cache) {
|
|
1357
|
+
cache.counter += 1;
|
|
1358
|
+
if (cache.maxCount <= cache.counter) {
|
|
1359
|
+
this.voiceCache.delete(audioBufferId);
|
|
1360
|
+
}
|
|
1361
|
+
return cache.audioBuffer;
|
|
1362
|
+
}
|
|
1363
|
+
else {
|
|
1364
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
1365
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
1366
|
+
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
1367
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
1368
|
+
return audioBuffer;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1325
1371
|
}
|
|
1326
|
-
async
|
|
1372
|
+
async setNoteAudioNode(channel, note, realtime) {
|
|
1327
1373
|
const now = this.audioContext.currentTime;
|
|
1374
|
+
const { noteNumber, velocity, startTime } = note;
|
|
1328
1375
|
const state = channel.state;
|
|
1329
1376
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
1330
|
-
const voiceParams = voice.getAllParams(controllerState);
|
|
1331
|
-
|
|
1332
|
-
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
1377
|
+
const voiceParams = note.voice.getAllParams(controllerState);
|
|
1378
|
+
note.voiceParams = voiceParams;
|
|
1379
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
1333
1380
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1334
1381
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
1335
1382
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
@@ -1340,7 +1387,7 @@ class MidyGM2 {
|
|
|
1340
1387
|
if (prevNote && prevNote.noteNumber !== noteNumber) {
|
|
1341
1388
|
note.portamentoNoteNumber = prevNote.noteNumber;
|
|
1342
1389
|
}
|
|
1343
|
-
if (
|
|
1390
|
+
if (!channel.isDrum && this.isPortamento(channel, note)) {
|
|
1344
1391
|
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
1345
1392
|
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
1346
1393
|
this.setPortamentoPitchEnvelope(note, now);
|
|
@@ -1368,22 +1415,6 @@ class MidyGM2 {
|
|
|
1368
1415
|
note.bufferSource.start(startTime);
|
|
1369
1416
|
return note;
|
|
1370
1417
|
}
|
|
1371
|
-
calcBank(channel) {
|
|
1372
|
-
switch (this.mode) {
|
|
1373
|
-
case "GM1":
|
|
1374
|
-
if (channel.isDrum)
|
|
1375
|
-
return 128;
|
|
1376
|
-
return 0;
|
|
1377
|
-
case "GM2":
|
|
1378
|
-
if (channel.bankMSB === 121)
|
|
1379
|
-
return 0;
|
|
1380
|
-
if (channel.isDrum)
|
|
1381
|
-
return 128;
|
|
1382
|
-
return channel.bank;
|
|
1383
|
-
default:
|
|
1384
|
-
return channel.bank;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
1418
|
handleExclusiveClass(note, channelNumber, startTime) {
|
|
1388
1419
|
const exclusiveClass = note.voiceParams.exclusiveClass;
|
|
1389
1420
|
if (exclusiveClass === 0)
|
|
@@ -1392,7 +1423,7 @@ class MidyGM2 {
|
|
|
1392
1423
|
if (prev) {
|
|
1393
1424
|
const [prevNote, prevChannelNumber] = prev;
|
|
1394
1425
|
if (prevNote && !prevNote.ending) {
|
|
1395
|
-
this.
|
|
1426
|
+
this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
|
|
1396
1427
|
startTime, true);
|
|
1397
1428
|
}
|
|
1398
1429
|
}
|
|
@@ -1412,23 +1443,14 @@ class MidyGM2 {
|
|
|
1412
1443
|
channelNumber;
|
|
1413
1444
|
const prevNote = this.drumExclusiveClassNotes[index];
|
|
1414
1445
|
if (prevNote && !prevNote.ending) {
|
|
1415
|
-
this.
|
|
1446
|
+
this.noteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
|
|
1416
1447
|
startTime, true);
|
|
1417
1448
|
}
|
|
1418
1449
|
this.drumExclusiveClassNotes[index] = note;
|
|
1419
1450
|
}
|
|
1420
|
-
|
|
1451
|
+
setNoteRouting(channelNumber, note, startTime) {
|
|
1421
1452
|
const channel = this.channels[channelNumber];
|
|
1422
|
-
const
|
|
1423
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
1424
|
-
.get(bankNumber);
|
|
1425
|
-
if (soundFontIndex === undefined)
|
|
1426
|
-
return;
|
|
1427
|
-
const soundFont = this.soundFonts[soundFontIndex];
|
|
1428
|
-
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
1429
|
-
if (!voice)
|
|
1430
|
-
return;
|
|
1431
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
1453
|
+
const { noteNumber, volumeEnvelopeNode } = note;
|
|
1432
1454
|
if (channel.isDrum) {
|
|
1433
1455
|
const { keyBasedGainLs, keyBasedGainRs } = channel;
|
|
1434
1456
|
let gainL = keyBasedGainLs[noteNumber];
|
|
@@ -1438,25 +1460,48 @@ class MidyGM2 {
|
|
|
1438
1460
|
gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
|
|
1439
1461
|
gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
|
|
1440
1462
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1463
|
+
volumeEnvelopeNode.connect(gainL);
|
|
1464
|
+
volumeEnvelopeNode.connect(gainR);
|
|
1443
1465
|
}
|
|
1444
1466
|
else {
|
|
1445
|
-
|
|
1446
|
-
|
|
1467
|
+
volumeEnvelopeNode.connect(channel.gainL);
|
|
1468
|
+
volumeEnvelopeNode.connect(channel.gainR);
|
|
1447
1469
|
}
|
|
1448
1470
|
if (0.5 <= channel.state.sustainPedal) {
|
|
1449
1471
|
channel.sustainNotes.push(note);
|
|
1450
1472
|
}
|
|
1451
1473
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1452
1474
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1475
|
+
}
|
|
1476
|
+
async noteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
1477
|
+
const channel = this.channels[channelNumber];
|
|
1478
|
+
const realtime = startTime === undefined;
|
|
1479
|
+
if (realtime)
|
|
1480
|
+
startTime = this.audioContext.currentTime;
|
|
1481
|
+
const note = new Note(noteNumber, velocity, startTime);
|
|
1453
1482
|
const scheduledNotes = channel.scheduledNotes;
|
|
1454
1483
|
note.index = scheduledNotes.length;
|
|
1455
1484
|
scheduledNotes.push(note);
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1485
|
+
const programNumber = channel.programNumber;
|
|
1486
|
+
const bankTable = this.soundFontTable[programNumber];
|
|
1487
|
+
if (!bankTable)
|
|
1488
|
+
return;
|
|
1489
|
+
const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
|
|
1490
|
+
const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
|
|
1491
|
+
const soundFontIndex = bankTable[bank];
|
|
1492
|
+
if (soundFontIndex === undefined)
|
|
1493
|
+
return;
|
|
1494
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
1495
|
+
note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
1496
|
+
if (!note.voice)
|
|
1497
|
+
return;
|
|
1498
|
+
await this.setNoteAudioNode(channel, note, realtime);
|
|
1499
|
+
this.setNoteRouting(channelNumber, note, startTime);
|
|
1500
|
+
note.pending = false;
|
|
1501
|
+
const off = note.offEvent;
|
|
1502
|
+
if (off) {
|
|
1503
|
+
this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
|
|
1504
|
+
}
|
|
1460
1505
|
}
|
|
1461
1506
|
disconnectNote(note) {
|
|
1462
1507
|
note.bufferSource.disconnect();
|
|
@@ -1479,6 +1524,7 @@ class MidyGM2 {
|
|
|
1479
1524
|
}
|
|
1480
1525
|
}
|
|
1481
1526
|
releaseNote(channel, note, endTime) {
|
|
1527
|
+
endTime ??= this.audioContext.currentTime;
|
|
1482
1528
|
const volRelease = endTime + note.voiceParams.volRelease;
|
|
1483
1529
|
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1484
1530
|
const stopTime = Math.min(volRelease, modRelease);
|
|
@@ -1499,7 +1545,7 @@ class MidyGM2 {
|
|
|
1499
1545
|
}, stopTime);
|
|
1500
1546
|
});
|
|
1501
1547
|
}
|
|
1502
|
-
|
|
1548
|
+
noteOff(channelNumber, noteNumber, velocity, endTime, force) {
|
|
1503
1549
|
const channel = this.channels[channelNumber];
|
|
1504
1550
|
const state = channel.state;
|
|
1505
1551
|
if (!force) {
|
|
@@ -1518,6 +1564,10 @@ class MidyGM2 {
|
|
|
1518
1564
|
if (index < 0)
|
|
1519
1565
|
return;
|
|
1520
1566
|
const note = channel.scheduledNotes[index];
|
|
1567
|
+
if (note.pending) {
|
|
1568
|
+
note.offEvent = { velocity, startTime: endTime };
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1521
1571
|
note.ending = true;
|
|
1522
1572
|
this.setNoteIndex(channel, index);
|
|
1523
1573
|
this.releaseNote(channel, note, endTime);
|
|
@@ -1548,16 +1598,12 @@ class MidyGM2 {
|
|
|
1548
1598
|
}
|
|
1549
1599
|
return -1;
|
|
1550
1600
|
}
|
|
1551
|
-
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1552
|
-
scheduleTime ??= this.audioContext.currentTime;
|
|
1553
|
-
return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
|
|
1554
|
-
}
|
|
1555
1601
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
1556
1602
|
const velocity = halfVelocity * 2;
|
|
1557
1603
|
const channel = this.channels[channelNumber];
|
|
1558
1604
|
const promises = [];
|
|
1559
1605
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1560
|
-
const promise = this.
|
|
1606
|
+
const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
|
|
1561
1607
|
promises.push(promise);
|
|
1562
1608
|
}
|
|
1563
1609
|
channel.sustainNotes = [];
|
|
@@ -1571,47 +1617,51 @@ class MidyGM2 {
|
|
|
1571
1617
|
channel.state.sostenutoPedal = 0;
|
|
1572
1618
|
for (let i = 0; i < sostenutoNotes.length; i++) {
|
|
1573
1619
|
const note = sostenutoNotes[i];
|
|
1574
|
-
const promise = this.
|
|
1620
|
+
const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
|
|
1575
1621
|
promises.push(promise);
|
|
1576
1622
|
}
|
|
1577
1623
|
channel.sostenutoNotes = [];
|
|
1578
1624
|
return promises;
|
|
1579
1625
|
}
|
|
1580
|
-
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
1626
|
+
createMessageHandlers() {
|
|
1627
|
+
const handlers = new Array(256);
|
|
1628
|
+
// Channel Message
|
|
1629
|
+
handlers[0x80] = (data, scheduleTime) => this.noteOff(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1630
|
+
handlers[0x90] = (data, scheduleTime) => this.noteOn(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1631
|
+
handlers[0xB0] = (data, scheduleTime) => this.setControlChange(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1632
|
+
handlers[0xC0] = (data, scheduleTime) => this.setProgramChange(data[0] & 0x0F, data[1], scheduleTime);
|
|
1633
|
+
handlers[0xD0] = (data, scheduleTime) => this.setChannelPressure(data[0] & 0x0F, data[1], scheduleTime);
|
|
1634
|
+
handlers[0xE0] = (data, scheduleTime) => this.handlePitchBendMessage(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1635
|
+
// System Real Time Message
|
|
1636
|
+
handlers[0xFE] = (_data, _scheduleTime) => this.activeSensing();
|
|
1637
|
+
return handlers;
|
|
1638
|
+
}
|
|
1639
|
+
handleMessage(data, scheduleTime) {
|
|
1640
|
+
const status = data[0];
|
|
1641
|
+
if (status === 0xF0) {
|
|
1642
|
+
return this.handleSysEx(data.subarray(1), scheduleTime);
|
|
1598
1643
|
}
|
|
1644
|
+
const handler = this.messageHandlers[status];
|
|
1645
|
+
if (handler)
|
|
1646
|
+
handler(data, scheduleTime);
|
|
1647
|
+
}
|
|
1648
|
+
activeSensing() {
|
|
1649
|
+
this.lastActiveSensing = performance.now();
|
|
1599
1650
|
}
|
|
1600
1651
|
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
1601
1652
|
const channel = this.channels[channelNumber];
|
|
1602
|
-
channel.bank = channel.bankMSB * 128 + channel.bankLSB;
|
|
1603
1653
|
channel.programNumber = programNumber;
|
|
1604
1654
|
if (this.mode === "GM2") {
|
|
1605
1655
|
switch (channel.bankMSB) {
|
|
1606
1656
|
case 120:
|
|
1607
1657
|
channel.isDrum = true;
|
|
1658
|
+
channel.keyBasedTable.fill(-1);
|
|
1608
1659
|
break;
|
|
1609
1660
|
case 121:
|
|
1610
1661
|
channel.isDrum = false;
|
|
1611
1662
|
break;
|
|
1612
1663
|
}
|
|
1613
1664
|
}
|
|
1614
|
-
channel.keyBasedTable.fill(-1);
|
|
1615
1665
|
}
|
|
1616
1666
|
setChannelPressure(channelNumber, value, scheduleTime) {
|
|
1617
1667
|
const channel = this.channels[channelNumber];
|
|
@@ -1936,22 +1986,20 @@ class MidyGM2 {
|
|
|
1936
1986
|
this.updateModulation(channel, scheduleTime);
|
|
1937
1987
|
}
|
|
1938
1988
|
updatePortamento(channel, scheduleTime) {
|
|
1989
|
+
if (channel.isDrum)
|
|
1990
|
+
return;
|
|
1939
1991
|
this.processScheduledNotes(channel, (note) => {
|
|
1940
|
-
if (
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
this.updateDetune(channel, note, scheduleTime);
|
|
1946
|
-
}
|
|
1992
|
+
if (this.isPortamento(channel, note)) {
|
|
1993
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
1994
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1995
|
+
this.setPortamentoPitchEnvelope(note, scheduleTime);
|
|
1996
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1947
1997
|
}
|
|
1948
1998
|
else {
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
this.updateDetune(channel, note, scheduleTime);
|
|
1954
|
-
}
|
|
1999
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
2000
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
2001
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
2002
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1955
2003
|
}
|
|
1956
2004
|
});
|
|
1957
2005
|
}
|
|
@@ -2057,6 +2105,9 @@ class MidyGM2 {
|
|
|
2057
2105
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
2058
2106
|
}
|
|
2059
2107
|
}
|
|
2108
|
+
isPortamento(channel, note) {
|
|
2109
|
+
return 0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber;
|
|
2110
|
+
}
|
|
2060
2111
|
setPortamento(channelNumber, value, scheduleTime) {
|
|
2061
2112
|
const channel = this.channels[channelNumber];
|
|
2062
2113
|
if (channel.isDrum)
|
|
@@ -2093,7 +2144,7 @@ class MidyGM2 {
|
|
|
2093
2144
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2094
2145
|
state.softPedal = softPedal / 127;
|
|
2095
2146
|
this.processScheduledNotes(channel, (note) => {
|
|
2096
|
-
if (
|
|
2147
|
+
if (this.isPortamento(channel, note)) {
|
|
2097
2148
|
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2098
2149
|
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2099
2150
|
}
|
|
@@ -2361,11 +2412,9 @@ class MidyGM2 {
|
|
|
2361
2412
|
const channel = this.channels[i];
|
|
2362
2413
|
channel.bankMSB = 0;
|
|
2363
2414
|
channel.bankLSB = 0;
|
|
2364
|
-
channel.bank = 0;
|
|
2365
2415
|
channel.isDrum = false;
|
|
2366
2416
|
}
|
|
2367
2417
|
this.channels[9].bankMSB = 1;
|
|
2368
|
-
this.channels[9].bank = 128;
|
|
2369
2418
|
this.channels[9].isDrum = true;
|
|
2370
2419
|
}
|
|
2371
2420
|
GM2SystemOn(scheduleTime) {
|
|
@@ -2376,11 +2425,9 @@ class MidyGM2 {
|
|
|
2376
2425
|
const channel = this.channels[i];
|
|
2377
2426
|
channel.bankMSB = 121;
|
|
2378
2427
|
channel.bankLSB = 0;
|
|
2379
|
-
channel.bank = 121 * 128;
|
|
2380
2428
|
channel.isDrum = false;
|
|
2381
2429
|
}
|
|
2382
2430
|
this.channels[9].bankMSB = 120;
|
|
2383
|
-
this.channels[9].bank = 120 * 128;
|
|
2384
2431
|
this.channels[9].isDrum = true;
|
|
2385
2432
|
}
|
|
2386
2433
|
handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
|
|
@@ -2425,16 +2472,11 @@ class MidyGM2 {
|
|
|
2425
2472
|
const volume = (data[5] * 128 + data[4]) / 16383;
|
|
2426
2473
|
this.setMasterVolume(volume, scheduleTime);
|
|
2427
2474
|
}
|
|
2428
|
-
setMasterVolume(
|
|
2475
|
+
setMasterVolume(value, scheduleTime) {
|
|
2429
2476
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
else {
|
|
2434
|
-
this.masterVolume.gain
|
|
2435
|
-
.cancelScheduledValues(scheduleTime)
|
|
2436
|
-
.setValueAtTime(volume * volume, scheduleTime);
|
|
2437
|
-
}
|
|
2477
|
+
this.masterVolume.gain
|
|
2478
|
+
.cancelScheduledValues(scheduleTime)
|
|
2479
|
+
.setValueAtTime(value * value, scheduleTime);
|
|
2438
2480
|
}
|
|
2439
2481
|
handleMasterFineTuningSysEx(data, scheduleTime) {
|
|
2440
2482
|
const value = (data[5] * 128 + data[4]) / 16383;
|
|
@@ -2797,6 +2839,22 @@ class MidyGM2 {
|
|
|
2797
2839
|
const controlValue = channel.keyBasedTable[index];
|
|
2798
2840
|
return controlValue;
|
|
2799
2841
|
}
|
|
2842
|
+
createKeyBasedControllerHandlers() {
|
|
2843
|
+
const handlers = new Array(128);
|
|
2844
|
+
handlers[7] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
|
|
2845
|
+
handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
|
|
2846
|
+
handlers[91] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
|
|
2847
|
+
if (note.noteNumber === keyNumber) {
|
|
2848
|
+
this.setReverbSend(channel, note, scheduleTime);
|
|
2849
|
+
}
|
|
2850
|
+
});
|
|
2851
|
+
handlers[93] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
|
|
2852
|
+
if (note.noteNumber === keyNumber) {
|
|
2853
|
+
this.setChorusSend(channel, note, scheduleTime);
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
return handlers;
|
|
2857
|
+
}
|
|
2800
2858
|
handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
|
|
2801
2859
|
const channelNumber = data[4];
|
|
2802
2860
|
const channel = this.channels[channelNumber];
|
|
@@ -2809,26 +2867,9 @@ class MidyGM2 {
|
|
|
2809
2867
|
const value = data[i + 1];
|
|
2810
2868
|
const index = keyNumber * 128 + controllerType;
|
|
2811
2869
|
table[index] = value;
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
|
|
2816
|
-
break;
|
|
2817
|
-
case 91:
|
|
2818
|
-
this.processScheduledNotes(channel, (note) => {
|
|
2819
|
-
if (note.noteNumber === keyNumber) {
|
|
2820
|
-
this.setReverbSend(channel, note, scheduleTime);
|
|
2821
|
-
}
|
|
2822
|
-
});
|
|
2823
|
-
break;
|
|
2824
|
-
case 93:
|
|
2825
|
-
this.processScheduledNotes(channel, (note) => {
|
|
2826
|
-
if (note.noteNumber === keyNumber) {
|
|
2827
|
-
this.setChorusSend(channel, note, scheduleTime);
|
|
2828
|
-
}
|
|
2829
|
-
});
|
|
2830
|
-
break;
|
|
2831
|
-
}
|
|
2870
|
+
const handler = this.keyBasedControllerHandlers[controllerType];
|
|
2871
|
+
if (handler)
|
|
2872
|
+
handler(channel, keyNumber, scheduleTime);
|
|
2832
2873
|
}
|
|
2833
2874
|
}
|
|
2834
2875
|
handleSysEx(data, scheduleTime) {
|
|
@@ -2870,7 +2911,6 @@ Object.defineProperty(MidyGM2, "channelSettings", {
|
|
|
2870
2911
|
scheduleIndex: 0,
|
|
2871
2912
|
detune: 0,
|
|
2872
2913
|
programNumber: 0,
|
|
2873
|
-
bank: 121 * 128,
|
|
2874
2914
|
bankMSB: 121,
|
|
2875
2915
|
bankLSB: 0,
|
|
2876
2916
|
dataMSB: 0,
|