diff options
Diffstat (limited to 'site/app/twist/_unlive/twist_instance_separation_WIP.js')
-rw-r--r-- | site/app/twist/_unlive/twist_instance_separation_WIP.js | 1250 |
1 files changed, 1250 insertions, 0 deletions
diff --git a/site/app/twist/_unlive/twist_instance_separation_WIP.js b/site/app/twist/_unlive/twist_instance_separation_WIP.js new file mode 100644 index 0000000..3805807 --- /dev/null +++ b/site/app/twist/_unlive/twist_instance_separation_WIP.js @@ -0,0 +1,1250 @@ +var NoteData = function() {
+ var self = this;
+ this.data = null;
+ fetch("../base/notedata.json").then(function(r) {
+ r.json().then(function(j) {
+ self.data = j;
+ });
+ });
+};
+
+
+
+
+var OperationWatchdog = function(twist) {
+ var self = this;
+ var active = false;
+ var lastValues = [true, false];
+ var firstActive = true;
+ var checkInterval;
+ var timeoutTime = 2000;
+ var alivetimeoutTime = 2000;
+ var context;
+
+ function crash() {
+ self.stop();
+ twist.sendErrorState("Unhandled exception in " + context);
+ var el = $("#twist_crash").show();
+ var elSr = $("#twist_crash_recovery");
+
+ function doomed() {
+ elSr.empty().append($("<h4 />").text("Sorry, unfortunately your work cannot be saved."));
+ }
+
+ var doomedTimeout = setTimeout(doomed, 6000);
+
+ var cbid = app.createCallback(function(ndata) {
+ if (doomedTimeout) clearTimeout(doomedTimeout);
+
+ if (!ndata.left && !ndata.right) {
+ return doomed();
+ }
+ elSr.empty();
+ var text;
+ var linkLeft = $("<a />").attr("href", "#").text("Download").click(function(e){
+ e.preventDefault();
+ twist.downloadFile("/crashL.wav");
+ });
+ if (ndata.left && !ndata.right) {
+ elSr.append($("<h4 />").text("Your work has been recovered:"));
+ elSr.append(linkLeft);
+ } else {
+ elSr.append($("<h4 />").text("Your work has been recovered as separate left/right channels:"));
+ linkLeft.text("Download left channel").appendTo(elSr);
+ elSr.append("<br />");
+ var linkRight = $("<a />").attr("href", "#").text("Download right channel").click(function(e){
+ e.preventDefault();
+ twist.downloadFile("/crashR.wav");
+ }).appendTo(elSr);
+ }
+
+ });
+ app.getCsound().compileOrc("iwrittenL = 0\niwrittenR = 0\nif (gitwst_bufferL[gitwst_instanceindex] > 0) then\niwrittenL ftaudio gitwst_bufferL[gitwst_instanceindex], \"/crashL.wav\", 14\nendif\nif (gitwst_bufferR[gitwst_instanceindex] > 0) then\niwrittenR ftaudio gitwst_bufferR[gitwst_instanceindex], \"/crashR.wav\", 14\nendif\nio_sendstring(\"callback\", sprintf(\"{\\\"cbid\\\":" + cbid + ",\\\"left\\\":%d,\\\"right\\\":%d}\", iwrittenL, iwrittenR))\n");
+ }
+
+ function checkAlive() {
+ var alive = false;
+ var aliveTimeout = setTimeout(crash, alivetimeoutTime);
+ var cbid = app.createCallback(function(){
+ clearTimeout(aliveTimeout);
+ alive = true;
+ });
+ app.insertScore("twst_checkalive", [0, 1, cbid]);
+ }
+
+ this.start = function(startContext) {
+ active = true;
+ context = startContext;
+ firstActive = true;
+ lastValues = [true, false];
+ if (checkInterval) clearInterval(checkInterval);
+ checkInterval = setInterval(function() {
+ if (lastValues[0] === lastValues[1]) {
+ checkAlive();
+ }
+ }, timeoutTime);
+ };
+
+ this.setActive = function(value) {
+ if (!active) return;
+ if (firstActive) {
+ firstActive = false;
+ } else {
+ lastValues[0] = lastValues[1];
+ }
+ lastValues[1] = value;
+ };
+
+ this.stop = function() {
+ active = false;
+ firstActive = true;
+ lastValues = [true, false];
+ if (checkInterval) clearInterval(checkInterval);
+ };
+};
+
+var Twist = function(appdata) {
+ var self = this;
+ var audioTypes = ["audio/mpeg", "audio/mp4", "audio/ogg", "audio/vorbis", "audio/x-flac","audio/aiff","audio/x-aiff", "audio/vnd.wav", "audio/wave", "audio/x-wav", "audio/wav", "audio/flac"];
+ var maxsize = 1e+8; // 100 MB
+ this.currentTransform = null;
+ var errorState;
+ var instanceIndex = 0;
+ this.appdata = appdata;
+ this.instances = [];
+ var playheadInterval;
+ var latencyCorrection = 100;
+ var playing = false;
+ var auditioning = false;
+ var scope;
+ var recording = false;
+ var elCrossfades = [];
+ this.onPlays = [];
+ var elToolTip = $("<div />").addClass("tooltip").appendTo($("body"));
+ this.audioContext = null;
+ var operationLog = [];
+ this.noteData = new NoteData();
+ var topMenu = new TopMenu(self, topMenuData, $("#twist_menubar"));
+ this.storage = localStorage.getItem("twist");
+ this.watchdog = new OperationWatchdog(self);
+
+ if (self.storage) {
+ self.storage = JSON.parse(self.storage);
+ } else {
+ self.storage = {};
+ }
+
+ this.tooltip = {
+ show: function(event, text) {
+ var margin = 100;
+ elToolTip.text(text).css("opacity", 0.9);
+
+ if (event.pageX >= window.innerWidth - margin) {
+ elToolTip.css({left: window.innerWidth - (margin * 2) + "px"});
+ } else {
+ elToolTip.css({left: (event.pageX + 20) + "px"});
+ }
+
+ if (event.pageY >= window.innerHeight - margin) {
+ elToolTip.css({top: window.innerHeight - (margin * 2) + "px"});
+ } else {
+ elToolTip.css({top: (event.pageY - 15) + "px"});
+ }
+
+ },
+ hide: function() {
+ elToolTip.css("opacity", 0);
+ }
+ };
+
+ this.setPlaying = function(state) {
+ if (playing == state) return;
+ playing = state;
+ for (var o of self.onPlays) {
+ o(playing, auditioning, recording);
+ }
+ if (self.currentTransform) {
+ self.currentTransform.setPlaying(state);
+ }
+ if (scope) {
+ scope.setPlaying(state);
+ }
+ };
+
+ this.saveStorage = function() {
+ localStorage.setItem("twist", JSON.stringify(self.storage));
+ };
+
+ function lastOperation() {
+ return operationLog[operationLog.length - 1];
+ }
+
+ function pushOperationLog(operation) {
+ var max = self.storage.commitHistoryLevel;
+ if (!max) {
+ self.storage.commitHistoryLevel = max = 16;
+ }
+ if (operationLog.length + 1 >= max) {
+ operationLog.shift();
+ }
+ operationLog.push(operation);
+ }
+
+
+ function showLoadNewPrompt() {
+ var elNewFile = $("<div />").css({"font-size": "var(--fontSizeDefault)"});
+ elNewFile.append($("<h3 />").text("Drag an audio file here to load")).append($("<p />").text("or"));
+
+ $("<h4 />").text("Create an empty file").css("cursor", "pointer").appendTo(elNewFile).click(function() {
+ elNewFile.show();
+ });
+
+ var tpDuration = new TransformParameter(null, {name: "Duration", min: 0.1, max: 60, dfault: 10, automatable: false, fireChanges: false}, null, null, twist);
+
+ var tpChannels = new TransformParameter(null, {name: "Channels", min: 1, max: 2, dfault: 2, step: 1, automatable: false, fireChanges: false}, null, null, twist);
+
+ var tpName = new TransformParameter(null, {name: "Name", type: "string", dfault: "New file", fireChanges: false}, null, null, twist);
+
+ var tb = $("<tbody />");
+ $("<table />").append(tb).css("margin", "0 auto").appendTo(elNewFile);
+ tb.append(tpDuration.getElementRow(true)).append(tpChannels.getElementRow(true)).append(tpName.getElementRow(true));
+
+ $("<button />").text("Create").appendTo(elNewFile).click(function() {
+ var name = tpName.getValue();
+ if (name.trim() == "") {
+ name = "New file";
+ }
+ var cbid = app.createCallback(async function(ndata) {
+ self.waveformTab.text(name);
+ await globalCallbackHandler(ndata);
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = name;
+ setLoadingStatus(false);
+ });
+ self.hidePrompt();
+ setLoadingStatus(true, false, "Creating");
+ app.insertScore("twst_createempty", [0, 1, cbid, tpDuration.getValue(), tpChannels.getValue()]);
+ });
+
+ self.showPrompt(elNewFile, null, true);
+ }
+
+ this.toggleScope = function(noSaveState) {
+ var height;
+ var top;
+ var state;
+ if (!scope) {
+ state = true;
+ height = "60%";
+ top = "40%";
+ var elScope = $("<div />").addClass("twist_scope").appendTo($("#twist_waveforms"));
+ var type = (self.storage.scopeType) ? self.storage.scopeType : "frequency";
+ scope = new Analyser(
+ type, self, elScope, app
+ );
+ } else {
+ state = false;
+ scope.remove();
+ delete scope;
+ scope = null;
+ height = "100%";
+ top = "0px";
+ }
+
+ if (!noSaveState) {
+ self.storage.showScope = state;
+ self.saveStorage();
+ }
+ $(".waveform").css({height: height, top: top});
+ }
+
+ this.createNewInstance = function() {
+ var element = $("<div />").addClass("waveform").appendTo("#twist_waveforms");
+ let index = waveformFiles.length;
+
+ if (index < 0) index = 0;
+ waveformTabs.push(
+ $("<td />").text("New file").click(function() {
+ if (self.isPlaying()) return;
+ self.waveform = index;
+ }).addClass("wtab_selected").appendTo("#twist_waveform_tabs")
+ );
+ undoLevels.push(0);
+ self.waveforms.push(
+ new Waveform({
+ target: element,
+ latencyCorrection: latencyCorrection,
+ showcrossfades: true,
+ crossFadeWidth: 1,
+ timeBar: true,
+ markers: [
+ {preset: "selectionstart"},
+ {preset: "selectionend"},
+ ]
+ })
+ );
+ showLoadNewPrompt();
+ self.waveform = index;
+ };
+
+
+
+
+
+ this.removeInstance = function(i) {
+ if (!i) i = instanceIndex;
+ if (i < 0 || i > this.instances.length - 1) {
+ return;
+ }
+ self.instances[instanceindex].close();
+ if (instanceIndex == i) {
+ instanceIndex = i + ((i == 0) ? 1 : -1);
+ self.instances[instanceIndex].show();
+ }
+ };
+
+ t
+
+ var remoteSessionID;
+ var remoteSending = false;
+ this.sendErrorState = async function (errorText) {
+ if (remoteSending) return;
+ remoteSending = true;
+ var data = {
+ request_type: "LogError",
+ error: {
+ text: errorText,
+ lastOperation: lastOperation()
+ }
+ };
+
+ if (self.currentTransform) {
+ var state = await self.currentTransform.getState();
+ data.error.transformState = state;
+ }
+
+ if (remoteSessionID) {
+ data.session_id = remoteSessionID;
+ }
+ var resp = await fetch("/service/", {
+ method: "POST",
+ headers: {
+ "Content-type": "application/json"
+ },
+ body: JSON.stringify(data)
+ });
+ var json = await resp.json();
+ if (json.session_id && !remoteSessionID) {
+ remoteSessionID = json.session_id;
+ }
+ remoteSending = false;
+ }
+
+ this.errorHandler = function(text, onComplete) {
+ var errorText = (!text) ? errorState : text;
+ self.sendErrorState(errorText);
+ self.setPlaying(false);
+ self.showPrompt(errorText, onComplete);
+ errorState = null;
+ };
+
+ function playPositionHandler(noPlayhead, onComplete) {
+ function callback(ndata) {
+ if (ndata.status == 1) {
+ self.setPlaying(true);
+ if (!noPlayhead) {
+ watchdog.start("audition");
+ if (playheadInterval) {
+ clearInterval(playheadInterval);
+ }
+ playheadInterval = setInterval(async function(){
+ var val = await app.getControlChannel("playposratio");
+ watchdog.setActive(val);
+ if (val < 0 || val > 1) {
+ clearInterval(playheadInterval);
+ }
+ self.waveform.movePlayhead(val);
+ }, 50);
+ }
+ } else {
+ self.setPlaying(false);
+ if (ndata.status == -1) {
+ self.errorHandler("Not enough processing power to transform in realtime");
+ }
+
+ app.removeCallback(ndata.cbid);
+ if (!noPlayhead) {
+ watchdog.stop();
+ self.waveform.movePlayhead(0);
+ if (playheadInterval) {
+ clearInterval(playheadInterval);
+ }
+ }
+ if (onComplete) onComplete();
+ }
+ }
+ return app.createCallback(callback, true);
+ }
+
+ function operation(instr, oncompleteOrCbidOverride, showLoading, selection, noLogScript) {
+ var s = (selection) ? selection : self.waveform.selected;
+ errorState = "Operation error";
+ if (showLoading) {
+ setLoadingStatus(true);
+ }
+ var cbid;
+ if (!oncompleteOrCbidOverride || typeof(oncompleteOrCbidOverride) == "function") {
+ cbid = app.createCallback(function(ndata) {
+ self.waveform.cover(false);
+ if (oncompleteOrCbidOverride) {
+ oncompleteOrCbidOverride(ndata);
+ } else if (ndata.status && ndata.status <= 0) {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file is too large";
+ }
+ self.errorHandler(text);
+ }
+ if (showLoading) {
+ setLoadingStatus(false);
+ }
+ });
+ } else {
+ cbid = oncompleteOrCbidOverride;
+ }
+ if (!noLogScript) {
+ pushOperationLog({type: "operation", instr: instr, selection: s, instanceIndex: instanceIndex});
+ }
+ app.insertScore(instr, [0, 1, cbid, s[0], s[1], s[2]]);
+ }
+
+ this.isPlaying = function() {
+ return playing;
+ };
+
+
+
+ this.pasteSpecial = function() {
+ if (playing) return;
+ var elPasteSpecial = $("<div />");
+ elPasteSpecial.append($("<h4 />").text("Paste special"));
+ var def = {
+ instr: "twst_pastespecial",
+ parameters: [
+ {name: "Repetitions", channel: "repetitions", min: 1, max: 40, step: 1, dfault: 1, automatable: false},
+ {name: "Mix paste", channel: "mixpaste", step: 1, dfault: 0, automatable: false},
+ {name: "Mix crossfade", channel: "mixfade", automatable: false, conditions: [{channel: "mixpaste", operator: "eq", value: 1}]}
+ ]
+ };
+ var tf = new Transform(elPasteSpecial, def, self);
+
+ $("<button />").text("Paste").click(function(){
+ self.hidePrompt();
+ self.waveform.cover(true);
+ operation("twst_pastespecial", globalCallbackHandler, true);
+ }).appendTo(elPasteSpecial);
+
+ $("<button />").text("Cancel").click(function(){
+ self.hidePrompt();
+ }).appendTo(elPasteSpecial);
+ self.showPrompt(elPasteSpecial, null, true);
+
+ };
+
+ this.developerConsole = function() {
+ $("#twist_developer").show();
+ $("#twist_inject_devcsound").click(async function() {
+ var code = $("#twist_devcsound").val();
+ var result = await app.compileOrc(code);
+ if (result == 0) {
+ if (!self.storage.develop) {
+ self.storage.develop = {};
+ }
+ self.storage.develop.csound = code;
+ self.saveStorage();
+ self.showPrompt("Successfully injected Csound code");
+ }
+ });
+ $("#twist_inject_devjson").click(async function() {
+ var code = $("#twist_devjson").val();
+ try {
+ var json = JSON.parse(code);
+ } catch (e) {
+ return self.errorHandler("Cannot parse JSON: " + e);
+ }
+ try {
+ self.loadTransforms(json);
+ } catch (e) {
+ return self.errorHandler("Cannot load transform: " + e);
+ }
+ if (!self.storage.develop) {
+ self.storage.develop = {};
+ }
+ self.storage.develop.json = code;
+ self.saveStorage();
+ self.showPrompt("Successfully injected transform definition");
+ });
+ $("#twist_exit_devcode").click(async function() {
+ $("#twist_developer").hide();
+ });
+ };
+
+ this.play = function() {
+ if (playing) return;
+ auditioning = false;
+ recording = false;
+ operation("twst_play", playPositionHandler(), false, null, true);
+ };
+
+ this.stop = function() {
+ if (!playing) return;
+ self.waveform.cover(false);
+ app.insertScore("twst_stop");
+ };
+
+ function formatFileName(name) {
+ if (!name) name = waveformTabs[instanceIndex].text();
+ if (!name.toLowerCase().endsWith(".wav")) {
+ name += ".wav";
+ }
+
+ // HACK TODO: WASM can't overwrite files
+ name = name.substr(0, name.lastIndexOf(".")) + "." + saveNumber + name.substr(name.lastIndexOf("."));
+ saveNumber ++;
+ // END HACK
+ return name;
+ }
+
+ this.downloadFile = async function(path, name) {
+ if (!name) name = formatFileName(name);
+ var content = await app.readFile(path);
+ var blob = new Blob([content], {type: "audio/wav"});
+ var url = window.URL.createObjectURL(blob);
+ var a = $("<a />").attr("href", url).attr("download", name).appendTo($("body")).css("display", "none");
+ a[0].click();
+ setTimeout(function(){
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ app.unlinkFile(path);
+ }, 20000);
+ };
+
+ var saveNumber = 1;
+ this.saveFile = function(name, onComplete) {
+ if (playing) return;
+ if (!name) name = formatFileName(name);
+ var cbid = app.createCallback(async function(ndata){
+ await self.downloadFile("/" + name, name);
+ if (onComplete) onComplete();
+ setLoadingStatus(false);
+ });
+ setLoadingStatus(true, true, "Saving");
+ app.insertScore("twst_savefile", [0, 1, cbid, name]);
+ };
+
+ function getAutomationData(start, end) {
+ var calls = [];
+ if (!self.currentTransform) return calls;
+ var automations = self.currentTransform.getAutomationData(start, end);
+ if (automations && automations.length > 0) {
+ for (let i in automations) {
+ if (automations[i].type == "modulation") {
+ calls.push(automations[i].data[0] + " \\\"" + automations[i].data[1] + "\\\"");
+ } else if (automations[i].type == "automation") {
+ calls.push("chnset linseg:k(" + automations[i].data + "), \\\"" + automations[i].channel + "\\\"");
+ }
+ }
+ }
+ return calls;
+ }
+
+ function handleAutomation(onready, calls) {
+ if (calls.length == 0) {
+ return onready(0);
+ }
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 1) {
+ onready(1);
+ } else {
+ self.errorHandler("Cannot parse automation data");
+ }
+ });
+
+ var call = [0, 1, cbid];
+ for (let c of calls) {
+ call.push(c);
+ }
+ app.insertScore("twst_automationprepare", call);
+ }
+
+ function compileVariScript(script, onComplete) {
+ var cbid = app.createCallback(function(ndata){
+ onComplete(ndata.status == 1);
+ // should maybe automatically refresh
+ });
+ }
+
+
+ function fftsizeCheck(selected, duration) {
+ if (self.currentTransform) {
+ for (var p in self.currentTransform.parameters) {
+ if (p.indexOf("fftsize") != -1) {
+ var val = self.currentTransform.parameters[p].getValue();
+ var minTime = (val / sr) * 2;
+ if ((selected[1] - selected[0]) * duration < minTime) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ this.record = async function() {
+ if (playing) return;
+ auditioning = false;
+ recording = true;
+ await app.enableAudioInput();
+ errorState = "Recording error";
+ self.waveform.cover(true);
+ var cbid = playPositionHandler();
+ var s = self.waveform.selected;
+ var items = [0, 1, cbid, s[0], s[1], s[2]];
+ app.insertScore("twst_record", items);
+ };
+
+ this.audition = function() {
+ if (playing) return;
+ if (!self.currentTransform) {
+ return self.play();
+ }
+ self.currentTransform.saveState();
+ var s = self.waveform.selected;
+ if (!fftsizeCheck(s, self.waveform.duration)) {
+ return self.errorHandler("Length too short for this transform");
+ }
+
+ auditioning = true;
+ recording = false;
+ errorState = "Playback error";
+ handleAutomation(function(automating){
+ var cbid = playPositionHandler();
+ var items = [
+ 0, 1, cbid, s[0], s[1], s[2],
+ self.currentTransform.instr, automating,
+ elCrossfades[0].val(), elCrossfades[1].val()
+ ];
+ app.insertScore("twst_audition", items);
+ }, getAutomationData(s[0], s[1]));
+
+ };
+
+
+ var scriptStack = [];
+ function applyScript(audition) {
+ if (playing) return;
+ var lastData;
+ var script = scriptStack.shift();
+ if (!script) {
+ setLoadingStatus(false);
+ if (lastData) {
+ console.log("ass", lastData);
+ globalCallbackHandler(lastData);
+ }
+ self.setPlaying(false);
+ return;
+ }
+
+ if (audition) auditioning = true;
+ self.setPlaying(true);
+ if (script.type == "operation") {
+ if (audition) {
+ return self.errorHandler("Only transform scripts can be auditioned");
+ }
+ self.waveform.cover(true);
+ onComplete = (script.instr == "twst_copy") ? null : globalCallbackHandler;
+ operation(script.instr, function(ndata){
+ lastData = ndata;
+ self.setPlaying(false);
+ applyScript(audition);
+ }, true, script.selection);
+ } else if (script.type == "transform") {
+ errorState = ((audition) ? "Audition" : "Transform" ) + " commit error";
+ if (!audition) {
+ setLoadingStatus(true, true);
+ }
+
+ for (let channel in script.channels) {
+ app.setControlChannel(channel, script.channels[channel]);
+ }
+ handleAutomation(function(automating){
+ if (audition) {
+ var cbid = playPositionHandler();
+ } else {
+ var cbid = app.createCallback(function(ndata) {
+ lastData = ndata;
+ self.setPlaying(false);
+ applyScript(audition);
+ });
+ }
+ var instr = "twst_" + ((audition) ? "audition" : "commit");
+
+ app.insertScore(instr, [
+ 0, -1, cbid, script.selection[0], script.selection[1], script.selection[2], script.instr, automating, script.crossfades[0], script.crossfades[1]
+ ]);
+ }, script.automation);
+ }
+ }
+
+ this.applyScript = async function(script, audition) {
+ if (playing) return;
+ scriptStack = [];
+ if (Array.isArray(script)) {
+ if (audition) {
+ return self.errorHandler("Only single scripts can be auditioned");
+ }
+ scriptStack = script;
+ } else {
+ scriptStack = [script];
+ }
+ if (self.storage.autosave && !audition) {
+ self.saveFile(null, function() {
+ applyScript(audition);
+ });
+ } else {
+ applyScript(audition);
+ }
+ };
+
+ async function innerCommit() {
+ if (playing) return;
+ if (!self.currentTransform) return;
+ var s = self.waveform.selected;
+ if (!fftsizeCheck(s, self.waveform.duration)) {
+ return self.errorHandler("Length too short for this transform");
+ }
+ watchdog.start("commit");
+ self.setPlaying(true);
+ setLoadingStatus(true, true);
+ var calls = getAutomationData(s[0], s[1]);
+
+ self.currentTransform.saveState();
+ var state = await self.currentTransform.getState();
+ state.type = "transform";
+ state.automation = calls;
+ state.crossfades = [elCrossfades[0].val(), elCrossfades[1].val()];
+ state.selection = [s[0], s[1], s[2]];
+ state.instanceIndex = instanceIndex;
+ pushOperationLog(state);
+
+ handleAutomation(function(automating){
+ var cbid = app.createCallback(function(ndata) {
+ watchdog.stop();
+ setLoadingStatus(false);
+ self.setPlaying(false);
+ if (ndata.status > 0) {
+ globalCallbackHandler(ndata);
+ } else {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file is too large";
+ }
+ self.errorHandler(text);
+ }
+ });
+ errorState = "Transform commit error";
+ app.insertScore("twst_commit", [0, -1, cbid, s[0], s[1], s[2], self.currentTransform.instr, automating, state.crossfades[0],state.crossfades[1]]);
+ }, calls);
+ }
+
+ this.commit = async function() {
+ if (self.storage.autosave) {
+ self.saveFile(null, function() {
+ innerCommit();
+ });
+ } else {
+ innerCommit();
+ }
+ };
+
+ this.createIcon = function(definition) {
+ var state = true;
+ var active = true;
+ function formatPath(i) {
+ return "../base/icon/" + i + ".svg";
+ }
+ var el = $("<img />");
+
+ var obj = {
+ el: el,
+ setState: function(tstate) {
+ if (!definition.icon2) return;
+ state = tstate;
+ if (state) {
+ el.attr("src", formatPath(definition.icon));
+ } else {
+ el.attr("src", formatPath(definition.icon2));
+ }
+
+ },
+ setActive: function(state) {
+ if (state) {
+ el.css("opacity", 1);
+ active = true;
+ } else {
+ el.css("opacity", 0.4);
+ active = false;
+ }
+ }
+ };
+
+ el.addClass("icon").css("opacity", 1).attr("src", formatPath(definition.icon)).on("mouseover", function(event){
+ var label = (!state && definition.label2) ? definition.label2 : definition.label;
+ self.tooltip.show(event, label);
+ }).on("mouseout", function(){
+ self.tooltip.hide();
+ }).click(function(el) {
+ if (active) definition.click(obj);
+ });
+ return obj;
+ }
+
+ function buildWavecontrols() {
+ var el = $("#twist_wavecontrols_inner");
+ var onPlayDisables = [];
+
+ var play = self.createIcon({label: "Play", icon: "play", label2: "Stop", icon2: "stop", click: function(obj){
+ if (self.isPlaying()) {
+ self.stop();
+ } else {
+ self.play();
+ }
+ }});
+ var audition = self.createIcon({label: "Audition", icon: "audition", label2: "Stop", icon2: "stop", click: function(obj){
+ if (self.isPlaying()) {
+ self.stop();
+ } else {
+ self.audition();
+ }
+ }});
+ var crossfade = self.createIcon({label: "Show crossfades", icon: "crossfade", label2: "Hide crossfades", icon2: "hide", click: function(obj){
+ var el = $(".crossfade");
+ if (el.is(":visible")) {
+ obj.setState(true);
+ el.hide();
+ self.storage.showCrossfades = false;
+ elCrossfades[0].val(0).trigger("input");
+ elCrossfades[1].val(0).trigger("input");
+ } else {
+ el.show();
+ obj.setState(false);
+ self.storage.showCrossfades = true;
+ }
+ self.saveStorage();
+ }});
+
+ var record = self.createIcon({label: "Record", icon: "record", label2: "Stop", icon2: "stop", click: function() {
+ if (self.isPlaying()) {
+ self.stop();
+ } else {
+ self.record();
+ }
+ }});
+
+ var items = [
+ {label: "Zoom selection", icon: "zoomSelection", click: function() {self.waveform.zoomSelection();}},
+ {label: "Zoom in", icon: "zoomIn", click: function() {self.waveform.zoomIn();}},
+ {label: "Zoom out", icon: "zoomOut", click: function() {self.waveform.zoomOut();}},
+ {label: "Show all", icon: "showAll", click: function() {self.waveform.setRegion(0, 1);}},
+ {label: "Cut", icon: "cut", disableOnPlay: true, click: self.cut},
+ {label: "Copy", icon: "copy", disableOnPlay: true, click: self.copy},
+ {label: "Paste", icon: "paste", disableOnPlay: true, click: self.paste},
+ {label: "Paste special", icon: "pasteSpecial", disableOnPlay: true, click: self.pasteSpecial},
+ {label: "Rewind", icon: "rewind", disableOnPlay: true, click: self.moveToStart},
+ play,
+ audition,
+ {label: "Commit", icon: "commit", disableOnPlay: true, click: self.commit},
+ record,
+ {label: "Save", icon: "save", disableOnPlay: true, click: self.saveFile},
+ {label: "Script", icon: "script", click: self.scriptEdit},
+ {label: "Developer", icon: "develop", click: self.developerConsole},
+ crossfade
+ ];
+
+ for (let i of items) {
+ var icon;
+ if (i.icon) {
+ icon = self.createIcon(i);
+ if (i.disableOnPlay) {
+ onPlayDisables.push(icon);
+ }
+ } else {
+ icon = i;
+ }
+ $("<td />").append(icon.el).appendTo(el);
+ }
+
+ twist.onPlays.push(async function(playing, auditioning, recording) {
+ if (playing) {
+ if (auditioning) {
+ play.setActive(false);
+ audition.setState(false);
+ record.setActive(false);
+ } else if (recording) {
+ audition.setActive(false);
+ play.setActive(false);
+ record.setState(false);
+ } else {
+ audition.setActive(false);
+ play.setState(false);
+ record.setActive(false);
+ }
+ } else {
+ audition.setActive(true);
+ play.setActive(true);
+ play.setState(true);
+ audition.setState(true);
+ record.setActive(true);
+ record.setState(true);
+ }
+ for (let o of onPlayDisables) {
+ o.setActive(!playing);
+ }
+ });
+
+ for (let e of ["In", "Out"]) {
+ let elRange = $("<input />").addClass("tp_slider").attr("type", "range").attr("min", 0).attr("max", 0.45).attr("step", 0.00001).val(0).on("input", function() {
+ if (e == "In") {
+ self.waveform.crossFadeInRatio = $(this).val();
+ } else {
+ self.waveform.crossFadeOutRatio = $(this).val();
+ }
+ });
+ elCrossfades.push(elRange);
+ $("<td />").addClass("crossfade").append($("<div />").css("font-size", "8pt").text("Crossfade " + e)).append(elRange).appendTo(el);
+ }
+
+ var el = $(".crossfade");
+ if (self.storage.hasOwnProperty("showCrossfades")) {
+ if (self.storage.showCrossfades) {
+ crossfade.setState(false);
+ el.show();
+ } else {
+ crossfade.setState(true);
+ el.hide();
+ }
+ } else {
+ crossfade.setState(false);
+ el.show();
+ }
+
+ }
+
+ this.loadTransforms = function(transform) {
+ if (transform) {
+ var developObj;
+ for (var t in appdata.transforms) {
+ if (appdata.transforms[t].name == "Develop") {
+ developObj = appdata.transforms[t];
+ break;
+ }
+ }
+ if (!developObj) {
+ developObj = {name: "Develop", contents: []};
+ appdata.transforms.push(developObj);
+ } else {
+ for (var c in developObj.contents) {
+ if (developObj.contents[c].name == transform.name) {
+ delete developObj.contents[c];
+ }
+ }
+ }
+ developObj.contents.push(transform);
+ }
+
+ $("#twist_panetree").empty();
+ var ttv = new TransformsTreeView({
+ target: "twist_panetree",
+ items: appdata.transforms
+ }, self);
+ };
+
+ this.showHelp = function() {
+ $("#twist_help").show();
+ };
+
+ this.showAbout = function() {
+ var el = $("<div />");
+ var x = $("<h3 />").text("twist").appendTo(el);
+ $("<p />").text("Version " + appdata.version.toFixed(1)).appendTo(el);
+ $("<p />").css("font-size", "12px").text("By Richard Knight 2024").appendTo(el);
+
+ var skewMax = 30;
+ var skew = 0;
+ var skewDirection = true;
+ var twistInterval = setInterval(function(){
+ if (skewDirection) {
+ if (skew < skewMax) {
+ skew ++;
+ } else {
+ skewDirection = false;
+ }
+ } else {
+ if (skew > -skewMax) {
+ skew --;
+ } else {
+ skewDirection = true;
+ }
+ }
+ x.css("transform", "skewX(" + skew + "deg)");
+ }, 10);
+
+ self.showPrompt(el, function(){
+ clearInterval(twistInterval);
+ });
+ };
+
+ async function handleFileDrop(e, obj) {
+ e.preventDefault();
+ if (!e.originalEvent.dataTransfer && !e.originalEvent.files) {
+ return;
+ }
+ if (e.originalEvent.dataTransfer.files.length == 0) {
+ return;
+ }
+ self.hidePrompt();
+ setLoadingStatus(true, false, "Loading");
+ for (const item of e.originalEvent.dataTransfer.files) {
+ if (!audioTypes.includes(item.type)) {
+ return self.errorHandler("Unsupported file type", showLoadNewPrompt);
+ }
+ if (item.size > maxsize) {
+ return self.errorHandler("File too large", showLoadNewPrompt);
+ }
+ errorState = "File loading error";
+ var content = await item.arrayBuffer();
+ const buffer = new Uint8Array(content);
+ await app.writeFile(item.name, buffer);
+ var cbid = app.createCallback(async function(ndata){
+ await app.unlinkFile(item.name);
+ if (ndata.status == -1) {
+ return self.errorHandler("File not valid", showLoadNewPrompt);
+ } else if (ndata.status == -2) {
+ return self.errorHandler("File too large", showLoadNewPrompt);
+ } else {
+ self.waveformTab.text(item.name);
+ await globalCallbackHandler(ndata);
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = item.name;
+ self.hidePrompt();
+ setLoadingStatus(false);
+ }
+ });
+ app.insertScore("twst_loadfile", [0, 1, cbid, item.name]);
+ }
+ }
+
+ async function globalCallbackHandler(ndata) {
+ if (ndata.status && ndata.status <= 0) {
+ self.errorHandler();
+ return;
+ }
+
+ if (ndata.hasOwnProperty("undolevel")) {
+ self.undoLevel = ndata.undolevel;
+ }
+
+ if (ndata.hasOwnProperty("delete")) {
+ if (typeof(ndata.delete) == "string") {
+ app.unlinkFile(ndata.delete);
+ } else {
+ for (let d of ndata.delete) {
+ app.unlinkFile(d);
+ }
+ }
+ }
+
+ if (ndata.hasOwnProperty("selstart")) {
+ self.waveform.setSelection(ndata.selstart, ndata.selend);
+ }
+
+ if (ndata.hasOwnProperty("waveL")) {
+ self.waveform.cover(true);
+ errorState = "Overview refresh error";
+ var wavedata = [];
+ var duration = ndata.duration;
+ var tbL = await app.getTable(ndata.waveL);
+ wavedata.push(tbL);
+ if (ndata.hasOwnProperty("waveR")) {
+ var tbR = app.getTable(ndata.waveR);
+ wavedata.push(tbR);
+ }
+ self.waveform.setData(wavedata, ndata.duration);
+ self.waveform.cover(false);
+ }
+
+ }
+
+ this.bootAudio = function() {
+ var channelDefaultItems = ["dcblockoutputs", "tanhoutputs", "maxundo"];
+
+ for (var i of channelDefaultItems) {
+ if (self.storage.hasOwnProperty(i)) {
+ app.setControlChannel(i, self.storage[i]);
+ }
+ }
+
+ twist.setLoadingStatus(false);
+
+ if (!self.storage.hasOwnProperty("firstLoadDone")) {
+ self.storage.firstLoadDone = true;
+ self.saveStorage();
+ self.showPrompt($("#twist_welcome").detach().show(), self.createNewInstance);
+ } else {
+ self.createNewInstance();
+ }
+
+ if (self.storage.showScope) {
+ self.toggleScope(true);
+ }
+ };
+
+ this.boot = function() {
+ self.audioContext = new AudioContext();
+ if (self.storage.theme) {
+ self.setTheme(self.storage.theme, true);
+ }
+
+ if (self.storage.hasOwnProperty("showShortcuts")) {
+ if (self.storage.showShortcuts) {
+ $("#twist_wavecontrols_inner").show();
+ } else {
+ $("#twist_wavecontrols_inner").hide();
+ }
+ }
+
+ if (self.storage.develop) {
+ if (self.storage.develop.csound) {
+ $("#twist_devcsound").val(self.storage.develop.csound);
+ }
+ if (self.storage.develop.json) {
+ $("#twist_devjson").val(self.storage.develop.json);
+ }
+ }
+ $("#loading_background").css("opacity", 1).animate({opacity: 0.2}, 1000);
+
+ Object.defineProperty(this, "waveformTab", {
+ get: function() { return waveformTabs[instanceIndex]; },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "otherInstanceNames", {
+ get: function() {
+ var data = {};
+ for (var i in waveformTabs) {
+ if (i != instanceIndex) {
+ data[i] = waveformTabs[i].text();
+ }
+ }
+ return data
+ },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "instanceIndex", {
+ get: function() {
+ return instanceIndex
+ },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "undoLevel", {
+ get: function() {
+ return undoLevels[instanceIndex];
+ },
+ set: function(x) {
+ undoLevels[instanceIndex] = x;
+ }
+ });
+
+ Object.defineProperty(this, "waveform", {
+ get: function() { return self.waveforms[instanceIndex]; },
+ set: function(x) {
+ if (instanceIndex != x) {
+ if (self.waveformTab) {
+ self.waveformTab.removeClass("wtab_selected").addClass("wtab_unselected");
+ }
+ if (self.waveform) {
+ self.waveform.hide();
+ }
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 1) {
+ instanceIndex = x;
+ self.waveformTab.removeClass("wtab_unselected").addClass("wtab_selected");
+ self.waveform.show();
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ } else {
+ self.showPrompt("Error changing instance");
+ }
+ });
+ app.insertScore("twst_setinstance", [0, 1, cbid, x]);
+
+ }
+ }
+ });
+
+ $("#twist_help").click(function() {
+ $(this).hide();
+ });
+
+ $("<td />").text("+").click(function() {
+ self.createNewInstance();
+ }).appendTo("#twist_waveform_tabs").addClass("wtab_selected");
+
+ $("body").on("dragover", function(e) {
+ e.preventDefault();
+ e.originalEvent.dataTransfer.effectAllowed = "all";
+ e.originalEvent.dataTransfer.dropEffect = "copy";
+ return false;
+ }).on("dragleave", function(e) {
+ e.preventDefault();
+ }).on("drop", function(e) {
+ handleFileDrop(e, self);
+ });
+
+ buildWavecontrols();
+ self.loadTransforms();
+ };
+
+}; // end twist
+
+$(function() {
+
+ var csOptions = ["--omacro:TWST_FAILONLAG=1"];
+ window.twist = new Twist(appdata);
+ window.app = new CSApplication({
+ csdUrl: "twist.csd",
+ csOptions: csOptions,
+ onPlay: function () {
+ twist.bootAudio();
+ },
+ errorHandler: twist.errorHandler,
+ ioReceivers: {percent: twist.setPercent}
+ });
+
+ $("#start").click(function() {
+ $("#start").hide();
+ twist.boot();
+ twist.setLoadingStatus(true, false, "Preparing audio engine");
+ app.play(function(text){
+ twist.setLoadingStatus(true, false, text);
+ }, twist.audioContext);
+ });
+
+});
\ No newline at end of file |