225 lines
9.4 KiB
JavaScript
225 lines
9.4 KiB
JavaScript
/////////////////////////
|
|
// MODULATORS
|
|
/////////////////////////
|
|
|
|
|
|
var TYPEOPTIONS = ["LFO", "Noise"];
|
|
var SHAPETYPES = ["Sine", "SawUp", "SawDown", "Tri", "Square", "Custom_1", "Custom_2", "Custom_3", "Custom_4"];
|
|
var NOISETYPES = ["Rand", "Line Int.", "Sine Int."]
|
|
|
|
var INSTANCEOPTIONS = ["1", "2", "3", "4", "5", "6"];
|
|
|
|
const MODPARAMOPTIONS = ["NONE", "stream", "pulse_length", "eventfulness", "event_length", "metriclarity",
|
|
"harmoniclarity", "melodic_cohesion", "melody_scope", "tonic_pitch", "pitch_center", "pitch_range", "dynamics",
|
|
"attenuation", "chordal_weight", "tonality-profile", "ostinato-buffer", "ostinato", "meter", "scale"];
|
|
|
|
const PhaseTypes = Object.freeze({
|
|
MUSICAL: Symbol("musical"),
|
|
TIME: Symbol("time")
|
|
});
|
|
|
|
function ControlType(){
|
|
return e('select', {className: 'control-type'}, Option("LFO"));
|
|
}
|
|
|
|
function DataCell(element) {
|
|
return e('td', null, element);
|
|
}
|
|
|
|
function LfoRow(props){
|
|
|
|
let linkedText = props.linked ? "-> enums" : "";
|
|
let center = props.centerVals[props.instanceNum][props.djParam];
|
|
if (!center)
|
|
center = 0;
|
|
|
|
let typeOption = null;
|
|
|
|
if (props.type == "LFO"){
|
|
typeOption = DataCell(DropDown({locked:props.locked, onChange: props.setShape, value:props.shape, options: SHAPETYPES}));
|
|
}
|
|
else if (props.type == "Noise"){
|
|
typeOption = DataCell(DropDown({locked:props.locked, onChange: props.setNoise, value:props.noise, options: NOISETYPES}));
|
|
}
|
|
|
|
let content = e('tr', {className: 'lfo-item'},
|
|
DataCell(DropDown({locked:props.locked, onChange: props.setInstanceNum, value:props.instanceNum, options: INSTANCEOPTIONS})),
|
|
DataCell(DropDown({locked:props.locked, options: TYPEOPTIONS, onChange: props.setType, value:props.type})),
|
|
typeOption,
|
|
DataCell(DropDown({locked:props.locked, onChange: props.setDjParam, value: props.djParam, options: MODPARAMOPTIONS})),
|
|
DataCell(e("input", {onChange:props.setFreq, value:props.freq, className:"timeInput"}, null)),
|
|
DataCell(e(NumberBox, {onChange:props.setMin, value:props.min, step:0.1}, null)),
|
|
DataCell(e(NumberBox, {onChange:props.setMax, value:props.max, step:0.1}, null)),
|
|
//DataCell(e(NumberBox, {onChange:props.setAmp, value:props.amp, step:0.1}, null)),
|
|
DataCell(e(NumberBox, {onChange:props.setPhase, value:props.phase, step:0.1}, null)),
|
|
DataCell(e("div", {className:"base-val"}, center.toString())),
|
|
DataCell(e("input", {type: 'range', min: 0, max: 1, step: 0.01, readonly: true, id: `slider-${props.instanceNum}-${props.djParam}`})),
|
|
DataCell(e(Button, {text:'+', onClick: props.addLfo, locked: props.locked}, null)),
|
|
DataCell(e(Button, {text:'-', onClick: props.removeLfo, locked: props.locked}, null)),
|
|
DataCell(e("div", {className:"linked"}, linkedText)),
|
|
);
|
|
if (props.visible){
|
|
return content;
|
|
};
|
|
}
|
|
|
|
function indexUserWave(phase, index, userDefinedWaves){
|
|
return parseFloat(userDefinedWaves[index][Math.floor(phase * 50)]) / 127;
|
|
}
|
|
|
|
function indexUserFunction(phase, index, userDefinedFunctions){
|
|
return parseFloat(userDefinedFunctions[index][Math.floor(phase * 101)]) / 127;
|
|
}
|
|
|
|
function indexWave(type, phase, userDefinedWaves, userDefinedFunctions, userDefinedTypes){
|
|
switch (type){
|
|
case "Sine":
|
|
return (Math.sin(phase * Math.PI * 2) / 2) + 0.5;
|
|
case "SawUp":
|
|
return phase;
|
|
case "SawDown":
|
|
return 1 - phase;
|
|
case "Tri":
|
|
return phase > 0.5? (1-phase) * 2 : phase * 2;
|
|
case "Square":
|
|
return +(phase > 0.5);
|
|
case "Custom_1":
|
|
return userDefinedTypes[0] == 0 ? indexUserWave(phase, 1, userDefinedWaves) : indexUserFunction(phase, 1, userDefinedFunctions);
|
|
case "Custom_2":
|
|
return userDefinedTypes[1] == 0 ? indexUserWave(phase, 2, userDefinedWaves) : indexUserFunction(phase, 2, userDefinedFunctions);
|
|
case "Custom_3":
|
|
return userDefinedTypes[2] == 0 ? indexUserWave(phase, 3, userDefinedWaves) : indexUserFunction(phase, 3, userDefinedFunctions);
|
|
case "Custom_4":
|
|
return userDefinedTypes[3] == 0 ? indexUserWave(phase, 4, userDefinedWaves) : indexUserFunction(phase, 4, userDefinedFunctions);
|
|
}
|
|
}
|
|
|
|
function operateModulators(visibleArr, typeArr, instanceNumArr, paramNames, centers, freqs, mins, maxs, waveTypes, phaseArr, noiseData, userDefinedWaves, userDefinedFunctions, userDefinedTypes, currTime, beatsInMeasure, ticks){
|
|
for (let i=0; i<paramNames.length; i++){
|
|
if (visibleArr[i]){
|
|
|
|
let name = paramNames[i];
|
|
let inst = instanceNumArr[i];
|
|
let center = 0;
|
|
if (centers[inst].hasOwnProperty(name)){
|
|
center = centers[inst][name];
|
|
}
|
|
|
|
let output = 0;
|
|
|
|
if (typeArr[i] == "LFO")
|
|
output = operateLFO(center, inst, freqs[i], mins[i], maxs[i], waveTypes[i], phaseArr, i, userDefinedWaves, userDefinedFunctions, userDefinedTypes, name, currTime, beatsInMeasure, ticks);
|
|
else
|
|
output = operateNoise(center, inst, freqs[i], mins[i], maxs[i], waveTypes[i], phaseArr, i, name, noiseData, currTime, beatsInMeasure, ticks);
|
|
if (name !== "NONE")
|
|
window.dispatchEvent(new CustomEvent('enum', {'detail' : [inst, name, output]}));
|
|
}
|
|
}
|
|
}
|
|
|
|
function operateLFO(center, inst, timeBaseStr, min, max, waveType, phaseArr, phaseIndex, userDefinedWaves, userDefinedFunctions, userDefinedTypes, name, currTime, beatsInMeasure, maxTicks){
|
|
let amp = parseFloat(max) - parseFloat(min);
|
|
let phaseType;
|
|
let timeBase;
|
|
|
|
[timeBase, phaseType] = parseLfoTime(timeBaseStr, beatsInMeasure);
|
|
let phase;
|
|
|
|
if (phaseType === PhaseTypes.TIME)
|
|
phase = (currTime * timeBase + parseFloat(phaseArr[phaseIndex])) % 1.00;
|
|
else if (phaseType === PhaseTypes.MUSICAL)
|
|
phase = (maxTicks % timeBase) / timeBase;
|
|
let unscaled = indexWave(waveType, phase, userDefinedWaves, userDefinedFunctions, userDefinedTypes);
|
|
syncDisplay(inst, name, unscaled);
|
|
|
|
return unscaled * amp + center + parseFloat(min);
|
|
}
|
|
|
|
function syncDisplay(inst, name, val) {
|
|
let el = document.getElementById(`slider-${inst}-${name}`);
|
|
|
|
if (el)
|
|
el.value = val;
|
|
}
|
|
|
|
// For now, we're only using sine interpolation
|
|
function operateNoise(center, inst, timeBaseStr, min, max, waveType, phaseArr, index, name, noiseData, currTime, beatsInMeasure, maxTicks){
|
|
|
|
let amp = parseFloat(max) - parseFloat(min);
|
|
let phaseType;
|
|
let timeBase;
|
|
let noiseType = noiseData.noiseTypeArr[index];
|
|
|
|
[timeBase, phaseType] = parseLfoTime(timeBaseStr, beatsInMeasure);
|
|
let phase;
|
|
|
|
if (phaseType === PhaseTypes.TIME)
|
|
phase = (currTime * timeBase + parseFloat(phaseArr[index])) % 1.00;
|
|
else if (phaseType === PhaseTypes.MUSICAL)
|
|
phase = (maxTicks % timeBase) / timeBase;
|
|
|
|
if (noiseData.cachedNoiseValueArr1[index] == 0 || noiseData.lastPhaseArr[index] > phase){ // occurs if the phase reset to 0 or at the very start
|
|
|
|
noiseData.cachedNoiseValueArr2[index] = noiseData.cachedNoiseValueArr1[index];
|
|
if (noiseData.cachedNoiseValueArr1[index] == 0)
|
|
noiseData.cachedNoiseValueArr2[index] = center;
|
|
|
|
noiseData.cachedNoiseValueArr1[index] = Math.random();
|
|
noiseData.setCachedNoiseValueArr1(noiseData.cachedNoiseValueArr1);
|
|
noiseData.setCachedNoiseValueArr2(noiseData.cachedNoiseValueArr2);
|
|
}
|
|
noiseData.lastPhaseArr[index] = phase;
|
|
noiseData.setLastPhaseArr(noiseData.lastPhaseArr);
|
|
|
|
//let unscaled = (noiseData.cachedNoiseValueArr[index][1] - noiseData.cachedNoiseValueArr[index][0]) * sinePhase + noiseData.cachedNoiseValueArr[index][0];
|
|
let unscaled = interpolateNoise(noiseType, noiseData.cachedNoiseValueArr1[index], noiseData.cachedNoiseValueArr2[index], phase);
|
|
syncDisplay(inst, name, unscaled);
|
|
|
|
return unscaled * amp + center + parseFloat(min);
|
|
}
|
|
|
|
function interpolateNoise(type, cachedVal1, cachedVal2, phase){
|
|
let interpVal;
|
|
|
|
switch (type){
|
|
case "Sine Int.":
|
|
interpVal = (Math.sin(Math.PI + Math.PI * phase) + 1) / 2;
|
|
break;
|
|
case "Rand":
|
|
interpVal = 0;
|
|
break;
|
|
case "Line Int.":
|
|
interpVal = phase;
|
|
break;
|
|
}
|
|
return (cachedVal2 - cachedVal1) * interpVal + cachedVal1;
|
|
}
|
|
|
|
// actual returns the period for musical timing, to avoid floating point errors
|
|
function parseLfoTime(lfoTime, beatsInMeasure){
|
|
if (lfoTime.slice(-2) == "hz"){
|
|
return [parseFloat(lfoTime.slice(0, -2)), PhaseTypes.TIME];
|
|
}
|
|
else if (lfoTime.slice(-2) == "ms"){
|
|
return [1000 / parseFloat(lfoTime.slice(0, -2)), PhaseTypes.TIME];
|
|
}
|
|
else if (lfoTime.slice(-1) == "s"){
|
|
return [1 / parseFloat(lfoTime.slice(0, -1)), PhaseTypes.TIME];
|
|
}
|
|
else if ((lfoTime.match(/:/g) || []).length == 2){
|
|
return [1 / moment.duration(lfoTime).asSeconds(), PhaseTypes.TIME];
|
|
}
|
|
else if ((lfoTime.match(/\./g) || []).length == 2){
|
|
return [musicalTimingToFreq(...lfoTime.split('.'), beatsInMeasure), PhaseTypes.MUSICAL];
|
|
}
|
|
else {
|
|
return [0, PhaseTypes.TIME];
|
|
}
|
|
}
|
|
|
|
function musicalTimingToFreq(bars, beats, ticks, beatsInMeasure){
|
|
let totalTicks = (parseFloat(bars) * parseFloat(beatsInMeasure) + parseFloat(beats)) * 480 + parseFloat(ticks);
|
|
return totalTicks;
|
|
}
|
|
|