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"
}
};
}