Move visualization code to a dedicated file
This commit is contained in:
parent
044c89e977
commit
a9554e265f
@ -3,7 +3,6 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Template</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -284,6 +283,9 @@
|
||||
</span>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<script src="configurationpage?name=visualizer.js"></script>
|
||||
|
||||
<script>
|
||||
// first and second episodes to fingerprint & compare
|
||||
var lhs = [];
|
||||
@ -468,19 +470,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// make an authenticated GET to the server and parse the response as JSON
|
||||
async function getJson(url, method = "GET") {
|
||||
url = ApiClient.serverAddress() + "/" + url;
|
||||
@ -551,109 +540,6 @@
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
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 += "<span>" + lTitle + ": " +
|
||||
secondsToString(lStart) + " - " + secondsToString(lEnd) + "</span> <br />";
|
||||
introsLog.innerHTML += "<span>" + rTitle + ": " +
|
||||
secondsToString(rStart) + " - " + secondsToString(rEnd) + "</span> <br />";
|
||||
}
|
||||
}
|
||||
|
||||
// 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(", ");
|
||||
}
|
||||
}
|
||||
|
||||
// check that the user is still on the configuration page
|
||||
function checkWindowHash() {
|
||||
const h = location.hash;
|
||||
@ -781,122 +667,6 @@
|
||||
timeContainer.style.top = (-1 * rect.height + y).toString() + "px";
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
@ -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 += "<span>" + lTitle + ": " +
|
||||
secondsToString(lStart) + " - " + secondsToString(lEnd) + "</span> <br />";
|
||||
introsLog.innerHTML += "<span>" + rTitle + ": " +
|
||||
secondsToString(rStart) + " - " + secondsToString(rEnd) + "</span> <br />";
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@
|
||||
<ItemGroup>
|
||||
<None Remove="Configuration\configPage.html" />
|
||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||
<EmbeddedResource Include="Configuration\visualizer.js" />
|
||||
<EmbeddedResource Include="Configuration\version.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -157,7 +157,12 @@ public class Plugin : BasePlugin<PluginConfiguration>, 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"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user