@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marmooo/midy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A MIDI player/synthesizer written in JavaScript that supports GM-Lite/GM1 and SF2/SF3.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test": "node test_runner.js"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@marmooo/soundfont-parser": "^0.1.
|
|
25
|
+
"@marmooo/soundfont-parser": "^0.1.4",
|
|
26
26
|
"midi-file": "^1.2.4"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
package/script/midy-GM1.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export class MidyGM1 {
|
|
2
2
|
static channelSettings: {
|
|
3
|
+
scheduleIndex: number;
|
|
3
4
|
detune: number;
|
|
4
5
|
programNumber: number;
|
|
5
|
-
bank: number;
|
|
6
6
|
dataMSB: number;
|
|
7
7
|
dataLSB: number;
|
|
8
8
|
rpnMSB: number;
|
|
@@ -22,9 +22,10 @@ export class MidyGM1 {
|
|
|
22
22
|
startTime: number;
|
|
23
23
|
resumeTime: number;
|
|
24
24
|
soundFonts: any[];
|
|
25
|
-
soundFontTable:
|
|
25
|
+
soundFontTable: never[][];
|
|
26
26
|
voiceCounter: Map<any, any>;
|
|
27
27
|
voiceCache: Map<any, any>;
|
|
28
|
+
realtimeVoiceCache: Map<any, any>;
|
|
28
29
|
isPlaying: boolean;
|
|
29
30
|
isPausing: boolean;
|
|
30
31
|
isPaused: boolean;
|
|
@@ -39,6 +40,7 @@ export class MidyGM1 {
|
|
|
39
40
|
masterVolume: any;
|
|
40
41
|
scheduler: any;
|
|
41
42
|
schedulerBuffer: any;
|
|
43
|
+
messageHandlers: any[];
|
|
42
44
|
voiceParamsHandlers: {
|
|
43
45
|
modLfoToPitch: (channel: any, note: any, scheduleTime: any) => void;
|
|
44
46
|
vibLfoToPitch: (_channel: any, _note: any, _scheduleTime: any) => void;
|
|
@@ -53,7 +55,6 @@ export class MidyGM1 {
|
|
|
53
55
|
};
|
|
54
56
|
controlChangeHandlers: any[];
|
|
55
57
|
channels: any[];
|
|
56
|
-
initSoundFontTable(): any[];
|
|
57
58
|
addSoundFont(soundFont: any): void;
|
|
58
59
|
toUint8Array(input: any): Promise<Uint8Array<ArrayBuffer>>;
|
|
59
60
|
loadSoundFont(input: any): Promise<void>;
|
|
@@ -68,13 +69,14 @@ export class MidyGM1 {
|
|
|
68
69
|
createChannels(audioContext: any): any[];
|
|
69
70
|
createAudioBuffer(voiceParams: any): Promise<any>;
|
|
70
71
|
createBufferSource(voiceParams: any, audioBuffer: any): any;
|
|
71
|
-
scheduleTimelineEvents(
|
|
72
|
+
scheduleTimelineEvents(scheduleTime: any, queueIndex: any): Promise<any>;
|
|
72
73
|
getQueueIndex(second: any): number;
|
|
73
74
|
resetAllStates(): void;
|
|
74
75
|
updateStates(queueIndex: any, nextQueueIndex: any): void;
|
|
75
76
|
playNotes(): Promise<void>;
|
|
76
77
|
ticksToSecond(ticks: any, secondsPerBeat: any): number;
|
|
77
78
|
secondToTicks(second: any, secondsPerBeat: any): number;
|
|
79
|
+
getSoundFontId(channel: any): string;
|
|
78
80
|
extractMidiData(midi: any): {
|
|
79
81
|
instruments: Set<any>;
|
|
80
82
|
timeline: any[];
|
|
@@ -103,19 +105,20 @@ export class MidyGM1 {
|
|
|
103
105
|
clampCutoffFrequency(frequency: any): number;
|
|
104
106
|
setFilterEnvelope(note: any, scheduleTime: any): void;
|
|
105
107
|
startModulation(channel: any, note: any, scheduleTime: any): void;
|
|
106
|
-
getAudioBuffer(channel: any, noteNumber: any, velocity: any, voiceParams: any): Promise<any>;
|
|
107
|
-
|
|
108
|
+
getAudioBuffer(channel: any, noteNumber: any, velocity: any, voiceParams: any, realtime: any): Promise<any>;
|
|
109
|
+
setNoteAudioNode(channel: any, note: any, realtime: any): Promise<any>;
|
|
108
110
|
handleExclusiveClass(note: any, channelNumber: any, startTime: any): void;
|
|
109
|
-
|
|
110
|
-
noteOn(channelNumber: any, noteNumber: any, velocity: any,
|
|
111
|
+
setNoteRouting(channelNumber: any, note: any, startTime: any): void;
|
|
112
|
+
noteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
|
|
111
113
|
disconnectNote(note: any): void;
|
|
112
114
|
releaseNote(channel: any, note: any, endTime: any): Promise<any>;
|
|
113
|
-
|
|
115
|
+
noteOff(channelNumber: any, noteNumber: any, velocity: any, endTime: any, force: any): void;
|
|
114
116
|
setNoteIndex(channel: any, index: any): void;
|
|
115
117
|
findNoteOffIndex(channel: any, noteNumber: any): any;
|
|
116
|
-
noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): void;
|
|
117
118
|
releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): void[];
|
|
118
|
-
|
|
119
|
+
createMessageHandlers(): any[];
|
|
120
|
+
handleMessage(data: any, scheduleTime: any): void;
|
|
121
|
+
handleChannelMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<void>;
|
|
119
122
|
setProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
|
|
120
123
|
handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
|
|
121
124
|
setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;
|
|
@@ -172,26 +175,8 @@ export class MidyGM1 {
|
|
|
172
175
|
GM1SystemOn(scheduleTime: any): void;
|
|
173
176
|
handleUniversalRealTimeExclusiveMessage(data: any, scheduleTime: any): void;
|
|
174
177
|
handleMasterVolumeSysEx(data: any, scheduleTime: any): void;
|
|
175
|
-
setMasterVolume(
|
|
178
|
+
setMasterVolume(value: any, scheduleTime: any): void;
|
|
176
179
|
handleSysEx(data: any, scheduleTime: any): void;
|
|
177
180
|
scheduleTask(callback: any, scheduleTime: any): Promise<any>;
|
|
178
181
|
}
|
|
179
|
-
declare class Note {
|
|
180
|
-
constructor(noteNumber: any, velocity: any, startTime: any, voice: any, voiceParams: any);
|
|
181
|
-
index: number;
|
|
182
|
-
ending: boolean;
|
|
183
|
-
bufferSource: any;
|
|
184
|
-
filterNode: any;
|
|
185
|
-
filterDepth: any;
|
|
186
|
-
volumeEnvelopeNode: any;
|
|
187
|
-
volumeDepth: any;
|
|
188
|
-
modulationLFO: any;
|
|
189
|
-
modulationDepth: any;
|
|
190
|
-
noteNumber: any;
|
|
191
|
-
velocity: any;
|
|
192
|
-
startTime: any;
|
|
193
|
-
voice: any;
|
|
194
|
-
voiceParams: any;
|
|
195
|
-
}
|
|
196
|
-
export {};
|
|
197
182
|
//# sourceMappingURL=midy-GM1.d.ts.map
|
package/script/midy-GM1.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AA6FA;IA0BE;;;;;;;;;;;MAWE;IAEF,+BAeC;IArDD,aAAa;IACb,oBAAiB;IACjB,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,0BAAuD;IACvD,4BAAyB;IACzB,0BAAuB;IACvB,kCAA+B;IAC/B,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,iBAAY;IACZ,gBAAc;IACd,oBAAkB;IAClB,sBAAwB;IACxB,2BAAqC;IAgBnC,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF,uBAAmD;IACnD;;;;;;;;;;;MAA2D;IAC3D,6BAA+D;IAC/D,gBAAiD;IAMnD,mCASC;IAED,2DAYC;IAED,yCAmBC;IAED,oCASC;IAED,sBAoCC;IAED,8DAWC;IAED;;;;MAeC;IAED,yCAaC;IAED,kDAUC;IAED,4DASC;IAED,yEAqDC;IAED,mCAOC;IAED,uBASC;IAED,yDA2BC;IAED,2BAwCC;IAED,uDAEC;IAED,wDAEC;IAED,qCAKC;IAED;;;MAwDC;IAED,kGAeC;IAED,mGAeC;IAED,wEAMC;IAED,uBAMC;IAED,sBAKC;IAED,uBAQC;IAED,wBAKC;IAED,0BAKC;IAED,wBAOC;IAED,sBAIC;IAED,yDAQC;IAED,yEASC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,qCAMC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,4GAkCC;IAED,uEAmCC;IAED,0EAiBC;IAED,oEASC;IAED,0FAwBC;IAED,gCASC;IAED,iEAqBC;IAED,4FAmBC;IAED,6CAUC;IAED,qDAUC;IAED,sFAeC;IAED,+BAmBC;IAED,kDAOC;IAED,uGA2BC;IAED,mFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAWC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MAiCC;IAED,oFAMC;IAED,6EA2BC;IAED,qCAeC;IAED,+FAWC;IAED,wDASC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,iEAMC;IAED,uEAQC;IAED,mEAKC;IAED,yEAQC;IAED,gFAGC;IAED,6CAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCASC;IAED,4EAaC;IAED,4DAGC;IAED,qDAKC;IAED,gDAYC;IAGD,6DAgBC;CACF"}
|
package/script/midy-GM1.js
CHANGED
|
@@ -4,7 +4,19 @@ exports.MidyGM1 = 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,
|
|
@@ -62,8 +80,6 @@ class Note {
|
|
|
62
80
|
this.noteNumber = noteNumber;
|
|
63
81
|
this.velocity = velocity;
|
|
64
82
|
this.startTime = startTime;
|
|
65
|
-
this.voice = voice;
|
|
66
|
-
this.voiceParams = voiceParams;
|
|
67
83
|
}
|
|
68
84
|
}
|
|
69
85
|
// normalized to 0-1 for use with the SF2 modulator model
|
|
@@ -203,7 +219,7 @@ class MidyGM1 {
|
|
|
203
219
|
enumerable: true,
|
|
204
220
|
configurable: true,
|
|
205
221
|
writable: true,
|
|
206
|
-
value:
|
|
222
|
+
value: Array.from({ length: 128 }, () => [])
|
|
207
223
|
});
|
|
208
224
|
Object.defineProperty(this, "voiceCounter", {
|
|
209
225
|
enumerable: true,
|
|
@@ -217,6 +233,12 @@ class MidyGM1 {
|
|
|
217
233
|
writable: true,
|
|
218
234
|
value: new Map()
|
|
219
235
|
});
|
|
236
|
+
Object.defineProperty(this, "realtimeVoiceCache", {
|
|
237
|
+
enumerable: true,
|
|
238
|
+
configurable: true,
|
|
239
|
+
writable: true,
|
|
240
|
+
value: new Map()
|
|
241
|
+
});
|
|
220
242
|
Object.defineProperty(this, "isPlaying", {
|
|
221
243
|
enumerable: true,
|
|
222
244
|
configurable: true,
|
|
@@ -284,6 +306,7 @@ class MidyGM1 {
|
|
|
284
306
|
length: 1,
|
|
285
307
|
sampleRate: audioContext.sampleRate,
|
|
286
308
|
});
|
|
309
|
+
this.messageHandlers = this.createMessageHandlers();
|
|
287
310
|
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
288
311
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
289
312
|
this.channels = this.createChannels(audioContext);
|
|
@@ -291,21 +314,14 @@ class MidyGM1 {
|
|
|
291
314
|
this.scheduler.connect(audioContext.destination);
|
|
292
315
|
this.GM1SystemOn();
|
|
293
316
|
}
|
|
294
|
-
initSoundFontTable() {
|
|
295
|
-
const table = new Array(128);
|
|
296
|
-
for (let i = 0; i < 128; i++) {
|
|
297
|
-
table[i] = new Map();
|
|
298
|
-
}
|
|
299
|
-
return table;
|
|
300
|
-
}
|
|
301
317
|
addSoundFont(soundFont) {
|
|
302
318
|
const index = this.soundFonts.length;
|
|
303
319
|
this.soundFonts.push(soundFont);
|
|
304
320
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
321
|
+
const soundFontTable = this.soundFontTable;
|
|
305
322
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
banks.set(presetHeader.bank, index);
|
|
323
|
+
const { preset, bank } = presetHeaders[i];
|
|
324
|
+
soundFontTable[preset][bank] = index;
|
|
309
325
|
}
|
|
310
326
|
}
|
|
311
327
|
async toUint8Array(input) {
|
|
@@ -383,13 +399,16 @@ class MidyGM1 {
|
|
|
383
399
|
this.GM1SystemOn();
|
|
384
400
|
}
|
|
385
401
|
getVoiceId(channel, noteNumber, velocity) {
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
|
|
402
|
+
const programNumber = channel.programNumber;
|
|
403
|
+
const bankTable = this.soundFontTable[programNumber];
|
|
404
|
+
if (!bankTable)
|
|
405
|
+
return;
|
|
406
|
+
const bank = channel.isDrum ? 128 : 0;
|
|
407
|
+
const soundFontIndex = bankTable[bank];
|
|
389
408
|
if (soundFontIndex === undefined)
|
|
390
409
|
return;
|
|
391
410
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
392
|
-
const voice = soundFont.getVoice(
|
|
411
|
+
const voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
393
412
|
const { instrument, sampleID } = voice.generators;
|
|
394
413
|
return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
|
|
395
414
|
}
|
|
@@ -438,19 +457,22 @@ class MidyGM1 {
|
|
|
438
457
|
}
|
|
439
458
|
return bufferSource;
|
|
440
459
|
}
|
|
441
|
-
async scheduleTimelineEvents(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
460
|
+
async scheduleTimelineEvents(scheduleTime, queueIndex) {
|
|
461
|
+
const timeOffset = this.resumeTime - this.startTime;
|
|
462
|
+
const lookAheadCheckTime = scheduleTime + timeOffset + this.lookAhead;
|
|
463
|
+
const schedulingOffset = this.startDelay - timeOffset;
|
|
464
|
+
const timeline = this.timeline;
|
|
465
|
+
while (queueIndex < timeline.length) {
|
|
466
|
+
const event = timeline[queueIndex];
|
|
467
|
+
if (lookAheadCheckTime < event.startTime)
|
|
445
468
|
break;
|
|
446
|
-
const
|
|
447
|
-
const startTime = event.startTime + delay;
|
|
469
|
+
const startTime = event.startTime + schedulingOffset;
|
|
448
470
|
switch (event.type) {
|
|
449
471
|
case "noteOn":
|
|
450
|
-
await this.
|
|
472
|
+
await this.noteOn(event.channel, event.noteNumber, event.velocity, startTime);
|
|
451
473
|
break;
|
|
452
474
|
case "noteOff": {
|
|
453
|
-
const notePromise = this.
|
|
475
|
+
const notePromise = this.noteOff(event.channel, event.noteNumber, event.velocity, startTime, false);
|
|
454
476
|
if (notePromise)
|
|
455
477
|
this.notePromises.push(notePromise);
|
|
456
478
|
break;
|
|
@@ -483,6 +505,7 @@ class MidyGM1 {
|
|
|
483
505
|
this.exclusiveClassNotes.fill(undefined);
|
|
484
506
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
485
507
|
this.voiceCache.clear();
|
|
508
|
+
this.realtimeVoiceCache.clear();
|
|
486
509
|
for (let i = 0; i < this.channels.length; i++) {
|
|
487
510
|
this.channels[i].scheduledNotes = [];
|
|
488
511
|
this.resetChannelStates(i);
|
|
@@ -516,13 +539,10 @@ class MidyGM1 {
|
|
|
516
539
|
this.isPaused = false;
|
|
517
540
|
this.startTime = this.audioContext.currentTime;
|
|
518
541
|
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
519
|
-
let resumeTime = this.resumeTime - this.startTime;
|
|
520
542
|
let finished = false;
|
|
521
543
|
this.notePromises = [];
|
|
522
544
|
while (queueIndex < this.timeline.length) {
|
|
523
545
|
const now = this.audioContext.currentTime;
|
|
524
|
-
const t = now + resumeTime;
|
|
525
|
-
queueIndex = await this.scheduleTimelineEvents(t, resumeTime, queueIndex);
|
|
526
546
|
if (this.isPausing) {
|
|
527
547
|
await this.stopNotes(0, true, now);
|
|
528
548
|
await this.audioContext.suspend();
|
|
@@ -541,10 +561,10 @@ class MidyGM1 {
|
|
|
541
561
|
const nextQueueIndex = this.getQueueIndex(this.resumeTime);
|
|
542
562
|
this.updateStates(queueIndex, nextQueueIndex);
|
|
543
563
|
queueIndex = nextQueueIndex;
|
|
544
|
-
resumeTime = this.resumeTime - this.startTime;
|
|
545
564
|
this.isSeeking = false;
|
|
546
565
|
continue;
|
|
547
566
|
}
|
|
567
|
+
queueIndex = await this.scheduleTimelineEvents(now, queueIndex);
|
|
548
568
|
const waitTime = now + this.noteCheckInterval;
|
|
549
569
|
await this.scheduleTask(() => { }, waitTime);
|
|
550
570
|
}
|
|
@@ -560,16 +580,16 @@ class MidyGM1 {
|
|
|
560
580
|
secondToTicks(second, secondsPerBeat) {
|
|
561
581
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
562
582
|
}
|
|
583
|
+
getSoundFontId(channel) {
|
|
584
|
+
const programNumber = channel.programNumber;
|
|
585
|
+
const bank = channel.isDrum ? "128" : "000";
|
|
586
|
+
const program = programNumber.toString().padStart(3, "0");
|
|
587
|
+
return `${bank}:${program}`;
|
|
588
|
+
}
|
|
563
589
|
extractMidiData(midi) {
|
|
564
590
|
const instruments = new Set();
|
|
565
591
|
const timeline = [];
|
|
566
|
-
const
|
|
567
|
-
for (let i = 0; i < tmpChannels.length; i++) {
|
|
568
|
-
tmpChannels[i] = {
|
|
569
|
-
programNumber: -1,
|
|
570
|
-
bank: this.channels[i].bank,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
592
|
+
const channels = this.channels;
|
|
573
593
|
for (let i = 0; i < midi.tracks.length; i++) {
|
|
574
594
|
const track = midi.tracks[i];
|
|
575
595
|
let currentTicks = 0;
|
|
@@ -579,17 +599,15 @@ class MidyGM1 {
|
|
|
579
599
|
event.ticks = currentTicks;
|
|
580
600
|
switch (event.type) {
|
|
581
601
|
case "noteOn": {
|
|
582
|
-
const channel =
|
|
583
|
-
|
|
584
|
-
instruments.add(`${channel.bank}:0`);
|
|
585
|
-
channel.programNumber = 0;
|
|
586
|
-
}
|
|
602
|
+
const channel = channels[event.channel];
|
|
603
|
+
instruments.add(this.getSoundFontId(channel));
|
|
587
604
|
break;
|
|
588
605
|
}
|
|
589
606
|
case "programChange": {
|
|
590
|
-
const channel =
|
|
591
|
-
channel
|
|
592
|
-
instruments.add(
|
|
607
|
+
const channel = channels[event.channel];
|
|
608
|
+
this.setProgramChange(event.channel, event.programNumber);
|
|
609
|
+
instruments.add(this.getSoundFontId(channel));
|
|
610
|
+
break;
|
|
593
611
|
}
|
|
594
612
|
}
|
|
595
613
|
delete event.deltaTime;
|
|
@@ -624,7 +642,7 @@ class MidyGM1 {
|
|
|
624
642
|
const channel = this.channels[channelNumber];
|
|
625
643
|
const promises = [];
|
|
626
644
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
627
|
-
const promise = this.
|
|
645
|
+
const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
628
646
|
this.notePromises.push(promise);
|
|
629
647
|
promises.push(promise);
|
|
630
648
|
});
|
|
@@ -634,7 +652,7 @@ class MidyGM1 {
|
|
|
634
652
|
const channel = this.channels[channelNumber];
|
|
635
653
|
const promises = [];
|
|
636
654
|
this.processScheduledNotes(channel, (note) => {
|
|
637
|
-
const promise = this.
|
|
655
|
+
const promise = this.noteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
638
656
|
this.notePromises.push(promise);
|
|
639
657
|
promises.push(promise);
|
|
640
658
|
});
|
|
@@ -667,7 +685,7 @@ class MidyGM1 {
|
|
|
667
685
|
if (!this.isPlaying || this.isPaused)
|
|
668
686
|
return;
|
|
669
687
|
const now = this.audioContext.currentTime;
|
|
670
|
-
this.resumeTime
|
|
688
|
+
this.resumeTime = now - this.startTime - this.startDelay;
|
|
671
689
|
this.isPausing = true;
|
|
672
690
|
await this.playPromise;
|
|
673
691
|
this.isPausing = false;
|
|
@@ -693,11 +711,13 @@ class MidyGM1 {
|
|
|
693
711
|
if (totalTime < event.startTime)
|
|
694
712
|
totalTime = event.startTime;
|
|
695
713
|
}
|
|
696
|
-
return totalTime;
|
|
714
|
+
return totalTime + this.startDelay;
|
|
697
715
|
}
|
|
698
716
|
currentTime() {
|
|
717
|
+
if (!this.isPlaying)
|
|
718
|
+
return this.resumeTime;
|
|
699
719
|
const now = this.audioContext.currentTime;
|
|
700
|
-
return
|
|
720
|
+
return now + this.resumeTime - this.startTime;
|
|
701
721
|
}
|
|
702
722
|
processScheduledNotes(channel, callback) {
|
|
703
723
|
const scheduledNotes = channel.scheduledNotes;
|
|
@@ -836,31 +856,42 @@ class MidyGM1 {
|
|
|
836
856
|
note.modulationLFO.connect(note.volumeDepth);
|
|
837
857
|
note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
|
|
838
858
|
}
|
|
839
|
-
async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
|
|
859
|
+
async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
|
|
840
860
|
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
this.voiceCache.delete(audioBufferId);
|
|
846
|
-
}
|
|
847
|
-
return cache.audioBuffer;
|
|
848
|
-
}
|
|
849
|
-
else {
|
|
850
|
-
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
861
|
+
if (realtime) {
|
|
862
|
+
const cachedAudioBuffer = this.realtimeVoiceCache.get(audioBufferId);
|
|
863
|
+
if (cachedAudioBuffer)
|
|
864
|
+
return cachedAudioBuffer;
|
|
851
865
|
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
852
|
-
|
|
853
|
-
this.voiceCache.set(audioBufferId, cache);
|
|
866
|
+
this.realtimeVoiceCache.set(audioBufferId, audioBuffer);
|
|
854
867
|
return audioBuffer;
|
|
855
868
|
}
|
|
869
|
+
else {
|
|
870
|
+
const cache = this.voiceCache.get(audioBufferId);
|
|
871
|
+
if (cache) {
|
|
872
|
+
cache.counter += 1;
|
|
873
|
+
if (cache.maxCount <= cache.counter) {
|
|
874
|
+
this.voiceCache.delete(audioBufferId);
|
|
875
|
+
}
|
|
876
|
+
return cache.audioBuffer;
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
880
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
881
|
+
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
882
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
883
|
+
return audioBuffer;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
856
886
|
}
|
|
857
|
-
async
|
|
887
|
+
async setNoteAudioNode(channel, note, realtime) {
|
|
858
888
|
const now = this.audioContext.currentTime;
|
|
889
|
+
const { noteNumber, velocity, startTime } = note;
|
|
859
890
|
const state = channel.state;
|
|
860
891
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
861
|
-
const voiceParams = voice.getAllParams(controllerState);
|
|
862
|
-
|
|
863
|
-
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
892
|
+
const voiceParams = note.voice.getAllParams(controllerState);
|
|
893
|
+
note.voiceParams = voiceParams;
|
|
894
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
864
895
|
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
865
896
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
866
897
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
@@ -887,37 +918,50 @@ class MidyGM1 {
|
|
|
887
918
|
if (prev) {
|
|
888
919
|
const [prevNote, prevChannelNumber] = prev;
|
|
889
920
|
if (prevNote && !prevNote.ending) {
|
|
890
|
-
this.
|
|
921
|
+
this.noteOff(prevChannelNumber, prevNote.noteNumber, 0, // velocity,
|
|
891
922
|
startTime, true);
|
|
892
923
|
}
|
|
893
924
|
}
|
|
894
925
|
this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
|
|
895
926
|
}
|
|
896
|
-
|
|
927
|
+
setNoteRouting(channelNumber, note, startTime) {
|
|
897
928
|
const channel = this.channels[channelNumber];
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
if (soundFontIndex === undefined)
|
|
902
|
-
return;
|
|
903
|
-
const soundFont = this.soundFonts[soundFontIndex];
|
|
904
|
-
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
905
|
-
if (!voice)
|
|
906
|
-
return;
|
|
907
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
908
|
-
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
909
|
-
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
929
|
+
const volumeEnvelopeNode = note.volumeEnvelopeNode;
|
|
930
|
+
volumeEnvelopeNode.connect(channel.gainL);
|
|
931
|
+
volumeEnvelopeNode.connect(channel.gainR);
|
|
910
932
|
if (0.5 <= channel.state.sustainPedal) {
|
|
911
933
|
channel.sustainNotes.push(note);
|
|
912
934
|
}
|
|
913
935
|
this.handleExclusiveClass(note, channelNumber, startTime);
|
|
936
|
+
}
|
|
937
|
+
async noteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
938
|
+
const channel = this.channels[channelNumber];
|
|
939
|
+
const realtime = startTime === undefined;
|
|
940
|
+
if (realtime)
|
|
941
|
+
startTime = this.audioContext.currentTime;
|
|
942
|
+
const note = new Note(noteNumber, velocity, startTime);
|
|
914
943
|
const scheduledNotes = channel.scheduledNotes;
|
|
915
944
|
note.index = scheduledNotes.length;
|
|
916
945
|
scheduledNotes.push(note);
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
946
|
+
const programNumber = channel.programNumber;
|
|
947
|
+
const bankTable = this.soundFontTable[programNumber];
|
|
948
|
+
if (!bankTable)
|
|
949
|
+
return;
|
|
950
|
+
const bank = channel.isDrum ? 128 : 0;
|
|
951
|
+
const soundFontIndex = bankTable[bank];
|
|
952
|
+
if (soundFontIndex === undefined)
|
|
953
|
+
return;
|
|
954
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
955
|
+
note.voice = soundFont.getVoice(bank, programNumber, noteNumber, velocity);
|
|
956
|
+
if (!note.voice)
|
|
957
|
+
return;
|
|
958
|
+
await this.setNoteAudioNode(channel, note, realtime);
|
|
959
|
+
this.setNoteRouting(channelNumber, note, startTime);
|
|
960
|
+
note.pending = false;
|
|
961
|
+
const off = note.offEvent;
|
|
962
|
+
if (off) {
|
|
963
|
+
this.noteOff(channelNumber, noteNumber, off.velocity, off.startTime);
|
|
964
|
+
}
|
|
921
965
|
}
|
|
922
966
|
disconnectNote(note) {
|
|
923
967
|
note.bufferSource.disconnect();
|
|
@@ -930,6 +974,7 @@ class MidyGM1 {
|
|
|
930
974
|
}
|
|
931
975
|
}
|
|
932
976
|
releaseNote(channel, note, endTime) {
|
|
977
|
+
endTime ??= this.audioContext.currentTime;
|
|
933
978
|
const volRelease = endTime + note.voiceParams.volRelease;
|
|
934
979
|
const modRelease = endTime + note.voiceParams.modRelease;
|
|
935
980
|
const stopTime = Math.min(volRelease, modRelease);
|
|
@@ -950,7 +995,7 @@ class MidyGM1 {
|
|
|
950
995
|
}, stopTime);
|
|
951
996
|
});
|
|
952
997
|
}
|
|
953
|
-
|
|
998
|
+
noteOff(channelNumber, noteNumber, velocity, endTime, force) {
|
|
954
999
|
const channel = this.channels[channelNumber];
|
|
955
1000
|
if (!force && 0.5 <= channel.state.sustainPedal)
|
|
956
1001
|
return;
|
|
@@ -958,6 +1003,10 @@ class MidyGM1 {
|
|
|
958
1003
|
if (index < 0)
|
|
959
1004
|
return;
|
|
960
1005
|
const note = channel.scheduledNotes[index];
|
|
1006
|
+
if (note.pending) {
|
|
1007
|
+
note.offEvent = { velocity, startTime: endTime };
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
961
1010
|
note.ending = true;
|
|
962
1011
|
this.setNoteIndex(channel, index);
|
|
963
1012
|
this.releaseNote(channel, note, endTime);
|
|
@@ -988,22 +1037,37 @@ class MidyGM1 {
|
|
|
988
1037
|
}
|
|
989
1038
|
return -1;
|
|
990
1039
|
}
|
|
991
|
-
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
992
|
-
scheduleTime ??= this.audioContext.currentTime;
|
|
993
|
-
return this.scheduleNoteOff(channelNumber, noteNumber, velocity, scheduleTime, false);
|
|
994
|
-
}
|
|
995
1040
|
releaseSustainPedal(channelNumber, halfVelocity, scheduleTime) {
|
|
996
1041
|
const velocity = halfVelocity * 2;
|
|
997
1042
|
const channel = this.channels[channelNumber];
|
|
998
1043
|
const promises = [];
|
|
999
1044
|
for (let i = 0; i < channel.sustainNotes.length; i++) {
|
|
1000
|
-
const promise = this.
|
|
1045
|
+
const promise = this.noteOff(channelNumber, channel.sustainNotes[i].noteNumber, velocity, scheduleTime);
|
|
1001
1046
|
promises.push(promise);
|
|
1002
1047
|
}
|
|
1003
1048
|
channel.sustainNotes = [];
|
|
1004
1049
|
return promises;
|
|
1005
1050
|
}
|
|
1006
|
-
|
|
1051
|
+
createMessageHandlers() {
|
|
1052
|
+
const handlers = new Array(256);
|
|
1053
|
+
// Channel Message
|
|
1054
|
+
handlers[0x80] = (data, scheduleTime) => this.noteOff(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1055
|
+
handlers[0x90] = (data, scheduleTime) => this.noteOn(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1056
|
+
handlers[0xB0] = (data, scheduleTime) => this.setControlChange(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1057
|
+
handlers[0xC0] = (data, scheduleTime) => this.setProgramChange(data[0] & 0x0F, data[1], scheduleTime);
|
|
1058
|
+
handlers[0xE0] = (data, scheduleTime) => this.handlePitchBendMessage(data[0] & 0x0F, data[1], data[2], scheduleTime);
|
|
1059
|
+
return handlers;
|
|
1060
|
+
}
|
|
1061
|
+
handleMessage(data, scheduleTime) {
|
|
1062
|
+
const status = data[0];
|
|
1063
|
+
if (status === 0xF0) {
|
|
1064
|
+
return this.handleSysEx(data.subarray(1), scheduleTime);
|
|
1065
|
+
}
|
|
1066
|
+
const handler = this.messageHandlers[status];
|
|
1067
|
+
if (handler)
|
|
1068
|
+
handler(data, scheduleTime);
|
|
1069
|
+
}
|
|
1070
|
+
handleChannelMessage(statusByte, data1, data2, scheduleTime) {
|
|
1007
1071
|
const channelNumber = statusByte & 0x0F;
|
|
1008
1072
|
const messageType = statusByte & 0xF0;
|
|
1009
1073
|
switch (messageType) {
|
|
@@ -1431,10 +1495,8 @@ class MidyGM1 {
|
|
|
1431
1495
|
for (let i = 0; i < this.channels.length; i++) {
|
|
1432
1496
|
this.allSoundOff(i, 0, scheduleTime);
|
|
1433
1497
|
const channel = this.channels[i];
|
|
1434
|
-
channel.bank = 0;
|
|
1435
1498
|
channel.isDrum = false;
|
|
1436
1499
|
}
|
|
1437
|
-
this.channels[9].bank = 128;
|
|
1438
1500
|
this.channels[9].isDrum = true;
|
|
1439
1501
|
}
|
|
1440
1502
|
handleUniversalRealTimeExclusiveMessage(data, scheduleTime) {
|
|
@@ -1455,16 +1517,11 @@ class MidyGM1 {
|
|
|
1455
1517
|
const volume = (data[5] * 128 + data[4]) / 16383;
|
|
1456
1518
|
this.setMasterVolume(volume, scheduleTime);
|
|
1457
1519
|
}
|
|
1458
|
-
setMasterVolume(
|
|
1520
|
+
setMasterVolume(value, scheduleTime) {
|
|
1459
1521
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
else {
|
|
1464
|
-
this.masterVolume.gain
|
|
1465
|
-
.cancelScheduledValues(scheduleTime)
|
|
1466
|
-
.setValueAtTime(volume * volume, scheduleTime);
|
|
1467
|
-
}
|
|
1522
|
+
this.masterVolume.gain
|
|
1523
|
+
.cancelScheduledValues(scheduleTime)
|
|
1524
|
+
.setValueAtTime(value * value, scheduleTime);
|
|
1468
1525
|
}
|
|
1469
1526
|
handleSysEx(data, scheduleTime) {
|
|
1470
1527
|
switch (data[0]) {
|
|
@@ -1502,9 +1559,9 @@ Object.defineProperty(MidyGM1, "channelSettings", {
|
|
|
1502
1559
|
configurable: true,
|
|
1503
1560
|
writable: true,
|
|
1504
1561
|
value: {
|
|
1562
|
+
scheduleIndex: 0,
|
|
1505
1563
|
detune: 0,
|
|
1506
1564
|
programNumber: 0,
|
|
1507
|
-
bank: 0,
|
|
1508
1565
|
dataMSB: 0,
|
|
1509
1566
|
dataLSB: 0,
|
|
1510
1567
|
rpnMSB: 127,
|