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/twist.js | 1248 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1248 insertions(+) create mode 100644 site/app/twist/twist.js (limited to 'site/app/twist/twist.js') diff --git a/site/app/twist/twist.js b/site/app/twist/twist.js new file mode 100644 index 0000000..a7a248c --- /dev/null +++ b/site/app/twist/twist.js @@ -0,0 +1,1248 @@ +var OperationWatchdog = function(twist) { + var self = this; + var active = false; + var lastValues = [true, true]; + var firstActive = true; + var checkInterval; + var timeoutTime = 30000; + var alivetimeoutTime = 3500; + var context; + + function crash() { + self.stop(); + twirl.sendErrorState({text: "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, true]; + 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, true]; + if (checkInterval) clearInterval(checkInterval); + }; +}; + +var Twist = function() { + twirl.init(); + var self = this; // TODO deprecate this in favour of below + var twist = this; + this.storage = localStorage.getItem("twist"); + if (self.storage) { + self.storage = JSON.parse(self.storage); + } else { + self.storage = { + dcblockoutputs: 1, + tanhoutputs: 1, + maxundo: 2, + showShortcuts: 1, + commitHistoryLevel: 16, + scopeType: 0 + }; + } + + twist.version = 1; + this.currentTransform = null; + var errorState; + var instanceIndex = 0; + this.waveforms = []; + var waveformFiles = []; + var waveformTabs = []; + var waveformLoaded = []; + this.playheadInterval = null; + var playing = false; + var auditioning = false; + var recording = false; + this.onPlays = []; + this.onInstanceChangeds = []; + this.operationLog = []; + var sr = 44100; + var undoLevels = []; + var onSave; + this.visible = false; + this.playbackLoop = false; + this.twine = null; + this.hasClipboard = false; + this.watchdog = new OperationWatchdog(twist); + this.ui = new TwistUI(twist); + + this.setPlaying = function(state) { + if (playing == state) return; + playing = state; + for (var o of twist.onPlays) { + o(playing, auditioning, recording); + } + if (twist.currentTransform) { + twist.currentTransform.setPlaying(state); + } + twist.ui.setPlaying(state); + + if (!state) { + twist.watchdog.stop(); + twist.waveform.movePlayhead(0); + if (twist.playheadInterval) { + clearInterval(twist.playheadInterval); + } + } + }; + + this.saveStorage = function() { + localStorage.setItem("twist", JSON.stringify(twist.storage)); + }; + + this.lastOperation = function() { + return twist.operationLog[twist.operationLog.length - 1]; + }; + + this.clearOperationLog = function() { + twist.operationLog = []; + }; + + async function pushOperationLog(operation, logChannels) { + var max = twist.storage.commitHistoryLevel; + if (!max) { + twist.storage.commitHistoryLevel = max = 16; + } + if (twist.operationLog.length + 1 >= max) { + twist.operationLog.shift(); + } + if (logChannels) { + if (!operation.channels) operation.channels = {}; + for (let c of logChannels) { + operation.channels[c] = await app.getControlChannel(c); + } + } + twist.operationLog.push(operation); + } + + this.createNewInstance = function(noShowLoadNew) { + var element = $("
").addClass("waveform").appendTo("#twist_waveforms"); + let index = waveformFiles.length; + + if (index < 0) index = 0; + waveformTabs.push( + $("").text("New file").click(function() { + if (twist.isPlaying()) return; + twist.waveform = index; + }).addClass("wtab_selected").appendTo("#twist_waveform_tabs") + ); + undoLevels.push(0); + var waveform = new Waveform({ + target: element, + latencyCorrection: twirl.latencyCorrection, + showcrossfades: true, + crossFadeWidth: 1, + timeBar: true, + markers: [ + {preset: "selectionstart"}, + {preset: "selectionend"}, + ] + }) + waveform.onRegionChange = function(region) { + if (twist.currentTransform) { + twist.currentTransform.redraw(region); + } + }; + twist.waveforms.push(waveform); + if (!noShowLoadNew) twist.ui.showLoadNewPrompt(); + twist.waveform = index; + for (let o of twist.onInstanceChangeds) { + o(true, index); + } + }; + + + function removeInstance(i) { + if (!i) i = instanceIndex; + if (twist.waveforms.length == 1 || i < 0 || i > twist.waveforms.length - 1) { + return; + } + twist.waveforms[i].destroy(); + delete twist.waveforms[i]; + waveformTabs[i].remove(); + waveformLoaded[instanceIndex] = false; + delete waveformTabs[i] + if (instanceIndex == i) { + instanceIndex = i + ((i == 0) ? 1 : -1); + twist.waveform.show(); + } + for (let o of twist.onInstanceChangeds) { + o(false, i); + } + } + + this.closeInstance = function(i) { + removeInstance(i); + }; + + + + this.errorHandler = async function(text, onComplete) { + var errorObj = { + lastOperation: twist.lastOperation() + }; + if (twist.currentTransform) { + var state = await twist.currentTransform.getState(); + errorObj.transformState = state; + } + + twirl.errorHandler(text, onComplete, errorObj); + twist.setPlaying(false); + }; + + function playPositionHandler(noPlayhead, onComplete, monitorChannels) { + function callback(ndata) { + if (ndata.status == 1) { // playing + twist.setPlaying(true); + if (!noPlayhead) { + twist.watchdog.start("audition"); + if (twist.playheadInterval) { + clearInterval(twist.playheadInterval); + } + twist.playheadInterval = setInterval(async function(){ + var val = await app.getControlChannel("twst_playposratio"); + twist.watchdog.setActive(val); + if (val < 0 || val > 1) { + clearInterval(twist.playheadInterval); + } + + var monitorValues; + if (monitorChannels) { + monitorValues = []; + monitorValues.push((monitorChannels[0]) ? await app.getControlChannel(monitorChannels[0]) : null); + monitorValues.push((monitorChannels[1]) ? await app.getControlChannel(monitorChannels[1]) : null); + } else { + monitorValues = null; + } + twist.waveform.movePlayhead(val, monitorValues); + }, 50); + } + return; + } + // stopped + app.removeCallback(ndata.cbid); + + if (twist.playbackLoop && ndata.status == 0 && onComplete) { + return onComplete(ndata); + } + twist.setPlaying(false); + + if (ndata.status == -1) { + var container = $("
"); + $("

").text("Not enough processing power to transform in realtime").appendTo(container); + var lagHintHtml = twist.currentTransform.getLagHints(); + if (lagHintHtml) { + $("

").html(lagHintHtml).appendTo(container); + } + + return twirl.prompt.show(container); + } else if (ndata.status == 2) { // record complete + globalCallbackHandler(ndata); + } + if (onComplete) onComplete(ndata); + + } + return app.createCallback(callback, true); + } + + function operation(options) { + var s = (options.selection) ? options.selection : twist.waveform.selected; + errorState = "Operation error"; + if (options.showLoading) { + twist.ui.setLoadingStatus(true); + } + var cbid; + if (!options.onComplete || typeof(options.onComplete) == "function") { + cbid = app.createCallback(function(ndata) { + twist.waveform.cover(false); + if (options.onComplete) { + options.onComplete(ndata); + } else if (ndata.status && ndata.status <= 0) { + var text; + if (ndata.status == -2) { + text = "Resulting file would be too large"; + } + twist.errorHandler(text); + } + if (options.showLoading) { + twist.ui.setLoadingStatus(false); + } + }); + } else { + cbid = options.onComplete; + } + if (!options.noLogScript) { + pushOperationLog({ + type: "operation", + instr: options.instr, + name: options.name, + selection: s, + instanceIndex: instanceIndex + }, options.logScriptChannels); + } + app.insertScore(options.instr, [0, 1, cbid, s[0], s[1], s[2], (options.noCheckpoint) ? 1 : 0]); + } + + this.isPlaying = function() { + return playing; + }; + + this.redraw = function() { + if (twist.currentTransform) { + twist.currentTransform.redraw(); + } + for (let w of twist.waveforms) { + w.redraw(); + } + }; + + this.undo = function() { + if (playing) return; + twist.waveform.cover(true); + operation({ + instr: "twst_undo", + name: "Undo", + onComplete: globalCallbackHandler, + showLoading: true, + noLogScript: true + }); + }; + + this.cut = function() { + if (playing) return; + twist.waveform.cover(true); + operation({ + instr: "twst_cut", + name: "Cut", + onComplete: globalCallbackHandler, + showLoading: true, + }); + twist.hasClipboard = true; + }; + + this.trim = function() { + if (playing) return; + twist.waveform.cover(true); + operation({ + instr: "twst_trim", + name: "Trim", + onComplete: globalCallbackHandler, + showLoading: true, + }); + }; + + this.delete = function() { + if (playing) return; + twist.waveform.cover(true); + operation({ + instr: "twst_delete", + name: "Delete", + onComplete: globalCallbackHandler, + showLoading: true, + }); + }; + + this.copy = function() { + if (playing) return; + twist.waveform.cover(true); + operation({ + instr: "twst_copy", + name: "Copy", + showLoading: true, + }); + twist.hasClipboard = true; + }; + + this.paste = function() { + if (playing) return; + twist.waveform.cover(true); + operation({ + instr: "twst_paste", + name: "Paste", + onComplete: globalCallbackHandler, + showLoading: true, + }); + }; + + this.moveToNextTransient = function() { + if (playing) return; + var cbid = app.createCallback(globalCallbackHandler); + var s = twist.waveform.selected; + app.insertScore("twst_nexttransient", + [0, 1, cbid, s[1], s[1], s[2]] + ); + }; + + this.selectToNextTransient = function() { + if (playing) return; + var cbid = app.createCallback(globalCallbackHandler); + var s = twist.waveform.selected; + var selend = (s[0] == s[1]) ? s[1] + 0.000001 : s[1]; + app.insertScore("twst_nexttransient", + [0, 1, cbid, s[0], selend, s[2]] + ); + }; + + this.moveToStart = function() { + if (playing) return; + twist.waveform.setSelection(0); + }; + + this.moveToEnd = function() { + if (playing) return; + twist.waveform.setSelection(1); + }; + + this.selectAll = function() { + if (playing) return; + twist.waveform.setSelection(0, 1); + }; + + this.selectNone = function() { + if (playing) return; + twist.waveform.setSelection(0); + }; + + this.selectToEnd = function() { + if (playing) return; + twist.waveform.alterSelection(null, 1); + } + + this.selectFromStart = function() { + if (playing) return; + twist.waveform.alterSelection(0, null); + } + + 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} + ] + }; + var tf = new twirl.transform.Transform({ + element: elPasteSpecial, + definition: def, + host: twist + }); + + $("