@marmooo/midy 0.4.6 → 0.4.7
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 +13 -4
- package/esm/midy-GM2.d.ts.map +1 -1
- package/esm/midy-GM2.js +251 -157
- 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 +29 -11
- package/esm/midy.d.ts.map +1 -1
- package/esm/midy.js +328 -221
- 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 +13 -4
- package/script/midy-GM2.d.ts.map +1 -1
- package/script/midy-GM2.js +251 -157
- 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 +29 -11
- package/script/midy.d.ts.map +1 -1
- package/script/midy.js +328 -221
package/esm/midy-GM2.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { parseMidi } from "midi-file";
|
|
2
2
|
import { parse, SoundFont } from "@marmooo/soundfont-parser";
|
|
3
|
+
import { OggVorbisDecoderWebWorker } from "@wasm-audio-decoders/ogg-vorbis";
|
|
4
|
+
let decoderPromise = null;
|
|
5
|
+
let decoderQueue = Promise.resolve();
|
|
6
|
+
function initDecoder() {
|
|
7
|
+
if (!decoderPromise) {
|
|
8
|
+
const instance = new OggVorbisDecoderWebWorker();
|
|
9
|
+
decoderPromise = instance.ready.then(() => instance);
|
|
10
|
+
}
|
|
11
|
+
return decoderPromise;
|
|
12
|
+
}
|
|
3
13
|
class Note {
|
|
4
14
|
constructor(noteNumber, velocity, startTime) {
|
|
5
15
|
Object.defineProperty(this, "voice", {
|
|
@@ -38,49 +48,49 @@ class Note {
|
|
|
38
48
|
writable: true,
|
|
39
49
|
value: void 0
|
|
40
50
|
});
|
|
41
|
-
Object.defineProperty(this, "
|
|
51
|
+
Object.defineProperty(this, "filterEnvelopeNode", {
|
|
42
52
|
enumerable: true,
|
|
43
53
|
configurable: true,
|
|
44
54
|
writable: true,
|
|
45
55
|
value: void 0
|
|
46
56
|
});
|
|
47
|
-
Object.defineProperty(this, "
|
|
57
|
+
Object.defineProperty(this, "volumeEnvelopeNode", {
|
|
48
58
|
enumerable: true,
|
|
49
59
|
configurable: true,
|
|
50
60
|
writable: true,
|
|
51
61
|
value: void 0
|
|
52
62
|
});
|
|
53
|
-
Object.defineProperty(this, "
|
|
63
|
+
Object.defineProperty(this, "modLfo", {
|
|
54
64
|
enumerable: true,
|
|
55
65
|
configurable: true,
|
|
56
66
|
writable: true,
|
|
57
67
|
value: void 0
|
|
58
|
-
});
|
|
59
|
-
Object.defineProperty(this, "
|
|
68
|
+
}); // CC#1 modulation LFO
|
|
69
|
+
Object.defineProperty(this, "modLfoToPitch", {
|
|
60
70
|
enumerable: true,
|
|
61
71
|
configurable: true,
|
|
62
72
|
writable: true,
|
|
63
73
|
value: void 0
|
|
64
74
|
});
|
|
65
|
-
Object.defineProperty(this, "
|
|
75
|
+
Object.defineProperty(this, "modLfoToFilterFc", {
|
|
66
76
|
enumerable: true,
|
|
67
77
|
configurable: true,
|
|
68
78
|
writable: true,
|
|
69
79
|
value: void 0
|
|
70
80
|
});
|
|
71
|
-
Object.defineProperty(this, "
|
|
81
|
+
Object.defineProperty(this, "modLfoToVolume", {
|
|
72
82
|
enumerable: true,
|
|
73
83
|
configurable: true,
|
|
74
84
|
writable: true,
|
|
75
85
|
value: void 0
|
|
76
86
|
});
|
|
77
|
-
Object.defineProperty(this, "
|
|
87
|
+
Object.defineProperty(this, "vibLfo", {
|
|
78
88
|
enumerable: true,
|
|
79
89
|
configurable: true,
|
|
80
90
|
writable: true,
|
|
81
91
|
value: void 0
|
|
82
|
-
});
|
|
83
|
-
Object.defineProperty(this, "
|
|
92
|
+
}); // vibrato LFO
|
|
93
|
+
Object.defineProperty(this, "vibLfoToPitch", {
|
|
84
94
|
enumerable: true,
|
|
85
95
|
configurable: true,
|
|
86
96
|
writable: true,
|
|
@@ -231,7 +241,20 @@ const pitchEnvelopeKeys = [
|
|
|
231
241
|
"playbackRate",
|
|
232
242
|
];
|
|
233
243
|
const pitchEnvelopeKeySet = new Set(pitchEnvelopeKeys);
|
|
244
|
+
const effectParameters = [
|
|
245
|
+
2400 / 64, // cent
|
|
246
|
+
9600 / 64, // cent
|
|
247
|
+
1 / 64,
|
|
248
|
+
600 / 127, // cent
|
|
249
|
+
2400 / 127, // cent
|
|
250
|
+
1 / 127,
|
|
251
|
+
];
|
|
252
|
+
const pressureBaselines = new Int8Array([64, 64, 0, 0, 0, 0]);
|
|
234
253
|
const defaultPressureValues = new Int8Array([64, 64, 64, 0, 0, 0]);
|
|
254
|
+
const defaultControlValues = new Int8Array([
|
|
255
|
+
...[-1, -1, -1, -1, -1, -1],
|
|
256
|
+
...defaultPressureValues,
|
|
257
|
+
]);
|
|
235
258
|
function cbToRatio(cb) {
|
|
236
259
|
return Math.pow(10, cb / 200);
|
|
237
260
|
}
|
|
@@ -380,6 +403,12 @@ export class MidyGM2 extends EventTarget {
|
|
|
380
403
|
writable: true,
|
|
381
404
|
value: new Map()
|
|
382
405
|
});
|
|
406
|
+
Object.defineProperty(this, "decodeMethod", {
|
|
407
|
+
enumerable: true,
|
|
408
|
+
configurable: true,
|
|
409
|
+
writable: true,
|
|
410
|
+
value: "wasm-audio-decoders"
|
|
411
|
+
});
|
|
383
412
|
Object.defineProperty(this, "isPlaying", {
|
|
384
413
|
enumerable: true,
|
|
385
414
|
configurable: true,
|
|
@@ -477,6 +506,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
477
506
|
this.voiceParamsHandlers = this.createVoiceParamsHandlers();
|
|
478
507
|
this.controlChangeHandlers = this.createControlChangeHandlers();
|
|
479
508
|
this.keyBasedControllerHandlers = this.createKeyBasedControllerHandlers();
|
|
509
|
+
this.effectHandlers = this.createEffectHandlers();
|
|
480
510
|
this.channels = this.createChannels(audioContext);
|
|
481
511
|
this.reverbEffect = this.createReverbEffect(audioContext);
|
|
482
512
|
this.chorusEffect = this.createChorusEffect(audioContext);
|
|
@@ -604,7 +634,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
604
634
|
};
|
|
605
635
|
}
|
|
606
636
|
resetChannelTable(channel) {
|
|
607
|
-
channel.controlTable.
|
|
637
|
+
channel.controlTable.set(defaultControlValues);
|
|
608
638
|
channel.scaleOctaveTuningTable.fill(0); // [-100, 100] cent
|
|
609
639
|
channel.channelPressureTable.set(defaultPressureValues);
|
|
610
640
|
channel.keyBasedTable.fill(-1);
|
|
@@ -620,7 +650,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
620
650
|
scheduledNotes: [],
|
|
621
651
|
sustainNotes: [],
|
|
622
652
|
sostenutoNotes: [],
|
|
623
|
-
controlTable:
|
|
653
|
+
controlTable: new Int8Array(defaultControlValues),
|
|
624
654
|
scaleOctaveTuningTable: new Int8Array(12), // [-64, 63] cent
|
|
625
655
|
channelPressureTable: new Int8Array(defaultPressureValues),
|
|
626
656
|
keyBasedTable: new Int8Array(128 * 128).fill(-1),
|
|
@@ -630,11 +660,57 @@ export class MidyGM2 extends EventTarget {
|
|
|
630
660
|
});
|
|
631
661
|
return channels;
|
|
632
662
|
}
|
|
663
|
+
decodeOggVorbis(sample) {
|
|
664
|
+
const task = decoderQueue.then(async () => {
|
|
665
|
+
const decoder = await initDecoder();
|
|
666
|
+
const slice = sample.data.slice();
|
|
667
|
+
const { channelData, sampleRate, errors } = await decoder.decodeFile(slice);
|
|
668
|
+
if (0 < errors.length) {
|
|
669
|
+
throw new Error(errors.join(", "));
|
|
670
|
+
}
|
|
671
|
+
const audioBuffer = new AudioBuffer({
|
|
672
|
+
numberOfChannels: channelData.length,
|
|
673
|
+
length: channelData[0].length,
|
|
674
|
+
sampleRate,
|
|
675
|
+
});
|
|
676
|
+
for (let ch = 0; ch < channelData.length; ch++) {
|
|
677
|
+
audioBuffer.getChannelData(ch).set(channelData[ch]);
|
|
678
|
+
}
|
|
679
|
+
return audioBuffer;
|
|
680
|
+
});
|
|
681
|
+
decoderQueue = task.catch(() => { });
|
|
682
|
+
return task;
|
|
683
|
+
}
|
|
633
684
|
async createAudioBuffer(voiceParams) {
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
685
|
+
const sample = voiceParams.sample;
|
|
686
|
+
if (sample.type === "compressed") {
|
|
687
|
+
switch (this.decodeMethod) {
|
|
688
|
+
case "decodeAudioData": {
|
|
689
|
+
// https://jakearchibald.com/2016/sounds-fun/
|
|
690
|
+
// https://github.com/WebAudio/web-audio-api/issues/1091
|
|
691
|
+
// decodeAudioData() has priming issues on Safari
|
|
692
|
+
const arrayBuffer = sample.data.slice().buffer;
|
|
693
|
+
return await this.audioContext.decodeAudioData(arrayBuffer);
|
|
694
|
+
}
|
|
695
|
+
case "wasm-audio-decoders":
|
|
696
|
+
return await this.decodeOggVorbis(sample);
|
|
697
|
+
default:
|
|
698
|
+
throw new Error(`Unknown decodeMethod: ${this.decodeMethod}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
const data = sample.data;
|
|
703
|
+
const end = data.length + voiceParams.end;
|
|
704
|
+
const subarray = data.subarray(voiceParams.start, end);
|
|
705
|
+
const pcm = sample.decodePCM(subarray);
|
|
706
|
+
const audioBuffer = new AudioBuffer({
|
|
707
|
+
numberOfChannels: 1,
|
|
708
|
+
length: pcm.length,
|
|
709
|
+
sampleRate: sample.sampleHeader.sampleRate,
|
|
710
|
+
});
|
|
711
|
+
audioBuffer.getChannelData(0).set(pcm);
|
|
712
|
+
return audioBuffer;
|
|
713
|
+
}
|
|
638
714
|
}
|
|
639
715
|
isLoopDrum(channel, noteNumber) {
|
|
640
716
|
const programNumber = channel.programNumber;
|
|
@@ -708,7 +784,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
708
784
|
this.voiceCache.clear();
|
|
709
785
|
this.realtimeVoiceCache.clear();
|
|
710
786
|
const channels = this.channels;
|
|
711
|
-
for (let ch = 0;
|
|
787
|
+
for (let ch = 0; ch < channels.length; ch++) {
|
|
712
788
|
channels[ch].scheduledNotes = [];
|
|
713
789
|
this.resetChannelStates(ch);
|
|
714
790
|
}
|
|
@@ -1198,16 +1274,8 @@ export class MidyGM2 extends EventTarget {
|
|
|
1198
1274
|
const pitchWheel = channel.state.pitchWheel * 2 - 1;
|
|
1199
1275
|
const pitchWheelSensitivity = channel.state.pitchWheelSensitivity * 12800;
|
|
1200
1276
|
const pitch = pitchWheel * pitchWheelSensitivity;
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1204
|
-
const channelPressure = channelPressureDepth *
|
|
1205
|
-
channel.state.channelPressure;
|
|
1206
|
-
return tuning + pitch + channelPressure;
|
|
1207
|
-
}
|
|
1208
|
-
else {
|
|
1209
|
-
return tuning + pitch;
|
|
1210
|
-
}
|
|
1277
|
+
const effect = this.getChannelPitchControl(channel);
|
|
1278
|
+
return tuning + pitch + effect;
|
|
1211
1279
|
}
|
|
1212
1280
|
updateChannelDetune(channel, scheduleTime) {
|
|
1213
1281
|
this.processScheduledNotes(channel, (note) => {
|
|
@@ -1225,8 +1293,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
1225
1293
|
calcNoteDetune(channel, note) {
|
|
1226
1294
|
const noteDetune = note.voiceParams.detune +
|
|
1227
1295
|
this.calcScaleOctaveTuning(channel, note);
|
|
1228
|
-
|
|
1229
|
-
return channel.detune + noteDetune + pitchControl;
|
|
1296
|
+
return channel.detune + noteDetune;
|
|
1230
1297
|
}
|
|
1231
1298
|
getPortamentoTime(channel, note) {
|
|
1232
1299
|
const deltaSemitone = Math.abs(note.noteNumber - note.portamentoNoteNumber);
|
|
@@ -1383,7 +1450,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
1383
1450
|
const portamentoTime = startTime + this.getPortamentoTime(channel, note);
|
|
1384
1451
|
const modDelay = startTime + voiceParams.modDelay;
|
|
1385
1452
|
note.adjustedBaseFreq = adjustedSustainFreq;
|
|
1386
|
-
note.
|
|
1453
|
+
note.filterEnvelopeNode.frequency
|
|
1387
1454
|
.cancelScheduledValues(scheduleTime)
|
|
1388
1455
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1389
1456
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
@@ -1409,7 +1476,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
1409
1476
|
const modHold = modAttack + voiceParams.modHold;
|
|
1410
1477
|
const decayDuration = voiceParams.modDecay;
|
|
1411
1478
|
note.adjustedBaseFreq = adjustedBaseFreq;
|
|
1412
|
-
note.
|
|
1479
|
+
note.filterEnvelopeNode.frequency
|
|
1413
1480
|
.cancelScheduledValues(scheduleTime)
|
|
1414
1481
|
.setValueAtTime(adjustedBaseFreq, startTime)
|
|
1415
1482
|
.setValueAtTime(adjustedBaseFreq, modDelay)
|
|
@@ -1420,37 +1487,37 @@ export class MidyGM2 extends EventTarget {
|
|
|
1420
1487
|
startModulation(channel, note, scheduleTime) {
|
|
1421
1488
|
const audioContext = this.audioContext;
|
|
1422
1489
|
const { voiceParams } = note;
|
|
1423
|
-
note.
|
|
1490
|
+
note.modLfo = new OscillatorNode(audioContext, {
|
|
1424
1491
|
frequency: this.centToHz(voiceParams.freqModLFO),
|
|
1425
1492
|
});
|
|
1426
|
-
note.
|
|
1493
|
+
note.modLfoToFilterFc = new GainNode(audioContext, {
|
|
1427
1494
|
gain: voiceParams.modLfoToFilterFc,
|
|
1428
1495
|
});
|
|
1429
|
-
note.
|
|
1496
|
+
note.modLfoToPitch = new GainNode(audioContext);
|
|
1430
1497
|
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
1431
|
-
note.
|
|
1498
|
+
note.modLfoToVolume = new GainNode(audioContext);
|
|
1432
1499
|
this.setModLfoToVolume(note, scheduleTime);
|
|
1433
|
-
note.
|
|
1434
|
-
note.
|
|
1435
|
-
note.
|
|
1436
|
-
note.
|
|
1437
|
-
note.
|
|
1438
|
-
note.
|
|
1439
|
-
note.
|
|
1500
|
+
note.modLfo.start(note.startTime + voiceParams.delayModLFO);
|
|
1501
|
+
note.modLfo.connect(note.modLfoToFilterFc);
|
|
1502
|
+
note.modLfoToFilterFc.connect(note.filterEnvelopeNode.frequency);
|
|
1503
|
+
note.modLfo.connect(note.modLfoToPitch);
|
|
1504
|
+
note.modLfoToPitch.connect(note.bufferSource.detune);
|
|
1505
|
+
note.modLfo.connect(note.modLfoToVolume);
|
|
1506
|
+
note.modLfoToVolume.connect(note.volumeEnvelopeNode.gain);
|
|
1440
1507
|
}
|
|
1441
1508
|
startVibrato(channel, note, scheduleTime) {
|
|
1442
1509
|
const { voiceParams } = note;
|
|
1443
1510
|
const state = channel.state;
|
|
1444
1511
|
const vibratoRate = state.vibratoRate * 2;
|
|
1445
1512
|
const vibratoDelay = state.vibratoDelay * 2;
|
|
1446
|
-
note.
|
|
1513
|
+
note.vibLfo = new OscillatorNode(this.audioContext, {
|
|
1447
1514
|
frequency: this.centToHz(voiceParams.freqVibLFO) * vibratoRate,
|
|
1448
1515
|
});
|
|
1449
|
-
note.
|
|
1450
|
-
note.
|
|
1516
|
+
note.vibLfo.start(note.startTime + voiceParams.delayVibLFO * vibratoDelay);
|
|
1517
|
+
note.vibLfoToPitch = new GainNode(this.audioContext);
|
|
1451
1518
|
this.setVibLfoToPitch(channel, note, scheduleTime);
|
|
1452
|
-
note.
|
|
1453
|
-
note.
|
|
1519
|
+
note.vibLfo.connect(note.vibLfoToPitch);
|
|
1520
|
+
note.vibLfoToPitch.connect(note.bufferSource.detune);
|
|
1454
1521
|
}
|
|
1455
1522
|
async getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime) {
|
|
1456
1523
|
const audioBufferId = this.getVoiceId(channel, noteNumber, velocity);
|
|
@@ -1491,7 +1558,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
1491
1558
|
const audioBuffer = await this.getAudioBuffer(channel, noteNumber, velocity, voiceParams, realtime);
|
|
1492
1559
|
note.bufferSource = this.createBufferSource(channel, noteNumber, voiceParams, audioBuffer);
|
|
1493
1560
|
note.volumeEnvelopeNode = new GainNode(audioContext);
|
|
1494
|
-
note.
|
|
1561
|
+
note.filterEnvelopeNode = new BiquadFilterNode(audioContext, {
|
|
1495
1562
|
type: "lowpass",
|
|
1496
1563
|
Q: voiceParams.initialFilterQ / 10, // dB
|
|
1497
1564
|
});
|
|
@@ -1521,8 +1588,8 @@ export class MidyGM2 extends EventTarget {
|
|
|
1521
1588
|
channel.currentBufferSource.stop(startTime);
|
|
1522
1589
|
channel.currentBufferSource = note.bufferSource;
|
|
1523
1590
|
}
|
|
1524
|
-
note.bufferSource.connect(note.
|
|
1525
|
-
note.
|
|
1591
|
+
note.bufferSource.connect(note.filterEnvelopeNode);
|
|
1592
|
+
note.filterEnvelopeNode.connect(note.volumeEnvelopeNode);
|
|
1526
1593
|
this.setChorusSend(channel, note, now);
|
|
1527
1594
|
this.setReverbSend(channel, note, now);
|
|
1528
1595
|
if (voiceParams.sample.type === "compressed") {
|
|
@@ -1603,8 +1670,6 @@ export class MidyGM2 extends EventTarget {
|
|
|
1603
1670
|
scheduledNotes.push(note);
|
|
1604
1671
|
const programNumber = channel.programNumber;
|
|
1605
1672
|
const bankTable = this.soundFontTable[programNumber];
|
|
1606
|
-
if (!bankTable)
|
|
1607
|
-
return;
|
|
1608
1673
|
let bank = channel.isDrum ? 128 : channel.bankLSB;
|
|
1609
1674
|
if (bankTable[bank] === undefined) {
|
|
1610
1675
|
if (channel.isDrum)
|
|
@@ -1624,16 +1689,16 @@ export class MidyGM2 extends EventTarget {
|
|
|
1624
1689
|
}
|
|
1625
1690
|
disconnectNote(note) {
|
|
1626
1691
|
note.bufferSource.disconnect();
|
|
1627
|
-
note.
|
|
1692
|
+
note.filterEnvelopeNode.disconnect();
|
|
1628
1693
|
note.volumeEnvelopeNode.disconnect();
|
|
1629
|
-
if (note.
|
|
1630
|
-
note.
|
|
1631
|
-
note.
|
|
1632
|
-
note.
|
|
1694
|
+
if (note.modLfoToPitch) {
|
|
1695
|
+
note.modLfoToVolume.disconnect();
|
|
1696
|
+
note.modLfoToPitch.disconnect();
|
|
1697
|
+
note.modLfo.stop();
|
|
1633
1698
|
}
|
|
1634
|
-
if (note.
|
|
1635
|
-
note.
|
|
1636
|
-
note.
|
|
1699
|
+
if (note.vibLfoToPitch) {
|
|
1700
|
+
note.vibLfoToPitch.disconnect();
|
|
1701
|
+
note.vibLfo.stop();
|
|
1637
1702
|
}
|
|
1638
1703
|
if (note.reverbSend) {
|
|
1639
1704
|
note.reverbSend.disconnect();
|
|
@@ -1646,7 +1711,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
1646
1711
|
endTime ??= this.audioContext.currentTime;
|
|
1647
1712
|
const volDuration = note.voiceParams.volRelease;
|
|
1648
1713
|
const volRelease = endTime + volDuration;
|
|
1649
|
-
note.
|
|
1714
|
+
note.filterEnvelopeNode.frequency
|
|
1650
1715
|
.cancelScheduledValues(endTime)
|
|
1651
1716
|
.setTargetAtTime(note.adjustedBaseFreq, endTime, note.voiceParams.modRelease * releaseCurve);
|
|
1652
1717
|
note.volumeEnvelopeNode.gain
|
|
@@ -1787,17 +1852,12 @@ export class MidyGM2 extends EventTarget {
|
|
|
1787
1852
|
const channel = this.channels[channelNumber];
|
|
1788
1853
|
if (channel.isDrum)
|
|
1789
1854
|
return;
|
|
1790
|
-
const prev = channel
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
if (0 <= channelPressureRaw) {
|
|
1795
|
-
const channelPressureDepth = (channelPressureRaw - 64) / 37.5; // 2400 / 64;
|
|
1796
|
-
channel.detune += channelPressureDepth * (next - prev);
|
|
1797
|
-
}
|
|
1798
|
-
const table = channel.channelPressureTable;
|
|
1855
|
+
const prev = this.calcChannelPressureEffectValue(channel, 0);
|
|
1856
|
+
channel.state.channelPressure = value / 127;
|
|
1857
|
+
const next = this.calcChannelPressureEffectValue(channel, 0);
|
|
1858
|
+
channel.detune += next - prev;
|
|
1799
1859
|
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
1800
|
-
this.
|
|
1860
|
+
this.setPressureEffects(channel, note, scheduleTime);
|
|
1801
1861
|
});
|
|
1802
1862
|
this.applyVoiceParams(channel, 13, scheduleTime);
|
|
1803
1863
|
}
|
|
@@ -1820,13 +1880,13 @@ export class MidyGM2 extends EventTarget {
|
|
|
1820
1880
|
this.applyVoiceParams(channel, 14, scheduleTime);
|
|
1821
1881
|
}
|
|
1822
1882
|
setModLfoToPitch(channel, note, scheduleTime) {
|
|
1823
|
-
if (note.
|
|
1883
|
+
if (note.modLfoToPitch) {
|
|
1824
1884
|
const modLfoToPitch = note.voiceParams.modLfoToPitch +
|
|
1825
1885
|
this.getLFOPitchDepth(channel, note);
|
|
1826
1886
|
const baseDepth = Math.abs(modLfoToPitch) +
|
|
1827
1887
|
channel.state.modulationDepthMSB;
|
|
1828
1888
|
const depth = baseDepth * Math.sign(modLfoToPitch);
|
|
1829
|
-
note.
|
|
1889
|
+
note.modLfoToPitch.gain
|
|
1830
1890
|
.cancelScheduledValues(scheduleTime)
|
|
1831
1891
|
.setValueAtTime(depth, scheduleTime);
|
|
1832
1892
|
}
|
|
@@ -1835,12 +1895,12 @@ export class MidyGM2 extends EventTarget {
|
|
|
1835
1895
|
}
|
|
1836
1896
|
}
|
|
1837
1897
|
setVibLfoToPitch(channel, note, scheduleTime) {
|
|
1838
|
-
if (note.
|
|
1898
|
+
if (note.vibLfoToPitch) {
|
|
1839
1899
|
const vibratoDepth = channel.state.vibratoDepth * 2;
|
|
1840
1900
|
const vibLfoToPitch = note.voiceParams.vibLfoToPitch;
|
|
1841
1901
|
const baseDepth = Math.abs(vibLfoToPitch) * vibratoDepth;
|
|
1842
1902
|
const depth = baseDepth * Math.sign(vibLfoToPitch);
|
|
1843
|
-
note.
|
|
1903
|
+
note.vibLfoToPitch.gain
|
|
1844
1904
|
.cancelScheduledValues(scheduleTime)
|
|
1845
1905
|
.setValueAtTime(depth, scheduleTime);
|
|
1846
1906
|
}
|
|
@@ -1851,18 +1911,18 @@ export class MidyGM2 extends EventTarget {
|
|
|
1851
1911
|
setModLfoToFilterFc(channel, note, scheduleTime) {
|
|
1852
1912
|
const modLfoToFilterFc = note.voiceParams.modLfoToFilterFc +
|
|
1853
1913
|
this.getLFOFilterDepth(channel);
|
|
1854
|
-
note.
|
|
1914
|
+
note.modLfoToFilterFc.gain
|
|
1855
1915
|
.cancelScheduledValues(scheduleTime)
|
|
1856
1916
|
.setValueAtTime(modLfoToFilterFc, scheduleTime);
|
|
1857
1917
|
}
|
|
1858
1918
|
setModLfoToVolume(channel, note, scheduleTime) {
|
|
1859
1919
|
const modLfoToVolume = note.voiceParams.modLfoToVolume;
|
|
1860
1920
|
const baseDepth = cbToRatio(Math.abs(modLfoToVolume)) - 1;
|
|
1861
|
-
const
|
|
1921
|
+
const depth = baseDepth * Math.sign(modLfoToVolume) *
|
|
1862
1922
|
(1 + this.getLFOAmplitudeDepth(channel));
|
|
1863
|
-
note.
|
|
1923
|
+
note.modLfoToVolume.gain
|
|
1864
1924
|
.cancelScheduledValues(scheduleTime)
|
|
1865
|
-
.setValueAtTime(
|
|
1925
|
+
.setValueAtTime(depth, scheduleTime);
|
|
1866
1926
|
}
|
|
1867
1927
|
setReverbSend(channel, note, scheduleTime) {
|
|
1868
1928
|
let value = note.voiceParams.reverbEffectsSend *
|
|
@@ -1927,13 +1987,13 @@ export class MidyGM2 extends EventTarget {
|
|
|
1927
1987
|
setDelayModLFO(note) {
|
|
1928
1988
|
const startTime = note.startTime + note.voiceParams.delayModLFO;
|
|
1929
1989
|
try {
|
|
1930
|
-
note.
|
|
1990
|
+
note.modLfo.start(startTime);
|
|
1931
1991
|
}
|
|
1932
1992
|
catch { /* empty */ }
|
|
1933
1993
|
}
|
|
1934
1994
|
setFreqModLFO(note, scheduleTime) {
|
|
1935
1995
|
const freqModLFO = note.voiceParams.freqModLFO;
|
|
1936
|
-
note.
|
|
1996
|
+
note.modLfo.frequency
|
|
1937
1997
|
.cancelScheduledValues(scheduleTime)
|
|
1938
1998
|
.setValueAtTime(freqModLFO, scheduleTime);
|
|
1939
1999
|
}
|
|
@@ -1942,14 +2002,14 @@ export class MidyGM2 extends EventTarget {
|
|
|
1942
2002
|
const value = note.voiceParams.delayVibLFO;
|
|
1943
2003
|
const startTime = note.startTime + value * vibratoDelay;
|
|
1944
2004
|
try {
|
|
1945
|
-
note.
|
|
2005
|
+
note.vibLfo.start(startTime);
|
|
1946
2006
|
}
|
|
1947
2007
|
catch { /* empty */ }
|
|
1948
2008
|
}
|
|
1949
2009
|
setFreqVibLFO(channel, note, scheduleTime) {
|
|
1950
2010
|
const vibratoRate = channel.state.vibratoRate * 2;
|
|
1951
2011
|
const freqVibLFO = note.voiceParams.freqVibLFO;
|
|
1952
|
-
note.
|
|
2012
|
+
note.vibLfo.frequency
|
|
1953
2013
|
.cancelScheduledValues(scheduleTime)
|
|
1954
2014
|
.setValueAtTime(freqVibLFO * vibratoRate, scheduleTime);
|
|
1955
2015
|
}
|
|
@@ -1993,7 +2053,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
1993
2053
|
},
|
|
1994
2054
|
delayVibLFO: (channel, note, _scheduleTime) => {
|
|
1995
2055
|
if (0 < channel.state.vibratoDepth) {
|
|
1996
|
-
setDelayVibLFO(channel, note);
|
|
2056
|
+
this.setDelayVibLFO(channel, note);
|
|
1997
2057
|
}
|
|
1998
2058
|
},
|
|
1999
2059
|
freqVibLFO: (channel, note, scheduleTime) => {
|
|
@@ -2082,12 +2142,16 @@ export class MidyGM2 extends EventTarget {
|
|
|
2082
2142
|
return handlers;
|
|
2083
2143
|
}
|
|
2084
2144
|
setControlChange(channelNumber, controllerType, value, scheduleTime) {
|
|
2145
|
+
if (!(0 <= scheduleTime))
|
|
2146
|
+
scheduleTime = this.audioContext.currentTime;
|
|
2085
2147
|
const handler = this.controlChangeHandlers[controllerType];
|
|
2086
2148
|
if (handler) {
|
|
2087
2149
|
handler.call(this, channelNumber, value, scheduleTime);
|
|
2088
2150
|
const channel = this.channels[channelNumber];
|
|
2089
2151
|
this.applyVoiceParams(channel, controllerType + 128, scheduleTime);
|
|
2090
|
-
this.
|
|
2152
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2153
|
+
this.setControlChangeEffects(channel, note, scheduleTime);
|
|
2154
|
+
});
|
|
2091
2155
|
}
|
|
2092
2156
|
else {
|
|
2093
2157
|
console.warn(`Unsupported Control change: controllerType=${controllerType} value=${value}`);
|
|
@@ -2100,8 +2164,8 @@ export class MidyGM2 extends EventTarget {
|
|
|
2100
2164
|
const depth = channel.state.modulationDepthMSB *
|
|
2101
2165
|
channel.modulationDepthRange;
|
|
2102
2166
|
this.processScheduledNotes(channel, (note) => {
|
|
2103
|
-
if (note.
|
|
2104
|
-
note.
|
|
2167
|
+
if (note.modLfoToPitch) {
|
|
2168
|
+
note.modLfoToPitch.gain.setValueAtTime(depth, scheduleTime);
|
|
2105
2169
|
}
|
|
2106
2170
|
else {
|
|
2107
2171
|
this.startModulation(channel, note, scheduleTime);
|
|
@@ -2195,7 +2259,8 @@ export class MidyGM2 extends EventTarget {
|
|
|
2195
2259
|
}
|
|
2196
2260
|
updateChannelVolume(channel, scheduleTime) {
|
|
2197
2261
|
const state = channel.state;
|
|
2198
|
-
const
|
|
2262
|
+
const effect = this.getChannelAmplitudeControl(channel);
|
|
2263
|
+
const gain = state.volumeMSB * state.expressionMSB * (1 + effect);
|
|
2199
2264
|
const { gainLeft, gainRight } = this.panToGain(state.panMSB);
|
|
2200
2265
|
channel.gainL.gain
|
|
2201
2266
|
.cancelScheduledValues(scheduleTime)
|
|
@@ -2644,8 +2709,8 @@ export class MidyGM2 extends EventTarget {
|
|
|
2644
2709
|
this.masterFineTuning = next;
|
|
2645
2710
|
const detuneChange = next - prev;
|
|
2646
2711
|
const channels = this.channels;
|
|
2647
|
-
for (let ch = 0;
|
|
2648
|
-
const channel =
|
|
2712
|
+
for (let ch = 0; ch < channels.length; ch++) {
|
|
2713
|
+
const channel = channels[ch];
|
|
2649
2714
|
if (channel.isDrum)
|
|
2650
2715
|
continue;
|
|
2651
2716
|
channel.detune += detuneChange;
|
|
@@ -2663,7 +2728,7 @@ export class MidyGM2 extends EventTarget {
|
|
|
2663
2728
|
const detuneChange = next - prev;
|
|
2664
2729
|
const channels = this.channels;
|
|
2665
2730
|
for (let ch = 0; ch < channels.length; ch++) {
|
|
2666
|
-
const channel =
|
|
2731
|
+
const channel = channels[ch];
|
|
2667
2732
|
if (channel.isDrum)
|
|
2668
2733
|
continue;
|
|
2669
2734
|
channel.detune += detuneChange;
|
|
@@ -2888,70 +2953,106 @@ export class MidyGM2 extends EventTarget {
|
|
|
2888
2953
|
this.updateChannelDetune(channel, scheduleTime);
|
|
2889
2954
|
}
|
|
2890
2955
|
}
|
|
2956
|
+
calcEffectValue(channel, destination) {
|
|
2957
|
+
return this.calcChannelEffectValue(channel, destination);
|
|
2958
|
+
}
|
|
2959
|
+
calcChannelEffectValue(channel, destination) {
|
|
2960
|
+
return this.calcControlChangeEffectValue(channel, destination) +
|
|
2961
|
+
this.calcChannelPressureEffectValue(channel, destination);
|
|
2962
|
+
}
|
|
2963
|
+
calcControlChangeEffectValue(channel, destination) {
|
|
2964
|
+
const controlType = channel.controlTable[destination];
|
|
2965
|
+
if (controlType < 0)
|
|
2966
|
+
return 0;
|
|
2967
|
+
const pressure = channel.state.array[controlType];
|
|
2968
|
+
if (pressure <= 0)
|
|
2969
|
+
return 0;
|
|
2970
|
+
const baseline = pressureBaselines[destination];
|
|
2971
|
+
const tableValue = channel.controlTable[destination + 6];
|
|
2972
|
+
const value = (tableValue - baseline) * pressure;
|
|
2973
|
+
return value * effectParameters[destination];
|
|
2974
|
+
}
|
|
2975
|
+
calcChannelPressureEffectValue(channel, destination) {
|
|
2976
|
+
const pressure = channel.state.channelPressure;
|
|
2977
|
+
if (pressure <= 0)
|
|
2978
|
+
return 0;
|
|
2979
|
+
const baseline = pressureBaselines[destination];
|
|
2980
|
+
const tableValue = channel.channelPressureTable[destination];
|
|
2981
|
+
const value = (tableValue - baseline) * pressure;
|
|
2982
|
+
return value * effectParameters[destination];
|
|
2983
|
+
}
|
|
2984
|
+
getChannelPitchControl(channel) {
|
|
2985
|
+
return this.calcChannelEffectValue(channel, 0);
|
|
2986
|
+
}
|
|
2987
|
+
getPitchControl(channel, note) {
|
|
2988
|
+
return this.calcEffectValue(channel, note, 0);
|
|
2989
|
+
}
|
|
2891
2990
|
getFilterCutoffControl(channel) {
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
return channelPressure * 15;
|
|
2897
|
-
}
|
|
2898
|
-
getAmplitudeControl(channel) {
|
|
2899
|
-
const channelPressureRaw = channel.channelPressureTable[2];
|
|
2900
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
2901
|
-
? channel.state.channelPressure * 127 / channelPressureRaw
|
|
2902
|
-
: 0;
|
|
2903
|
-
return channelPressure;
|
|
2991
|
+
return this.calcEffectValue(channel, 1);
|
|
2992
|
+
}
|
|
2993
|
+
getChannelAmplitudeControl(channel) {
|
|
2994
|
+
return this.calcChannelEffectValue(channel, 2);
|
|
2904
2995
|
}
|
|
2905
2996
|
getLFOPitchDepth(channel) {
|
|
2906
|
-
|
|
2907
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
2908
|
-
? channelPressureRaw * channel.state.channelPressure
|
|
2909
|
-
: 0;
|
|
2910
|
-
return channelPressure / 127 * 600;
|
|
2997
|
+
return this.calcEffectValue(channel, 3);
|
|
2911
2998
|
}
|
|
2912
2999
|
getLFOFilterDepth(channel) {
|
|
2913
|
-
|
|
2914
|
-
const channelPressure = (0 <= channelPressureRaw)
|
|
2915
|
-
? channelPressureRaw * channel.state.channelPressure
|
|
2916
|
-
: 0;
|
|
2917
|
-
return channelPressure / 127 * 2400;
|
|
3000
|
+
return this.calcEffectValue(channel, 4);
|
|
2918
3001
|
}
|
|
2919
3002
|
getLFOAmplitudeDepth(channel) {
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
}
|
|
2926
|
-
setEffects(channel, note, table, scheduleTime) {
|
|
2927
|
-
if (0 < table[0]) {
|
|
3003
|
+
return this.calcEffectValue(channel, 5);
|
|
3004
|
+
}
|
|
3005
|
+
createEffectHandlers() {
|
|
3006
|
+
const handlers = new Array(6);
|
|
3007
|
+
handlers[0] = (channel, note, scheduleTime) => {
|
|
2928
3008
|
if (this.isPortamento(channel, note)) {
|
|
2929
3009
|
this.setPortamentoDetune(channel, note, scheduleTime);
|
|
2930
3010
|
}
|
|
2931
3011
|
else {
|
|
2932
3012
|
this.setDetune(channel, note, scheduleTime);
|
|
2933
3013
|
}
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
if (0
|
|
3014
|
+
};
|
|
3015
|
+
handlers[1] = (channel, note, scheduleTime) => {
|
|
3016
|
+
if (0.5 <= channel.state.portamento && 0 <= note.portamentoNoteNumber) {
|
|
2937
3017
|
this.setPortamentoFilterEnvelope(channel, note, scheduleTime);
|
|
2938
3018
|
}
|
|
2939
|
-
|
|
3019
|
+
else {
|
|
3020
|
+
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
3021
|
+
}
|
|
3022
|
+
};
|
|
3023
|
+
handlers[2] = (channel, note, scheduleTime) => {
|
|
3024
|
+
if (0.5 <= channel.state.portamemento && 0 <= note.portamentoNoteNumber) {
|
|
2940
3025
|
this.setPortamentoVolumeEnvelope(channel, note, scheduleTime);
|
|
2941
3026
|
}
|
|
2942
|
-
|
|
2943
|
-
else {
|
|
2944
|
-
if (0 < table[1])
|
|
2945
|
-
this.setFilterEnvelope(channel, note, scheduleTime);
|
|
2946
|
-
if (0 < table[2])
|
|
3027
|
+
else {
|
|
2947
3028
|
this.setVolumeEnvelope(channel, note, scheduleTime);
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
3031
|
+
handlers[3] = (channel, note, scheduleTime) => this.setModLfoToPitch(channel, note, scheduleTime);
|
|
3032
|
+
handlers[4] = (channel, note, scheduleTime) => this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
3033
|
+
handlers[5] = (channel, note, scheduleTime) => this.setModLfoToVolume(channel, note, scheduleTime);
|
|
3034
|
+
return handlers;
|
|
3035
|
+
}
|
|
3036
|
+
setControlChangeEffects(channel, note, scheduleTime) {
|
|
3037
|
+
const handlers = this.effectHandlers;
|
|
3038
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
3039
|
+
const baseline = pressureBaselines[i];
|
|
3040
|
+
const tableValue = channel.controlTable[i + 6];
|
|
3041
|
+
if (baseline === tableValue)
|
|
3042
|
+
continue;
|
|
3043
|
+
handlers[i](channel, note, scheduleTime);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
setPressureEffects(channel, note, tableName, scheduleTime) {
|
|
3047
|
+
const handlers = this.effectHandlers;
|
|
3048
|
+
const table = channel[tableName];
|
|
3049
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
3050
|
+
const baseline = pressureBaselines[i];
|
|
3051
|
+
const tableValue = table[i];
|
|
3052
|
+
if (baseline === tableValue)
|
|
3053
|
+
continue;
|
|
3054
|
+
handlers[i](channel, note, scheduleTime);
|
|
2948
3055
|
}
|
|
2949
|
-
if (0 < table[3])
|
|
2950
|
-
this.setModLfoToPitch(channel, note, scheduleTime);
|
|
2951
|
-
if (0 < table[4])
|
|
2952
|
-
this.setModLfoToFilterFc(channel, note, scheduleTime);
|
|
2953
|
-
if (0 < table[5])
|
|
2954
|
-
this.setModLfoToVolume(channel, note, scheduleTime);
|
|
2955
3056
|
}
|
|
2956
3057
|
handlePressureSysEx(data, tableName, scheduleTime) {
|
|
2957
3058
|
const channelNumber = data[4];
|
|
@@ -2963,39 +3064,32 @@ export class MidyGM2 extends EventTarget {
|
|
|
2963
3064
|
const pp = data[i];
|
|
2964
3065
|
const rr = data[i + 1];
|
|
2965
3066
|
table[pp] = rr;
|
|
3067
|
+
const handler = this.effectHandlers[pp];
|
|
3068
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3069
|
+
if (handler)
|
|
3070
|
+
handler(channel, note, scheduleTime);
|
|
3071
|
+
});
|
|
2966
3072
|
}
|
|
2967
|
-
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
2968
|
-
this.setEffects(channel, note, table, scheduleTime);
|
|
2969
|
-
});
|
|
2970
|
-
}
|
|
2971
|
-
initControlTable() {
|
|
2972
|
-
const ccCount = 128;
|
|
2973
|
-
const slotSize = 6;
|
|
2974
|
-
return new Int8Array(ccCount * slotSize).fill(-1);
|
|
2975
|
-
}
|
|
2976
|
-
setControlChangeEffects(channel, controllerType, scheduleTime) {
|
|
2977
|
-
const slotSize = 6;
|
|
2978
|
-
const offset = controllerType * slotSize;
|
|
2979
|
-
const table = channel.controlTable.subarray(offset, offset + slotSize);
|
|
2980
|
-
this.processScheduledNotes(channel, (note) => {
|
|
2981
|
-
this.setEffects(channel, note, table, scheduleTime);
|
|
2982
|
-
});
|
|
2983
3073
|
}
|
|
2984
3074
|
handleControlChangeSysEx(data, scheduleTime) {
|
|
2985
3075
|
const channelNumber = data[4];
|
|
2986
3076
|
const channel = this.channels[channelNumber];
|
|
2987
3077
|
if (channel.isDrum)
|
|
2988
3078
|
return;
|
|
2989
|
-
const slotSize = 6;
|
|
2990
|
-
const controllerType = data[5];
|
|
2991
|
-
const offset = controllerType * slotSize;
|
|
2992
3079
|
const table = channel.controlTable;
|
|
3080
|
+
table.set(defaultControlValues);
|
|
3081
|
+
const controllerType = data[5];
|
|
2993
3082
|
for (let i = 6; i < data.length; i += 2) {
|
|
2994
3083
|
const pp = data[i];
|
|
2995
3084
|
const rr = data[i + 1];
|
|
2996
|
-
table[
|
|
3085
|
+
table[pp] = controllerType;
|
|
3086
|
+
table[pp + 6] = rr;
|
|
3087
|
+
const handler = this.effectHandlers[pp];
|
|
3088
|
+
this.processActiveNotes(channel, scheduleTime, (note) => {
|
|
3089
|
+
if (handler)
|
|
3090
|
+
handler(channel, note, scheduleTime);
|
|
3091
|
+
});
|
|
2997
3092
|
}
|
|
2998
|
-
this.setControlChangeEffects(channel, controllerType, scheduleTime);
|
|
2999
3093
|
}
|
|
3000
3094
|
getKeyBasedValue(channel, keyNumber, controllerType) {
|
|
3001
3095
|
const index = keyNumber * 128 + controllerType;
|