@marmooo/midy 0.0.4 → 0.0.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/esm/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.d.ts +13 -6
- package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.d.ts.map +1 -0
- package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.js +5 -5
- package/esm/midy-GM1.d.ts +36 -42
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +214 -148
- package/esm/midy-GM2.d.ts +133 -25
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +230 -163
- package/esm/midy-GMLite.d.ts +37 -43
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +215 -149
- package/esm/midy.d.ts +41 -33
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +263 -179
- package/package.json +1 -1
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.d.ts +13 -6
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.d.ts.map +1 -0
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/{soundfont-parser@0.0.1 → soundfont-parser@0.0.2}/+esm.js +5 -5
- package/script/midy-GM1.d.ts +36 -42
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +214 -148
- package/script/midy-GM2.d.ts +133 -25
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +230 -163
- package/script/midy-GMLite.d.ts +37 -43
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +215 -149
- package/script/midy.d.ts +41 -33
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +263 -179
- package/esm/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +0 -1
- package/script/deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.1/+esm.d.ts.map +0 -1
package/esm/midy-GMLite.js
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
import { parseMidi } from "./deps/cdn.jsdelivr.net/npm/midi-file@1.2.4/+esm.js";
|
|
2
|
-
import { parse, SoundFont, } from "./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.
|
|
2
|
+
import { parse, SoundFont, } from "./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.js";
|
|
3
|
+
class Note {
|
|
4
|
+
constructor(noteNumber, velocity, startTime, instrumentKey) {
|
|
5
|
+
Object.defineProperty(this, "bufferSource", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: void 0
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "gainNode", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, "filterNode", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: void 0
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "modLFO", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: void 0
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, "modLFOGain", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: void 0
|
|
34
|
+
});
|
|
35
|
+
this.noteNumber = noteNumber;
|
|
36
|
+
this.velocity = velocity;
|
|
37
|
+
this.startTime = startTime;
|
|
38
|
+
this.instrumentKey = instrumentKey;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
3
41
|
export class MidyGMLite {
|
|
4
42
|
constructor(audioContext) {
|
|
5
43
|
Object.defineProperty(this, "ticksPerBeat", {
|
|
@@ -145,20 +183,16 @@ export class MidyGMLite {
|
|
|
145
183
|
this.totalTime = this.calcTotalTime();
|
|
146
184
|
}
|
|
147
185
|
setChannelAudioNodes(audioContext) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
modulationEffect.lfo.start();
|
|
156
|
-
pannerNode.connect(gainNode);
|
|
157
|
-
gainNode.connect(this.masterGain);
|
|
186
|
+
const { gainLeft, gainRight } = this.panToGain(MidyGMLite.channelSettings.pan);
|
|
187
|
+
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
188
|
+
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
189
|
+
const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
|
|
190
|
+
gainL.connect(merger, 0, 0);
|
|
191
|
+
gainR.connect(merger, 0, 1);
|
|
192
|
+
merger.connect(this.masterGain);
|
|
158
193
|
return {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
modulationEffect,
|
|
194
|
+
gainL,
|
|
195
|
+
gainR,
|
|
162
196
|
};
|
|
163
197
|
}
|
|
164
198
|
createChannels(audioContext) {
|
|
@@ -168,16 +202,15 @@ export class MidyGMLite {
|
|
|
168
202
|
...MidyGMLite.effectSettings,
|
|
169
203
|
...this.setChannelAudioNodes(audioContext),
|
|
170
204
|
scheduledNotes: new Map(),
|
|
171
|
-
sostenutoNotes: new Map(),
|
|
172
205
|
};
|
|
173
206
|
});
|
|
174
207
|
return channels;
|
|
175
208
|
}
|
|
176
|
-
async createNoteBuffer(
|
|
177
|
-
const sampleEnd =
|
|
209
|
+
async createNoteBuffer(instrumentKey, isSF3) {
|
|
210
|
+
const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
|
|
178
211
|
if (isSF3) {
|
|
179
|
-
const sample = new Uint8Array(
|
|
180
|
-
sample.set(
|
|
212
|
+
const sample = new Uint8Array(instrumentKey.sample.length);
|
|
213
|
+
sample.set(instrumentKey.sample);
|
|
181
214
|
const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
|
|
182
215
|
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
|
|
183
216
|
const channelData = audioBuffer.getChannelData(channel);
|
|
@@ -186,26 +219,27 @@ export class MidyGMLite {
|
|
|
186
219
|
return audioBuffer;
|
|
187
220
|
}
|
|
188
221
|
else {
|
|
189
|
-
const sample =
|
|
222
|
+
const sample = instrumentKey.sample.subarray(0, sampleEnd);
|
|
190
223
|
const floatSample = this.convertToFloat32Array(sample);
|
|
191
224
|
const audioBuffer = new AudioBuffer({
|
|
192
225
|
numberOfChannels: 1,
|
|
193
226
|
length: sample.length,
|
|
194
|
-
sampleRate:
|
|
227
|
+
sampleRate: instrumentKey.sampleRate,
|
|
195
228
|
});
|
|
196
229
|
const channelData = audioBuffer.getChannelData(0);
|
|
197
230
|
channelData.set(floatSample);
|
|
198
231
|
return audioBuffer;
|
|
199
232
|
}
|
|
200
233
|
}
|
|
201
|
-
async createNoteBufferNode(
|
|
234
|
+
async createNoteBufferNode(instrumentKey, isSF3) {
|
|
202
235
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
203
|
-
const audioBuffer = await this.createNoteBuffer(
|
|
236
|
+
const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
|
|
204
237
|
bufferSource.buffer = audioBuffer;
|
|
205
|
-
bufferSource.loop =
|
|
238
|
+
bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
|
|
206
239
|
if (bufferSource.loop) {
|
|
207
|
-
bufferSource.loopStart =
|
|
208
|
-
|
|
240
|
+
bufferSource.loopStart = instrumentKey.loopStart /
|
|
241
|
+
instrumentKey.sampleRate;
|
|
242
|
+
bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
|
|
209
243
|
}
|
|
210
244
|
return bufferSource;
|
|
211
245
|
}
|
|
@@ -243,7 +277,7 @@ export class MidyGMLite {
|
|
|
243
277
|
this.handleProgramChange(event.channel, event.programNumber);
|
|
244
278
|
break;
|
|
245
279
|
case "pitchBend":
|
|
246
|
-
this.
|
|
280
|
+
this.setPitchBend(event.channel, event.value);
|
|
247
281
|
break;
|
|
248
282
|
case "sysEx":
|
|
249
283
|
this.handleSysEx(event.data);
|
|
@@ -323,7 +357,6 @@ export class MidyGMLite {
|
|
|
323
357
|
const tmpChannels = new Array(16);
|
|
324
358
|
for (let i = 0; i < tmpChannels.length; i++) {
|
|
325
359
|
tmpChannels[i] = {
|
|
326
|
-
durationTicks: new Map(),
|
|
327
360
|
programNumber: -1,
|
|
328
361
|
bank: this.channels[i].bank,
|
|
329
362
|
};
|
|
@@ -340,16 +373,6 @@ export class MidyGMLite {
|
|
|
340
373
|
instruments.add(`${channel.bank}:0`);
|
|
341
374
|
channel.programNumber = 0;
|
|
342
375
|
}
|
|
343
|
-
channel.durationTicks.set(event.noteNumber, {
|
|
344
|
-
ticks: event.ticks,
|
|
345
|
-
noteOn: event,
|
|
346
|
-
});
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
case "noteOff": {
|
|
350
|
-
const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
|
|
351
|
-
.get(event.noteNumber);
|
|
352
|
-
noteOn.durationTicks = event.ticks - ticks;
|
|
353
376
|
break;
|
|
354
377
|
}
|
|
355
378
|
case "programChange": {
|
|
@@ -363,8 +386,8 @@ export class MidyGMLite {
|
|
|
363
386
|
});
|
|
364
387
|
});
|
|
365
388
|
const priority = {
|
|
366
|
-
|
|
367
|
-
|
|
389
|
+
controller: 0,
|
|
390
|
+
sysEx: 1,
|
|
368
391
|
};
|
|
369
392
|
timeline.sort((a, b) => {
|
|
370
393
|
if (a.ticks !== b.ticks)
|
|
@@ -447,30 +470,26 @@ export class MidyGMLite {
|
|
|
447
470
|
const now = this.audioContext.currentTime;
|
|
448
471
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
449
472
|
}
|
|
450
|
-
getActiveNotes(channel) {
|
|
473
|
+
getActiveNotes(channel, time) {
|
|
451
474
|
const activeNotes = new Map();
|
|
452
|
-
channel.scheduledNotes.forEach((
|
|
453
|
-
const activeNote = this.
|
|
475
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
476
|
+
const activeNote = this.getActiveNote(noteList, time);
|
|
454
477
|
if (activeNote) {
|
|
455
478
|
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
456
479
|
}
|
|
457
480
|
});
|
|
458
481
|
return activeNotes;
|
|
459
482
|
}
|
|
460
|
-
|
|
461
|
-
for (let i =
|
|
462
|
-
const
|
|
463
|
-
if (
|
|
464
|
-
return
|
|
483
|
+
getActiveNote(noteList, time) {
|
|
484
|
+
for (let i = noteList.length - 1; i >= 0; i--) {
|
|
485
|
+
const note = noteList[i];
|
|
486
|
+
if (!note)
|
|
487
|
+
return;
|
|
488
|
+
if (time < note.startTime)
|
|
489
|
+
continue;
|
|
490
|
+
return (note.ending) ? null : note;
|
|
465
491
|
}
|
|
466
|
-
|
|
467
|
-
createModulationEffect(audioContext) {
|
|
468
|
-
const lfo = new OscillatorNode(audioContext, {
|
|
469
|
-
frequency: 5,
|
|
470
|
-
});
|
|
471
|
-
return {
|
|
472
|
-
lfo,
|
|
473
|
-
};
|
|
492
|
+
return noteList[0];
|
|
474
493
|
}
|
|
475
494
|
connectNoteEffects(channel, gainNode) {
|
|
476
495
|
gainNode.connect(channel.pannerNode);
|
|
@@ -482,73 +501,89 @@ export class MidyGMLite {
|
|
|
482
501
|
return 8.176 * Math.pow(2, cent / 1200);
|
|
483
502
|
}
|
|
484
503
|
calcSemitoneOffset(channel) {
|
|
485
|
-
return channel.pitchBend * channel.pitchBendRange
|
|
504
|
+
return channel.pitchBend * channel.pitchBendRange;
|
|
486
505
|
}
|
|
487
|
-
calcPlaybackRate(
|
|
488
|
-
return
|
|
506
|
+
calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
|
|
507
|
+
return instrumentKey.playbackRate(noteNumber) *
|
|
508
|
+
Math.pow(2, semitoneOffset / 12);
|
|
489
509
|
}
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
|
|
494
|
-
// volume envelope
|
|
495
|
-
const gainNode = new GainNode(this.audioContext, {
|
|
496
|
-
gain: 0,
|
|
497
|
-
});
|
|
510
|
+
setVolumeEnvelope(channel, note) {
|
|
511
|
+
const { instrumentKey, startTime, velocity } = note;
|
|
512
|
+
note.gainNode = new GainNode(this.audioContext, { gain: 0 });
|
|
498
513
|
let volume = (velocity / 127) * channel.volume * channel.expression;
|
|
499
514
|
if (volume === 0)
|
|
500
515
|
volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
|
|
501
|
-
const attackVolume = this.cbToRatio(-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
516
|
+
const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
|
|
517
|
+
volume;
|
|
518
|
+
const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
|
|
519
|
+
const volDelay = startTime + instrumentKey.volDelay;
|
|
520
|
+
const volAttack = volDelay + instrumentKey.volAttack;
|
|
521
|
+
const volHold = volAttack + instrumentKey.volHold;
|
|
522
|
+
const volDecay = volHold + instrumentKey.volDecay;
|
|
523
|
+
note.gainNode.gain
|
|
508
524
|
.setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
509
525
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
510
526
|
.setValueAtTime(attackVolume, volHold)
|
|
511
527
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
512
|
-
|
|
528
|
+
}
|
|
529
|
+
setFilterEnvelope(channel, note) {
|
|
530
|
+
const { instrumentKey, startTime, noteNumber } = note;
|
|
531
|
+
const softPedalFactor = 1 -
|
|
532
|
+
(0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
|
|
513
533
|
const maxFreq = this.audioContext.sampleRate / 2;
|
|
514
|
-
const baseFreq = this.centToHz(
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
534
|
+
const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
|
|
535
|
+
softPedalFactor;
|
|
536
|
+
const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
|
|
537
|
+
const sustainFreq = (baseFreq +
|
|
538
|
+
(peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
|
|
539
|
+
const modDelay = startTime + instrumentKey.modDelay;
|
|
540
|
+
const modAttack = modDelay + instrumentKey.modAttack;
|
|
541
|
+
const modHold = modAttack + instrumentKey.modHold;
|
|
542
|
+
const modDecay = modHold + instrumentKey.modDecay;
|
|
518
543
|
const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
|
|
519
544
|
const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
|
|
520
545
|
const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
|
|
521
|
-
|
|
546
|
+
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
522
547
|
type: "lowpass",
|
|
523
|
-
Q:
|
|
548
|
+
Q: instrumentKey.initialFilterQ / 10, // dB
|
|
524
549
|
frequency: adjustedBaseFreq,
|
|
525
550
|
});
|
|
526
|
-
|
|
527
|
-
const modAttack = modDelay + noteInfo.modAttack;
|
|
528
|
-
const modHold = modAttack + noteInfo.modHold;
|
|
529
|
-
const modDecay = modHold + noteInfo.modDecay;
|
|
530
|
-
filterNode.frequency
|
|
551
|
+
note.filterNode.frequency
|
|
531
552
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
532
553
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
533
554
|
.setValueAtTime(adjustedPeekFreq, modHold)
|
|
534
555
|
.linearRampToValueAtTime(adjustedSustainFreq, modDecay);
|
|
535
|
-
|
|
556
|
+
note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
|
|
557
|
+
}
|
|
558
|
+
startModulation(channel, note, time) {
|
|
559
|
+
const { instrumentKey } = note;
|
|
560
|
+
note.modLFOGain = new GainNode(this.audioContext, {
|
|
561
|
+
gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
|
|
562
|
+
});
|
|
563
|
+
note.modLFO = new OscillatorNode(this.audioContext, {
|
|
564
|
+
frequency: this.centToHz(instrumentKey.freqModLFO),
|
|
565
|
+
});
|
|
566
|
+
note.modLFO.start(time);
|
|
567
|
+
note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
|
|
568
|
+
note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
|
|
569
|
+
note.modLFO.connect(note.modLFOGain);
|
|
570
|
+
note.modLFOGain.connect(note.bufferSource.detune);
|
|
571
|
+
}
|
|
572
|
+
async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
|
|
573
|
+
const semitoneOffset = this.calcSemitoneOffset(channel);
|
|
574
|
+
const note = new Note(noteNumber, velocity, startTime, instrumentKey);
|
|
575
|
+
note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
|
|
576
|
+
note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
|
|
577
|
+
this.setVolumeEnvelope(channel, note);
|
|
578
|
+
this.setFilterEnvelope(channel, note);
|
|
536
579
|
if (channel.modulation > 0) {
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
lfoGain = new GainNode(this.audioContext, {
|
|
540
|
-
gain: 0,
|
|
541
|
-
});
|
|
542
|
-
lfoGain.gain
|
|
543
|
-
.setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
544
|
-
.exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
|
|
545
|
-
channel.modulationEffect.lfo.connect(lfoGain);
|
|
546
|
-
lfoGain.connect(bufferSource.detune);
|
|
580
|
+
const delayModLFO = startTime + instrumentKey.delayModLFO;
|
|
581
|
+
this.startModulation(channel, note, delayModLFO);
|
|
547
582
|
}
|
|
548
|
-
bufferSource.connect(filterNode);
|
|
549
|
-
filterNode.connect(gainNode);
|
|
550
|
-
bufferSource.start(startTime,
|
|
551
|
-
return
|
|
583
|
+
note.bufferSource.connect(note.filterNode);
|
|
584
|
+
note.filterNode.connect(note.gainNode);
|
|
585
|
+
note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
|
|
586
|
+
return note;
|
|
552
587
|
}
|
|
553
588
|
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
554
589
|
const channel = this.channels[channelNumber];
|
|
@@ -558,27 +593,17 @@ export class MidyGMLite {
|
|
|
558
593
|
return;
|
|
559
594
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
560
595
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
561
|
-
const
|
|
562
|
-
if (!
|
|
596
|
+
const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
|
|
597
|
+
if (!instrumentKey)
|
|
563
598
|
return;
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
this.connectNoteEffects(channel, gainNode);
|
|
599
|
+
const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
|
|
600
|
+
this.connectNoteEffects(channel, note.gainNode);
|
|
567
601
|
const scheduledNotes = channel.scheduledNotes;
|
|
568
|
-
const scheduledNote = {
|
|
569
|
-
bufferSource,
|
|
570
|
-
filterNode,
|
|
571
|
-
gainNode,
|
|
572
|
-
lfoGain,
|
|
573
|
-
noteInfo,
|
|
574
|
-
noteNumber,
|
|
575
|
-
startTime,
|
|
576
|
-
};
|
|
577
602
|
if (scheduledNotes.has(noteNumber)) {
|
|
578
|
-
scheduledNotes.get(noteNumber).push(
|
|
603
|
+
scheduledNotes.get(noteNumber).push(note);
|
|
579
604
|
}
|
|
580
605
|
else {
|
|
581
|
-
scheduledNotes.set(noteNumber, [
|
|
606
|
+
scheduledNotes.set(noteNumber, [note]);
|
|
582
607
|
}
|
|
583
608
|
}
|
|
584
609
|
noteOn(channelNumber, noteNumber, velocity) {
|
|
@@ -598,15 +623,15 @@ export class MidyGMLite {
|
|
|
598
623
|
continue;
|
|
599
624
|
if (targetNote.ending)
|
|
600
625
|
continue;
|
|
601
|
-
const { bufferSource, filterNode, gainNode,
|
|
626
|
+
const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, instrumentKey, } = targetNote;
|
|
602
627
|
const velocityRate = (velocity + 127) / 127;
|
|
603
|
-
const volEndTime = stopTime +
|
|
628
|
+
const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
|
|
604
629
|
gainNode.gain.cancelScheduledValues(stopTime);
|
|
605
630
|
gainNode.gain.linearRampToValueAtTime(0, volEndTime);
|
|
606
631
|
const maxFreq = this.audioContext.sampleRate / 2;
|
|
607
|
-
const baseFreq = this.centToHz(
|
|
632
|
+
const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
|
|
608
633
|
const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
|
|
609
|
-
const modEndTime = stopTime +
|
|
634
|
+
const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
|
|
610
635
|
filterNode.frequency
|
|
611
636
|
.cancelScheduledValues(stopTime)
|
|
612
637
|
.linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
|
|
@@ -620,8 +645,10 @@ export class MidyGMLite {
|
|
|
620
645
|
bufferSource.disconnect(0);
|
|
621
646
|
filterNode.disconnect(0);
|
|
622
647
|
gainNode.disconnect(0);
|
|
623
|
-
if (
|
|
624
|
-
|
|
648
|
+
if (modLFOGain)
|
|
649
|
+
modLFOGain.disconnect(0);
|
|
650
|
+
if (modLFO)
|
|
651
|
+
modLFO.stop();
|
|
625
652
|
resolve();
|
|
626
653
|
};
|
|
627
654
|
bufferSource.stop(volEndTime);
|
|
@@ -672,20 +699,22 @@ export class MidyGMLite {
|
|
|
672
699
|
}
|
|
673
700
|
handlePitchBendMessage(channelNumber, lsb, msb) {
|
|
674
701
|
const pitchBend = msb * 128 + lsb;
|
|
675
|
-
this.
|
|
702
|
+
this.setPitchBend(channelNumber, pitchBend);
|
|
676
703
|
}
|
|
677
|
-
|
|
704
|
+
setPitchBend(channelNumber, pitchBend) {
|
|
678
705
|
const now = this.audioContext.currentTime;
|
|
679
706
|
const channel = this.channels[channelNumber];
|
|
707
|
+
const prevPitchBend = channel.pitchBend;
|
|
680
708
|
channel.pitchBend = (pitchBend - 8192) / 8192;
|
|
681
|
-
const
|
|
682
|
-
|
|
709
|
+
const detuneChange = (channel.pitchBend - prevPitchBend) *
|
|
710
|
+
channel.pitchBendRange * 100;
|
|
711
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
683
712
|
activeNotes.forEach((activeNote) => {
|
|
684
|
-
const { bufferSource
|
|
685
|
-
const
|
|
686
|
-
bufferSource.
|
|
713
|
+
const { bufferSource } = activeNote;
|
|
714
|
+
const detune = bufferSource.detune.value + detuneChange;
|
|
715
|
+
bufferSource.detune
|
|
687
716
|
.cancelScheduledValues(now)
|
|
688
|
-
.setValueAtTime(
|
|
717
|
+
.setValueAtTime(detune, now);
|
|
689
718
|
});
|
|
690
719
|
}
|
|
691
720
|
handleControlChange(channelNumber, controller, value) {
|
|
@@ -719,21 +748,37 @@ export class MidyGMLite {
|
|
|
719
748
|
}
|
|
720
749
|
}
|
|
721
750
|
setModulation(channelNumber, modulation) {
|
|
751
|
+
const now = this.audioContext.currentTime;
|
|
722
752
|
const channel = this.channels[channelNumber];
|
|
723
753
|
channel.modulation = (modulation / 127) *
|
|
724
754
|
(channel.modulationDepthRange * 100);
|
|
755
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
756
|
+
activeNotes.forEach((activeNote) => {
|
|
757
|
+
if (activeNote.modLFO) {
|
|
758
|
+
activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
|
|
759
|
+
channel.modulation, now);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
this.startModulation(channel, activeNote, now);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
725
765
|
}
|
|
726
766
|
setVolume(channelNumber, volume) {
|
|
727
767
|
const channel = this.channels[channelNumber];
|
|
728
768
|
channel.volume = volume / 127;
|
|
729
769
|
this.updateChannelGain(channel);
|
|
730
770
|
}
|
|
771
|
+
panToGain(pan) {
|
|
772
|
+
const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
|
|
773
|
+
return {
|
|
774
|
+
gainLeft: Math.cos(theta),
|
|
775
|
+
gainRight: Math.sin(theta),
|
|
776
|
+
};
|
|
777
|
+
}
|
|
731
778
|
setPan(channelNumber, pan) {
|
|
732
|
-
const now = this.audioContext.currentTime;
|
|
733
779
|
const channel = this.channels[channelNumber];
|
|
734
|
-
channel.pan = pan
|
|
735
|
-
|
|
736
|
-
channel.pannerNode.pan.setValueAtTime(channel.pan, now);
|
|
780
|
+
channel.pan = pan;
|
|
781
|
+
this.updateChannelGain(channel);
|
|
737
782
|
}
|
|
738
783
|
setExpression(channelNumber, expression) {
|
|
739
784
|
const channel = this.channels[channelNumber];
|
|
@@ -743,8 +788,13 @@ export class MidyGMLite {
|
|
|
743
788
|
updateChannelGain(channel) {
|
|
744
789
|
const now = this.audioContext.currentTime;
|
|
745
790
|
const volume = channel.volume * channel.expression;
|
|
746
|
-
channel.
|
|
747
|
-
channel.
|
|
791
|
+
const { gainLeft, gainRight } = this.panToGain(channel.pan);
|
|
792
|
+
channel.gainL.gain
|
|
793
|
+
.cancelScheduledValues(now)
|
|
794
|
+
.setValueAtTime(volume * gainLeft, now);
|
|
795
|
+
channel.gainR.gain
|
|
796
|
+
.cancelScheduledValues(now)
|
|
797
|
+
.setValueAtTime(volume * gainRight, now);
|
|
748
798
|
}
|
|
749
799
|
setSustainPedal(channelNumber, value) {
|
|
750
800
|
const isOn = value >= 64;
|
|
@@ -766,20 +816,39 @@ export class MidyGMLite {
|
|
|
766
816
|
const { dataMSB, dataLSB } = channel;
|
|
767
817
|
switch (rpn) {
|
|
768
818
|
case 0:
|
|
769
|
-
|
|
770
|
-
break;
|
|
819
|
+
return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
|
|
771
820
|
default:
|
|
772
821
|
console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
|
|
773
822
|
}
|
|
774
823
|
}
|
|
824
|
+
handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
|
|
825
|
+
const pitchBendRange = dataMSB + dataLSB / 100;
|
|
826
|
+
this.setPitchBendRange(channelNumber, pitchBendRange);
|
|
827
|
+
}
|
|
828
|
+
setPitchBendRange(channelNumber, pitchBendRange) {
|
|
829
|
+
const now = this.audioContext.currentTime;
|
|
830
|
+
const channel = this.channels[channelNumber];
|
|
831
|
+
const prevPitchBendRange = channel.pitchBendRange;
|
|
832
|
+
channel.pitchBendRange = pitchBendRange;
|
|
833
|
+
const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
|
|
834
|
+
channel.pitchBend * 100;
|
|
835
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
836
|
+
activeNotes.forEach((activeNote) => {
|
|
837
|
+
const { bufferSource } = activeNote;
|
|
838
|
+
const detune = bufferSource.detune.value + detuneChange;
|
|
839
|
+
bufferSource.detune
|
|
840
|
+
.cancelScheduledValues(now)
|
|
841
|
+
.setValueAtTime(detune, now);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
775
844
|
allSoundOff(channelNumber) {
|
|
776
845
|
const now = this.audioContext.currentTime;
|
|
777
846
|
const channel = this.channels[channelNumber];
|
|
778
847
|
const velocity = 0;
|
|
779
848
|
const stopPedal = true;
|
|
780
849
|
const promises = [];
|
|
781
|
-
channel.scheduledNotes.forEach((
|
|
782
|
-
const activeNote = this.
|
|
850
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
851
|
+
const activeNote = this.getActiveNote(noteList, now);
|
|
783
852
|
if (activeNote) {
|
|
784
853
|
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
785
854
|
promises.push(notePromise);
|
|
@@ -796,8 +865,8 @@ export class MidyGMLite {
|
|
|
796
865
|
const velocity = 0;
|
|
797
866
|
const stopPedal = false;
|
|
798
867
|
const promises = [];
|
|
799
|
-
channel.scheduledNotes.forEach((
|
|
800
|
-
const activeNote = this.
|
|
868
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
869
|
+
const activeNote = this.getActiveNote(noteList, now);
|
|
801
870
|
if (activeNote) {
|
|
802
871
|
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
803
872
|
promises.push(notePromise);
|
|
@@ -847,9 +916,9 @@ export class MidyGMLite {
|
|
|
847
916
|
}
|
|
848
917
|
handleMasterVolumeSysEx(data) {
|
|
849
918
|
const volume = (data[5] * 128 + data[4]) / 16383;
|
|
850
|
-
this.
|
|
919
|
+
this.setMasterVolume(volume);
|
|
851
920
|
}
|
|
852
|
-
|
|
921
|
+
setMasterVolume(volume) {
|
|
853
922
|
if (volume < 0 && 1 < volume) {
|
|
854
923
|
console.error("Master Volume is out of range");
|
|
855
924
|
}
|
|
@@ -890,10 +959,7 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
|
|
|
890
959
|
writable: true,
|
|
891
960
|
value: {
|
|
892
961
|
volume: 100 / 127,
|
|
893
|
-
pan:
|
|
894
|
-
vibratoRate: 5,
|
|
895
|
-
vibratoDepth: 0.5,
|
|
896
|
-
vibratoDelay: 2.5,
|
|
962
|
+
pan: 64,
|
|
897
963
|
bank: 0,
|
|
898
964
|
dataMSB: 0,
|
|
899
965
|
dataLSB: 0,
|