Files
am-deduper/am_deduper/acoustic.py
T

51 lines
1.5 KiB
Python

from __future__ import annotations
import json
import subprocess
from pathlib import Path
MAX_SECONDS = 120
MAX_BIT_ERROR = 2
MAX_ALIGN_OFFSET = 120
def chromaprint(path: Path) -> str | None:
try:
p = subprocess.run(
["fpcalc", "-length", str(MAX_SECONDS), "-json", str(path)],
capture_output=True, text=True, timeout=180, check=True,
)
value = json.loads(p.stdout).get("fingerprint")
return value if isinstance(value, str) and value else None
except (OSError, subprocess.SubprocessError, ValueError, json.JSONDecodeError):
return None
def _popcount(value: int) -> int:
return value.bit_count()
def similarity(a: str, b: str) -> float:
"""Compare Chromaprint fingerprints using the AcoustID alignment method.
This is deliberately bounded to the local fingerprint comparison and does
not contact the AcoustID service.
"""
if not a or not b:
return 0.0
try:
import chromaprint as cp
aa = [int(x) for x in cp.decode_fingerprint(a.encode())[0]]
bb = [int(x) for x in cp.decode_fingerprint(b.encode())[0]]
except (ImportError, AttributeError, TypeError, ValueError):
return 0.0
if not aa or not bb:
return 0.0
counts = [0] * (len(aa) + len(bb) + 1)
for i, av in enumerate(aa):
for j in range(max(0, i - MAX_ALIGN_OFFSET), min(len(bb), i + MAX_ALIGN_OFFSET)):
if _popcount(av ^ bb[j]) <= MAX_BIT_ERROR:
counts[i - j + len(bb)] += 1
return max(counts) / min(len(aa), len(bb))