aboutsummaryrefslogtreecommitdiff
path: root/site/app/twirl/transform.js
diff options
context:
space:
mode:
Diffstat (limited to 'site/app/twirl/transform.js')
-rw-r--r--site/app/twirl/transform.js1212
1 files changed, 1212 insertions, 0 deletions
diff --git a/site/app/twirl/transform.js b/site/app/twirl/transform.js
new file mode 100644
index 0000000..1feff51
--- /dev/null
+++ b/site/app/twirl/transform.js
@@ -0,0 +1,1212 @@
+twirl.transform = {};
+twirl.transform.Parameter = function(options) {
+ var self = this;
+ var instr = options.instrument;
+ var tDefinition = options.definition;
+ var parent = options.parent;
+ var transform = options.transform;
+ var host = options.host;
+ var onChange;
+ var refreshable = false;
+ var changeFunc;
+ var initval = true;
+ var definition = {};
+ var randomiseAllowed = true;
+ var visible = true;
+ var uniqueTransformID = (options.transform) ? options.transform.uniqueID : "";
+ if (parent) {
+ Object.assign(definition, tDefinition);
+ } else {
+ definition = tDefinition;
+ }
+
+ if (options.onChange || definition.onChange) {
+ onChange = function(val) {
+ if (options.onChange) options.onChange(val);
+ if (definition.onChange) definition.onChange(val);
+ }
+ }
+
+ if (definition.hasOwnProperty("preset")) {
+ var save = {};
+ for (var s of ["dfault", "name", "channel", "automatable", "description"]) {
+ if (definition.hasOwnProperty(s)) {
+ save[s] = definition[s];
+ }
+ }
+
+ if (definition.preset == "amp") {
+ Object.assign(definition, {name: "Amplitude", channel: "amp", description: "Amplitude", dfault: 1, min: 0, max: 1});
+ } else if (definition.preset == "pvslock") {
+ Object.assign(definition, {name: "Peak lock", channel: "pvslock", description: "Lock frequencies around peaks", min: 0, max: 1, step: 1, dfault: 0});
+ } else if (definition.preset == "fftsize") {
+ Object.assign(definition, {name: "FFT size", channel: "fftsize", description: "FFT size", options: [256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65535], dfault: 2, asvalue: true, automatable: false, lagHint: -1});
+ } else if (definition.preset == "wave") {
+ Object.assign(definition, {name: "Wave", description: "Wave shape to use", options: ["Sine", "Square", "Saw", "Pulse", "Triangle"], dfault: 0, channel: "wave"});
+ } else if (definition.preset == "wintype") {
+ Object.assign(definition, {name: "Window type", channel: "wintype", description: "Window shape", options: ["Hanning", "Hamming", "Half sine"], dfault: 0, automatable: false});
+
+ } else if (definition.preset == "instanceloop") {
+ Object.assign(definition, {name: "Cross instance loop type", channel: "otlooptype", description: "Loop type of other instance", options: ["None", "Forward", "Backward", "Ping-pong"], dfault: 0});
+
+ } else if (definition.preset == "note") {
+ var notes = {};
+ for (var i = 21; i < 128; i++) {
+ var v = twirl.noteData.data.notes[i];
+ notes[v[0]] = v[1];
+ }
+ Object.assign(definition, {name: "Note", channel: "note", description: "Note to use", options: notes, dfault: 69, automatable: true});
+ } else if (definition.preset == "instance") {
+ var c = (!definition.channel) ? "ot" : definition.channel;
+ initval = false;
+ if (transform) transform.refreshable = true;
+ refreshable = true;
+ Object.assign(definition, {
+ name: "Instance", description: "Other wave to use", channel: "instance",
+ options: options.otherInstanceNamesFunc(),
+ automatable: false
+ });
+ changeFunc = function(index) {
+ var oif = options.instancesFunc();
+ if (!oif[index]) return;
+ var s = oif[index].selected;
+ app.setControlChannel(instr + "_" + "inststart" + uniqueTransformID, s[0]);
+ app.setControlChannel(instr + "_" + "instend" + uniqueTransformID, s[1]);
+ app.setControlChannel(instr + "_" + "instchan" + uniqueTransformID, s[2]);
+ };
+ }
+ if (save) {
+ Object.assign(definition, save);
+ }
+ } // if preset
+
+ if (definition.channel == "applymode" || definition.noRandomisation) {
+ randomiseAllowed = false;
+ }
+
+ var type;
+
+ if (definition.hasOwnProperty("conditions") && !parent) {
+ refreshable = true;
+ if (transform) transform.refreshable = refreshable;
+ }
+
+ var channel = "";
+ if (!definition.hasOwnProperty("absolutechannel")) {
+ channel = (parent) ? parent.sendChannel : instr + "_";
+ }
+
+ if (definition.hasOwnProperty("channel")) {
+ channel += definition.channel;
+ } else {
+ channel += definition.name.toLowerCase();
+ }
+
+ this.sendChannel = channel;
+ if (!parent) {
+ this.sendChannel += uniqueTransformID;
+ }
+ var elContainer = $("<div />");
+ var elValueLabel = $("<div />");
+ var elValueInput;
+ var elModulations;
+ var elInput;
+ var elRow;
+ var elModSelect;
+ var automation = [];
+
+ this.definition = definition;
+ this.modulation = null;
+ this.automation = null;
+ this.channel = channel;
+ this.modulationParameters = null;
+
+ this.setPlaying = async function(state) {
+ if (definition.automatable || definition.hidden) return;
+
+ if (definition.disableOnPlay) {
+ if (elValueInput) {
+ elValueInput.prop("disabled", state);
+ elValueInput.css("opacity", (state) ? 0.8 : 1);
+ }
+
+ if (elInput) {
+ elInput.prop("disabled", state);
+ elInput.css("opacity", (state) ? 0.8 : 1);
+ }
+ } else {
+ if (state) {
+ var text = "Changes will be applied upon next run";
+ elContainer.on("mouseover", function(event){
+ twirl.tooltip.show(event, text);
+ }).on("mouseout", function(){
+ twirl.tooltip.hide();
+ });
+ } else {
+ elContainer.off("mouseover").off("mouseout");
+ }
+ }
+ };
+
+
+ if (!definition.hasOwnProperty("hidden")) {
+ definition.hidden = false;
+ }
+
+ if (!definition.step) {
+ definition.step = 0.0000001;
+ }
+
+ if (definition.min == null) {
+ definition.min = 0;
+ }
+
+ if (definition.max == null) {
+ definition.max = 1;
+ }
+
+ if (!definition.hasOwnProperty("fireChanges")) {
+ definition.fireChanges = true;
+ }
+
+ if (definition.dfault == null) {
+ definition.dfault = 1;
+ }
+
+ if (parent) {
+ if (definition.hostrange) {
+ var items = ["step", "min", "max", "options", "conditions", "hostrange"];
+ if (definition.dfault == "hostrangemin") {
+ definition.dfault = parent.definition.min;
+ } else if (definition.dfault == "hostrangemax") {
+ definition.dfault = parent.definition.max;
+ } else {
+ items.push("dfault");
+ }
+ for (let o of items) {
+ if (parent.definition.hasOwnProperty(o)) {
+ definition[o] = parent.definition[o];
+ }
+ }
+ } else if (definition.preset == "hostrangemin") {
+ definition.min = definition.max = definition.dfault = parent.definition.min;
+ } else if (definition.preset == "hostrangemax") {
+ definition.min = definition.max = definition.dfault = parent.definition.max;
+ }
+ }
+
+ if (definition.options) {
+ type = "select";
+ definition.min = 0;
+ definition.max = definition.options.length - 1;
+ definition.step = 1;
+ } else if (definition.hasOwnProperty("type")) {
+ type = definition.type;
+ } else if (definition.min == 0 && definition.max == 1 && definition.step == 1) {
+ type = "checkbox";
+ } else {
+ type = "range";
+ }
+
+ if (!definition.hasOwnProperty("automatable")) {
+ definition.automatable = ((type == "range" || type == "checkbox") && !parent);
+ }
+
+ this.getLagHint = function() {
+ if (!definition.lagHint || !visible) return;
+ var lagHint;
+ if (typeof(definition.lagHint) == "object") {
+ lagHint = "setting <i>" + definition.name + "</i> to <i>"
+ + definition.options[definition.lagHint.option] + "</i>";
+ } else {
+ lagHint = ((definition.lagHint < 0) ? "reducing" : "increasing")
+ + " <i>" + definition.name + "</i>";
+ }
+ return lagHint;
+ };
+
+ this.setRawValue = function(val) {
+ if (type == "checkbox") {
+ elInput[0].checked = (val == 0) ? false : true;
+ } else {
+ elInput.val(val);
+ }
+ elInput.trigger("change");
+ }
+
+ this.getRawValue = function() {
+ return elInput.val();
+ }
+
+ this.getValue = function() {
+ var val;
+ if (type == "range" || type == "string") {
+ val = elInput.val();
+ } else if (type == "select") {
+ val = (definition.asvalue) ? elInput.find("option:selected").text() : elInput.val();
+ } else if (type == "checkbox") {
+ val = (elInput[0].checked) ? 1 : 0;
+ }
+ return val;
+ };
+
+ this.reset = function() {
+ self.setRawValue(definition.dfault);
+ if (!options.unmanagedAutomation) {
+ if (automationActive) disableAutomation();
+ if (self.automation) {
+ delete self.automation;
+ self.automation = null;
+ }
+ if (elSpline) {
+ elSpline.remove();
+ delete elSpline;
+ }
+ }
+ if (modulationShown && !options.unmanagedModulation) {
+ hideModulations();
+ }
+ };
+
+ this.randomise = function() {
+ if (!randomiseAllowed) return;
+ var val;
+ if (!options.unmanagedModulation && definition.automatable) {
+ if (Math.random() >= 0.5) {
+ modButton.el.click();
+ }
+ }
+
+ if (type == "select") {
+ val = Math.round(Math.random() * (definition.options.length - 1));
+ } else if (type == "range") {
+ val = (Math.random() * (definition.max - definition.min)) + definition.min;
+ if (definition.step == 1) {
+ val = Math.round(val);
+ } else {
+ val = Math.ceil((val - definition.min) / definition.step) * definition.step + definition.min;
+ }
+ } else if (type = "checkbox") {
+ val = (Math.round(Math.random()));
+ }
+ self.setRawValue(val);
+
+ if (self.modulationParameters && !options.unmanagedModulation) {
+ // 4 = just the non-crossadaptive ones
+ elModSelect.val(Math.round(Math.random() * 4)).trigger("change");
+ for (let mp in self.modulationParameters) {
+ self.modulationParameters[mp].randomise();
+ }
+ }
+ };
+
+
+ this.refresh = function() {
+ if (!refreshable || !transform) {
+ return;
+ }
+ if (definition.preset == "instance") {
+ createSelectOptions(elInput, options.otherInstanceNamesFunc(), true);
+ }
+ for (var k in definition.conditions) {
+ var c = definition.conditions[k];
+ var chan = (c.absolutechannel) ? c.channel : transform.instr + "_" + c.channel;
+ var val = transform.parameters[chan].getValue();
+ if (
+ (c.operator == "eq" && val != c.value) ||
+ (c.operator == "neq" && val == c.value) ||
+ (c.operator == "lt" && val >= c.value) ||
+ (c.operator == "gt" && val <= c.value) ||
+ (c.operator == "le" && val > c.value) ||
+ (c.operator == "ge" && val < c.value)
+ ) {
+ visible = false;
+ app.setControlChannel(self.sendChannel, definition.dfault);
+ return elRow.hide();
+ }
+ }
+ // app.setControlChannel(self.sendChannel, self.getValue());
+ visible = true;
+ elRow.show();
+ };
+
+ function createSelectOptions(elSelect, options, sendValue) {
+ var selected = elInput.val();
+ elSelect.empty();
+ var applied;
+ var firstOption;
+ for (var x in options) {
+ if (!firstOption) firstOption = x;
+ var opt = $("<option />").text(options[x]).val(x).appendTo(elSelect);
+ if (x == selected) {
+ opt.attr("selected", "1");
+ if (changeFunc) changeFunc(x);
+ applied = true;
+ }
+ }
+ if (!applied) {
+ elInput.val(firstOption);
+ if (changeFunc) changeFunc(firstOption);
+ }
+ if (sendValue) {
+ app.setControlChannel(self.sendChannel, self.getValue());
+ }
+ definition.min = 0;
+ definition.max = (Array.isArray(options)) ? options.length - 1 : Object.keys(options).length - 1;
+ }
+
+ function updateLabel() {
+ if (elValueInput) {
+ var val = self.getValue();
+ updateinput = false;
+ rounding = 10000;
+ val = Math.round(val * rounding) / rounding;
+ elValueInput.val(val);
+ updateinput = true;
+ }
+ }
+
+ if (type == "select") {
+ elInput = $("<select />");
+ elInput.change(function(){
+ var val = self.getValue();
+ if (transform) transform.refresh();
+ if (definition.fireChanges) {
+ if (changeFunc) changeFunc(val);
+ if (!host.offline) app.setControlChannel(self.sendChannel, val);
+ }
+ if (onChange) {
+ onChange(val);
+ }
+ });
+
+ var selectOptions = (definition.hostrange && parent) ? parent.definitions.options : definition.options;
+ createSelectOptions(elInput, selectOptions);
+
+ } else if (type == "string") {
+ elInput = $("<input />").change(function() {
+ if (transform) transform.refresh();
+ var val = self.getValue();
+ if (definition.fireChanges) {
+ app.setStringChannel(self.sendChannel, val);
+ }
+ if (onChange) {
+ onChange(val);
+ }
+ });
+
+ } else if (type == "checkbox") {
+ elInput = $("<input />").addClass("twirl_checkbox").attr("type", "checkbox").on("change", function() {
+ if (transform) transform.refresh();
+ var val = self.getValue();
+ if (definition.fireChanges) {
+ app.setControlChannel(self.sendChannel, val);
+ }
+ if (onChange) {
+ onChange(val);
+ }
+ });
+ } else if (type == "range") {
+ var updateinput = true;
+ var max = definition.max;
+ var min = definition.min;
+ var step = definition.step;
+ var dfault = definition.dfault;
+
+ elInput = $("<input />").addClass("twirl_slider").attr("type", "range").on("input", function() {
+ updateLabel();
+ if (definition.fireChanges) {
+ app.setControlChannel(self.sendChannel, self.getValue());
+ }
+ }).change(function() {
+ updateLabel();
+ if (transform) transform.refresh();
+ var val = self.getValue();
+ if (definition.fireChanges && !host.offline) {
+ app.setControlChannel(self.sendChannel, val);
+ }
+ if (onChange) {
+ onChange(val);
+ }
+ }).attr("min", min).attr("max", max).attr("step", step).val(dfault);
+
+ elValueInput = $("<input />").attr("type", "number").attr("min", min).attr("max", max).attr("step", step).addClass("twirl_transparentinput").appendTo(elValueLabel).change(function() {
+ if (updateinput) {
+ elInput.val($(this).val()).trigger("change").trigger("input");
+ }
+ });
+ }
+ /*
+ elInput.on("contextmenu", function(e){
+ var items = [{name: "Reset", click: function(){
+ self.reset();
+ }}];
+ if (definition.automatable) {
+ items.push({
+ name: "Automate",
+ click: function(){
+ if (!options.unmanagedAutomation) {
+ transform.showAutomation(definition.name, elSpline);
+ }
+ }
+ });
+ }
+
+ items.push({
+ name: "Randomise",
+ click: function(){
+ self.randomise();
+ }
+ });
+
+ items.push({
+ name: ((randomiseAllowed) ? "Exclude from" : "Include in") + " randomisation",
+ click: randomiseButton.click
+ });
+
+ twirl.contextMenu.show(e, items);
+ });*/
+
+ elContainer.append(elInput);
+ if (initval) {
+ self.setRawValue(definition.dfault);
+ if (definition.fireChanges) {
+ elInput.trigger("change");
+ }
+ }
+
+
+ this.setDefault = function() {
+ elInput.val(definition.dfault).trigger("change");
+ //app.setControlChannel(sendChannel, definition.dfault);
+ };
+
+ this.remove = function() {
+ disableAutomation();
+ elRow.remove();
+ if (elSpline) {
+ elSpline.remove();
+ }
+ if (self.modulation) {
+ self.modulation = null;
+ }
+
+ if (self.automation) {
+ self.automation = null;
+ }
+ };
+
+ this.getAutomationData = function(start, end) {
+ if (self.modulation) {
+ var m = twirl.appdata.modulations[self.modulation];
+ return {type: "modulation", data: [m.instr, self.sendChannel]};
+ } else if (automationActive && self.automation) {
+ return {type: "automation", channel: self.sendChannel, data: self.automation.getLinsegData(start, end, options.getRegionFunc())};
+ }
+ };
+
+ var resetButton = twirl.createIcon({
+ label: "Reset parameter",
+ icon: "reset",
+ click: function() {
+ self.reset();
+ }
+ });
+
+ var randomiseButton = twirl.createIcon({
+ label: "Include in randomisation",
+ icon: "randomise",
+ click: function(obj) {
+ randomiseAllowed = !randomiseAllowed;
+ var opacity = (randomiseAllowed) ? 1 : 0.4;
+ obj.el.css("opacity", opacity);
+ }
+ });
+ if (!randomiseAllowed) {
+ randomiseButton.el.css("opacity", 0.4);
+ }
+
+ var elSpline;
+ var editAutomationButton = twirl.createIcon({
+ label: "Select automation",
+ icon: "show",
+ click: function() {
+ if (!transform) return;
+ if (elSpline) {
+ automationShown = true;
+ transform.showAutomation(definition.name, elSpline);
+ }
+ }
+ });
+ editAutomationButton.el.hide();
+
+ var automationButton = twirl.createIcon({
+ label: "Automate",
+ label2: "Close automation",
+ icon: "automate",
+ icon2: "close",
+ click: function() {
+ if (elSpline && automationActive) {
+ disableAutomation();
+ if (options.onAutomationClick) options.onAutomationClick(false);
+ } else {
+ showAutomation();
+ if (options.onAutomationClick) options.onAutomationClick(true);
+ }
+ }
+ });
+
+ var automationActive = false;
+ var automationShown = false;
+
+ this.hideAutomation = function() {
+ if (!transform) return;
+ automationShown = false;
+ if (elSpline) {
+ transform.hideAutomation(definition.name);
+ }
+ }
+
+ function disableAutomation() {
+ if (!transform) return;
+ automationActive = false;
+ automationShown = false;
+ if (!host.offline) app.setControlChannel(self.sendChannel, self.getValue());
+ elValueLabel.show();
+ elInput.show();
+ modButton.el.show();
+ automationButton.setState(true);
+ editAutomationButton.el.hide();
+ self.hideAutomation();
+ }
+
+ this.redraw = function(region) {
+ if (self.automation && !options.unmanagedAutomation) {
+ if (region && region[0] != null && region[1] != null) {
+ self.automation.setRange(region[0], region[1]);
+ } else {
+ self.automation.redraw();
+ }
+ }
+ };
+
+ this.createAutomationSpline = function(elTarget, colour) {
+ if (!colour) colour = twirl.random.rgbColour();
+ if (!self.automation) {
+ self.automation = new SplineEdit(
+ elTarget, colour,
+ options.getDurationFunc,
+ [definition.min, definition.max, self.getValue(), definition.step],
+ definition.name
+ );
+ }
+ };
+
+ function showAutomation() {
+ if (!transform) return;
+ var colour = twirl.random.rgbColour();
+ automationShown = true;
+ automationActive = true;
+
+ if (!elSpline) {
+ elSpline = $("<div />").attr("id", "spl_" + channel).css({
+ position: "absolute", width: "100%", height: "100%", overflow: "hidden"
+ });
+ }
+
+ transform.showAutomation(definition.name, elSpline);
+ self.createAutomationSpline(elSpline, colour);
+
+
+ elValueLabel.hide();
+ elInput.hide();
+ modButton.el.hide();
+ elSpline.show();
+ editAutomationButton.el.show(); //.css("background-color", colour);
+ automationButton.setState(false);
+ }
+
+
+ elModulations = $("<div />").addClass("twirl_tf_container").hide().appendTo(elContainer);
+ var modulationShown = false;
+
+
+ var modButton = twirl.createIcon({
+ label: "Modulate",
+ label2: "Close modulation",
+ icon: "modulate",
+ icon2: "close",
+ click: function() {
+ if (elModulations && modulationShown) {
+ hideModulations();
+ } else {
+ showModulations();
+ }
+ }
+ });
+
+ function hideModulations() {
+ app.setControlChannel(self.sendChannel, self.getValue());
+ modulationShown = false;
+ elValueLabel.show();
+ elInput.show();
+ automationButton.el.show();
+ self.modulation = null;
+ modButton.setState(true);
+ if (elModulations) {
+ elModulations.hide();
+ }
+ }
+
+ function showModulations() {
+ if (!transform) return;
+ modulationShown = true;
+ elValueLabel.hide();
+ elInput.hide();
+ automationButton.el.hide();
+ elModulations.show();
+ modButton.setState(false);
+ if (elModulations.children().length != 0) {
+ elModSelect.val(0).trigger("change");
+ return;
+ }
+ var tb = $("<tbody />");
+ function buildModulation(i) {
+ tb.empty();
+ self.modulationParameters = {};
+ self.modulation = i;
+ let m = twirl.appdata.modulations[i];
+ for (let x of m.parameters) {
+ var tp = new twirl.transform.Parameter({
+ instrument: m.instr,
+ definition: x,
+ transform: transform,
+ parent: self,
+ onAutomationClick: options.onAutomationClick,
+ getDurationFunc: options.getDurationFunc,
+ getRegionFunc: options.getRegionFunc,
+ otherInstanceNamesFunc: options.otherInstanceNamesFunc,
+ instancesFunc: options.instancesFunc,
+ host: options.host
+ });
+ self.modulationParameters[tp.channel] = tp;
+ tb.append(tp.getElementRow(true)); // hmm modulate the modulation with false
+ }
+ }
+ var selecttb = $("<tbody />").appendTo($("<table />)").appendTo(elModulations));
+ var row = $("<tr />").append($("<td />").addClass("twirl_tf_cell_text").text("Modulation type")).appendTo(selecttb);
+ var elConditionalOptions = [];
+
+ if (host.onInstanceChangeds) {
+ host.onInstanceChangeds.push(function(){
+
+ for (let o of elConditionalOptions) {
+ if (options.instancesFunc().length == 1) {
+ o.prop("disabled", true);
+ } else {
+ o.prop("disabled", false);
+ }
+ }
+ });
+ }
+
+ elModSelect = $("<select />").change(function() {
+ self.modulation = $(this).val();
+ buildModulation(self.modulation);
+ }).appendTo($("<td />").appendTo(row));
+ $("<table />").append(tb).appendTo(elModulations);
+
+ for (let i in twirl.appdata.modulations) {
+ var m = twirl.appdata.modulations[i];
+ var o = $("<option />").text(m.name).val(i).appendTo(elModSelect);
+ if (m.inputs > 1) {
+ elConditionalOptions.push(o);
+ if (!options.instancesFunc || options.instancesFunc().length == 1) {
+ o.prop("disabled", true);
+ }
+ }
+ }
+ elModSelect.val(0).trigger("change");
+ }
+
+ this.getElementRow = function(nocontrols) {
+ if (definition.hidden) {
+ return null;
+ };
+ if (elRow) {
+ return elRow;
+ }
+ elRow = $("<tr />");
+ var name = $("<td />").addClass("twirl_tf_cell_text").text(definition.name).appendTo(elRow);
+ if (definition.description) {
+ name.on("mouseover", function(event){
+ twirl.tooltip.show(event, definition.description);
+ }).on("mouseout", function(){
+ twirl.tooltip.hide();
+ });
+ }
+
+ $("<td />").addClass("twirl_tf_cell").append(elContainer).appendTo(elRow);
+ $("<td />").addClass("twirl_tf_cellfixed").append(elValueLabel).appendTo(elRow);
+ if (!nocontrols) {
+ for (let b of [resetButton, randomiseButton]) $("<td />").addClass("twirl_tf_cell_plainbg").append(b.el).appendTo(elRow);
+
+ if (definition.automatable) {
+ var items = [];
+ if (!options.unmanagedAutomation) {
+ items.push(automationButton);
+ items.push(editAutomationButton);
+ }
+ if (!options.unmanagedModulation) {
+ items.push(modButton);
+ }
+ for (let b of items) $("<td />").addClass("twirl_tf_cell_plainbg").append(b.el).appendTo(elRow);
+ }
+
+ }
+ return elRow;
+ };
+};
+
+
+
+twirl.transform.Transform = function(options) {
+ var self = this;
+ var elTarget = options.element;
+ var def = options.definition;
+ var host = options.host;
+ var elTb;
+ var pAddOdd = true;
+ this.path = (options.path) ? options.path : def.name;
+ this.instr = def.instr;
+ this.name = def.name;
+ this.refreshable = false;
+ var elSplineOverlay;
+ var hideAutomationButton;
+ this.parameters = {};
+ this.uniqueID = 0;
+
+ if (options.uniqueID) {
+ this.uniqueID = options.uniqueID;
+ }
+
+ var automationEls = {};
+ this.showAutomation = function(name, el) {
+ if (!elSplineOverlay) {
+ elSplineOverlay = $("<div />").addClass("twirl_spline_overlay").appendTo(options.splineElement);
+ }
+ for (var e in automationEls) {
+ automationEls[e].css({"z-index": 23, opacity: 0.4});
+ }
+ if (!el) {
+ el = automationEls[name];
+ } else {
+ automationEls[name] = el;
+ }
+ el.css({"z-index": 24, opacity: 1}).show();
+ hideAutomationButton.el.show();
+ elSplineOverlay.show();
+ if (el.parents(elSplineOverlay).length == 0) {
+ elSplineOverlay.append(el);
+ }
+ options.splineElement.show();
+ if (options.onShowAutomation) {
+ options.onShowAutomation();
+ }
+ };
+
+ this.getLagHints = function() {
+ var lagHints = [];
+ for (let i in self.parameters) {
+ var p = self.parameters[i];
+ var lagHint = p.getLagHint();
+ if (lagHint) lagHints.push(lagHint);
+ }
+ var lagHintHtml;
+ if (lagHints.length != 0) {
+ lagHintHtml = "Try ";
+ for (var i in lagHints) {
+ lagHintHtml += lagHints[i];
+ if (i != lagHints.length - 1) {
+ lagHintHtml += ((i == lagHints.length - 2) ? " or " : ", ");
+ }
+ }
+ }
+ return lagHintHtml;
+ };
+
+ this.hideAutomation = function(name) {
+ if (automationEls[name]) {
+ automationEls[name].hide();
+ delete automationEls[name];
+ if (Object.keys(automationEls).length == 0) {
+ elSplineOverlay.hide();
+ hideAutomationButton.el.hide();
+ options.splineElement.hide();
+ if (options.onHideAllAutomation) {
+ options.onHideAllAutomation();
+ }
+ }
+ }
+ }
+
+ this.hideAllAutomation = function(name) {
+ for (let p in self.parameters) {
+ self.parameters[p].hideAutomation();
+ }
+ };
+
+ this.redraw = function(region) {
+ for (let p in self.parameters) {
+ self.parameters[p].redraw(region);
+ }
+ };
+
+ this.refresh = function() {
+ if (!self.refreshable) {
+ return;
+ }
+ for (var k in self.parameters) {
+ self.parameters[k].refresh();
+ }
+ };
+
+ this.getAutomationData = function(start, end) {
+ var automations = [];
+ for (var k in self.parameters) {
+ var data = self.parameters[k].getAutomationData(start, end);
+ if (data) {
+ automations.push(data);
+ }
+ }
+ return automations;
+ };
+
+ this.getState = async function() {
+ var data = {instr: def.instr, name: self.path, channels: {}};
+ var value;
+ for (let chan in self.parameters) {
+ value = await app.getControlChannel(self.parameters[chan].sendChannel);
+ data.channels[chan] = value;
+ if (self.parameters[chan].modulationParameters) {
+ for (let modchan in self.parameters[chan].modulationParameters) {
+ value = await app.getControlChannel(self.parameters[chan].modulationParameters[modchan].sendChannel);
+ data.channels[modchan] = value;
+ }
+ }
+ }
+ return data;
+ };
+
+
+ this.reset = function() {
+ for (let p in self.parameters) {
+ self.parameters[p].reset();
+ }
+ };
+
+ this.randomise = function() {
+ for (let p in self.parameters) {
+ self.parameters[p].randomise();
+ if (!options.unmanagedModulation && self.parameters[p].modulationParameters) {
+ for (let mp in self.parameters[p].modulationParameters) {
+ self.parameters[p].modulationParameters[mp].randomise();
+ }
+ }
+ }
+ };
+
+ this.saveState = function() {
+ if (!options.useStorage || !host.storage) return;
+ var state = {};
+ for (let p in self.parameters) {
+ state[p] = self.parameters[p].getRawValue();
+ }
+ if (!host.storage.transforms) {
+ host.storage.transforms = {};
+ }
+ host.storage.transforms[def.instr] = state;
+ host.saveStorage();
+ };
+
+ this.remove = function() {
+ self.saveState();
+ for (let p in self.parameters) {
+ self.parameters[p].remove();
+ }
+ if (elSplineOverlay) {
+ elSplineOverlay.remove();
+ }
+ }
+
+ this.removeParameter = function(channel) {
+ if (self.parameters.hasOwnProperty(channel)) {
+ self.parameters[channel].remove();
+ delete self.parameters[channel]
+ }
+ };
+
+ function addParameter(pdef) {
+ var tp = new twirl.transform.Parameter({
+ instrument: def.instr,
+ definition: pdef,
+ transform: self,
+ getDurationFunc: options.getDurationFunc,
+ getRegionFunc: options.getRegionFunc,
+ otherInstanceNamesFunc: options.otherInstanceNamesFunc,
+ instancesFunc: options.instancesFunc,
+ unmanagedAutomation: options.unmanagedAutomation,
+ unmanagedModulation: options.unmanagedModulation,
+ host: host
+ });
+ self.parameters[tp.channel] = tp;
+ var er = tp.getElementRow();
+ if (er) {
+ elTb.append(er.addClass("twirl_tf_row_" + ((pAddOdd) ? "odd" : "even")));
+ pAddOdd = !pAddOdd;
+ };
+ };
+
+ this.setPlaying = function(state) {
+ for (let i in self.parameters) {
+ self.parameters[i].setPlaying(state);
+ }
+ };
+
+ function namePrepend(name, pdef) {
+ if (!pdef.hasOwnProperty("nameprepend")) return name;
+ name = pdef.nameprepend + " " + name;
+ return name[0] + name.substr(1).toLowerCase()
+ }
+
+ this.addParameter = function(pdef) {
+ if (!pdef.hasOwnProperty("presetgroup")) {
+ return addParameter(pdef);
+ }
+ var name;
+ var conditions;
+ var groupParameters = [];
+ var channelPrepend = (pdef.hasOwnProperty("channelprepend")) ? pdef.channelprepend : "";
+
+ if (pdef.presetgroup == "pvsynth") {
+ var dfaultMode = (pdef.hasOwnProperty("dfault")) ? pdef.dfault : 0;
+ conditions = [
+ {channel: channelPrepend + "pvresmode", operator: "eq", value: 1}
+ ];
+ groupParameters = [
+ {name: namePrepend("Resynth mode", pdef), channel: channelPrepend + "pvresmode", description: "Type of FFT resynthesis used", dfault: dfaultMode, options: ["Overlap-add", "Additive"], automatable: false},
+ {name: namePrepend("Oscillator spread", pdef), channel: channelPrepend + "pvaoscnum", description: "Number of oscillators used", automatable: false, conditions: conditions, lagHint: -1},
+ {name: namePrepend("Frequency modulation", pdef), channel: channelPrepend + "pvafreqmod", description: "Frequency modulation", dfault: 1, min: 0.01, max: 2, conditions: conditions},
+ {name: namePrepend("Oscillator offset", pdef), channel: channelPrepend + "pvabinoffset", description: "Oscillator bin offset", automatable: false, conditions: conditions, dfault: 0, lagHint: 1},
+ {name: namePrepend("Oscillator increment", pdef), channel: channelPrepend + "pvabinincr", description: "Oscillator bin increment", min: 1, max: 8, dfault: 1, step: 1, automatable: false, conditions: conditions, lagHint: -1}
+ ];
+
+ } else if (pdef.presetgroup == "applymode") {
+ var conditionsMix = [{channel: "applymode", operator: "eq", value: 1, absolutechannel: true}];
+ var conditionsFilter = [{channel: "applymode", operator: "eq", value: 4, absolutechannel: true}];
+ if (pdef.conditions) {
+ for (let c of pdef.conditions) {
+ conditionsMix.push(c);
+ conditionsFilter.push(c);
+ }
+ }
+ groupParameters = [
+ {name: "Apply mode", channel: "applymode", absolutechannel: true, description: "Apply mode", automatable: true, options: ["Replace", "Mix", "Modulate", "Demodulate", "Filter"], dfault: 0, conditions: pdef.conditions},
+ {name: "Dry mix", description: "Original signal amplitude", channel: "applymodedry", absolutechannel: true, conditions: conditionsMix, min: 0, max: 1, dfault: 1},
+ {name: "Wet mix", description: "Transformed signal amplitude", channel: "applymodewet", absolutechannel: true, conditions: conditionsMix, min: 0, max: 1, dfault: 1},
+ {name: "Minimum frequency", description: "Minimum frequency to transform", channel: "applymodedry", absolutechannel: true, conditions: conditionsFilter, min: 20, max: 44100, dfault: 500},
+ {name: "Maximum frequency", description: "Maximum frequency to transform", channel: "applymodedry", absolutechannel: true, conditions: conditionsFilter, min: 20, max: 44100, dfault: 2000}
+ ];
+
+ } else if (pdef.presetgroup == "pvanal") {
+ /* LPC unstable with WASM
+ groupParameters = [
+ {name: "Analysis type", channel: "pvstype", options: ["Overlap-add", "Linear prediction"], dfault: 0, automatable: false},
+ {preset: "fftsize"},
+ {name: "Overlap decimation", options: [2, 4, 8, 16], asvalue: true, dfault: 1, channel: "pvsdecimation", automatable: false, lagHint: -1},
+ {preset: "pvslock"},
+ {name: "Window size multiplier", min: 1, max: 4, dfault: 1, step :1, channel: "pvswinsizem", automatable: false, lagHint: -1, conditions: [{channel: "pvstype", operator: "eq", value: 0}]},
+ {name: "Order multiplier", description: "Linear predictor order (FFT size multiplier)", min: 0.001, max: 1, dfault: 0.25, channel: "pvsorderm", automatable: false, lagHint: -1, conditions: [{channel: "pvstype", operator: "eq", value: 1}]},
+ {name: "Window type", channel: "pvswintype", options: ["Hamming", "Von Hann", "Kaiser"], dfault: 1, automatable: false, conditions: [{channel: "pvstype", operator: "eq", value: 0}]},
+ {name: "Window type", channel: "pvswintypelpc", options: ["Hanning", "Hamming", "Half sine"], dfault: 0, automatable: false, conditions: [{channel: "pvstype", operator: "eq", value: 1}]}
+ ];
+ */
+ groupParameters = [
+ {preset: "fftsize"},
+ {name: "Overlap decimation", options: [2, 4, 8, 16], asvalue: true, dfault: 1, channel: "pvsdecimation", automatable: false, lagHint: -1},
+ {preset: "pvslock"},
+ {name: "Window size multiplier", min: 1, max: 4, dfault: 1, step :1, channel: "pvswinsizem", automatable: false, lagHint: -1},
+ {name: "Window type", channel: "pvswintype", options: ["Hamming", "Von Hann", "Kaiser"], dfault: 1, automatable: false}
+ ];
+ } else if (pdef.presetgroup == "pitchscale") {
+ groupParameters = [
+ {name: namePrepend("Pitch scale mode", pdef), channel: channelPrepend + "pitchscalemode", options: ["Ratio", "Semitone"], dfault: 0},
+ {name: namePrepend("Pitch scale", pdef), channel: channelPrepend + "pitchscale", description: "Pitch scaling", dfault: 1, min: 0.01, max: 10, conditions: [{channel: channelPrepend + "pitchscalemode", operator: "eq", value: 0}]},
+ {name: namePrepend("Semitones", pdef), channel: channelPrepend + "pitchsemitones", min: -24, max: 24, step: 1, dfault: 0, conditions: [{channel: channelPrepend + "pitchscalemode", operator: "eq", value: 1}]}
+ ];
+
+ } else if (pdef.presetgroup == "notefreq") {
+ var base = {name: namePrepend("Frequency mode", pdef), channel: channelPrepend + "freqmode", description: "Frequency mode", options: ["Frequency", "Note"], dfault: 0};
+ if (pdef.hasOwnProperty("conditions")) {
+ base["conditions"] = pdef.conditions;
+ }
+ groupParameters.push(base);
+
+ conditions = [{channel: channelPrepend + "freqmode", operator: "eq", value: 0}];
+ if (pdef.hasOwnProperty("conditions")) {
+ Array.prototype.push.apply(conditions, pdef.conditions);
+ }
+
+ var dfaultFreq = (pdef.hasOwnProperty("dfault")) ? pdef.dfault : 440;
+
+ var freq = {name: namePrepend("Frequency", pdef), channel: channelPrepend + "freq", description: "Frequency", dfault: dfaultFreq, min: 20, max: 22000, conditions: conditions}
+ if (pdef.hasOwnProperty("lagHint")) {
+ freq.lagHint = pdef.lagHint;
+ }
+ groupParameters.push(freq);
+
+ conditions = [{channel: channelPrepend + "freqmode", operator: "eq", value: 1}];
+ if (pdef.hasOwnProperty("conditions")) {
+ Array.prototype.push.apply(conditions, pdef.conditions);
+ }
+ var note = {preset: "note", name: namePrepend("Note", pdef), conditions: conditions, channel: channelPrepend + "note"};
+ if (pdef.hasOwnProperty("lagHint")) {
+ note.lagHint = pdef.lagHint;
+ }
+ groupParameters.push(note);
+
+ }
+ for (let gp of groupParameters) {
+ if (pdef.hasOwnProperty("automatable")) {
+ gp.automatable = pdef.automatable;
+ }
+ addParameter(gp);
+ }
+ }
+
+ function build() {
+ elTarget.empty();
+ var elContainer = $("<div />").addClass("twirl_tf_container").appendTo(elTarget);
+ hideAutomationButton = twirl.createIcon({label: "Hide automation", icon: "hide", click: function() {
+ self.hideAllAutomation();
+ }});
+ hideAutomationButton.el.hide();
+
+ if (!host.offline) {
+ app.setControlChannel("applymode" + self.uniqueID, 0); // not all transforms will set this
+ }
+
+ var header = $("<div />").addClass("twirl_tf_header");
+ if (def.unstable) {
+ $("<div />").css({
+ "background-color": "#aa0000",
+ color: "#ffffff",
+ "font-size": "var(--fontSizeSmall)"
+ }).text("Instabilities have been reported with this transform. It is recommended you save your work before using it.").appendTo(header);
+ }
+ $("<div />").text(def.name).appendTo(header);
+ if (options.onClose) {
+ header.append(twirl.createIcon({
+ label: "Close",
+ icon: "close",
+ click: function() { options.onClose()},
+ size: 20
+ }).el.css("float", "right"));
+ }
+ var el = $("<div />").addClass("twirl_tf_container").append(header).appendTo(elContainer);
+
+ if (def.description) {
+ header.on("mouseover", function(event){
+ twirl.tooltip.show(event, def.description);
+ }).on("mouseout", function(){
+ twirl.tooltip.hide();
+ });
+ }
+
+ $("<div />").css({"float": "right"}).append(
+ hideAutomationButton.el
+ ).append(
+ twirl.createIcon({
+ label: "Randomise parameters",
+ icon: "randomise",
+ click: function() {
+ self.randomise();
+ }
+ }).el
+ ).append(
+ twirl.createIcon({
+ label: "Reset parameters",
+ icon: "reset",
+ click: function() {
+ self.reset();
+ }
+ }).el
+ ).appendTo(el);
+
+
+
+ var tbl = $("<table />").appendTo(elContainer);
+ elTb = $("<tbody />").appendTo(tbl);
+ for (let p of def.parameters) {
+ self.addParameter(p);
+ }
+
+ if (options.useStorage && host.storage && host.storage.transforms && host.storage.transforms[def.instr]) {
+ var state = host.storage.transforms[def.instr];
+ for (var p in state) {
+ self.parameters[p].setRawValue(state[p]);
+ }
+ }
+ self.refresh();
+ }
+ build();
+};
+
+twirl.transform.TreeView = function(options) {
+ var self = this;
+
+ function recurse(items, descended, path) {
+ if (!path) path = "";
+ items = (items) ? items : options.items;
+ var ul = $("<ul />").addClass("twirl_treeview_treelist").addClass((descended) ? "twirl_treeview_nested" : "ttwirl_treeview_reelist");
+
+ for (let k in items) {
+ let name = items[k].name;
+ let thisPath = path + "> " + items[k].name;
+ var li = $("<li />");
+ if (items[k].description) {
+ li.on("mouseover", function(event){
+ twirl.tooltip.show(event, items[k].description);
+ }).on("mouseout", function(){
+ twirl.tooltip.hide();
+ });
+ }
+ if (items[k].hasOwnProperty("contents")) {
+ $("<span />").addClass("twirl_treeview_caret").text(name).click(function() {
+ $(this).parent().children(".twirl_treeview_nested").toggleClass("twirl_treeview_active");
+ $(this).toggleClass("twirl_treeview_caret-down");
+ }).appendTo(li);
+ var subitems = recurse(items[k].contents, true, thisPath);
+ li.append(subitems);
+
+ } else {
+ var content = name;
+ if (items[k].hasOwnProperty("added")) {
+ var dp = items[k].added.split("-");
+ var added = new Date(dp[0], dp[1] - 1, dp[2]);
+ if (Math.round((new Date() - added) / (1000 * 60 * 60 * 24)) <= 14) {
+ x.html(name + " <p style=\"display:inline;color:#ff2222;\">&nbsp;&nbsp;[new]</p>");
+ }
+ }
+ li.html(content).css("cursor", "pointer").click(function() {
+ options.click(items[k], thisPath);
+ });
+ }
+ ul.append(li);
+ }
+ options.element.append(ul);
+ return ul;
+ }
+
+ options.element.append(recurse());
+}; \ No newline at end of file