var Clip = function(twine, data, parent) { var clip = this; var loaded = false; var waveformClip; var waveformEdit; var datatable; this.channel = null; var minWidth = 10; this.types = {AUDIO: 0, SCRIPT: 1}; if (!data) { var id = twine.getNewID(); var data = { name: "Clip " + id, type: (parent) ? parent.type : null, id: id, clipindex: null, playLength: 1, pitch: 1, colour: "#" + (Math.random() * 0xFFFFFF << 0).toString(16), position: 0, // debugs: duration: 1, warp: 0, loop: 0, script: "" }; } else { data.id = twine.getNewID(); loaded = true; } this.data = data; Object.defineProperty(this, "colour", { get: function() { return data.colour; }, set: function(x) { data.colour = x; clip.element.css("background-color", data.colour); } }); Object.defineProperty(this, "isAudio", { get: function() { return (clip.data.type == clip.types.AUDIO); }, set: function(x) {} }); twine.undo.add("add clip", function(){ clip.destroy(); twine.timeline.redraw(); }); this.exportData = async function() { // tablecopyout messes first few values. so loop... var len = await app.getCsound().tableLength(datatable); var items = []; for (var i = 0; i < len; i ++) { items.push(await app.getCsound().tableGet(datatable, i)); } var local = {}; for (var d in dataMode) { if (dataMode[d] < 0) { local[d] = data[d]; } } return {table: items, local: local}; }; this.importData = async function(loadData) { if (datatable && clip.data.clipindex) { await app.insertScoreAsync("twine_removeclip", [clip.data.clipindex]); } datatable = await twine.timeline.copyNewTableIn(loadData.table); var ndata = await app.insertScoreAsync("twine_importclip", [datatable]); for (var d in loadData.local) { data[d] = loadData.local[d]; } data.clipindex = ndata.data.clipindex; await getDataFromTable(); console.log("import", loadData, data); loaded = true; clip.redraw(); // race here? }; this.destroy = function(onComplete) { function done() { clip.element.remove(); clip.channel.removeClip(clip); if (onComplete) { onComplete(); } } if (clip.isAudio) { app.insertScore("twine_removeclip", [0, 1, app.createCallback(done), clip.data.clipindex]); } else { done(); } }; var dataMode = { fnL: 0, fnR: 1, divisionsPerBeat: 2, duration: 3, beatsLength: 4, utilisedLength: 5, warpMode: 6, pitch: 7, amp: 8, fftSize: 9, txtWinSize: 10, txtRandom: 11, txtOverlap: 12, loop: 13, warp: 14, txtWinType: 15, utilisedStart: 16, phaseLock: 17, sr: 18, // warp points are after this+ in table position: -1, name: -2, clipindex: -3, playLength: -4, type: -5 }; this.element = $("
").addClass("twine_clip").css({ "background-color": data.colour }).on("contextmenu", function(e){ return twirl.contextMenu.show(e, [ {name: "Delete", click: function(){ clip.destroy(); }}, {name: "Audition", click: function(){ clip.play(); }} ]); }).click(function(e){ if (e.ctrlKey) { twine.timeline.selectedClips.push(clip); } else { $(".twine_clip").css("outline", "none"); twine.timeline.selectedClips = [clip]; } var uiType; var cui = twine.ui.clip; clip.markSelected(); if (clip.isAudio) { uiType = twine.ui.pane.CLIPAUDIO; var items = [ "name", "colour", "amp", "warp", "warpMode", "pitch", "fftSize", "txtWinSize", "txtRandom", "txtOverlap", "txtWinType", "phaseLock" // , "loop" ]; for (let i of items) { cui[i].setValue(data[i]); } showEditWaveform($("#twine_clipdetailsrightaudio")); } else { uiType = twine.ui.pane.CLIPSCRIPT; cui.scriptEdit.setValue(data.script); } twine.ui.showPane(uiType); }); var elWaveClip = $("
").css({position: "absolute", width: "100%", height: "100%", top: "0px", left: "0px"}).appendTo(clip.element); var elWaveText = $("
").css({position: "absolute", width: "100%", height: "100%", top: "0px", left: "0px", "font-size": "var(--fontSizeSmall)", color: "var(--fgColor1)"}).text(data.name).appendTo(clip.element); var elResizeLeft = $("
").addClass("twine_clip_edge_left").appendTo(clip.element); var elResizeRight = $("
").addClass("twine_clip_edge_right").appendTo(clip.element); var elMove = $("
").addClass("twine_clip_centre").appendTo(clip.element); var elWaveEdit = $("
").css({width: "100%", height: "100%", top: "0px", left: "0px"}); async function getDataFromTable() { if (!clip.isAudio) return; async function setFromKey(key) { if (dataMode[key] < 0) return; var value = await app.getCsound().tableGet(datatable, dataMode[key]) data[key] = value; } for (var k in dataMode) { await setFromKey(k); } } function setClipAudioUnique(onComplete) { if (!clip.isAudio) return onComplete(); twirl.loading.show(); var cbid = app.createCallback(async function(ndata){ await getDataFromTable(); twirl.loading.hide(); if (onComplete) onComplete(); }); app.insertScore("twine_setclipaudiounique", [0, 1, cbid]); } function getScriptInstrName() { return "twinescript" + id; } this.setScript = function(script, onready) { var originalScript = clip.data.script; if (script) { clip.data.script = script; } var instr = "instr " + getScriptInstrName() + "\n" + "iduration = p5\nichannel = p6\n" + clip.data.script + "\nendin"; app.compileOrc(instr).then(function(status){ if (status >= 0 && onready) { // errors will be caught by app and shown twine.undo.add("set script", function(){ clip.data.script = originalScript; if (twine.timeline.selectedClip == clip) { cui.scriptEdit.setValue(data.script); } }); onready(); } }); }; this.initScript = function() { data.warp = 1; data.duration = 1; clip.data.playLength = 4; clip.data.type = clip.types.SCRIPT; clip.data.script = "; your Csound instrument here"; loaded = true; }; function reloadAfterEdit(tables) { twirl.loading.show("Loading"); var cbid = app.createCallback(async function(ndata){ datatable = ndata.datatable; await getDataFromTable(); clip.redraw(); twine.setVisible(true); twigs.setVisible(false); twist.setVisible(false); twirl.loading.hide(); }); var call = [0, 1, cbid, data.clipindex]; for (let t of tables) { call.push(t); } app.insertScore("twine_clipreplacetables", call); } this.editInTwist = function(asUnique) { if (!window.twist) return twirl.prompt.show("twist is unavailable in this session"); function edit() { twist.boot(twine); twist.bootAudio(twine); var tables = [data.fnL]; if (data.fnR) tables.push(data.fnR); twist.loadFileFromFtable(data.name, tables, function(ndata){ if (ndata.status > 0) { twine.setVisible(false); twist.setVisible(true); } }, reloadAfterEdit); } function checksr() { // twist uses tfi transforms that may require sample sr to be running sr.. if (twine.sr != data.sr) { twirl.loading.show(); var cbid = app.createCallback(async function(ndata){ await getDataFromTable(); twirl.loading.hide(); edit(); }); app.insertScore("twine_convertsr", [0, 1, cbid, data.clipindex, twine.sr]); } else { edit(); } } if (asUnique) { setClipAudioUnique(checksr); } else { checksr(); } }; this.editInTwigs = function(asUnique) { if (!window.twigs) return twirl.prompt.show("twigs is unavailable in this session"); function edit() { twigs.boot(twine); var tables = [data.fnL]; if (data.fnR) tables.push(data.fnR); twigs.loadFileFromFtable(data.name, tables, function(ndata){ if (ndata.status > 0) { twine.setVisible(false); twigs.setVisible(true); } }, reloadAfterEdit); } if (asUnique) { setClipAudioUnique(edit); } else { edit(); } }; this.setData = function(modeString, v, onComplete) { data[modeString] = v; if (dataMode[modeString] < 0) { if (modeString == "name") { elWaveText.text(data.name); setClipWaveform(); } return; } if (!twine.offline || !clip.isAudio) app.getCsound().tableSet(datatable, dataMode[modeString], v); if (onComplete) onComplete(); }; var playbackcbid; this.play = function(onCallback) { var instr; var args; var channel = clip.channel.getCsChannelName(); var cbid = app.createCallback(function(ndata) { if (ndata.status == 0) { app.removeCallback(ndata.cbid); playbackcbid = null; } else { playbackcbid = ndata.cbid; } if (onCallback) { onCallback(ndata); } }, true); if (clip.isAudio) { instr = "ecp_playaudition"; args = [0, data.playLength, cbid, data.clipindex, data.playLength, channel]; } else { instr = getScriptInstrName(); args = [0, data.playLength, cbid, data.playLength, channel]; } app.insertScore(instr, args); }; this.stop = function(onCallback) { if (!playbackcbid) return; app.insertScore("ecp_stopaudition", [0, 1, playbackcbid]); }; async function getSourceTableData() { if (!clip.isAudio) return; var wavedata = []; var tbL = await app.getTable(data.fnL); wavedata.push(tbL); if (data.hasOwnProperty("fnR") && data.fnR > 0) { var tbR = await app.getTable(data.fnR); wavedata.push(tbR); } return wavedata; } async function setClipWaveform(noRedraw) { if (twine.offline || !clip.isAudio || !twine.storage.showClipWaveforms) return; if (!waveformClip) { waveformClip = new Waveform({ target: elWaveClip, allowSelect: false, showGrid: false, bgColor: "rgb(255, 255, 255, 0)", fgColor: "#000000" }); setTimeout(async function(){ var sourceTables = await getSourceTableData(); waveformClip.setData(sourceTables, data.duration); }, 100); } else if (!noRedraw) { console.log("redraw wave"); waveformClip.redraw(); } } async function showEditWaveform(target) { if (twine.offline || !clip.isAudio) return; target.empty().append(elWaveEdit); if (!waveformEdit) { waveformEdit = new Waveform({ target: elWaveEdit, allowSelect: true, showGrid: true, latencyCorrection: twirl.latencyCorrection // , markers: }); setTimeout(async function(){ var sourceTables = await getSourceTableData(); waveformEdit.setData(sourceTables, data.duration); }, 100); } else { waveformEdit.redraw(); } } this.setWarp = function(v) { clip.setData("warp", v); if (!data.warp && !data.loop && data.playLength > data.duration) { data.playLength = data.duration; clip.setSize(); } }; this.setLoop = function(v) { clip.setData("loop", v); if (!data.warp && !data.loop && data.playLength > data.duration) { data.playLength = data.duration; clip.setSize(); } }; this.setPitch = function(semitones) { var pitchRatio = Math.pow(2, (semitones / 12)); clip.setData("pitch", semitones); if (data.warpMode == 0 && data.loop == 0 && data.warp == 0) { data.playLength = data.duration / pitchRatio; clip.setSize(); } }; this.setWarpMode = function(v) { var prevMode = data.warpMode; clip.setData("warpMode", v); if (prevMode == 0 && data.warpMode != 0 && !data.loop && !data.warp) { data.playLength = data.duration; clip.setSize(); } }; this.setSize = function(noWaveRedraw) { var width = data.playLength * twine.timeline.pixelsPerBeat; clip.element.css("width", width + "px"); setClipWaveform(noWaveRedraw); } this.redraw = function(noWaveRedraw) { if (!loaded) return; var b = twine.timeline.beatRegion; clip.setSize(noWaveRedraw); var endPos = data.position + data.playLength; if (endPos < b[0] || data.position > b[1]) { return clip.element.hide(); } var css = { height: clip.channel.height + "px", left: (data.position - b[0]) * twine.timeline.pixelsPerBeat + "px" }; elWaveText.text(data.name); clip.element.show().css(css); if (endPos > twine.timeline.beatRegion[1] - 8) { var extension = endPos + 8; twine.timeline.extend(twine.timeline.beatRegion[1] + extension); } }; this.clone = async function() { var newData = Object.assign({}, data); newData.id = twine.getNewID(); var c = new Clip(twine, newData, clip); clip.channel.addClip(c); if (!twine.offline && clip.isAudio) { var ndata = await app.insertScoreAsync("twine_cloneclip", [clip.data.clipindex]); await c.loadFromDataTable(ndata.datatable, ndata.clipindex); } else { loaded = true; c.setScript(); c.redraw(); } return c; }; async function loadData(ndata, name, colour, defaultLength) { twirl.loading.show("Loading"); if (ndata.status == -1) { return twirl.errorHandler("File not valid"); } else if (ndata.status == -2) { return twirl.errorHandler("File too large"); } datatable = ndata.data.datatable; await getDataFromTable(); data.clipindex = ndata.data.clipindex; if (name) { data.name = name; } setTimeout(function(){ if (defaultLength) { data.playLength = data.duration / (60 / twine.timeline.data.bpm); console.log("deflength", data.playLength); } if (!colour) colour = twine.randomColour(); data.colour = colour; loaded = true; clip.redraw(); }, 50); // csound race }; this.loadFromDataTable = async function(newDatatable, clipindex) { datatable = newDatatable; await getDataFromTable(); data.clipindex = clipindex; clip.data.type = clip.types.AUDIO; loaded = true; setTimeout(clip.redraw, 20); //clip.redraw(); }; this.loadFromFtables = function(name, tables, colour) { clip.data.type = clip.types.AUDIO; twirl.loading.show("Loading"); var cbid = app.createCallback(async function(ndata){ await loadData(ndata, name, colour); twirl.loading.hide(); }); var call = [0, 1, cbid]; for (let t of tables) { call.push(t); } app.insertScore("twine_loadftables", call); }; this.loadFromPath = function(path, colour) { clip.data.type = clip.types.AUDIO; if (twine.offline) { loaded = true; clip.redraw(); return; } twirl.loading.show("Loading"); var cbid = app.createCallback(async function(ndata){ await loadData(ndata, path, colour, true); twirl.loading.hide(); }); app.insertScore("twine_loadpath", [0, 1, cbid, path]); }; this.createSilence = function(stereo, duration, name, colour) { clip.data.type = clip.types.AUDIO; twirl.loading.show("Creating"); var cbid = app.createCallback(async function(ndata){ await loadData(ndata, name, colour); twirl.loading.hide(); }); app.insertScore("twine_createblankclip", [0, 1, cbid, stereo, duration]); }; function getMaxClipWidth() { var maxWidth = 9999; if (!data.warp && !data.loop) { maxWidth = data.duration * twine.timeline.pixelsPerBeat; } return maxWidth; } this.markSelected = function (unselected) { if (unselected) { clip.element.css("outline", "none"); } else { clip.element.css("outline", "1px dashed white"); } }; clip.movement = { startX: 0, startY: 0, startXabs: 0, startYabs: 0, clipWidth: 0, clipLeft: 0, clipTop: 0, lastLeft: 0, isCopying: false, lastYabs: 0, extendHold: false, doClipRedraw: false, originalClipData: null, mouseMoveInnerFunc: null, clonedClips: null, masterClip: null, moved: false, channel: null, mouseDown: function(e, dragType) { e.preventDefault(); e.stopPropagation(); var cm = clip.movement; cm.moved = false; cm.originalClipData = []; cm.clonedClips = []; cm.mouseMoveInnerFunc = function(e) { clip.movement.doDragInner(e, dragType); }; for (let c of twine.timeline.selectedClips) { cm.originalClipData.push({ clip: c, position: c.data.position, playLength: c.data.playLength }); c.movement.mouseDownInner(e, dragType); } $("html").on("mouseup", cm.endDragMaster); }, mouseDownInner: function(e, dragType) { var cm = clip.movement; $("html").on("mousemove", cm.mouseMoveInnerFunc).on("mouseup", cm.endDrag); cm.masterClip = clip; // for cloning cm.channel = clip.channel; cm.originalPlaylength = data.playlength; cm.originalPosition = data.position; cm.isCopying = false; cm.clipWidth = parseFloat(clip.element.css("width")); cm.clipTop = parseFloat(clip.element.css("top")); cm.clipLeft = parseFloat(clip.element.css("left")); cm.startXabs = e.clientX; cm.startYabs = e.clientY; cm.startX = cm.startXabs - e.target.getBoundingClientRect().left; cm.startY = cm.startYabs - e.target.getBoundingClientRect().top; cm.lastLeft = (e.clientX - cm.startX - clip.channel.offset.left); cm.lastYabs = cm.startYabs; //$("#container").css("cursor", "e-resize"); }, endDragMaster: function(e) { var cm = clip.movement; $("html").off("mouseup", cm.endDragMaster); if (!cm.moved) return; var clipData = [...cm.originalClipData]; var clonedClips = [...cm.clonedClips]; console.log("add move undo"); twine.undo.add("move/resize clip", function(){ clipData.forEach(function(d){ d.clip.data.position = d.position; d.clip.data.playLength = d.playLength; d.clip.redraw(); }); clonedClips.forEach(function(c){ c.destroy(); }); }); }, endDrag: function(e) { e.preventDefault(); var cm = clip.movement; cm.isCopying = false; $("html").off("mouseup", cm.endDrag).off("mousemove", cm.mouseMoveInnerFunc); $("#container").css("cursor", "pointer"); if (cm.doClipRedraw) { setClipWaveform(); } }, doDrag: function(e, dragType) { e.preventDefault(); //$("html").off("mouseup", this.initialMouseUp); twine.timeline.selectedClips.forEach(function(c){ c.movement.doDragInner(e, dragType); }); }, setExtendHold: function() { var cm = clip.movement; cm.extendHold = true; setTimeout(function() { cm.extendHold = false; }, 250); }, doDragInner: async function (e, dragType) { var cm = clip.movement; if (dragType == "right") { var maxWidth = getMaxClipWidth(); var xMovement = e.clientX - cm.startXabs; var newWidth = xMovement + cm.clipWidth; newWidth = twine.timeline.roundToGrid(newWidth); if (newWidth > maxWidth) newWidth = maxWidth; if (newWidth < minWidth) newWidth = minWidth; if (newWidth != parseFloat(clip.element.css("width"))) { cm.moved = true; cm.doClipRedraw = true; } var playLength = newWidth / twine.timeline.pixelsPerBeat; if (!cm.masterClip.channel.hasOverlap(cm.masterClip, null, playLength)) { data.playLength = playLength; clip.element.css("width", newWidth + "px"); } } else if (dragType == "left") { var maxWidth = getMaxClipWidth(); var xMovement = e.clientX - cm.startXabs; var left = cm.clipLeft + xMovement; //var left = (e.clientX - cm.startX - clip.channel.offset.left); left = twine.timeline.roundToGrid(left); if (left < 0) left = 0; var newWidth = (cm.clipWidth - left) + cm.clipLeft; var cWidth, cLeft; if (newWidth < minWidth) { cWidth = minWidth, cm.clipLeft + minWidth; //(minWidth - left) + clipLeft; } else if (newWidth > maxWidth) { cWidth = maxWidth, cLeft = cm.lastLeft; } else { lastLeft = left; cWidth = newWidth, cLeft = left; } if (cWidth != parseFloat(clip.element.css("width"))) { cm.moved = true; cm.doClipRedraw = true; } var position = Math.min(0, (left / twine.timeline.pixelsPerBeat) + twine.timeline.beatRegion[0]); var playLength = newWidth / twine.timeline.pixelsPerBeat; if (!cm.masterClip.channel.hasOverlap(cm.masterClip, position, playLength)) { data.position = position; data.playLength = playLength clip.element.css({width: cWidth + "px", left: cLeft + "px"}); } } else { if (cm.extendHold) return; if (e.ctrlKey && !cm.isCopying) { cm.isCopying = true; cm.masterClip = await clip.clone(); } var xMovement = e.clientX - cm.startXabs; var left = xMovement + cm.clipLeft; left = twine.timeline.roundToGrid(left); var yMovement = e.clientY - cm.lastYabs; var top = (e.clientY - cm.lastYabs) + cm.clipTop; //console.log(top); var ttop = (e.clientY - cm.startY) + cm.clipTop; var tshift = ttop / cm.channel.height; //cm.masterClip.channel.height; tshift = (tshift > 0) ? Math.floor(tshift) : Math.ceil(tshift); console.log(tshift); var channelShift = top / cm.masterClip.channel.height; channelShift = (channelShift > 0) ? Math.floor(channelShift) : Math.ceil(channelShift); if (channelShift != 0 || left != parseFloat(clip.element.css("left"))) { cm.moved = true; } if (channelShift != 0) { cm.lastYabs = e.clientY; var newChannel = cm.masterClip.channel.index + channelShift; if (newChannel < twine.timeline.channels.length && newChannel >= 0) { if (!twine.timeline.channels[newChannel].hasOverlap(cm.masterClip)) { cm.masterClip.channel.removeClip(cm.masterClip); twine.timeline.channels[newChannel].addClip(cm.masterClip); } } } var doRedraw = false; var extension = cm.masterClip.data.playLength + 8; var newBeats; var d = twine.timeline.data; if (left < 0) { cm.setExtendHold(); extension = -extension; left = 0; newBeats = twine.timeline.beatRegion[1]; doRedraw = true; } var position = (left / twine.timeline.pixelsPerBeat) + twine.timeline.beatRegion[0]; if (!cm.masterClip.channel.hasOverlap(cm.masterClip, position)) { data.position = position; cm.masterClip.element.css("left", left + "px"); } if (left + cm.clipWidth > cm.masterClip.channel.width) { cm.setExtendHold(); doRedraw = true; newBeats = twine.timeline.beatRegion[1] + extension; twine.timeline.extend(newBeats, true); } if (doRedraw) { var extensionRatio = (extension / newBeats); var newStart = d.regionStart + ((d.regionEnd - d.regionStart) * (extensionRatio)); var newEnd = d.regionEnd + extensionRatio; newStart = Math.max(0, newStart); newEnd = Math.max(0, newEnd); twine.timeline.setRegion(newStart, newEnd, true); } } // move type } // end doDragInner }; elMove.mousedown(function(e){ clip.movement.mouseDown(e, "mid"); }); elResizeRight.mousedown(function(e){ clip.movement.mouseDown(e, "right"); }); elResizeLeft.mousedown(function(e){ clip.movement.mouseDown(e, "left"); }); };