Fingerprinting de audio con Python: acoustid y chromaprint
El fingerprinting (huella digital) de audio es una técnica que genera un identificador único a partir del contenido sonoro de un archivo, independientemente de su formato o metadatos. Se usa para identificar canciones, detectar duplicados, sincronizar subtítulos y poblar metadatos automáticamente desde bases de datos como MusicBrainz.
Cómo funciona Chromaprint
Chromaprint es el motor de fingerprinting creado por Lukáš Lalinský. El proceso:
- Convierte el audio a señal mono de 44.1 kHz
- Divide en ventanas solapadas de ~1.2 segundos
- Aplica FFT para obtener el espectro de frecuencias
- Calcula un vector de características cromáticas (12 semitonos por octava)
- Genera una huella de 32 bits por cada ventana → concatenación = fingerprint final
El resultado es un array de enteros de 32 bits codificado en base64.
Instalación
# Instalar chromaprint (binario fpcalc)
# Windows: descargar desde https://acoustid.org/chromaprint
# Linux:
apt-get install libchromaprint-tools
# macOS:
brew install chromaprint
# Binding Python:
pip install pyacoustid mutagen
Generar una huella digital
import acoustid
import chromaprint
def generar_huella(ruta_audio):
"""Genera fingerprint de audio con duración."""
duracion, huella = acoustid.fingerprint_file(ruta_audio)
print(f"Archivo: {ruta_audio}")
print(f"Duración: {duracion:.1f} segundos")
print(f"Huella: {huella[:60]}...")
return duracion, huella
duracion, huella = generar_huella("cancion.mp3")
Usando fpcalc directamente
import subprocess
import json
def fpcalc(ruta_audio, longitud=120):
"""Llama a fpcalc para obtener la huella."""
resultado = subprocess.run(
['fpcalc', '-json', '-length', str(longitud), ruta_audio],
capture_output=True, text=True
)
if resultado.returncode != 0:
raise RuntimeError(f"fpcalc error: {resultado.stderr}")
return json.loads(resultado.stdout)
datos = fpcalc("cancion.ogg")
print(f"Duración: {datos['duration']:.1f}s")
print(f"Huella: {datos['fingerprint'][:50]}...")
Identificar canciones con AcoustID
AcoustID es el servicio público que mapea huellas a IDs de MusicBrainz. Necesitas una API key gratuita en acoustid.org.
import acoustid
API_KEY = "tu_api_key_aqui"
def identificar_cancion(ruta_audio):
resultados = acoustid.match(API_KEY, ruta_audio)
for puntuacion, id_grabacion, titulo, artista in resultados:
print(f"Score: {puntuacion:.0%}")
print(f"Título: {titulo}")
print(f"Artista: {artista}")
print(f"MBID: {id_grabacion}")
print("---")
identificar_cancion("pista_desconocida.mp3")
Salida típica:
Score: 98%
Título: Bohemian Rhapsody
Artista: Queen
MBID: b7b38bff-b9c7-3c7e-b82d-1f49a1f0e4f2
---
Identificación con metadatos extendidos
import acoustid
import requests
def buscar_metadata_completa(ruta_audio, api_key):
duracion, huella = acoustid.fingerprint_file(ruta_audio)
# Consultar AcoustID con metadatos extendidos
url = "https://api.acoustid.org/v2/lookup"
params = {
'client': api_key,
'meta': 'recordings releases releasegroups tracks',
'duration': int(duracion),
'fingerprint': huella,
}
resp = requests.get(url, params=params)
data = resp.json()
if data['status'] != 'ok' or not data.get('results'):
print("No encontrado")
return None
mejor = data['results'][0]
score = mejor.get('score', 0)
if not mejor.get('recordings'):
print(f"Encontrado (score {score:.0%}) pero sin metadatos")
return None
rec = mejor['recordings'][0]
info = {
'titulo': rec.get('title', 'Desconocido'),
'artistas': [a['name'] for a in rec.get('artists', [])],
'duracion': rec.get('duration', 0),
'mbid': rec.get('id', ''),
'score': score,
}
# Álbum del primer release
if rec.get('releases'):
release = rec['releases'][0]
info['album'] = release.get('title', '')
info['año'] = release.get('date', {}).get('year', '')
return info
meta = buscar_metadata_completa("cancion.flac", API_KEY)
if meta:
print(f"Título: {meta['titulo']}")
print(f"Artista: {', '.join(meta['artistas'])}")
print(f"Álbum: {meta.get('album', 'N/A')}")
print(f"Año: {meta.get('año', 'N/A')}")
print(f"Score: {meta['score']:.0%}")
Detectar duplicados en una colección
import acoustid
import chromaprint
from pathlib import Path
from collections import defaultdict
def extraer_huella_cruda(ruta_audio):
"""Decodifica la huella a lista de enteros para comparación directa."""
_, fingerprint_b64 = acoustid.fingerprint_file(ruta_audio)
return chromaprint.decode_fingerprint(fingerprint_b64)[0] # lista de int32
def similitud_huellas(h1, h2):
"""Compara dos huellas por distancia de Hamming sobre los bits."""
if not h1 or not h2:
return 0.0
longitud = min(len(h1), len(h2))
coincidencias = sum(
bin(a ^ b).count('0') - (32 - 32)
for a, b in zip(h1[:longitud], h2[:longitud])
)
# Contar bits iguales / bits totales
bits_iguales = sum(
32 - bin(a ^ b).count('1')
for a, b in zip(h1[:longitud], h2[:longitud])
)
return bits_iguales / (longitud * 32)
def detectar_duplicados(carpeta, umbral=0.80, extensiones=('mp3','flac','ogg','wav','m4a')):
carpeta = Path(carpeta)
archivos = [f for ext in extensiones for f in carpeta.rglob(f'*.{ext}')]
print(f"Analizando {len(archivos)} archivos de audio...")
huellas = {}
for archivo in archivos:
try:
huellas[archivo] = extraer_huella_cruda(str(archivo))
print(f" OK {archivo.name}")
except Exception as e:
print(f" ERR {archivo.name}: {e}")
# Comparar todos contra todos
duplicados = []
archivos_lista = list(huellas.keys())
for i in range(len(archivos_lista)):
for j in range(i + 1, len(archivos_lista)):
a, b = archivos_lista[i], archivos_lista[j]
sim = similitud_huellas(huellas[a], huellas[b])
if sim >= umbral:
duplicados.append((sim, a, b))
if duplicados:
print(f"\nDuplicados encontrados (umbral {umbral:.0%}):")
for sim, a, b in sorted(duplicados, reverse=True):
print(f" [{sim:.1%}] {a.name} ↔ {b.name}")
else:
print("\nNo se encontraron duplicados")
return duplicados
detectar_duplicados("/Música/colección", umbral=0.85)
Poblar etiquetas ID3 automáticamente
import acoustid
import requests
from mutagen.mp3 import MP3
from mutagen.id3 import ID3, TIT2, TPE1, TALB, TDRC, TRCK, ID3NoHeaderError
def etiquetar_automaticamente(ruta_mp3, api_key):
print(f"Procesando: {ruta_mp3}")
meta = buscar_metadata_completa(ruta_mp3, api_key)
if not meta or meta['score'] < 0.85:
print(" → Confianza insuficiente, saltando")
return False
try:
tags = ID3(ruta_mp3)
except ID3NoHeaderError:
tags = ID3()
tags[TIT2.FrameID] = TIT2(encoding=3, text=meta['titulo'])
if meta['artistas']:
tags[TPE1.FrameID] = TPE1(encoding=3, text=', '.join(meta['artistas']))
if meta.get('album'):
tags[TALB.FrameID] = TALB(encoding=3, text=meta['album'])
if meta.get('año'):
tags[TDRC.FrameID] = TDRC(encoding=3, text=str(meta['año']))
tags.save(ruta_mp3)
print(f" → Etiquetado: {meta['titulo']} — {', '.join(meta['artistas'])}")
return True
# Etiquetar toda una carpeta
from pathlib import Path
import time
def etiquetar_carpeta(carpeta, api_key, pausa=1.0):
archivos = list(Path(carpeta).rglob('*.mp3'))
exito = 0
for i, archivo in enumerate(archivos, 1):
print(f"[{i}/{len(archivos)}]", end=" ")
if etiquetar_automaticamente(str(archivo), api_key):
exito += 1
time.sleep(pausa) # respetar límite de tasa de la API
print(f"\nResultado: {exito}/{len(archivos)} archivos etiquetados")
etiquetar_carpeta("/Música/sin_etiquetar", API_KEY)
Sincronización de subtítulos por huella
Detectar el offset temporal entre el audio de un vídeo y un archivo de subtítulos:
import acoustid
import subprocess
import json
def extraer_audio_video(ruta_video, ruta_audio_salida, duracion=120):
"""Extrae los primeros N segundos de audio de un vídeo."""
subprocess.run([
'ffmpeg', '-y', '-i', ruta_video,
'-vn', '-ac', '1', '-ar', '44100',
'-t', str(duracion),
ruta_audio_salida
], capture_output=True, check=True)
def comparar_huellas_temporales(audio1, audio2, ventana=10):
"""Compara huellas en ventanas temporales para encontrar el offset."""
datos1 = fpcalc(audio1)
datos2 = fpcalc(audio2)
print(f"Audio 1: {datos1['duration']:.1f}s | Audio 2: {datos2['duration']:.1f}s")
# Aquí se implementaría la correlación cruzada de las huellas
# Devuelve el offset en segundos
return 0.0 # placeholder
# Extraer audio de la versión original y de la versión local
extraer_audio_video("pelicula_original.mkv", "audio_orig.wav")
extraer_audio_video("pelicula_local.mp4", "audio_local.wav")
offset = comparar_huellas_temporales("audio_orig.wav", "audio_local.wav")
print(f"Offset de subtítulos: {offset:.2f}s")
Casos de uso reales
| Caso | Herramienta | Descripción |
|---|---|---|
| Identificar canción desconocida | acoustid.match() | Consulta MusicBrainz/AcoustID |
| Limpiar duplicados musicales | similitud_huellas() | Compara sin depender del nombre |
| Auto-etiquetar colección MP3 | mutagen + acoustid | Llena ID3 automáticamente |
| Detectar copyright en vídeos | chromaprint + BD propia | Compara con catálogo de referencia |
| Sincronizar subtítulos | fpcalc + correlación | Ajusta timing automáticamente |
Recurso adicional
Para convertir entre formatos de audio (MP3, FLAC, OGG, WAV, AAC, M4A) sin necesidad de programar, usa KaijuConverter — gratis y sin registro.
Conversiones relacionadas
Pares de formatos de audio más frecuentes: