@marmooo/midy 0.2.4 → 0.2.5
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 +20 -3
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +104 -10
- package/esm/midy-GM2.d.ts +22 -5
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +116 -25
- package/esm/midy-GMLite.d.ts +20 -3
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +104 -10
- package/esm/midy.d.ts +23 -5
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +145 -27
- package/package.json +1 -1
- package/script/midy-GM1.d.ts +20 -3
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +104 -10
- package/script/midy-GM2.d.ts +22 -5
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +116 -25
- package/script/midy-GMLite.d.ts +20 -3
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +104 -10
- package/script/midy.d.ts +23 -5
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +145 -27
package/esm/midy-GM2.js
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
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
|
+
}
|
|
3
55
|
class Note {
|
|
4
56
|
constructor(noteNumber, velocity, startTime, voice, voiceParams) {
|
|
5
57
|
Object.defineProperty(this, "bufferSource", {
|
|
@@ -280,6 +332,18 @@ export class MidyGM2 {
|
|
|
280
332
|
writable: true,
|
|
281
333
|
value: this.initSoundFontTable()
|
|
282
334
|
});
|
|
335
|
+
Object.defineProperty(this, "audioBufferCounter", {
|
|
336
|
+
enumerable: true,
|
|
337
|
+
configurable: true,
|
|
338
|
+
writable: true,
|
|
339
|
+
value: new Map()
|
|
340
|
+
});
|
|
341
|
+
Object.defineProperty(this, "audioBufferCache", {
|
|
342
|
+
enumerable: true,
|
|
343
|
+
configurable: true,
|
|
344
|
+
writable: true,
|
|
345
|
+
value: new Map()
|
|
346
|
+
});
|
|
283
347
|
Object.defineProperty(this, "isPlaying", {
|
|
284
348
|
enumerable: true,
|
|
285
349
|
configurable: true,
|
|
@@ -332,7 +396,7 @@ export class MidyGM2 {
|
|
|
332
396
|
enumerable: true,
|
|
333
397
|
configurable: true,
|
|
334
398
|
writable: true,
|
|
335
|
-
value: new
|
|
399
|
+
value: new SparseMap(128)
|
|
336
400
|
});
|
|
337
401
|
Object.defineProperty(this, "defaultOptions", {
|
|
338
402
|
enumerable: true,
|
|
@@ -372,7 +436,7 @@ export class MidyGM2 {
|
|
|
372
436
|
initSoundFontTable() {
|
|
373
437
|
const table = new Array(128);
|
|
374
438
|
for (let i = 0; i < 128; i++) {
|
|
375
|
-
table[i] = new
|
|
439
|
+
table[i] = new SparseMap(128);
|
|
376
440
|
}
|
|
377
441
|
return table;
|
|
378
442
|
}
|
|
@@ -426,8 +490,8 @@ export class MidyGM2 {
|
|
|
426
490
|
state: new ControllerState(),
|
|
427
491
|
controlTable: this.initControlTable(),
|
|
428
492
|
...this.setChannelAudioNodes(audioContext),
|
|
429
|
-
scheduledNotes: new
|
|
430
|
-
sostenutoNotes: new
|
|
493
|
+
scheduledNotes: new SparseMap(128),
|
|
494
|
+
sostenutoNotes: new SparseMap(128),
|
|
431
495
|
};
|
|
432
496
|
});
|
|
433
497
|
return channels;
|
|
@@ -461,9 +525,8 @@ export class MidyGM2 {
|
|
|
461
525
|
return audioBuffer;
|
|
462
526
|
}
|
|
463
527
|
}
|
|
464
|
-
|
|
528
|
+
createNoteBufferNode(audioBuffer, voiceParams) {
|
|
465
529
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
466
|
-
const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
|
|
467
530
|
bufferSource.buffer = audioBuffer;
|
|
468
531
|
bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
|
|
469
532
|
if (bufferSource.loop) {
|
|
@@ -552,6 +615,7 @@ export class MidyGM2 {
|
|
|
552
615
|
await Promise.all(this.notePromises);
|
|
553
616
|
this.notePromises = [];
|
|
554
617
|
this.exclusiveClassMap.clear();
|
|
618
|
+
this.audioBufferCache.clear();
|
|
555
619
|
resolve();
|
|
556
620
|
return;
|
|
557
621
|
}
|
|
@@ -567,8 +631,9 @@ export class MidyGM2 {
|
|
|
567
631
|
}
|
|
568
632
|
else if (this.isStopping) {
|
|
569
633
|
await this.stopNotes(0, true);
|
|
570
|
-
this.exclusiveClassMap.clear();
|
|
571
634
|
this.notePromises = [];
|
|
635
|
+
this.exclusiveClassMap.clear();
|
|
636
|
+
this.audioBufferCache.clear();
|
|
572
637
|
resolve();
|
|
573
638
|
this.isStopping = false;
|
|
574
639
|
this.isPaused = false;
|
|
@@ -599,6 +664,9 @@ export class MidyGM2 {
|
|
|
599
664
|
secondToTicks(second, secondsPerBeat) {
|
|
600
665
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
601
666
|
}
|
|
667
|
+
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
668
|
+
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
669
|
+
}
|
|
602
670
|
extractMidiData(midi) {
|
|
603
671
|
const instruments = new Set();
|
|
604
672
|
const timeline = [];
|
|
@@ -620,6 +688,8 @@ export class MidyGM2 {
|
|
|
620
688
|
switch (event.type) {
|
|
621
689
|
case "noteOn": {
|
|
622
690
|
const channel = tmpChannels[event.channel];
|
|
691
|
+
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
692
|
+
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
623
693
|
if (channel.programNumber < 0) {
|
|
624
694
|
channel.programNumber = event.programNumber;
|
|
625
695
|
switch (channel.bankMSB) {
|
|
@@ -669,6 +739,10 @@ export class MidyGM2 {
|
|
|
669
739
|
timeline.push(event);
|
|
670
740
|
}
|
|
671
741
|
}
|
|
742
|
+
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
743
|
+
if (count === 1)
|
|
744
|
+
this.audioBufferCounter.delete(audioBufferId);
|
|
745
|
+
}
|
|
672
746
|
const priority = {
|
|
673
747
|
controller: 0,
|
|
674
748
|
sysEx: 1,
|
|
@@ -762,7 +836,7 @@ export class MidyGM2 {
|
|
|
762
836
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
763
837
|
}
|
|
764
838
|
getActiveNotes(channel, time) {
|
|
765
|
-
const activeNotes = new
|
|
839
|
+
const activeNotes = new SparseMap(128);
|
|
766
840
|
channel.scheduledNotes.forEach((noteList) => {
|
|
767
841
|
const activeNote = this.getActiveNote(noteList, time);
|
|
768
842
|
if (activeNote) {
|
|
@@ -1097,12 +1171,31 @@ export class MidyGM2 {
|
|
|
1097
1171
|
note.vibratoLFO.connect(note.vibratoDepth);
|
|
1098
1172
|
note.vibratoDepth.connect(note.bufferSource.detune);
|
|
1099
1173
|
}
|
|
1174
|
+
async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
|
|
1175
|
+
const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
|
|
1176
|
+
const cache = this.audioBufferCache.get(audioBufferId);
|
|
1177
|
+
if (cache) {
|
|
1178
|
+
cache.counter += 1;
|
|
1179
|
+
if (cache.maxCount <= cache.counter) {
|
|
1180
|
+
this.audioBufferCache.delete(audioBufferId);
|
|
1181
|
+
}
|
|
1182
|
+
return cache.audioBuffer;
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
|
|
1186
|
+
const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
|
|
1187
|
+
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
1188
|
+
this.audioBufferCache.set(audioBufferId, cache);
|
|
1189
|
+
return audioBuffer;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1100
1192
|
async createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3) {
|
|
1101
1193
|
const state = channel.state;
|
|
1102
1194
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
1103
1195
|
const voiceParams = voice.getAllParams(controllerState);
|
|
1104
1196
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
1105
|
-
|
|
1197
|
+
const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
|
|
1198
|
+
note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
|
|
1106
1199
|
note.volumeNode = new GainNode(this.audioContext);
|
|
1107
1200
|
note.gainL = new GainNode(this.audioContext);
|
|
1108
1201
|
note.gainR = new GainNode(this.audioContext);
|
|
@@ -1162,10 +1255,10 @@ export class MidyGM2 {
|
|
|
1162
1255
|
if (soundFontIndex === undefined)
|
|
1163
1256
|
return;
|
|
1164
1257
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
1165
|
-
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
1166
1258
|
const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
|
|
1167
1259
|
if (!voice)
|
|
1168
1260
|
return;
|
|
1261
|
+
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
1169
1262
|
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, portamento, isSF3);
|
|
1170
1263
|
note.gainL.connect(channel.gainL);
|
|
1171
1264
|
note.gainR.connect(channel.gainR);
|
|
@@ -1334,6 +1427,7 @@ export class MidyGM2 {
|
|
|
1334
1427
|
channel.program = program;
|
|
1335
1428
|
}
|
|
1336
1429
|
handleChannelPressure(channelNumber, value) {
|
|
1430
|
+
const now = this.audioContext.currentTime;
|
|
1337
1431
|
const channel = this.channels[channelNumber];
|
|
1338
1432
|
const prev = channel.state.channelPressure;
|
|
1339
1433
|
const next = value / 127;
|
|
@@ -1343,13 +1437,8 @@ export class MidyGM2 {
|
|
|
1343
1437
|
channel.detune += pressureDepth * (next - prev);
|
|
1344
1438
|
}
|
|
1345
1439
|
const table = channel.channelPressureTable;
|
|
1346
|
-
channel.
|
|
1347
|
-
|
|
1348
|
-
const note = noteList[i];
|
|
1349
|
-
if (!note)
|
|
1350
|
-
continue;
|
|
1351
|
-
this.applyDestinationSettings(channel, note, table);
|
|
1352
|
-
}
|
|
1440
|
+
this.getActiveNotes(channel, now).forEach((note) => {
|
|
1441
|
+
this.applyDestinationSettings(channel, note, table);
|
|
1353
1442
|
});
|
|
1354
1443
|
// this.applyVoiceParams(channel, 13);
|
|
1355
1444
|
}
|
|
@@ -1750,8 +1839,7 @@ export class MidyGM2 {
|
|
|
1750
1839
|
channel.state.sostenutoPedal = value / 127;
|
|
1751
1840
|
if (64 <= value) {
|
|
1752
1841
|
const now = this.audioContext.currentTime;
|
|
1753
|
-
|
|
1754
|
-
channel.sostenutoNotes = new Map(activeNotes);
|
|
1842
|
+
channel.sostenutoNotes = this.getActiveNotes(channel, now);
|
|
1755
1843
|
}
|
|
1756
1844
|
else {
|
|
1757
1845
|
this.releaseSostenutoPedal(channelNumber, value);
|
|
@@ -2004,7 +2092,7 @@ export class MidyGM2 {
|
|
|
2004
2092
|
switch (data[3]) {
|
|
2005
2093
|
case 8:
|
|
2006
2094
|
// https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca21.pdf
|
|
2007
|
-
return this.handleScaleOctaveTuning1ByteFormatSysEx(data);
|
|
2095
|
+
return this.handleScaleOctaveTuning1ByteFormatSysEx(data, false);
|
|
2008
2096
|
default:
|
|
2009
2097
|
console.warn(`Unsupported Exclusive Message: ${data}`);
|
|
2010
2098
|
}
|
|
@@ -2326,8 +2414,8 @@ export class MidyGM2 {
|
|
|
2326
2414
|
}
|
|
2327
2415
|
return bitmap;
|
|
2328
2416
|
}
|
|
2329
|
-
handleScaleOctaveTuning1ByteFormatSysEx(data) {
|
|
2330
|
-
if (data.length <
|
|
2417
|
+
handleScaleOctaveTuning1ByteFormatSysEx(data, realtime) {
|
|
2418
|
+
if (data.length < 19) {
|
|
2331
2419
|
console.error("Data length is too short");
|
|
2332
2420
|
return;
|
|
2333
2421
|
}
|
|
@@ -2335,10 +2423,13 @@ export class MidyGM2 {
|
|
|
2335
2423
|
for (let i = 0; i < channelBitmap.length; i++) {
|
|
2336
2424
|
if (!channelBitmap[i])
|
|
2337
2425
|
continue;
|
|
2426
|
+
const channel = this.channels[i];
|
|
2338
2427
|
for (let j = 0; j < 12; j++) {
|
|
2339
|
-
const
|
|
2340
|
-
|
|
2428
|
+
const centValue = data[j + 7] - 64;
|
|
2429
|
+
channel.scaleOctaveTuningTable[j] = centValue;
|
|
2341
2430
|
}
|
|
2431
|
+
if (realtime)
|
|
2432
|
+
this.updateChannelDetune(channel);
|
|
2342
2433
|
}
|
|
2343
2434
|
}
|
|
2344
2435
|
applyDestinationSettings(channel, note, table) {
|
|
@@ -2470,7 +2561,7 @@ Object.defineProperty(MidyGM2, "channelSettings", {
|
|
|
2470
2561
|
value: {
|
|
2471
2562
|
currentBufferSource: null,
|
|
2472
2563
|
detune: 0,
|
|
2473
|
-
scaleOctaveTuningTable: new
|
|
2564
|
+
scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
|
|
2474
2565
|
channelPressureTable: new Uint8Array([64, 64, 64, 0, 0, 0]),
|
|
2475
2566
|
keyBasedInstrumentControlTable: new Int8Array(128 * 128), // [-64, 63]
|
|
2476
2567
|
program: 0,
|
package/esm/midy-GMLite.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export class MidyGMLite {
|
|
|
19
19
|
resumeTime: number;
|
|
20
20
|
soundFonts: any[];
|
|
21
21
|
soundFontTable: any[];
|
|
22
|
+
audioBufferCounter: Map<any, any>;
|
|
23
|
+
audioBufferCache: Map<any, any>;
|
|
22
24
|
isPlaying: boolean;
|
|
23
25
|
isPausing: boolean;
|
|
24
26
|
isPaused: boolean;
|
|
@@ -27,7 +29,7 @@ export class MidyGMLite {
|
|
|
27
29
|
timeline: any[];
|
|
28
30
|
instruments: any[];
|
|
29
31
|
notePromises: any[];
|
|
30
|
-
exclusiveClassMap:
|
|
32
|
+
exclusiveClassMap: SparseMap;
|
|
31
33
|
audioContext: any;
|
|
32
34
|
masterVolume: any;
|
|
33
35
|
voiceParamsHandlers: {
|
|
@@ -68,12 +70,13 @@ export class MidyGMLite {
|
|
|
68
70
|
};
|
|
69
71
|
createChannels(audioContext: any): any[];
|
|
70
72
|
createNoteBuffer(voiceParams: any, isSF3: any): Promise<any>;
|
|
71
|
-
createNoteBufferNode(
|
|
73
|
+
createNoteBufferNode(audioBuffer: any, voiceParams: any): any;
|
|
72
74
|
scheduleTimelineEvents(t: any, offset: any, queueIndex: any): Promise<any>;
|
|
73
75
|
getQueueIndex(second: any): number;
|
|
74
76
|
playNotes(): Promise<any>;
|
|
75
77
|
ticksToSecond(ticks: any, secondsPerBeat: any): number;
|
|
76
78
|
secondToTicks(second: any, secondsPerBeat: any): number;
|
|
79
|
+
getAudioBufferId(programNumber: any, noteNumber: any, velocity: any): string;
|
|
77
80
|
extractMidiData(midi: any): {
|
|
78
81
|
instruments: Set<any>;
|
|
79
82
|
timeline: any[];
|
|
@@ -87,7 +90,7 @@ export class MidyGMLite {
|
|
|
87
90
|
seekTo(second: any): void;
|
|
88
91
|
calcTotalTime(): number;
|
|
89
92
|
currentTime(): number;
|
|
90
|
-
getActiveNotes(channel: any, time: any):
|
|
93
|
+
getActiveNotes(channel: any, time: any): SparseMap;
|
|
91
94
|
getActiveNote(noteList: any, time: any): any;
|
|
92
95
|
cbToRatio(cb: any): number;
|
|
93
96
|
rateToCent(rate: any): number;
|
|
@@ -101,6 +104,7 @@ export class MidyGMLite {
|
|
|
101
104
|
clampCutoffFrequency(frequency: any): number;
|
|
102
105
|
setFilterEnvelope(note: any): void;
|
|
103
106
|
startModulation(channel: any, note: any, startTime: any): void;
|
|
107
|
+
getAudioBuffer(program: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
|
|
104
108
|
createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any, isSF3: any): Promise<Note>;
|
|
105
109
|
scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
|
|
106
110
|
noteOn(channelNumber: any, noteNumber: any, velocity: any): Promise<void>;
|
|
@@ -177,6 +181,19 @@ export class MidyGMLite {
|
|
|
177
181
|
handleSysEx(data: any): void;
|
|
178
182
|
scheduleTask(callback: any, startTime: any): Promise<any>;
|
|
179
183
|
}
|
|
184
|
+
declare class SparseMap {
|
|
185
|
+
constructor(size: any);
|
|
186
|
+
data: any[];
|
|
187
|
+
activeIndices: any[];
|
|
188
|
+
set(key: any, value: any): void;
|
|
189
|
+
get(key: any): any;
|
|
190
|
+
delete(key: any): boolean;
|
|
191
|
+
has(key: any): boolean;
|
|
192
|
+
get size(): number;
|
|
193
|
+
clear(): void;
|
|
194
|
+
forEach(callback: any): void;
|
|
195
|
+
[Symbol.iterator](): Generator<any[], void, unknown>;
|
|
196
|
+
}
|
|
180
197
|
declare class Note {
|
|
181
198
|
constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
|
|
182
199
|
bufferSource: any;
|
package/esm/midy-GMLite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AAgJA;IAsBE;;;;;;;;;MASE;IAEF,+BAQC;IAxCD,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,kCAA+B;IAC/B,gCAA6B;IAC7B,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,mBAAiB;IACjB,oBAAkB;IAClB,6BAAuC;IAcrC,kBAAgC;IAChC,kBAA8C;IAC9C;;;;;;;;;;;MAA2D;IAC3D;;;;;;;;;;;;;MAA+D;IAC/D,gBAAiD;IAKnD,4BAMC;IAED,mCAWC;IAED,gDAMC;IAED,sCASC;IAED;;;;MAeC;IAED,yCAUC;IAED,6DA2BC;IAED,8DASC;IAED,2EA+CC;IAED,mCAOC;IAED,0BAoDC;IAED,uDAEC;IAED,wDAEC;IAED,6EAEC;IAED;;;MA4EC;IAED,+EAmBC;IAED,qDAKC;IAED,uBAKC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,mDASC;IAED,6CAQC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,wCAIC;IAED,wCAQC;IAED,4CAKC;IAED,mCAgBC;IAED,kCAqBC;IAED,6CAIC;IAED,mCAuBC;IAED,+DAoBC;IAED,yGAgBC;IAED,gHAuCC;IAED,kGAgDC;IAED,0EAGC;IAED,qFAwBC;IAED,6HAuBC;IAED,0FAGC;IAED,kEAeC;IAED,gFAiBC;IAED,4DAGC;IAED,qEAGC;IAED,mDASC;IAED,gDASC;IAED,qCAMC;IAED,mCAQC;IAED,gCAOC;IAED,+BAMC;IAED;;;;;;;;;;;MAqBC;IAED,oFAMC;IAED,0DA6CC;IAED;;;;;;;;;;;;;MAeC;IAED,+EAWC;IAED,qCAeC;IAED,8DAIC;IACD,iDAIC;IAED;;;MAMC;IAED,2CAIC;IAED,yDAIC;IAED,mDAGC;IAED,wCAWC;IAED,sDAKC;IAED,kFAeC;IAED,oCAYC;IAED,gDAEC;IAED,gDAEC;IAED,mDAGC;IAED,kDAKC;IAED,wDASC;IAED,+CAEC;IAED,8CAqBC;IAED,+CAEC;IAED,4DAgBC;IAED,oBAMC;IAED,yDAaC;IAED,yCAGC;IAED,mCAQC;IAED,wCAEC;IAED,6BASC;IAED,0DAUC;CACF;AAv0CD;IACE,uBAGC;IAFC,YAA2B;IAC3B,qBAAuB;IAGzB,gCAKC;IAED,mBAEC;IAED,0BAUC;IAED,uBAEC;IAED,mBAEC;IAED,cAMC;IASD,6BAKC;IAZD,qDAKC;CAQF;AAED;IAQE,0FAMC;IAbD,kBAAa;IACb,gBAAW;IACX,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
|
package/esm/midy-GMLite.js
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
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
|
+
}
|
|
3
55
|
class Note {
|
|
4
56
|
constructor(noteNumber, velocity, startTime, voice, voiceParams) {
|
|
5
57
|
Object.defineProperty(this, "bufferSource", {
|
|
@@ -166,6 +218,18 @@ export class MidyGMLite {
|
|
|
166
218
|
writable: true,
|
|
167
219
|
value: this.initSoundFontTable()
|
|
168
220
|
});
|
|
221
|
+
Object.defineProperty(this, "audioBufferCounter", {
|
|
222
|
+
enumerable: true,
|
|
223
|
+
configurable: true,
|
|
224
|
+
writable: true,
|
|
225
|
+
value: new Map()
|
|
226
|
+
});
|
|
227
|
+
Object.defineProperty(this, "audioBufferCache", {
|
|
228
|
+
enumerable: true,
|
|
229
|
+
configurable: true,
|
|
230
|
+
writable: true,
|
|
231
|
+
value: new Map()
|
|
232
|
+
});
|
|
169
233
|
Object.defineProperty(this, "isPlaying", {
|
|
170
234
|
enumerable: true,
|
|
171
235
|
configurable: true,
|
|
@@ -218,7 +282,7 @@ export class MidyGMLite {
|
|
|
218
282
|
enumerable: true,
|
|
219
283
|
configurable: true,
|
|
220
284
|
writable: true,
|
|
221
|
-
value: new
|
|
285
|
+
value: new SparseMap(128)
|
|
222
286
|
});
|
|
223
287
|
this.audioContext = audioContext;
|
|
224
288
|
this.masterVolume = new GainNode(audioContext);
|
|
@@ -231,7 +295,7 @@ export class MidyGMLite {
|
|
|
231
295
|
initSoundFontTable() {
|
|
232
296
|
const table = new Array(128);
|
|
233
297
|
for (let i = 0; i < 128; i++) {
|
|
234
|
-
table[i] = new
|
|
298
|
+
table[i] = new SparseMap(128);
|
|
235
299
|
}
|
|
236
300
|
return table;
|
|
237
301
|
}
|
|
@@ -284,7 +348,7 @@ export class MidyGMLite {
|
|
|
284
348
|
...this.constructor.channelSettings,
|
|
285
349
|
state: new ControllerState(),
|
|
286
350
|
...this.setChannelAudioNodes(audioContext),
|
|
287
|
-
scheduledNotes: new
|
|
351
|
+
scheduledNotes: new SparseMap(128),
|
|
288
352
|
};
|
|
289
353
|
});
|
|
290
354
|
return channels;
|
|
@@ -318,9 +382,8 @@ export class MidyGMLite {
|
|
|
318
382
|
return audioBuffer;
|
|
319
383
|
}
|
|
320
384
|
}
|
|
321
|
-
|
|
385
|
+
createNoteBufferNode(audioBuffer, voiceParams) {
|
|
322
386
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
323
|
-
const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
|
|
324
387
|
bufferSource.buffer = audioBuffer;
|
|
325
388
|
bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
|
|
326
389
|
if (bufferSource.loop) {
|
|
@@ -385,6 +448,7 @@ export class MidyGMLite {
|
|
|
385
448
|
await Promise.all(this.notePromises);
|
|
386
449
|
this.notePromises = [];
|
|
387
450
|
this.exclusiveClassMap.clear();
|
|
451
|
+
this.audioBufferCache.clear();
|
|
388
452
|
resolve();
|
|
389
453
|
return;
|
|
390
454
|
}
|
|
@@ -400,8 +464,9 @@ export class MidyGMLite {
|
|
|
400
464
|
}
|
|
401
465
|
else if (this.isStopping) {
|
|
402
466
|
await this.stopNotes(0, true);
|
|
403
|
-
this.exclusiveClassMap.clear();
|
|
404
467
|
this.notePromises = [];
|
|
468
|
+
this.exclusiveClassMap.clear();
|
|
469
|
+
this.audioBufferCache.clear();
|
|
405
470
|
resolve();
|
|
406
471
|
this.isStopping = false;
|
|
407
472
|
this.isPaused = false;
|
|
@@ -432,6 +497,9 @@ export class MidyGMLite {
|
|
|
432
497
|
secondToTicks(second, secondsPerBeat) {
|
|
433
498
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
434
499
|
}
|
|
500
|
+
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
501
|
+
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
502
|
+
}
|
|
435
503
|
extractMidiData(midi) {
|
|
436
504
|
const instruments = new Set();
|
|
437
505
|
const timeline = [];
|
|
@@ -452,6 +520,8 @@ export class MidyGMLite {
|
|
|
452
520
|
switch (event.type) {
|
|
453
521
|
case "noteOn": {
|
|
454
522
|
const channel = tmpChannels[event.channel];
|
|
523
|
+
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
524
|
+
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
455
525
|
if (channel.programNumber < 0) {
|
|
456
526
|
instruments.add(`${channel.bank}:0`);
|
|
457
527
|
channel.programNumber = 0;
|
|
@@ -468,6 +538,10 @@ export class MidyGMLite {
|
|
|
468
538
|
timeline.push(event);
|
|
469
539
|
}
|
|
470
540
|
}
|
|
541
|
+
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
542
|
+
if (count === 1)
|
|
543
|
+
this.audioBufferCounter.delete(audioBufferId);
|
|
544
|
+
}
|
|
471
545
|
const priority = {
|
|
472
546
|
controller: 0,
|
|
473
547
|
sysEx: 1,
|
|
@@ -558,7 +632,7 @@ export class MidyGMLite {
|
|
|
558
632
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
559
633
|
}
|
|
560
634
|
getActiveNotes(channel, time) {
|
|
561
|
-
const activeNotes = new
|
|
635
|
+
const activeNotes = new SparseMap(128);
|
|
562
636
|
channel.scheduledNotes.forEach((noteList) => {
|
|
563
637
|
const activeNote = this.getActiveNote(noteList, time);
|
|
564
638
|
if (activeNote) {
|
|
@@ -698,11 +772,31 @@ export class MidyGMLite {
|
|
|
698
772
|
note.modulationLFO.connect(note.volumeDepth);
|
|
699
773
|
note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
|
|
700
774
|
}
|
|
775
|
+
async getAudioBuffer(program, noteNumber, velocity, voiceParams, isSF3) {
|
|
776
|
+
const audioBufferId = this.getAudioBufferId(program, noteNumber, velocity);
|
|
777
|
+
const cache = this.audioBufferCache.get(audioBufferId);
|
|
778
|
+
if (cache) {
|
|
779
|
+
cache.counter += 1;
|
|
780
|
+
if (cache.maxCount <= cache.counter) {
|
|
781
|
+
this.audioBufferCache.delete(audioBufferId);
|
|
782
|
+
}
|
|
783
|
+
return cache.audioBuffer;
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
const maxCount = this.audioBufferCounter.get(audioBufferId) ?? 0;
|
|
787
|
+
const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
|
|
788
|
+
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
789
|
+
this.audioBufferCache.set(audioBufferId, cache);
|
|
790
|
+
return audioBuffer;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
701
793
|
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
702
794
|
const state = channel.state;
|
|
703
|
-
const
|
|
795
|
+
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
796
|
+
const voiceParams = voice.getAllParams(controllerState);
|
|
704
797
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
705
|
-
|
|
798
|
+
const audioBuffer = await this.getAudioBuffer(channel.program, noteNumber, velocity, voiceParams, isSF3);
|
|
799
|
+
note.bufferSource = this.createNoteBufferNode(audioBuffer, voiceParams);
|
|
706
800
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
707
801
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
708
802
|
type: "lowpass",
|
|
@@ -726,10 +820,10 @@ export class MidyGMLite {
|
|
|
726
820
|
if (soundFontIndex === undefined)
|
|
727
821
|
return;
|
|
728
822
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
729
|
-
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
730
823
|
const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
|
|
731
824
|
if (!voice)
|
|
732
825
|
return;
|
|
826
|
+
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
733
827
|
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
734
828
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
735
829
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
package/esm/midy.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export class Midy {
|
|
|
2
2
|
static channelSettings: {
|
|
3
3
|
currentBufferSource: null;
|
|
4
4
|
detune: number;
|
|
5
|
-
scaleOctaveTuningTable:
|
|
5
|
+
scaleOctaveTuningTable: Float32Array<ArrayBuffer>;
|
|
6
6
|
channelPressureTable: Uint8Array<ArrayBuffer>;
|
|
7
7
|
polyphonicKeyPressureTable: Uint8Array<ArrayBuffer>;
|
|
8
8
|
keyBasedInstrumentControlTable: Int8Array<ArrayBuffer>;
|
|
@@ -48,6 +48,8 @@ export class Midy {
|
|
|
48
48
|
resumeTime: number;
|
|
49
49
|
soundFonts: any[];
|
|
50
50
|
soundFontTable: any[];
|
|
51
|
+
audioBufferCounter: Map<any, any>;
|
|
52
|
+
audioBufferCache: Map<any, any>;
|
|
51
53
|
isPlaying: boolean;
|
|
52
54
|
isPausing: boolean;
|
|
53
55
|
isPaused: boolean;
|
|
@@ -56,7 +58,7 @@ export class Midy {
|
|
|
56
58
|
timeline: any[];
|
|
57
59
|
instruments: any[];
|
|
58
60
|
notePromises: any[];
|
|
59
|
-
exclusiveClassMap:
|
|
61
|
+
exclusiveClassMap: SparseMap;
|
|
60
62
|
defaultOptions: {
|
|
61
63
|
reverbAlgorithm: (audioContext: any) => {
|
|
62
64
|
input: any;
|
|
@@ -144,13 +146,14 @@ export class Midy {
|
|
|
144
146
|
};
|
|
145
147
|
createChannels(audioContext: any): any[];
|
|
146
148
|
createNoteBuffer(voiceParams: any, isSF3: any): Promise<any>;
|
|
147
|
-
createNoteBufferNode(
|
|
149
|
+
createNoteBufferNode(audioBuffer: any, voiceParams: any): any;
|
|
148
150
|
findPortamentoTarget(queueIndex: any): any;
|
|
149
151
|
scheduleTimelineEvents(t: any, offset: any, queueIndex: any): Promise<any>;
|
|
150
152
|
getQueueIndex(second: any): number;
|
|
151
153
|
playNotes(): Promise<any>;
|
|
152
154
|
ticksToSecond(ticks: any, secondsPerBeat: any): number;
|
|
153
155
|
secondToTicks(second: any, secondsPerBeat: any): number;
|
|
156
|
+
getAudioBufferId(programNumber: any, noteNumber: any, velocity: any): string;
|
|
154
157
|
extractMidiData(midi: any): {
|
|
155
158
|
instruments: Set<any>;
|
|
156
159
|
timeline: any[];
|
|
@@ -164,7 +167,7 @@ export class Midy {
|
|
|
164
167
|
seekTo(second: any): void;
|
|
165
168
|
calcTotalTime(): number;
|
|
166
169
|
currentTime(): number;
|
|
167
|
-
getActiveNotes(channel: any, time: any):
|
|
170
|
+
getActiveNotes(channel: any, time: any): SparseMap;
|
|
168
171
|
getActiveNote(noteList: any, time: any): any;
|
|
169
172
|
createConvolutionReverbImpulse(audioContext: any, decay: any, preDecay: any): any;
|
|
170
173
|
createConvolutionReverb(audioContext: any, impulse: any): {
|
|
@@ -205,6 +208,7 @@ export class Midy {
|
|
|
205
208
|
setFilterEnvelope(channel: any, note: any, pressure: any): void;
|
|
206
209
|
startModulation(channel: any, note: any, startTime: any): void;
|
|
207
210
|
startVibrato(channel: any, note: any, startTime: any): void;
|
|
211
|
+
getAudioBuffer(program: any, noteNumber: any, velocity: any, voiceParams: any, isSF3: any): Promise<any>;
|
|
208
212
|
createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any, portamento: any, isSF3: any): Promise<Note>;
|
|
209
213
|
calcBank(channel: any, channelNumber: any): any;
|
|
210
214
|
scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any, portamento: any): Promise<void>;
|
|
@@ -362,7 +366,8 @@ export class Midy {
|
|
|
362
366
|
setChorusSendToReverb(value: any): void;
|
|
363
367
|
getChorusSendToReverb(value: any): number;
|
|
364
368
|
getChannelBitmap(data: any): any[];
|
|
365
|
-
handleScaleOctaveTuning1ByteFormatSysEx(data: any): void;
|
|
369
|
+
handleScaleOctaveTuning1ByteFormatSysEx(data: any, realtime: any): void;
|
|
370
|
+
handleScaleOctaveTuning2ByteFormatSysEx(data: any, realtime: any): void;
|
|
366
371
|
applyDestinationSettings(channel: any, note: any, table: any): void;
|
|
367
372
|
handleChannelPressureSysEx(data: any, tableName: any): void;
|
|
368
373
|
initControlTable(): Uint8Array<ArrayBuffer>;
|
|
@@ -374,6 +379,19 @@ export class Midy {
|
|
|
374
379
|
handleSysEx(data: any): any;
|
|
375
380
|
scheduleTask(callback: any, startTime: any): Promise<any>;
|
|
376
381
|
}
|
|
382
|
+
declare class SparseMap {
|
|
383
|
+
constructor(size: any);
|
|
384
|
+
data: any[];
|
|
385
|
+
activeIndices: any[];
|
|
386
|
+
set(key: any, value: any): void;
|
|
387
|
+
get(key: any): any;
|
|
388
|
+
delete(key: any): boolean;
|
|
389
|
+
has(key: any): boolean;
|
|
390
|
+
get size(): number;
|
|
391
|
+
clear(): void;
|
|
392
|
+
forEach(callback: any): void;
|
|
393
|
+
[Symbol.iterator](): Generator<any[], void, unknown>;
|
|
394
|
+
}
|
|
377
395
|
declare class Note {
|
|
378
396
|
constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
|
|
379
397
|
bufferSource: any;
|