@marmooo/midy 0.4.6 → 0.4.8
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 +3 -0
- package/esm/midy-GM1.d.ts.map +1 -1
- package/esm/midy-GM1.js +115 -53
- package/esm/midy-GM2.d.ts +16 -4
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +255 -159
- package/esm/midy-GMLite.d.ts +2 -0
- package/esm/midy-GMLite.d.ts.map +1 -1
- package/esm/midy-GMLite.js +109 -53
- package/esm/midy.d.ts +34 -11
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +352 -223
- package/package.json +3 -2
- package/script/midy-GM1.d.ts +3 -0
- package/script/midy-GM1.d.ts.map +1 -1
- package/script/midy-GM1.js +115 -53
- package/script/midy-GM2.d.ts +16 -4
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +255 -159
- package/script/midy-GMLite.d.ts +2 -0
- package/script/midy-GMLite.d.ts.map +1 -1
- package/script/midy-GMLite.js +109 -53
- package/script/midy.d.ts +34 -11
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +352 -223
package/script/midy-GM2.js
CHANGED
|
@@ -3,6 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MidyGM2 = void 0;
|
|
4
4
|
const midi_file_1 = require("midi-file");
|
|
5
5
|
const soundfont_parser_1 = require("@marmooo/soundfont-parser");
|
|
6
|
+
const ogg_vorbis_1 = require("@wasm-audio-decoders/ogg-vorbis");
|
|
7
|
+
let decoderPromise = null;
|
|
8
|
+
let decoderQueue = Promise.resolve();
|
|
9
|
+
function initDecoder() {
|
|
10
|
+
if (!decoderPromise) {
|
|
11
|
+
const instance = new ogg_vorbis_1.OggVorbisDecoderWebWorker();
|
|
12
|
+
decoderPromise = instance.ready.then(() => instance);
|
|
13
|
+
}
|
|
14
|
+
return decoderPromise;
|
|
15
|
+
}
|
|
6
16
|
class Note {
|
|
7
17
|
constructor(noteNumber, velocity, startTime) {
|
|
8
18
|
Object.defineProperty(this, "voice", {
|
|
@@ -41,49 +51,49 @@ class Note {
|
|
|
41
51
|
writable: true,
|
|
42
52
|
value: void 0
|
|
43
53
|
});
|
|
44
|
-
Object.defineProperty(this, "
|
|
54
|
+
Object.defineProperty(this, "filterEnvelopeNode", {
|
|
45
55
|
enumerable: true,
|
|
46
56
|
configurable: true,
|
|
47
57
|
writable: true,
|
|
48
58
|
value: void 0
|
|
49
59
|
});
|
|
50
|
-
Object.defineProperty(this, "
|
|
60
|
+
Object.defineProperty(this, "volumeEnvelopeNode", {
|
|
51
61
|
enumerable: true,
|
|
52
62
|
configurable: true,
|
|
53
63
|
writable: true,
|
|
54
64
|
value: void 0
|
|
55
65
|
});
|
|
56
|
-
Object.defineProperty(this, "
|
|
66
|
+
Object.defineProperty(this, "modLfo", {
|
|
57
67
|
enumerable: true,
|
|
58
68
|
configurable: true,
|
|
59
69
|
writable: true,
|
|
60
70
|
value: void 0
|
|
61
|
-
});
|
|
62
|
-
Object.defineProperty(this, "
|
|
71
|
+
}); // CC#1 modulation LFO
|
|
72
|
+
Object.defineProperty(this, "modLfoToPitch", {
|
|
63
73
|
enumerable: true,
|
|
64
74
|
configurable: true,
|
|
65
75
|
writable: true,
|
|
66
76
|
value: void 0
|
|
67
77
|
});
|
|
68
|
-
Object.defineProperty(this, "
|
|
78
|
+
Object.defineProperty(this, "modLfoToFilterFc", {
|
|
69
79
|
enumerable: true,
|
|
70
80
|
configurable: true,
|
|
71
81
|
writable: true,
|
|
72
82
|
value: void 0
|
|
73
83
|
});
|
|
74
|
-
Object.defineProperty(this, "
|
|
84
|
+
Object.defineProperty(this, "modLfoToVolume", {
|
|
75
85
|
enumerable: true,
|
|
76
86
|
configurable: true,
|
|
77
87
|
writable: true,
|
|
78
88
|
value: void 0
|
|
79
89
|
});
|
|
80
|
-
Object.defineProperty(this, "
|
|
90
|
+
Object.defineProperty(this, "vibLfo", {
|
|
81
91
|
enumerable: true,
|
|
82
92
|
configurable: true,
|
|
83
93
|
writable: true,
|
|
84
94
|
value: void 0
|
|
85
|
-
});
|
|
86
|
-
Object.defineProperty(this, "
|
|
95
|
+
}); // vibrato LFO
|
|
96
|
+
Object.defineProperty(this, "vibLfoToPitch", {
|
|
87
97
|
enumerable: true,
|
|
88
98
|
configurable: true,
|
|
89
99
|
writable: true,
|
|
@@ -234,7 +244,20 @@ const pitchEnvelopeKeys = [
|
|
|
234
244
|
"playbackRate",
|
|
235
245
|
];
|
|
236
246
|
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
247
|
+
const effectParameters = [
|
|
248
|
+
2400 / 64, // cent
|
|
249
|
+
9600 / 64, // cent
|
|
250
|
+
1 / 64,
|
|
251
|
+
600 / 127, // cent
|
|
252
|
+
2400 / 127, // cent
|
|
253
|
+
1 / 127,
|
|
254
|
+
];
|
|
255
|
+
const pressureBaselines = new Int8Array([64, 64, 0, 0, 0, 0]);
|
|
237
256
|
const defaultPressureValues = new Int8Array([64, 64, 64, 0, 0, 0]);
|
|
257
|
+
const defaultControlValues = new Int8Array([
|
|
258
|
+
...[-1, -1, -1, -1, -1, -1],
|
|
259
|
+
...defaultPressureValues,
|
|
260
|
+
]);
|
|
238
261
|
function cbToRatio(cb) {
|
|
239
262
|
return Math.pow(10, cb / 200);
|
|
240
263
|
}
|
|
@@ -383,6 +406,12 @@ class MidyGM2 extends EventTarget {
|
|
|
383
406
|
writable: true,
|
|
384
407
|
value: new Map()
|
|
385
408
|
});
|
|
409
|
+
Object.defineProperty(this, "decodeMethod", {
|
|
410
|
+
enumerable: true,
|
|
411
|
+
configurable: true,
|
|
412
|
+
writable: true,
|
|
413
|
+
value: "wasm-audio-decoders"
|
|
414
|
+
});
|
|
386
415
|
Object.defineProperty(this, "isPlaying", {
|
|
387
416
|
enumerable: true,
|
|
388
417
|
configurable: true,
|
|
@@ -480,6 +509,7 @@ class MidyGM2 extends EventTarget {
|
|
|
480
509
|
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
481
510
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
482
511
|
this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
|
|
512
|
+
this.effectHandlers = this.createEffectHandlers();
|
|
483
513
|
this.channels = this.createChannels(audioContext);
|
|
484
514
|
this.reverbEffect = this.createReverbEffect(audioContext);
|
|
485
515
|
this.chorusEffect = this.createChorusEffect(audioContext);
|
|
@@ -607,7 +637,7 @@ class MidyGM2 extends EventTarget {
|
|
|
607
637
|
};
|
|
608
638
|
}
|
|
609
639
|
resetChannelTable(channel) {
|
|
610
|
-
channel.controlTable.
|
|
640
|
+
channel.controlTable.set(defaultControlValues);
|
|
611
641
|
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
612
642
|
channel.channelPressureTable.set(defaultPressureValues);
|
|
613
643
|
channel.keyBasedTable.fill(-1);
|
|
@@ -623,7 +653,7 @@ class MidyGM2 extends EventTarget {
|
|
|
623
653
|
scheduledNotes: [],
|
|
624
654
|
sustainNotes: [],
|
|
625
655
|
sostenutoNotes: [],
|
|
626
|
-
controlTable:
|
|
656
|
+
controlTable: new Int8Array(defaultControlValues),
|
|
627
657
|
scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
|
|
628
658
|
channelPressureTable: new Int8Array(defaultPressureValues),
|
|
629
659
|
keyBasedTable: new Int8Array(128 * 128).fill(-1),
|
|
@@ -633,11 +663,57 @@ class MidyGM2 extends EventTarget {
|
|
|
633
663
|
});
|
|
634
664
|
return channels;
|
|
635
665
|
}
|
|
666
|
+
decodeOggVorbis(sample) {
|
|
667
|
+
const task = decoderQueue.then(async () => {
|
|
668
|
+
const decoder = await initDecoder();
|
|
669
|
+
const slice = sample.data.slice();
|
|
670
|
+
const { channelData, sampleRate, errors } = await decoder.decodeFile(slice);
|
|
671
|
+
if (0 < errors.length) {
|
|
672
|
+
throw new Error(errors.join(", "));
|
|
673
|
+
}
|
|
674
|
+
const audioBuffer = new AudioBuffer({
|
|
675
|
+
numberOfChannels: channelData.length,
|
|
676
|
+
length: channelData[0].length,
|
|
677
|
+
sampleRate,
|
|
678
|
+
});
|
|
679
|
+
for (let ch = 0; ch < channelData.length; ch++) {
|
|
680
|
+
audioBuffer.getChannelData(ch).set(channelData[ch]);
|
|
681
|
+
}
|
|
682
|
+
return audioBuffer;
|
|
683
|
+
});
|
|
684
|
+
decoderQueue = task.catch(() => { });
|
|
685
|
+
return task;
|
|
686
|
+
}
|
|
636
687
|
async createAudioBuffer(voiceParams) {
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
688
|
+
const sample = voiceParams.sample;
|
|
689
|
+
if (sample.type === "compressed") {
|
|
690
|
+
switch (this.decodeMethod) {
|
|
691
|
+
case "decodeAudioData": {
|
|
692
|
+
// https://jakearchibald.com/2016/sounds-fun/
|
|
693
|
+
// https://github.com/WebAudio/web-audio-api/issues/1091
|
|
694
|
+
// decodeAudioData() has priming issues on Safari
|
|
695
|
+
const arrayBuffer = sample.data.slice().buffer;
|
|
696
|
+
return await this.audioContext.decodeAudioData(arrayBuffer);
|
|
697
|
+
}
|
|
698
|
+
case "wasm-audio-decoders":
|
|
699
|
+
return await this.decodeOggVorbis(sample);
|
|
700
|
+
default:
|
|
701
|
+
throw new Error(`Unknown decodeMethod: ${this.decodeMethod}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
const data = sample.data;
|
|
706
|
+
const end = data.length + voiceParams.end;
|
|
707
|
+
const subarray = data.subarray(voiceParams.start, end);
|
|
708
|
+
const pcm = sample.decodePCM(subarray);
|
|
709
|
+
const audioBuffer = new AudioBuffer({
|
|
710
|
+
numberOfChannels: 1,
|
|
711
|
+
length: pcm.length,
|
|
712
|
+
sampleRate: sample.sampleHeader.sampleRate,
|
|
713
|
+
});
|
|
714
|
+
audioBuffer.getChannelData(0).set(pcm);
|
|
715
|
+
return audioBuffer;
|
|
716
|
+
}
|
|
641
717
|
}
|
|
642
718
|
isLoopDrum(channel, noteNumber) {
|
|
643
719
|
const programNumber = channel.programNumber;
|
|
@@ -711,7 +787,7 @@ class MidyGM2 extends EventTarget {
|
|
|
711
787
|
this.voiceCache.clear();
|
|
712
788
|
this.realtimeVoiceCache.clear();
|
|
713
789
|
const channels = this.channels;
|
|
714
|
-
for (let ch = 0;
|
|
790
|
+
for (let ch = 0; ch < channels.length; ch++) {
|
|
715
791
|
channels[ch].scheduledNotes = [];
|
|
716
792
|
this.resetChannelStates(ch);
|
|
717
793
|
}
|
|
@@ -1201,16 +1277,8 @@ class MidyGM2 extends EventTarget {
|
|
|
1201
1277
|
const pitchWheel = channel.state.pitchWheel * 2 - 1;
|
|
1202
1278
|
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
|
|
1203
1279
|
const pitch = pitchWheel * pitchWheelSensitivity;
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1207
|
-
const channelPressure = channelPressureDepth *
|
|
1208
|
-
channel.state.channelPressure;
|
|
1209
|
-
return tuning + pitch + channelPressure;
|
|
1210
|
-
}
|
|
1211
|
-
else {
|
|
1212
|
-
return tuning + pitch;
|
|
1213
|
-
}
|
|
1280
|
+
const effect = this.getChannelPitchControl(channel);
|
|
1281
|
+
return tuning + pitch + effect;
|
|
1214
1282
|
}
|
|
1215
1283
|
updateChannelDetune(channel, scheduleTime) {
|
|
1216
1284
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1228,8 +1296,7 @@ class MidyGM2 extends EventTarget {
|
|
|
1228
1296
|
calcNoteDetune(channel, note) {
|
|
1229
1297
|
const noteDetune = note.voiceParams.detune +
|
|
1230
1298
|
this.calcScaleOctaveTuning(channel, note);
|
|
1231
|
-
|
|
1232
|
-
return channel.detune + noteDetune + pitchControl;
|
|
1299
|
+
return channel.detune + noteDetune;
|
|
1233
1300
|
}
|
|
1234
1301
|
getPortamentoTime(channel, note) {
|
|
1235
1302
|
const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
|
|
@@ -1386,7 +1453,7 @@ class MidyGM2 extends EventTarget {
|
|
|
1386
1453
|
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1387
1454
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1388
1455
|
note.adjustedBaseFreq = adjustedSustainFreq;
|
|
1389
|
-
note.
|
|
1456
|
+
note.filterEnvelopeNode.frequency
|
|
1390
1457
|
.cancelScheduledValues(scheduleTime)
|
|
1391
1458
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1392
1459
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
@@ -1412,7 +1479,7 @@ class MidyGM2 extends EventTarget {
|
|
|
1412
1479
|
const modHold = modAttack + voiceParams.modHold;
|
|
1413
1480
|
const decayDuration = voiceParams.modDecay;
|
|
1414
1481
|
note.adjustedBaseFreq = adjustedBaseFreq;
|
|
1415
|
-
note.
|
|
1482
|
+
note.filterEnvelopeNode.frequency
|
|
1416
1483
|
.cancelScheduledValues(scheduleTime)
|
|
1417
1484
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1418
1485
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
@@ -1423,37 +1490,37 @@ class MidyGM2 extends EventTarget {
|
|
|
1423
1490
|
startModulation(channel, note, scheduleTime) {
|
|
1424
1491
|
const audioContext = this.audioContext;
|
|
1425
1492
|
const { voiceParams } = note;
|
|
1426
|
-
note.
|
|
1493
|
+
note.modLfo = new OscillatorNode(audioContext, {
|
|
1427
1494
|
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
1428
1495
|
});
|
|
1429
|
-
note.
|
|
1496
|
+
note.modLfoToFilterFc = new GainNode(audioContext, {
|
|
1430
1497
|
gain: voiceParams.modLfoToFilterFc,
|
|
1431
1498
|
});
|
|
1432
|
-
note.
|
|
1499
|
+
note.modLfoToPitch = new GainNode(audioContext);
|
|
1433
1500
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
1434
|
-
note.
|
|
1501
|
+
note.modLfoToVolume = new GainNode(audioContext);
|
|
1435
1502
|
this.setModLfoToVolume(note, scheduleTime);
|
|
1436
|
-
note.
|
|
1437
|
-
note.
|
|
1438
|
-
note.
|
|
1439
|
-
note.
|
|
1440
|
-
note.
|
|
1441
|
-
note.
|
|
1442
|
-
note.
|
|
1503
|
+
note.modLfo.start(note.startTime + voiceParams.delayModLFO);
|
|
1504
|
+
note.modLfo.connect(note.modLfoToFilterFc);
|
|
1505
|
+
note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
|
|
1506
|
+
note.modLfo.connect(note.modLfoToPitch);
|
|
1507
|
+
note.modLfoToPitch.connect(note.bufferSource.detune);
|
|
1508
|
+
note.modLfo.connect(note.modLfoToVolume);
|
|
1509
|
+
note.modLfoToVolume.connect(note.volumeEnvelopeNode.gain);
|
|
1443
1510
|
}
|
|
1444
1511
|
startVibrato(channel, note, scheduleTime) {
|
|
1445
1512
|
const { voiceParams } = note;
|
|
1446
1513
|
const state = channel.state;
|
|
1447
1514
|
const vibratoRate = state.vibratoRate * 2;
|
|
1448
1515
|
const vibratoDelay = state.vibratoDelay * 2;
|
|
1449
|
-
note.
|
|
1516
|
+
note.vibLfo = new OscillatorNode(this.audioContext, {
|
|
1450
1517
|
frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
|
|
1451
1518
|
});
|
|
1452
|
-
note.
|
|
1453
|
-
note.
|
|
1519
|
+
note.vibLfo.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
|
|
1520
|
+
note.vibLfoToPitch = new GainNode(this.audioContext);
|
|
1454
1521
|
this.setVibLfoToPitch(channel, note, scheduleTime);
|
|
1455
|
-
note.
|
|
1456
|
-
note.
|
|
1522
|
+
note.vibLfo.connect(note.vibLfoToPitch);
|
|
1523
|
+
note.vibLfoToPitch.connect(note.bufferSource.detune);
|
|
1457
1524
|
}
|
|
1458
1525
|
async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
|
|
1459
1526
|
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
@@ -1494,7 +1561,7 @@ class MidyGM2 extends EventTarget {
|
|
|
1494
1561
|
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
1495
1562
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1496
1563
|
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
1497
|
-
note.
|
|
1564
|
+
note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
|
|
1498
1565
|
type: "lowpass",
|
|
1499
1566
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
1500
1567
|
});
|
|
@@ -1524,8 +1591,8 @@ class MidyGM2 extends EventTarget {
|
|
|
1524
1591
|
channel.currentBufferSource.stop(startTime);
|
|
1525
1592
|
channel.currentBufferSource = note.bufferSource;
|
|
1526
1593
|
}
|
|
1527
|
-
note.bufferSource.connect(note.
|
|
1528
|
-
note.
|
|
1594
|
+
note.bufferSource.connect(note.filterEnvelopeNode);
|
|
1595
|
+
note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
|
|
1529
1596
|
this.setChorusSend(channel, note, now);
|
|
1530
1597
|
this.setReverbSend(channel, note, now);
|
|
1531
1598
|
if (voiceParams.sample.type === "compressed") {
|
|
@@ -1606,8 +1673,6 @@ class MidyGM2 extends EventTarget {
|
|
|
1606
1673
|
scheduledNotes.push(note);
|
|
1607
1674
|
const programNumber = channel.programNumber;
|
|
1608
1675
|
const bankTable = this.soundFontTable[programNumber];
|
|
1609
|
-
if (!bankTable)
|
|
1610
|
-
return;
|
|
1611
1676
|
let bank = channel.isDrum ? 128 : channel.bankLSB;
|
|
1612
1677
|
if (bankTable[bank] === undefined) {
|
|
1613
1678
|
if (channel.isDrum)
|
|
@@ -1627,16 +1692,16 @@ class MidyGM2 extends EventTarget {
|
|
|
1627
1692
|
}
|
|
1628
1693
|
disconnectNote(note) {
|
|
1629
1694
|
note.bufferSource.disconnect();
|
|
1630
|
-
note.
|
|
1695
|
+
note.filterEnvelopeNode.disconnect();
|
|
1631
1696
|
note.volumeEnvelopeNode.disconnect();
|
|
1632
|
-
if (note.
|
|
1633
|
-
note.
|
|
1634
|
-
note.
|
|
1635
|
-
note.
|
|
1697
|
+
if (note.modLfoToPitch) {
|
|
1698
|
+
note.modLfoToVolume.disconnect();
|
|
1699
|
+
note.modLfoToPitch.disconnect();
|
|
1700
|
+
note.modLfo.stop();
|
|
1636
1701
|
}
|
|
1637
|
-
if (note.
|
|
1638
|
-
note.
|
|
1639
|
-
note.
|
|
1702
|
+
if (note.vibLfoToPitch) {
|
|
1703
|
+
note.vibLfoToPitch.disconnect();
|
|
1704
|
+
note.vibLfo.stop();
|
|
1640
1705
|
}
|
|
1641
1706
|
if (note.reverbSend) {
|
|
1642
1707
|
note.reverbSend.disconnect();
|
|
@@ -1649,7 +1714,7 @@ class MidyGM2 extends EventTarget {
|
|
|
1649
1714
|
endTime ??= this.audioContext.currentTime;
|
|
1650
1715
|
const volDuration = note.voiceParams.volRelease;
|
|
1651
1716
|
const volRelease = endTime + volDuration;
|
|
1652
|
-
note.
|
|
1717
|
+
note.filterEnvelopeNode.frequency
|
|
1653
1718
|
.cancelScheduledValues(endTime)
|
|
1654
1719
|
.setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
|
|
1655
1720
|
note.volumeEnvelopeNode.gain
|
|
@@ -1790,17 +1855,12 @@ class MidyGM2 extends EventTarget {
|
|
|
1790
1855
|
const channel = this.channels[channelNumber];
|
|
1791
1856
|
if (channel.isDrum)
|
|
1792
1857
|
return;
|
|
1793
|
-
const prev = channel
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
if (0 <= channelPressureRaw) {
|
|
1798
|
-
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1799
|
-
channel.detune += channelPressureDepth * (next - prev);
|
|
1800
|
-
}
|
|
1801
|
-
const table = channel.channelPressureTable;
|
|
1858
|
+
const prev = this.calcChannelPressureEffectValue(channel, 0);
|
|
1859
|
+
channel.state.channelPressure = value / 127;
|
|
1860
|
+
const next = this.calcChannelPressureEffectValue(channel, 0);
|
|
1861
|
+
channel.detune += next - prev;
|
|
1802
1862
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1803
|
-
this.
|
|
1863
|
+
this.setChannelPressureEffects(channel, note, scheduleTime);
|
|
1804
1864
|
});
|
|
1805
1865
|
this.applyVoiceParams(channel, 13, scheduleTime);
|
|
1806
1866
|
}
|
|
@@ -1823,13 +1883,13 @@ class MidyGM2 extends EventTarget {
|
|
|
1823
1883
|
this.applyVoiceParams(channel, 14, scheduleTime);
|
|
1824
1884
|
}
|
|
1825
1885
|
setModLfoToPitch(channel, note, scheduleTime) {
|
|
1826
|
-
if (note.
|
|
1886
|
+
if (note.modLfoToPitch) {
|
|
1827
1887
|
const modLfoToPitch = note.voiceParams.modLfoToPitch +
|
|
1828
1888
|
this.getLFOPitchDepth(channel, note);
|
|
1829
1889
|
const baseDepth = Math.abs(modLfoToPitch) +
|
|
1830
1890
|
channel.state.modulationDepthMSB;
|
|
1831
1891
|
const depth = baseDepth * Math.sign(modLfoToPitch);
|
|
1832
|
-
note.
|
|
1892
|
+
note.modLfoToPitch.gain
|
|
1833
1893
|
.cancelScheduledValues(scheduleTime)
|
|
1834
1894
|
.setValueAtTime(depth, scheduleTime);
|
|
1835
1895
|
}
|
|
@@ -1838,12 +1898,12 @@ class MidyGM2 extends EventTarget {
|
|
|
1838
1898
|
}
|
|
1839
1899
|
}
|
|
1840
1900
|
setVibLfoToPitch(channel, note, scheduleTime) {
|
|
1841
|
-
if (note.
|
|
1901
|
+
if (note.vibLfoToPitch) {
|
|
1842
1902
|
const vibratoDepth = channel.state.vibratoDepth * 2;
|
|
1843
1903
|
const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
|
|
1844
1904
|
const baseDepth = Math.abs(vibLfoToPitch) * vibratoDepth;
|
|
1845
1905
|
const depth = baseDepth * Math.sign(vibLfoToPitch);
|
|
1846
|
-
note.
|
|
1906
|
+
note.vibLfoToPitch.gain
|
|
1847
1907
|
.cancelScheduledValues(scheduleTime)
|
|
1848
1908
|
.setValueAtTime(depth, scheduleTime);
|
|
1849
1909
|
}
|
|
@@ -1854,18 +1914,18 @@ class MidyGM2 extends EventTarget {
|
|
|
1854
1914
|
setModLfoToFilterFc(channel, note, scheduleTime) {
|
|
1855
1915
|
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
|
|
1856
1916
|
this.getLFOFilterDepth(channel);
|
|
1857
|
-
note.
|
|
1917
|
+
note.modLfoToFilterFc.gain
|
|
1858
1918
|
.cancelScheduledValues(scheduleTime)
|
|
1859
1919
|
.setValueAtTime(modLfoToFilterFc, scheduleTime);
|
|
1860
1920
|
}
|
|
1861
1921
|
setModLfoToVolume(channel, note, scheduleTime) {
|
|
1862
1922
|
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
1863
1923
|
const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
1864
|
-
const
|
|
1924
|
+
const depth = baseDepth * Math.sign(modLfoToVolume) *
|
|
1865
1925
|
(1 + this.getLFOAmplitudeDepth(channel));
|
|
1866
|
-
note.
|
|
1926
|
+
note.modLfoToVolume.gain
|
|
1867
1927
|
.cancelScheduledValues(scheduleTime)
|
|
1868
|
-
.setValueAtTime(
|
|
1928
|
+
.setValueAtTime(depth, scheduleTime);
|
|
1869
1929
|
}
|
|
1870
1930
|
setReverbSend(channel, note, scheduleTime) {
|
|
1871
1931
|
let value = note.voiceParams.reverbEffectsSend *
|
|
@@ -1930,13 +1990,13 @@ class MidyGM2 extends EventTarget {
|
|
|
1930
1990
|
setDelayModLFO(note) {
|
|
1931
1991
|
const startTime = note.startTime + note.voiceParams.delayModLFO;
|
|
1932
1992
|
try {
|
|
1933
|
-
note.
|
|
1993
|
+
note.modLfo.start(startTime);
|
|
1934
1994
|
}
|
|
1935
1995
|
catch { /* empty */ }
|
|
1936
1996
|
}
|
|
1937
1997
|
setFreqModLFO(note, scheduleTime) {
|
|
1938
1998
|
const freqModLFO = note.voiceParams.freqModLFO;
|
|
1939
|
-
note.
|
|
1999
|
+
note.modLfo.frequency
|
|
1940
2000
|
.cancelScheduledValues(scheduleTime)
|
|
1941
2001
|
.setValueAtTime(freqModLFO, scheduleTime);
|
|
1942
2002
|
}
|
|
@@ -1945,14 +2005,14 @@ class MidyGM2 extends EventTarget {
|
|
|
1945
2005
|
const value = note.voiceParams.delayVibLFO;
|
|
1946
2006
|
const startTime = note.startTime + value * vibratoDelay;
|
|
1947
2007
|
try {
|
|
1948
|
-
note.
|
|
2008
|
+
note.vibLfo.start(startTime);
|
|
1949
2009
|
}
|
|
1950
2010
|
catch { /* empty */ }
|
|
1951
2011
|
}
|
|
1952
2012
|
setFreqVibLFO(channel, note, scheduleTime) {
|
|
1953
2013
|
const vibratoRate = channel.state.vibratoRate * 2;
|
|
1954
2014
|
const freqVibLFO = note.voiceParams.freqVibLFO;
|
|
1955
|
-
note.
|
|
2015
|
+
note.vibLfo.frequency
|
|
1956
2016
|
.cancelScheduledValues(scheduleTime)
|
|
1957
2017
|
.setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
|
|
1958
2018
|
}
|
|
@@ -1996,7 +2056,7 @@ class MidyGM2 extends EventTarget {
|
|
|
1996
2056
|
},
|
|
1997
2057
|
delayVibLFO: (channel, note, _scheduleTime) => {
|
|
1998
2058
|
if (0 < channel.state.vibratoDepth) {
|
|
1999
|
-
setDelayVibLFO(channel, note);
|
|
2059
|
+
this.setDelayVibLFO(channel, note);
|
|
2000
2060
|
}
|
|
2001
2061
|
},
|
|
2002
2062
|
freqVibLFO: (channel, note, scheduleTime) => {
|
|
@@ -2085,12 +2145,16 @@ class MidyGM2 extends EventTarget {
|
|
|
2085
2145
|
return handlers;
|
|
2086
2146
|
}
|
|
2087
2147
|
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
2148
|
+
if (!(0 <= scheduleTime))
|
|
2149
|
+
scheduleTime = this.audioContext.currentTime;
|
|
2088
2150
|
const handler = this.controlChangeHandlers[controllerType];
|
|
2089
2151
|
if (handler) {
|
|
2090
2152
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
2091
2153
|
const channel = this.channels[channelNumber];
|
|
2092
2154
|
this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
|
|
2093
|
-
this.
|
|
2155
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2156
|
+
this.setControlChangeEffects(channel, note, scheduleTime);
|
|
2157
|
+
});
|
|
2094
2158
|
}
|
|
2095
2159
|
else {
|
|
2096
2160
|
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
@@ -2103,8 +2167,8 @@ class MidyGM2 extends EventTarget {
|
|
|
2103
2167
|
const depth = channel.state.modulationDepthMSB *
|
|
2104
2168
|
channel.modulationDepthRange;
|
|
2105
2169
|
this.processScheduledNotes(channel, (note) => {
|
|
2106
|
-
if (note.
|
|
2107
|
-
note.
|
|
2170
|
+
if (note.modLfoToPitch) {
|
|
2171
|
+
note.modLfoToPitch.gain.setValueAtTime(depth, scheduleTime);
|
|
2108
2172
|
}
|
|
2109
2173
|
else {
|
|
2110
2174
|
this.startModulation(channel, note, scheduleTime);
|
|
@@ -2152,6 +2216,9 @@ class MidyGM2 extends EventTarget {
|
|
|
2152
2216
|
scheduleTime = this.audioContext.currentTime;
|
|
2153
2217
|
const channel = this.channels[channelNumber];
|
|
2154
2218
|
channel.state.volumeMSB = value / 127;
|
|
2219
|
+
this.applyVolume(channel, scheduleTime);
|
|
2220
|
+
}
|
|
2221
|
+
applyVolume(channel, scheduleTime) {
|
|
2155
2222
|
if (channel.isDrum) {
|
|
2156
2223
|
for (let i = 0; i < 128; i++) {
|
|
2157
2224
|
this.updateKeyBasedVolume(channel, i, scheduleTime);
|
|
@@ -2198,7 +2265,8 @@ class MidyGM2 extends EventTarget {
|
|
|
2198
2265
|
}
|
|
2199
2266
|
updateChannelVolume(channel, scheduleTime) {
|
|
2200
2267
|
const state = channel.state;
|
|
2201
|
-
const
|
|
2268
|
+
const effect = this.getChannelAmplitudeControl(channel);
|
|
2269
|
+
const gain = state.volumeMSB * state.expressionMSB * (1 + effect);
|
|
2202
2270
|
const { gainLeft, gainRight } = this.panToGain(state.panMSB);
|
|
2203
2271
|
channel.gainL.gain
|
|
2204
2272
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -2606,7 +2674,7 @@ class MidyGM2 extends EventTarget {
|
|
|
2606
2674
|
case 9:
|
|
2607
2675
|
switch (data[3]) {
|
|
2608
2676
|
case 1: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
2609
|
-
return this.
|
|
2677
|
+
return this.handleChannelPressureSysEx(data, scheduelTime);
|
|
2610
2678
|
case 3: // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/ca22.pdf
|
|
2611
2679
|
return this.handleControlChangeSysEx(data, scheduleTime);
|
|
2612
2680
|
default:
|
|
@@ -2647,8 +2715,8 @@ class MidyGM2 extends EventTarget {
|
|
|
2647
2715
|
this.masterFineTuning = next;
|
|
2648
2716
|
const detuneChange = next - prev;
|
|
2649
2717
|
const channels = this.channels;
|
|
2650
|
-
for (let ch = 0;
|
|
2651
|
-
const channel =
|
|
2718
|
+
for (let ch = 0; ch < channels.length; ch++) {
|
|
2719
|
+
const channel = channels[ch];
|
|
2652
2720
|
if (channel.isDrum)
|
|
2653
2721
|
continue;
|
|
2654
2722
|
channel.detune += detuneChange;
|
|
@@ -2666,7 +2734,7 @@ class MidyGM2 extends EventTarget {
|
|
|
2666
2734
|
const detuneChange = next - prev;
|
|
2667
2735
|
const channels = this.channels;
|
|
2668
2736
|
for (let ch = 0; ch < channels.length; ch++) {
|
|
2669
|
-
const channel =
|
|
2737
|
+
const channel = channels[ch];
|
|
2670
2738
|
if (channel.isDrum)
|
|
2671
2739
|
continue;
|
|
2672
2740
|
channel.detune += detuneChange;
|
|
@@ -2891,70 +2959,105 @@ class MidyGM2 extends EventTarget {
|
|
|
2891
2959
|
this.updateChannelDetune(channel, scheduleTime);
|
|
2892
2960
|
}
|
|
2893
2961
|
}
|
|
2962
|
+
calcEffectValue(channel, destination) {
|
|
2963
|
+
return this.calcChannelEffectValue(channel, destination);
|
|
2964
|
+
}
|
|
2965
|
+
calcChannelEffectValue(channel, destination) {
|
|
2966
|
+
return this.calcControlChangeEffectValue(channel, destination) +
|
|
2967
|
+
this.calcChannelPressureEffectValue(channel, destination);
|
|
2968
|
+
}
|
|
2969
|
+
calcControlChangeEffectValue(channel, destination) {
|
|
2970
|
+
const controlType = channel.controlTable[destination];
|
|
2971
|
+
if (controlType < 0)
|
|
2972
|
+
return 0;
|
|
2973
|
+
const pressure = channel.state.array[controlType];
|
|
2974
|
+
if (pressure <= 0)
|
|
2975
|
+
return 0;
|
|
2976
|
+
const baseline = pressureBaselines[destination];
|
|
2977
|
+
const tableValue = channel.controlTable[destination + 6];
|
|
2978
|
+
const value = (tableValue - baseline) * pressure;
|
|
2979
|
+
return value * effectParameters[destination];
|
|
2980
|
+
}
|
|
2981
|
+
calcChannelPressureEffectValue(channel, destination) {
|
|
2982
|
+
const pressure = channel.state.channelPressure;
|
|
2983
|
+
if (pressure <= 0)
|
|
2984
|
+
return 0;
|
|
2985
|
+
const baseline = pressureBaselines[destination];
|
|
2986
|
+
const tableValue = channel.channelPressureTable[destination];
|
|
2987
|
+
const value = (tableValue - baseline) * pressure;
|
|
2988
|
+
return value * effectParameters[destination];
|
|
2989
|
+
}
|
|
2990
|
+
getChannelPitchControl(channel) {
|
|
2991
|
+
return this.calcChannelEffectValue(channel, 0);
|
|
2992
|
+
}
|
|
2993
|
+
getPitchControl(channel, note) {
|
|
2994
|
+
return this.calcEffectValue(channel, note, 0);
|
|
2995
|
+
}
|
|
2894
2996
|
getFilterCutoffControl(channel) {
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
return channelPressure * 15;
|
|
2900
|
-
}
|
|
2901
|
-
getAmplitudeControl(channel) {
|
|
2902
|
-
const channelPressureRaw = channel.channelPressureTable[2];
|
|
2903
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
2904
|
-
? channel.state.channelPressure * 127 / channelPressureRaw
|
|
2905
|
-
: 0;
|
|
2906
|
-
return channelPressure;
|
|
2997
|
+
return this.calcEffectValue(channel, 1);
|
|
2998
|
+
}
|
|
2999
|
+
getChannelAmplitudeControl(channel) {
|
|
3000
|
+
return this.calcChannelEffectValue(channel, 2);
|
|
2907
3001
|
}
|
|
2908
3002
|
getLFOPitchDepth(channel) {
|
|
2909
|
-
|
|
2910
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
2911
|
-
? channelPressureRaw * channel.state.channelPressure
|
|
2912
|
-
: 0;
|
|
2913
|
-
return channelPressure / 127 * 600;
|
|
3003
|
+
return this.calcEffectValue(channel, 3);
|
|
2914
3004
|
}
|
|
2915
3005
|
getLFOFilterDepth(channel) {
|
|
2916
|
-
|
|
2917
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
2918
|
-
? channelPressureRaw * channel.state.channelPressure
|
|
2919
|
-
: 0;
|
|
2920
|
-
return channelPressure / 127 * 2400;
|
|
3006
|
+
return this.calcEffectValue(channel, 4);
|
|
2921
3007
|
}
|
|
2922
3008
|
getLFOAmplitudeDepth(channel) {
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
}
|
|
2929
|
-
setEffects(channel, note, table, scheduleTime) {
|
|
2930
|
-
if (0 < table[0]) {
|
|
3009
|
+
return this.calcEffectValue(channel, 5);
|
|
3010
|
+
}
|
|
3011
|
+
createEffectHandlers() {
|
|
3012
|
+
const handlers = new Array(6);
|
|
3013
|
+
handlers[0] = (channel, note, scheduleTime) => {
|
|
2931
3014
|
if (this.isPortamento(channel, note)) {
|
|
2932
3015
|
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
2933
3016
|
}
|
|
2934
3017
|
else {
|
|
2935
3018
|
this.setDetune(channel, note, scheduleTime);
|
|
2936
3019
|
}
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
if (0
|
|
3020
|
+
};
|
|
3021
|
+
handlers[1] = (channel, note, scheduleTime) => {
|
|
3022
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2940
3023
|
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2941
3024
|
}
|
|
2942
|
-
|
|
2943
|
-
this.
|
|
3025
|
+
else {
|
|
3026
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
2944
3027
|
}
|
|
3028
|
+
};
|
|
3029
|
+
handlers[2] = (channel, note, scheduleTime) => this.applyVolume(channel, note, scheduleTime);
|
|
3030
|
+
handlers[3] = (channel, note, scheduleTime) => this.setModLfoToPitch(channel, note, scheduleTime);
|
|
3031
|
+
handlers[4] = (channel, note, scheduleTime) => this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
3032
|
+
handlers[5] = (channel, note, scheduleTime) => this.setModLfoToVolume(channel, note, scheduleTime);
|
|
3033
|
+
return handlers;
|
|
3034
|
+
}
|
|
3035
|
+
setControlChangeEffects(channel, note, scheduleTime) {
|
|
3036
|
+
const handlers = this.effectHandlers;
|
|
3037
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
3038
|
+
const baseline = pressureBaselines[i];
|
|
3039
|
+
const tableValue = channel.controlTable[i + 6];
|
|
3040
|
+
if (baseline === tableValue)
|
|
3041
|
+
continue;
|
|
3042
|
+
handlers[i](channel, note, scheduleTime);
|
|
2945
3043
|
}
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3044
|
+
}
|
|
3045
|
+
setChannelPressureEffects(channel, note, scheduleTime) {
|
|
3046
|
+
this.setPressureEffects(channel, note, "channelPressureTable", scheduleTime);
|
|
3047
|
+
}
|
|
3048
|
+
setPressureEffects(channel, note, tableName, scheduleTime) {
|
|
3049
|
+
const handlers = this.effectHandlers;
|
|
3050
|
+
const table = channel[tableName];
|
|
3051
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
3052
|
+
const baseline = pressureBaselines[i];
|
|
3053
|
+
const tableValue = table[i];
|
|
3054
|
+
if (baseline === tableValue)
|
|
3055
|
+
continue;
|
|
3056
|
+
handlers[i](channel, note, scheduleTime);
|
|
2951
3057
|
}
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
2956
|
-
if (0 < table[5])
|
|
2957
|
-
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
3058
|
+
}
|
|
3059
|
+
handleChannelPressureSysEx(data, scheduleTime) {
|
|
3060
|
+
this.handlePressureSysEx(data, "channelPressureTable", scheduleTime);
|
|
2958
3061
|
}
|
|
2959
3062
|
handlePressureSysEx(data, tableName, scheduleTime) {
|
|
2960
3063
|
const channelNumber = data[4];
|
|
@@ -2966,39 +3069,32 @@ class MidyGM2 extends EventTarget {
|
|
|
2966
3069
|
const pp = data[i];
|
|
2967
3070
|
const rr = data[i + 1];
|
|
2968
3071
|
table[pp] = rr;
|
|
3072
|
+
const handler = this.effectHandlers[pp];
|
|
3073
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3074
|
+
if (handler)
|
|
3075
|
+
handler(channel, note, scheduleTime);
|
|
3076
|
+
});
|
|
2969
3077
|
}
|
|
2970
|
-
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2971
|
-
this.setEffects(channel, note, table, scheduleTime);
|
|
2972
|
-
});
|
|
2973
|
-
}
|
|
2974
|
-
initControlTable() {
|
|
2975
|
-
const ccCount = 128;
|
|
2976
|
-
const slotSize = 6;
|
|
2977
|
-
return new Int8Array(ccCount * slotSize).fill(-1);
|
|
2978
|
-
}
|
|
2979
|
-
setControlChangeEffects(channel, controllerType, scheduleTime) {
|
|
2980
|
-
const slotSize = 6;
|
|
2981
|
-
const offset = controllerType * slotSize;
|
|
2982
|
-
const table = channel.controlTable.subarray(offset, offset + slotSize);
|
|
2983
|
-
this.processScheduledNotes(channel, (note) => {
|
|
2984
|
-
this.setEffects(channel, note, table, scheduleTime);
|
|
2985
|
-
});
|
|
2986
3078
|
}
|
|
2987
3079
|
handleControlChangeSysEx(data, scheduleTime) {
|
|
2988
3080
|
const channelNumber = data[4];
|
|
2989
3081
|
const channel = this.channels[channelNumber];
|
|
2990
3082
|
if (channel.isDrum)
|
|
2991
3083
|
return;
|
|
2992
|
-
const slotSize = 6;
|
|
2993
|
-
const controllerType = data[5];
|
|
2994
|
-
const offset = controllerType * slotSize;
|
|
2995
3084
|
const table = channel.controlTable;
|
|
3085
|
+
table.set(defaultControlValues);
|
|
3086
|
+
const controllerType = data[5];
|
|
2996
3087
|
for (let i = 6; i < data.length; i += 2) {
|
|
2997
3088
|
const pp = data[i];
|
|
2998
3089
|
const rr = data[i + 1];
|
|
2999
|
-
table[
|
|
3090
|
+
table[pp] = controllerType;
|
|
3091
|
+
table[pp + 6] = rr;
|
|
3092
|
+
const handler = this.effectHandlers[pp];
|
|
3093
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3094
|
+
if (handler)
|
|
3095
|
+
handler(channel, note, scheduleTime);
|
|
3096
|
+
});
|
|
3000
3097
|
}
|
|
3001
|
-
this.setControlChangeEffects(channel, controllerType, scheduleTime);
|
|
3002
3098
|
}
|
|
3003
3099
|
getKeyBasedValue(channel, keyNumber, controllerType) {
|
|
3004
3100
|
const index = keyNumber * 128 + controllerType;
|