@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.
Files changed (45) hide show
  1. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts +128 -0
  2. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +1 -0
  3. package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.js +155 -0
  4. package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts +84 -0
  5. package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts.map +1 -0
  6. package/esm/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js +216 -0
  7. package/esm/midy-GM1.d.ts +187 -0
  8. package/esm/midy-GM1.d.ts.map +1 -0
  9. package/esm/midy-GM1.js +966 -0
  10. package/esm/midy-GM2.d.ts +280 -0
  11. package/esm/midy-GM2.d.ts.map +1 -0
  12. package/esm/midy-GM2.js +1303 -0
  13. package/esm/midy-GMLite.d.ts +181 -0
  14. package/esm/midy-GMLite.d.ts.map +1 -0
  15. package/esm/midy-GMLite.js +957 -0
  16. package/esm/midy.d.ts +285 -0
  17. package/esm/midy.d.ts.map +1 -0
  18. package/esm/midy.js +1372 -0
  19. package/esm/mod.d.ts +5 -0
  20. package/esm/mod.d.ts.map +1 -0
  21. package/esm/mod.js +4 -0
  22. package/esm/package.json +3 -0
  23. package/package.json +29 -0
  24. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts +128 -0
  25. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +1 -0
  26. package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.js +162 -0
  27. package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts +84 -0
  28. package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.d.ts.map +1 -0
  29. package/script/deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js +221 -0
  30. package/script/midy-GM1.d.ts +187 -0
  31. package/script/midy-GM1.d.ts.map +1 -0
  32. package/script/midy-GM1.js +970 -0
  33. package/script/midy-GM2.d.ts +280 -0
  34. package/script/midy-GM2.d.ts.map +1 -0
  35. package/script/midy-GM2.js +1307 -0
  36. package/script/midy-GMLite.d.ts +181 -0
  37. package/script/midy-GMLite.d.ts.map +1 -0
  38. package/script/midy-GMLite.js +961 -0
  39. package/script/midy.d.ts +285 -0
  40. package/script/midy.d.ts.map +1 -0
  41. package/script/midy.js +1376 -0
  42. package/script/mod.d.ts +5 -0
  43. package/script/mod.d.ts.map +1 -0
  44. package/script/mod.js +11 -0
  45. package/script/package.json +3 -0
package/script/midy.js ADDED
@@ -0,0 +1,1376 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Midy = 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 Midy {
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, "reverbFactor", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: 0.1
31
+ });
32
+ Object.defineProperty(this, "masterFineTuning", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: 0
37
+ });
38
+ Object.defineProperty(this, "masterCoarseTuning", {
39
+ enumerable: true,
40
+ configurable: true,
41
+ writable: true,
42
+ value: 0
43
+ });
44
+ Object.defineProperty(this, "mono", {
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ value: false
49
+ }); // CC#124, CC#125
50
+ Object.defineProperty(this, "omni", {
51
+ enumerable: true,
52
+ configurable: true,
53
+ writable: true,
54
+ value: false
55
+ }); // CC#126, CC#127
56
+ Object.defineProperty(this, "noteCheckInterval", {
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ value: 0.1
61
+ });
62
+ Object.defineProperty(this, "lookAhead", {
63
+ enumerable: true,
64
+ configurable: true,
65
+ writable: true,
66
+ value: 1
67
+ });
68
+ Object.defineProperty(this, "startDelay", {
69
+ enumerable: true,
70
+ configurable: true,
71
+ writable: true,
72
+ value: 0.1
73
+ });
74
+ Object.defineProperty(this, "startTime", {
75
+ enumerable: true,
76
+ configurable: true,
77
+ writable: true,
78
+ value: 0
79
+ });
80
+ Object.defineProperty(this, "resumeTime", {
81
+ enumerable: true,
82
+ configurable: true,
83
+ writable: true,
84
+ value: 0
85
+ });
86
+ Object.defineProperty(this, "soundFonts", {
87
+ enumerable: true,
88
+ configurable: true,
89
+ writable: true,
90
+ value: []
91
+ });
92
+ Object.defineProperty(this, "soundFontTable", {
93
+ enumerable: true,
94
+ configurable: true,
95
+ writable: true,
96
+ value: this.initSoundFontTable()
97
+ });
98
+ Object.defineProperty(this, "isPlaying", {
99
+ enumerable: true,
100
+ configurable: true,
101
+ writable: true,
102
+ value: false
103
+ });
104
+ Object.defineProperty(this, "isPausing", {
105
+ enumerable: true,
106
+ configurable: true,
107
+ writable: true,
108
+ value: false
109
+ });
110
+ Object.defineProperty(this, "isPaused", {
111
+ enumerable: true,
112
+ configurable: true,
113
+ writable: true,
114
+ value: false
115
+ });
116
+ Object.defineProperty(this, "isStopping", {
117
+ enumerable: true,
118
+ configurable: true,
119
+ writable: true,
120
+ value: false
121
+ });
122
+ Object.defineProperty(this, "isSeeking", {
123
+ enumerable: true,
124
+ configurable: true,
125
+ writable: true,
126
+ value: false
127
+ });
128
+ Object.defineProperty(this, "timeline", {
129
+ enumerable: true,
130
+ configurable: true,
131
+ writable: true,
132
+ value: []
133
+ });
134
+ Object.defineProperty(this, "instruments", {
135
+ enumerable: true,
136
+ configurable: true,
137
+ writable: true,
138
+ value: []
139
+ });
140
+ Object.defineProperty(this, "notePromises", {
141
+ enumerable: true,
142
+ configurable: true,
143
+ writable: true,
144
+ value: []
145
+ });
146
+ this.audioContext = audioContext;
147
+ this.masterGain = new GainNode(audioContext);
148
+ this.masterGain.connect(audioContext.destination);
149
+ this.channels = this.createChannels(audioContext);
150
+ this.GM2SystemOn();
151
+ }
152
+ initSoundFontTable() {
153
+ const table = new Array(128);
154
+ for (let i = 0; i < 128; i++) {
155
+ table[i] = new Map();
156
+ }
157
+ return table;
158
+ }
159
+ addSoundFont(soundFont) {
160
+ const index = this.soundFonts.length;
161
+ this.soundFonts.push(soundFont);
162
+ soundFont.parsed.presetHeaders.forEach((presetHeader) => {
163
+ if (!presetHeader.presetName.startsWith("\u0000")) { // TODO: Only SF3 generated by PolyPone?
164
+ const banks = this.soundFontTable[presetHeader.preset];
165
+ banks.set(presetHeader.bank, index);
166
+ }
167
+ });
168
+ }
169
+ async loadSoundFont(soundFontUrl) {
170
+ const response = await fetch(soundFontUrl);
171
+ const arrayBuffer = await response.arrayBuffer();
172
+ const parsed = (0, _esm_js_2.parse)(new Uint8Array(arrayBuffer));
173
+ const soundFont = new _esm_js_2.SoundFont(parsed);
174
+ this.addSoundFont(soundFont);
175
+ }
176
+ async loadMIDI(midiUrl) {
177
+ const response = await fetch(midiUrl);
178
+ const arrayBuffer = await response.arrayBuffer();
179
+ const midi = (0, _esm_js_1.parseMidi)(new Uint8Array(arrayBuffer));
180
+ const midiData = this.extractMidiData(midi);
181
+ this.instruments = midiData.instruments;
182
+ this.timeline = midiData.timeline;
183
+ this.ticksPerBeat = midi.header.ticksPerBeat;
184
+ this.totalTime = this.calcTotalTime();
185
+ }
186
+ setChannelAudioNodes(audioContext) {
187
+ const gainNode = new GainNode(audioContext, {
188
+ gain: Midy.channelSettings.volume,
189
+ });
190
+ const pannerNode = new StereoPannerNode(audioContext, {
191
+ pan: Midy.channelSettings.pan,
192
+ });
193
+ const modulationEffect = this.createModulationEffect(audioContext);
194
+ const reverbEffect = this.createReverbEffect(audioContext);
195
+ const chorusEffect = this.createChorusEffect(audioContext);
196
+ modulationEffect.lfo.start();
197
+ chorusEffect.lfo.start();
198
+ reverbEffect.dryGain.connect(pannerNode);
199
+ reverbEffect.wetGain.connect(pannerNode);
200
+ pannerNode.connect(gainNode);
201
+ gainNode.connect(this.masterGain);
202
+ return {
203
+ gainNode,
204
+ pannerNode,
205
+ modulationEffect,
206
+ reverbEffect,
207
+ chorusEffect,
208
+ };
209
+ }
210
+ createChannels(audioContext) {
211
+ const channels = Array.from({ length: 16 }, () => {
212
+ return {
213
+ ...Midy.channelSettings,
214
+ ...Midy.effectSettings,
215
+ ...this.setChannelAudioNodes(audioContext),
216
+ scheduledNotes: new Map(),
217
+ sostenutoNotes: new Map(),
218
+ };
219
+ });
220
+ return channels;
221
+ }
222
+ async createNoteBuffer(noteInfo, isSF3) {
223
+ const sampleEnd = noteInfo.sample.length + noteInfo.end;
224
+ if (isSF3) {
225
+ const sample = new Uint8Array(noteInfo.sample.length);
226
+ sample.set(noteInfo.sample);
227
+ const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
228
+ for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
229
+ const channelData = audioBuffer.getChannelData(channel);
230
+ channelData.set(channelData.subarray(0, sampleEnd));
231
+ }
232
+ return audioBuffer;
233
+ }
234
+ else {
235
+ const sample = noteInfo.sample.subarray(0, sampleEnd);
236
+ const floatSample = this.convertToFloat32Array(sample);
237
+ const audioBuffer = new AudioBuffer({
238
+ numberOfChannels: 1,
239
+ length: sample.length,
240
+ sampleRate: noteInfo.sampleRate,
241
+ });
242
+ const channelData = audioBuffer.getChannelData(0);
243
+ channelData.set(floatSample);
244
+ return audioBuffer;
245
+ }
246
+ }
247
+ async createNoteBufferNode(noteInfo, isSF3) {
248
+ const bufferSource = new AudioBufferSourceNode(this.audioContext);
249
+ const audioBuffer = await this.createNoteBuffer(noteInfo, isSF3);
250
+ bufferSource.buffer = audioBuffer;
251
+ bufferSource.loop = noteInfo.sampleModes % 2 !== 0;
252
+ if (bufferSource.loop) {
253
+ bufferSource.loopStart = noteInfo.loopStart / noteInfo.sampleRate;
254
+ bufferSource.loopEnd = noteInfo.loopEnd / noteInfo.sampleRate;
255
+ }
256
+ return bufferSource;
257
+ }
258
+ convertToFloat32Array(uint8Array) {
259
+ const int16Array = new Int16Array(uint8Array.buffer);
260
+ const float32Array = new Float32Array(int16Array.length);
261
+ for (let i = 0; i < int16Array.length; i++) {
262
+ float32Array[i] = int16Array[i] / 32768;
263
+ }
264
+ return float32Array;
265
+ }
266
+ async scheduleTimelineEvents(t, offset, queueIndex) {
267
+ while (queueIndex < this.timeline.length) {
268
+ const event = this.timeline[queueIndex];
269
+ const time = this.ticksToSecond(event.ticks, this.secondsPerBeat);
270
+ if (time > t + this.lookAhead)
271
+ break;
272
+ switch (event.type) {
273
+ case "controller":
274
+ this.handleControlChange(this.omni ? 0 : event.channel, event.controllerType, event.value);
275
+ break;
276
+ case "noteOn":
277
+ await this.scheduleNoteOn(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, time + this.startDelay - offset);
278
+ break;
279
+ case "noteOff": {
280
+ const notePromise = this.scheduleNoteRelease(this.omni ? 0 : event.channel, event.noteNumber, event.velocity, time + this.startDelay - offset);
281
+ if (notePromise) {
282
+ this.notePromises.push(notePromise);
283
+ }
284
+ break;
285
+ }
286
+ case "programChange":
287
+ this.handleProgramChange(event.channel, event.programNumber);
288
+ break;
289
+ case "setTempo":
290
+ this.secondsPerBeat = event.microsecondsPerBeat / 1000000;
291
+ break;
292
+ case "sysEx":
293
+ this.handleSysEx(event.data);
294
+ }
295
+ queueIndex++;
296
+ }
297
+ return queueIndex;
298
+ }
299
+ getQueueIndex(second) {
300
+ const ticks = this.secondToTicks(second, this.secondsPerBeat);
301
+ for (let i = 0; i < this.timeline.length; i++) {
302
+ if (ticks <= this.timeline[i].ticks) {
303
+ return i;
304
+ }
305
+ }
306
+ return 0;
307
+ }
308
+ playNotes() {
309
+ return new Promise((resolve) => {
310
+ this.isPlaying = true;
311
+ this.isPaused = false;
312
+ this.startTime = this.audioContext.currentTime;
313
+ let queueIndex = this.getQueueIndex(this.resumeTime);
314
+ let offset = this.resumeTime - this.startTime;
315
+ this.notePromises = [];
316
+ const schedulePlayback = async () => {
317
+ if (queueIndex >= this.timeline.length) {
318
+ await Promise.all(this.notePromises);
319
+ this.notePromises = [];
320
+ resolve();
321
+ return;
322
+ }
323
+ const t = this.audioContext.currentTime + offset;
324
+ queueIndex = await this.scheduleTimelineEvents(t, offset, queueIndex);
325
+ if (this.isPausing) {
326
+ await this.stopNotes();
327
+ this.notePromises = [];
328
+ resolve();
329
+ this.isPausing = false;
330
+ this.isPaused = true;
331
+ return;
332
+ }
333
+ else if (this.isStopping) {
334
+ await this.stopNotes();
335
+ this.notePromises = [];
336
+ resolve();
337
+ this.isStopping = false;
338
+ this.isPaused = false;
339
+ return;
340
+ }
341
+ else if (this.isSeeking) {
342
+ this.stopNotes();
343
+ this.startTime = this.audioContext.currentTime;
344
+ queueIndex = this.getQueueIndex(this.resumeTime);
345
+ offset = this.resumeTime - this.startTime;
346
+ this.isSeeking = false;
347
+ await schedulePlayback();
348
+ }
349
+ else {
350
+ const now = this.audioContext.currentTime;
351
+ const waitTime = now + this.noteCheckInterval;
352
+ await this.scheduleTask(() => { }, waitTime);
353
+ await schedulePlayback();
354
+ }
355
+ };
356
+ schedulePlayback();
357
+ });
358
+ }
359
+ ticksToSecond(ticks, secondsPerBeat) {
360
+ return ticks * secondsPerBeat / this.ticksPerBeat;
361
+ }
362
+ secondToTicks(second, secondsPerBeat) {
363
+ return second * this.ticksPerBeat / secondsPerBeat;
364
+ }
365
+ extractMidiData(midi) {
366
+ const instruments = new Set();
367
+ const timeline = [];
368
+ const tmpChannels = new Array(16);
369
+ for (let i = 0; i < tmpChannels.length; i++) {
370
+ tmpChannels[i] = {
371
+ durationTicks: new Map(),
372
+ programNumber: -1,
373
+ bankMSB: this.channels[i].bankMSB,
374
+ bankLSB: this.channels[i].bankLSB,
375
+ };
376
+ }
377
+ midi.tracks.forEach((track) => {
378
+ let currentTicks = 0;
379
+ track.forEach((event) => {
380
+ currentTicks += event.deltaTime;
381
+ event.ticks = currentTicks;
382
+ switch (event.type) {
383
+ case "noteOn": {
384
+ const channel = tmpChannels[event.channel];
385
+ if (channel.programNumber < 0) {
386
+ channel.programNumber = event.programNumber;
387
+ switch (channel.bankMSB) {
388
+ case 120:
389
+ instruments.add(`128:0`);
390
+ break;
391
+ case 121:
392
+ instruments.add(`${channel.bankLSB}:0`);
393
+ break;
394
+ default: {
395
+ const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
396
+ instruments.add(`${bankNumber}:0`);
397
+ }
398
+ }
399
+ channel.programNumber = 0;
400
+ }
401
+ channel.durationTicks.set(event.noteNumber, {
402
+ ticks: event.ticks,
403
+ noteOn: event,
404
+ });
405
+ break;
406
+ }
407
+ case "noteOff": {
408
+ const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
409
+ .get(event.noteNumber);
410
+ noteOn.durationTicks = event.ticks - ticks;
411
+ break;
412
+ }
413
+ case "controller":
414
+ switch (event.controllerType) {
415
+ case 0:
416
+ tmpChannels[event.channel].bankMSB = event.value;
417
+ break;
418
+ case 32:
419
+ tmpChannels[event.channel].bankLSB = event.value;
420
+ break;
421
+ }
422
+ break;
423
+ case "programChange": {
424
+ const channel = tmpChannels[event.channel];
425
+ channel.programNumber = event.programNumber;
426
+ switch (channel.bankMSB) {
427
+ case 120:
428
+ instruments.add(`128:${channel.programNumber}`);
429
+ break;
430
+ case 121:
431
+ instruments.add(`${channel.bankLSB}:${channel.programNumber}`);
432
+ break;
433
+ default: {
434
+ const bankNumber = channel.bankMSB * 128 + channel.bankLSB;
435
+ instruments.add(`${bankNumber}:${channel.programNumber}`);
436
+ }
437
+ }
438
+ }
439
+ }
440
+ delete event.deltaTime;
441
+ timeline.push(event);
442
+ });
443
+ });
444
+ timeline.sort((a, b) => {
445
+ if (a.ticks !== b.ticks) {
446
+ return a.ticks - b.ticks;
447
+ }
448
+ if (a.type !== "controller" && b.type === "controller") {
449
+ return -1;
450
+ }
451
+ if (a.type === "controller" && b.type !== "controller") {
452
+ return 1;
453
+ }
454
+ return 0;
455
+ });
456
+ return { instruments, timeline };
457
+ }
458
+ stopNotes() {
459
+ const now = this.audioContext.currentTime;
460
+ const velocity = 0;
461
+ const stopPedal = true;
462
+ this.channels.forEach((channel, channelNumber) => {
463
+ channel.scheduledNotes.forEach((scheduledNotes) => {
464
+ scheduledNotes.forEach((scheduledNote) => {
465
+ if (scheduledNote) {
466
+ const promise = this.scheduleNoteRelease(channelNumber, scheduledNote.noteNumber, velocity, now, stopPedal);
467
+ this.notePromises.push(promise);
468
+ }
469
+ });
470
+ });
471
+ channel.scheduledNotes.clear();
472
+ });
473
+ return Promise.all(this.notePromises);
474
+ }
475
+ async start() {
476
+ if (this.isPlaying || this.isPaused)
477
+ return;
478
+ this.resumeTime = 0;
479
+ await this.playNotes();
480
+ this.isPlaying = false;
481
+ }
482
+ stop() {
483
+ if (!this.isPlaying)
484
+ return;
485
+ this.isStopping = true;
486
+ }
487
+ pause() {
488
+ if (!this.isPlaying || this.isPaused)
489
+ return;
490
+ const now = this.audioContext.currentTime;
491
+ this.resumeTime += now - this.startTime - this.startDelay;
492
+ this.isPausing = true;
493
+ }
494
+ async resume() {
495
+ if (!this.isPaused)
496
+ return;
497
+ await this.playNotes();
498
+ this.isPlaying = false;
499
+ }
500
+ seekTo(second) {
501
+ this.resumeTime = second;
502
+ if (this.isPlaying) {
503
+ this.isSeeking = true;
504
+ }
505
+ }
506
+ calcTotalTime() {
507
+ const endOfTracks = [];
508
+ let prevTicks = 0;
509
+ let totalTime = 0;
510
+ let secondsPerBeat = 0.5;
511
+ for (let i = 0; i < this.timeline.length; i++) {
512
+ const event = this.timeline[i];
513
+ switch (event.type) {
514
+ case "setTempo": {
515
+ const durationTicks = event.ticks - prevTicks;
516
+ totalTime += this.ticksToSecond(durationTicks, secondsPerBeat);
517
+ secondsPerBeat = event.microsecondsPerBeat / 1000000;
518
+ prevTicks = event.ticks;
519
+ break;
520
+ }
521
+ case "endOfTrack":
522
+ endOfTracks.push(event);
523
+ }
524
+ }
525
+ let maxTicks = 0;
526
+ for (let i = 0; i < endOfTracks.length; i++) {
527
+ const event = endOfTracks[i];
528
+ if (maxTicks < event.ticks)
529
+ maxTicks = event.ticks;
530
+ }
531
+ const durationTicks = maxTicks - prevTicks;
532
+ totalTime += this.ticksToSecond(durationTicks, secondsPerBeat);
533
+ return totalTime;
534
+ }
535
+ currentTime() {
536
+ const now = this.audioContext.currentTime;
537
+ return this.resumeTime + now - this.startTime - this.startDelay;
538
+ }
539
+ getActiveNotes(channel) {
540
+ const activeNotes = new Map();
541
+ channel.scheduledNotes.forEach((scheduledNotes) => {
542
+ const activeNote = this.getActiveChannelNotes(scheduledNotes);
543
+ if (activeNote) {
544
+ activeNotes.set(activeNote.noteNumber, activeNote);
545
+ }
546
+ });
547
+ return activeNotes;
548
+ }
549
+ getActiveChannelNotes(scheduledNotes) {
550
+ for (let i = 0; i < scheduledNotes; i++) {
551
+ const scheduledNote = scheduledNotes[i];
552
+ if (scheduledNote)
553
+ return scheduledNote;
554
+ }
555
+ }
556
+ createModulationEffect(audioContext) {
557
+ const lfo = new OscillatorNode(audioContext, {
558
+ frequency: 5,
559
+ });
560
+ const lfoGain = new GainNode(audioContext);
561
+ lfo.connect(lfoGain);
562
+ return {
563
+ lfo,
564
+ lfoGain,
565
+ };
566
+ }
567
+ createReverbEffect(audioContext, options = {}) {
568
+ const { decay = 0.8, preDecay = 0, } = options;
569
+ const sampleRate = audioContext.sampleRate;
570
+ const length = sampleRate * decay;
571
+ const impulse = new AudioBuffer({
572
+ numberOfChannels: 2,
573
+ length,
574
+ sampleRate,
575
+ });
576
+ const preDecayLength = Math.min(sampleRate * preDecay, length);
577
+ for (let channel = 0; channel < impulse.numberOfChannels; channel++) {
578
+ const channelData = impulse.getChannelData(channel);
579
+ for (let i = 0; i < preDecayLength; i++) {
580
+ channelData[i] = Math.random() * 2 - 1;
581
+ }
582
+ for (let i = preDecayLength; i < length; i++) {
583
+ const attenuation = Math.exp(-(i - preDecayLength) / sampleRate / decay);
584
+ channelData[i] = (Math.random() * 2 - 1) * attenuation;
585
+ }
586
+ }
587
+ const convolverNode = new ConvolverNode(audioContext, {
588
+ buffer: impulse,
589
+ });
590
+ const dryGain = new GainNode(audioContext);
591
+ const wetGain = new GainNode(audioContext);
592
+ convolverNode.connect(wetGain);
593
+ return {
594
+ convolverNode,
595
+ dryGain,
596
+ wetGain,
597
+ };
598
+ }
599
+ createChorusEffect(audioContext, options = {}) {
600
+ const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
601
+ const lfo = new OscillatorNode(audioContext, {
602
+ frequency: chorusRate,
603
+ });
604
+ const lfoGain = new GainNode(audioContext, {
605
+ gain: chorusDepth,
606
+ });
607
+ const chorusGains = [];
608
+ const delayNodes = [];
609
+ const baseGain = 1 / chorusCount;
610
+ for (let i = 0; i < chorusCount; i++) {
611
+ const randomDelayFactor = (Math.random() - 0.5) * variance;
612
+ const delayTime = (i + 1) * delay + randomDelayFactor;
613
+ const delayNode = new DelayNode(audioContext, {
614
+ maxDelayTime: delayTime,
615
+ });
616
+ delayNodes.push(delayNode);
617
+ const chorusGain = new GainNode(audioContext, {
618
+ gain: baseGain,
619
+ });
620
+ chorusGains.push(chorusGain);
621
+ lfo.connect(lfoGain);
622
+ lfoGain.connect(delayNode.delayTime);
623
+ delayNode.connect(chorusGain);
624
+ }
625
+ return {
626
+ lfo,
627
+ lfoGain,
628
+ delayNodes,
629
+ chorusGains,
630
+ };
631
+ }
632
+ connectNoteEffects(channel, gainNode) {
633
+ if (channel.reverb === 0) {
634
+ if (channel.chorus === 0) { // no effect
635
+ gainNode.connect(channel.pannerNode);
636
+ }
637
+ else { // chorus
638
+ channel.chorusEffect.delayNodes.forEach((delayNode) => {
639
+ gainNode.connect(delayNode);
640
+ });
641
+ channel.chorusEffect.chorusGains.forEach((chorusGain) => {
642
+ chorusGain.connect(channel.pannerNode);
643
+ });
644
+ }
645
+ }
646
+ else {
647
+ if (channel.chorus === 0) { // reverb
648
+ gainNode.connect(channel.reverbEffect.convolverNode);
649
+ gainNode.connect(channel.reverbEffect.dryGain);
650
+ }
651
+ else { // reverb + chorus
652
+ gainNode.connect(channel.reverbEffect.convolverNode);
653
+ gainNode.connect(channel.reverbEffect.dryGain);
654
+ channel.chorusEffect.delayNodes.forEach((delayNode) => {
655
+ gainNode.connect(delayNode);
656
+ });
657
+ channel.chorusEffect.chorusGains.forEach((chorusGain) => {
658
+ chorusGain.connect(channel.reverbEffect.convolverNode);
659
+ });
660
+ }
661
+ }
662
+ }
663
+ cbToRatio(cb) {
664
+ return Math.pow(10, cb / 200);
665
+ }
666
+ centToHz(cent) {
667
+ return 8.176 * Math.pow(2, cent / 1200);
668
+ }
669
+ async createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3) {
670
+ const masterTuning = this.masterCoarseTuning + this.masterFineTuning;
671
+ const channelTuning = channel.coarseTuning + channel.fineTuning;
672
+ const tuning = masterTuning + channelTuning;
673
+ const semitoneOffset = channel.pitchBend * channel.pitchBendRange + tuning;
674
+ const playbackRate = noteInfo.playbackRate(noteNumber) *
675
+ Math.pow(2, semitoneOffset / 12);
676
+ const bufferSource = await this.createNoteBufferNode(noteInfo, isSF3);
677
+ bufferSource.playbackRate.value = playbackRate;
678
+ // volume envelope
679
+ const gainNode = new GainNode(this.audioContext, {
680
+ gain: 0,
681
+ });
682
+ let volume = (velocity / 127) * channel.volume * channel.expression;
683
+ if (volume === 0)
684
+ volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
685
+ const attackVolume = this.cbToRatio(-noteInfo.initialAttenuation) * volume;
686
+ const sustainVolume = attackVolume * (1 - noteInfo.volSustain);
687
+ const volDelay = startTime + noteInfo.volDelay;
688
+ const volAttack = volDelay + noteInfo.volAttack;
689
+ const volHold = volAttack + noteInfo.volHold;
690
+ const volDecay = volHold + noteInfo.volDecay;
691
+ gainNode.gain
692
+ .setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
693
+ .exponentialRampToValueAtTime(attackVolume, volAttack)
694
+ .setValueAtTime(attackVolume, volHold)
695
+ .linearRampToValueAtTime(sustainVolume, volDecay);
696
+ if (channel.modulation > 0) {
697
+ const lfoGain = channel.modulationEffect.lfoGain;
698
+ lfoGain.connect(bufferSource.detune);
699
+ lfoGain.gain.cancelScheduledValues(startTime + channel.vibratoDelay);
700
+ lfoGain.gain.setValueAtTime(channel.modulation, startTime + channel.vibratoDelay);
701
+ }
702
+ // filter envelope
703
+ const softPedalFactor = 1 -
704
+ (0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
705
+ const baseFreq = this.centToHz(noteInfo.initialFilterFc) * softPedalFactor;
706
+ const peekFreq = this.centToHz(noteInfo.initialFilterFc + noteInfo.modEnvToFilterFc) * softPedalFactor;
707
+ const sustainFreq = (baseFreq +
708
+ (peekFreq - baseFreq) * (1 - noteInfo.modSustain)) * softPedalFactor;
709
+ const filterNode = new BiquadFilterNode(this.audioContext, {
710
+ type: "lowpass",
711
+ Q: this.cbToRatio(noteInfo.initialFilterQ),
712
+ frequency: baseFreq,
713
+ });
714
+ const modDelay = startTime + noteInfo.modDelay;
715
+ const modAttack = modDelay + noteInfo.modAttack;
716
+ const modHold = modAttack + noteInfo.modHold;
717
+ const modDecay = modHold + noteInfo.modDecay;
718
+ filterNode.frequency
719
+ .setValueAtTime(baseFreq, modDelay)
720
+ .exponentialRampToValueAtTime(peekFreq, modAttack)
721
+ .setValueAtTime(peekFreq, modHold)
722
+ .linearRampToValueAtTime(sustainFreq, modDecay);
723
+ bufferSource.connect(filterNode);
724
+ filterNode.connect(gainNode);
725
+ if (this.mono && channel.currentBufferSource) {
726
+ channel.currentBufferSource.stop(startTime);
727
+ channel.currentBufferSource = bufferSource;
728
+ }
729
+ bufferSource.start(startTime, noteInfo.start / noteInfo.sampleRate);
730
+ return { bufferSource, gainNode, filterNode };
731
+ }
732
+ calcBank(channel, channelNumber) {
733
+ if (channel.bankMSB === 121) {
734
+ return 0;
735
+ }
736
+ if (channelNumber % 9 <= 1 && channel.bankMSB === 120) {
737
+ return 128;
738
+ }
739
+ return channel.bank;
740
+ }
741
+ async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
742
+ const channel = this.channels[channelNumber];
743
+ const bankNumber = this.calcBank(channel, channelNumber);
744
+ const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
745
+ if (soundFontIndex === undefined)
746
+ return;
747
+ const soundFont = this.soundFonts[soundFontIndex];
748
+ const isSF3 = soundFont.parsed.info.version.major === 3;
749
+ const noteInfo = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
750
+ if (!noteInfo)
751
+ return;
752
+ const { bufferSource, gainNode, filterNode } = await this
753
+ .createNoteAudioChain(channel, noteInfo, noteNumber, velocity, startTime, isSF3);
754
+ this.connectNoteEffects(channel, gainNode);
755
+ if (channel.sostenutoPedal) {
756
+ channel.sostenutoNotes.set(noteNumber, {
757
+ gainNode,
758
+ filterNode,
759
+ bufferSource,
760
+ noteNumber,
761
+ noteInfo,
762
+ });
763
+ }
764
+ const scheduledNotes = channel.scheduledNotes;
765
+ const scheduledNote = {
766
+ gainNode,
767
+ filterNode,
768
+ bufferSource,
769
+ noteNumber,
770
+ noteInfo,
771
+ startTime,
772
+ };
773
+ if (scheduledNotes.has(noteNumber)) {
774
+ scheduledNotes.get(noteNumber).push(scheduledNote);
775
+ }
776
+ else {
777
+ scheduledNotes.set(noteNumber, [scheduledNote]);
778
+ }
779
+ }
780
+ noteOn(channelNumber, noteNumber, velocity) {
781
+ const now = this.audioContext.currentTime;
782
+ return this.scheduleNoteOn(channelNumber, noteNumber, velocity, now);
783
+ }
784
+ scheduleNoteRelease(channelNumber, noteNumber, velocity, stopTime, stopPedal = false) {
785
+ const channel = this.channels[channelNumber];
786
+ if (stopPedal && channel.sustainPedal)
787
+ return;
788
+ if (stopPedal && channel.sostenutoNotes.has(noteNumber))
789
+ return;
790
+ if (!channel.scheduledNotes.has(noteNumber))
791
+ return;
792
+ const targetNotes = channel.scheduledNotes.get(noteNumber);
793
+ for (let i = 0; i < targetNotes.length; i++) {
794
+ const targetNote = targetNotes[i];
795
+ if (!targetNote)
796
+ continue;
797
+ if (targetNote.ending)
798
+ continue;
799
+ const { bufferSource, filterNode, gainNode, noteInfo } = targetNote;
800
+ const velocityRate = (velocity + 127) / 127;
801
+ const volEndTime = stopTime + noteInfo.volRelease * velocityRate;
802
+ gainNode.gain.cancelScheduledValues(stopTime);
803
+ gainNode.gain.linearRampToValueAtTime(0, volEndTime);
804
+ const baseFreq = this.centToHz(noteInfo.initialFilterFc);
805
+ const modEndTime = stopTime + noteInfo.modRelease * velocityRate;
806
+ filterNode.frequency.cancelScheduledValues(stopTime);
807
+ filterNode.frequency.linearRampToValueAtTime(baseFreq, modEndTime);
808
+ targetNote.ending = true;
809
+ this.scheduleTask(() => {
810
+ bufferSource.loop = false;
811
+ }, stopTime);
812
+ return new Promise((resolve) => {
813
+ bufferSource.onended = () => {
814
+ targetNotes[i] = null;
815
+ bufferSource.disconnect(0);
816
+ filterNode.disconnect(0);
817
+ gainNode.disconnect(0);
818
+ resolve();
819
+ };
820
+ bufferSource.stop(volEndTime);
821
+ });
822
+ }
823
+ }
824
+ releaseNote(channelNumber, noteNumber, velocity) {
825
+ const now = this.audioContext.currentTime;
826
+ return this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now);
827
+ }
828
+ releaseSustainPedal(channelNumber) {
829
+ const now = this.audioContext.currentTime;
830
+ const channel = this.channels[channelNumber];
831
+ channel.sustainPedal = false;
832
+ channel.scheduledNotes.forEach((scheduledNotes) => {
833
+ scheduledNotes.forEach((scheduledNote) => {
834
+ if (scheduledNote) {
835
+ const { gainNode, filterNode, bufferSource, noteInfo } = scheduledNote;
836
+ const volEndTime = now + noteInfo.volRelease;
837
+ gainNode.gain.cancelScheduledValues(now);
838
+ gainNode.gain.linearRampToValueAtTime(0, volEndTime);
839
+ const baseFreq = this.centToHz(noteInfo.initialFilterFc);
840
+ const modEndTime = now + noteInfo.modRelease;
841
+ filterNode.frequency.cancelScheduledValues(now);
842
+ filterNode.frequency.linearRampToValueAtTime(baseFreq, modEndTime);
843
+ bufferSource.stop(volEndTime);
844
+ }
845
+ });
846
+ });
847
+ }
848
+ releaseSostenuto(channelNumber) {
849
+ const now = this.audioContext.currentTime;
850
+ const channel = this.channels[channelNumber];
851
+ channel.sostenutoPedal = false;
852
+ channel.sostenutoNotes.forEach((activeNote) => {
853
+ const { gainNode, bufferSource, noteInfo } = activeNote;
854
+ const fadeTime = noteInfo.volRelease;
855
+ gainNode.gain.cancelScheduledValues(now);
856
+ gainNode.gain.linearRampToValueAtTime(0, now + fadeTime);
857
+ bufferSource.stop(now + fadeTime);
858
+ });
859
+ channel.sostenutoNotes.clear();
860
+ }
861
+ handleMIDIMessage(statusByte, data1, data2) {
862
+ const channelNumber = omni ? 0 : statusByte & 0x0F;
863
+ const messageType = statusByte & 0xF0;
864
+ switch (messageType) {
865
+ case 0x80:
866
+ return this.releaseNote(channelNumber, data1, data2);
867
+ case 0x90:
868
+ return this.noteOn(channelNumber, data1, data2);
869
+ case 0xA0:
870
+ return this.handlePolyphonicKeyPressure(channelNumber, data1, data2);
871
+ case 0xB0:
872
+ return this.handleControlChange(channelNumber, data1, data2);
873
+ case 0xC0:
874
+ return this.handleProgramChange(channelNumber, data1);
875
+ case 0xD0:
876
+ return this.handleChannelPressure(channelNumber, data1);
877
+ case 0xE0:
878
+ return this.handlePitchBend(channelNumber, data1, data2);
879
+ default:
880
+ console.warn(`Unsupported MIDI message: ${messageType.toString(16)}`);
881
+ }
882
+ }
883
+ handlePolyphonicKeyPressure(channelNumber, noteNumber, pressure) {
884
+ const now = this.audioContext.currentTime;
885
+ const channel = this.channels[channelNumber];
886
+ const scheduledNotes = channel.scheduledNotes.get(noteNumber);
887
+ pressure /= 127;
888
+ if (scheduledNotes) {
889
+ scheduledNotes.forEach((scheduledNote) => {
890
+ if (scheduledNote) {
891
+ const { initialAttenuation } = scheduledNote.noteInfo;
892
+ const gain = this.cbToRatio(initialAttenuation) * pressure;
893
+ scheduledNote.gainNode.gain.cancelScheduledValues(now);
894
+ scheduledNote.gainNode.gain.setValueAtTime(gain, now);
895
+ }
896
+ });
897
+ }
898
+ }
899
+ handleProgramChange(channelNumber, program) {
900
+ const channel = this.channels[channelNumber];
901
+ channel.bank = channel.bankMSB * 128 + channel.bankLSB;
902
+ channel.program = program;
903
+ }
904
+ handleChannelPressure(channelNumber, pressure) {
905
+ this.channels[channelNumber].channelPressure = pressure;
906
+ }
907
+ handlePitchBend(channelNumber, lsb, msb) {
908
+ const pitchBend = (msb * 128 + lsb - 8192) / 8192;
909
+ this.channels[channelNumber].pitchBend = pitchBend;
910
+ }
911
+ handleControlChange(channelNumber, controller, value) {
912
+ switch (controller) {
913
+ case 0:
914
+ return this.setBankMSB(channelNumber, value);
915
+ case 1:
916
+ return this.setModulation(channelNumber, value);
917
+ case 5:
918
+ return this.setPortamentoTime(channelNumber, value);
919
+ case 6:
920
+ return this.setDataEntry(channelNumber, value, true);
921
+ case 7:
922
+ return this.setVolume(channelNumber, value);
923
+ case 10:
924
+ return this.setPan(channelNumber, value);
925
+ case 11:
926
+ return this.setExpression(channelNumber, value);
927
+ case 32:
928
+ return this.setBankLSB(channelNumber, value);
929
+ case 38:
930
+ return this.setDataEntry(channelNumber, value, false);
931
+ case 64:
932
+ return this.setSustainPedal(channelNumber, value);
933
+ case 65:
934
+ return this.setPortamento(channelNumber, value);
935
+ case 66:
936
+ return this.setSostenutoPedal(channelNumber, value);
937
+ case 67:
938
+ return this.setSoftPedal(channelNumber, value);
939
+ // TODO: 71-75
940
+ case 76:
941
+ return this.setVibratoRate(channelNumber, value);
942
+ case 77:
943
+ return this.setVibratoDepth(channelNumber, value);
944
+ case 78:
945
+ return this.setVibratoDelay(channelNumber, value);
946
+ case 91:
947
+ return this.setReverb(channelNumber, value);
948
+ case 93:
949
+ return this.setChorus(channelNumber, value);
950
+ case 96:
951
+ return incrementRPNValue(channelNumber);
952
+ case 97:
953
+ return decrementRPNValue(channelNumber);
954
+ case 100:
955
+ return this.setRPNMSB(channelNumber, value);
956
+ case 101:
957
+ return this.setRPNLSB(channelNumber, value);
958
+ case 120:
959
+ return this.allSoundOff(channelNumber);
960
+ case 121:
961
+ return this.resetAllControllers(channelNumber);
962
+ case 123:
963
+ return this.allNotesOff(channelNumber);
964
+ case 124:
965
+ return this.omniOff();
966
+ case 125:
967
+ return this.omniOn();
968
+ case 126:
969
+ return this.monoOn();
970
+ case 127:
971
+ return this.polyOn();
972
+ default:
973
+ console.warn(`Unsupported Control change: controller=${controller} value=${value}`);
974
+ }
975
+ }
976
+ setBankMSB(channelNumber, msb) {
977
+ this.channels[channelNumber].bankMSB = msb;
978
+ }
979
+ setModulation(channelNumber, modulation) {
980
+ const now = this.audioContext.currentTime;
981
+ const channel = this.channels[channelNumber];
982
+ channel.modulation = (modulation * 100 / 127) *
983
+ channel.modulationDepthRange;
984
+ const lfoGain = channel.modulationEffect.lfoGain;
985
+ lfoGain.gain.cancelScheduledValues(now);
986
+ lfoGain.gain.setValueAtTime(channel.modulation, now);
987
+ }
988
+ setPortamentoTime(channelNumber, portamentoTime) {
989
+ this.channels[channelNumber].portamentoTime = portamentoTime / 127;
990
+ }
991
+ setVolume(channelNumber, volume) {
992
+ const channel = this.channels[channelNumber];
993
+ channel.volume = volume / 127;
994
+ this.updateChannelGain(channel);
995
+ }
996
+ setPan(channelNumber, pan) {
997
+ const now = this.audioContext.currentTime;
998
+ const channel = this.channels[channelNumber];
999
+ channel.pan = pan / 127 * 2 - 1; // -1 (left) - +1 (right)
1000
+ channel.pannerNode.pan.cancelScheduledValues(now);
1001
+ channel.pannerNode.pan.setValueAtTime(channel.pan, now);
1002
+ }
1003
+ setExpression(channelNumber, expression) {
1004
+ const channel = this.channels[channelNumber];
1005
+ channel.expression = expression / 127;
1006
+ this.updateChannelGain(channel);
1007
+ }
1008
+ setBankLSB(channelNumber, lsb) {
1009
+ this.channels[channelNumber].bankLSB = lsb;
1010
+ }
1011
+ updateChannelGain(channel) {
1012
+ const now = this.audioContext.currentTime;
1013
+ const volume = channel.volume * channel.expression;
1014
+ channel.gainNode.gain.cancelScheduledValues(now);
1015
+ channel.gainNode.gain.setValueAtTime(volume, now);
1016
+ }
1017
+ setSustainPedal(channelNumber, value) {
1018
+ const isOn = value >= 64;
1019
+ this.channels[channelNumber].sustainPedal = isOn;
1020
+ if (!isOn) {
1021
+ this.releaseSustainPedal(channelNumber);
1022
+ }
1023
+ }
1024
+ setPortamento(channelNumber, value) {
1025
+ this.channels[channelNumber].portamento = value >= 64;
1026
+ }
1027
+ setReverb(channelNumber, reverb) {
1028
+ const now = this.audioContext.currentTime;
1029
+ const channel = this.channels[channelNumber];
1030
+ const reverbEffect = channel.reverbEffect;
1031
+ channel.reverb = reverb / 127 * this.reverbFactor;
1032
+ reverbEffect.dryGain.gain.cancelScheduledValues(now);
1033
+ reverbEffect.dryGain.gain.setValueAtTime(1 - channel.reverb, now);
1034
+ reverbEffect.wetGain.gain.cancelScheduledValues(now);
1035
+ reverbEffect.wetGain.gain.setValueAtTime(channel.reverb, now);
1036
+ }
1037
+ setChorus(channelNumber, chorus) {
1038
+ const channel = this.channels[channelNumber];
1039
+ channel.chorus = chorus / 127;
1040
+ channel.chorusEffect.lfoGain = channel.chorus;
1041
+ }
1042
+ setSostenutoPedal(channelNumber, value) {
1043
+ const isOn = value >= 64;
1044
+ const channel = this.channels[channelNumber];
1045
+ channel.sostenutoPedal = isOn;
1046
+ if (isOn) {
1047
+ const activeNotes = this.getActiveNotes(channel);
1048
+ channel.sostenutoNotes = new Map(activeNotes);
1049
+ }
1050
+ }
1051
+ setSoftPedal(channelNumber, softPedal) {
1052
+ const channel = this.channels[channelNumber];
1053
+ channel.softPedal = softPedal / 127;
1054
+ this.updateChannelGain(channel);
1055
+ }
1056
+ setVibratoRate(channelNumber, vibratoRate) {
1057
+ const now = this.audioContext.currentTime;
1058
+ const channel = this.channels[channelNumber];
1059
+ channel.vibratoRate = vibratoRate / 127 * 4 + 3; // 3-7Hz
1060
+ channel.modulationEffect.lfo.frequency.cancelScheduledValues(now);
1061
+ channel.modulationEffect.lfo.frequency.setValueAtTime(depth, now);
1062
+ }
1063
+ setVibratoDepth(channelNumber, vibratoDepth) {
1064
+ const now = this.audioContext.currentTime;
1065
+ const channel = this.channels[channelNumber];
1066
+ channel.vibratoDepth = vibratoDepth / 127;
1067
+ channel.modulationEffect.lfoGain.gain.cancelScheduledValues(now);
1068
+ channel.modulationEffect.lfoGain.gain.setValueAtTime(depth, now);
1069
+ }
1070
+ setVibratoDelay(channelNumber, vibratoDelay) {
1071
+ // Access Virus: 0-10sec
1072
+ // Elektron: 0-5sec
1073
+ // Korg: 0-5sec
1074
+ // Nord: 0-5sec
1075
+ // Roland: 0-5sec
1076
+ // Yamaha: 0-8sec
1077
+ const channel = this.channels[channelNumber];
1078
+ channel.vibratoDelay = vibratoDelay / 127 * 5; // 0-5sec
1079
+ }
1080
+ incrementRPNValue(channelNumber) {
1081
+ const channel = this.channels[channelNumber];
1082
+ const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1083
+ switch (rpn) {
1084
+ case 0:
1085
+ channel.pitchBendRange = Math.min(1, channel.pitchBendRange + 1);
1086
+ break;
1087
+ case 1:
1088
+ channel.fineTuning = Math.min(1, channel.fineTuning + 1);
1089
+ break;
1090
+ case 2:
1091
+ channel.coarseTuning = Math.min(88, channel.coarseTuning + 1);
1092
+ break;
1093
+ default:
1094
+ console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1095
+ }
1096
+ }
1097
+ decrementRPNValue(channelNumber) {
1098
+ const channel = this.channels[channelNumber];
1099
+ const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1100
+ switch (rpn) {
1101
+ case 0:
1102
+ channel.pitchBendRange = Math.max(-1, channel.pitchBendRange - 1);
1103
+ break;
1104
+ case 1:
1105
+ channel.fineTuning = Math.max(-1, channel.fineTuning - 1);
1106
+ break;
1107
+ case 2:
1108
+ channel.coarseTuning = Math.max(40, channel.coarseTuning - 1);
1109
+ break;
1110
+ default:
1111
+ console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB}, LSB=${channel.rpnLSB}.`);
1112
+ }
1113
+ }
1114
+ setRPNMSB(channelNumber, value) {
1115
+ this.channels[channelNumber].rpnMSB = value;
1116
+ }
1117
+ setRPNLSB(channelNumber, value) {
1118
+ this.channels[channelNumber].rpnLSB = value;
1119
+ }
1120
+ // TODO: support 3-4?
1121
+ setDataEntry(channelNumber, value, isMSB) {
1122
+ const channel = this.channels[channelNumber];
1123
+ const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
1124
+ isMSB ? channel.dataMSB = value : channel.dataLSB = value;
1125
+ const { dataMSB, dataLSB } = channel;
1126
+ switch (rpn) {
1127
+ case 0:
1128
+ channel.pitchBendRange = dataMSB + dataLSB / 100;
1129
+ break;
1130
+ case 1:
1131
+ channel.fineTuning = (dataMSB * 128 + dataLSB - 8192) / 8192;
1132
+ break;
1133
+ case 2:
1134
+ channel.coarseTuning = dataMSB - 64;
1135
+ break;
1136
+ case 5:
1137
+ channel.modulationDepthRange = dataMSB + dataLSB / 128;
1138
+ break;
1139
+ default:
1140
+ console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
1141
+ }
1142
+ }
1143
+ allSoundOff(channelNumber) {
1144
+ const now = this.audioContext.currentTime;
1145
+ const channel = this.channels[channelNumber];
1146
+ const velocity = 0;
1147
+ const stopPedal = true;
1148
+ const promises = [];
1149
+ channel.scheduledNotes.forEach((scheduledNotes) => {
1150
+ const activeNote = this.getActiveChannelNotes(scheduledNotes);
1151
+ if (activeNote) {
1152
+ const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
1153
+ promises.push(notePromise);
1154
+ }
1155
+ });
1156
+ return promises;
1157
+ }
1158
+ resetAllControllers(channelNumber) {
1159
+ Object.assign(this.channels[channelNumber], this.effectSettings);
1160
+ }
1161
+ allNotesOff(channelNumber) {
1162
+ const now = this.audioContext.currentTime;
1163
+ const channel = this.channels[channelNumber];
1164
+ const velocity = 0;
1165
+ const stopPedal = false;
1166
+ const promises = [];
1167
+ channel.scheduledNotes.forEach((scheduledNotes) => {
1168
+ const activeNote = this.getActiveChannelNotes(scheduledNotes);
1169
+ if (activeNote) {
1170
+ const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
1171
+ promises.push(notePromise);
1172
+ }
1173
+ });
1174
+ return promises;
1175
+ }
1176
+ omniOff() {
1177
+ this.omni = false;
1178
+ }
1179
+ omniOn() {
1180
+ this.omni = true;
1181
+ }
1182
+ monoOn() {
1183
+ this.mono = true;
1184
+ }
1185
+ polyOn() {
1186
+ this.mono = false;
1187
+ }
1188
+ handleUniversalNonRealTimeExclusiveMessage(data) {
1189
+ switch (data[2]) {
1190
+ case 9:
1191
+ switch (data[3]) {
1192
+ case 1:
1193
+ this.GM1SystemOn();
1194
+ break;
1195
+ case 2: // GM System Off
1196
+ break;
1197
+ case 3:
1198
+ this.GM2SystemOn();
1199
+ break;
1200
+ default:
1201
+ console.warn(`Unsupported Exclusive Message ${data}`);
1202
+ }
1203
+ break;
1204
+ default:
1205
+ console.warn(`Unsupported Exclusive Message ${data}`);
1206
+ }
1207
+ }
1208
+ GM1SystemOn() {
1209
+ this.channels.forEach((channel) => {
1210
+ channel.bankMSB = 0;
1211
+ channel.bankLSB = 0;
1212
+ channel.bank = 0;
1213
+ });
1214
+ this.channels[9].bankMSB = 120;
1215
+ this.channels[9].bank = 120 * 128;
1216
+ }
1217
+ GM2SystemOn() {
1218
+ this.channels.forEach((channel) => {
1219
+ channel.bankMSB = 121;
1220
+ channel.bankLSB = 0;
1221
+ channel.bank = 121 * 128;
1222
+ });
1223
+ this.channels[9].bankMSB = 120;
1224
+ this.channels[9].bank = 120 * 128;
1225
+ }
1226
+ handleUniversalRealTimeExclusiveMessage(data) {
1227
+ switch (data[2]) {
1228
+ case 4:
1229
+ switch (data[3]) {
1230
+ case 1:
1231
+ return this.handleMasterVolumeSysEx(data);
1232
+ case 3:
1233
+ return this.handleMasterFineTuning(data);
1234
+ case 4:
1235
+ return this.handleMasterCoarseTuning(data);
1236
+ // case 5: // TODO: Global Parameter Control
1237
+ default:
1238
+ console.warn(`Unsupported Exclusive Message ${data}`);
1239
+ }
1240
+ break;
1241
+ case 8:
1242
+ switch (data[3]) {
1243
+ // case 8:
1244
+ // // TODO
1245
+ // return this.handleScaleOctaveTuning1ByteFormat();
1246
+ default:
1247
+ console.warn(`Unsupported Exclusive Message ${data}`);
1248
+ }
1249
+ break;
1250
+ case 9:
1251
+ switch (data[3]) {
1252
+ // case 1:
1253
+ // // TODO
1254
+ // return this.handleChannelPressure();
1255
+ // case 3:
1256
+ // // TODO
1257
+ // return this.handleControlChange();
1258
+ default:
1259
+ console.warn(`Unsupported Exclusive Message ${data}`);
1260
+ }
1261
+ break;
1262
+ case 10:
1263
+ switch (data[3]) {
1264
+ // case 1:
1265
+ // // TODO
1266
+ // return this.handleKeyBasedInstrumentControl();
1267
+ default:
1268
+ console.warn(`Unsupported Exclusive Message ${data}`);
1269
+ }
1270
+ break;
1271
+ default:
1272
+ console.warn(`Unsupported Exclusive Message ${data}`);
1273
+ }
1274
+ }
1275
+ handleMasterVolumeSysEx(data) {
1276
+ const volume = (data[5] * 128 + data[4] - 8192) / 8192;
1277
+ this.handleMasterVolume(volume);
1278
+ }
1279
+ handleMasterVolume(volume) {
1280
+ const now = this.audioContext.currentTime;
1281
+ this.masterGain.gain.cancelScheduledValues(now);
1282
+ this.masterGain.gain.setValueAtTime(volume * volume, now);
1283
+ }
1284
+ handleMasterFineTuningSysEx(data) {
1285
+ const fineTuning = (data[5] * 128 + data[4] - 8192) / 8192;
1286
+ this.handleMasterFineTuning(fineTuning);
1287
+ }
1288
+ handleMasterFineTuning(fineTuning) {
1289
+ if (fineTuning < 0 && 1 < fineTuning) {
1290
+ console.error("Master Fine Tuning value is out of range");
1291
+ }
1292
+ else {
1293
+ this.masterFineTuning = fineTuning;
1294
+ }
1295
+ }
1296
+ handleMasterCoarseTuningSysEx(data) {
1297
+ const coarseTuning = data[4];
1298
+ this.handleMasterCoarseTuning(coarseTuning);
1299
+ }
1300
+ handleMasterCoarseTuning(coarseTuning) {
1301
+ if (coarseTuning < 0 && 127 < coarseTuning) {
1302
+ console.error("Master Coarse Tuning value is out of range");
1303
+ }
1304
+ else {
1305
+ this.masterCoarseTuning = coarseTuning - 64;
1306
+ }
1307
+ }
1308
+ handleExclusiveMessage(data) {
1309
+ console.warn(`Unsupported Exclusive Message ${data}`);
1310
+ }
1311
+ handleSysEx(data) {
1312
+ switch (data[0]) {
1313
+ case 126:
1314
+ return this.handleUniversalNonRealTimeExclusiveMessage(data);
1315
+ case 127:
1316
+ return this.handleUniversalRealTimeExclusiveMessage(data);
1317
+ default:
1318
+ return this.handleExclusiveMessage(data);
1319
+ }
1320
+ }
1321
+ scheduleTask(callback, startTime) {
1322
+ return new Promise((resolve) => {
1323
+ const bufferSource = new AudioBufferSourceNode(this.audioContext);
1324
+ bufferSource.onended = () => {
1325
+ callback();
1326
+ resolve();
1327
+ };
1328
+ bufferSource.start(startTime);
1329
+ bufferSource.stop(startTime);
1330
+ });
1331
+ }
1332
+ }
1333
+ exports.Midy = Midy;
1334
+ Object.defineProperty(Midy, "channelSettings", {
1335
+ enumerable: true,
1336
+ configurable: true,
1337
+ writable: true,
1338
+ value: {
1339
+ currentBufferSource: null,
1340
+ volume: 1,
1341
+ pan: 0,
1342
+ portamentoTime: 0,
1343
+ reverb: 0,
1344
+ chorus: 0,
1345
+ vibratoRate: 5,
1346
+ vibratoDepth: 0.5,
1347
+ vibratoDelay: 2.5,
1348
+ bank: 121 * 128,
1349
+ bankMSB: 121,
1350
+ bankLSB: 0,
1351
+ dataMSB: 0,
1352
+ dataLSB: 0,
1353
+ program: 0,
1354
+ pitchBend: 0,
1355
+ fineTuning: 0,
1356
+ coarseTuning: 0,
1357
+ modulationDepthRange: 2,
1358
+ }
1359
+ });
1360
+ Object.defineProperty(Midy, "effectSettings", {
1361
+ enumerable: true,
1362
+ configurable: true,
1363
+ writable: true,
1364
+ value: {
1365
+ expression: 1,
1366
+ modulation: 0,
1367
+ sustainPedal: false,
1368
+ portamento: false,
1369
+ sostenutoPedal: false,
1370
+ softPedal: 0,
1371
+ rpnMSB: 127,
1372
+ rpnLSB: 127,
1373
+ channelPressure: 0,
1374
+ pitchBendRange: 2,
1375
+ }
1376
+ });