aboutsummaryrefslogtreecommitdiff
path: root/site/app/twine/twine.js
diff options
context:
space:
mode:
Diffstat (limited to 'site/app/twine/twine.js')
-rw-r--r--site/app/twine/twine.js517
1 files changed, 517 insertions, 0 deletions
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 = $("<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 = $("<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 = $("<div />").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