Son las tres de la mañana, la sala está a tope y el DJ acaba de meter un tema que no conoces. Sacas el móvil, pulsas un botón y, unos segundos después, sabes a cuántos BPM iba aquello. Esa es la idea de Echo Pulse, una de las funciones de Echoes, la app para la cultura de club que estoy construyendo.
Sobre el papel parece sencillo. En la práctica hay que resolver dónde se analiza el audio, qué pasa cuando no hay cobertura, cómo se lee lo que devuelve la herramienta y qué hacer cuando el algoritmo se inventa un tempo. Te cuento cómo lo he montado, con el código real.
Cuánto audio hace falta
Empecé con 10 segundos. Durante un tiempo la documentación decía 10 y la app grababa 12, una de esas incoherencias que salen cuando vas rápido. Al rediseñar la pantalla lo unifiqué en 15 segundos, y el número no es caprichoso: una frase de 8 compases a 128 BPM son 32 tiempos, y 32 × 60 / 128 da exactamente 15 segundos. Cabe una subida o un drop entero sin que aquello se convierta en grabar el set.
En la app, hecha con Expo y React Native, la grabación usa expo-audio con el preset de alta calidad y la medición de volumen activada. Esa medición no sirve para el BPM: sirve para que el botón reaccione al sonido real mientras grabas. Cada 70 ms leo el nivel del micrófono, y cuando el volumen salta claramente por encima de su media reciente lanzo un anillo, como si fuera el bombo. Al llegar a 15 segundos se para sola y el móvil vibra.

Primero guardar, luego subir
Los clubs son sótanos. La cobertura desaparece justo donde la app tiene que funcionar, así que una de las decisiones de arquitectura que no negocio en Echoes es que todo sea offline-first. El Pulse se guarda en una cola local antes de intentar subirlo, con la hora real de la grabación. Si no hay red, se queda ahí y se reintenta solo al volver la cobertura o al abrir la app.
Para que reintentar no duplique nada, el cliente genera un UUID y el servidor es idempotente con él: si ese Pulse ya existe, devuelve el que hay en lugar de crear otro. La validación de la subida también es corta y concreta:
'duration' => ['required', 'numeric',
'min:0', 'max:60'],
'file' => ['required', 'file', 'max:15360',
'mimes:m4a,aac,mp3,wav'],
El resultado aparece al instante con el audio que ya tiene el móvil. La subida y el análisis siguen por detrás, y mientras el servidor no responde la app consulta el estado cada 2,5 segundos hasta que el Pulse pasa a analizado o fallido.
ffmpeg y aubio en el servidor
Para detectar el tempo había dos caminos: pagar una API de análisis de audio o hacerlo yo. Echoes arrancó con presupuesto cero, así que elegí aubio, una librería libre de análisis de audio con una herramienta de línea de comandos, instalada junto a ffmpeg en el mismo servidor donde corre la API de Laravel.
El proceso tiene dos pasos. Primero ffmpeg convierte el clip del móvil a un WAV mono a 44,1 kHz, que es lo que aubio lee mejor. Después, aubio tempo calcula el tempo:
ffmpeg -y -i pulse.m4a -ac 1 -ar 44100 pulse.wav
aubio tempo pulse.wav
Desde PHP lanzo ambos con el componente Process de Symfony, con un tiempo máximo de 60 segundos por comando. La versión 0.4 de aubio imprime el BPM global como «128.0 bpm», así que basta con una expresión regular. Y descarto lo que no tiene sentido musical:
if (preg_match('/([\d.]+)\s*bpm/i', $out, $m)) {
$bpm = round((float) $m[1], 1);
return ($bpm >= 40 && $bpm <= 300)
? $bpm : null;
}
Por si otra versión de aubio devuelve una lista de golpes en vez del total, hay un plan B: calculo los intervalos entre golpes, me quedo con los que caen entre 0,2 y 2 segundos y uso la mediana. La mediana aguanta mejor que la media un golpe perdido o uno de más.
En el mismo paso saco dos datos más con ffmpeg. La energía, a partir del volumen medio que da el filtro volumedetect, convertida a una escala de 0 a 1 donde -45 dB es 0 y 0 dB es 1. Y la onda: 64 picos de amplitud normalizados, para que el reproductor de la app se ilumine con el audio real y no con un dibujo genérico.
El análisis se ejecuta en la misma petición de subida y tarda unos 3 o 4 segundos, así que el BPM vuelve ya en la respuesta. Lo hago síncrono a propósito: la cola de la API también procesa los vídeos de las noches, y así el Pulse no depende de ella.
Cuando aubio se inventa el tempo
El fallo más curioso apareció probando en silencio. aubio siempre encuentra un tempo, incluso cuando no suena nada: con un audio completamente mudo me devolvía 137 BPM. Un usuario que graba en la cola del ropero y ve «137 BPM» deja de fiarse de la función.
La solución fue usar la energía como filtro. Por debajo de 0,1 en mi escala, unos -40 dB de volumen medio, no hay música que medir y no doy BPM. La app lo explica en lugar de mostrar un número falso:
// aubio encuentra tempo hasta en el silencio
'bpm' => $energy < self::MIN_ENERGY_FOR_BPM
? null
: $this->detectBpm($wav),
Este caso tiene un test: genero cinco segundos de silencio con ffmpeg y compruebo que el BPM sale nulo y la energía es cero. Otro test sintetiza un tono de 440 Hz y comprueba que la onda tiene los picos pedidos, todos entre 0 y 1.
Sobre la precisión prefiero ser honesto. La prueba real que he hecho fue reproducir por los altavoces un tema de 125 BPM y grabarlo con la app: el servidor dio 125,2. Buena señal, pero es una prueba con música limpia, no una sala llena de gente gritando. Los detectores de tempo pueden confundirse con la mitad o el doble del tempo real, y el ruido del público o alguien hablando junto al micrófono no ayudan. Aún me falta probarlo a fondo en un club, y hasta entonces no voy a prometer cifras.
Guardar siempre el audio original
La decisión que más tranquilidad me da es esta: el clip original se guarda siempre, salga bien el análisis o no. Si el servidor no tiene ffmpeg o aubio instalados, el Pulse queda como fallido pero no se pierde nada. Cuando las herramientas están disponibles, un comando lo vuelve a encolar:
php artisan echoes:reanalyze-pulses
php artisan echoes:reanalyze-pulses --all
La opción --all reanaliza también los que ya estaban bien. Así, si mejoro el algoritmo o cambio el umbral de silencio, puedo recalcular todo el histórico. Y si algún día hay presupuesto para reconocer canciones, el reconocimiento está pensado detrás de una interfaz de drivers, y los audios de hoy se podrán analizar entonces.
Con el BPM ya guardado empiezan a salir cosas. Tu «Music DNA» calcula la intensidad a partir de la media de tus Pulses, y el panel para salas dibuja la curva de la noche con el BPM por horas.
Si estás pensando en algo parecido, mi consejo es empezar por la herramienta libre en tu propio servidor, guardar siempre el dato original y desconfiar de cualquier número que el algoritmo dé sin sonido detrás. Si tienes un proyecto con audio, móvil o datos en tiempo real, cuéntamelo y lo vemos.

