From 9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 13 Apr 2025 18:48:02 +0100 Subject: initial --- .../twist/_unlive/twist_instance_separation_WIP.js | 1250 ++++++++++++++++++++ 1 file changed, 1250 insertions(+) create mode 100644 site/app/twist/_unlive/twist_instance_separation_WIP.js (limited to 'site/app/twist/_unlive/twist_instance_separation_WIP.js') 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($("

").text("Sorry, unfortunately your work cannot be saved.")); + } + + var doomedTimeout = setTimeout(doomed, 6000); + + var cbid = app.createCallback(function(ndata) { + if (doomedTimeout) clearTimeout(doomedTimeout); + + if (!ndata.left && !ndata.right) { + return doomed(); + } + elSr.empty(); + var text; + var linkLeft = $("").attr("href", "#").text("Download").click(function(e){ + e.preventDefault(); + twist.downloadFile("/crashL.wav"); + }); + if (ndata.left && !ndata.right) { + elSr.append($("

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

").text("Your work has been recovered as separate left/right channels:")); + linkLeft.text("Download left channel").appendTo(elSr); + elSr.append("
"); + var linkRight = $("
").attr("href", "#").text("Download right channel").click(function(e){ + e.preventDefault(); + twist.downloadFile("/crashR.wav"); + }).appendTo(elSr); + } + + }); + app.getCsound().compileOrc("iwrittenL = 0\niwrittenR = 0\nif (gitwst_bufferL[gitwst_instanceindex] > 0) then\niwrittenL ftaudio gitwst_bufferL[gitwst_instanceindex], \"/crashL.wav\", 14\nendif\nif (gitwst_bufferR[gitwst_instanceindex] > 0) then\niwrittenR ftaudio gitwst_bufferR[gitwst_instanceindex], \"/crashR.wav\", 14\nendif\nio_sendstring(\"callback\", sprintf(\"{\\\"cbid\\\":" + cbid + ",\\\"left\\\":%d,\\\"right\\\":%d}\", iwrittenL, iwrittenR))\n"); + } + + function checkAlive() { + var alive = false; + var aliveTimeout = setTimeout(crash, alivetimeoutTime); + var cbid = app.createCallback(function(){ + clearTimeout(aliveTimeout); + alive = true; + }); + app.insertScore("twst_checkalive", [0, 1, cbid]); + } + + this.start = function(startContext) { + active = true; + context = startContext; + firstActive = true; + lastValues = [true, 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 = $("
").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 = $("
").css({"font-size": "var(--fontSizeDefault)"}); + elNewFile.append($("

").text("Drag an audio file here to load")).append($("

").text("or")); + + $("

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