@marmooo/midy 0.0.1
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/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts +128 -0
- package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +1 -0
- package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.js +155 -0
- package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts +84 -0
- package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts.map +1 -0
- package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js +216 -0
- package/esm/midy-GM1.d.ts +187 -0
- package/esm/midy-GM1.d.ts.map +1 -0
- package/esm/midy-GM1.js +966 -0
- package/esm/midy-GM2.d.ts +280 -0
- package/esm/midy-GM2.d.ts.map +1 -0
- package/esm/midy-GM2.js +1303 -0
- package/esm/midy-GMLite.d.ts +181 -0
- package/esm/midy-GMLite.d.ts.map +1 -0
- package/esm/midy-GMLite.js +957 -0
- package/esm/midy.d.ts +285 -0
- package/esm/midy.d.ts.map +1 -0
- package/esm/midy.js +1372 -0
- package/esm/mod.d.ts +5 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +4 -0
- package/esm/package.json +3 -0
- package/package.json +29 -0
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts +128 -0
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +1 -0
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.js +162 -0
- package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts +84 -0
- package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts.map +1 -0
- package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js +221 -0
- package/script/midy-GM1.d.ts +187 -0
- package/script/midy-GM1.d.ts.map +1 -0
- package/script/midy-GM1.js +970 -0
- package/script/midy-GM2.d.ts +280 -0
- package/script/midy-GM2.d.ts.map +1 -0
- package/script/midy-GM2.js +1307 -0
- package/script/midy-GMLite.d.ts +181 -0
- package/script/midy-GMLite.d.ts.map +1 -0
- package/script/midy-GMLite.js +961 -0
- package/script/midy.d.ts +285 -0
- package/script/midy.d.ts.map +1 -0
- package/script/midy.js +1376 -0
- package/script/mod.d.ts +5 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +11 -0
- package/script/package.json +3 -0
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MidyGMLite = void 0;
|
|
4
|
+
const _esm_js_1 = require("./deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js");
|
|
5
|
+
const _esm_js_2 = require("./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.js");
|
|
6
|
+
class MidyGMLite {
|
|
7
|
+
constructor(audioContext) {
|
|
8
|
+
Object.defineProperty(this, "ticksPerBeat", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: 120
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "secondsPerBeat", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: 0.5
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(this, "totalTime", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: 0
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(this, "noteCheckInterval", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
value: 0.1
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(this, "lookAhead", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: 1
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(this, "startDelay", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: 0.1
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(this, "startTime", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: 0
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(this, "resumeTime", {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
writable: true,
|
|
54
|
+
value: 0
|
|
55
|
+
});
|
|
56
|
+
Object.defineProperty(this, "soundFonts", {
|
|
57
|
+
enumerable: true,
|
|
58
|
+
configurable: true,
|
|
59
|
+
writable: true,
|
|
60
|
+
value: []
|
|
61
|
+
});
|
|
62
|
+
Object.defineProperty(this, "soundFontTable", {
|
|
63
|
+
enumerable: true,
|
|
64
|
+
configurable: true,
|
|
65
|
+
writable: true,
|
|
66
|
+
value: this.initSoundFontTable()
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(this, "isPlaying", {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
configurable: true,
|
|
71
|
+
writable: true,
|
|
72
|
+
value: false
|
|
73
|
+
});
|
|
74
|
+
Object.defineProperty(this, "isPausing", {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
configurable: true,
|
|
77
|
+
writable: true,
|
|
78
|
+
value: false
|
|
79
|
+
});
|
|
80
|
+
Object.defineProperty(this, "isPaused", {
|
|
81
|
+
enumerable: true,
|
|
82
|
+
configurable: true,
|
|
83
|
+
writable: true,
|
|
84
|
+
value: false
|
|
85
|
+
});
|
|
86
|
+
Object.defineProperty(this, "isStopping", {
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
writable: true,
|
|
90
|
+
value: false
|
|
91
|
+
});
|
|
92
|
+
Object.defineProperty(this, "isSeeking", {
|
|
93
|
+
enumerable: true,
|
|
94
|
+
configurable: true,
|
|
95
|
+
writable: true,
|
|
96
|
+
value: false
|
|
97
|
+
});
|
|
98
|
+
Object.defineProperty(this, "timeline", {
|
|
99
|
+
enumerable: true,
|
|
100
|
+
configurable: true,
|
|
101
|
+
writable: true,
|
|
102
|
+
value: []
|
|
103
|
+
});
|
|
104
|
+
Object.defineProperty(this, "instruments", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
writable: true,
|
|
108
|
+
value: []
|
|
109
|
+
});
|
|
110
|
+
Object.defineProperty(this, "notePromises", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: []
|
|
115
|
+
});
|
|
116
|
+
this.audioContext = audioContext;
|
|
117
|
+
this.masterGain = new GainNode(audioContext);
|
|
118
|
+
this.masterGain.connect(audioContext.destination);
|
|
119
|
+
this.channels = this.createChannels(audioContext);
|
|
120
|
+
this.GM1SystemOn();
|
|
121
|
+
}
|
|
122
|
+
initSoundFontTable() {
|
|
123
|
+
const table = new Array(128);
|
|
124
|
+
for (let i = 0; i < 128; i++) {
|
|
125
|
+
table[i] = new Map();
|
|
126
|
+
}
|
|
127
|
+
return table;
|
|
128
|
+
}
|
|
129
|
+
addSoundFont(soundFont) {
|
|
130
|
+
const index = this.soundFonts.length;
|
|
131
|
+
this.soundFonts.push(soundFont);
|
|
132
|
+
soundFont.parsed.presetHeaders.forEach((presetHeader) => {
|
|
133
|
+
if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
|
|
134
|
+
const banks = this.soundFontTable[presetHeader.preset];
|
|
135
|
+
banks.set(presetHeader.bank, index);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async loadSoundFont(soundFontUrl) {
|
|
140
|
+
const response = await fetch(soundFontUrl);
|
|
141
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
142
|
+
const parsed = (0, _esm_js_2.parse)(new Uint8Array(arrayBuffer));
|
|
143
|
+
const soundFont = new _esm_js_2.SoundFont(parsed);
|
|
144
|
+
this.addSoundFont(soundFont);
|
|
145
|
+
}
|
|
146
|
+
async loadMIDI(midiUrl) {
|
|
147
|
+
const response = await fetch(midiUrl);
|
|
148
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
149
|
+
const midi = (0, _esm_js_1.parseMidi)(new Uint8Array(arrayBuffer));
|
|
150
|
+
const midiData = this.extractMidiData(midi);
|
|
151
|
+
this.instruments = midiData.instruments;
|
|
152
|
+
this.timeline = midiData.timeline;
|
|
153
|
+
this.ticksPerBeat = midi.header.ticksPerBeat;
|
|
154
|
+
this.totalTime = this.calcTotalTime();
|
|
155
|
+
}
|
|
156
|
+
setChannelAudioNodes(audioContext) {
|
|
157
|
+
const gainNode = new GainNode(audioContext, {
|
|
158
|
+
gain: MidyGMLite.channelSettings.volume,
|
|
159
|
+
});
|
|
160
|
+
const pannerNode = new StereoPannerNode(audioContext, {
|
|
161
|
+
pan: MidyGMLite.channelSettings.pan,
|
|
162
|
+
});
|
|
163
|
+
const modulationEffect = this.createModulationEffect(audioContext);
|
|
164
|
+
modulationEffect.lfo.start();
|
|
165
|
+
pannerNode.connect(gainNode);
|
|
166
|
+
gainNode.connect(this.masterGain);
|
|
167
|
+
return {
|
|
168
|
+
gainNode,
|
|
169
|
+
pannerNode,
|
|
170
|
+
modulationEffect,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
createChannels(audioContext) {
|
|
174
|
+
const channels = Array.from({ length: 16 }, () => {
|
|
175
|
+
return {
|
|
176
|
+
...MidyGMLite.channelSettings,
|
|
177
|
+
...MidyGMLite.effectSettings,
|
|
178
|
+
...this.setChannelAudioNodes(audioContext),
|
|
179
|
+
scheduledNotes: new Map(),
|
|
180
|
+
sostenutoNotes: new Map(),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
return channels;
|
|
184
|
+
}
|
|
185
|
+
async createNoteBuffer(noteInfo, isSF3) {
|
|
186
|
+
const sampleEnd = noteInfo.sample.length + noteInfo.end;
|
|
187
|
+
if (isSF3) {
|
|
188
|
+
const sample = new Uint8Array(noteInfo.sample.length);
|
|
189
|
+
sample.set(noteInfo.sample);
|
|
190
|
+
const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
|
|
191
|
+
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
|
|
192
|
+
const channelData = audioBuffer.getChannelData(channel);
|
|
193
|
+
channelData.set(channelData.subarray(0, sampleEnd));
|
|
194
|
+
}
|
|
195
|
+
return audioBuffer;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const sample = noteInfo.sample.subarray(0, sampleEnd);
|
|
199
|
+
const floatSample = this.convertToFloat32Array(sample);
|
|
200
|
+
const audioBuffer = new AudioBuffer({
|
|
201
|
+
numberOfChannels: 1,
|
|
202
|
+
length: sample.length,
|
|
203
|
+
sampleRate: noteInfo.sampleRate,
|
|
204
|
+
});
|
|
205
|
+
const channelData = audioBuffer.getChannelData(0);
|
|
206
|
+
channelData.set(floatSample);
|
|
207
|
+
return audioBuffer;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async createNoteBufferNode(noteInfo, isSF3) {
|
|
211
|
+
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
212
|
+
const audioBuffer = await this.createNoteBuffer(noteInfo, isSF3);
|
|
213
|
+
bufferSource.buffer = audioBuffer;
|
|
214
|
+
bufferSource.loop = noteInfo.sampleModes % 2 !== 0;
|
|
215
|
+
if (bufferSource.loop) {
|
|
216
|
+
bufferSource.loopStart = noteInfo.loopStart / noteInfo.sampleRate;
|
|
217
|
+
bufferSource.loopEnd = noteInfo.loopEnd / noteInfo.sampleRate;
|
|
218
|
+
}
|
|
219
|
+
return bufferSource;
|
|
220
|
+
}
|
|
221
|
+
convertToFloat32Array(uint8Array) {
|
|
222
|
+
const int16Array = new Int16Array(uint8Array.buffer);
|
|
223
|
+
const float32Array = new Float32Array(int16Array.length);
|
|
224
|
+
for (let i = 0; i < int16Array.length; i++) {
|
|
225
|
+
float32Array[i] = int16Array[i] / 32768;
|
|
226
|
+
}
|
|
227
|
+
return float32Array;
|
|
228
|
+
}
|
|
229
|
+
async scheduleTimelineEvents(t, offset, queueIndex) {
|
|
230
|
+
while (queueIndex < this.timeline.length) {
|
|
231
|
+
const event = this.timeline[queueIndex];
|
|
232
|
+
const time = this.ticksToSecond(event.ticks, this.secondsPerBeat);
|
|
233
|
+
if (time > t + this.lookAhead)
|
|
234
|
+
break;
|
|
235
|
+
switch (event.type) {
|
|
236
|
+
case "controller":
|
|
237
|
+
this.handleControlChange(event.channel, event.controllerType, event.value);
|
|
238
|
+
break;
|
|
239
|
+
case "noteOn":
|
|
240
|
+
await this.scheduleNoteOn(event.channel, event.noteNumber, event.velocity, time + this.startDelay - offset);
|
|
241
|
+
break;
|
|
242
|
+
case "noteOff": {
|
|
243
|
+
const notePromise = this.scheduleNoteRelease(event.channel, event.noteNumber, event.velocity, time + this.startDelay - offset);
|
|
244
|
+
if (notePromise) {
|
|
245
|
+
this.notePromises.push(notePromise);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case "programChange":
|
|
250
|
+
this.handleProgramChange(event.channel, event.programNumber);
|
|
251
|
+
break;
|
|
252
|
+
case "setTempo":
|
|
253
|
+
this.secondsPerBeat = event.microsecondsPerBeat / 1000000;
|
|
254
|
+
break;
|
|
255
|
+
case "sysEx":
|
|
256
|
+
this.handleSysEx(event.data);
|
|
257
|
+
}
|
|
258
|
+
queueIndex++;
|
|
259
|
+
}
|
|
260
|
+
return queueIndex;
|
|
261
|
+
}
|
|
262
|
+
getQueueIndex(second) {
|
|
263
|
+
const ticks = this.secondToTicks(second, this.secondsPerBeat);
|
|
264
|
+
for (let i = 0; i < this.timeline.length; i++) {
|
|
265
|
+
if (ticks <= this.timeline[i].ticks) {
|
|
266
|
+
return i;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
playNotes() {
|
|
272
|
+
return new Promise((resolve) => {
|
|
273
|
+
this.isPlaying = true;
|
|
274
|
+
this.isPaused = false;
|
|
275
|
+
this.startTime = this.audioContext.currentTime;
|
|
276
|
+
let queueIndex = this.getQueueIndex(this.resumeTime);
|
|
277
|
+
let offset = this.resumeTime - this.startTime;
|
|
278
|
+
this.notePromises = [];
|
|
279
|
+
const schedulePlayback = async () => {
|
|
280
|
+
if (queueIndex >= this.timeline.length) {
|
|
281
|
+
await Promise.all(this.notePromises);
|
|
282
|
+
this.notePromises = [];
|
|
283
|
+
resolve();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const t = this.audioContext.currentTime + offset;
|
|
287
|
+
queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
|
|
288
|
+
if (this.isPausing) {
|
|
289
|
+
await this.stopNotes();
|
|
290
|
+
this.notePromises = [];
|
|
291
|
+
resolve();
|
|
292
|
+
this.isPausing = false;
|
|
293
|
+
this.isPaused = true;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
else if (this.isStopping) {
|
|
297
|
+
await this.stopNotes();
|
|
298
|
+
this.notePromises = [];
|
|
299
|
+
resolve();
|
|
300
|
+
this.isStopping = false;
|
|
301
|
+
this.isPaused = false;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
else if (this.isSeeking) {
|
|
305
|
+
this.stopNotes();
|
|
306
|
+
this.startTime = this.audioContext.currentTime;
|
|
307
|
+
queueIndex = this.getQueueIndex(this.resumeTime);
|
|
308
|
+
offset = this.resumeTime - this.startTime;
|
|
309
|
+
this.isSeeking = false;
|
|
310
|
+
await schedulePlayback();
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
const now = this.audioContext.currentTime;
|
|
314
|
+
const waitTime = now + this.noteCheckInterval;
|
|
315
|
+
await this.scheduleTask(() => { }, waitTime);
|
|
316
|
+
await schedulePlayback();
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
schedulePlayback();
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
ticksToSecond(ticks, secondsPerBeat) {
|
|
323
|
+
return ticks * secondsPerBeat / this.ticksPerBeat;
|
|
324
|
+
}
|
|
325
|
+
secondToTicks(second, secondsPerBeat) {
|
|
326
|
+
return second * this.ticksPerBeat / secondsPerBeat;
|
|
327
|
+
}
|
|
328
|
+
extractMidiData(midi) {
|
|
329
|
+
const instruments = new Set();
|
|
330
|
+
const timeline = [];
|
|
331
|
+
const tmpChannels = new Array(16);
|
|
332
|
+
for (let i = 0; i < tmpChannels.length; i++) {
|
|
333
|
+
tmpChannels[i] = {
|
|
334
|
+
durationTicks: new Map(),
|
|
335
|
+
programNumber: -1,
|
|
336
|
+
bank: this.channels[i].bank,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
midi.tracks.forEach((track) => {
|
|
340
|
+
let currentTicks = 0;
|
|
341
|
+
track.forEach((event) => {
|
|
342
|
+
currentTicks += event.deltaTime;
|
|
343
|
+
event.ticks = currentTicks;
|
|
344
|
+
switch (event.type) {
|
|
345
|
+
case "noteOn": {
|
|
346
|
+
const channel = tmpChannels[event.channel];
|
|
347
|
+
if (channel.programNumber < 0) {
|
|
348
|
+
instruments.add(`${channel.bank}:0`);
|
|
349
|
+
channel.programNumber = 0;
|
|
350
|
+
}
|
|
351
|
+
channel.durationTicks.set(event.noteNumber, {
|
|
352
|
+
ticks: event.ticks,
|
|
353
|
+
noteOn: event,
|
|
354
|
+
});
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case "noteOff": {
|
|
358
|
+
const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
|
|
359
|
+
.get(event.noteNumber);
|
|
360
|
+
noteOn.durationTicks = event.ticks - ticks;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case "programChange": {
|
|
364
|
+
const channel = tmpChannels[event.channel];
|
|
365
|
+
channel.programNumber = event.programNumber;
|
|
366
|
+
instruments.add(`${channel.bankNumber}:${channel.programNumber}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
delete event.deltaTime;
|
|
370
|
+
timeline.push(event);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
timeline.sort((a, b) => {
|
|
374
|
+
if (a.ticks !== b.ticks) {
|
|
375
|
+
return a.ticks - b.ticks;
|
|
376
|
+
}
|
|
377
|
+
if (a.type !== "controller" && b.type === "controller") {
|
|
378
|
+
return -1;
|
|
379
|
+
}
|
|
380
|
+
if (a.type === "controller" && b.type !== "controller") {
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
return 0;
|
|
384
|
+
});
|
|
385
|
+
return { instruments, timeline };
|
|
386
|
+
}
|
|
387
|
+
stopNotes() {
|
|
388
|
+
const now = this.audioContext.currentTime;
|
|
389
|
+
const velocity = 0;
|
|
390
|
+
const stopPedal = true;
|
|
391
|
+
this.channels.forEach((channel, channelNumber) => {
|
|
392
|
+
channel.scheduledNotes.forEach((scheduledNotes) => {
|
|
393
|
+
scheduledNotes.forEach((scheduledNote) => {
|
|
394
|
+
if (scheduledNote) {
|
|
395
|
+
const promise = this.scheduleNoteRelease(channelNumber, scheduledNote.noteNumber, velocity, now, stopPedal);
|
|
396
|
+
this.notePromises.push(promise);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
channel.scheduledNotes.clear();
|
|
401
|
+
});
|
|
402
|
+
return Promise.all(this.notePromises);
|
|
403
|
+
}
|
|
404
|
+
async start() {
|
|
405
|
+
if (this.isPlaying || this.isPaused)
|
|
406
|
+
return;
|
|
407
|
+
this.resumeTime = 0;
|
|
408
|
+
await this.playNotes();
|
|
409
|
+
this.isPlaying = false;
|
|
410
|
+
}
|
|
411
|
+
stop() {
|
|
412
|
+
if (!this.isPlaying)
|
|
413
|
+
return;
|
|
414
|
+
this.isStopping = true;
|
|
415
|
+
}
|
|
416
|
+
pause() {
|
|
417
|
+
if (!this.isPlaying || this.isPaused)
|
|
418
|
+
return;
|
|
419
|
+
const now = this.audioContext.currentTime;
|
|
420
|
+
this.resumeTime += now - this.startTime - this.startDelay;
|
|
421
|
+
this.isPausing = true;
|
|
422
|
+
}
|
|
423
|
+
async resume() {
|
|
424
|
+
if (!this.isPaused)
|
|
425
|
+
return;
|
|
426
|
+
await this.playNotes();
|
|
427
|
+
this.isPlaying = false;
|
|
428
|
+
}
|
|
429
|
+
seekTo(second) {
|
|
430
|
+
this.resumeTime = second;
|
|
431
|
+
if (this.isPlaying) {
|
|
432
|
+
this.isSeeking = true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
calcTotalTime() {
|
|
436
|
+
const endOfTracks = [];
|
|
437
|
+
let prevTicks = 0;
|
|
438
|
+
let totalTime = 0;
|
|
439
|
+
let secondsPerBeat = 0.5;
|
|
440
|
+
for (let i = 0; i < this.timeline.length; i++) {
|
|
441
|
+
const event = this.timeline[i];
|
|
442
|
+
switch (event.type) {
|
|
443
|
+
case "setTempo": {
|
|
444
|
+
const durationTicks = event.ticks - prevTicks;
|
|
445
|
+
totalTime += this.ticksToSecond(durationTicks, secondsPerBeat);
|
|
446
|
+
secondsPerBeat = event.microsecondsPerBeat / 1000000;
|
|
447
|
+
prevTicks = event.ticks;
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case "endOfTrack":
|
|
451
|
+
endOfTracks.push(event);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
let maxTicks = 0;
|
|
455
|
+
for (let i = 0; i < endOfTracks.length; i++) {
|
|
456
|
+
const event = endOfTracks[i];
|
|
457
|
+
if (maxTicks < event.ticks)
|
|
458
|
+
maxTicks = event.ticks;
|
|
459
|
+
}
|
|
460
|
+
const durationTicks = maxTicks - prevTicks;
|
|
461
|
+
totalTime += this.ticksToSecond(durationTicks, secondsPerBeat);
|
|
462
|
+
return totalTime;
|
|
463
|
+
}
|
|
464
|
+
currentTime() {
|
|
465
|
+
const now = this.audioContext.currentTime;
|
|
466
|
+
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
467
|
+
}
|
|
468
|
+
getActiveNotes(channel) {
|
|
469
|
+
const activeNotes = new Map();
|
|
470
|
+
channel.scheduledNotes.forEeach((scheduledNotes) => {
|
|
471
|
+
const activeNote = this.getActiveChannelNotes(scheduledNotes);
|
|
472
|
+
if (activeNote) {
|
|
473
|
+
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
return activeNotes;
|
|
477
|
+
}
|
|
478
|
+
getActiveChannelNotes(scheduledNotes) {
|
|
479
|
+
for (let i = 0; i < scheduledNotes; i++) {
|
|
480
|
+
const scheduledNote = scheduledNotes[i];
|
|
481
|
+
if (scheduledNote)
|
|
482
|
+
return scheduledNote;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
createModulationEffect(audioContext) {
|
|
486
|
+
const lfo = new OscillatorNode(audioContext, {
|
|
487
|
+
frequency: 5,
|
|
488
|
+
});
|
|
489
|
+
const lfoGain = new GainNode(audioContext);
|
|
490
|
+
lfo.connect(lfoGain);
|
|
491
|
+
return {
|
|
492
|
+
lfo,
|
|
493
|
+
lfoGain,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
createReverbEffect(audioContext, options = {}) {
|
|
497
|
+
const { decay = 0.8, preDecay = 0, } = options;
|
|
498
|
+
const sampleRate = audioContext.sampleRate;
|
|
499
|
+
const length = sampleRate * decay;
|
|
500
|
+
const impulse = new AudioBuffer({
|
|
501
|
+
numberOfChannels: 2,
|
|
502
|
+
length,
|
|
503
|
+
sampleRate,
|
|
504
|
+
});
|
|
505
|
+
const preDecayLength = Math.min(sampleRate * preDecay, length);
|
|
506
|
+
for (let channel = 0; channel < impulse.numberOfChannels; channel++) {
|
|
507
|
+
const channelData = impulse.getChannelData(channel);
|
|
508
|
+
for (let i = 0; i < preDecayLength; i++) {
|
|
509
|
+
channelData[i] = Math.random() * 2 - 1;
|
|
510
|
+
}
|
|
511
|
+
for (let i = preDecayLength; i < length; i++) {
|
|
512
|
+
const attenuation = Math.exp(-(i - preDecayLength) / sampleRate / decay);
|
|
513
|
+
channelData[i] = (Math.random() * 2 - 1) * attenuation;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const convolverNode = new ConvolverNode(audioContext, {
|
|
517
|
+
buffer: impulse,
|
|
518
|
+
});
|
|
519
|
+
const dryGain = new GainNode(audioContext);
|
|
520
|
+
const wetGain = new GainNode(audioContext);
|
|
521
|
+
convolverNode.connect(wetGain);
|
|
522
|
+
return {
|
|
523
|
+
convolverNode,
|
|
524
|
+
dryGain,
|
|
525
|
+
wetGain,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
connectNoteEffects(channel, gainNode) {
|
|
529
|
+
gainNode.connect(channel.pannerNode);
|
|
530
|
+
}
|
|
531
|
+
cbToRatio(cb) {
|
|
532
|
+
return Math.pow(10, cb / 200);
|
|
533
|
+
}
|
|
534
|
+
centToHz(cent) {
|
|
535
|
+
return 8.176 * Math.pow(2, cent / 1200);
|
|
536
|
+
}
|
|
537
|
+
async createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3) {
|
|
538
|
+
const semitoneOffset = channel.pitchBend * channel.pitchBendRange;
|
|
539
|
+
const playbackRate = noteInfo.playbackRate(noteNumber) *
|
|
540
|
+
Math.pow(2, semitoneOffset / 12);
|
|
541
|
+
const bufferSource = await this.createNoteBufferNode(noteInfo, isSF3);
|
|
542
|
+
bufferSource.playbackRate.value = playbackRate;
|
|
543
|
+
// volume envelope
|
|
544
|
+
const gainNode = new GainNode(this.audioContext, {
|
|
545
|
+
gain: 0,
|
|
546
|
+
});
|
|
547
|
+
let volume = (velocity / 127) * channel.volume * channel.expression;
|
|
548
|
+
if (volume === 0)
|
|
549
|
+
volume = 1e-6; // exponentialRampToValueAtTime() requirea a non-zero value
|
|
550
|
+
const attackVolume = this.cbToRatio(-noteInfo.initialAttenuation) * volume;
|
|
551
|
+
const sustainVolume = attackVolume * (1 - noteInfo.volSustain);
|
|
552
|
+
const volDelay = startTime + noteInfo.volDelay;
|
|
553
|
+
const volAttack = volDelay + noteInfo.volAttack;
|
|
554
|
+
const volHold = volAttack + noteInfo.volHold;
|
|
555
|
+
const volDecay = volHold + noteInfo.volDecay;
|
|
556
|
+
gainNode.gain
|
|
557
|
+
.setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
558
|
+
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
559
|
+
.setValueAtTime(attackVolume, volHold)
|
|
560
|
+
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
561
|
+
if (channel.modulation > 0) {
|
|
562
|
+
const lfoGain = channel.modulationEffect.lfoGain;
|
|
563
|
+
lfoGain.connect(bufferSource.detune);
|
|
564
|
+
lfoGain.gain.cancelScheduledValues(startTime + channel.vibratoDelay);
|
|
565
|
+
lfoGain.gain.setValueAtTime(channel.modulation, startTime + channel.vibratoDelay);
|
|
566
|
+
}
|
|
567
|
+
// filter envelope
|
|
568
|
+
const baseFreq = this.centToHz(noteInfo.initialFilterFc);
|
|
569
|
+
const peekFreq = this.centToHz(noteInfo.initialFilterFc + noteInfo.modEnvToFilterFc);
|
|
570
|
+
const sustainFreq = baseFreq +
|
|
571
|
+
(peekFreq - baseFreq) * (1 - noteInfo.modSustain);
|
|
572
|
+
const filterNode = new BiquadFilterNode(this.audioContext, {
|
|
573
|
+
type: "lowpass",
|
|
574
|
+
Q: this.cbToRatio(noteInfo.initialFilterQ),
|
|
575
|
+
frequency: baseFreq,
|
|
576
|
+
});
|
|
577
|
+
const modDelay = startTime + noteInfo.modDelay;
|
|
578
|
+
const modAttack = modDelay + noteInfo.modAttack;
|
|
579
|
+
const modHold = modAttack + noteInfo.modHold;
|
|
580
|
+
const modDecay = modHold + noteInfo.modDecay;
|
|
581
|
+
filterNode.frequency
|
|
582
|
+
.setValueAtTime(baseFreq, modDelay)
|
|
583
|
+
.exponentialRampToValueAtTime(peekFreq, modAttack)
|
|
584
|
+
.setValueAtTime(peekFreq, modHold)
|
|
585
|
+
.linearRampToValueAtTime(sustainFreq, modDecay);
|
|
586
|
+
bufferSource.connect(filterNode);
|
|
587
|
+
filterNode.connect(gainNode);
|
|
588
|
+
bufferSource.start(startTime, noteInfo.start / noteInfo.sampleRate);
|
|
589
|
+
return { bufferSource, gainNode, filterNode };
|
|
590
|
+
}
|
|
591
|
+
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
592
|
+
const channel = this.channels[channelNumber];
|
|
593
|
+
const bankNumber = channel.bank;
|
|
594
|
+
const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
|
|
595
|
+
if (soundFontIndex === undefined)
|
|
596
|
+
return;
|
|
597
|
+
const soundFont = this.soundFonts[soundFontIndex];
|
|
598
|
+
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
599
|
+
const noteInfo = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
|
|
600
|
+
if (!noteInfo)
|
|
601
|
+
return;
|
|
602
|
+
const { bufferSource, gainNode, filterNode } = await this
|
|
603
|
+
.createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3);
|
|
604
|
+
this.connectNoteEffects(channel, gainNode);
|
|
605
|
+
const scheduledNotes = channel.scheduledNotes;
|
|
606
|
+
const scheduledNote = {
|
|
607
|
+
gainNode,
|
|
608
|
+
filterNode,
|
|
609
|
+
bufferSource,
|
|
610
|
+
noteNumber,
|
|
611
|
+
noteInfo,
|
|
612
|
+
startTime,
|
|
613
|
+
};
|
|
614
|
+
if (scheduledNotes.has(noteNumber)) {
|
|
615
|
+
scheduledNotes.get(noteNumber).push(scheduledNote);
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
scheduledNotes.set(noteNumber, [scheduledNote]);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
noteOn(channelNumber, noteNumber, velocity) {
|
|
622
|
+
const now = this.audioContext.currentTime;
|
|
623
|
+
return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
|
|
624
|
+
}
|
|
625
|
+
scheduleNoteRelease(channelNumber, noteNumber, velocity, stopTime, stopPedal = false) {
|
|
626
|
+
const channel = this.channels[channelNumber];
|
|
627
|
+
if (stopPedal && channel.sustainPedal)
|
|
628
|
+
return;
|
|
629
|
+
if (!channel.scheduledNotes.has(noteNumber))
|
|
630
|
+
return;
|
|
631
|
+
const targetNotes = channel.scheduledNotes.get(noteNumber);
|
|
632
|
+
for (let i = 0; i < targetNotes.length; i++) {
|
|
633
|
+
const targetNote = targetNotes[i];
|
|
634
|
+
if (!targetNote)
|
|
635
|
+
continue;
|
|
636
|
+
if (targetNote.ending)
|
|
637
|
+
continue;
|
|
638
|
+
const { bufferSource, filterNode, gainNode, noteInfo } = targetNote;
|
|
639
|
+
const velocityRate = (velocity + 127) / 127;
|
|
640
|
+
const volEndTime = stopTime + noteInfo.volRelease * velocityRate;
|
|
641
|
+
gainNode.gain.cancelScheduledValues(stopTime);
|
|
642
|
+
gainNode.gain.linearRampToValueAtTime(0, volEndTime);
|
|
643
|
+
const baseFreq = this.centToHz(noteInfo.initialFilterFc);
|
|
644
|
+
const modEndTime = stopTime + noteInfo.modRelease * velocityRate;
|
|
645
|
+
filterNode.frequency.cancelScheduledValues(stopTime);
|
|
646
|
+
filterNode.frequency.linearRampToValueAtTime(baseFreq, modEndTime);
|
|
647
|
+
targetNote.ending = true;
|
|
648
|
+
this.scheduleTask(() => {
|
|
649
|
+
bufferSource.loop = false;
|
|
650
|
+
}, stopTime);
|
|
651
|
+
return new Promise((resolve) => {
|
|
652
|
+
bufferSource.onended = () => {
|
|
653
|
+
targetNotes[i] = null;
|
|
654
|
+
bufferSource.disconnect(0);
|
|
655
|
+
filterNode.disconnect(0);
|
|
656
|
+
gainNode.disconnect(0);
|
|
657
|
+
resolve();
|
|
658
|
+
};
|
|
659
|
+
bufferSource.stop(volEndTime);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
releaseNote(channelNumber, noteNumber, velocity) {
|
|
664
|
+
const now = this.audioContext.currentTime;
|
|
665
|
+
return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
|
|
666
|
+
}
|
|
667
|
+
releaseSustainPedal(channelNumber) {
|
|
668
|
+
const now = this.audioContext.currentTime;
|
|
669
|
+
const channel = this.channels[channelNumber];
|
|
670
|
+
channel.sustainPedal = false;
|
|
671
|
+
channel.scheduledNotes.forEach((scheduledNotes) => {
|
|
672
|
+
scheduledNotes.forEach((scheduledNote) => {
|
|
673
|
+
if (scheduledNote) {
|
|
674
|
+
const { gainNode, bufferSource, noteInfo } = scheduledNote;
|
|
675
|
+
const volEndTime = now + noteInfo.volRelease;
|
|
676
|
+
gainNode.gain.cancelScheduledValues(now);
|
|
677
|
+
gainNode.gain.linearRampToValueAtTime(0, volEndTime);
|
|
678
|
+
const baseFreq = this.centToHz(noteInfo.initialFilterFc);
|
|
679
|
+
const modEndTime = stopTime + noteInfo.modRelease;
|
|
680
|
+
filterNode.frequency.cancelScheduledValues(stopTime);
|
|
681
|
+
filterNode.frequency.linearRampToValueAtTime(baseFreq, modEndTime);
|
|
682
|
+
bufferSource.stop(volEndTime);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
handleMIDIMessage(statusByte, data1, data2) {
|
|
688
|
+
const channelNumber = statusByte & 0x0F;
|
|
689
|
+
const messageType = statusByte & 0xF0;
|
|
690
|
+
switch (messageType) {
|
|
691
|
+
case 0x80:
|
|
692
|
+
return this.releaseNote(channelNumber, data1, data2);
|
|
693
|
+
case 0x90:
|
|
694
|
+
return this.noteOn(channelNumber, data1, data2);
|
|
695
|
+
case 0xA0:
|
|
696
|
+
return this.handlePolyphonicKeyPressure(channelNumber, data1, data2);
|
|
697
|
+
case 0xB0:
|
|
698
|
+
return this.handleControlChange(channelNumber, data1, data2);
|
|
699
|
+
case 0xC0:
|
|
700
|
+
return this.handleProgramChange(channelNumber, data1);
|
|
701
|
+
case 0xD0:
|
|
702
|
+
return this.handleChannelPressure(channelNumber, data1);
|
|
703
|
+
case 0xE0:
|
|
704
|
+
return this.handlePitchBend(channelNumber, data1, data2);
|
|
705
|
+
default:
|
|
706
|
+
console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure) {
|
|
710
|
+
const now = this.audioContext.currentTime;
|
|
711
|
+
const channel = this.channels[channelNumber];
|
|
712
|
+
const scheduledNotes = channel.scheduledNotes.get(noteNumber);
|
|
713
|
+
pressure /= 127;
|
|
714
|
+
if (scheduledNotes) {
|
|
715
|
+
scheduledNotes.forEach((scheduledNote) => {
|
|
716
|
+
if (scheduledNote) {
|
|
717
|
+
const { initialAttenuation } = scheduledNote.noteInfo;
|
|
718
|
+
const gain = this.cbToRatio(initialAttenuation) * pressure;
|
|
719
|
+
scheduledNote.gainNode.gain.cancelScheduledValues(now);
|
|
720
|
+
scheduledNote.gainNode.gain.setValueAtTime(gain, now);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
handleProgramChange(channelNumber, program) {
|
|
726
|
+
const channel = this.channels[channelNumber];
|
|
727
|
+
channel.program = program;
|
|
728
|
+
}
|
|
729
|
+
handleChannelPressure(channelNumber, pressure) {
|
|
730
|
+
this.channels[channelNumber].channelPressure = pressure;
|
|
731
|
+
}
|
|
732
|
+
handlePitchBend(channelNumber, lsb, msb) {
|
|
733
|
+
const pitchBend = (msb * 128 + lsb - 8192) / 8192;
|
|
734
|
+
this.channels[channelNumber].pitchBend = pitchBend;
|
|
735
|
+
}
|
|
736
|
+
handleControlChange(channelNumber, controller, value) {
|
|
737
|
+
switch (controller) {
|
|
738
|
+
case 1:
|
|
739
|
+
return this.setModulation(channelNumber, value);
|
|
740
|
+
case 6:
|
|
741
|
+
return this.setDataEntry(channelNumber, value, true);
|
|
742
|
+
case 7:
|
|
743
|
+
return this.setVolume(channelNumber, value);
|
|
744
|
+
case 10:
|
|
745
|
+
return this.setPan(channelNumber, value);
|
|
746
|
+
case 11:
|
|
747
|
+
return this.setExpression(channelNumber, value);
|
|
748
|
+
case 38:
|
|
749
|
+
return this.setDataEntry(channelNumber, value, false);
|
|
750
|
+
case 64:
|
|
751
|
+
return this.setSustainPedal(channelNumber, value);
|
|
752
|
+
case 100:
|
|
753
|
+
return this.setRPNMSB(channelNumber, value);
|
|
754
|
+
case 101:
|
|
755
|
+
return this.setRPNLSB(channelNumber, value);
|
|
756
|
+
case 120:
|
|
757
|
+
return this.allSoundOff(channelNumber);
|
|
758
|
+
case 121:
|
|
759
|
+
return this.resetAllControllers(channelNumber);
|
|
760
|
+
case 123:
|
|
761
|
+
return this.allNotesOff(channelNumber);
|
|
762
|
+
default:
|
|
763
|
+
console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
setModulation(channelNumber, modulation) {
|
|
767
|
+
const now = this.audioContext.currentTime;
|
|
768
|
+
const channel = this.channels[channelNumber];
|
|
769
|
+
channel.modulation = (modulation * 100 / 127) *
|
|
770
|
+
channel.modulationDepthRange;
|
|
771
|
+
const lfoGain = channel.modulationEffect.lfoGain;
|
|
772
|
+
lfoGain.gain.cancelScheduledValues(now);
|
|
773
|
+
lfoGain.gain.setValueAtTime(channel.modulation, now);
|
|
774
|
+
}
|
|
775
|
+
setVolume(channelNumber, volume) {
|
|
776
|
+
const channel = this.channels[channelNumber];
|
|
777
|
+
channel.volume = volume / 127;
|
|
778
|
+
this.updateChannelGain(channel);
|
|
779
|
+
}
|
|
780
|
+
setPan(channelNumber, pan) {
|
|
781
|
+
const now = this.audioContext.currentTime;
|
|
782
|
+
const channel = this.channels[channelNumber];
|
|
783
|
+
channel.pan = pan / 127 * 2 - 1; // -1 (left) - +1 (right)
|
|
784
|
+
channel.pannerNode.pan.cancelScheduledValues(now);
|
|
785
|
+
channel.pannerNode.pan.setValueAtTime(channel.pan, now);
|
|
786
|
+
}
|
|
787
|
+
setExpression(channelNumber, expression) {
|
|
788
|
+
const channel = this.channels[channelNumber];
|
|
789
|
+
channel.expression = expression / 127;
|
|
790
|
+
this.updateChannelGain(channel);
|
|
791
|
+
}
|
|
792
|
+
updateChannelGain(channel) {
|
|
793
|
+
const now = this.audioContext.currentTime;
|
|
794
|
+
const volume = channel.volume * channel.expression;
|
|
795
|
+
channel.gainNode.gain.cancelScheduledValues(now);
|
|
796
|
+
channel.gainNode.gain.setValueAtTime(volume, now);
|
|
797
|
+
}
|
|
798
|
+
setSustainPedal(channelNumber, value) {
|
|
799
|
+
this.channels[channelNumber].sustainPedal = value >= 64;
|
|
800
|
+
if (!isOn) {
|
|
801
|
+
this.releaseSustainPedal(channelNumber);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
setRPNMSB(channelNumber, value) {
|
|
805
|
+
this.channels[channelNumber].rpnMSB = value;
|
|
806
|
+
}
|
|
807
|
+
setRPNLSB(channelNumber, value) {
|
|
808
|
+
this.channels[channelNumber].rpnLSB = value;
|
|
809
|
+
}
|
|
810
|
+
setDataEntry(channelNumber, value, isMSB) {
|
|
811
|
+
const channel = this.channels[channelNumber];
|
|
812
|
+
const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
|
|
813
|
+
isMSB ? channel.dataMSB = value : channel.dataLSB = value;
|
|
814
|
+
const { dataMSB, dataLSB } = channel;
|
|
815
|
+
switch (rpn) {
|
|
816
|
+
case 0:
|
|
817
|
+
channel.pitchBendRange = dataMSB + dataLSB / 100;
|
|
818
|
+
break;
|
|
819
|
+
default:
|
|
820
|
+
console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
allSoundOff(channelNumber) {
|
|
824
|
+
const now = this.audioContext.currentTime;
|
|
825
|
+
const channel = this.channels[channelNumber];
|
|
826
|
+
const velocity = 0;
|
|
827
|
+
const stopPedal = true;
|
|
828
|
+
const promises = [];
|
|
829
|
+
channel.scheduledNotes.forEach((scheduledNotes) => {
|
|
830
|
+
const activeNote = this.getActiveChannelNotes(scheduledNotes);
|
|
831
|
+
if (activeNote) {
|
|
832
|
+
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
833
|
+
promises.push(notePromise);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
return promises;
|
|
837
|
+
}
|
|
838
|
+
resetAllControllers(channelNumber) {
|
|
839
|
+
Object.assign(this.channels[channelNumber], this.effectSettings);
|
|
840
|
+
}
|
|
841
|
+
allNotesOff(channelNumber) {
|
|
842
|
+
const now = this.audioContext.currentTime;
|
|
843
|
+
const channel = this.channels[channelNumber];
|
|
844
|
+
const velocity = 0;
|
|
845
|
+
const stopPedal = false;
|
|
846
|
+
const promises = [];
|
|
847
|
+
channel.scheduledNotes.forEach((scheduledNotes) => {
|
|
848
|
+
const activeNote = this.getActiveChannelNotes(scheduledNotes);
|
|
849
|
+
if (activeNote) {
|
|
850
|
+
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
851
|
+
promises.push(notePromise);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
return promises;
|
|
855
|
+
}
|
|
856
|
+
handleUniversalNonRealTimeExclusiveMessage(data) {
|
|
857
|
+
switch (data[2]) {
|
|
858
|
+
case 9:
|
|
859
|
+
switch (data[3]) {
|
|
860
|
+
case 1:
|
|
861
|
+
this.GM1SystemOn();
|
|
862
|
+
break;
|
|
863
|
+
case 2: // GM System Off
|
|
864
|
+
break;
|
|
865
|
+
default:
|
|
866
|
+
console.warn(`Unsupported Exclusive Message ${data}`);
|
|
867
|
+
}
|
|
868
|
+
break;
|
|
869
|
+
default:
|
|
870
|
+
console.warn(`Unsupported Exclusive Message ${data}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
GM1SystemOn() {
|
|
874
|
+
this.channels.forEach((channel) => {
|
|
875
|
+
channel.bankMSB = 0;
|
|
876
|
+
channel.bankLSB = 0;
|
|
877
|
+
channel.bank = 0;
|
|
878
|
+
});
|
|
879
|
+
this.channels[9].bankMSB = 1;
|
|
880
|
+
this.channels[9].bank = 128;
|
|
881
|
+
}
|
|
882
|
+
handleUniversalRealTimeExclusiveMessage(data) {
|
|
883
|
+
switch (data[2]) {
|
|
884
|
+
case 4:
|
|
885
|
+
switch (data[3]) {
|
|
886
|
+
case 1:
|
|
887
|
+
return this.handleMasterVolumeSysEx(data);
|
|
888
|
+
default:
|
|
889
|
+
console.warn(`Unsupported Exclusive Message ${data}`);
|
|
890
|
+
}
|
|
891
|
+
break;
|
|
892
|
+
default:
|
|
893
|
+
console.warn(`Unsupported Exclusive Message ${data}`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
handleMasterVolumeSysEx(data) {
|
|
897
|
+
const volume = (data[5] * 128 + data[4] - 8192) / 8192;
|
|
898
|
+
this.handleMasterVolume(volume);
|
|
899
|
+
}
|
|
900
|
+
handleMasterVolume(volume) {
|
|
901
|
+
const now = this.audioContext.currentTime;
|
|
902
|
+
this.masterGain.gain.cancelScheduledValues(now);
|
|
903
|
+
this.masterGain.gain.setValueAtTime(volume * volume, now);
|
|
904
|
+
}
|
|
905
|
+
handleExclusiveMessage(data) {
|
|
906
|
+
console.warn(`Unsupported Exclusive Message ${data}`);
|
|
907
|
+
}
|
|
908
|
+
handleSysEx(data) {
|
|
909
|
+
switch (data[0]) {
|
|
910
|
+
case 126:
|
|
911
|
+
return this.handleUniversalNonRealTimeExclusiveMessage(data);
|
|
912
|
+
case 127:
|
|
913
|
+
return this.handleUniversalRealTimeExclusiveMessage(data);
|
|
914
|
+
default:
|
|
915
|
+
return this.handleExclusiveMessage(data);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
scheduleTask(callback, startTime) {
|
|
919
|
+
return new Promise((resolve) => {
|
|
920
|
+
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
921
|
+
bufferSource.onended = () => {
|
|
922
|
+
callback();
|
|
923
|
+
resolve();
|
|
924
|
+
};
|
|
925
|
+
bufferSource.start(startTime);
|
|
926
|
+
bufferSource.stop(startTime);
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
exports.MidyGMLite = MidyGMLite;
|
|
931
|
+
Object.defineProperty(MidyGMLite, "channelSettings", {
|
|
932
|
+
enumerable: true,
|
|
933
|
+
configurable: true,
|
|
934
|
+
writable: true,
|
|
935
|
+
value: {
|
|
936
|
+
volume: 1,
|
|
937
|
+
pan: 0,
|
|
938
|
+
vibratoRate: 5,
|
|
939
|
+
vibratoDepth: 0.5,
|
|
940
|
+
vibratoDelay: 2.5,
|
|
941
|
+
bank: 0,
|
|
942
|
+
dataMSB: 0,
|
|
943
|
+
dataLSB: 0,
|
|
944
|
+
program: 0,
|
|
945
|
+
pitchBend: 0,
|
|
946
|
+
modulationDepthRange: 2,
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
Object.defineProperty(MidyGMLite, "effectSettings", {
|
|
950
|
+
enumerable: true,
|
|
951
|
+
configurable: true,
|
|
952
|
+
writable: true,
|
|
953
|
+
value: {
|
|
954
|
+
expression: 1,
|
|
955
|
+
modulation: 0,
|
|
956
|
+
sustainPedal: false,
|
|
957
|
+
rpnMSB: 127,
|
|
958
|
+
rpnLSB: 127,
|
|
959
|
+
pitchBendRange: 2,
|
|
960
|
+
}
|
|
961
|
+
});
|