@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marmooo/midy",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "A MIDI player/synthesizer written in JavaScript that supports GM-Lite/GM1 and SF2/SF3.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test": "node test_runner.js"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@marmooo/soundfont-parser": "^0.1.
|
|
25
|
+
"@marmooo/soundfont-parser": "^0.1.2",
|
|
26
26
|
"midi-file": "^1.2.4"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
package/script/midy-GM1.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export class MidyGM1 {
|
|
|
23
23
|
resumeTime: number;
|
|
24
24
|
soundFonts: any[];
|
|
25
25
|
soundFontTable: any[];
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
voiceCounter: Map<any, any>;
|
|
27
|
+
voiceCache: Map<any, any>;
|
|
28
28
|
isPlaying: boolean;
|
|
29
29
|
isPausing: boolean;
|
|
30
30
|
isPaused: boolean;
|
|
@@ -54,22 +54,24 @@ export class MidyGM1 {
|
|
|
54
54
|
channels: any[];
|
|
55
55
|
initSoundFontTable(): any[];
|
|
56
56
|
addSoundFont(soundFont: any): void;
|
|
57
|
+
toUint8Array(input: any): Promise<Uint8Array<ArrayBuffer>>;
|
|
57
58
|
loadSoundFont(input: any): Promise<void>;
|
|
58
59
|
loadMIDI(input: any): Promise<void>;
|
|
59
|
-
|
|
60
|
+
cacheVoiceIds(): void;
|
|
61
|
+
getVoiceId(channel: any, noteNumber: any, velocity: any): string | undefined;
|
|
62
|
+
createChannelAudioNodes(audioContext: any): {
|
|
60
63
|
gainL: any;
|
|
61
64
|
gainR: any;
|
|
62
65
|
merger: any;
|
|
63
66
|
};
|
|
64
67
|
createChannels(audioContext: any): any[];
|
|
65
|
-
|
|
68
|
+
createAudioBuffer(voiceParams: any): Promise<any>;
|
|
66
69
|
createBufferSource(voiceParams: any, audioBuffer: any): any;
|
|
67
70
|
scheduleTimelineEvents(t: any, resumeTime: any, queueIndex: any): Promise<any>;
|
|
68
71
|
getQueueIndex(second: any): number;
|
|
69
72
|
playNotes(): Promise<any>;
|
|
70
73
|
ticksToSecond(ticks: any, secondsPerBeat: any): number;
|
|
71
74
|
secondToTicks(second: any, secondsPerBeat: any): number;
|
|
72
|
-
getAudioBufferId(programNumber: any, noteNumber: any, velocity: any): string;
|
|
73
75
|
extractMidiData(midi: any): {
|
|
74
76
|
instruments: Set<any>;
|
|
75
77
|
timeline: any[];
|
|
@@ -98,19 +100,20 @@ export class MidyGM1 {
|
|
|
98
100
|
clampCutoffFrequency(frequency: any): number;
|
|
99
101
|
setFilterEnvelope(note: any, scheduleTime: any): void;
|
|
100
102
|
startModulation(channel: any, note: any, scheduleTime: any): void;
|
|
101
|
-
getAudioBuffer(
|
|
102
|
-
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>;
|
|
103
105
|
handleExclusiveClass(note: any, channelNumber: any, startTime: any): void;
|
|
104
|
-
scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any
|
|
106
|
+
scheduleNoteOn(channelNumber: any, noteNumber: any, velocity: any, startTime: any): Promise<void>;
|
|
105
107
|
noteOn(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): Promise<void>;
|
|
106
108
|
disconnectNote(note: any): void;
|
|
107
109
|
releaseNote(channel: any, note: any, endTime: any): Promise<any>;
|
|
108
110
|
scheduleNoteOff(channelNumber: any, noteNumber: any, _velocity: any, endTime: any, force: any): void;
|
|
109
|
-
|
|
111
|
+
setNoteIndex(channel: any, index: any): void;
|
|
112
|
+
findNoteOffIndex(channel: any, noteNumber: any): any;
|
|
110
113
|
noteOff(channelNumber: any, noteNumber: any, velocity: any, scheduleTime: any): void;
|
|
111
114
|
releaseSustainPedal(channelNumber: any, halfVelocity: any, scheduleTime: any): void[];
|
|
112
115
|
handleMIDIMessage(statusByte: any, data1: any, data2: any, scheduleTime: any): void | Promise<void>;
|
|
113
|
-
|
|
116
|
+
setProgramChange(channelNumber: any, programNumber: any, _scheduleTime: any): void;
|
|
114
117
|
handlePitchBendMessage(channelNumber: any, lsb: any, msb: any, scheduleTime: any): void;
|
|
115
118
|
setPitchBend(channelNumber: any, value: any, scheduleTime: any): void;
|
|
116
119
|
setModLfoToPitch(channel: any, note: any, scheduleTime: any): void;
|
|
@@ -133,7 +136,7 @@ export class MidyGM1 {
|
|
|
133
136
|
getControllerState(channel: any, noteNumber: any, velocity: any): Float32Array<any>;
|
|
134
137
|
applyVoiceParams(channel: any, controllerType: any, scheduleTime: any): void;
|
|
135
138
|
createControlChangeHandlers(): any[];
|
|
136
|
-
|
|
139
|
+
setControlChange(channelNumber: any, controllerType: any, value: any, scheduleTime: any): void;
|
|
137
140
|
updateModulation(channel: any, scheduleTime: any): void;
|
|
138
141
|
setModulationDepth(channelNumber: any, modulation: any, scheduleTime: any): void;
|
|
139
142
|
setVolume(channelNumber: any, volume: any, scheduleTime: any): void;
|
package/script/midy-GM1.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"midy-GM1.d.ts","sourceRoot":"","sources":["../src/midy-GM1.js"],"names":[],"mappings":"AA4FA;IAwBE;;;;;;;;;;;MAWE;IAEF,+BAcC;IAlDD,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;IAgBnC,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,4DASC;IAED,+EAkDC;IAED,mCAOC;IAED,0BAgEC;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,qCAMC;IAED,2DAIC;IAED,+DAIC;IAED,sDAeC;IAED,qDAoBC;IAED,6CAIC;IAED,sDAsBC;IAED,kEAoBC;IAED,6FAyBC;IAED,oGAuCC;IAED,0EAiBC;IAED,kGAmCC;IAED,6FASC;IAED,gCASC;IAED,iEAoBC;IAED,qGAeC;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,2DAMC;IAED,uDAkBC;IAED,gDAEC;IAED,gDAEC;IAED,sEAGC;IAED,qEAKC;IAED,2EAUC;IAED,iEAKC;IAED,uEAQC;IAED,mEAKC;IAED,yEAQC;IAED,gFAGC;IAED,yCAqBC;IAGD,8EAgCC;IAED,gFAGC;IAED,+EAgBC;IAED,qCAWC;IAED,4EAaC;IAED,4DAGC;IAED,sDASC;IAED,gDAYC;IAGD,6DAgBC;CACF;AA//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-GM1.js
CHANGED
|
@@ -73,13 +73,11 @@ const defaultControllerState = {
|
|
|
73
73
|
pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
|
|
74
74
|
pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
|
|
75
75
|
link: { type: 127, defaultValue: 0 },
|
|
76
|
-
// bankMSB: { type: 128 + 0, defaultValue: 121, },
|
|
77
76
|
modulationDepth: { type: 128 + 1, defaultValue: 0 },
|
|
78
77
|
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
79
78
|
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
80
79
|
pan: { type: 128 + 10, defaultValue: 64 / 127 },
|
|
81
80
|
expression: { type: 128 + 11, defaultValue: 1 },
|
|
82
|
-
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
83
81
|
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
84
82
|
sustainPedal: { type: 128 + 64, defaultValue: 0 },
|
|
85
83
|
// rpnLSB: { type: 128 + 100, defaultValue: 127 },
|
|
@@ -108,6 +106,16 @@ class ControllerState {
|
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
108
|
}
|
|
109
|
+
const volumeEnvelopeKeys = [
|
|
110
|
+
"volDelay",
|
|
111
|
+
"volAttack",
|
|
112
|
+
"volHold",
|
|
113
|
+
"volDecay",
|
|
114
|
+
"volSustain",
|
|
115
|
+
"volRelease",
|
|
116
|
+
"initialAttenuation",
|
|
117
|
+
];
|
|
118
|
+
const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
|
|
111
119
|
const filterEnvelopeKeys = [
|
|
112
120
|
"modEnvToPitch",
|
|
113
121
|
"initialFilterFc",
|
|
@@ -117,20 +125,18 @@ const filterEnvelopeKeys = [
|
|
|
117
125
|
"modHold",
|
|
118
126
|
"modDecay",
|
|
119
127
|
"modSustain",
|
|
120
|
-
"modRelease",
|
|
121
|
-
"playbackRate",
|
|
122
128
|
];
|
|
123
129
|
const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
|
|
124
|
-
const
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
130
|
+
const pitchEnvelopeKeys = [
|
|
131
|
+
"modEnvToPitch",
|
|
132
|
+
"modDelay",
|
|
133
|
+
"modAttack",
|
|
134
|
+
"modHold",
|
|
135
|
+
"modDecay",
|
|
136
|
+
"modSustain",
|
|
137
|
+
"playbackRate",
|
|
132
138
|
];
|
|
133
|
-
const
|
|
139
|
+
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
134
140
|
class MidyGM1 {
|
|
135
141
|
constructor(audioContext) {
|
|
136
142
|
Object.defineProperty(this, "mode", {
|
|
@@ -199,13 +205,13 @@ class MidyGM1 {
|
|
|
199
205
|
writable: true,
|
|
200
206
|
value: this.initSoundFontTable()
|
|
201
207
|
});
|
|
202
|
-
Object.defineProperty(this, "
|
|
208
|
+
Object.defineProperty(this, "voiceCounter", {
|
|
203
209
|
enumerable: true,
|
|
204
210
|
configurable: true,
|
|
205
211
|
writable: true,
|
|
206
212
|
value: new Map()
|
|
207
213
|
});
|
|
208
|
-
Object.defineProperty(this, "
|
|
214
|
+
Object.defineProperty(this, "voiceCache", {
|
|
209
215
|
enumerable: true,
|
|
210
216
|
configurable: true,
|
|
211
217
|
writable: true,
|
|
@@ -292,13 +298,11 @@ class MidyGM1 {
|
|
|
292
298
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
293
299
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
294
300
|
const presetHeader = presetHeaders[i];
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
banks.set(presetHeader.bank, index);
|
|
298
|
-
}
|
|
301
|
+
const banks = this.soundFontTable[presetHeader.preset];
|
|
302
|
+
banks.set(presetHeader.bank, index);
|
|
299
303
|
}
|
|
300
304
|
}
|
|
301
|
-
async
|
|
305
|
+
async toUint8Array(input) {
|
|
302
306
|
let uint8Array;
|
|
303
307
|
if (typeof input === "string") {
|
|
304
308
|
const response = await fetch(input);
|
|
@@ -311,23 +315,32 @@ class MidyGM1 {
|
|
|
311
315
|
else {
|
|
312
316
|
throw new TypeError("input must be a URL string or Uint8Array");
|
|
313
317
|
}
|
|
314
|
-
|
|
315
|
-
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
316
|
-
this.addSoundFont(soundFont);
|
|
318
|
+
return uint8Array;
|
|
317
319
|
}
|
|
318
|
-
async
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
320
|
+
async loadSoundFont(input) {
|
|
321
|
+
this.voiceCounter.clear();
|
|
322
|
+
if (Array.isArray(input)) {
|
|
323
|
+
const promises = new Array(input.length);
|
|
324
|
+
for (let i = 0; i < input.length; i++) {
|
|
325
|
+
promises[i] = this.toUint8Array(input[i]);
|
|
326
|
+
}
|
|
327
|
+
const uint8Arrays = await Promise.all(promises);
|
|
328
|
+
for (let i = 0; i < uint8Arrays.length; i++) {
|
|
329
|
+
const parsed = (0, soundfont_parser_1.parse)(uint8Arrays[i]);
|
|
330
|
+
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
331
|
+
this.addSoundFont(soundFont);
|
|
332
|
+
}
|
|
327
333
|
}
|
|
328
334
|
else {
|
|
329
|
-
|
|
335
|
+
const uint8Array = await this.toUint8Array(input);
|
|
336
|
+
const parsed = (0, soundfont_parser_1.parse)(uint8Array);
|
|
337
|
+
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
338
|
+
this.addSoundFont(soundFont);
|
|
330
339
|
}
|
|
340
|
+
}
|
|
341
|
+
async loadMIDI(input) {
|
|
342
|
+
this.voiceCounter.clear();
|
|
343
|
+
const uint8Array = await this.toUint8Array(input);
|
|
331
344
|
const midi = (0, midi_file_1.parseMidi)(uint8Array);
|
|
332
345
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
333
346
|
const midiData = this.extractMidiData(midi);
|
|
@@ -335,7 +348,46 @@ class MidyGM1 {
|
|
|
335
348
|
this.timeline = midiData.timeline;
|
|
336
349
|
this.totalTime = this.calcTotalTime();
|
|
337
350
|
}
|
|
338
|
-
|
|
351
|
+
cacheVoiceIds() {
|
|
352
|
+
const timeline = this.timeline;
|
|
353
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
354
|
+
const event = timeline[i];
|
|
355
|
+
switch (event.type) {
|
|
356
|
+
case "noteOn": {
|
|
357
|
+
const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
|
|
358
|
+
this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case "controller":
|
|
362
|
+
if (event.controllerType === 0) {
|
|
363
|
+
this.setBankMSB(event.channel, event.value);
|
|
364
|
+
}
|
|
365
|
+
else if (event.controllerType === 32) {
|
|
366
|
+
this.setBankLSB(event.channel, event.value);
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
case "programChange":
|
|
370
|
+
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const [audioBufferId, count] of this.voiceCounter) {
|
|
374
|
+
if (count === 1)
|
|
375
|
+
this.voiceCounter.delete(audioBufferId);
|
|
376
|
+
}
|
|
377
|
+
this.GM1SystemOn();
|
|
378
|
+
}
|
|
379
|
+
getVoiceId(channel, noteNumber, velocity) {
|
|
380
|
+
const bankNumber = this.calcBank(channel);
|
|
381
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
382
|
+
.get(bankNumber);
|
|
383
|
+
if (soundFontIndex === undefined)
|
|
384
|
+
return;
|
|
385
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
386
|
+
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
387
|
+
const { instrument, sampleID } = voice.generators;
|
|
388
|
+
return `${soundFontIndex}:${instrument}:${sampleID}`;
|
|
389
|
+
}
|
|
390
|
+
createChannelAudioNodes(audioContext) {
|
|
339
391
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
340
392
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
341
393
|
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
@@ -356,41 +408,19 @@ class MidyGM1 {
|
|
|
356
408
|
isDrum: false,
|
|
357
409
|
state: new ControllerState(),
|
|
358
410
|
...this.constructor.channelSettings,
|
|
359
|
-
...this.
|
|
411
|
+
...this.createChannelAudioNodes(audioContext),
|
|
360
412
|
scheduledNotes: [],
|
|
361
413
|
sustainNotes: [],
|
|
362
414
|
};
|
|
363
415
|
});
|
|
364
416
|
return channels;
|
|
365
417
|
}
|
|
366
|
-
async
|
|
418
|
+
async createAudioBuffer(voiceParams) {
|
|
419
|
+
const sample = voiceParams.sample;
|
|
367
420
|
const sampleStart = voiceParams.start;
|
|
368
|
-
const sampleEnd =
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const start = sample.byteOffset + sampleStart;
|
|
372
|
-
const end = sample.byteOffset + sampleEnd;
|
|
373
|
-
const buffer = sample.buffer.slice(start, end);
|
|
374
|
-
const audioBuffer = await this.audioContext.decodeAudioData(buffer);
|
|
375
|
-
return audioBuffer;
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
const sample = voiceParams.sample;
|
|
379
|
-
const start = sample.byteOffset + sampleStart;
|
|
380
|
-
const end = sample.byteOffset + sampleEnd;
|
|
381
|
-
const buffer = sample.buffer.slice(start, end);
|
|
382
|
-
const audioBuffer = new AudioBuffer({
|
|
383
|
-
numberOfChannels: 1,
|
|
384
|
-
length: sample.length,
|
|
385
|
-
sampleRate: voiceParams.sampleRate,
|
|
386
|
-
});
|
|
387
|
-
const channelData = audioBuffer.getChannelData(0);
|
|
388
|
-
const int16Array = new Int16Array(buffer);
|
|
389
|
-
for (let i = 0; i < int16Array.length; i++) {
|
|
390
|
-
channelData[i] = int16Array[i] / 32768;
|
|
391
|
-
}
|
|
392
|
-
return audioBuffer;
|
|
393
|
-
}
|
|
421
|
+
const sampleEnd = sample.data.length + voiceParams.end;
|
|
422
|
+
const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
|
|
423
|
+
return audioBuffer;
|
|
394
424
|
}
|
|
395
425
|
createBufferSource(voiceParams, audioBuffer) {
|
|
396
426
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
@@ -420,10 +450,10 @@ class MidyGM1 {
|
|
|
420
450
|
break;
|
|
421
451
|
}
|
|
422
452
|
case "controller":
|
|
423
|
-
this.
|
|
453
|
+
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
424
454
|
break;
|
|
425
455
|
case "programChange":
|
|
426
|
-
this.
|
|
456
|
+
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
427
457
|
break;
|
|
428
458
|
case "pitchBend":
|
|
429
459
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
@@ -456,8 +486,9 @@ class MidyGM1 {
|
|
|
456
486
|
await Promise.all(this.notePromises);
|
|
457
487
|
this.notePromises = [];
|
|
458
488
|
this.exclusiveClassNotes.fill(undefined);
|
|
459
|
-
this.
|
|
489
|
+
this.voiceCache.clear();
|
|
460
490
|
for (let i = 0; i < this.channels.length; i++) {
|
|
491
|
+
this.channels[i].scheduledNotes = [];
|
|
461
492
|
this.resetAllStates(i);
|
|
462
493
|
}
|
|
463
494
|
resolve();
|
|
@@ -478,8 +509,9 @@ class MidyGM1 {
|
|
|
478
509
|
await this.stopNotes(0, true, now);
|
|
479
510
|
this.notePromises = [];
|
|
480
511
|
this.exclusiveClassNotes.fill(undefined);
|
|
481
|
-
this.
|
|
512
|
+
this.voiceCache.clear();
|
|
482
513
|
for (let i = 0; i < this.channels.length; i++) {
|
|
514
|
+
this.channels[i].scheduledNotes = [];
|
|
483
515
|
this.resetAllStates(i);
|
|
484
516
|
}
|
|
485
517
|
this.isStopping = false;
|
|
@@ -511,11 +543,7 @@ class MidyGM1 {
|
|
|
511
543
|
secondToTicks(second, secondsPerBeat) {
|
|
512
544
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
513
545
|
}
|
|
514
|
-
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
515
|
-
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
516
|
-
}
|
|
517
546
|
extractMidiData(midi) {
|
|
518
|
-
this.audioBufferCounter.clear();
|
|
519
547
|
const instruments = new Set();
|
|
520
548
|
const timeline = [];
|
|
521
549
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -535,8 +563,6 @@ class MidyGM1 {
|
|
|
535
563
|
switch (event.type) {
|
|
536
564
|
case "noteOn": {
|
|
537
565
|
const channel = tmpChannels[event.channel];
|
|
538
|
-
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
539
|
-
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
540
566
|
if (channel.programNumber < 0) {
|
|
541
567
|
instruments.add(`${channel.bank}:0`);
|
|
542
568
|
channel.programNumber = 0;
|
|
@@ -553,10 +579,6 @@ class MidyGM1 {
|
|
|
553
579
|
timeline.push(event);
|
|
554
580
|
}
|
|
555
581
|
}
|
|
556
|
-
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
557
|
-
if (count === 1)
|
|
558
|
-
this.audioBufferCounter.delete(audioBufferId);
|
|
559
|
-
}
|
|
560
582
|
const priority = {
|
|
561
583
|
controller: 0,
|
|
562
584
|
sysEx: 1,
|
|
@@ -599,7 +621,6 @@ class MidyGM1 {
|
|
|
599
621
|
this.notePromises.push(promise);
|
|
600
622
|
promises.push(promise);
|
|
601
623
|
});
|
|
602
|
-
channel.scheduledNotes = [];
|
|
603
624
|
return Promise.all(promises);
|
|
604
625
|
}
|
|
605
626
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -613,6 +634,8 @@ class MidyGM1 {
|
|
|
613
634
|
if (this.isPlaying || this.isPaused)
|
|
614
635
|
return;
|
|
615
636
|
this.resumeTime = 0;
|
|
637
|
+
if (this.voiceCounter.size === 0)
|
|
638
|
+
this.cacheVoiceIds();
|
|
616
639
|
await this.playNotes();
|
|
617
640
|
this.isPlaying = false;
|
|
618
641
|
}
|
|
@@ -655,7 +678,7 @@ class MidyGM1 {
|
|
|
655
678
|
}
|
|
656
679
|
processScheduledNotes(channel, callback) {
|
|
657
680
|
const scheduledNotes = channel.scheduledNotes;
|
|
658
|
-
for (let i =
|
|
681
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
659
682
|
const note = scheduledNotes[i];
|
|
660
683
|
if (!note)
|
|
661
684
|
continue;
|
|
@@ -666,14 +689,14 @@ class MidyGM1 {
|
|
|
666
689
|
}
|
|
667
690
|
processActiveNotes(channel, scheduleTime, callback) {
|
|
668
691
|
const scheduledNotes = channel.scheduledNotes;
|
|
669
|
-
for (let i =
|
|
692
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
670
693
|
const note = scheduledNotes[i];
|
|
671
694
|
if (!note)
|
|
672
695
|
continue;
|
|
673
696
|
if (note.ending)
|
|
674
697
|
continue;
|
|
675
698
|
if (scheduleTime < note.startTime)
|
|
676
|
-
|
|
699
|
+
break;
|
|
677
700
|
callback(note);
|
|
678
701
|
}
|
|
679
702
|
}
|
|
@@ -790,31 +813,31 @@ class MidyGM1 {
|
|
|
790
813
|
note.modulationLFO.connect(note.volumeDepth);
|
|
791
814
|
note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
|
|
792
815
|
}
|
|
793
|
-
async getAudioBuffer(
|
|
794
|
-
const audioBufferId = this.
|
|
795
|
-
const cache = this.
|
|
816
|
+
async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
|
|
817
|
+
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
818
|
+
const cache = this.voiceCache.get(audioBufferId);
|
|
796
819
|
if (cache) {
|
|
797
820
|
cache.counter += 1;
|
|
798
821
|
if (cache.maxCount <= cache.counter) {
|
|
799
|
-
this.
|
|
822
|
+
this.voiceCache.delete(audioBufferId);
|
|
800
823
|
}
|
|
801
824
|
return cache.audioBuffer;
|
|
802
825
|
}
|
|
803
826
|
else {
|
|
804
|
-
const maxCount = this.
|
|
805
|
-
const audioBuffer = await this.
|
|
827
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
828
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
806
829
|
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
807
|
-
this.
|
|
830
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
808
831
|
return audioBuffer;
|
|
809
832
|
}
|
|
810
833
|
}
|
|
811
|
-
async createNote(channel, voice, noteNumber, velocity, startTime
|
|
834
|
+
async createNote(channel, voice, noteNumber, velocity, startTime) {
|
|
812
835
|
const now = this.audioContext.currentTime;
|
|
813
836
|
const state = channel.state;
|
|
814
837
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
815
838
|
const voiceParams = voice.getAllParams(controllerState);
|
|
816
839
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
817
|
-
const audioBuffer = await this.getAudioBuffer(channel
|
|
840
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
818
841
|
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
819
842
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
820
843
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
@@ -847,19 +870,18 @@ class MidyGM1 {
|
|
|
847
870
|
}
|
|
848
871
|
this.exclusiveClassNotes[exclusiveClass] = [note, channelNumber];
|
|
849
872
|
}
|
|
850
|
-
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime
|
|
873
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
851
874
|
const channel = this.channels[channelNumber];
|
|
852
875
|
const bankNumber = channel.bank;
|
|
853
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
876
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
877
|
+
.get(bankNumber);
|
|
854
878
|
if (soundFontIndex === undefined)
|
|
855
879
|
return;
|
|
856
880
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
857
881
|
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
858
882
|
if (!voice)
|
|
859
883
|
return;
|
|
860
|
-
const
|
|
861
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
862
|
-
note.noteOffEvent = noteOffEvent;
|
|
884
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
863
885
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
864
886
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
865
887
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -909,15 +931,29 @@ class MidyGM1 {
|
|
|
909
931
|
const channel = this.channels[channelNumber];
|
|
910
932
|
if (!force && 0.5 <= channel.state.sustainPedal)
|
|
911
933
|
return;
|
|
912
|
-
const
|
|
913
|
-
if (
|
|
934
|
+
const index = this.findNoteOffIndex(channel, noteNumber);
|
|
935
|
+
if (index < 0)
|
|
914
936
|
return;
|
|
937
|
+
const note = channel.scheduledNotes[index];
|
|
915
938
|
note.ending = true;
|
|
939
|
+
this.setNoteIndex(channel, index);
|
|
916
940
|
this.releaseNote(channel, note, endTime);
|
|
917
941
|
}
|
|
918
|
-
|
|
942
|
+
setNoteIndex(channel, index) {
|
|
943
|
+
let allEnds = true;
|
|
944
|
+
for (let i = channel.scheduleIndex; i < index; i++) {
|
|
945
|
+
const note = channel.scheduledNotes[i];
|
|
946
|
+
if (note && !note.ending) {
|
|
947
|
+
allEnds = false;
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (allEnds)
|
|
952
|
+
channel.scheduleIndex = index + 1;
|
|
953
|
+
}
|
|
954
|
+
findNoteOffIndex(channel, noteNumber) {
|
|
919
955
|
const scheduledNotes = channel.scheduledNotes;
|
|
920
|
-
for (let i =
|
|
956
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
921
957
|
const note = scheduledNotes[i];
|
|
922
958
|
if (!note)
|
|
923
959
|
continue;
|
|
@@ -925,8 +961,9 @@ class MidyGM1 {
|
|
|
925
961
|
continue;
|
|
926
962
|
if (note.noteNumber !== noteNumber)
|
|
927
963
|
continue;
|
|
928
|
-
return
|
|
964
|
+
return i;
|
|
929
965
|
}
|
|
966
|
+
return -1;
|
|
930
967
|
}
|
|
931
968
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
932
969
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -952,16 +989,16 @@ class MidyGM1 {
|
|
|
952
989
|
case 0x90:
|
|
953
990
|
return this.noteOn(channelNumber, data1, data2, scheduleTime);
|
|
954
991
|
case 0xB0:
|
|
955
|
-
return this.
|
|
992
|
+
return this.setControlChange(channelNumber, data1, data2, scheduleTime);
|
|
956
993
|
case 0xC0:
|
|
957
|
-
return this.
|
|
994
|
+
return this.setProgramChange(channelNumber, data1, scheduleTime);
|
|
958
995
|
case 0xE0:
|
|
959
996
|
return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
|
|
960
997
|
default:
|
|
961
998
|
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
962
999
|
}
|
|
963
1000
|
}
|
|
964
|
-
|
|
1001
|
+
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
965
1002
|
const channel = this.channels[channelNumber];
|
|
966
1003
|
channel.programNumber = programNumber;
|
|
967
1004
|
}
|
|
@@ -1054,8 +1091,9 @@ class MidyGM1 {
|
|
|
1054
1091
|
this.processScheduledNotes(channel, (note) => {
|
|
1055
1092
|
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
1056
1093
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
1057
|
-
let
|
|
1058
|
-
let
|
|
1094
|
+
let applyVolumeEnvelope = false;
|
|
1095
|
+
let applyFilterEnvelope = false;
|
|
1096
|
+
let applyPitchEnvelope = false;
|
|
1059
1097
|
for (const [key, value] of Object.entries(voiceParams)) {
|
|
1060
1098
|
const prevValue = note.voiceParams[key];
|
|
1061
1099
|
if (value === prevValue)
|
|
@@ -1064,32 +1102,21 @@ class MidyGM1 {
|
|
|
1064
1102
|
if (key in this.voiceParamsHandlers) {
|
|
1065
1103
|
this.voiceParamsHandlers[key](channel, note, prevValue, scheduleTime);
|
|
1066
1104
|
}
|
|
1067
|
-
else
|
|
1068
|
-
if (
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
if (key in voiceParams)
|
|
1075
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1076
|
-
}
|
|
1077
|
-
this.setFilterEnvelope(note, scheduleTime);
|
|
1078
|
-
this.setPitchEnvelope(note, scheduleTime);
|
|
1079
|
-
}
|
|
1080
|
-
else if (volumeEnvelopeKeySet.has(key)) {
|
|
1081
|
-
if (appliedVolumeEnvelope)
|
|
1082
|
-
continue;
|
|
1083
|
-
appliedVolumeEnvelope = true;
|
|
1084
|
-
const noteVoiceParams = note.voiceParams;
|
|
1085
|
-
for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
|
|
1086
|
-
const key = volumeEnvelopeKeys[i];
|
|
1087
|
-
if (key in voiceParams)
|
|
1088
|
-
noteVoiceParams[key] = voiceParams[key];
|
|
1089
|
-
}
|
|
1090
|
-
this.setVolumeEnvelope(note, scheduleTime);
|
|
1105
|
+
else {
|
|
1106
|
+
if (volumeEnvelopeKeySet.has(key))
|
|
1107
|
+
applyVolumeEnvelope = true;
|
|
1108
|
+
if (filterEnvelopeKeySet.has(key))
|
|
1109
|
+
applyFilterEnvelope = true;
|
|
1110
|
+
if (pitchEnvelopeKeySet.has(key))
|
|
1111
|
+
applyPitchEnvelope = true;
|
|
1091
1112
|
}
|
|
1092
1113
|
}
|
|
1114
|
+
if (applyVolumeEnvelope)
|
|
1115
|
+
this.setVolumeEnvelope(note, scheduleTime);
|
|
1116
|
+
if (applyFilterEnvelope)
|
|
1117
|
+
this.setFilterEnvelope(note, scheduleTime);
|
|
1118
|
+
if (applyPitchEnvelope)
|
|
1119
|
+
this.setPitchEnvelope(note, scheduleTime);
|
|
1093
1120
|
});
|
|
1094
1121
|
}
|
|
1095
1122
|
createControlChangeHandlers() {
|
|
@@ -1105,9 +1132,10 @@ class MidyGM1 {
|
|
|
1105
1132
|
handlers[101] = this.setRPNMSB;
|
|
1106
1133
|
handlers[120] = this.allSoundOff;
|
|
1107
1134
|
handlers[121] = this.resetAllControllers;
|
|
1135
|
+
handlers[123] = this.allNotesOff;
|
|
1108
1136
|
return handlers;
|
|
1109
1137
|
}
|
|
1110
|
-
|
|
1138
|
+
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1111
1139
|
const handler = this.controlChangeHandlers[controllerType];
|
|
1112
1140
|
if (handler) {
|
|
1113
1141
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
@@ -1300,7 +1328,7 @@ class MidyGM1 {
|
|
|
1300
1328
|
const entries = Object.entries(defaultControllerState);
|
|
1301
1329
|
for (const [key, { type, defaultValue }] of entries) {
|
|
1302
1330
|
if (128 <= type) {
|
|
1303
|
-
this.
|
|
1331
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1304
1332
|
}
|
|
1305
1333
|
else {
|
|
1306
1334
|
state[key] = defaultValue;
|
|
@@ -1325,7 +1353,7 @@ class MidyGM1 {
|
|
|
1325
1353
|
const key = keys[i];
|
|
1326
1354
|
const { type, defaultValue } = defaultControllerState[key];
|
|
1327
1355
|
if (128 <= type) {
|
|
1328
|
-
this.
|
|
1356
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1329
1357
|
}
|
|
1330
1358
|
else {
|
|
1331
1359
|
state[key] = defaultValue;
|