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(); }); });