From 9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 13 Apr 2025 18:48:02 +0100 Subject: initial --- site/app/twigs/twigs.js | 1165 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1165 insertions(+) create mode 100644 site/app/twigs/twigs.js (limited to 'site/app/twigs/twigs.js') 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 = $("").css({ + position: "absolute", width: "100%", height: "100%" + }).attr("width", width) + .attr("height", height); + elCanvas = $("").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 = $("").css({ + position: "absolute", width: "100%", height: "100%", top: "0px", left: "0px", "z-index": 12 + }).attr("width", width).attr("height", height).appendTo(elContainer); + + elSelectCanvas = $("").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 = $("").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 -- cgit v1.2.3