@marmooo/midy 0.3.3 → 0.3.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/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 +159 -131
- package/esm/midy-GM2.d.ts +28 -42
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +309 -297
- package/esm/midy-GMLite.d.ts +15 -11
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +158 -132
- package/esm/midy.d.ts +30 -43
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +348 -315
- 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 +159 -131
- package/script/midy-GM2.d.ts +28 -42
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +309 -297
- package/script/midy-GMLite.d.ts +15 -11
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +158 -132
- package/script/midy.d.ts +30 -43
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +348 -315
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,8 +22,8 @@ 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;
|
|
@@ -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>;
|
|
58
|
-
|
|
60
|
+
cacheVoiceIds(): void;
|
|
61
|
+
getVoiceId(channel: any, noteNumber: any, velocity: any): string | undefined;
|
|
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[];
|
|
@@ -97,20 +100,21 @@ 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
|
-
scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any
|
|
107
|
+
scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
|
|
105
108
|
noteOn(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<void>;
|
|
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":"
|
|
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,mBAAiB;IACjB,oBAAkB;IAClB,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,6EAcC;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,mEAQC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EAgCC;IAED,qCAeC;IAED,+FAWC;IAED,wDAUC;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;AAt/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
|
@@ -86,13 +86,11 @@ const defaultControllerState = {
|
|
|
86
86
|
pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
|
|
87
87
|
pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
|
|
88
88
|
link: { type: 127, defaultValue: 0 },
|
|
89
|
-
// bankMSB: { type: 128 + 0, defaultValue: 121, },
|
|
90
89
|
modulationDepth: { type: 128 + 1, defaultValue: 0 },
|
|
91
90
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
92
91
|
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
93
92
|
pan: { type: 128 + 10, defaultValue: 64 / 127 },
|
|
94
93
|
expression: { type: 128 + 11, defaultValue: 1 },
|
|
95
|
-
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
96
94
|
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
97
95
|
sustainPedal: { type: 128 + 64, defaultValue: 0 },
|
|
98
96
|
// rpnLSB: { type: 128 + 100, defaultValue: 127 },
|
|
@@ -121,6 +119,16 @@ class ControllerState {
|
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
121
|
}
|
|
122
|
+
const volumeEnvelopeKeys = [
|
|
123
|
+
"volDelay",
|
|
124
|
+
"volAttack",
|
|
125
|
+
"volHold",
|
|
126
|
+
"volDecay",
|
|
127
|
+
"volSustain",
|
|
128
|
+
"volRelease",
|
|
129
|
+
"initialAttenuation",
|
|
130
|
+
];
|
|
131
|
+
const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
|
|
124
132
|
const filterEnvelopeKeys = [
|
|
125
133
|
"modEnvToPitch",
|
|
126
134
|
"initialFilterFc",
|
|
@@ -130,20 +138,18 @@ const filterEnvelopeKeys = [
|
|
|
130
138
|
"modHold",
|
|
131
139
|
"modDecay",
|
|
132
140
|
"modSustain",
|
|
133
|
-
"modRelease",
|
|
134
|
-
"playbackRate",
|
|
135
141
|
];
|
|
136
142
|
const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
|
|
137
|
-
const
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
143
|
+
const pitchEnvelopeKeys = [
|
|
144
|
+
"modEnvToPitch",
|
|
145
|
+
"modDelay",
|
|
146
|
+
"modAttack",
|
|
147
|
+
"modHold",
|
|
148
|
+
"modDecay",
|
|
149
|
+
"modSustain",
|
|
150
|
+
"playbackRate",
|
|
145
151
|
];
|
|
146
|
-
const
|
|
152
|
+
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
147
153
|
class MidyGMLite {
|
|
148
154
|
constructor(audioContext) {
|
|
149
155
|
Object.defineProperty(this, "mode", {
|
|
@@ -212,13 +218,13 @@ class MidyGMLite {
|
|
|
212
218
|
writable: true,
|
|
213
219
|
value: this.initSoundFontTable()
|
|
214
220
|
});
|
|
215
|
-
Object.defineProperty(this, "
|
|
221
|
+
Object.defineProperty(this, "voiceCounter", {
|
|
216
222
|
enumerable: true,
|
|
217
223
|
configurable: true,
|
|
218
224
|
writable: true,
|
|
219
225
|
value: new Map()
|
|
220
226
|
});
|
|
221
|
-
Object.defineProperty(this, "
|
|
227
|
+
Object.defineProperty(this, "voiceCache", {
|
|
222
228
|
enumerable: true,
|
|
223
229
|
configurable: true,
|
|
224
230
|
writable: true,
|
|
@@ -311,13 +317,11 @@ class MidyGMLite {
|
|
|
311
317
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
312
318
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
313
319
|
const presetHeader = presetHeaders[i];
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
banks.set(presetHeader.bank, index);
|
|
317
|
-
}
|
|
320
|
+
const banks = this.soundFontTable[presetHeader.preset];
|
|
321
|
+
banks.set(presetHeader.bank, index);
|
|
318
322
|
}
|
|
319
323
|
}
|
|
320
|
-
async
|
|
324
|
+
async toUint8Array(input) {
|
|
321
325
|
let uint8Array;
|
|
322
326
|
if (typeof input === "string") {
|
|
323
327
|
const response = await fetch(input);
|
|
@@ -330,23 +334,32 @@ class MidyGMLite {
|
|
|
330
334
|
else {
|
|
331
335
|
throw new TypeError("input must be a URL string or Uint8Array");
|
|
332
336
|
}
|
|
333
|
-
|
|
334
|
-
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
335
|
-
this.addSoundFont(soundFont);
|
|
337
|
+
return uint8Array;
|
|
336
338
|
}
|
|
337
|
-
async
|
|
338
|
-
|
|
339
|
-
if (
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
+
}
|
|
346
352
|
}
|
|
347
353
|
else {
|
|
348
|
-
|
|
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);
|
|
349
358
|
}
|
|
359
|
+
}
|
|
360
|
+
async loadMIDI(input) {
|
|
361
|
+
this.voiceCounter.clear();
|
|
362
|
+
const uint8Array = await this.toUint8Array(input);
|
|
350
363
|
const midi = (0, midi_file_1.parseMidi)(uint8Array);
|
|
351
364
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
352
365
|
const midiData = this.extractMidiData(midi);
|
|
@@ -354,7 +367,46 @@ class MidyGMLite {
|
|
|
354
367
|
this.timeline = midiData.timeline;
|
|
355
368
|
this.totalTime = this.calcTotalTime();
|
|
356
369
|
}
|
|
357
|
-
|
|
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}:${instrument}:${sampleID}`;
|
|
408
|
+
}
|
|
409
|
+
createChannelAudioNodes(audioContext) {
|
|
358
410
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
359
411
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
360
412
|
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
@@ -375,41 +427,19 @@ class MidyGMLite {
|
|
|
375
427
|
isDrum: false,
|
|
376
428
|
state: new ControllerState(),
|
|
377
429
|
...this.constructor.channelSettings,
|
|
378
|
-
...this.
|
|
430
|
+
...this.createChannelAudioNodes(audioContext),
|
|
379
431
|
scheduledNotes: [],
|
|
380
432
|
sustainNotes: [],
|
|
381
433
|
};
|
|
382
434
|
});
|
|
383
435
|
return channels;
|
|
384
436
|
}
|
|
385
|
-
async
|
|
437
|
+
async createAudioBuffer(voiceParams) {
|
|
438
|
+
const sample = voiceParams.sample;
|
|
386
439
|
const sampleStart = voiceParams.start;
|
|
387
|
-
const sampleEnd =
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const start = sample.byteOffset + sampleStart;
|
|
391
|
-
const end = sample.byteOffset + sampleEnd;
|
|
392
|
-
const buffer = sample.buffer.slice(start, end);
|
|
393
|
-
const audioBuffer = await this.audioContext.decodeAudioData(buffer);
|
|
394
|
-
return audioBuffer;
|
|
395
|
-
}
|
|
396
|
-
else {
|
|
397
|
-
const sample = voiceParams.sample;
|
|
398
|
-
const start = sample.byteOffset + sampleStart;
|
|
399
|
-
const end = sample.byteOffset + sampleEnd;
|
|
400
|
-
const buffer = sample.buffer.slice(start, end);
|
|
401
|
-
const audioBuffer = new AudioBuffer({
|
|
402
|
-
numberOfChannels: 1,
|
|
403
|
-
length: sample.length,
|
|
404
|
-
sampleRate: voiceParams.sampleRate,
|
|
405
|
-
});
|
|
406
|
-
const channelData = audioBuffer.getChannelData(0);
|
|
407
|
-
const int16Array = new Int16Array(buffer);
|
|
408
|
-
for (let i = 0; i < int16Array.length; i++) {
|
|
409
|
-
channelData[i] = int16Array[i] / 32768;
|
|
410
|
-
}
|
|
411
|
-
return audioBuffer;
|
|
412
|
-
}
|
|
440
|
+
const sampleEnd = sample.data.length + voiceParams.end;
|
|
441
|
+
const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
|
|
442
|
+
return audioBuffer;
|
|
413
443
|
}
|
|
414
444
|
createBufferSource(channel, voiceParams, audioBuffer) {
|
|
415
445
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
@@ -441,10 +471,10 @@ class MidyGMLite {
|
|
|
441
471
|
break;
|
|
442
472
|
}
|
|
443
473
|
case "controller":
|
|
444
|
-
this.
|
|
474
|
+
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
445
475
|
break;
|
|
446
476
|
case "programChange":
|
|
447
|
-
this.
|
|
477
|
+
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
448
478
|
break;
|
|
449
479
|
case "pitchBend":
|
|
450
480
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
@@ -478,7 +508,7 @@ class MidyGMLite {
|
|
|
478
508
|
this.notePromises = [];
|
|
479
509
|
this.exclusiveClassNotes.fill(undefined);
|
|
480
510
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
481
|
-
this.
|
|
511
|
+
this.voiceCache.clear();
|
|
482
512
|
for (let i = 0; i < this.channels.length; i++) {
|
|
483
513
|
this.resetAllStates(i);
|
|
484
514
|
}
|
|
@@ -501,7 +531,7 @@ class MidyGMLite {
|
|
|
501
531
|
this.notePromises = [];
|
|
502
532
|
this.exclusiveClassNotes.fill(undefined);
|
|
503
533
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
504
|
-
this.
|
|
534
|
+
this.voiceCache.clear();
|
|
505
535
|
for (let i = 0; i < this.channels.length; i++) {
|
|
506
536
|
this.resetAllStates(i);
|
|
507
537
|
}
|
|
@@ -535,11 +565,7 @@ class MidyGMLite {
|
|
|
535
565
|
secondToTicks(second, secondsPerBeat) {
|
|
536
566
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
537
567
|
}
|
|
538
|
-
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
539
|
-
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
540
|
-
}
|
|
541
568
|
extractMidiData(midi) {
|
|
542
|
-
this.audioBufferCounter.clear();
|
|
543
569
|
const instruments = new Set();
|
|
544
570
|
const timeline = [];
|
|
545
571
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -559,8 +585,6 @@ class MidyGMLite {
|
|
|
559
585
|
switch (event.type) {
|
|
560
586
|
case "noteOn": {
|
|
561
587
|
const channel = tmpChannels[event.channel];
|
|
562
|
-
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
563
|
-
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
564
588
|
if (channel.programNumber < 0) {
|
|
565
589
|
instruments.add(`${channel.bank}:0`);
|
|
566
590
|
channel.programNumber = 0;
|
|
@@ -577,10 +601,6 @@ class MidyGMLite {
|
|
|
577
601
|
timeline.push(event);
|
|
578
602
|
}
|
|
579
603
|
}
|
|
580
|
-
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
581
|
-
if (count === 1)
|
|
582
|
-
this.audioBufferCounter.delete(audioBufferId);
|
|
583
|
-
}
|
|
584
604
|
const priority = {
|
|
585
605
|
controller: 0,
|
|
586
606
|
sysEx: 1,
|
|
@@ -623,7 +643,6 @@ class MidyGMLite {
|
|
|
623
643
|
this.notePromises.push(promise);
|
|
624
644
|
promises.push(promise);
|
|
625
645
|
});
|
|
626
|
-
channel.scheduledNotes = [];
|
|
627
646
|
return Promise.all(promises);
|
|
628
647
|
}
|
|
629
648
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -637,6 +656,8 @@ class MidyGMLite {
|
|
|
637
656
|
if (this.isPlaying || this.isPaused)
|
|
638
657
|
return;
|
|
639
658
|
this.resumeTime = 0;
|
|
659
|
+
if (this.voiceCounter.size === 0)
|
|
660
|
+
this.cacheVoiceIds();
|
|
640
661
|
await this.playNotes();
|
|
641
662
|
this.isPlaying = false;
|
|
642
663
|
}
|
|
@@ -679,7 +700,7 @@ class MidyGMLite {
|
|
|
679
700
|
}
|
|
680
701
|
processScheduledNotes(channel, callback) {
|
|
681
702
|
const scheduledNotes = channel.scheduledNotes;
|
|
682
|
-
for (let i =
|
|
703
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
683
704
|
const note = scheduledNotes[i];
|
|
684
705
|
if (!note)
|
|
685
706
|
continue;
|
|
@@ -690,14 +711,14 @@ class MidyGMLite {
|
|
|
690
711
|
}
|
|
691
712
|
processActiveNotes(channel, scheduleTime, callback) {
|
|
692
713
|
const scheduledNotes = channel.scheduledNotes;
|
|
693
|
-
for (let i =
|
|
714
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
694
715
|
const note = scheduledNotes[i];
|
|
695
716
|
if (!note)
|
|
696
717
|
continue;
|
|
697
718
|
if (note.ending)
|
|
698
719
|
continue;
|
|
699
720
|
if (scheduleTime < note.startTime)
|
|
700
|
-
|
|
721
|
+
break;
|
|
701
722
|
callback(note);
|
|
702
723
|
}
|
|
703
724
|
}
|
|
@@ -812,32 +833,32 @@ class MidyGMLite {
|
|
|
812
833
|
note.modulationLFO.connect(note.volumeDepth);
|
|
813
834
|
note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
|
|
814
835
|
}
|
|
815
|
-
async getAudioBuffer(
|
|
816
|
-
const audioBufferId = this.
|
|
817
|
-
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);
|
|
818
839
|
if (cache) {
|
|
819
840
|
cache.counter += 1;
|
|
820
841
|
if (cache.maxCount <= cache.counter) {
|
|
821
|
-
this.
|
|
842
|
+
this.voiceCache.delete(audioBufferId);
|
|
822
843
|
}
|
|
823
844
|
return cache.audioBuffer;
|
|
824
845
|
}
|
|
825
846
|
else {
|
|
826
|
-
const maxCount = this.
|
|
827
|
-
const audioBuffer = await this.
|
|
847
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
848
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
828
849
|
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
829
|
-
this.
|
|
850
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
830
851
|
return audioBuffer;
|
|
831
852
|
}
|
|
832
853
|
}
|
|
833
|
-
async createNote(channel, voice, noteNumber, velocity, startTime
|
|
854
|
+
async createNote(channel, voice, noteNumber, velocity, startTime) {
|
|
834
855
|
const now = this.audioContext.currentTime;
|
|
835
856
|
const state = channel.state;
|
|
836
857
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
837
858
|
const voiceParams = voice.getAllParams(controllerState);
|
|
838
859
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
839
|
-
const audioBuffer = await this.getAudioBuffer(channel
|
|
840
|
-
note.bufferSource = this.createBufferSource(
|
|
860
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
861
|
+
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
841
862
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
842
863
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
843
864
|
type: "lowpass",
|
|
@@ -884,19 +905,18 @@ class MidyGMLite {
|
|
|
884
905
|
}
|
|
885
906
|
this.drumExclusiveClassNotes[index] = note;
|
|
886
907
|
}
|
|
887
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime
|
|
908
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
888
909
|
const channel = this.channels[channelNumber];
|
|
889
910
|
const bankNumber = channel.bank;
|
|
890
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
911
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
912
|
+
.get(bankNumber);
|
|
891
913
|
if (soundFontIndex === undefined)
|
|
892
914
|
return;
|
|
893
915
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
894
916
|
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
895
917
|
if (!voice)
|
|
896
918
|
return;
|
|
897
|
-
const
|
|
898
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
899
|
-
note.noteOffEvent = noteOffEvent;
|
|
919
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
900
920
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
901
921
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
902
922
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -951,15 +971,29 @@ class MidyGMLite {
|
|
|
951
971
|
if (0.5 <= channel.state.sustainPedal)
|
|
952
972
|
return;
|
|
953
973
|
}
|
|
954
|
-
const
|
|
955
|
-
if (
|
|
974
|
+
const index = this.findNoteOffIndex(channel, noteNumber);
|
|
975
|
+
if (index < 0)
|
|
956
976
|
return;
|
|
977
|
+
const note = channel.scheduledNotes[index];
|
|
957
978
|
note.ending = true;
|
|
979
|
+
this.setNoteIndex(channel, index);
|
|
958
980
|
this.releaseNote(channel, note, endTime);
|
|
959
981
|
}
|
|
960
|
-
|
|
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) {
|
|
961
995
|
const scheduledNotes = channel.scheduledNotes;
|
|
962
|
-
for (let i =
|
|
996
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
963
997
|
const note = scheduledNotes[i];
|
|
964
998
|
if (!note)
|
|
965
999
|
continue;
|
|
@@ -967,8 +1001,9 @@ class MidyGMLite {
|
|
|
967
1001
|
continue;
|
|
968
1002
|
if (note.noteNumber !== noteNumber)
|
|
969
1003
|
continue;
|
|
970
|
-
return
|
|
1004
|
+
return i;
|
|
971
1005
|
}
|
|
1006
|
+
return -1;
|
|
972
1007
|
}
|
|
973
1008
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
974
1009
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -994,16 +1029,16 @@ class MidyGMLite {
|
|
|
994
1029
|
case 0x90:
|
|
995
1030
|
return this.noteOn(channelNumber, data1, data2, scheduleTime);
|
|
996
1031
|
case 0xB0:
|
|
997
|
-
return this.
|
|
1032
|
+
return this.setControlChange(channelNumber, data1, data2, scheduleTime);
|
|
998
1033
|
case 0xC0:
|
|
999
|
-
return this.
|
|
1034
|
+
return this.setProgramChange(channelNumber, data1, scheduleTime);
|
|
1000
1035
|
case 0xE0:
|
|
1001
1036
|
return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
|
|
1002
1037
|
default:
|
|
1003
1038
|
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
1004
1039
|
}
|
|
1005
1040
|
}
|
|
1006
|
-
|
|
1041
|
+
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
1007
1042
|
const channel = this.channels[channelNumber];
|
|
1008
1043
|
channel.programNumber = programNumber;
|
|
1009
1044
|
}
|
|
@@ -1096,8 +1131,9 @@ class MidyGMLite {
|
|
|
1096
1131
|
this.processScheduledNotes(channel, (note) => {
|
|
1097
1132
|
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
1098
1133
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
1099
|
-
let
|
|
1100
|
-
let
|
|
1134
|
+
let applyVolumeEnvelope = false;
|
|
1135
|
+
let applyFilterEnvelope = false;
|
|
1136
|
+
let applyPitchEnvelope = false;
|
|
1101
1137
|
for (const [key, value] of Object.entries(voiceParams)) {
|
|
1102
1138
|
const prevValue = note.voiceParams[key];
|
|
1103
1139
|
if (value === prevValue)
|
|
@@ -1106,32 +1142,21 @@ class MidyGMLite {
|
|
|
1106
1142
|
if (key in this.voiceParamsHandlers) {
|
|
1107
1143
|
this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
|
|
1108
1144
|
}
|
|
1109
|
-
else
|
|
1110
|
-
if (
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (key in voiceParams)
|
|
1117
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1118
|
-
}
|
|
1119
|
-
this.setFilterEnvelope(note, scheduleTime);
|
|
1120
|
-
this.setPitchEnvelope(note, scheduleTime);
|
|
1121
|
-
}
|
|
1122
|
-
else if (volumeEnvelopeKeySet.has(key)) {
|
|
1123
|
-
if (appliedVolumeEnvelope)
|
|
1124
|
-
continue;
|
|
1125
|
-
appliedVolumeEnvelope = true;
|
|
1126
|
-
const noteVoiceParams = note.voiceParams;
|
|
1127
|
-
for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
|
|
1128
|
-
const key = volumeEnvelopeKeys[i];
|
|
1129
|
-
if (key in voiceParams)
|
|
1130
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1131
|
-
}
|
|
1132
|
-
this.setVolumeEnvelope(note, scheduleTime);
|
|
1145
|
+
else {
|
|
1146
|
+
if (volumeEnvelopeKeySet.has(key))
|
|
1147
|
+
applyVolumeEnvelope = true;
|
|
1148
|
+
if (filterEnvelopeKeySet.has(key))
|
|
1149
|
+
applyFilterEnvelope = true;
|
|
1150
|
+
if (pitchEnvelopeKeySet.has(key))
|
|
1151
|
+
applyPitchEnvelope = true;
|
|
1133
1152
|
}
|
|
1134
1153
|
}
|
|
1154
|
+
if (applyVolumeEnvelope)
|
|
1155
|
+
this.setVolumeEnvelope(note, scheduleTime);
|
|
1156
|
+
if (applyFilterEnvelope)
|
|
1157
|
+
this.setFilterEnvelope(note, scheduleTime);
|
|
1158
|
+
if (applyPitchEnvelope)
|
|
1159
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1135
1160
|
});
|
|
1136
1161
|
}
|
|
1137
1162
|
createControlChangeHandlers() {
|
|
@@ -1150,7 +1175,7 @@ class MidyGMLite {
|
|
|
1150
1175
|
handlers[123] = this.allNotesOff;
|
|
1151
1176
|
return handlers;
|
|
1152
1177
|
}
|
|
1153
|
-
|
|
1178
|
+
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1154
1179
|
const handler = this.controlChangeHandlers[controllerType];
|
|
1155
1180
|
if (handler) {
|
|
1156
1181
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
@@ -1299,7 +1324,7 @@ class MidyGMLite {
|
|
|
1299
1324
|
const entries = Object.entries(defaultControllerState);
|
|
1300
1325
|
for (const [key, { type, defaultValue }] of entries) {
|
|
1301
1326
|
if (128 <= type) {
|
|
1302
|
-
this.
|
|
1327
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1303
1328
|
}
|
|
1304
1329
|
else {
|
|
1305
1330
|
state[key] = defaultValue;
|
|
@@ -1324,7 +1349,7 @@ class MidyGMLite {
|
|
|
1324
1349
|
const key = keys[i];
|
|
1325
1350
|
const { type, defaultValue } = defaultControllerState[key];
|
|
1326
1351
|
if (128 <= type) {
|
|
1327
|
-
this.
|
|
1352
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1328
1353
|
}
|
|
1329
1354
|
else {
|
|
1330
1355
|
state[key] = defaultValue;
|
|
@@ -1438,6 +1463,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
|
|
|
1438
1463
|
configurable: true,
|
|
1439
1464
|
writable: true,
|
|
1440
1465
|
value: {
|
|
1466
|
+
scheduleIndex: 0,
|
|
1441
1467
|
detune: 0,
|
|
1442
1468
|
programNumber: 0,
|
|
1443
1469
|
bank: 0,
|