aboutsummaryrefslogtreecommitdiff
path: root/site/app/twist
diff options
context:
space:
mode:
Diffstat (limited to 'site/app/twist')
-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
-rw-r--r--site/app/twist/developer_documentation.html742
-rw-r--r--site/app/twist/documentation.html181
-rw-r--r--site/app/twist/index.html98
-rw-r--r--site/app/twist/twist.csd19
-rw-r--r--site/app/twist/twist.css309
-rw-r--r--site/app/twist/twist.js1248
-rw-r--r--site/app/twist/twist_ui.js674
-rw-r--r--site/app/twist/version notes.txt11
15 files changed, 8171 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
diff --git a/site/app/twist/developer_documentation.html b/site/app/twist/developer_documentation.html
new file mode 100644
index 0000000..f44f778
--- /dev/null
+++ b/site/app/twist/developer_documentation.html
@@ -0,0 +1,742 @@
+<html>
+<head>
+ <script type="text/javascript" src="https://apps.csound.1bpm.net/code/jquery.js"></script>
+ <style type="text/css">
+ body {
+ font-family: Arial, sans-serif;
+ }
+
+ table {
+ border-collapse: collapse;
+ }
+
+ td {
+ border: 1px solid black;
+ }
+
+ pre {
+ font-family: Monospace, Courier, sans-serif;
+ background-color: #ccffff;
+ }
+
+ #container_overview {
+ background-color: #ddddff;
+ }
+
+ #container_json {
+ background-color: #ddffdd;
+ }
+
+ #container_csound {
+ background-color: #ddffff;
+ }
+
+ #container_examples {
+ background-color: #ffffdd;
+ }
+
+ </style>
+ <script type="text/javascript">
+ var documentation = {};
+ documentation.opcodes = [
+ {
+ name: "twst_param",
+ ins: [
+ ["Sname", "Name of the parameter to obtain"]
+ ],
+ outs: [
+ ["kvalue", "Value of the parameter"]
+ ],
+ description: "Obtain a parameter at k-rate. The name should correspond to the {json(channel)} in the definition for the given transform",
+ exampleJson: {
+ name: "Oscillator",
+ instr: "twst_tf_example_osc",
+ parameters: [
+ {name: "Frequency", min: 20, max: 8000, dfault: 440}
+ ]
+ },
+ exampleCsound: 'aL, aR, ileft, iright twst_getinput\nkfreq twst_param "frequency"\nif (ileft == 1) then\n\t aL oscil 1, kfreq\nendif\nif (iright == 1) then\n\taR oscil 1, kfreq\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_parami",
+ ins: [
+ ["Sname", "Name of the parameter to obtain"]
+ ],
+ outs: [
+ ["ivalue", "Value of the parameter"]
+ ],
+ description: "Obtain a parameter at init time. The name should correspond to the {json(channel)} in the definition for the given transform",
+ exampleJson: {
+ name: "Oscillator",
+ instr: "twst_tf_example_osc",
+ parameters: [
+ {name: "Frequency", min: 20, max: 8000, dfault: 440}
+ ]
+ },
+ exampleCsound: 'ifreq twst_parami "frequency"\naout oscil 1, ifreq\nouts aout, aout'
+ },
+ {
+ name: "twst_tf_isoffline",
+ outs: [
+ ["ioffline", "Whether processing is offline"]
+ ],
+ description: "Get a boolean signifying whether the current operation is a commit (1) or audition (0)",
+ exampleJson: {
+ name: "Reverse",
+ instr: "twst_tfi_example_reverse",
+ parameters: []
+ },
+ exampleCsound: 'ileft, iright, istartsamp, iendsamp, idocut, ilength twst_tf_getstate\nifnL, ifnR twst_tfi_getfn\nioffline twst_tf_isoffline\napos linseg (iendsamp - istartsamp) - 1, ilength, 0\nif (ileft == 1) then\n\tif (ioffline == 1) then\n\t\tifntempL ftgentmp 0, 0, -ftlen(ifnL), -2, 0\n\t\ttableicopy ifntempL, ifnL\n\t\taL table3 apos, ifntempL\n\telse\n\t\taL table3 apos, ifnL\n\tendif\nendif\nif (iright == 1) then\n\tif (ioffline == 1) then\n\t\tifntempR ftgentmp 0, 0, -ftlen(ifnR), -2, 0\n\t\ttableicopy ifntempR, ifnR\n\t\taR table3 apos, ifntempR\n\telse\n\t\taR table3 apos, ifnR\n\tendif\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_getinput",
+ outs: [
+ ["aL", "Left channel"],
+ ["aR", "Right channel"],
+ ["ileft", "Whether the left channel is to be processed"],
+ ["iright", "Whether the right channel is to be processed"]
+ ],
+ description: "Get input audio and channel flags for the current transform process. The transform should utilise ileft and iright, which are set to either 0 or 1, to process the inputs accordingly. If the instrument does not process audio (ie, generates new audio), then this opcode can still be used to obtain ileft and iright. This can also be obtained from {csound(twst_tf_getstate)}",
+ exampleJson: {
+ name: "Gain",
+ instr: "twst_tf_example_gain",
+ parameters: [
+ {name: "Gain"}
+ ]
+ },
+ exampleCsound: 'aL, aR, ileft, iright twst_getinput\nkgain twst_param "gain"\nif (ileft == 1) then\n\taL *= kgain\nendif\nif (iright == 1) then\n\taR *= kgain\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_getfinput",
+ ins: [
+ ["fftsize", "FFT size", "{preset(fftsize)}"]
+ ],
+ outs: [
+ ["fL", "Left channel"],
+ ["fR", "Right channel"],
+ ["aL", "Left channel"],
+ ["aR", "Right channel"],
+ ["ileft", "Whether the left channel is to be processed"],
+ ["iright", "Whether the right channel is to be processed"]
+ ],
+ description: "Get input audio as a PVS stream along with channel flags for the current transform process. The transform should utilise ileft and iright, which are set to either 0 or 1, to process the inputs accordingly. FFT size is optional and obtains the value from {preset(fftsize)} if not specified. Frequency/phase lock is also applied according to the parameter in the preset group {presetgroup(pvsynth)}",
+ exampleJson: {
+ name: "Repitcher",
+ instr: "twst_tf_example_pvscale",
+ parameters: [
+ {name: "Pitch scaling", channel: "scale", min: 0.5, max: 2, dfault: 1},
+ {presetgroup: "pvanal"},
+ {presetgroup: "pvsynth"}
+ ]
+ },
+ exampleCsound: 'fL, fR, aL, aR, ileft, iright twst_getfinput\nkscale twst_param "scale"\nif (ileft == 1) then\n\tfoutL pvscale fL, kscale\n\taL twst_tf_fresynth foutL\nendif\nif (iright == 1) then\n\tfoutL pvscale fL, kscale\n\taR twst_tf_fresynth foutR\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_tf_fresynth",
+ ins: [
+ ["fsig", "Input PVS stream"]
+ ],
+ outs: [
+ ["aout", "Resynthesised audio"]
+ ],
+ description: "Resynthesise the PVS stream provided using the values of parameter preset group {presetgroup(pvsynth)}",
+ exampleJson: {
+ name: "Repitcher",
+ instr: "twst_tf_example_pvscale",
+ parameters: [
+ {name: "Pitch scaling", channel: "scale", min: 0.5, max: 2, dfault: 1},
+ {presetgroup: "pvanal"},
+ {presetgroup: "pvsynth"},
+ {preset: "applymode"}
+ ]
+ },
+ exampleCsound: 'fL, fR, aL, aR, ileft, iright twst_getfinput\nkscale twst_param "scale"\nif (ileft == 1) then\n\tfoutL pvscale fL, kscale\n\taL twst_tf_fresynth foutL\nendif\nif (iright == 1) then\n\tfoutL pvscale fL, kscale\n\taR twst_tf_fresynth foutR\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_tf_pitchscale",
+ outs: [
+ ["kpitchscale", "Pitch scaling ratio (1 = normal; 2 = one octave above)"]
+ ],
+ description: "Obtain the pitch scaling value from the parameter preset group {presetgroup(pitchscale)}",
+ exampleJson: {
+ name: "Repitcher",
+ instr: "twst_tf_example_pvscale",
+ parameters: [
+ {presetgroup: "pvanal"},
+ {presetgroup: "pvsynth"},
+ {presetgroup: "pitchscale"}
+ ]
+ },
+ exampleCsound: 'fL, fR, aL, aR, ileft, iright twst_getfinput\nkscale twst_tf_pitchscale\nif (ileft == 1) then\n\tfoutL pvscale fL, kscale\n\taL twst_tf_fresynth foutL\nendif\nif (iright == 1) then\n\tfoutL pvscale fL, kscale\n\taR twst_tf_fresynth foutR\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_tf_pitchscale_custom",
+ ins: [
+ ["SchanPrepend", "String to prepend the preset channel name with"]
+ ],
+ outs: [
+ ["kpitchscale", "Pitch scaling ratio (1 = normal; 2 = one octave above)"]
+ ],
+ description: "Obtain the pitch scaling value from the parameter preset group {presetgroup(pitchscale)} but with channel names prepended with the specified string, to be used with the {json(channelprepend)} attribute",
+ exampleJson: {
+ name: "Repitcher",
+ instr: "twst_tf_example_pvscale",
+ parameters: [
+ {presetgroup: "pvanal"},
+ {presetgroup: "pvsynth"},
+ {presetgroup: "pitchscale", channelprepend: "custom"}
+ ]
+ },
+ exampleCsound: 'fL, fR, aL, aR, ileft, iright twst_getfinput\nkscale twst_tf_pitchscale \"custom\"\nif (ileft == 1) then\n\tfoutL pvscale fL, kscale\n\taL twst_tf_fresynth foutL\nendif\nif (iright == 1) then\n\tfoutL pvscale fL, kscale\n\taR twst_tf_fresynth foutR\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_tf_freq",
+ outs: [
+ ["kfreq", "Frequency in Hz"]
+ ],
+ description: "Obtain the frequency value at k-rate from the parameter preset group {presetgroup(notefreq)}",
+ exampleJson: {
+ name: "Oscillator",
+ instr: "twst_tf_example_osc",
+ parameters: [
+ {presetgroup: "notefreq"}
+ ]
+ },
+ exampleCsound: 'kfreq twst_tf_freq\naout oscil 1, kfreq\nouts aout, aout'
+ },
+ {
+ name: "twst_tf_freqi",
+ outs: [
+ ["ifreq", "Frequency in Hz"]
+ ],
+ description: "Obtain the frequency value at init time from the parameter preset group {presetgroup(notefreq)}, which should also have the {json(automatable)} attribute set to false",
+ exampleJson: {
+ name: "Oscillator",
+ instr: "twst_tf_example_osc",
+ parameters: [
+ {presetgroup: "notefreq", automatable: false},
+ {preset: "applymode"}
+ ]
+ },
+ exampleCsound: 'ifreq twst_tf_freqi\naout oscil 1, ifreq\nouts aout, aout'
+ },
+ {
+ name: "twst_tf_freq_custom",
+ ins: [
+ ["SchanPrepend", "String to prepend the preset channel name with"]
+ ],
+ outs: [
+ ["kfreq", "Frequency in Hz"]
+ ],
+ description: "Obtain the frequency value at k-rate from the parameter preset group {presetgroup(notefreq)} but with channel names prepended with the specified string, to be used with the {json(channelprepend)} attribute",
+ exampleJson: {
+ name: "Oscillator",
+ instr: "twst_tf_example_osc",
+ parameters: [
+ {presetgroup: "notefreq", channelprepend: "custom"}
+ ]
+ },
+ exampleCsound: 'kfreq twst_tf_freq_custom "custom"\naout oscil 1, kfreq\nouts aout, aout'
+ },
+ {
+ name: "twst_tf_freqi_custom",
+ ins: [
+ ["SchanPrepend", "String to prepend the preset channel name with"]
+ ],
+ outs: [
+ ["ifreq", "Frequency in Hz"]
+ ],
+ description: "Obtain the frequency value at init time from the parameter preset group {presetgroup(notefreq)} but with channel names prepended with the specified string, to be used with the {json(channelprepend)} attribute",
+ exampleJson: {
+ name: "Oscillator",
+ instr: "twst_tf_example_osc",
+ parameters: [
+ {presetgroup: "notefreq", channelprepend: "custom", automatable: false}
+ ]
+ },
+ exampleCsound: 'ifreq twst_tf_freqi_custom "custom"\naout oscil 1, ifreq\nouts aout, aout'
+ },
+ {
+ name: "twst_tf_getwaveform",
+ ins: [
+ ["inumber", "Reference number of the wave", "{preset(wave)}"]
+ ],
+ outs: [
+ ["ifn", "f-table number"]
+ ],
+ description: "Obtain an f-table at init time from the parameter preset {preset(wave)}. This can be overridden with {p(inumber)}, a value which corresponds to the array of available waveforms which is [Sine, Saw, Pulse, Triangle]",
+ exampleJson: {
+ name: "Oscillator",
+ instr: "twst_tf_example_osc",
+ parameters: [
+ {preset: "wave", automatable: false}
+ ]
+ },
+ exampleCsound: 'ifn twst_tf_getwaveform\naout oscil 1, 440, ifn\outs aout, aout'
+ },
+ {
+ name: "twst_tf_getwaveformk",
+ ins: [
+ ["inumber", "Reference number of the wave", "{preset(wave)}"]
+ ],
+ outs: [
+ ["kfn", "f-table number"]
+ ],
+ description: "Obtain an f-table at k-rate from the parameter preset group {preset(wave)}. This can be overridden with inumber, a value which corresponds to the array of available waveforms which is [Sine, Saw, Pulse, Triangle]",
+ exampleJson: {
+ name: "Oscillator",
+ instr: "twst_tf_example_osc",
+ parameters: [
+ {preset: "wave"},
+ {preset: "applymode"}
+ ]
+ },
+ exampleCsound: 'kfn twst_tf_getwaveformk\naout oscilikt 1, 440, kfn\outs aout, aout'
+ },
+ {
+ name: "twst_tf_getwintype",
+ outs: [
+ ["ifn", "f-table number"]
+ ],
+ description: "Obtain an f-table at init time from the parameter preset group {preset(wintype)}. This may be Hanning, Hamming or Half sine as selected in the UI",
+ exampleJson: {
+ name: "Sndwarp",
+ instr: "twst_tfi_exsndwarp",
+ parameters: [
+ {name: "Time scale", channel: "timescale", min: 0.1, max: 10, dfault: 1, automatable: false},
+ {preset: "wintype", automatable: false}
+ ]
+ },
+ exampleCsound: 'ileft, iright, istartsamp, iendsamp, idocut, ilength twst_tf_getstate\nitimescale = twst_parami("timescale")\nifnWindow = twst_tf_getwintype()\np3 = ilength * itimescale\natime linseg 0, p3, ilength\nifnL, ifnR twst_tfi_getfn\nif (ileft == 1) then\n\taL sndwarp 1, atime, 1, ifnL, 0, 4410, 441, 4, ifnWindow, 1\nendif\nif (iright == 1) then\n\taR sndwarp 1, atime, apitchscale, ifnR, 0, 4410, 441, 4, ifnWindow, 1\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_tf_getwintypek",
+ outs: [
+ ["kfn", "f-table number"]
+ ],
+ description: "Obtain an f-table at k-rate from the parameter preset group {preset(wintype)}. This may be Hanning, Hamming or Half sine as selected in the UI",
+ exampleJson: {
+ name: "Wintype",
+ instr: "twst_tf_wintype",
+ parameters: [
+ {preset: "wintype"}
+ ]
+ },
+ exampleCsound: 'kfnWindow = twst_tf_getwintypek()\naout oscil 1, 440\naenv oscilikt 1, 10, kfnWindow\naout *= aenv\nouts aout, aout'
+ },
+ {
+ name: "twst_getcrossinput",
+ outs: [
+ ["aL", "Left channel from instance"],
+ ["aR", "Right channel from instance"],
+ ["ileft", "whether the left channel is selected"],
+ ["iright", "whether the right channel is selected"]
+ ],
+ description: "Obtain an audio stream from the file instance selected in a parameter preset {preset(instance)}. ileft and iright correspond to the selected channels, and the audio is played according to the parameter preset {preset(looptype)}",
+ exampleJson: {
+ name: "Multiplier",
+ instr: "twst_tf_multiplier",
+ inputs: 2,
+ parameters: [
+ {preset: "instance"},
+ {preset: "instanceloop"}
+ ]
+ },
+ exampleCsound: 'aL, aR, ileft, iright twst_getinput\naxL, axR, ixleft, ixright twst_getcrossinput\nif (ileft == 1 && ixleft == 1) then\n\taL *= axL\nendif\nif (iright == 1 && ixright == 1) then\n\taR *= axR\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_getfcrossinput",
+ outs: [
+ ["fL", "Left channel from instance"],
+ ["fR", "Right channel from instance"],
+ ["ileft", "whether the left channel is selected"],
+ ["iright", "whether the right channel is selected"]
+ ],
+ description: "Obtain a PVS stream from the file instance selected in a parameter preset {preset(instance)}. ileft and iright correspond to the selected channels, and the audio is played according to the parameter preset {preset(looptype)}. The FFT size is taken from {preset(fftsize)}",
+ exampleJson: {
+ name: "Morpher",
+ instr: "twst_tf_exmorph",
+ inputs: 2,
+ parameters: [
+ {preset: "instance", name: "Cross instance"},
+ {preset: "instanceloop"},
+ {name: "Amplitude amount", channel: "amp", description: "Amplitude interpolation", dfault: 1, min: 0, max: 1},
+ {name: "Frequency amount", channel: "freq", description: "Frequency interpolation", dfault: 1, min: 0, max: 1},
+ {presetgroup: "pvanal"},
+ {presetgroup: "pvsynth"},
+ {preset: "applymode"}
+ ]
+ },
+ exampleCsound: 'fL, fR, aL, aR, ileft, iright twst_getfinput\nfLo, fRo, ilefto, irighto twst_getfcrossinput\nkamp = twst_param:k("amp")\nkfreq = twst_param:k("freq")\n\nif (ileft == 1 && ilefto == 1) then\n\tfoutL pvsmorph fL, fLo, kamp, kfreq\n\taL twst_tf_fresynth foutL\nendif\n\nif (iright == 1 && irighto == 1) then\n\tfoutR pvsmorph fR, fRo, kamp, kfreq\n\taR twst_tf_fresynth foutR\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_getcrossdata",
+ outs: [
+ ["ifnL", "f-table containing left channel audio"],
+ ["ifnR", "f-table containing right channel audio"],
+ ["istart", "selection start in samples"],
+ ["ilen", "selection length in samples"],
+ ["ileft", "whether the left channel is selected"],
+ ["iright", "whether the right channel is selected"]
+ ],
+ description: "Obtain data from the file instance selected in a parameter preset {preset(instance)}. The f-tables are the complete file, and the start and length correspond to the selected area. ileft and iright also correspond to the selected channels",
+ exampleJson: {
+ name: "Multiplier",
+ instr: "twst_tf_multiplier",
+ inputs: 2,
+ parameters: [
+ {preset: "instance"},
+ {preset: "instanceloop"}
+ ]
+ },
+ exampleCsound: 'ixfnL, ixfnR, ixstart, ixlen, ixleft, ixright twst_getcrossdata\nprint ixlen\naL, aR, ileft, iright twst_getinput\naxL, axR, ixleft, ixright twst_getcrossinput\nif (ileft == 1 && ixleft == 1) then\n\taL *= axL\nendif\nif (iright == 1 && ixright == 1) then\n\taR *= axR\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_tfi_getcrossfn",
+ outs: [
+ ["ifnL", "f-table containing left channel audio"],
+ ["ifnR", "f-table containing right channel audio"],
+ ],
+ description: "Obtain an f-table from the file instance selected in a parameter preset {preset(instance)}, which has been sliced from the original file, based on the selection area. This may be necessary for some opcodes that read from a f-table, such as those that have no control over the read point, when used for convolution or cross synthesis",
+ exampleJson: {
+ name: "Convolve",
+ instr: "twst_tfi_exdconv",
+ inputs: 2,
+ parameters: [
+ {preset: "instance"},
+ {name: "Size ratio", min: 0.00001, max: 1, dfault: 0.1, lagHint: -1, channel: "sizeratio", automatable: false},
+ {presetgroup: "applymode"}
+ ]
+ },
+ exampleCsound: 'aL, aR, ileft, iright twst_getinput \nifnLo, ifnRo twst_tfi_getcrossfn\nisizeratio = twst_parami("sizeratio")\nif (ileft == 1) then\n\taL dconv aL, isizeratio * ftlen(ifnLo), ifnLo\nendif\nif (iright == 1) then\n\taR dconv aR, isizeratio * ftlen(ifnRo), ifnRo\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_tfi_getfn",
+ outs: [
+ ["ifnL", "f-table containing left channel audio"],
+ ["ifnR", "f-table containing right channel audio"],
+ ],
+ description: "Obtain f-tables which have been sliced from the original file, based on the selection area. This may be necessary for some opcodes that read from a f-table, such as those that have no control over the read point",
+ exampleJson: {
+ name: "Sndwarp",
+ instr: "twst_tfi_exsndwarp",
+ parameters: [
+ {name: "Time scale", channel: "timescale", min: 0.1, max: 10, dfault: 1, automatable: false},
+ {preset: "wintype", automatable: false}
+ ]
+ },
+ exampleCsound: 'ileft, iright, istartsamp, iendsamp, idocut, ilength twst_tf_getstate\nitimescale = twst_parami("timescale")\nifnWindow = twst_tf_getwintype()\np3 = ilength * itimescale\natime linseg 0, p3, ilength\nifnL, ifnR twst_tfi_getfn\nif (ileft == 1) then\n\taL sndwarp 1, atime, 1, ifnL, 0, 4410, 441, 4, ifnWindow, 1\nendif\nif (iright == 1) then\n\taR sndwarp 1, atime, apitchscale, ifnR, 0, 4410, 441, 4, ifnWindow, 1\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_setlatencysamples",
+ ins: [
+ ["isamples", "Number of samples latency"]
+ ],
+ description: "Set the expected processing latency in samples. This is then used to offset any writing and output accordingly. This should ideally be used for any buffered or windowed operations. However, {csound(twst_getfinput)} already sets this based on the FFT size",
+ exampleJson: {
+ name: "Hilbert pitch scale",
+ instr: "twst_tf_exhilbertpitchscale",
+ parameters: [
+ {presetgroup: "pitchscale"},
+ {preset: "fftsize"},
+ {preset: "applymode"}
+ ]
+ },
+ exampleCsound: 'aL, aR, ileft, iright twst_getinput\nifftsize = twst_parami("fftsize")\nkscale = twst_tf_pitchscale()\ntwst_setlatencysamples(ifftsize)\n\nif (ileft == 1) then\n\tahL1, ahL2 hilbert2 aL, ifftsize, ifftsize / 4\n\tamL, afmL fmanal ahL1, ahL2\n\taLx oscil amL, afmL * kscale\nendif\nif (iright == 1) then\n\tahR1, ahR2 hilbert2 aR, ifftsize, ifftsize / 4\n\tamR, afmR fmanal ahR1, ahR2\n\taRx oscil amR, afmR * kscale\nendif\nouts aLx, aRx'
+ },
+ {
+ name: "twst_setlatencyseconds",
+ ins: [
+ ["iseconds", "Number of seconds latency"]
+ ],
+ description: "Set the expected processing latency in seconds. This is then used to offset any writing and output accordingly. This should ideally be used for any buffered or windowed operations. However, {csound(twst_getfinput)} already sets this based on the FFT size",
+ exampleJson: {
+ name: "Hilbert pitch scale",
+ instr: "twst_tf_exhilbertpitchscale",
+ parameters: [
+ {presetgroup: "pitchscale"},
+ {preset: "fftsize"},
+ {preset: "applymode"}
+ ]
+ },
+ exampleCsound: 'aL, aR, ileft, iright twst_getinput\nifftsize = twst_parami("fftsize")\nkscale = twst_tf_pitchscale()\ntwst_setlatencyseconds(1 / ifftsize)\n\nif (ileft == 1) then\n\tahL1, ahL2 hilbert2 aL, ifftsize, ifftsize / 4\n\tamL, afmL fmanal ahL1, ahL2\n\taLx oscil amL, afmL * kscale\nendif\nif (iright == 1) then\n\tahR1, ahR2 hilbert2 aR, ifftsize, ifftsize / 4\n\tamR, afmR fmanal ahR1, ahR2\n\taRx oscil amR, afmR * kscale\nendif\nouts aLx, aRx'
+ },
+ {
+ name: "twst_getlatencyseconds",
+ outs: [
+ ["iseconds", "Number of seconds latency"]
+ ],
+ description: "Obtain the expected processing latency in seconds, as previously set by {csound(twst_setlatencysamples)}, {csound(twst_setlatencyseconds)} or {csound(twst_getfinput)}",
+ exampleJson: {
+ name: "Hilbert pitch scale",
+ instr: "twst_tf_exhilbertpitchscale",
+ parameters: [
+ {presetgroup: "pitchscale"},
+ {preset: "fftsize"},
+ {preset: "applymode"}
+ ]
+ },
+ exampleCsound: 'aL, aR, ileft, iright twst_getinput\nifftsize = twst_parami("fftsize")\nkscale = twst_tf_pitchscale()\ntwst_setlatencysamples(ifftsize)\nprint twst_getlatencyseconds()\nif (ileft == 1) then\n\tahL1, ahL2 hilbert2 aL, ifftsize, ifftsize / 4\n\tamL, afmL fmanal ahL1, ahL2\n\taLx oscil amL, afmL * kscale\nendif\nif (iright == 1) then\n\tahR1, ahR2 hilbert2 aR, ifftsize, ifftsize / 4\n\tamR, afmR fmanal ahR1, ahR2\n\taRx oscil amR, afmR * kscale\nendif\nouts aLx, aRx'
+ },
+ {
+ name: "twst_tf_setplayposition",
+ ins: [
+ ["kposition", "Playback position ratio"]
+ ],
+ description: "Override the displayed playback position. The supplied kposition must be between 0 and 1, and corresponds to the selected area being played or auditioned. Normal operation would be equivalent to line(0, p3, 1)",
+ exampleJson: {
+ name: "Direct time sndwarp",
+ instr: "twst_tfi_directsndwarp",
+ parameters: [
+ {name: "Read time", channel: "readtime"},
+ {preset: "wintype", automatable: false}
+ ]
+ },
+ exampleCsound: 'ileft, iright, istartsamp, iendsamp, idocut, ilength twst_tf_getstate\nktime = twst_param:k("readtime")\ntwst_tf_setplayposition ktime\natime = a(ktime * ilength)\nifnWindow = twst_tf_getwintype()\nifnL, ifnR twst_tfi_getfn\nif (ileft == 1) then\n\taL sndwarp 1, atime, 1, ifnL, 0, 4410, 441, 4, ifnWindow, 1\nendif\nif (iright == 1) then\n\taR sndwarp 1, atime, apitchscale, ifnR, 0, 4410, 441, 4, ifnWindow, 1\nendif\nouts aL, aR'
+ },
+ {
+ name: "twst_tf_getstate",
+ outs: [
+ ["ileft", "Whether the left channel is selected"],
+ ["iright", "Whether the right channel is selected"],
+ ["istart", "Start sample number of selection"],
+ ["iend", "End sample number of selection"],
+ ["ilens", "Length of selection in seconds"]
+ ],
+ description: "Obtain details about the current selection. If the source is mono, only ileft will be equal to 1",
+ exampleJson: {
+ name: "Direct time sndwarp",
+ instr: "twst_tfi_directsndwarp",
+ parameters: [
+ {name: "Read time", channel: "readtime"},
+ {preset: "wintype", automatable: false}
+ ]
+ },
+ exampleCsound: 'ileft, iright, istartsamp, iendsamp, idocut, ilength twst_tf_getstate\nktime = twst_param:k("readtime")\ntwst_tf_setplayposition ktime\natime = a(ktime * ilength)\nifnWindow = twst_tf_getwintype()\nifnL, ifnR twst_tfi_getfn\nif (ileft == 1) then\n\taL sndwarp 1, atime, 1, ifnL, 0, 4410, 441, 4, ifnWindow, 1\nendif\nif (iright == 1) then\n\taR sndwarp 1, atime, apitchscale, ifnR, 0, 4410, 441, 4, ifnWindow, 1\nendif\nouts aL, aR'
+ }
+ ];
+
+ function linkage(text) {
+ var re = /{(preset|presetgroup|json|csound)\((.*?)\)}/g;
+ do {
+ match = re.exec(text);
+ if (match) {
+ text = text.replaceAll(match[0], "<a href=\"#" + match[1] + "_" + match[2] + "\">" + match[2] + "</a>");
+ }
+ } while (match);
+ return text;
+ }
+
+ function buildOpcodes() {
+ var elTarget = $("#opcodes");
+ for (let index in documentation.opcodes) {
+ let o = documentation.opcodes[index];
+ var html = ""
+ if (o.outs) {
+ var names = [];
+ for (var x of o.outs) names.push(x[0]);
+ html += "<i>" + names.join(", ") + "</i> "
+ }
+ html += "<b>" + o.name + "</b>";
+ if (o.ins) {
+ var names = [];
+ for (var x of o.ins) {
+ if (x[2]) { // has default
+ names.push("[" + x[0] + "=" + x[2] + "]");
+ } else {
+ names.push(x[0]);
+ }
+ }
+ html += " <i>" + names.join(", ") + "</i>"
+ }
+
+ $("<p />").html(linkage(html)).attr("id", "csound_" + o.name).appendTo(elTarget);
+ if (o.ins || o.outs) {
+ var ul = $("<ul />").appendTo(elTarget);
+ if (o.outs) {
+ for (var x of o.outs) {
+ var html = "<i>" + x[0] + "</i> &nbsp;&nbsp;" + x[1];
+ $("<li />").html(linkage(html)).appendTo(ul);
+ }
+ }
+ if (o.ins) {
+ for (var x of o.ins) {
+ var html = "<i>" + x[0] + "</i> " + x[1];
+ if (x[2]) html += " <i>[default: " + x[2] + "</i>]"
+ $("<li />").html(linkage(html)).appendTo(ul);
+ }
+ }
+ }
+ elTarget.append($("<p />").html(linkage(o.description)));
+
+ if (o.exampleCsound && o.exampleJson) {
+ var cs = "instr " + o.exampleJson.instr + "\n\t$TWST_TRANSFORM\n"
+ cs += o.exampleCsound.replace(/^/gm, "\t");
+ cs += "\nendin"
+ var exampleCsound = $("<pre />").hide().text(cs).attr("id", "csex_" + index);
+ let exampleCsoundShown = false;
+ $("<a />").attr("href", "#").text("Csound example").click(function(e) {
+ e.preventDefault();
+ if (!exampleCsoundShown) {
+ $("#csex_" + index).show();
+ exampleCsoundShown = true;
+ } else {
+ $("#csex_" + index).hide();
+ exampleCsoundShown = false;
+ }
+ }).appendTo($("<p />").appendTo(elTarget));
+ exampleCsound.appendTo(elTarget);
+ var exampleJson = $("<pre />").hide().text(JSON.stringify(o.exampleJson, null, "\t")).attr("id", "jsex_" + index);
+ let exampleJsonShown = false;
+ $("<a />").attr("href", "#").text("Corresponding definition example").click(function(e) {
+ e.preventDefault();
+ if (!exampleJsonShown) {
+ $("#jsex_" + index).show();
+ exampleJsonShown = true;
+ } else {
+ $("#jsex_" + index).hide();
+ exampleJsonShown = false;
+ }
+ }).appendTo($("<p />").appendTo(elTarget));
+ exampleJson.appendTo(elTarget);
+ }
+
+ elTarget.append($("<hr />"));
+ }
+ }
+
+
+ $(function() {
+ buildOpcodes();
+ });
+ </script>
+
+</head>
+<body>
+ <h1>Extending twist</h1>
+ <div id="container_overview">
+ <h2>Overview</h2>
+ Twist can quite easily be extended to feature additional transforms. These can be tested and used on the fly with the <i>developer console</i> within twist, and are encouraged to be submitted for inclusion in the live application which can be done <a href="https://csound.1bpm.net/contact/?type=twist_submit">here</a>, via the <i>Help > Submit transform code</i> menu option, or via the link in the <i>developer console</i><br /><br />
+ In order to write new transforms for twist, familiarity with JSON and Csound is required. The UI components including parameters available to the end user are defined with JSON (detailed in the <a href="#json">transform definition</a> section, and the actual audio processing is defined with Csound code (detailed in the <a href="#csound">audio processing with Csound</a> section. Additional twist opcodes are provided to the developer in order to ease integration.<br />
+ Each transform requires a transform definition as a JSON object, and at least one Csound instrument. While each section describes the API to be used, full examples are provided in the <a href="#opcodes">Csound opcode</a> subsection.
+
+
+ </div><div id="container_json">
+ <h2 id="json">Transform definition with JSON</h2>
+ The transform definition is a JSON object, which should at the very least have the keys <a href="#json_name">name</a> and <a href="#json_instr">instr</a> defined. The possible keys for the top-level transform definition are as follows, and if a default value is applicable it is shown to the right of the equals sign in the name column:
+
+ <table><tbody>
+ <tr><td><h5 id="json_name">name</h5></td><td>Name as seen in the twist user interface</td></tr>
+ <tr><td><h5 id="json_instr">instr</h5></td><td>Csound named instrument, which is to be called to carry out the processing</td></tr>
+ <tr><td><h5>inputs = 1</h5></td><td>Number of input files the transform requires. This defaults to 1 but may be set to 2 for cross-processing transforms and such</td></tr>
+ <tr><td><h5>description = ""</h5></td><td>Description of the transform</td></tr>
+ <tr><td><h5>author = ""</h5></td><td>Author name and other relevant details</td></tr>
+ <tr><td><h5>parameters = []</h5></td><td><a href="#parameter">Parameter definitions</a> in an array</td></tr>
+ <tr><td><h5>twine = false</h5></td><td>Whether the transform will be available as a twine insert. Only <a href="#csound_rule_tfi">transforms using live input</a> can be used for this purpose</td></tr>
+ <tr><td><h5>unstable = false</h5></td><td>Should be set to true if the transform is expected to be unstable and may cause crashes. This will result in a warning displayed when the transform is loaded</td></tr>
+ </tbody></table>
+
+
+ <h4 id="parameter">Parameter definition</h4>
+ The parameter definition is a JSON object, which may have any of the following keys. If min == 0, max == 1 and step == 1, the parameter appears as a checkbox which provides the value 1 when checked and 0 when unchecked. If <a href="#parameter_options">options</a> is supplied, then the parameter appears as a drop-down select box. In other cases the parameter is displayed as a range slider with an adjacent number input box.<br /><br />
+ The only required key for a parameter definition is <a href="#parameter_name">name</a>. In this case, a parameter would be with a range of 0 to 1, with the default step amount and sending on the channel with the lowercase equivalent of the name.<br /><br />
+ The possible keys for a parameter definition are as follows, and if a default value is applicable it is shown to the right of the equals sign in the name column:
+
+ <table><tbody>
+ <tr><td><h5 id="parameter_name">name</h5></td><td>Name of the parameter to be shown in the interface</td></tr>
+ <tr><td><h5>description = ""</h5></td><td>Description of the parameter</td></tr>
+ <tr><td><h5 id="parameter_channel">channel = name.toLowerCase()</h5></td><td>Channel name which should correspond to that which is requested by <a href="#csound_twst_param">twst_param</a> in the <a href="#json_instr">transform instrument</a>. Defaults to the lowercase parameter name</td></tr>
+ <tr><td><h5>min = 0</h5></td><td>Numeric minimum accepted value</td></tr>
+ <tr><td><h5>max = 1</h5></td><td>Numeric maximum accepted value</td></tr>
+ <tr><td><h5>step = 0.0000001</h5></td><td>Incremental allowance of the value, should numeric</td></tr>
+ <tr><td><h5 id="parameter_dfault">dfault = 1</h5></td><td>Numeric default value</td></tr>
+ <tr><td><h5 id="parameter_options">options = null</h5></td><td>Array containing options to be displayed in a drop-down select box. If supplied, the minimum, maximum and step values are redundant. <a href="#parameter_dfault">dfault</a> corresponds to the index of the array to be the default value. If <a href="#parameter_asvalue">asvalue</a> is set, then the value supplied to Csound will be the value provided in the options array; otherwise it will be the index of the value</td></tr>
+ <tr><td><h5 id="parameter_asvalue">asvalue = false</h5></td><td>Whether the selected item from <a href="#parameter_options">options</a> should be provided to Csound as the actual value rather than the array index</td></tr>
+ <tr><td><h5>hidden = false</h5></td><td>Whether the parameter should be hidden. May be useful passing static data from the interface to Csound</td></tr>
+ <tr><td><h5>conditions = null</h5></td><td>An array of <a href="#condition">Condition</a> objects which are all to be met for the parameter to be shown</td></tr>
+ <tr><td><h5>hostrange = false</h5></td><td>For child parameters (namely those in modulations), whether the min, max, step, dfault, options and asvalue attributes should be inherited from the parent</td></tr>
+ <tr><td><h5 id="parameter_preset">preset = null</h5></td><td>The name of a <a href="#preset">preset</a> to be used. Any definition attributes provided by the preset may be overriden</td></tr>
+ <tr><td><h5 id="parameter_presetgroup">presetgroup = null</h5></td><td>The name of a <a href="#presetgroup">presetgroup</a> to be used, which will provide a number of parameters in place of the current definition
+ <tr><td><h5>nameprepend = null</h5></td><td>The string which will be prepended to parameter names, if presetgroup is specified
+ <tr><td><h5>channelprepend = null</h5></td><td>The string which will be prepended to parameter channels, if presetgroup is specified
+ </tbody></table>
+
+ <hr />
+ <h4 id="condition">Condition</h4>
+ The condition definition is a JSON object, which should include all of the following keys:
+ <table><tbody>
+ <tr><td><h5>channel</h5></td><td>Parameter <a href="#parameter_channel">channel</a> to evaluate</td></tr>
+ <tr><td><h5>operator</h5></td><td>Operator type, which may be eq (equal), neq (not equal), lt (less than), gt (greater than), le (less than or equal to) or ge (greater than or equal to)</td></tr>
+ <tr><td><h5>value</h5></td><td>Static value to check against the above</td></tr>
+ </tbody></table>
+
+ <hr />
+ <h4 id="preset">Presets</h4>
+ These are available as values to specify in the <a href="#parameter_presetgroup">presetgroup</a> parameter attribute and alter set the parameter up as follows
+ <table><tbody>
+ <tr><td><h5>amp</h5></td><td>Amplitude slider with min: 0 and max: 1, channel: "amp"</td></tr>
+ <tr><td><h5 id="preset_fftsize">fftsize</h5></td><td>FFT size drop down which may be transparently utilised by <a href="#csound_twst_getfinput">twst_getfinput</a> and <a href="#csound_twst_getfcrossinput">twst_getfcrossinput</a>, or accessed directly via the channel "fftsize" or the specified channel name with <a href="#csound_twst_param">twst_param</a></td></tr>
+ <tr><td><h5 id="preset_wave">wave</h5></td><td>f-table selector which may be transparently utilised by <a href="#csound_twst_tf_getwaveform">twst_tf_getwaveform</a>, <a href="#csound_twst_tf_getwaveformk">twst_tf_getwaveformk</a>, or accessed directly via the channel "wave" or the specified channel name with <a href="#csound_twst_param">twst_param</a> or <a href="#csound_twst_paramk">twst_paramk</a></td></tr>
+ <tr><td><h5>applymode</h5></td><td>Apply mode drop down, which may be Replace, Mix, Modulate or Demodulate. Used internally by twist at the rendering stage</td></tr>
+ <tr><td><h5>note</h5></td><td>MIDI note number drop-down, displaying note names between MIDI note number 21 (A0) and 127 (G#9) and returning the MIDI note number to the channel</td></tr>
+ <tr><td><h5 id="preset_wintype">wintype</h5></td><td>Window type drop-down which may be utilised by <a href="#csound_twst_tf_getwintype">twst_tf_getwintype</a>, <a href="#csound_twst_tf_getwintypek">twst_tf_getwintypek</a> or accessed directly via the channel "wintype" or the specified channel name with <a href="#csound_twst_param">twst_param</a> or <a href="#csound_twst_paramk">twst_paramk</a></td></tr>
+ <tr><td><h5 id="preset_instance">instance</h5></td><td>Drop down selecting a file open in twist, other than that which is currently open. Utilised interally by <a href="#csound_twst_getcrossinput">twst_getcrossinput</a> and <a href="#csound_twst_getfcrossinput">twst_getfcrossinput</a></td></tr>
+ <tr><td><h5 id="preset_instanceloop">instanceloop</h5></td><td>Drop down selecting either None, Forward, Backward or Ping-pong to denote the loop type of the other selected instance, used internally for cross-processing transforms within <a href="#csound_twst_getcrossinput">twst_getcrossinput</a>, <a href="#csound_twst_getfcrossinput">twst_getfcrossinput</a> and <a href="#csound_twst_getfcrossdata">twst_getfcrossdata</a></td></tr>
+ </tbody></table>
+
+ <hr />
+ <h4 id="presetgroup">Preset groups</h4>
+ These are available as values to specify in the <a href="#parameter_presetgroup">presetgroup</a> parameter attribute.
+ <table><tbody>
+ <tr><td><h5>pvanal</h5></td><td>Provides <a href="#preset_fftsize">FFT size</a> and a frequency/phase locking checkbox, used internally in the provision of PVS stream data within <a href="#csound_twst_getfinput">twst_getfinput</a> and <a href="#csound_twst_getfcrossinput">twst_getfcrossinput</a></td></tr>
+ <tr><td><h5>pvresmode</h5></td><td>Provides parameters which control the resynthesis approach as used by <a href="#csound_twst_tf_fresynth">twst_tf_fresynth</a>. A drop down permits selection between overlap-add and additive approaches, with the latter showing several further parameters when selected</td></tr>
+ <tr><td><h5 id="presetgroup_pitchscale">pitchscale</h5></td><td>Provides a scaling mode drop down with semitones or ratio as options. The selected scaling is presented via <a href="#csound_twst_tf_pitchscale">twst_tf_pitchscale</a> as a ratio</td></tr>
+ <tr><td><h5 id="presetgroup_notefreq">notefreq</h5></td><td>Shows an option of selecting a note name from a drop down, or specifying the frequency in Hz. The computed frequency is provided to Csound via <a href="#csound_twst_tf_freq">twst_tf_freq</a> and <a href="#csound_twst_tf_freqi">twst_tf_freqi</a></td></tr>
+ </tbody></table>
+
+
+ </div><div id="container_csound">
+
+ <h2 id="csound">Audio processing with Csound</h2>
+ Audio processing is carried out for the corresponding JSON transform definition by invoking the Csound instrument specified in the <a href="#json_instr">instr</a> key. Multiple Csound instruments and opcodes may be utilised, however it should be noted that the Csound instrument is called using <i>subinstr</i>, and offline/commit processing is carried out using audio rate processing within a k-rate loop. The only known limitation this imposes is that additional/auxilliary instruments may not usually be called from the initial instrument in a way that would affect synchronisation of the offline processing aspect - ie, opcodes such as <i>schedule</i> and <i>event</i> should not be used except in careful circumstances where the synchronisation is respected - for example where the scheduled instrument only complete init time processing, or completes in a single k-cycle. However, auxilliary instruments may be called using <i>subinstr</i>.<br /><br />
+ Instruments can generate audio, utilise direct feed of audio, or access table data directly. The latter is useful if the output should be a different duration to the input. <br />
+ A number of opcodes to ease integration with the UI and transform definition are provided by twist, detailed below with examples in the <a href="#opcodes">Opcode reference</a> subsection.
+ <h3>Rules and style guide</h3>
+ The rules and style guide should be adhered to where appropriate, especially if opcodes are to be submitted for inclusion in the live application.
+ <ul>
+ <li id="csound_rule_tfi">Instruments referenced by the transform definition should be named prepended with <i>twst_tf_</i> if they generate audio or use direct input (obtained with <a href="#csound_twst_getinput">twst_getinput</a> or <a href="#csound_twst_getfinput">twst_getfinput</a>) - or prepended with <i>twst_tfi_</i> if table access is to be used (with <a href="#csound_twst_tfi_getfn">twst_tfi_getfn</a></li>
+ <li>The first line of the instrument must be <i>$TWST_TRANSFORM</i> to mark it as a twist transform</li>
+ <li>Auxilliary instruments and user-defined opcodes should be named prepended with the name of the initial instrument</li>
+ <li>Instruments may generate audio or process audio input obtained from calls to <a href="#csound_twst_getinput">twst_getinput</a>, <a href="#csound_twst_getfinput">twst_getfinput</a> or <a href="#csound_twst_tfi_getfn">twst_tfi_getfn</a></li>
+ <li>Instruments must emit stereo audio signals using the <i>outs</i> opcode. Depending on the processing action, either of the outputs may be a silent signal or the same as the input</li>
+ <li>Instruments should be prepared to process or generate left and right channels according to the <i>ileft</i> and <i>iright</i> values from the call to <a href="#csound_twst_getinput">twst_getinput</a>, <a href="#csound_twst_getfinput">twst_getfinput</a> or <a href="#csound_twst_tf_getstate">twst_tf_getstate</a>. A channel not applicable to the request must still be emitted, but may be a silent signal or the same as the input - the audition/commit process will only use the output for the channel requested by the user in the UI</li>
+ <li>Any global objects created must not persist after the instrument has finished. For example, <i>ftgentmp</i> must be used rather than <i>ftgen</i> - unless <i>ftfree</i> is used on the f-table accordingly</li>
+ <li><i>print</i> opcodes and other console output opcodes should not be used except for debugging purposes</li>
+ <li>0dbfs is set at 1, so anything reliant on this should adhere accordingly</li>
+ <li>Opcodes are limited to those available in the Csound WASM build - this is generally everything, but one noted example of an exclusion is <i>fractalnoise</i>. If otherwise unexplainable errors are encountered, this may be due to an unavailable opcode</li>
+ </ul>
+ <h3>Opcode reference</h3>
+ <div id="opcodes"></div>
+
+ </div><div id="container_examples">
+
+ <h2>Examples from the live application</h2>
+ All of the JSON transform definitions <a href="../twirl/appdata.js">can be seen here</a>, under the <i>transforms</i> key.<br />
+ Csound code in the live application is split across several files which are as follows. Each file generally corresponds to the section in the JSON.
+ <ul>
+ <li><a href="/udo/twist/transforms/amplitude.udo">amplitude</a></li>
+ <li><a href="/udo/twist/transforms/cross_processing.udo">cross_processing</a></li>
+ <li><a href="/udo/twist/transforms/delay.udo">delay</a></li>
+ <li><a href="/udo/twist/transforms/filter.udo">filter</a></li>
+ <li><a href="/udo/twist/transforms/frequency.udo">frequency</a></li>
+ <li><a href="/udo/twist/transforms/amplitude.udo">amplitude</a></li>
+ <li><a href="/udo/twist/transforms/general.udo">general</a></li>
+ <li><a href="/udo/twist/transforms/generate.udo">generate</a></li>
+ <li><a href="/udo/twist/transforms/granular.udo">granular</a></li>
+ <li><a href="/udo/twist/transforms/harmonic.udo">harmonic</a></li>
+ <li><a href="/udo/twist/transforms/reverb.udo">reverb</a></li>
+ <li><a href="/udo/twist/transforms/spectral.udo">spectral</a></li>
+ <li><a href="/udo/twist/transforms/warping.udo">warping</a></li>
+ </ul>
+</body>
+</html> \ No newline at end of file
diff --git a/site/app/twist/documentation.html b/site/app/twist/documentation.html
new file mode 100644
index 0000000..694279e
--- /dev/null
+++ b/site/app/twist/documentation.html
@@ -0,0 +1,181 @@
+<html>
+<head>
+ <script type="text/javascript" src="/code/jquery.js"></script>
+ <style type="text/css">
+ body {
+ font-family: Arial, sans-serif;
+ }
+
+ table {
+ border-collapse: collapse;
+ }
+
+ td {
+ border: 1px solid black;
+ }
+
+ pre {
+ font-family: Monospace, Courier, sans-serif;
+ background-color: #ccffff;
+ }
+
+ </style>
+
+</head>
+<body>
+ <h1>Overview</h1>
+ twist is an audio editor and transformer, inspired by Cooledit, Cecilia, Audition, Mammut, Soundshaper and CDP among others. It provides wave editing functions in addition to unique audio effects and transforms.
+
+ <h1>Concept</h1>
+ twist allows for waveform editing and applying transforms. Transforms include effects and other more involved sound processing techniques. Centrally there is a waveform editor, and to the left is a tree view list of available transforms by category. A transform can be loaded by pressing on the name in the relevant submenu, after which the parameters will be displayed at the bottom of the screen. <br /><br />
+ A transform can be <i>auditioned</i> (previewed) or <i>committed</i> (applied), which will be applicable to the selected region of the waveform, or all of it if nothing is selected.
+ <br /><br />
+ At current, twist can only load and process sounds of up to about 5 minutes in length. Some transforms require extensive computation and may not properly audition - in these cases a message will be shown and playback stopped. As a result, twist is ideal for detailed processing of short sounds which may be then utilised as samples or compositional components in a DAW or other such software.
+
+ <h1>Basic usage</h1>
+ Sounds can be loaded by dragging them into the browser, or new files may be created. When you load twist or create a new instance with the <i>+</i> button to the bottom left of the waveform view, you'll be prompted to drag a file into the browser or create a new file of a fixed duration. <br /><br />
+
+ Once you have a sound ready in twist, you can use the waveform view as you would with a typical waveform editor. Regions can be selected by click/dragging the mouse or using the handles at the top of the waveform view. Zooming can be accomplished with the relevant options under the <i>view</i> menu, or with the shortcut buttons. Typical cut/copy/paste and undo operations are also provided under the <i>edit</i> menu and with keyboard shortcuts. <br /><br />
+
+ Icons may be hovered over to see a tooltip detailing what pressing it will do, while keyboard shortcuts for items are shown on the drop-down menu. <br /><br />
+
+ <table><tbody>
+ <tr>
+ <td><img src="../twirl/icon/showAll.svg"></td>
+ <td>Show all</td>
+ <td>Reset the zoom level so all of the waveform can be seen</td>
+ </tr>
+
+ <tr>
+ <td><img src="../twirl/icon/rewind.svg"></td>
+ <td>Rewind</td>
+ <td>Rewind the playback point and clear the current waveform selection</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/play.svg"></td>
+ <td>Play</td>
+ <td>Play the selected region of the waveform</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/audition.svg"></td>
+ <td>Audition</td>
+ <td>Preview the currently-loaded transform on the selected region of the waveform</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/commit.svg"></td>
+ <td>Commit</td>
+ <td>Apply the currently-loaded transform to the selected region of the waveform</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/record.svg"></td>
+ <td>Record</td>
+ <td>Record live input to the selected region of the waveform</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/cut.svg"></td>
+ <td>Cut</td>
+ <td>Cut the selected region of the waveform to the clipboard</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/copy.svg"></td>
+ <td>Copy</td>
+ <td>Copy the selected region of the waveform to the clipboard</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/paste.svg"></td>
+ <td>Paste</td>
+ <td>Paste the clipboard contents to the playback point or start of the selected waveform region</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/pasteSpecial.svg"></td>
+ <td>Paste special</td>
+ <td>Open a dialog to allow pasting the clipboard contents with extended options such as repeat number and mixing</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/zoomSelection.svg"></td>
+ <td>Zoom selection</td>
+ <td>Zoom the view to the selected region of the waveform</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/zoomIn.svg"></td>
+ <td>Zoom in</td>
+ <td>Zoom in the waveform view</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/zoomOut.svg"></td>
+ <td>Zoom out</td>
+ <td>Zoom out the waveform view</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/showAll.svg"></td>
+ <td>Show all</td>
+ <td>Zoom out to show all of the waveform</td>
+ </tr>
+
+ </tbody></table>
+
+ <h1>Applying transforms</h1>
+ When a transform has been loaded, the parameters at the bottom of the screen can be modified and are applicable to auditioning and committing. Certain parameters may be altered while auditioning, and can also be automated or modulated, while other parameters are only applicable at initialisation of the auditioning process and thus cannot be modified during. <br /><br />
+ A crossfade between the original and transformed audio can be specified with the crossfade in/out sliders underneath the waveform. The applicable crossfade will be shown on the waveform view relative to the region which is to be transformed. <br /><br />
+ At the side of each parameter, there are up to four icons, which are as follows:
+ <table><tbody>
+ <tr>
+ <td><img src="../twirl/icon/reset.svg"></td>
+ <td>Reset</td>
+ <td>Reset the parameter to the default value</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/randomise.svg"></td>
+ <td>Include in randomisation</td>
+ <td>Include the parameter in transform randomisation. Unselected parameters will display the icon in a lighter colour</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/automate.svg"></td>
+ <td>Automate</td>
+ <td>Display spline automation for the parameter to allow a time-varying value to be entered</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/modulate.svg"></td>
+ <td>Modulate</td>
+ <td>Display modulation options for the parameter to allow for a parametric time-varying value</td>
+ </tr>
+ </tbody></table><br />
+
+ Additionally to the top-right there are two icons globally applicable to all parameters:
+ <table><tbody>
+ <tr>
+ <td><img src="../twirl/icon/randomise.svg"></td>
+ <td>Randomise</td>
+ <td>Randomise all parameters that are included in randomisation according to the icon detailed above. Randomisation may also affect modulations, which will consequently display the modulation details for the given parameter</td>
+ </tr>
+ <tr>
+ <td><img src="../twirl/icon/reset.svg"></td>
+ <td>Reset</td>
+ <td>Reset all parameters to the default value, removing modulations and automation</td>
+ </tr>
+ </tbody></table>
+
+ <h2>Modulation</h2>
+ When a parameter has modulation enabled, the normal parameter input will not be shown, and instead an area will appear with modulation settings. This can be closed and the normal parameter input returned to with the following icon aside the parameter row: <br />
+ <img src="../twirl/icon/close.svg">
+ <br /><br />
+ When there is more than one waveform open in twist, cross-adaptive modulations are available in the <i>modulation type</i> drop-down box. Cross-adaptive modulations use analysis of another waveform to inform the value of the parameter. For each cross-adaptive modulation, there is an <i>instance</i> drop-down box to select the other waveform to use as an analysis source (from which the selected region will be used), and <i>cross instance loop type</i> to specify the loop type should the length of the region in the analysis waveform be shorter than that of the current waveform.
+
+ <h2>Automation</h2>
+ When a parameter has automation enabled, the normal parameter input will not be shown, and an overlay atop the waveform with a spline editor will be displayed. As a result, the selection cannot be altered by dragging across the waveform unless the automation is hidden. However, the selection shortcuts and menu options can be used, in addition to the selection handles at the top of the waveform view. Multiple automated parameters can be displayed at once, although only one can be edited. In the case that multiple parameters are automated, the following button aside the parameter row can be used to select it: <br />
+ <img src="../twirl/icon/show.svg"><br />
+ Anywhere within the spline editor can be pressed to add a new point. Existing points can be hovered over to see the parameter name, time and value. <br /><br />
+ At the side of the parameter row, this icon can be pressed to disable the automation:<br />
+ <img src="../twirl/icon/close.svg"><br /><br />
+ At the top right of the transform parameters, the following icon can be used to hide all automation, but keep it enabled:<br />
+ <img src="../twirl/icon/hide.svg"><br />
+ To display the automation again, the select icon next to the parameter should be used.
+
+ <h1>Scripting</h1>
+ Scripting is not currently documented in full but is fairly self-explanatory. By using the <i>script</i> toolbar shortcut, or the <i>Action > Scripting</i> option on the menu, the script page is shown. By using <i>Load last operation</i> or <i>Load all session operations</i>, you can see the transforms and operations you have previously applied. The script then may be edited, saved and/or auditioned/committed again. <br />
+ Scripts may be a single JSON object or an array of JSON objects which will be applied serially. Each of the parameter details are stored including automation and modulation. The <i>selection</i> key contains an array of three numbers: the first and second and normalised values between 0 and 1 specifying the region of the waveform which is to be transformed, and the third designates the channel (0 = left, 1 = right (if applicable), -1 = all applicable). The <i>instanceIndex</i> key and others referencing index are a 0-based index of the loaded waveforms in twist.
+
+ <h1>Developer</h1>
+ New transforms can be developed and tested in twist. Full details and API reference are on the <a href="developer_documentation.html">developer documentation</a> page.
+</body>
+</html> \ No newline at end of file
diff --git a/site/app/twist/index.html b/site/app/twist/index.html
new file mode 100644
index 0000000..4652991
--- /dev/null
+++ b/site/app/twist/index.html
@@ -0,0 +1,98 @@
+<!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/base.js"></script>
+ <script type="text/javascript" src="../twirl/twirl.js"></script>
+ <script type="text/javascript" src="../twirl/appdata.js"></script>
+ <script type="text/javascript" src="../twirl/transform.js"></script>
+ <script type="text/javascript" src="../base/waveform.js"></script>
+ <script type="text/javascript" src="../base/spline-edit.js"></script>
+ <script type="text/javascript" src="../base/analyser.js"></script>
+ <script type="text/javascript" src="twist_ui.js"></script>
+ <script type="text/javascript" src="twist.js"></script>
+ <link rel="stylesheet" href="../twirl/theme.css">
+ <link rel="stylesheet" href="../twirl/twirl.css">
+ <link rel="stylesheet" href="twist.css">
+ <script type="text/javascript">
+ $(twist_startisolated);
+ </script>
+ </head>
+ <body>
+ <div id="twist_start">
+ <div id="twist_start_invoke">
+ <h1>twist</h1>
+ <p>audio transformer</p>
+ <div id="twist_start_invokebig">Press to begin</div>
+ </div>
+ </div>
+
+ <div id="twist">
+ <div id="twist_menubar"></div>
+ <div id="twist_main">
+ <div id="twist_views">
+ <div id="twist_analyser"></div>
+ <div id="twist_waveforms"></div>
+ <div id="twist_splines"></div>
+ </div>
+ <div id="twist_sidepane">
+ <div id="twist_panetree"></div>
+ </div>
+ <div id="twist_controls">
+ <div id="twist_wavecontrols">
+ <table><tbody><tr id="twist_waveform_tabs"></tr><tbody></table>
+ <table><tbody><tr id="twist_wavecontrols_inner"></tr><tbody></table>
+ </div>
+ <div id="twist_controls_inner"></div>
+ </div>
+ </div>
+ <div id="twist_welcome">
+ <h4>Hello</h4>
+ Hover over icons and parameter names to see what they do. Transforms can be selected
+ from the menu on the left; the current file can have the transform auditioned (previewed) or committed (applied). Check out the help and settings for further tips and customisation.<br />
+ At the moment, there is a limitation on files to around five minutes in duration.
+ </div>
+ <div id="twist_script" class="fullscreen_overlay">
+ <h3>Scripting</h3>
+ Scripts can be an individual JSON object or an array of objects in which case they will be committed sequentially. Only single transform scripts can be auditioned.
+ <hr />
+ <textarea id="twist_scriptsource" class="twist_devcode"></textarea>
+ <br />
+ <button id="twist_scriptstop">Stop</button>
+ <button id="twist_scriptaudition" class="twist_scriptbutton">Audition</button>
+ <button id="twist_scriptcommit" class="twist_scriptbutton">Commit</button>
+ <button id="twist_scriptloadlast" class="twist_scriptbutton">Load last</button>
+ <button id="twist_scriptloadall" class="twist_scriptbutton">Load all</button>
+ <button id="twist_scriptcancel" class="twist_scriptbutton">Cancel</button>
+ </div>
+ <div id="twist_developer" class="fullscreen_overlay">
+ <h3>Developer console</h3>
+ Code for transforms can be tested here. The code and definition should follow the guidance and API documentation <a id="twist_developer_documentation" href="developer_documentation.html" target="_blank">provided here.</a> The JSON definition should be a single transform as a JSON object, but mutiple transforms may be loaded individually.<br />
+ Contributions of transforms are warmly welcomed and <a id="twist_developer_submit" href="https://csound.1bpm.net/contact/?type=twist_submit" target="_blank">can be submitted here.</a>
+ <h4>Csound code</h4>
+ <textarea class="twist_devcode" id="twist_devcsound"></textarea>
+ <br /><button id="twist_inject_devcsound">Load Csound orchestra code</button>
+ <hr />
+ <h4>JSON transform definition</h4>
+ <textarea class="twist_devcode" id="twist_devjson"></textarea>
+ <br /><button id="twist_inject_devjson">Load JSON</button>
+ <hr />
+ <button id="twist_exit_devcode">Exit</button>
+ </div>
+ <div id="twist_crash">
+ <h2>twist has crashed.</h2>
+ We are working hard on ironing out all the bugs, but some still occur. To help, details of the last transform you attempted to audition or commit have been sent to the developers.
+ <a href=".">Press here to reload the application.</a>
+ <hr />
+ <div id="twist_crash_recovery">Attempting to recover your work...</div>
+ </div>
+ <div id="twist_hidden_links">
+ <a id="twist_contact" href="https://csound.1bpm.net/contact/?type=general&app=twist" target="_blank">Contact</a>
+ <a id="twist_reportbug" href="https://csound.1bpm.net/contact/?type=report_bug&app=twist" target="_blank">Report bug</a>
+ <a id="twist_documentation" href="documentation.html" target="_blank">Documentation</a>
+ </div>
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/site/app/twist/twist.csd b/site/app/twist/twist.csd
new file mode 100644
index 0000000..ccd4085
--- /dev/null
+++ b/site/app/twist/twist.csd
@@ -0,0 +1,19 @@
+<CsoundSynthesizer>
+<CsOptions>
+-odac
+</CsOptions>
+<CsInstruments>
+sr = 44100
+ksmps = 64
+nchnls = 2
+0dbfs = 1
+seed 0
+nchnls_i = 2
+
+#include "/twist/twist.udo"
+
+</CsInstruments>
+<CsScore>
+f0 z
+</CsScore>
+</CsoundSynthesizer> \ No newline at end of file
diff --git a/site/app/twist/twist.css b/site/app/twist/twist.css
new file mode 100644
index 0000000..935ef88
--- /dev/null
+++ b/site/app/twist/twist.css
@@ -0,0 +1,309 @@
+body {
+ font-family: var(--fontFace);
+ background-color: #000000;
+ color: var(--fgColor1);
+ user-select: none;
+ cursor: arrow;
+}
+
+#twist_hidden_links {
+ display: none;
+}
+
+#twist_crash {
+ font-family: "Nouveau IBM";
+ background-color: #b3240b;
+ color: #e8dedc;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ width: 100%;
+ height: 100%;
+ z-index: 666;
+ user-select: none;
+ cursor: not-allowed;
+ display: none;
+}
+
+#twist_scriptstop {
+ display: none;
+}
+
+#twist_menubar {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ right: 0px;
+ height: 20px;
+ z-index: 6;
+}
+
+a {
+ color: var(--fgColor3);
+ font-weight: bold;
+ text-decoration: none;
+}
+
+#twist_welcome {
+ display: none;
+ font-size: var(--fontSizeDefault);
+}
+
+#twist_main {
+ position: absolute;
+ z-index: 5;
+ background-color: var(--bgColor1);
+ left: 0px;
+ top: 20px;
+ width: 100%;
+ bottom: 0px;
+}
+
+.waveform {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+#twist_views {
+ position: absolute;
+ left: 15%;
+ right: 0px;
+ top: 0px;
+ height: 50%;
+}
+
+#twist_analyser {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ height: 40%;
+ width: 100%;
+ background-color: var(--bgColor1);
+ display: none;
+}
+
+#twist_waveforms {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ bottom: 0px;
+ width: 100%;
+}
+
+#twist_splines {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ bottom: 0px;
+ margin-top: 15px;
+ margin-bottom: 15px;
+ width: 100%;
+ display: none;
+ z-index: 20;
+ background-color: var(--waveformOverlayColor);
+ opacity: 0.5;
+}
+
+.twist_scope {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0px;
+ left: 0px;
+}
+
+.waveform_overlay {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background-color: var(--waveformOverlayColor);
+ opacity: 0.95;
+ left: 0px;
+ top: 0px;
+ z-index: 30;
+}
+
+.waveform_overlay_mid {
+ font-size: 12pt;
+ padding-top: 100px;
+ text-align: center;
+}
+
+
+#twist_sidepane {
+ position: absolute;
+ background-color: var(--bgColor3);
+ left: 0px;
+ top: 0px;
+ height: 100%;
+ width: 15%;
+ overflow-y: scroll;
+ overflow-x: auto;
+ scrollbar-color: var(--scrollbarColor);
+}
+
+#twist_controls {
+ position: absolute;
+ background-color: var(--bgColor1);
+ left: 15%;
+ top: 50%;
+ bottom: 0px;
+ right: 0px;
+}
+
+#twist_controls_inner {
+ position: absolute;
+ background-color: var(--bgColor4);
+ left: 0px;
+ top: 70px;
+ bottom: 0px;
+ width: 100%;
+ overflow-y: scroll;
+ overflow-x: auto;
+ scrollbar-color: var(--scrollbarColor);
+}
+
+#twist_wavecontrols {
+ position: absolute;
+ overflow: hidden;
+ left: 0px;
+ top: 0px;
+ height: 70px;
+ width: 100%;
+}
+
+#twist_waveform_tabs {
+ cursor: pointer;
+}
+
+#twist_help {
+ z-index: 60;
+ position: absolute;
+ background-color: var(--bgColor1);
+ opacity: 0.9;
+ width: 100%;
+ height: 100%;
+ top: 0px;
+ left: 0px;
+ overflow-y: scroll;
+ overflow-x: auto;
+ scrollbar-color: var(--scrollbarColor);
+ display: none;
+ cursor: pointer;
+}
+
+#twist_panetree {
+ font-size: var(--fontSizeDefault);
+ font-family: var(--fontFace);
+}
+
+button {
+ border: var(--buttonBorder);
+ font-color: var(--fgColor3);
+ color: var(--fgColor3);
+ background-color: var(--bgColor4);
+ font-size: var(--fontSizeDefault);
+ padding: 2px;
+ font-family: var(--fontFace);
+ white-space: nowrap;
+}
+
+select {
+ background-color: var(--bgColor2);
+ color: var(--fgColor2);
+}
+
+input[type="checkbox"] {
+ accent-color: var(--bgColor1);
+}
+
+.automate_container {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 125;
+ display: none;
+}
+
+.twist_devcode {
+ background-color: var(--codeBgColor);
+ color: var(--codeFgColor);
+ font-size: var(--codeFontSize);
+ font-family: var(--codeFontFace);
+ width: 80%;
+ height: 20%;
+}
+
+#twist_scriptsource {
+ height: 60%;
+ overflow-y: auto;
+ overflow-x: hide;
+}
+
+#twist_developer {
+ overflow-y: auto;
+ overflow-x: hide;
+}
+
+.fullscreen_overlay {
+ position: fixed;
+ display: none;
+ z-index: 60;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ background-color: var(--bgColor3);
+ font-size: var(--fontSizeDefault);
+ opacity: 0.96;
+}
+
+#twist_start {
+ z-index: 300;
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ background-color: var(--bgColor2);
+ cursor: pointer;
+}
+
+#twist_start_invoke {
+ z-index: 202;
+ text-align: centre;
+ margin: 0px;
+ position: absolute;
+ top: 20%;
+ left: 20%;
+ width: 60%;
+ height: 40%;
+}
+
+#twist_start_invokebig {
+ font-size: 48pt;
+}
+
+.wtab_selected {
+ font-size: var(--fontSizeDefault);
+ font-weight: bold;
+ background-color: var(--tabSelectedBgColor);
+ color: var(--tabSelectedFgColor);
+ padding: 3px;
+ border: 1px solid black;
+ border-top: 0;
+}
+
+.wtab_unselected {
+ font-size: var(--fontSizeDefault);
+ background-color: var(--tabUnselectedBgColor);
+ color: var(--tabUnselectedFgColor);
+ font-weight: normal;
+ padding: 3px;
+ border: 1px solid black;
+}
+
diff --git a/site/app/twist/twist.js b/site/app/twist/twist.js
new file mode 100644
index 0000000..a7a248c
--- /dev/null
+++ b/site/app/twist/twist.js
@@ -0,0 +1,1248 @@
+var OperationWatchdog = function(twist) {
+ var self = this;
+ var active = false;
+ var lastValues = [true, true];
+ var firstActive = true;
+ var checkInterval;
+ var timeoutTime = 30000;
+ var alivetimeoutTime = 3500;
+ var context;
+
+ function crash() {
+ self.stop();
+ twirl.sendErrorState({text: "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, true];
+ 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, true];
+ if (checkInterval) clearInterval(checkInterval);
+ };
+};
+
+var Twist = function() {
+ twirl.init();
+ var self = this; // TODO deprecate this in favour of below
+ var twist = this;
+ this.storage = localStorage.getItem("twist");
+ if (self.storage) {
+ self.storage = JSON.parse(self.storage);
+ } else {
+ self.storage = {
+ dcblockoutputs: 1,
+ tanhoutputs: 1,
+ maxundo: 2,
+ showShortcuts: 1,
+ commitHistoryLevel: 16,
+ scopeType: 0
+ };
+ }
+
+ twist.version = 1;
+ this.currentTransform = null;
+ var errorState;
+ var instanceIndex = 0;
+ this.waveforms = [];
+ var waveformFiles = [];
+ var waveformTabs = [];
+ var waveformLoaded = [];
+ this.playheadInterval = null;
+ var playing = false;
+ var auditioning = false;
+ var recording = false;
+ this.onPlays = [];
+ this.onInstanceChangeds = [];
+ this.operationLog = [];
+ var sr = 44100;
+ var undoLevels = [];
+ var onSave;
+ this.visible = false;
+ this.playbackLoop = false;
+ this.twine = null;
+ this.hasClipboard = false;
+ this.watchdog = new OperationWatchdog(twist);
+ this.ui = new TwistUI(twist);
+
+ this.setPlaying = function(state) {
+ if (playing == state) return;
+ playing = state;
+ for (var o of twist.onPlays) {
+ o(playing, auditioning, recording);
+ }
+ if (twist.currentTransform) {
+ twist.currentTransform.setPlaying(state);
+ }
+ twist.ui.setPlaying(state);
+
+ if (!state) {
+ twist.watchdog.stop();
+ twist.waveform.movePlayhead(0);
+ if (twist.playheadInterval) {
+ clearInterval(twist.playheadInterval);
+ }
+ }
+ };
+
+ this.saveStorage = function() {
+ localStorage.setItem("twist", JSON.stringify(twist.storage));
+ };
+
+ this.lastOperation = function() {
+ return twist.operationLog[twist.operationLog.length - 1];
+ };
+
+ this.clearOperationLog = function() {
+ twist.operationLog = [];
+ };
+
+ async function pushOperationLog(operation, logChannels) {
+ var max = twist.storage.commitHistoryLevel;
+ if (!max) {
+ twist.storage.commitHistoryLevel = max = 16;
+ }
+ if (twist.operationLog.length + 1 >= max) {
+ twist.operationLog.shift();
+ }
+ if (logChannels) {
+ if (!operation.channels) operation.channels = {};
+ for (let c of logChannels) {
+ operation.channels[c] = await app.getControlChannel(c);
+ }
+ }
+ twist.operationLog.push(operation);
+ }
+
+ this.createNewInstance = function(noShowLoadNew) {
+ 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 (twist.isPlaying()) return;
+ twist.waveform = index;
+ }).addClass("wtab_selected").appendTo("#twist_waveform_tabs")
+ );
+ undoLevels.push(0);
+ var waveform = new Waveform({
+ target: element,
+ latencyCorrection: twirl.latencyCorrection,
+ showcrossfades: true,
+ crossFadeWidth: 1,
+ timeBar: true,
+ markers: [
+ {preset: "selectionstart"},
+ {preset: "selectionend"},
+ ]
+ })
+ waveform.onRegionChange = function(region) {
+ if (twist.currentTransform) {
+ twist.currentTransform.redraw(region);
+ }
+ };
+ twist.waveforms.push(waveform);
+ if (!noShowLoadNew) twist.ui.showLoadNewPrompt();
+ twist.waveform = index;
+ for (let o of twist.onInstanceChangeds) {
+ o(true, index);
+ }
+ };
+
+
+ function removeInstance(i) {
+ if (!i) i = instanceIndex;
+ if (twist.waveforms.length == 1 || i < 0 || i > twist.waveforms.length - 1) {
+ return;
+ }
+ twist.waveforms[i].destroy();
+ delete twist.waveforms[i];
+ waveformTabs[i].remove();
+ waveformLoaded[instanceIndex] = false;
+ delete waveformTabs[i]
+ if (instanceIndex == i) {
+ instanceIndex = i + ((i == 0) ? 1 : -1);
+ twist.waveform.show();
+ }
+ for (let o of twist.onInstanceChangeds) {
+ o(false, i);
+ }
+ }
+
+ this.closeInstance = function(i) {
+ removeInstance(i);
+ };
+
+
+
+ this.errorHandler = async function(text, onComplete) {
+ var errorObj = {
+ lastOperation: twist.lastOperation()
+ };
+ if (twist.currentTransform) {
+ var state = await twist.currentTransform.getState();
+ errorObj.transformState = state;
+ }
+
+ twirl.errorHandler(text, onComplete, errorObj);
+ twist.setPlaying(false);
+ };
+
+ function playPositionHandler(noPlayhead, onComplete, monitorChannels) {
+ function callback(ndata) {
+ if (ndata.status == 1) { // playing
+ twist.setPlaying(true);
+ if (!noPlayhead) {
+ twist.watchdog.start("audition");
+ if (twist.playheadInterval) {
+ clearInterval(twist.playheadInterval);
+ }
+ twist.playheadInterval = setInterval(async function(){
+ var val = await app.getControlChannel("twst_playposratio");
+ twist.watchdog.setActive(val);
+ if (val < 0 || val > 1) {
+ clearInterval(twist.playheadInterval);
+ }
+
+ var monitorValues;
+ if (monitorChannels) {
+ monitorValues = [];
+ monitorValues.push((monitorChannels[0]) ? await app.getControlChannel(monitorChannels[0]) : null);
+ monitorValues.push((monitorChannels[1]) ? await app.getControlChannel(monitorChannels[1]) : null);
+ } else {
+ monitorValues = null;
+ }
+ twist.waveform.movePlayhead(val, monitorValues);
+ }, 50);
+ }
+ return;
+ }
+ // stopped
+ app.removeCallback(ndata.cbid);
+
+ if (twist.playbackLoop && ndata.status == 0 && onComplete) {
+ return onComplete(ndata);
+ }
+ twist.setPlaying(false);
+
+ if (ndata.status == -1) {
+ var container = $("<div />");
+ $("<p />").text("Not enough processing power to transform in realtime").appendTo(container);
+ var lagHintHtml = twist.currentTransform.getLagHints();
+ if (lagHintHtml) {
+ $("<p />").html(lagHintHtml).appendTo(container);
+ }
+
+ return twirl.prompt.show(container);
+ } else if (ndata.status == 2) { // record complete
+ globalCallbackHandler(ndata);
+ }
+ if (onComplete) onComplete(ndata);
+
+ }
+ return app.createCallback(callback, true);
+ }
+
+ function operation(options) {
+ var s = (options.selection) ? options.selection : twist.waveform.selected;
+ errorState = "Operation error";
+ if (options.showLoading) {
+ twist.ui.setLoadingStatus(true);
+ }
+ var cbid;
+ if (!options.onComplete || typeof(options.onComplete) == "function") {
+ cbid = app.createCallback(function(ndata) {
+ twist.waveform.cover(false);
+ if (options.onComplete) {
+ options.onComplete(ndata);
+ } else if (ndata.status && ndata.status <= 0) {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file would be too large";
+ }
+ twist.errorHandler(text);
+ }
+ if (options.showLoading) {
+ twist.ui.setLoadingStatus(false);
+ }
+ });
+ } else {
+ cbid = options.onComplete;
+ }
+ if (!options.noLogScript) {
+ pushOperationLog({
+ type: "operation",
+ instr: options.instr,
+ name: options.name,
+ selection: s,
+ instanceIndex: instanceIndex
+ }, options.logScriptChannels);
+ }
+ app.insertScore(options.instr, [0, 1, cbid, s[0], s[1], s[2], (options.noCheckpoint) ? 1 : 0]);
+ }
+
+ this.isPlaying = function() {
+ return playing;
+ };
+
+ this.redraw = function() {
+ if (twist.currentTransform) {
+ twist.currentTransform.redraw();
+ }
+ for (let w of twist.waveforms) {
+ w.redraw();
+ }
+ };
+
+ this.undo = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_undo",
+ name: "Undo",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ noLogScript: true
+ });
+ };
+
+ this.cut = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_cut",
+ name: "Cut",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ });
+ twist.hasClipboard = true;
+ };
+
+ this.trim = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_trim",
+ name: "Trim",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ });
+ };
+
+ this.delete = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_delete",
+ name: "Delete",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ });
+ };
+
+ this.copy = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_copy",
+ name: "Copy",
+ showLoading: true,
+ });
+ twist.hasClipboard = true;
+ };
+
+ this.paste = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_paste",
+ name: "Paste",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ });
+ };
+
+ this.moveToNextTransient = function() {
+ if (playing) return;
+ var cbid = app.createCallback(globalCallbackHandler);
+ var s = twist.waveform.selected;
+ app.insertScore("twst_nexttransient",
+ [0, 1, cbid, s[1], s[1], s[2]]
+ );
+ };
+
+ this.selectToNextTransient = function() {
+ if (playing) return;
+ var cbid = app.createCallback(globalCallbackHandler);
+ var s = twist.waveform.selected;
+ var selend = (s[0] == s[1]) ? s[1] + 0.000001 : s[1];
+ app.insertScore("twst_nexttransient",
+ [0, 1, cbid, s[0], selend, s[2]]
+ );
+ };
+
+ this.moveToStart = function() {
+ if (playing) return;
+ twist.waveform.setSelection(0);
+ };
+
+ this.moveToEnd = function() {
+ if (playing) return;
+ twist.waveform.setSelection(1);
+ };
+
+ this.selectAll = function() {
+ if (playing) return;
+ twist.waveform.setSelection(0, 1);
+ };
+
+ this.selectNone = function() {
+ if (playing) return;
+ twist.waveform.setSelection(0);
+ };
+
+ this.selectToEnd = function() {
+ if (playing) return;
+ twist.waveform.alterSelection(null, 1);
+ }
+
+ this.selectFromStart = function() {
+ if (playing) return;
+ twist.waveform.alterSelection(0, null);
+ }
+
+ 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}
+ ]
+ };
+ var tf = new twirl.transform.Transform({
+ element: elPasteSpecial,
+ definition: def,
+ host: twist
+ });
+
+ $("<button />").text("Paste").click(function(){
+ twist.ui.hidePrompt();
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_pastespecial",
+ name: "Paste special",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ logScriptChannels: ["twst_pastespecial_repetitions", "twst_pastespecial_mixpaste"]
+ });
+
+ }).appendTo(elPasteSpecial);
+
+ $("<button />").text("Cancel").click(function(){
+ twist.ui.hidePrompt();
+ }).appendTo(elPasteSpecial);
+ twist.ui.showPrompt(elPasteSpecial, null, true);
+
+ };
+
+
+ this.play = function(playOverride) {
+ if (!waveformLoaded[instanceIndex] || (playing && !playOverride)) return;
+ auditioning = false;
+ recording = false;
+ operation({
+ instr: "twst_play",
+ name: "Play",
+ onComplete: playPositionHandler(false, function(ndata){
+ if (ndata.status != 3 && twist.playbackLoop) { // 3 = user-stopped
+ twist.play(true);
+ }
+ }),
+ noLogScript: true
+ });
+ };
+
+ this.stop = function() {
+ if (!playing || !waveformLoaded[instanceIndex]) return;
+ twist.waveform.cover(false);
+ app.insertScore("twst_stop");
+ };
+
+ var saveNumber = 1;
+ 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("."));
+ // 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);
+ };
+
+ this.saveFile = function(name, onComplete) {
+ if (playing) return;
+ if (onSave) {
+ twirl.loading.show("Processing");
+ var cbid = app.createCallback(function(ndata){
+ twirl.loading.hide();
+ onSave(ndata.tables);
+ });
+ app.insertScore("twst_getbuffers", [0, 1, cbid]);
+ return;
+ }
+ if (!name) name = formatFileName(name);
+ var cbid = app.createCallback(async function(ndata){
+ await self.downloadFile("/" + name, name);
+ if (onComplete) onComplete();
+ self.ui.setLoadingStatus(false);
+ });
+ self.ui.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 instr = "instr twst_automaterun\n";
+ for (let c of calls) {
+ instr += c + "\n";
+ }
+ instr += "a_ init 0\nout a_\nendin\n";
+ app.compileOrc(instr).then(function(status){
+ if (status < 0) {
+ self.errorHandler("Cannot parse automation data");
+ } else {
+ onready(1);
+ }
+ });
+ /*
+ 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 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 (!waveformLoaded[instanceIndex] ||playing) return;
+ auditioning = false;
+ recording = true;
+ await app.enableAudioInput();
+ errorState = "Recording error";
+ self.waveform.cover(true);
+ var s = self.waveform.selected;
+ var monitorChannels;
+ if (self.waveform.channels == 1) {
+ monitorChannels = ["recordmonitorL"];
+ } else {
+ if (s[2] == -1) {
+ monitorChannels = ["recordmonitorL", "recordmonitorR"];
+ } else if (s[2] == 0) {
+ monitorChannels = ["recordmonitorL", null];
+ } else if (s[2] == 1) {
+ monitorChannels = [null, "recordmonitorR"];
+ }
+ }
+ self.waveform.resetDrawOneValue();
+ var cbid = playPositionHandler(null, null, monitorChannels);
+ var items = [0, 1, cbid, s[0], s[1], s[2]];
+ app.insertScore("twst_record", items);
+ };
+
+ this.audition = function(playOverride) {
+ if (!waveformLoaded[instanceIndex] || (playing && !playOverride)) return;
+ if (!self.currentTransform) {
+ return self.play();
+ }
+ self.currentTransform.saveState();
+ var s = self.waveform.selected;
+ if (s[0] == s[1]) {
+ s[0] = 0;
+ s[1] = 1;
+ }
+ 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(false, function(ndata){
+ if (ndata.status != 3 && self.playbackLoop) { // 3 = user-stopped
+ self.audition(true);
+ }
+ });
+ var xfade = self.ui.getCrossFadeValues();
+ var items = [
+ 0, 1, cbid, s[0], s[1], s[2],
+ self.currentTransform.instr, automating,
+ xfade[0], xfade[1]
+ ];
+ app.insertScore("twst_audition", items);
+ }, getAutomationData(s[0], s[1]));
+
+ };
+
+
+ var scriptStack = [];
+ function applyScript(audition, first, lastData) {
+ if (playing) return;
+ var noCheckpoint = !first;
+ var script = scriptStack.shift();
+ if (!script) {
+ self.ui.setLoadingStatus(false);
+ if (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.ui.setLoadingStatus(true);
+ self.waveform.cover(true);
+ onComplete = (script.instr == "twst_copy") ? null : globalCallbackHandler;
+
+ operation({
+ instr: script.instr,
+ name: script.name,
+ onComplete: function(ndata){
+ lastData = ndata;
+ self.setPlaying(false);
+ applyScript(audition, false, lastData);
+ },
+ showLoading: true,
+ selection: script.selection,
+ noLogScript: true,
+ noCheckpoint: noCheckpoint
+ });
+
+ } else if (script.type == "transform") {
+ errorState = ((audition) ? "Audition" : "Transform" ) + " commit error";
+ if (!audition) {
+ self.ui.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, false, lastData);
+ });
+ }
+ 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], (noCheckpoint) ? 1 : 0
+ ]);
+ }, 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, true);
+ });
+ } else {
+ applyScript(audition, true);
+ }
+ };
+
+ async function innerCommit() {
+ if (playing) return;
+ if (!self.currentTransform) return;
+ var s = self.waveform.selected;
+ if (s[0] == s[1]) {
+ s[0] = 0;
+ s[1] = 1;
+ }
+ if (!fftsizeCheck(s, self.waveform.duration)) {
+ return self.errorHandler("Length too short for this transform");
+ }
+ self.watchdog.start("commit");
+ self.setPlaying(true);
+ self.ui.setLoadingStatus(true, true, null);
+ var calls = getAutomationData(s[0], s[1]);
+
+ self.currentTransform.saveState();
+ var state = await self.currentTransform.getState();
+ state.type = "transform";
+ state.automation = calls;
+ state.crossfades = self.ui.getCrossFadeValues();
+ state.selection = [s[0], s[1], s[2]];
+ state.instanceIndex = instanceIndex;
+ pushOperationLog(state);
+
+ handleAutomation(function(automating){
+ var cbid = app.createCallback(function(ndata) {
+ self.watchdog.stop();
+ self.ui.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 (!waveformLoaded[instanceIndex]) return;
+ if (self.storage.autosave) {
+ self.saveFile(null, function() {
+ innerCommit();
+ });
+ } else {
+ innerCommit();
+ }
+ };
+
+ this.loadTransforms = function(transform) {
+ if (transform) {
+ var developObj;
+ for (var t in twirl.appdata.transforms) {
+ if (twirl.appdata.transforms[t].name == "Develop") {
+ developObj = twirl.appdata.transforms[t];
+ break;
+ }
+ }
+ if (!developObj) {
+ developObj = {name: "Develop", contents: []};
+ twirl.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 twirl.transform.TreeView({
+ element: $("#twist_panetree"),
+ items: twirl.appdata.transforms,
+ click: function(definition, path) {
+ if (twist.currentTransform) {
+ twist.currentTransform.remove();
+ }
+ twist.currentTransform = new twirl.transform.Transform({
+ element: $("#twist_controls_inner"),
+ definition: definition,
+ splineElement: $("#twist_splines"),
+ useStorage: true,
+ path: path,
+ otherInstanceNamesFunc: function() {
+ return twist.otherInstanceNames;
+ },
+ instancesFunc: function() {
+ return twist.waveforms;
+ },
+ getRegionFunc: function() {
+ return twist.waveform.getRegion();
+ },
+ getDurationFunc: function() {
+ return twist.waveform.getDuration();
+ },
+ onHideAutomation: function() {
+ twist.ui.deleteSupressed = false;
+ console.log("twist.ui.deleteSupressed", twist.ui.deleteSupressed);
+ },
+ onShowAutomation: function() {
+ twist.ui.deleteSupressed = true;
+ console.log("twist.ui.deleteSupressed", twist.ui.deleteSupressed);
+ },
+ host: twist
+ });
+ }
+ });
+ };
+
+ this.createEmpty = function(name, duration, channels) {
+ if (name.trim() == "") {
+ name = "New file";
+ }
+ var cbid = app.createCallback(async function(ndata) {
+ twist.waveformTab.text(name);
+ await globalCallbackHandler(ndata);
+ if (twist.currentTransform) {
+ twist.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = name;
+ waveformLoaded[instanceIndex] = true;
+ twist.ui.setLoadingStatus(false);
+ });
+ twist.ui.hidePrompt();
+ twist.ui.setLoadingStatus(true, false, "Creating");
+ app.insertScore("twst_createempty", [0, 1, cbid, duration, channels]);
+ };
+
+ this.setVisible = function(state) {
+ twist.visible = state;
+ var el = $("#twist");
+ if (state) {
+ el.show();
+ } else {
+ el.hide();
+ }
+ };
+
+ this.editInTwigs = function() {
+ if (!window.twigs) {
+ return twirl.prompt.show("twigs is unavailable in this session");
+ }
+ twirl.loading.show("Processing");
+ var cbid = app.createCallback(function(ndata){
+ twirl.loading.hide();
+ twigs.loadFileFromFtable(waveformFiles[instanceIndex], ndata.tables, function(ldata){
+ if (ldata.status > 0) {
+ self.setVisible(false);
+ twigs.setVisible(true);
+ }
+ }, onSave);
+ });
+ app.insertScore("twst_getbuffers", [0, 1, cbid]);
+ };
+
+ this.loadFileFromClipboard = function() {
+ if (!twist.hasClipboard) {
+ return twirl.prompt.show("Cannot create: clipboard is empty");
+ }
+ errorState = "File loading error";
+ twirl.loading.show("Loading");
+ var cbid = app.createCallback(async function(ndata){
+ self.waveformTab.text("Clipboard");
+ await globalCallbackHandler(ndata);
+ waveformFiles[instanceIndex] = "Clipboard";
+ waveformLoaded[instanceIndex] = true;
+ twirl.loading.hide();
+ });
+ app.insertScore("twst_loadclipboard", [0, 1, cbid]);
+ };
+
+ this.loadFileFromFtable = function(name, tables, onComplete, onSaveFunc) {
+ errorState = "File loading error";
+ twirl.loading.show("Loading file");
+
+ var cbid = app.createCallback(async function(ndata){
+ twirl.loading.hide();
+ if (ndata.status > 0) {
+ if (waveformTabs.length == 0) {
+ self.createNewInstance(true);
+ instanceIndex = 0;
+ }
+ self.waveformTab.text(name);
+ waveformLoaded[instanceIndex] = true;
+ await globalCallbackHandler(ndata);
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = name;
+ self.ui.hidePrompt();
+ onSave = onSaveFunc;
+ } else if (ndata.status == -1) {
+ twirl.prompt.show("File not valid");
+ } else if (ndata.status == -2) {
+ twirl.prompt.show("File too large");
+ } else {
+ twirl.prompt.show("File loading error");
+ }
+ if (onComplete) {
+ onComplete(ndata);
+ }
+ });
+ var call = [0, 1, cbid];
+ for (let t of tables) {
+ call.push(t);
+ }
+ app.insertScore("twst_loadftable", call);
+ };
+
+
+ async function handleFileDrop(e, obj) {
+ e.preventDefault();
+ if (!e.originalEvent.dataTransfer && !e.originalEvent.files) {
+ return;
+ }
+ if (e.originalEvent.dataTransfer.files.length == 0) {
+ return;
+ }
+ self.ui.hidePrompt();
+ self.ui.setLoadingStatus(true, true, "Loading");
+ for (const item of e.originalEvent.dataTransfer.files) {
+ if (!twirl.audioTypes.includes(item.type)) {
+ return self.errorHandler("Unsupported file type", self.ui.showLoadNewPrompt);
+ }
+ if (item.size > twirl.maxFileSize) {
+ return self.errorHandler("File too large", self.ui.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", self.ui.showLoadNewPrompt);
+ } else if (ndata.status == -2) {
+ return self.errorHandler("File too large", self.ui.showLoadNewPrompt);
+ } else {
+ self.waveformTab.text(item.name);
+ await globalCallbackHandler(ndata);
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = item.name;
+ waveformLoaded[instanceIndex] = true;
+ self.ui.hidePrompt();
+ self.ui.setLoadingStatus(false);
+ onSave = false;
+ }
+ });
+ app.insertScore("twst_loadfile", [0, 1, cbid, item.name]);
+ }
+ }
+
+ async function globalCallbackHandler(ndata) {
+ if (ndata.status && ndata.status <= 0) {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file would be too large";
+ }
+ self.errorHandler(text);
+ return;
+ }
+
+ self.watchdog.start("refresh");
+
+ 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";
+ setTimeout(async function(){
+ var wavedata = [];
+ 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);
+ }, 10);
+ }
+ self.watchdog.stop();
+ }
+
+ this.bootAudio = async function(twine) {
+ var channelDefaultItems = ["dcblockoutputs", "tanhoutputs", "maxundo"];
+
+ for (var i of channelDefaultItems) {
+ if (self.storage.hasOwnProperty(i)) {
+ app.setControlChannel("twst_" + i, self.storage[i]);
+ }
+ }
+ sr = await app.getCsound().getSr();
+ if (!twine) self.ui.postBoot();
+ };
+
+ var booted = false;
+ this.boot = function(twine) {
+ if (booted) return;
+ booted = true;
+ twirl.boot();
+ self.ui.boot();
+
+ 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();
+ self.currentTransform.redraw(self.waveform.getRegion());
+ }
+ } else {
+ self.ui.showPrompt("Error changing instance");
+ }
+ });
+ app.insertScore("twst_setinstance", [0, 1, cbid, x]);
+
+ }
+ }
+ });
+
+ if (!twine) {
+ $("<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);
+ });
+ } else {
+ self.twine = twine;
+ }
+
+ self.loadTransforms();
+ };
+
+}; // end twist
+
+function twist_startisolated() {
+ var csOptions = ["--omacro:TWST_FAILONLAG=1"];
+ window.twist = new Twist();
+ twist.setVisible(true);
+ window.app = new CSApplication({
+ csdUrl: "twist.csd",
+ csOptions: csOptions,
+ onPlay: function () {
+ twist.bootAudio();
+ },
+ errorHandler: twist.errorHandler,
+ ioReceivers: {percent: twist.ui.setPercent}
+ });
+
+ $("#twist_start").click(function() {
+ $(this).hide();
+ twist.boot();
+ twist.ui.setLoadingStatus(true, false, "Preparing audio engine");
+ app.play(function(text){
+ twist.ui.setLoadingStatus(true, false, text);
+ twirl.latencyCorrection = twirl.audioContext.outputLatency * 1000;
+ }, twirl.audioContext);
+ });
+}
+
+ \ No newline at end of file
diff --git a/site/app/twist/twist_ui.js b/site/app/twist/twist_ui.js
new file mode 100644
index 0000000..08e5fe1
--- /dev/null
+++ b/site/app/twist/twist_ui.js
@@ -0,0 +1,674 @@
+var twistTopMenuData = [
+ {name: "File", contents: [
+ {name: "New", disableOnPlay: true, shortcut: {name: "Ctrl N", ctrlKey: true, key: "n"}, click: function(twist) {
+ twist.createNewInstance();
+ }, condition: function(twist) {
+ return (!twist.twine);
+ }},
+ {name: "Save", disableOnPlay: true, shortcut: {name: "Ctrl S", ctrlKey: true, key: "s"}, click: function(twist) {
+ twist.saveFile();
+ }},
+ {name: "Close", disableOnPlay: true, shortcut: {name: "Ctrl W", ctrlKey: true, key: "w"}, click: function(twist) {
+ twist.closeInstance();
+ }, condition: function(twist) {
+ return (!twist.twine && twist.waveforms.length != 1);
+ }},
+ {name: "Edit in twigs", click: function(twist) {
+ twist.editInTwigs();
+ }, condition: function(twist) {
+ return window.hasOwnProperty("Twigs");
+ }}
+ ]},
+ {name: "Edit", contents: [
+ {name: "Undo", disableOnPlay: true, shortcut: {name: "Ctrl Z", ctrlKey: true, key: "z"}, click: function(twist) {
+ twist.undo();
+ }, condition: function(twist) {
+ return (twist.storage.maxundo > 0 && twist.undoLevel > 0);
+ }},
+ {preset: "divider"},
+ {name: "Copy", disableOnPlay: true, shortcut: {name: "Ctrl C", ctrlKey: true, key: "c"}, click: function(twist) {
+ twist.copy();
+ }},
+ {name: "Cut", disableOnPlay: true, shortcut: {name: "Ctrl X", ctrlKey: true, key: "x"}, click: function(twist) {
+ twist.cut();
+ }},
+ {name: "Paste", disableOnPlay: true, shortcut: {name: "Ctrl V", ctrlKey: true, key: "v"}, click: function(twist) {
+ twist.paste();
+ }, condition: function(twist) {
+ return twist.hasClipboard;
+ }},
+ {name: "Paste special", disableOnPlay: true, shortcut: {name: "Ctrl shift V", ctrlKey: true, shiftKey: true, key: "v"}, click: function() {
+ twist.pasteSpecial();
+ }, condition: function(twist) {
+ return twist.hasClipboard;
+ }},
+ {name: "Trim", disableOnPlay: true, shortcut: {name: "T", key: "t"}, click: function() {
+ twist.trim();
+ }},
+ {name: "Delete", disableOnPlay: true, shortcut: {name: "Del", key: "delete"}, keyCondition: function(twist) {
+ return !twist.ui.deleteSupressed;
+ }, click: function(twist) {
+ twist.delete();
+ }},
+ {preset: "divider"},
+ {name: "Select all", shortcut: {name: "Ctrl A", ctrlKey: true, key: "a"}, click: function(twist) {
+ twist.selectAll();
+ }},
+ {name: "Select to end", shortcut: {name: "W", key: "w"}, click: function(twist) {
+ twist.selectToEnd();
+ }},
+ {name: "Select from start", shortcut: {name: "Q", key: "q"}, click: function(twist) {
+ twist.selectFromStart();
+ }},
+ {name: "Select none", shortcut: {name: "Ctrl M", ctrlKey: true, key: "m"}, click: function(twist) {
+ twist.selectNone();
+ }},
+ {name: "Move to next transient", shortcut: {name: "[",key: "["}, click: function(twist) {
+ twist.moveToNextTransient();
+ }},
+ {name: "Select to next transient", shortcut: {name: "]",key: "]"}, click: function(twist) {
+ twist.selectToNextTransient();
+ }}
+ ]},
+ {name: "View", contents: [
+ {name: "Zoom selection", shortcut: {name: "Z", key: "z"}, click: function(twist) {
+ twist.waveform.zoomSelection();
+ }},
+ {name: "Zoom in", shortcut: {name: "+", key: "+"}, click: function(twist) {
+ twist.waveform.zoomIn();
+ }},
+ {name: "Zoom out", shortcut: {name: "-", key: "-"}, click: function(twist) {
+ twist.waveform.zoomOut();
+ }},
+ {name: "Show all", shortcut: {name: "0", key: "0"}, click: function(twist) {
+ twist.waveform.setRegion(0, 1);
+ }},
+ {preset: "divider"},
+ {name: "Toggle analysis", click: function(twist){
+ twist.ui.toggleScope();
+ }},
+ {name: "Toggle layout", shortcut: {name: "L", key: "l"}, click: function(twist){
+ twist.ui.toggleLayout();
+ }},
+ ]},
+ {name: "Action", contents: [
+ {name: "Play/stop", shortcut: {name: "Space", key: " "}, click: function(twist) {
+ if (twist.isPlaying()) {
+ twist.stop();
+ } else {
+ twist.play();
+ }
+ }},
+ {name: "Audition", disableOnPlay: true, shortcut: {name: "Enter", key: "enter"}, click: function(twist) {
+ twist.audition();
+ }},
+ {name: "Commit", disableOnPlay: true, shortcut: {name: "Alt enter", altKey: true, key: "enter"}, click: function(twist) {
+ twist.commit();
+ }},
+ {name: "Record", disableOnPlay: true, shortcut: {name: "R", key: "r"}, click: function(twist) {
+ twist.record();
+ }},
+ {preset: "divider"},
+ {name: "Scripting", shortcut: {name: "Ctrl K", ctrlKey: true, key: "k"}, click: function(twist) {
+ twist.ui.scriptEdit();
+ }},
+ {name: "Developer", shortcut: {name: "Ctrl L", ctrlKey: true, key: "l"}, click: function(twist) {
+ twist.ui.developerConsole();
+ }},
+
+ ]},
+ {name: "Transform", contents: [
+ {name: "Randomise", shortcut: {name: "Z", key: "z"}, click: function(twist) {
+ twist.currentTransform.randomise();
+ }, condition: function(twist) {
+ return (twist.currentTransform) ? true : false;
+ }},
+ {name: "Reset", shortcut: {name: "R", key: "r"}, click: function(twist) {
+ twist.currentTransform.reset();
+ }, condition: function(twist) {
+ return (twist.currentTransform) ? true : false;
+ }},
+ {name: "Hide automation", shortcut: {name: "H", key: "h"}, click: function(twist) {
+ twist.currentTransform.hideAllAutomation();
+ }, condition: function(twist) {
+ return (twist.currentTransform) ? true : false;
+ }}
+ ]},
+ {name: "Options", contents: [
+ {name: "Settings", click: function(twist) {
+ twist.ui.showSettings();
+ }}
+ ]},
+ {name: "Help", contents: [
+ {name: "Help", click: function(twist){
+ $("#twist_documentation")[0].click();
+ }},
+ {name: "Developer reference", click: function(twist){
+ $("#twist_developer_documentation")[0].click();
+ }},
+ {name: "Report bug", click: function(twist){
+ $("#twist_reportbug")[0].click();
+ }},
+ {name: "Contact owner", click: function(twist){
+ $("#twist_contact")[0].click();
+ }},
+ {name: "Submit transform code", click: function(twist){
+ $("#twist_developer_submit")[0].click();
+ }},
+ {name: "About", click: function(twist) {
+ twist.ui.showAbout();
+ }},
+ ]},
+];
+
+
+var TwistUI = function(twist) {
+ var self = this;
+ var scope;
+ var elCrossfades = [];
+ var topMenu = new twirl.TopMenu(twist, twistTopMenuData, $("#twist_menubar"));
+ this.deleteSupressed = false;
+
+ this.setPlaying = function(state) {
+ if (scope) {
+ scope.setPlaying(state);
+ }
+ if (state) {
+ $(".twist_scriptbutton").hide();
+ $("#twist_scriptstop").show();
+ } else {
+ $(".twist_scriptbutton").show();
+ $("#twist_scriptstop").hide();
+ }
+ };
+
+ this.getCrossFadeValues = function() {
+ return [elCrossfades[0].val(), elCrossfades[1].val()];
+ };
+
+
+ var contractedWaveform = false;
+ function setLayout() {
+ var elViews = $("#twist_views");
+ var elWave = $("#twist_waveforms");
+ var elSpline = $("#twist_splines");
+ var elScope = $("#twist_analyser");
+ var elControls = $("#twist_controls");
+
+ if (contractedWaveform) {
+ elViews.css({height: "20%"});
+ elControls.css({top: "20%"});
+ } else {
+ elViews.css({height: "50%"});
+ elControls.css({top: "50%"});
+ }
+
+ if (scope) {
+ elScope.css({height: "40%", top: "0px"});
+ elWave.css({top: "40%"});
+ elSpline.css({top: elWave.css("top")});
+ } else {
+ elWave.css({top: "0px"});
+ elSpline.css({top: "0px"});
+ }
+
+ twist.redraw();
+ }
+
+ this.toggleLayout = function() {
+ contractedWaveform = !contractedWaveform;
+ setLayout();
+ };
+
+ this.toggleScope = function(noSaveState) {
+ var state;
+ if (!scope) {
+ state = true;
+ var elScope = $("<div />").addClass("twist_scope").appendTo($("#twist_analyser"));
+ var type = (twist.storage.scopeType) ? twist.storage.scopeType : 0;
+ scope = new Analyser(
+ type, twist, elScope, app
+ );
+ $("#twist_analyser").show();
+ } else {
+ $("#twist_analyser").hide();
+ state = false;
+ scope.remove();
+ delete scope;
+ scope = null;
+ }
+
+ if (!noSaveState) {
+ twist.storage.showScope = state;
+ twist.saveStorage();
+ }
+ setLayout();
+ };
+
+
+ this.tooltip = twirl.tooltip;
+
+ this.boot = function() {
+ if (twist.storage.hasOwnProperty("showShortcuts")) {
+ if (twist.storage.showShortcuts) {
+ $("#twist_wavecontrols_inner").show();
+ } else {
+ $("#twist_wavecontrols_inner").hide();
+ }
+ }
+
+ if (twist.storage.develop) {
+ if (twist.storage.develop.csound) {
+ $("#twist_devcsound").val(twist.storage.develop.csound);
+ }
+ if (twist.storage.develop.json) {
+ $("#twist_devjson").val(twist.storage.develop.json);
+ }
+ }
+ $("#loading_background").css("opacity", 1).animate({opacity: 0.2}, 1000);
+ };
+
+ this.postBoot = function() {
+ self.setLoadingStatus(false);
+
+ if (!twist.storage.hasOwnProperty("firstLoadDone")) {
+ twist.storage.firstLoadDone = true;
+ twist.saveStorage();
+ self.showPrompt($("#twist_welcome").detach().show(), twist.createNewInstance);
+ } else {
+ twist.createNewInstance();
+ }
+
+ if (twist.storage.showScope) {
+ self.toggleScope(true);
+ }
+ };
+
+ this.hidePrompt = function() {
+ twirl.prompt.hide();
+ };
+
+ this.showPrompt = function(text, oncomplete, noButton) {
+ twirl.prompt.show(text, oncomplete, noButton);
+ if (twist.playheadInterval) {
+ twist.waveform.movePlayhead(0);
+ clearInterval(twist.playheadInterval);
+ }
+ if (self.waveform) {
+ self.waveform.cover(false);
+ }
+ };
+
+
+ this.showLoadNewPrompt = function() {
+ var elNewFile = $("<div />").css({"font-size": "var(--fontSizeDefault)"});
+ if (twist.hasClipboard) {
+ $("<button />").text("Create from clipboard").appendTo(elNewFile).click(function(){
+ twist.loadFileFromClipboard();
+ twirl.prompt.hide();
+ });
+ }
+
+ elNewFile.append($("<h3 />").text("Drag an audio file here to load")).append($("<p />").text("or"));
+
+ var elEmpty = $("<div />").appendTo(elNewFile);
+ $("<h4 />").text("Create an empty file").css("cursor", "pointer").appendTo(elEmpty);
+
+ var tpDuration = new twirl.transform.Parameter({
+ definition: {name: "Duration", min: 0.1, max: 60, dfault: 10, automatable: false, fireChanges: false},
+ host: twist
+ });
+
+ var tpChannels = new twirl.transform.Parameter({
+ definition: {name: "Channels", min: 1, max: 2, dfault: 2, step: 1, automatable: false, fireChanges: false},
+ host: twist
+ });
+
+ var tpName = new twirl.transform.Parameter({
+ definition: {name: "Name", type: "string", dfault: "New file", fireChanges: false},
+ host: twist
+ });
+
+ var tb = $("<tbody />");
+ $("<table />").append(tb).css("margin", "0 auto").appendTo(elEmpty);
+ tb.append(tpDuration.getElementRow(true)).append(tpChannels.getElementRow(true)).append(tpName.getElementRow(true));
+
+ $("<button />").text("Create").appendTo(elEmpty).click(function() {
+ twist.createEmpty(tpName.getValue(), tpDuration.getValue(), tpChannels.getValue());
+ });
+
+
+ self.showPrompt(elNewFile, null, true);
+ }
+
+
+ this.setTheme = function(name, nosave) {
+ twirl.setTheme(name, nosave);
+ };
+
+ this.showSettings = function() {
+ var settings = [
+ {
+ name: "Commit history limit",
+ description: "Number of transform states to store (can be accessed via the script editor). 0 = infinite",
+ min: 0, max: 32, step: 1, dfault: 16, storageKey: "commitHistoryLevel"
+ },
+ {
+ name: "Undo levels",
+ description: "Number of undo levels stored. Large numbers may affect memory usage",
+ min: 0, max: 32, step: 1, dfault: 2, storageKey: "maxundo",
+ onChange: function(val) {
+ app.setControlChannel("twst_maxundo", val);
+ }
+ },
+ {
+ name: "Analysis type",
+ description: "Type of analysis to be shown",
+ options: ["Frequency", "Oscilloscope"],
+ dfault: 0,
+ storageKey: "scopeType",
+ onChange: function(val) {
+ if (scope) scope.setType(val);
+ }
+ },
+ {
+ name: "Show shortcuts",
+ description: "Show shortcuts toolbar below waveform",
+ bool: true,
+ storageKey: "showShortcuts",
+ dfault: 1,
+ onChange: function(val) {
+ if (val) {
+ $("#twist_wavecontrols_inner").show();
+ } else {
+ $("#twist_wavecontrols_inner").hide();
+ }
+ }
+ },
+ {
+ name: "DC block processing",
+ description: "Apply DC blocking to all processing",
+ bool: true,
+ storageKey: "dcblockoutputs",
+ dfault: 0,
+ onChange: function(val) {
+ app.setControlChannel("twst_dcblockoutputs", val);
+ }
+ },
+ {
+ name: "Tanh limit all processing",
+ description: "Apply tanh to all processing",
+ bool: true,
+ storageKey: "tanhoutputs",
+ dfault: 0,
+ onChange: function(val) {
+ app.setControlChannel("twst_tanhtanhoutputs", val);
+ }
+ },
+ {
+ name: "Autosave before each commit",
+ description: "Automatically save file locally before each new commit",
+ bool: true,
+ storageKey: "autosave",
+ dfault: 0
+ }
+ ];
+ twirl.showSettings(twist, settings);
+ };
+
+
+ this.setPercent = function(percent) {
+ twist.watchdog.setActive(percent);
+ twirl.loading.setPercent(percent);
+ };
+
+ this.setLoadingStatus = function(state, showpercent, text) {
+ if (state) {
+ twirl.loading.show(text, showpercent);
+ } else {
+ twirl.loading.hide();
+ }
+ };
+
+ this.scriptEdit = function() {
+ var el = $("#twist_script").show();
+ var te = $("#twist_scriptsource");
+
+
+ function runScript(audition) {
+ try {
+ var script = JSON.parse(te.val());
+ } catch (e) {
+ twist.errorHandler("Cannot parse script: " + e);
+ return false;
+ }
+ twist.applyScript(script, audition);
+ return true;
+ };
+
+
+ $("#twist_scriptaudition").unbind().click(function(){
+ runScript(true);
+ });
+
+ $("#twist_scriptstop").unbind().click(function(){
+ twist.stop();
+ });
+
+ $("#twist_scriptcommit").unbind().click(function(){
+ if (runScript(false)) {
+ el.hide();
+ }
+ });
+
+ $("#twist_scriptloadlast").unbind().click(function(){
+ te.val(JSON.stringify(twist.lastOperation(), null, 2));
+ });
+
+ $("#twist_scriptloadall").unbind().click(function(){
+ te.val(JSON.stringify(twist.operationLog, null, 2));
+ });
+
+ $("#twist_scriptcancel").unbind().click(function(){
+ el.hide();
+ });
+ };
+
+ 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 (!twist.storage.develop) {
+ twist.storage.develop = {};
+ }
+ twist.storage.develop.csound = code;
+ twist.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 twist.errorHandler("Cannot parse JSON: " + e);
+ }
+ try {
+ twist.loadTransforms(json);
+ } catch (e) {
+ return twist.errorHandler("Cannot load transform: " + e);
+ }
+ if (!twist.storage.develop) {
+ twist.storage.develop = {};
+ }
+ twist.storage.develop.json = code;
+ twist.saveStorage();
+ self.showPrompt("Successfully injected transform definition");
+ });
+ $("#twist_exit_devcode").click(async function() {
+ $("#twist_developer").hide();
+ });
+ };
+
+ function buildWavecontrols() {
+ var el = $("#twist_wavecontrols_inner");
+ var onPlayDisables = [];
+
+ var play = twirl.createIcon({label: "Play", icon: "play", label2: "Stop", icon2: "stop", click: function(obj){
+ if (twist.isPlaying()) {
+ twist.stop();
+ } else {
+ twist.play();
+ }
+ }});
+ var audition = twirl.createIcon({label: "Audition", icon: "audition", label2: "Stop", icon2: "stop", click: function(obj){
+ if (twist.isPlaying()) {
+ twist.stop();
+ } else {
+ twist.audition();
+ }
+ }});
+
+
+ var record = twirl.createIcon({label: "Record", icon: "record", label2: "Stop", icon2: "stop", click: function() {
+ if (twist.isPlaying()) {
+ twist.stop();
+ } else {
+ twist.record();
+ }
+ }});
+
+ var items = [
+ {label: "Rewind", icon: "rewind", disableOnPlay: true, click: function() { twist.moveToStart() }},
+ play,
+ audition,
+ {label: "Commit", icon: "commit", disableOnPlay: true, click: function() { twist.commit() }},
+ record,
+ {preset: "spacer"},
+ {label: "Cut", icon: "cut", disableOnPlay: true, click: function() { twist.cut() }},
+ {label: "Copy", icon: "copy", disableOnPlay: true, click: function() { twist.copy() }},
+ {label: "Paste", icon: "paste", disableOnPlay: true, click: function() { twist.paste() }},
+ {label: "Paste special", icon: "pasteSpecial", disableOnPlay: true, click: function() { twist.pasteSpecial() }},
+ {label: "Trim", icon: "trim", disableOnPlay: true, click: function() { twist.trim() }},
+ {preset: "spacer"}
+ ];
+
+ for (let i of items) {
+ var icon;
+ var td = $("<td />");
+ if (i.preset && i.preset == "spacer") {
+ td.css("width", "20px");
+ } else {
+ if (i.icon) {
+ icon = twirl.createIcon(i);
+ if (i.disableOnPlay) {
+ onPlayDisables.push(icon);
+ }
+ } else {
+ icon = i;
+ }
+ td.append(icon.el);
+ }
+ td.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("twirl_slider").attr("type", "range").attr("min", 0).attr("max", 0.45).attr("step", 0.00001).val(0).on("input", function() {
+ if (e == "In") {
+ twist.waveform.crossFadeInRatio = $(this).val();
+ } else {
+ twist.waveform.crossFadeOutRatio = $(this).val();
+ }
+ });
+ elCrossfades.push(elRange);
+ $("<td />").addClass("crossfade").append($("<div />").css("font-size", "var(--fontSizeSmall)").text("Crossfade " + e)).append(elRange).appendTo(el);
+ }
+
+ $("<td />").css("font-size", "var(--fontSizeSmall").append("Loop playback<br />").append(
+ $("<input />").addClass("tp_checkbox").attr("type", "checkbox").change(function(){
+ twist.playbackLoop = $(this).is(":checked");
+ })
+ ).appendTo(el);
+
+ };
+
+ function formatVersion(ver) {
+ ver = ver.toString();
+ var major = ver.substr(0, 1);
+ var remainder = ver.substr(1);
+ if (remainder.length == 2) {
+ return major + "." + remainder;
+ } else {
+ var mid = remainder.substr(1, 2);
+ var minor = remainder.substr(2);
+ return major + "." + mid + "." + minor;
+ }
+ }
+
+ this.showAbout = async function() {
+ var csVer = await app.getCsound().getVersion();
+ var apiVer = await app.getCsound().getAPIVersion();
+ var el = $("<div />");
+ var x = $("<h3 />").text("twist").appendTo(el);
+ $("<p />").css("font-size", "12px").text("By Richard Knight 2024").appendTo(el);
+ $("<p />").text("Version " + twist.version.toFixed(1)).appendTo(el);
+ $("<p />").text("Csound " + formatVersion(csVer) + "; API " + formatVersion(apiVer)).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);
+ });
+ };
+
+
+ buildWavecontrols();
+}; \ No newline at end of file
diff --git a/site/app/twist/version notes.txt b/site/app/twist/version notes.txt
new file mode 100644
index 0000000..462fe92
--- /dev/null
+++ b/site/app/twist/version notes.txt
@@ -0,0 +1,11 @@
+0.1
+ transient detect movement
+ new transforms
+ feedback
+ convolution feedback
+ instance chopper
+ strobe
+ convolution impulses/preset sound loads
+ source release
+ filter apply mode
+ display csound version \ No newline at end of file