var Locators = function(timeline, elTimebar, elChannelOverlay) { var locators = this; var items = { start: { elMain: $("
").addClass("twine_timeline_timebar_locatorhead").appendTo(elTimebar), elLine: $("
").addClass("twine_timeline_timebar_locatorline").appendTo(elChannelOverlay), setLeft: function(px){ items.start.elMain.show().css("left", px - (items.start.elMain.width() / 2)); items.start.elLine.show().css("left", px); }, hide: function() { items.start.elMain.hide(); items.start.elLine.hide(); } }, regionStart: { elMain: $("
").addClass("twine_timeline_timebar_regionstart").appendTo(elTimebar), elLine: $("
").addClass("twine_timeline_timebar_regionstart").appendTo(elChannelOverlay), setLeft: function(px){ items.regionStart.elMain.show().css("left", px - (items.regionStart.elMain.width() / 2)); items.regionStart.elLine.show().css("left", px); }, hide: function() { items.regionStart.elMain.hide(); items.regionStart.elLine.hide(); } }, regionEnd: { elMain: $("
").addClass("twine_timeline_timebar_regionstart").appendTo(elTimebar), elLine: $("
").addClass("twine_timeline_timebar_regionstart").appendTo(elChannelOverlay), setLeft: function(px){ items.regionEnd.elMain.show().css("left", px - (items.regionEnd.elMain.width() / 2)); items.regionEnd.elLine.show().css("left", px); }, hide: function() { items.regionEnd.elMain.hide(); items.regionEnd.elLine.hide(); } } }; items.regionStart.elMain.on("mousedown", function(es) { es.preventDefault(); es.stopPropagation(); var offset = elTimebar.offset().left; var max = elTimebar.width(); var startLeft = parseFloat(items.regionStart.elLine.css("left")); function mouseup() { $("body").off("mousemove", mousemove).off("mouseup", mouseup); } function mousemove(e) { var newPos = (e.clientX - es.clientX) + startLeft; newPos = timeline.roundToGrid(newPos); if (newPos < 0) { newPos = 0; } else if (newPos > max) { newPos = max; } var offset = elTimebar.offset().left; var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; var beat = timeline.beatRegion[0] + (Math.round((newPos / (max - offset)) * beats * 100) / 100); if (beat > timeline.data.beatEnd - 1) { return; } items.regionStart.setLeft(newPos); timeline.data.beatStart = beat; } $("body").on("mousemove", mousemove).on("mouseup", mouseup); }); items.regionEnd.elMain.on("mousedown", function(es) { es.preventDefault(); es.stopPropagation(); var offset = elTimebar.offset().left; var max = elTimebar.width(); var startLeft = parseFloat(items.regionEnd.elLine.css("left")); function mouseup() { $("body").off("mousemove", mousemove).off("mouseup", mouseup); } function mousemove(e) { var newPos = (e.clientX - es.clientX) + startLeft; newPos = timeline.roundToGrid(newPos); if (newPos < 0) { newPos = 0; } else if (newPos > max) { newPos = max; } var offset = elTimebar.offset().left; var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; var beat = timeline.beatRegion[0] + (Math.round((newPos / (max - offset)) * beats * 100) / 100); if (beat < timeline.data.beatStart + 1) { return; } items.regionEnd.setLeft(newPos); timeline.data.beatEnd = beat; } $("body").on("mousemove", mousemove).on("mouseup", mouseup); }); items.start.elMain.on("mousedown", function(es) { es.preventDefault(); es.stopPropagation(); var offset = elTimebar.offset().left; var max = elTimebar.width(); var startLeft = parseFloat(items.start.elLine.css("left")); function mouseup() { $("body").off("mousemove", mousemove).off("mouseup", mouseup); } function mousemove(e) { var newPos = (e.clientX - es.clientX) + startLeft; newPos = timeline.roundToGrid(newPos); if (newPos < 0) { newPos = 0; } else if (newPos > max) { newPos = max; } items.start.setLeft(newPos); var offset = elTimebar.offset().left; var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; var beat = timeline.beatRegion[0] + (Math.round((newPos / (max - offset)) * beats * 100) / 100); timeline.startLocation = beat; } $("body").on("mousemove", mousemove).on("mouseup", mouseup); }); this.redrawStart = function() { if (timeline.startLocation >= timeline.beatRegion[1] || timeline.startLocation < timeline.beatRegion[0]) { items.start.hide(); return; } var ratio = (timeline.startLocation - timeline.beatRegion[0]) / (timeline.beatRegion[1] - timeline.beatRegion[0]); var eltWidth = elTimebar.width() - elTimebar.offset().left; var px = ratio * eltWidth; items.start.setLeft(px); } }; // end locators var Timeline = function(twine, data) { var timeline = this; var elDragSelection; timeline.selectedClips = []; timeline.selectedChannel = null; timeline.startLocation = 0; Object.defineProperty(timeline, "selectedClip", { get: function() { return timeline.selectedClips[timeline.selectedClips.length - 1]; }, set: function(c) { timeline.selectedClips = [c]; } }); var container = $("#twine_timeline"); var elTimebarContainer = $("
").attr("id", "twine_timeline_timebar").appendTo(container); var elTimebar = $("
").attr("id", "twine_timeline_timebar_inner").appendTo(elTimebarContainer); this.element = $("
").attr("id", "twine_timeline_inner").appendTo(container); $("
").attr("id", "twine_timelineoverlay").appendTo(timeline.element); var elChannelOverlay = $("
").attr("id", "twine_timeline_channeloverlay").appendTo(container); var elScrollOuter = $("
").attr("id", "twine_timeline_scroll_outer").appendTo(container); var elScrollInner = $("
").attr("id", "twine_timeline_scroll_inner").appendTo(elScrollOuter); var elIcons = $("
").attr("id", "twine_timeline_scroll_filler").appendTo(container); var elPlayhead = $("
").attr("id", "twine_timeline_playposition").appendTo(elChannelOverlay); var locators = new Locators(timeline, elTimebar, elChannelOverlay); elIcons.append(twirl.createIcon({ label: "Zoom in", size: 20, icon: "zoomIn", click: function() { timeline.zoomIn(); } }).el); elIcons.append(twirl.createIcon({ label: "Zoom out", size: 20, icon: "zoomOut", click: function() { timeline.zoomOut(); } }).el); elIcons.append(twirl.createIcon({ label: "Show all", size: 20, icon: "showAll", click: function() { timeline.showAll(); } }).el); var channelIndex = 0; this.channels = []; this.gridPixels = 40; this.pixelsPerBeat = 10; this.beatRegion = []; this.playbackBeatStart = 0; if (data) { this.data = data; } else { this.data = { name: "New arrangement", snapToGrid: 4, gridVisible: true, beatStart: 0, beatEnd: 16, bpm: 120, timeSigMarker: 4, regionStart: 0, regionEnd: 1 }; } elTimebar.click(function(e) { var offset = elTimebar.offset().left; var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; var px = timeline.roundToGrid(e.clientX - offset); var width = elTimebar.width() - offset; // ???? var beat = timeline.beatRegion[0] + (Math.round((px / width) * beats * 100) / 100); timeline.startLocation = beat; locators.redrawStart(); twine.stopAndPlay(beat); }); this.getTotalBeatDuration = function() { return timeline.data.beatEnd * (60 / timeline.data.bpm); }; this.changeBeatEnd = function(newBeatEnd, noredraw) { var original = timeline.data.beatEnd; timeline.data.beatEnd = newBeatEnd; for (let c of timeline.channels) { c.changeBeatEnd(original, newBeatEnd, noredraw); } }; this.extend = function(newBeatEnd, noredraw) { if (newBeatEnd > timeline.data.beatEnd) { timeline.changeBeatEnd(newBeatEnd, noredraw); } }; this.reduce = function() { var end = 0; for (let ch of timeline.channels) { for (let i in ch.clips) { if (ch.clips[i]) { var e = ch.clips[i].data.position + ch.clips[i].data.playLength; if (e > end) { end = e; } } } } end += 8; timeline.data.beatEnd = end; }; this.exportData = async function() { var saveData = { channels: [], data: timeline.data, masterAmp: (await app.getControlChannel(twine.mixer.masterAmpChannel)), ftables: {} }; for (let c of timeline.channels) { saveData.channels.push(await c.exportData()); } for (let ch of saveData.channels) { for (let cl of ch.clips) { var fnL = cl.table[0]; var fnR = cl.table[1]; if (!saveData.ftables[fnL] && fnL > 0) { saveData.ftables[fnL] = { length: await app.getCsound().tableLength(fnL), data: (await app.getCsound().tableCopyOut(fnL)).values().toArray() }; } if (!saveData.ftables[fnR] && fnR > 0) { saveData.ftables[fnR] = { length: await app.getCsound().tableLength(fnR), data: (await app.getCsound().tableCopyOut(fnR)).values().toArray() }; } } } return saveData; }; this.createFtable = async function(length) { return new Promise(function(resolve, reject) { var cbid = app.createCallback(function(ndata){ resolve(ndata.table); }); app.insertScore("twine_createtable", [0, 1, cbid, length]); }); }; this.copyNewTableIn = async function(data) { var ftable = await timeline.createFtable(data.length); await app.getCsound().tableCopyIn(ftable, data); return ftable; }; this.importData = async function(loadData) { timeline.data = loadData.data; await app.setControlChannel(twine.mixer.masterAmpChannel, loadData.masterAmp); var ftMap = {}; console.log("load ftables", loadData.ftables); for (let i in loadData.ftables) { var fn = await timeline.copyNewTableIn(loadData.ftables[i].data); console.log("copy into", fn, loadData.ftables[i]); window.fn = loadData.ftables[i]; ftMap[i] = fn; } while (timeline.channels.length > 0) { timeline.channels[0].remove(); } timeline.channels = []; var channelIndex = 0; for (let c of loadData.channels) { var channel = new Channel(timeline, channelIndex ++); timeline.channels.push(channel); await channel.importData(c, ftMap); } timeline.redraw(); }; var playheadInterval; this.setPlaying = function(state) { twine.ui.head.play.setValue(state); if (playheadInterval) { clearInterval(playheadInterval); } if (state) { playheadInterval = setInterval(async function(){ var val = await app.getControlChannel("twine_playpos"); var beat = (val * (timeline.data.bpm / 60)) + timeline.playbackBeatStart; if (beat > timeline.beatRegion[1]) { clearInterval(playheadInterval); elPlayhead.hide(); } var pos = beat * timeline.pixelsPerBeat; elPlayhead.css("left", pos + "px"); }, 50); elPlayhead.show(); } else { elPlayhead.hide(); } }; this.zoomIn = function() { timeline.setRegion( timeline.data.regionStart * 1.1, timeline.data.regionEnd * 0.9 ); }; this.zoomOut = function() { timeline.setRegion( timeline.data.regionStart * 0.9, timeline.data.regionEnd * 1.1 ); timeline.redraw(); }; this.showAll = function() { timeline.setRegion(0, 1); }; this.compileAutomationData = function(onready) { var changed = false; for (let c of timeline.channels) { if (c.automationChanged()) { changed = true; break; } } if (!changed) { return onready(1); } var start = timeline.data.beatStart / (timeline.data.beatEnd - timeline.data.beatStart); var instr = "instr twine_automaterun\n"; for (let c of timeline.channels) { for (let a of c.getAutomationData(start, 1)) { instr += a + "\n" } } instr += "a_ init 0\nout a_\nendin\n"; console.log(instr); app.compileOrc(instr).then(function(status){ if (status < 0) { self.errorHandler("Cannot parse automation data"); } else { onready(1); } }); }; function determineChannelForAdd() { var channel; if (!timeline.selectedChannel) { if (timeline.channels.length == 0) { channel = timeline.addChannel(); } else { channel = timeline.channels[0]; } } else { channel = timeline.selectedChannel; } return channel; } this.addScriptClip = function() { var channel = determineChannelForAdd(); var clip = new Clip(twine); clip.data.position = timeline.playbackBeatStart; //timeline.beatRegion[0]; channel.addClip(clip); clip.initScript(); clip.redraw(); }; this.addBlankClip = function() { var el = $("
"); el.append($("

").text("New blank clip")); let pStereo = new twirl.transform.Parameter({ definition: { name: "Stereo", description: "Whether the clip should be stereo or mono", fireChanges: false, automatable: false, min: 0, max: 1, step: 1, dfault: 1 }, host: twine }); let pDuration = new twirl.transform.Parameter({ definition: { name: "Duration", description: "Duration of the clip in seconds", fireChanges: false, automatable: false, min: 0.1, max: 30, step: 0.01, dfault: 10 }, host: twine }); var tb = $("").appendTo($("").appendTo(el)); tb.append(pStereo.getElementRow(true)); tb.append(pDuration.getElementRow(true)); twirl.prompt.show(el, function(){ var channel = determineChannelForAdd(); var clip = new Clip(twine); clip.data.position = timeline.playbackBeatStart; //timeline.beatRegion[0]; channel.addClip(clip); clip.createSilence(pStereo.getValue(), pDuration.getValue(), name); }); }; this.addChannel = function() { var channel = new Channel(timeline, channelIndex ++); timeline.channels.push(channel); timeline.drawGrid(); return channel; }; this.contractChannels = function() { for (let c of timeline.channels) { c.contract(); } }; this.roundToGrid = function(px) { if (timeline.data.snapToGrid) { return twine.roundToNearest(px, twine.timeline.gridPixels, twine.timeline.gridOffset); } else { return px; } }; this.roundToChannel = function(px) { var total = 0; var rounded = px; for (let i in timeline.channels) { var c = timeline.channels[i]; if (i == 0) rounded = c.height; total += c.height + 1; if (px > total) { rounded = total; } } return rounded; }; this.determineChannelFromTop = function(posTop) { var top = 0; for (var c of timeline.channels) { top += c.height; if (posTop < top) { return c; } } return c; }; function calcViewport() { var cc = $(".twine_channelclips"); var width = $("#twine_timeline_channeloverlay").width(); //cc.width(); var d = timeline.data; timeline.beatRegion = [ d.regionStart * d.beatEnd, d.regionEnd * d.beatEnd ]; var gridScale = 1; var beats = timeline.beatRegion[1] - timeline.beatRegion[0]; timeline.pixelsPerBeat = width / beats; timeline.gridPixels = timeline.pixelsPerBeat / parseInt(d.snapToGrid); var beat = timeline.beatRegion[0]; timeline.gridOffset = (parseInt(beat) - beat) * timeline.pixelsPerBeat; return { minLeft: 0, //cc.offset().left, width: width, beat: beat } } this.drawGrid = function() { if (timeline.channels.length == 0) return; $(".twine_timelinemarker").remove(); $(".twine_timelinetext").remove(); var vp = calcViewport(); if (!timeline.data.gridVisible) return; var width; var fontWeight; beat = Math.floor(vp.beat); for (var x = vp.minLeft + timeline.gridOffset; x < vp.width; x += timeline.pixelsPerBeat) { if ((beat - 1) % timeline.data.timeSigMarker == 0) { width = 2; fontWeight = "bold"; } else { width = 1; fontWeight = "normal"; } if (x >= 0) { $("
").attr("class", "twine_vline twine_timelinemarker").appendTo(elChannelOverlay).css("width", width).css("left", x).css("top", "0px"); $("
").attr("class", "twine_timelinetext").appendTo(elChannelOverlay).css("font-weight", fontWeight).css("left", x + 2).text(beat); } beat ++; } locators.redrawStart(); } var drawing; this.redraw = function(noClipWaveRedraw) { if (drawing) return; drawing = true; timeline.drawGrid(); for (let c of timeline.channels) { c.redraw(noClipWaveRedraw); } drawing = false; }; this.setRegion = function(start, end, noClipWaveRedraw) { timeline.reduce(); if (end <= start) return; if (end > 1) end = 1; if (start < 0) start = 0; timeline.data.regionStart = start; timeline.data.regionEnd = end; timeline.redraw(noClipWaveRedraw); var elTbcw = elScrollOuter.width(); elScrollInner.css({left: (timeline.data.regionStart * elTbcw) + "px", right: ((1 - timeline.data.regionEnd) * elTbcw) + "px"}); if (self.onRegionChange) { self.onRegionChange([timeline.data.regionStart, timeline.data.regionEnd]); } }; this.onRegionChange = function(region) { }; function setScrollBarPosition(displayLeft, displayRight, setRegion) { if (displayLeft >= 0 && displayRight >= 0) { elScrollInner.css({left: displayLeft, right: displayRight}); var w = elScrollOuter.width(); if (setRegion) { timeline.data.regionStart = displayLeft / w; timeline.data.regionEnd = 1 - (displayRight / w); if (self.onRegionChange) { self.onRegionChange([timeline.data.regionStart, timeline.data.regionEnd]); } } } } elScrollOuter.mousedown(function() { var increment = 20; var apos = event.pageX - elScrollOuter.offset().left; var left = parseInt(elScrollInner.css("left")); var right = parseInt(elScrollInner.css("right")); var tbWidth = parseInt(elScrollInner.css("width")); if (apos < left) { left -= increment; right += increment; } else if (apos > left + tbWidth) { left += increment; right -= increment; } else { return; } setScrollBarPosition(left, right, true); timeline.redraw(); }); elScrollInner.mousedown(function(e){ var pageX = e.pageX; var offset = elScrollOuter.offset(); var cWidth = elScrollOuter.width(); var tbWidth = elScrollInner.width(); var sLeft = pageX - offset.left - parseInt(elScrollInner.css("left")); function handleDrag(e) { var left = ((e.pageX - pageX) + (pageX - offset.left)); left = left - sLeft; var end = left + tbWidth; var right = cWidth - end; setScrollBarPosition(left, cWidth - end, true); timeline.redraw(true); } function handleMouseUp(e) { $("body").off("mousemove", handleDrag).off("mouseup", handleMouseUp); function ensureDraw() { if (drawing) return setTimeout(ensureDraw, 20); timeline.redraw(); } ensureDraw(); } $("body").on("mouseup", handleMouseUp).on("mousemove", handleDrag); }); this.deselectClips = function() { timeline.selectedClips = []; $(".twine_clip").css("outline", "none"); }; this.dragSelection = function(e){ var elChannelOverlay = $("#twine_timeline_channeloverlay"); var pageX = e.pageX; var pageY = e.pageY; var offset = elChannelOverlay.offset(); var width = elChannelOverlay.width(); var height = elChannelOverlay.height(); var left = (pageX - offset.left); var top = (pageY - offset.top); if (!elDragSelection) { elDragSelection = $("
").addClass("drag_selection").appendTo(elChannelOverlay); } elDragSelection.hide().css({left: left + "px", top: top + "px"}); function handleDrag(e) { elDragSelection.show(); var xMovement = e.pageX - pageX; var yMovement = e.pageY - pageY; if (xMovement < 0) { elDragSelection.css({ left: (left + xMovement) + "px" }); } elDragSelection.css({ width: Math.abs(xMovement) + "px" }); if (yMovement < 0) { elDragSelection.css({ top: (top + yMovement) + "px" }); } elDragSelection.css({ height: Math.abs(yMovement) + "px" }); } function handleMouseUp(e) { elDragSelection.hide(); var left = parseFloat(elDragSelection.css("left")); var width = parseFloat(elDragSelection.css("width")); var top = parseFloat(elDragSelection.css("top")); var height = parseFloat(elDragSelection.css("height")) - 10; var channelStart = timeline.determineChannelFromTop(top).index; var channelEnd = timeline.determineChannelFromTop(top + height).index; var beatStart = ((left / timeline.pixelsPerBeat) + timeline.beatRegion[0]) - 0.01; var beatEnd = (((left + width) / timeline.pixelsPerBeat) + timeline.beatRegion[0]) + 0.01; timeline.deselectClips(); for (let ch of timeline.channels) { if (ch.index >= channelStart && ch.index <= channelEnd) { for (let ci in ch.clips) { var cl = ch.clips[ci]; if (cl) { if (cl.data.position >= beatStart && cl.data.position + cl.data.playLength <= beatEnd) { cl.markSelected(); timeline.selectedClips.push(cl); } } } } } $("body").off("mousemove", handleDrag).off("mouseup", handleMouseUp); } $("body").on("mouseup", handleMouseUp).on("mousemove", handleDrag); }; timeline.drawGrid(); };