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