@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/script/midy.js
CHANGED
|
@@ -2,7 +2,57 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Midy = void 0;
|
|
4
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.
|
|
5
|
+
const _esm_js_2 = require("./deps/cdn.jsdelivr.net/npm/@marmooo/soundfont-parser@0.0.2/+esm.js");
|
|
6
|
+
class Note {
|
|
7
|
+
constructor(noteNumber, velocity, startTime, instrumentKey) {
|
|
8
|
+
Object.defineProperty(this, "bufferSource", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: void 0
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "gainNode", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: void 0
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(this, "filterNode", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: void 0
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(this, "modLFO", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
value: void 0
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(this, "modLFOGain", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: void 0
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(this, "vibLFO", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: void 0
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(this, "vibLFOGain", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: void 0
|
|
49
|
+
});
|
|
50
|
+
this.noteNumber = noteNumber;
|
|
51
|
+
this.velocity = velocity;
|
|
52
|
+
this.startTime = startTime;
|
|
53
|
+
this.instrumentKey = instrumentKey;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
6
56
|
class Midy {
|
|
7
57
|
constructor(audioContext) {
|
|
8
58
|
Object.defineProperty(this, "ticksPerBeat", {
|
|
@@ -178,25 +228,23 @@ class Midy {
|
|
|
178
228
|
this.totalTime = this.calcTotalTime();
|
|
179
229
|
}
|
|
180
230
|
setChannelAudioNodes(audioContext) {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
});
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
231
|
+
const { gainLeft, gainRight } = this.panToGain(Midy.channelSettings.pan);
|
|
232
|
+
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
233
|
+
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
234
|
+
const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
|
|
235
|
+
gainL.connect(merger, 0, 0);
|
|
236
|
+
gainR.connect(merger, 0, 1);
|
|
237
|
+
merger.connect(this.masterGain);
|
|
188
238
|
const reverbEffect = this.createReverbEffect(audioContext);
|
|
189
239
|
const chorusEffect = this.createChorusEffect(audioContext);
|
|
190
|
-
modulationEffect.lfo.start();
|
|
191
240
|
chorusEffect.lfo.start();
|
|
192
|
-
reverbEffect.dryGain.connect(
|
|
193
|
-
reverbEffect.
|
|
194
|
-
|
|
195
|
-
|
|
241
|
+
reverbEffect.dryGain.connect(gainL);
|
|
242
|
+
reverbEffect.dryGain.connect(gainR);
|
|
243
|
+
reverbEffect.wetGain.connect(gainL);
|
|
244
|
+
reverbEffect.wetGain.connect(gainR);
|
|
196
245
|
return {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
modulationEffect,
|
|
246
|
+
gainL,
|
|
247
|
+
gainR,
|
|
200
248
|
reverbEffect,
|
|
201
249
|
chorusEffect,
|
|
202
250
|
};
|
|
@@ -219,11 +267,11 @@ class Midy {
|
|
|
219
267
|
});
|
|
220
268
|
return channels;
|
|
221
269
|
}
|
|
222
|
-
async createNoteBuffer(
|
|
223
|
-
const sampleEnd =
|
|
270
|
+
async createNoteBuffer(instrumentKey, isSF3) {
|
|
271
|
+
const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
|
|
224
272
|
if (isSF3) {
|
|
225
|
-
const sample = new Uint8Array(
|
|
226
|
-
sample.set(
|
|
273
|
+
const sample = new Uint8Array(instrumentKey.sample.length);
|
|
274
|
+
sample.set(instrumentKey.sample);
|
|
227
275
|
const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
|
|
228
276
|
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
|
|
229
277
|
const channelData = audioBuffer.getChannelData(channel);
|
|
@@ -232,26 +280,27 @@ class Midy {
|
|
|
232
280
|
return audioBuffer;
|
|
233
281
|
}
|
|
234
282
|
else {
|
|
235
|
-
const sample =
|
|
283
|
+
const sample = instrumentKey.sample.subarray(0, sampleEnd);
|
|
236
284
|
const floatSample = this.convertToFloat32Array(sample);
|
|
237
285
|
const audioBuffer = new AudioBuffer({
|
|
238
286
|
numberOfChannels: 1,
|
|
239
287
|
length: sample.length,
|
|
240
|
-
sampleRate:
|
|
288
|
+
sampleRate: instrumentKey.sampleRate,
|
|
241
289
|
});
|
|
242
290
|
const channelData = audioBuffer.getChannelData(0);
|
|
243
291
|
channelData.set(floatSample);
|
|
244
292
|
return audioBuffer;
|
|
245
293
|
}
|
|
246
294
|
}
|
|
247
|
-
async createNoteBufferNode(
|
|
295
|
+
async createNoteBufferNode(instrumentKey, isSF3) {
|
|
248
296
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
249
|
-
const audioBuffer = await this.createNoteBuffer(
|
|
297
|
+
const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
|
|
250
298
|
bufferSource.buffer = audioBuffer;
|
|
251
|
-
bufferSource.loop =
|
|
299
|
+
bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
|
|
252
300
|
if (bufferSource.loop) {
|
|
253
|
-
bufferSource.loopStart =
|
|
254
|
-
|
|
301
|
+
bufferSource.loopStart = instrumentKey.loopStart /
|
|
302
|
+
instrumentKey.sampleRate;
|
|
303
|
+
bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
|
|
255
304
|
}
|
|
256
305
|
return bufferSource;
|
|
257
306
|
}
|
|
@@ -295,7 +344,7 @@ class Midy {
|
|
|
295
344
|
this.handleChannelPressure(event.channel, event.amount);
|
|
296
345
|
break;
|
|
297
346
|
case "pitchBend":
|
|
298
|
-
this.
|
|
347
|
+
this.setPitchBend(event.channel, event.value);
|
|
299
348
|
break;
|
|
300
349
|
case "sysEx":
|
|
301
350
|
this.handleSysEx(event.data);
|
|
@@ -375,7 +424,6 @@ class Midy {
|
|
|
375
424
|
const tmpChannels = new Array(16);
|
|
376
425
|
for (let i = 0; i < tmpChannels.length; i++) {
|
|
377
426
|
tmpChannels[i] = {
|
|
378
|
-
durationTicks: new Map(),
|
|
379
427
|
programNumber: -1,
|
|
380
428
|
bankMSB: this.channels[i].bankMSB,
|
|
381
429
|
bankLSB: this.channels[i].bankLSB,
|
|
@@ -405,16 +453,6 @@ class Midy {
|
|
|
405
453
|
}
|
|
406
454
|
channel.programNumber = 0;
|
|
407
455
|
}
|
|
408
|
-
channel.durationTicks.set(event.noteNumber, {
|
|
409
|
-
ticks: event.ticks,
|
|
410
|
-
noteOn: event,
|
|
411
|
-
});
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
case "noteOff": {
|
|
415
|
-
const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
|
|
416
|
-
.get(event.noteNumber);
|
|
417
|
-
noteOn.durationTicks = event.ticks - ticks;
|
|
418
456
|
break;
|
|
419
457
|
}
|
|
420
458
|
case "controller":
|
|
@@ -449,8 +487,8 @@ class Midy {
|
|
|
449
487
|
});
|
|
450
488
|
});
|
|
451
489
|
const priority = {
|
|
452
|
-
|
|
453
|
-
|
|
490
|
+
controller: 0,
|
|
491
|
+
sysEx: 1,
|
|
454
492
|
};
|
|
455
493
|
timeline.sort((a, b) => {
|
|
456
494
|
if (a.ticks !== b.ticks)
|
|
@@ -533,30 +571,26 @@ class Midy {
|
|
|
533
571
|
const now = this.audioContext.currentTime;
|
|
534
572
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
535
573
|
}
|
|
536
|
-
getActiveNotes(channel) {
|
|
574
|
+
getActiveNotes(channel, time) {
|
|
537
575
|
const activeNotes = new Map();
|
|
538
|
-
channel.scheduledNotes.forEach((
|
|
539
|
-
const activeNote = this.
|
|
576
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
577
|
+
const activeNote = this.getActiveNote(noteList, time);
|
|
540
578
|
if (activeNote) {
|
|
541
579
|
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
542
580
|
}
|
|
543
581
|
});
|
|
544
582
|
return activeNotes;
|
|
545
583
|
}
|
|
546
|
-
|
|
547
|
-
for (let i =
|
|
548
|
-
const
|
|
549
|
-
if (
|
|
550
|
-
return
|
|
584
|
+
getActiveNote(noteList, time) {
|
|
585
|
+
for (let i = noteList.length - 1; i >= 0; i--) {
|
|
586
|
+
const note = noteList[i];
|
|
587
|
+
if (!note)
|
|
588
|
+
return;
|
|
589
|
+
if (time < note.startTime)
|
|
590
|
+
continue;
|
|
591
|
+
return (note.ending) ? null : note;
|
|
551
592
|
}
|
|
552
|
-
|
|
553
|
-
createModulationEffect(audioContext) {
|
|
554
|
-
const lfo = new OscillatorNode(audioContext, {
|
|
555
|
-
frequency: 5,
|
|
556
|
-
});
|
|
557
|
-
return {
|
|
558
|
-
lfo,
|
|
559
|
-
};
|
|
593
|
+
return noteList[0];
|
|
560
594
|
}
|
|
561
595
|
createReverbEffect(audioContext, options = {}) {
|
|
562
596
|
const { decay = 0.8, preDecay = 0, } = options;
|
|
@@ -592,12 +626,8 @@ class Midy {
|
|
|
592
626
|
}
|
|
593
627
|
createChorusEffect(audioContext, options = {}) {
|
|
594
628
|
const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
|
|
595
|
-
const lfo = new OscillatorNode(audioContext, {
|
|
596
|
-
|
|
597
|
-
});
|
|
598
|
-
const lfoGain = new GainNode(audioContext, {
|
|
599
|
-
gain: chorusDepth,
|
|
600
|
-
});
|
|
629
|
+
const lfo = new OscillatorNode(audioContext, { frequency: chorusRate });
|
|
630
|
+
const lfoGain = new GainNode(audioContext, { gain: chorusDepth });
|
|
601
631
|
const chorusGains = [];
|
|
602
632
|
const delayNodes = [];
|
|
603
633
|
const baseGain = 1 / chorusCount;
|
|
@@ -608,9 +638,7 @@ class Midy {
|
|
|
608
638
|
maxDelayTime: delayTime,
|
|
609
639
|
});
|
|
610
640
|
delayNodes.push(delayNode);
|
|
611
|
-
const chorusGain = new GainNode(audioContext, {
|
|
612
|
-
gain: baseGain,
|
|
613
|
-
});
|
|
641
|
+
const chorusGain = new GainNode(audioContext, { gain: baseGain });
|
|
614
642
|
chorusGains.push(chorusGain);
|
|
615
643
|
lfo.connect(lfoGain);
|
|
616
644
|
lfoGain.connect(delayNode.delayTime);
|
|
@@ -666,77 +694,108 @@ class Midy {
|
|
|
666
694
|
const tuning = masterTuning + channelTuning;
|
|
667
695
|
return channel.pitchBend * channel.pitchBendRange + tuning;
|
|
668
696
|
}
|
|
669
|
-
calcPlaybackRate(
|
|
670
|
-
return
|
|
697
|
+
calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
|
|
698
|
+
return instrumentKey.playbackRate(noteNumber) *
|
|
699
|
+
Math.pow(2, semitoneOffset / 12);
|
|
671
700
|
}
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
|
|
676
|
-
// volume envelope
|
|
677
|
-
const gainNode = new GainNode(this.audioContext, {
|
|
678
|
-
gain: 0,
|
|
679
|
-
});
|
|
701
|
+
setVolumeEnvelope(channel, note) {
|
|
702
|
+
const { instrumentKey, startTime, velocity } = note;
|
|
703
|
+
note.gainNode = new GainNode(this.audioContext, { gain: 0 });
|
|
680
704
|
let volume = (velocity / 127) * channel.volume * channel.expression;
|
|
681
705
|
if (volume === 0)
|
|
682
706
|
volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
|
|
683
|
-
const attackVolume = this.cbToRatio(-
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
const
|
|
687
|
-
const
|
|
688
|
-
const
|
|
689
|
-
|
|
707
|
+
const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
|
|
708
|
+
volume;
|
|
709
|
+
const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
|
|
710
|
+
const volDelay = startTime + instrumentKey.volDelay;
|
|
711
|
+
const volAttack = volDelay + instrumentKey.volAttack;
|
|
712
|
+
const volHold = volAttack + instrumentKey.volHold;
|
|
713
|
+
const volDecay = volHold + instrumentKey.volDecay;
|
|
714
|
+
note.gainNode.gain
|
|
690
715
|
.setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
691
716
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
692
717
|
.setValueAtTime(attackVolume, volHold)
|
|
693
718
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
694
|
-
|
|
719
|
+
}
|
|
720
|
+
setFilterEnvelope(channel, note) {
|
|
721
|
+
const { instrumentKey, startTime, noteNumber } = note;
|
|
695
722
|
const softPedalFactor = 1 -
|
|
696
723
|
(0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
|
|
697
724
|
const maxFreq = this.audioContext.sampleRate / 2;
|
|
698
|
-
const baseFreq = this.centToHz(
|
|
699
|
-
|
|
725
|
+
const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
|
|
726
|
+
softPedalFactor;
|
|
727
|
+
const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
|
|
700
728
|
const sustainFreq = (baseFreq +
|
|
701
|
-
(peekFreq - baseFreq) * (1 -
|
|
729
|
+
(peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
|
|
730
|
+
const modDelay = startTime + instrumentKey.modDelay;
|
|
731
|
+
const modAttack = modDelay + instrumentKey.modAttack;
|
|
732
|
+
const modHold = modAttack + instrumentKey.modHold;
|
|
733
|
+
const modDecay = modHold + instrumentKey.modDecay;
|
|
702
734
|
const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
|
|
703
735
|
const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
|
|
704
736
|
const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
|
|
705
|
-
|
|
737
|
+
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
706
738
|
type: "lowpass",
|
|
707
|
-
Q:
|
|
739
|
+
Q: instrumentKey.initialFilterQ / 10, // dB
|
|
708
740
|
frequency: adjustedBaseFreq,
|
|
709
741
|
});
|
|
710
|
-
|
|
711
|
-
const modAttack = modDelay + noteInfo.modAttack;
|
|
712
|
-
const modHold = modAttack + noteInfo.modHold;
|
|
713
|
-
const modDecay = modHold + noteInfo.modDecay;
|
|
714
|
-
filterNode.frequency
|
|
742
|
+
note.filterNode.frequency
|
|
715
743
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
716
744
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
717
745
|
.setValueAtTime(adjustedPeekFreq, modHold)
|
|
718
746
|
.linearRampToValueAtTime(adjustedSustainFreq, modDecay);
|
|
719
|
-
|
|
747
|
+
note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
|
|
748
|
+
}
|
|
749
|
+
startModulation(channel, note, time) {
|
|
750
|
+
const { instrumentKey } = note;
|
|
751
|
+
note.modLFOGain = new GainNode(this.audioContext, {
|
|
752
|
+
gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
|
|
753
|
+
});
|
|
754
|
+
note.modLFO = new OscillatorNode(this.audioContext, {
|
|
755
|
+
frequency: this.centToHz(instrumentKey.freqModLFO),
|
|
756
|
+
});
|
|
757
|
+
note.modLFO.start(time);
|
|
758
|
+
note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
|
|
759
|
+
note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
|
|
760
|
+
note.modLFO.connect(note.modLFOGain);
|
|
761
|
+
note.modLFOGain.connect(note.bufferSource.detune);
|
|
762
|
+
}
|
|
763
|
+
startVibrato(channel, note, time) {
|
|
764
|
+
const { instrumentKey } = note;
|
|
765
|
+
note.vibLFOGain = new GainNode(this.audioContext, {
|
|
766
|
+
gain: channel.vibratoDepth,
|
|
767
|
+
});
|
|
768
|
+
note.vibLFO = new OscillatorNode(this.audioContext, {
|
|
769
|
+
frequency: this.centToHz(instrumentKey.freqModLFO) +
|
|
770
|
+
channel.vibratoRate,
|
|
771
|
+
});
|
|
772
|
+
note.vibLFO.start(time + channel.vibratoDelay);
|
|
773
|
+
note.vibLFO.connect(note.vibLFOGain);
|
|
774
|
+
note.vibLFOGain.connect(note.bufferSource.detune);
|
|
775
|
+
}
|
|
776
|
+
async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
|
|
777
|
+
const semitoneOffset = this.calcSemitoneOffset(channel);
|
|
778
|
+
const note = new Note(noteNumber, velocity, startTime, instrumentKey);
|
|
779
|
+
note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
|
|
780
|
+
note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
|
|
781
|
+
this.setVolumeEnvelope(channel, note);
|
|
782
|
+
this.setFilterEnvelope(channel, note);
|
|
720
783
|
if (channel.modulation > 0) {
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
.setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
728
|
-
.exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
|
|
729
|
-
channel.modulationEffect.lfo.connect(lfoGain);
|
|
730
|
-
lfoGain.connect(bufferSource.detune);
|
|
784
|
+
const delayModLFO = startTime + instrumentKey.delayModLFO;
|
|
785
|
+
this.startModulation(channel, note, delayModLFO);
|
|
786
|
+
}
|
|
787
|
+
if (channel.vibratoDepth > 0) {
|
|
788
|
+
const delayVibLFO = startTime + instrumentKey.delayVibLFO;
|
|
789
|
+
this.startVibrato(channel, note, delayVibLFO);
|
|
731
790
|
}
|
|
732
|
-
bufferSource.connect(filterNode);
|
|
733
|
-
filterNode.connect(gainNode);
|
|
734
791
|
if (this.mono && channel.currentBufferSource) {
|
|
735
792
|
channel.currentBufferSource.stop(startTime);
|
|
736
|
-
channel.currentBufferSource = bufferSource;
|
|
793
|
+
channel.currentBufferSource = note.bufferSource;
|
|
737
794
|
}
|
|
738
|
-
bufferSource.
|
|
739
|
-
|
|
795
|
+
note.bufferSource.connect(note.filterNode);
|
|
796
|
+
note.filterNode.connect(note.gainNode);
|
|
797
|
+
note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
|
|
798
|
+
return note;
|
|
740
799
|
}
|
|
741
800
|
calcBank(channel, channelNumber) {
|
|
742
801
|
if (channel.bankMSB === 121) {
|
|
@@ -755,36 +814,20 @@ class Midy {
|
|
|
755
814
|
return;
|
|
756
815
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
757
816
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
758
|
-
const
|
|
759
|
-
if (!
|
|
817
|
+
const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
|
|
818
|
+
if (!instrumentKey)
|
|
760
819
|
return;
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
this.connectNoteEffects(channel, gainNode);
|
|
820
|
+
const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
|
|
821
|
+
this.connectNoteEffects(channel, note.gainNode);
|
|
764
822
|
if (channel.sostenutoPedal) {
|
|
765
|
-
channel.sostenutoNotes.set(noteNumber,
|
|
766
|
-
gainNode,
|
|
767
|
-
filterNode,
|
|
768
|
-
bufferSource,
|
|
769
|
-
noteNumber,
|
|
770
|
-
noteInfo,
|
|
771
|
-
});
|
|
823
|
+
channel.sostenutoNotes.set(noteNumber, note);
|
|
772
824
|
}
|
|
773
825
|
const scheduledNotes = channel.scheduledNotes;
|
|
774
|
-
const scheduledNote = {
|
|
775
|
-
bufferSource,
|
|
776
|
-
filterNode,
|
|
777
|
-
gainNode,
|
|
778
|
-
lfoGain,
|
|
779
|
-
noteInfo,
|
|
780
|
-
noteNumber,
|
|
781
|
-
startTime,
|
|
782
|
-
};
|
|
783
826
|
if (scheduledNotes.has(noteNumber)) {
|
|
784
|
-
scheduledNotes.get(noteNumber).push(
|
|
827
|
+
scheduledNotes.get(noteNumber).push(note);
|
|
785
828
|
}
|
|
786
829
|
else {
|
|
787
|
-
scheduledNotes.set(noteNumber, [
|
|
830
|
+
scheduledNotes.set(noteNumber, [note]);
|
|
788
831
|
}
|
|
789
832
|
}
|
|
790
833
|
noteOn(channelNumber, noteNumber, velocity) {
|
|
@@ -806,15 +849,15 @@ class Midy {
|
|
|
806
849
|
continue;
|
|
807
850
|
if (targetNote.ending)
|
|
808
851
|
continue;
|
|
809
|
-
const { bufferSource, filterNode, gainNode,
|
|
852
|
+
const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, vibLFO, vibLFOGain, instrumentKey, } = targetNote;
|
|
810
853
|
const velocityRate = (velocity + 127) / 127;
|
|
811
|
-
const volEndTime = stopTime +
|
|
854
|
+
const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
|
|
812
855
|
gainNode.gain.cancelScheduledValues(stopTime);
|
|
813
856
|
gainNode.gain.linearRampToValueAtTime(0, volEndTime);
|
|
814
857
|
const maxFreq = this.audioContext.sampleRate / 2;
|
|
815
|
-
const baseFreq = this.centToHz(
|
|
858
|
+
const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
|
|
816
859
|
const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
|
|
817
|
-
const modEndTime = stopTime +
|
|
860
|
+
const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
|
|
818
861
|
filterNode.frequency
|
|
819
862
|
.cancelScheduledValues(stopTime)
|
|
820
863
|
.linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
|
|
@@ -828,8 +871,14 @@ class Midy {
|
|
|
828
871
|
bufferSource.disconnect(0);
|
|
829
872
|
filterNode.disconnect(0);
|
|
830
873
|
gainNode.disconnect(0);
|
|
831
|
-
if (
|
|
832
|
-
|
|
874
|
+
if (modLFOGain)
|
|
875
|
+
modLFOGain.disconnect(0);
|
|
876
|
+
if (vibLFOGain)
|
|
877
|
+
vibLFOGain.disconnect(0);
|
|
878
|
+
if (modLFO)
|
|
879
|
+
modLFO.stop();
|
|
880
|
+
if (vibLFO)
|
|
881
|
+
vibLFO.stop();
|
|
833
882
|
resolve();
|
|
834
883
|
};
|
|
835
884
|
bufferSource.stop(volEndTime);
|
|
@@ -895,7 +944,7 @@ class Midy {
|
|
|
895
944
|
const now = this.audioContext.currentTime;
|
|
896
945
|
const channel = this.channels[channelNumber];
|
|
897
946
|
pressure /= 64;
|
|
898
|
-
const activeNotes = this.getActiveNotes(channel);
|
|
947
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
899
948
|
if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
|
|
900
949
|
if (activeNotes.has(noteNumber)) {
|
|
901
950
|
const activeNote = activeNotes.get(noteNumber);
|
|
@@ -916,7 +965,7 @@ class Midy {
|
|
|
916
965
|
const channel = this.channels[channelNumber];
|
|
917
966
|
pressure /= 64;
|
|
918
967
|
channel.channelPressure = pressure;
|
|
919
|
-
const activeNotes = this.getActiveNotes(channel);
|
|
968
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
920
969
|
if (channel.channelPressure.amplitudeControl !== 1) {
|
|
921
970
|
activeNotes.forEach((activeNote) => {
|
|
922
971
|
const gain = activeNote.gainNode.gain.value;
|
|
@@ -928,20 +977,22 @@ class Midy {
|
|
|
928
977
|
}
|
|
929
978
|
handlePitchBendMessage(channelNumber, lsb, msb) {
|
|
930
979
|
const pitchBend = msb * 128 + lsb;
|
|
931
|
-
this.
|
|
980
|
+
this.setPitchBend(channelNumber, pitchBend);
|
|
932
981
|
}
|
|
933
|
-
|
|
982
|
+
setPitchBend(channelNumber, pitchBend) {
|
|
934
983
|
const now = this.audioContext.currentTime;
|
|
935
984
|
const channel = this.channels[channelNumber];
|
|
985
|
+
const prevPitchBend = channel.pitchBend;
|
|
936
986
|
channel.pitchBend = (pitchBend - 8192) / 8192;
|
|
937
|
-
const
|
|
938
|
-
|
|
987
|
+
const detuneChange = (channel.pitchBend - prevPitchBend) *
|
|
988
|
+
channel.pitchBendRange * 100;
|
|
989
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
939
990
|
activeNotes.forEach((activeNote) => {
|
|
940
|
-
const { bufferSource
|
|
941
|
-
const
|
|
942
|
-
bufferSource.
|
|
991
|
+
const { bufferSource } = activeNote;
|
|
992
|
+
const detune = bufferSource.detune.value + detuneChange;
|
|
993
|
+
bufferSource.detune
|
|
943
994
|
.cancelScheduledValues(now)
|
|
944
|
-
.setValueAtTime(
|
|
995
|
+
.setValueAtTime(detune, now);
|
|
945
996
|
});
|
|
946
997
|
}
|
|
947
998
|
handleControlChange(channelNumber, controller, value) {
|
|
@@ -1013,9 +1064,20 @@ class Midy {
|
|
|
1013
1064
|
this.channels[channelNumber].bankMSB = msb;
|
|
1014
1065
|
}
|
|
1015
1066
|
setModulation(channelNumber, modulation) {
|
|
1067
|
+
const now = this.audioContext.currentTime;
|
|
1016
1068
|
const channel = this.channels[channelNumber];
|
|
1017
1069
|
channel.modulation = (modulation / 127) *
|
|
1018
1070
|
(channel.modulationDepthRange * 100);
|
|
1071
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
1072
|
+
activeNotes.forEach((activeNote) => {
|
|
1073
|
+
if (activeNote.modLFO) {
|
|
1074
|
+
activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
|
|
1075
|
+
channel.modulation, now);
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
this.startModulation(channel, activeNote, now);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1019
1081
|
}
|
|
1020
1082
|
setPortamentoTime(channelNumber, portamentoTime) {
|
|
1021
1083
|
this.channels[channelNumber].portamentoTime = portamentoTime / 127;
|
|
@@ -1025,12 +1087,17 @@ class Midy {
|
|
|
1025
1087
|
channel.volume = volume / 127;
|
|
1026
1088
|
this.updateChannelGain(channel);
|
|
1027
1089
|
}
|
|
1090
|
+
panToGain(pan) {
|
|
1091
|
+
const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
|
|
1092
|
+
return {
|
|
1093
|
+
gainLeft: Math.cos(theta),
|
|
1094
|
+
gainRight: Math.sin(theta),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1028
1097
|
setPan(channelNumber, pan) {
|
|
1029
|
-
const now = this.audioContext.currentTime;
|
|
1030
1098
|
const channel = this.channels[channelNumber];
|
|
1031
|
-
channel.pan = pan
|
|
1032
|
-
|
|
1033
|
-
channel.pannerNode.pan.setValueAtTime(channel.pan, now);
|
|
1099
|
+
channel.pan = pan;
|
|
1100
|
+
this.updateChannelGain(channel);
|
|
1034
1101
|
}
|
|
1035
1102
|
setExpression(channelNumber, expression) {
|
|
1036
1103
|
const channel = this.channels[channelNumber];
|
|
@@ -1043,8 +1110,13 @@ class Midy {
|
|
|
1043
1110
|
updateChannelGain(channel) {
|
|
1044
1111
|
const now = this.audioContext.currentTime;
|
|
1045
1112
|
const volume = channel.volume * channel.expression;
|
|
1046
|
-
channel.
|
|
1047
|
-
channel.
|
|
1113
|
+
const { gainLeft, gainRight } = this.panToGain(channel.pan);
|
|
1114
|
+
channel.gainL.gain
|
|
1115
|
+
.cancelScheduledValues(now)
|
|
1116
|
+
.setValueAtTime(volume * gainLeft, now);
|
|
1117
|
+
channel.gainR.gain
|
|
1118
|
+
.cancelScheduledValues(now)
|
|
1119
|
+
.setValueAtTime(volume * gainRight, now);
|
|
1048
1120
|
}
|
|
1049
1121
|
setSustainPedal(channelNumber, value) {
|
|
1050
1122
|
const isOn = value >= 64;
|
|
@@ -1076,7 +1148,8 @@ class Midy {
|
|
|
1076
1148
|
const channel = this.channels[channelNumber];
|
|
1077
1149
|
channel.sostenutoPedal = isOn;
|
|
1078
1150
|
if (isOn) {
|
|
1079
|
-
const
|
|
1151
|
+
const now = this.audioContext.currentTime;
|
|
1152
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
1080
1153
|
channel.sostenutoNotes = new Map(activeNotes);
|
|
1081
1154
|
}
|
|
1082
1155
|
else {
|
|
@@ -1088,20 +1161,12 @@ class Midy {
|
|
|
1088
1161
|
channel.softPedal = softPedal / 127;
|
|
1089
1162
|
}
|
|
1090
1163
|
setVibratoRate(channelNumber, vibratoRate) {
|
|
1091
|
-
const now = this.audioContext.currentTime;
|
|
1092
1164
|
const channel = this.channels[channelNumber];
|
|
1093
1165
|
channel.vibratoRate = vibratoRate / 127 * 4 + 3; // 3-7Hz
|
|
1094
|
-
channel.modulationEffect.lfo.frequency
|
|
1095
|
-
.cancelScheduledValues(now)
|
|
1096
|
-
.setValueAtTime(channel.vibratoRate, now);
|
|
1097
1166
|
}
|
|
1098
1167
|
setVibratoDepth(channelNumber, vibratoDepth) {
|
|
1099
|
-
const now = this.audioContext.currentTime;
|
|
1100
1168
|
const channel = this.channels[channelNumber];
|
|
1101
1169
|
channel.vibratoDepth = vibratoDepth / 127;
|
|
1102
|
-
channel.modulationEffect.lfoGain.gain
|
|
1103
|
-
.cancelScheduledValues(now)
|
|
1104
|
-
.setValueAtTime(channel.vibratoDepth, now);
|
|
1105
1170
|
}
|
|
1106
1171
|
setVibratoDelay(channelNumber, vibratoDelay) {
|
|
1107
1172
|
// Access Virus: 0-10sec
|
|
@@ -1161,8 +1226,7 @@ class Midy {
|
|
|
1161
1226
|
const { dataMSB, dataLSB } = channel;
|
|
1162
1227
|
switch (rpn) {
|
|
1163
1228
|
case 0:
|
|
1164
|
-
|
|
1165
|
-
break;
|
|
1229
|
+
return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
|
|
1166
1230
|
case 1:
|
|
1167
1231
|
channel.fineTuning = (dataMSB * 128 + dataLSB - 8192) / 8192;
|
|
1168
1232
|
break;
|
|
@@ -1176,14 +1240,34 @@ class Midy {
|
|
|
1176
1240
|
console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
|
|
1177
1241
|
}
|
|
1178
1242
|
}
|
|
1243
|
+
handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
|
|
1244
|
+
const pitchBendRange = dataMSB + dataLSB / 100;
|
|
1245
|
+
this.setPitchBendRange(channelNumber, pitchBendRange);
|
|
1246
|
+
}
|
|
1247
|
+
setPitchBendRange(channelNumber, pitchBendRange) {
|
|
1248
|
+
const now = this.audioContext.currentTime;
|
|
1249
|
+
const channel = this.channels[channelNumber];
|
|
1250
|
+
const prevPitchBendRange = channel.pitchBendRange;
|
|
1251
|
+
channel.pitchBendRange = pitchBendRange;
|
|
1252
|
+
const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
|
|
1253
|
+
channel.pitchBend * 100;
|
|
1254
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
1255
|
+
activeNotes.forEach((activeNote) => {
|
|
1256
|
+
const { bufferSource } = activeNote;
|
|
1257
|
+
const detune = bufferSource.detune.value + detuneChange;
|
|
1258
|
+
bufferSource.detune
|
|
1259
|
+
.cancelScheduledValues(now)
|
|
1260
|
+
.setValueAtTime(detune, now);
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1179
1263
|
allSoundOff(channelNumber) {
|
|
1180
1264
|
const now = this.audioContext.currentTime;
|
|
1181
1265
|
const channel = this.channels[channelNumber];
|
|
1182
1266
|
const velocity = 0;
|
|
1183
1267
|
const stopPedal = true;
|
|
1184
1268
|
const promises = [];
|
|
1185
|
-
channel.scheduledNotes.forEach((
|
|
1186
|
-
const activeNote = this.
|
|
1269
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
1270
|
+
const activeNote = this.getActiveNote(noteList, now);
|
|
1187
1271
|
if (activeNote) {
|
|
1188
1272
|
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
1189
1273
|
promises.push(notePromise);
|
|
@@ -1200,8 +1284,8 @@ class Midy {
|
|
|
1200
1284
|
const velocity = 0;
|
|
1201
1285
|
const stopPedal = false;
|
|
1202
1286
|
const promises = [];
|
|
1203
|
-
channel.scheduledNotes.forEach((
|
|
1204
|
-
const activeNote = this.
|
|
1287
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
1288
|
+
const activeNote = this.getActiveNote(noteList, now);
|
|
1205
1289
|
if (activeNote) {
|
|
1206
1290
|
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
1207
1291
|
promises.push(notePromise);
|
|
@@ -1266,9 +1350,9 @@ class Midy {
|
|
|
1266
1350
|
case 1:
|
|
1267
1351
|
return this.handleMasterVolumeSysEx(data);
|
|
1268
1352
|
case 3:
|
|
1269
|
-
return this.
|
|
1353
|
+
return this.handleMasterFineTuningSysEx(data);
|
|
1270
1354
|
case 4:
|
|
1271
|
-
return this.
|
|
1355
|
+
return this.handleMasterCoarseTuningSysEx(data);
|
|
1272
1356
|
// case 5: // TODO: Global Parameter Control
|
|
1273
1357
|
default:
|
|
1274
1358
|
console.warn(`Unsupported Exclusive Message ${data}`);
|
|
@@ -1310,9 +1394,9 @@ class Midy {
|
|
|
1310
1394
|
}
|
|
1311
1395
|
handleMasterVolumeSysEx(data) {
|
|
1312
1396
|
const volume = (data[5] * 128 + data[4]) / 16383;
|
|
1313
|
-
this.
|
|
1397
|
+
this.setMasterVolume(volume);
|
|
1314
1398
|
}
|
|
1315
|
-
|
|
1399
|
+
setMasterVolume(volume) {
|
|
1316
1400
|
if (volume < 0 && 1 < volume) {
|
|
1317
1401
|
console.error("Master Volume is out of range");
|
|
1318
1402
|
}
|
|
@@ -1324,9 +1408,9 @@ class Midy {
|
|
|
1324
1408
|
}
|
|
1325
1409
|
handleMasterFineTuningSysEx(data) {
|
|
1326
1410
|
const fineTuning = (data[5] * 128 + data[4] - 8192) / 8192;
|
|
1327
|
-
this.
|
|
1411
|
+
this.setMasterFineTuning(fineTuning);
|
|
1328
1412
|
}
|
|
1329
|
-
|
|
1413
|
+
setMasterFineTuning(fineTuning) {
|
|
1330
1414
|
if (fineTuning < -1 && 1 < fineTuning) {
|
|
1331
1415
|
console.error("Master Fine Tuning value is out of range");
|
|
1332
1416
|
}
|
|
@@ -1336,9 +1420,9 @@ class Midy {
|
|
|
1336
1420
|
}
|
|
1337
1421
|
handleMasterCoarseTuningSysEx(data) {
|
|
1338
1422
|
const coarseTuning = data[4];
|
|
1339
|
-
this.
|
|
1423
|
+
this.setMasterCoarseTuning(coarseTuning);
|
|
1340
1424
|
}
|
|
1341
|
-
|
|
1425
|
+
setMasterCoarseTuning(coarseTuning) {
|
|
1342
1426
|
if (coarseTuning < 0 && 127 < coarseTuning) {
|
|
1343
1427
|
console.error("Master Coarse Tuning value is out of range");
|
|
1344
1428
|
}
|
|
@@ -1379,7 +1463,7 @@ Object.defineProperty(Midy, "channelSettings", {
|
|
|
1379
1463
|
value: {
|
|
1380
1464
|
currentBufferSource: null,
|
|
1381
1465
|
volume: 100 / 127,
|
|
1382
|
-
pan:
|
|
1466
|
+
pan: 64,
|
|
1383
1467
|
portamentoTime: 0,
|
|
1384
1468
|
reverb: 0,
|
|
1385
1469
|
chorus: 0,
|