@marmooo/midy 0.3.4 → 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 +13 -10
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +126 -95
- package/esm/midy-GM2.d.ts +17 -13
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +157 -124
- package/esm/midy-GMLite.d.ts +14 -10
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +125 -96
- package/esm/midy.d.ts +18 -14
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +172 -139
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +13 -10
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +126 -95
- package/script/midy-GM2.d.ts +17 -13
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +157 -124
- package/script/midy-GMLite.d.ts +14 -10
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +125 -96
- package/script/midy.d.ts +18 -14
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +172 -139
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>;
|
|
60
|
+
cacheVoiceIds(): void;
|
|
61
|
+
getVoiceId(channel: any, noteNumber: any, velocity: any): string | undefined;
|
|
59
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[];
|
|
@@ -84,7 +86,7 @@ export class MidyGM1 {
|
|
|
84
86
|
seekTo(second: any): void;
|
|
85
87
|
calcTotalTime(): number;
|
|
86
88
|
currentTime(): number;
|
|
87
|
-
processScheduledNotes(channel: any,
|
|
89
|
+
processScheduledNotes(channel: any, callback: any): void;
|
|
88
90
|
processActiveNotes(channel: any, scheduleTime: any, callback: any): void;
|
|
89
91
|
cbToRatio(cb: any): number;
|
|
90
92
|
rateToCent(rate: any): number;
|
|
@@ -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
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":"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,
|
|
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
|
@@ -205,13 +205,13 @@ class MidyGM1 {
|
|
|
205
205
|
writable: true,
|
|
206
206
|
value: this.initSoundFontTable()
|
|
207
207
|
});
|
|
208
|
-
Object.defineProperty(this, "
|
|
208
|
+
Object.defineProperty(this, "voiceCounter", {
|
|
209
209
|
enumerable: true,
|
|
210
210
|
configurable: true,
|
|
211
211
|
writable: true,
|
|
212
212
|
value: new Map()
|
|
213
213
|
});
|
|
214
|
-
Object.defineProperty(this, "
|
|
214
|
+
Object.defineProperty(this, "voiceCache", {
|
|
215
215
|
enumerable: true,
|
|
216
216
|
configurable: true,
|
|
217
217
|
writable: true,
|
|
@@ -298,13 +298,11 @@ class MidyGM1 {
|
|
|
298
298
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
299
299
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
300
300
|
const presetHeader = presetHeaders[i];
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
banks.set(presetHeader.bank, index);
|
|
304
|
-
}
|
|
301
|
+
const banks = this.soundFontTable[presetHeader.preset];
|
|
302
|
+
banks.set(presetHeader.bank, index);
|
|
305
303
|
}
|
|
306
304
|
}
|
|
307
|
-
async
|
|
305
|
+
async toUint8Array(input) {
|
|
308
306
|
let uint8Array;
|
|
309
307
|
if (typeof input === "string") {
|
|
310
308
|
const response = await fetch(input);
|
|
@@ -317,23 +315,32 @@ class MidyGM1 {
|
|
|
317
315
|
else {
|
|
318
316
|
throw new TypeError("input must be a URL string or Uint8Array");
|
|
319
317
|
}
|
|
320
|
-
|
|
321
|
-
const soundFont = new soundfont_parser_1.SoundFont(parsed);
|
|
322
|
-
this.addSoundFont(soundFont);
|
|
318
|
+
return uint8Array;
|
|
323
319
|
}
|
|
324
|
-
async
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
}
|
|
333
333
|
}
|
|
334
334
|
else {
|
|
335
|
-
|
|
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);
|
|
336
339
|
}
|
|
340
|
+
}
|
|
341
|
+
async loadMIDI(input) {
|
|
342
|
+
this.voiceCounter.clear();
|
|
343
|
+
const uint8Array = await this.toUint8Array(input);
|
|
337
344
|
const midi = (0, midi_file_1.parseMidi)(uint8Array);
|
|
338
345
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
339
346
|
const midiData = this.extractMidiData(midi);
|
|
@@ -341,6 +348,45 @@ class MidyGM1 {
|
|
|
341
348
|
this.timeline = midiData.timeline;
|
|
342
349
|
this.totalTime = this.calcTotalTime();
|
|
343
350
|
}
|
|
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
|
+
}
|
|
344
390
|
createChannelAudioNodes(audioContext) {
|
|
345
391
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
346
392
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
@@ -369,34 +415,12 @@ class MidyGM1 {
|
|
|
369
415
|
});
|
|
370
416
|
return channels;
|
|
371
417
|
}
|
|
372
|
-
async
|
|
418
|
+
async createAudioBuffer(voiceParams) {
|
|
419
|
+
const sample = voiceParams.sample;
|
|
373
420
|
const sampleStart = voiceParams.start;
|
|
374
|
-
const sampleEnd =
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const start = sample.byteOffset + sampleStart;
|
|
378
|
-
const end = sample.byteOffset + sampleEnd;
|
|
379
|
-
const buffer = sample.buffer.slice(start, end);
|
|
380
|
-
const audioBuffer = await this.audioContext.decodeAudioData(buffer);
|
|
381
|
-
return audioBuffer;
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
const sample = voiceParams.sample;
|
|
385
|
-
const start = sample.byteOffset + sampleStart;
|
|
386
|
-
const end = sample.byteOffset + sampleEnd;
|
|
387
|
-
const buffer = sample.buffer.slice(start, end);
|
|
388
|
-
const audioBuffer = new AudioBuffer({
|
|
389
|
-
numberOfChannels: 1,
|
|
390
|
-
length: sample.length,
|
|
391
|
-
sampleRate: voiceParams.sampleRate,
|
|
392
|
-
});
|
|
393
|
-
const channelData = audioBuffer.getChannelData(0);
|
|
394
|
-
const int16Array = new Int16Array(buffer);
|
|
395
|
-
for (let i = 0; i < int16Array.length; i++) {
|
|
396
|
-
channelData[i] = int16Array[i] / 32768;
|
|
397
|
-
}
|
|
398
|
-
return audioBuffer;
|
|
399
|
-
}
|
|
421
|
+
const sampleEnd = sample.data.length + voiceParams.end;
|
|
422
|
+
const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
|
|
423
|
+
return audioBuffer;
|
|
400
424
|
}
|
|
401
425
|
createBufferSource(voiceParams, audioBuffer) {
|
|
402
426
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
@@ -426,10 +450,10 @@ class MidyGM1 {
|
|
|
426
450
|
break;
|
|
427
451
|
}
|
|
428
452
|
case "controller":
|
|
429
|
-
this.
|
|
453
|
+
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
430
454
|
break;
|
|
431
455
|
case "programChange":
|
|
432
|
-
this.
|
|
456
|
+
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
433
457
|
break;
|
|
434
458
|
case "pitchBend":
|
|
435
459
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
@@ -462,8 +486,9 @@ class MidyGM1 {
|
|
|
462
486
|
await Promise.all(this.notePromises);
|
|
463
487
|
this.notePromises = [];
|
|
464
488
|
this.exclusiveClassNotes.fill(undefined);
|
|
465
|
-
this.
|
|
489
|
+
this.voiceCache.clear();
|
|
466
490
|
for (let i = 0; i < this.channels.length; i++) {
|
|
491
|
+
this.channels[i].scheduledNotes = [];
|
|
467
492
|
this.resetAllStates(i);
|
|
468
493
|
}
|
|
469
494
|
resolve();
|
|
@@ -484,8 +509,9 @@ class MidyGM1 {
|
|
|
484
509
|
await this.stopNotes(0, true, now);
|
|
485
510
|
this.notePromises = [];
|
|
486
511
|
this.exclusiveClassNotes.fill(undefined);
|
|
487
|
-
this.
|
|
512
|
+
this.voiceCache.clear();
|
|
488
513
|
for (let i = 0; i < this.channels.length; i++) {
|
|
514
|
+
this.channels[i].scheduledNotes = [];
|
|
489
515
|
this.resetAllStates(i);
|
|
490
516
|
}
|
|
491
517
|
this.isStopping = false;
|
|
@@ -517,11 +543,7 @@ class MidyGM1 {
|
|
|
517
543
|
secondToTicks(second, secondsPerBeat) {
|
|
518
544
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
519
545
|
}
|
|
520
|
-
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
521
|
-
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
522
|
-
}
|
|
523
546
|
extractMidiData(midi) {
|
|
524
|
-
this.audioBufferCounter.clear();
|
|
525
547
|
const instruments = new Set();
|
|
526
548
|
const timeline = [];
|
|
527
549
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -541,8 +563,6 @@ class MidyGM1 {
|
|
|
541
563
|
switch (event.type) {
|
|
542
564
|
case "noteOn": {
|
|
543
565
|
const channel = tmpChannels[event.channel];
|
|
544
|
-
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
545
|
-
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
546
566
|
if (channel.programNumber < 0) {
|
|
547
567
|
instruments.add(`${channel.bank}:0`);
|
|
548
568
|
channel.programNumber = 0;
|
|
@@ -559,10 +579,6 @@ class MidyGM1 {
|
|
|
559
579
|
timeline.push(event);
|
|
560
580
|
}
|
|
561
581
|
}
|
|
562
|
-
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
563
|
-
if (count === 1)
|
|
564
|
-
this.audioBufferCounter.delete(audioBufferId);
|
|
565
|
-
}
|
|
566
582
|
const priority = {
|
|
567
583
|
controller: 0,
|
|
568
584
|
sysEx: 1,
|
|
@@ -600,12 +616,11 @@ class MidyGM1 {
|
|
|
600
616
|
stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
|
|
601
617
|
const channel = this.channels[channelNumber];
|
|
602
618
|
const promises = [];
|
|
603
|
-
this.processScheduledNotes(channel,
|
|
619
|
+
this.processScheduledNotes(channel, (note) => {
|
|
604
620
|
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
605
621
|
this.notePromises.push(promise);
|
|
606
622
|
promises.push(promise);
|
|
607
623
|
});
|
|
608
|
-
channel.scheduledNotes = [];
|
|
609
624
|
return Promise.all(promises);
|
|
610
625
|
}
|
|
611
626
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -619,6 +634,8 @@ class MidyGM1 {
|
|
|
619
634
|
if (this.isPlaying || this.isPaused)
|
|
620
635
|
return;
|
|
621
636
|
this.resumeTime = 0;
|
|
637
|
+
if (this.voiceCounter.size === 0)
|
|
638
|
+
this.cacheVoiceIds();
|
|
622
639
|
await this.playNotes();
|
|
623
640
|
this.isPlaying = false;
|
|
624
641
|
}
|
|
@@ -659,22 +676,20 @@ class MidyGM1 {
|
|
|
659
676
|
const now = this.audioContext.currentTime;
|
|
660
677
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
661
678
|
}
|
|
662
|
-
processScheduledNotes(channel,
|
|
679
|
+
processScheduledNotes(channel, callback) {
|
|
663
680
|
const scheduledNotes = channel.scheduledNotes;
|
|
664
|
-
for (let i =
|
|
681
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
665
682
|
const note = scheduledNotes[i];
|
|
666
683
|
if (!note)
|
|
667
684
|
continue;
|
|
668
685
|
if (note.ending)
|
|
669
686
|
continue;
|
|
670
|
-
if (note.startTime < scheduleTime)
|
|
671
|
-
continue;
|
|
672
687
|
callback(note);
|
|
673
688
|
}
|
|
674
689
|
}
|
|
675
690
|
processActiveNotes(channel, scheduleTime, callback) {
|
|
676
691
|
const scheduledNotes = channel.scheduledNotes;
|
|
677
|
-
for (let i =
|
|
692
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
678
693
|
const note = scheduledNotes[i];
|
|
679
694
|
if (!note)
|
|
680
695
|
continue;
|
|
@@ -705,7 +720,7 @@ class MidyGM1 {
|
|
|
705
720
|
return tuning + pitch;
|
|
706
721
|
}
|
|
707
722
|
updateChannelDetune(channel, scheduleTime) {
|
|
708
|
-
this.processScheduledNotes(channel,
|
|
723
|
+
this.processScheduledNotes(channel, (note) => {
|
|
709
724
|
this.updateDetune(channel, note, scheduleTime);
|
|
710
725
|
});
|
|
711
726
|
}
|
|
@@ -798,31 +813,31 @@ class MidyGM1 {
|
|
|
798
813
|
note.modulationLFO.connect(note.volumeDepth);
|
|
799
814
|
note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
|
|
800
815
|
}
|
|
801
|
-
async getAudioBuffer(
|
|
802
|
-
const audioBufferId = this.
|
|
803
|
-
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);
|
|
804
819
|
if (cache) {
|
|
805
820
|
cache.counter += 1;
|
|
806
821
|
if (cache.maxCount <= cache.counter) {
|
|
807
|
-
this.
|
|
822
|
+
this.voiceCache.delete(audioBufferId);
|
|
808
823
|
}
|
|
809
824
|
return cache.audioBuffer;
|
|
810
825
|
}
|
|
811
826
|
else {
|
|
812
|
-
const maxCount = this.
|
|
813
|
-
const audioBuffer = await this.
|
|
827
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
828
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
814
829
|
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
815
|
-
this.
|
|
830
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
816
831
|
return audioBuffer;
|
|
817
832
|
}
|
|
818
833
|
}
|
|
819
|
-
async createNote(channel, voice, noteNumber, velocity, startTime
|
|
834
|
+
async createNote(channel, voice, noteNumber, velocity, startTime) {
|
|
820
835
|
const now = this.audioContext.currentTime;
|
|
821
836
|
const state = channel.state;
|
|
822
837
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
823
838
|
const voiceParams = voice.getAllParams(controllerState);
|
|
824
839
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
825
|
-
const audioBuffer = await this.getAudioBuffer(channel
|
|
840
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
826
841
|
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
827
842
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
828
843
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
@@ -858,15 +873,15 @@ class MidyGM1 {
|
|
|
858
873
|
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
859
874
|
const channel = this.channels[channelNumber];
|
|
860
875
|
const bankNumber = channel.bank;
|
|
861
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
876
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
877
|
+
.get(bankNumber);
|
|
862
878
|
if (soundFontIndex === undefined)
|
|
863
879
|
return;
|
|
864
880
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
865
881
|
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
866
882
|
if (!voice)
|
|
867
883
|
return;
|
|
868
|
-
const
|
|
869
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
884
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
870
885
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
871
886
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
872
887
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -916,15 +931,29 @@ class MidyGM1 {
|
|
|
916
931
|
const channel = this.channels[channelNumber];
|
|
917
932
|
if (!force && 0.5 <= channel.state.sustainPedal)
|
|
918
933
|
return;
|
|
919
|
-
const
|
|
920
|
-
if (
|
|
934
|
+
const index = this.findNoteOffIndex(channel, noteNumber);
|
|
935
|
+
if (index < 0)
|
|
921
936
|
return;
|
|
937
|
+
const note = channel.scheduledNotes[index];
|
|
922
938
|
note.ending = true;
|
|
939
|
+
this.setNoteIndex(channel, index);
|
|
923
940
|
this.releaseNote(channel, note, endTime);
|
|
924
941
|
}
|
|
925
|
-
|
|
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) {
|
|
926
955
|
const scheduledNotes = channel.scheduledNotes;
|
|
927
|
-
for (let i =
|
|
956
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
928
957
|
const note = scheduledNotes[i];
|
|
929
958
|
if (!note)
|
|
930
959
|
continue;
|
|
@@ -932,8 +961,9 @@ class MidyGM1 {
|
|
|
932
961
|
continue;
|
|
933
962
|
if (note.noteNumber !== noteNumber)
|
|
934
963
|
continue;
|
|
935
|
-
return
|
|
964
|
+
return i;
|
|
936
965
|
}
|
|
966
|
+
return -1;
|
|
937
967
|
}
|
|
938
968
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
939
969
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -959,16 +989,16 @@ class MidyGM1 {
|
|
|
959
989
|
case 0x90:
|
|
960
990
|
return this.noteOn(channelNumber, data1, data2, scheduleTime);
|
|
961
991
|
case 0xB0:
|
|
962
|
-
return this.
|
|
992
|
+
return this.setControlChange(channelNumber, data1, data2, scheduleTime);
|
|
963
993
|
case 0xC0:
|
|
964
|
-
return this.
|
|
994
|
+
return this.setProgramChange(channelNumber, data1, scheduleTime);
|
|
965
995
|
case 0xE0:
|
|
966
996
|
return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
|
|
967
997
|
default:
|
|
968
998
|
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
969
999
|
}
|
|
970
1000
|
}
|
|
971
|
-
|
|
1001
|
+
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
972
1002
|
const channel = this.channels[channelNumber];
|
|
973
1003
|
channel.programNumber = programNumber;
|
|
974
1004
|
}
|
|
@@ -1058,7 +1088,7 @@ class MidyGM1 {
|
|
|
1058
1088
|
return state;
|
|
1059
1089
|
}
|
|
1060
1090
|
applyVoiceParams(channel, controllerType, scheduleTime) {
|
|
1061
|
-
this.processScheduledNotes(channel,
|
|
1091
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1062
1092
|
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
1063
1093
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
1064
1094
|
let applyVolumeEnvelope = false;
|
|
@@ -1102,9 +1132,10 @@ class MidyGM1 {
|
|
|
1102
1132
|
handlers[101] = this.setRPNMSB;
|
|
1103
1133
|
handlers[120] = this.allSoundOff;
|
|
1104
1134
|
handlers[121] = this.resetAllControllers;
|
|
1135
|
+
handlers[123] = this.allNotesOff;
|
|
1105
1136
|
return handlers;
|
|
1106
1137
|
}
|
|
1107
|
-
|
|
1138
|
+
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1108
1139
|
const handler = this.controlChangeHandlers[controllerType];
|
|
1109
1140
|
if (handler) {
|
|
1110
1141
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
@@ -1117,7 +1148,7 @@ class MidyGM1 {
|
|
|
1117
1148
|
}
|
|
1118
1149
|
updateModulation(channel, scheduleTime) {
|
|
1119
1150
|
const depth = channel.state.modulationDepth * channel.modulationDepthRange;
|
|
1120
|
-
this.processScheduledNotes(channel,
|
|
1151
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1121
1152
|
if (note.modulationDepth) {
|
|
1122
1153
|
note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
|
|
1123
1154
|
}
|
|
@@ -1178,7 +1209,7 @@ class MidyGM1 {
|
|
|
1178
1209
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1179
1210
|
channel.state.sustainPedal = value / 127;
|
|
1180
1211
|
if (64 <= value) {
|
|
1181
|
-
this.processScheduledNotes(channel,
|
|
1212
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1182
1213
|
channel.sustainNotes.push(note);
|
|
1183
1214
|
});
|
|
1184
1215
|
}
|
|
@@ -1297,7 +1328,7 @@ class MidyGM1 {
|
|
|
1297
1328
|
const entries = Object.entries(defaultControllerState);
|
|
1298
1329
|
for (const [key, { type, defaultValue }] of entries) {
|
|
1299
1330
|
if (128 <= type) {
|
|
1300
|
-
this.
|
|
1331
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1301
1332
|
}
|
|
1302
1333
|
else {
|
|
1303
1334
|
state[key] = defaultValue;
|
|
@@ -1322,7 +1353,7 @@ class MidyGM1 {
|
|
|
1322
1353
|
const key = keys[i];
|
|
1323
1354
|
const { type, defaultValue } = defaultControllerState[key];
|
|
1324
1355
|
if (128 <= type) {
|
|
1325
|
-
this.
|
|
1356
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1326
1357
|
}
|
|
1327
1358
|
else {
|
|
1328
1359
|
state[key] = defaultValue;
|