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