@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.js
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
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
|
+
Object.defineProperty(this, "vibLFO", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: void 0
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(this, "vibLFOGain", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: void 0
|
|
46
|
+
});
|
|
47
|
+
this.noteNumber = noteNumber;
|
|
48
|
+
this.velocity = velocity;
|
|
49
|
+
this.startTime = startTime;
|
|
50
|
+
this.instrumentKey = instrumentKey;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
3
53
|
export class Midy {
|
|
4
54
|
constructor(audioContext) {
|
|
5
55
|
Object.defineProperty(this, "ticksPerBeat", {
|
|
@@ -175,25 +225,23 @@ export class Midy {
|
|
|
175
225
|
this.totalTime = this.calcTotalTime();
|
|
176
226
|
}
|
|
177
227
|
setChannelAudioNodes(audioContext) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
});
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
228
|
+
const { gainLeft, gainRight } = this.panToGain(Midy.channelSettings.pan);
|
|
229
|
+
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
230
|
+
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
231
|
+
const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
|
|
232
|
+
gainL.connect(merger, 0, 0);
|
|
233
|
+
gainR.connect(merger, 0, 1);
|
|
234
|
+
merger.connect(this.masterGain);
|
|
185
235
|
const reverbEffect = this.createReverbEffect(audioContext);
|
|
186
236
|
const chorusEffect = this.createChorusEffect(audioContext);
|
|
187
|
-
modulationEffect.lfo.start();
|
|
188
237
|
chorusEffect.lfo.start();
|
|
189
|
-
reverbEffect.dryGain.connect(
|
|
190
|
-
reverbEffect.
|
|
191
|
-
|
|
192
|
-
|
|
238
|
+
reverbEffect.dryGain.connect(gainL);
|
|
239
|
+
reverbEffect.dryGain.connect(gainR);
|
|
240
|
+
reverbEffect.wetGain.connect(gainL);
|
|
241
|
+
reverbEffect.wetGain.connect(gainR);
|
|
193
242
|
return {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
modulationEffect,
|
|
243
|
+
gainL,
|
|
244
|
+
gainR,
|
|
197
245
|
reverbEffect,
|
|
198
246
|
chorusEffect,
|
|
199
247
|
};
|
|
@@ -216,11 +264,11 @@ export class Midy {
|
|
|
216
264
|
});
|
|
217
265
|
return channels;
|
|
218
266
|
}
|
|
219
|
-
async createNoteBuffer(
|
|
220
|
-
const sampleEnd =
|
|
267
|
+
async createNoteBuffer(instrumentKey, isSF3) {
|
|
268
|
+
const sampleEnd = instrumentKey.sample.length + instrumentKey.end;
|
|
221
269
|
if (isSF3) {
|
|
222
|
-
const sample = new Uint8Array(
|
|
223
|
-
sample.set(
|
|
270
|
+
const sample = new Uint8Array(instrumentKey.sample.length);
|
|
271
|
+
sample.set(instrumentKey.sample);
|
|
224
272
|
const audioBuffer = await this.audioContext.decodeAudioData(sample.buffer);
|
|
225
273
|
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
|
|
226
274
|
const channelData = audioBuffer.getChannelData(channel);
|
|
@@ -229,26 +277,27 @@ export class Midy {
|
|
|
229
277
|
return audioBuffer;
|
|
230
278
|
}
|
|
231
279
|
else {
|
|
232
|
-
const sample =
|
|
280
|
+
const sample = instrumentKey.sample.subarray(0, sampleEnd);
|
|
233
281
|
const floatSample = this.convertToFloat32Array(sample);
|
|
234
282
|
const audioBuffer = new AudioBuffer({
|
|
235
283
|
numberOfChannels: 1,
|
|
236
284
|
length: sample.length,
|
|
237
|
-
sampleRate:
|
|
285
|
+
sampleRate: instrumentKey.sampleRate,
|
|
238
286
|
});
|
|
239
287
|
const channelData = audioBuffer.getChannelData(0);
|
|
240
288
|
channelData.set(floatSample);
|
|
241
289
|
return audioBuffer;
|
|
242
290
|
}
|
|
243
291
|
}
|
|
244
|
-
async createNoteBufferNode(
|
|
292
|
+
async createNoteBufferNode(instrumentKey, isSF3) {
|
|
245
293
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
246
|
-
const audioBuffer = await this.createNoteBuffer(
|
|
294
|
+
const audioBuffer = await this.createNoteBuffer(instrumentKey, isSF3);
|
|
247
295
|
bufferSource.buffer = audioBuffer;
|
|
248
|
-
bufferSource.loop =
|
|
296
|
+
bufferSource.loop = instrumentKey.sampleModes % 2 !== 0;
|
|
249
297
|
if (bufferSource.loop) {
|
|
250
|
-
bufferSource.loopStart =
|
|
251
|
-
|
|
298
|
+
bufferSource.loopStart = instrumentKey.loopStart /
|
|
299
|
+
instrumentKey.sampleRate;
|
|
300
|
+
bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
|
|
252
301
|
}
|
|
253
302
|
return bufferSource;
|
|
254
303
|
}
|
|
@@ -292,7 +341,7 @@ export class Midy {
|
|
|
292
341
|
this.handleChannelPressure(event.channel, event.amount);
|
|
293
342
|
break;
|
|
294
343
|
case "pitchBend":
|
|
295
|
-
this.
|
|
344
|
+
this.setPitchBend(event.channel, event.value);
|
|
296
345
|
break;
|
|
297
346
|
case "sysEx":
|
|
298
347
|
this.handleSysEx(event.data);
|
|
@@ -372,7 +421,6 @@ export class Midy {
|
|
|
372
421
|
const tmpChannels = new Array(16);
|
|
373
422
|
for (let i = 0; i < tmpChannels.length; i++) {
|
|
374
423
|
tmpChannels[i] = {
|
|
375
|
-
durationTicks: new Map(),
|
|
376
424
|
programNumber: -1,
|
|
377
425
|
bankMSB: this.channels[i].bankMSB,
|
|
378
426
|
bankLSB: this.channels[i].bankLSB,
|
|
@@ -402,16 +450,6 @@ export class Midy {
|
|
|
402
450
|
}
|
|
403
451
|
channel.programNumber = 0;
|
|
404
452
|
}
|
|
405
|
-
channel.durationTicks.set(event.noteNumber, {
|
|
406
|
-
ticks: event.ticks,
|
|
407
|
-
noteOn: event,
|
|
408
|
-
});
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
case "noteOff": {
|
|
412
|
-
const { ticks, noteOn } = tmpChannels[event.channel].durationTicks
|
|
413
|
-
.get(event.noteNumber);
|
|
414
|
-
noteOn.durationTicks = event.ticks - ticks;
|
|
415
453
|
break;
|
|
416
454
|
}
|
|
417
455
|
case "controller":
|
|
@@ -446,8 +484,8 @@ export class Midy {
|
|
|
446
484
|
});
|
|
447
485
|
});
|
|
448
486
|
const priority = {
|
|
449
|
-
|
|
450
|
-
|
|
487
|
+
controller: 0,
|
|
488
|
+
sysEx: 1,
|
|
451
489
|
};
|
|
452
490
|
timeline.sort((a, b) => {
|
|
453
491
|
if (a.ticks !== b.ticks)
|
|
@@ -530,30 +568,26 @@ export class Midy {
|
|
|
530
568
|
const now = this.audioContext.currentTime;
|
|
531
569
|
return this.resumeTime + now - this.startTime - this.startDelay;
|
|
532
570
|
}
|
|
533
|
-
getActiveNotes(channel) {
|
|
571
|
+
getActiveNotes(channel, time) {
|
|
534
572
|
const activeNotes = new Map();
|
|
535
|
-
channel.scheduledNotes.forEach((
|
|
536
|
-
const activeNote = this.
|
|
573
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
574
|
+
const activeNote = this.getActiveNote(noteList, time);
|
|
537
575
|
if (activeNote) {
|
|
538
576
|
activeNotes.set(activeNote.noteNumber, activeNote);
|
|
539
577
|
}
|
|
540
578
|
});
|
|
541
579
|
return activeNotes;
|
|
542
580
|
}
|
|
543
|
-
|
|
544
|
-
for (let i =
|
|
545
|
-
const
|
|
546
|
-
if (
|
|
547
|
-
return
|
|
581
|
+
getActiveNote(noteList, time) {
|
|
582
|
+
for (let i = noteList.length - 1; i >= 0; i--) {
|
|
583
|
+
const note = noteList[i];
|
|
584
|
+
if (!note)
|
|
585
|
+
return;
|
|
586
|
+
if (time < note.startTime)
|
|
587
|
+
continue;
|
|
588
|
+
return (note.ending) ? null : note;
|
|
548
589
|
}
|
|
549
|
-
|
|
550
|
-
createModulationEffect(audioContext) {
|
|
551
|
-
const lfo = new OscillatorNode(audioContext, {
|
|
552
|
-
frequency: 5,
|
|
553
|
-
});
|
|
554
|
-
return {
|
|
555
|
-
lfo,
|
|
556
|
-
};
|
|
590
|
+
return noteList[0];
|
|
557
591
|
}
|
|
558
592
|
createReverbEffect(audioContext, options = {}) {
|
|
559
593
|
const { decay = 0.8, preDecay = 0, } = options;
|
|
@@ -589,12 +623,8 @@ export class Midy {
|
|
|
589
623
|
}
|
|
590
624
|
createChorusEffect(audioContext, options = {}) {
|
|
591
625
|
const { chorusCount = 2, chorusRate = 0.6, chorusDepth = 0.15, delay = 0.01, variance = delay * 0.1, } = options;
|
|
592
|
-
const lfo = new OscillatorNode(audioContext, {
|
|
593
|
-
|
|
594
|
-
});
|
|
595
|
-
const lfoGain = new GainNode(audioContext, {
|
|
596
|
-
gain: chorusDepth,
|
|
597
|
-
});
|
|
626
|
+
const lfo = new OscillatorNode(audioContext, { frequency: chorusRate });
|
|
627
|
+
const lfoGain = new GainNode(audioContext, { gain: chorusDepth });
|
|
598
628
|
const chorusGains = [];
|
|
599
629
|
const delayNodes = [];
|
|
600
630
|
const baseGain = 1 / chorusCount;
|
|
@@ -605,9 +635,7 @@ export class Midy {
|
|
|
605
635
|
maxDelayTime: delayTime,
|
|
606
636
|
});
|
|
607
637
|
delayNodes.push(delayNode);
|
|
608
|
-
const chorusGain = new GainNode(audioContext, {
|
|
609
|
-
gain: baseGain,
|
|
610
|
-
});
|
|
638
|
+
const chorusGain = new GainNode(audioContext, { gain: baseGain });
|
|
611
639
|
chorusGains.push(chorusGain);
|
|
612
640
|
lfo.connect(lfoGain);
|
|
613
641
|
lfoGain.connect(delayNode.delayTime);
|
|
@@ -663,77 +691,108 @@ export class Midy {
|
|
|
663
691
|
const tuning = masterTuning + channelTuning;
|
|
664
692
|
return channel.pitchBend * channel.pitchBendRange + tuning;
|
|
665
693
|
}
|
|
666
|
-
calcPlaybackRate(
|
|
667
|
-
return
|
|
694
|
+
calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset) {
|
|
695
|
+
return instrumentKey.playbackRate(noteNumber) *
|
|
696
|
+
Math.pow(2, semitoneOffset / 12);
|
|
668
697
|
}
|
|
669
|
-
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
bufferSource.playbackRate.value = this.calcPlaybackRate(noteInfo, noteNumber, semitoneOffset);
|
|
673
|
-
// volume envelope
|
|
674
|
-
const gainNode = new GainNode(this.audioContext, {
|
|
675
|
-
gain: 0,
|
|
676
|
-
});
|
|
698
|
+
setVolumeEnvelope(channel, note) {
|
|
699
|
+
const { instrumentKey, startTime, velocity } = note;
|
|
700
|
+
note.gainNode = new GainNode(this.audioContext, { gain: 0 });
|
|
677
701
|
let volume = (velocity / 127) * channel.volume * channel.expression;
|
|
678
702
|
if (volume === 0)
|
|
679
703
|
volume = 1e-6; // exponentialRampToValueAtTime() requires a non-zero value
|
|
680
|
-
const attackVolume = this.cbToRatio(-
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
const
|
|
685
|
-
const
|
|
686
|
-
|
|
704
|
+
const attackVolume = this.cbToRatio(-instrumentKey.initialAttenuation) *
|
|
705
|
+
volume;
|
|
706
|
+
const sustainVolume = attackVolume * (1 - instrumentKey.volSustain);
|
|
707
|
+
const volDelay = startTime + instrumentKey.volDelay;
|
|
708
|
+
const volAttack = volDelay + instrumentKey.volAttack;
|
|
709
|
+
const volHold = volAttack + instrumentKey.volHold;
|
|
710
|
+
const volDecay = volHold + instrumentKey.volDecay;
|
|
711
|
+
note.gainNode.gain
|
|
687
712
|
.setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
688
713
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
689
714
|
.setValueAtTime(attackVolume, volHold)
|
|
690
715
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
691
|
-
|
|
716
|
+
}
|
|
717
|
+
setFilterEnvelope(channel, note) {
|
|
718
|
+
const { instrumentKey, startTime, noteNumber } = note;
|
|
692
719
|
const softPedalFactor = 1 -
|
|
693
720
|
(0.1 + (noteNumber / 127) * 0.2) * channel.softPedal;
|
|
694
721
|
const maxFreq = this.audioContext.sampleRate / 2;
|
|
695
|
-
const baseFreq = this.centToHz(
|
|
696
|
-
|
|
722
|
+
const baseFreq = this.centToHz(instrumentKey.initialFilterFc) *
|
|
723
|
+
softPedalFactor;
|
|
724
|
+
const peekFreq = this.centToHz(instrumentKey.initialFilterFc + instrumentKey.modEnvToFilterFc) * softPedalFactor;
|
|
697
725
|
const sustainFreq = (baseFreq +
|
|
698
|
-
(peekFreq - baseFreq) * (1 -
|
|
726
|
+
(peekFreq - baseFreq) * (1 - instrumentKey.modSustain)) * softPedalFactor;
|
|
727
|
+
const modDelay = startTime + instrumentKey.modDelay;
|
|
728
|
+
const modAttack = modDelay + instrumentKey.modAttack;
|
|
729
|
+
const modHold = modAttack + instrumentKey.modHold;
|
|
730
|
+
const modDecay = modHold + instrumentKey.modDecay;
|
|
699
731
|
const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
|
|
700
732
|
const adjustedPeekFreq = Math.min(maxFreq, peekFreq);
|
|
701
733
|
const adjustedSustainFreq = Math.min(maxFreq, sustainFreq);
|
|
702
|
-
|
|
734
|
+
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
703
735
|
type: "lowpass",
|
|
704
|
-
Q:
|
|
736
|
+
Q: instrumentKey.initialFilterQ / 10, // dB
|
|
705
737
|
frequency: adjustedBaseFreq,
|
|
706
738
|
});
|
|
707
|
-
|
|
708
|
-
const modAttack = modDelay + noteInfo.modAttack;
|
|
709
|
-
const modHold = modAttack + noteInfo.modHold;
|
|
710
|
-
const modDecay = modHold + noteInfo.modDecay;
|
|
711
|
-
filterNode.frequency
|
|
739
|
+
note.filterNode.frequency
|
|
712
740
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
713
741
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
714
742
|
.setValueAtTime(adjustedPeekFreq, modHold)
|
|
715
743
|
.linearRampToValueAtTime(adjustedSustainFreq, modDecay);
|
|
716
|
-
|
|
744
|
+
note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modEnvToPitch, modDelay);
|
|
745
|
+
}
|
|
746
|
+
startModulation(channel, note, time) {
|
|
747
|
+
const { instrumentKey } = note;
|
|
748
|
+
note.modLFOGain = new GainNode(this.audioContext, {
|
|
749
|
+
gain: this.cbToRatio(instrumentKey.modLfoToVolume) * channel.modulation,
|
|
750
|
+
});
|
|
751
|
+
note.modLFO = new OscillatorNode(this.audioContext, {
|
|
752
|
+
frequency: this.centToHz(instrumentKey.freqModLFO),
|
|
753
|
+
});
|
|
754
|
+
note.modLFO.start(time);
|
|
755
|
+
note.filterNode.frequency.setValueAtTime(note.filterNode.frequency.value + instrumentKey.modLfoToFilterFc, time);
|
|
756
|
+
note.bufferSource.detune.setValueAtTime(note.bufferSource.detune.value + instrumentKey.modLfoToPitch, time);
|
|
757
|
+
note.modLFO.connect(note.modLFOGain);
|
|
758
|
+
note.modLFOGain.connect(note.bufferSource.detune);
|
|
759
|
+
}
|
|
760
|
+
startVibrato(channel, note, time) {
|
|
761
|
+
const { instrumentKey } = note;
|
|
762
|
+
note.vibLFOGain = new GainNode(this.audioContext, {
|
|
763
|
+
gain: channel.vibratoDepth,
|
|
764
|
+
});
|
|
765
|
+
note.vibLFO = new OscillatorNode(this.audioContext, {
|
|
766
|
+
frequency: this.centToHz(instrumentKey.freqModLFO) +
|
|
767
|
+
channel.vibratoRate,
|
|
768
|
+
});
|
|
769
|
+
note.vibLFO.start(time + channel.vibratoDelay);
|
|
770
|
+
note.vibLFO.connect(note.vibLFOGain);
|
|
771
|
+
note.vibLFOGain.connect(note.bufferSource.detune);
|
|
772
|
+
}
|
|
773
|
+
async createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3) {
|
|
774
|
+
const semitoneOffset = this.calcSemitoneOffset(channel);
|
|
775
|
+
const note = new Note(noteNumber, velocity, startTime, instrumentKey);
|
|
776
|
+
note.bufferSource = await this.createNoteBufferNode(instrumentKey, isSF3);
|
|
777
|
+
note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
|
|
778
|
+
this.setVolumeEnvelope(channel, note);
|
|
779
|
+
this.setFilterEnvelope(channel, note);
|
|
717
780
|
if (channel.modulation > 0) {
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
.setValueAtTime(1e-6, vibratoDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
725
|
-
.exponentialRampToValueAtTime(channel.modulation, vibratoAttack);
|
|
726
|
-
channel.modulationEffect.lfo.connect(lfoGain);
|
|
727
|
-
lfoGain.connect(bufferSource.detune);
|
|
781
|
+
const delayModLFO = startTime + instrumentKey.delayModLFO;
|
|
782
|
+
this.startModulation(channel, note, delayModLFO);
|
|
783
|
+
}
|
|
784
|
+
if (channel.vibratoDepth > 0) {
|
|
785
|
+
const delayVibLFO = startTime + instrumentKey.delayVibLFO;
|
|
786
|
+
this.startVibrato(channel, note, delayVibLFO);
|
|
728
787
|
}
|
|
729
|
-
bufferSource.connect(filterNode);
|
|
730
|
-
filterNode.connect(gainNode);
|
|
731
788
|
if (this.mono && channel.currentBufferSource) {
|
|
732
789
|
channel.currentBufferSource.stop(startTime);
|
|
733
|
-
channel.currentBufferSource = bufferSource;
|
|
790
|
+
channel.currentBufferSource = note.bufferSource;
|
|
734
791
|
}
|
|
735
|
-
bufferSource.
|
|
736
|
-
|
|
792
|
+
note.bufferSource.connect(note.filterNode);
|
|
793
|
+
note.filterNode.connect(note.gainNode);
|
|
794
|
+
note.bufferSource.start(startTime, instrumentKey.start / instrumentKey.sampleRate);
|
|
795
|
+
return note;
|
|
737
796
|
}
|
|
738
797
|
calcBank(channel, channelNumber) {
|
|
739
798
|
if (channel.bankMSB === 121) {
|
|
@@ -752,36 +811,20 @@ export class Midy {
|
|
|
752
811
|
return;
|
|
753
812
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
754
813
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
755
|
-
const
|
|
756
|
-
if (!
|
|
814
|
+
const instrumentKey = soundFont.getInstrumentKey(bankNumber, channel.program, noteNumber);
|
|
815
|
+
if (!instrumentKey)
|
|
757
816
|
return;
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
this.connectNoteEffects(channel, gainNode);
|
|
817
|
+
const note = await this.createNote(channel, instrumentKey, noteNumber, velocity, startTime, isSF3);
|
|
818
|
+
this.connectNoteEffects(channel, note.gainNode);
|
|
761
819
|
if (channel.sostenutoPedal) {
|
|
762
|
-
channel.sostenutoNotes.set(noteNumber,
|
|
763
|
-
gainNode,
|
|
764
|
-
filterNode,
|
|
765
|
-
bufferSource,
|
|
766
|
-
noteNumber,
|
|
767
|
-
noteInfo,
|
|
768
|
-
});
|
|
820
|
+
channel.sostenutoNotes.set(noteNumber, note);
|
|
769
821
|
}
|
|
770
822
|
const scheduledNotes = channel.scheduledNotes;
|
|
771
|
-
const scheduledNote = {
|
|
772
|
-
bufferSource,
|
|
773
|
-
filterNode,
|
|
774
|
-
gainNode,
|
|
775
|
-
lfoGain,
|
|
776
|
-
noteInfo,
|
|
777
|
-
noteNumber,
|
|
778
|
-
startTime,
|
|
779
|
-
};
|
|
780
823
|
if (scheduledNotes.has(noteNumber)) {
|
|
781
|
-
scheduledNotes.get(noteNumber).push(
|
|
824
|
+
scheduledNotes.get(noteNumber).push(note);
|
|
782
825
|
}
|
|
783
826
|
else {
|
|
784
|
-
scheduledNotes.set(noteNumber, [
|
|
827
|
+
scheduledNotes.set(noteNumber, [note]);
|
|
785
828
|
}
|
|
786
829
|
}
|
|
787
830
|
noteOn(channelNumber, noteNumber, velocity) {
|
|
@@ -803,15 +846,15 @@ export class Midy {
|
|
|
803
846
|
continue;
|
|
804
847
|
if (targetNote.ending)
|
|
805
848
|
continue;
|
|
806
|
-
const { bufferSource, filterNode, gainNode,
|
|
849
|
+
const { bufferSource, filterNode, gainNode, modLFO, modLFOGain, vibLFO, vibLFOGain, instrumentKey, } = targetNote;
|
|
807
850
|
const velocityRate = (velocity + 127) / 127;
|
|
808
|
-
const volEndTime = stopTime +
|
|
851
|
+
const volEndTime = stopTime + instrumentKey.volRelease * velocityRate;
|
|
809
852
|
gainNode.gain.cancelScheduledValues(stopTime);
|
|
810
853
|
gainNode.gain.linearRampToValueAtTime(0, volEndTime);
|
|
811
854
|
const maxFreq = this.audioContext.sampleRate / 2;
|
|
812
|
-
const baseFreq = this.centToHz(
|
|
855
|
+
const baseFreq = this.centToHz(instrumentKey.initialFilterFc);
|
|
813
856
|
const adjustedBaseFreq = Math.min(maxFreq, baseFreq);
|
|
814
|
-
const modEndTime = stopTime +
|
|
857
|
+
const modEndTime = stopTime + instrumentKey.modRelease * velocityRate;
|
|
815
858
|
filterNode.frequency
|
|
816
859
|
.cancelScheduledValues(stopTime)
|
|
817
860
|
.linearRampToValueAtTime(adjustedBaseFreq, modEndTime);
|
|
@@ -825,8 +868,14 @@ export class Midy {
|
|
|
825
868
|
bufferSource.disconnect(0);
|
|
826
869
|
filterNode.disconnect(0);
|
|
827
870
|
gainNode.disconnect(0);
|
|
828
|
-
if (
|
|
829
|
-
|
|
871
|
+
if (modLFOGain)
|
|
872
|
+
modLFOGain.disconnect(0);
|
|
873
|
+
if (vibLFOGain)
|
|
874
|
+
vibLFOGain.disconnect(0);
|
|
875
|
+
if (modLFO)
|
|
876
|
+
modLFO.stop();
|
|
877
|
+
if (vibLFO)
|
|
878
|
+
vibLFO.stop();
|
|
830
879
|
resolve();
|
|
831
880
|
};
|
|
832
881
|
bufferSource.stop(volEndTime);
|
|
@@ -892,7 +941,7 @@ export class Midy {
|
|
|
892
941
|
const now = this.audioContext.currentTime;
|
|
893
942
|
const channel = this.channels[channelNumber];
|
|
894
943
|
pressure /= 64;
|
|
895
|
-
const activeNotes = this.getActiveNotes(channel);
|
|
944
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
896
945
|
if (channel.polyphonicKeyPressure.amplitudeControl !== 1) {
|
|
897
946
|
if (activeNotes.has(noteNumber)) {
|
|
898
947
|
const activeNote = activeNotes.get(noteNumber);
|
|
@@ -913,7 +962,7 @@ export class Midy {
|
|
|
913
962
|
const channel = this.channels[channelNumber];
|
|
914
963
|
pressure /= 64;
|
|
915
964
|
channel.channelPressure = pressure;
|
|
916
|
-
const activeNotes = this.getActiveNotes(channel);
|
|
965
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
917
966
|
if (channel.channelPressure.amplitudeControl !== 1) {
|
|
918
967
|
activeNotes.forEach((activeNote) => {
|
|
919
968
|
const gain = activeNote.gainNode.gain.value;
|
|
@@ -925,20 +974,22 @@ export class Midy {
|
|
|
925
974
|
}
|
|
926
975
|
handlePitchBendMessage(channelNumber, lsb, msb) {
|
|
927
976
|
const pitchBend = msb * 128 + lsb;
|
|
928
|
-
this.
|
|
977
|
+
this.setPitchBend(channelNumber, pitchBend);
|
|
929
978
|
}
|
|
930
|
-
|
|
979
|
+
setPitchBend(channelNumber, pitchBend) {
|
|
931
980
|
const now = this.audioContext.currentTime;
|
|
932
981
|
const channel = this.channels[channelNumber];
|
|
982
|
+
const prevPitchBend = channel.pitchBend;
|
|
933
983
|
channel.pitchBend = (pitchBend - 8192) / 8192;
|
|
934
|
-
const
|
|
935
|
-
|
|
984
|
+
const detuneChange = (channel.pitchBend - prevPitchBend) *
|
|
985
|
+
channel.pitchBendRange * 100;
|
|
986
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
936
987
|
activeNotes.forEach((activeNote) => {
|
|
937
|
-
const { bufferSource
|
|
938
|
-
const
|
|
939
|
-
bufferSource.
|
|
988
|
+
const { bufferSource } = activeNote;
|
|
989
|
+
const detune = bufferSource.detune.value + detuneChange;
|
|
990
|
+
bufferSource.detune
|
|
940
991
|
.cancelScheduledValues(now)
|
|
941
|
-
.setValueAtTime(
|
|
992
|
+
.setValueAtTime(detune, now);
|
|
942
993
|
});
|
|
943
994
|
}
|
|
944
995
|
handleControlChange(channelNumber, controller, value) {
|
|
@@ -1010,9 +1061,20 @@ export class Midy {
|
|
|
1010
1061
|
this.channels[channelNumber].bankMSB = msb;
|
|
1011
1062
|
}
|
|
1012
1063
|
setModulation(channelNumber, modulation) {
|
|
1064
|
+
const now = this.audioContext.currentTime;
|
|
1013
1065
|
const channel = this.channels[channelNumber];
|
|
1014
1066
|
channel.modulation = (modulation / 127) *
|
|
1015
1067
|
(channel.modulationDepthRange * 100);
|
|
1068
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
1069
|
+
activeNotes.forEach((activeNote) => {
|
|
1070
|
+
if (activeNote.modLFO) {
|
|
1071
|
+
activeNote.gainNode.gain.setValueAtTime(this.cbToRatio(activeNote.instrumentKey.modLfoToVolume) *
|
|
1072
|
+
channel.modulation, now);
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
this.startModulation(channel, activeNote, now);
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1016
1078
|
}
|
|
1017
1079
|
setPortamentoTime(channelNumber, portamentoTime) {
|
|
1018
1080
|
this.channels[channelNumber].portamentoTime = portamentoTime / 127;
|
|
@@ -1022,12 +1084,17 @@ export class Midy {
|
|
|
1022
1084
|
channel.volume = volume / 127;
|
|
1023
1085
|
this.updateChannelGain(channel);
|
|
1024
1086
|
}
|
|
1087
|
+
panToGain(pan) {
|
|
1088
|
+
const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
|
|
1089
|
+
return {
|
|
1090
|
+
gainLeft: Math.cos(theta),
|
|
1091
|
+
gainRight: Math.sin(theta),
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1025
1094
|
setPan(channelNumber, pan) {
|
|
1026
|
-
const now = this.audioContext.currentTime;
|
|
1027
1095
|
const channel = this.channels[channelNumber];
|
|
1028
|
-
channel.pan = pan
|
|
1029
|
-
|
|
1030
|
-
channel.pannerNode.pan.setValueAtTime(channel.pan, now);
|
|
1096
|
+
channel.pan = pan;
|
|
1097
|
+
this.updateChannelGain(channel);
|
|
1031
1098
|
}
|
|
1032
1099
|
setExpression(channelNumber, expression) {
|
|
1033
1100
|
const channel = this.channels[channelNumber];
|
|
@@ -1040,8 +1107,13 @@ export class Midy {
|
|
|
1040
1107
|
updateChannelGain(channel) {
|
|
1041
1108
|
const now = this.audioContext.currentTime;
|
|
1042
1109
|
const volume = channel.volume * channel.expression;
|
|
1043
|
-
channel.
|
|
1044
|
-
channel.
|
|
1110
|
+
const { gainLeft, gainRight } = this.panToGain(channel.pan);
|
|
1111
|
+
channel.gainL.gain
|
|
1112
|
+
.cancelScheduledValues(now)
|
|
1113
|
+
.setValueAtTime(volume * gainLeft, now);
|
|
1114
|
+
channel.gainR.gain
|
|
1115
|
+
.cancelScheduledValues(now)
|
|
1116
|
+
.setValueAtTime(volume * gainRight, now);
|
|
1045
1117
|
}
|
|
1046
1118
|
setSustainPedal(channelNumber, value) {
|
|
1047
1119
|
const isOn = value >= 64;
|
|
@@ -1073,7 +1145,8 @@ export class Midy {
|
|
|
1073
1145
|
const channel = this.channels[channelNumber];
|
|
1074
1146
|
channel.sostenutoPedal = isOn;
|
|
1075
1147
|
if (isOn) {
|
|
1076
|
-
const
|
|
1148
|
+
const now = this.audioContext.currentTime;
|
|
1149
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
1077
1150
|
channel.sostenutoNotes = new Map(activeNotes);
|
|
1078
1151
|
}
|
|
1079
1152
|
else {
|
|
@@ -1085,20 +1158,12 @@ export class Midy {
|
|
|
1085
1158
|
channel.softPedal = softPedal / 127;
|
|
1086
1159
|
}
|
|
1087
1160
|
setVibratoRate(channelNumber, vibratoRate) {
|
|
1088
|
-
const now = this.audioContext.currentTime;
|
|
1089
1161
|
const channel = this.channels[channelNumber];
|
|
1090
1162
|
channel.vibratoRate = vibratoRate / 127 * 4 + 3; // 3-7Hz
|
|
1091
|
-
channel.modulationEffect.lfo.frequency
|
|
1092
|
-
.cancelScheduledValues(now)
|
|
1093
|
-
.setValueAtTime(channel.vibratoRate, now);
|
|
1094
1163
|
}
|
|
1095
1164
|
setVibratoDepth(channelNumber, vibratoDepth) {
|
|
1096
|
-
const now = this.audioContext.currentTime;
|
|
1097
1165
|
const channel = this.channels[channelNumber];
|
|
1098
1166
|
channel.vibratoDepth = vibratoDepth / 127;
|
|
1099
|
-
channel.modulationEffect.lfoGain.gain
|
|
1100
|
-
.cancelScheduledValues(now)
|
|
1101
|
-
.setValueAtTime(channel.vibratoDepth, now);
|
|
1102
1167
|
}
|
|
1103
1168
|
setVibratoDelay(channelNumber, vibratoDelay) {
|
|
1104
1169
|
// Access Virus: 0-10sec
|
|
@@ -1158,8 +1223,7 @@ export class Midy {
|
|
|
1158
1223
|
const { dataMSB, dataLSB } = channel;
|
|
1159
1224
|
switch (rpn) {
|
|
1160
1225
|
case 0:
|
|
1161
|
-
|
|
1162
|
-
break;
|
|
1226
|
+
return this.handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB);
|
|
1163
1227
|
case 1:
|
|
1164
1228
|
channel.fineTuning = (dataMSB * 128 + dataLSB - 8192) / 8192;
|
|
1165
1229
|
break;
|
|
@@ -1173,14 +1237,34 @@ export class Midy {
|
|
|
1173
1237
|
console.warn(`Channel ${channelNumber}: Unsupported RPN MSB=${channel.rpnMSB} LSB=${channel.rpnLSB}`);
|
|
1174
1238
|
}
|
|
1175
1239
|
}
|
|
1240
|
+
handlePitchBendRangeMessage(channelNumber, dataMSB, dataLSB) {
|
|
1241
|
+
const pitchBendRange = dataMSB + dataLSB / 100;
|
|
1242
|
+
this.setPitchBendRange(channelNumber, pitchBendRange);
|
|
1243
|
+
}
|
|
1244
|
+
setPitchBendRange(channelNumber, pitchBendRange) {
|
|
1245
|
+
const now = this.audioContext.currentTime;
|
|
1246
|
+
const channel = this.channels[channelNumber];
|
|
1247
|
+
const prevPitchBendRange = channel.pitchBendRange;
|
|
1248
|
+
channel.pitchBendRange = pitchBendRange;
|
|
1249
|
+
const detuneChange = (channel.pitchBendRange - prevPitchBendRange) *
|
|
1250
|
+
channel.pitchBend * 100;
|
|
1251
|
+
const activeNotes = this.getActiveNotes(channel, now);
|
|
1252
|
+
activeNotes.forEach((activeNote) => {
|
|
1253
|
+
const { bufferSource } = activeNote;
|
|
1254
|
+
const detune = bufferSource.detune.value + detuneChange;
|
|
1255
|
+
bufferSource.detune
|
|
1256
|
+
.cancelScheduledValues(now)
|
|
1257
|
+
.setValueAtTime(detune, now);
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1176
1260
|
allSoundOff(channelNumber) {
|
|
1177
1261
|
const now = this.audioContext.currentTime;
|
|
1178
1262
|
const channel = this.channels[channelNumber];
|
|
1179
1263
|
const velocity = 0;
|
|
1180
1264
|
const stopPedal = true;
|
|
1181
1265
|
const promises = [];
|
|
1182
|
-
channel.scheduledNotes.forEach((
|
|
1183
|
-
const activeNote = this.
|
|
1266
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
1267
|
+
const activeNote = this.getActiveNote(noteList, now);
|
|
1184
1268
|
if (activeNote) {
|
|
1185
1269
|
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
1186
1270
|
promises.push(notePromise);
|
|
@@ -1197,8 +1281,8 @@ export class Midy {
|
|
|
1197
1281
|
const velocity = 0;
|
|
1198
1282
|
const stopPedal = false;
|
|
1199
1283
|
const promises = [];
|
|
1200
|
-
channel.scheduledNotes.forEach((
|
|
1201
|
-
const activeNote = this.
|
|
1284
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
1285
|
+
const activeNote = this.getActiveNote(noteList, now);
|
|
1202
1286
|
if (activeNote) {
|
|
1203
1287
|
const notePromise = this.scheduleNoteRelease(channelNumber, noteNumber, velocity, now, stopPedal);
|
|
1204
1288
|
promises.push(notePromise);
|
|
@@ -1263,9 +1347,9 @@ export class Midy {
|
|
|
1263
1347
|
case 1:
|
|
1264
1348
|
return this.handleMasterVolumeSysEx(data);
|
|
1265
1349
|
case 3:
|
|
1266
|
-
return this.
|
|
1350
|
+
return this.handleMasterFineTuningSysEx(data);
|
|
1267
1351
|
case 4:
|
|
1268
|
-
return this.
|
|
1352
|
+
return this.handleMasterCoarseTuningSysEx(data);
|
|
1269
1353
|
// case 5: // TODO: Global Parameter Control
|
|
1270
1354
|
default:
|
|
1271
1355
|
console.warn(`Unsupported Exclusive Message ${data}`);
|
|
@@ -1307,9 +1391,9 @@ export class Midy {
|
|
|
1307
1391
|
}
|
|
1308
1392
|
handleMasterVolumeSysEx(data) {
|
|
1309
1393
|
const volume = (data[5] * 128 + data[4]) / 16383;
|
|
1310
|
-
this.
|
|
1394
|
+
this.setMasterVolume(volume);
|
|
1311
1395
|
}
|
|
1312
|
-
|
|
1396
|
+
setMasterVolume(volume) {
|
|
1313
1397
|
if (volume < 0 && 1 < volume) {
|
|
1314
1398
|
console.error("Master Volume is out of range");
|
|
1315
1399
|
}
|
|
@@ -1321,9 +1405,9 @@ export class Midy {
|
|
|
1321
1405
|
}
|
|
1322
1406
|
handleMasterFineTuningSysEx(data) {
|
|
1323
1407
|
const fineTuning = (data[5] * 128 + data[4] - 8192) / 8192;
|
|
1324
|
-
this.
|
|
1408
|
+
this.setMasterFineTuning(fineTuning);
|
|
1325
1409
|
}
|
|
1326
|
-
|
|
1410
|
+
setMasterFineTuning(fineTuning) {
|
|
1327
1411
|
if (fineTuning < -1 && 1 < fineTuning) {
|
|
1328
1412
|
console.error("Master Fine Tuning value is out of range");
|
|
1329
1413
|
}
|
|
@@ -1333,9 +1417,9 @@ export class Midy {
|
|
|
1333
1417
|
}
|
|
1334
1418
|
handleMasterCoarseTuningSysEx(data) {
|
|
1335
1419
|
const coarseTuning = data[4];
|
|
1336
|
-
this.
|
|
1420
|
+
this.setMasterCoarseTuning(coarseTuning);
|
|
1337
1421
|
}
|
|
1338
|
-
|
|
1422
|
+
setMasterCoarseTuning(coarseTuning) {
|
|
1339
1423
|
if (coarseTuning < 0 && 127 < coarseTuning) {
|
|
1340
1424
|
console.error("Master Coarse Tuning value is out of range");
|
|
1341
1425
|
}
|
|
@@ -1375,7 +1459,7 @@ Object.defineProperty(Midy, "channelSettings", {
|
|
|
1375
1459
|
value: {
|
|
1376
1460
|
currentBufferSource: null,
|
|
1377
1461
|
volume: 100 / 127,
|
|
1378
|
-
pan:
|
|
1462
|
+
pan: 64,
|
|
1379
1463
|
portamentoTime: 0,
|
|
1380
1464
|
reverb: 0,
|
|
1381
1465
|
chorus: 0,
|