aboutsummaryrefslogtreecommitdiff
path: root/site/app/twist/_unlive
diff options
context:
space:
mode:
Diffstat (limited to 'site/app/twist/_unlive')
-rw-r--r--site/app/twist/_unlive/apid.js978
-rw-r--r--site/app/twist/_unlive/index.api.html1144
-rw-r--r--site/app/twist/_unlive/splinetest.html42
-rw-r--r--site/app/twist/_unlive/transform.js1024
-rw-r--r--site/app/twist/_unlive/twist_fxtester.csd101
-rw-r--r--site/app/twist/_unlive/twist_instance_WIP.js350
-rw-r--r--site/app/twist/_unlive/twist_instance_separation_WIP.js1250
7 files changed, 4889 insertions, 0 deletions
diff --git a/site/app/twist/_unlive/apid.js b/site/app/twist/_unlive/apid.js
new file mode 100644
index 0000000..28a00f2
--- /dev/null
+++ b/site/app/twist/_unlive/apid.js
@@ -0,0 +1,978 @@
+var twst = {};
+
+
+
+
+twst.Parameter = function(instr, definition, parent, transform, twist) {
+ var self = this;
+ var refreshable = false;
+ var changeFunc;
+ var value;
+ var initval = true;
+ var type;
+ var applicable;
+ var channel = (parent) ? parent.channel : instr + "_";
+ if (definition.hasOwnProperty("channel")) {
+ channel += definition.channel;
+ } else {
+ channel += definition.name.toLowerCase();
+ }
+
+ Object.defineProperty(this, "channel", {
+ get: function() { return channel; },
+ set: function(x) {}
+ });
+
+
+ if (definition.hasOwnProperty("options")) {
+ if (!definition.hasOwnProperty("automatable")) {
+ definition.automatable = false;
+ }
+ }
+
+ if (definition.hasOwnProperty("preset")) {
+ var save = {};
+ if (definition.hasOwnProperty("dfault")) {
+ save.dfault = definition.dfault;
+ }
+
+ if (definition.hasOwnProperty("name")) {
+ save.name = definition.name;
+ }
+
+ if (definition.preset == "fftsize") {
+ Object.assign(definition, {name: "FFT size", channel: "fftsize", description: "FFT size", options: [256, 512, 1024, 2048, 4096], dfault: 1, asvalue: true, automatable: false});
+ } else if (definition.preset == "wave") {
+ Object.assign(definition, {name: "Wave", description: "Wave shape to use", options: ["Sine", "Square", "Saw", "Pulse", "Triangle"], dfault: 0});
+ } else if (definition.preset == "instance") {
+ initval = false;
+ transform.refreshable = true;
+ refreshable = true;
+ Object.assign(definition, {
+ name: "Instance", description: "Other wave to use", channel: "instanceindex",
+ options: twist.otherInstanceNames,
+ automatable: false
+ });
+ changeFunc = function(index) {
+ var s = twist.waveforms[index].selected;
+ app.setControlChannel(instr + "_" + "otinststart", s[0]);
+ app.setControlChannel(instr + "_" + "otinstend", s[1]);
+ app.setControlChannel(instr + "_" + "otiinstchan", s[2]);
+ }
+ }
+ if (save) {
+ Object.assign(definition, save);
+ }
+ } // if preset
+
+
+
+ if (definition.hasOwnProperty("options") || (definition.hostrange && parent.definition.hasOwnProperty("options"))) {
+ type = "select";
+ } else {
+ type = "range";
+ }
+
+
+ if (definition.hasOwnProperty("conditions") && !parent) {
+ transform.refreshable = refreshable = true;
+ }
+
+ Object.defineProperty(this, "applicable", {
+ get: function() { return applicable; },
+ set: function(v) { }
+ });
+
+ Object.defineProperty(this, "value", {
+ get: function() { return value; },
+ set: function(v) {
+ if (type == "select") {
+ if (v < 0) {
+ v = 0;
+ } else if (v >= definition.options.length) {
+ v = defintion.options.length - 1;
+ }
+ if (definition.asvalue) {
+ value = definition.options[v];
+ } else {
+ value = v;
+ }
+ } else if (type == "range") {
+ if (v > definition.max) {
+ v = definition.max;
+ } else if (v < definition.min) {
+ v = definition.min;
+ } else if (v % definition.step != 0) {
+ if (definition.step == 1) {
+ v = Math.round(v);
+ } else {
+ v = Math.ceil((v - definition.min) / definition.step) * definition.step + definition.min;
+ }
+ }
+ value = v;
+ }
+ twist.csapp.setControlChannel(channel, value);
+ }
+ });
+
+
+
+
+ var automation = [];
+
+ this.definition = definition;
+ this.modulation = null;
+ this.channel = channel;
+ var modulationParameters = null;
+
+
+ if (!definition.hasOwnProperty("step")) {
+ definition.step = 0.0000001;
+ }
+
+ if (!definition.hasOwnProperty("min")) {
+ definition.min = 0;
+ }
+
+ if (!definition.hasOwnProperty("max")) {
+ definition.max = 1;
+ }
+
+ if (!definition.hasOwnProperty("automatable")) {
+ definition.automatable = true;
+ }
+
+ if (!definition.hasOwnProperty("dfault")) {
+ definition.dfault = 1;
+ }
+
+
+ if (parent && definition.hostrange) {
+ for (var o of ["step", "min", "max", "dfault", "options", "condition", "hostrange"]) {
+ if (parent.definition.hasOwnProperty(o)) {
+ definition[o] = parent.definition[o];
+ }
+ }
+ }
+
+ this.refresh = function() {
+ if (!refreshable) {
+ return;
+ }
+ for (var k in definition.conditions) {
+ var c = definition.conditions[k];
+ var val = transform.parameters[transform.instr + "_" + c.channel].getValue();
+ if (
+ (c.operator == "eq" && 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)
+ ) {
+ applicable = false;
+ }
+ }
+ applicable = true;
+ };
+
+ this.setDefault = function() {
+ value = definition.dfault;
+ };
+
+ if (initval) {
+ self.setDefault();
+ }
+
+ this.getAutomationData = function() {
+ if (!self.modulation) return;
+ var m = twist.appdata.modulations[self.modulation];
+ return [m.instr, self.channel];
+ };
+
+ function showModulations() {
+ modulationShown = true;
+ elValueLabel.hide();
+ elInput.hide();
+ elModulations.show();
+ elModButton.text("Close");
+ if (elModulations.children().length != 0) {
+ elModSelect.val(0).trigger("change");
+ return;
+ }
+ var tb = $("<tbody />");
+ function buildModulation(i) {
+ tb.empty();
+ modulationParameters = [];
+ self.modulation = i;
+ let m = twist.appdata.modulations[i];
+ for (let x of m.parameters) {
+ var tp = new twst.Parameter(m.instr, x, self, transform, twist);
+ modulationParameters.push(tp);
+ tb.append(tp.getElementRow(true)); // hmm modulate the modulation with false
+ }
+ }
+ var selecttb = $("<tbody />").appendTo($("<table />)").appendTo(elModulations));
+ var row = $("<tr />").append($("<td />").text("Modulation type")).appendTo(selecttb);
+
+ elModSelect = $("<select />").change(function() {
+ self.modulation = $(this).val();
+ buildModulation(self.modulation);
+ automation.push(self);
+ }).appendTo($("<td />").appendTo(row));
+ $("<table />").append(tb).appendTo(elModulations);
+
+ for (let i in twist.appdata.modulations) {
+ var m = twist.appdata.modulations[i];
+ $("<option />").text(m.name).val(i).appendTo(elModSelect);
+ }
+ elModSelect.val(0).trigger("change");
+ }
+
+};
+
+function getTransformContainer(name) {
+ return $("<div />").addClass("tfv_container").append(
+ $("<div />").addClass("tfv_header").text(name)
+ );
+}
+
+twst.ParameterGroup = function(def, instance, twist) {
+ var self = this;
+ this.instr = def.instr;
+ this.refreshable = false;
+ var presetParameters;
+ this.parameters = {};
+
+ if (def.hasOwnProperty("preset") && def.preset == "pvsynth") {
+ var conditions = [
+ {channel: "pvresmode", operator: "eq", value: 1}
+ ];
+ presetParameters = [
+ {name: "Resynth mode", channel: "pvresmode", description: "Type of FFT resynthesis used", dfault: 0, options: ["Overlap-add", "Additive"], automatable: false},
+ {name: "Oscillator spread", channel: "pvaoscnum", description: "Number of oscillators used", automatable: false, conditions: conditions},
+ {name: "Frequency modulation", channel: "pvafreqmod", description: "Frequency modulation", dfault: 1, min: 0.01, max: 2, conditions: conditions},
+ {name: "Oscillator offset", channel: "pvabinoffset", description: "Oscillator bin offset", automatable: false, conditions: conditions},
+ {name: "Oscillator increment", channel: "pvabinoffset", description: "Oscillator bin increment", min: 1, max: 32, dfault: 1, step: 1, automatable: false, conditions: conditions}
+ ];
+ }
+
+ this.refresh = function() {
+ if (!self.refreshable) {
+ return;
+ }
+ for (var k in self.parameters) {
+ self.parameters[k].refresh();
+ }
+ };
+
+ this.getAutomationData = function() {
+ var automations = [];
+ for (var k in self.parameters) {
+ var data = self.parameters[k].getAutomationData();
+ if (data) {
+ automations.push(data);
+ }
+ }
+ return automations;
+ };
+
+ this.removeParameter = function(channel) {
+ if (self.parameters.hasOwnProperty(channel)) {
+ self.parameters[channel].remove();
+ delete self.parameters[channel]
+ }
+ };
+
+ this.addParameter = function(pdef) {
+ var tp = new twst.Parameter(def.instr, pdef, null, self, twist);
+ self.parameters[tp.channel] = tp;
+ return tp;
+ };
+
+ function build() {
+ getTransformContainer(def.name).appendTo(elContainer);
+
+ var tbl = $("<table />").appendTo(elContainer);
+ elTb = $("<tbody />").appendTo(tbl);
+
+ for (let p of def.parameters) {
+ self.addParameter(p);
+ }
+
+ if (presetParameters) {
+ for (let p of presetParameters) {
+ self.addParameter(p);
+ }
+ }
+ self.refresh();
+ }
+ build();
+};
+
+
+var Transform = function(definition, instance, twist) {
+ var parameterGroup = new ParameterGroup(definition, instance, twist);
+
+ Object.defineProperty(this, "parameterGroup", {
+ get: function() { return parameterGroup; },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "parameters", {
+ get: function() { return parameterGroup.parameters; },
+ set: function(x) {}
+ });
+
+
+ function handleAutomation(onready) {
+ if (transform) {
+ var automations = transform.getAutomationData();
+ if (automations && automations.length > 0) {
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 1) {
+ onready(1);
+ } else {
+ return twist.errorHandler("Cannot parse automation data");
+ }
+ });
+ var call = [0, 1, cbid];
+ for (let i in automations) {
+ call.push(automations[i][0] + " \\\"" + automations[i][1] + "\\\"");
+ }
+ twist.csapp.insertScore("twst_automationprepare", call);
+ } else {
+ onready(0);
+ }
+ }
+ }
+
+ this.audition = function(start, end, timeUnit) {
+ if (twist.isProcessing || twist.inUse) return twist.errorHandler("Already in use");
+ errorState = "Playback error";
+
+ if (!start) {
+ start = instance.selection.ratio[0];
+ end = instance.selection.ratio[1];
+ } else {
+ if (!timeUnit) timeUnit = "seconds");
+ start = timeConvert(start, timeUnit);
+ end = timeConvert(end, timeUnit);
+ }
+
+ handleAutomation(function(automating){
+var cbid = playPositionHandler();
+ operation({
+ instr: "twst_audition",
+ score: [start, end, instance.selectedChannel, definition.instr, automating]
+ });
+
+
+
+ });
+
+ };
+
+ this.commit = function(start, end, timeUnitPos, crossfadeIn, crossfadeOut, timeUnitCrossfade) {
+ if (twist.isProcessing || twist.inUse) return twist.errorHandler("Already in use");
+ handleAutomation(function(automating){
+ if (!start) {
+ start = instance.selection.start.ratio;
+ end = instance.selection.end.ratio;
+ } else {
+ if (!timeUnitPos) timeUnitPos = "seconds");
+ start = timeConvert(start, timeUnitPos);
+ end = timeConvert(end, timeUnitPos);
+ }
+
+ if (!crossfadeIn) {
+ crossfadeIn = instance.selection.ratio[0];
+ crossfadeOut = instance.selection.ratio[1];
+ } else {
+ if (!timeUnitPos) timeUnitPos = "seconds");
+ crossfadeIn = timeConvert(start, timeUnitPos);
+ crossfadeOut = timeConvert(end, timeUnitPos);
+ }
+
+ errorState = "Transform commit error";
+ operation({
+ instr: "twst_commit",
+ refresh: true,
+ score: [start, end, instance.selectedChannel, definition.instr, automating, instance.crossFade.start.ratio, instance.crossFade.end.ratio]
+ });
+
+ });
+ };
+};
+
+
+
+
+
+var TwistInstance = function(instanceIndex, twist, options) {
+ var self = this;
+ if (!options) options = {};
+ var transform;
+ var channels;
+ var durationSamples;
+ var selectedChannel = -1;
+ var filename;
+ var sr;
+ var csTables = [];
+
+ var Time = function(dfault, onValidate, onChange) {
+ var tself = this;
+ var value = dfault;
+
+ Object.defineProperty(this, "samples", {
+ get: function() { return value; },
+ set: function(v) {
+ if (value == v) return;
+ value = v;
+ if (onValidate) {
+ var res = onValidate(value);
+ if (res) {
+ value = res;
+ }
+ }
+ if (onChange) onChange(tself);
+ }
+ });
+
+ Object.defineProperty(this, "seconds", {
+ get: function() { return value / sr; },
+ set: function(v) {
+ tself.samples = Math.round(v * sr);
+ }
+ });
+
+ Object.defineProperty(this, "ratio", {
+ get: function() { return value / durationSamples; },
+ set: function(v) {
+ tself.samples = Math.round(v * durationSamples);
+ }
+ });
+ };
+
+ var playPosition = new Time(0);
+ var selection = new Time({start: 0, end: 0}, function(v) {
+ if (typeof(v) != "object") {
+ v = {start: v, end: v};
+ return v;
+ }
+ if (v.start > v.end) {
+ v.start = v.end
+ }
+ if (v.end > durationSamples) {
+ v.end = durationSamples);
+ }
+ }, options.onSelectionChange);
+
+ var crossFade = new Time({start: 0, end: 0}, function(v) {
+ iif (typeof(v) != "object") {
+ v = {start: v, end: v};
+ return v;
+ }
+ var half = Math.round(durationSamples * 0.5);
+ if (v.start > half) {
+ v.start = half;
+ }
+ if (v.end > half) {
+ v.end = half;
+ }
+ }, options.onCrossFadeChange);
+
+
+ Object.defineProperty(this, "selectedChannel", {
+ get: function() { return selectedChannel; },
+ set: function(v) {
+ if (channels == 1) return;
+ if (v >= channels) {
+ selectedChannel = channels - 1;
+ } else if (v < 0) {
+ selectedChannel = 0;
+ } else {
+ selectedChannel = v;
+ }
+ }
+ });
+
+ Object.defineProperty(this, "playPosition", {
+ get: function() { return playPosition; },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "selection", {
+ get: function() { return selection; },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "crossFade", {
+ get: function() { return crossFade; },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "instanceIndex", {
+ get: function() { return instanceIndex; },
+ set: function(v) {}
+ });
+
+ var durationObj;
+ Object.defineProperty(durationObj, "samples", {
+ get: function() { return durationSamples; },
+ set: function(v) {}
+ });
+ Object.defineProperty(durationObj, "seconds", {
+ get: function() { return durationSamples / sr; },
+ set: function(v) {}
+ });
+ Object.defineProperty(this, "duration", {
+ get: function() { return durationObj; },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "filename", {
+ get: function() { return filename; },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "sr", {
+ get: function() { return sr; },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "csTables", {
+ get: function() { return csTables; },
+ set: function(v) {}
+ });
+
+
+ function refresh(data) {
+ twist.errorState = "Overview refresh error";
+ csTables = [data.waveL];
+ if (data.hasOwnProperty("waveR")) {
+ csTables.push(data.waveR);
+ }
+ sr = data.sr;
+ durationSamples = Math.round(data.sr * data.duration);
+ if (options.onRefresh) options.onRefresh(self);
+ }
+
+
+
+ function getTransform(path) {
+ if (!twist.transforms.hasOwnProperty(path)) {
+ return;
+ }
+ return new ParameterGroup(transforms[path], self, twist);
+ }
+
+
+ function operation(data) {
+ if (!data.setUsage) data.setUsage = true;
+ if (!data.setProcessing) data.setProcessing = true;
+ if (twist.inUse || twist.isProcessing) {
+ return twist.errorHandler("Already processing");
+ }
+
+ var score = [0, -1];
+ if (!data.noCallback) {
+ cbid = twist.csapp.createCallback(function(ndata){
+ if (data.refresh) refresh();
+ if (data.onComplete) data.onComplete(ndata);
+ if (data.setUsage) twist.inUse = false;
+ if (data.setProcessing) twist.isProcessing = false;
+ });
+ score.push(cbid);
+ if (data.setUsage) twist.inUse = true;
+ if (data.setProcessing) twist.isProcessing = true;
+ }
+ if (data.onRun) options.onRun();
+ if (data.score) {
+ for (s of data.score) {
+ score.push(s);
+ }
+ }
+
+ twist.csapp.insertScore(data.instr, score);
+ } // operation
+
+ function loadFile(name) {
+ var cbid = twist.csapp.createCallback(async function(ndata){
+ await app.getCsound().fs.unlink(name);
+ if (ndata.status == 0) {
+ return twist.errorHandler("File not valid");
+ } else {
+ refresh(ndata);
+ }
+ twist.inUse = false;
+ twist.isProcessing = false;
+ });
+ twist.inUse = true;
+ twist.isProcessing = true;
+ app.insertScore("twst_loadfile", [0, -1, cbid, item.name]);
+ }
+
+ this.loadUrl = function(url, onLoad) {
+ twist.csapp.loadFile(url, loadFile);
+ };
+
+ this.loadBuffer = function(arrayBuffer, onLoad) {
+ twist.csapp.loadBuffer(url, loadFile);
+ };
+
+ this.saveFile = function(name, onSave) {
+ if (!onSave) {
+ onSave = options.onSave;
+ }
+
+ if (!onSave) {
+ return twist.errorHandler("Instance or saveFile onSave option has not been provided");
+ }
+
+ twist.inUse = true;
+ twist.isProcessing = true;
+
+ if (!name) {
+ name = filename;
+ }
+ if (!name) {
+ name = "export.wav";
+ }
+
+ if (!name.toLowerCase().endsWith(".wav")) {
+ name += ".wav";
+ }
+ var cbid = twist.csapp.createCallback(async function(ndata){
+ var content = await twist.csapp.getCsound().fs.readFile(name);
+ var blob = new Blob(content, {type: "audio/wav"});
+ var url = window.URL.createObjectURL(blob);
+ onSave(url);
+ twist.inUse = false;
+ twist.isProcessing = false;
+ });
+ twist.csapp.insertScore("twst_savefile", [0, -1, cbid, name]);
+ };
+
+ function timeConvert(val, mode) { // returns ratio right now
+ if (mode == "ratio") {
+ return val;
+ } else if (mode == "samples") {
+ return val / durationSamples;
+ } else if (mode == "seconds") {
+ return val / (durationSamples / sr);
+ }
+ }
+
+ this.cut = function(start, end, timeUnit) {
+ if (!start) {
+ start = self.selection.ratio[0];
+ end = self.selection.ratio[1];
+ } else {
+ if (!timeUnit) timeUnit = "seconds");
+ start = timeConvert(start, timeUnit);
+ end = timeConvert(end, timeUnit);
+ }
+ operation({
+ instr: "twst_cut",
+ score: [start, end, selectedChannel],
+ refresh: true,
+ });
+ };
+
+ this.copy = function(start, end, timeUnit) {
+ if (!start) {
+ start = self.selection.ratio[0];
+ end = self.selection.ratio[1];
+ } else {
+ if (!timeUnit) timeUnit = "seconds");
+ start = timeConvert(start, timeUnit);
+ end = timeConvert(end, timeUnit);
+ }
+ operation({
+ instr: "twst_copy",
+ score: [start, end, selectedChannel],
+ });
+ };
+
+ this.paste = function(start, end, timeUnit) {
+ if (!start) {
+ start = self.selection.ratio[0];
+ end = self.selection.ratio[1];
+ } else {
+ if (!timeUnit) timeUnit = "seconds");
+ start = timeConvert(start, timeUnit);
+ end = timeConvert(end, timeUnit);
+ }
+ operation({
+ instr: "twst_paste",
+ score: [start, end, selectedChannel],
+ });
+ };
+
+ this.pasteSpecial = function(start, end, timeUnit) {
+ pasteSpecial: {instr: "twst_pastespecial", refresh: true, parameters: [
+ {name: "Repetitions", channel: "repetitions", min: 1, max: 40, step: 1, dfault: 1, automatable: false},
+ {name: "Repetition random time variance ratio", channel: "timevar", min: 0, max: 1, step: 0.000001, dfault: 0, automatable: false},
+ {name: "Mix paste", channel: "mixpaste", step: 1, dfault: 0, automatable: false},
+ {name: "Mix crossfade", channel: "mixfade", automatable: false, conditions: [{channel: "mixpaste", operator: "eq", value: 1}]}
+ ]},
+ };
+
+ this.play = function(start, end, timeUnit) {
+ errorState = "Playback error";
+ if (!start) {
+ start = self.selection.ratio[0];
+ end = self.selection.ratio[1];
+ } else {
+ if (!timeUnit) timeUnit = "seconds");
+ start = timeConvert(start, timeUnit);
+ end = timeConvert(end, timeUnit);
+ }
+ operation({
+ instr: "twst_play",
+ score: [start, end, selectedChannel],
+ });
+ };
+
+ this.stop = function() {
+ operation({
+ instr: "twst_stop"
+ });
+ };
+
+
+
+
+
+
+
+
+
+};
+
+
+var Twist = function(options) {
+ var twist = this;
+ var inUse = false;
+ var isProcessing = false;
+ var instanceIndex = 0;
+ var instances = [];
+ var transforms;
+ var modulations;
+ var onRunFunc;
+ this.errorState = null;
+
+ if (!options) options = {};
+
+ if (!options.appdata) {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", "appdata.json", false);
+ xhr.send();
+ if (xhr.status == 200) {
+ options.appdata = JSON.parse(xhr.responseText);
+ } else {
+ throw "No appdata available";
+ }
+ }
+
+ function errorHandlerInner(error, func) {
+ if (!error && twist.errorState) {
+ error = twist.errorState;
+ twist.errorState = null;
+ } elseif (!error && !twist.errorState) {
+ error = "Unhandled error";
+ }
+ func(error);
+ }
+
+ this.errorHandler = function(error) {
+ if (!error && twist.errorState) {
+ error = twist.errorState;
+ twist.errorState = null;
+ } elseif (!error && !twist.errorState) {
+ error = "Unhandled error";
+ }
+ if (options.errorHandler) {
+ options.errorHandler(error);
+ } else {
+ throw error;
+ }
+ };
+
+ this.setPercent = function(percent) {
+ if (options.onPercentChange) {
+ options.onPercentChange(percent);
+ }
+ };
+
+ if (!csapp) {
+ csapp = new CSApplication({
+ csdUrl: "twist.csd",
+ csOptions: ["--omacro:TWST_FAILONLAG=1"],
+ onPlay: function () {
+ if (onRunFunc) onRunFunc();
+ },
+ errorHandler: options.errorHandler,
+ ioReceivers: {percent: twist.setPercent}
+ });
+ }
+
+
+ Object.defineProperty(this, "csapp", {
+ get: function() {
+ return csapp;
+ },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "instances", {
+ get: function() {
+ return instances;
+ },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "transforms", {
+ get: function() {
+ return transforms;
+ },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "modulations", {
+ get: function() {
+ return modulations;
+ },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "appdata", {
+ get: function() {
+ return options.appdata;
+ },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "inUse", {
+ get: function() {
+ return inUse;
+ },
+ set: function(v) {
+ if (inUse != v) {
+ inUse = v;
+ if (options.onUsage) options.onUsage(v);
+ }
+ }
+ });
+
+ Object.defineProperty(this, "isProcessing", {
+ get: function() {
+ return isProcessing;
+ },
+ set: function(v) {
+ if (isProcessing != v) {
+ isProcessing = v;
+ if (options.onUsage) options.onUsage(v);
+ }
+ }
+ });
+
+ this.run = function(onRunFunc) {
+ onRunFunc = onRun;
+ csapp.play();
+ };
+
+ this.createInstance = function() {
+ var instance = new TwistInstance(instanceIndex, twist, options.instance);
+ instances[instanceIndex] = instance;
+ instanceIndex ++;
+ return instance;
+ };
+
+ this.removeInstanceByIndex = function(index) {
+ if (i < 0 || i > instances.length - 2) return;
+ delete instances[index];
+ };
+
+ function getProcesses(appdata, type) {
+ var processes = {};
+
+ function recurse(items, prefix) {
+ if (!prefix) {
+ prefix = "/";
+ }
+ for (let item of items) {
+ if (item.hasOwnProperty("contents")) {
+ var subitems = recurse(item.contents, prefix + item.name + "/");
+ } else {
+ processes[prefix + item.name] = item;
+ }
+ }
+ }
+ recurse(appdata[type]);
+ return processes;
+ }
+
+ transforms = getProcesses(options.appdata, "transforms");
+ modulations = getProcesses(options.appdata, "modulations");
+
+};
+
+
+
+
+
+
+
+
+
+
+
+window.t = new Twist({
+ csapp: null,
+ appdata: null,
+ latencyCorrection: 170,
+ onPercentChange,
+ onProcessing: state => {
+
+ },
+ onUsage: state => {
+
+ },
+ instance: {
+
+ onPlayPositionChange: position => {
+
+ },
+ onSelectionChange: selection => {
+
+ },
+ onCrossFadeChange: crossfades => {
+
+ },
+ onRefresh: () => {
+
+ },
+ onPlay: () => {
+
+ },
+ onSave: () => {
+
+ }
+ }
+});
+
+
+
+
+
+
+$("#start_invoke").click(function(){
+ $("#loading").show();
+ t.run(function(){
+ $("#start").hide();
+ $("#loading").hide();
+ });
+});
+
+
diff --git a/site/app/twist/_unlive/index.api.html b/site/app/twist/_unlive/index.api.html
new file mode 100644
index 0000000..80c5eb9
--- /dev/null
+++ b/site/app/twist/_unlive/index.api.html
@@ -0,0 +1,1144 @@
+<html>
+ <head>
+ <title>twist</title>
+ <script type="text/javascript" src="/code/jquery.js"></script>
+ <script type="text/javascript" src="../base/base.js"></script>
+ <script type="text/javascript" src="../base/waveform.js"></script>
+ <script type="text/javascript">
+
+
+ var TransformParameterUI = function(instr, definition, parent, transform, twist) {
+ var self = this;
+ var parameter = twist.Parameter(instr, definition, transform, twist);
+ var initval = true;
+ var elContainer = $("<div />");
+ var elValueLabel = $("<div />");
+ var elValueInput;
+ var elModulations;
+ var elInput;
+ var elRow;
+ var elModSelect;
+
+ this.refresh = function() {
+ if (!refreshable) {
+ return;
+ }
+ parameter.refresh();
+
+ if (definition.preset == "instance") {
+ createSelectOptions(elInput, twist.otherInstanceNames);
+ }
+ if (parameter.applicable) {
+ elRow.show();
+ } else {
+ elRow.hide();
+ }
+ };
+
+ function createSelectOptions(elSelect, options) {
+ elSelect.empty();
+ var selected = elInput.val();
+ for (var x in options) {
+ var opt = $("<option />").text(options[x]).val(x).appendTo(elSelect);
+ if (x == selected) {
+ opt.attr("selected", "1");
+ }
+ }
+ }
+
+ function updateLabel() {
+ if (elValueInput) {
+ var val = self.getValue();
+ updateinput = false;
+ elValueInput.val(val);
+ updateinput = true;
+ //elValueLabel.text(val);
+ }
+ }
+
+ if (type == "select") {
+ elInput = $("<select />").change(function(){
+ transform.refresh();
+ parameter.value = $(this).val();
+ if (changeFunc) changeFunc(parameter.value);
+ });
+ var options = (definition.hostrange) ? parent.definitions.options : definition.options;
+ createSelectOptions(elInput, options);
+ } else {
+ var updateinput = true;
+ var max = definition.max;
+ var min = definition.min;
+ var step = definition.step;
+ var dfault = definition.dfault;
+
+
+
+ elInput = $("<input />").attr("type", "range").on("input", function() {
+ updateLabel();
+ }).change(function() {
+ transform.refresh();
+ parameter.value = $(this).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("transparentinput").appendTo(elValueLabel).change(function() {
+ if (updateinput) {
+ elInput.val($(this).val()).trigger("change").trigger("input");
+ }
+ });
+ }
+
+ elContainer.append(elInput);
+ if (initval) {
+ elInput.val(definition.dfault).trigger("change");
+ updateLabel();
+ }
+
+
+ this.setDefault = function() {
+ elInput.val(definition.dfault).trigger("change");
+ //app.setControlChannel(channel, definition.dfault);
+ };
+
+ this.remove = function() {
+ elContainer.remove();
+ };
+
+ this.getAutomationData = function() {
+ if (!self.modulation) return;
+ var m = twist.appdata.modulations[self.modulation];
+ return [m.instr, self.channel];
+ };
+
+ var elAutomation = $("<button />").text("Automate").click(function() {
+
+ });
+
+ var modulationShown = false;
+ var elModButton = $("<button />").text("Modulate").click(function() {
+ if (elModulations && modulationShown) {
+ hideModulations();
+ } else {
+ showModulations();
+ }
+ });
+
+ function hideModulations() {
+ app.setControlChannel(channel, self.getValue());
+ modulationShown = false;
+ elValueLabel.show();
+ elInput.show();
+ self.modulation = null;
+ elModButton.text("Modulate");
+ if (elModulations) {
+ elModulations.hide();
+ if (automation.includes(self)) {
+ delete automation[automation.indexOf(self)];
+ }
+ }
+ }
+
+
+ elModulations = $("<div />").addClass("tfv_container").hide().appendTo(elContainer);
+
+ function showModulations() {
+ modulationShown = true;
+ elValueLabel.hide();
+ elInput.hide();
+ elModulations.show();
+ elModButton.text("Close");
+ if (elModulations.children().length != 0) {
+ elModSelect.val(0).trigger("change");
+ return;
+ }
+ var tb = $("<tbody />");
+ function buildModulation(i) {
+ tb.empty();
+ modulationParameters = [];
+ self.modulation = i;
+ let m = twist.appdata.modulations[i];
+ for (let x of m.parameters) {
+ var tp = new TransformParameter(m.instr, x, self, transform, twist);
+ modulationParameters.push(tp);
+ tb.append(tp.getElementRow(true)); // hmm modulate the modulation with false
+ }
+ }
+ var selecttb = $("<tbody />").appendTo($("<table />)").appendTo(elModulations));
+ var row = $("<tr />").append($("<td />").text("Modulation type")).appendTo(selecttb);
+
+ elModSelect = $("<select />").change(function() {
+ self.modulation = $(this).val();
+ buildModulation(self.modulation);
+ automation.push(self);
+ }).appendTo($("<td />").appendTo(row));
+ $("<table />").append(tb).appendTo(elModulations);
+
+ for (let i in twist.appdata.modulations) {
+ var m = twist.appdata.modulations[i];
+ $("<option />").text(m.name).val(i).appendTo(elModSelect);
+ }
+ elModSelect.val(0).trigger("change");
+ }
+
+ this.getElementRow = function(nocontrols) {
+ if (elRow) {
+ return elRow;
+ }
+ elRow = $("<tr />");
+ $("<td />").addClass("tfv_cell").text(definition.name).appendTo(elRow);
+ $("<td />").addClass("tfv_cell").append(elContainer).appendTo(elRow);
+ $("<td />").addClass("tfv_cellfixed").append(elValueLabel).appendTo(elRow);
+ if (!nocontrols) {
+ if (definition.automatable) {
+ $("<td />").addClass("tfv_cell").append(elAutomation).appendTo(elRow);
+ $("<td />").addClass("tfv_cell").append(elModButton).appendTo(elRow);
+ }
+ }
+ return elRow;
+ };
+
+
+ this.automate = function(start, end) {
+ app.insertScore("twst_createautomation", []);
+ };
+ };
+
+ function getTransformContainer(name) {
+ return $("<div />").addClass("tfv_container").append(
+ $("<div />").addClass("tfv_header").text(name)
+ );
+ }
+
+ var TransformUI = function(target, def, twist) {
+ var self = this;
+ var transform = new Transform(target, def, twist);
+ var elContainer = $("<div />").addClass("tfv_container").appendTo(target);
+ var elTb;
+ var pAddOdd = true;
+ this.uiparameters = [];
+ this.instr = def.instr;
+ this.refreshable = false;
+
+
+ this.refresh = function() {
+ if (!self.refreshable) {
+ return;
+ }
+ for (var k in self.uiparameters) {
+ self.uiparameters[k].refresh();
+ }
+ };
+
+ this.getAutomationData = function() {
+ return transform.getAutomationData();;
+ };
+
+ this.removeParameter = function(channel) {
+ if (self.uiparameters.hasOwnProperty(channel)) {
+ self.uiparameters[channel].remove();
+ delete self.uiparameters[channel]
+ }
+ transform.parameterGroup.removeParameter(channel);
+ };
+
+ this.addParameter = function(definition) {
+ self.addParameterUI(transform.parameterGroup.addParameter(definition));
+ };
+
+ this.addParameterUI = function(parameter) {
+ var tp = new TransformParameterUI(def.instr, parameter, null, self, twist);
+ self.uiparameters[tp.channel] = tp;
+ elTb.append(tp.getElementRow().addClass("tfv_row_" + ((pAddOdd) ? "odd" : "even")));
+ pAddOdd = !pAddOdd;
+ };
+
+ function build() {
+ getTransformContainer(def.name).appendTo(elContainer);
+
+ var tbl = $("<table />").appendTo(elContainer);
+ elTb = $("<tbody />").appendTo(tbl);
+
+ for (let p of transform.parameters) {
+ self.addParameter(p);
+ }
+ self.refresh();
+ }
+ build();
+ };
+
+ var TransformsTreeView = function(options, twist) {
+ var self = this;
+ var elTarget = $("#" + options.target);
+
+
+ function recurse(items, descended) {
+ items = (items) ? items : options.items;
+ var ul = $("<ul />").css({"border-bottom": "1px solid #878787", "padding-inline-start": 10}).addClass((descended) ? "nested" : "treelist");
+
+ for (let k in items) {
+ var li = $("<li />");
+ if (items[k].hasOwnProperty("contents")) {
+ $("<span />").addClass("caret").text(items[k].name).click(function() {
+ $(this).parent().children(".nested").toggleClass("active");
+ $(this).toggleClass("caret-down");
+ }).appendTo(li);
+ var subitems = recurse(items[k].contents, true);
+ li.append(subitems);
+
+ } else {
+ li.text(items[k].name).css("cursor", "pointer").click(function(){
+ var el = $("#controls").empty();
+ if (items[k].hasOwnProperty("display")) {
+ var container = getTransformContainer(items[k].name).appendTo(el);
+ twist.currentTransform = items[k].display(container);
+ } else {
+ twist.currentTransform = new Transform(el, items[k], twist);
+ }
+ });
+ }
+ ul.append(li);
+ }
+ elTarget.append(ul);
+ return ul;
+ }
+
+ elTarget.append(recurse());
+ };
+
+
+
+
+ var TwistUI = function(options) {
+ var self = this;
+ var toptions = {
+ errorHandler: function(text) {
+ self.showPrompt(text);
+ },
+ onProcessing: function(state) {
+
+ },
+ onUsage: function(state) {
+ self.waveform.cover(state);
+ }
+ };
+ var twist = new Twist(options);
+ var audioTypes = ["audio/mpeg", "audio/mp4", "audio/ogg", "audio/vorbis", "audio/x-flac","audio/aiff","audio/x-aiff", "audio/vnd.wav", "audio/wave", "audio/x-wav", "audio/wav"];
+ var maxsize = 1e+8; // 100 MB
+
+ var instanceIndex = 0;
+ this.stoptoggle = null;
+ this.waveforms = [];
+ var waveformTabs = [];
+ var keyModifier = {shift: false, alt: false, ctrl: false};
+ var playheadInterval;
+ var playing = false;
+ var elCrossfades = [];
+ var elPasteSpecial;
+
+ if (!options) options = {};
+ if (!options.latencyCorrection) options.latencyCorrection = 0;
+
+ function newInstance() {
+ var instance = twist.createInstance();
+ var element = $("<div />").addClass("waveform").appendTo("#waveforms");
+ let index = self.waveforms.length;
+ if (index < 0) index = 0;
+ waveformTabs.push(
+ $("<td />").text("New file").click(function() {
+ self.waveform = index;
+ }).addClass("wtab_selected").appendTo("#waveform_tabs")
+ );
+
+ self.waveforms.push(
+ new Waveform({target: element, latencyCorrection: options.latencyCorrection, showcrossfades: true})
+ );
+
+ self.waveform = index;
+ }
+
+ function removeInstance(i) {
+ if (i < 0 || i > this.waveforms.length - 2) {
+ return;
+ }
+ self.waveform.destroy();
+ twist.removeInstanceByIndex(i);
+ if (instanceIndex == i) {
+ instanceIndex = i + ((i == 0) ? 1 : -1);
+ self.waveform.show();
+ }
+ }
+
+ this.setPercent = function(percent) {
+ $("#loading_percent_inner").width(percent + "%");
+ };
+
+
+ function setLoadingStatus(state, showpercent) {
+ var el = $("#loading");
+ if (state) {
+ el.show();
+ if (showpercent) {
+ $("#loading_percent").show();
+ } else {
+ $("#loading_percent").hide();
+ }
+ } else {
+ el.hide();
+ }
+ }
+
+ this.setLoadingStatus = setLoadingStatus;
+
+ this.showPrompt = function(text, oncomplete) {
+ setLoadingStatus(false);
+ $("#prompt").show();
+ $("#prompt_text").text(text);
+ $("#prompt_button").unbind().click(function(){
+ if (oncomplete) {
+ oncomplete();
+ }
+ $("#prompt").hide();
+ });
+ };
+
+
+ function playPositionHandler(noPlayhead) {
+ function callback(ndata) {
+ if (ndata.status == 1) {
+ playing = true;
+ if (!noPlayhead) {
+ if (playheadInterval) {
+ clearInterval(playheadInterval);
+ }
+ playheadInterval = setInterval(async function(){
+ var val = await app.getControlChannel("playposratio");
+ if (val < 0 || val >= 1) {
+ clearInterval(playheadInterval);
+ }
+ self.waveform.movePlayhead(val);
+ }, 50);
+ }
+ } else {
+ playing = false;
+ if (ndata.status == -1) {
+ self.errorHandler("Not enough processing power to transform in realtime");
+ }
+ if (self.stoptoggle) {
+ setTimeout(self.stoptoggle, latencyCorrection);
+ }
+ app.removeCallback(ndata.cbid);
+ if (!noPlayhead) {
+ self.waveform.movePlayhead(0);
+ if (playheadInterval) {
+ clearInterval(playheadInterval);
+ }
+ }
+ self.waveform.cover(false);
+ }
+ }
+ return app.createCallback(callback, true);
+ }
+
+
+
+ async function refreshOverviews(ndata) {
+ errorState = "Overview refresh error";
+ var wavedata = [];
+ var duration = ndata.duration;
+
+ wavedata.push(async function(v) {
+ if (v < 0) {
+ return await app.getCsound().tableLength(ndata.waveL);
+ } else {
+ return await app.getCsound().tableGet(ndata.waveL, v);
+ }
+ });
+
+ if (ndata.hasOwnProperty("waveR")) {
+ wavedata.push(async function(v) {
+ return await app.getCsound().tableGet(ndata.waveR, v);
+ });
+ }
+ self.waveform.setData(wavedata, ndata.duration);
+
+ self.waveform.cover(false);
+ }
+
+ this.cut = function() {
+ self.instance.cut();
+ };
+
+ this.copy = function() {
+ self.instance.copy();
+ };
+
+ this.paste = function() {
+ self.instance.paste();
+ };
+
+ this.moveToStart = function() {
+ self.waveform.setSelection(0);
+ };
+
+ this.moveToEnd = function() {
+ self.waveform.setSelection(1);
+ };
+
+
+ this.pasteSpecial = function() {
+ if (!elPasteSpecial) {
+ elPasteSpecial = $("<div />").addClass("waveform_overlay").appendTo($("#waveforms"));
+ elPasteSpecial.append($("<h2 />").text("Paste special"));
+ var def = {
+ instr: "twst_pastespecial",
+ parameters: [
+ {name: "Repetitions", channel: "repetitions", min: 1, max: 40, step: 1, dfault: 1, automatable: false},
+ {name: "Repetition random time variance ratio", channel: "timevar", min: 0, max: 1, step: 0.000001, dfault: 0, automatable: false},
+ {name: "Mix paste", channel: "mixpaste", step: 1, dfault: 0, automatable: false},
+ {name: "Mix crossfade", channel: "mixfade", automatable: false, conditions: [{channel: "mixpaste", operator: "eq", value: 1}]}
+ ]
+ };
+ var tf = new Transform(elPasteSpecial, def, self);
+
+ $("<button />").text("Paste").click(function(){
+ elPasteSpecial.hide();
+ self.waveform.cover(true);
+ operation("twst_pastespecial", refreshOverviews, true);
+ }).appendTo(elPasteSpecial);
+
+ } else {
+ elPasteSpecial.show();
+ }
+ };
+
+
+ this.play = function() {
+ self.instance.play();
+ };
+
+ this.stop = function() {
+ self.instance.stop();
+ };
+
+ this.saveFile = function(name) {
+ self.instance.saveFile(name, function(url) {
+ var a = $("<a />").attr("href", url).attr("download", name).appendTo($("body")).css("display", "none");
+ a.click();
+ setTimeout(function(){
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ app.getCsound().fs.unlink(name);
+ }, 2000);
+ });
+ };
+
+ this.audition = function() {
+ self.instance.audition();
+
+ };
+
+ this.commit = function() {
+ self.instance.commit();
+ }
+
+ function buildWavecontrols() {
+ var el = $("#wavecontrols_inner");
+ var items = [
+ {label: "Zoom sel", click: function() {self.waveform.zoomSelection();}},
+ {label: "Zoom in", click: function() {self.waveform.zoomIn();}},
+ {label: "Zoom out", click: function() {self.waveform.zoomOut();}},
+ {label: "Show all", click: function() {self.waveform.zoomLevel = 1;}},
+
+ {label: "Cut", click: self.cut, css: {"background-color": "#a5ab30"}},
+ {label: "Copy", click: self.copy, css: {"background-color": "#d5db60"}},
+ {label: "Paste", click: self.paste, css: {"background-color": "#e5df90"}},
+ {label: "Paste special", click: self.pasteSpecial, css: {"background-color": "#e9dfa5"}},
+
+ {label: "Play", click: function(b) {
+ self.stoptoggle = function() {
+ b.text("Play");
+ };
+ if (b.text() == "Play") {
+ b.text("Stop");
+ self.play();
+ } else {
+ b.text("Play");
+ self.stop();
+ }
+ }, css: {"background-color": "#cc9999"}},
+ {label: "Audition", click: function(b) {
+ self.stoptoggle = function() {
+ b.text("Audition");
+ };
+ if (b.text() == "Audition") {
+ b.text("Stop");
+ self.audition();
+ } else {
+ b.text("Audition");
+ self.stop();
+ }
+ }, css: {"background-color": "#aa6666"}},
+ {label: "Commit", click: self.commit, css: {"background-color": "#ff4444"}},
+ ];
+
+ for (let i of items) {
+ let button = $("<button />").text(i.label)
+ button.click(function() {
+ i.click(button);
+ });
+ if (i.hasOwnProperty("css")) {
+ button.css(i.css);
+ }
+ $("<td />").append(
+ button
+ ).appendTo(el);
+ }
+
+ for (let e of ["in", "out"]) {
+ var elRange = $("<input />").attr("type", "range").attr("min", 0).attr("max", 0.45).attr("step", 0.00001).val(0).on("input", function() {
+ if (e == "in") {
+ self.waveform.crossFadeInRatio = $(this).val();
+ } else {
+ self.waveform.crossFadeOutRatio = $(this).val();
+ }
+ });
+ elCrossfades.push(elRange);
+ $("<td />").append($("<div />").text("Commit crossfade " + e)).append(elRange).appendTo(el);
+ }
+ }
+
+ function createLeftPane() {
+ var el = $("<div />").addClass("treetop").appendTo($("#panetree"));
+ $("<div />").addClass("treetop_header").text("twist").appendTo(el);
+
+ var ttv = new TransformsTreeView({
+ target: "panetree",
+ items: appdata.transforms
+ }, self);
+ }
+
+ async function handleFileDrop(e, obj) {
+ e.preventDefault();
+ if (!e.originalEvent.dataTransfer.files) {
+ return;
+ }
+ twist.isProcessing = true;
+ for (const item of e.originalEvent.dataTransfer.files) {
+ //item.size;
+ //item.type "audio/mpeg";
+ if (!audioTypes.includes(item.type)) {
+ return self.errorHandler("Unsupported file type");
+ }
+ if (item.size > maxsize) {
+ return self.errorHandler("File too big");
+ }
+ twist.errorState = "File loading error";
+ var content = await item.arrayBuffer();
+ self.instance.loadBuffer(content, function(ndata){
+ self.waveformTab.text(item.name);
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ });
+ }
+ }
+
+ this.run = function() {
+ twist.run();
+
+ Object.defineProperty(this, "waveformTab", {
+ get: function() { return waveformTabs[instanceIndex]; },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "otherInstanceNames", {
+ get: function() {
+ var data = {};
+ for (var i in waveformTabs) {
+ if (i != instanceIndex) {
+ data[i] = waveformTabs[i].text();
+ }
+ }
+ return data
+ },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "waveform", {
+ get: function() { return self.waveforms[instanceIndex]; },
+ set: function(x) {
+ if (instanceIndex != x) {
+ self.waveformTab.removeClass("wtab_selected").addClass("wtab_unselected");
+ self.waveform.hide();
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 1) {
+ instanceIndex = x;
+ self.waveformTab.removeClass("wtab_unselected").addClass("wtab_selected");
+ self.waveform.show();
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ } else {
+ self.showPrompt("Error changing instance");
+ }
+ });
+ app.insertScore("twst_setinstance", [0, 1, cbid, x]);
+
+ }
+ }
+ });
+
+ $("<td />").text("+").click(function() {
+ newInstance();
+ }).appendTo("#waveform_tabs").addClass("wtab_selected");
+
+ $("body").on("dragover", function(e) {
+ e.preventDefault();
+ e.originalEvent.dataTransfer.effectAllowed = "all";
+ e.originalEvent.dataTransfer.dropEffect = "copy";
+ return false;
+ }).on("dragleave", function(e) {
+ e.preventDefault();
+ }).on("drop", function(e) {
+ handleFileDrop(e, self);
+ }).on("keydown", function(e){
+ switch (e.which) {
+ case 32: // space
+ if (playing) {
+ self.stop();
+ } else {
+ self.play;
+ }
+ break;
+ case 13: // enter
+ if (keyModifier.alt) self.commit();
+ break;
+ case 67: // c
+ if (keyModifier.ctrl) self.copy();
+ break;
+ case 88: // x
+ if (keyModifier.ctrl) self.cut();
+ break;
+ case 86: // v
+ if (keyModifier.ctrl) {
+ if (keyModifier.shift) {
+ self.pasteSpecial();
+ } else {
+ self.paste();
+ }
+ }
+ break;
+ case 17:
+ keyModifier.ctrl = true;
+ break;
+ case 18:
+ keyModifier.alt = true;
+ break;
+ case 16:
+ keyModifier.shift = true;
+ break;
+ default:
+ return;
+ }
+ e.preventDefault();
+ }).on("keyup", function(e){
+ switch (e.which) {
+ case 17:
+ keyModifier.ctrl = false;
+ break;
+ case 18:
+ keyModifier.alt = false;
+ break;
+ case 16:
+ keyModifier.shift = false;
+ break;
+ default:
+ return;
+ }
+ e.preventDefault();
+ });
+
+ newInstance();
+ buildWavecontrols();
+ createLeftPane();
+ };
+
+ }; // end twist
+
+ $(function() {
+ window.twist = new Twist(appdata);
+ window.app = new CSApplication({
+ csdUrl: "twist.csd",
+ csOptions: ["--omacro:TWST_FAILONLAG=1"],
+ onPlay: function () {
+ twist.setLoadingStatus(false);
+ },
+ errorHandler: twist.errorHandler,
+ ioReceivers: {percent: twist.setPercent}
+ });
+
+ $("#start_invoke").click(function() {
+ $("#start").hide();
+ twist.run();
+ twist.setLoadingStatus(true);
+ app.play();
+ });
+
+ });
+
+
+ </script>
+ <style type="text/css">
+ /* Remove default bullets */
+ ul, .treelist {
+ list-style-type: none;
+ }
+
+ /* Remove margins and padding from the parent ul */
+ .treelist {
+ margin: 0;
+ padding: 0;
+ }
+
+ /* Style the caret/arrow */
+ .caret {
+ cursor: pointer;
+ font-weight: bold;
+ user-select: none; /* Prevent text selection */
+ }
+
+ /* Create the caret/arrow with a unicode, and style it */
+ .caret::before {
+ content: "\25B6";
+ color: black;
+ display: inline-block;
+ margin-right: 6px;
+ }
+
+ /* Rotate the caret/arrow icon when clicked on (using JavaScript) */
+ .caret-down::before {
+ transform: rotate(90deg);
+ }
+
+ /* Hide the nested list */
+ .nested {
+ display: none;
+ }
+
+ /* Show the nested list when the user clicks on the caret/arrow (with JavaScript) */
+ .active {
+ display: block;
+ }
+
+
+ .treetop_header {
+ font-size: 16pt;
+ font-weight: bold;
+ padding-top: 10px;
+ width: 100%;
+ text-align: center;
+ top: 0px;
+ }
+
+ .treetop {
+ width: 100%;
+ height: 50px;
+ border-bottom: 1px solid black;
+ }
+ </style>
+ <style type="text/css">
+ #loading {
+ position: fixed;
+ display: none;
+ z-index: 161;
+ background-color: #7e80f2;
+ text-align: center;
+ font-size: 64pt;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ }
+
+ #loading_percent {
+ position: absolute;
+ top: 20%;
+ left: 30%;
+ width: 40%;
+ height: 10%;
+ background-color: #9a88f7;
+ }
+
+ #loading_percent_inner {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ height: 100%;
+ width: 1%;
+ background-color: #5e50a2;
+ }
+
+ #main {
+ position: absolute;
+ z-index: 5;
+ background-color: "#c5c5f0;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ }
+
+ .waveform {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ }
+
+ #waveforms {
+ position: absolute;
+ left: 15%;
+ top: 0px;
+ height: 60%;
+ width: 85%;
+ }
+
+ .waveform_overlay {
+ position: absolute;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ background-color: #fcf6de;
+ opacity: 0.9;
+ left: 0px;
+ top: 0px;
+ z-index: 20;
+ }
+
+ #sidepane {
+ position: absolute;
+ background-color: #a5a5d9;
+ left: 0px;
+ top: 0px;
+ height: 100%;
+ width: 15%;
+ border-right: 1px solid black;
+ }
+
+ #controls {
+ position: absolute;
+ background-color: #9d9ded;
+ left: 15%;
+ top: 65%;
+ height: 40%;
+ width: 85%;
+ border-top: 1px solid black;
+ }
+
+ #wavecontrols {
+ position: absolute;
+ overflow: hidden;
+ background-color: #323265;
+ left: 15%;
+ top: 60%;
+ height: 5%;
+ width: 85%;
+ border-top: 1px solid black;
+ }
+
+ #waveform_tabs {
+ cursor: pointer;
+ }
+
+ #panetree {
+ font-size: 8pt;
+ font-family: Arial, sans-serif;
+ }
+
+ button {
+ border: none;
+ background-color: #8787bd;
+ font-size: 8pt;
+ padding: 1px;
+ font-family: Arial, sans-serif;
+ }
+
+ .tfv_script {
+ background-color: #323265;
+ color: #ffc67a;
+ font-size: 10pt;
+ font-family: monospace, Courier;
+ width: 100%;
+ height: 100%;
+ }
+
+ .tfv_container {
+ background-color: #8181fa;
+ font-size: 10pt;
+ font-family: Arial, sans-serif;
+ }
+
+ .tfv_row_odd {
+ background-color: #8181c9;
+ font-size: 10pt;
+ font-family: Arial, sans-serif;
+ }
+
+ .tfv_row_even {
+ background-color: #7171ba;
+ font-size: 10pt;
+ font-family: Arial, sans-serif;
+ }
+
+ .tfv_cell {
+ font-size: 10pt;
+ font-family: Arial, sans-serif;
+ }
+
+ .tfv_cellfixed {
+ font-size: 8pt;
+ font-family: Arial, sans-serif;
+ overflow: hidden;
+ width: 40px;
+ }
+
+ .tfv_header {
+ background-color: #565698;
+ font-size: 11pt;
+ font-weight: bold;
+ }
+
+ .automate_container {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 125;
+ display: none;
+ }
+
+ #prompt {
+ z-index: 201;
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ background-color: #3434d9;
+ display: none;
+ }
+
+ #prompt_centre {
+ z-index: 202;
+ position: relative;
+ height: 200px;
+ }
+
+ #prompt_inner {
+ z-index: 203;
+ margin: 0;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 24pt;
+ cursor: pointer;
+ text-align: centre;
+ }
+
+ #prompt_button {
+ font-size: 24pt;
+ padding: 20px;
+ }
+
+ #start {
+ z-index: 200;
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ background-color: #5656d1;
+ }
+
+ #start_centre {
+ z-index: 201;
+ position: relative;
+ height: 200px;
+ }
+
+ .transparentinput {
+ font-size: 8pt;
+ background-color: 9898ff;
+ color: #000000";
+ border: none;
+ }
+
+ .wtab_selected {
+ font-size: 8pt;
+ font-weight: bold;
+ background-color: 9898ff;
+ color: #000000";
+ padding: 3px;
+ border: 1px solid black;
+ border-top: 0;
+ }
+
+ .wtab_unselected {
+ font-size: 8pt;
+ background-color: #8888df;
+ font-weight: normal;
+ color: #000000";
+ padding: 3px;
+ border: 1px solid black;
+ }
+
+ #start_invoke {
+ z-index: 202;
+ text-align: centre;
+ margin: 0;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 72pt;
+ cursor: pointer;
+ }
+
+ body {
+ font-family: Arial, sans-serif;
+ }
+
+ </style>
+ </head>
+ <body>
+ <div id="prompt">
+ <div id="prompt_centre">
+ <div id="prompt_inner">
+ <p id="prompt_text"></p>
+ <button id="prompt_button">OK</button>
+ </div>
+ </div>
+ </div>
+ <div id="start">
+ <div id="start_centre">
+ <h1>twist</h1>
+ <p>Web based audio transformer</p>
+ <p id="start_invoke">Press to begin</p>
+ </div>
+ </div>
+ <div id="loading">
+ Processing
+ <div id="loading_percent"><div id="loading_percent_inner"></div></div>
+ </div>
+ <div id="main">
+ <div id="waveforms"></div>
+ <div id= "automate_container"></div>
+ <div id="wavecontrols">
+ <table><tbody><tr id="waveform_tabs"></tr><tbody></table>
+ <table><tbody><tr id="wavecontrols_inner"></tr><tbody></table>
+ </div>
+ <div id="sidepane">
+ <div id="panetree"></div>
+ </div>
+ <div id="controls"></div>
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/site/app/twist/_unlive/splinetest.html b/site/app/twist/_unlive/splinetest.html
new file mode 100644
index 0000000..cda0969
--- /dev/null
+++ b/site/app/twist/_unlive/splinetest.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<html lang="en-GB">
+ <head>
+ <title>twist</title>
+ <script type="text/javascript" src="https://apps.csound.1bpm.net/code/jquery.js"></script>
+ <script src="https://apps.csound.1bpm.net/code/d3.v7.min.js"></script>
+ <script type="text/javascript" src="../base/spline-edit.js"></script>
+ <script type="text/javascript">
+ $(function() {
+ window.spline = new SplineEdit(
+ $("#spline"),
+ "#992222",
+ 30,
+ [-500, 500, 100, 0.0001],
+ "ass"
+ );
+ });
+ </script>
+ <style type="text/css">
+ .tooltip-splineedit {
+ position: absolute;
+ text-align: center;
+ border-radius: 5px;
+ pointer-events: none;
+ padding: 2px;
+ color: #000000;
+ opacity: 0;
+ font-family: Arial, sans-serif;
+ font-size: 8pt;
+ text-shadow: 1px 1px #ffffff;
+ z-index: 42;
+ }
+ #spline {
+ position: absolute;left:0px;top:20%;width: 100%;height: 50%;background-color:#bdbdbd;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="tooltip-splineedit" class="tooltip-splineedit"></div>
+ <div id="spline"></div>
+ </body>
+</html> \ No newline at end of file
diff --git a/site/app/twist/_unlive/transform.js b/site/app/twist/_unlive/transform.js
new file mode 100644
index 0000000..3810359
--- /dev/null
+++ b/site/app/twist/_unlive/transform.js
@@ -0,0 +1,1024 @@
+var TransformParameter = function(instr, tDefinition, parent, transform, twist, onChange) {
+ var self = this;
+ var refreshable = false;
+ var changeFunc;
+ var initval = true;
+ var definition = {};
+ var randomiseAllowed = true;
+ var visible = true;
+
+ if (parent) {
+ Object.assign(definition, tDefinition);
+ } else {
+ definition = tDefinition;
+ }
+
+ if (definition.channel == "applymode") {
+ randomiseAllowed = false;
+ }
+
+ 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", 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 == "applymode") {
+ Object.assign(definition, {name: "Apply mode", channel: "applymode", absolutechannel: true, description: "Apply mode", automatable: false, options: ["Replace", "Mix", "Modulate", "Demodulate"], dfault: 0});
+ } else if (definition.preset == "note") {
+ var notes = {};
+ for (var i = 21; i < 128; i++) {
+ var v = twist.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: instr + "_" + "instance",
+ options: twist.otherInstanceNames,
+ automatable: false
+ });
+ changeFunc = function(index) {
+ var s = twist.waveforms[index].selected;
+ app.setControlChannel(instr + "_" + "inststart", s[0]);
+ app.setControlChannel(instr + "_" + "instend", s[1]);
+ app.setControlChannel(instr + "_" + "instchan", s[2]);
+ };
+ }
+ if (save) {
+ Object.assign(definition, save);
+ }
+ } // if preset
+
+ var type;
+
+ if (definition.hasOwnProperty("conditions") && !parent) {
+ refreshable = true;
+ if (transform) transform.refreshable = refreshable;
+ }
+
+ var channel = "";
+ if (!definition.hasOwnProperty("absolutechannel")) {
+ channel = (parent) ? parent.channel : instr + "_";
+ }
+
+ if (definition.hasOwnProperty("channel")) {
+ channel += definition.channel;
+ } else {
+ channel += definition.name.toLowerCase();
+ }
+
+ 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 (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);
+ }
+ };
+
+
+ if (!definition.hasOwnProperty("hidden")) {
+ definition.hidden = false;
+ }
+
+ if (!definition.hasOwnProperty("step")) {
+ definition.step = 0.0000001;
+ }
+
+ if (!definition.hasOwnProperty("min")) {
+ definition.min = 0;
+ }
+
+ if (!definition.hasOwnProperty("max")) {
+ definition.max = 1;
+ }
+
+ if (!definition.hasOwnProperty("fireChanges")) {
+ definition.fireChanges = true;
+ }
+
+ if (!definition.hasOwnProperty("dfault")) {
+ 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.hasOwnProperty("options")) {
+ type = "select";
+ } 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 (automationActive) disableAutomation();
+ if (self.automation) {
+ delete self.automation;
+ self.automation = null;
+ }
+ if (elSpline) {
+ elSpline.remove();
+ delete elSpline;
+ }
+ if (modulationShown) hideModulations();
+ };
+
+ this.randomise = function() {
+ if (!randomiseAllowed) return;
+ var val;
+ if (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) {
+ // 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, twist.otherInstanceNames);
+ }
+ for (var k in definition.conditions) {
+ var c = definition.conditions[k];
+ var val = transform.parameters[transform.instr + "_" + c.channel].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;
+ return elRow.hide();
+ }
+ }
+ visible = true;
+ elRow.show();
+ };
+
+ function createSelectOptions(elSelect, options) {
+ var selected = elInput.val();
+ elSelect.empty();
+ for (var x in options) {
+ var opt = $("<option />").text(options[x]).val(x).appendTo(elSelect);
+ if (x == selected) {
+ opt.attr("selected", "1");
+ if (changeFunc) changeFunc(x);
+ }
+ }
+ 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);
+ app.setControlChannel(channel, val);
+ }
+ if (onChange) {
+ onChange(val);
+ }
+ });
+
+ var options = (definition.hostrange && parent) ? parent.definitions.options : definition.options;
+ createSelectOptions(elInput, options);
+
+ } else if (type == "string") {
+ elInput = $("<input />").change(function() {
+ if (transform) transform.refresh();
+ var val = self.getValue();
+ if (definition.fireChanges) {
+ app.setStringChannel(channel, val);
+ }
+ if (onChange) {
+ onChange(val);
+ }
+ });
+
+ } else if (type == "checkbox") {
+ elInput = $("<input />").addClass("tp_checkbox").attr("type", "checkbox").on("change", function() {
+ if (transform) transform.refresh();
+ var val = self.getValue();
+ if (definition.fireChanges) {
+ app.setControlChannel(channel, 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("tp_slider").attr("type", "range").on("input", function() {
+ updateLabel();
+ if (definition.fireChanges) {
+ app.setControlChannel(channel, self.getValue());
+ }
+ }).change(function() {
+ updateLabel();
+ if (transform) transform.refresh();
+ var val = self.getValue();
+ if (definition.fireChanges) {
+ app.setControlChannel(channel, 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("transparentinput").appendTo(elValueLabel).change(function() {
+ if (updateinput) {
+ elInput.val($(this).val()).trigger("change").trigger("input");
+ }
+ });
+ }
+
+ 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(channel, definition.dfault);
+ };
+
+ this.remove = function() {
+ 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 = twist.appdata.modulations[self.modulation];
+ return {type: "modulation", data: [m.instr, self.channel]};
+ } else if (automationActive && self.automation) {
+ return {type: "automation", channel: self.channel, data: self.automation.getLinsegData(start, end, twist.waveform.getRegion())};
+ }
+ };
+
+ 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",
+ 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();
+ } else {
+ showAutomation();
+ }
+ }
+ });
+
+ 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;
+ app.setControlChannel(channel, self.getValue());
+ elValueLabel.show();
+ elInput.show();
+ modButton.el.show();
+ automationButton.setState(true);
+ editAutomationButton.el.hide();
+ self.hideAutomation();
+ }
+
+ this.redraw = function(region) {
+ if (self.automation) {
+ if (region && region[0] != null && region[1] != null) {
+ self.automation.setRange(region[0], region[1]);
+ } else {
+ self.automation.redraw();
+ }
+ }
+ };
+
+ function showAutomation() {
+ if (!transform) return;
+ var colour = "rgb(" + (Math.round(Math.random() * 50) + 205) + ","
+ + (Math.round(Math.random() * 50) + 205) + ","
+ + (Math.round(Math.random() * 50) + 205) + ")";
+ 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);
+
+ if (!self.automation) {
+ self.automation = new SplineEdit(
+ elSpline, colour,
+ twist.waveform.getDuration,
+ [definition.min, definition.max, self.getValue(), definition.step],
+ definition.name
+ );
+ }
+
+ elValueLabel.hide();
+ elInput.hide();
+ modButton.el.hide();
+ elSpline.show();
+ editAutomationButton.el.show(); //.css("background-color", colour);
+ automationButton.setState(false);
+ }
+
+
+ elModulations = $("<div />").addClass("tfv_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(channel, 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 = twist.appdata.modulations[i];
+ for (let x of m.parameters) {
+ var tp = new TransformParameter(m.instr, x, self, transform, twist);
+ 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 />").text("Modulation type")).appendTo(selecttb);
+ var elConditionalOptions = [];
+
+ twist.onInstanceChangeds.push(function(){
+ for (let o of elConditionalOptions) {
+ if (twist.waveforms.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 twist.appdata.modulations) {
+ var m = twist.appdata.modulations[i];
+ var o = $("<option />").text(m.name).val(i).appendTo(elModSelect);
+ if (m.inputs > 1) {
+ elConditionalOptions.push(o);
+ if (twist.waveforms.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("tfv_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("tfv_cell").append(elContainer).appendTo(elRow);
+ $("<td />").addClass("tfv_cellfixed").append(elValueLabel).appendTo(elRow);
+ if (!nocontrols) {
+ for (let b of [resetButton, randomiseButton]) $("<td />").addClass("tfv_cell_plainbg").append(b.el).appendTo(elRow);
+
+ if (definition.automatable) {
+ for (let b of [automationButton, editAutomationButton, modButton]) $("<td />").addClass("tfv_cell_plainbg").append(b.el).appendTo(elRow);
+ }
+
+ }
+ return elRow;
+ };
+};
+
+
+
+function getTransformContainer(nameOrElement) {
+ var el = $("<div />").addClass("tfv_header");
+ if (typeof(nameOrElement) == "string") {
+ el.text(nameOrElement);
+ } else {
+ el.append(nameOrElement);
+ }
+ return $("<div />").addClass("tfv_container").append(el);
+}
+
+var Transform = function(target, def, twist) {
+ var self = this;
+ var elTb;
+ var pAddOdd = true;
+ this.instr = def.instr;
+ this.refreshable = false;
+ var elSplineOverlay;
+ var hideAutomationButton;
+ this.parameters = {};
+
+ var automationEls = {};
+ this.showAutomation = function(name, el) {
+ if (!elSplineOverlay) {
+ elSplineOverlay = $("<div />").addClass("spline_overlay").appendTo($("#twist_splines"));
+ }
+ 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);
+ }
+ $("#twist_splines").show();
+ };
+
+ 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();
+ $("#twist_splines").hide();
+ }
+ }
+ }
+
+ 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, channels: {}};
+ var value;
+ for (let chan in self.parameters) {
+ value = await app.getControlChannel(chan);
+ data.channels[chan] = value;
+ if (self.parameters[chan].modulationParameters) {
+ for (let modchan in self.parameters[chan].modulationParameters) {
+ value = await app.getControlChannel(modchan);
+ 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 (self.parameters[p].modulationParameters) {
+ for (let mp in self.parameters[p].modulationParameters) {
+ self.parameters[p].modulationParameters[mp].randomise();
+ }
+ }
+ }
+ };
+
+ this.saveState = function() {
+ var state = {};
+ for (let p in self.parameters) {
+ state[p] = self.parameters[p].getRawValue();
+ }
+ if (!twist.storage.transforms) {
+ twist.storage.transforms = {};
+ }
+ twist.storage.transforms[def.instr] = state;
+ twist.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 TransformParameter(def.instr, pdef, null, self, twist);
+ self.parameters[tp.channel] = tp;
+ var er = tp.getElementRow();
+ if (er) {
+ elTb.append(er.addClass("tfv_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 == "pvanal") {
+ groupParameters = [
+ {preset: "fftsize"},
+ {preset: "pvslock"},
+ {name: "Overlap decimation", min: 4, max: 16, step: 1, dfault: 4, channel: "pvsdecimation", automatable: false, lagHint: -1},
+ {name: "Window size multiplier", min: 1, max: 4, dfaut: 1, step :1, channel: "pvswinsizem", automatable: false, lagHint: -1},
+ {name: "Window type", 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() {
+ target.empty();
+ var elContainer = $("<div />").addClass("tfv_container").appendTo(target);
+ hideAutomationButton = twirl.createIcon({label: "Hide automation", icon: "hide", click: function() {
+ self.hideAllAutomation();
+ }});
+ hideAutomationButton.el.hide();
+
+ app.setControlChannel("applymode", 0); // not all transforms will set this
+ var el = $("<div />");
+ var header = $("<div />").text(def.name).appendTo(el);
+
+ 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);
+
+ $("<div />").addClass("tfv_container").append(
+ $("<div />").addClass("tfv_header").append(el)
+ ).appendTo(elContainer);
+
+ //getTransformContainer(el).appendTo(elContainer);
+ var tbl = $("<table />").appendTo(elContainer);
+ elTb = $("<tbody />").appendTo(tbl);
+
+ for (let p of def.parameters) {
+ self.addParameter(p);
+ }
+
+ if (twist.storage && twist.storage.transforms && twist.storage.transforms[def.instr]) {
+ var state = twist.storage.transforms[def.instr];
+ for (var p in state) {
+ self.parameters[p].setRawValue(state[p]);
+ }
+ }
+ self.refresh();
+ }
+ build();
+};
+
+var TransformsTreeView = function(options, twist) {
+ var self = this;
+ var elTarget = $("#" + options.target);
+
+
+ function recurse(items, descended) {
+ items = (items) ? items : options.items;
+ var ul = $("<ul />").css({"border-bottom": "1px solid #878787", "padding-inline-start": 10}).addClass((descended) ? "nested" : "treelist");
+
+ for (let k in items) {
+ var li = $("<li />");
+ if (items[k].hasOwnProperty("contents")) {
+ $("<span />").addClass("caret").text(items[k].name).click(function() {
+ $(this).parent().children(".nested").toggleClass("active");
+ $(this).toggleClass("caret-down");
+ }).appendTo(li);
+ var subitems = recurse(items[k].contents, true);
+ li.append(subitems);
+
+ } else {
+ li.text(items[k].name).css("cursor", "pointer").click(function(){
+ if (twist.currentTransform) {
+ twist.currentTransform.remove();
+ }
+ twist.currentTransform = new Transform($("#twist_controls"), items[k], twist);
+ });
+ }
+ ul.append(li);
+ }
+ elTarget.append(ul);
+ return ul;
+ }
+
+ elTarget.append(recurse());
+}; \ No newline at end of file
diff --git a/site/app/twist/_unlive/twist_fxtester.csd b/site/app/twist/_unlive/twist_fxtester.csd
new file mode 100644
index 0000000..9f12cb7
--- /dev/null
+++ b/site/app/twist/_unlive/twist_fxtester.csd
@@ -0,0 +1,101 @@
+<CsoundSynthesizer>
+<CsOptions>
+-odac
+</CsOptions>
+<CsInstruments>
+sr = 44100
+ksmps = 64
+nchnls = 2
+0dbfs = 1
+seed 0
+
+#include "/twist/twist.udo"
+
+gStransform = "twst_tf_fftpitchscale";, "twst_tf_morph"
+SchanSet[] fillarray "timescale", "pitchscale", "winsize", "randwin", "overlap", "wintype", "shift", "amp", "freq", "fftsize", "otinstchan", "instanceindex", "otinststart", "otinstend", "pvresmode", "time", "scale", "formants"
+ichanset[] fillarray 1.5, 1, 4410, 441, 4, 0, 900, 0, 1, 512, -1, 1, 0, 1, 0, 0.4, 2, 0
+index = 0
+while (index < lenarray(ichanset)) do
+ chnset ichanset[index], sprintf("%s_%s", gStransform, SchanSet[index])
+ index += 1
+od
+
+
+gSfile1 = "d:/temp/kord.wav"
+gSfile2 = "d:/temp/drive.wav"
+
+instr load1
+ gitwst_instanceindex = 0
+ schedule("twst_loadfile", 0, 1, 0, gSfile1)
+endin
+
+instr load2
+ gitwst_instanceindex = 1
+ schedule("twst_loadfile", 0, 1, 0, gSfile2)
+endin
+
+instr boot
+ schedule("load1", 0, 1)
+ schedule("load2", 1, 1)
+ schedule("audition", 2, 2)
+endin
+
+instr apply
+ schedule("twst_commit", 0, -1, 0, 0, 1, -1, gStransform, 0, 0, 0)
+ turnoff
+endin
+
+instr playback
+ schedule("twst_audition", 0, p3, 0, 0, 1, -1, "", 0)
+ turnoff
+endin
+
+instr audition
+ gitwst_instanceindex = 0
+ schedule("twst_audition", 0, p3, 0, 0, 1, -1, gStransform, 0)
+ turnoff
+endin
+
+
+/*
+gStransform = "twst_tf_freqshift1"
+gStransform = "twst_tfi_sndwarp"
+
+SchanSet[] fillarray "timescale", "pitchscale", "winsize", "randwin", "overlap", "wintype", "shift"
+ichanset[] fillarray 1.5, 1, 4410, 441, 4, 0, 900
+index = 0
+while (index < lenarray(ichanset)) do
+ chnset ichanset[index], sprintf("%s_%s", gStransform, SchanSet[index])
+ index += 1
+od
+
+
+gSfile = "d:/temp/sinetest.wav"
+
+instr boot
+ schedule("twst_loadfile", 0, 1, 0, gSfile)
+ schedule("audition", 1, 2)
+ schedule("apply", 3, 1)
+ schedule("playback", 4, 2)
+endin
+
+instr apply
+ schedule("twst_commit", 0, -1, 0, 0, 1, -1, gStransform, 0, 0, 0)
+ turnoff
+endin
+
+instr playback
+ schedule("twst_audition", 0, p3, 0, 0, 1, -1, "", 0)
+ turnoff
+endin
+
+instr audition
+ schedule("twst_audition", 0, p3, 0, 0, 1, -1, gStransform, 0)
+ turnoff
+endin
+*/
+</CsInstruments>
+<CsScore>
+i"boot" 0 10
+</CsScore>
+</CsoundSynthesizer> \ No newline at end of file
diff --git a/site/app/twist/_unlive/twist_instance_WIP.js b/site/app/twist/_unlive/twist_instance_WIP.js
new file mode 100644
index 0000000..1c56cb6
--- /dev/null
+++ b/site/app/twist/_unlive/twist_instance_WIP.js
@@ -0,0 +1,350 @@
+var TwistInstance = function(index, twist) {
+ var self = this;
+ this.appdata = appdata;
+ this.waveform = null;
+ var waveformFile;
+ var waveformTab;
+ this.onPlays = [];
+ var sr = 44100;
+ var undoLevel;
+
+ function pushOperationLog(operation) {
+ var max = twist.storage.commitHistoryLevel;
+ if (!max) {
+ twist.storage.commitHistoryLevel = max = 16;
+ }
+ if (operationLog.length + 1 >= max) {
+ operationLog.shift();
+ }
+ operationLog.push(operation);
+ }
+
+ this.redraw = function() {
+ self.waveform.redraw();
+ };
+
+ this.close = function() {
+ self.waveform.destroy();
+ delete self.waveform;
+ };
+
+ this.show = function() {
+ self.waveform.show();
+ };
+
+ this.movePlayhead = function() {
+
+ };
+
+ function removeInstance(i) {
+ if (i < 0 || i > this.waveforms.length - 2) {
+ return;
+ }
+ self.waveform.destroy();
+ if (instanceIndex == i) {
+ instanceIndex = i + ((i == 0) ? 1 : -1);
+ self.waveform.show();
+ }
+ }
+
+
+ this.undo = function() {
+ if (playing) return;
+ self.waveform.cover(true);
+ operation("twst_undo", globalCallbackHandler, true, null, true);
+ };
+
+ this.cut = function() {
+ if (playing) return;
+ self.waveform.cover(true);
+ operation("twst_cut", globalCallbackHandler, true);
+ };
+
+ this.delete = function() {
+ if (playing) return;
+ self.waveform.cover(true);
+ operation("twst_delete", globalCallbackHandler, true);
+ };
+
+ this.copy = function() {
+ if (playing) return;
+ self.waveform.cover(true);
+ operation("twst_copy", null, true);
+ };
+
+ this.paste = function() {
+ if (playing) return;
+ self.waveform.cover(true);
+ operation("twst_paste", globalCallbackHandler, true);
+ // keep original play position / offset new
+ };
+
+ this.moveToStart = function() {
+ if (playing) return;
+ self.waveform.setSelection(0);
+ };
+
+ this.moveToEnd = function() {
+ if (playing) return;
+ self.waveform.setSelection(1);
+ };
+
+ this.selectAll = function() {
+ if (playing) return;
+ self.waveform.setSelection(0, 1);
+ };
+
+ this.selectNone = function() {
+ if (playing) return;
+ self.waveform.setSelection(0);
+ };
+
+ this.selectToEnd = function() {
+ if (playing) return;
+ self.waveform.alterSelection(null, 1);
+ }
+
+ this.selectFromStart = function() {
+ if (playing) return;
+ self.waveform.alterSelection(0, null);
+ }
+
+ this.play = function() {
+ if (playing) return;
+ auditioning = false;
+ recording = false;
+ operation("twst_play", playPositionHandler(), false, null, true);
+ };
+
+ this.stop = function() {
+ if (!playing) return;
+ self.waveform.cover(false);
+ app.insertScore("twst_stop");
+ };
+
+ function getAutomationData(start, end) {
+ var calls = [];
+ if (!self.currentTransform) return calls;
+ var automations = self.currentTransform.getAutomationData(start, end);
+ if (automations && automations.length > 0) {
+ for (let i in automations) {
+ if (automations[i].type == "modulation") {
+ calls.push(automations[i].data[0] + " \\\"" + automations[i].data[1] + "\\\"");
+ } else if (automations[i].type == "automation") {
+ calls.push("chnset linseg:k(" + automations[i].data + "), \\\"" + automations[i].channel + "\\\"");
+ }
+ }
+ }
+ return calls;
+ }
+
+ function handleAutomation(onready, calls) {
+ if (calls.length == 0) {
+ return onready(0);
+ }
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 1) {
+ onready(1);
+ } else {
+ self.errorHandler("Cannot parse automation data");
+ }
+ });
+
+ var call = [0, 1, cbid];
+ for (let c of calls) {
+ call.push(c);
+ }
+ app.insertScore("twst_automationprepare", call);
+ }
+
+ function compileVariScript(script, onComplete) {
+ var cbid = app.createCallback(function(ndata){
+ onComplete(ndata.status == 1);
+ // should maybe automatically refresh
+ });
+ }
+
+
+ function fftsizeCheck(selected, duration) {
+ if (self.currentTransform) {
+ for (var p in self.currentTransform.parameters) {
+ if (p.indexOf("fftsize") != -1) {
+ var val = self.currentTransform.parameters[p].getValue();
+ var minTime = (val / sr) * 2;
+ if ((selected[1] - selected[0]) * duration < minTime) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ this.record = async function() {
+ if (playing) return;
+ auditioning = false;
+ recording = true;
+ await app.enableAudioInput();
+ errorState = "Recording error";
+ self.waveform.cover(true);
+ var cbid = playPositionHandler();
+ var s = self.waveform.selected;
+ var items = [0, 1, cbid, s[0], s[1], s[2]];
+ app.insertScore("twst_record", items);
+ };
+
+ this.audition = function() {
+ if (playing) return;
+ if (!self.currentTransform) {
+ return self.play();
+ }
+ self.currentTransform.saveState();
+ var s = self.waveform.selected;
+ if (!fftsizeCheck(s, self.waveform.duration)) {
+ return self.errorHandler("Length too short for this transform");
+ }
+
+ auditioning = true;
+ recording = false;
+ errorState = "Playback error";
+ handleAutomation(function(automating){
+ var cbid = playPositionHandler();
+ var items = [
+ 0, 1, cbid, s[0], s[1], s[2],
+ self.currentTransform.instr, automating,
+ elCrossfades[0].val(), elCrossfades[1].val()
+ ];
+ app.insertScore("twst_audition", items);
+ }, getAutomationData(s[0], s[1]));
+
+ };
+
+
+ var scriptStack = [];
+ function applyScript(audition) {
+ if (playing) return;
+ var lastData;
+ var script = scriptStack.shift();
+ if (!script) {
+ setLoadingStatus(false);
+ if (lastData) {
+ console.log("ass", lastData);
+ globalCallbackHandler(lastData);
+ }
+ twist.setPlaying(false);
+ return;
+ }
+
+ if (audition) auditioning = true;
+ twist.setPlaying(true);
+ if (script.type == "operation") {
+ if (audition) {
+ return self.errorHandler("Only transform scripts can be auditioned");
+ }
+ self.waveform.cover(true);
+ onComplete = (script.instr == "twst_copy") ? null : globalCallbackHandler;
+ operation(script.instr, function(ndata){
+ lastData = ndata;
+ self.setPlaying(false);
+ applyScript(audition);
+ }, true, script.selection);
+ } else if (script.type == "transform") {
+ errorState = ((audition) ? "Audition" : "Transform" ) + " commit error";
+ if (!audition) {
+ setLoadingStatus(true, true);
+ }
+
+ for (let channel in script.channels) {
+ app.setControlChannel(channel, script.channels[channel]);
+ }
+ handleAutomation(function(automating){
+ if (audition) {
+ var cbid = playPositionHandler();
+ } else {
+ var cbid = app.createCallback(function(ndata) {
+ lastData = ndata;
+ self.setPlaying(false);
+ applyScript(audition);
+ });
+ }
+ var instr = "twst_" + ((audition) ? "audition" : "commit");
+
+ app.insertScore(instr, [
+ 0, -1, cbid, script.selection[0], script.selection[1], script.selection[2], script.instr, automating, script.crossfades[0], script.crossfades[1]
+ ]);
+ }, script.automation);
+ }
+ }
+
+ this.applyScript = async function(script, audition) {
+ if (playing) return;
+ scriptStack = [];
+ if (Array.isArray(script)) {
+ if (audition) {
+ return self.errorHandler("Only single scripts can be auditioned");
+ }
+ scriptStack = script;
+ } else {
+ scriptStack = [script];
+ }
+ if (self.storage.autosave && !audition) {
+ self.saveFile(null, function() {
+ applyScript(audition);
+ });
+ } else {
+ applyScript(audition);
+ }
+ };
+
+ async function innerCommit() {
+ if (playing) return;
+ if (!self.currentTransform) return;
+ var s = self.waveform.selected;
+ if (!fftsizeCheck(s, self.waveform.duration)) {
+ return self.errorHandler("Length too short for this transform");
+ }
+ watchdog.start("commit");
+ self.setPlaying(true);
+ setLoadingStatus(true, true);
+ var calls = getAutomationData(s[0], s[1]);
+
+ self.currentTransform.saveState();
+ var state = await self.currentTransform.getState();
+ state.type = "transform";
+ state.automation = calls;
+ state.crossfades = [elCrossfades[0].val(), elCrossfades[1].val()];
+ state.selection = [s[0], s[1], s[2]];
+ state.instanceIndex = instanceIndex;
+ pushOperationLog(state);
+
+ handleAutomation(function(automating){
+ var cbid = app.createCallback(function(ndata) {
+ watchdog.stop();
+ setLoadingStatus(false);
+ self.setPlaying(false);
+ if (ndata.status > 0) {
+ globalCallbackHandler(ndata);
+ } else {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file is too large";
+ }
+ self.errorHandler(text);
+ }
+ });
+ errorState = "Transform commit error";
+ app.insertScore("twst_commit", [0, -1, cbid, s[0], s[1], s[2], self.currentTransform.instr, automating, state.crossfades[0],state.crossfades[1]]);
+ }, calls);
+ }
+
+ this.commit = async function() {
+ if (self.storage.autosave) {
+ self.saveFile(null, function() {
+ innerCommit();
+ });
+ } else {
+ innerCommit();
+ }
+ };
+
+}; // end twist \ No newline at end of file
diff --git a/site/app/twist/_unlive/twist_instance_separation_WIP.js b/site/app/twist/_unlive/twist_instance_separation_WIP.js
new file mode 100644
index 0000000..3805807
--- /dev/null
+++ b/site/app/twist/_unlive/twist_instance_separation_WIP.js
@@ -0,0 +1,1250 @@
+var NoteData = function() {
+ var self = this;
+ this.data = null;
+ fetch("../base/notedata.json").then(function(r) {
+ r.json().then(function(j) {
+ self.data = j;
+ });
+ });
+};
+
+
+
+
+var OperationWatchdog = function(twist) {
+ var self = this;
+ var active = false;
+ var lastValues = [true, false];
+ var firstActive = true;
+ var checkInterval;
+ var timeoutTime = 2000;
+ var alivetimeoutTime = 2000;
+ var context;
+
+ function crash() {
+ self.stop();
+ twist.sendErrorState("Unhandled exception in " + context);
+ var el = $("#twist_crash").show();
+ var elSr = $("#twist_crash_recovery");
+
+ function doomed() {
+ elSr.empty().append($("<h4 />").text("Sorry, unfortunately your work cannot be saved."));
+ }
+
+ var doomedTimeout = setTimeout(doomed, 6000);
+
+ var cbid = app.createCallback(function(ndata) {
+ if (doomedTimeout) clearTimeout(doomedTimeout);
+
+ if (!ndata.left && !ndata.right) {
+ return doomed();
+ }
+ elSr.empty();
+ var text;
+ var linkLeft = $("<a />").attr("href", "#").text("Download").click(function(e){
+ e.preventDefault();
+ twist.downloadFile("/crashL.wav");
+ });
+ if (ndata.left && !ndata.right) {
+ elSr.append($("<h4 />").text("Your work has been recovered:"));
+ elSr.append(linkLeft);
+ } else {
+ elSr.append($("<h4 />").text("Your work has been recovered as separate left/right channels:"));
+ linkLeft.text("Download left channel").appendTo(elSr);
+ elSr.append("<br />");
+ var linkRight = $("<a />").attr("href", "#").text("Download right channel").click(function(e){
+ e.preventDefault();
+ twist.downloadFile("/crashR.wav");
+ }).appendTo(elSr);
+ }
+
+ });
+ app.getCsound().compileOrc("iwrittenL = 0\niwrittenR = 0\nif (gitwst_bufferL[gitwst_instanceindex] > 0) then\niwrittenL ftaudio gitwst_bufferL[gitwst_instanceindex], \"/crashL.wav\", 14\nendif\nif (gitwst_bufferR[gitwst_instanceindex] > 0) then\niwrittenR ftaudio gitwst_bufferR[gitwst_instanceindex], \"/crashR.wav\", 14\nendif\nio_sendstring(\"callback\", sprintf(\"{\\\"cbid\\\":" + cbid + ",\\\"left\\\":%d,\\\"right\\\":%d}\", iwrittenL, iwrittenR))\n");
+ }
+
+ function checkAlive() {
+ var alive = false;
+ var aliveTimeout = setTimeout(crash, alivetimeoutTime);
+ var cbid = app.createCallback(function(){
+ clearTimeout(aliveTimeout);
+ alive = true;
+ });
+ app.insertScore("twst_checkalive", [0, 1, cbid]);
+ }
+
+ this.start = function(startContext) {
+ active = true;
+ context = startContext;
+ firstActive = true;
+ lastValues = [true, false];
+ if (checkInterval) clearInterval(checkInterval);
+ checkInterval = setInterval(function() {
+ if (lastValues[0] === lastValues[1]) {
+ checkAlive();
+ }
+ }, timeoutTime);
+ };
+
+ this.setActive = function(value) {
+ if (!active) return;
+ if (firstActive) {
+ firstActive = false;
+ } else {
+ lastValues[0] = lastValues[1];
+ }
+ lastValues[1] = value;
+ };
+
+ this.stop = function() {
+ active = false;
+ firstActive = true;
+ lastValues = [true, false];
+ if (checkInterval) clearInterval(checkInterval);
+ };
+};
+
+var Twist = function(appdata) {
+ var self = this;
+ var audioTypes = ["audio/mpeg", "audio/mp4", "audio/ogg", "audio/vorbis", "audio/x-flac","audio/aiff","audio/x-aiff", "audio/vnd.wav", "audio/wave", "audio/x-wav", "audio/wav", "audio/flac"];
+ var maxsize = 1e+8; // 100 MB
+ this.currentTransform = null;
+ var errorState;
+ var instanceIndex = 0;
+ this.appdata = appdata;
+ this.instances = [];
+ var playheadInterval;
+ var latencyCorrection = 100;
+ var playing = false;
+ var auditioning = false;
+ var scope;
+ var recording = false;
+ var elCrossfades = [];
+ this.onPlays = [];
+ var elToolTip = $("<div />").addClass("tooltip").appendTo($("body"));
+ this.audioContext = null;
+ var operationLog = [];
+ this.noteData = new NoteData();
+ var topMenu = new TopMenu(self, topMenuData, $("#twist_menubar"));
+ this.storage = localStorage.getItem("twist");
+ this.watchdog = new OperationWatchdog(self);
+
+ if (self.storage) {
+ self.storage = JSON.parse(self.storage);
+ } else {
+ self.storage = {};
+ }
+
+ this.tooltip = {
+ show: function(event, text) {
+ var margin = 100;
+ elToolTip.text(text).css("opacity", 0.9);
+
+ if (event.pageX >= window.innerWidth - margin) {
+ elToolTip.css({left: window.innerWidth - (margin * 2) + "px"});
+ } else {
+ elToolTip.css({left: (event.pageX + 20) + "px"});
+ }
+
+ if (event.pageY >= window.innerHeight - margin) {
+ elToolTip.css({top: window.innerHeight - (margin * 2) + "px"});
+ } else {
+ elToolTip.css({top: (event.pageY - 15) + "px"});
+ }
+
+ },
+ hide: function() {
+ elToolTip.css("opacity", 0);
+ }
+ };
+
+ this.setPlaying = function(state) {
+ if (playing == state) return;
+ playing = state;
+ for (var o of self.onPlays) {
+ o(playing, auditioning, recording);
+ }
+ if (self.currentTransform) {
+ self.currentTransform.setPlaying(state);
+ }
+ if (scope) {
+ scope.setPlaying(state);
+ }
+ };
+
+ this.saveStorage = function() {
+ localStorage.setItem("twist", JSON.stringify(self.storage));
+ };
+
+ function lastOperation() {
+ return operationLog[operationLog.length - 1];
+ }
+
+ function pushOperationLog(operation) {
+ var max = self.storage.commitHistoryLevel;
+ if (!max) {
+ self.storage.commitHistoryLevel = max = 16;
+ }
+ if (operationLog.length + 1 >= max) {
+ operationLog.shift();
+ }
+ operationLog.push(operation);
+ }
+
+
+ function showLoadNewPrompt() {
+ var elNewFile = $("<div />").css({"font-size": "var(--fontSizeDefault)"});
+ elNewFile.append($("<h3 />").text("Drag an audio file here to load")).append($("<p />").text("or"));
+
+ $("<h4 />").text("Create an empty file").css("cursor", "pointer").appendTo(elNewFile).click(function() {
+ elNewFile.show();
+ });
+
+ var tpDuration = new TransformParameter(null, {name: "Duration", min: 0.1, max: 60, dfault: 10, automatable: false, fireChanges: false}, null, null, twist);
+
+ var tpChannels = new TransformParameter(null, {name: "Channels", min: 1, max: 2, dfault: 2, step: 1, automatable: false, fireChanges: false}, null, null, twist);
+
+ var tpName = new TransformParameter(null, {name: "Name", type: "string", dfault: "New file", fireChanges: false}, null, null, twist);
+
+ var tb = $("<tbody />");
+ $("<table />").append(tb).css("margin", "0 auto").appendTo(elNewFile);
+ tb.append(tpDuration.getElementRow(true)).append(tpChannels.getElementRow(true)).append(tpName.getElementRow(true));
+
+ $("<button />").text("Create").appendTo(elNewFile).click(function() {
+ var name = tpName.getValue();
+ if (name.trim() == "") {
+ name = "New file";
+ }
+ var cbid = app.createCallback(async function(ndata) {
+ self.waveformTab.text(name);
+ await globalCallbackHandler(ndata);
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = name;
+ setLoadingStatus(false);
+ });
+ self.hidePrompt();
+ setLoadingStatus(true, false, "Creating");
+ app.insertScore("twst_createempty", [0, 1, cbid, tpDuration.getValue(), tpChannels.getValue()]);
+ });
+
+ self.showPrompt(elNewFile, null, true);
+ }
+
+ this.toggleScope = function(noSaveState) {
+ var height;
+ var top;
+ var state;
+ if (!scope) {
+ state = true;
+ height = "60%";
+ top = "40%";
+ var elScope = $("<div />").addClass("twist_scope").appendTo($("#twist_waveforms"));
+ var type = (self.storage.scopeType) ? self.storage.scopeType : "frequency";
+ scope = new Analyser(
+ type, self, elScope, app
+ );
+ } else {
+ state = false;
+ scope.remove();
+ delete scope;
+ scope = null;
+ height = "100%";
+ top = "0px";
+ }
+
+ if (!noSaveState) {
+ self.storage.showScope = state;
+ self.saveStorage();
+ }
+ $(".waveform").css({height: height, top: top});
+ }
+
+ this.createNewInstance = function() {
+ var element = $("<div />").addClass("waveform").appendTo("#twist_waveforms");
+ let index = waveformFiles.length;
+
+ if (index < 0) index = 0;
+ waveformTabs.push(
+ $("<td />").text("New file").click(function() {
+ if (self.isPlaying()) return;
+ self.waveform = index;
+ }).addClass("wtab_selected").appendTo("#twist_waveform_tabs")
+ );
+ undoLevels.push(0);
+ self.waveforms.push(
+ new Waveform({
+ target: element,
+ latencyCorrection: latencyCorrection,
+ showcrossfades: true,
+ crossFadeWidth: 1,
+ timeBar: true,
+ markers: [
+ {preset: "selectionstart"},
+ {preset: "selectionend"},
+ ]
+ })
+ );
+ showLoadNewPrompt();
+ self.waveform = index;
+ };
+
+
+
+
+
+ this.removeInstance = function(i) {
+ if (!i) i = instanceIndex;
+ if (i < 0 || i > this.instances.length - 1) {
+ return;
+ }
+ self.instances[instanceindex].close();
+ if (instanceIndex == i) {
+ instanceIndex = i + ((i == 0) ? 1 : -1);
+ self.instances[instanceIndex].show();
+ }
+ };
+
+ t
+
+ var remoteSessionID;
+ var remoteSending = false;
+ this.sendErrorState = async function (errorText) {
+ if (remoteSending) return;
+ remoteSending = true;
+ var data = {
+ request_type: "LogError",
+ error: {
+ text: errorText,
+ lastOperation: lastOperation()
+ }
+ };
+
+ if (self.currentTransform) {
+ var state = await self.currentTransform.getState();
+ data.error.transformState = state;
+ }
+
+ if (remoteSessionID) {
+ data.session_id = remoteSessionID;
+ }
+ var resp = await fetch("/service/", {
+ method: "POST",
+ headers: {
+ "Content-type": "application/json"
+ },
+ body: JSON.stringify(data)
+ });
+ var json = await resp.json();
+ if (json.session_id && !remoteSessionID) {
+ remoteSessionID = json.session_id;
+ }
+ remoteSending = false;
+ }
+
+ this.errorHandler = function(text, onComplete) {
+ var errorText = (!text) ? errorState : text;
+ self.sendErrorState(errorText);
+ self.setPlaying(false);
+ self.showPrompt(errorText, onComplete);
+ errorState = null;
+ };
+
+ function playPositionHandler(noPlayhead, onComplete) {
+ function callback(ndata) {
+ if (ndata.status == 1) {
+ self.setPlaying(true);
+ if (!noPlayhead) {
+ watchdog.start("audition");
+ if (playheadInterval) {
+ clearInterval(playheadInterval);
+ }
+ playheadInterval = setInterval(async function(){
+ var val = await app.getControlChannel("playposratio");
+ watchdog.setActive(val);
+ if (val < 0 || val > 1) {
+ clearInterval(playheadInterval);
+ }
+ self.waveform.movePlayhead(val);
+ }, 50);
+ }
+ } else {
+ self.setPlaying(false);
+ if (ndata.status == -1) {
+ self.errorHandler("Not enough processing power to transform in realtime");
+ }
+
+ app.removeCallback(ndata.cbid);
+ if (!noPlayhead) {
+ watchdog.stop();
+ self.waveform.movePlayhead(0);
+ if (playheadInterval) {
+ clearInterval(playheadInterval);
+ }
+ }
+ if (onComplete) onComplete();
+ }
+ }
+ return app.createCallback(callback, true);
+ }
+
+ function operation(instr, oncompleteOrCbidOverride, showLoading, selection, noLogScript) {
+ var s = (selection) ? selection : self.waveform.selected;
+ errorState = "Operation error";
+ if (showLoading) {
+ setLoadingStatus(true);
+ }
+ var cbid;
+ if (!oncompleteOrCbidOverride || typeof(oncompleteOrCbidOverride) == "function") {
+ cbid = app.createCallback(function(ndata) {
+ self.waveform.cover(false);
+ if (oncompleteOrCbidOverride) {
+ oncompleteOrCbidOverride(ndata);
+ } else if (ndata.status && ndata.status <= 0) {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file is too large";
+ }
+ self.errorHandler(text);
+ }
+ if (showLoading) {
+ setLoadingStatus(false);
+ }
+ });
+ } else {
+ cbid = oncompleteOrCbidOverride;
+ }
+ if (!noLogScript) {
+ pushOperationLog({type: "operation", instr: instr, selection: s, instanceIndex: instanceIndex});
+ }
+ app.insertScore(instr, [0, 1, cbid, s[0], s[1], s[2]]);
+ }
+
+ this.isPlaying = function() {
+ return playing;
+ };
+
+
+
+ this.pasteSpecial = function() {
+ if (playing) return;
+ var elPasteSpecial = $("<div />");
+ elPasteSpecial.append($("<h4 />").text("Paste special"));
+ var def = {
+ instr: "twst_pastespecial",
+ parameters: [
+ {name: "Repetitions", channel: "repetitions", min: 1, max: 40, step: 1, dfault: 1, automatable: false},
+ {name: "Mix paste", channel: "mixpaste", step: 1, dfault: 0, automatable: false},
+ {name: "Mix crossfade", channel: "mixfade", automatable: false, conditions: [{channel: "mixpaste", operator: "eq", value: 1}]}
+ ]
+ };
+ var tf = new Transform(elPasteSpecial, def, self);
+
+ $("<button />").text("Paste").click(function(){
+ self.hidePrompt();
+ self.waveform.cover(true);
+ operation("twst_pastespecial", globalCallbackHandler, true);
+ }).appendTo(elPasteSpecial);
+
+ $("<button />").text("Cancel").click(function(){
+ self.hidePrompt();
+ }).appendTo(elPasteSpecial);
+ self.showPrompt(elPasteSpecial, null, true);
+
+ };
+
+ this.developerConsole = function() {
+ $("#twist_developer").show();
+ $("#twist_inject_devcsound").click(async function() {
+ var code = $("#twist_devcsound").val();
+ var result = await app.compileOrc(code);
+ if (result == 0) {
+ if (!self.storage.develop) {
+ self.storage.develop = {};
+ }
+ self.storage.develop.csound = code;
+ self.saveStorage();
+ self.showPrompt("Successfully injected Csound code");
+ }
+ });
+ $("#twist_inject_devjson").click(async function() {
+ var code = $("#twist_devjson").val();
+ try {
+ var json = JSON.parse(code);
+ } catch (e) {
+ return self.errorHandler("Cannot parse JSON: " + e);
+ }
+ try {
+ self.loadTransforms(json);
+ } catch (e) {
+ return self.errorHandler("Cannot load transform: " + e);
+ }
+ if (!self.storage.develop) {
+ self.storage.develop = {};
+ }
+ self.storage.develop.json = code;
+ self.saveStorage();
+ self.showPrompt("Successfully injected transform definition");
+ });
+ $("#twist_exit_devcode").click(async function() {
+ $("#twist_developer").hide();
+ });
+ };
+
+ this.play = function() {
+ if (playing) return;
+ auditioning = false;
+ recording = false;
+ operation("twst_play", playPositionHandler(), false, null, true);
+ };
+
+ this.stop = function() {
+ if (!playing) return;
+ self.waveform.cover(false);
+ app.insertScore("twst_stop");
+ };
+
+ function formatFileName(name) {
+ if (!name) name = waveformTabs[instanceIndex].text();
+ if (!name.toLowerCase().endsWith(".wav")) {
+ name += ".wav";
+ }
+
+ // HACK TODO: WASM can't overwrite files
+ name = name.substr(0, name.lastIndexOf(".")) + "." + saveNumber + name.substr(name.lastIndexOf("."));
+ saveNumber ++;
+ // END HACK
+ return name;
+ }
+
+ this.downloadFile = async function(path, name) {
+ if (!name) name = formatFileName(name);
+ var content = await app.readFile(path);
+ var blob = new Blob([content], {type: "audio/wav"});
+ var url = window.URL.createObjectURL(blob);
+ var a = $("<a />").attr("href", url).attr("download", name).appendTo($("body")).css("display", "none");
+ a[0].click();
+ setTimeout(function(){
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ app.unlinkFile(path);
+ }, 20000);
+ };
+
+ var saveNumber = 1;
+ this.saveFile = function(name, onComplete) {
+ if (playing) return;
+ if (!name) name = formatFileName(name);
+ var cbid = app.createCallback(async function(ndata){
+ await self.downloadFile("/" + name, name);
+ if (onComplete) onComplete();
+ setLoadingStatus(false);
+ });
+ setLoadingStatus(true, true, "Saving");
+ app.insertScore("twst_savefile", [0, 1, cbid, name]);
+ };
+
+ function getAutomationData(start, end) {
+ var calls = [];
+ if (!self.currentTransform) return calls;
+ var automations = self.currentTransform.getAutomationData(start, end);
+ if (automations && automations.length > 0) {
+ for (let i in automations) {
+ if (automations[i].type == "modulation") {
+ calls.push(automations[i].data[0] + " \\\"" + automations[i].data[1] + "\\\"");
+ } else if (automations[i].type == "automation") {
+ calls.push("chnset linseg:k(" + automations[i].data + "), \\\"" + automations[i].channel + "\\\"");
+ }
+ }
+ }
+ return calls;
+ }
+
+ function handleAutomation(onready, calls) {
+ if (calls.length == 0) {
+ return onready(0);
+ }
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 1) {
+ onready(1);
+ } else {
+ self.errorHandler("Cannot parse automation data");
+ }
+ });
+
+ var call = [0, 1, cbid];
+ for (let c of calls) {
+ call.push(c);
+ }
+ app.insertScore("twst_automationprepare", call);
+ }
+
+ function compileVariScript(script, onComplete) {
+ var cbid = app.createCallback(function(ndata){
+ onComplete(ndata.status == 1);
+ // should maybe automatically refresh
+ });
+ }
+
+
+ function fftsizeCheck(selected, duration) {
+ if (self.currentTransform) {
+ for (var p in self.currentTransform.parameters) {
+ if (p.indexOf("fftsize") != -1) {
+ var val = self.currentTransform.parameters[p].getValue();
+ var minTime = (val / sr) * 2;
+ if ((selected[1] - selected[0]) * duration < minTime) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ this.record = async function() {
+ if (playing) return;
+ auditioning = false;
+ recording = true;
+ await app.enableAudioInput();
+ errorState = "Recording error";
+ self.waveform.cover(true);
+ var cbid = playPositionHandler();
+ var s = self.waveform.selected;
+ var items = [0, 1, cbid, s[0], s[1], s[2]];
+ app.insertScore("twst_record", items);
+ };
+
+ this.audition = function() {
+ if (playing) return;
+ if (!self.currentTransform) {
+ return self.play();
+ }
+ self.currentTransform.saveState();
+ var s = self.waveform.selected;
+ if (!fftsizeCheck(s, self.waveform.duration)) {
+ return self.errorHandler("Length too short for this transform");
+ }
+
+ auditioning = true;
+ recording = false;
+ errorState = "Playback error";
+ handleAutomation(function(automating){
+ var cbid = playPositionHandler();
+ var items = [
+ 0, 1, cbid, s[0], s[1], s[2],
+ self.currentTransform.instr, automating,
+ elCrossfades[0].val(), elCrossfades[1].val()
+ ];
+ app.insertScore("twst_audition", items);
+ }, getAutomationData(s[0], s[1]));
+
+ };
+
+
+ var scriptStack = [];
+ function applyScript(audition) {
+ if (playing) return;
+ var lastData;
+ var script = scriptStack.shift();
+ if (!script) {
+ setLoadingStatus(false);
+ if (lastData) {
+ console.log("ass", lastData);
+ globalCallbackHandler(lastData);
+ }
+ self.setPlaying(false);
+ return;
+ }
+
+ if (audition) auditioning = true;
+ self.setPlaying(true);
+ if (script.type == "operation") {
+ if (audition) {
+ return self.errorHandler("Only transform scripts can be auditioned");
+ }
+ self.waveform.cover(true);
+ onComplete = (script.instr == "twst_copy") ? null : globalCallbackHandler;
+ operation(script.instr, function(ndata){
+ lastData = ndata;
+ self.setPlaying(false);
+ applyScript(audition);
+ }, true, script.selection);
+ } else if (script.type == "transform") {
+ errorState = ((audition) ? "Audition" : "Transform" ) + " commit error";
+ if (!audition) {
+ setLoadingStatus(true, true);
+ }
+
+ for (let channel in script.channels) {
+ app.setControlChannel(channel, script.channels[channel]);
+ }
+ handleAutomation(function(automating){
+ if (audition) {
+ var cbid = playPositionHandler();
+ } else {
+ var cbid = app.createCallback(function(ndata) {
+ lastData = ndata;
+ self.setPlaying(false);
+ applyScript(audition);
+ });
+ }
+ var instr = "twst_" + ((audition) ? "audition" : "commit");
+
+ app.insertScore(instr, [
+ 0, -1, cbid, script.selection[0], script.selection[1], script.selection[2], script.instr, automating, script.crossfades[0], script.crossfades[1]
+ ]);
+ }, script.automation);
+ }
+ }
+
+ this.applyScript = async function(script, audition) {
+ if (playing) return;
+ scriptStack = [];
+ if (Array.isArray(script)) {
+ if (audition) {
+ return self.errorHandler("Only single scripts can be auditioned");
+ }
+ scriptStack = script;
+ } else {
+ scriptStack = [script];
+ }
+ if (self.storage.autosave && !audition) {
+ self.saveFile(null, function() {
+ applyScript(audition);
+ });
+ } else {
+ applyScript(audition);
+ }
+ };
+
+ async function innerCommit() {
+ if (playing) return;
+ if (!self.currentTransform) return;
+ var s = self.waveform.selected;
+ if (!fftsizeCheck(s, self.waveform.duration)) {
+ return self.errorHandler("Length too short for this transform");
+ }
+ watchdog.start("commit");
+ self.setPlaying(true);
+ setLoadingStatus(true, true);
+ var calls = getAutomationData(s[0], s[1]);
+
+ self.currentTransform.saveState();
+ var state = await self.currentTransform.getState();
+ state.type = "transform";
+ state.automation = calls;
+ state.crossfades = [elCrossfades[0].val(), elCrossfades[1].val()];
+ state.selection = [s[0], s[1], s[2]];
+ state.instanceIndex = instanceIndex;
+ pushOperationLog(state);
+
+ handleAutomation(function(automating){
+ var cbid = app.createCallback(function(ndata) {
+ watchdog.stop();
+ setLoadingStatus(false);
+ self.setPlaying(false);
+ if (ndata.status > 0) {
+ globalCallbackHandler(ndata);
+ } else {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file is too large";
+ }
+ self.errorHandler(text);
+ }
+ });
+ errorState = "Transform commit error";
+ app.insertScore("twst_commit", [0, -1, cbid, s[0], s[1], s[2], self.currentTransform.instr, automating, state.crossfades[0],state.crossfades[1]]);
+ }, calls);
+ }
+
+ this.commit = async function() {
+ if (self.storage.autosave) {
+ self.saveFile(null, function() {
+ innerCommit();
+ });
+ } else {
+ innerCommit();
+ }
+ };
+
+ this.createIcon = function(definition) {
+ var state = true;
+ var active = true;
+ function formatPath(i) {
+ return "../base/icon/" + i + ".svg";
+ }
+ var el = $("<img />");
+
+ var obj = {
+ el: el,
+ setState: function(tstate) {
+ if (!definition.icon2) return;
+ state = tstate;
+ if (state) {
+ el.attr("src", formatPath(definition.icon));
+ } else {
+ el.attr("src", formatPath(definition.icon2));
+ }
+
+ },
+ setActive: function(state) {
+ if (state) {
+ el.css("opacity", 1);
+ active = true;
+ } else {
+ el.css("opacity", 0.4);
+ active = false;
+ }
+ }
+ };
+
+ el.addClass("icon").css("opacity", 1).attr("src", formatPath(definition.icon)).on("mouseover", function(event){
+ var label = (!state && definition.label2) ? definition.label2 : definition.label;
+ self.tooltip.show(event, label);
+ }).on("mouseout", function(){
+ self.tooltip.hide();
+ }).click(function(el) {
+ if (active) definition.click(obj);
+ });
+ return obj;
+ }
+
+ function buildWavecontrols() {
+ var el = $("#twist_wavecontrols_inner");
+ var onPlayDisables = [];
+
+ var play = self.createIcon({label: "Play", icon: "play", label2: "Stop", icon2: "stop", click: function(obj){
+ if (self.isPlaying()) {
+ self.stop();
+ } else {
+ self.play();
+ }
+ }});
+ var audition = self.createIcon({label: "Audition", icon: "audition", label2: "Stop", icon2: "stop", click: function(obj){
+ if (self.isPlaying()) {
+ self.stop();
+ } else {
+ self.audition();
+ }
+ }});
+ var crossfade = self.createIcon({label: "Show crossfades", icon: "crossfade", label2: "Hide crossfades", icon2: "hide", click: function(obj){
+ var el = $(".crossfade");
+ if (el.is(":visible")) {
+ obj.setState(true);
+ el.hide();
+ self.storage.showCrossfades = false;
+ elCrossfades[0].val(0).trigger("input");
+ elCrossfades[1].val(0).trigger("input");
+ } else {
+ el.show();
+ obj.setState(false);
+ self.storage.showCrossfades = true;
+ }
+ self.saveStorage();
+ }});
+
+ var record = self.createIcon({label: "Record", icon: "record", label2: "Stop", icon2: "stop", click: function() {
+ if (self.isPlaying()) {
+ self.stop();
+ } else {
+ self.record();
+ }
+ }});
+
+ var items = [
+ {label: "Zoom selection", icon: "zoomSelection", click: function() {self.waveform.zoomSelection();}},
+ {label: "Zoom in", icon: "zoomIn", click: function() {self.waveform.zoomIn();}},
+ {label: "Zoom out", icon: "zoomOut", click: function() {self.waveform.zoomOut();}},
+ {label: "Show all", icon: "showAll", click: function() {self.waveform.setRegion(0, 1);}},
+ {label: "Cut", icon: "cut", disableOnPlay: true, click: self.cut},
+ {label: "Copy", icon: "copy", disableOnPlay: true, click: self.copy},
+ {label: "Paste", icon: "paste", disableOnPlay: true, click: self.paste},
+ {label: "Paste special", icon: "pasteSpecial", disableOnPlay: true, click: self.pasteSpecial},
+ {label: "Rewind", icon: "rewind", disableOnPlay: true, click: self.moveToStart},
+ play,
+ audition,
+ {label: "Commit", icon: "commit", disableOnPlay: true, click: self.commit},
+ record,
+ {label: "Save", icon: "save", disableOnPlay: true, click: self.saveFile},
+ {label: "Script", icon: "script", click: self.scriptEdit},
+ {label: "Developer", icon: "develop", click: self.developerConsole},
+ crossfade
+ ];
+
+ for (let i of items) {
+ var icon;
+ if (i.icon) {
+ icon = self.createIcon(i);
+ if (i.disableOnPlay) {
+ onPlayDisables.push(icon);
+ }
+ } else {
+ icon = i;
+ }
+ $("<td />").append(icon.el).appendTo(el);
+ }
+
+ twist.onPlays.push(async function(playing, auditioning, recording) {
+ if (playing) {
+ if (auditioning) {
+ play.setActive(false);
+ audition.setState(false);
+ record.setActive(false);
+ } else if (recording) {
+ audition.setActive(false);
+ play.setActive(false);
+ record.setState(false);
+ } else {
+ audition.setActive(false);
+ play.setState(false);
+ record.setActive(false);
+ }
+ } else {
+ audition.setActive(true);
+ play.setActive(true);
+ play.setState(true);
+ audition.setState(true);
+ record.setActive(true);
+ record.setState(true);
+ }
+ for (let o of onPlayDisables) {
+ o.setActive(!playing);
+ }
+ });
+
+ for (let e of ["In", "Out"]) {
+ let elRange = $("<input />").addClass("tp_slider").attr("type", "range").attr("min", 0).attr("max", 0.45).attr("step", 0.00001).val(0).on("input", function() {
+ if (e == "In") {
+ self.waveform.crossFadeInRatio = $(this).val();
+ } else {
+ self.waveform.crossFadeOutRatio = $(this).val();
+ }
+ });
+ elCrossfades.push(elRange);
+ $("<td />").addClass("crossfade").append($("<div />").css("font-size", "8pt").text("Crossfade " + e)).append(elRange).appendTo(el);
+ }
+
+ var el = $(".crossfade");
+ if (self.storage.hasOwnProperty("showCrossfades")) {
+ if (self.storage.showCrossfades) {
+ crossfade.setState(false);
+ el.show();
+ } else {
+ crossfade.setState(true);
+ el.hide();
+ }
+ } else {
+ crossfade.setState(false);
+ el.show();
+ }
+
+ }
+
+ this.loadTransforms = function(transform) {
+ if (transform) {
+ var developObj;
+ for (var t in appdata.transforms) {
+ if (appdata.transforms[t].name == "Develop") {
+ developObj = appdata.transforms[t];
+ break;
+ }
+ }
+ if (!developObj) {
+ developObj = {name: "Develop", contents: []};
+ appdata.transforms.push(developObj);
+ } else {
+ for (var c in developObj.contents) {
+ if (developObj.contents[c].name == transform.name) {
+ delete developObj.contents[c];
+ }
+ }
+ }
+ developObj.contents.push(transform);
+ }
+
+ $("#twist_panetree").empty();
+ var ttv = new TransformsTreeView({
+ target: "twist_panetree",
+ items: appdata.transforms
+ }, self);
+ };
+
+ this.showHelp = function() {
+ $("#twist_help").show();
+ };
+
+ this.showAbout = function() {
+ var el = $("<div />");
+ var x = $("<h3 />").text("twist").appendTo(el);
+ $("<p />").text("Version " + appdata.version.toFixed(1)).appendTo(el);
+ $("<p />").css("font-size", "12px").text("By Richard Knight 2024").appendTo(el);
+
+ var skewMax = 30;
+ var skew = 0;
+ var skewDirection = true;
+ var twistInterval = setInterval(function(){
+ if (skewDirection) {
+ if (skew < skewMax) {
+ skew ++;
+ } else {
+ skewDirection = false;
+ }
+ } else {
+ if (skew > -skewMax) {
+ skew --;
+ } else {
+ skewDirection = true;
+ }
+ }
+ x.css("transform", "skewX(" + skew + "deg)");
+ }, 10);
+
+ self.showPrompt(el, function(){
+ clearInterval(twistInterval);
+ });
+ };
+
+ async function handleFileDrop(e, obj) {
+ e.preventDefault();
+ if (!e.originalEvent.dataTransfer && !e.originalEvent.files) {
+ return;
+ }
+ if (e.originalEvent.dataTransfer.files.length == 0) {
+ return;
+ }
+ self.hidePrompt();
+ setLoadingStatus(true, false, "Loading");
+ for (const item of e.originalEvent.dataTransfer.files) {
+ if (!audioTypes.includes(item.type)) {
+ return self.errorHandler("Unsupported file type", showLoadNewPrompt);
+ }
+ if (item.size > maxsize) {
+ return self.errorHandler("File too large", showLoadNewPrompt);
+ }
+ errorState = "File loading error";
+ var content = await item.arrayBuffer();
+ const buffer = new Uint8Array(content);
+ await app.writeFile(item.name, buffer);
+ var cbid = app.createCallback(async function(ndata){
+ await app.unlinkFile(item.name);
+ if (ndata.status == -1) {
+ return self.errorHandler("File not valid", showLoadNewPrompt);
+ } else if (ndata.status == -2) {
+ return self.errorHandler("File too large", showLoadNewPrompt);
+ } else {
+ self.waveformTab.text(item.name);
+ await globalCallbackHandler(ndata);
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = item.name;
+ self.hidePrompt();
+ setLoadingStatus(false);
+ }
+ });
+ app.insertScore("twst_loadfile", [0, 1, cbid, item.name]);
+ }
+ }
+
+ async function globalCallbackHandler(ndata) {
+ if (ndata.status && ndata.status <= 0) {
+ self.errorHandler();
+ return;
+ }
+
+ if (ndata.hasOwnProperty("undolevel")) {
+ self.undoLevel = ndata.undolevel;
+ }
+
+ if (ndata.hasOwnProperty("delete")) {
+ if (typeof(ndata.delete) == "string") {
+ app.unlinkFile(ndata.delete);
+ } else {
+ for (let d of ndata.delete) {
+ app.unlinkFile(d);
+ }
+ }
+ }
+
+ if (ndata.hasOwnProperty("selstart")) {
+ self.waveform.setSelection(ndata.selstart, ndata.selend);
+ }
+
+ if (ndata.hasOwnProperty("waveL")) {
+ self.waveform.cover(true);
+ errorState = "Overview refresh error";
+ var wavedata = [];
+ var duration = ndata.duration;
+ var tbL = await app.getTable(ndata.waveL);
+ wavedata.push(tbL);
+ if (ndata.hasOwnProperty("waveR")) {
+ var tbR = app.getTable(ndata.waveR);
+ wavedata.push(tbR);
+ }
+ self.waveform.setData(wavedata, ndata.duration);
+ self.waveform.cover(false);
+ }
+
+ }
+
+ this.bootAudio = function() {
+ var channelDefaultItems = ["dcblockoutputs", "tanhoutputs", "maxundo"];
+
+ for (var i of channelDefaultItems) {
+ if (self.storage.hasOwnProperty(i)) {
+ app.setControlChannel(i, self.storage[i]);
+ }
+ }
+
+ twist.setLoadingStatus(false);
+
+ if (!self.storage.hasOwnProperty("firstLoadDone")) {
+ self.storage.firstLoadDone = true;
+ self.saveStorage();
+ self.showPrompt($("#twist_welcome").detach().show(), self.createNewInstance);
+ } else {
+ self.createNewInstance();
+ }
+
+ if (self.storage.showScope) {
+ self.toggleScope(true);
+ }
+ };
+
+ this.boot = function() {
+ self.audioContext = new AudioContext();
+ if (self.storage.theme) {
+ self.setTheme(self.storage.theme, true);
+ }
+
+ if (self.storage.hasOwnProperty("showShortcuts")) {
+ if (self.storage.showShortcuts) {
+ $("#twist_wavecontrols_inner").show();
+ } else {
+ $("#twist_wavecontrols_inner").hide();
+ }
+ }
+
+ if (self.storage.develop) {
+ if (self.storage.develop.csound) {
+ $("#twist_devcsound").val(self.storage.develop.csound);
+ }
+ if (self.storage.develop.json) {
+ $("#twist_devjson").val(self.storage.develop.json);
+ }
+ }
+ $("#loading_background").css("opacity", 1).animate({opacity: 0.2}, 1000);
+
+ Object.defineProperty(this, "waveformTab", {
+ get: function() { return waveformTabs[instanceIndex]; },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "otherInstanceNames", {
+ get: function() {
+ var data = {};
+ for (var i in waveformTabs) {
+ if (i != instanceIndex) {
+ data[i] = waveformTabs[i].text();
+ }
+ }
+ return data
+ },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "instanceIndex", {
+ get: function() {
+ return instanceIndex
+ },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "undoLevel", {
+ get: function() {
+ return undoLevels[instanceIndex];
+ },
+ set: function(x) {
+ undoLevels[instanceIndex] = x;
+ }
+ });
+
+ Object.defineProperty(this, "waveform", {
+ get: function() { return self.waveforms[instanceIndex]; },
+ set: function(x) {
+ if (instanceIndex != x) {
+ if (self.waveformTab) {
+ self.waveformTab.removeClass("wtab_selected").addClass("wtab_unselected");
+ }
+ if (self.waveform) {
+ self.waveform.hide();
+ }
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 1) {
+ instanceIndex = x;
+ self.waveformTab.removeClass("wtab_unselected").addClass("wtab_selected");
+ self.waveform.show();
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ } else {
+ self.showPrompt("Error changing instance");
+ }
+ });
+ app.insertScore("twst_setinstance", [0, 1, cbid, x]);
+
+ }
+ }
+ });
+
+ $("#twist_help").click(function() {
+ $(this).hide();
+ });
+
+ $("<td />").text("+").click(function() {
+ self.createNewInstance();
+ }).appendTo("#twist_waveform_tabs").addClass("wtab_selected");
+
+ $("body").on("dragover", function(e) {
+ e.preventDefault();
+ e.originalEvent.dataTransfer.effectAllowed = "all";
+ e.originalEvent.dataTransfer.dropEffect = "copy";
+ return false;
+ }).on("dragleave", function(e) {
+ e.preventDefault();
+ }).on("drop", function(e) {
+ handleFileDrop(e, self);
+ });
+
+ buildWavecontrols();
+ self.loadTransforms();
+ };
+
+}; // end twist
+
+$(function() {
+
+ var csOptions = ["--omacro:TWST_FAILONLAG=1"];
+ window.twist = new Twist(appdata);
+ window.app = new CSApplication({
+ csdUrl: "twist.csd",
+ csOptions: csOptions,
+ onPlay: function () {
+ twist.bootAudio();
+ },
+ errorHandler: twist.errorHandler,
+ ioReceivers: {percent: twist.setPercent}
+ });
+
+ $("#start").click(function() {
+ $("#start").hide();
+ twist.boot();
+ twist.setLoadingStatus(true, false, "Preparing audio engine");
+ app.play(function(text){
+ twist.setLoadingStatus(true, false, text);
+ }, twist.audioContext);
+ });
+
+}); \ No newline at end of file