@marmooo/midy 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -11
- package/esm/midy-GM1.d.ts +14 -11
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +140 -106
- package/esm/midy-GM2.d.ts +26 -22
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +285 -279
- package/esm/midy-GMLite.d.ts +15 -11
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +139 -107
- package/esm/midy.d.ts +31 -27
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +304 -298
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +14 -11
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +140 -106
- package/script/midy-GM2.d.ts +26 -22
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +285 -279
- package/script/midy-GMLite.d.ts +15 -11
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +139 -107
- package/script/midy.d.ts +31 -27
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +304 -298
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ functions.
|
|
|
53
53
|
```js
|
|
54
54
|
midy.handleMIDIMessage(statusByte, data1, data2, scheduleTime);
|
|
55
55
|
midy.noteOn(channelNumber, noteNumber, velocity);
|
|
56
|
-
midy.
|
|
56
|
+
midy.setProgramChange(channelNumber, program);
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
### Control Change
|
|
@@ -62,7 +62,7 @@ There are functions that handle control changes as they are, as well as
|
|
|
62
62
|
simplified functions.
|
|
63
63
|
|
|
64
64
|
```js
|
|
65
|
-
midy.
|
|
65
|
+
midy.setControlChange(
|
|
66
66
|
channelNumber,
|
|
67
67
|
controller,
|
|
68
68
|
value,
|
|
@@ -91,17 +91,25 @@ optimized for playback on the web. The following example loads only the minimum
|
|
|
91
91
|
presets required for playback.
|
|
92
92
|
|
|
93
93
|
```js
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
const soundFontURL = "https://soundfonts.pages.dev/GeneralUser_GS_v1.471";
|
|
95
|
+
|
|
96
|
+
function getSoundFontPaths() {
|
|
97
|
+
const paths = [];
|
|
98
|
+
for (const instrument of midy.instruments) {
|
|
99
|
+
const [bankNumber, programNumber] = instrument.split(":").map(Number);
|
|
100
|
+
const table = midy.soundFontTable[programNumber];
|
|
101
|
+
if (table.has(bankNumber)) continue;
|
|
102
|
+
const program = programNumber.toString().padStart(3, "0");
|
|
103
|
+
const path = bankNumber === 128
|
|
104
|
+
? `${soundFontURL}/128.sf3`
|
|
105
|
+
: `${soundFontURL}/${program}.sf3`;
|
|
106
|
+
paths.push(path);
|
|
103
107
|
}
|
|
108
|
+
return paths;
|
|
104
109
|
}
|
|
110
|
+
|
|
111
|
+
const paths = this.getSoundFontPaths();
|
|
112
|
+
await midy.loadSoundFont(paths);
|
|
105
113
|
```
|
|
106
114
|
|
|
107
115
|
## Build
|
package/esm/midy-GM1.d.ts
CHANGED
|
@@ -23,16 +23,16 @@ 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;
|
|
31
31
|
isStopping: boolean;
|
|
32
32
|
isSeeking: boolean;
|
|
33
33
|
timeline: any[];
|
|
34
|
-
instruments: any[];
|
|
35
34
|
notePromises: any[];
|
|
35
|
+
instruments: Set<any>;
|
|
36
36
|
exclusiveClassNotes: any[];
|
|
37
37
|
audioContext: any;
|
|
38
38
|
masterVolume: any;
|
|
@@ -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): any;
|
|
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/esm/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,oBAAkB;IAClB,sBAAwB;IACxB,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,8DAcC;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,mEAWC;IAED,wDAKC;IAED,sDAOC;IAED,mDAMC;IAED,kDAKC;IAED;;;;;;;;;;;MA2BC;IAED,oFAMC;IAED,6EAgCC;IAED,qCAeC;IAED,+FAWC;IAED,wDASC;IAED,iFAKC;IAED,oEAKC;IAED;;;MAMC;IAED,8DAKC;IAED,4EAKC;IAED,sEAGC;IAED,2DAUC;IAED,yEAWC;IAED,kFAeC;IAED,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;AAjgDD;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-GM1.js
CHANGED
|
@@ -202,13 +202,13 @@ export class MidyGM1 {
|
|
|
202
202
|
writable: true,
|
|
203
203
|
value: this.initSoundFontTable()
|
|
204
204
|
});
|
|
205
|
-
Object.defineProperty(this, "
|
|
205
|
+
Object.defineProperty(this, "voiceCounter", {
|
|
206
206
|
enumerable: true,
|
|
207
207
|
configurable: true,
|
|
208
208
|
writable: true,
|
|
209
209
|
value: new Map()
|
|
210
210
|
});
|
|
211
|
-
Object.defineProperty(this, "
|
|
211
|
+
Object.defineProperty(this, "voiceCache", {
|
|
212
212
|
enumerable: true,
|
|
213
213
|
configurable: true,
|
|
214
214
|
writable: true,
|
|
@@ -250,17 +250,17 @@ export class MidyGM1 {
|
|
|
250
250
|
writable: true,
|
|
251
251
|
value: []
|
|
252
252
|
});
|
|
253
|
-
Object.defineProperty(this, "
|
|
253
|
+
Object.defineProperty(this, "notePromises", {
|
|
254
254
|
enumerable: true,
|
|
255
255
|
configurable: true,
|
|
256
256
|
writable: true,
|
|
257
257
|
value: []
|
|
258
258
|
});
|
|
259
|
-
Object.defineProperty(this, "
|
|
259
|
+
Object.defineProperty(this, "instruments", {
|
|
260
260
|
enumerable: true,
|
|
261
261
|
configurable: true,
|
|
262
262
|
writable: true,
|
|
263
|
-
value:
|
|
263
|
+
value: new Set()
|
|
264
264
|
});
|
|
265
265
|
Object.defineProperty(this, "exclusiveClassNotes", {
|
|
266
266
|
enumerable: true,
|
|
@@ -295,13 +295,11 @@ export class MidyGM1 {
|
|
|
295
295
|
const presetHeaders = soundFont.parsed.presetHeaders;
|
|
296
296
|
for (let i = 0; i < presetHeaders.length; i++) {
|
|
297
297
|
const presetHeader = presetHeaders[i];
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
banks.set(presetHeader.bank, index);
|
|
301
|
-
}
|
|
298
|
+
const banks = this.soundFontTable[presetHeader.preset];
|
|
299
|
+
banks.set(presetHeader.bank, index);
|
|
302
300
|
}
|
|
303
301
|
}
|
|
304
|
-
async
|
|
302
|
+
async toUint8Array(input) {
|
|
305
303
|
let uint8Array;
|
|
306
304
|
if (typeof input === "string") {
|
|
307
305
|
const response = await fetch(input);
|
|
@@ -314,23 +312,32 @@ export class MidyGM1 {
|
|
|
314
312
|
else {
|
|
315
313
|
throw new TypeError("input must be a URL string or Uint8Array");
|
|
316
314
|
}
|
|
317
|
-
|
|
318
|
-
const soundFont = new SoundFont(parsed);
|
|
319
|
-
this.addSoundFont(soundFont);
|
|
315
|
+
return uint8Array;
|
|
320
316
|
}
|
|
321
|
-
async
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
317
|
+
async loadSoundFont(input) {
|
|
318
|
+
this.voiceCounter.clear();
|
|
319
|
+
if (Array.isArray(input)) {
|
|
320
|
+
const promises = new Array(input.length);
|
|
321
|
+
for (let i = 0; i < input.length; i++) {
|
|
322
|
+
promises[i] = this.toUint8Array(input[i]);
|
|
323
|
+
}
|
|
324
|
+
const uint8Arrays = await Promise.all(promises);
|
|
325
|
+
for (let i = 0; i < uint8Arrays.length; i++) {
|
|
326
|
+
const parsed = parse(uint8Arrays[i]);
|
|
327
|
+
const soundFont = new SoundFont(parsed);
|
|
328
|
+
this.addSoundFont(soundFont);
|
|
329
|
+
}
|
|
330
330
|
}
|
|
331
331
|
else {
|
|
332
|
-
|
|
332
|
+
const uint8Array = await this.toUint8Array(input);
|
|
333
|
+
const parsed = parse(uint8Array);
|
|
334
|
+
const soundFont = new SoundFont(parsed);
|
|
335
|
+
this.addSoundFont(soundFont);
|
|
333
336
|
}
|
|
337
|
+
}
|
|
338
|
+
async loadMIDI(input) {
|
|
339
|
+
this.voiceCounter.clear();
|
|
340
|
+
const uint8Array = await this.toUint8Array(input);
|
|
334
341
|
const midi = parseMidi(uint8Array);
|
|
335
342
|
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
336
343
|
const midiData = this.extractMidiData(midi);
|
|
@@ -338,6 +345,45 @@ export class MidyGM1 {
|
|
|
338
345
|
this.timeline = midiData.timeline;
|
|
339
346
|
this.totalTime = this.calcTotalTime();
|
|
340
347
|
}
|
|
348
|
+
cacheVoiceIds() {
|
|
349
|
+
const timeline = this.timeline;
|
|
350
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
351
|
+
const event = timeline[i];
|
|
352
|
+
switch (event.type) {
|
|
353
|
+
case "noteOn": {
|
|
354
|
+
const audioBufferId = this.getVoiceId(this.channels[event.channel], event.noteNumber, event.velocity);
|
|
355
|
+
this.voiceCounter.set(audioBufferId, (this.voiceCounter.get(audioBufferId) ?? 0) + 1);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case "controller":
|
|
359
|
+
if (event.controllerType === 0) {
|
|
360
|
+
this.setBankMSB(event.channel, event.value);
|
|
361
|
+
}
|
|
362
|
+
else if (event.controllerType === 32) {
|
|
363
|
+
this.setBankLSB(event.channel, event.value);
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
case "programChange":
|
|
367
|
+
this.setProgramChange(event.channel, event.programNumber, event.startTime);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
for (const [audioBufferId, count] of this.voiceCounter) {
|
|
371
|
+
if (count === 1)
|
|
372
|
+
this.voiceCounter.delete(audioBufferId);
|
|
373
|
+
}
|
|
374
|
+
this.GM1SystemOn();
|
|
375
|
+
}
|
|
376
|
+
getVoiceId(channel, noteNumber, velocity) {
|
|
377
|
+
const bankNumber = this.calcBank(channel);
|
|
378
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
379
|
+
.get(bankNumber);
|
|
380
|
+
if (soundFontIndex === undefined)
|
|
381
|
+
return;
|
|
382
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
383
|
+
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
384
|
+
const { instrument, sampleID } = voice.generators;
|
|
385
|
+
return soundFontIndex * (2 ** 32) + (instrument << 16) + sampleID;
|
|
386
|
+
}
|
|
341
387
|
createChannelAudioNodes(audioContext) {
|
|
342
388
|
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
343
389
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
@@ -366,34 +412,12 @@ export class MidyGM1 {
|
|
|
366
412
|
});
|
|
367
413
|
return channels;
|
|
368
414
|
}
|
|
369
|
-
async
|
|
415
|
+
async createAudioBuffer(voiceParams) {
|
|
416
|
+
const sample = voiceParams.sample;
|
|
370
417
|
const sampleStart = voiceParams.start;
|
|
371
|
-
const sampleEnd =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const start = sample.byteOffset + sampleStart;
|
|
375
|
-
const end = sample.byteOffset + sampleEnd;
|
|
376
|
-
const buffer = sample.buffer.slice(start, end);
|
|
377
|
-
const audioBuffer = await this.audioContext.decodeAudioData(buffer);
|
|
378
|
-
return audioBuffer;
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
const sample = voiceParams.sample;
|
|
382
|
-
const start = sample.byteOffset + sampleStart;
|
|
383
|
-
const end = sample.byteOffset + sampleEnd;
|
|
384
|
-
const buffer = sample.buffer.slice(start, end);
|
|
385
|
-
const audioBuffer = new AudioBuffer({
|
|
386
|
-
numberOfChannels: 1,
|
|
387
|
-
length: sample.length,
|
|
388
|
-
sampleRate: voiceParams.sampleRate,
|
|
389
|
-
});
|
|
390
|
-
const channelData = audioBuffer.getChannelData(0);
|
|
391
|
-
const int16Array = new Int16Array(buffer);
|
|
392
|
-
for (let i = 0; i < int16Array.length; i++) {
|
|
393
|
-
channelData[i] = int16Array[i] / 32768;
|
|
394
|
-
}
|
|
395
|
-
return audioBuffer;
|
|
396
|
-
}
|
|
418
|
+
const sampleEnd = sample.data.length + voiceParams.end;
|
|
419
|
+
const audioBuffer = await sample.toAudioBuffer(this.audioContext, sampleStart, sampleEnd);
|
|
420
|
+
return audioBuffer;
|
|
397
421
|
}
|
|
398
422
|
createBufferSource(voiceParams, audioBuffer) {
|
|
399
423
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
@@ -423,10 +447,10 @@ export class MidyGM1 {
|
|
|
423
447
|
break;
|
|
424
448
|
}
|
|
425
449
|
case "controller":
|
|
426
|
-
this.
|
|
450
|
+
this.setControlChange(event.channel, event.controllerType, event.value, startTime);
|
|
427
451
|
break;
|
|
428
452
|
case "programChange":
|
|
429
|
-
this.
|
|
453
|
+
this.setProgramChange(event.channel, event.programNumber, startTime);
|
|
430
454
|
break;
|
|
431
455
|
case "pitchBend":
|
|
432
456
|
this.setPitchBend(event.channel, event.value + 8192, startTime);
|
|
@@ -459,8 +483,9 @@ export class MidyGM1 {
|
|
|
459
483
|
await Promise.all(this.notePromises);
|
|
460
484
|
this.notePromises = [];
|
|
461
485
|
this.exclusiveClassNotes.fill(undefined);
|
|
462
|
-
this.
|
|
486
|
+
this.voiceCache.clear();
|
|
463
487
|
for (let i = 0; i < this.channels.length; i++) {
|
|
488
|
+
this.channels[i].scheduledNotes = [];
|
|
464
489
|
this.resetAllStates(i);
|
|
465
490
|
}
|
|
466
491
|
resolve();
|
|
@@ -481,8 +506,9 @@ export class MidyGM1 {
|
|
|
481
506
|
await this.stopNotes(0, true, now);
|
|
482
507
|
this.notePromises = [];
|
|
483
508
|
this.exclusiveClassNotes.fill(undefined);
|
|
484
|
-
this.
|
|
509
|
+
this.voiceCache.clear();
|
|
485
510
|
for (let i = 0; i < this.channels.length; i++) {
|
|
511
|
+
this.channels[i].scheduledNotes = [];
|
|
486
512
|
this.resetAllStates(i);
|
|
487
513
|
}
|
|
488
514
|
this.isStopping = false;
|
|
@@ -514,11 +540,7 @@ export class MidyGM1 {
|
|
|
514
540
|
secondToTicks(second, secondsPerBeat) {
|
|
515
541
|
return second * this.ticksPerBeat / secondsPerBeat;
|
|
516
542
|
}
|
|
517
|
-
getAudioBufferId(programNumber, noteNumber, velocity) {
|
|
518
|
-
return `${programNumber}:${noteNumber}:${velocity}`;
|
|
519
|
-
}
|
|
520
543
|
extractMidiData(midi) {
|
|
521
|
-
this.audioBufferCounter.clear();
|
|
522
544
|
const instruments = new Set();
|
|
523
545
|
const timeline = [];
|
|
524
546
|
const tmpChannels = new Array(this.channels.length);
|
|
@@ -538,8 +560,6 @@ export class MidyGM1 {
|
|
|
538
560
|
switch (event.type) {
|
|
539
561
|
case "noteOn": {
|
|
540
562
|
const channel = tmpChannels[event.channel];
|
|
541
|
-
const audioBufferId = this.getAudioBufferId(channel.programNumber, event.noteNumber, event.velocity);
|
|
542
|
-
this.audioBufferCounter.set(audioBufferId, (this.audioBufferCounter.get(audioBufferId) ?? 0) + 1);
|
|
543
563
|
if (channel.programNumber < 0) {
|
|
544
564
|
instruments.add(`${channel.bank}:0`);
|
|
545
565
|
channel.programNumber = 0;
|
|
@@ -556,10 +576,6 @@ export class MidyGM1 {
|
|
|
556
576
|
timeline.push(event);
|
|
557
577
|
}
|
|
558
578
|
}
|
|
559
|
-
for (const [audioBufferId, count] of this.audioBufferCounter) {
|
|
560
|
-
if (count === 1)
|
|
561
|
-
this.audioBufferCounter.delete(audioBufferId);
|
|
562
|
-
}
|
|
563
579
|
const priority = {
|
|
564
580
|
controller: 0,
|
|
565
581
|
sysEx: 1,
|
|
@@ -597,12 +613,11 @@ export class MidyGM1 {
|
|
|
597
613
|
stopChannelNotes(channelNumber, velocity, force, scheduleTime) {
|
|
598
614
|
const channel = this.channels[channelNumber];
|
|
599
615
|
const promises = [];
|
|
600
|
-
this.processScheduledNotes(channel,
|
|
616
|
+
this.processScheduledNotes(channel, (note) => {
|
|
601
617
|
const promise = this.scheduleNoteOff(channelNumber, note.noteNumber, velocity, scheduleTime, force);
|
|
602
618
|
this.notePromises.push(promise);
|
|
603
619
|
promises.push(promise);
|
|
604
620
|
});
|
|
605
|
-
channel.scheduledNotes = [];
|
|
606
621
|
return Promise.all(promises);
|
|
607
622
|
}
|
|
608
623
|
stopNotes(velocity, force, scheduleTime) {
|
|
@@ -616,6 +631,8 @@ export class MidyGM1 {
|
|
|
616
631
|
if (this.isPlaying || this.isPaused)
|
|
617
632
|
return;
|
|
618
633
|
this.resumeTime = 0;
|
|
634
|
+
if (this.voiceCounter.size === 0)
|
|
635
|
+
this.cacheVoiceIds();
|
|
619
636
|
await this.playNotes();
|
|
620
637
|
this.isPlaying = false;
|
|
621
638
|
}
|
|
@@ -656,22 +673,20 @@ export class MidyGM1 {
|
|
|
656
673
|
const now = this.audioContext.currentTime;
|
|
657
674
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
658
675
|
}
|
|
659
|
-
processScheduledNotes(channel,
|
|
676
|
+
processScheduledNotes(channel, callback) {
|
|
660
677
|
const scheduledNotes = channel.scheduledNotes;
|
|
661
|
-
for (let i =
|
|
678
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
662
679
|
const note = scheduledNotes[i];
|
|
663
680
|
if (!note)
|
|
664
681
|
continue;
|
|
665
682
|
if (note.ending)
|
|
666
683
|
continue;
|
|
667
|
-
if (note.startTime < scheduleTime)
|
|
668
|
-
continue;
|
|
669
684
|
callback(note);
|
|
670
685
|
}
|
|
671
686
|
}
|
|
672
687
|
processActiveNotes(channel, scheduleTime, callback) {
|
|
673
688
|
const scheduledNotes = channel.scheduledNotes;
|
|
674
|
-
for (let i =
|
|
689
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
675
690
|
const note = scheduledNotes[i];
|
|
676
691
|
if (!note)
|
|
677
692
|
continue;
|
|
@@ -702,7 +717,7 @@ export class MidyGM1 {
|
|
|
702
717
|
return tuning + pitch;
|
|
703
718
|
}
|
|
704
719
|
updateChannelDetune(channel, scheduleTime) {
|
|
705
|
-
this.processScheduledNotes(channel,
|
|
720
|
+
this.processScheduledNotes(channel, (note) => {
|
|
706
721
|
this.updateDetune(channel, note, scheduleTime);
|
|
707
722
|
});
|
|
708
723
|
}
|
|
@@ -795,31 +810,31 @@ export class MidyGM1 {
|
|
|
795
810
|
note.modulationLFO.connect(note.volumeDepth);
|
|
796
811
|
note.volumeDepth.connect(note.volumeEnvelopeNode.gain);
|
|
797
812
|
}
|
|
798
|
-
async getAudioBuffer(
|
|
799
|
-
const audioBufferId = this.
|
|
800
|
-
const cache = this.
|
|
813
|
+
async getAudioBuffer(channel, noteNumber, velocity, voiceParams) {
|
|
814
|
+
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
815
|
+
const cache = this.voiceCache.get(audioBufferId);
|
|
801
816
|
if (cache) {
|
|
802
817
|
cache.counter += 1;
|
|
803
818
|
if (cache.maxCount <= cache.counter) {
|
|
804
|
-
this.
|
|
819
|
+
this.voiceCache.delete(audioBufferId);
|
|
805
820
|
}
|
|
806
821
|
return cache.audioBuffer;
|
|
807
822
|
}
|
|
808
823
|
else {
|
|
809
|
-
const maxCount = this.
|
|
810
|
-
const audioBuffer = await this.
|
|
824
|
+
const maxCount = this.voiceCounter.get(audioBufferId) ?? 0;
|
|
825
|
+
const audioBuffer = await this.createAudioBuffer(voiceParams);
|
|
811
826
|
const cache = { audioBuffer, maxCount, counter: 1 };
|
|
812
|
-
this.
|
|
827
|
+
this.voiceCache.set(audioBufferId, cache);
|
|
813
828
|
return audioBuffer;
|
|
814
829
|
}
|
|
815
830
|
}
|
|
816
|
-
async createNote(channel, voice, noteNumber, velocity, startTime
|
|
831
|
+
async createNote(channel, voice, noteNumber, velocity, startTime) {
|
|
817
832
|
const now = this.audioContext.currentTime;
|
|
818
833
|
const state = channel.state;
|
|
819
834
|
const controllerState = this.getControllerState(channel, noteNumber, velocity);
|
|
820
835
|
const voiceParams = voice.getAllParams(controllerState);
|
|
821
836
|
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
822
|
-
const audioBuffer = await this.getAudioBuffer(channel
|
|
837
|
+
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams);
|
|
823
838
|
note.bufferSource = this.createBufferSource(voiceParams, audioBuffer);
|
|
824
839
|
note.volumeEnvelopeNode = new GainNode(this.audioContext);
|
|
825
840
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
@@ -855,15 +870,15 @@ export class MidyGM1 {
|
|
|
855
870
|
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
856
871
|
const channel = this.channels[channelNumber];
|
|
857
872
|
const bankNumber = channel.bank;
|
|
858
|
-
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
873
|
+
const soundFontIndex = this.soundFontTable[channel.programNumber]
|
|
874
|
+
.get(bankNumber);
|
|
859
875
|
if (soundFontIndex === undefined)
|
|
860
876
|
return;
|
|
861
877
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
862
878
|
const voice = soundFont.getVoice(bankNumber, channel.programNumber, noteNumber, velocity);
|
|
863
879
|
if (!voice)
|
|
864
880
|
return;
|
|
865
|
-
const
|
|
866
|
-
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
881
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime);
|
|
867
882
|
note.volumeEnvelopeNode.connect(channel.gainL);
|
|
868
883
|
note.volumeEnvelopeNode.connect(channel.gainR);
|
|
869
884
|
if (0.5 <= channel.state.sustainPedal) {
|
|
@@ -913,15 +928,29 @@ export class MidyGM1 {
|
|
|
913
928
|
const channel = this.channels[channelNumber];
|
|
914
929
|
if (!force && 0.5 <= channel.state.sustainPedal)
|
|
915
930
|
return;
|
|
916
|
-
const
|
|
917
|
-
if (
|
|
931
|
+
const index = this.findNoteOffIndex(channel, noteNumber);
|
|
932
|
+
if (index < 0)
|
|
918
933
|
return;
|
|
934
|
+
const note = channel.scheduledNotes[index];
|
|
919
935
|
note.ending = true;
|
|
936
|
+
this.setNoteIndex(channel, index);
|
|
920
937
|
this.releaseNote(channel, note, endTime);
|
|
921
938
|
}
|
|
922
|
-
|
|
939
|
+
setNoteIndex(channel, index) {
|
|
940
|
+
let allEnds = true;
|
|
941
|
+
for (let i = channel.scheduleIndex; i < index; i++) {
|
|
942
|
+
const note = channel.scheduledNotes[i];
|
|
943
|
+
if (note && !note.ending) {
|
|
944
|
+
allEnds = false;
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (allEnds)
|
|
949
|
+
channel.scheduleIndex = index + 1;
|
|
950
|
+
}
|
|
951
|
+
findNoteOffIndex(channel, noteNumber) {
|
|
923
952
|
const scheduledNotes = channel.scheduledNotes;
|
|
924
|
-
for (let i =
|
|
953
|
+
for (let i = channel.scheduleIndex; i < scheduledNotes.length; i++) {
|
|
925
954
|
const note = scheduledNotes[i];
|
|
926
955
|
if (!note)
|
|
927
956
|
continue;
|
|
@@ -929,8 +958,9 @@ export class MidyGM1 {
|
|
|
929
958
|
continue;
|
|
930
959
|
if (note.noteNumber !== noteNumber)
|
|
931
960
|
continue;
|
|
932
|
-
return
|
|
961
|
+
return i;
|
|
933
962
|
}
|
|
963
|
+
return -1;
|
|
934
964
|
}
|
|
935
965
|
noteOff(channelNumber, noteNumber, velocity, scheduleTime) {
|
|
936
966
|
scheduleTime ??= this.audioContext.currentTime;
|
|
@@ -956,16 +986,16 @@ export class MidyGM1 {
|
|
|
956
986
|
case 0x90:
|
|
957
987
|
return this.noteOn(channelNumber, data1, data2, scheduleTime);
|
|
958
988
|
case 0xB0:
|
|
959
|
-
return this.
|
|
989
|
+
return this.setControlChange(channelNumber, data1, data2, scheduleTime);
|
|
960
990
|
case 0xC0:
|
|
961
|
-
return this.
|
|
991
|
+
return this.setProgramChange(channelNumber, data1, scheduleTime);
|
|
962
992
|
case 0xE0:
|
|
963
993
|
return this.handlePitchBendMessage(channelNumber, data1, data2, scheduleTime);
|
|
964
994
|
default:
|
|
965
995
|
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
966
996
|
}
|
|
967
997
|
}
|
|
968
|
-
|
|
998
|
+
setProgramChange(channelNumber, programNumber, _scheduleTime) {
|
|
969
999
|
const channel = this.channels[channelNumber];
|
|
970
1000
|
channel.programNumber = programNumber;
|
|
971
1001
|
}
|
|
@@ -985,13 +1015,17 @@ export class MidyGM1 {
|
|
|
985
1015
|
this.applyVoiceParams(channel, 14, scheduleTime);
|
|
986
1016
|
}
|
|
987
1017
|
setModLfoToPitch(channel, note, scheduleTime) {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
channel.state.modulationDepth;
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1018
|
+
if (note.modulationDepth) {
|
|
1019
|
+
const modLfoToPitch = note.voiceParams.modLfoToPitch;
|
|
1020
|
+
const baseDepth = Math.abs(modLfoToPitch) + channel.state.modulationDepth;
|
|
1021
|
+
const modulationDepth = baseDepth * Math.sign(modLfoToPitch);
|
|
1022
|
+
note.modulationDepth.gain
|
|
1023
|
+
.cancelScheduledValues(scheduleTime)
|
|
1024
|
+
.setValueAtTime(modulationDepth, scheduleTime);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
this.startModulation(channel, note, scheduleTime);
|
|
1028
|
+
}
|
|
995
1029
|
}
|
|
996
1030
|
setModLfoToFilterFc(note, scheduleTime) {
|
|
997
1031
|
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
|
|
@@ -1055,7 +1089,7 @@ export class MidyGM1 {
|
|
|
1055
1089
|
return state;
|
|
1056
1090
|
}
|
|
1057
1091
|
applyVoiceParams(channel, controllerType, scheduleTime) {
|
|
1058
|
-
this.processScheduledNotes(channel,
|
|
1092
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1059
1093
|
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
1060
1094
|
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
1061
1095
|
let applyVolumeEnvelope = false;
|
|
@@ -1099,9 +1133,10 @@ export class MidyGM1 {
|
|
|
1099
1133
|
handlers[101] = this.setRPNMSB;
|
|
1100
1134
|
handlers[120] = this.allSoundOff;
|
|
1101
1135
|
handlers[121] = this.resetAllControllers;
|
|
1136
|
+
handlers[123] = this.allNotesOff;
|
|
1102
1137
|
return handlers;
|
|
1103
1138
|
}
|
|
1104
|
-
|
|
1139
|
+
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
1105
1140
|
const handler = this.controlChangeHandlers[controllerType];
|
|
1106
1141
|
if (handler) {
|
|
1107
1142
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
@@ -1114,12 +1149,11 @@ export class MidyGM1 {
|
|
|
1114
1149
|
}
|
|
1115
1150
|
updateModulation(channel, scheduleTime) {
|
|
1116
1151
|
const depth = channel.state.modulationDepth * channel.modulationDepthRange;
|
|
1117
|
-
this.processScheduledNotes(channel,
|
|
1152
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1118
1153
|
if (note.modulationDepth) {
|
|
1119
1154
|
note.modulationDepth.gain.setValueAtTime(depth, scheduleTime);
|
|
1120
1155
|
}
|
|
1121
1156
|
else {
|
|
1122
|
-
this.setPitchEnvelope(note, scheduleTime);
|
|
1123
1157
|
this.startModulation(channel, note, scheduleTime);
|
|
1124
1158
|
}
|
|
1125
1159
|
});
|
|
@@ -1175,7 +1209,7 @@ export class MidyGM1 {
|
|
|
1175
1209
|
scheduleTime ??= this.audioContext.currentTime;
|
|
1176
1210
|
channel.state.sustainPedal = value / 127;
|
|
1177
1211
|
if (64 <= value) {
|
|
1178
|
-
this.processScheduledNotes(channel,
|
|
1212
|
+
this.processScheduledNotes(channel, (note) => {
|
|
1179
1213
|
channel.sustainNotes.push(note);
|
|
1180
1214
|
});
|
|
1181
1215
|
}
|
|
@@ -1294,7 +1328,7 @@ export class MidyGM1 {
|
|
|
1294
1328
|
const entries = Object.entries(defaultControllerState);
|
|
1295
1329
|
for (const [key, { type, defaultValue }] of entries) {
|
|
1296
1330
|
if (128 <= type) {
|
|
1297
|
-
this.
|
|
1331
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1298
1332
|
}
|
|
1299
1333
|
else {
|
|
1300
1334
|
state[key] = defaultValue;
|
|
@@ -1319,7 +1353,7 @@ export class MidyGM1 {
|
|
|
1319
1353
|
const key = keys[i];
|
|
1320
1354
|
const { type, defaultValue } = defaultControllerState[key];
|
|
1321
1355
|
if (128 <= type) {
|
|
1322
|
-
this.
|
|
1356
|
+
this.setControlChange(channelNumber, type - 128, Math.ceil(defaultValue * 127), scheduleTime);
|
|
1323
1357
|
}
|
|
1324
1358
|
else {
|
|
1325
1359
|
state[key] = defaultValue;
|