@marmooo/midy 0.1.7 → 0.2.0
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/midy-GM1.d.ts +48 -23
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +358 -135
- package/esm/midy-GM2.d.ts +50 -33
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +512 -220
- package/esm/midy-GMLite.d.ts +46 -22
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +339 -137
- package/esm/midy.d.ts +50 -38
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +526 -237
- package/package.json +2 -2
- package/script/midy-GM1.d.ts +48 -23
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +358 -135
- package/script/midy-GM2.d.ts +50 -33
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +512 -220
- package/script/midy-GMLite.d.ts +46 -22
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +339 -137
- package/script/midy.d.ts +50 -38
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +526 -237
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);
|
|
@@ -522,41 +588,49 @@ class MidyGMLite {
|
|
|
522
588
|
return 8.176 * Math.pow(2, cent / 1200);
|
|
523
589
|
}
|
|
524
590
|
calcSemitoneOffset(channel) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
return instrumentKey.playbackRate(noteNumber) *
|
|
529
|
-
Math.pow(2, semitoneOffset / 12);
|
|
591
|
+
const pitchWheel = channel.state.pitchWheel * 2 - 1;
|
|
592
|
+
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 128;
|
|
593
|
+
return pitchWheel * pitchWheelSensitivity;
|
|
530
594
|
}
|
|
531
595
|
setVolumeEnvelope(note) {
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
const
|
|
596
|
+
const now = this.audioContext.currentTime;
|
|
597
|
+
const { voiceParams, startTime } = note;
|
|
598
|
+
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
|
|
599
|
+
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
600
|
+
const volDelay = startTime + voiceParams.volDelay;
|
|
601
|
+
const volAttack = volDelay + voiceParams.volAttack;
|
|
602
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
603
|
+
const volDecay = volHold + voiceParams.volDecay;
|
|
539
604
|
note.volumeNode.gain
|
|
540
|
-
.cancelScheduledValues(
|
|
605
|
+
.cancelScheduledValues(now)
|
|
541
606
|
.setValueAtTime(0, startTime)
|
|
542
607
|
.setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
543
608
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
544
609
|
.setValueAtTime(attackVolume, volHold)
|
|
545
610
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
546
611
|
}
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
612
|
+
setPlaybackRate(note) {
|
|
613
|
+
const now = this.audioContext.currentTime;
|
|
614
|
+
note.bufferSource.playbackRate
|
|
615
|
+
.cancelScheduledValues(now)
|
|
616
|
+
.setValueAtTime(note.voiceParams.playbackRate, now);
|
|
617
|
+
}
|
|
618
|
+
setPitch(channel, note) {
|
|
619
|
+
const now = this.audioContext.currentTime;
|
|
620
|
+
const { startTime } = note;
|
|
621
|
+
const basePitch = this.calcSemitoneOffset(channel) * 100;
|
|
622
|
+
note.bufferSource.detune
|
|
623
|
+
.cancelScheduledValues(now)
|
|
624
|
+
.setValueAtTime(basePitch, startTime);
|
|
625
|
+
const modEnvToPitch = note.voiceParams.modEnvToPitch;
|
|
551
626
|
if (modEnvToPitch === 0)
|
|
552
627
|
return;
|
|
553
|
-
const
|
|
554
|
-
const
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
note.bufferSource.playbackRate.value
|
|
628
|
+
const peekPitch = basePitch + modEnvToPitch;
|
|
629
|
+
const modDelay = startTime + voiceParams.modDelay;
|
|
630
|
+
const modAttack = modDelay + voiceParams.modAttack;
|
|
631
|
+
const modHold = modAttack + voiceParams.modHold;
|
|
632
|
+
const modDecay = modHold + voiceParams.modDecay;
|
|
633
|
+
note.bufferSource.detune
|
|
560
634
|
.setValueAtTime(basePitch, modDelay)
|
|
561
635
|
.exponentialRampToValueAtTime(peekPitch, modAttack)
|
|
562
636
|
.setValueAtTime(peekPitch, modHold)
|
|
@@ -568,20 +642,21 @@ class MidyGMLite {
|
|
|
568
642
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
569
643
|
}
|
|
570
644
|
setFilterEnvelope(note) {
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
const
|
|
645
|
+
const now = this.audioContext.currentTime;
|
|
646
|
+
const { voiceParams, startTime } = note;
|
|
647
|
+
const baseFreq = this.centToHz(voiceParams.initialFilterFc);
|
|
648
|
+
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
|
|
574
649
|
const sustainFreq = baseFreq +
|
|
575
|
-
(peekFreq - baseFreq) * (1 -
|
|
650
|
+
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
576
651
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
577
652
|
const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
|
|
578
653
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
579
|
-
const modDelay = startTime +
|
|
580
|
-
const modAttack = modDelay +
|
|
581
|
-
const modHold = modAttack +
|
|
582
|
-
const modDecay = modHold +
|
|
654
|
+
const modDelay = startTime + voiceParams.modDelay;
|
|
655
|
+
const modAttack = modDelay + voiceParams.modAttack;
|
|
656
|
+
const modHold = modAttack + voiceParams.modHold;
|
|
657
|
+
const modDecay = modHold + voiceParams.modDecay;
|
|
583
658
|
note.filterNode.frequency
|
|
584
|
-
.cancelScheduledValues(
|
|
659
|
+
.cancelScheduledValues(now)
|
|
585
660
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
586
661
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
587
662
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
@@ -589,25 +664,18 @@ class MidyGMLite {
|
|
|
589
664
|
.linearRampToValueAtTime(adjustedSustainFreq, modDecay);
|
|
590
665
|
}
|
|
591
666
|
startModulation(channel, note, startTime) {
|
|
592
|
-
const {
|
|
593
|
-
const { modLfoToPitch, modLfoToVolume } = instrumentKey;
|
|
667
|
+
const { voiceParams } = note;
|
|
594
668
|
note.modulationLFO = new OscillatorNode(this.audioContext, {
|
|
595
|
-
frequency: this.centToHz(
|
|
669
|
+
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
596
670
|
});
|
|
597
671
|
note.filterDepth = new GainNode(this.audioContext, {
|
|
598
|
-
gain:
|
|
672
|
+
gain: voiceParams.modLfoToFilterFc,
|
|
599
673
|
});
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
note.
|
|
603
|
-
|
|
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,
|
|
609
|
-
});
|
|
610
|
-
note.modulationLFO.start(startTime + instrumentKey.delayModLFO);
|
|
674
|
+
note.modulationDepth = new GainNode(this.audioContext);
|
|
675
|
+
this.setModLfoToPitch(channel, note);
|
|
676
|
+
note.volumeDepth = new GainNode(this.audioContext);
|
|
677
|
+
this.setModLfoToVolume(note);
|
|
678
|
+
note.modulationLFO.start(startTime + voiceParams.delayModLFO);
|
|
611
679
|
note.modulationLFO.connect(note.filterDepth);
|
|
612
680
|
note.filterDepth.connect(note.filterNode.frequency);
|
|
613
681
|
note.modulationLFO.connect(note.modulationDepth);
|
|
@@ -615,24 +683,23 @@ class MidyGMLite {
|
|
|
615
683
|
note.modulationLFO.connect(note.volumeDepth);
|
|
616
684
|
note.volumeDepth.connect(note.volumeNode.gain);
|
|
617
685
|
}
|
|
618
|
-
async createNote(channel,
|
|
619
|
-
const
|
|
620
|
-
const
|
|
621
|
-
note
|
|
686
|
+
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
687
|
+
const state = channel.state;
|
|
688
|
+
const voiceParams = voice.getAllParams(state.array);
|
|
689
|
+
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
690
|
+
note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
|
|
622
691
|
note.volumeNode = new GainNode(this.audioContext);
|
|
623
692
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
624
693
|
type: "lowpass",
|
|
625
|
-
Q:
|
|
694
|
+
Q: voiceParams.initialFilterQ / 10, // dB
|
|
626
695
|
});
|
|
627
696
|
this.setVolumeEnvelope(note);
|
|
628
697
|
this.setFilterEnvelope(note);
|
|
629
|
-
|
|
630
|
-
|
|
698
|
+
this.setPlaybackRate(note);
|
|
699
|
+
if (0 < state.modulationDepth) {
|
|
700
|
+
this.setPitch(channel, note);
|
|
631
701
|
this.startModulation(channel, note, startTime);
|
|
632
702
|
}
|
|
633
|
-
else {
|
|
634
|
-
note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
|
|
635
|
-
}
|
|
636
703
|
note.bufferSource.connect(note.filterNode);
|
|
637
704
|
note.filterNode.connect(note.volumeNode);
|
|
638
705
|
note.bufferSource.start(startTime);
|
|
@@ -646,13 +713,13 @@ class MidyGMLite {
|
|
|
646
713
|
return;
|
|
647
714
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
648
715
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
649
|
-
const
|
|
650
|
-
if (!
|
|
716
|
+
const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
|
|
717
|
+
if (!voice)
|
|
651
718
|
return;
|
|
652
|
-
const note = await this.createNote(channel,
|
|
719
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
653
720
|
note.volumeNode.connect(channel.gainL);
|
|
654
721
|
note.volumeNode.connect(channel.gainR);
|
|
655
|
-
const exclusiveClass =
|
|
722
|
+
const exclusiveClass = note.voiceParams.exclusiveClass;
|
|
656
723
|
if (exclusiveClass !== 0) {
|
|
657
724
|
if (this.exclusiveClassMap.has(exclusiveClass)) {
|
|
658
725
|
const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
|
|
@@ -697,10 +764,6 @@ class MidyGMLite {
|
|
|
697
764
|
note.modulationDepth.disconnect();
|
|
698
765
|
note.modulationLFO.stop();
|
|
699
766
|
}
|
|
700
|
-
if (note.vibratoDepth) {
|
|
701
|
-
note.vibratoDepth.disconnect();
|
|
702
|
-
note.vibratoLFO.stop();
|
|
703
|
-
}
|
|
704
767
|
resolve();
|
|
705
768
|
};
|
|
706
769
|
note.bufferSource.stop(stopTime);
|
|
@@ -708,7 +771,7 @@ class MidyGMLite {
|
|
|
708
771
|
}
|
|
709
772
|
scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
710
773
|
const channel = this.channels[channelNumber];
|
|
711
|
-
if (!force && channel.sustainPedal)
|
|
774
|
+
if (!force && 0.5 < channel.state.sustainPedal)
|
|
712
775
|
return;
|
|
713
776
|
if (!channel.scheduledNotes.has(noteNumber))
|
|
714
777
|
return;
|
|
@@ -719,8 +782,8 @@ class MidyGMLite {
|
|
|
719
782
|
continue;
|
|
720
783
|
if (note.ending)
|
|
721
784
|
continue;
|
|
722
|
-
const volRelease = endTime + note.
|
|
723
|
-
const modRelease = endTime + note.
|
|
785
|
+
const volRelease = endTime + note.voiceParams.volRelease;
|
|
786
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
724
787
|
note.filterNode.frequency
|
|
725
788
|
.cancelScheduledValues(endTime)
|
|
726
789
|
.linearRampToValueAtTime(0, modRelease);
|
|
@@ -736,7 +799,7 @@ class MidyGMLite {
|
|
|
736
799
|
const velocity = halfVelocity * 2;
|
|
737
800
|
const channel = this.channels[channelNumber];
|
|
738
801
|
const promises = [];
|
|
739
|
-
channel.sustainPedal =
|
|
802
|
+
channel.state.sustainPedal = halfVelocity;
|
|
740
803
|
channel.scheduledNotes.forEach((noteList) => {
|
|
741
804
|
for (let i = 0; i < noteList.length; i++) {
|
|
742
805
|
const note = noteList[i];
|
|
@@ -772,17 +835,137 @@ class MidyGMLite {
|
|
|
772
835
|
channel.program = program;
|
|
773
836
|
}
|
|
774
837
|
handlePitchBendMessage(channelNumber, lsb, msb) {
|
|
775
|
-
const pitchBend = msb * 128 + lsb
|
|
838
|
+
const pitchBend = msb * 128 + lsb;
|
|
776
839
|
this.setPitchBend(channelNumber, pitchBend);
|
|
777
840
|
}
|
|
778
|
-
setPitchBend(channelNumber,
|
|
841
|
+
setPitchBend(channelNumber, value) {
|
|
779
842
|
const channel = this.channels[channelNumber];
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
843
|
+
const state = channel.state;
|
|
844
|
+
state.pitchWheel = value / 16383;
|
|
845
|
+
const pitchWheel = (value - 8192) / 8192;
|
|
846
|
+
const detuneChange = pitchWheel * state.pitchWheelSensitivity * 12800;
|
|
784
847
|
this.updateDetune(channel, detuneChange);
|
|
785
848
|
}
|
|
849
|
+
setModLfoToPitch(channel, note) {
|
|
850
|
+
const now = this.audioContext.currentTime;
|
|
851
|
+
const modLfoToPitch = note.voiceParams.modLfoToPitch;
|
|
852
|
+
const modulationDepth = Math.abs(modLfoToPitch) +
|
|
853
|
+
channel.state.modulationDepth;
|
|
854
|
+
const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
|
|
855
|
+
note.modulationDepth.gain
|
|
856
|
+
.cancelScheduledValues(now)
|
|
857
|
+
.setValueAtTime(modulationDepth * modulationDepthSign, now);
|
|
858
|
+
}
|
|
859
|
+
setModLfoToVolume(note) {
|
|
860
|
+
const now = this.audioContext.currentTime;
|
|
861
|
+
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
862
|
+
const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
863
|
+
const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
|
|
864
|
+
note.volumeDepth.gain
|
|
865
|
+
.cancelScheduledValues(now)
|
|
866
|
+
.setValueAtTime(volumeDepth * volumeDepthSign, now);
|
|
867
|
+
}
|
|
868
|
+
setModLfoToFilterFc(note) {
|
|
869
|
+
const now = this.audioContext.currentTime;
|
|
870
|
+
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
|
|
871
|
+
note.filterDepth.gain
|
|
872
|
+
.cancelScheduledValues(now)
|
|
873
|
+
.setValueAtTime(modLfoToFilterFc, now);
|
|
874
|
+
}
|
|
875
|
+
setDelayModLFO(note) {
|
|
876
|
+
const now = this.audioContext.currentTime;
|
|
877
|
+
const startTime = note.startTime;
|
|
878
|
+
if (startTime < now)
|
|
879
|
+
return;
|
|
880
|
+
note.modulationLFO.stop(now);
|
|
881
|
+
note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
|
|
882
|
+
note.modulationLFO.connect(note.filterDepth);
|
|
883
|
+
}
|
|
884
|
+
setFreqModLFO(note) {
|
|
885
|
+
const now = this.audioContext.currentTime;
|
|
886
|
+
const freqModLFO = note.voiceParams.freqModLFO;
|
|
887
|
+
note.modulationLFO.frequency
|
|
888
|
+
.cancelScheduledValues(now)
|
|
889
|
+
.setValueAtTime(freqModLFO, now);
|
|
890
|
+
}
|
|
891
|
+
createVoiceParamsHandlers() {
|
|
892
|
+
return {
|
|
893
|
+
modLfoToPitch: (channel, note, _prevValue) => {
|
|
894
|
+
if (0 < channel.state.modulationDepth) {
|
|
895
|
+
this.setModLfoToPitch(channel, note);
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
vibLfoToPitch: (_channel, _note, _prevValue) => { },
|
|
899
|
+
modLfoToFilterFc: (channel, note, _prevValue) => {
|
|
900
|
+
if (0 < channel.state.modulationDepth)
|
|
901
|
+
this.setModLfoToFilterFc(note);
|
|
902
|
+
},
|
|
903
|
+
modLfoToVolume: (channel, note) => {
|
|
904
|
+
if (0 < channel.state.modulationDepth)
|
|
905
|
+
this.setModLfoToVolume(note);
|
|
906
|
+
},
|
|
907
|
+
chorusEffectsSend: (_channel, _note, _prevValue) => { },
|
|
908
|
+
reverbEffectsSend: (_channel, _note, _prevValue) => { },
|
|
909
|
+
delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
|
|
910
|
+
freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
|
|
911
|
+
delayVibLFO: (_channel, _note, _prevValue) => { },
|
|
912
|
+
freqVibLFO: (_channel, _note, _prevValue) => { },
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
getControllerState(channel, noteNumber, velocity) {
|
|
916
|
+
const state = new Float32Array(channel.state.array.length);
|
|
917
|
+
state.set(channel.state.array);
|
|
918
|
+
state[2] = velocity / 127;
|
|
919
|
+
state[3] = noteNumber / 127;
|
|
920
|
+
return state;
|
|
921
|
+
}
|
|
922
|
+
applyVoiceParams(channel, controllerType) {
|
|
923
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
924
|
+
for (let i = 0; i < noteList.length; i++) {
|
|
925
|
+
const note = noteList[i];
|
|
926
|
+
if (!note)
|
|
927
|
+
continue;
|
|
928
|
+
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
929
|
+
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
930
|
+
let appliedFilterEnvelope = false;
|
|
931
|
+
let appliedVolumeEnvelope = false;
|
|
932
|
+
for (const [key, value] of Object.entries(voiceParams)) {
|
|
933
|
+
const prevValue = note.voiceParams[key];
|
|
934
|
+
if (value === prevValue)
|
|
935
|
+
continue;
|
|
936
|
+
note.voiceParams[key] = value;
|
|
937
|
+
if (key in this.voiceParamsHandlers) {
|
|
938
|
+
this.voiceParamsHandlers[key](channel, note, prevValue);
|
|
939
|
+
}
|
|
940
|
+
else if (filterEnvelopeKeySet.has(key)) {
|
|
941
|
+
if (appliedFilterEnvelope)
|
|
942
|
+
continue;
|
|
943
|
+
appliedFilterEnvelope = true;
|
|
944
|
+
const noteVoiceParams = note.voiceParams;
|
|
945
|
+
for (let i = 0; i < filterEnvelopeKeys.length; i++) {
|
|
946
|
+
const key = filterEnvelopeKeys[i];
|
|
947
|
+
if (key in voiceParams)
|
|
948
|
+
noteVoiceParams[key] = voiceParams[key];
|
|
949
|
+
}
|
|
950
|
+
this.setFilterEnvelope(channel, note);
|
|
951
|
+
this.setPitch(channel, note);
|
|
952
|
+
}
|
|
953
|
+
else if (volumeEnvelopeKeySet.has(key)) {
|
|
954
|
+
if (appliedVolumeEnvelope)
|
|
955
|
+
continue;
|
|
956
|
+
appliedVolumeEnvelope = true;
|
|
957
|
+
const noteVoiceParams = note.voiceParams;
|
|
958
|
+
for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
|
|
959
|
+
const key = volumeEnvelopeKeys[i];
|
|
960
|
+
if (key in voiceParams)
|
|
961
|
+
noteVoiceParams[key] = voiceParams[key];
|
|
962
|
+
}
|
|
963
|
+
this.setVolumeEnvelope(channel, note);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
}
|
|
786
969
|
createControlChangeHandlers() {
|
|
787
970
|
return {
|
|
788
971
|
1: this.setModulationDepth,
|
|
@@ -799,13 +982,13 @@ class MidyGMLite {
|
|
|
799
982
|
123: this.allNotesOff,
|
|
800
983
|
};
|
|
801
984
|
}
|
|
802
|
-
handleControlChange(channelNumber,
|
|
803
|
-
const handler = this.controlChangeHandlers[
|
|
985
|
+
handleControlChange(channelNumber, controllerType, value) {
|
|
986
|
+
const handler = this.controlChangeHandlers[controllerType];
|
|
804
987
|
if (handler) {
|
|
805
988
|
handler.call(this, channelNumber, value);
|
|
806
989
|
}
|
|
807
990
|
else {
|
|
808
|
-
console.warn(`Unsupported Control change:
|
|
991
|
+
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
809
992
|
}
|
|
810
993
|
}
|
|
811
994
|
updateModulation(channel) {
|
|
@@ -816,11 +999,10 @@ class MidyGMLite {
|
|
|
816
999
|
if (!note)
|
|
817
1000
|
continue;
|
|
818
1001
|
if (note.modulationDepth) {
|
|
819
|
-
note.modulationDepth.gain.setValueAtTime(channel.modulationDepth, now);
|
|
1002
|
+
note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
|
|
820
1003
|
}
|
|
821
1004
|
else {
|
|
822
|
-
|
|
823
|
-
this.setPitch(note, semitoneOffset);
|
|
1005
|
+
this.setPitch(channel, note);
|
|
824
1006
|
this.startModulation(channel, note, now);
|
|
825
1007
|
}
|
|
826
1008
|
}
|
|
@@ -828,16 +1010,17 @@ class MidyGMLite {
|
|
|
828
1010
|
}
|
|
829
1011
|
setModulationDepth(channelNumber, modulation) {
|
|
830
1012
|
const channel = this.channels[channelNumber];
|
|
831
|
-
channel.modulationDepth = (modulation / 127) *
|
|
1013
|
+
channel.state.modulationDepth = (modulation / 127) *
|
|
1014
|
+
channel.modulationDepthRange;
|
|
832
1015
|
this.updateModulation(channel);
|
|
833
1016
|
}
|
|
834
1017
|
setVolume(channelNumber, volume) {
|
|
835
1018
|
const channel = this.channels[channelNumber];
|
|
836
|
-
channel.volume = volume / 127;
|
|
1019
|
+
channel.state.volume = volume / 127;
|
|
837
1020
|
this.updateChannelVolume(channel);
|
|
838
1021
|
}
|
|
839
1022
|
panToGain(pan) {
|
|
840
|
-
const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
|
|
1023
|
+
const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
|
|
841
1024
|
return {
|
|
842
1025
|
gainLeft: Math.cos(theta),
|
|
843
1026
|
gainRight: Math.sin(theta),
|
|
@@ -845,12 +1028,12 @@ class MidyGMLite {
|
|
|
845
1028
|
}
|
|
846
1029
|
setPan(channelNumber, pan) {
|
|
847
1030
|
const channel = this.channels[channelNumber];
|
|
848
|
-
channel.pan = pan;
|
|
1031
|
+
channel.state.pan = pan / 127;
|
|
849
1032
|
this.updateChannelVolume(channel);
|
|
850
1033
|
}
|
|
851
1034
|
setExpression(channelNumber, expression) {
|
|
852
1035
|
const channel = this.channels[channelNumber];
|
|
853
|
-
channel.expression = expression / 127;
|
|
1036
|
+
channel.state.expression = expression / 127;
|
|
854
1037
|
this.updateChannelVolume(channel);
|
|
855
1038
|
}
|
|
856
1039
|
dataEntryLSB(channelNumber, value) {
|
|
@@ -859,8 +1042,9 @@ class MidyGMLite {
|
|
|
859
1042
|
}
|
|
860
1043
|
updateChannelVolume(channel) {
|
|
861
1044
|
const now = this.audioContext.currentTime;
|
|
862
|
-
const
|
|
863
|
-
const
|
|
1045
|
+
const state = channel.state;
|
|
1046
|
+
const volume = state.volume * state.expression;
|
|
1047
|
+
const { gainLeft, gainRight } = this.panToGain(state.pan);
|
|
864
1048
|
channel.gainL.gain
|
|
865
1049
|
.cancelScheduledValues(now)
|
|
866
1050
|
.setValueAtTime(volume * gainLeft, now);
|
|
@@ -869,12 +1053,29 @@ class MidyGMLite {
|
|
|
869
1053
|
.setValueAtTime(volume * gainRight, now);
|
|
870
1054
|
}
|
|
871
1055
|
setSustainPedal(channelNumber, value) {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (!isOn) {
|
|
1056
|
+
this.channels[channelNumber].state.sustainPedal = value / 127;
|
|
1057
|
+
if (value < 64) {
|
|
875
1058
|
this.releaseSustainPedal(channelNumber, value);
|
|
876
1059
|
}
|
|
877
1060
|
}
|
|
1061
|
+
limitData(channel, minMSB, maxMSB, minLSB, maxLSB) {
|
|
1062
|
+
if (maxLSB < channel.dataLSB) {
|
|
1063
|
+
channel.dataMSB++;
|
|
1064
|
+
channel.dataLSB = minLSB;
|
|
1065
|
+
}
|
|
1066
|
+
else if (channel.dataLSB < 0) {
|
|
1067
|
+
channel.dataMSB--;
|
|
1068
|
+
channel.dataLSB = maxLSB;
|
|
1069
|
+
}
|
|
1070
|
+
if (maxMSB < channel.dataMSB) {
|
|
1071
|
+
channel.dataMSB = maxMSB;
|
|
1072
|
+
channel.dataLSB = maxLSB;
|
|
1073
|
+
}
|
|
1074
|
+
else if (channel.dataMSB < 0) {
|
|
1075
|
+
channel.dataMSB = minMSB;
|
|
1076
|
+
channel.dataLSB = minLSB;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
878
1079
|
handleRPN(channelNumber) {
|
|
879
1080
|
const channel = this.channels[channelNumber];
|
|
880
1081
|
const rpn = channel.rpnMSB * 128 + channel.rpnLSB;
|
|
@@ -896,7 +1097,7 @@ class MidyGMLite {
|
|
|
896
1097
|
this.channels[channelNumber].dataMSB = value;
|
|
897
1098
|
this.handleRPN(channelNumber);
|
|
898
1099
|
}
|
|
899
|
-
updateDetune(channel,
|
|
1100
|
+
updateDetune(channel, detune) {
|
|
900
1101
|
const now = this.audioContext.currentTime;
|
|
901
1102
|
channel.scheduledNotes.forEach((noteList) => {
|
|
902
1103
|
for (let i = 0; i < noteList.length; i++) {
|
|
@@ -904,7 +1105,6 @@ class MidyGMLite {
|
|
|
904
1105
|
if (!note)
|
|
905
1106
|
continue;
|
|
906
1107
|
const { bufferSource } = note;
|
|
907
|
-
const detune = bufferSource.detune.value + detuneChange;
|
|
908
1108
|
bufferSource.detune
|
|
909
1109
|
.cancelScheduledValues(now)
|
|
910
1110
|
.setValueAtTime(detune, now);
|
|
@@ -917,19 +1117,38 @@ class MidyGMLite {
|
|
|
917
1117
|
const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
|
|
918
1118
|
this.setPitchBendRange(channelNumber, pitchBendRange);
|
|
919
1119
|
}
|
|
920
|
-
setPitchBendRange(channelNumber,
|
|
1120
|
+
setPitchBendRange(channelNumber, pitchWheelSensitivity) {
|
|
921
1121
|
const channel = this.channels[channelNumber];
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
this.
|
|
1122
|
+
const state = channel.state;
|
|
1123
|
+
state.pitchWheelSensitivity = pitchWheelSensitivity / 128;
|
|
1124
|
+
const detune = (state.pitchWheel * 2 - 1) * pitchWheelSensitivity * 100;
|
|
1125
|
+
this.updateDetune(channel, detune);
|
|
1126
|
+
this.applyVoiceParams(channel, 16);
|
|
927
1127
|
}
|
|
928
1128
|
allSoundOff(channelNumber) {
|
|
929
1129
|
return this.stopChannelNotes(channelNumber, 0, true);
|
|
930
1130
|
}
|
|
931
1131
|
resetAllControllers(channelNumber) {
|
|
932
|
-
|
|
1132
|
+
const stateTypes = [
|
|
1133
|
+
"expression",
|
|
1134
|
+
"modulationDepth",
|
|
1135
|
+
"sustainPedal",
|
|
1136
|
+
"pitchWheelSensitivity",
|
|
1137
|
+
];
|
|
1138
|
+
const channel = this.channels[channelNumber];
|
|
1139
|
+
const state = channel.state;
|
|
1140
|
+
for (let i = 0; i < stateTypes.length; i++) {
|
|
1141
|
+
const type = stateTypes[i];
|
|
1142
|
+
state[type] = defaultControllerState[type];
|
|
1143
|
+
}
|
|
1144
|
+
const settingTypes = [
|
|
1145
|
+
"rpnMSB",
|
|
1146
|
+
"rpnLSB",
|
|
1147
|
+
];
|
|
1148
|
+
for (let i = 0; i < settingTypes.length; i++) {
|
|
1149
|
+
const type = settingTypes[i];
|
|
1150
|
+
channel[type] = this.constructor.channelSettings[type];
|
|
1151
|
+
}
|
|
933
1152
|
}
|
|
934
1153
|
allNotesOff(channelNumber) {
|
|
935
1154
|
return this.stopChannelNotes(channelNumber, 0, false);
|
|
@@ -954,11 +1173,8 @@ class MidyGMLite {
|
|
|
954
1173
|
GM1SystemOn() {
|
|
955
1174
|
for (let i = 0; i < this.channels.length; i++) {
|
|
956
1175
|
const channel = this.channels[i];
|
|
957
|
-
channel.bankMSB = 0;
|
|
958
|
-
channel.bankLSB = 0;
|
|
959
1176
|
channel.bank = 0;
|
|
960
1177
|
}
|
|
961
|
-
this.channels[9].bankMSB = 1;
|
|
962
1178
|
this.channels[9].bank = 128;
|
|
963
1179
|
}
|
|
964
1180
|
handleUniversalRealTimeExclusiveMessage(data) {
|
|
@@ -1020,26 +1236,12 @@ Object.defineProperty(MidyGMLite, "channelSettings", {
|
|
|
1020
1236
|
configurable: true,
|
|
1021
1237
|
writable: true,
|
|
1022
1238
|
value: {
|
|
1023
|
-
|
|
1024
|
-
|
|
1239
|
+
currentBufferSource: null,
|
|
1240
|
+
program: 0,
|
|
1025
1241
|
bank: 0,
|
|
1026
1242
|
dataMSB: 0,
|
|
1027
1243
|
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
1244
|
rpnMSB: 127,
|
|
1042
1245
|
rpnLSB: 127,
|
|
1043
|
-
pitchBendRange: 2,
|
|
1044
1246
|
}
|
|
1045
1247
|
});
|