@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/esm/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;
|
package/esm/midy-GMLite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"midy-GMLite.d.ts","sourceRoot":"","sources":["../src/midy-GMLite.js"],"names":[],"mappings":"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/esm/midy-GMLite.js
CHANGED
|
@@ -83,13 +83,11 @@ const defaultControllerState = {
|
|
|
83
83
|
pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
|
|
84
84
|
pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
|
|
85
85
|
link: { type: 127, defaultValue: 0 },
|
|
86
|
-
// bankMSB: { type: 128 + 0, defaultValue: 121, },
|
|
87
86
|
modulationDepth: { type: 128 + 1, defaultValue: 0 },
|
|
88
87
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
89
88
|
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
90
89
|
pan: { type: 128 + 10, defaultValue: 64 / 127 },
|
|
91
90
|
expression: { type: 128 + 11, defaultValue: 1 },
|
|
92
|
-
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
93
91
|
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
94
92
|
sustainPedal: { type: 128 + 64, defaultValue: 0 },
|
|
95
93
|
// rpnLSB: { type: 128 + 100, defaultValue: 127 },
|
|
@@ -118,6 +116,16 @@ class ControllerState {
|
|
|
118
116
|
}
|
|
119
117
|
}
|
|
120
118
|
}
|
|
119
|
+
const volumeEnvelopeKeys = [
|
|
120
|
+
"volDelay",
|
|
121
|
+
"volAttack",
|
|
122
|
+
"volHold",
|
|
123
|
+
"volDecay",
|
|
124
|
+
"volSustain",
|
|
125
|
+
"volRelease",
|
|
126
|
+
"initialAttenuation",
|
|
127
|
+
];
|
|
128
|
+
const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
|
|
121
129
|
const filterEnvelopeKeys = [
|
|
122
130
|
"modEnvToPitch",
|
|
123
131
|
"initialFilterFc",
|
|
@@ -127,20 +135,18 @@ const filterEnvelopeKeys = [
|
|
|
127
135
|
"modHold",
|
|
128
136
|
"modDecay",
|
|
129
137
|
"modSustain",
|
|
130
|
-
"modRelease",
|
|
131
|
-
"playbackRate",
|
|
132
138
|
];
|
|
133
139
|
const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
|
|
134
|
-
const
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
140
|
+
const pitchEnvelopeKeys = [
|
|
141
|
+
"modEnvToPitch",
|
|
142
|
+
"modDelay",
|
|
143
|
+
"modAttack",
|
|
144
|
+
"modHold",
|
|
145
|
+
"modDecay",
|
|
146
|
+
"modSustain",
|
|
147
|
+
"playbackRate",
|
|
142
148
|
];
|
|
143
|
-
const
|
|
149
|
+
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
144
150
|
export class MidyGMLite {
|
|
145
151
|
constructor(audioContext) {
|
|
146
152
|
Object.defineProperty(this, "mode", {
|
|
@@ -209,13 +215,13 @@ export class MidyGMLite {
|
|
|
209
215
|
writable: true,
|
|
210
216
|
value: this.initSoundFontTable()
|
|
211
217
|
});
|
|
212
|
-
Object.defineProperty(this, "
|
|
218
|
+
Object.defineProperty(this, "voiceCounter", {
|
|
213
219
|
enumerable: true,
|
|
214
220
|
configurable: true,
|
|
215
221
|
writable: true,
|
|
216
222
|
value: new Map()
|
|
217
223
|
});
|
|
218
|
-
Object.defineProperty(this, "
|
|
224
|
+
Object.defineProperty(this, "voiceCache", {
|
|
219
225
|
enumerable: true,
|
|
220
226
|
configurable: true,
|
|
221
227
|
writable: true,
|
|
@@ -308,13 +314,11 @@ export class MidyGMLite {
|
|
|
308
314
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
309
315
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
310
316
|
const presetHeader = presetHeaders[i];
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
banks.set(presetHeader.bank, index);
|
|
314
|
-
}
|
|
317
|
+
const banks = this.soundFontTable[presetHeader.preset];
|
|
318
|
+
banks.set(presetHeader.bank, index);
|
|
315
319
|
}
|
|
316
320
|
}
|
|
317
|
-
async
|
|
321
|
+
async toUint8Array(input) {
|
|
318
322
|
let uint8Array;
|
|
319
323
|
if (typeof input === "string") {
|
|
320
324
|
const response = await fetch(input);
|
|
@@ -327,23 +331,32 @@ export class MidyGMLite {
|
|
|
327
331
|
else {
|
|
328
332
|
throw new TypeError("input must be a URL string or Uint8Array");
|
|
329
333
|
}
|
|
330
|
-
|
|
331
|
-
const soundFont = new SoundFont(parsed);
|
|
332
|
-
this.addSoundFont(soundFont);
|
|
334
|
+
return uint8Array;
|
|
333
335
|
}
|
|
334
|
-
async
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
336
|
+
async loadSoundFont(input) {
|
|
337
|
+
this.voiceCounter.clear();
|
|
338
|
+
if (Array.isArray(input)) {
|
|
339
|
+
const promises = new Array(input.length);
|
|
340
|
+
for (let i = 0; i < input.length; i++) {
|
|
341
|
+
promises[i] = this.toUint8Array(input[i]);
|
|
342
|
+
}
|
|
343
|
+
const uint8Arrays = await Promise.all(promises);
|
|
344
|
+
for (let i = 0; i < uint8Arrays.length; i++) {
|
|
345
|
+
const parsed = parse(uint8Arrays[i]);
|
|
346
|
+
const soundFont = new SoundFont(parsed);
|
|
347
|
+
this.addSoundFont(soundFont);
|
|
348
|
+
}
|
|
343
349
|
}
|
|
344
350
|
else {
|
|
345
|
-
|
|
351
|
+
const uint8Array = await this.toUint8Array(input);
|
|
352
|
+
const parsed = parse(uint8Array);
|
|
353
|
+
const soundFont = new SoundFont(parsed);
|
|
354
|
+
this.addSoundFont(soundFont);
|
|
346
355
|
}
|
|
356
|
+
}
|
|
357
|
+
async loadMIDI(input) {
|
|
358
|
+
this.voiceCounter.clear();
|
|
359
|
+
const uint8Array = await this.toUint8Array(input);
|
|
347
360
|
const midi = parseMidi(uint8Array);
|
|
348
361
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
349
362
|
const midiData = this.extractMidiData(midi);
|
|
@@ -351,7 +364,46 @@ export class MidyGMLite {
|
|
|
351
364
|
this.timeline = midiData.timeline;
|
|
352
365
|
this.totalTime = this.calcTotalTime();
|
|
353
366
|
}
|
|
354
|
-
|
|
367
|
+
cacheVoiceIds() {
|
|
368
|
+
const timeline = this.timeline;
|
|
369
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
370
|
+
const event = timeline[i];
|
|
371
|
+
switch (event.type) {
|
|
372
|
+
case "noteOn": {
|
|
373
|
+
const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
|
|
374
|
+
this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
case "controller":
|
|
378
|
+
if (event.controllerType === 0) {
|
|
379
|
+
this.setBankMSB(event.channel, event.value);
|
|
380
|
+
}
|
|
381
|
+
else if (event.controllerType === 32) {
|
|
382
|
+
this.setBankLSB(event.channel, event.value);
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
case "programChange":
|
|
386
|
+
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
for (const [audioBufferId, count] of this.voiceCounter) {
|
|
390
|
+
if (count === 1)
|
|
391
|
+
this.voiceCounter.delete(audioBufferId);
|
|
392
|
+
}
|
|
393
|
+
this.GM1SystemOn();
|
|
394
|
+
}
|
|
395
|
+
getVoiceId(channel, noteNumber, velocity) {
|
|
396
|
+
const bankNumber = this.calcBank(channel);
|
|
397
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
398
|
+
.get(bankNumber);
|
|
399
|
+
if (soundFontIndex === undefined)
|
|
400
|
+
return;
|
|
401
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
402
|
+
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
403
|
+
const { instrument, sampleID } = voice.generators;
|
|
404
|
+
return `${soundFontIndex}:${instrument}:${sampleID}`;
|
|
405
|
+
}
|
|
406
|
+
createChannelAudioNodes(audioContext) {
|
|
355
407
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
356
408
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
357
409
|
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
@@ -372,41 +424,19 @@ export class MidyGMLite {
|
|
|
372
424
|
isDrum: false,
|
|
373
425
|
state: new ControllerState(),
|
|
374
426
|
...this.constructor.channelSettings,
|
|
375
|
-
...this.
|
|
427
|
+
...this.createChannelAudioNodes(audioContext),
|
|
376
428
|
scheduledNotes: [],
|
|
377
429
|
sustainNotes: [],
|
|
378
430
|
};
|
|
379
431
|
});
|
|
380
432
|
return channels;
|
|
381
433
|
}
|
|
382
|
-
async
|
|
434
|
+
async createAudioBuffer(voiceParams) {
|
|
435
|
+
const sample = voiceParams.sample;
|
|
383
436
|
const sampleStart = voiceParams.start;
|
|
384
|
-
const sampleEnd =
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const start = sample.byteOffset + sampleStart;
|
|
388
|
-
const end = sample.byteOffset + sampleEnd;
|
|
389
|
-
const buffer = sample.buffer.slice(start, end);
|
|
390
|
-
const audioBuffer = await this.audioContext.decodeAudioData(buffer);
|
|
391
|
-
return audioBuffer;
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
const sample = voiceParams.sample;
|
|
395
|
-
const start = sample.byteOffset + sampleStart;
|
|
396
|
-
const end = sample.byteOffset + sampleEnd;
|
|
397
|
-
const buffer = sample.buffer.slice(start, end);
|
|
398
|
-
const audioBuffer = new AudioBuffer({
|
|
399
|
-
numberOfChannels: 1,
|
|
400
|
-
length: sample.length,
|
|
401
|
-
sampleRate: voiceParams.sampleRate,
|
|
402
|
-
});
|
|
403
|
-
const channelData = audioBuffer.getChannelData(0);
|
|
404
|
-
const int16Array = new Int16Array(buffer);
|
|
405
|
-
for (let i = 0; i < int16Array.length; i++) {
|
|
406
|
-
channelData[i] = int16Array[i] / 32768;
|
|
407
|
-
}
|
|
408
|
-
return audioBuffer;
|
|
409
|
-
}
|
|
437
|
+
const sampleEnd = sample.data.length + voiceParams.end;
|
|
438
|
+
const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
|
|
439
|
+
return audioBuffer;
|
|
410
440
|
}
|
|
411
441
|
createBufferSource(channel, voiceParams, audioBuffer) {
|
|
412
442
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
@@ -438,10 +468,10 @@ export class MidyGMLite {
|
|
|
438
468
|
break;
|
|
439
469
|
}
|
|
440
470
|
case "controller":
|
|
441
|
-
this.
|
|
471
|
+
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
442
472
|
break;
|
|
443
473
|
case "programChange":
|
|
444
|
-
this.
|
|
474
|
+
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
445
475
|
break;
|
|
446
476
|
case "pitchBend":
|
|
447
477
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
@@ -475,7 +505,7 @@ export class MidyGMLite {
|
|
|
475
505
|
this.notePromises = [];
|
|
476
506
|
this.exclusiveClassNotes.fill(undefined);
|
|
477
507
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
478
|
-
this.
|
|
508
|
+
this.voiceCache.clear();
|
|
479
509
|
for (let i = 0; i < this.channels.length; i++) {
|
|
480
510
|
this.resetAllStates(i);
|
|
481
511
|
}
|
|
@@ -498,7 +528,7 @@ export class MidyGMLite {
|
|
|
498
528
|
this.notePromises = [];
|
|
499
529
|
this.exclusiveClassNotes.fill(undefined);
|
|
500
530
|
this.drumExclusiveClassNotes.fill(undefined);
|
|
501
|
-
this.
|
|
531
|
+
this.voiceCache.clear();
|
|
502
532
|
for (let i = 0; i < this.channels.length; i++) {
|
|
503
533
|
this.resetAllStates(i);
|
|
504
534
|
}
|
|
@@ -532,11 +562,7 @@ export class MidyGMLite {
|
|
|
532
562
|
secondToTicks(second, secondsPerBeat) {
|
|
533
563
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
534
564
|
}
|
|
535
|
-
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
536
|
-
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
537
|
-
}
|
|
538
565
|
extractMidiData(midi) {
|
|
539
|
-
this.audioBufferCounter.clear();
|
|
540
566
|
const instruments = new Set();
|
|
541
567
|
const timeline = [];
|
|
542
568
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -556,8 +582,6 @@ export class MidyGMLite {
|
|
|
556
582
|
switch (event.type) {
|
|
557
583
|
case "noteOn": {
|
|
558
584
|
const channel = tmpChannels[event.channel];
|
|
559
|
-
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
560
|
-
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
561
585
|
if (channel.programNumber < 0) {
|
|
562
586
|
instruments.add(`${channel.bank}:0`);
|
|
563
587
|
channel.programNumber = 0;
|
|
@@ -574,10 +598,6 @@ export class MidyGMLite {
|
|
|
574
598
|
timeline.push(event);
|
|
575
599
|
}
|
|
576
600
|
}
|
|
577
|
-
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
578
|
-
if (count === 1)
|
|
579
|
-
this.audioBufferCounter.delete(audioBufferId);
|
|
580
|
-
}
|
|
581
601
|
const priority = {
|
|
582
602
|
controller: 0,
|
|
583
603
|
sysEx: 1,
|
|
@@ -620,7 +640,6 @@ export class MidyGMLite {
|
|
|
620
640
|
this.notePromises.push(promise);
|
|
621
641
|
promises.push(promise);
|
|
622
642
|
});
|
|
623
|
-
channel.scheduledNotes = [];
|
|
624
643
|
return Promise.all(promises);
|
|
625
644
|
}
|
|
626
645
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -634,6 +653,8 @@ export class MidyGMLite {
|
|
|
634
653
|
if (this.isPlaying || this.isPaused)
|
|
635
654
|
return;
|
|
636
655
|
this.resumeTime = 0;
|
|
656
|
+
if (this.voiceCounter.size === 0)
|
|
657
|
+
this.cacheVoiceIds();
|
|
637
658
|
await this.playNotes();
|
|
638
659
|
this.isPlaying = false;
|
|
639
660
|
}
|
|
@@ -676,7 +697,7 @@ export class MidyGMLite {
|
|
|
676
697
|
}
|
|
677
698
|
processScheduledNotes(channel, callback) {
|
|
678
699
|
const scheduledNotes = channel.scheduledNotes;
|
|
679
|
-
for (let i =
|
|
700
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
680
701
|
const note = scheduledNotes[i];
|
|
681
702
|
if (!note)
|
|
682
703
|
continue;
|
|
@@ -687,14 +708,14 @@ export class MidyGMLite {
|
|
|
687
708
|
}
|
|
688
709
|
processActiveNotes(channel, scheduleTime, callback) {
|
|
689
710
|
const scheduledNotes = channel.scheduledNotes;
|
|
690
|
-
for (let i =
|
|
711
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
691
712
|
const note = scheduledNotes[i];
|
|
692
713
|
if (!note)
|
|
693
714
|
continue;
|
|
694
715
|
if (note.ending)
|
|
695
716
|
continue;
|
|
696
717
|
if (scheduleTime < note.startTime)
|
|
697
|
-
|
|
718
|
+
break;
|
|
698
719
|
callback(note);
|
|
699
720
|
}
|
|
700
721
|
}
|
|
@@ -809,32 +830,32 @@ export class MidyGMLite {
|
|
|
809
830
|
note.modulationLFO.connect(note.volumeDepth);
|
|
810
831
|
note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
|
|
811
832
|
}
|
|
812
|
-
async getAudioBuffer(
|
|
813
|
-
const audioBufferId = this.
|
|
814
|
-
const cache = this.
|
|
833
|
+
async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
|
|
834
|
+
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
835
|
+
const cache = this.voiceCache.get(audioBufferId);
|
|
815
836
|
if (cache) {
|
|
816
837
|
cache.counter += 1;
|
|
817
838
|
if (cache.maxCount <= cache.counter) {
|
|
818
|
-
this.
|
|
839
|
+
this.voiceCache.delete(audioBufferId);
|
|
819
840
|
}
|
|
820
841
|
return cache.audioBuffer;
|
|
821
842
|
}
|
|
822
843
|
else {
|
|
823
|
-
const maxCount = this.
|
|
824
|
-
const audioBuffer = await this.
|
|
844
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
845
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
825
846
|
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
826
|
-
this.
|
|
847
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
827
848
|
return audioBuffer;
|
|
828
849
|
}
|
|
829
850
|
}
|
|
830
|
-
async createNote(channel, voice, noteNumber, velocity, startTime
|
|
851
|
+
async createNote(channel, voice, noteNumber, velocity, startTime) {
|
|
831
852
|
const now = this.audioContext.currentTime;
|
|
832
853
|
const state = channel.state;
|
|
833
854
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
834
855
|
const voiceParams = voice.getAllParams(controllerState);
|
|
835
856
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
836
|
-
const audioBuffer = await this.getAudioBuffer(channel
|
|
837
|
-
note.bufferSource = this.createBufferSource(
|
|
857
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
858
|
+
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
838
859
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
839
860
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
840
861
|
type: "lowpass",
|
|
@@ -881,19 +902,18 @@ export class MidyGMLite {
|
|
|
881
902
|
}
|
|
882
903
|
this.drumExclusiveClassNotes[index] = note;
|
|
883
904
|
}
|
|
884
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime
|
|
905
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
885
906
|
const channel = this.channels[channelNumber];
|
|
886
907
|
const bankNumber = channel.bank;
|
|
887
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
908
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
909
|
+
.get(bankNumber);
|
|
888
910
|
if (soundFontIndex === undefined)
|
|
889
911
|
return;
|
|
890
912
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
891
913
|
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
892
914
|
if (!voice)
|
|
893
915
|
return;
|
|
894
|
-
const
|
|
895
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
896
|
-
note.noteOffEvent = noteOffEvent;
|
|
916
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
897
917
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
898
918
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
899
919
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -948,15 +968,29 @@ export class MidyGMLite {
|
|
|
948
968
|
if (0.5 <= channel.state.sustainPedal)
|
|
949
969
|
return;
|
|
950
970
|
}
|
|
951
|
-
const
|
|
952
|
-
if (
|
|
971
|
+
const index = this.findNoteOffIndex(channel, noteNumber);
|
|
972
|
+
if (index < 0)
|
|
953
973
|
return;
|
|
974
|
+
const note = channel.scheduledNotes[index];
|
|
954
975
|
note.ending = true;
|
|
976
|
+
this.setNoteIndex(channel, index);
|
|
955
977
|
this.releaseNote(channel, note, endTime);
|
|
956
978
|
}
|
|
957
|
-
|
|
979
|
+
setNoteIndex(channel, index) {
|
|
980
|
+
let allEnds = true;
|
|
981
|
+
for (let i = channel.scheduleIndex; i < index; i++) {
|
|
982
|
+
const note = channel.scheduledNotes[i];
|
|
983
|
+
if (note && !note.ending) {
|
|
984
|
+
allEnds = false;
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (allEnds)
|
|
989
|
+
channel.scheduleIndex = index + 1;
|
|
990
|
+
}
|
|
991
|
+
findNoteOffIndex(channel, noteNumber) {
|
|
958
992
|
const scheduledNotes = channel.scheduledNotes;
|
|
959
|
-
for (let i =
|
|
993
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
960
994
|
const note = scheduledNotes[i];
|
|
961
995
|
if (!note)
|
|
962
996
|
continue;
|
|
@@ -964,8 +998,9 @@ export class MidyGMLite {
|
|
|
964
998
|
continue;
|
|
965
999
|
if (note.noteNumber !== noteNumber)
|
|
966
1000
|
continue;
|
|
967
|
-
return
|
|
1001
|
+
return i;
|
|
968
1002
|
}
|
|
1003
|
+
return -1;
|
|
969
1004
|
}
|
|
970
1005
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
971
1006
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -991,16 +1026,16 @@ export class MidyGMLite {
|
|
|
991
1026
|
case 0x90:
|
|
992
1027
|
return this.noteOn(channelNumber, data1, data2, scheduleTime);
|
|
993
1028
|
case 0xB0:
|
|
994
|
-
return this.
|
|
1029
|
+
return this.setControlChange(channelNumber, data1, data2, scheduleTime);
|
|
995
1030
|
case 0xC0:
|
|
996
|
-
return this.
|
|
1031
|
+
return this.setProgramChange(channelNumber, data1, scheduleTime);
|
|
997
1032
|
case 0xE0:
|
|
998
1033
|
return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
|
|
999
1034
|
default:
|
|
1000
1035
|
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
1001
1036
|
}
|
|
1002
1037
|
}
|
|
1003
|
-
|
|
1038
|
+
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
1004
1039
|
const channel = this.channels[channelNumber];
|
|
1005
1040
|
channel.programNumber = programNumber;
|
|
1006
1041
|
}
|
|
@@ -1093,8 +1128,9 @@ export class MidyGMLite {
|
|
|
1093
1128
|
this.processScheduledNotes(channel, (note) => {
|
|
1094
1129
|
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
1095
1130
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
1096
|
-
let
|
|
1097
|
-
let
|
|
1131
|
+
let applyVolumeEnvelope = false;
|
|
1132
|
+
let applyFilterEnvelope = false;
|
|
1133
|
+
let applyPitchEnvelope = false;
|
|
1098
1134
|
for (const [key, value] of Object.entries(voiceParams)) {
|
|
1099
1135
|
const prevValue = note.voiceParams[key];
|
|
1100
1136
|
if (value === prevValue)
|
|
@@ -1103,32 +1139,21 @@ export class MidyGMLite {
|
|
|
1103
1139
|
if (key in this.voiceParamsHandlers) {
|
|
1104
1140
|
this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
|
|
1105
1141
|
}
|
|
1106
|
-
else
|
|
1107
|
-
if (
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (key in voiceParams)
|
|
1114
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1115
|
-
}
|
|
1116
|
-
this.setFilterEnvelope(note, scheduleTime);
|
|
1117
|
-
this.setPitchEnvelope(note, scheduleTime);
|
|
1118
|
-
}
|
|
1119
|
-
else if (volumeEnvelopeKeySet.has(key)) {
|
|
1120
|
-
if (appliedVolumeEnvelope)
|
|
1121
|
-
continue;
|
|
1122
|
-
appliedVolumeEnvelope = true;
|
|
1123
|
-
const noteVoiceParams = note.voiceParams;
|
|
1124
|
-
for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
|
|
1125
|
-
const key = volumeEnvelopeKeys[i];
|
|
1126
|
-
if (key in voiceParams)
|
|
1127
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1128
|
-
}
|
|
1129
|
-
this.setVolumeEnvelope(note, scheduleTime);
|
|
1142
|
+
else {
|
|
1143
|
+
if (volumeEnvelopeKeySet.has(key))
|
|
1144
|
+
applyVolumeEnvelope = true;
|
|
1145
|
+
if (filterEnvelopeKeySet.has(key))
|
|
1146
|
+
applyFilterEnvelope = true;
|
|
1147
|
+
if (pitchEnvelopeKeySet.has(key))
|
|
1148
|
+
applyPitchEnvelope = true;
|
|
1130
1149
|
}
|
|
1131
1150
|
}
|
|
1151
|
+
if (applyVolumeEnvelope)
|
|
1152
|
+
this.setVolumeEnvelope(note, scheduleTime);
|
|
1153
|
+
if (applyFilterEnvelope)
|
|
1154
|
+
this.setFilterEnvelope(note, scheduleTime);
|
|
1155
|
+
if (applyPitchEnvelope)
|
|
1156
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1132
1157
|
});
|
|
1133
1158
|
}
|
|
1134
1159
|
createControlChangeHandlers() {
|
|
@@ -1147,7 +1172,7 @@ export class MidyGMLite {
|
|
|
1147
1172
|
handlers[123] = this.allNotesOff;
|
|
1148
1173
|
return handlers;
|
|
1149
1174
|
}
|
|
1150
|
-
|
|
1175
|
+
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1151
1176
|
const handler = this.controlChangeHandlers[controllerType];
|
|
1152
1177
|
if (handler) {
|
|
1153
1178
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
@@ -1296,7 +1321,7 @@ export class MidyGMLite {
|
|
|
1296
1321
|
const entries = Object.entries(defaultControllerState);
|
|
1297
1322
|
for (const [key, { type, defaultValue }] of entries) {
|
|
1298
1323
|
if (128 <= type) {
|
|
1299
|
-
this.
|
|
1324
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1300
1325
|
}
|
|
1301
1326
|
else {
|
|
1302
1327
|
state[key] = defaultValue;
|
|
@@ -1321,7 +1346,7 @@ export class MidyGMLite {
|
|
|
1321
1346
|
const key = keys[i];
|
|
1322
1347
|
const { type, defaultValue } = defaultControllerState[key];
|
|
1323
1348
|
if (128 <= type) {
|
|
1324
|
-
this.
|
|
1349
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1325
1350
|
}
|
|
1326
1351
|
else {
|
|
1327
1352
|
state[key] = defaultValue;
|
|
@@ -1434,6 +1459,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
|
|
|
1434
1459
|
configurable: true,
|
|
1435
1460
|
writable: true,
|
|
1436
1461
|
value: {
|
|
1462
|
+
scheduleIndex: 0,
|
|
1437
1463
|
detune: 0,
|
|
1438
1464
|
programNumber: 0,
|
|
1439
1465
|
bank: 0,
|