From 9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 13 Apr 2025 18:48:02 +0100 Subject: initial --- site/app/twine/twine.js | 517 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 site/app/twine/twine.js (limited to 'site/app/twine/twine.js') diff --git a/site/app/twine/twine.js b/site/app/twine/twine.js new file mode 100644 index 0000000..c81cc66 --- /dev/null +++ b/site/app/twine/twine.js @@ -0,0 +1,517 @@ +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); + } +} \ No newline at end of file -- cgit v1.2.3