diff --git a/common.js b/common.js index 44ad3cf..59c8f6c 100644 --- a/common.js +++ b/common.js @@ -12,7 +12,7 @@ function Label(text){ } function NumberBox(props){ - return e('input', {type: "number", onChange: props.onChange, value: props.value, className: props.className}, null); + return e('input', {type: "number", onChange: props.onChange, step: props.step, value: props.value, className: props.className}, null); } function TextBox(props){ diff --git a/enums.js b/enums.js index cfc45e8..2277207 100644 --- a/enums.js +++ b/enums.js @@ -18,8 +18,8 @@ function EnumeratorItems(index, enumBreakPoints, setEnumBreakPoints, enumNames, function EnumeratorRow(props){ let content = e('ul', {className: 'lfo-item'}, ListItem(DropDown({onChange: props.setDjParam, value: props.djParam, options: PARAMOPTIONS})), - ListItem(e(NumberBox, {onChange: props.setEnumItemCounts, value:props.enumItems, className: 'enum-count'}, null)), - ListItem(e(NumberBox, {onChange: CreateMatrixParamChanger(props.enumBreakPoints, props.setEnumBreakPoints, props.index, 0), value:props.enumBreakPoints[props.index][0]}, null)), + ListItem(e(NumberBox, {onChange: props.setEnumItemCounts, step:1, value:props.enumItems, className: 'enum-count'}, null)), + ListItem(e(NumberBox, {onChange: CreateMatrixParamChanger(props.enumBreakPoints, props.setEnumBreakPoints, props.index, 0), value:props.enumBreakPoints[props.index][0], step:0.1}, null)), ...(EnumeratorItems(props.index, props.enumBreakPoints, props.setEnumBreakPoints, props.enumNames, props.setEnumNames).slice(0, props.enumItems * 2)), ListItem(e(Button, {text:'+', onClick: props.addEnum}, null)), ListItem(e(Button, {text:'-', onClick: props.removeEnum}, null)) diff --git a/example.maxpat b/example.maxpat index c552bf5..be4a3ae 100644 --- a/example.maxpat +++ b/example.maxpat @@ -3,14 +3,14 @@ "fileversion" : 1, "appversion" : { "major" : 8, - "minor" : 5, - "revision" : 6, + "minor" : 6, + "revision" : 2, "architecture" : "x64", "modernui" : 1 } , "classnamespace" : "box", - "rect" : [ 495.0, 87.0, 815.0, 715.0 ], + "rect" : [ 292.0, 100.0, 799.0, 715.0 ], "bglocked" : 0, "openinpresentation" : 0, "default_fontsize" : 12.0, @@ -39,6 +39,54 @@ "subpatcher_template" : "", "assistshowspatchername" : 0, "boxes" : [ { + "box" : { + "id" : "obj-17", + "linecount" : 2, + "maxclass" : "comment", + "numinlets" : 1, + "numoutlets" : 0, + "patching_rect" : [ 520.0, 441.0, 150.0, 34.0 ], + "text" : "question for Georg: what should `phase` do?" + } + + } +, { + "box" : { + "id" : "obj-13", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 130.0, 183.0, 88.0, 22.0 ], + "text" : "param pass 30" + } + + } +, { + "box" : { + "id" : "obj-8", + "maxclass" : "message", + "numinlets" : 2, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 228.0, 294.0, 129.0, 22.0 ], + "text" : "param attenuation 200" + } + + } +, { + "box" : { + "id" : "obj-5", + "maxclass" : "button", + "numinlets" : 1, + "numoutlets" : 1, + "outlettype" : [ "bang" ], + "parameter_enable" : 0, + "patching_rect" : [ 365.0, 582.0, 24.0, 24.0 ] + } + + } +, { "box" : { "id" : "obj-16", "maxclass" : "message", @@ -64,11 +112,12 @@ "id" : "obj-4", "maxclass" : "newobj", "numinlets" : 2, - "numoutlets" : 4, - "outlettype" : [ "dictionary", "", "", "" ], + "numoutlets" : 5, + "outlettype" : [ "dictionary", "", "", "", "" ], "patching_rect" : [ 693.0, 69.0, 159.0, 22.0 ], "saved_object_attributes" : { "embed" : 1, + "legacy" : 1, "parameter_enable" : 0, "parameter_mappable" : 0 } @@ -95,7 +144,7 @@ "maxclass" : "comment", "numinlets" : 1, "numoutlets" : 0, - "patching_rect" : [ 438.52631402015686, 27.5, 150.0, 33.0 ], + "patching_rect" : [ 438.52631402015686, 27.5, 150.0, 34.0 ], "text" : "required due to the asynchronous operation" } @@ -191,6 +240,13 @@ "source" : [ "obj-12", 0 ] } + } +, { + "patchline" : { + "destination" : [ "obj-2", 0 ], + "source" : [ "obj-13", 0 ] + } + } , { "patchline" : { @@ -216,6 +272,15 @@ , { "patchline" : { "destination" : [ "obj-10", 0 ], + "order" : 1, + "source" : [ "obj-2", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-5", 0 ], + "order" : 0, "source" : [ "obj-2", 0 ] } @@ -242,8 +307,28 @@ "source" : [ "obj-3", 0 ] } + } +, { + "patchline" : { + "destination" : [ "obj-2", 0 ], + "source" : [ "obj-8", 0 ] + } + } ], + "parameters" : { + "parameterbanks" : { + "0" : { + "index" : 0, + "name" : "", + "parameters" : [ "-", "-", "-", "-", "-", "-", "-", "-" ] + } + + } +, + "inherited_shortname" : 1 + } +, "dependency_cache" : [ ], "autosave" : 0 } diff --git a/lfogui.js b/lfogui.js index 263fa21..381bba9 100644 --- a/lfogui.js +++ b/lfogui.js @@ -20,6 +20,10 @@ const ViewModes = Object.freeze({ ENUM: 1 }); + +var modPhases = Array(MAXLFOS).fill(0); +var lastUpdateTime = Date.now(); + const MODULATORLABELS = ["-type-", "---shape---", "----param----", "freq", "amp", "phase"]; const ENUMERATORLABELS = ["--parameter--", "-points-"]; @@ -41,6 +45,8 @@ function MasterLfoHandler(){ const [modVisibleArr, setModVisibleArr] = React.useState(initVisArr); + const [modCenterVals, setModCenterVals] = React.useState({}); + const [shapeArr, setShapeArr] = React.useState(Array(MAXLFOS).fill('Sine')); const [djParamArr, setDjParamArr] = React.useState(Array(MAXLFOS).fill('attenuation')); @@ -114,16 +120,50 @@ function MasterLfoHandler(){ window.max.setDict(event.detail, {"data" : data}); } + function handleParam(event) { + + let name = event.detail[0]; + let val = event.detail[1]; + + // if none of the LFOs use this param, then we output it raw + let i = 0; + while (i < MAXLFOS){ + if (modVisibleArr[i] && djParamArr[i] == name) + break; + i++ + } + if (i == MAXLFOS){ + window.max.outlet(name + ' ' + val); + } + + modCenterVals[name] = val; + setModCenterVals(modCenterVals); + + + + } + + function handleTick(event) { + + let newTime = Date.now() + let delta = (newTime - lastUpdateTime) / 1000; + lastUpdateTime = newTime + operateModulators(modVisibleArr, djParamArr, modCenterVals, freqArr, ampArr, shapeArr, modPhases, delta); + } + window.addEventListener('loadDict', handleLoad); - window.addEventListener('saveDict', handleSave); + window.addEventListener('tick', handleTick); + window.addEventListener('param', handleParam); return () => { window.removeEventListener('loadDict', handleLoad); window.removeEventListener('saveDict', handleSave); + window.removeEventListener('tick', handleTick); + window.removeEventListener('param', handleParam); }; - }, [...allModArrays, ...allEnumArrays, ...allEnumMats]); + }, [...allModArrays, ...allEnumArrays, ...allEnumMats, modCenterVals]); @@ -288,7 +328,15 @@ if (!DEBUG){ window.max.bindInlet("save", (dictId) => { window.dispatchEvent(new CustomEvent('saveDict', {'detail' : dictId})); - }) + }); + + window.max.bindInlet("param", (paramName, val) => { + window.dispatchEvent(new CustomEvent('param', {'detail' : [paramName, val]})); + }); + + setInterval(() => { + window.dispatchEvent(new CustomEvent('tick')); + }, 200); } diff --git a/modulators.js b/modulators.js index 20e7910..d407a81 100644 --- a/modulators.js +++ b/modulators.js @@ -3,7 +3,9 @@ ///////////////////////// var SHAPETYPES = ["Sine", "SawUp", "SawDown", "Tri", "Square"]; -const PARAMOPTIONS = ["attenuation", "melody_scope"]; +const PARAMOPTIONS = ["pulse_length", "eventfulness", "event_length", "metriclarity", + "harmoniclarity", "melodic_cohesion", "melody_scope", "tonic_pitch", "pitch_center", "pitch_range", "dynamics", + "attenuation", "chordal_weight"] function ControlType(){ return e('select', {className: 'control-type'}, Option("LFO")); @@ -16,13 +18,52 @@ function LfoRow(props){ ListItem(ControlType()), ListItem(DropDown({onChange: props.setShape, value:props.shape, options: SHAPETYPES})), ListItem(DropDown({onChange: props.setDjParam, value: props.djParam, options: PARAMOPTIONS})), - ListItem(e(NumberBox, {onChange:props.setFreq, value:props.freq}, null)), - ListItem(e(NumberBox, {onChange:props.setAmp, value:props.amp}, null)), - ListItem(e(NumberBox, {onChange:props.setPhase, value:props.phase}, null)), + ListItem(e(NumberBox, {onChange:props.setFreq, value:props.freq, step: 0.1}, null)), + ListItem(e(NumberBox, {onChange:props.setAmp, value:props.amp, step:0.1}, null)), + ListItem(e(NumberBox, {onChange:props.setPhase, value:props.phase, step:0.1}, null)), ListItem(e(Button, {text:'+', onClick: props.addLfo}, null)), ListItem(e(Button, {text:'-', onClick: props.removeLfo}, null)) ); if (props.visible){ return content }; +} + +function indexWave(type, phase){ + switch (type){ + case "Sine": + return Math.sin(phase * Math.PI * 2); + 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); + } +} + +function operateModulators(visibleArr, paramNames, centers, freqs, amps, waveTypes, phaseArr, delta){ + for (let i=0; i