
Si has seguido el podcast estas últimas semanas, ya sabrás que le hemos dado a nuestra IA una memoria bastante decente. Hemos convertido seis mil notas en un cerebro conectado, hemos montado un GraphRAG que responde sin inventarse nada y hemos domado SQLite con sqlite-utils para que todo eso se pudiera consultar. Pero un modelo de lenguaje con memoria y sin sentidos es como una biblioteca con las luces fundidas. Sabe muchísimo y no puede sacarle partido a casi nada de lo que guarda dentro. Le falta oído, le falta vista y le falta voz.
Uno de mis hijos es un poco gamberro y siempre me está preguntando por qué le hablo a la IA cuando podría escribirle. Yo no tengo una respuesta mejor que la de cualquier persona que trabaja con las manos, hablar es más rápido que teclear. Y ahí está el problema, porque cuando hablo no hablo en un estudio insonorizado. Hablo en casa, con el ventilador de fondo, a veces con la cafetera silbando, y el audio que llega al modelo está tan sucio como suena. Esa es la pregunta que da título al episodio de hoy, ¿quién prepara el audio para que Whisper lo entienda perfectamente? Y la respuesta, desde hace años, se llama FFmpeg.
Un modelo con memoria necesita sentidos
Esa es la idea que tengo en la cabeza desde hace tiempo y con la que arranco la serie de los sentidos de la IA. Un modelo de lenguaje, o un agente, no vive solo de texto. Necesita poder oír, necesita poder ver y necesita poder hablar. Y FFmpeg es el más básico de todos esos sentidos, porque es el que aparece primero y el más olvidado. Nadie presume de tener FFmpeg instalado, es como presumir de tener ls. Sin embargo, cuando entiendes lo que hace por tu IA, deja de ser una herramienta aburrida y pasa a ser la pieza que sostiene todo lo demás.
Piensa en lo que tienes delante cuando intentas hablar con tu IA. Tienes un micrófono que captura tu voz mezclada con el ruido de la habitación. Tienes un motor de reconocimiento de voz, Whisper en mi caso, que espera un formato muy concreto. Tienes un modelo de lenguaje al que le llega texto. Y tienes, cuando vas de vuelta, un motor de texto a voz que genera un audio sintético. Entre todos esos pasos hay una herramienta invisible que no se queja, que no necesita GPU y que no cuesta un céntimo, FFmpeg.
Por eso me gusta la analogía del pegamento. Sin FFmpeg, Whisper entiende algo, pero le cuesta. Digamos que es el pegamento que hace que tu IA funcione en el mundo real, con el audio sucio que el mundo real te da. Y también es una navaja suiza, porque con el mismo binario conviertes, cortas, unes, limpias y normalizas, sin instalar nada más.
FFmpeg, el pegamento que ya tienes instalado
FFmpeg es varias cosas a la vez. Por dentro lleva un conjunto de bibliotecas, libavcodec para los códecs, libavformat para los contenedores, libavfilter para los filtros, libavutil, libavdevice, libswscale y libswresample. Y por fuera te da tres comandos. El propio ffmpeg, que es el que vamos a usar casi siempre. ffplay, que es un reproductor mínimo. Y ffprobe, que es con el que vas a inspeccionar qué hay dentro de un archivo cuando no te cuadren las cuentas.
Si quieres saber exactamente qué versión llevas, este es el comando que yo ejecuto siempre lo primero,
# Versión, librerías y línea de compilación completa (los --enable-*)
ffmpeg -version
# ¿Está disponible un filtro concreto?
ffmpeg -filters | grep -E "loudnorm|afftdn|arnndn|silenceremove"
# La mejor chuleta que existe para un filtro, ayuda y valores por defecto
ffmpeg -h filter=loudnorm
En mi equipo, un Arch Linux con el paquete oficial, ffmpeg -version me devuelve la rama estable más reciente, la 9.0.2 bautizada como Lei, publicada en septiembre de 2026. El binario trae --enable-gpl, --enable-libmp3lame, --enable-libvorbis, --enable-libopus y hasta --enable-libsoxr, que es el remuestreador de mayor calidad que vas a encontrar dentro del propio FFmpeg. En números redondos, hablamos de más de 500 códecs, más de 300 filtros y más de 400 formatos. Cuando digo navaja suiza no exagero, digo que con el mismo binario haces de todo.
Un detalle que conviene tener claro, la licencia. FFmpeg es LGPL v2.1 o posterior, pero incorpora partes opcionales bajo GPL v2 o posterior, y si esas partes se activan al compilar, la GPL se contagia a todo el conjunto. Ejemplos típicos de partes GPL son libx264 y libx265. Los paquetes que distribuyen las distros, como el de Arch, casi siempre vienen compilados con --enable-gpl, así que ese binario es GPL. No hay licencia comercial alternativa, y esto no es una pega para nada de lo que vamos a hacer hoy.
Y sí, FFmpeg está detrás de casi todo lo que reproduces sin saberlo. VLC, Blender, casi cualquier vídeo que se reproduce en tu navegador, el pipeline de muchas plataformas de vídeo. La lista de quién lo usa circula por internet sin una fuente oficial que la respalde, pero lo que sí es verificable es que cualquier programa que incorpore libavformat está usando FFmpeg por dentro. Firefox lleva su propio fork de las libav*. La conclusión práctica es que llevas años usándolo sin saberlo.
Instalarlo es un chiste, y en cualquier distribución de las habituales está en los repos oficiales,
# Debian, Ubuntu y derivadas
sudo apt install ffmpeg
# Arch Linux y Manjaro
sudo pacman -S ffmpeg
# Fedora y RHEL (necesita RPM Fusion para las partes con patentes)
sudo dnf install ffmpeg
Hay una diferencia entre builds que conviene conocer. En Arch, el paquete oficial de ffmpeg cubre casi todo, pero hay dos cosas que no trae. La primera es el filtro whisper de FFmpeg, esa integración de whisper.cpp dentro del propio FFmpeg, que en el paquete oficial no está configurada y necesita el paquete ffmpeg-full del AUR. Y ojo, porque es un filtro muy goloso. La segunda es el filtro arnndn, la reducción de ruido con red neuronal, y ese sí está disponible aunque no lo parezca, porque FFmpeg incorpora su propia implementación. Lo compruebas con ffmpeg -h filter=arnndn y ahí lo tienes.
Convertir formatos en una sola línea
La sintaxis base de FFmpeg no necesita explicación porque no tiene ninguna. Le das una entrada y una salida, y él deduce el contenedor y el códec por la extensión del archivo de salida. Así de simple,
# MP3 a WAV. Decodifica a PCM sin comprimir. El paso previo a cualquier filtrado serio.
ffmpeg -i entrada.mp3 salida.wav
# WAV a FLAC. Compresión SIN pérdida, ideal para archivar sin renunciar a nada.
ffmpeg -i entrada.wav -c:a flac salida.flac
# FLAC a OGG Vorbis. Con pérdida, pero -q:a controla la calidad variable (VBR).
ffmpeg -i entrada.flac -c:a libvorbis -q:a 4 salida.ogg
# WAV a MP3 en VBR de calidad alta. -q:a 0 es el -V0 de LAME, mejor que 320k CBR.
ffmpeg -i entrada.wav -c:a libmp3lame -q:a 0 salida.mp3
# Cambiar SOLO el contenedor, sin recodificar. Rápido y sin otra generación de pérdida.
ffmpeg -i entrada.m4a -c copy salida.aac
Fíjate que en el primer comando no le digo nada del códec ni del contenedor. FFmpeg es suficientemente listo como para entender que una salida .wav quiere PCM. Solo tengo que intervenir cuando quiero controlar la calidad o cuando quiero copiar sin recodificar. Y aquí te doy un aviso que le he repetido a más de uno, convertir un MP3 a 320 kbps no mejora absolutamente nada. Recodificar con pérdida sobre pérdida añade una segunda generación de artefactos y solo engorda el archivo. Si quieres máxima calidad y partes de un archivo ya comprimido con pérdida, lo honesto es -c copy para copiar el stream sin tocarlo, o volver a empezar desde un archivo sin pérdida.
Esta tabla te la dejo para tener delante, porque decidir el formato según para qué lo quieres es la mitad del trabajo,
| Formato | Tipo | Uso recomendado | Notas |
|---|---|---|---|
| WAV (PCM s16le) | Sin comprimir | Trabajo intermedio, entrada de Whisper | Enorme, unos 10 MB por minuto a 44,1 kHz estéreo |
| FLAC | Sin pérdida | Archivo maestro, biblioteca musical | Suele quedarse en un 50 o 60 % del WAV |
MP3 (libmp3lame) | Con pérdida | Compatibilidad universal | -q:a 0 en VBR rinde mejor que -b:a 320k |
OGG Vorbis (libvorbis) | Con pérdida | Alternativa libre al MP3 | -q:a 4 ronda los 128 kbps |
Opus (libopus) | Con pérdida | Voz, streaming, TTS | La mejor relación calidad/bitrate en voz, 64 a 96 kbps |
Y el aviso para los que vais a pasar el audio a un modelo, si vas a filtrar o a transcribir, no te quedes en MP3. Cada decodificación con pérdida degrada lo que el modelo va a analizar. Conviértelo a WAV una vez y trabaja ahí hasta el final.
El meollo, el formato exacto que espera Whisper
Llegamos al corazón del episodio. Whisper se entrenó con unas 680.000 horas de audio multilingüe, pero no con el audio que tú y yo grabamos. Se entrenó con audio de internet decodificado y normalizado a 16 kHz mono. Esa es la clave de todo. Si le das una entrada distinta a esa, el modelo entiende algo, pero le estás poniendo piedras en el camino.
Y hay un detalle que a mí me parece precioso. Whisper llama a FFmpeg por dentro. La función load_audio() de su código no sabe abrir un MP3, así que lanza FFmpeg como subproceso y le pide PCM crudo por tubería, con exactamente estos parámetros, 16 kHz, mono y pcm_s16le. O sea que el formato que espera Whisper es literalmente el que produce FFmpeg, no es una licencia poética. Por eso tiene tanto sentido prepararlo tú con el mismo binario.
La versión más simple del comando, la que convierte cualquier entrada al formato exacto que Whisper quiere, es esta,
# Convierte CUALQUIER entrada al formato exacto de Whisper: 16 kHz, mono, PCM s16le.
# -vn descarta el vídeo si la entrada es un mp4 o un mkv.
ffmpeg -i entrada_cualquiera.mp3 \
-vn \
-ar 16000 \
-ac 1 \
-c:a pcm_s16le \
salida_whisper.wav
Los tres parámetros que importan son -ar 16000, que fija la frecuencia de muestreo en 16 kHz, -ac 1, que hace el downmix a mono, y -c:a pcm_s16le, que deja el audio en PCM con signo de 16 bits little-endian, sin compresión. Y como el FFmpeg de mi build trae libsoxr, el remuestreo que hace por dentro es de los buenos.
¿Y por qué insisto tanto con esto? Porque de aquí salen las alucinaciones de Whisper. Esas veces en las que le pasas un audio y te devuelve frases que nadie ha dicho, palabras inventadas, cosas repetidas en bucle o directamente texto en otro idioma. Mira, el problema casi nunca es el modelo. El problema es cómo le damos la entrada. Y esto mismo te pasa con los prompts de texto, que dependiendo de cómo se lo pidas, el resultado es completamente distinto. Pues con el audio, igual.
El motivo técnico no tiene nada de místico. Whisper trabaja a ventanas de 30 segundos y de forma autorregresiva, y va prediciendo qué debería venir después. Si en una ventana solo hay silencio, el decodificador tiene que decidir qué poner, y lo que pone son frases plausibles del corpus con el que se entrenó. Ahí nacen los Subtítulos realizados por la comunidad y las repeticiones en bucle. Añade estéreo con voces distintas por canal, y el downmix a mono suma canales que mezclan una voz con un ruido que no existía junto a ella. Añade un volumen que sube y baja, y le das un rango dinámico que nunca vio. Añade ruido de banda ancha, un ventilador o un aire acondicionado, y el espectro queda contaminado en todas las bandas.
Si usas faster-whisper en vez del whisper original, tienes una red de seguridad muy interesante. faster-whisper trae Silero VAD integrado, un detector de actividad de voz que por defecto elimina los silencios de más de dos segundos. Se activa con vad_filter=True y, de paso, ese paquete no necesita FFmpeg instalado, porque decodifica con PyAV, que lleva las bibliotecas de FFmpeg dentro del propio paquete de Python. Mismo motor, otro envoltorio.
Y un aviso sobre el modelo por defecto del CLI, porque mucha gente lo pisa. Desde hace tiempo, el modelo por defecto del comando whisper es turbo, no large. turbo es una versión optimizada de large-v3 con una degradación mínima de precisión, y va unas ocho veces más rápido. La trampa es que turbo no está entrenado para traducir, así que si le pides --task translate te devuelve el idioma original y tú te quedas pensando qué ha pasado. Para traducir de verdad, tira de medium o de large.
La función whisperize
Escribir ese comando cada vez que quieres transcribir algo es una tortura, así que lo suyo es envolverlo en algo más cómodo. Y aquí conviene pararse, porque el primer instinto de casi todo el mundo, el mío incluido, es hacerse un alias. Pues resulta que un alias no te sirve para esto, y es un error clásico que se comete mucho. En bash y en zsh un alias no es más que una sustitución de texto y no recibe argumentos, así que dentro de él $1 se expande vacío, el comando se queda sin fichero de entrada y ffmpeg aborta con un No such file or directory. Lo que sí recibe argumentos es una función, así que a mí me gusta llamarla whisperize y la puedes meter en tu .bashrc o en tu .zshrc, con el shell que uses,
# Función para convertir cualquier audio al formato que espera Whisper
# Uso: whisperize grabacion.mp3 -> grabacion_whisper.wav
whisperize() {
ffmpeg -i "$1" -ar 16000 -ac 1 -c:a pcm_s16le "${1%.*}_whisper.wav"
}
Y a partir de ahí, whisperize grabacion.mp3 te deja un grabacion_whisper.wav listo para transcribir. Es de esas cosas tontas que te ahorran media vida, porque dejas de pensar en el formato y piensas en el resultado.
Si prefieres algo más elaborado que una función suelta, y a mí me gusta tenerlo, en el gist del episodio te dejo un script whisperize.sh autónomo que hace lo mismo, pero además comprueba que FFmpeg esté instalado, te muestra la información del audio de entrada con ffprobe, aplica la normalización y te resume al final qué formato tiene la salida. Lo tienes enlazado al final del artículo.
Limpiar el audio antes de que llegue a Whisper
El formato es la mitad del trabajo. La otra mitad es la limpieza. Y aquí conviene pararse, porque hay dos filtros que casi todo el mundo usa mal, y yo el primero hasta que me puse a medirlo.
El primero es loudnorm, el normalizador de volumen. La idea es que tu audio quede a un nivel percibido constante, y loudnorm lo hace siguiendo el estándar EBU R 128. Aquí está el detalle que se cuenta mal por ahí, loudnorm por defecto normaliza a -24 LUFS, que es el valor por defecto de FFmpeg, mientras que el estándar EBU R 128 prescribe -23 LUFS. Son dos números distintos y no es lo mismo decir uno que otro. Si quieres cumplir R 128 de verdad, sé explícito,
# Normalización con objetivo EBU R 128 explícito: -23 LUFS, true peak -1 dBTP
ffmpeg -i entrada.wav -af "loudnorm=i=-23:tp=-1:lra=7" salida.wav
# Para alimentar a Whisper, con que el nivel sea consistente ya vale.
# Aquí uso -23 porque es el estándar y así lo dejo claro en el comando.
ffmpeg -i entrada.wav -af "loudnorm=i=-23" salida.wav
Para el caso de Whisper, el valor exacto da un poco igual, lo que importa es que el nivel sea consistente. Pero me parece importante que sepas qué número estás usando y por qué, porque si no, acabas copiando comandos de internet sin entenderlos.
Hay otra cosa de loudnorm que merece dos líneas, y es que tiene dos modos. En una sola pasada trabaja en modo dinámico, moviendo la ganancia fotograma a fotograma y con un limitador. En dos pasadas trabaja en modo lineal, aplicando una ganancia constante calculada a partir de una medición previa, y no toca la dinámica. Para preparar material que luego vas a filtrar, el modo lineal es el correcto. Si necesitas esa precisión, el flujo es medir primero y aplicar después,
# Paso 1: medir y volcar los valores medidos a un JSON
ffmpeg -i entrada.wav -af loudnorm=print_format=json -f null -
# Paso 2: aplicar los valores medidos, normalización lineal
ffmpeg -i entrada.wav \
-af "loudnorm=i=-23:tp=-2:lra=7:measured_I=-22.5:measured_LRA=5.0:measured_TP=-1.5:measured_thresh=-32.0:offset=0.0" \
salida.wav
El segundo filtro que se usa mal es silenceremove, y este es el error que más me fastidia porque afecta directamente a las alucinaciones. Todo el mundo repite el comando silenceremove=1:0:-50dB pensando que quita los silencios de más de un segundo. Y no. Ese comando quita solo el silencio del principio del audio, y el 1 no significa un segundo, significa el número de periodos de no-silencio al arranque. Lo medí con tres audios de prueba y el resultado cantó. Un audio con silencio al principio sí se recorta, pero uno con un silencio en medio se deja intacto. Y ese silencio interno es justo el que hace que Whisper se ponga a inventar.
Para atacar los silencios internos hay que usar stop_periods con valor negativo, que es lo que reinicia el proceso en medio del audio, junto con stop_duration y stop_threshold. Esta es la sintaxis correcta y funcional,
# SOLO quita el silencio del PRINCIPIO (umbral -50 dB). No toca los internos.
ffmpeg -i entrada.wav -af "silenceremove=1:0:-50dB" salida.wav
# CORRECTO: quita TODOS los silencios internos de más de 1 segundo
ffmpeg -i entrada.wav \
-af "silenceremove=stop_periods=-1:stop_duration=1:stop_threshold=-50dB" \
salida.wav
Te lo resumo en una tabla para que no haya dudas,
| Comando | Silencio al inicio | Silencio interno | Silencio al final |
|---|---|---|---|
silenceremove=1:0:-50dB | ✅ lo quita | ❌ no lo toca | ❌ no lo toca |
silenceremove=stop_periods=-1:stop_duration=1:stop_threshold=-50dB | ✅ lo quita | ✅ lo quita | ✅ lo quita |
Y el comando definitivo para dejar el audio listo para Whisper, con limpieza y todo, es este. Fíjate en el orden de los filtros, que es donde está la miga,
# Preprocesado completo: recorta el espectro fuera de la voz, limpia el ruido,
# elimina los silencios largos y normaliza el volumen. El orden importa.
ffmpeg -i entrada.mp3 \
-ar 16000 -ac 1 -c:a pcm_s16le \
-af "highpass=f=80,lowpass=f=8000,afftdn=nr=12:nf=-50,silenceremove=stop_periods=-1:stop_duration=1:stop_threshold=-50dB,loudnorm=i=-23" \
salida_whisper_limpia.wav
Si tienes prisa y solo quieres un filtro, quédate con loudnorm. Pero si de verdad quieres que Whisper acierte, la cadena completa es la que marca la diferencia. Y para verificar que el resultado es lo que crees, nada de fiarse a ojo,
# Muestra sample rate, canales y códec. Sin esto, vas a ciegas.
ffprobe -v error -show_entries stream=sample_rate,channels,codec_name \
-of default=noprint_wrappers=1 salida_whisper_limpia.wav
Cortar y unir sin salir de la terminal
Si haces un podcast, o si montas respuestas de voz con la IA, tarde o temprano tienes que cortar y pegar audio. Y para eso FFmpeg es un editor minimalista que funciona en una línea.
Cortar se hace con -ss, que es el punto de inicio, y con -t, que es la duración, o con -to, que es el punto final. Así,
# Del minuto 5, 30 segundos de duración.
ffmpeg -i grabacion.mp3 -ss 00:05:00 -t 00:00:30 fragmento.wav
# Lo mismo, pero indicando el punto final exacto en vez de la duración.
ffmpeg -i grabacion.mp3 -ss 00:05:00 -to 00:05:30 fragmento.wav
Y aquí viene el truco que más me gusta y la trampa que casi nadie te cuenta, y van juntos. Cuando pones -ss antes de -i, FFmpeg salta al punto sin decodificar todo lo anterior, así que el corte es muchísimo más rápido. Es el input seeking, y con archivos largos es una barbaridad lo que se nota. El precio es que la precisión puede resentirse, aunque en la práctica, para lo que hacemos, es más que suficiente,
# Seek RÁPIDO: -ss antes de -i. FFmpeg salta sin decodificar todo lo previo.
ffmpeg -ss 00:05:00 -i grabacion.mp3 -t 00:00:30 fragmento.wav
Pues ahí está la trampa. Si combinas el truco del -ss antes de -i con un -to después de -i, el resultado no es el que esperas, porque -to cambia de significado según dónde lo pongas. Con -ss después de -i, -to es un valor absoluto del archivo original. Con -ss antes de -i, FFmpeg reinicia los timestamps y -to pasa a ser relativo al punto del salto. Lo medí con un archivo de 60 segundos y esto es lo que sale,
| Comando | Duración obtenida | Comentario |
|---|---|---|
ffmpeg -i in.wav -ss 5 -to 20 out.wav | 15 s | Lo esperado, -to es absoluto |
ffmpeg -ss 5 -i in.wav -to 20 out.wav | 20 s | Ojo, -to es relativo al salto |
ffmpeg -i in.wav -ss 5 -t 15 out.wav | 15 s | -t siempre es duración |
ffmpeg -ss 5 -t 15 -i in.wav out.wav | 15 s | También correcto |
La regla práctica es sencilla. Si vas a mezclar el seek rápido con un punto final, o pones las dos opciones antes de -i, o usas -t, que siempre es duración y no tiene este problema. Yo, por costumbre, tiro de -t casi siempre.
Unir audio tiene tres caminos, y elegir bien te ahorra un dolor de cabeza,
# Opción A: demuxer concat con lista de archivos. La más fiable sin recodificar.
# Requiere el mismo códec, sample rate y canales.
printf "file 'parte1.mp3'\nfile 'parte2.mp3'\nfile 'parte3.mp3'\n" > lista.txt
ffmpeg -f concat -safe 0 -i lista.txt -c copy union.mp3
# Opción B: sintaxis inline del protocolo concat. Cómoda para tres fragmentos.
ffmpeg -i "concat:parte1.mp3|parte2.mp3|parte3.mp3" -c copy todas.mp3
# Opción C: filtro concat. Recodifica, así que SÍ admite formatos mixtos.
ffmpeg -i parte1.wav -i parte2.mp3 -i parte3.ogg \
-filter_complex "[0:a][1:a][2:a]concat=n=3:v=0:a=1[out]" \
-map "[out]" union.wav
La clave es que las dos primeras opciones no recodifican, así que son rapidísimas, pero exigen que todos los fragmentos compartan códec, frecuencia y canales. Si mezclas un MP3 con un WAV, o el resultado es basura o te da error. Cuando los formatos son distintos, no te queda otra que recodificar con el filtro concat de la opción C.
Y para darle un acabado profesional, dos filtros más. afade para los fundidos de entrada y salida, y acrossfade para cruzar dos pistas y montar una cortinilla de las de verdad,
# Fade de entrada de 3 segundos y de salida, que arranca en el segundo 20
ffmpeg -i entrada.mp3 -af "afade=t=in:st=0:d=3,afade=t=out:st=20:d=3" salida.mp3
# Cortinilla real: cruzar la sintonía y la locución 2 segundos
ffmpeg -i cortinilla.mp3 -i locucion.mp3 \
-filter_complex "acrossfade=d=2" montaje.mp3
Con esto tienes el podcast montado, con su cortinilla al principio y al final, sin abrir un editor gráfico. Y lo mismo te sirve para encadenar la respuesta de la IA con una locución tuya, que es justo lo que hacíamos en el episodio en el que te enseñé a dialogar con la IA.
Postprocesar la voz del TTS, de radio AM a FM
FFmpeg no solo prepara lo que entra, también embellece lo que sale. Cuando un motor de texto a voz, ya sea Piper o Kokoro, convierte la respuesta del modelo en audio, el resultado tiene un problema muy concreto. Suena planísimo. Suena a robot. Y si me apuras un poco, a robot con resaca. El nivel es constante frase a frase pero cambia de una a otra, no hay dinámica natural, no hay aire, y el vocoder arrastra un rumor grave que no existe en la voz humana.
Tres filtros arreglan casi todo. loudnorm para igualar el volumen percibido, compand para devolver algo de dinámica, y highpass para quitar el retumbe de abajo. Empiezo por el que se usa mal en el guión del episodio, compand. Este filtro es un compresor de rango dinámico que trabaja con una curva de transferencia en decibelios, y la sintaxis tiene una trampa, dentro del parámetro points el separador entre puntos es |, nunca :. Si mezclas los dos, FFmpeg aborta con un Error parsing filterchain. Este es el comando correcto y funcional,
# Compresión suave de voz. Ojo: dentro de points el separador es |, no :
ffmpeg -i tts_raw.wav \
-af "compand=attacks=0.1:decays=0.2:points=-80/-80|-45/-45|-30/-30|-25/-20|-20/-15|-15/-12|-10/-6|-6/-3|0/-0.5" \
salida_comp.wav
La curva de points, que es lo que más asusta a primera vista, se lee así. Los valores muy bajos, hasta -30 dB, no se tocan, así que el ruido de fondo no se amplifica. Los sonidos medios-bajos, alrededor de -25 dB, suben unos 5 dB. Los medios suben 3 dB. Y los picos, al llegar a 0, se aplastan medio decibelio para dejar un margen de seguridad. En román paladino, los bajos no los toques, los medios súbelos un poco y los picos aplánalos suavemente.
Y este es el comando completo, el que convierte la salida del TTS en algo que apetece escuchar. El orden importa, compand actúa sobre el material limpio y loudnorm va al final,
# EL COMANDO COMPLETO: de TTS crudo a audio que no suena a radio AM.
ffmpeg -i tts_raw.wav \
-af "highpass=f=80,compand=attacks=0.1:decays=0.2:points=-80/-80|-45/-45|-30/-30|-25/-20|-20/-15|-15/-12|-10/-6|-6/-3|0/-0.5,loudnorm=i=-16:tp=-1.5:lra=11" \
tts_final.wav
Fíjate que aquí uso loudnorm=i=-16, que es un objetivo de podcast y streaming, más alto que el de emisión, porque para reproducir en tus altavoces queda mucho mejor. Y de propina, si el TTS genera respuestas largas con pausas artificiales entre frases, un silenceremove al principio de la cadena te quita ese aire que un humano no haría,
# Recorta las pausas largas que mete el TTS entre frases y normaliza después
ffmpeg -i tts_raw.wav \
-af "silenceremove=stop_periods=-1:stop_duration=0.4:stop_threshold=-50dB,loudnorm=i=-16" \
tts_compacta.wav
Una advertencia que te doy por experiencia, no te pases con la compresión. Bajar el umbral demasiado convierte la voz en algo fatigoso y bombeante. Empieza suave y sube solo si hace falta. Y no apiles compand y acompressor a la vez sin motivo, porque hacen lo mismo con filosofías distintas.
Cuando el audio viene muy sucio
Hay grabaciones que no salvan ni las buenas intenciones. Una entrevista en una cafetería, una nota de voz con el viento pegando, una charla grabada junto a un ventilador. Para esos casos tengo tres filtros que me han salvado más de una vez, y uno de ellos es mi favorito absoluto.
El primero es afftdn, la reducción de ruido por FFT. Analiza el audio y estima un perfil de ruido constante, así que se le da de maravilla todo lo que no cambia, un ventilador, un aire acondicionado, el tráfico de fondo. Con los valores por defecto ya funciona, pero puedes subir la agresividad,
# Reducción automática con los valores por defecto (12 dB, suelo a -50 dB)
ffmpeg -i grabacion_ruidosa.mp3 -af "afftdn" salida_limpia.wav
# Más agresivo: 24 dB de reducción y suelo de ruido a -35 dB
ffmpeg -i grabacion_ruidosa.mp3 -af "afftdn=nr=24:nf=-35" salida_limpia.wav
# Con seguimiento automático del suelo, útil si el ruido cambia con el tiempo
ffmpeg -i grabacion_ruidosa.mp3 -af "afftdn=tn=1" salida_limpia.wav
Un truco que me encanta de afftdn, si le pones om=noise te devuelve solo el ruido que ha quitado. Así puedes escuchar cuánto está trabajando el filtro y ajustar la reducción sin destrozar la voz.
El segundo es adeclick, que quita los chasquidos y los pops de las grabaciones. Y cuidado, no lo confundas con adeclip, que es su hermano y sirve para otra cosa. adeclick reconstruye muestras corruptas aisladas, clics sueltos. adeclip reconstruye tramos donde la señal se ha recortado contra el techo, o sea, cuando saturaste la interfaz de audio. Confundirlos es el error típico y no arreglan lo mismo.
El tercero es el equalizer, un ecualizador paramétrico con el que le devuelves la claridad a la voz. La voz humana vive entre los 300 y los 4000 Hz, aunque el rango completo con formantes llega más arriba, hasta 8 o 10 kHz. La banda de 1 a 3 kHz es la de presencia, la que hace que la voz se entienda. Un realce suave ahí hace maravillas,
# +5 dB centrados en 3 kHz, la banda de presencia de la voz
ffmpeg -i entrada.wav -af "equalizer=f=3000:t=q:w=1:g=5" salida_eq.wav
# Curva completa: quita graves embarrados, realza presencia, suaviza el siseo
ffmpeg -i entrada.wav \
-af "equalizer=f=120:t=q:w=1:g=-6,equalizer=f=300:t=q:w=1:g=-3,equalizer=f=1500:t=q:w=1:g=3,equalizer=f=3000:t=q:w=1:g=5,equalizer=f=7000:t=q:w=1:g=-3" \
salida_voz.wav
Con estos tres filtros, más el recorte de espectro y la normalización, una grabación hecha en una cafetería queda perfectamente usable para Whisper. Y todo, como siempre, en una sola línea,
# Grabación muy sucia -> audio limpio y listo para transcribir
ffmpeg -i grabacion_mala.mp3 \
-af "highpass=f=80,lowpass=f=8000,afftdn=nr=12:nf=-50,adeclick,equalizer=f=3000:t=q:w=1:g=5,silenceremove=stop_periods=-1:stop_duration=1:stop_threshold=-50dB,loudnorm=i=-23" \
salida_limpia_normalizada.wav
La regla de oro, limpiar antes de normalizar
Si te llevas una sola idea de este artículo, que sea esta, y me da igual que olvides todos los comandos de arriba. Limpia antes de normalizar. Nunca al revés. El orden correcto es highpass → lowpass → afftdn → silenceremove → loudnorm. Y el incorrecto es exactamente el mismo al revés.
La razón no tiene nada de magia. Un denoisador como afftdn estima un perfil de ruido para saber qué es señal y qué es ruido, y se apoya en los tramos sin voz para calibrar. Si normalizas primero, el loudnorm en modo dinámico te mueve la ganancia y te rellena las pausas con ruido amplificado, así que el filtro de ruido ya no tiene una referencia limpia que medir y su tracking se descuadra. En cristiano, normalizas primero y amplificas el ruido junto con la voz, y después cuesta mucho más quitarlo. Limpias primero y subes el volumen de lo que queda, que ya está limpio. La diferencia se oye, y mucho.
La pipeline completa de voz
Con todo lo que hemos visto, la pipeline completa de voz queda así,
Micrófono
│ arecord / pw-record / ffmpeg -f pulse
▼
FFmpeg (limpia y normaliza: highpass → lowpass → afftdn → loudnorm, 16 kHz mono)
▼
Whisper (speech to text: faster-whisper o whisper.cpp)
▼
LLM local (Ollama, llama.cpp...)
▼
TTS (Piper o Kokoro)
▼
FFmpeg (highpass → compand → loudnorm)
▼
aplay / pw-play
Y grabar según el stack de audio que tengas montado es igual de fácil,
# PipeWire o PulseAudio: FFmpeg habla con Pulse directamente
ffmpeg -f pulse -i default -ac 1 -ar 16000 -c:a pcm_s16le grabacion.wav
# ALSA directo, sin servidor de sonido
arecord -f S16_LE -r 16000 -c 1 -t wav grabacion.wav
# PipeWire nativo
pw-record --rate 16000 --channels 1 grabacion.wav
Fíjate en lo bonito del asunto. Cada pieza del pipeline suena bien porque nos hemos preocupado de que FFmpeg haga su trabajo en los dos extremos. En un extremo para que el modelo de lenguaje entienda el audio perfectamente, y en el otro para que lo que sale de nuestro modelo suene humano. Y esto sin que ninguno de los dos haya necesitado un modelo de lenguaje por su parte. FFmpeg no sabe nada de IA y aun así es imprescindible para ella.
Como esto son muchas piezas y muchos comandos, he dejado los scripts en un gist para que los puedas copiar y pegar sin pelearte con el formato. Ahí tienes el script whisperize.sh autónomo, las dos cadenas de filtros en archivos de texto para reutilizarlas, y el script completo de la pipeline en Python que une el micrófono, FFmpeg, Whisper, Ollama, Piper y la reproducción. Lo tienes enlazado en la sección de más información.
Lo que viene en la serie
Hoy hemos puesto los cimientos de los sentidos de la IA. Hemos preparado el audio para que la IA lo pueda oír y lo hemos embellecido para que la IA pueda hablar. Y te avanzo lo que viene, porque va a ser entretenido. En el próximo episodio le toca el turno a Whisper, el oído propiamente dicho, con todo el detalle de los modelos, los idiomas y las trampas que tiene. Después vendrá el texto a voz con Piper y Kokoro, la voz de tu IA. Y más adelante la visión artificial, la vista, con modelos capaces de mirar una imagen y contarte lo que hay en ella.
Ah, y un apunte que se me quedó en el tintero. También habrá un episodio sobre OCR, porque están saliendo modelos que reconocen texto en imágenes de una manera impresionante, y alguno de los recientes es brutal. Con lo que se viene, a la IA le van a sobrar sentidos.
Y como conclusión, me quedo con tres frases que te van a quedar claras. Sin FFmpeg, Whisper es muy probable que falle o que no te dé el resultado que esperas. Sin FFmpeg, tu motor de texto a voz va a sonar a lata. Y sin FFmpeg, la pipeline de voz sencillamente no existe. Hoy hemos dejado la base lista, y a partir de ahora toca seguir construyendo encima.
Y poco más que contarte. Si te ha gustado el episodio y lo has disfrutado tanto como yo al grabarlo, me alegro un montón. Recuerda que este es un podcast de la red de Podcast de Sospechosos Habituales, donde puedes encontrar fantásticos y maravillosos podcasts, y puedes participar en esa red entrando en su web. De la misma manera, si quieres participar en el grupo de atareao con Linux, simplemente busca ese nombre y allí nos vas a encontrar hablando de modelos de lenguaje, de IA, de teclados y de cualquier cosa relacionada con Linux y con el mundo del open source. Recuerda que la vida son dos días y uno ya ha pasado, así que disfruta como si no hubiera mañana y, si puede ser con Linux y en este caso con FFmpeg, mejor que mejor. Un saludo y nos escuchamos el próximo lunes. 🐧
Más información
- FFmpeg — documentación oficial — La referencia completa de la herramienta, con manuales de
ffmpeg,ffprobeyffplay - FFmpeg — descargas y versiones — Aquí se confirma la rama estable actual, la 9.0.2 Lei de septiembre de 2026
- FFmpeg — licencia y consideraciones legales — LGPL 2.1+ y las partes opcionales bajo GPL 2+
- FFmpeg — documentación de filtros de audio — Todo el catálogo de filtros, incluidos
loudnorm,afftdn,compandysilenceremove - Arch Wiki — FFmpeg — Instalación, el paquete
ffmpeg-full, el filtrowhispery la normalización de volumen - OpenAI Whisper — repositorio oficial — Modelos, el modelo
turbopor defecto y la licencia MIT - Whisper — código de carga de audio — Donde se ve que
Whisperllama aFFmpegy qué formato exacto le pide - Paper de Whisper — Robust Speech Recognition via Large-Scale Weak Supervision — Las 680.000 horas de entrenamiento y el detalle de los hiperparámetros
- faster-whisper — repositorio — La reimplementación con CTranslate2, Silero VAD incluido y sin dependencia de
FFmpeg - EBU R 128 — estándar de loudness — El documento que prescribe los -23 LUFS de la normalización de emisión
- Piper TTS — OHF-Voice — El motor de texto a voz local, con sus voces en español
- Kokoro-82M — model card — El TTS local de 82 millones de parámetros, nativo a 24 kHz
- ffmpeg-normalize — repositorio — El envoltorio de
loudnormen dos pasadas, con presets y soporte para lotes de archivos - Gist — Scripts del episodio 842 —
whisperize.sh, las cadenas de filtros y la pipeline completa en Python - Episodio 840 — sqlite-utils, tu navaja suiza para SQLite — El episodio anterior, cerrando la parte de memoria de tu IA
- Episodio 839 — Convierte 6000 notas en un cerebro conectado — Cómo le dimos a la IA la memoria que hoy le complementamos con sentidos
- Episodio 837 — Conecta tus notas con IA, el fin de las búsquedas que fallan — Base de la serie del cerebro digital
- Episodio — OCR inteligente con IA en Linux — El sentido de la vista aplicado al texto, que retomaremos más adelante
- Episodio 809 — Un RAG casero con Ollama y tus notas — Los primeros pasos de la IA local que hoy usa esta pipeline de voz