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