From 9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 13 Apr 2025 18:48:02 +0100 Subject: initial --- site/app/twist/_unlive/apid.js | 978 +++++++++++++++ site/app/twist/_unlive/index.api.html | 1144 ++++++++++++++++++ site/app/twist/_unlive/splinetest.html | 42 + site/app/twist/_unlive/transform.js | 1024 ++++++++++++++++ site/app/twist/_unlive/twist_fxtester.csd | 101 ++ site/app/twist/_unlive/twist_instance_WIP.js | 350 ++++++ .../twist/_unlive/twist_instance_separation_WIP.js | 1250 ++++++++++++++++++++ site/app/twist/developer_documentation.html | 742 ++++++++++++ site/app/twist/documentation.html | 181 +++ site/app/twist/index.html | 98 ++ site/app/twist/twist.csd | 19 + site/app/twist/twist.css | 309 +++++ site/app/twist/twist.js | 1248 +++++++++++++++++++ site/app/twist/twist_ui.js | 674 +++++++++++ site/app/twist/version notes.txt | 11 + 15 files changed, 8171 insertions(+) create mode 100644 site/app/twist/_unlive/apid.js create mode 100644 site/app/twist/_unlive/index.api.html create mode 100644 site/app/twist/_unlive/splinetest.html create mode 100644 site/app/twist/_unlive/transform.js create mode 100644 site/app/twist/_unlive/twist_fxtester.csd create mode 100644 site/app/twist/_unlive/twist_instance_WIP.js create mode 100644 site/app/twist/_unlive/twist_instance_separation_WIP.js create mode 100644 site/app/twist/developer_documentation.html create mode 100644 site/app/twist/documentation.html create mode 100644 site/app/twist/index.html create mode 100644 site/app/twist/twist.csd create mode 100644 site/app/twist/twist.css create mode 100644 site/app/twist/twist.js create mode 100644 site/app/twist/twist_ui.js create mode 100644 site/app/twist/version notes.txt (limited to 'site/app/twist') 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 = $(""); + 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 = $("").appendTo($(")").appendTo(elModulations)); + var row = $("").append($(""); + 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 = $("").appendTo($("
").text("Modulation type")).appendTo(selecttb); + + elModSelect = $("").appendTo(row)); + $("").append(tb).appendTo(elModulations); + + for (let i in twist.appdata.modulations) { + var m = twist.appdata.modulations[i]; + $("
").appendTo(elContainer); + elTb = $("").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 @@ + + + twist + + + + + + + + +
+
+
+

+ +
+
+
+
+
+

twist

+

Web based audio transformer

+

Press to begin

+
+
+
+ Processing +
+
+
+
+
+
+
+
+ +
+
+
+
+ + + \ 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 @@ + + + + twist + + + + + + + +
+
+ + \ 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 = $("
"); + var elValueLabel = $("
"); + 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 " + definition.name + " to " + + definition.options[definition.lagHint.option] + ""; + } else { + lagHint = ((definition.lagHint < 0) ? "reducing" : "increasing") + + " " + definition.name + ""; + } + 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 = $("
)").appendTo(elModulations)); + var row = $("").append($("
").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 = $("").appendTo(row)); + $("").append(tb).appendTo(elModulations); + + for (let i in twist.appdata.modulations) { + var m = twist.appdata.modulations[i]; + var o = $(""); + var name = $("
").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(); + }); + } + + $("").addClass("tfv_cell").append(elContainer).appendTo(elRow); + $("").addClass("tfv_cellfixed").append(elValueLabel).appendTo(elRow); + if (!nocontrols) { + for (let b of [resetButton, randomiseButton]) $("").addClass("tfv_cell_plainbg").append(b.el).appendTo(elRow); + + if (definition.automatable) { + for (let b of [automationButton, editAutomationButton, modButton]) $("").addClass("tfv_cell_plainbg").append(b.el).appendTo(elRow); + } + + } + return elRow; + }; +}; + + + +function getTransformContainer(nameOrElement) { + var el = $("
").addClass("tfv_header"); + if (typeof(nameOrElement) == "string") { + el.text(nameOrElement); + } else { + el.append(nameOrElement); + } + return $("
").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 = $("
").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 = $("
").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 = $("
"); + var header = $("
").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(); + }); + } + + $("
").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); + + $("
").addClass("tfv_container").append( + $("
").addClass("tfv_header").append(el) + ).appendTo(elContainer); + + //getTransformContainer(el).appendTo(elContainer); + var tbl = $("").appendTo(elContainer); + elTb = $("").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 = $(""); + $("
").append(tb).css("margin", "0 auto").appendTo(elNewFile); + tb.append(tpDuration.getElementRow(true)).append(tpChannels.getElementRow(true)).append(tpName.getElementRow(true)); + + $(""); + $("
").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 = $("
"); + elPasteSpecial.append($("

").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); + + $("

").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 = $("").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); + $("").addClass("crossfade").append($("
").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 = $("
"); + var x = $("

").text("twist").appendTo(el); + $("

").text("Version " + appdata.version.toFixed(1)).appendTo(el); + $("

").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(); + }); + + $("

").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 @@ + + + + + + + + +

Extending twist

+
+

Overview

+ Twist can quite easily be extended to feature additional transforms. These can be tested and used on the fly with the developer console within twist, and are encouraged to be submitted for inclusion in the live application which can be done here, via the Help > Submit transform code menu option, or via the link in the developer console

+ 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 transform definition section, and the actual audio processing is defined with Csound code (detailed in the audio processing with Csound section. Additional twist opcodes are provided to the developer in order to ease integration.
+ 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 Csound opcode subsection. + + +
+

Transform definition with JSON

+ The transform definition is a JSON object, which should at the very least have the keys name and instr 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: + + + + + + + + + + +
name
Name as seen in the twist user interface
instr
Csound named instrument, which is to be called to carry out the processing
inputs = 1
Number of input files the transform requires. This defaults to 1 but may be set to 2 for cross-processing transforms and such
description = ""
Description of the transform
author = ""
Author name and other relevant details
parameters = []
Parameter definitions in an array
twine = false
Whether the transform will be available as a twine insert. Only transforms using live input can be used for this purpose
unstable = false
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
+ + +

Parameter definition

+ 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 options 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.

+ The only required key for a parameter definition is name. 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.

+ 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: + + + + + + + + + + + + + + + +
name
Name of the parameter to be shown in the interface
description = ""
Description of the parameter
channel = name.toLowerCase()
Channel name which should correspond to that which is requested by twst_param in the transform instrument. Defaults to the lowercase parameter name
min = 0
Numeric minimum accepted value
max = 1
Numeric maximum accepted value
step = 0.0000001
Incremental allowance of the value, should numeric
dfault = 1
Numeric default value
options = null
Array containing options to be displayed in a drop-down select box. If supplied, the minimum, maximum and step values are redundant. dfault corresponds to the index of the array to be the default value. If asvalue 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
asvalue = false
Whether the selected item from options should be provided to Csound as the actual value rather than the array index
hidden = false
Whether the parameter should be hidden. May be useful passing static data from the interface to Csound
conditions = null
An array of Condition objects which are all to be met for the parameter to be shown
hostrange = false
For child parameters (namely those in modulations), whether the min, max, step, dfault, options and asvalue attributes should be inherited from the parent
preset = null
The name of a preset to be used. Any definition attributes provided by the preset may be overriden
presetgroup = null
The name of a presetgroup to be used, which will provide a number of parameters in place of the current definition +
nameprepend = null
The string which will be prepended to parameter names, if presetgroup is specified +
channelprepend = null
The string which will be prepended to parameter channels, if presetgroup is specified +
+ +
+

Condition

+ The condition definition is a JSON object, which should include all of the following keys: + + + + +
channel
Parameter channel to evaluate
operator
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)
value
Static value to check against the above
+ +
+

Presets

+ These are available as values to specify in the presetgroup parameter attribute and alter set the parameter up as follows + + + + + + + + + +
amp
Amplitude slider with min: 0 and max: 1, channel: "amp"
fftsize
FFT size drop down which may be transparently utilised by twst_getfinput and twst_getfcrossinput, or accessed directly via the channel "fftsize" or the specified channel name with twst_param
wave
f-table selector which may be transparently utilised by twst_tf_getwaveform, twst_tf_getwaveformk, or accessed directly via the channel "wave" or the specified channel name with twst_param or twst_paramk
applymode
Apply mode drop down, which may be Replace, Mix, Modulate or Demodulate. Used internally by twist at the rendering stage
note
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
wintype
Window type drop-down which may be utilised by twst_tf_getwintype, twst_tf_getwintypek or accessed directly via the channel "wintype" or the specified channel name with twst_param or twst_paramk
instance
Drop down selecting a file open in twist, other than that which is currently open. Utilised interally by twst_getcrossinput and twst_getfcrossinput
instanceloop
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 twst_getcrossinput, twst_getfcrossinput and twst_getfcrossdata
+ +
+

Preset groups

+ These are available as values to specify in the presetgroup parameter attribute. + + + + + +
pvanal
Provides FFT size and a frequency/phase locking checkbox, used internally in the provision of PVS stream data within twst_getfinput and twst_getfcrossinput
pvresmode
Provides parameters which control the resynthesis approach as used by twst_tf_fresynth. A drop down permits selection between overlap-add and additive approaches, with the latter showing several further parameters when selected
pitchscale
Provides a scaling mode drop down with semitones or ratio as options. The selected scaling is presented via twst_tf_pitchscale as a ratio
notefreq
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 twst_tf_freq and twst_tf_freqi
+ + +
+ +

Audio processing with Csound

+ Audio processing is carried out for the corresponding JSON transform definition by invoking the Csound instrument specified in the instr key. Multiple Csound instruments and opcodes may be utilised, however it should be noted that the Csound instrument is called using subinstr, 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 schedule and event 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 subinstr.

+ 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.
+ A number of opcodes to ease integration with the UI and transform definition are provided by twist, detailed below with examples in the Opcode reference subsection. +

Rules and style guide

+ The rules and style guide should be adhered to where appropriate, especially if opcodes are to be submitted for inclusion in the live application. +
    +
  • Instruments referenced by the transform definition should be named prepended with twst_tf_ if they generate audio or use direct input (obtained with twst_getinput or twst_getfinput) - or prepended with twst_tfi_ if table access is to be used (with twst_tfi_getfn
  • +
  • The first line of the instrument must be $TWST_TRANSFORM to mark it as a twist transform
  • +
  • Auxilliary instruments and user-defined opcodes should be named prepended with the name of the initial instrument
  • +
  • Instruments may generate audio or process audio input obtained from calls to twst_getinput, twst_getfinput or twst_tfi_getfn
  • +
  • Instruments must emit stereo audio signals using the outs opcode. Depending on the processing action, either of the outputs may be a silent signal or the same as the input
  • +
  • Instruments should be prepared to process or generate left and right channels according to the ileft and iright values from the call to twst_getinput, twst_getfinput or twst_tf_getstate. 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
  • +
  • Any global objects created must not persist after the instrument has finished. For example, ftgentmp must be used rather than ftgen - unless ftfree is used on the f-table accordingly
  • +
  • print opcodes and other console output opcodes should not be used except for debugging purposes
  • +
  • 0dbfs is set at 1, so anything reliant on this should adhere accordingly
  • +
  • Opcodes are limited to those available in the Csound WASM build - this is generally everything, but one noted example of an exclusion is fractalnoise. If otherwise unexplainable errors are encountered, this may be due to an unavailable opcode
  • +
+

Opcode reference

+
+ +
+ +

Examples from the live application

+ All of the JSON transform definitions can be seen here, under the transforms key.
+ 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. + + + \ 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 @@ + + + + + + + +

Overview

+ 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. + +

Concept

+ 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.

+ A transform can be auditioned (previewed) or committed (applied), which will be applicable to the selected region of the waveform, or all of it if nothing is selected. +

+ 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. + +

Basic usage

+ 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 + 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.

+ + 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 view menu, or with the shortcut buttons. Typical cut/copy/paste and undo operations are also provided under the edit menu and with keyboard shortcuts.

+ + 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.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Show allReset the zoom level so all of the waveform can be seen
RewindRewind the playback point and clear the current waveform selection
PlayPlay the selected region of the waveform
AuditionPreview the currently-loaded transform on the selected region of the waveform
CommitApply the currently-loaded transform to the selected region of the waveform
RecordRecord live input to the selected region of the waveform
CutCut the selected region of the waveform to the clipboard
CopyCopy the selected region of the waveform to the clipboard
PastePaste the clipboard contents to the playback point or start of the selected waveform region
Paste specialOpen a dialog to allow pasting the clipboard contents with extended options such as repeat number and mixing
Zoom selectionZoom the view to the selected region of the waveform
Zoom inZoom in the waveform view
Zoom outZoom out the waveform view
Show allZoom out to show all of the waveform
+ +

Applying transforms

+ 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.

+ 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.

+ At the side of each parameter, there are up to four icons, which are as follows: + + + + + + + + + + + + + + + + + + + + + +
ResetReset the parameter to the default value
Include in randomisationInclude the parameter in transform randomisation. Unselected parameters will display the icon in a lighter colour
AutomateDisplay spline automation for the parameter to allow a time-varying value to be entered
ModulateDisplay modulation options for the parameter to allow for a parametric time-varying value

+ + Additionally to the top-right there are two icons globally applicable to all parameters: + + + + + + + + + + + +
RandomiseRandomise 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
ResetReset all parameters to the default value, removing modulations and automation
+ +

Modulation

+ 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:
+ +

+ When there is more than one waveform open in twist, cross-adaptive modulations are available in the modulation type 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 instance drop-down box to select the other waveform to use as an analysis source (from which the selected region will be used), and cross instance loop type to specify the loop type should the length of the region in the analysis waveform be shorter than that of the current waveform. + +

Automation

+ 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:
+
+ 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.

+ At the side of the parameter row, this icon can be pressed to disable the automation:
+

+ At the top right of the transform parameters, the following icon can be used to hide all automation, but keep it enabled:
+
+ To display the automation again, the select icon next to the parameter should be used. + +

Scripting

+ Scripting is not currently documented in full but is fairly self-explanatory. By using the script toolbar shortcut, or the Action > Scripting option on the menu, the script page is shown. By using Load last operation or Load all session operations, you can see the transforms and operations you have previously applied. The script then may be edited, saved and/or auditioned/committed again.
+ 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 selection 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 instanceIndex key and others referencing index are a 0-based index of the loaded waveforms in twist. + +

Developer

+ New transforms can be developed and tested in twist. Full details and API reference are on the developer documentation page. + + \ 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 @@ + + + + twist + + + + + + + + + + + + + + + + + +
+
+

twist

+

audio transformer

+
Press to begin
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Hello

+ 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.
+ At the moment, there is a limitation on files to around five minutes in duration. +
+
+

Scripting

+ 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. +
+ +
+ + + + + + +
+
+

Developer console

+ Code for transforms can be tested here. The code and definition should follow the guidance and API documentation provided here. The JSON definition should be a single transform as a JSON object, but mutiple transforms may be loaded individually.
+ Contributions of transforms are warmly welcomed and can be submitted here. +

Csound code

+ +
+
+

JSON transform definition

+ +
+
+ +
+
+

twist has crashed.

+ 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. + Press here to reload the application. +
+
Attempting to recover your work...
+
+ +
+ + \ 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 @@ + + +-odac + + +sr = 44100 +ksmps = 64 +nchnls = 2 +0dbfs = 1 +seed 0 +nchnls_i = 2 + +#include "/twist/twist.udo" + + + +f0 z + + \ 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($("

").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 = $("").attr("href", "#").text("Download").click(function(e){ + e.preventDefault(); + twist.downloadFile("/crashL.wav"); + }); + if (ndata.left && !ndata.right) { + elSr.append($("

").text("Your work has been recovered:")); + elSr.append(linkLeft); + } else { + elSr.append($("

").text("Your work has been recovered as separate left/right channels:")); + linkLeft.text("Download left channel").appendTo(elSr); + elSr.append("
"); + var linkRight = $("
").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 = $("
").addClass("waveform").appendTo("#twist_waveforms"); + let index = waveformFiles.length; + + if (index < 0) index = 0; + waveformTabs.push( + $("

").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 = $("
"); + $("

").text("Not enough processing power to transform in realtime").appendTo(container); + var lagHintHtml = twist.currentTransform.getLagHints(); + if (lagHintHtml) { + $("

").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 = $("

"); + elPasteSpecial.append($("

").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 + }); + + $("

").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 = $("
").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 = $("
").css({"font-size": "var(--fontSizeDefault)"}); + if (twist.hasClipboard) { + $("
").append(tb).css("margin", "0 auto").appendTo(elEmpty); + tb.append(tpDuration.getElementRow(true)).append(tpChannels.getElementRow(true)).append(tpName.getElementRow(true)); + + $("
"); + 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 = $("").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); + $("").addClass("crossfade").append($("
").css("font-size", "var(--fontSizeSmall)").text("Crossfade " + e)).append(elRange).appendTo(el); + } + + $("
").css("font-size", "var(--fontSizeSmall").append("Loop playback
").append( + $("").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 = $("
"); + var x = $("

").text("twist").appendTo(el); + $("

").css("font-size", "12px").text("By Richard Knight 2024").appendTo(el); + $("

").text("Version " + twist.version.toFixed(1)).appendTo(el); + $("

").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 -- cgit v1.2.3