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 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 978 insertions(+) create mode 100644 site/app/twist/_unlive/apid.js (limited to 'site/app/twist/_unlive/apid.js') 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($("
").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(); + }); +}); + + -- cgit v1.2.3