diff options
Diffstat (limited to 'site/app/twigs')
-rw-r--r-- | site/app/twigs/index.html | 54 | ||||
-rw-r--r-- | site/app/twigs/twigs.csd | 18 | ||||
-rw-r--r-- | site/app/twigs/twigs.css | 167 | ||||
-rw-r--r-- | site/app/twigs/twigs.js | 1165 | ||||
-rw-r--r-- | site/app/twigs/twigs_ui.js | 577 |
5 files changed, 1981 insertions, 0 deletions
diff --git a/site/app/twigs/index.html b/site/app/twigs/index.html new file mode 100644 index 0000000..6251fe8 --- /dev/null +++ b/site/app/twigs/index.html @@ -0,0 +1,54 @@ +<html>
+ <head>
+ <title>twigs</title>
+ <link rel="stylesheet" href="../twirl/theme.css">
+ <link rel="stylesheet" href="../twirl/twirl.css">
+ <link rel="stylesheet" href="twigs.css">
+ <script type="text/javascript" src="https://apps.csound.1bpm.net/code/jquery.js"></script>
+ <script type="text/javascript" src="../base/base.js"></script>
+ <script type="text/javascript" src="../twirl/twirl.js"></script>
+ <script type="text/javascript" src="../twirl/appdata.js"></script>
+ <script type="text/javascript" src="../twirl/transform.js"></script>
+ <script type="text/javascript" src="twigs_ui.js"></script>
+ <script type="text/javascript" src="twigs.js"></script>
+ <script type="text/javascript">
+ $(twigs_startisolated);
+ </script>
+ </head>
+
+ <body>
+ <div id="twigs">
+ <div id="twigs_hidden_links">
+ <a id="twigs_contact" href="https://csound.1bpm.net/contact/?type=general&app=twigs" target="_blank">Contact</a>
+ <a id="twigs_reportbug" href="https://csound.1bpm.net/contact/?type=report_bug&app=twigs" target="_blank">Report bug</a>
+ <a id="twigs_documentation" href="documentation.html" target="_blank">Documentation</a>
+ </div>
+ <div id="twigs_menubar"></div>
+ <div id="twigs_main">
+ <div id="twigs_sidebar"></div>
+ <div id="twigs_options"></div>
+ <div id="twigs_editor">
+ <div id="twigs_editor_inner">
+ <div id="twigs_playhead"></div>
+ <div id="twigs_selection"></div>
+ </div>
+ <div id="twigs_editor_hscrollouter">
+ <div id="twigs_editor_hscrollinner"></div>
+ </div>
+ <div id="twigs_editor_vscrollouter">
+ <div id="twigs_editor_vscrollinner"></div>
+ </div>
+ <div id="twigs_editor_vzoom"></div>
+ <div id="twigs_editor_hzoom"></div>
+ </div>
+ </div>
+ </div>
+ <div id="twigs_start">
+ <div id="twigs_startinner">
+ <h1>twigs</h1>
+ <p>spectral transformer</p>
+ <div id="twigs_startbig">Press to begin</div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/site/app/twigs/twigs.csd b/site/app/twigs/twigs.csd new file mode 100644 index 0000000..d86616d --- /dev/null +++ b/site/app/twigs/twigs.csd @@ -0,0 +1,18 @@ +<CsoundSynthesizer>
+<CsOptions>
+-odac
+</CsOptions>
+<CsInstruments>
+sr = 44100
+ksmps = 64
+nchnls = 2
+0dbfs = 1
+seed 0
+
+#include "/twigs/twigs.udo"
+
+</CsInstruments>
+<CsScore>
+f0 z
+</CsScore>
+</CsoundSynthesizer>
\ No newline at end of file diff --git a/site/app/twigs/twigs.css b/site/app/twigs/twigs.css new file mode 100644 index 0000000..9e73df3 --- /dev/null +++ b/site/app/twigs/twigs.css @@ -0,0 +1,167 @@ +body {
+ font-family: var(--fontFace);
+ color: var(--fgColor1);
+ user-select: none;
+ cursor: arrow;
+}
+
+#twigs_playhead {
+ position: absolute;
+ top: 0px;
+ bottom: 0px;
+ left: 0px;
+ width: 1px;
+ background-color: var(--waveformPlayheadColor);
+ border: 1px solid var(--waveformPlayheadColor);
+ pointer-events: none;
+ z-index: 23;
+ display: none;
+}
+
+#twigs_main {
+ position: absolute;
+ top: 20px;
+ bottom: 0px;
+ left: 0px;
+ right: 0px;
+}
+
+#twigs_options {
+ position: absolute;
+ bottom: 0px;
+ left: 60px;
+ right: 0px;
+ height: 20%;
+ background-color: var(--bgColor1);
+}
+
+#twigs_editor {
+ position: absolute;
+ top: 0px;
+ bottom: 20%;
+ left: 60px;
+ right: 0px;
+}
+
+#twigs_editor_inner {
+ position: absolute;
+ top: 0px;
+ bottom: 20px;
+ left: 0px;
+ right: 20px;
+}
+
+#twigs_selection {
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ height: 0px;
+ width: 0px;
+ display: none;
+ background-color: var(--waveformSelectColor);
+ opacity: var(--waveformSelectOpacity);
+ z-index: 15;
+}
+
+#twigs_editor_vscrollouter {
+ position: absolute;
+ top: 0px;
+ bottom: 80px;
+ width: 20px;
+ right: 0px;
+ background-color: var(--waveformTimeBarBgColor);
+}
+
+#twigs_editor_vscrollinner {
+ position: absolute;
+ left: 4px;
+ right: 4px;
+ top: 0px;
+ bottom: 0px;
+ background-color: var(--waveformTimeBarFgColor);
+}
+
+#twigs_editor_vzoom {
+ position: absolute;
+ right: 0px;
+ bottom: 20px;
+ height: 60px;
+ width: 20px;
+ background-color: var(--bgColor3);
+}
+
+#twigs_editor_hzoom {
+ position: absolute;
+ background-color: var(--bgColor3);
+ right: 0px;
+ bottom: 0px;
+ width: 80px;
+ height: 20px;
+}
+
+#twigs_editor_hscrollouter {
+ position: absolute;
+ height: 20px;
+ bottom: 0px;
+ left: 0px;
+ right: 80px;
+ background-color: var(--waveformTimeBarBgColor);
+}
+
+#twigs_editor_hscrollinner {
+ position: absolute;
+ top: 4px;
+ bottom: 4px;
+ left: 0px;
+ right: 0px;
+ background-color: var(--waveformTimeBarFgColor);
+}
+
+#twigs_sidebar {
+ position: absolute;
+ top: 0px;
+ bottom: 0px;
+ left: 0px;
+ width: 60px;
+ background-color: var(--bgColor1);
+}
+
+#twigs_start {
+ z-index: 300;
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ background-color: var(--bgColor2);
+ cursor: pointer;
+}
+
+#twigs_startinner {
+ z-index: 202;
+ text-align: centre;
+ margin: 0px;
+ position: absolute;
+ top: 20%;
+ left: 20%;
+ width: 60%;
+ height: 40%;
+}
+
+#twigs_startbig {
+ font-size: 48pt;
+}
+
+#twigs_menubar {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ right: 0px;
+ height: 20px;
+ z-index: 40;
+}
+
+#twigs_hidden_links {
+ display: none;
+}
\ No newline at end of file diff --git a/site/app/twigs/twigs.js b/site/app/twigs/twigs.js new file mode 100644 index 0000000..7567dd0 --- /dev/null +++ b/site/app/twigs/twigs.js @@ -0,0 +1,1165 @@ +var Twigs = function() {
+ var twigs = this;
+ twigs.version = 1;
+ twirl.init();
+ twigs.SELECTIONMODE = {
+ singleBin: 0,
+ dragBins: 1,
+ dragArea: 2,
+ lasso: 3,
+ binAppend: 4,
+ draw: 5,
+ move: -1,
+ transpose: -2
+ };
+ twigs.selectionMode = twigs.SELECTIONMODE.singleBin;
+ twigs.undoLevel = 0;
+ var elCanvas;
+ var elHitCanvas;
+ var elSelectCanvas;
+ var elGridCanvas;
+ var elSelection = $("#twigs_selection");
+ var elContainer;
+ var width;
+ var height;
+ var playing;
+ var loaded = {
+ name: null,
+ channels: null,
+ bins: null,
+ tables: null,
+ length: null,
+ binSelectionTable: null,
+ binTimeSelectionTable: null
+ };
+ var maxfreq = 22050;
+ var amplitudeScaling = 40;
+
+ var ctx;
+ var ctxHit;
+ var ctxSelect;
+ var ctxGrid;
+
+ var xsteps;
+ var xstep;
+ var tabstep;
+ var region = {frequency: [0, 1], time: [0, 1]};
+ var selection = {bins: {}};
+ var binColourHash = {};
+ var colIDs;
+ var onSave;
+ twigs.visible = false;
+ twigs.twine = null;
+
+ twigs.storage = localStorage.getItem("twigs");
+ if (twigs.storage) {
+ twigs.storage = JSON.parse(twigs.storage);
+ } else {
+ twigs.storage = {
+ graphType: 0,
+ maxundo: 2,
+ drawFrequencyGrid: 1,
+ drawTimeGrid: 1
+ };
+ }
+
+ this.saveStorage = function() {
+ localStorage.setItem("twigs", JSON.stringify(twigs.storage));
+ };
+
+ function absPosToDisplayPos(x) {
+ var pos = (x - region.time[0]) / (region.time[1] - region.time[0]);
+ return (pos >= 0 && pos <= 1) ? pos : null;
+ }
+
+ function displayPosToAbsPos(x) {
+ return ((region.time[1] - region.time[0]) * x) + region.time[0];
+ }
+
+ function pxToFreq(px, height) {
+ return ((px / height) * ((region.frequency[1] - region.frequency[0]) * maxfreq));
+ //return (((region.frequency[1] - region.frequency[0]) * maxfreq) / height) * px;
+ }
+
+ function pxToFrames(px, width) {
+ var frames = Math.round(loaded.length / loaded.bins);
+ console.log("move ", px, "wid", width);
+ return Math.round(((px / width) * (region.time[1] - region.time[0]) * frames));
+ //return Math.round((((region.time[1] - region.time[0]) * (loaded.length / loaded.bins)) / width) * px);
+ }
+
+ var playheadInterval;
+ function playPositionHandler(onComplete) {
+ function callback(ndata) {
+ if (ndata.status == 1) {
+ twigs.setPlaying(true);
+ if (playheadInterval) {
+ clearInterval(playheadInterval);
+ }
+ playheadInterval = setInterval(async function(){
+ var val = await app.getControlChannel("twgs_playposratio");
+ if (val < 0 || val > 1) {
+ clearInterval(playheadInterval);
+ }
+ movePlayhead(val);
+ }, 50);
+ } else { // stopped for some reason
+ if (ndata.status == -1) {
+ twirl.prompt.show("Not enough processing power to play in realtime");
+ }
+ twigs.setPlaying(false);
+ app.removeCallback(ndata.cbid);
+ movePlayhead(0);
+ if (playheadInterval) clearInterval(playheadInterval);
+ if (onComplete) onComplete(ndata);
+ }
+ }
+ return app.createCallback(callback, true);
+ }
+
+ function movePlayhead(xratio) {
+ setTimeout(function() {
+ var p = $("#twigs_playhead");
+ var displayPos = absPosToDisplayPos(xratio);
+ if (!displayPos || displayPos <= 0 || displayPos >= 1) {
+ return p.hide();
+ }
+ var width = elContainer.width();
+ var left = Math.min(width * displayPos, width -1);
+ p.show().css({left: left + "px"});
+ }, twirl.latencyCorrection);
+ }
+
+ this.hasSelection = function() {
+ return (Object.keys(selection.bins) != 0);
+ }
+
+ this.selectionOperation = {
+ move: async function(freqshift, timeshiftframes) {
+ console.log("ts", timeshiftframes, "fs", freqshift);
+ console.log("move duration", (timeshiftframes / Math.round(loaded.length / loaded.bins)) * loaded.duration);
+ twirl.loading.show("Applying");
+ await setBinSelectionTable();
+ var cbid1 = app.createCallback(function(ndata1){
+ var cbid2 = app.createCallback(function(ndata2){
+ clearSelection();
+ globalCallbackHandler(ndata2);
+ });
+ app.insertScore("twgs_movement", [0, 1, cbid2, timeshiftframes]);
+ });
+ app.insertScore("twgs_freqshift", [0, 1, cbid1, freqshift, 1]);
+ },
+ shift: async function(freqshift) {
+ twirl.loading.show("Applying");
+ await setBinSelectionTable();
+ var cbid = app.createCallback(function(ndata){
+ clearSelection();
+ globalCallbackHandler(ndata);
+ });
+ app.insertScore("twgs_freqshift", [0, 1, cbid, freqshift]);
+
+ /*for (let b of selection.bins) {
+ for (var i = b; i < loaded.length; i += loaded.bins) {
+ var val = await app.getCsound().tableGet(loaded.tables[1], index);
+ val += freqshift;
+ val = Math.max(Math.min(val, maxfreq), 0);
+ await app.getCsound().tableSet(loaded.tables[1], index, val)
+ }
+ }
+ clearSelection();
+ twigs.redraw();
+ */
+ },
+ amplify: async function(factor) {
+ twirl.loading.show("Applying");
+ await setBinSelectionTable();
+ var cbid = app.createCallback(function(ndata){
+ globalCallbackHandler(ndata);
+ });
+ app.insertScore("twgs_amplify", [0, 1, cbid, factor]);
+ }
+ };
+
+ async function setBinSelectionTable() {
+ var length = await app.getCsound().tableLength(loaded.binSelectionTable);
+ var selectionData = [];
+ var timeData = [];
+ var region = [1, 0];
+ for (var i = 0; i < length; i++) {
+ if (selection.bins[i]) {
+ selectionData[i] = 1;
+ timeData[i] = selection.bins[i][0];
+ timeData[i + length] = selection.bins[i][1];
+ if (selection.bins[i][0] < region[0]) region[0] = selection.bins[i][0];
+ if (selection.bins[i][1] > region[1]) region[1] = selection.bins[i][1];
+ } else {
+ selectionData[i] = 0;
+ timeData[i] = -1;
+ timeData[i + length] = -1;
+ }
+ }
+ await app.getCsound().tableCopyIn(loaded.binSelectionTable, selectionData);
+ await app.getCsound().tableCopyIn(loaded.binTimeSelectionTable, timeData);
+ return region;
+ }
+
+
+ this.setSelectionMode = function(mode) {
+ twigs.selectionMode = mode;
+ if (mode >= 0) {
+ selection.bins = {};
+ clearSelection();
+ }
+ elSelection.hide();
+ };
+
+ this.setTimeRegion = function(start, end, noRedraw) {
+ region.time[0] = start;
+ region.time[1] = end;
+ var outerWidth = elScrollOuterH.width();
+ elScrollInnerH.css({
+ left: (start * outerWidth) + "px",
+ right: ((1 - end) * outerWidth) + "px"
+ });
+ if (!noRedraw) twigs.redraw();
+ };
+
+ this.setFrequencyRegion = function(start, end, noRedraw) {
+ region.frequency[0] = start;
+ region.frequency[1] = end;
+ var outerHeight = elScrollOuterV.height();
+ elScrollInnerV.css({
+ bottom: (start * outerHeight) + "px",
+ top: ((1 - end) * outerHeight) + "px"
+ });
+ if (!noRedraw) twigs.redraw();
+ };
+
+ function setScrollPositionV(displayTop, displayBottom, setRegion) {
+ if (displayTop >= 0 && displayBottom >= 0) {
+ elScrollInnerV.css({top: displayTop, bottom: displayBottom});
+ var h = elScrollOuterV.height();
+ region.frequency[0] = displayTop / h;
+ region.frequency[1] = 1 - (displayBottom / h);
+ }
+ }
+
+ function handleScrollOuterV(e) {
+ var increment = 20;
+ var apos = event.pageX - elScrollOuterV.offset().left;
+ var top = parseInt(elScrollInnerV.css("bottom"));
+ var bottom = parseInt(elScrollInnerV.css("bottom"));
+ var tbHeight = parseInt(elScrollInnerV.css("height"));
+ if (apos < top) {
+ top -= increment;
+ bottom += increment;
+ } else if (apos > top + tbHeight) {
+ top += increment;
+ bottom -= increment;
+ } else {
+ return;
+ }
+ setScrollPositionV(top, bottom);
+ twigs.redraw();
+ }
+
+ function handleScrollInnerV(e) {
+ var pageY = e.pageY;
+ var offset = elScrollOuterV.offset();
+ var cHeight = elScrollOuterV.height();
+ var tHeight = elScrollInnerV.height();
+ var sTop = pageY - offset.top - parseInt(elScrollInnerV.css("top"));
+
+ function handleDrag(e) {
+ var top = ((e.pageY - pageY) + (pageY - offset.top));
+ top = top - sTop;
+ var end = top + tHeight;
+ var bottom = cHeight - end;
+ setScrollPositionV(top, cHeight - end);
+ twigs.redraw(20, true);
+
+ }
+ function handleMouseUp(e) {
+ $("body").off("mousemove", handleDrag).off("mouseup", handleMouseUp);
+ function ensureDraw() {
+ if (drawing) return setTimeout(ensureDraw, 20);
+ twigs.redraw();
+ }
+ ensureDraw();
+ }
+ $("body").on("mouseup", handleMouseUp).on("mousemove", handleDrag);
+ }
+
+ function setScrollPositionH(displayLeft, displayRight) {
+ if (displayLeft >= 0 && displayRight >= 0) {
+ elScrollInnerH.css({left: displayLeft, right: displayRight});
+ var w = elScrollOuterH.width();
+ region.time[0] = displayLeft / w;
+ region.time[1] = 1 - (displayRight / w);
+ }
+ }
+
+ function handleScrollOuterH(e) {
+ var increment = 20;
+ var apos = event.pageX - elScrollOuterH.offset().left;
+ var left = parseInt(elScrollInnerH.css("left"));
+ var right = parseInt(elScrollInnerH.css("right"));
+ var tbWidth = parseInt(elScrollInnerH.css("width"));
+ if (apos < left) {
+ left -= increment;
+ right += increment;
+ } else if (apos > left + tbWidth) {
+ left += increment;
+ right -= increment;
+ } else {
+ return;
+ }
+ setScrollPositionH(left, right);
+ twigs.redraw();
+ }
+
+ function handleScrollInnerH(e) {
+ var pageX = e.pageX;
+ var offset = elScrollOuterH.offset();
+ var cWidth = elScrollOuterH.width();
+ var tbWidth = elScrollInnerH.width();
+ var sLeft = pageX - offset.left - parseInt(elScrollInnerH.css("left"));
+
+ function handleDrag(e) {
+ var left = ((e.pageX - pageX) + (pageX - offset.left));
+ left = left - sLeft;
+ var end = left + tbWidth;
+ var right = cWidth - end;
+ setScrollPositionH(left, cWidth - end);
+ twigs.redraw(20, true);
+
+ }
+ function handleMouseUp(e) {
+ $("body").off("mousemove", handleDrag).off("mouseup", handleMouseUp);
+ function ensureDraw() {
+ if (drawing) return setTimeout(ensureDraw, 20);
+ twigs.redraw();
+ }
+ ensureDraw();
+ }
+ $("body").on("mouseup", handleMouseUp).on("mousemove", handleDrag);
+ }
+
+ var elScrollOuterH = $("#twigs_editor_hscrollouter").click(handleScrollOuterH);
+ var elScrollInnerH = $("#twigs_editor_hscrollinner").click(handleScrollInnerH);
+ var elScrollOuterV = $("#twigs_editor_vscrollouter").click(handleScrollOuterV);
+ var elScrollInnerV = $("#twigs_editor_vscrollinner").click(handleScrollInnerV);
+
+
+
+ this.vzoomIn = function() {
+ twigs.setFrequencyRegion(region.frequency[0] * 1.1, region.frequency[1] * 0.9);
+ };
+
+ this.vzoomOut = function() {
+ twigs.setFrequencyRegion(region.frequency[0] * 0.9, region.frequency[1] * 1.1);
+ };
+
+ this.hzoomIn = function() {
+ twigs.setTimeRegion(region.time[0] * 1.1, region.time[1] * 0.9);
+ };
+
+ this.hzoomOut = function() {
+ twigs.setTimeRegion(region.time[0] * 0.9, region.time[1] * 1.1);
+ };
+
+ async function withBinPoints(bin, func) {
+ for (var x = 0, i = parseInt(bin); i < loaded.length; i += tabstep, x += xstep) {
+ await func(i, x)
+ }
+ }
+
+ function clearSelection() {
+ selection.bins = {};
+ ctxSelect.clearRect(0, 0, width, height);
+ }
+
+ async function drawSelection() {
+ var startFreq = region.frequency[0] * maxfreq;
+ var endFreq = region.frequency[1] * maxfreq;
+ var freqRange = endFreq - startFreq;
+ var height = elContainer.height();
+ var width = elContainer.width();
+ var freqTable = await app.getCsound().getTable(loaded.tables[1]);
+ ctxSelect.clearRect(0, 0, width, height);
+ ctxSelect.strokeStyle = "red";
+ ctxSelect.lineWidth = 2;
+
+ async function drawBin(bin, times) {
+ if (twigs.storage.graphType == 0) ctxSelect.beginPath();
+
+ var lastfreq = null;
+ await withBinPoints(bin, function(i, x){
+ var freq = freqTable[i];
+ var xRatio = x / width;
+ if (xRatio < absPosToDisplayPos(times[0]) || xRatio > absPosToDisplayPos(times[1])) return;
+
+ if (twigs.storage.graphType == 1) {
+ var yPos = ((freqRange - freq) / freqRange) * height;
+ ctxSelect.beginPath();
+ ctxSelect.moveTo(x, yPos);
+ ctxSelect.lineTo(x + xstep, yPos);
+ ctxSelect.stroke();
+ ctxSelect.closePath();
+ } else {
+ if (lastfreq) {
+ var yPos = [
+ ((freqRange - lastfreq) / freqRange) * height,
+ ((freqRange - freq) / freqRange) * height
+ ];
+ ctxSelect.moveTo(x - xstep, yPos[0]);
+ ctxSelect.lineTo(x, yPos[1]);
+ }
+ lastfreq = freq;
+ }
+ });
+ if (twigs.storage.graphType == 0) {
+ ctxSelect.stroke();
+ ctxSelect.closePath();
+ }
+ }
+ for (let b in selection.bins) {
+ await drawBin(b, selection.bins[b]);
+ }
+
+ }
+
+
+ function setup() {
+ elContainer = $("#twigs_editor_inner");
+ width = elContainer.width(); // deprecate it
+ height = elContainer.height();
+
+ var dragStart = [];
+ var dragLast = [];
+ var offset;
+
+ elHitCanvas = $("<canvas />").css({
+ position: "absolute", width: "100%", height: "100%"
+ }).attr("width", width)
+ .attr("height", height);
+ elCanvas = $("<canvas />").css({position: "absolute", width: "100%", height: "100%", top: "0px", left: "0px", "z-index": 12})
+ .attr("width", width)
+ .attr("height", height)
+ .appendTo(elContainer);
+
+ function mouseMove(e){
+ if (dragStart.length == 0) {
+ return;
+ }
+ if (twigs.selectionMode == twigs.SELECTIONMODE.singleBin || twigs.selectionMode == twigs.SELECTIONMODE.binAppend) {
+ return;
+ }
+
+ var x = e.clientX - offset.left;
+ var y = e.clientY - offset.top;
+ if (twigs.selectionMode == twigs.SELECTIONMODE.lasso) {
+ //ctxSelect.moveTo(dragLast[0], dragLast[1]);
+ ctxSelect.lineTo(x, y);
+ dragLast[0] = x;
+ dragLast[1] = y;
+ ctxSelect.stroke();
+ return;
+ }
+
+ if (twigs.selectionMode == twigs.SELECTIONMODE.move) {
+ var xMovement = x - dragLast[0];
+ var yMovement = y - dragLast[1];
+ ctxSelect.globalCompositeOperation = "copy";
+ ctxSelect.drawImage(ctxSelect.canvas, xMovement, yMovement);
+ ctxSelect.globalCompositeOperation = "source-over";
+
+ dragLast[0] = x;
+ dragLast[1] = y;
+ return;
+ }
+
+ if (twigs.selectionMode == twigs.SELECTIONMODE.transpose) {
+ var xMovement = x - dragLast[0];
+ }
+
+ var x, cx, width;
+ if (twigs.selectionMode == twigs.SELECTIONMODE.dragBins) {
+ x = 0;
+ width = elContainer.width();
+ } else {
+ x = dragStart[0];
+ cx = e.clientX - offset.left
+ if (x > cx) {
+ width = x - cx;
+ x = cx;
+ } else {
+ width = cx - x;
+ }
+ }
+ var y = dragStart[1];
+ var cy = e.clientY - offset.top;
+ var height = cy - y;
+ elSelection.css({
+ left: x + "px", top: y + "px",
+ width: width + "px", height: height + "px"
+ }).show();
+ }
+
+ function mouseUp(e){
+ var x = e.clientX - offset.left;
+ var y = e.clientY - offset.top;
+ var xMovement = x - dragStart[0];
+ var yMovement = y - dragStart[1];
+ if (twigs.selectionMode < 0) {
+ twigs.selectionOperation.move(
+ pxToFreq(-yMovement, elContainer.height()),
+ pxToFrames(xMovement, elContainer.width())
+ );
+ //twigs.selectionOperation.shift(pxToFreq(-yMovement));
+ } else if (twigs.selectionMode == twigs.SELECTIONMODE.lasso) {
+ //ctxSelect.moveTo(dragLast[0], dragLast[1]);
+ ctxSelect.lineTo(dragStart[0], dragStart[1]);
+ ctxSelect.stroke();
+ ctxSelect.fillStyle = "rgb(255, 0, 0)";
+ ctxSelect.fill();
+ ctxSelect.closePath();
+ twirl.loading.show("Finding frequencies");
+ setTimeout(function(){
+ var width = elContainer.width();
+ var id = ctxSelect.getImageData(0, 0, width, elContainer.height());
+
+ var x = 0;
+ var y = 0;
+ var bins = {};
+ for (var i = 0; i < id.data.length; i += 4) {
+ if (id.data[i] == "255") {
+ var pixel = ctxHit.getImageData(x, y, 1, 1);
+ var colour = "rgb(" + pixel.data[0] + ","
+ + pixel.data[1] + ","
+ + pixel.data[2] + ")";
+ var bin = binColourHash[colour];
+ if (bin) {
+ if (bins[bin]) {
+ if (x < bins[bin][0]) bins[bin][0] = x;
+ if (x > bins[bin][1]) bins[bin][1] = x;
+ } else {
+ bins[bin] = [x, x];
+ }
+ }
+ }
+
+ x ++;
+ if (x >= width) {
+ x = 0;
+ y ++;
+ }
+ }
+ for (var b in bins) {
+ selection.bins[b] = [
+ displayPosToAbsPos(bins[b][0] / width),
+ displayPosToAbsPos(bins[b][1] / width)
+ ]
+ }
+ ctxSelect.clearRect(0, 0, width, height);
+ drawSelection();
+ twirl.loading.hide();
+ }, 10);
+
+ } else if (twigs.selectionMode == twigs.SELECTIONMODE.dragBins || twigs.selectionMode == twigs.SELECTIONMODE.dragArea) {
+ var selWidth = x - xMovement;
+ var time;
+ if (twigs.selectionMode == twigs.SELECTIONMODE.dragArea) {
+ var containerWidth = elContainer.width();
+ time = [
+ dragStart[0] / containerWidth,
+ x / containerWidth
+ ];
+ } else {
+ time = [0, 1];
+ }
+ var bins = [];
+ var pixels = ctxHit.getImageData(dragStart[0], dragStart[1], xMovement, yMovement); //x, y, xMovement, yMovement);
+ for (var i = 0; i < pixels.data.length; i += 4) {
+ var colour = "rgb(" + pixels.data[i] + ","
+ + pixels.data[i + 1] + ","
+ + pixels.data[i + 2] + ")";
+ var bin = binColourHash[colour];
+ if (bin && bins.indexOf(bin) < 0) {
+ bins.push(bin);
+ selection.bins[bin] = [
+ displayPosToAbsPos(time[0]),
+ displayPosToAbsPos(time[1])
+ ]
+ }
+ }
+ drawSelection();
+ }
+ elSelection.hide();
+ dragStart = [];
+ dragLast = [];
+ $("body").off("mouseup", mouseUp).off("mousemove", mouseMove);
+ }
+
+ elGridCanvas = $("<canvas />").css({
+ position: "absolute", width: "100%", height: "100%", top: "0px", left: "0px", "z-index": 12
+ }).attr("width", width).attr("height", height).appendTo(elContainer);
+
+ elSelectCanvas = $("<canvas />").css({
+ position: "absolute", width: "100%", height: "100%", top: "0px", left: "0px", "z-index": 13
+ }).attr("width", width) .attr("height", height).appendTo(elContainer).on("mousedown", function(e){
+ offset = $(this).offset();
+ var x = e.clientX - offset.left;
+ var y = e.clientY - offset.top;
+ dragStart = [x, y];
+ dragLast = [x, y];
+ if (twigs.selectionMode == twigs.SELECTIONMODE.singleBin || twigs.selectionMode == twigs.SELECTIONMODE.binAppend) {
+ const pixel = ctxHit.getImageData(x, y, 1, 1);
+ const colour = "rgb(" + pixel.data[0] + ","
+ + pixel.data[1] + ","
+ + pixel.data[2] + ")";
+ const bin = binColourHash[colour];
+ if (bin) {
+ var binTime = [
+ displayPosToAbsPos(0),
+ displayPosToAbsPos(1),
+ ]
+ if (twigs.selectionMode == twigs.SELECTIONMODE.binAppend) {
+ selection.bins[bin] = binTime;
+ } else {
+ clearSelection();
+ selection.bins[bin] = binTime;
+ }
+ drawSelection();
+ }
+ } else {
+ if (twigs.selectionMode == twigs.SELECTIONMODE.lasso) {
+ clearSelection();
+ ctxSelect.strokeStyle = "black";
+ ctxSelect.fillStyle = "rgb(10, 10, 10, 50)";
+ ctxSelect.lineWidth = 2;
+ ctxSelect.moveTo(dragStart[0], dragStart[1]);
+ ctxSelect.beginPath();
+ }
+ $("body").on("mouseup", mouseUp).on("mousemove", mouseMove);
+ }
+ });
+ ctx = elCanvas[0].getContext("2d");
+ ctxHit = elHitCanvas[0].getContext("2d", {willReadFrequently: true});
+ ctxSelect = elSelectCanvas[0].getContext("2d", {willReadFrequently: true});
+ ctxGrid = elGridCanvas[0].getContext("2d");
+ }
+
+
+
+ function getNextColour() {
+ if (colIDs[0] < 255) {
+ colIDs[0] += 5;
+ } else if (colIDs[1] < 255) {
+ colIDs[1] += 5;
+ } else {
+ colIDs[2] += 5;
+ }
+ return "rgb(" + colIDs.join(",") + ")";
+ }
+
+ this.setPlaying = function(state) {
+ playing = state;
+ };
+
+ this.undo = function() {
+ if (playing) return;
+ twirl.loading.show("Applying");
+ var cbid = app.createCallback(globalCallbackHandler);
+ app.insertScore("twgs_undo", [0, 1, cbid]);
+ };
+
+ this.play = async function(selectedOnly) {
+ if (playing) return;
+ if (!twigs.storage.resynthType) {
+ twigs.storage.resynthType = 0;
+ twigs.saveStorage();
+ }
+ errorState = "Playback error";
+ var region = [0, 1];
+ if (selectedOnly) {
+ region = await setBinSelectionTable();
+ }
+ app.insertScore("twgs_play", [
+ 0, 1, playPositionHandler(),
+ 0, ((selectedOnly) ? 1 : 0),
+ region[0], region[1],
+ twigs.storage.resynthType
+ ]);
+ };
+
+ this.increaseAmpScaling = function() {
+ amplitudeScaling += 20;
+ twigs.redraw();
+ };
+
+ this.decreaseAmpScaling = function() {
+ amplitudeScaling -= 20;
+ if (amplitudeScaling <= 0) amplitudeScaling = 20;
+ twigs.redraw();
+ };
+
+
+ function spectroColour(value) {
+ var min = 16711680
+ var max = 255
+ var colourNumber = parseInt(((max - min) * value) + min);
+ function toHex(n) {
+ n = n.toString(16) + '';
+ return n.length >= 2 ? n : new Array(2 - n.length + 1).join('0') + n;
+ }
+
+ var r = toHex(colourNumber % 256),
+ g = toHex(Math.floor(colourNumber / 256 ) % 256),
+ b = toHex(Math.floor(Math.floor(colourNumber / 256) / 256 ) % 256);
+ return '#' + r + g + b;
+ }
+
+ var drawing = false;
+ this.redraw = async function(efficiency, noLoadingPrompt) {
+ if (drawing) return;
+ drawing = true;
+ if (!efficiency) efficiency = 4;
+ if (!noLoadingPrompt) twirl.loading.show("Drawing");
+ if (twigs.storage.basicLines == null) twigs.storage.basicLines = 0;
+ var style = getComputedStyle(document.body);
+ var height = elContainer.height();
+ var width = elContainer.width();
+ [ctx, ctxSelect, ctxHit, ctxGrid].forEach(function(c){
+ c.clearRect(0, 0, width, height);
+ });
+ binColourHash = {};
+ colIDs = [0, 0, 0];
+
+ var startFreq = region.frequency[0] * maxfreq;
+ var endFreq = region.frequency[1] * maxfreq;
+ var freqRange = endFreq - startFreq;
+
+ var ampTableNum = loaded.tables[0];
+ var freqTableNum = loaded.tables[1];
+ var tableLength = await app.getCsound().tableLength(ampTableNum);
+ var ampTable = await app.getCsound().getTable(ampTableNum);
+ var freqTable = await app.getCsound().getTable(freqTableNum);
+
+ var totalFrames = tableLength / loaded.bins;
+ var startFrame = parseInt(region.time[0] * totalFrames);
+ var endFrame = parseInt(region.time[1] * totalFrames);
+ var frameRegion = endFrame - startFrame;
+ var startIndex = startFrame * loaded.bins;
+ var endIndex = endFrame * loaded.bins;
+ var indexStep = (frameRegion / width) * efficiency;
+ xstep = parseInt(Math.max(1, width / frameRegion) * efficiency);
+ tabstep = loaded.bins * Math.max(1, Math.round(indexStep));
+
+ if (!twigs.storage.basicLines) {
+ ctx.lineWidth = 2;
+ ctx.shadowBlur = 2;
+ } else {
+ ctx.lineWidth = 1;
+ ctx.shadowBlur = null;
+ ctx.shadowColor = null;
+ }
+
+ for (var b = 0; b < loaded.bins; b ++) {
+ var hitColour = getNextColour();
+ if (!binColourHash[hitColour]) {
+ binColourHash[hitColour] = b;
+ }
+
+ ctxHit.lineWidth = 4;
+ ctxHit.strokeStyle = hitColour;
+
+ if (twigs.storage.graphType == 0) ctxHit.beginPath();
+
+ var lastfreq = null;
+ for (var x = 0, i = b + startIndex; i < endIndex; i += tabstep, x += xstep) {
+ var freq = freqTable[i];
+
+ var colour;
+ if (twigs.storage.colourType == 1) {
+ colour = spectroColour(ampTable[i] * amplitudeScaling);
+ } else {
+ var cval = 255 - Math.round((ampTable[i] * amplitudeScaling) * 255);
+ cval = Math.min(Math.max(0, cval), 255);
+ colour = "rgb(" + cval + "," + cval + "," + cval + ")";
+ }
+
+ if (twigs.storage.graphType == 1) {
+ var yPos = ((freqRange - freq) / freqRange) * height;
+ ctx.beginPath();
+ ctx.moveTo(x, yPos);
+ ctx.lineTo(x + xstep, yPos);
+ ctx.strokeStyle = colour;
+ if (!twigs.storage.basicLines) ctx.shadowColor = colour;
+ ctx.stroke();
+ ctx.closePath();
+ ctxHit.beginPath();
+ ctxHit.moveTo(x, yPos);
+ ctxHit.lineTo(x + xstep, yPos);
+ ctxHit.stroke();
+ ctxHit.closePath();
+ } else if (twigs.storage.graphType == 0) {
+
+ if (lastfreq && lastfreq >= startFreq && lastfreq <= endFreq && freq >= startFreq && freq <= endFreq) {
+ var yPos = [
+ ((freqRange - lastfreq) / freqRange) * height,
+ ((freqRange - freq) / freqRange) * height
+ ];
+ ctx.beginPath();
+ ctx.moveTo(x - xstep, yPos[0]);
+ ctx.lineTo(x, yPos[1]);
+ ctx.strokeStyle = colour;
+ if (!twigs.storage.basicLines) ctx.shadowColor = colour;
+ ctx.stroke();
+ ctx.closePath();
+
+ ctxHit.moveTo(x - xstep, yPos[0]);
+ ctxHit.lineTo(x, yPos[1]);
+
+ }
+ lastfreq = freq;
+ }
+ }
+ if (twigs.storage.graphType == 0) {
+ ctxHit.stroke();
+ ctxHit.closePath();
+ }
+ }
+ /*
+ if (twigs.storage.drawFrequencyGrid) {
+ var lines = 10;
+ var ystep = height / lines;
+ var freqstep = (endFreq - startFreq) / lines;
+ ctxGrid.lineCap = "butt";
+ ctxGrid.lineWidth = 1;
+ for (var y = 0, freq = endFreq; y += ystep, freq -= freqstep; y < height) {
+ ctxGrid.strokeStyle = ctxGrid.fillStyle = "rgb(100, 0, 0, 40)"; //style.getPropertyValue("--waveformGridColor");
+ ctxGrid.beginPath();
+ ctxGrid.moveTo(0, y);
+ ctxGrid.lineTo(width, y);
+ ctxGrid.stroke();
+ ctxGrid.closePath();
+ //ctx.strokeStyle = ctx.fillStyle = style.getPropertyValue("--waveformGridTextColor");
+ ctxGrid.fillText(Math.round(freq), 0, y - 2);
+ }
+ }
+
+ if (twigs.storage.drawTimeGrid) {
+ var lines = 10;
+ var startTime = region.time[0] * loaded.duration;
+ var endTime = region.time[1] * loaded.duration;
+ var xstep = width / lines;
+ var timestep = (endTime - startTime) / lines;
+ ctxGrid.lineCap = "butt";
+ ctxGrid.lineWidth = 1;
+ for (var x = 0, time = startTime; x += xstep, time += timestep; x < width) {
+ if (x > width) break; // wtf??????? this is strangely happening
+ ctxGrid.strokeStyle = ctxGrid.fillStyle = "rgb(100, 0, 0, 40)"; //style.getPropertyValue("--waveformGridColor");
+ ctxGrid.beginPath();
+ ctxGrid.moveTo(x, 0);
+ ctxGrid.lineTo(x, height);
+ ctxGrid.stroke();
+ ctxGrid.closePath();
+ //ctx.strokeStyle = ctx.fillStyle = style.getPropertyValue("--waveformGridTextColor");
+ ctxGrid.fillText(Math.round(time * 1000) / 1000, x + 2, height - 2);
+ }
+ }
+ */
+
+ drawSelection();
+ if (!noLoadingPrompt) twirl.loading.hide();
+ drawing = false;
+ }
+
+ async function globalCallbackHandler(ndata) {
+ if (ndata.status && ndata.status <= 0) {
+ return self.errorHandler();
+ }
+
+ if (ndata.hasOwnProperty("undolevel")) {
+ twigs.undoLevel = ndata.undolevel;
+ }
+
+ if (ndata.channels) {
+ loaded.channels = ndata.channels;
+ }
+
+ var initialLoad = false;
+ if (ndata.bins) {
+ loaded.bins = ndata.bins;
+ initialLoad = true;
+ }
+
+ if (ndata.fftdecim) {
+ loaded.fftdecimation = ndata.fftdecim;
+ }
+
+ if (ndata.duration) {
+ loaded.duration = ndata.duration;
+ }
+
+ if (ndata.sr) {
+ maxfreq = ndata.sr / 2;
+ }
+
+ if (ndata.binseltab) {
+ loaded.binSelectionTable = ndata.binseltab;
+ }
+
+ if (ndata.bintimeseltab) {
+ loaded.binTimeSelectionTable = ndata.bintimeseltab;
+ }
+
+ if (ndata.tables) {
+ setTimeout(async function() {
+ loaded.tables = ndata.tables;
+ loaded.length = await app.getCsound().tableLength(loaded.tables[0]);
+ if (!twigs.storage.zoomOnLoad) twigs.storage.zoomOnLoad = 1;
+ if (initialLoad && twigs.storage.zoomOnLoad) {
+ twigs.setFrequencyRegion(0, 0.2, true);
+ twigs.setTimeRegion(0, 0.2);
+ } else {
+ twigs.redraw();
+ }
+ }, 10); // csound may not be ready
+ }
+ }
+
+ this.editInTwist = function() {
+ if (playing) return;
+ if (!twigs.storage.resynthType) {
+ twigs.storage.resynthType = 0;
+ twigs.saveStorage();
+ }
+ if (!window.twist) {
+ return twirl.prompt.show("twist is unavailable in this session");
+ }
+ twirl.loading.show("Processing");
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 3) {
+ return twirl.loading.show("Resynthesising");
+ }
+ twirl.loading.hide();
+ app.removeCallback(ndata.cbid);
+
+ twist.loadFileFromFtable(loaded.name, ndata.tables, function(ldata){
+ if (ldata.status > 0) {
+ self.setVisible(false);
+ twist.setVisible(true);
+ }
+ }, onSave);
+
+ }, true);
+ app.insertScore("twgs_resynth", [0, 1, cbid, "twgs_getbuffers", twigs.storage.resynthType]);
+ };
+
+ this.setVisible = function(state) {
+ twigs.visible = state;
+ var el = $("#twigs");
+ if (state) {
+ el.show();
+ } else {
+ el.hide();
+ }
+ };
+
+
+ function formatFileName(name) {
+ if (!name) name = waveformTabs[instanceIndex].text();
+ if (!name.toLowerCase().endsWith(".wav")) {
+ name += ".wav";
+ }
+
+ // HACK TODO: WASM can't overwrite files
+ name = name.substr(0, name.lastIndexOf(".")) + "." + saveNumber + name.substr(name.lastIndexOf("."));
+ saveNumber ++;
+ // END HACK
+ return name;
+ }
+
+ this.downloadFile = async function(path, name) {
+ if (!name) name = formatFileName(name);
+ 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);
+ };
+
+ var saveNumber = 1;
+ this.saveFile = function(name, onComplete) {
+ if (!twigs.storage.resynthType) {
+ twigs.storage.resynthType = 0;
+ twigs.saveStorage();
+ }
+ if (playing) return;
+ if (onSave) {
+ twirl.loading.show("Processing");
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 3) {
+ return twirl.loading.show("Resynthesising");
+ }
+ twirl.loading.hide();
+ app.removeCallback(ndata.cbid);
+ onSave(ndata.tables);
+ }, true);
+ app.insertScore("twgs_resynth", [0, 1, cbid, "twgs_getbuffers", twigs.storage.resynthType]);
+ return;
+ }
+ if (!name) name = formatFileName(name);
+ var cbid = app.createCallback(async function(ndata){
+ if (ndata.status == 3) {
+ return twirl.loading.show("Resynthesising");
+ }
+ app.removeCallback(ndata.cbid);
+
+ twirl.loading.show("Saving");
+ app.insertScore("twgs_savefile", [0, 1, app.createCallback(async function(ldata){
+ if (onComplete) onComplete();
+ await self.downloadFile(ndata.path, ndata.path);
+ twirl.loading.hide();
+ }), name]);
+
+ }, true);
+ twist.loading.show("Processing");
+ app.insertScore("twgs_resynth", [0, 1, cbid, "twgs_resynth_response", twigs.storage.resynthType]);
+ };
+
+
+ this.loadFileFromFtable = function(name, tables, onComplete, onSaveFunc) {
+ errorState = "File loading error";
+ twigs.ui.showLoadFileFFTPrompt(function(fftSize, fftDecim){
+ twirl.loading.show("Loading file");
+
+ var cbid = app.createCallback(async function(ndata){
+ twirl.loading.hide();
+ if (ndata.status > 0) {
+ loaded.name = name;
+ await globalCallbackHandler(ndata);
+ onSave = onSaveFunc;
+ } else if (ndata.status == -1) {
+ twirl.prompt.show("File not valid");
+ } else if (ndata.status == -2) {
+ twirl.prompt.show("File too large");
+ } else {
+ twirl.prompt.show("File loading error");
+ }
+ if (onComplete) {
+ onComplete(ndata);
+ }
+ });
+ var call = [0, 1, cbid, fftSize, fftDecim];
+ for (let t of tables) {
+ call.push(t);
+ }
+ app.insertScore("twgs_loadftable", call);
+ });
+ };
+
+ async function handleFileDrop(e, obj) {
+ e.preventDefault();
+ if (!e.originalEvent.dataTransfer && !e.originalEvent.files) {
+ return;
+ }
+ if (e.originalEvent.dataTransfer.files.length == 0) {
+ return;
+ }
+ twirl.prompt.hide();
+ twirl.loading.show("Loading");
+ for (const item of e.originalEvent.dataTransfer.files) {
+ if (!twirl.audioTypes.includes(item.type)) {
+ return twirl.errorHandler("Unsupported file type");
+ }
+ if (item.size > twirl.maxFileSize) {
+ return twirl.errorHandler("File too large", twigs.ui.showLoadNewPrompt);
+ }
+ errorState = "File loading error";
+ var content = await item.arrayBuffer();
+ const buffer = new Uint8Array(content);
+ await app.writeFile(item.name, buffer);
+
+ twigs.ui.showLoadFileFFTPrompt(function(fftSize, fftDecim){
+ twirl.loading.show("Loading file");
+ var cbid = app.createCallback(async function(ndata){
+ await app.unlinkFile(item.name);
+ if (ndata.status == -1) {
+ return twirl.errorHandler("File not valid", twigs.ui.showLoadNewPrompt);
+ } else if (ndata.status == -2) {
+ return twirl.errorHandler("File too large", twigs.ui.showLoadNewPrompt);
+ } else {
+ loaded.name = item.name;
+ await globalCallbackHandler(ndata);
+ onSave = false;
+ }
+ });
+ app.insertScore("twgs_loadfile", [0, 1, cbid, item.name, fftSize, fftDecim]);
+ });
+ }
+ }
+
+ this.bootAudio = function(twine) {
+ var channelDefaultItems = ["maxundo"];
+
+ for (var i of channelDefaultItems) {
+ if (twigs.storage.hasOwnProperty(i)) {
+ app.setControlChannel("twgs_" + i, twigs.storage[i]);
+ }
+ }
+ };
+
+ var booted = false;
+ this.boot = function(twine) {
+ if (booted) return;
+ booted = true;
+ setup();
+ twigs.ui = new TwigsUI(twigs);
+ if (!twine) {
+ $("body").on("dragover", function(e) {
+ e.preventDefault();
+ e.originalEvent.dataTransfer.effectAllowed = "all";
+ e.originalEvent.dataTransfer.dropEffect = "copy";
+ return false;
+ }).on("dragleave", function(e) {
+ e.preventDefault();
+ }).on("drop", function(e) {
+ handleFileDrop(e, self);
+ });
+ } else {
+ twigs.twine = twine;
+ }
+ };
+
+};
+
+
+function twigs_startisolated() {
+ window.twigs = new Twigs();
+ twigs.setVisible(true);
+ window.app = new CSApplication({
+ csdUrl: "twigs.csd",
+ onPlay: function() {
+ twigs.bootAudio();
+ twirl.prompt.show("Drag a file here to begin", null, true);
+ twirl.loading.hide();
+ },
+ errorHandler: twirl.errorHandler
+ });
+ $("#twigs_start").click(function(){
+ $(this).hide();
+ twigs.boot();
+ twirl.loading.show("Preparing audio engine");
+ app.play(function(text){
+ twirl.loading.show(text);
+ });
+ });
+}
\ No newline at end of file diff --git a/site/app/twigs/twigs_ui.js b/site/app/twigs/twigs_ui.js new file mode 100644 index 0000000..1186dac --- /dev/null +++ b/site/app/twigs/twigs_ui.js @@ -0,0 +1,577 @@ +var twigsTopMenuData = [
+ {name: "File", contents: [
+ {name: "New", disableOnPlay: true, shortcut: {name: "Ctrl N", ctrlKey: true, key: "n"}, click: function(twigs) {
+ twigs.createNewInstance();
+ }},
+ {name: "Save", disableOnPlay: true, shortcut: {name: "Ctrl S", ctrlKey: true, key: "s"}, click: function(twigs) {
+ twigs.saveFile();
+ }},
+ {name: "Close", disableOnPlay: true, shortcut: {name: "Ctrl W", ctrlKey: true, key: "w"}, click: function(twigs) {
+ twigs.closeInstance();
+ }, condition: function(twist) {
+ return (!twist.twine);
+ }},
+ {name: "Edit in twist", click: function(twigs) {
+ twigs.editInTwist();
+ }, condition: function(twigs) {
+ return window.hasOwnProperty("Twist");
+ }}
+ ]},
+ {name: "Edit", contents: [
+ {name: "Undo", disableOnPlay: true, shortcut: {name: "Ctrl Z", ctrlKey: true, key: "z"}, click: function(twigs) {
+ twigs.undo();
+ }, condition: function(twigs) {
+ return (twigs.storage.maxundo > 0 && twigs.undoLevel > 0);
+ }}
+ ]},
+ {name: "View", contents: [
+ {name: "Contract channels", shortcut: {name: "C", key: "c"}, click: function(twigs) {
+ twigs.timeline.contractChannels();
+ }}
+ ]},
+ {name: "Action", contents: []},
+ {name: "Options", contents: [
+ {name: "Settings", click: function(twigs) {
+ twigs.ui.showSettings();
+ }}
+ ]},
+ {name: "Help", contents: [
+ {name: "Help", click: function(twigs){
+ $("#twigs_documentation")[0].click();
+ }},
+ {name: "Report bug", click: function(twist){
+ $("#twigs_reportbug")[0].click();
+ }},
+ {name: "Contact owner", click: function(twist){
+ $("#twigs_contact")[0].click();
+ }},
+ {name: "About", click: function(twigs) {
+ twigs.ui.showAbout();
+ }},
+ ]}
+];
+
+var TwigsUI = function(twigs) {
+ var ui = this;
+ var el = $("#twigs_sidebar");
+ var elEditor = $("#twigs_editor_inner");
+
+ ui.showLoadFileFFTPrompt = function(onComplete) {
+ var t = $("<table />");
+ var tb = $("<tbody />").appendTo(t);
+ var ksmps = 64;
+
+ var tr = $("<tr />").appendTo(tb);
+ $("<td />").text("FFT size").appendTo(tr);
+
+ var fftSize = $("<select />").change(function(){
+ updateDecimation();
+ });
+ $("<td />").appendTo(tr).append(fftSize);
+ for (let o of [256, 512, 1024, 2048]) {
+ $("<option />").val(o).text(o).appendTo(fftSize);
+ }
+
+ tr = $("<tr />").appendTo(tb);
+ $("<td />").text("FFT decimation").appendTo(tr);
+ var fftDecim = $("<select />");
+ $("<td />").appendTo(tr).append(fftDecim);
+
+ function updateDecimation() {
+ fftDecim.empty();
+ var max = fftSize.val() / 64;
+ var min = max / 2;
+ for (let o of [min, max]) {
+ $("<option />").val(o).text(o).appendTo(fftDecim);
+ }
+ }
+ twirl.prompt.show(t, function() {
+ onComplete(fftSize.val(), fftDecim.val());
+ });
+ fftSize.val(512);
+ updateDecimation();
+ };
+
+
+ function addActionMenu(menu, item) {
+ for (let i in twigsTopMenuData) {
+ if (twigsTopMenuData[i].name.toLowerCase() == menu.toLowerCase()) {
+ twigsTopMenuData[i].contents.push(item);
+ }
+ }
+ }
+
+ var zoomIcons = [
+ {
+ label: "Zoom in frequency",
+ icon: "zoomIn",
+ size: 20,
+ click: function() {
+ twigs.vzoomIn();
+ },
+ shortcut: {name: "W", key: "w"},
+ menuAdd: "view",
+ target: "#twigs_editor_vzoom"
+ },
+ {
+ label: "Zoom out frequency",
+ icon: "zoomOut",
+ size: 20,
+ click: function() {
+ twigs.vzoomOut();
+ },
+ shortcut: {name: "W", key: "w"},
+ menuAdd: "view",
+ target: "#twigs_editor_vzoom"
+ },
+ {
+ label: "Show all frequency",
+ icon: "showAll",
+ size: 20,
+ click: function() {
+ twigs.setFrequencyRegion(0, 1);
+ },
+ shortcut: {name: "W", key: "w"},
+ menuAdd: "view",
+ target: "#twigs_editor_vzoom"
+ },
+ {
+ label: "Zoom in time",
+ icon: "zoomIn",
+ size: 20,
+ click: function() {
+ twigs.hzoomIn();
+ },
+ shortcut: {name: "W", key: "w"},
+ menuAdd: "view",
+ target: "#twigs_editor_hzoom"
+ },
+ {
+ label: "Zoom out time",
+ icon: "zoomOut",
+ size: 20,
+ click: function() {
+ twigs.hzoomOut();
+ },
+ shortcut: {name: "W", key: "w"},
+ menuAdd: "view",
+ target: "#twigs_editor_hzoom"
+ },
+ {
+ label: "Show all time",
+ icon: "showAll",
+ size: 20,
+ click: function() {
+ twigs.setTimeRegion(0, 1);
+ },
+ shortcut: {name: "W", key: "w"},
+ menuAdd: "view",
+ target: "#twigs_editor_hzoom"
+ },
+ {
+ label: "Show all",
+ icon: "showAll",
+ size: 20,
+ click: function() {
+ twigs.setFrequencyRegion(0, 1, true);
+ twigs.setTimeRegion(0, 1);
+ },
+ shortcut: {name: "W", key: "w"},
+ menuAdd: "view",
+ target: "#twigs_editor_hzoom"
+ }
+
+ ];
+
+
+ this.showAbout = function() {
+ var el = $("<div />");
+ var x = $("<div />").appendTo(el);
+ var string = "twigs";
+ var intervals = [];
+
+ function addChar(c) {
+ var elC = $("<h2 />").text(c).css("display", "inline-block").appendTo(x);
+ var rate = (Math.random() * 0.005) + 0.001;
+ var scale = 1;
+ var scaleDirection = false;
+ return setInterval(function(){
+ if (scaleDirection) {
+ if (scale < 1) {
+ scale += rate;
+ } else {
+ scaleDirection = false;
+ }
+ } else {
+ if (scale > 0.05) {
+ scale -= rate;
+ } else {
+ scaleDirection = true;
+ }
+ }
+ elC.css("transform", "scaleX(" + scale + ")");
+ }, (Math.random() * 10) + 8);
+ }
+ for (let c of string) {
+ intervals.push(addChar(c));
+ }
+
+ $("<p />").text("Version " + twigs.version.toFixed(1)).appendTo(el);
+ $("<p />").css("font-size", "12px").text("By Richard Knight 2024").appendTo(el);
+
+ twirl.prompt.show(el, function(){
+ for (let i of intervals) clearInterval(i);
+ });
+ };
+
+ this.showSettings = function() {
+ var settings = [
+ {
+ name: "Undo levels",
+ description: "Number of undo levels stored. Large numbers may affect memory usage",
+ min: 0, max: 32, step: 1, dfault: 2, storageKey: "maxundo",
+ onChange: function(val) {
+ app.setControlChannel("twgs_maxundo", val);
+ }
+ },
+ {
+ name: "Resynthesis type",
+ description: "Type of resynthesis to be used",
+ options: ["Overlap-add", "Additive"],
+ dfault: 0,
+ storageKey: "resynthType"
+ },
+ {
+ name: "Colour type",
+ description: "Type of colouration to use in graphing",
+ options: ["Monochrome", "Colour"],
+ dfault: 0,
+ storageKey: "colourType",
+ onChange: function() {
+ twigs.redraw();
+ }
+ },
+ {
+ name: "Graph type",
+ description: "Approach to graphing partials used",
+ options: ["Joined line", "Separate lines"],
+ dfault: 0,
+ storageKey: "graphType",
+ onChange: function() {
+ twigs.redraw();
+ }
+ },
+ {
+ name: "Basic lines",
+ description: "Show thin, basic lines in graphing",
+ bool: true,
+ dfault: 0,
+ storageKey: "basicLines",
+ onChange: function() {
+ twigs.redraw();
+ }
+ },
+ {
+ name: "Frequency gridlines",
+ description: "Draw the vertical frequency grid",
+ bool: true,
+ dfault: 1,
+ storageKey: "drawFrequencyGrid",
+ onChange: function() {
+ twigs.redraw();
+ }
+ },
+ {
+ name: "Time gridlines",
+ description: "Draw the horizontal time grid",
+ bool: true,
+ dfault: 1,
+ storageKey: "drawTimeGrid",
+ onChange: function() {
+ twigs.redraw();
+ }
+ },
+ {
+ name: "Zoom to start on load",
+ description: "Zoom to the start portion of time and frequency when loading a new file",
+ bool: true,
+ dfault: 1,
+ storageKey: "zoomOnLoad"
+ }
+ ];
+ twirl.showSettings(twigs, settings);
+ };
+
+ var icon_groups = {};
+
+ ui.setSelectionMode = function(mode, iconObj) {
+ for (let s of icon_groups.selection) s.setActive(false);
+ iconObj.setActive(true);
+ elEditor.css("cursor", iconObj.definition.cursor);
+ twigs.setSelectionMode(mode);
+ }
+
+ function setOptionsArea(el) {
+ var o = $("#twigs_options").empty();
+ if (el) o.append(el);
+ }
+
+ ui.icons = {};
+
+ var icons = [
+ [{
+ label: "Select single bin",
+ icon: "pointer",
+ shortcut: {name: "Q", key: "q"},
+ menuAdd: "action",
+ group: "selection",
+ cursor: "default",
+ bgColor: 1,
+ clickOnInactive: true,
+ click: function(obj) {
+ ui.setSelectionMode(twigs.SELECTIONMODE.singleBin, obj);
+ }
+ },
+ {
+ label: "Select area",
+ icon: "areaSelect",
+ shortcut: {name: "W", key: "w"},
+ menuAdd: "action",
+ group: "selection",
+ cursor: "crosshair",
+ bgColor: 1,
+ clickOnInactive: true,
+ click: function(obj) {
+ ui.setSelectionMode(twigs.SELECTIONMODE.dragArea, obj);
+ }
+ }],
+ [{
+ label: "Select bins",
+ icon: "verticalArrows",
+ shortcut: {name: "A", key: "a"},
+ menuAdd: "action",
+ group: "selection",
+ cursor: "vertical-text",
+ bgColor: 1,
+ clickOnInactive: true,
+ click: function(obj) {
+ ui.setSelectionMode(twigs.SELECTIONMODE.dragBins, obj);
+ }
+ },
+ {
+ label: "Free select",
+ icon: "lasso",
+ shortcut: {name: "S", key: "s"},
+ menuAdd: "action",
+ group: "selection",
+ cursor: "crosshair",
+ bgColor: 1,
+ clickOnInactive: true,
+ click: function(obj) {
+ ui.setSelectionMode(twigs.SELECTIONMODE.lasso, obj);
+ }
+ }],
+ [{
+ label: "Bin append select",
+ icon: "waves",
+ shortcut: {name: "Z", key: "z"},
+ menuAdd: "action",
+ group: "selection",
+ cursor: "copy",
+ bgColor: 1,
+ clickOnInactive: true,
+ click: function(obj) {
+ ui.setSelectionMode(twigs.SELECTIONMODE.binAppend, obj);
+ }
+ },
+ {
+ label: "Play selection",
+ icon: "ear",
+ shortcut: {name: "X", key: "x"},
+ menuAdd: "action",
+ bgColor: 2,
+ click: function(obj) {
+ twigs.play(true);
+ }
+ }],
+ [{
+ label: "Move",
+ icon: "move",
+ shortcut: {name: "E", key: "e"},
+ menuAdd: "action",
+ group: "selection",
+ cursor: "grab",
+ bgColor: 3,
+ clickOnInactive: true,
+ click: function(obj) {
+ ui.setSelectionMode(twigs.SELECTIONMODE.move, obj);
+ },
+ optionsArea: function() {
+ if (!twigs.storage.movementType) twigs.storage.movementType = 2;
+ if (!twigs.storage.interpolateVoid) twigs.storage.interpolateVoid = 1;
+ if (!twigs.storage.interpolateRatio) twigs.storage.interpolateRatio = 0;
+ var el = $("<div />");
+ var typeOptions = new twirl.transform.Transform({host: twigs, element: el, definition: {
+ name: "Movement",
+ instr: "twgs_movement",
+ parameters: [
+ {name: "Movement type", description: "Type of movement to apply", channel: "twgs_movementtype", absolutechannel: true, options: ["Copy", "Leave void", "Retain amp/freq in void", "Retain amp in void", "Retain freq in void"], automatable: false, dfault: twigs.storage.movementType, onChange: function(val){
+ twigs.storage.movementType = val;
+ twigs.saveStorage();
+ }},
+ {name: "Interpolate void", description: "Interpolate values in the void created by the movement", channel: "twgs_interpolatevoid", absolutechannel: true, min: 0, max: 1, step: 1, automatable: false, dfault: twigs.storage.interpolateVoid, onChange: function(val){
+ twigs.storage.interpolateVoid = val;
+ twigs.saveStorage();
+ }, conditions: [{channel: "twgs_movementtype", absolutechannel: true, operator: "ge", value: 2}]},
+ {name: "Interpolation ratio", description: "Ratio of interpolation to integrate with target position", channel: "twgs_interpolateratio", absolutechannel: true, min: 0, max: 0.45, automatable: false, dfault: twigs.storage.interpolateRatio, onChange: function(val){
+ twigs.storage.interpolateRatio = val;
+ twigs.saveStorage();
+ }}
+ ]
+ }});
+ return el;
+ }
+ },
+ {
+ label: "Transpose",
+ icon: "arrowsUpDown",
+ shortcut: {name: "R", key: "r"},
+ menuAdd: "action",
+ group: "selection",
+ cursor: "row-resize",
+ bgColor: 3,
+ clickOnInactive: true,
+ click: function(obj) {
+ setSelectionMode(twigs.SELECTIONMODE.transpose, obj);
+ }
+ }],
+ [{
+ label: "Amplify",
+ icon: "fileVolume",
+ shortcut: {name: "D", key: "d"},
+ menuAdd: "action",
+ bgColor: 3,
+ click: function(obj) {
+ if (!twigs.hasSelection) return;
+ var el = $("<div />");
+ $("<h4 />").text("Amplitude scale").appendTo(el);
+ var amp = $("<input />").attr("type", "range").attr("max", 10).attr("min", 0).attr("step", 0.000001).val(1).appendTo(el);
+ twirl.prompt.show(el, function(){
+ twigs.selectionOperation.amplify(amp.val());
+ });
+ }
+ },
+ {
+ label: "Draw",
+ icon: "pencil",
+ shortcut: {name: "F", key: "f"},
+ menuAdd: "action",
+ group: "selection",
+ cursor: "row-resize",
+ bgColor: 3,
+ clickOnInactive: true,
+ click: function(obj) {
+ setSelectionMode(twigs.SELECTIONMODE.draw, obj);
+ }
+ }],
+ [{
+ label: "Play",
+ icon: "play",
+ shortcut: {name: "Space", key: "space"},
+ menuAdd: "action",
+ bgColor: 2,
+ click: function(obj) {
+ twigs.play();
+ }
+ },
+ {
+ label: "Stop",
+ icon: "stop",
+ bgColor: 2,
+ click: function(obj) {
+ twigs.stop();
+ }
+ }],
+ [{
+ label: "Zoom in amplitude",
+ icon: "brightnessIncrease",
+ shortcut: {name: "C", key: "c"},
+ menuAdd: "action",
+ bgColor: 1,
+ click: function(obj) {
+ twigs.increaseAmpScaling();
+ }
+ },
+ {
+ label: "Zoom out amplitude",
+ icon: "brightnessDecrease",
+ shortcut: {name: "V", key: "v"},
+ menuAdd: "action",
+ bgColor: 1,
+ click: function(obj) {
+ twigs.decreaseAmpScaling();
+ }
+ }]
+ ];
+
+ function addIcon(def) {
+ let ops = {};
+ Object.assign(ops, def);
+ if (ops.shortcut) {
+ ops.label += " (" + ops.shortcut.name + ")";
+ }
+ ops.click = function(obj) {
+ def.click(obj);
+ if (def.optionsArea) {
+ setOptionsArea(def.optionsArea())
+ } else {
+ setOptionsArea();
+ }
+ }
+ let icon = twirl.createIcon(ops);
+ if (ops.menuAdd) {
+ let menuData = {
+ name: ops.label,
+ click: function() {
+ icon.el.click();
+ }
+ };
+ if (ops.shortcut) {
+ menuData.shortcut = ops.shortcut;
+ }
+ addActionMenu(ops.menuAdd, menuData);
+ }
+ return icon;
+ }
+
+ function create() {
+ for (let z of zoomIcons) {
+ var icon = addIcon(z);
+ $(z.target).append(icon.el);
+ }
+
+ var tb = $("<tbody />").appendTo($("<table />").appendTo(el));
+ var icol = 0;
+ var first;
+ for (let row of icons) {
+ var tr = $("<tr />").appendTo(tb);
+ for (let col of row) {
+ let icon = addIcon(col);
+ if (!first) first = icon;
+ if (col.group) {
+ if (!icon_groups[col.group]) icon_groups[col.group] = [];
+ icon_groups[col.group].push(icon);
+ }
+ var td = $("<td />").append(icon.el).appendTo(tr);
+ if (col.bgColor) {
+ td.css("background-color", "var(--bgColor" + col.bgColor + ")");
+ }
+ icons[col.key] = icon;
+ }
+ }
+ first.click();
+ }
+
+ create();
+ var topMenu = new twirl.TopMenu(twigs, twigsTopMenuData, $("#twigs_menubar"));
+};
\ No newline at end of file |