@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-GM1.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,
|
|
@@ -53,9 +53,75 @@ class Note {
|
|
|
53
53
|
this.noteNumber = noteNumber;
|
|
54
54
|
this.velocity = velocity;
|
|
55
55
|
this.startTime = startTime;
|
|
56
|
-
this.
|
|
56
|
+
this.voice = voice;
|
|
57
|
+
this.voiceParams = voiceParams;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
// normalized to 0-1 for use with the SF2 modulator model
|
|
61
|
+
const defaultControllerState = {
|
|
62
|
+
noteOnVelocity: { type: 2, defaultValue: 0 },
|
|
63
|
+
noteOnKeyNumber: { type: 3, defaultValue: 0 },
|
|
64
|
+
pitchWheel: { type: 14, defaultValue: 8192 / 16383 },
|
|
65
|
+
pitchWheelSensitivity: { type: 16, defaultValue: 2 / 128 },
|
|
66
|
+
link: { type: 127, defaultValue: 0 },
|
|
67
|
+
// bankMSB: { type: 128 + 0, defaultValue: 121, },
|
|
68
|
+
modulationDepth: { type: 128 + 1, defaultValue: 0 },
|
|
69
|
+
// dataMSB: { type: 128 + 6, defaultValue: 0, },
|
|
70
|
+
volume: { type: 128 + 7, defaultValue: 100 / 127 },
|
|
71
|
+
pan: { type: 128 + 10, defaultValue: 0.5 },
|
|
72
|
+
expression: { type: 128 + 11, defaultValue: 1 },
|
|
73
|
+
// bankLSB: { type: 128 + 32, defaultValue: 0, },
|
|
74
|
+
// dataLSB: { type: 128 + 38, defaultValue: 0, },
|
|
75
|
+
sustainPedal: { type: 128 + 64, defaultValue: 0 },
|
|
76
|
+
// rpnLSB: { type: 128 + 100, defaultValue: 127 },
|
|
77
|
+
// rpnMSB: { type: 128 + 101, defaultValue: 127 },
|
|
78
|
+
// allSoundOff: { type: 128 + 120, defaultValue: 0 },
|
|
79
|
+
// resetAllControllers: { type: 128 + 121, defaultValue: 0 },
|
|
80
|
+
// allNotesOff: { type: 128 + 123, defaultValue: 0 },
|
|
81
|
+
};
|
|
82
|
+
class ControllerState {
|
|
83
|
+
constructor() {
|
|
84
|
+
Object.defineProperty(this, "array", {
|
|
85
|
+
enumerable: true,
|
|
86
|
+
configurable: true,
|
|
87
|
+
writable: true,
|
|
88
|
+
value: new Float32Array(256)
|
|
89
|
+
});
|
|
90
|
+
const entries = Object.entries(defaultControllerState);
|
|
91
|
+
for (const [name, { type, defaultValue }] of entries) {
|
|
92
|
+
this.array[type] = defaultValue;
|
|
93
|
+
Object.defineProperty(this, name, {
|
|
94
|
+
get: () => this.array[type],
|
|
95
|
+
set: (value) => this.array[type] = value,
|
|
96
|
+
enumerable: true,
|
|
97
|
+
configurable: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const filterEnvelopeKeys = [
|
|
103
|
+
"modEnvToPitch",
|
|
104
|
+
"initialFilterFc",
|
|
105
|
+
"modEnvToFilterFc",
|
|
106
|
+
"modDelay",
|
|
107
|
+
"modAttack",
|
|
108
|
+
"modHold",
|
|
109
|
+
"modDecay",
|
|
110
|
+
"modSustain",
|
|
111
|
+
"modRelease",
|
|
112
|
+
"playbackRate",
|
|
113
|
+
];
|
|
114
|
+
const filterEnvelopeKeySet = new Set(filterEnvelopeKeys);
|
|
115
|
+
const volumeEnvelopeKeys = [
|
|
116
|
+
"volDelay",
|
|
117
|
+
"volAttack",
|
|
118
|
+
"volHold",
|
|
119
|
+
"volDecay",
|
|
120
|
+
"volSustain",
|
|
121
|
+
"volRelease",
|
|
122
|
+
"initialAttenuation",
|
|
123
|
+
];
|
|
124
|
+
const volumeEnvelopeKeySet = new Set(volumeEnvelopeKeys);
|
|
59
125
|
export class MidyGM1 {
|
|
60
126
|
constructor(audioContext) {
|
|
61
127
|
Object.defineProperty(this, "ticksPerBeat", {
|
|
@@ -168,6 +234,7 @@ export class MidyGM1 {
|
|
|
168
234
|
});
|
|
169
235
|
this.audioContext = audioContext;
|
|
170
236
|
this.masterGain = new GainNode(audioContext);
|
|
237
|
+
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
171
238
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
172
239
|
this.channels = this.createChannels(audioContext);
|
|
173
240
|
this.masterGain.connect(audioContext.destination);
|
|
@@ -210,7 +277,7 @@ export class MidyGM1 {
|
|
|
210
277
|
this.totalTime = this.calcTotalTime();
|
|
211
278
|
}
|
|
212
279
|
setChannelAudioNodes(audioContext) {
|
|
213
|
-
const { gainLeft, gainRight } = this.panToGain(
|
|
280
|
+
const { gainLeft, gainRight } = this.panToGain(defaultControllerState.pan.defaultValue);
|
|
214
281
|
const gainL = new GainNode(audioContext, { gain: gainLeft });
|
|
215
282
|
const gainR = new GainNode(audioContext, { gain: gainRight });
|
|
216
283
|
const merger = new ChannelMergerNode(audioContext, { numberOfInputs: 2 });
|
|
@@ -227,18 +294,18 @@ export class MidyGM1 {
|
|
|
227
294
|
const channels = Array.from({ length: 16 }, () => {
|
|
228
295
|
return {
|
|
229
296
|
...this.constructor.channelSettings,
|
|
230
|
-
|
|
297
|
+
state: new ControllerState(),
|
|
231
298
|
...this.setChannelAudioNodes(audioContext),
|
|
232
299
|
scheduledNotes: new Map(),
|
|
233
300
|
};
|
|
234
301
|
});
|
|
235
302
|
return channels;
|
|
236
303
|
}
|
|
237
|
-
async createNoteBuffer(
|
|
238
|
-
const sampleStart =
|
|
239
|
-
const sampleEnd =
|
|
304
|
+
async createNoteBuffer(voiceParams, isSF3) {
|
|
305
|
+
const sampleStart = voiceParams.start;
|
|
306
|
+
const sampleEnd = voiceParams.sample.length + voiceParams.end;
|
|
240
307
|
if (isSF3) {
|
|
241
|
-
const sample =
|
|
308
|
+
const sample = voiceParams.sample;
|
|
242
309
|
const start = sample.byteOffset + sampleStart;
|
|
243
310
|
const end = sample.byteOffset + sampleEnd;
|
|
244
311
|
const buffer = sample.buffer.slice(start, end);
|
|
@@ -246,14 +313,14 @@ export class MidyGM1 {
|
|
|
246
313
|
return audioBuffer;
|
|
247
314
|
}
|
|
248
315
|
else {
|
|
249
|
-
const sample =
|
|
316
|
+
const sample = voiceParams.sample;
|
|
250
317
|
const start = sample.byteOffset + sampleStart;
|
|
251
318
|
const end = sample.byteOffset + sampleEnd;
|
|
252
319
|
const buffer = sample.buffer.slice(start, end);
|
|
253
320
|
const audioBuffer = new AudioBuffer({
|
|
254
321
|
numberOfChannels: 1,
|
|
255
322
|
length: sample.length,
|
|
256
|
-
sampleRate:
|
|
323
|
+
sampleRate: voiceParams.sampleRate,
|
|
257
324
|
});
|
|
258
325
|
const channelData = audioBuffer.getChannelData(0);
|
|
259
326
|
const int16Array = new Int16Array(buffer);
|
|
@@ -263,15 +330,14 @@ export class MidyGM1 {
|
|
|
263
330
|
return audioBuffer;
|
|
264
331
|
}
|
|
265
332
|
}
|
|
266
|
-
async createNoteBufferNode(
|
|
333
|
+
async createNoteBufferNode(voiceParams, isSF3) {
|
|
267
334
|
const bufferSource = new AudioBufferSourceNode(this.audioContext);
|
|
268
|
-
const audioBuffer = await this.createNoteBuffer(
|
|
335
|
+
const audioBuffer = await this.createNoteBuffer(voiceParams, isSF3);
|
|
269
336
|
bufferSource.buffer = audioBuffer;
|
|
270
|
-
bufferSource.loop =
|
|
337
|
+
bufferSource.loop = voiceParams.sampleModes % 2 !== 0;
|
|
271
338
|
if (bufferSource.loop) {
|
|
272
|
-
bufferSource.loopStart =
|
|
273
|
-
|
|
274
|
-
bufferSource.loopEnd = instrumentKey.loopEnd / instrumentKey.sampleRate;
|
|
339
|
+
bufferSource.loopStart = voiceParams.loopStart / voiceParams.sampleRate;
|
|
340
|
+
bufferSource.loopEnd = voiceParams.loopEnd / voiceParams.sampleRate;
|
|
275
341
|
}
|
|
276
342
|
return bufferSource;
|
|
277
343
|
}
|
|
@@ -301,7 +367,7 @@ export class MidyGM1 {
|
|
|
301
367
|
this.handleProgramChange(event.channel, event.programNumber);
|
|
302
368
|
break;
|
|
303
369
|
case "pitchBend":
|
|
304
|
-
this.setPitchBend(event.channel, event.value);
|
|
370
|
+
this.setPitchBend(event.channel, event.value + 8192);
|
|
305
371
|
break;
|
|
306
372
|
case "sysEx":
|
|
307
373
|
this.handleSysEx(event.data);
|
|
@@ -532,41 +598,50 @@ export class MidyGM1 {
|
|
|
532
598
|
}
|
|
533
599
|
calcSemitoneOffset(channel) {
|
|
534
600
|
const tuning = channel.coarseTuning + channel.fineTuning;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return
|
|
539
|
-
Math.pow(2, semitoneOffset / 12);
|
|
601
|
+
const pitchWheel = channel.state.pitchWheel * 2 - 1;
|
|
602
|
+
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 128;
|
|
603
|
+
const pitch = pitchWheel * pitchWheelSensitivity;
|
|
604
|
+
return tuning + pitch;
|
|
540
605
|
}
|
|
541
606
|
setVolumeEnvelope(note) {
|
|
542
|
-
const
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
const
|
|
548
|
-
const
|
|
607
|
+
const now = this.audioContext.currentTime;
|
|
608
|
+
const { voiceParams, startTime } = note;
|
|
609
|
+
const attackVolume = this.cbToRatio(-voiceParams.initialAttenuation);
|
|
610
|
+
const sustainVolume = attackVolume * (1 - voiceParams.volSustain);
|
|
611
|
+
const volDelay = startTime + voiceParams.volDelay;
|
|
612
|
+
const volAttack = volDelay + voiceParams.volAttack;
|
|
613
|
+
const volHold = volAttack + voiceParams.volHold;
|
|
614
|
+
const volDecay = volHold + voiceParams.volDecay;
|
|
549
615
|
note.volumeNode.gain
|
|
550
|
-
.cancelScheduledValues(
|
|
616
|
+
.cancelScheduledValues(now)
|
|
551
617
|
.setValueAtTime(0, startTime)
|
|
552
618
|
.setValueAtTime(1e-6, volDelay) // exponentialRampToValueAtTime() requires a non-zero value
|
|
553
619
|
.exponentialRampToValueAtTime(attackVolume, volAttack)
|
|
554
620
|
.setValueAtTime(attackVolume, volHold)
|
|
555
621
|
.linearRampToValueAtTime(sustainVolume, volDecay);
|
|
556
622
|
}
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
623
|
+
setPlaybackRate(note) {
|
|
624
|
+
const now = this.audioContext.currentTime;
|
|
625
|
+
note.bufferSource.playbackRate
|
|
626
|
+
.cancelScheduledValues(now)
|
|
627
|
+
.setValueAtTime(note.voiceParams.playbackRate, now);
|
|
628
|
+
}
|
|
629
|
+
setPitch(channel, note) {
|
|
630
|
+
const now = this.audioContext.currentTime;
|
|
631
|
+
const { startTime } = note;
|
|
632
|
+
const basePitch = this.calcSemitoneOffset(channel) * 100;
|
|
633
|
+
note.bufferSource.detune
|
|
634
|
+
.cancelScheduledValues(now)
|
|
635
|
+
.setValueAtTime(basePitch, startTime);
|
|
636
|
+
const modEnvToPitch = note.voiceParams.modEnvToPitch;
|
|
561
637
|
if (modEnvToPitch === 0)
|
|
562
638
|
return;
|
|
563
|
-
const
|
|
564
|
-
const
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
note.bufferSource.playbackRate.value
|
|
639
|
+
const peekPitch = basePitch + modEnvToPitch;
|
|
640
|
+
const modDelay = startTime + voiceParams.modDelay;
|
|
641
|
+
const modAttack = modDelay + voiceParams.modAttack;
|
|
642
|
+
const modHold = modAttack + voiceParams.modHold;
|
|
643
|
+
const modDecay = modHold + voiceParams.modDecay;
|
|
644
|
+
note.bufferSource.detune
|
|
570
645
|
.setValueAtTime(basePitch, modDelay)
|
|
571
646
|
.exponentialRampToValueAtTime(peekPitch, modAttack)
|
|
572
647
|
.setValueAtTime(peekPitch, modHold)
|
|
@@ -578,20 +653,21 @@ export class MidyGM1 {
|
|
|
578
653
|
return Math.max(minFrequency, Math.min(frequency, maxFrequency));
|
|
579
654
|
}
|
|
580
655
|
setFilterEnvelope(note) {
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
const
|
|
656
|
+
const now = this.audioContext.currentTime;
|
|
657
|
+
const { voiceParams, startTime } = note;
|
|
658
|
+
const baseFreq = this.centToHz(voiceParams.initialFilterFc);
|
|
659
|
+
const peekFreq = this.centToHz(voiceParams.initialFilterFc + voiceParams.modEnvToFilterFc);
|
|
584
660
|
const sustainFreq = baseFreq +
|
|
585
|
-
(peekFreq - baseFreq) * (1 -
|
|
661
|
+
(peekFreq - baseFreq) * (1 - voiceParams.modSustain);
|
|
586
662
|
const adjustedBaseFreq = this.clampCutoffFrequency(baseFreq);
|
|
587
663
|
const adjustedPeekFreq = this.clampCutoffFrequency(peekFreq);
|
|
588
664
|
const adjustedSustainFreq = this.clampCutoffFrequency(sustainFreq);
|
|
589
|
-
const modDelay = startTime +
|
|
590
|
-
const modAttack = modDelay +
|
|
591
|
-
const modHold = modAttack +
|
|
592
|
-
const modDecay = modHold +
|
|
665
|
+
const modDelay = startTime + voiceParams.modDelay;
|
|
666
|
+
const modAttack = modDelay + voiceParams.modAttack;
|
|
667
|
+
const modHold = modAttack + voiceParams.modHold;
|
|
668
|
+
const modDecay = modHold + voiceParams.modDecay;
|
|
593
669
|
note.filterNode.frequency
|
|
594
|
-
.cancelScheduledValues(
|
|
670
|
+
.cancelScheduledValues(now)
|
|
595
671
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
596
672
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
597
673
|
.exponentialRampToValueAtTime(adjustedPeekFreq, modAttack)
|
|
@@ -599,25 +675,18 @@ export class MidyGM1 {
|
|
|
599
675
|
.linearRampToValueAtTime(adjustedSustainFreq, modDecay);
|
|
600
676
|
}
|
|
601
677
|
startModulation(channel, note, startTime) {
|
|
602
|
-
const {
|
|
603
|
-
const { modLfoToPitch, modLfoToVolume } = instrumentKey;
|
|
678
|
+
const { voiceParams } = note;
|
|
604
679
|
note.modulationLFO = new OscillatorNode(this.audioContext, {
|
|
605
|
-
frequency: this.centToHz(
|
|
680
|
+
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
606
681
|
});
|
|
607
682
|
note.filterDepth = new GainNode(this.audioContext, {
|
|
608
|
-
gain:
|
|
609
|
-
});
|
|
610
|
-
const modulationDepth = Math.abs(modLfoToPitch) + channel.modulationDepth;
|
|
611
|
-
const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
|
|
612
|
-
note.modulationDepth = new GainNode(this.audioContext, {
|
|
613
|
-
gain: modulationDepth * modulationDepthSign,
|
|
614
|
-
});
|
|
615
|
-
const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
616
|
-
const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
|
|
617
|
-
note.volumeDepth = new GainNode(this.audioContext, {
|
|
618
|
-
gain: volumeDepth * volumeDepthSign,
|
|
683
|
+
gain: voiceParams.modLfoToFilterFc,
|
|
619
684
|
});
|
|
620
|
-
note.
|
|
685
|
+
note.modulationDepth = new GainNode(this.audioContext);
|
|
686
|
+
this.setModLfoToPitch(channel, note);
|
|
687
|
+
note.volumeDepth = new GainNode(this.audioContext);
|
|
688
|
+
this.setModLfoToVolume(note);
|
|
689
|
+
note.modulationLFO.start(startTime + voiceParams.delayModLFO);
|
|
621
690
|
note.modulationLFO.connect(note.filterDepth);
|
|
622
691
|
note.filterDepth.connect(note.filterNode.frequency);
|
|
623
692
|
note.modulationLFO.connect(note.modulationDepth);
|
|
@@ -625,24 +694,23 @@ export class MidyGM1 {
|
|
|
625
694
|
note.modulationLFO.connect(note.volumeDepth);
|
|
626
695
|
note.volumeDepth.connect(note.volumeNode.gain);
|
|
627
696
|
}
|
|
628
|
-
async createNote(channel,
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
note
|
|
697
|
+
async createNote(channel, voice, noteNumber, velocity, startTime, isSF3) {
|
|
698
|
+
const state = channel.state;
|
|
699
|
+
const voiceParams = voice.getAllParams(state.array);
|
|
700
|
+
const note = new Note(noteNumber, velocity, startTime, voice, voiceParams);
|
|
701
|
+
note.bufferSource = await this.createNoteBufferNode(voiceParams, isSF3);
|
|
632
702
|
note.volumeNode = new GainNode(this.audioContext);
|
|
633
703
|
note.filterNode = new BiquadFilterNode(this.audioContext, {
|
|
634
704
|
type: "lowpass",
|
|
635
|
-
Q:
|
|
705
|
+
Q: voiceParams.initialFilterQ / 10, // dB
|
|
636
706
|
});
|
|
637
707
|
this.setVolumeEnvelope(note);
|
|
638
708
|
this.setFilterEnvelope(note);
|
|
639
|
-
|
|
640
|
-
|
|
709
|
+
this.setPlaybackRate(note);
|
|
710
|
+
if (0 < state.modulationDepth) {
|
|
711
|
+
this.setPitch(channel, note);
|
|
641
712
|
this.startModulation(channel, note, startTime);
|
|
642
713
|
}
|
|
643
|
-
else {
|
|
644
|
-
note.bufferSource.playbackRate.value = this.calcPlaybackRate(instrumentKey, noteNumber, semitoneOffset);
|
|
645
|
-
}
|
|
646
714
|
note.bufferSource.connect(note.filterNode);
|
|
647
715
|
note.filterNode.connect(note.volumeNode);
|
|
648
716
|
note.bufferSource.start(startTime);
|
|
@@ -650,19 +718,19 @@ export class MidyGM1 {
|
|
|
650
718
|
}
|
|
651
719
|
async scheduleNoteOn(channelNumber, noteNumber, velocity, startTime) {
|
|
652
720
|
const channel = this.channels[channelNumber];
|
|
653
|
-
const bankNumber =
|
|
721
|
+
const bankNumber = channel.bank;
|
|
654
722
|
const soundFontIndex = this.soundFontTable[channel.program].get(bankNumber);
|
|
655
723
|
if (soundFontIndex === undefined)
|
|
656
724
|
return;
|
|
657
725
|
const soundFont = this.soundFonts[soundFontIndex];
|
|
658
726
|
const isSF3 = soundFont.parsed.info.version.major === 3;
|
|
659
|
-
const
|
|
660
|
-
if (!
|
|
727
|
+
const voice = soundFont.getVoice(bankNumber, channel.program, noteNumber, velocity);
|
|
728
|
+
if (!voice)
|
|
661
729
|
return;
|
|
662
|
-
const note = await this.createNote(channel,
|
|
730
|
+
const note = await this.createNote(channel, voice, noteNumber, velocity, startTime, isSF3);
|
|
663
731
|
note.volumeNode.connect(channel.gainL);
|
|
664
732
|
note.volumeNode.connect(channel.gainR);
|
|
665
|
-
const exclusiveClass =
|
|
733
|
+
const exclusiveClass = note.voiceParams.exclusiveClass;
|
|
666
734
|
if (exclusiveClass !== 0) {
|
|
667
735
|
if (this.exclusiveClassMap.has(exclusiveClass)) {
|
|
668
736
|
const prevEntry = this.exclusiveClassMap.get(exclusiveClass);
|
|
@@ -718,7 +786,7 @@ export class MidyGM1 {
|
|
|
718
786
|
}
|
|
719
787
|
scheduleNoteRelease(channelNumber, noteNumber, _velocity, endTime, force) {
|
|
720
788
|
const channel = this.channels[channelNumber];
|
|
721
|
-
if (!force && channel.sustainPedal)
|
|
789
|
+
if (!force && 0.5 < channel.state.sustainPedal)
|
|
722
790
|
return;
|
|
723
791
|
if (!channel.scheduledNotes.has(noteNumber))
|
|
724
792
|
return;
|
|
@@ -729,8 +797,8 @@ export class MidyGM1 {
|
|
|
729
797
|
continue;
|
|
730
798
|
if (note.ending)
|
|
731
799
|
continue;
|
|
732
|
-
const volRelease = endTime + note.
|
|
733
|
-
const modRelease = endTime + note.
|
|
800
|
+
const volRelease = endTime + note.voiceParams.volRelease;
|
|
801
|
+
const modRelease = endTime + note.voiceParams.modRelease;
|
|
734
802
|
note.filterNode.frequency
|
|
735
803
|
.cancelScheduledValues(endTime)
|
|
736
804
|
.linearRampToValueAtTime(0, modRelease);
|
|
@@ -746,7 +814,7 @@ export class MidyGM1 {
|
|
|
746
814
|
const velocity = halfVelocity * 2;
|
|
747
815
|
const channel = this.channels[channelNumber];
|
|
748
816
|
const promises = [];
|
|
749
|
-
channel.sustainPedal =
|
|
817
|
+
channel.state.sustainPedal = halfVelocity;
|
|
750
818
|
channel.scheduledNotes.forEach((noteList) => {
|
|
751
819
|
for (let i = 0; i < noteList.length; i++) {
|
|
752
820
|
const note = noteList[i];
|
|
@@ -782,17 +850,170 @@ export class MidyGM1 {
|
|
|
782
850
|
channel.program = program;
|
|
783
851
|
}
|
|
784
852
|
handlePitchBendMessage(channelNumber, lsb, msb) {
|
|
785
|
-
const pitchBend = msb * 128 + lsb
|
|
853
|
+
const pitchBend = msb * 128 + lsb;
|
|
786
854
|
this.setPitchBend(channelNumber, pitchBend);
|
|
787
855
|
}
|
|
788
|
-
setPitchBend(channelNumber,
|
|
856
|
+
setPitchBend(channelNumber, value) {
|
|
789
857
|
const channel = this.channels[channelNumber];
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
858
|
+
const state = channel.state;
|
|
859
|
+
state.pitchWheel = value / 16383;
|
|
860
|
+
const pitchWheel = (value - 8192) / 8192;
|
|
861
|
+
const detuneChange = pitchWheel * state.pitchWheelSensitivity * 12800;
|
|
794
862
|
this.updateDetune(channel, detuneChange);
|
|
795
863
|
}
|
|
864
|
+
setModLfoToPitch(channel, note) {
|
|
865
|
+
const now = this.audioContext.currentTime;
|
|
866
|
+
const modLfoToPitch = note.voiceParams.modLfoToPitch;
|
|
867
|
+
const modulationDepth = Math.abs(modLfoToPitch) +
|
|
868
|
+
channel.state.modulationDepth;
|
|
869
|
+
const modulationDepthSign = (0 < modLfoToPitch) ? 1 : -1;
|
|
870
|
+
note.modulationDepth.gain
|
|
871
|
+
.cancelScheduledValues(now)
|
|
872
|
+
.setValueAtTime(modulationDepth * modulationDepthSign, now);
|
|
873
|
+
}
|
|
874
|
+
setModLfoToVolume(note) {
|
|
875
|
+
const now = this.audioContext.currentTime;
|
|
876
|
+
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
877
|
+
const volumeDepth = this.cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
878
|
+
const volumeDepthSign = (0 < modLfoToVolume) ? 1 : -1;
|
|
879
|
+
note.volumeDepth.gain
|
|
880
|
+
.cancelScheduledValues(now)
|
|
881
|
+
.setValueAtTime(volumeDepth * volumeDepthSign, now);
|
|
882
|
+
}
|
|
883
|
+
setVibLfoToPitch(channel, note) {
|
|
884
|
+
const now = this.audioContext.currentTime;
|
|
885
|
+
const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
|
|
886
|
+
const vibratoDepth = Math.abs(vibLfoToPitch) * channel.state.vibratoDepth *
|
|
887
|
+
2;
|
|
888
|
+
const vibratoDepthSign = 0 < vibLfoToPitch;
|
|
889
|
+
note.vibratoDepth.gain
|
|
890
|
+
.cancelScheduledValues(now)
|
|
891
|
+
.setValueAtTime(vibratoDepth * vibratoDepthSign, now);
|
|
892
|
+
}
|
|
893
|
+
setModLfoToFilterFc(note) {
|
|
894
|
+
const now = this.audioContext.currentTime;
|
|
895
|
+
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc;
|
|
896
|
+
note.filterDepth.gain
|
|
897
|
+
.cancelScheduledValues(now)
|
|
898
|
+
.setValueAtTime(modLfoToFilterFc, now);
|
|
899
|
+
}
|
|
900
|
+
setDelayModLFO(note) {
|
|
901
|
+
const now = this.audioContext.currentTime;
|
|
902
|
+
const startTime = note.startTime;
|
|
903
|
+
if (startTime < now)
|
|
904
|
+
return;
|
|
905
|
+
note.modulationLFO.stop(now);
|
|
906
|
+
note.modulationLFO.start(startTime + note.voiceParams.delayModLFO);
|
|
907
|
+
note.modulationLFO.connect(note.filterDepth);
|
|
908
|
+
}
|
|
909
|
+
setFreqModLFO(note) {
|
|
910
|
+
const now = this.audioContext.currentTime;
|
|
911
|
+
const freqModLFO = note.voiceParams.freqModLFO;
|
|
912
|
+
note.modulationLFO.frequency
|
|
913
|
+
.cancelScheduledValues(now)
|
|
914
|
+
.setValueAtTime(freqModLFO, now);
|
|
915
|
+
}
|
|
916
|
+
createVoiceParamsHandlers() {
|
|
917
|
+
return {
|
|
918
|
+
modLfoToPitch: (channel, note, _prevValue) => {
|
|
919
|
+
if (0 < channel.state.modulationDepth) {
|
|
920
|
+
this.setModLfoToPitch(channel, note);
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
vibLfoToPitch: (channel, note, _prevValue) => {
|
|
924
|
+
if (0 < channel.state.vibratoDepth) {
|
|
925
|
+
this.setVibLfoToPitch(channel, note);
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
modLfoToFilterFc: (channel, note, _prevValue) => {
|
|
929
|
+
if (0 < channel.state.modulationDepth)
|
|
930
|
+
this.setModLfoToFilterFc(note);
|
|
931
|
+
},
|
|
932
|
+
modLfoToVolume: (channel, note) => {
|
|
933
|
+
if (0 < channel.state.modulationDepth)
|
|
934
|
+
this.setModLfoToVolume(note);
|
|
935
|
+
},
|
|
936
|
+
chorusEffectsSend: (_channel, _note, _prevValue) => { },
|
|
937
|
+
reverbEffectsSend: (_channel, _note, _prevValue) => { },
|
|
938
|
+
delayModLFO: (_channel, note, _prevValue) => this.setDelayModLFO(note),
|
|
939
|
+
freqModLFO: (_channel, note, _prevValue) => this.setFreqModLFO(note),
|
|
940
|
+
delayVibLFO: (channel, note, prevValue) => {
|
|
941
|
+
if (0 < channel.state.vibratoDepth) {
|
|
942
|
+
const now = this.audioContext.currentTime;
|
|
943
|
+
const prevStartTime = note.startTime +
|
|
944
|
+
prevValue * channel.state.vibratoDelay * 2;
|
|
945
|
+
if (now < prevStartTime)
|
|
946
|
+
return;
|
|
947
|
+
const startTime = note.startTime +
|
|
948
|
+
value * channel.state.vibratoDelay * 2;
|
|
949
|
+
note.vibratoLFO.stop(now);
|
|
950
|
+
note.vibratoLFO.start(startTime);
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
freqVibLFO: (channel, note, _prevValue) => {
|
|
954
|
+
if (0 < channel.state.vibratoDepth) {
|
|
955
|
+
const now = this.audioContext.currentTime;
|
|
956
|
+
note.vibratoLFO.frequency
|
|
957
|
+
.cancelScheduledValues(now)
|
|
958
|
+
.setValueAtTime(value * sate.vibratoRate, now);
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
getControllerState(channel, noteNumber, velocity) {
|
|
964
|
+
const state = new Float32Array(channel.state.array.length);
|
|
965
|
+
state.set(channel.state.array);
|
|
966
|
+
state[2] = velocity / 127;
|
|
967
|
+
state[3] = noteNumber / 127;
|
|
968
|
+
return state;
|
|
969
|
+
}
|
|
970
|
+
applyVoiceParams(channel, controllerType) {
|
|
971
|
+
channel.scheduledNotes.forEach((noteList) => {
|
|
972
|
+
for (let i = 0; i < noteList.length; i++) {
|
|
973
|
+
const note = noteList[i];
|
|
974
|
+
if (!note)
|
|
975
|
+
continue;
|
|
976
|
+
const controllerState = this.getControllerState(channel, note.noteNumber, note.velocity);
|
|
977
|
+
const voiceParams = note.voice.getParams(controllerType, controllerState);
|
|
978
|
+
let appliedFilterEnvelope = false;
|
|
979
|
+
let appliedVolumeEnvelope = false;
|
|
980
|
+
for (const [key, value] of Object.entries(voiceParams)) {
|
|
981
|
+
const prevValue = note.voiceParams[key];
|
|
982
|
+
if (value === prevValue)
|
|
983
|
+
continue;
|
|
984
|
+
note.voiceParams[key] = value;
|
|
985
|
+
if (key in this.voiceParamsHandlers) {
|
|
986
|
+
this.voiceParamsHandlers[key](channel, note, prevValue);
|
|
987
|
+
}
|
|
988
|
+
else if (filterEnvelopeKeySet.has(key)) {
|
|
989
|
+
if (appliedFilterEnvelope)
|
|
990
|
+
continue;
|
|
991
|
+
appliedFilterEnvelope = true;
|
|
992
|
+
const noteVoiceParams = note.voiceParams;
|
|
993
|
+
for (let i = 0; i < filterEnvelopeKeys.length; i++) {
|
|
994
|
+
const key = filterEnvelopeKeys[i];
|
|
995
|
+
if (key in voiceParams)
|
|
996
|
+
noteVoiceParams[key] = voiceParams[key];
|
|
997
|
+
}
|
|
998
|
+
this.setFilterEnvelope(channel, note);
|
|
999
|
+
this.setPitch(channel, note);
|
|
1000
|
+
}
|
|
1001
|
+
else if (volumeEnvelopeKeySet.has(key)) {
|
|
1002
|
+
if (appliedVolumeEnvelope)
|
|
1003
|
+
continue;
|
|
1004
|
+
appliedVolumeEnvelope = true;
|
|
1005
|
+
const noteVoiceParams = note.voiceParams;
|
|
1006
|
+
for (let i = 0; i < volumeEnvelopeKeys.length; i++) {
|
|
1007
|
+
const key = volumeEnvelopeKeys[i];
|
|
1008
|
+
if (key in voiceParams)
|
|
1009
|
+
noteVoiceParams[key] = voiceParams[key];
|
|
1010
|
+
}
|
|
1011
|
+
this.setVolumeEnvelope(channel, note);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
796
1017
|
createControlChangeHandlers() {
|
|
797
1018
|
return {
|
|
798
1019
|
1: this.setModulationDepth,
|
|
@@ -809,13 +1030,13 @@ export class MidyGM1 {
|
|
|
809
1030
|
123: this.allNotesOff,
|
|
810
1031
|
};
|
|
811
1032
|
}
|
|
812
|
-
handleControlChange(channelNumber,
|
|
813
|
-
const handler = this.controlChangeHandlers[
|
|
1033
|
+
handleControlChange(channelNumber, controllerType, value) {
|
|
1034
|
+
const handler = this.controlChangeHandlers[controllerType];
|
|
814
1035
|
if (handler) {
|
|
815
1036
|
handler.call(this, channelNumber, value);
|
|
816
1037
|
}
|
|
817
1038
|
else {
|
|
818
|
-
console.warn(`Unsupported Control change:
|
|
1039
|
+
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
819
1040
|
}
|
|
820
1041
|
}
|
|
821
1042
|
updateModulation(channel) {
|
|
@@ -826,11 +1047,10 @@ export class MidyGM1 {
|
|
|
826
1047
|
if (!note)
|
|
827
1048
|
continue;
|
|
828
1049
|
if (note.modulationDepth) {
|
|
829
|
-
note.modulationDepth.gain.setValueAtTime(channel.modulationDepth, now);
|
|
1050
|
+
note.modulationDepth.gain.setValueAtTime(channel.state.modulationDepth, now);
|
|
830
1051
|
}
|
|
831
1052
|
else {
|
|
832
|
-
|
|
833
|
-
this.setPitch(note, semitoneOffset);
|
|
1053
|
+
this.setPitch(channel, note);
|
|
834
1054
|
this.startModulation(channel, note, now);
|
|
835
1055
|
}
|
|
836
1056
|
}
|
|
@@ -838,16 +1058,17 @@ export class MidyGM1 {
|
|
|
838
1058
|
}
|
|
839
1059
|
setModulationDepth(channelNumber, modulation) {
|
|
840
1060
|
const channel = this.channels[channelNumber];
|
|
841
|
-
channel.modulationDepth = (modulation / 127) *
|
|
1061
|
+
channel.state.modulationDepth = (modulation / 127) *
|
|
1062
|
+
channel.modulationDepthRange;
|
|
842
1063
|
this.updateModulation(channel);
|
|
843
1064
|
}
|
|
844
1065
|
setVolume(channelNumber, volume) {
|
|
845
1066
|
const channel = this.channels[channelNumber];
|
|
846
|
-
channel.volume = volume / 127;
|
|
1067
|
+
channel.state.volume = volume / 127;
|
|
847
1068
|
this.updateChannelVolume(channel);
|
|
848
1069
|
}
|
|
849
1070
|
panToGain(pan) {
|
|
850
|
-
const theta = Math.PI / 2 * Math.max(0, pan - 1) / 126;
|
|
1071
|
+
const theta = Math.PI / 2 * Math.max(0, pan * 127 - 1) / 126;
|
|
851
1072
|
return {
|
|
852
1073
|
gainLeft: Math.cos(theta),
|
|
853
1074
|
gainRight: Math.sin(theta),
|
|
@@ -855,12 +1076,12 @@ export class MidyGM1 {
|
|
|
855
1076
|
}
|
|
856
1077
|
setPan(channelNumber, pan) {
|
|
857
1078
|
const channel = this.channels[channelNumber];
|
|
858
|
-
channel.pan = pan;
|
|
1079
|
+
channel.state.pan = pan / 127;
|
|
859
1080
|
this.updateChannelVolume(channel);
|
|
860
1081
|
}
|
|
861
1082
|
setExpression(channelNumber, expression) {
|
|
862
1083
|
const channel = this.channels[channelNumber];
|
|
863
|
-
channel.expression = expression / 127;
|
|
1084
|
+
channel.state.expression = expression / 127;
|
|
864
1085
|
this.updateChannelVolume(channel);
|
|
865
1086
|
}
|
|
866
1087
|
dataEntryLSB(channelNumber, value) {
|
|
@@ -869,8 +1090,9 @@ export class MidyGM1 {
|
|
|
869
1090
|
}
|
|
870
1091
|
updateChannelVolume(channel) {
|
|
871
1092
|
const now = this.audioContext.currentTime;
|
|
872
|
-
const
|
|
873
|
-
const
|
|
1093
|
+
const state = channel.state;
|
|
1094
|
+
const volume = state.volume * state.expression;
|
|
1095
|
+
const { gainLeft, gainRight } = this.panToGain(state.pan);
|
|
874
1096
|
channel.gainL.gain
|
|
875
1097
|
.cancelScheduledValues(now)
|
|
876
1098
|
.setValueAtTime(volume * gainLeft, now);
|
|
@@ -879,9 +1101,8 @@ export class MidyGM1 {
|
|
|
879
1101
|
.setValueAtTime(volume * gainRight, now);
|
|
880
1102
|
}
|
|
881
1103
|
setSustainPedal(channelNumber, value) {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if (!isOn) {
|
|
1104
|
+
this.channels[channelNumber].state.sustainPedal = value / 127;
|
|
1105
|
+
if (value < 64) {
|
|
885
1106
|
this.releaseSustainPedal(channelNumber, value);
|
|
886
1107
|
}
|
|
887
1108
|
}
|
|
@@ -938,7 +1159,7 @@ export class MidyGM1 {
|
|
|
938
1159
|
this.channels[channelNumber].dataMSB = value;
|
|
939
1160
|
this.handleRPN(channelNumber);
|
|
940
1161
|
}
|
|
941
|
-
updateDetune(channel,
|
|
1162
|
+
updateDetune(channel, detune) {
|
|
942
1163
|
const now = this.audioContext.currentTime;
|
|
943
1164
|
channel.scheduledNotes.forEach((noteList) => {
|
|
944
1165
|
for (let i = 0; i < noteList.length; i++) {
|
|
@@ -946,7 +1167,6 @@ export class MidyGM1 {
|
|
|
946
1167
|
if (!note)
|
|
947
1168
|
continue;
|
|
948
1169
|
const { bufferSource } = note;
|
|
949
|
-
const detune = bufferSource.detune.value + detuneChange;
|
|
950
1170
|
bufferSource.detune
|
|
951
1171
|
.cancelScheduledValues(now)
|
|
952
1172
|
.setValueAtTime(detune, now);
|
|
@@ -959,13 +1179,13 @@ export class MidyGM1 {
|
|
|
959
1179
|
const pitchBendRange = channel.dataMSB + channel.dataLSB / 100;
|
|
960
1180
|
this.setPitchBendRange(channelNumber, pitchBendRange);
|
|
961
1181
|
}
|
|
962
|
-
setPitchBendRange(channelNumber,
|
|
1182
|
+
setPitchBendRange(channelNumber, pitchWheelSensitivity) {
|
|
963
1183
|
const channel = this.channels[channelNumber];
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
this.
|
|
1184
|
+
const state = channel.state;
|
|
1185
|
+
state.pitchWheelSensitivity = pitchWheelSensitivity / 128;
|
|
1186
|
+
const detune = (state.pitchWheel * 2 - 1) * pitchWheelSensitivity * 100;
|
|
1187
|
+
this.updateDetune(channel, detune);
|
|
1188
|
+
this.applyVoiceParams(channel, 16);
|
|
969
1189
|
}
|
|
970
1190
|
handleFineTuningRPN(channelNumber) {
|
|
971
1191
|
const channel = this.channels[channelNumber];
|
|
@@ -997,7 +1217,26 @@ export class MidyGM1 {
|
|
|
997
1217
|
return this.stopChannelNotes(channelNumber, 0, true);
|
|
998
1218
|
}
|
|
999
1219
|
resetAllControllers(channelNumber) {
|
|
1000
|
-
|
|
1220
|
+
const stateTypes = [
|
|
1221
|
+
"expression",
|
|
1222
|
+
"modulationDepth",
|
|
1223
|
+
"sustainPedal",
|
|
1224
|
+
"pitchWheelSensitivity",
|
|
1225
|
+
];
|
|
1226
|
+
const channel = this.channels[channelNumber];
|
|
1227
|
+
const state = channel.state;
|
|
1228
|
+
for (let i = 0; i < stateTypes.length; i++) {
|
|
1229
|
+
const type = stateTypes[i];
|
|
1230
|
+
state[type] = defaultControllerState[type];
|
|
1231
|
+
}
|
|
1232
|
+
const settingTypes = [
|
|
1233
|
+
"rpnMSB",
|
|
1234
|
+
"rpnLSB",
|
|
1235
|
+
];
|
|
1236
|
+
for (let i = 0; i < settingTypes.length; i++) {
|
|
1237
|
+
const type = settingTypes[i];
|
|
1238
|
+
channel[type] = this.constructor.channelSettings[type];
|
|
1239
|
+
}
|
|
1001
1240
|
}
|
|
1002
1241
|
allNotesOff(channelNumber) {
|
|
1003
1242
|
return this.stopChannelNotes(channelNumber, 0, false);
|
|
@@ -1022,11 +1261,8 @@ export class MidyGM1 {
|
|
|
1022
1261
|
GM1SystemOn() {
|
|
1023
1262
|
for (let i = 0; i < this.channels.length; i++) {
|
|
1024
1263
|
const channel = this.channels[i];
|
|
1025
|
-
channel.bankMSB = 0;
|
|
1026
|
-
channel.bankLSB = 0;
|
|
1027
1264
|
channel.bank = 0;
|
|
1028
1265
|
}
|
|
1029
|
-
this.channels[9].bankMSB = 1;
|
|
1030
1266
|
this.channels[9].bank = 128;
|
|
1031
1267
|
}
|
|
1032
1268
|
handleUniversalRealTimeExclusiveMessage(data) {
|
|
@@ -1087,28 +1323,15 @@ Object.defineProperty(MidyGM1, "channelSettings", {
|
|
|
1087
1323
|
configurable: true,
|
|
1088
1324
|
writable: true,
|
|
1089
1325
|
value: {
|
|
1090
|
-
|
|
1091
|
-
|
|
1326
|
+
currentBufferSource: null,
|
|
1327
|
+
program: 0,
|
|
1092
1328
|
bank: 0,
|
|
1093
1329
|
dataMSB: 0,
|
|
1094
1330
|
dataLSB: 0,
|
|
1095
|
-
|
|
1096
|
-
|
|
1331
|
+
rpnMSB: 127,
|
|
1332
|
+
rpnLSB: 127,
|
|
1097
1333
|
fineTuning: 0, // cb
|
|
1098
1334
|
coarseTuning: 0, // cb
|
|
1099
1335
|
modulationDepthRange: 50, // cent
|
|
1100
1336
|
}
|
|
1101
1337
|
});
|
|
1102
|
-
Object.defineProperty(MidyGM1, "effectSettings", {
|
|
1103
|
-
enumerable: true,
|
|
1104
|
-
configurable: true,
|
|
1105
|
-
writable: true,
|
|
1106
|
-
value: {
|
|
1107
|
-
expression: 1,
|
|
1108
|
-
modulationDepth: 0,
|
|
1109
|
-
sustainPedal: false,
|
|
1110
|
-
rpnMSB: 127,
|
|
1111
|
-
rpnLSB: 127,
|
|
1112
|
-
pitchBendRange: 2,
|
|
1113
|
-
}
|
|
1114
|
-
});
|