@marmooo/midy 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +118 -0
- package/esm/midy-GM1.d.ts +53 -26
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +397 -167
- package/esm/midy-GM2.d.ts +61 -38
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +617 -278
- package/esm/midy-GMLite.d.ts +49 -23
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +361 -156
- package/esm/midy.d.ts +63 -45
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +699 -354
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +53 -26
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +397 -167
- package/script/midy-GM2.d.ts +61 -38
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +617 -278
- package/script/midy-GMLite.d.ts +49 -23
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +361 -156
- package/script/midy.d.ts +63 -45
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +699 -354
package/script/midy-GMLite.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.MidyGMLite = void 0;
|
|
|
4
4
|
const midi_file_1 = require("midi-file");
|
|
5
5
|
const soundfont_parser_1 = require("@marmooo/soundfont-parser");
|
|
6
6
|
class Note {
|
|
7
|
-
constructor(noteNumber, velocity, startTime,
|
|
7
|
+
constructor(noteNumber, velocity, startTime, voice, voiceParams) {
|
|
8
8
|
Object.defineProperty(this, "bufferSource", {
|
|
9
9
|
enumerable: true,
|
|
10
10
|
configurable: true,
|
|
@@ -44,9 +44,75 @@ class Note {
|
|
|
44
44
|
this.noteNumber = noteNumber;
|
|
45
45
|
this.velocity = velocity;
|
|
46
46
|
this.startTime = startTime;
|
|
47
|
-
this.
|
|
47
|
+
this.voice = voice;
|
|
48
|
+
this.voiceParams = voiceParams;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
// normalized to 0-1 for use with the SF2 modulator model
|
|
52
|
+
const defaultControllerState = {
|
|
53
|
+
noteOnVelocity: { type: 2, defaultValue: 0 },
|
|
54
|
+
noteOnKeyNumber: { type: 3, defaultValue: 0 },
|
|
55
|
+
pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
|
|
56
|
+
pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
|
|
57
|
+
link: { type: 127, defaultValue: 0 },
|
|
58
|
+
// bankMSB: { type: 128 + 0, defaultValue: 121, },
|
|
59
|
+
modulationDepth: { type: 128 + 1, defaultValue: 0 },
|
|
60
|
+
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
61
|
+
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
62
|
+
pan: { type: 128 + 10, defaultValue: 0.5 },
|
|
63
|
+
expression: { type: 128 + 11, defaultValue: 1 },
|
|
64
|
+
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
65
|
+
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
66
|
+
sustainPedal: { type: 128 + 64, defaultValue: 0 },
|
|
67
|
+
// rpnLSB: { type: 128 + 100, defaultValue: 127 },
|
|
68
|
+
// rpnMSB: { type: 128 + 101, defaultValue: 127 },
|
|
69
|
+
// allSoundOff: { type: 128 + 120, defaultValue: 0 },
|
|
70
|
+
// resetAllControllers: { type: 128 + 121, defaultValue: 0 },
|
|
71
|
+
// allNotesOff: { type: 128 + 123, defaultValue: 0 },
|
|
72
|
+
};
|
|
73
|
+
class ControllerState {
|
|
74
|
+
constructor() {
|
|
75
|
+
Object.defineProperty(this, "array", {
|
|
76
|
+
enumerable: true,
|
|
77
|
+
configurable: true,
|
|
78
|
+
writable: true,
|
|
79
|
+
value: new Float32Array(256)
|
|
80
|
+
});
|
|
81
|
+
const entries = Object.entries(defaultControllerState);
|
|
82
|
+
for (const [name, { type, defaultValue }] of entries) {
|
|
83
|
+
this.array[type] = defaultValue;
|
|
84
|
+
Object.defineProperty(this, name, {
|
|
85
|
+
get: () => this.array[type],
|
|
86
|
+
set: (value) => this.array[type] = value,
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const filterEnvelopeKeys = [
|
|
94
|
+
"modEnvToPitch",
|
|
95
|
+
"initialFilterFc",
|
|
96
|
+
"modEnvToFilterFc",
|
|
97
|
+
"modDelay",
|
|
98
|
+
"modAttack",
|
|
99
|
+
"modHold",
|
|
100
|
+
"modDecay",
|
|
101
|
+
"modSustain",
|
|
102
|
+
"modRelease",
|
|
103
|
+
"playbackRate",
|
|
104
|
+
];
|
|
105
|
+
const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
|
|
106
|
+
const volumeEnvelopeKeys = [
|
|
107
|
+
"volDelay",
|
|
108
|
+
"volAttack",
|
|
109
|
+
"volHold",
|
|
110
|
+
"volDecay",
|
|
111
|
+
"volSustain",
|
|
112
|
+
"volRelease",
|
|
113
|
+
"initialAttenuation",
|
|
114
|
+
];
|
|
115
|
+
const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
|
|
50
116
|
class MidyGMLite {
|
|
51
117
|
constructor(audioContext) {
|
|
52
118
|
Object.defineProperty(this, "ticksPerBeat", {
|
|
@@ -159,6 +225,7 @@ class MidyGMLite {
|
|
|
159
225
|
});
|
|
160
226
|
this.audioContext = audioContext;
|
|
161
227
|
this.masterGain = new GainNode(audioContext);
|
|
228
|
+
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
162
229
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
163
230
|
this.channels = this.createChannels(audioContext);
|
|
164
231
|
this.masterGain.connect(audioContext.destination);
|
|
@@ -201,7 +268,7 @@ class MidyGMLite {
|
|
|
201
268
|
this.totalTime = this.calcTotalTime();
|
|
202
269
|
}
|
|
203
270
|
setChannelAudioNodes(audioContext) {
|
|
204
|
-
const { gainLeft, gainRight } = this.panToGain(
|
|
271
|
+
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
205
272
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
206
273
|
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
207
274
|
const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
|
|
@@ -218,18 +285,18 @@ class MidyGMLite {
|
|
|
218
285
|
const channels = Array.from({ length: 16 }, () => {
|
|
219
286
|
return {
|
|
220
287
|
...this.constructor.channelSettings,
|
|
221
|
-
|
|
288
|
+
state: new ControllerState(),
|
|
222
289
|
...this.setChannelAudioNodes(audioContext),
|
|
223
290
|
scheduledNotes: new Map(),
|
|
224
291
|
};
|
|
225
292
|
});
|
|
226
293
|
return channels;
|
|
227
294
|
}
|
|
228
|
-
async createNoteBuffer(
|
|
229
|
-
const sampleStart =
|
|
230
|
-
const sampleEnd =
|
|
295
|
+
async createNoteBuffer(voiceParams, isSF3) {
|
|
296
|
+
const sampleStart = voiceParams.start;
|
|
297
|
+
const sampleEnd = voiceParams.sample.length + voiceParams.end;
|
|
231
298
|
if (isSF3) {
|
|
232
|
-
const sample =
|
|
299
|
+
const sample = voiceParams.sample;
|
|
233
300
|
const start = sample.byteOffset + sampleStart;
|
|
234
301
|
const end = sample.byteOffset + sampleEnd;
|
|
235
302
|
const buffer = sample.buffer.slice(start, end);
|
|
@@ -237,14 +304,14 @@ class MidyGMLite {
|
|
|
237
304
|
return audioBuffer;
|
|
238
305
|
}
|
|
239
306
|
else {
|
|
240
|
-
const sample =
|
|
307
|
+
const sample = voiceParams.sample;
|
|
241
308
|
const start = sample.byteOffset + sampleStart;
|
|
242
309
|
const end = sample.byteOffset + sampleEnd;
|
|
243
310
|
const buffer = sample.buffer.slice(start, end);
|
|
244
311
|
const audioBuffer = new AudioBuffer({
|
|
245
312
|
numberOfChannels: 1,
|
|
246
313
|
length: sample.length,
|
|
247
|
-
sampleRate:
|
|
314
|
+
sampleRate: voiceParams.sampleRate,
|
|
248
315
|
});
|
|
249
316
|
const channelData = audioBuffer.getChannelData(0);
|
|
250
317
|
const int16Array = new Int16Array(buffer);
|
|
@@ -254,15 +321,14 @@ class MidyGMLite {
|
|
|
254
321
|
return audioBuffer;
|
|
255
322
|
}
|
|
256
323
|
}
|
|
257
|
-
async createNoteBufferNode(
|
|
324
|
+
async createNoteBufferNode(voiceParams, isSF3) {
|
|
258
325
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
259
|
-
const audioBuffer = await this.createNoteBuffer(
|
|
326
|
+
const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
|
|
260
327
|
bufferSource.buffer = audioBuffer;
|
|
261
|
-
bufferSource.loop =
|
|
328
|
+
bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
|
|
262
329
|
if (bufferSource.loop) {
|
|
263
|
-
bufferSource.loopStart =
|
|
264
|
-
|
|
265
|
-
bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
|
|
330
|
+
bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
331
|
+
bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
|
|
266
332
|
}
|
|
267
333
|
return bufferSource;
|
|
268
334
|
}
|
|
@@ -292,7 +358,7 @@ class MidyGMLite {
|
|
|
292
358
|
this.handleProgramChange(event.channel, event.programNumber);
|
|
293
359
|
break;
|
|
294
360
|
case "pitchBend":
|
|
295
|
-
this.setPitchBend(event.channel, event.value);
|
|
361
|
+
this.setPitchBend(event.channel, event.value + 8192);
|
|
296
362
|
break;
|
|
297
363
|
case "sysEx":
|
|
298
364
|
this.handleSysEx(event.data);
|
|
@@ -518,49 +584,72 @@ class MidyGMLite {
|
|
|
518
584
|
cbToRatio(cb) {
|
|
519
585
|
return Math.pow(10, cb / 200);
|
|
520
586
|
}
|
|
587
|
+
rateToCent(rate) {
|
|
588
|
+
return 1200 * Math.log2(rate);
|
|
589
|
+
}
|
|
590
|
+
centToRate(cent) {
|
|
591
|
+
return Math.pow(2, cent / 1200);
|
|
592
|
+
}
|
|
521
593
|
centToHz(cent) {
|
|
522
|
-
return 8.176 *
|
|
594
|
+
return 8.176 * this.centToRate(cent);
|
|
523
595
|
}
|
|
524
|
-
|
|
525
|
-
|
|
596
|
+
calcChannelDetune(channel) {
|
|
597
|
+
const pitchWheel = channel.state.pitchWheel * 2 - 1;
|
|
598
|
+
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
|
|
599
|
+
return pitchWheel * pitchWheelSensitivity;
|
|
526
600
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
601
|
+
updateDetune(channel) {
|
|
602
|
+
const now = this.audioContext.currentTime;
|
|
603
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
604
|
+
for (let i = 0; i < noteList.length; i++) {
|
|
605
|
+
const note = noteList[i];
|
|
606
|
+
if (!note)
|
|
607
|
+
continue;
|
|
608
|
+
note.bufferSource.detune
|
|
609
|
+
.cancelScheduledValues(now)
|
|
610
|
+
.setValueAtTime(channel.detune, now);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
530
613
|
}
|
|
531
614
|
setVolumeEnvelope(note) {
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
const
|
|
615
|
+
const now = this.audioContext.currentTime;
|
|
616
|
+
const { voiceParams, startTime } = note;
|
|
617
|
+
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
|
|
618
|
+
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
619
|
+
const volDelay = startTime + voiceParams.volDelay;
|
|
620
|
+
const volAttack = volDelay + voiceParams.volAttack;
|
|
621
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
622
|
+
const volDecay = volHold + voiceParams.volDecay;
|
|
539
623
|
note.volumeNode.gain
|
|
540
|
-
.cancelScheduledValues(
|
|
624
|
+
.cancelScheduledValues(now)
|
|
541
625
|
.setValueAtTime(0, startTime)
|
|
542
626
|
.setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
543
627
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
544
628
|
.setValueAtTime(attackVolume, volHold)
|
|
545
629
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
546
630
|
}
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
|
|
631
|
+
setPitchEnvelope(note) {
|
|
632
|
+
const now = this.audioContext.currentTime;
|
|
633
|
+
const { voiceParams } = note;
|
|
634
|
+
const baseRate = voiceParams.playbackRate;
|
|
635
|
+
note.bufferSource.playbackRate
|
|
636
|
+
.cancelScheduledValues(now)
|
|
637
|
+
.setValueAtTime(baseRate, now);
|
|
638
|
+
const modEnvToPitch = voiceParams.modEnvToPitch;
|
|
551
639
|
if (modEnvToPitch === 0)
|
|
552
640
|
return;
|
|
553
|
-
const basePitch =
|
|
554
|
-
const peekPitch =
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
.
|
|
562
|
-
.
|
|
563
|
-
.
|
|
641
|
+
const basePitch = this.rateToCent(baseRate);
|
|
642
|
+
const peekPitch = basePitch + modEnvToPitch;
|
|
643
|
+
const peekRate = this.centToRate(peekPitch);
|
|
644
|
+
const modDelay = startTime + voiceParams.modDelay;
|
|
645
|
+
const modAttack = modDelay + voiceParams.modAttack;
|
|
646
|
+
const modHold = modAttack + voiceParams.modHold;
|
|
647
|
+
const modDecay = modHold + voiceParams.modDecay;
|
|
648
|
+
note.bufferSource.playbackRate
|
|
649
|
+
.setValueAtTime(baseRate, modDelay)
|
|
650
|
+
.exponentialRampToValueAtTime(peekRate, modAttack)
|
|
651
|
+
.setValueAtTime(peekRate, modHold)
|
|
652
|
+
.linearRampToValueAtTime(baseRate, modDecay);
|
|
564
653
|
}
|
|
565
654
|
clampCutoffFrequency(frequency) {
|
|
566
655
|
const minFrequency = 20; // min Hz of initialFilterFc
|
|
@@ -568,20 +657,21 @@ class MidyGMLite {
|
|
|
568
657
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
569
658
|
}
|
|
570
659
|
setFilterEnvelope(note) {
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
const
|
|
660
|
+
const now = this.audioContext.currentTime;
|
|
661
|
+
const { voiceParams, startTime } = note;
|
|
662
|
+
const baseFreq = this.centToHz(voiceParams.initialFilterFc);
|
|
663
|
+
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
|
|
574
664
|
const sustainFreq = baseFreq +
|
|
575
|
-
(peekFreq - baseFreq) * (1 -
|
|
665
|
+
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
576
666
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
577
667
|
const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
|
|
578
668
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
579
|
-
const modDelay = startTime +
|
|
580
|
-
const modAttack = modDelay +
|
|
581
|
-
const modHold = modAttack +
|
|
582
|
-
const modDecay = modHold +
|
|
669
|
+
const modDelay = startTime + voiceParams.modDelay;
|
|
670
|
+
const modAttack = modDelay + voiceParams.modAttack;
|
|
671
|
+
const modHold = modAttack + voiceParams.modHold;
|
|
672
|
+
const modDecay = modHold + voiceParams.modDecay;
|
|
583
673
|
note.filterNode.frequency
|
|
584
|
-
.cancelScheduledValues(
|
|
674
|
+
.cancelScheduledValues(now)
|
|
585
675
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
586
676
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
587
677
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
@@ -589,25 +679,18 @@ class MidyGMLite {
|
|
|
589
679
|
.linearRampToValueAtTime(adjustedSustainFreq, modDecay);
|
|
590
680
|
}
|
|
591
681
|
startModulation(channel, note, startTime) {
|
|
592
|
-
const {
|
|
593
|
-
const { modLfoToPitch, modLfoToVolume } = instrumentKey;
|
|
682
|
+
const { voiceParams } = note;
|
|
594
683
|
note.modulationLFO = new OscillatorNode(this.audioContext, {
|
|
595
|
-
frequency: this.centToHz(
|
|
684
|
+
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
596
685
|
});
|
|
597
686
|
note.filterDepth = new GainNode(this.audioContext, {
|
|
598
|
-
gain:
|
|
599
|
-
});
|
|
600
|
-
const modulationDepth = Math.abs(modLfoToPitch) + channel.modulationDepth;
|
|
601
|
-
const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
|
|
602
|
-
note.modulationDepth = new GainNode(this.audioContext, {
|
|
603
|
-
gain: modulationDepth * modulationDepthSign,
|
|
604
|
-
});
|
|
605
|
-
const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
606
|
-
const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
|
|
607
|
-
note.volumeDepth = new GainNode(this.audioContext, {
|
|
608
|
-
gain: volumeDepth * volumeDepthSign,
|
|
687
|
+
gain: voiceParams.modLfoToFilterFc,
|
|
609
688
|
});
|
|
610
|
-
note.
|
|
689
|
+
note.modulationDepth = new GainNode(this.audioContext);
|
|
690
|
+
this.setModLfoToPitch(channel, note);
|
|
691
|
+
note.volumeDepth = new GainNode(this.audioContext);
|
|
692
|
+
this.setModLfoToVolume(note);
|
|
693
|
+
note.modulationLFO.start(startTime + voiceParams.delayModLFO);
|
|
611
694
|
note.modulationLFO.connect(note.filterDepth);
|
|
612
695
|
note.filterDepth.connect(note.filterNode.frequency);
|
|
613
696
|
note.modulationLFO.connect(note.modulationDepth);
|
|
@@ -615,24 +698,22 @@ class MidyGMLite {
|
|
|
615
698
|
note.modulationLFO.connect(note.volumeDepth);
|
|
616
699
|
note.volumeDepth.connect(note.volumeNode.gain);
|
|
617
700
|
}
|
|
618
|
-
async createNote(channel,
|
|
619
|
-
const
|
|
620
|
-
const
|
|
621
|
-
note
|
|
701
|
+
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
702
|
+
const state = channel.state;
|
|
703
|
+
const voiceParams = voice.getAllParams(state.array);
|
|
704
|
+
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
705
|
+
note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
|
|
622
706
|
note.volumeNode = new GainNode(this.audioContext);
|
|
623
707
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
624
708
|
type: "lowpass",
|
|
625
|
-
Q:
|
|
709
|
+
Q: voiceParams.initialFilterQ / 10, // dB
|
|
626
710
|
});
|
|
627
711
|
this.setVolumeEnvelope(note);
|
|
628
712
|
this.setFilterEnvelope(note);
|
|
629
|
-
|
|
630
|
-
|
|
713
|
+
this.setPitchEnvelope(note);
|
|
714
|
+
if (0 < state.modulationDepth) {
|
|
631
715
|
this.startModulation(channel, note, startTime);
|
|
632
716
|
}
|
|
633
|
-
else {
|
|
634
|
-
note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
|
|
635
|
-
}
|
|
636
717
|
note.bufferSource.connect(note.filterNode);
|
|
637
718
|
note.filterNode.connect(note.volumeNode);
|
|
638
719
|
note.bufferSource.start(startTime);
|
|
@@ -646,13 +727,13 @@ class MidyGMLite {
|
|
|
646
727
|
return;
|
|
647
728
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
648
729
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
649
|
-
const
|
|
650
|
-
if (!
|
|
730
|
+
const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
|
|
731
|
+
if (!voice)
|
|
651
732
|
return;
|
|
652
|
-
const note = await this.createNote(channel,
|
|
733
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
653
734
|
note.volumeNode.connect(channel.gainL);
|
|
654
735
|
note.volumeNode.connect(channel.gainR);
|
|
655
|
-
const exclusiveClass =
|
|
736
|
+
const exclusiveClass = note.voiceParams.exclusiveClass;
|
|
656
737
|
if (exclusiveClass !== 0) {
|
|
657
738
|
if (this.exclusiveClassMap.has(exclusiveClass)) {
|
|
658
739
|
const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
|
|
@@ -697,10 +778,6 @@ class MidyGMLite {
|
|
|
697
778
|
note.modulationDepth.disconnect();
|
|
698
779
|
note.modulationLFO.stop();
|
|
699
780
|
}
|
|
700
|
-
if (note.vibratoDepth) {
|
|
701
|
-
note.vibratoDepth.disconnect();
|
|
702
|
-
note.vibratoLFO.stop();
|
|
703
|
-
}
|
|
704
781
|
resolve();
|
|
705
782
|
};
|
|
706
783
|
note.bufferSource.stop(stopTime);
|
|
@@ -708,7 +785,7 @@ class MidyGMLite {
|
|
|
708
785
|
}
|
|
709
786
|
scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
710
787
|
const channel = this.channels[channelNumber];
|
|
711
|
-
if (!force && channel.sustainPedal)
|
|
788
|
+
if (!force && 0.5 < channel.state.sustainPedal)
|
|
712
789
|
return;
|
|
713
790
|
if (!channel.scheduledNotes.has(noteNumber))
|
|
714
791
|
return;
|
|
@@ -719,8 +796,8 @@ class MidyGMLite {
|
|
|
719
796
|
continue;
|
|
720
797
|
if (note.ending)
|
|
721
798
|
continue;
|
|
722
|
-
const volRelease = endTime + note.
|
|
723
|
-
const modRelease = endTime + note.
|
|
799
|
+
const volRelease = endTime + note.voiceParams.volRelease;
|
|
800
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
724
801
|
note.filterNode.frequency
|
|
725
802
|
.cancelScheduledValues(endTime)
|
|
726
803
|
.linearRampToValueAtTime(0, modRelease);
|
|
@@ -736,7 +813,7 @@ class MidyGMLite {
|
|
|
736
813
|
const velocity = halfVelocity * 2;
|
|
737
814
|
const channel = this.channels[channelNumber];
|
|
738
815
|
const promises = [];
|
|
739
|
-
channel.sustainPedal =
|
|
816
|
+
channel.state.sustainPedal = halfVelocity;
|
|
740
817
|
channel.scheduledNotes.forEach((noteList) => {
|
|
741
818
|
for (let i = 0; i < noteList.length; i++) {
|
|
742
819
|
const note = noteList[i];
|
|
@@ -772,16 +849,138 @@ class MidyGMLite {
|
|
|
772
849
|
channel.program = program;
|
|
773
850
|
}
|
|
774
851
|
handlePitchBendMessage(channelNumber, lsb, msb) {
|
|
775
|
-
const pitchBend = msb * 128 + lsb
|
|
852
|
+
const pitchBend = msb * 128 + lsb;
|
|
776
853
|
this.setPitchBend(channelNumber, pitchBend);
|
|
777
854
|
}
|
|
778
|
-
setPitchBend(channelNumber,
|
|
855
|
+
setPitchBend(channelNumber, value) {
|
|
779
856
|
const channel = this.channels[channelNumber];
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
857
|
+
const state = channel.state;
|
|
858
|
+
const prev = state.pitchWheel * 2 - 1;
|
|
859
|
+
const next = (value - 8192) / 8192;
|
|
860
|
+
state.pitchWheel = value / 16383;
|
|
861
|
+
channel.detune += (next - prev) * state.pitchWheelSensitivity * 12800;
|
|
862
|
+
this.updateDetune(channel);
|
|
863
|
+
this.applyVoiceParams(channel, 14);
|
|
864
|
+
}
|
|
865
|
+
setModLfoToPitch(channel, note) {
|
|
866
|
+
const now = this.audioContext.currentTime;
|
|
867
|
+
const modLfoToPitch = note.voiceParams.modLfoToPitch;
|
|
868
|
+
const modulationDepth = Math.abs(modLfoToPitch) +
|
|
869
|
+
channel.state.modulationDepth;
|
|
870
|
+
const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
|
|
871
|
+
note.modulationDepth.gain
|
|
872
|
+
.cancelScheduledValues(now)
|
|
873
|
+
.setValueAtTime(modulationDepth * modulationDepthSign, now);
|
|
874
|
+
}
|
|
875
|
+
setModLfoToFilterFc(note) {
|
|
876
|
+
const now = this.audioContext.currentTime;
|
|
877
|
+
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
|
|
878
|
+
note.filterDepth.gain
|
|
879
|
+
.cancelScheduledValues(now)
|
|
880
|
+
.setValueAtTime(modLfoToFilterFc, now);
|
|
881
|
+
}
|
|
882
|
+
setModLfoToVolume(note) {
|
|
883
|
+
const now = this.audioContext.currentTime;
|
|
884
|
+
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
885
|
+
const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
886
|
+
const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
|
|
887
|
+
note.volumeDepth.gain
|
|
888
|
+
.cancelScheduledValues(now)
|
|
889
|
+
.setValueAtTime(volumeDepth * volumeDepthSign, now);
|
|
890
|
+
}
|
|
891
|
+
setDelayModLFO(note) {
|
|
892
|
+
const now = this.audioContext.currentTime;
|
|
893
|
+
const startTime = note.startTime;
|
|
894
|
+
if (startTime < now)
|
|
895
|
+
return;
|
|
896
|
+
note.modulationLFO.stop(now);
|
|
897
|
+
note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
|
|
898
|
+
note.modulationLFO.connect(note.filterDepth);
|
|
899
|
+
}
|
|
900
|
+
setFreqModLFO(note) {
|
|
901
|
+
const now = this.audioContext.currentTime;
|
|
902
|
+
const freqModLFO = note.voiceParams.freqModLFO;
|
|
903
|
+
note.modulationLFO.frequency
|
|
904
|
+
.cancelScheduledValues(now)
|
|
905
|
+
.setValueAtTime(freqModLFO, now);
|
|
906
|
+
}
|
|
907
|
+
createVoiceParamsHandlers() {
|
|
908
|
+
return {
|
|
909
|
+
modLfoToPitch: (channel, note, _prevValue) => {
|
|
910
|
+
if (0 < channel.state.modulationDepth) {
|
|
911
|
+
this.setModLfoToPitch(channel, note);
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
vibLfoToPitch: (_channel, _note, _prevValue) => { },
|
|
915
|
+
modLfoToFilterFc: (channel, note, _prevValue) => {
|
|
916
|
+
if (0 < channel.state.modulationDepth)
|
|
917
|
+
this.setModLfoToFilterFc(note);
|
|
918
|
+
},
|
|
919
|
+
modLfoToVolume: (channel, note) => {
|
|
920
|
+
if (0 < channel.state.modulationDepth)
|
|
921
|
+
this.setModLfoToVolume(note);
|
|
922
|
+
},
|
|
923
|
+
chorusEffectsSend: (_channel, _note, _prevValue) => { },
|
|
924
|
+
reverbEffectsSend: (_channel, _note, _prevValue) => { },
|
|
925
|
+
delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
|
|
926
|
+
freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
|
|
927
|
+
delayVibLFO: (_channel, _note, _prevValue) => { },
|
|
928
|
+
freqVibLFO: (_channel, _note, _prevValue) => { },
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
getControllerState(channel, noteNumber, velocity) {
|
|
932
|
+
const state = new Float32Array(channel.state.array.length);
|
|
933
|
+
state.set(channel.state.array);
|
|
934
|
+
state[2] = velocity / 127;
|
|
935
|
+
state[3] = noteNumber / 127;
|
|
936
|
+
return state;
|
|
937
|
+
}
|
|
938
|
+
applyVoiceParams(channel, controllerType) {
|
|
939
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
940
|
+
for (let i = 0; i < noteList.length; i++) {
|
|
941
|
+
const note = noteList[i];
|
|
942
|
+
if (!note)
|
|
943
|
+
continue;
|
|
944
|
+
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
945
|
+
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
946
|
+
let appliedFilterEnvelope = false;
|
|
947
|
+
let appliedVolumeEnvelope = false;
|
|
948
|
+
for (const [key, value] of Object.entries(voiceParams)) {
|
|
949
|
+
const prevValue = note.voiceParams[key];
|
|
950
|
+
if (value === prevValue)
|
|
951
|
+
continue;
|
|
952
|
+
note.voiceParams[key] = value;
|
|
953
|
+
if (key in this.voiceParamsHandlers) {
|
|
954
|
+
this.voiceParamsHandlers[key](channel, note, prevValue);
|
|
955
|
+
}
|
|
956
|
+
else if (filterEnvelopeKeySet.has(key)) {
|
|
957
|
+
if (appliedFilterEnvelope)
|
|
958
|
+
continue;
|
|
959
|
+
appliedFilterEnvelope = true;
|
|
960
|
+
const noteVoiceParams = note.voiceParams;
|
|
961
|
+
for (let i = 0; i < filterEnvelopeKeys.length; i++) {
|
|
962
|
+
const key = filterEnvelopeKeys[i];
|
|
963
|
+
if (key in voiceParams)
|
|
964
|
+
noteVoiceParams[key] = voiceParams[key];
|
|
965
|
+
}
|
|
966
|
+
this.setFilterEnvelope(channel, note);
|
|
967
|
+
this.setPitchEnvelope(note);
|
|
968
|
+
}
|
|
969
|
+
else if (volumeEnvelopeKeySet.has(key)) {
|
|
970
|
+
if (appliedVolumeEnvelope)
|
|
971
|
+
continue;
|
|
972
|
+
appliedVolumeEnvelope = true;
|
|
973
|
+
const noteVoiceParams = note.voiceParams;
|
|
974
|
+
for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
|
|
975
|
+
const key = volumeEnvelopeKeys[i];
|
|
976
|
+
if (key in voiceParams)
|
|
977
|
+
noteVoiceParams[key] = voiceParams[key];
|
|
978
|
+
}
|
|
979
|
+
this.setVolumeEnvelope(channel, note);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
});
|
|
785
984
|
}
|
|
786
985
|
createControlChangeHandlers() {
|
|
787
986
|
return {
|
|
@@ -799,13 +998,13 @@ class MidyGMLite {
|
|
|
799
998
|
123: this.allNotesOff,
|
|
800
999
|
};
|
|
801
1000
|
}
|
|
802
|
-
handleControlChange(channelNumber,
|
|
803
|
-
const handler = this.controlChangeHandlers[
|
|
1001
|
+
handleControlChange(channelNumber, controllerType, value) {
|
|
1002
|
+
const handler = this.controlChangeHandlers[controllerType];
|
|
804
1003
|
if (handler) {
|
|
805
1004
|
handler.call(this, channelNumber, value);
|
|
806
1005
|
}
|
|
807
1006
|
else {
|
|
808
|
-
console.warn(`Unsupported Control change:
|
|
1007
|
+
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
809
1008
|
}
|
|
810
1009
|
}
|
|
811
1010
|
updateModulation(channel) {
|
|
@@ -816,11 +1015,10 @@ class MidyGMLite {
|
|
|
816
1015
|
if (!note)
|
|
817
1016
|
continue;
|
|
818
1017
|
if (note.modulationDepth) {
|
|
819
|
-
note.modulationDepth.gain.setValueAtTime(channel.modulationDepth, now);
|
|
1018
|
+
note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
|
|
820
1019
|
}
|
|
821
1020
|
else {
|
|
822
|
-
|
|
823
|
-
this.setPitch(note, semitoneOffset);
|
|
1021
|
+
this.setPitchEnvelope(note);
|
|
824
1022
|
this.startModulation(channel, note, now);
|
|
825
1023
|
}
|
|
826
1024
|
}
|
|
@@ -828,16 +1026,17 @@ class MidyGMLite {
|
|
|
828
1026
|
}
|
|
829
1027
|
setModulationDepth(channelNumber, modulation) {
|
|
830
1028
|
const channel = this.channels[channelNumber];
|
|
831
|
-
channel.modulationDepth = (modulation / 127) *
|
|
1029
|
+
channel.state.modulationDepth = (modulation / 127) *
|
|
1030
|
+
channel.modulationDepthRange;
|
|
832
1031
|
this.updateModulation(channel);
|
|
833
1032
|
}
|
|
834
1033
|
setVolume(channelNumber, volume) {
|
|
835
1034
|
const channel = this.channels[channelNumber];
|
|
836
|
-
channel.volume = volume / 127;
|
|
1035
|
+
channel.state.volume = volume / 127;
|
|
837
1036
|
this.updateChannelVolume(channel);
|
|
838
1037
|
}
|
|
839
1038
|
panToGain(pan) {
|
|
840
|
-
const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
|
|
1039
|
+
const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
|
|
841
1040
|
return {
|
|
842
1041
|
gainLeft: Math.cos(theta),
|
|
843
1042
|
gainRight: Math.sin(theta),
|
|
@@ -845,12 +1044,12 @@ class MidyGMLite {
|
|
|
845
1044
|
}
|
|
846
1045
|
setPan(channelNumber, pan) {
|
|
847
1046
|
const channel = this.channels[channelNumber];
|
|
848
|
-
channel.pan = pan;
|
|
1047
|
+
channel.state.pan = pan / 127;
|
|
849
1048
|
this.updateChannelVolume(channel);
|
|
850
1049
|
}
|
|
851
1050
|
setExpression(channelNumber, expression) {
|
|
852
1051
|
const channel = this.channels[channelNumber];
|
|
853
|
-
channel.expression = expression / 127;
|
|
1052
|
+
channel.state.expression = expression / 127;
|
|
854
1053
|
this.updateChannelVolume(channel);
|
|
855
1054
|
}
|
|
856
1055
|
dataEntryLSB(channelNumber, value) {
|
|
@@ -859,8 +1058,9 @@ class MidyGMLite {
|
|
|
859
1058
|
}
|
|
860
1059
|
updateChannelVolume(channel) {
|
|
861
1060
|
const now = this.audioContext.currentTime;
|
|
862
|
-
const
|
|
863
|
-
const
|
|
1061
|
+
const state = channel.state;
|
|
1062
|
+
const volume = state.volume * state.expression;
|
|
1063
|
+
const { gainLeft, gainRight } = this.panToGain(state.pan);
|
|
864
1064
|
channel.gainL.gain
|
|
865
1065
|
.cancelScheduledValues(now)
|
|
866
1066
|
.setValueAtTime(volume * gainLeft, now);
|
|
@@ -869,12 +1069,29 @@ class MidyGMLite {
|
|
|
869
1069
|
.setValueAtTime(volume * gainRight, now);
|
|
870
1070
|
}
|
|
871
1071
|
setSustainPedal(channelNumber, value) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (!isOn) {
|
|
1072
|
+
this.channels[channelNumber].state.sustainPedal = value / 127;
|
|
1073
|
+
if (value < 64) {
|
|
875
1074
|
this.releaseSustainPedal(channelNumber, value);
|
|
876
1075
|
}
|
|
877
1076
|
}
|
|
1077
|
+
limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
|
|
1078
|
+
if (maxLSB < channel.dataLSB) {
|
|
1079
|
+
channel.dataMSB++;
|
|
1080
|
+
channel.dataLSB = minLSB;
|
|
1081
|
+
}
|
|
1082
|
+
else if (channel.dataLSB < 0) {
|
|
1083
|
+
channel.dataMSB--;
|
|
1084
|
+
channel.dataLSB = maxLSB;
|
|
1085
|
+
}
|
|
1086
|
+
if (maxMSB < channel.dataMSB) {
|
|
1087
|
+
channel.dataMSB = maxMSB;
|
|
1088
|
+
channel.dataLSB = maxLSB;
|
|
1089
|
+
}
|
|
1090
|
+
else if (channel.dataMSB < 0) {
|
|
1091
|
+
channel.dataMSB = minMSB;
|
|
1092
|
+
channel.dataLSB = minLSB;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
878
1095
|
handleRPN(channelNumber) {
|
|
879
1096
|
const channel = this.channels[channelNumber];
|
|
880
1097
|
const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
|
|
@@ -896,40 +1113,44 @@ class MidyGMLite {
|
|
|
896
1113
|
this.channels[channelNumber].dataMSB = value;
|
|
897
1114
|
this.handleRPN(channelNumber);
|
|
898
1115
|
}
|
|
899
|
-
updateDetune(channel, detuneChange) {
|
|
900
|
-
const now = this.audioContext.currentTime;
|
|
901
|
-
channel.scheduledNotes.forEach((noteList) => {
|
|
902
|
-
for (let i = 0; i < noteList.length; i++) {
|
|
903
|
-
const note = noteList[i];
|
|
904
|
-
if (!note)
|
|
905
|
-
continue;
|
|
906
|
-
const { bufferSource } = note;
|
|
907
|
-
const detune = bufferSource.detune.value + detuneChange;
|
|
908
|
-
bufferSource.detune
|
|
909
|
-
.cancelScheduledValues(now)
|
|
910
|
-
.setValueAtTime(detune, now);
|
|
911
|
-
}
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
1116
|
handlePitchBendRangeRPN(channelNumber) {
|
|
915
1117
|
const channel = this.channels[channelNumber];
|
|
916
1118
|
this.limitData(channel, 0, 127, 0, 99);
|
|
917
1119
|
const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
|
|
918
1120
|
this.setPitchBendRange(channelNumber, pitchBendRange);
|
|
919
1121
|
}
|
|
920
|
-
setPitchBendRange(channelNumber,
|
|
1122
|
+
setPitchBendRange(channelNumber, pitchWheelSensitivity) {
|
|
921
1123
|
const channel = this.channels[channelNumber];
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
this.
|
|
1124
|
+
const state = channel.state;
|
|
1125
|
+
state.pitchWheelSensitivity = pitchWheelSensitivity / 128;
|
|
1126
|
+
const detune = (state.pitchWheel * 2 - 1) * pitchWheelSensitivity * 100;
|
|
1127
|
+
this.updateDetune(channel, detune);
|
|
1128
|
+
this.applyVoiceParams(channel, 16);
|
|
927
1129
|
}
|
|
928
1130
|
allSoundOff(channelNumber) {
|
|
929
1131
|
return this.stopChannelNotes(channelNumber, 0, true);
|
|
930
1132
|
}
|
|
931
1133
|
resetAllControllers(channelNumber) {
|
|
932
|
-
|
|
1134
|
+
const stateTypes = [
|
|
1135
|
+
"expression",
|
|
1136
|
+
"modulationDepth",
|
|
1137
|
+
"sustainPedal",
|
|
1138
|
+
"pitchWheelSensitivity",
|
|
1139
|
+
];
|
|
1140
|
+
const channel = this.channels[channelNumber];
|
|
1141
|
+
const state = channel.state;
|
|
1142
|
+
for (let i = 0; i < stateTypes.length; i++) {
|
|
1143
|
+
const type = stateTypes[i];
|
|
1144
|
+
state[type] = defaultControllerState[type];
|
|
1145
|
+
}
|
|
1146
|
+
const settingTypes = [
|
|
1147
|
+
"rpnMSB",
|
|
1148
|
+
"rpnLSB",
|
|
1149
|
+
];
|
|
1150
|
+
for (let i = 0; i < settingTypes.length; i++) {
|
|
1151
|
+
const type = settingTypes[i];
|
|
1152
|
+
channel[type] = this.constructor.channelSettings[type];
|
|
1153
|
+
}
|
|
933
1154
|
}
|
|
934
1155
|
allNotesOff(channelNumber) {
|
|
935
1156
|
return this.stopChannelNotes(channelNumber, 0, false);
|
|
@@ -954,11 +1175,8 @@ class MidyGMLite {
|
|
|
954
1175
|
GM1SystemOn() {
|
|
955
1176
|
for (let i = 0; i < this.channels.length; i++) {
|
|
956
1177
|
const channel = this.channels[i];
|
|
957
|
-
channel.bankMSB = 0;
|
|
958
|
-
channel.bankLSB = 0;
|
|
959
1178
|
channel.bank = 0;
|
|
960
1179
|
}
|
|
961
|
-
this.channels[9].bankMSB = 1;
|
|
962
1180
|
this.channels[9].bank = 128;
|
|
963
1181
|
}
|
|
964
1182
|
handleUniversalRealTimeExclusiveMessage(data) {
|
|
@@ -1020,26 +1238,13 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
|
|
|
1020
1238
|
configurable: true,
|
|
1021
1239
|
writable: true,
|
|
1022
1240
|
value: {
|
|
1023
|
-
|
|
1024
|
-
|
|
1241
|
+
currentBufferSource: null,
|
|
1242
|
+
detune: 0,
|
|
1243
|
+
program: 0,
|
|
1025
1244
|
bank: 0,
|
|
1026
1245
|
dataMSB: 0,
|
|
1027
1246
|
dataLSB: 0,
|
|
1028
|
-
program: 0,
|
|
1029
|
-
pitchBend: 0,
|
|
1030
|
-
modulationDepthRange: 50, // cent
|
|
1031
|
-
}
|
|
1032
|
-
});
|
|
1033
|
-
Object.defineProperty(MidyGMLite, "effectSettings", {
|
|
1034
|
-
enumerable: true,
|
|
1035
|
-
configurable: true,
|
|
1036
|
-
writable: true,
|
|
1037
|
-
value: {
|
|
1038
|
-
expression: 1,
|
|
1039
|
-
modulationDepth: 0,
|
|
1040
|
-
sustainPedal: false,
|
|
1041
1247
|
rpnMSB: 127,
|
|
1042
1248
|
rpnLSB: 127,
|
|
1043
|
-
pitchBendRange: 2,
|
|
1044
1249
|
}
|
|
1045
1250
|
});
|