From a9554e265f126d2f6f90fbb39731fe9c31b147c5 Mon Sep 17 00:00:00 2001 From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Sat, 16 Jul 2022 23:35:15 -0500 Subject: [PATCH] Move visualization code to a dedicated file --- .../Configuration/configPage.html | 236 +----------------- .../Configuration/visualizer.js | 229 +++++++++++++++++ ...nfusedPolarBear.Plugin.IntroSkipper.csproj | 1 + .../Plugin.cs | 7 +- 4 files changed, 239 insertions(+), 234 deletions(-) create mode 100644 ConfusedPolarBear.Plugin.IntroSkipper/Configuration/visualizer.js diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html index 2dd55ef..fcc5007 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html @@ -3,7 +3,6 @@ - Template @@ -284,6 +283,9 @@ + + + - - diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/visualizer.js b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/visualizer.js new file mode 100644 index 0000000..ddd0345 --- /dev/null +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/visualizer.js @@ -0,0 +1,229 @@ +// re-render the troubleshooter with the latest offset +function renderTroubleshooter() { + paintFingerprintDiff(canvas, lhs, rhs, Number(offset.value)); + findIntros(); +} + +// refresh the upper & lower bounds for the offset +function refreshBounds() { + const len = Math.min(lhs.length, rhs.length) - 1; + offset.min = -1 * len; + offset.max = len; +} + +function findIntros() { + let times = []; + + // get the times of all similar fingerprint points + for (let i in fprDiffs) { + if (fprDiffs[i] > fprDiffMinimum) { + times.push(i * 0.128); + } + } + + // always close the last range + times.push(Number.MAX_VALUE); + + let last = times[0]; + let start = last; + let end = last; + let ranges = []; + + for (let t of times) { + const diff = t - last; + + if (diff <= 3.5) { + end = t; + last = t; + continue; + } + + const dur = Math.round(end - start); + if (dur >= 15) { + ranges.push({ + "start": start, + "end": end, + "duration": dur + }); + } + + start = t; + end = t; + last = t; + } + + const introsLog = document.querySelector("span#intros"); + introsLog.style.position = "relative"; + introsLog.style.left = "115px"; + introsLog.innerHTML = ""; + + const offset = Number(txtOffset.value) * 0.128; + for (let r of ranges) { + let lStart, lEnd, rStart, rEnd; + + if (offset < 0) { + // negative offset, the diff is aligned with the RHS + lStart = r.start - offset; + lEnd = r.end - offset; + rStart = r.start; + rEnd = r.end; + + } else { + // positive offset, the diff is aligned with the LHS + lStart = r.start; + lEnd = r.end; + rStart = r.start + offset; + rEnd = r.end + offset; + } + + const lTitle = selectEpisode1.options[selectEpisode1.selectedIndex].text; + const rTitle = selectEpisode2.options[selectEpisode2.selectedIndex].text; + introsLog.innerHTML += "" + lTitle + ": " + + secondsToString(lStart) + " - " + secondsToString(lEnd) + "
"; + introsLog.innerHTML += "" + rTitle + ": " + + secondsToString(rStart) + " - " + secondsToString(rEnd) + "
"; + } +} + +// find all shifts which align exact matches of audio. +function findExactMatches() { + let shifts = []; + + for (let lhsIndex in lhs) { + let lhsPoint = lhs[lhsIndex]; + let rhsIndex = rhs.findIndex((x) => x === lhsPoint); + + if (rhsIndex === -1) { + continue; + } + + let shift = rhsIndex - lhsIndex; + if (shifts.includes(shift)) { + continue; + } + + shifts.push(shift); + } + + txtSuggested.textContent = "Suggested shifts: "; + if (shifts.length === 0) { + txtSuggested.textContent += "none available"; + } else { + shifts.sort((a, b) => { return a - b }); + txtSuggested.textContent += shifts.join(", "); + } +} + +// The below two functions were modified from https://github.com/dnknth/acoustid-match/blob/ffbf21d8c53c40d3b3b4c92238c35846545d3cd7/fingerprints/static/fingerprints/fputils.js +// Originally licensed as MIT. +function renderFingerprintData(ctx, fp, xor = false) { + const pixels = ctx.createImageData(32, fp.length); + let idx = 0; + + for (let i = 0; i < fp.length; i++) { + for (let j = 0; j < 32; j++) { + if (fp[i] & (1 << j)) { + pixels.data[idx + 0] = 255; + pixels.data[idx + 1] = 255; + pixels.data[idx + 2] = 255; + + } else { + pixels.data[idx + 0] = 0; + pixels.data[idx + 1] = 0; + pixels.data[idx + 2] = 0; + } + + pixels.data[idx + 3] = 255; + idx += 4; + } + } + + if (!xor) { + return pixels; + } + + // if rendering the XOR of the fingerprints, count how many bits are different at each timecode + fprDiffs = []; + + for (let i = 0; i < fp.length; i++) { + let count = 0; + + for (let j = 0; j < 32; j++) { + if (fp[i] & (1 << j)) { + count++; + } + } + + // push the percentage similarity + fprDiffs[i] = 100 - (count * 100) / 32; + } + + return pixels; +} + +function paintFingerprintDiff(canvas, fp1, fp2, offset) { + if (fp1.length == 0) { + return; + } + + let leftOffset = 0, rightOffset = 0; + if (offset < 0) { + leftOffset -= offset; + } else { + rightOffset += offset; + } + + let fpDiff = []; + fpDiff.length = Math.min(fp1.length, fp2.length) - Math.abs(offset); + for (let i = 0; i < fpDiff.length; i++) { + fpDiff[i] = fp1[i + leftOffset] ^ fp2[i + rightOffset]; + } + + const ctx = canvas.getContext('2d'); + const pixels1 = renderFingerprintData(ctx, fp1); + const pixels2 = renderFingerprintData(ctx, fp2); + const pixelsDiff = renderFingerprintData(ctx, fpDiff, true); + const border = 4; + + canvas.width = pixels1.width + border + // left fingerprint + pixels2.width + border + // right fingerprint + pixelsDiff.width + border // fingerprint diff + + 4; // if diff[x] >= fprDiffMinimum + + canvas.height = Math.max(pixels1.height, pixels2.height) + Math.abs(offset); + + ctx.rect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#C5C5C5"; + ctx.fill(); + + // draw left fingerprint + let dx = 0; + ctx.putImageData(pixels1, dx, rightOffset); + dx += pixels1.width + border; + + // draw right fingerprint + ctx.putImageData(pixels2, dx, leftOffset); + dx += pixels2.width + border; + + // draw fingerprint diff + ctx.putImageData(pixelsDiff, dx, Math.abs(offset)); + dx += pixelsDiff.width + border; + + // draw the fingerprint diff similarity indicator + // https://davidmathlogic.com/colorblind/#%23EA3535-%232C92EF + for (let i in fprDiffs) { + const j = Number(i); + const y = Math.abs(offset) + j; + const point = fprDiffs[j]; + + if (point >= 100) { + ctx.fillStyle = "#002FFF" + } else if (point >= fprDiffMinimum) { + ctx.fillStyle = "#2C92EF"; + } else { + ctx.fillStyle = "#EA3535"; + } + + ctx.fillRect(dx, y, 4, 1); + } +} diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj b/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj index c715c59..63ff668 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj +++ b/ConfusedPolarBear.Plugin.IntroSkipper/ConfusedPolarBear.Plugin.IntroSkipper.csproj @@ -23,6 +23,7 @@ + diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs index 18fcf78..6b417d7 100644 --- a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs +++ b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs @@ -157,7 +157,12 @@ public class Plugin : BasePlugin, IHasWebPages new PluginPageInfo { Name = this.Name, - EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.Configuration.configPage.html", GetType().Namespace) + EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html" + }, + new PluginPageInfo + { + Name = "visualizer.js", + EmbeddedResourcePath = GetType().Namespace + ".Configuration.visualizer.js" } }; }