83 Commits

Author SHA1 Message Date
53a88ca374 restructuring repo 2025-08-11 14:51:25 +02:00
7f464366af Merge pull request 'how to integrate tab' (#22) from help-patch-integration into main
Reviewed-on: #22
2025-08-04 16:28:21 +02:00
51420cbd5f how to integrate tab 2025-08-04 16:27:13 +02:00
533de4668a Merge pull request 'initial help patch' (#19) from help-patch into main
Reviewed-on: #19
2025-07-29 14:10:37 +02:00
b850440287 improved text placement 2025-07-29 14:10:14 +02:00
7ac026f35f initial help patch 2025-07-22 15:27:16 +02:00
7cb4180d97 Merge pull request 'setting viewMode directly from Max' (#16) from viewMode-max into main
Reviewed-on: #16
2025-07-21 16:15:52 +02:00
51767ff058 setting viewMode directly from Max 2025-07-21 16:14:12 +02:00
Eveline-97
fe68764e7d fix aligning 2025-07-02 14:29:59 +02:00
Eveline-97
40d0927a0e streamline layout patch 2025-07-02 12:16:30 +02:00
Eveline-97
92b7545e90 match Max layout to jweb 2025-07-02 12:07:53 +02:00
Eveline-97
4912b398dd Merge remote-tracking branch 'refs/remotes/origin/main'
merge
2025-07-01 16:34:50 +02:00
Eveline-97
02d5c6e914 cleanup modulator table 2025-07-01 16:34:27 +02:00
13f3a18c4c Merge pull request 'bpatcher-embed' (#15) from bpatcher-embed into main
Reviewed-on: #15
2025-07-01 16:32:05 +02:00
f21220e7b2 Merge branch 'main' into bpatcher-embed 2025-07-01 16:31:40 +02:00
fe165b77e5 working saving for bpatcher 2025-07-01 16:30:08 +02:00
Eveline-97
a7b0de9e66 navigation bar 2025-07-01 16:15:41 +02:00
43d37f215f Merge branch 'main' into bpatcher-embed 2025-07-01 16:12:51 +02:00
Eveline-97
c1a0a4d919 basic design 2025-07-01 15:04:49 +02:00
e2f33b8ba7 Merge pull request 'properly saving on adding rows, changing enum length, changing enum params' (#14) from hotfix into main
Reviewed-on: #14
2025-07-01 09:54:22 +02:00
a19717a759 properly saving on adding rows, changing enum length, changing enum params 2025-07-01 09:53:39 +02:00
64da80b742 WIP 2025-07-01 09:25:27 +02:00
Eveline-97
34549daf67 also enumerator items as datacells 2025-07-01 09:20:30 +02:00
Eveline-97
dd47c0b030 enumerators as table, jweb bit wider 2025-07-01 09:18:02 +02:00
0b7dbbb775 Merge branch 'main' into bpatcher-embed 2025-06-30 12:00:29 +02:00
290ff73524 Merge pull request 'more instances of DJster' (#13) from more-dj-instances into main
Reviewed-on: #13
2025-06-30 11:59:31 +02:00
9b1a9f2e03 more instances 2025-06-30 11:58:56 +02:00
541ab6d459 created initial bpatcher file and maxhelp 2025-06-30 11:44:03 +02:00
b96f2aa53b Merge pull request 'button for showing and hiding everything' (#12) from showhide-button into main
Reviewed-on: #12
2025-06-30 11:00:09 +02:00
edcddde844 button for showing and hiding everything 2025-06-30 10:58:51 +02:00
e41567c101 Merge pull request 'buttons for switching between modes' (#11) from button-headers into main
Reviewed-on: #11
2025-06-30 10:22:01 +02:00
8002ae8bdb Merge branch 'main' into button-headers 2025-06-30 10:21:44 +02:00
286a43b0b2 buttons for switching between modes 2025-06-30 10:19:42 +02:00
229ddb9c37 Merge pull request 'increased max number of lfos and enums to 200' (#10) from increase-maxlfos into main
Reviewed-on: #10
2025-06-30 09:46:03 +02:00
bbcfd8faab increased max number of lfos and enums to 200 2025-06-30 09:40:14 +02:00
5d092a607c Merge pull request 'removed lock' (#9) from removing-lock into main
Reviewed-on: #9
2025-06-30 09:32:36 +02:00
de28fd87fa removed lock 2025-06-30 09:31:23 +02:00
Eveline-97
07c4826bd9 table layout for modulators 2025-06-29 17:53:12 +02:00
Eveline-97
501fca079e autopattr save and load waves, functions, types 2025-05-15 10:32:39 +02:00
Eveline-97
3507899eb3 debug function waves works now 2025-05-15 10:18:10 +02:00
Eveline-97
dc2daade1b add function functionality for custom shape 2025-05-14 14:42:34 +02:00
149d1a0b8d save on every UI change 2025-05-13 11:24:39 +02:00
4ee8a93791 four user defined waves 2025-02-12 11:02:51 +01:00
c706461443 Merge pull request 'nn-communication' (#8) from nn-communication into main
Reviewed-on: #8
2024-11-29 14:21:11 +01:00
081e33dd07 modulator noise function previous val bug 2024-11-29 14:20:29 +01:00
eb02657b4a center vals included in NN data 2024-10-22 13:27:07 +02:00
d670aefcbd enums start with NONE 2024-10-22 12:32:42 +02:00
68baa3c8de Merge pull request 'nn-communication' (#7) from nn-communication into main
Reviewed-on: #7
2024-10-22 11:00:25 +02:00
a35fecafae NN data receiving 2024-10-22 10:58:46 +02:00
b3ba9b0f2d cachedNoise no longer an array 2024-10-18 15:34:53 +02:00
102a88b242 bugfix - modulators not properly initializing 2024-10-15 14:54:37 +02:00
aebfbe5277 Merge pull request 'main' (#6) from main into nn-communication
Reviewed-on: #6
2024-10-15 12:27:53 +02:00
2d81a832af Merge branch 'nn-communication' into main 2024-10-15 12:27:08 +02:00
cdbcf24dfc Merge pull request 'fixed userwave update' (#5) from lock into main
Reviewed-on: #5
2024-10-15 12:21:08 +02:00
64f1455f69 fixed userwave update 2024-10-14 09:16:05 +02:00
trian-gles
a855d254cd non musical timing to ms value for input from neural net 2024-10-11 15:34:37 +02:00
09a1b5f20f Merge pull request 'lock mode' (#4) from lock into main
Reviewed-on: #4
2024-10-11 13:36:54 +02:00
34e99f09fd lock mode 2024-10-11 12:52:54 +02:00
86a994546e Merge pull request 'noise-functions' (#3) from noise-functions into main
Reviewed-on: #3
2024-10-10 12:25:22 +02:00
d22cdc8401 basic random value noise functions 2024-10-10 12:23:50 +02:00
trian-gles
43e517cc4e added new init values 2024-09-26 12:40:47 +02:00
a608b083f3 working sine interpolation 2024-09-25 15:59:27 +02:00
83cf801ec3 merge conflict 2024-09-24 14:35:37 +02:00
trian-gles
45e168ba11 UI changes 2024-09-24 14:35:01 +02:00
37a65058bc Merge pull request 'musical-timing-phase' (#2) from musical-timing-phase into main
Reviewed-on: #2
2024-09-03 11:37:51 +02:00
trian-gles
45847fbae4 phase calculated from ticks 2024-09-03 11:34:32 +02:00
trian-gles
c794b5bc2f separated phase handling into two options 2024-09-02 16:58:18 +02:00
trian-gles
c8c410a38f UI changes 2024-08-08 15:31:57 +02:00
985b6c832f Merge pull request 'user defined wavetable shape' (#1) from user-defined-table into main
Reviewed-on: #1
2024-08-08 14:32:34 +02:00
trian-gles
ff0e8443fc user defined wavetable shape 2024-08-08 14:31:53 +02:00
446713d5d3 cleaned up example patch 2024-08-05 16:54:59 +02:00
2369ed5ddf instance numbering 2024-08-05 15:32:06 +02:00
071a78dc20 separate css 2024-08-05 10:08:29 +02:00
1d8610fa5f musical timing 2024-07-31 17:02:09 +02:00
ebd09dfd26 parsing for HZ 2024-07-30 15:53:35 +02:00
13cddd8f03 linking indicator 2024-07-29 13:56:45 +02:00
1f3972fc6f bugfix for deenumeration and parsing 2024-07-29 12:10:59 +02:00
b860254884 range instead of amp 2024-07-29 11:56:40 +02:00
a4842b4865 save message 2024-07-29 11:19:18 +02:00
315a8df1a0 denumeration 2024-07-26 13:52:04 +02:00
f9a8c05955 unfinished de enumeration 2024-07-24 19:31:08 +02:00
8381daf468 min and max display 2024-07-23 15:59:56 +02:00
e50e4c1e0c example with djster 2024-07-23 11:07:20 +02:00
18 changed files with 6605 additions and 1146 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

1740
ModEnum.maxhelp Normal file

File diff suppressed because it is too large Load Diff

3345
ModEnum.maxpat Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,56 +0,0 @@
function DropDown(props) {
return e('select', {type: "number", onChange: props.onChange, value: props.value},
...props.options.map((item) => Option(item)));
}
function ListItem(child){
return e('li', null, child);
}
function Label(text){
return e('div', {className: 'label'}, text);
}
function NumberBox(props){
return e('input', {type: "number", onChange: props.onChange, step: props.step, value: props.value, className: props.className}, null);
}
function TextBox(props){
return e('input', {type: "text", value: props.value, onChange: props.onChange, id: props.id});
}
function Option(str, value){
return e("option", {value: value}, str);
}
function Button(props){
return e('button', {onClick: props.onClick}, props.text);
}
function Switch(props){
return e('label', {className: 'switch'},
e('input', {type: 'checkbox', onClick: props.ontoggle}, null),
e('span', {className: 'slider round'}, null))
}
function CreateParamChanger(arr, setArr, index, cb=() => {}){
return (event) => {
let newArr = arr.slice();
newArr[index] = event.target.value;
setArr(newArr);
cb();
log(`${index} ${event.target.value}`);
}
}
function CreateMatrixParamChanger(matrix, setMatrix, i, j){
return (event) => {
var newMatrix = matrix.map(function(arr) {
return arr.slice();
});
newMatrix[i][j] = event.target.value;
setMatrix(newMatrix);
log(`${i}, ${j} ${event.target.value}`);
}
}

View File

@@ -1,57 +0,0 @@
/////////////////////////
// ENUMERATORS
/////////////////////////
// NOT A REACT FUNCTIONAL COMPONENT. MERELY RETURNS AN ARRAY WHICH IS UNPACKED
function EnumeratorItems(index, enumBreakPoints, setEnumBreakPoints, enumNames, setEnumNames, djParam){
let items = [];
for (let i = 0; i < MAXENUMPOINTS; i++){
items.push(ListItem(e(TextBox, {onChange: CreateMatrixParamChanger(enumNames, setEnumNames, index, i), value: enumNames[index][i], id:`text-${djParam}-${enumNames[index][i]}`}, null)));
// Add 1 to make up for the lower enum bound
items.push(ListItem(e(NumberBox, {onChange: CreateMatrixParamChanger(enumBreakPoints, setEnumBreakPoints, index, i + 1), value:enumBreakPoints[index][i + 1]}, null)));
}
return items;
}
function EnumeratorRow(props){
let content = e('ul', {className: 'lfo-item', id: `${props.djParam}-enum-row`},
ListItem(DropDown({onChange: props.setDjParam, value: props.djParam, options: MODPARAMOPTIONS})),
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, props.djParam).slice(0, props.enumItems * 2)),
ListItem(e(Button, {text:'+', onClick: props.addEnum}, null)),
ListItem(e(Button, {text:'-', onClick: props.removeEnum}, null))
);
if (props.visible){
return content;
};
}
function enumerate(name, inval, count, keys, vals){
let output = "OUT OF RANGE";
for (let i=0; i < count + 1; i++){
if (inval <= keys[i]){
if (i > 0)
output = vals[i - 1];
break
}
}
let highlightedItem = document.getElementById(`text-${name}-${output}`);
if (highlightedItem){
highlightedItem.style.animation = "pulse-animation 0.5s normal";
highlightedItem.addEventListener('animationend', () => {
highlightedItem.style.animation = "";
});
}
if (name !== "NONE")
window.max.outlet(name + " " + output);
}

View File

@@ -1,404 +0,0 @@
{
"patcher" : {
"fileversion" : 1,
"appversion" : {
"major" : 8,
"minor" : 5,
"revision" : 6,
"architecture" : "x64",
"modernui" : 1
}
,
"classnamespace" : "box",
"rect" : [ 299.0, 99.0, 1220.0, 715.0 ],
"bglocked" : 0,
"openinpresentation" : 0,
"default_fontsize" : 12.0,
"default_fontface" : 0,
"default_fontname" : "Arial",
"gridonopen" : 1,
"gridsize" : [ 15.0, 15.0 ],
"gridsnaponopen" : 1,
"objectsnaponopen" : 1,
"statusbarvisible" : 2,
"toolbarvisible" : 1,
"lefttoolbarpinned" : 0,
"toptoolbarpinned" : 0,
"righttoolbarpinned" : 0,
"bottomtoolbarpinned" : 0,
"toolbars_unpinned_last_save" : 0,
"tallnewobj" : 0,
"boxanimatetime" : 200,
"enablehscroll" : 1,
"enablevscroll" : 1,
"devicewidth" : 0.0,
"description" : "",
"digest" : "",
"tags" : "",
"style" : "",
"subpatcher_template" : "",
"assistshowspatchername" : 0,
"boxes" : [ {
"box" : {
"id" : "obj-28",
"linecount" : 2,
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 493.0, 444.0, 161.0, 35.0 ],
"text" : "\"harmoniclarity 0.1955362124045653\""
}
}
, {
"box" : {
"id" : "obj-19",
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 541.52631402015686, 69.0, 150.0, 20.0 ],
"text" : "self explanatory"
}
}
, {
"box" : {
"id" : "obj-14",
"linecount" : 3,
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 817.0, 45.0, 150.0, 47.0 ],
"text" : "self explanatory. Warning-will overwrite whatever is saved."
}
}
, {
"box" : {
"id" : "obj-11",
"linecount" : 4,
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 1039.0, 50.0, 150.0, 60.0 ],
"text" : "You can use the `phase` control to phase offset two LFOs of the same frequency"
}
}
, {
"box" : {
"id" : "obj-7",
"linecount" : 3,
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 109.0, 285.0, 197.0, 47.0 ],
"presentation_linecount" : 3,
"text" : "This parameter is defined in the enumerators but not the modulators"
}
}
, {
"box" : {
"id" : "obj-5",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 146.0, 334.0, 94.0, 22.0 ],
"text" : "param scale 1.6"
}
}
, {
"box" : {
"id" : "obj-25",
"maxclass" : "newobj",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "bang" ],
"patching_rect" : [ 454.0, 27.5, 48.0, 22.0 ],
"text" : "del 200"
}
}
, {
"box" : {
"id" : "obj-24",
"linecount" : 3,
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 89.0, 358.0, 254.0, 47.0 ],
"text" : "This parameter is not defined by either the Modulators or Enumerators, so it will be passed directly to the output"
}
}
, {
"box" : {
"id" : "obj-23",
"linecount" : 3,
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 115.0, 201.0, 197.0, 47.0 ],
"text" : "This parameter is defined in the modulators, and when sent will act as the low value for that LFO"
}
}
, {
"box" : {
"id" : "obj-21",
"linecount" : 10,
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 89.0, 463.0, 254.0, 141.0 ],
"text" : "The operation runs\n\nInput > Modulators > Enumerators > Output\n\nA parameter from the input not established by a Modulator will be passed directly to the Enumerators\n\nLikewise, the Enumerators will pass not established parameters"
}
}
, {
"box" : {
"id" : "obj-15",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 135.0, 259.0, 121.0, 22.0 ],
"text" : "param metriclarity 40"
}
}
, {
"box" : {
"id" : "obj-8",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 115.0, 409.0, 129.0, 22.0 ],
"text" : "param attenuation 200"
}
}
, {
"box" : {
"id" : "obj-16",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 438.52631402015686, 69.0, 101.0, 22.0 ],
"text" : "load localStorage"
}
}
, {
"box" : {
"data" : {
"data" : {
"enumArrays" : [ [ 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], [ "2", 2, 2, 2, "2", "2", "2", "2", "2", "2" ], [ "event_length", "meter", "stream", "scale", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation" ] ],
"enumMats" : [ [ [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ "0", "1", "2", 3, 4, 5, 6, 7, 8, 9, 10 ], [ "0", "1", "2", 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] ], [ [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "3 4", "7 8", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "0", "1", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "pentatonic", "major", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ], [ "param", "param", "param", "param", "param", "param", "param", "param", "param", "param" ] ] ],
"modArrays" : [ [ 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], [ "Sine", "SawUp", "SawDown", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine", "Sine" ], [ "metriclarity", "stream", "meter", "harmoniclarity", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation", "attenuation" ], [ "0.6", "0.3", "0.1", "0.6", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1" ], [ "30", "2", "2", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1" ], [ "0", "0", "0", "0.5", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" ] ]
}
}
,
"id" : "obj-4",
"maxclass" : "newobj",
"numinlets" : 2,
"numoutlets" : 4,
"outlettype" : [ "dictionary", "", "", "" ],
"patching_rect" : [ 22.0, 102.0, 159.0, 22.0 ],
"saved_object_attributes" : {
"embed" : 1,
"parameter_enable" : 0,
"parameter_mappable" : 0
}
,
"text" : "dict localStorage @embed 1"
}
}
, {
"box" : {
"id" : "obj-6",
"linecount" : 2,
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 504.0, 6.0, 150.0, 33.0 ],
"text" : "required due to the asynchronous operation"
}
}
, {
"box" : {
"id" : "obj-3",
"maxclass" : "newobj",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "bang" ],
"patching_rect" : [ 374.157894849777222, 27.5, 55.0, 22.0 ],
"text" : "del 1000"
}
}
, {
"box" : {
"id" : "obj-45",
"linecount" : 5,
"maxclass" : "comment",
"numinlets" : 1,
"numoutlets" : 0,
"patching_rect" : [ 31.0, 18.0, 150.0, 74.0 ],
"text" : "Storage for the matrix. Unfortunately, jsweb dictionary handling isn't great, so we can't use it like a native dict object"
}
}
, {
"box" : {
"id" : "obj-20",
"maxclass" : "newobj",
"numinlets" : 1,
"numoutlets" : 1,
"outlettype" : [ "bang" ],
"patching_rect" : [ 217.0, 6.0, 58.0, 22.0 ],
"text" : "loadbang"
}
}
, {
"box" : {
"id" : "obj-12",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 700.894735336303711, 69.0, 104.0, 22.0 ],
"text" : "save localStorage"
}
}
, {
"box" : {
"id" : "obj-18",
"maxclass" : "message",
"numinlets" : 2,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 217.0, 69.0, 106.0, 22.0 ],
"text" : "readfile lfogui.html"
}
}
, {
"box" : {
"id" : "obj-2",
"maxclass" : "jweb",
"numinlets" : 1,
"numoutlets" : 1,
"outlettype" : [ "" ],
"patching_rect" : [ 438.52631402015686, 115.0, 643.105266809463501, 318.0 ],
"rendermode" : 0,
"url" : "file://lfogui.html"
}
}
],
"lines" : [ {
"patchline" : {
"destination" : [ "obj-2", 0 ],
"source" : [ "obj-12", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-2", 0 ],
"source" : [ "obj-15", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-2", 0 ],
"source" : [ "obj-16", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-2", 0 ],
"source" : [ "obj-18", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-28", 1 ],
"source" : [ "obj-2", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-18", 0 ],
"order" : 1,
"source" : [ "obj-20", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-3", 0 ],
"order" : 0,
"source" : [ "obj-20", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-15", 0 ],
"source" : [ "obj-25", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-16", 0 ],
"order" : 1,
"source" : [ "obj-3", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-25", 0 ],
"order" : 0,
"source" : [ "obj-3", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-2", 0 ],
"source" : [ "obj-5", 0 ]
}
}
, {
"patchline" : {
"destination" : [ "obj-2", 0 ],
"source" : [ "obj-8", 0 ]
}
}
],
"dependency_cache" : [ ],
"autosave" : 0
}
}

View File

@@ -1,188 +1,22 @@
<!DOCTYPE html>
<html>
<!--
We start with a basic html 'page' that is the size of the jweb object,
but has no scrollbars nor floating content.
-->
<head>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0px;
border: 0;
overflow: hidden; /* Disable scrollbars */
display: block; /* No floating content on sides */
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333333;
}
li {
float: left;
}
input[type=number] {
width: 50px;
margin: 0;
padding: 0;
}
input[type=text] {
width: 60px;
margin: 0;
padding: 0;
font-weight: bold;
}
#matrix {
background-color: aquamarine;
height: 100%;
width: 100%;
}
.numbox-unclicked {
user-select: none;
border: solid;
font-size: 12vw;
}
.numbox-clicked {
user-select: none;
border : solid;
font-size: 12vw;
}
.param-input-label {
width: 93%;
font-size: 5vw;
}
.lfo-input-label {
width: 40%;
font-size: 5vw;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 30px;
height: 17px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 13px;
width: 13px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(13px);
-ms-transform: translateX(13px);
transform: translateX(13px);
}
/* Rounded sliders */
.slider.round {
border-radius: 17px;
}
.slider.round:before {
border-radius: 50%;
}
h5 {
margin: 0;
padding: 0;
}
.enum-count {
background-color: aquamarine;
}
.label {
background-color: aliceblue;
padding: 0 4px 0 4px;
margin: 0 2px 0 2px;
border-color: #333333;
border-width: 1px;
}
@keyframes pulse-animation {
0% {
color: black;
}
100% {
color: red;
}
}
#pulse {
animation: pulse-animation 0.2s normal;
}
</style>
<meta charset="utf-8">
<link rel="stylesheet" href="./src/lfogui.css">
</head>
<body>
<div id="lfo-container"></div>
<div id="lfo-container"></div>
<script src="./react.js"></script>
<script src="./react-dom.js"></script>
<script src="./common.js"></script>
<script src="./enums.js"></script>
<script src="./modulators.js"></script>
<script src="./lfogui.js">
</script>
<script src="./lib/react.js"></script>
<script src="./lib/react-dom.js"></script>
<script src="./lib/moment.js"></script>
<script src="./src/common.js"></script>
<script src="./src/enums.js"></script>
<script src="./src/modulators.js"></script>
<script src="./src/lfogui.js"></script>
</body>
</html>
</html>

379
lfogui.js
View File

@@ -1,379 +0,0 @@
// const { createElement } = require("./react");
const DEBUG = false;
var log;
if (DEBUG)
log = console.log;
else
log = window.max.outlet;
const e = React.createElement;
let lfos = [];
const MAXLFOS = 20;
const MAXENUMS = 20;
const MAXENUMPOINTS = 10;
const ViewModes = Object.freeze({
MOD: 0,
ENUM: 1
});
var modPhases = Array(MAXLFOS).fill(0);
var firstUpdateTime = Date.now();
const MODULATORLABELS = ["-type-", "---shape---", "-------param-------", "--freq--", "--amp--", "--phase--"];
const ENUMERATORLABELS = ["---parameter---", "-# points-"];
function MasterLfoHandler(){
let initVisArr = Array(MAXLFOS).fill(false);
initVisArr[0] = true;
const [viewMode, setViewMode] = React.useState(ViewModes.MOD);
const toggleViewMode = () => {
if (viewMode === ViewModes.MOD)
setViewMode(ViewModes.ENUM);
else
setViewMode(ViewModes.MOD);
};
/// MODULATOR ARRAYS
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('NONE'));
const [freqArr, setFreqArr] = React.useState(Array(MAXLFOS).fill('1'));
const [ampArr, setAmpArr] = React.useState(Array(MAXLFOS).fill('1'));
const [phaseArr, setPhaseArr] = React.useState(Array(MAXLFOS).fill('0'));
const allModArrays = [modVisibleArr, shapeArr, djParamArr, freqArr, ampArr, phaseArr];
const allModSetters = [setModVisibleArr, setShapeArr, setDjParamArr, setFreqArr, setAmpArr, setPhaseArr];
const modBlankVals = [true, SHAPETYPES[0], MODPARAMOPTIONS[0], '1', '1', '0'];
/// ENUMERATOR ARRAYS
const [enumVisibleArr, setEnumVisibleArr] = React.useState(initVisArr);
const [enumItemCounts, setEnumItemCounts] = React.useState(Array(MAXENUMPOINTS).fill('2'));
const [enumDjParamArr, setEnumDjParamArr] = React.useState(Array(MAXENUMPOINTS).fill('attenuation'));
let baseEnumBreakpoints = Array(MAXENUMS).fill(0).map(x => Array(MAXENUMPOINTS+ 1).fill(0));
for (let i = 0; i < MAXENUMS; i++){
for (let j=0; j < MAXENUMPOINTS + 1; j++){
baseEnumBreakpoints[i][j] = j;
}
}
const [enumBreakPoints, setEnumBreakPoints] = React.useState(baseEnumBreakpoints);
const getBlankEnumBreakPointRow = () => {
let arr = []
for (let i=0; i< MAXENUMPOINTS + 1; i++)
arr.push(i)
return arr;
}
const getBlankEnumNameRow = () => {return Array(MAXENUMPOINTS).fill('param')};
let baseEnumNames = Array(MAXENUMS).fill(0).map(x => Array(MAXENUMPOINTS).fill('param'));
const [enumNames, setEnumNames] = React.useState(baseEnumNames);
const allEnumArrays = [enumVisibleArr, enumItemCounts, enumDjParamArr];
const allEnumArrSetters = [setEnumVisibleArr, setEnumItemCounts, setEnumDjParamArr];
const allEnumMats = [enumBreakPoints, enumNames];
const allEnumMatSetters = [setEnumBreakPoints, setEnumNames];
const allGetEnumMatBlankVals = [getBlankEnumBreakPointRow, getBlankEnumNameRow]
const enumBlankVals = [true, 2, MODPARAMOPTIONS[0]];
const [render, rerender] = React.useState(false); // BAD. I SHOULDN'T BE DOING THIS
React.useEffect(() => {
function handleLoad(event) {
window.max.getDict(event.detail, (dict) => {
for (let i = 0; i<allModArrays.length; i++) {
allModSetters[i](dict.data.modArrays[i]);
}
for (let i = 0; i<allEnumArrays.length; i++) {
allEnumArrSetters[i](dict.data.enumArrays[i]);
}
for (let i = 0; i<allEnumMats.length; i++) {
allEnumMatSetters[i](dict.data.enumMats[i]);
}
})
}
function handleSave(event) {
let data = {
'modArrays' : allModArrays,
'enumArrays' : allEnumArrays,
'enumMats' : allEnumMats
}
window.max.setDict(event.detail, {"data" : data});
}
// only called internally by 1. Handler after modulator processing 2. LFO outputs
function handleEnum(event){
let name = event.detail[0];
let val = event.detail[1];
// if none of the Enums use this param, then we output it
let i = 0;
while (i < MAXENUMS){
if (enumVisibleArr[i] && enumDjParamArr[i] == name)
break;
i++
}
if (i == MAXENUMS){
window.max.outlet(name + ' ' + val);
}
else {
enumerate(name, val, enumItemCounts[i], enumBreakPoints[i], enumNames[i]);
}
}
function handleParam(event) {
let name = event.detail[0];
let val = event.detail[1];
// if none of the LFOs use this param, then we send it straight to the enum
let i = 0;
while (i < MAXLFOS){
if (modVisibleArr[i] && djParamArr[i] == name)
break;
i++
}
if (i == MAXLFOS){
window.dispatchEvent(new CustomEvent('enum', {'detail' : [name, val]}));
}
modCenterVals[name] = val;
setModCenterVals(modCenterVals);
}
function handleTick(event) {
let time = (Date.now() - firstUpdateTime) / 1000;
operateModulators(modVisibleArr, djParamArr, modCenterVals, freqArr, ampArr, shapeArr, phaseArr, time);
}
window.addEventListener('loadDict', handleLoad);
window.addEventListener('saveDict', handleSave);
window.addEventListener('tick', handleTick);
window.addEventListener('param', handleParam);
window.addEventListener('enum', handleEnum);
return () => {
window.removeEventListener('loadDict', handleLoad);
window.removeEventListener('saveDict', handleSave);
window.removeEventListener('tick', handleTick);
window.removeEventListener('param', handleParam);
window.removeEventListener('enum', handleEnum);
};
}, [...allModArrays, ...allEnumArrays, ...allEnumMats, modCenterVals]);
///////
// Generate Modulators
///////
let modContents = []
for (var i = 0; i<MAXLFOS; i++){
let id = i;
modContents.push(
e(LfoRow, {
shape: shapeArr[i],
setShape: CreateParamChanger(shapeArr, setShapeArr, i),
djParam: djParamArr[i],
setDjParam: CreateParamChanger(djParamArr, setDjParamArr, i),
freq: freqArr[i],
setFreq: CreateParamChanger(freqArr, setFreqArr, i),
amp: ampArr[i],
setAmp: CreateParamChanger(ampArr, setAmpArr, i),
phase: phaseArr[i],
setPhase: CreateParamChanger(phaseArr, setPhaseArr, i),
visible: modVisibleArr[i],
addLfo: () => {
if (id < MAXLFOS - 1){
if (modVisibleArr[id + 1]){
let emptyIndex = modVisibleArr.findIndex((item) => !item);
if (emptyIndex != -1){
for (var j = 0; j < allModArrays.length; j++){
let array = allModArrays[j];
// remove from all arrays
array.splice(emptyIndex, 1);
// add empty item at opened index
array.splice(id + 1, 0, modBlankVals[j]);
allModSetters[j](array);
}
}
}
else {
for (var j = 0; j < allModArrays.length; j++){ // no space below, easy.
let array = allModArrays[j];
array[id + 1] = modBlankVals[j];
allModSetters[j](array);
}
}
rerender(!render);
}
},
removeLfo: () => {
if (modVisibleArr.filter(x=>x).length > 1){
let newArr = modVisibleArr.slice();
newArr[id] = false;
setModVisibleArr(newArr);
}
}
},
null));
}
///////
// Generate Enumerators
///////
let enumContents = []
for (var i = 0; i<MAXLFOS; i++){
let id = i;
enumContents.push(
e(EnumeratorRow, {
index: i,
enumItems: enumItemCounts[i],
setEnumItemCounts: CreateParamChanger(enumItemCounts, setEnumItemCounts, i),
enumBreakPoints: enumBreakPoints,
setEnumBreakPoints: setEnumBreakPoints,
enumNames: enumNames,
setEnumNames: setEnumNames,
visible: enumVisibleArr[i],
djParam: enumDjParamArr[i],
setDjParam: CreateParamChanger(enumDjParamArr, setEnumDjParamArr, i),
addEnum: () => {
if (id < MAXLFOS - 1){
if (enumVisibleArr[id + 1]){ // if we need to open up space
let emptyIndex = enumVisibleArr.findIndex((item) => !item);
if (emptyIndex != -1){
for (var j = 0; j < allEnumArrays.length; j++){
let array = allEnumArrays[j];
// remove from all arrays
array.splice(emptyIndex, 1);
// add empty item at opened index
array.splice(id + 1, 0, enumBlankVals[j]);
allEnumArrSetters[j](array);
}
// Now do the same with matrices
for (var j = 0; j < allEnumMats.length; j++){
let mat = allEnumMats[j];
mat.splice(emptyIndex, 1);
mat.splice(id + 1, 0, 0);
mat[id + 1] = allGetEnumMatBlankVals[j]();
allEnumMatSetters[j](mat);
}
}
}
else {
for (var j = 0; j < allEnumArrays.length; j++){
let array = allEnumArrays[j];
array[id+1] = enumBlankVals[j];
allEnumArrSetters[j](array);
}
// Now do the same with matrices
for (var j = 0; j < allEnumMats.length; j++){
let mat = allEnumMats[j];
mat[id + 1] = allGetEnumMatBlankVals[j]();
allEnumMatSetters[j](mat);
}
}
rerender(!render);
}
},
removeEnum: () => {
if (enumVisibleArr.filter(x=>x).length > 1){
let newArr = enumVisibleArr.slice();
newArr[id] = false;
setEnumVisibleArr(newArr);
}
}
}, null)
);
}
var grid;
var title;
var labels;
if (viewMode === ViewModes.MOD){
grid = modContents;
title = "MODULATORS";
labels = MODULATORLABELS;
}
else {
grid = enumContents;
title = "ENUMERATORS";
labels = ENUMERATORLABELS;
}
return e('div', null,
e(Switch, {ontoggle: toggleViewMode}, null),
e('h5', null, title),
e('ul', null, ...labels.map(x => ListItem(Label(x)))),
e('div', {id: 'grid'}, ...grid)
);
}
if (!DEBUG){
window.max.bindInlet("load", (dictId) => {
window.dispatchEvent(new CustomEvent('loadDict', {'detail' : dictId}));
});
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);
}
const root = ReactDOM.createRoot(document.getElementById('lfo-container'));
root.render(e(MasterLfoHandler, null, null));

2
lib/moment.js Normal file

File diff suppressed because one or more lines are too long

View File

View File

View File

@@ -1,73 +0,0 @@
/////////////////////////
// MODULATORS
/////////////////////////
var SHAPETYPES = ["Sine", "SawUp", "SawDown", "Tri", "Square"];
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"];
function ControlType(){
return e('select', {className: 'control-type'}, Option("LFO"));
}
function LfoRow(props){
let content = e('ul', {className: 'lfo-item'},
ListItem(ControlType()),
ListItem(DropDown({onChange: props.setShape, value:props.shape, options: SHAPETYPES})),
ListItem(DropDown({onChange: props.setDjParam, value: props.djParam, options: MODPARAMOPTIONS})),
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("input", {type: 'range', min: 0, max: 1, step: 0.01, readonly: true, id: `slider-${props.djParam}`})),
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) / 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);
}
}
function operateModulators(visibleArr, paramNames, centers, freqs, amps, waveTypes, phaseArr, time){
for (let i=0; i<paramNames.length; i++){
if (visibleArr[i]){
let name = paramNames[i];
let center = 0;
if (centers.hasOwnProperty(name)){
center = centers[name];
}
let output = operateModulator(center, freqs[i], amps[i], waveTypes[i], phaseArr, i, name, time);
if (name !== "NONE")
window.dispatchEvent(new CustomEvent('enum', {'detail' : [name, output]}));
}
}
}
function operateModulator(center, freq, amp, waveType, phaseArr, phaseI, name, time){
let phase = (time * freq + parseFloat(phaseArr[phaseI])) % 1.00;
let unscaled = indexWave(waveType, phase);
let el = document.getElementById(`slider-${name}`);
if (el)
el.value = unscaled;
return unscaled * amp + center;
}

93
myStorage.json Normal file

File diff suppressed because one or more lines are too long

75
src/common.js Normal file
View File

@@ -0,0 +1,75 @@
function isNumeric(str) {
if (typeof str != "string") return false // we only process strings!
return !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
!isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail
}
function DropDown(props) {
let className = props.locked ? 'locked-component' : '';
return e('select', {className, type: "number", onChange: props.onChange, value: props.value},
...props.options.map((item) => Option(item)));
}
function ListItem(child){
return e('li', null, child);
}
function Label(text){
return e('div', {className: 'label'}, text);
}
function NumberBox(props){
let extraClassName = props.locked ? ' locked-component' : '';
return e('input', {type: "number", onChange: props.onChange, step: props.step, value: props.value, className: props.className + extraClassName}, null);
}
function TextBox(props){
let className = props.locked ? 'locked-component' : '';
return e('input', {className, type: "text", value: props.value, onChange: props.onChange, id: props.id});
}
function Option(str, value){
return e("option", {value: value}, str);
}
function Button(props){
let className = props.locked ? 'locked-component' : '';
return e('button', {onClick: props.onClick, className}, props.text);
}
function Switch(props){
return e('label', {className: 'switch'},
e('input', {type: 'checkbox', onClick: props.ontoggle}, null),
e('span', {className: 'slider round'}, null))
}
function SendSaveEvent(){
setTimeout(() => {
window.dispatchEvent(new CustomEvent('saveDict', {'detail' : "localStorage"}));
}, 50)
}
function CreateParamChanger(arr, setArr, index, postCB=() => {}, preCB=(val) => val){
return (event) => {
let newArr = arr.slice();
newArr[index] = preCB(event.target.value);
setArr(newArr);
postCB();
SendSaveEvent();
log(`${index} ${event.target.value}`);
}
}
function CreateMatrixParamChanger(matrix, setMatrix, i, j){
return (event) => {
var newMatrix = matrix.map(function(arr) {
return arr.slice();
});
newMatrix[i][j] = event.target.value;
setMatrix(newMatrix);
SendSaveEvent();
log(`${i}, ${j} ${event.target.value}`);
}
}

76
src/enums.js Normal file
View File

@@ -0,0 +1,76 @@
/////////////////////////
// ENUMERATORS
/////////////////////////
function DataCell(element) {
return e('td', null, element);
}
// NOT A REACT FUNCTIONAL COMPONENT. MERELY RETURNS AN ARRAY WHICH IS UNPACKED
function EnumeratorItems(index, enumBreakPoints, setEnumBreakPoints, enumNames, setEnumNames, djParam, locked){
let items = [];
for (let i = 0; i < MAXENUMPOINTS; i++){
items.push(DataCell(e(TextBox, {locked, onChange: CreateMatrixParamChanger(enumNames, setEnumNames, index, i), value: enumNames[index][i], id:`text-${djParam}-${enumNames[index][i]}`}, null)));
// Add 1 to make up for the lower enum bound
items.push(DataCell(e(NumberBox, {locked, onChange: CreateMatrixParamChanger(enumBreakPoints, setEnumBreakPoints, index, i + 1), value:enumBreakPoints[index][i + 1]}, null)));
}
return items;
}
function EnumeratorRow(props){
let linkedText = props.linked ? "<- mods" : "";
let content = e('tr', {className: 'lfo-item', id: `${props.djParam}-enum-row`},
DataCell(DropDown({locked:props.locked, onChange: props.setInstanceNum, value:props.instanceNum, options: INSTANCEOPTIONS})),
DataCell(DropDown({locked:props.locked, onChange: props.setDjParam, value: props.djParam, options: MODPARAMOPTIONS})),
DataCell(e(NumberBox, {locked:props.locked, onChange: props.setEnumItemCounts, step:1, value:props.enumItems, className: 'enum-count'}, null)),
DataCell(e(NumberBox, {locked:props.locked, 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, props.djParam, props.locked).slice(0, props.enumItems * 2)),
DataCell(e(Button, {locked:props.locked, text:'+', onClick: props.addEnum}, null)),
DataCell(e(Button, {locked:props.locked, text:'-', onClick: props.removeEnum}, null)),
DataCell(e("div", {className:"linked"}, linkedText))
);
if (props.visible){
return content;
};
}
function denumerate(inval, count, keys, vals){
let output = inval;
for (let i=0; i < count; i++){
log(vals[i]);
if (inval == vals[i]){
output = (parseFloat(keys[i]) + parseFloat(keys[i+1])) / 2; // linear interpolate
}
}
return output;
}
function enumerate(name, inst, inval, count, keys, vals){
let output = "OUT OF RANGE";
for (let i=0; i < count + 1; i++){
if (inval <= keys[i]){
if (i > 0)
output = vals[i - 1];
break
}
}
let highlightedItem = document.getElementById(`text-${name}-${output}`);
if (highlightedItem){
highlightedItem.style.animation = "pulse-animation 0.5s normal";
highlightedItem.addEventListener('animationend', () => {
highlightedItem.style.animation = "";
});
}
if (name !== "NONE")
window.max.outlet(inst + " " + name + " " + output);
}

378
src/lfogui.css Normal file
View File

@@ -0,0 +1,378 @@
* {
--locked-color: #5fadbf;
--unlocked-color: #ff5153;
}
:root {
--background: white;
--active: royalblue;
--nonactive: rgb(205, 205, 205);
--alert: red;
--textcolor: black;
}
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
html {
background-color: var(--background);
}
body {
font-family: "Poppins", sans-serif;
}
html,
body {
width: 100%;
height: 100%;
margin: 0px;
border: 0;
overflow-x: scroll;
overflow-y: scroll;
display: block;
/* No floating content on sides */
}
/*navigation*/
.header {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
margin: 1em;
border-bottom: 1px solid var(--active);
}
.header button {
border: 1px solid var(--active);
color: var(--active);
background-color: white;
padding: 4px;
text-align: center;
display: inline-block;
padding: 0.5em;
margin: 4px;
font-size: 0.9em;
min-width: 4.5em;
}
td button {
background-color: white;
color: var(--active);
border: 1px solid var(--active);
}
/* :::::::::::::: SELECTING MODULATORS/ENUMERATORS */
.header button.highlighted-button {
color: var(--active);
border: 1px solid var(--active);
}
.header button.unhighlighted-button {
color: var(--nonactive);
border: 1px solid var(--nonactive);
}
/* table */
table {
margin: 1em;
padding: 0em;
border-collapse: collapse;
background-color: lightsteelblue;
}
/* points datacells should have a min-width*/
.enum-count {
min-width: 5.6em;
}
th {
padding: 0.4em 0.3em;
text-align: left;
}
thead {
color: white;
background-color: var(--active);
}
tr,
td {
margin: 0em;
}
td:last-child {
white-space: nowrap;
overflow: hidden;
padding: 0;
}
/* input types */
/* dropdown list */
select {
width: 100%;
}
/*
option, select>* {
font-size: 0.8em !important;
padding: 0em !important;
margin: 0em !important;
min-height: 0em !important;
}
*/
option {
background-color: var(--active);
}
option:not(:checked) {
background-color: white;
}
/* input */
input,
select {
border: 1px solid var(--active);
color: var(--active);
margin: 0;
padding: 0;
line-height: 1.5em;
height: 1.5em;
box-sizing: border-box;
}
input {
padding-left: 5px;
/*slight padding on left*/
}
input[type=number] {
width: 50px;
}
input[type=text] {
width: 60px;
font-weight: bold;
}
.timeInput {
width: 80px;
}
#matrix {
background-color: aquamarine;
height: 100%;
width: 100%;
}
.numbox-unclicked {
user-select: none;
border: solid;
font-size: 12vw;
}
.numbox-clicked {
user-select: none;
border: solid;
font-size: 12vw;
}
.param-input-label {
width: 93%;
font-size: 5vw;
}
.lfo-input-label {
width: 40%;
font-size: 5vw;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 30px;
height: 17px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
input[type="range"] {
appearance: none;
-webkit-appearance: none;
height: 8px;
border-radius: 6px;
background-color: white;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
overflow: hidden;
}
/*slider knob*/
input[type="range"]::-webkit-slider-runnable-track {
-webkit-appearance: none;
color: var(--active);
margin-top: -1px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--active);
box-shadow: -80px 0 0 80px var(--active);
}
input[type="range"]::-moz-range-progress {
background-color: var(--active);
}
h5 {
margin: 0;
padding: 0;
}
.label {
background-color: aliceblue;
padding: 0 4px 0 4px;
margin: 0 2px 0 2px;
border-color: #333333;
border-width: 1px;
}
.base-val {
border: none;
color: white;
text-align: center;
font-size: 0.9em;
width: 50px;
margin: 0;
padding: 0;
}
.linked {
color: red;
border-width: 1px;
width: 50px;
font-size: small;
margin-left: 2px;
margin-top: 5px;
display: none;
/*hide*/
}
@keyframes pulse-animation {
0% {
color: black;
}
100% {
color: red;
}
}
#pulse {
animation: pulse-animation 0.2s normal;
}
/* :::::::::::::: LOCK CSS */
.locked-component {
pointer-events: none;
background-color: #333333;
color: white;
}
/* Locked */
.lock {
margin-top: 14px;
width: 24px;
height: 21px;
border: 3px solid var(--locked-color);
border-radius: 5px;
position: relative;
cursor: pointer;
-webkit-transition: all 0.1s ease-in-out;
transition: all 0.1s ease-in-out;
}
.lock:after {
content: "";
display: block;
background: var(--locked-color);
width: 3px;
height: 7px;
position: absolute;
top: 50%;
left: 50%;
margin: -3.5px 0 0 -2px;
-webkit-transition: all 0.1s ease-in-out;
transition: all 0.1s ease-in-out;
}
.lock:before {
content: "";
display: block;
width: 10px;
height: 10px;
bottom: 100%;
position: absolute;
left: 50%;
margin-left: -8px;
border: 3px solid var(--locked-color);
border-top-right-radius: 50%;
border-top-left-radius: 50%;
border-bottom: 0;
-webkit-transition: all 0.1s ease-in-out;
transition: all 0.1s ease-in-out;
}
/* Locked Hover */
.lock:hover:before {
height: 12px;
}
/* Unlocked */
.unlocked {
transform: rotate(10deg);
}
.unlocked:before {
bottom: 130%;
left: 31%;
margin-left: -11.5px;
transform: rotate(-45deg);
}
.unlocked,
.unlocked:before {
border-color: var(--unlocked-color);
}
.unlocked:after {
background: var(--unlocked-color);
}
/* Unlocked Hover */
.unlocked:hover {
transform: rotate(3deg);
}
.unlocked:hover:before {
height: 10px;
left: 40%;
bottom: 124%;
transform: rotate(-30deg);
}

660
src/lfogui.js Normal file
View File

@@ -0,0 +1,660 @@
// const { createElement } = require("./react");
const DEBUG = false;
var log;
if (DEBUG)
log = console.log;
else
log = (msg) => { window.max.outlet("debug " + msg) };
const e = React.createElement;
let lfos = [];
const MAXLFOS = 200;
const MAXENUMS = 200;
const MAXENUMPOINTS = 10;
const MAXUSERDEFINED = 4;
const ViewModes = Object.freeze({
MOD: 0,
ENUM: 1
});
const LockModes = Object.freeze({
UNLOCK: 0,
LOCK: 1
});
var modPhases = Array(MAXLFOS).fill(0);
var firstUpdateTime = Date.now();
const MODULATORLABELS = ["inst", "type", "shape", "param", "timebase", "min", "max", "phase", "center", "result", "", ""];
const ENUMERATORLABELS = ["inst", "parameter", "# points"];
function parseLfoTimeNonMusical(lfoTime) {
if (lfoTime.slice(-2) == "hz") {
return parseFloat(lfoTime.slice(0, -2));
}
else if (lfoTime.slice(-2) == "ms") {
return 1000 / parseFloat(lfoTime.slice(0, -2));
}
else if (lfoTime.slice(-1) == "s") {
return 1 / parseFloat(lfoTime.slice(0, -1));
}
else if ((lfoTime.match(/:/g) || []).length == 2) {
return 1 / moment.duration(lfoTime).asSeconds();
}
else if ((lfoTime.match(/\./g) || []).length == 2) {
return 0; // ignore musical timings
}
else {
return 0;
}
}
function nnFreqToHzString(num) {
return `${num}hz`;
}
function MasterLfoHandler() {
let initVisArr = Array(MAXLFOS).fill(false);
initVisArr[0] = true;
const [viewMode, setViewMode] = React.useState(ViewModes.MOD);
const [lockMode, setLockMode] = React.useState(LockModes.UNLOCK);
const toggleLockMode = () => {
if (lockMode === LockModes.UNLOCK)
setLockMode(LockModes.LOCK);
else
setLockMode(LockModes.UNLOCK);
};
const [enabled, setEnabled] = React.useState(false);
const toggleEnabled = () => {
setEnabled(!enabled);
};
const displayIfEnabled = (content) => {
if (enabled)
return content
}
let toggleEnabledText = enabled ? `Hide \u{25BE}` : `Show \u{25B8}`;
/// MODULATOR ARRAYS
let userDefinedWavesBase = [];
let userDefinedFunctionsBase = [];
let userDefinedTypesBase = [0, 0, 0, 0]; //0 = wave, 1 = function
for (let i = 0; i < MAXUSERDEFINED; i++) {
userDefinedWavesBase.push(Array(50).fill(0));
userDefinedFunctionsBase.push(Array(101).fill(0));
}
const [userDefinedWaves, setUserDefinedWaves] = React.useState(userDefinedWavesBase);
const [userDefinedFunctions, setUserDefinedFunctions] = React.useState(userDefinedFunctionsBase);
const [userDefinedTypes, setUserDefinedTypes] = React.useState(userDefinedTypesBase);
const [modVisibleArr, setModVisibleArr] = React.useState(initVisArr);
const [modTypeArr, setModTypeArr] = React.useState(Array(MAXLFOS).fill('LFO'));
const [modInstanceNumArr, setModInstanceNumArr] = React.useState(Array(MAXLFOS).fill('1'));
const [modCenterVals, setModCenterVals] = React.useState({ '1': {}, '2': {}, '3': {}, '4': {}, '5': {}, '6': {}});
const [ticks, setTicks] = React.useState(0);
const [beatsInMeasure, setBeatsInMeasure] = React.useState(4);
const [noiseTypeArr, setNoiseTypeArr] = React.useState(Array(MAXLFOS).fill('Sine Int.'));
const [shapeArr, setShapeArr] = React.useState(Array(MAXLFOS).fill('Sine'));
const [djParamArr, setDjParamArr] = React.useState(Array(MAXLFOS).fill('NONE'));
const [freqArr, setFreqArr] = React.useState(Array(MAXLFOS).fill('1hz'));
// const [ampArr, setAmpArr] = React.useState(Array(MAXLFOS).fill('1'));
const [minArr, setMinArr] = React.useState(Array(MAXLFOS).fill('0'));
const [maxArr, setMaxArr] = React.useState(Array(MAXLFOS).fill('1'));
const [initPhaseArr, setInitPhaseArr] = React.useState(Array(MAXLFOS).fill('0'));
const [lastPhaseArr, setLastPhaseArr] = React.useState(Array(MAXLFOS).fill(0));
const [cachedNoiseValueArr1, setCachedNoiseValueArr1] = React.useState(Array(MAXLFOS).fill(0));
const [cachedNoiseValueArr2, setCachedNoiseValueArr2] = React.useState(Array(MAXLFOS).fill(0));
const allModArrays = [modVisibleArr, modTypeArr, modInstanceNumArr, shapeArr, noiseTypeArr, djParamArr, freqArr, minArr, maxArr, initPhaseArr, lastPhaseArr, cachedNoiseValueArr1, cachedNoiseValueArr2];
const allModSetters = [setModVisibleArr, setModTypeArr, setModInstanceNumArr, setShapeArr, setNoiseTypeArr, setDjParamArr, setFreqArr, setMinArr, setMaxArr, setInitPhaseArr, setLastPhaseArr, setCachedNoiseValueArr1, setCachedNoiseValueArr2];
const modBlankVals = [true, 'LFO', '1', SHAPETYPES[0], NOISETYPES[0], MODPARAMOPTIONS[0], '1hz', '0', '1', '0', 0, 0, 0];
/// ENUMERATOR ARRAYS
const [enumVisibleArr, setEnumVisibleArr] = React.useState(initVisArr);
const [enumInstanceNumArr, setEnumInstanceNumArr] = React.useState(Array(MAXLFOS).fill('1'));
const [enumItemCounts, setEnumItemCounts] = React.useState(Array(MAXENUMPOINTS).fill('2'));
const [enumDjParamArr, setEnumDjParamArr] = React.useState(Array(MAXENUMPOINTS).fill('NONE'));
let baseEnumBreakpoints = Array(MAXENUMS).fill(0).map(x => Array(MAXENUMPOINTS + 1).fill(0));
for (let i = 0; i < MAXENUMS; i++) {
for (let j = 0; j < MAXENUMPOINTS + 1; j++) {
baseEnumBreakpoints[i][j] = j - 0.5;
}
}
const [enumBreakPoints, setEnumBreakPoints] = React.useState(baseEnumBreakpoints);
const getBlankEnumBreakPointRow = () => {
let arr = []
for (let i = 0; i < MAXENUMPOINTS + 1; i++)
arr.push(i - 0.5)
return arr;
}
const getBlankEnumNameRow = () => { return Array(MAXENUMPOINTS).fill('param') };
let baseEnumNames = Array(MAXENUMS).fill(0).map(x => Array(MAXENUMPOINTS).fill('param'));
const [enumNames, setEnumNames] = React.useState(baseEnumNames);
const allEnumArrays = [enumVisibleArr, enumInstanceNumArr, enumItemCounts, enumDjParamArr];
const allEnumArrSetters = [setEnumVisibleArr, setEnumInstanceNumArr, setEnumItemCounts, setEnumDjParamArr];
const allEnumMats = [enumBreakPoints, enumNames];
const allEnumMatSetters = [setEnumBreakPoints, setEnumNames];
const allGetEnumMatBlankVals = [getBlankEnumBreakPointRow, getBlankEnumNameRow]
const enumBlankVals = [true, '1', 2, MODPARAMOPTIONS[0]];
const [render, rerender] = React.useState(false); // BAD. I SHOULDN'T BE DOING THIS
React.useEffect(() => {
function handleLoad(event) {
window.max.getDict(event.detail, (dict) => {
for (let i = 0; i < dict.data.modArrays.length; i++) {
allModSetters[i](dict.data.modArrays[i]);
}
for (let i = 0; i < allEnumArrays.length; i++) {
allEnumArrSetters[i](dict.data.enumArrays[i]);
}
for (let i = 0; i < allEnumMats.length; i++) {
allEnumMatSetters[i](dict.data.enumMats[i]);
}
})
}
function handleSave(event) {
let data = {
'modArrays': allModArrays,
'enumArrays': allEnumArrays,
'enumMats': allEnumMats
}
window.max.setDict(event.detail, { "data": data });
window.max.outlet("saved");
}
// only called internally by 1. Handler after modulator processing 2. LFO outputs
function handleEnum(event) {
let inst = event.detail[0];
let name = event.detail[1];
let val = event.detail[2];
// if none of the Enums use this param, then we output it
let i = 0;
while (i < MAXENUMS) {
if (enumVisibleArr[i] && enumDjParamArr[i] == name && enumInstanceNumArr[i] == inst)
break;
i++
}
if (i == MAXENUMS) {
window.max.outlet(inst + ' ' + name + ' ' + val);
}
else {
enumerate(name, inst, val, enumItemCounts[i], enumBreakPoints[i], enumNames[i]);
}
}
function handleParam(event) {
let inst = event.detail[0]; // djster instance
let name = event.detail[1];
let val = event.detail[2];
// CHECK FOR INDEX OF THIS NAME IN ENUM MATRIX, AND IF IT IS THERE DENUMERATE
let index = -1;
for (let i = 0; i < MAXENUMS; i++) {
if (enumDjParamArr[i] == name && enumInstanceNumArr[i] == inst)
index = i;
}
if (index != -1) {
val = denumerate(val, enumItemCounts[index], enumBreakPoints[index], enumNames[index]);
}
// if none of the LFOs use this param, then we send it straight to the enum
let i = 0;
while (i < MAXLFOS) {
if (modVisibleArr[i] && djParamArr[i] == name && modInstanceNumArr[i] == inst)
break;
i++;
}
if (i == MAXLFOS) {
window.dispatchEvent(new CustomEvent('enum', { 'detail': [inst, name, val] }));
}
modCenterVals[inst][name] = val;
setModCenterVals(modCenterVals);
rerender(!render); // BAD! SHOULD NOT BE DOING THIS!
}
function handleTick(event) {
let time = (Date.now() - firstUpdateTime) / 1000;
let noiseData = { lastPhaseArr, setLastPhaseArr, cachedNoiseValueArr1, setCachedNoiseValueArr1, cachedNoiseValueArr2, setCachedNoiseValueArr2, noiseTypeArr };
operateModulators(modVisibleArr, modTypeArr, modInstanceNumArr, djParamArr, modCenterVals, freqArr, minArr, maxArr, shapeArr, initPhaseArr, noiseData, userDefinedWaves, userDefinedFunctions, userDefinedTypes, time, beatsInMeasure, ticks);
}
function handleTimeSig(event) {
setBeatsInMeasure(parseFloat(event.detail[0]) * parseFloat(event.detail[1]) / 4);
}
function handleChangeUserWave(event) {
userDefinedWaves[event.detail.index] = event.detail.points;
setUserDefinedWaves(userDefinedWaves);
}
function handleChangeUserFunction(event) {
userDefinedFunctions[event.detail.index] = event.detail.points;
setUserDefinedFunctions(userDefinedFunctions);
}
function handleChangeUserDefinedType(event) {
userDefinedTypes[event.detail.index - 1] = event.detail.type;
setUserDefinedTypes(userDefinedTypes);
}
function handleMaxTicks(event) {
setTicks(event.detail);
}
function handleChangeViewMode(event){
setEnabled(true);
setViewMode(event.detail);
}
function setNN(event) {
for (let i = 0; i < MAXLFOS; i++) {
freqArr[i] = nnFreqToHzString(event.detail[i]);
}
setFreqArr(freqArr)
for (let i = MAXLFOS; i < MAXLFOS * 2; i++) {
minArr[i - MAXLFOS] = event.detail[i];
}
setMinArr(minArr);
for (let i = MAXLFOS * 2; i < MAXLFOS * 3; i++) {
maxArr[i - MAXLFOS * 2] = event.detail[i];
}
setMaxArr(maxArr);
for (let i = MAXLFOS * 3; i < MAXLFOS * 4; i++) {
initPhaseArr[i - MAXLFOS * 3] = parseFloat(event.detail[i]);
}
setInitPhaseArr(initPhaseArr);
for (let i = MAXLFOS * 4; i < MAXLFOS * 5; i++) {
let index = i - MAXLFOS * 4;
let inst = modInstanceNumArr[index];
let param = djParamArr[index];
modCenterVals[inst][param] = parseFloat(event.detail[i]);
}
setModCenterVals(modCenterVals);
rerender(!render); // BAD! SHOULD NOT BE DOING THIS!
}
function dumpNN(event) {
let allNNData = [];
freqArr.forEach(element => {
allNNData.push(parseLfoTimeNonMusical(element));
});
allNNData = allNNData.concat(minArr);
allNNData = allNNData.concat(maxArr);
allNNData = allNNData.concat(initPhaseArr);
let lfoMatchedCenterVals = [];
for (let i = 0; i < MAXLFOS; i++) {
let inst = modInstanceNumArr[i];
let param = djParamArr[i];
lfoMatchedCenterVals.push(modCenterVals[inst][param]);
if (!lfoMatchedCenterVals[i])
lfoMatchedCenterVals[i] = 0
}
allNNData = allNNData.concat(lfoMatchedCenterVals);
window.max.outlet("NNdata " + allNNData.join(" "));
}
window.addEventListener('loadDict', handleLoad);
window.addEventListener('saveDict', handleSave);
window.addEventListener('dumpNN', dumpNN);
window.addEventListener('setNN', setNN);
window.addEventListener('tick', handleTick);
window.addEventListener('param', handleParam);
window.addEventListener('enum', handleEnum);
window.addEventListener('timesig', handleTimeSig);
window.addEventListener('userWave', handleChangeUserWave);
window.addEventListener('userFunction', handleChangeUserFunction);
window.addEventListener('userDefinedType', handleChangeUserDefinedType);
window.addEventListener('maxTicks', handleMaxTicks);
window.addEventListener('viewMode', handleChangeViewMode);
return () => {
window.removeEventListener('loadDict', handleLoad);
window.removeEventListener('saveDict', handleSave);
window.removeEventListener('dumpNN', dumpNN);
window.removeEventListener('setNN', setNN);
window.removeEventListener('tick', handleTick);
window.removeEventListener('param', handleParam);
window.removeEventListener('enum', handleEnum);
window.removeEventListener('timesig', handleTimeSig);
window.removeEventListener('userWave', handleChangeUserWave);
window.removeEventListener('userFunction', handleChangeUserFunction);
window.removeEventListener('userDefinedType', handleChangeUserDefinedType);
window.removeEventListener('maxTicks', handleMaxTicks);
window.removeEventListener('viewMode', handleChangeViewMode);
};
}, [...allModArrays, ...allEnumArrays, ...allEnumMats, userDefinedWaves, userDefinedFunctions, userDefinedTypes, modCenterVals, render, beatsInMeasure, ticks]);
function CheckLinked(inst, param, checkInstArr, checkParamArr) {
for (let i = 0; i < checkInstArr.length; i++) {
if (checkInstArr[i] == inst && checkParamArr[i] == param)
return true;
}
return false;
}
///////
// Generate Modulators
///////
let modContents = []
for (var i = 0; i < MAXLFOS; i++) {
let id = i;
modContents.push(
e(LfoRow, {
locked: lockMode,
instanceNum: modInstanceNumArr[i],
setInstanceNum: CreateParamChanger(modInstanceNumArr, setModInstanceNumArr, i),
type: modTypeArr[i],
setType: CreateParamChanger(modTypeArr, setModTypeArr, i),
shape: shapeArr[i],
setShape: CreateParamChanger(shapeArr, setShapeArr, i),
noise: noiseTypeArr[i],
setNoise: CreateParamChanger(noiseTypeArr, setNoiseTypeArr, i),
djParam: djParamArr[i],
setDjParam: CreateParamChanger(djParamArr, setDjParamArr, i),
centerVals: modCenterVals,
freq: freqArr[i],
setFreq: CreateParamChanger(freqArr, setFreqArr, i),
//amp: ampArr[i],
//setAmp: CreateParamChanger(ampArr, setAmpArr, i),
min: minArr[i],
setMin: CreateParamChanger(minArr, setMinArr, i),
max: maxArr[i],
setMax: CreateParamChanger(maxArr, setMaxArr, i),
phase: initPhaseArr[i],
setPhase: CreateParamChanger(initPhaseArr, setInitPhaseArr, i),
visible: modVisibleArr[i],
linked: CheckLinked(modInstanceNumArr[i], djParamArr[i], enumInstanceNumArr, enumDjParamArr),
addLfo: () => {
if (id < MAXLFOS - 1) {
if (modVisibleArr[id + 1]) {
let emptyIndex = modVisibleArr.findIndex((item) => !item);
if (emptyIndex != -1) {
for (var j = 0; j < allModArrays.length; j++) {
let array = allModArrays[j];
// remove from all arrays
array.splice(emptyIndex, 1);
// add empty item at opened index
array.splice(id + 1, 0, modBlankVals[j]);
allModSetters[j](array);
}
}
}
else {
for (var j = 0; j < allModArrays.length; j++) { // no space below, easy.
let array = allModArrays[j];
array[id + 1] = modBlankVals[j];
allModSetters[j](array);
}
}
SendSaveEvent();
rerender(!render);
}
},
removeLfo: () => {
if (modVisibleArr.filter(x => x).length > 1) {
let newArr = modVisibleArr.slice();
newArr[id] = false;
setModVisibleArr(newArr);
SendSaveEvent();
}
}
},
null));
}
///////
// Generate Enumerators
///////
let enumContents = []
for (var i = 0; i < MAXLFOS; i++) {
let id = i;
enumContents.push(
e(EnumeratorRow, {
index: i,
locked: lockMode,
instanceNum: enumInstanceNumArr[i],
setInstanceNum: CreateParamChanger(enumInstanceNumArr, setEnumInstanceNumArr, i),
enumItems: enumItemCounts[i],
setEnumItemCounts: CreateParamChanger(enumItemCounts, setEnumItemCounts, i),
enumBreakPoints: enumBreakPoints,
setEnumBreakPoints: (val) => {
setEnumBreakPoints(val);
SendSaveEvent
},
enumNames: enumNames,
setEnumNames: (val) => {
setEnumNames(val);
SendSaveEvent
},
visible: enumVisibleArr[i],
djParam: enumDjParamArr[i],
setDjParam: CreateParamChanger(enumDjParamArr, setEnumDjParamArr, i),
linked: CheckLinked(enumInstanceNumArr[i], enumDjParamArr[i], modInstanceNumArr, djParamArr),
addEnum: () => {
if (id < MAXLFOS - 1) {
if (enumVisibleArr[id + 1]) { // if we need to open up space
let emptyIndex = enumVisibleArr.findIndex((item) => !item);
if (emptyIndex != -1) {
for (var j = 0; j < allEnumArrays.length; j++) {
let array = allEnumArrays[j];
// remove from all arrays
array.splice(emptyIndex, 1);
// add empty item at opened index
array.splice(id + 1, 0, enumBlankVals[j]);
allEnumArrSetters[j](array);
}
// Now do the same with matrices
for (var j = 0; j < allEnumMats.length; j++) {
let mat = allEnumMats[j];
mat.splice(emptyIndex, 1);
mat.splice(id + 1, 0, 0);
mat[id + 1] = allGetEnumMatBlankVals[j]();
allEnumMatSetters[j](mat);
}
}
}
else {
for (var j = 0; j < allEnumArrays.length; j++) {
let array = allEnumArrays[j];
array[id + 1] = enumBlankVals[j];
allEnumArrSetters[j](array);
}
// Now do the same with matrices
for (var j = 0; j < allEnumMats.length; j++) {
let mat = allEnumMats[j];
mat[id + 1] = allGetEnumMatBlankVals[j]();
allEnumMatSetters[j](mat);
}
}
rerender(!render);
SendSaveEvent();
}
},
removeEnum: () => {
if (enumVisibleArr.filter(x => x).length > 1) {
let newArr = enumVisibleArr.slice();
newArr[id] = false;
setEnumVisibleArr(newArr);
SendSaveEvent();
}
}
}, null)
);
}
var grid;
var modButtonClass;
var enumButtonClass;
var labels;
if (viewMode === ViewModes.MOD) {
grid = modContents;
modButtonClass = "highlighted-button";
enumButtonClass = "unhighlighted-button";
labels = MODULATORLABELS;
}
else {
grid = enumContents;
modButtonClass = "unhighlighted-button";
enumButtonClass = "highlighted-button";
labels = ENUMERATORLABELS;
}
let lockClass;
if (lockMode == LockModes.LOCK) {
lockClass = 'lock';
}
else {
lockClass = 'lock unlocked';
}
return e('div', null,
e('div', { className: 'header' },
e('div', {className: 'nav'},
displayIfEnabled(e('button', { onClick: () => setViewMode(ViewModes.MOD), className: modButtonClass}, 'Modulators')),
displayIfEnabled(e('button', { onClick: () => setViewMode(ViewModes.ENUM), className: enumButtonClass }, 'Enumerators'))
),
e('button', { onClick: toggleEnabled, id: 'hide-button'}, toggleEnabledText),
//allows lock mode
//e('span', { className: lockClass, onClick: toggleLockMode }, null)
),
displayIfEnabled(
e('table', { id: 'table' },
e('thead', null, e('tr', { id: 'headers' }, ...labels.map(x => e('th', {id: x == '# points' ? 'points' : x}, x)))),
e('tbody', null, ...grid)
))
);
}
if (!DEBUG) {
window.max.bindInlet("load", (dictId) => {
window.dispatchEvent(new CustomEvent('loadDict', { 'detail': dictId }));
});
window.max.bindInlet("save", (dictId) => {
window.dispatchEvent(new CustomEvent('saveDict', { 'detail': dictId }));
});
window.max.bindInlet("dumpNN", () => {
window.dispatchEvent(new CustomEvent('dumpNN'));
});
window.max.bindInlet("setNN", (...data) => {
window.dispatchEvent(new CustomEvent('setNN', { 'detail': data }));
});
window.max.bindInlet("param", (inst, paramName, val) => {
window.dispatchEvent(new CustomEvent('param', { 'detail': [inst, paramName, val] }));
});
window.max.bindInlet("timesig", (top, bottom) => {
window.dispatchEvent(new CustomEvent('timesig', { 'detail': [top, bottom] }));
});
window.max.bindInlet("ticks", (val) => {
window.dispatchEvent(new CustomEvent('maxTicks', { 'detail': val }));
});
window.max.bindInlet("userWave", (index, ...points) => {
let data = { points, index };
window.dispatchEvent(new CustomEvent('userWave', { 'detail': data }));
});
window.max.bindInlet("viewMode", (mode) => {
window.dispatchEvent(new CustomEvent('viewMode', { 'detail': parseInt(mode) }));
});
window.max.bindInlet("userFunction", (index, ...points) => {
//list of 101 points between 0-100
let data = { points, index };
window.dispatchEvent(new CustomEvent('userFunction', { 'detail': data }));
});
window.max.bindInlet("userDefinedType", (index, type) => {
let data = { index, type };
window.dispatchEvent(new CustomEvent('userDefinedType', { 'detail': data }));
})
setInterval(() => {
window.dispatchEvent(new CustomEvent('tick'));
}, 200);
}
const root = ReactDOM.createRoot(document.getElementById('lfo-container'));
root.render(e(MasterLfoHandler, null, null));

224
src/modulators.js Normal file
View File

@@ -0,0 +1,224 @@
/////////////////////////
// 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;
}