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 ++++++++++++++++++++ 7 files changed, 4889 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 (limited to 'site/app/twist/_unlive') 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 -- cgit v1.2.3