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