@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/esm/midy-GM2.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { parseMidi } from "midi-file";
|
|
2
2
|
import { parse, SoundFont } from "@marmooo/soundfont-parser";
|
|
3
3
|
class Note {
|
|
4
|
-
constructor(noteNumber, velocity, startTime
|
|
4
|
+
constructor(noteNumber, velocity, startTime) {
|
|
5
|
+
Object.defineProperty(this, "voice", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: void 0
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "voiceParams", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
5
17
|
Object.defineProperty(this, "index", {
|
|
6
18
|
enumerable: true,
|
|
7
19
|
configurable: true,
|
|
@@ -14,6 +26,12 @@ class Note {
|
|
|
14
26
|
writable: true,
|
|
15
27
|
value: false
|
|
16
28
|
});
|
|
29
|
+
Object.defineProperty(this, "pending", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: true
|
|
34
|
+
});
|
|
17
35
|
Object.defineProperty(this, "bufferSource", {
|
|
18
36
|
enumerable: true,
|
|
19
37
|
configurable: true,
|
|
@@ -89,8 +107,6 @@ class Note {
|
|
|
89
107
|
this.noteNumber = noteNumber;
|
|
90
108
|
this.velocity = velocity;
|
|
91
109
|
this.startTime = startTime;
|
|
92
|
-
this.voice = voice;
|
|
93
|
-
this.voiceParams = voiceParams;
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
const drumExclusiveClassesByKit = new Array(57);
|
|
@@ -272,6 +288,18 @@ export class MidyGM2 {
|
|
|
272
288
|
writable: true,
|
|
273
289
|
value: 0
|
|
274
290
|
});
|
|
291
|
+
Object.defineProperty(this, "lastActiveSensing", {
|
|
292
|
+
enumerable: true,
|
|
293
|
+
configurable: true,
|
|
294
|
+
writable: true,
|
|
295
|
+
value: 0
|
|
296
|
+
});
|
|
297
|
+
Object.defineProperty(this, "activeSensingThreshold", {
|
|
298
|
+
enumerable: true,
|
|
299
|
+
configurable: true,
|
|
300
|
+
writable: true,
|
|
301
|
+
value: 0.3
|
|
302
|
+
});
|
|
275
303
|
Object.defineProperty(this, "noteCheckInterval", {
|
|
276
304
|
enumerable: true,
|
|
277
305
|
configurable: true,
|
|
@@ -312,7 +340,7 @@ export class MidyGM2 {
|
|
|
312
340
|
enumerable: true,
|
|
313
341
|
configurable: true,
|
|
314
342
|
writable: true,
|
|
315
|
-
value:
|
|
343
|
+
value: Array.from({ length: 128 }, () => [])
|
|
316
344
|
});
|
|
317
345
|
Object.defineProperty(this, "voiceCounter", {
|
|
318
346
|
enumerable: true,
|
|
@@ -326,6 +354,12 @@ export class MidyGM2 {
|
|
|
326
354
|
writable: true,
|
|
327
355
|
value: new Map()
|
|
328
356
|
});
|
|
357
|
+
Object.defineProperty(this, "realtimeVoiceCache", {
|
|
358
|
+
enumerable: true,
|
|
359
|
+
configurable: true,
|
|
360
|
+
writable: true,
|
|
361
|
+
value: new Map()
|
|
362
|
+
});
|
|
329
363
|
Object.defineProperty(this, "isPlaying", {
|
|
330
364
|
enumerable: true,
|
|
331
365
|
configurable: true,
|
|
@@ -399,8 +433,10 @@ export class MidyGM2 {
|
|
|
399
433
|
length: 1,
|
|
400
434
|
sampleRate: audioContext.sampleRate,
|
|
401
435
|
});
|
|
436
|
+
this.messageHandlers = this.createMessageHandlers();
|
|
402
437
|
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
403
438
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
439
|
+
this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
|
|
404
440
|
this.channels = this.createChannels(audioContext);
|
|
405
441
|
this.reverbEffect = this.createReverbEffect(audioContext);
|
|
406
442
|
this.chorusEffect = this.createChorusEffect(audioContext);
|
|
@@ -410,21 +446,14 @@ export class MidyGM2 {
|
|
|
410
446
|
this.scheduler.connect(audioContext.destination);
|
|
411
447
|
this.GM2SystemOn();
|
|
412
448
|
}
|
|
413
|
-
initSoundFontTable() {
|
|
414
|
-
const table = new Array(128);
|
|
415
|
-
for (let i = 0; i < 128; i++) {
|
|
416
|
-
table[i] = new Map();
|
|
417
|
-
}
|
|
418
|
-
return table;
|
|
419
|
-
}
|
|
420
449
|
addSoundFont(soundFont) {
|
|
421
450
|
const index = this.soundFonts.length;
|
|
422
451
|
this.soundFonts.push(soundFont);
|
|
423
452
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
453
|
+
const soundFontTable = this.soundFontTable;
|
|
424
454
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
banks.set(presetHeader.bank, index);
|
|
455
|
+
const { preset, bank } = presetHeaders[i];
|
|
456
|
+
soundFontTable[preset][bank] = index;
|
|
428
457
|
}
|
|
429
458
|
}
|
|
430
459
|
async toUint8Array(input) {
|
|
@@ -502,13 +531,17 @@ export class MidyGM2 {
|
|
|
502
531
|
this.GM2SystemOn();
|
|
503
532
|
}
|
|
504
533
|
getVoiceId(channel, noteNumber, velocity) {
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
534
|
+
const programNumber = channel.programNumber;
|
|
535
|
+
const bankTable = this.soundFontTable[programNumber];
|
|
536
|
+
if (!bankTable)
|
|
537
|
+
return;
|
|
538
|
+
const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
|
|
539
|
+
const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
|
|
540
|
+
const soundFontIndex = bankTable[bank];
|
|
508
541
|
if (soundFontIndex === undefined)
|
|
509
542
|
return;
|
|
510
543
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
511
|
-
const voice = soundFont.getVoice(
|
|
544
|
+
const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
512
545
|
const { instrument, sampleID } = voice.generators;
|
|
513
546
|
return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
|
|
514
547
|
}
|
|
@@ -577,19 +610,22 @@ export class MidyGM2 {
|
|
|
577
610
|
}
|
|
578
611
|
return bufferSource;
|
|
579
612
|
}
|
|
580
|
-
async scheduleTimelineEvents(
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
613
|
+
async scheduleTimelineEvents(scheduleTime, queueIndex) {
|
|
614
|
+
const timeOffset = this.resumeTime - this.startTime;
|
|
615
|
+
const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
|
|
616
|
+
const schedulingOffset = this.startDelay - timeOffset;
|
|
617
|
+
const timeline = this.timeline;
|
|
618
|
+
while (queueIndex < timeline.length) {
|
|
619
|
+
const event = timeline[queueIndex];
|
|
620
|
+
if (lookAheadCheckTime < event.startTime)
|
|
584
621
|
break;
|
|
585
|
-
const
|
|
586
|
-
const startTime = event.startTime + delay;
|
|
622
|
+
const startTime = event.startTime + schedulingOffset;
|
|
587
623
|
switch (event.type) {
|
|
588
624
|
case "noteOn":
|
|
589
|
-
await this.
|
|
625
|
+
await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
590
626
|
break;
|
|
591
627
|
case "noteOff": {
|
|
592
|
-
const notePromise = this.
|
|
628
|
+
const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
593
629
|
if (notePromise)
|
|
594
630
|
this.notePromises.push(notePromise);
|
|
595
631
|
break;
|
|
@@ -625,6 +661,7 @@ export class MidyGM2 {
|
|
|
625
661
|
this.exclusiveClassNotes.fill(undefined);
|
|
626
662
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
627
663
|
this.voiceCache.clear();
|
|
664
|
+
this.realtimeVoiceCache.clear();
|
|
628
665
|
for (let i = 0; i < this.channels.length; i++) {
|
|
629
666
|
this.channels[i].scheduledNotes = [];
|
|
630
667
|
this.resetChannelStates(i);
|
|
@@ -658,13 +695,17 @@ export class MidyGM2 {
|
|
|
658
695
|
this.isPaused = false;
|
|
659
696
|
this.startTime = this.audioContext.currentTime;
|
|
660
697
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
661
|
-
let resumeTime = this.resumeTime - this.startTime;
|
|
662
698
|
let finished = false;
|
|
663
699
|
this.notePromises = [];
|
|
664
700
|
while (queueIndex < this.timeline.length) {
|
|
665
701
|
const now = this.audioContext.currentTime;
|
|
666
|
-
|
|
667
|
-
|
|
702
|
+
if (0 < this.lastActiveSensing &&
|
|
703
|
+
this.activeSensingThreshold < performance.now() - this.lastActiveSensing) {
|
|
704
|
+
await this.stopNotes(0, true, now);
|
|
705
|
+
await this.audioContext.suspend();
|
|
706
|
+
finished = true;
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
668
709
|
if (this.isPausing) {
|
|
669
710
|
await this.stopNotes(0, true, now);
|
|
670
711
|
await this.audioContext.suspend();
|
|
@@ -683,16 +724,17 @@ export class MidyGM2 {
|
|
|
683
724
|
const nextQueueIndex = this.getQueueIndex(this.resumeTime);
|
|
684
725
|
this.updateStates(queueIndex, nextQueueIndex);
|
|
685
726
|
queueIndex = nextQueueIndex;
|
|
686
|
-
resumeTime = this.resumeTime - this.startTime;
|
|
687
727
|
this.isSeeking = false;
|
|
688
728
|
continue;
|
|
689
729
|
}
|
|
730
|
+
queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
|
|
690
731
|
const waitTime = now + this.noteCheckInterval;
|
|
691
732
|
await this.scheduleTask(() => { }, waitTime);
|
|
692
733
|
}
|
|
693
734
|
if (finished) {
|
|
694
735
|
this.notePromises = [];
|
|
695
736
|
this.resetAllStates();
|
|
737
|
+
this.lastActiveSensing = 0;
|
|
696
738
|
}
|
|
697
739
|
this.isPlaying = false;
|
|
698
740
|
}
|
|
@@ -702,17 +744,17 @@ export class MidyGM2 {
|
|
|
702
744
|
secondToTicks(second, secondsPerBeat) {
|
|
703
745
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
704
746
|
}
|
|
747
|
+
getSoundFontId(channel) {
|
|
748
|
+
const programNumber = channel.programNumber;
|
|
749
|
+
const bankNumber = channel.isDrum ? 128 : channel.bankLSB;
|
|
750
|
+
const bank = bankNumber.toString().padStart(3, "0");
|
|
751
|
+
const program = programNumber.toString().padStart(3, "0");
|
|
752
|
+
return `${bank}:${program}`;
|
|
753
|
+
}
|
|
705
754
|
extractMidiData(midi) {
|
|
706
755
|
const instruments = new Set();
|
|
707
756
|
const timeline = [];
|
|
708
|
-
const
|
|
709
|
-
for (let i = 0; i < tmpChannels.length; i++) {
|
|
710
|
-
tmpChannels[i] = {
|
|
711
|
-
programNumber: -1,
|
|
712
|
-
bankMSB: this.channels[i].bankMSB,
|
|
713
|
-
bankLSB: this.channels[i].bankLSB,
|
|
714
|
-
};
|
|
715
|
-
}
|
|
757
|
+
const channels = this.channels;
|
|
716
758
|
for (let i = 0; i < midi.tracks.length; i++) {
|
|
717
759
|
const track = midi.tracks[i];
|
|
718
760
|
let currentTicks = 0;
|
|
@@ -722,48 +764,40 @@ export class MidyGM2 {
|
|
|
722
764
|
event.ticks = currentTicks;
|
|
723
765
|
switch (event.type) {
|
|
724
766
|
case "noteOn": {
|
|
725
|
-
const channel =
|
|
726
|
-
|
|
727
|
-
channel.programNumber = event.programNumber;
|
|
728
|
-
switch (channel.bankMSB) {
|
|
729
|
-
case 120:
|
|
730
|
-
instruments.add(`128:0`);
|
|
731
|
-
break;
|
|
732
|
-
case 121:
|
|
733
|
-
instruments.add(`${channel.bankLSB}:0`);
|
|
734
|
-
break;
|
|
735
|
-
default: {
|
|
736
|
-
const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
|
|
737
|
-
instruments.add(`${bankNumber}:0`);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
channel.programNumber = 0;
|
|
741
|
-
}
|
|
767
|
+
const channel = channels[event.channel];
|
|
768
|
+
instruments.add(this.getSoundFontId(channel));
|
|
742
769
|
break;
|
|
743
770
|
}
|
|
744
771
|
case "controller":
|
|
745
772
|
switch (event.controllerType) {
|
|
746
773
|
case 0:
|
|
747
|
-
|
|
774
|
+
this.setBankMSB(event.channel, event.value);
|
|
748
775
|
break;
|
|
749
776
|
case 32:
|
|
750
|
-
|
|
777
|
+
this.setBankLSB(event.channel, event.value);
|
|
751
778
|
break;
|
|
752
779
|
}
|
|
753
780
|
break;
|
|
754
781
|
case "programChange": {
|
|
755
|
-
const channel =
|
|
756
|
-
channel
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
782
|
+
const channel = channels[event.channel];
|
|
783
|
+
this.setProgramChange(event.channel, event.programNumber);
|
|
784
|
+
instruments.add(this.getSoundFontId(channel));
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
case "sysEx": {
|
|
788
|
+
const data = event.data;
|
|
789
|
+
if (data[0] === 126 && data[1] === 9 && data[2] === 3) {
|
|
790
|
+
switch (data[3]) {
|
|
791
|
+
case 1:
|
|
792
|
+
this.GM1SystemOn(scheduleTime);
|
|
793
|
+
break;
|
|
794
|
+
case 2: // GM System Off
|
|
795
|
+
break;
|
|
796
|
+
case 3:
|
|
797
|
+
this.GM2SystemOn(scheduleTime);
|
|
798
|
+
break;
|
|
799
|
+
default:
|
|
800
|
+
console.warn(`Unsupported Exclusive Message: ${data}`);
|
|
767
801
|
}
|
|
768
802
|
}
|
|
769
803
|
}
|
|
@@ -802,7 +836,7 @@ export class MidyGM2 {
|
|
|
802
836
|
const channel = this.channels[channelNumber];
|
|
803
837
|
const promises = [];
|
|
804
838
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
805
|
-
const promise = this.
|
|
839
|
+
const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
806
840
|
this.notePromises.push(promise);
|
|
807
841
|
promises.push(promise);
|
|
808
842
|
});
|
|
@@ -812,7 +846,7 @@ export class MidyGM2 {
|
|
|
812
846
|
const channel = this.channels[channelNumber];
|
|
813
847
|
const promises = [];
|
|
814
848
|
this.processScheduledNotes(channel, (note) => {
|
|
815
|
-
const promise = this.
|
|
849
|
+
const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
816
850
|
this.notePromises.push(promise);
|
|
817
851
|
promises.push(promise);
|
|
818
852
|
});
|
|
@@ -845,7 +879,7 @@ export class MidyGM2 {
|
|
|
845
879
|
if (!this.isPlaying || this.isPaused)
|
|
846
880
|
return;
|
|
847
881
|
const now = this.audioContext.currentTime;
|
|
848
|
-
this.resumeTime
|
|
882
|
+
this.resumeTime = now - this.startTime - this.startDelay;
|
|
849
883
|
this.isPausing = true;
|
|
850
884
|
await this.playPromise;
|
|
851
885
|
this.isPausing = false;
|
|
@@ -871,11 +905,13 @@ export class MidyGM2 {
|
|
|
871
905
|
if (totalTime < event.startTime)
|
|
872
906
|
totalTime = event.startTime;
|
|
873
907
|
}
|
|
874
|
-
return totalTime;
|
|
908
|
+
return totalTime + this.startDelay;
|
|
875
909
|
}
|
|
876
910
|
currentTime() {
|
|
911
|
+
if (!this.isPlaying)
|
|
912
|
+
return this.resumeTime;
|
|
877
913
|
const now = this.audioContext.currentTime;
|
|
878
|
-
return
|
|
914
|
+
return now + this.resumeTime - this.startTime;
|
|
879
915
|
}
|
|
880
916
|
processScheduledNotes(channel, callback) {
|
|
881
917
|
const scheduledNotes = channel.scheduledNotes;
|
|
@@ -1086,7 +1122,7 @@ export class MidyGM2 {
|
|
|
1086
1122
|
updateDetune(channel, note, scheduleTime) {
|
|
1087
1123
|
const noteDetune = this.calcNoteDetune(channel, note);
|
|
1088
1124
|
const detune = channel.detune + noteDetune;
|
|
1089
|
-
if (
|
|
1125
|
+
if (this.isPortamento(channel, note)) {
|
|
1090
1126
|
const startTime = note.startTime;
|
|
1091
1127
|
const deltaCent = (note.noteNumber - note.portamentoNoteNumber) * 100;
|
|
1092
1128
|
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
@@ -1302,31 +1338,42 @@ export class MidyGM2 {
|
|
|
1302
1338
|
note.vibratoLFO.connect(note.vibratoDepth);
|
|
1303
1339
|
note.vibratoDepth.connect(note.bufferSource.detune);
|
|
1304
1340
|
}
|
|
1305
|
-
async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
|
|
1341
|
+
async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
|
|
1306
1342
|
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
this.voiceCache.delete(audioBufferId);
|
|
1312
|
-
}
|
|
1313
|
-
return cache.audioBuffer;
|
|
1314
|
-
}
|
|
1315
|
-
else {
|
|
1316
|
-
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
1343
|
+
if (realtime) {
|
|
1344
|
+
const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
|
|
1345
|
+
if (cachedAudioBuffer)
|
|
1346
|
+
return cachedAudioBuffer;
|
|
1317
1347
|
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
1318
|
-
|
|
1319
|
-
this.voiceCache.set(audioBufferId, cache);
|
|
1348
|
+
this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
|
|
1320
1349
|
return audioBuffer;
|
|
1321
1350
|
}
|
|
1351
|
+
else {
|
|
1352
|
+
const cache = this.voiceCache.get(audioBufferId);
|
|
1353
|
+
if (cache) {
|
|
1354
|
+
cache.counter += 1;
|
|
1355
|
+
if (cache.maxCount <= cache.counter) {
|
|
1356
|
+
this.voiceCache.delete(audioBufferId);
|
|
1357
|
+
}
|
|
1358
|
+
return cache.audioBuffer;
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
1362
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
1363
|
+
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
1364
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
1365
|
+
return audioBuffer;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1322
1368
|
}
|
|
1323
|
-
async
|
|
1369
|
+
async setNoteAudioNode(channel, note, realtime) {
|
|
1324
1370
|
const now = this.audioContext.currentTime;
|
|
1371
|
+
const { noteNumber, velocity, startTime } = note;
|
|
1325
1372
|
const state = channel.state;
|
|
1326
1373
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
1327
|
-
const voiceParams = voice.getAllParams(controllerState);
|
|
1328
|
-
|
|
1329
|
-
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
1374
|
+
const voiceParams = note.voice.getAllParams(controllerState);
|
|
1375
|
+
note.voiceParams = voiceParams;
|
|
1376
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
1330
1377
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1331
1378
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
1332
1379
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
@@ -1337,7 +1384,7 @@ export class MidyGM2 {
|
|
|
1337
1384
|
if (prevNote && prevNote.noteNumber !== noteNumber) {
|
|
1338
1385
|
note.portamentoNoteNumber = prevNote.noteNumber;
|
|
1339
1386
|
}
|
|
1340
|
-
if (
|
|
1387
|
+
if (!channel.isDrum && this.isPortamento(channel, note)) {
|
|
1341
1388
|
this.setPortamentoVolumeEnvelope(channel, note, now);
|
|
1342
1389
|
this.setPortamentoFilterEnvelope(channel, note, now);
|
|
1343
1390
|
this.setPortamentoPitchEnvelope(note, now);
|
|
@@ -1365,22 +1412,6 @@ export class MidyGM2 {
|
|
|
1365
1412
|
note.bufferSource.start(startTime);
|
|
1366
1413
|
return note;
|
|
1367
1414
|
}
|
|
1368
|
-
calcBank(channel) {
|
|
1369
|
-
switch (this.mode) {
|
|
1370
|
-
case "GM1":
|
|
1371
|
-
if (channel.isDrum)
|
|
1372
|
-
return 128;
|
|
1373
|
-
return 0;
|
|
1374
|
-
case "GM2":
|
|
1375
|
-
if (channel.bankMSB === 121)
|
|
1376
|
-
return 0;
|
|
1377
|
-
if (channel.isDrum)
|
|
1378
|
-
return 128;
|
|
1379
|
-
return channel.bank;
|
|
1380
|
-
default:
|
|
1381
|
-
return channel.bank;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
1415
|
handleExclusiveClass(note, channelNumber, startTime) {
|
|
1385
1416
|
const exclusiveClass = note.voiceParams.exclusiveClass;
|
|
1386
1417
|
if (exclusiveClass === 0)
|
|
@@ -1389,7 +1420,7 @@ export class MidyGM2 {
|
|
|
1389
1420
|
if (prev) {
|
|
1390
1421
|
const [prevNote, prevChannelNumber] = prev;
|
|
1391
1422
|
if (prevNote && !prevNote.ending) {
|
|
1392
|
-
this.
|
|
1423
|
+
this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
|
|
1393
1424
|
startTime, true);
|
|
1394
1425
|
}
|
|
1395
1426
|
}
|
|
@@ -1409,23 +1440,14 @@ export class MidyGM2 {
|
|
|
1409
1440
|
channelNumber;
|
|
1410
1441
|
const prevNote = this.drumExclusiveClassNotes[index];
|
|
1411
1442
|
if (prevNote && !prevNote.ending) {
|
|
1412
|
-
this.
|
|
1443
|
+
this.noteOff(channelNumber, prevNote.noteNumber, 0, // velocity,
|
|
1413
1444
|
startTime, true);
|
|
1414
1445
|
}
|
|
1415
1446
|
this.drumExclusiveClassNotes[index] = note;
|
|
1416
1447
|
}
|
|
1417
|
-
|
|
1448
|
+
setNoteRouting(channelNumber, note, startTime) {
|
|
1418
1449
|
const channel = this.channels[channelNumber];
|
|
1419
|
-
const
|
|
1420
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
1421
|
-
.get(bankNumber);
|
|
1422
|
-
if (soundFontIndex === undefined)
|
|
1423
|
-
return;
|
|
1424
|
-
const soundFont = this.soundFonts[soundFontIndex];
|
|
1425
|
-
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
1426
|
-
if (!voice)
|
|
1427
|
-
return;
|
|
1428
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
1450
|
+
const { noteNumber, volumeEnvelopeNode } = note;
|
|
1429
1451
|
if (channel.isDrum) {
|
|
1430
1452
|
const { keyBasedGainLs, keyBasedGainRs } = channel;
|
|
1431
1453
|
let gainL = keyBasedGainLs[noteNumber];
|
|
@@ -1435,25 +1457,48 @@ export class MidyGM2 {
|
|
|
1435
1457
|
gainL = keyBasedGainLs[noteNumber] = audioNodes.gainL;
|
|
1436
1458
|
gainR = keyBasedGainRs[noteNumber] = audioNodes.gainR;
|
|
1437
1459
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1460
|
+
volumeEnvelopeNode.connect(gainL);
|
|
1461
|
+
volumeEnvelopeNode.connect(gainR);
|
|
1440
1462
|
}
|
|
1441
1463
|
else {
|
|
1442
|
-
|
|
1443
|
-
|
|
1464
|
+
volumeEnvelopeNode.connect(channel.gainL);
|
|
1465
|
+
volumeEnvelopeNode.connect(channel.gainR);
|
|
1444
1466
|
}
|
|
1445
1467
|
if (0.5 <= channel.state.sustainPedal) {
|
|
1446
1468
|
channel.sustainNotes.push(note);
|
|
1447
1469
|
}
|
|
1448
1470
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
1449
1471
|
this.handleDrumExclusiveClass(note, channelNumber, startTime);
|
|
1472
|
+
}
|
|
1473
|
+
async noteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
1474
|
+
const channel = this.channels[channelNumber];
|
|
1475
|
+
const realtime = startTime === undefined;
|
|
1476
|
+
if (realtime)
|
|
1477
|
+
startTime = this.audioContext.currentTime;
|
|
1478
|
+
const note = new Note(noteNumber, velocity, startTime);
|
|
1450
1479
|
const scheduledNotes = channel.scheduledNotes;
|
|
1451
1480
|
note.index = scheduledNotes.length;
|
|
1452
1481
|
scheduledNotes.push(note);
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1482
|
+
const programNumber = channel.programNumber;
|
|
1483
|
+
const bankTable = this.soundFontTable[programNumber];
|
|
1484
|
+
if (!bankTable)
|
|
1485
|
+
return;
|
|
1486
|
+
const bankLSB = channel.isDrum ? 128 : channel.bankLSB;
|
|
1487
|
+
const bank = bankTable[bankLSB] !== undefined ? bankLSB : 0;
|
|
1488
|
+
const soundFontIndex = bankTable[bank];
|
|
1489
|
+
if (soundFontIndex === undefined)
|
|
1490
|
+
return;
|
|
1491
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
1492
|
+
note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
1493
|
+
if (!note.voice)
|
|
1494
|
+
return;
|
|
1495
|
+
await this.setNoteAudioNode(channel, note, realtime);
|
|
1496
|
+
this.setNoteRouting(channelNumber, note, startTime);
|
|
1497
|
+
note.pending = false;
|
|
1498
|
+
const off = note.offEvent;
|
|
1499
|
+
if (off) {
|
|
1500
|
+
this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
|
|
1501
|
+
}
|
|
1457
1502
|
}
|
|
1458
1503
|
disconnectNote(note) {
|
|
1459
1504
|
note.bufferSource.disconnect();
|
|
@@ -1476,6 +1521,7 @@ export class MidyGM2 {
|
|
|
1476
1521
|
}
|
|
1477
1522
|
}
|
|
1478
1523
|
releaseNote(channel, note, endTime) {
|
|
1524
|
+
endTime ??= this.audioContext.currentTime;
|
|
1479
1525
|
const volRelease = endTime + note.voiceParams.volRelease;
|
|
1480
1526
|
const modRelease = endTime + note.voiceParams.modRelease;
|
|
1481
1527
|
const stopTime = Math.min(volRelease, modRelease);
|
|
@@ -1496,7 +1542,7 @@ export class MidyGM2 {
|
|
|
1496
1542
|
}, stopTime);
|
|
1497
1543
|
});
|
|
1498
1544
|
}
|
|
1499
|
-
|
|
1545
|
+
noteOff(channelNumber, noteNumber, velocity, endTime, force) {
|
|
1500
1546
|
const channel = this.channels[channelNumber];
|
|
1501
1547
|
const state = channel.state;
|
|
1502
1548
|
if (!force) {
|
|
@@ -1515,6 +1561,10 @@ export class MidyGM2 {
|
|
|
1515
1561
|
if (index < 0)
|
|
1516
1562
|
return;
|
|
1517
1563
|
const note = channel.scheduledNotes[index];
|
|
1564
|
+
if (note.pending) {
|
|
1565
|
+
note.offEvent = { velocity, startTime: endTime };
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1518
1568
|
note.ending = true;
|
|
1519
1569
|
this.setNoteIndex(channel, index);
|
|
1520
1570
|
this.releaseNote(channel, note, endTime);
|
|
@@ -1545,16 +1595,12 @@ export class MidyGM2 {
|
|
|
1545
1595
|
}
|
|
1546
1596
|
return -1;
|
|
1547
1597
|
}
|
|
1548
|
-
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
1549
|
-
scheduleTime ??= this.audioContext.currentTime;
|
|
1550
|
-
return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
|
|
1551
|
-
}
|
|
1552
1598
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
1553
1599
|
const velocity = halfVelocity * 2;
|
|
1554
1600
|
const channel = this.channels[channelNumber];
|
|
1555
1601
|
const promises = [];
|
|
1556
1602
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1557
|
-
const promise = this.
|
|
1603
|
+
const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
|
|
1558
1604
|
promises.push(promise);
|
|
1559
1605
|
}
|
|
1560
1606
|
channel.sustainNotes = [];
|
|
@@ -1568,47 +1614,51 @@ export class MidyGM2 {
|
|
|
1568
1614
|
channel.state.sostenutoPedal = 0;
|
|
1569
1615
|
for (let i = 0; i < sostenutoNotes.length; i++) {
|
|
1570
1616
|
const note = sostenutoNotes[i];
|
|
1571
|
-
const promise = this.
|
|
1617
|
+
const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime);
|
|
1572
1618
|
promises.push(promise);
|
|
1573
1619
|
}
|
|
1574
1620
|
channel.sostenutoNotes = [];
|
|
1575
1621
|
return promises;
|
|
1576
1622
|
}
|
|
1577
|
-
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
1623
|
+
createMessageHandlers() {
|
|
1624
|
+
const handlers = new Array(256);
|
|
1625
|
+
// Channel Message
|
|
1626
|
+
handlers[0x80] = (data, scheduleTime) => this.noteOff(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1627
|
+
handlers[0x90] = (data, scheduleTime) => this.noteOn(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1628
|
+
handlers[0xB0] = (data, scheduleTime) => this.setControlChange(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1629
|
+
handlers[0xC0] = (data, scheduleTime) => this.setProgramChange(data[0] & 0x0F, data[1], scheduleTime);
|
|
1630
|
+
handlers[0xD0] = (data, scheduleTime) => this.setChannelPressure(data[0] & 0x0F, data[1], scheduleTime);
|
|
1631
|
+
handlers[0xE0] = (data, scheduleTime) => this.handlePitchBendMessage(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1632
|
+
// System Real Time Message
|
|
1633
|
+
handlers[0xFE] = (_data, _scheduleTime) => this.activeSensing();
|
|
1634
|
+
return handlers;
|
|
1635
|
+
}
|
|
1636
|
+
handleMessage(data, scheduleTime) {
|
|
1637
|
+
const status = data[0];
|
|
1638
|
+
if (status === 0xF0) {
|
|
1639
|
+
return this.handleSysEx(data.subarray(1), scheduleTime);
|
|
1595
1640
|
}
|
|
1641
|
+
const handler = this.messageHandlers[status];
|
|
1642
|
+
if (handler)
|
|
1643
|
+
handler(data, scheduleTime);
|
|
1644
|
+
}
|
|
1645
|
+
activeSensing() {
|
|
1646
|
+
this.lastActiveSensing = performance.now();
|
|
1596
1647
|
}
|
|
1597
1648
|
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
1598
1649
|
const channel = this.channels[channelNumber];
|
|
1599
|
-
channel.bank = channel.bankMSB * 128 + channel.bankLSB;
|
|
1600
1650
|
channel.programNumber = programNumber;
|
|
1601
1651
|
if (this.mode === "GM2") {
|
|
1602
1652
|
switch (channel.bankMSB) {
|
|
1603
1653
|
case 120:
|
|
1604
1654
|
channel.isDrum = true;
|
|
1655
|
+
channel.keyBasedTable.fill(-1);
|
|
1605
1656
|
break;
|
|
1606
1657
|
case 121:
|
|
1607
1658
|
channel.isDrum = false;
|
|
1608
1659
|
break;
|
|
1609
1660
|
}
|
|
1610
1661
|
}
|
|
1611
|
-
channel.keyBasedTable.fill(-1);
|
|
1612
1662
|
}
|
|
1613
1663
|
setChannelPressure(channelNumber, value, scheduleTime) {
|
|
1614
1664
|
const channel = this.channels[channelNumber];
|
|
@@ -1933,22 +1983,20 @@ export class MidyGM2 {
|
|
|
1933
1983
|
this.updateModulation(channel, scheduleTime);
|
|
1934
1984
|
}
|
|
1935
1985
|
updatePortamento(channel, scheduleTime) {
|
|
1986
|
+
if (channel.isDrum)
|
|
1987
|
+
return;
|
|
1936
1988
|
this.processScheduledNotes(channel, (note) => {
|
|
1937
|
-
if (
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
this.updateDetune(channel, note, scheduleTime);
|
|
1943
|
-
}
|
|
1989
|
+
if (this.isPortamento(channel, note)) {
|
|
1990
|
+
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
1991
|
+
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
1992
|
+
this.setPortamentoPitchEnvelope(note, scheduleTime);
|
|
1993
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1944
1994
|
}
|
|
1945
1995
|
else {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
this.updateDetune(channel, note, scheduleTime);
|
|
1951
|
-
}
|
|
1996
|
+
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
1997
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
1998
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1999
|
+
this.updateDetune(channel, note, scheduleTime);
|
|
1952
2000
|
}
|
|
1953
2001
|
});
|
|
1954
2002
|
}
|
|
@@ -2054,6 +2102,9 @@ export class MidyGM2 {
|
|
|
2054
2102
|
this.releaseSustainPedal(channelNumber, value, scheduleTime);
|
|
2055
2103
|
}
|
|
2056
2104
|
}
|
|
2105
|
+
isPortamento(channel, note) {
|
|
2106
|
+
return 0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber;
|
|
2107
|
+
}
|
|
2057
2108
|
setPortamento(channelNumber, value, scheduleTime) {
|
|
2058
2109
|
const channel = this.channels[channelNumber];
|
|
2059
2110
|
if (channel.isDrum)
|
|
@@ -2090,7 +2141,7 @@ export class MidyGM2 {
|
|
|
2090
2141
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2091
2142
|
state.softPedal = softPedal / 127;
|
|
2092
2143
|
this.processScheduledNotes(channel, (note) => {
|
|
2093
|
-
if (
|
|
2144
|
+
if (this.isPortamento(channel, note)) {
|
|
2094
2145
|
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2095
2146
|
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2096
2147
|
}
|
|
@@ -2358,11 +2409,9 @@ export class MidyGM2 {
|
|
|
2358
2409
|
const channel = this.channels[i];
|
|
2359
2410
|
channel.bankMSB = 0;
|
|
2360
2411
|
channel.bankLSB = 0;
|
|
2361
|
-
channel.bank = 0;
|
|
2362
2412
|
channel.isDrum = false;
|
|
2363
2413
|
}
|
|
2364
2414
|
this.channels[9].bankMSB = 1;
|
|
2365
|
-
this.channels[9].bank = 128;
|
|
2366
2415
|
this.channels[9].isDrum = true;
|
|
2367
2416
|
}
|
|
2368
2417
|
GM2SystemOn(scheduleTime) {
|
|
@@ -2373,11 +2422,9 @@ export class MidyGM2 {
|
|
|
2373
2422
|
const channel = this.channels[i];
|
|
2374
2423
|
channel.bankMSB = 121;
|
|
2375
2424
|
channel.bankLSB = 0;
|
|
2376
|
-
channel.bank = 121 * 128;
|
|
2377
2425
|
channel.isDrum = false;
|
|
2378
2426
|
}
|
|
2379
2427
|
this.channels[9].bankMSB = 120;
|
|
2380
|
-
this.channels[9].bank = 120 * 128;
|
|
2381
2428
|
this.channels[9].isDrum = true;
|
|
2382
2429
|
}
|
|
2383
2430
|
handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
|
|
@@ -2422,16 +2469,11 @@ export class MidyGM2 {
|
|
|
2422
2469
|
const volume = (data[5] * 128 + data[4]) / 16383;
|
|
2423
2470
|
this.setMasterVolume(volume, scheduleTime);
|
|
2424
2471
|
}
|
|
2425
|
-
setMasterVolume(
|
|
2472
|
+
setMasterVolume(value, scheduleTime) {
|
|
2426
2473
|
scheduleTime ??= this.audioContext.currentTime;
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
else {
|
|
2431
|
-
this.masterVolume.gain
|
|
2432
|
-
.cancelScheduledValues(scheduleTime)
|
|
2433
|
-
.setValueAtTime(volume * volume, scheduleTime);
|
|
2434
|
-
}
|
|
2474
|
+
this.masterVolume.gain
|
|
2475
|
+
.cancelScheduledValues(scheduleTime)
|
|
2476
|
+
.setValueAtTime(value * value, scheduleTime);
|
|
2435
2477
|
}
|
|
2436
2478
|
handleMasterFineTuningSysEx(data, scheduleTime) {
|
|
2437
2479
|
const value = (data[5] * 128 + data[4]) / 16383;
|
|
@@ -2794,6 +2836,22 @@ export class MidyGM2 {
|
|
|
2794
2836
|
const controlValue = channel.keyBasedTable[index];
|
|
2795
2837
|
return controlValue;
|
|
2796
2838
|
}
|
|
2839
|
+
createKeyBasedControllerHandlers() {
|
|
2840
|
+
const handlers = new Array(128);
|
|
2841
|
+
handlers[7] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
|
|
2842
|
+
handlers[10] = (channel, keyNumber, scheduleTime) => this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
|
|
2843
|
+
handlers[91] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
|
|
2844
|
+
if (note.noteNumber === keyNumber) {
|
|
2845
|
+
this.setReverbSend(channel, note, scheduleTime);
|
|
2846
|
+
}
|
|
2847
|
+
});
|
|
2848
|
+
handlers[93] = (channel, keyNumber, scheduleTime) => this.processScheduledNotes(channel, (note) => {
|
|
2849
|
+
if (note.noteNumber === keyNumber) {
|
|
2850
|
+
this.setChorusSend(channel, note, scheduleTime);
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
2853
|
+
return handlers;
|
|
2854
|
+
}
|
|
2797
2855
|
handleKeyBasedInstrumentControlSysEx(data, scheduleTime) {
|
|
2798
2856
|
const channelNumber = data[4];
|
|
2799
2857
|
const channel = this.channels[channelNumber];
|
|
@@ -2806,26 +2864,9 @@ export class MidyGM2 {
|
|
|
2806
2864
|
const value = data[i + 1];
|
|
2807
2865
|
const index = keyNumber * 128 + controllerType;
|
|
2808
2866
|
table[index] = value;
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
this.updateKeyBasedVolume(channel, keyNumber, scheduleTime);
|
|
2813
|
-
break;
|
|
2814
|
-
case 91:
|
|
2815
|
-
this.processScheduledNotes(channel, (note) => {
|
|
2816
|
-
if (note.noteNumber === keyNumber) {
|
|
2817
|
-
this.setReverbSend(channel, note, scheduleTime);
|
|
2818
|
-
}
|
|
2819
|
-
});
|
|
2820
|
-
break;
|
|
2821
|
-
case 93:
|
|
2822
|
-
this.processScheduledNotes(channel, (note) => {
|
|
2823
|
-
if (note.noteNumber === keyNumber) {
|
|
2824
|
-
this.setChorusSend(channel, note, scheduleTime);
|
|
2825
|
-
}
|
|
2826
|
-
});
|
|
2827
|
-
break;
|
|
2828
|
-
}
|
|
2867
|
+
const handler = this.keyBasedControllerHandlers[controllerType];
|
|
2868
|
+
if (handler)
|
|
2869
|
+
handler(channel, keyNumber, scheduleTime);
|
|
2829
2870
|
}
|
|
2830
2871
|
}
|
|
2831
2872
|
handleSysEx(data, scheduleTime) {
|
|
@@ -2866,7 +2907,6 @@ Object.defineProperty(MidyGM2, "channelSettings", {
|
|
|
2866
2907
|
scheduleIndex: 0,
|
|
2867
2908
|
detune: 0,
|
|
2868
2909
|
programNumber: 0,
|
|
2869
|
-
bank: 121 * 128,
|
|
2870
2910
|
bankMSB: 121,
|
|
2871
2911
|
bankLSB: 0,
|
|
2872
2912
|
dataMSB: 0,
|