// 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.1238); } } // 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.1238; 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); } // Only suggest up to 20 shifts shifts = shifts.slice(0, 20); 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; } canvas.style.display = "unset"; 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); } }