var Twine = function() { var twine = this; var hrefSplit = window.location.href.split("?"); if (hrefSplit.length == 2 && hrefSplit[1] == "offline") { twine.offline = true; } else { twine.offline = false; } twine.version = 1; twirl.init(); twine.visible = true; var playing = false; var onStop; twine.timeline = new Timeline(twine); twine.ui = new TwineUI(twine); twine.mixer = new Mixer(twine); twine.sr = null; twine.arrangementName = "New twine arrangement"; var undoHistory = []; var playbackTable; var playbackTableLength; twine.ui.head.name.element.val(twine.arrangementName); twine.ui.head.grid.setValue(1, true); twine.ui.head.snap.setValue(1, true); twine.storage = localStorage.getItem("twine"); if (twine.storage) { twine.storage = JSON.parse(twine.storage); } else { twine.storage = { showMasterVu: 0, showClipWaveforms: 1 }; } this.saveStorage = function() { localStorage.setItem("twine", JSON.stringify(twine.storage)); }; var maxID = 0; this.getNewID = function() { return maxID++; }; twine.undo = { add: function(name, func) { undoHistory.push({name: name, func: func}); }, apply: function() { var item = undoHistory.pop(); item.func(); //twine.timeline.redraw(); // should be handled in the undo func }, clear: function() { undoHistory = []; }, has: function() { return (undoHistory.length > 0); }, lastName: function() { var name = ""; if (undoHistory.length > 0) { name = undoHistory[undoHistory.length - 1].name; } return name ; } }; this.setPlaying = function(state) { playing = state; playHandler(state); twine.timeline.setPlaying(playing); }; this.roundToNearest = function(val, multiple, offset) { if (!offset) offset = 0; return (Math.round((val - offset) / multiple) * multiple) + offset; }; this.stop = function() { if (!playing) return; app.insertScore("twine_stopplayback", [0, 1]); }; this.exportData = async function() { var saveData = { timeline: await twine.timeline.exportData(), maxID: maxID, arrangementName: twine.arrangementName, version: twine.version, sr: twine.sr } return saveData; }; this.importData = async function(loadData) { maxID = loadData.maxID; twine.undo.clear(); twine.arrangementName = loadData.arrangementName; await twine.timeline.importData(loadData.timeline); twine.ui.head.name.element.val(twine.arrangementName); }; this.downloadExportData = async function() { twirl.loading.show(); const saveData = await twine.exportData(); const stream = new Blob([JSON.stringify(saveData)], {type: "application/json"}).stream(); const crs = stream.pipeThrough(new CompressionStream("gzip")); const resp = await new Response(crs); const blob = await resp.blob(); var name = twine.arrangementName + ".twine"; var url = window.URL.createObjectURL(blob); var a = $("").attr("href", url).attr("download", name).appendTo($("body")).css("display", "none"); a[0].click(); twirl.loading.hide(); setTimeout(function(){ a.remove(); window.URL.revokeObjectURL(url); }, 20000); }; this.uploadImportData = async function(blob, onComplete) { twirl.loading.show(); // const stream = new Blob([data], {type: "application/json"}); const stream = blob.stream(); const crs = stream.pipeThrough(new DecompressionStream("gzip")); const resp = await new Response(crs); const dblob = await resp.blob(); await twine.importData(JSON.parse(await dblob.text())); if (onComplete) { onComplete(); } twirl.loading.hide(); }; this.showMixer = function() { twine.ui.showPane(twine.ui.pane.MIXER); twine.mixer.show(); }; var playHandlerInterval; function playHandler(playing) { if (playHandlerInterval) { clearInterval(playHandlerInterval); } if (playing) { if (twine.storage.showMasterVu) { playHandlerInterval = setInterval(async function(){ if (twine.mixer.visible) { twine.mixer.setVu(await app.getControlChannel("twine_mastervu")); } }, 50); } } } async function setPlaybackArtefacts(beatStart, beatEnd, onReady) { if (!beatStart) beatStart = 0; twine.timeline.playbackBeatStart = beatStart; var data = []; var time; var playLength; var reltime; var maxbeat = 0; var beatTime = 60 / twine.timeline.data.bpm; for (let ch of twine.timeline.channels) { for (let i in ch.clips) { var cl = ch.clips[i]; if (!cl) continue; time = cl.data.position; playLength = cl.data.playLength; if (time + playLength >= beatStart && (!beatEnd || (time <= beatEnd))) { if (time >= beatStart) { offset = 0; } else { offset = beatStart - time; } reltime = time - beatStart; if (reltime + playLength > maxbeat) { maxbeat = reltime + playLength; } if (cl.isAudio) { data.push(cl.data.clipindex); } else { data.push(-cl.data.id); } data.push(Math.max(0, reltime) * beatTime); data.push(playLength * beatTime); data.push(cl.channel.index); data.push(offset * beatTime); console.log( "clipindex " + cl.data.clipindex + ", " + "channel " + cl.channel.index + ", " + "beat " + reltime + ", " + "len " + playLength + ", " + "offset " + offset ); } } } console.log(data); if (data.length == 0) return; async function doPlay() { await app.getCsound().tableCopyIn(playbackTable, data); onReady(maxbeat * beatTime, data.length); } twine.timeline.compileAutomationData(function(){ if (data.length > playbackTableLength) { app.insertScore("twine_removetable", [0, 1, playbackTable, async function(ndata){ createPlaybackTable(playbackTableLength * 2, doPlay); }]); } else { doPlay(); } }); } var bounceNumber = 1; this.renderToClip = function(beatStart, beatEnd) { if (playing) return; twirl.loading.show("Rendering", true); setPlaybackArtefacts(beatStart, beatEnd, function(maxtime, maxtablen){ var cbid = app.createCallback(function(ndata2){ if (ndata2.status == 1) { var channel = twine.timeline.addChannel(); var clip = new Clip(twine); clip.data.position = beatStart; clip.data.playLength = beatEnd - beatStart; channel.addClip(clip); clip.loadFromFtables("Bounce " + (bounceNumber ++), [ndata2.fnL, ndata2.fnR]); } else if (ndata2.status == -2) { twirl.errorHandler("Resulting output is too long"); } else { twirl.errorHandler("Render cannot be completed"); } twirl.loading.hide(); }); app.insertScore("twine_render", [0, 1, cbid, playbackTable, twine.timeline.channels.length, maxtime, 0, maxtablen]); }); }; var saveNumber = 1; this.renderToFile = function(beatStart, beatEnd, name) { if (playing) return; if (!name) { name = twine.arrangementName + ".wav"; } // HACK TODO: WASM can't overwrite files name = name.substr(0, name.lastIndexOf(".")) + "." + (saveNumber ++) + name.substr(name.lastIndexOf(".")); // END HACK var path = "/" + name; twirl.loading.show("Rendering", true); setPlaybackArtefacts(beatStart, beatEnd, function(maxtime, maxtablen){ var cbid = app.createCallback(async function(ndata2){ if (ndata2.status == 1) { var content = await app.readFile(path); var blob = new Blob([content], {type: "audio/wav"}); var url = window.URL.createObjectURL(blob); var 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); twirl.loading.hide(); } else { twirl.errorHandler("Could not save file"); } }); app.insertScore("twine_render", [0, 1, cbid, playbackTable, twine.timeline.channels.length, maxtime, 1, maxtablen, path]); }); }; this.play = function() { if (playing) return; setPlaybackArtefacts(twine.timeline.startLocation, null, function(maxtime, maxtablen){ var cbid = app.createCallback(function(ndata){ if (ndata.status <= 0) { twine.setPlaying(false); app.removeCallback(ndata.cbid); if (ndata.status == -1) { twirl.prompt.show("Not enough processing power to play in realtime"); } else { if (onStop) { setTimeout(function(){ onStop(ndata); onStop = null; }, 100); // race condition on ftable somehow } } } else { twine.setPlaying(true); } }, true); app.insertScore("twine_playback", [0, maxtime, cbid, playbackTable, twine.timeline.channels.length, maxtablen]); }); }; /* this.play = function(beatStart, beatEnd) { if (playing) return; if (!beatStart) beatStart = 0; var time; var reltime; var maxtime = 0; var beatTime = 60 / twine.timeline.data.bpm; for (let ch of twine.timeline.channels) { for (let i in ch.clips) { var cl = ch.clips[i]; time = cl.data.position; if (time > beatStart && (!beatEnd || (time <= beatEnd))) { reltime = time - beatStart; if (reltime + cl.duration > maxtime) { maxtime = reltime + cl.duration; } app.insertScore("ecp_playback", cl.getPlaybackArgs(-1, reltime * beatTime)); } } } var cbid = app.createCallback(function() { twine.setPlaying(false); }); app.insertScore("twine_playbackwatchdog", [0, maxtime, cbid]); }; */ this.stop = function(onStopFunc) { if (!playing) return; onStop = onStopFunc; app.insertScore("twine_stopplayback"); }; this.stopAndPlay = function(beatStart, beatEnd) { function doPlay() { twine.play(beatStart, beatEnd); } if (playing) { twine.stop(doPlay); } else { doPlay(); } }; this.setVisible = function(state) { var el = $("#twine"); if (state) { el.show(); } else { el.hide(); } }; async function handleFileDrop(e, posTop, posLeft, colour) { e.preventDefault(); twirl.loading.show(); var channel = twine.timeline.determineChannelFromTop(posTop); if (!channel) { channel = twine.timeline.addChannel(); } var relPosition = 0; for (const item of e.originalEvent.dataTransfer.files) { if (item.name.endsWith(".twine")) { window.ass = item; twine.uploadImportData(item); return; } if (!twirl.audioTypes.includes(item.type)) { return twirl.errorHandler("Unsupported file type"); } if (item.size > twirl.maxFileSize) { return twirl.errorHandler("File too large"); } errorState = "File loading error"; var content = await item.arrayBuffer(); const buffer = new Uint8Array(content); if (!twine.offline) await app.getCsound().fs.writeFile(item.name, buffer); var clip = new Clip(twine); posLeft = twine.timeline.roundToGrid(posLeft); var position = (posLeft / twine.timeline.pixelsPerBeat) + twine.timeline.beatRegion[0]; clip.data.position = Math.max(0, position + relPosition); channel.addClip(clip); clip.loadFromPath(item.name, colour); relPosition += 1; } if (twine.offline) twirl.loading.hide(); } this.randomColour = function() { return "rgb(" + ([Math.round(Math.random() * 255), Math.round(Math.random() * 255), Math.round(Math.random() * 255)].join(",")) + ")"; }; function fileDropHandler() { var tempclip = null; function calcPosition(e) { var tlo = $("#twine_timelineoverlay").show(); var o = tlo.offset(); var top = e.pageY - o.top; var left = e.pageX - o.left; var visible = true; if (top < 0 || left < 0 || top > tlo.height() || left > tlo.width()) { visible = false; } return { overlay: tlo, left: left, top: top, visible: visible } } window.addEventListener("resize", twine.timeline.redraw); $("body").on("dragover", function(e) { e.preventDefault(); e.originalEvent.dataTransfer.effectAllowed = "all"; e.originalEvent.dataTransfer.dropEffect = "copy"; var pos = calcPosition(e); if (!tempclip) { tempclip = $("
").addClass("twine_clip").css({"background-color": twine.randomColour(), height: "25px", width: "100px"}).text("New Clip"); pos.overlay.append(tempclip); } if (!pos.visible) { tempclip.hide(); } else { tempclip.show().css("top", pos.top + "px").css("left", pos.left + "px"); } return false; }).on("dragleave", function(e) { e.preventDefault(); if (e.currentTarget.contains(e.relatedTarget)) return; $("#twine_timelineoverlay").hide(); if (tempclip) { tempclip.remove(); tempclip = null; } }).on("drop", function(e) { var pos = calcPosition(e); $("#twine_timelineoverlay").hide(); var colour = tempclip.css("background-color"); if (tempclip) { tempclip.remove(); tempclip = null; } if (pos.visible) { handleFileDrop(e, pos.top, pos.left, colour); } }); } function createPlaybackTable(length, onComplete) { if (twine.offline) return; if (!length) length = 5000; app.insertScore("twine_createtable", [0, 1, app.createCallback(function(ndata){ playbackTable = ndata.table; playbackTableLength = length; if (onComplete) onComplete(); }), length]); } this.bootAudio = async function() { for (var i = 0; i < 8; i++) { twine.timeline.addChannel(); } if (!twine.offline) { twine.mixer.bootAudio(); twine.sr = (await app.getCsound().getSr()); } createPlaybackTable(); }; this.boot = function() { fileDropHandler(); }; }; function twine_start() { var elStart = $("#twine_start"); function boot() { elStart.hide(); twirl.boot(); twine.boot(); if (!twine.offline) { twirl.loading.show("Preparing audio engine"); app.play(function(text){ twirl.loading.show(text); twirl.latencyCorrection = twirl.audioContext.outputLatency * 1000; }, twirl.audioContext); } } window.twine = new Twine(); window.twigs = new Twigs(); window.twist = new Twist(); if (twine.offline) { window.app = null; boot(); twine.bootAudio(); } else { window.app = new CSApplication({ csdUrl: "twine.csd", onPlay: function() { twine.bootAudio(); twigs.bootAudio(); twirl.loading.hide(); }, errorHandler: twirl.errorHandler }); $("#twine_start").click(boot); } }