@marmooo/midy 0.3.4 → 0.3.6
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 +19 -11
- package/esm/midy-GM1.d.ts +14 -11
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +140 -106
- package/esm/midy-GM2.d.ts +26 -22
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +285 -279
- package/esm/midy-GMLite.d.ts +15 -11
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +139 -107
- package/esm/midy.d.ts +31 -27
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +304 -298
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +14 -11
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +140 -106
- package/script/midy-GM2.d.ts +26 -22
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +285 -279
- package/script/midy-GMLite.d.ts +15 -11
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +139 -107
- package/script/midy.d.ts +31 -27
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +304 -298
package/script/midy-GMLite.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export class MidyGMLite {
|
|
2
2
|
static channelSettings: {
|
|
3
|
+
scheduleIndex: number;
|
|
3
4
|
detune: number;
|
|
4
5
|
programNumber: number;
|
|
5
6
|
bank: number;
|
|
@@ -21,16 +22,16 @@ export class MidyGMLite {
|
|
|
21
22
|
resumeTime: number;
|
|
22
23
|
soundFonts: any[];
|
|
23
24
|
soundFontTable: any[];
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
voiceCounter: Map<any, any>;
|
|
26
|
+
voiceCache: Map<any, any>;
|
|
26
27
|
isPlaying: boolean;
|
|
27
28
|
isPausing: boolean;
|
|
28
29
|
isPaused: boolean;
|
|
29
30
|
isStopping: boolean;
|
|
30
31
|
isSeeking: boolean;
|
|
31
32
|
timeline: any[];
|
|
32
|
-
instruments: any[];
|
|
33
33
|
notePromises: any[];
|
|
34
|
+
instruments: Set<any>;
|
|
34
35
|
exclusiveClassNotes: any[];
|
|
35
36
|
drumExclusiveClassNotes: any[];
|
|
36
37
|
audioContext: any;
|
|
@@ -53,22 +54,24 @@ export class MidyGMLite {
|
|
|
53
54
|
channels: any[];
|
|
54
55
|
initSoundFontTable(): any[];
|
|
55
56
|
addSoundFont(soundFont: any): void;
|
|
57
|
+
toUint8Array(input: any): Promise<Uint8Array<ArrayBuffer>>;
|
|
56
58
|
loadSoundFont(input: any): Promise<void>;
|
|
57
59
|
loadMIDI(input: any): Promise<void>;
|
|
60
|
+
cacheVoiceIds(): void;
|
|
61
|
+
getVoiceId(channel: any, noteNumber: any, velocity: any): any;
|
|
58
62
|
createChannelAudioNodes(audioContext: any): {
|
|
59
63
|
gainL: any;
|
|
60
64
|
gainR: any;
|
|
61
65
|
merger: any;
|
|
62
66
|
};
|
|
63
67
|
createChannels(audioContext: any): any[];
|
|
64
|
-
|
|
68
|
+
createAudioBuffer(voiceParams: any): Promise<any>;
|
|
65
69
|
createBufferSource(channel: any, voiceParams: any, audioBuffer: any): any;
|
|
66
70
|
scheduleTimelineEvents(t: any, resumeTime: any, queueIndex: any): Promise<any>;
|
|
67
71
|
getQueueIndex(second: any): number;
|
|
68
72
|
playNotes(): Promise<any>;
|
|
69
73
|
ticksToSecond(ticks: any, secondsPerBeat: any): number;
|
|
70
74
|
secondToTicks(second: any, secondsPerBeat: any): number;
|
|
71
|
-
getAudioBufferId(programNumber: any, noteNumber: any, velocity: any): string;
|
|
72
75
|
extractMidiData(midi: any): {
|
|
73
76
|
instruments: Set<any>;
|
|
74
77
|
timeline: any[];
|
|
@@ -83,7 +86,7 @@ export class MidyGMLite {
|
|
|
83
86
|
seekTo(second: any): void;
|
|
84
87
|
calcTotalTime(): number;
|
|
85
88
|
currentTime(): number;
|
|
86
|
-
processScheduledNotes(channel: any,
|
|
89
|
+
processScheduledNotes(channel: any, callback: any): void;
|
|
87
90
|
processActiveNotes(channel: any, scheduleTime: any, callback: any): void;
|
|
88
91
|
cbToRatio(cb: any): number;
|
|
89
92
|
rateToCent(rate: any): number;
|
|
@@ -97,8 +100,8 @@ export class MidyGMLite {
|
|
|
97
100
|
clampCutoffFrequency(frequency: any): number;
|
|
98
101
|
setFilterEnvelope(note: any, scheduleTime: any): void;
|
|
99
102
|
startModulation(channel: any, note: any, scheduleTime: any): void;
|
|
100
|
-
getAudioBuffer(
|
|
101
|
-
createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any
|
|
103
|
+
getAudioBuffer(channel: any, noteNumber: any, velocity: any, voiceParams: any): Promise<any>;
|
|
104
|
+
createNote(channel: any, voice: any, noteNumber: any, velocity: any, startTime: any): Promise<Note>;
|
|
102
105
|
handleExclusiveClass(note: any, channelNumber: any, startTime: any): void;
|
|
103
106
|
handleDrumExclusiveClass(note: any, channelNumber: any, startTime: any): void;
|
|
104
107
|
scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
|
|
@@ -106,11 +109,12 @@ export class MidyGMLite {
|
|
|
106
109
|
disconnectNote(note: any): void;
|
|
107
110
|
releaseNote(channel: any, note: any, endTime: any): Promise<any>;
|
|
108
111
|
scheduleNoteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): void;
|
|
109
|
-
|
|
112
|
+
setNoteIndex(channel: any, index: any): void;
|
|
113
|
+
findNoteOffIndex(channel: any, noteNumber: any): any;
|
|
110
114
|
noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): void;
|
|
111
115
|
releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): void[];
|
|
112
116
|
handleMIDIMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<void>;
|
|
113
|
-
|
|
117
|
+
setProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
|
|
114
118
|
handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
|
|
115
119
|
setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;
|
|
116
120
|
setModLfoToPitch(channel: any, note: any, scheduleTime: any): void;
|
|
@@ -133,7 +137,7 @@ export class MidyGMLite {
|
|
|
133
137
|
getControllerState(channel: any, noteNumber: any, velocity: any): Float32Array<any>;
|
|
134
138
|
applyVoiceParams(channel: any, controllerType: any, scheduleTime: any): void;
|
|
135
139
|
createControlChangeHandlers(): any[];
|
|
136
|
-
|
|
140
|
+
setControlChange(channelNumber: any, controllerType: any, value: any, scheduleTime: any): void;
|
|
137
141
|
updateModulation(channel: any, scheduleTime: any): void;
|
|
138
142
|
setModulationDepth(channelNumber: any, modulation: any, scheduleTime: any): void;
|
|
139
143
|
setVolume(channelNumber: any, volume: any, scheduleTime: any): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AA0GA;IA2BE
|
|
1
|
+
{"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"AA0GA;IA2BE;;;;;;;;;;MAUE;IAEF,+BAcC;IApDD,aAAa;IACb,oBAAiB;IACjB,qBAAmB;IACnB,kBAAc;IACd,0BAAwB;IACxB,kBAAc;IACd,mBAAiB;IACjB,kBAAc;IACd,mBAAe;IACf,kBAAgB;IAChB,sBAA2C;IAC3C,4BAAyB;IACzB,0BAAuB;IACvB,mBAAkB;IAClB,mBAAkB;IAClB,kBAAiB;IACjB,oBAAmB;IACnB,mBAAkB;IAClB,gBAAc;IACd,oBAAkB;IAClB,sBAAwB;IACxB,2BAAqC;IACrC,+BAEE;IAeA,kBAAgC;IAChC,kBAA8C;IAC9C,eAAwD;IACxD,qBAGE;IACF;;;;;;;;;;;MAA2D;IAC3D,6BAA+D;IAC/D,gBAAiD;IAMnD,4BAMC;IAED,mCASC;IAED,2DAYC;IAED,yCAmBC;IAED,oCASC;IAED,sBAoCC;IAED,8DAcC;IAED;;;;MAeC;IAED,yCAaC;IAED,kDAUC;IAED,0EAUC;IAED,+EAkDC;IAED,mCAOC;IAED,0BAiEC;IAED,uDAEC;IAED,wDAEC;IAED;;;MAgEC;IAED,kGAeC;IAED,mGAeC;IAED,wEAMC;IAED,uBAMC;IAED,aAGC;IAED,cAKC;IAED,wBAIC;IAED,0BAKC;IAED,wBAOC;IAED,sBAGC;IAED,yDAQC;IAED,yEASC;IAED,2BAEC;IAED,8BAEC;IAED,8BAEC;IAED,4BAEC;IAED,wCAIC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,6FAyBC;IAED,oGAuCC;IAED,0EAiBC;IAED,8EAiBC;IAED,kGAoCC;IAED,6FASC;IAED,gCASC;IAED,iEAoBC;IAED,qGAkBC;IAED,6CAUC;IAED,qDAUC;IAED,qFASC;IAED,sFAeC;IAED,oGA2BC;IAED,mFAGC;IAED,wFAGC;IAED,sEAUC;IAED,mEAWC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EAgCC;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,uDAYC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,gFAGC;IAED,yCAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AAx/CD;IAWE,0FAMC;IAhBD,cAAW;IACX,gBAAe;IACf,kBAAa;IACb,gBAAW;IACX,iBAAY;IACZ,wBAAmB;IACnB,iBAAY;IACZ,mBAAc;IACd,qBAAgB;IAGd,gBAA4B;IAC5B,cAAwB;IACxB,eAA0B;IAC1B,WAAkB;IAClB,iBAA8B;CAEjC"}
|
package/script/midy-GMLite.js
CHANGED
|
@@ -218,13 +218,13 @@ class MidyGMLite {
|
|
|
218
218
|
writable: true,
|
|
219
219
|
value: this.initSoundFontTable()
|
|
220
220
|
});
|
|
221
|
-
Object.defineProperty(this, "
|
|
221
|
+
Object.defineProperty(this, "voiceCounter", {
|
|
222
222
|
enumerable: true,
|
|
223
223
|
configurable: true,
|
|
224
224
|
writable: true,
|
|
225
225
|
value: new Map()
|
|
226
226
|
});
|
|
227
|
-
Object.defineProperty(this, "
|
|
227
|
+
Object.defineProperty(this, "voiceCache", {
|
|
228
228
|
enumerable: true,
|
|
229
229
|
configurable: true,
|
|
230
230
|
writable: true,
|
|
@@ -266,17 +266,17 @@ class MidyGMLite {
|
|
|
266
266
|
writable: true,
|
|
267
267
|
value: []
|
|
268
268
|
});
|
|
269
|
-
Object.defineProperty(this, "
|
|
269
|
+
Object.defineProperty(this, "notePromises", {
|
|
270
270
|
enumerable: true,
|
|
271
271
|
configurable: true,
|
|
272
272
|
writable: true,
|
|
273
273
|
value: []
|
|
274
274
|
});
|
|
275
|
-
Object.defineProperty(this, "
|
|
275
|
+
Object.defineProperty(this, "instruments", {
|
|
276
276
|
enumerable: true,
|
|
277
277
|
configurable: true,
|
|
278
278
|
writable: true,
|
|
279
|
-
value:
|
|
279
|
+
value: new Set()
|
|
280
280
|
});
|
|
281
281
|
Object.defineProperty(this, "exclusiveClassNotes", {
|
|
282
282
|
enumerable: true,
|
|
@@ -317,13 +317,11 @@ class MidyGMLite {
|
|
|
317
317
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
318
318
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
319
319
|
const presetHeader = presetHeaders[i];
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
banks.set(presetHeader.bank, index);
|
|
323
|
-
}
|
|
320
|
+
const banks = this.soundFontTable[presetHeader.preset];
|
|
321
|
+
banks.set(presetHeader.bank, index);
|
|
324
322
|
}
|
|
325
323
|
}
|
|
326
|
-
async
|
|
324
|
+
async toUint8Array(input) {
|
|
327
325
|
let uint8Array;
|
|
328
326
|
if (typeof input === "string") {
|
|
329
327
|
const response = await fetch(input);
|
|
@@ -336,23 +334,32 @@ class MidyGMLite {
|
|
|
336
334
|
else {
|
|
337
335
|
throw new TypeError("input must be a URL string or Uint8Array");
|
|
338
336
|
}
|
|
339
|
-
|
|
340
|
-
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
341
|
-
this.addSoundFont(soundFont);
|
|
337
|
+
return uint8Array;
|
|
342
338
|
}
|
|
343
|
-
async
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
339
|
+
async loadSoundFont(input) {
|
|
340
|
+
this.voiceCounter.clear();
|
|
341
|
+
if (Array.isArray(input)) {
|
|
342
|
+
const promises = new Array(input.length);
|
|
343
|
+
for (let i = 0; i < input.length; i++) {
|
|
344
|
+
promises[i] = this.toUint8Array(input[i]);
|
|
345
|
+
}
|
|
346
|
+
const uint8Arrays = await Promise.all(promises);
|
|
347
|
+
for (let i = 0; i < uint8Arrays.length; i++) {
|
|
348
|
+
const parsed = (0, soundfont_parser_1.parse)(uint8Arrays[i]);
|
|
349
|
+
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
350
|
+
this.addSoundFont(soundFont);
|
|
351
|
+
}
|
|
352
352
|
}
|
|
353
353
|
else {
|
|
354
|
-
|
|
354
|
+
const uint8Array = await this.toUint8Array(input);
|
|
355
|
+
const parsed = (0, soundfont_parser_1.parse)(uint8Array);
|
|
356
|
+
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
357
|
+
this.addSoundFont(soundFont);
|
|
355
358
|
}
|
|
359
|
+
}
|
|
360
|
+
async loadMIDI(input) {
|
|
361
|
+
this.voiceCounter.clear();
|
|
362
|
+
const uint8Array = await this.toUint8Array(input);
|
|
356
363
|
const midi = (0, midi_file_1.parseMidi)(uint8Array);
|
|
357
364
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
358
365
|
const midiData = this.extractMidiData(midi);
|
|
@@ -360,6 +367,45 @@ class MidyGMLite {
|
|
|
360
367
|
this.timeline = midiData.timeline;
|
|
361
368
|
this.totalTime = this.calcTotalTime();
|
|
362
369
|
}
|
|
370
|
+
cacheVoiceIds() {
|
|
371
|
+
const timeline = this.timeline;
|
|
372
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
373
|
+
const event = timeline[i];
|
|
374
|
+
switch (event.type) {
|
|
375
|
+
case "noteOn": {
|
|
376
|
+
const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
|
|
377
|
+
this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case "controller":
|
|
381
|
+
if (event.controllerType === 0) {
|
|
382
|
+
this.setBankMSB(event.channel, event.value);
|
|
383
|
+
}
|
|
384
|
+
else if (event.controllerType === 32) {
|
|
385
|
+
this.setBankLSB(event.channel, event.value);
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
case "programChange":
|
|
389
|
+
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const [audioBufferId, count] of this.voiceCounter) {
|
|
393
|
+
if (count === 1)
|
|
394
|
+
this.voiceCounter.delete(audioBufferId);
|
|
395
|
+
}
|
|
396
|
+
this.GM1SystemOn();
|
|
397
|
+
}
|
|
398
|
+
getVoiceId(channel, noteNumber, velocity) {
|
|
399
|
+
const bankNumber = this.calcBank(channel);
|
|
400
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
401
|
+
.get(bankNumber);
|
|
402
|
+
if (soundFontIndex === undefined)
|
|
403
|
+
return;
|
|
404
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
405
|
+
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
406
|
+
const { instrument, sampleID } = voice.generators;
|
|
407
|
+
return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
|
|
408
|
+
}
|
|
363
409
|
createChannelAudioNodes(audioContext) {
|
|
364
410
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
365
411
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
@@ -388,34 +434,12 @@ class MidyGMLite {
|
|
|
388
434
|
});
|
|
389
435
|
return channels;
|
|
390
436
|
}
|
|
391
|
-
async
|
|
437
|
+
async createAudioBuffer(voiceParams) {
|
|
438
|
+
const sample = voiceParams.sample;
|
|
392
439
|
const sampleStart = voiceParams.start;
|
|
393
|
-
const sampleEnd =
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const start = sample.byteOffset + sampleStart;
|
|
397
|
-
const end = sample.byteOffset + sampleEnd;
|
|
398
|
-
const buffer = sample.buffer.slice(start, end);
|
|
399
|
-
const audioBuffer = await this.audioContext.decodeAudioData(buffer);
|
|
400
|
-
return audioBuffer;
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
const sample = voiceParams.sample;
|
|
404
|
-
const start = sample.byteOffset + sampleStart;
|
|
405
|
-
const end = sample.byteOffset + sampleEnd;
|
|
406
|
-
const buffer = sample.buffer.slice(start, end);
|
|
407
|
-
const audioBuffer = new AudioBuffer({
|
|
408
|
-
numberOfChannels: 1,
|
|
409
|
-
length: sample.length,
|
|
410
|
-
sampleRate: voiceParams.sampleRate,
|
|
411
|
-
});
|
|
412
|
-
const channelData = audioBuffer.getChannelData(0);
|
|
413
|
-
const int16Array = new Int16Array(buffer);
|
|
414
|
-
for (let i = 0; i < int16Array.length; i++) {
|
|
415
|
-
channelData[i] = int16Array[i] / 32768;
|
|
416
|
-
}
|
|
417
|
-
return audioBuffer;
|
|
418
|
-
}
|
|
440
|
+
const sampleEnd = sample.data.length + voiceParams.end;
|
|
441
|
+
const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
|
|
442
|
+
return audioBuffer;
|
|
419
443
|
}
|
|
420
444
|
createBufferSource(channel, voiceParams, audioBuffer) {
|
|
421
445
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
@@ -447,10 +471,10 @@ class MidyGMLite {
|
|
|
447
471
|
break;
|
|
448
472
|
}
|
|
449
473
|
case "controller":
|
|
450
|
-
this.
|
|
474
|
+
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
451
475
|
break;
|
|
452
476
|
case "programChange":
|
|
453
|
-
this.
|
|
477
|
+
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
454
478
|
break;
|
|
455
479
|
case "pitchBend":
|
|
456
480
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
@@ -484,7 +508,7 @@ class MidyGMLite {
|
|
|
484
508
|
this.notePromises = [];
|
|
485
509
|
this.exclusiveClassNotes.fill(undefined);
|
|
486
510
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
487
|
-
this.
|
|
511
|
+
this.voiceCache.clear();
|
|
488
512
|
for (let i = 0; i < this.channels.length; i++) {
|
|
489
513
|
this.resetAllStates(i);
|
|
490
514
|
}
|
|
@@ -507,7 +531,7 @@ class MidyGMLite {
|
|
|
507
531
|
this.notePromises = [];
|
|
508
532
|
this.exclusiveClassNotes.fill(undefined);
|
|
509
533
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
510
|
-
this.
|
|
534
|
+
this.voiceCache.clear();
|
|
511
535
|
for (let i = 0; i < this.channels.length; i++) {
|
|
512
536
|
this.resetAllStates(i);
|
|
513
537
|
}
|
|
@@ -541,11 +565,7 @@ class MidyGMLite {
|
|
|
541
565
|
secondToTicks(second, secondsPerBeat) {
|
|
542
566
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
543
567
|
}
|
|
544
|
-
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
545
|
-
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
546
|
-
}
|
|
547
568
|
extractMidiData(midi) {
|
|
548
|
-
this.audioBufferCounter.clear();
|
|
549
569
|
const instruments = new Set();
|
|
550
570
|
const timeline = [];
|
|
551
571
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -565,8 +585,6 @@ class MidyGMLite {
|
|
|
565
585
|
switch (event.type) {
|
|
566
586
|
case "noteOn": {
|
|
567
587
|
const channel = tmpChannels[event.channel];
|
|
568
|
-
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
569
|
-
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
570
588
|
if (channel.programNumber < 0) {
|
|
571
589
|
instruments.add(`${channel.bank}:0`);
|
|
572
590
|
channel.programNumber = 0;
|
|
@@ -583,10 +601,6 @@ class MidyGMLite {
|
|
|
583
601
|
timeline.push(event);
|
|
584
602
|
}
|
|
585
603
|
}
|
|
586
|
-
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
587
|
-
if (count === 1)
|
|
588
|
-
this.audioBufferCounter.delete(audioBufferId);
|
|
589
|
-
}
|
|
590
604
|
const priority = {
|
|
591
605
|
controller: 0,
|
|
592
606
|
sysEx: 1,
|
|
@@ -624,12 +638,11 @@ class MidyGMLite {
|
|
|
624
638
|
stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
|
|
625
639
|
const channel = this.channels[channelNumber];
|
|
626
640
|
const promises = [];
|
|
627
|
-
this.processScheduledNotes(channel,
|
|
641
|
+
this.processScheduledNotes(channel, (note) => {
|
|
628
642
|
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
629
643
|
this.notePromises.push(promise);
|
|
630
644
|
promises.push(promise);
|
|
631
645
|
});
|
|
632
|
-
channel.scheduledNotes = [];
|
|
633
646
|
return Promise.all(promises);
|
|
634
647
|
}
|
|
635
648
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -643,6 +656,8 @@ class MidyGMLite {
|
|
|
643
656
|
if (this.isPlaying || this.isPaused)
|
|
644
657
|
return;
|
|
645
658
|
this.resumeTime = 0;
|
|
659
|
+
if (this.voiceCounter.size === 0)
|
|
660
|
+
this.cacheVoiceIds();
|
|
646
661
|
await this.playNotes();
|
|
647
662
|
this.isPlaying = false;
|
|
648
663
|
}
|
|
@@ -683,22 +698,20 @@ class MidyGMLite {
|
|
|
683
698
|
const now = this.audioContext.currentTime;
|
|
684
699
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
685
700
|
}
|
|
686
|
-
processScheduledNotes(channel,
|
|
701
|
+
processScheduledNotes(channel, callback) {
|
|
687
702
|
const scheduledNotes = channel.scheduledNotes;
|
|
688
|
-
for (let i =
|
|
703
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
689
704
|
const note = scheduledNotes[i];
|
|
690
705
|
if (!note)
|
|
691
706
|
continue;
|
|
692
707
|
if (note.ending)
|
|
693
708
|
continue;
|
|
694
|
-
if (note.startTime < scheduleTime)
|
|
695
|
-
continue;
|
|
696
709
|
callback(note);
|
|
697
710
|
}
|
|
698
711
|
}
|
|
699
712
|
processActiveNotes(channel, scheduleTime, callback) {
|
|
700
713
|
const scheduledNotes = channel.scheduledNotes;
|
|
701
|
-
for (let i =
|
|
714
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
702
715
|
const note = scheduledNotes[i];
|
|
703
716
|
if (!note)
|
|
704
717
|
continue;
|
|
@@ -727,7 +740,7 @@ class MidyGMLite {
|
|
|
727
740
|
return pitchWheel * pitchWheelSensitivity;
|
|
728
741
|
}
|
|
729
742
|
updateChannelDetune(channel, scheduleTime) {
|
|
730
|
-
this.processScheduledNotes(channel,
|
|
743
|
+
this.processScheduledNotes(channel, (note) => {
|
|
731
744
|
this.updateDetune(channel, note, scheduleTime);
|
|
732
745
|
});
|
|
733
746
|
}
|
|
@@ -820,32 +833,32 @@ class MidyGMLite {
|
|
|
820
833
|
note.modulationLFO.connect(note.volumeDepth);
|
|
821
834
|
note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
|
|
822
835
|
}
|
|
823
|
-
async getAudioBuffer(
|
|
824
|
-
const audioBufferId = this.
|
|
825
|
-
const cache = this.
|
|
836
|
+
async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
|
|
837
|
+
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
838
|
+
const cache = this.voiceCache.get(audioBufferId);
|
|
826
839
|
if (cache) {
|
|
827
840
|
cache.counter += 1;
|
|
828
841
|
if (cache.maxCount <= cache.counter) {
|
|
829
|
-
this.
|
|
842
|
+
this.voiceCache.delete(audioBufferId);
|
|
830
843
|
}
|
|
831
844
|
return cache.audioBuffer;
|
|
832
845
|
}
|
|
833
846
|
else {
|
|
834
|
-
const maxCount = this.
|
|
835
|
-
const audioBuffer = await this.
|
|
847
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
848
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
836
849
|
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
837
|
-
this.
|
|
850
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
838
851
|
return audioBuffer;
|
|
839
852
|
}
|
|
840
853
|
}
|
|
841
|
-
async createNote(channel, voice, noteNumber, velocity, startTime
|
|
854
|
+
async createNote(channel, voice, noteNumber, velocity, startTime) {
|
|
842
855
|
const now = this.audioContext.currentTime;
|
|
843
856
|
const state = channel.state;
|
|
844
857
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
845
858
|
const voiceParams = voice.getAllParams(controllerState);
|
|
846
859
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
847
|
-
const audioBuffer = await this.getAudioBuffer(channel
|
|
848
|
-
note.bufferSource = this.createBufferSource(
|
|
860
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
861
|
+
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
849
862
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
850
863
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
851
864
|
type: "lowpass",
|
|
@@ -895,15 +908,15 @@ class MidyGMLite {
|
|
|
895
908
|
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
896
909
|
const channel = this.channels[channelNumber];
|
|
897
910
|
const bankNumber = channel.bank;
|
|
898
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
911
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
912
|
+
.get(bankNumber);
|
|
899
913
|
if (soundFontIndex === undefined)
|
|
900
914
|
return;
|
|
901
915
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
902
916
|
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
903
917
|
if (!voice)
|
|
904
918
|
return;
|
|
905
|
-
const
|
|
906
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
919
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
907
920
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
908
921
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
909
922
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -958,15 +971,29 @@ class MidyGMLite {
|
|
|
958
971
|
if (0.5 <= channel.state.sustainPedal)
|
|
959
972
|
return;
|
|
960
973
|
}
|
|
961
|
-
const
|
|
962
|
-
if (
|
|
974
|
+
const index = this.findNoteOffIndex(channel, noteNumber);
|
|
975
|
+
if (index < 0)
|
|
963
976
|
return;
|
|
977
|
+
const note = channel.scheduledNotes[index];
|
|
964
978
|
note.ending = true;
|
|
979
|
+
this.setNoteIndex(channel, index);
|
|
965
980
|
this.releaseNote(channel, note, endTime);
|
|
966
981
|
}
|
|
967
|
-
|
|
982
|
+
setNoteIndex(channel, index) {
|
|
983
|
+
let allEnds = true;
|
|
984
|
+
for (let i = channel.scheduleIndex; i < index; i++) {
|
|
985
|
+
const note = channel.scheduledNotes[i];
|
|
986
|
+
if (note && !note.ending) {
|
|
987
|
+
allEnds = false;
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (allEnds)
|
|
992
|
+
channel.scheduleIndex = index + 1;
|
|
993
|
+
}
|
|
994
|
+
findNoteOffIndex(channel, noteNumber) {
|
|
968
995
|
const scheduledNotes = channel.scheduledNotes;
|
|
969
|
-
for (let i =
|
|
996
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
970
997
|
const note = scheduledNotes[i];
|
|
971
998
|
if (!note)
|
|
972
999
|
continue;
|
|
@@ -974,8 +1001,9 @@ class MidyGMLite {
|
|
|
974
1001
|
continue;
|
|
975
1002
|
if (note.noteNumber !== noteNumber)
|
|
976
1003
|
continue;
|
|
977
|
-
return
|
|
1004
|
+
return i;
|
|
978
1005
|
}
|
|
1006
|
+
return -1;
|
|
979
1007
|
}
|
|
980
1008
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
981
1009
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -1001,16 +1029,16 @@ class MidyGMLite {
|
|
|
1001
1029
|
case 0x90:
|
|
1002
1030
|
return this.noteOn(channelNumber, data1, data2, scheduleTime);
|
|
1003
1031
|
case 0xB0:
|
|
1004
|
-
return this.
|
|
1032
|
+
return this.setControlChange(channelNumber, data1, data2, scheduleTime);
|
|
1005
1033
|
case 0xC0:
|
|
1006
|
-
return this.
|
|
1034
|
+
return this.setProgramChange(channelNumber, data1, scheduleTime);
|
|
1007
1035
|
case 0xE0:
|
|
1008
1036
|
return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
|
|
1009
1037
|
default:
|
|
1010
1038
|
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
1011
1039
|
}
|
|
1012
1040
|
}
|
|
1013
|
-
|
|
1041
|
+
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
1014
1042
|
const channel = this.channels[channelNumber];
|
|
1015
1043
|
channel.programNumber = programNumber;
|
|
1016
1044
|
}
|
|
@@ -1030,13 +1058,17 @@ class MidyGMLite {
|
|
|
1030
1058
|
this.applyVoiceParams(channel, 14, scheduleTime);
|
|
1031
1059
|
}
|
|
1032
1060
|
setModLfoToPitch(channel, note, scheduleTime) {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
channel.state.modulationDepth;
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1061
|
+
if (note.modulationDepth) {
|
|
1062
|
+
const modLfoToPitch = note.voiceParams.modLfoToPitch;
|
|
1063
|
+
const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
|
|
1064
|
+
const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
|
|
1065
|
+
note.modulationDepth.gain
|
|
1066
|
+
.cancelScheduledValues(scheduleTime)
|
|
1067
|
+
.setValueAtTime(modulationDepth, scheduleTime);
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
this.startModulation(channel, note, scheduleTime);
|
|
1071
|
+
}
|
|
1040
1072
|
}
|
|
1041
1073
|
setModLfoToFilterFc(note, scheduleTime) {
|
|
1042
1074
|
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
|
|
@@ -1100,7 +1132,7 @@ class MidyGMLite {
|
|
|
1100
1132
|
return state;
|
|
1101
1133
|
}
|
|
1102
1134
|
applyVoiceParams(channel, controllerType, scheduleTime) {
|
|
1103
|
-
this.processScheduledNotes(channel,
|
|
1135
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1104
1136
|
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
1105
1137
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
1106
1138
|
let applyVolumeEnvelope = false;
|
|
@@ -1147,7 +1179,7 @@ class MidyGMLite {
|
|
|
1147
1179
|
handlers[123] = this.allNotesOff;
|
|
1148
1180
|
return handlers;
|
|
1149
1181
|
}
|
|
1150
|
-
|
|
1182
|
+
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1151
1183
|
const handler = this.controlChangeHandlers[controllerType];
|
|
1152
1184
|
if (handler) {
|
|
1153
1185
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
@@ -1160,12 +1192,11 @@ class MidyGMLite {
|
|
|
1160
1192
|
}
|
|
1161
1193
|
updateModulation(channel, scheduleTime) {
|
|
1162
1194
|
const depth = channel.state.modulationDepth * channel.modulationDepthRange;
|
|
1163
|
-
this.processScheduledNotes(channel,
|
|
1195
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1164
1196
|
if (note.modulationDepth) {
|
|
1165
1197
|
note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
|
|
1166
1198
|
}
|
|
1167
1199
|
else {
|
|
1168
|
-
this.setPitchEnvelope(note, scheduleTime);
|
|
1169
1200
|
this.startModulation(channel, note, scheduleTime);
|
|
1170
1201
|
}
|
|
1171
1202
|
});
|
|
@@ -1221,7 +1252,7 @@ class MidyGMLite {
|
|
|
1221
1252
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1222
1253
|
channel.state.sustainPedal = value / 127;
|
|
1223
1254
|
if (64 <= value) {
|
|
1224
|
-
this.processScheduledNotes(channel,
|
|
1255
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1225
1256
|
channel.sustainNotes.push(note);
|
|
1226
1257
|
});
|
|
1227
1258
|
}
|
|
@@ -1296,7 +1327,7 @@ class MidyGMLite {
|
|
|
1296
1327
|
const entries = Object.entries(defaultControllerState);
|
|
1297
1328
|
for (const [key, { type, defaultValue }] of entries) {
|
|
1298
1329
|
if (128 <= type) {
|
|
1299
|
-
this.
|
|
1330
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1300
1331
|
}
|
|
1301
1332
|
else {
|
|
1302
1333
|
state[key] = defaultValue;
|
|
@@ -1321,7 +1352,7 @@ class MidyGMLite {
|
|
|
1321
1352
|
const key = keys[i];
|
|
1322
1353
|
const { type, defaultValue } = defaultControllerState[key];
|
|
1323
1354
|
if (128 <= type) {
|
|
1324
|
-
this.
|
|
1355
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1325
1356
|
}
|
|
1326
1357
|
else {
|
|
1327
1358
|
state[key] = defaultValue;
|
|
@@ -1435,6 +1466,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
|
|
|
1435
1466
|
configurable: true,
|
|
1436
1467
|
writable: true,
|
|
1437
1468
|
value: {
|
|
1469
|
+
scheduleIndex: 0,
|
|
1438
1470
|
detune: 0,
|
|
1439
1471
|
programNumber: 0,
|
|
1440
1472
|
bank: 0,
|