Skip to content

Commit

Permalink
V1 (#11)
Browse files Browse the repository at this point in the history
* Made all played notes react to ui changes

* Added an actual keyboard and mobile compatibility

* fix deployed code

* messed it up again

* why

* better start and ui

* made fully editable envelopes

* bugfixing and real const nodes

* added export and prepared import

* Imported nodes, modulations missing

* import export DONE!!!!
  • Loading branch information
HyperLan-git committed May 25, 2024
1 parent 65d364c commit 7970a51
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 45 deletions.
185 changes: 173 additions & 12 deletions scripts/FX.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//TODO implement event listeners for clones of main nodes' params
const FX_TYPES = {
"gain": GainNode,
"biquadfilter": BiquadFilterNode,
Expand Down Expand Up @@ -65,6 +64,10 @@ const CONST_EXTERNAL_PARAM = [
//"VELOCITY"
];

function deserializeEnvelope(json) {
return new Envelope(json.attack, json.decay, json.sustain, json.release);
}

class Envelope {
attack;
decay;
Expand Down Expand Up @@ -106,7 +109,18 @@ function uidGen(length) {
return result;
}

const FX_EVENTS = ['paramChange', 'valueChange', 'connect', 'disconnect'];
const FX_EVENTS = ['paramChange', 'valueChange', 'labelChange', 'connect', 'disconnect'];

class AudioNodeLabelChangeEvent extends Event {
fx;
label;

constructor(fx, label) {
super('labelChange', {cancelable: true});
this.fx = fx;
this.label = label;
}
}

class AudioParamChangeEvent extends Event {
fx;
Expand All @@ -124,6 +138,27 @@ class AudioParamChangeEvent extends Event {
}
}

class AudioValueChangeEvent extends Event {
fx;
value;
newValue;

constructor(fx, value, newValue) {
super('valueChange', {cancelable: true});

this.fx = fx;
this.value = value;
this.newValue = newValue;
}
}

const labelListener = (e) => {
const list = document.getElementsByClassName("name_" + e.fx.name);
for(let i = 0; i < list.length; i++) {
list.item(i).innerHTML = e.label;
}
};

/**
* Wrapper for every node in the webapi (why do we have no way to access data about connections?)
*/
Expand All @@ -147,9 +182,12 @@ class FX extends EventTarget {
// editable by user
label;

constructor(node) {
constructor(node, updateUILabels = true) {
super();

if(updateUILabels)
this.addEventListener('labelChange', labelListener);

this.inputs = {};
this.inputParams = {};
this.outputs = [];
Expand All @@ -167,7 +205,7 @@ class FX extends EventTarget {
}

setValue(valueName, value) {
if (this.dispatchEvent(new Event('valueChange', {cancelable: true}))) {
if (this.dispatchEvent(new AudioValueChangeEvent(this, valueName, value))) {
this.node[valueName] = value;
return true;
}
Expand All @@ -186,19 +224,39 @@ class FX extends EventTarget {
return false;
}

setLabel(label) {
let ev = new AudioNodeLabelChangeEvent(this, label);
if (this.dispatchEvent(ev)) {
this.label = label;
return true;
}
return false;
}

// inputs are {fx, idx, output} where idx is the id of input and output is the other node's output
// outputs are {fx, idx, input} where idx is the id of output and input id the other node's input
connect(fx, output = 0, input = 0) {
if(fx.inputs[this.name + input] != undefined) {
return false;
}
this.outputs.push({fx: fx, idx: output, input: input});
fx.inputs[this.name + input] = {fx: this, idx: input, output: output};

this.node.connect(fx.node, output, input);
return fx;
}

// input params are {fx, param, output} where output is the other node's output
// output params are {fx, param, output} where output is the id of output
connectParam(fx, param, output = 0) {
if(MODULATIONS[fx.fxtype].indexOf(param) === -1) throw new TypeError(param + " is not a param of the fx type " + fx.fxtype + " !");
if(fx.inputParams[this.name + param + output] != undefined) {
return false;
}
this.outputParams.push({fx: fx, param: param, idx: output});
fx.inputParams[this.name + param + output] = {fx: this, param: param, output: output};
this.node.connect(fx.node[param], output);
return true;
}

disconnectParam(fx, param, output = 0) {
Expand Down Expand Up @@ -285,8 +343,8 @@ class FX extends EventTarget {

disconnectInputs() {
for(const k in this.inputParams) {
const v = this.inputs[k];
v["fx"].disconnect(this);
const v = this.inputParams[k];
v["fx"].disconnect(this.node[v.param]);
}
for(const k in this.inputs) {
const v = this.inputs[k];
Expand Down Expand Up @@ -340,7 +398,7 @@ class FX extends EventTarget {
fx.node.channelInterpretation = this.node.channelInterpretation;

for(const v of PARAMS[this.fxtype])
if(MODULATIONS[this.fxtype].indexOf(v) === -1)
if(!MODULATIONS[this.fxtype].includes(v))
fx.node[v] = this.node[v];
else
fx.node[v].value = this.node[v].value; // It is an audioparam
Expand All @@ -364,12 +422,45 @@ class FX extends EventTarget {
}
return fx;
}

serialize() {
const params = {};

for(let p of PARAMS[this.fxtype]) {
if(MODULATIONS[this.fxtype].includes(p))
params[p] = {audioParam: true, value: this.node[p].value};
else
params[p] = {audioParam: false, value: this.node[p]};
}
return {type: this.fxtype, label: this.label, params: params};
}
}

function deserializeFX(AC, json, updateUILabels = true) {
let node = FX_TYPES[json.type];
if(node === undefined) throw new Error("Invalid node type !");

node = new node(AC);
const res = new FX(node, updateUILabels);

res.label = json.label;
for(let k in json.params) {
const val = json.params[k];
if(val["audioParam"] === true) {
res.node[k].value = val.value;
} else if(json.type == "constant" && k == "data" && json.params["type"]["value"] === "ENVELOPE") {
res.node[k] = new Envelope(val.value.attack, val.value.decay, val.value.sustain, val.value.release);
} else {
res.node[k] = val.value;
}
}
return res;
}


// Copies a list of fx and their connections, returns a dict with keys equal to the corresponding fx' uid
// This is useful for the monophonic fx graph
function copyFxs(...fxs) {
// TODO event system for params
const res = {};
if(fxs.length == 0) return res;
if(fxs.length == 1 && fxs[0].constructor === Array) fxs = fxs[0];
Expand Down Expand Up @@ -436,7 +527,7 @@ class FXGraph {
if(this.defaultNodes[k].gid === e) {
this.defaultNodes[k].gid =
this.drawflow.addNode(this.defaultNodes[k].name, this.defaultNodes[k].node.numberOfInputs, this.defaultNodes[k].node.numberOfOutputs,
300, 200, "", {node: this.defaultNodes[k].name}, "<span id='graph_" + this.defaultNodes[k].name + "'>" + this.defaultNodes[k].label, false);
300, 200, "", {node: this.defaultNodes[k].name}, "<span class='name_" + this.defaultNodes[k].name + "'>" + this.defaultNodes[k].label, false);
this.defaultNodes[k].disconnectInputs();
this.defaultNodes[k].disconnect();
return;
Expand Down Expand Up @@ -466,7 +557,8 @@ class FXGraph {
let y = 0, x = 0;
for(let k in this.defaultNodes) {
this.defaultNodes[k].gid = this.drawflow.addNode(this.defaultNodes[k].name, this.defaultNodes[k].node.numberOfInputs, this.defaultNodes[k].node.numberOfOutputs,
x * 300, y * 100, "", {node: this.defaultNodes[k].name}, "<span id='graph_" + this.defaultNodes[k].name + "'>" + this.defaultNodes[k].label + "</span>", false);
x * 300, y * 100, "", {node: this.defaultNodes[k].name}, "<span class='name_" + this.defaultNodes[k].name + "'>" + this.defaultNodes[k].label + "</span>", false);

this.nodes[this.defaultNodes[k].name] = this.defaultNodes[k];
x++;
if(x > 1) {
Expand Down Expand Up @@ -519,19 +611,88 @@ class FXGraph {
const fx = new FX(node);
this.nodes[fx.name] = fx;
fx.gid = this.drawflow.addNode(fx.name, node.numberOfInputs, node.numberOfOutputs, 0, 200, "", {node: fx.name},
"<span id='graph_" + fx.name + "'>" + fx.label + "</span>", false);
"<span class='name_" + fx.name + "'>" + fx.label + "</span>", false);
return fx.gid;
}

deleteNode(name) {
if(!(name in nodes)) return false;
if(!(name in this.nodes)) return false;
const fx = this.nodes[name];
fx.disconnectInputs();
fx.disconnectAll();
fx.removeEventListener('labelChange', fx.graphListener);
delete fx.graphListener;
delete this.nodes[name];
return true;
}

getAllNodes() {
return valuesOf(this.nodes);
}

serialize() {
const nodes = {};
for(let k in this.nodes) nodes[k] = this.nodes[k].serialize();
const pos = {};
for(let k in this.nodes) {
const node = this.drawflow.getNodeFromId(this.nodes[k].gid);
pos[k] = {x: node.pos_x, y: node.pos_y};
}
const connections = {};
for(let k in this.nodes) {
for(let k2 in this.nodes[k].inputs) {
const v = this.nodes[k].inputs[k2];
if(nodes[v.fx.name] == undefined) continue;
connections[this.nodes[k].name + ":" + v.fx.name] = {in: this.nodes[k].name, out: v.fx.name, input: v.idx, output: v.output};
}
for(let v of this.nodes[k].outputs) {
if(nodes[v.fx.name] == undefined) continue;
connections[v.fx.name + ":" + this.nodes[k].name] = {in: v.fx.name, out: this.nodes[k].name, input: v.input, output: v.idx};
}
}
const defaults = [];
for(let k in this.defaultNodes) {
defaults.push({label:k, name: this.defaultNodes[k].name});
}
return {nodes: nodes, pos: pos, connections: connections, output: this.outputNode.name, defaultNodes: defaults};
}

deserialize(json) {
for(let n in this.nodes) {
if(n != this.outputNode.name) {
this.drawflow.removeNodeId("node-" + this.nodes[n].gid);
this.deleteNode(n);
}
}
this.outputNode.disconnectAll();
const nodes = {};
for(let k in json.nodes) {
if(k == json.output) continue;
nodes[k] = deserializeFX(AC, json.nodes[k]);

nodes[k].gid = this.drawflow.addNode(nodes[k].name, nodes[k].node.numberOfInputs, nodes[k].node.numberOfOutputs,
json.pos[k].x, json.pos[k].y, "", {node: nodes[k].name}, "<span class='name_" + nodes[k].name + "'>" + nodes[k].label + "</span>", false);

this.nodes[nodes[k].name] = nodes[k];
}

for(let k in json.connections) {
const con = json.connections[k];
if(con.in == json.output) {
if(nodes[con.out] == undefined) continue;
nodes[con.out].connect(this.outputNode, con.output, con.input);
this.drawflow.addConnection(nodes[con.out].gid, this.outputNode.gid, 'output_' + String(con.output + 1), 'input_' + String(con.input + 1));
} else {
if(nodes[con.out] == undefined || nodes[con.in] == undefined) continue;
nodes[con.out].connect(nodes[con.in], con.output, con.input);
this.drawflow.addConnection(nodes[con.out].gid, nodes[con.in].gid, 'output_' + String(con.output + 1), 'input_' + String(con.input + 1));
}
}

this.defaultNodes = {};
for(let v of json.defaultNodes) {
this.defaultNodes[v.label] = nodes[v.name];
}
return nodes;
}
};
15 changes: 14 additions & 1 deletion scripts/FXUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,20 @@ function updateConstant(name) {
if(fx == null) return;
const newtype = get("consttype_" + name).value;
if(newtype != fx.node.type) {
get("constdata_" + name).innerHTML = drawConstantData(name, fx.node.type, fx);
switch(newtype) {
case "CONSTANT":
fx.node.data = null;
break;
case "EXT_PARAM":
if(!(fx.node.data instanceof String))
fx.node.data = "FREQUENCY";
break;
case "ENVELOPE":
if(!(fx.node.data instanceof Envelope))
fx.node.data = new Envelope(10, 0, 1, 25);
break;
}
get("constdata_" + name).innerHTML = drawConstantData(name, newtype, fx);
fx.node.type = newtype;
return;
}
Expand Down
56 changes: 56 additions & 0 deletions scripts/io.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const VERSION = 1;
const MAGIC = "PLO";

function export_all() {
const data = new Blob(
[MAGIC, String(VERSION), '\n', JSON.stringify({fx: fx.serialize(), mod: serializeModulations()})],
{
type: "application/octet-stream",
}
);
const a = document.createElement('a');
a.download = 'plox.afx';
a.href = window.URL.createObjectURL(data);
a.dataset.downloadurl = [data.type, a.download, a.href].join(':');
a.click();
}

async function import_all() {
const pickerOpts = {
types: [
{
description: "Audio fx exported file",
accept: {
"application/octet-stream": [".afx"],
},
},
],
excludeAcceptAllOption: true,
multiple: false
};

const contents = await window.showOpenFilePicker(pickerOpts).then((handle) => {
return handle[0].getFile().then((file) => file.text());
});

if(!contents.startsWith(MAGIC)) {
return false;
}

closeFx();

const lines = contents.split("\n");
const header = lines[0];
const data = JSON.parse(lines[1]);

switch(header.substring(3)) {
case '1':
const nodes = fx.deserialize(data.fx);
deserializeModulations(nodes, data.mod);
break;
case '2':
default:
return false;
}
return true;
}
Loading

0 comments on commit 7970a51

Please sign in to comment.