Codifica audio WAV como MP3 en Python con Pydub y FFmpeg
Convierte una grabación o un lote de archivos WAV a MP3 con un script de Python que informa qué archivos se convirtieron correctamente y devuelve un estado de salida de error si alguna conversión falla. El siguiente ejemplo usa Pydub y FFmpeg, no modifica los archivos de origen y no permite sobrescribir los archivos de salida existentes.
Este tutorial usa Bash en Linux y grabaciones WAV locales mono o estéreo. Cada archivo de salida es un MP3 de 192 kbps a 44,1 kHz, con el mismo número de canales que su origen. Pydub mantiene el audio decodificado en memoria, así que procesa los archivos de forma secuencial y usa grabaciones que quepan en la RAM. Este es un script de conversión local, no un servicio de subida ni un codificador de streaming.
Prepara tu entorno
El ejemplo se probó con Python 3.14.7, Pydub 0.25.1, audioop-lts 0.2.2 y FFmpeg 9.0.1 en Linux. Los comandos usan un entorno virtual de forma explícita, por lo que no necesitas activarlo.
Instala FFmpeg
Instala Python con soporte para entornos virtuales, FFmpeg y ffprobe. En Ubuntu:
sudo apt-get update &&
sudo apt-get install -y python3 python3-venv ffmpeg
Otras distribuciones de Linux ofrecen sus propios paquetes; la
página de descargas de FFmpeg enlaza a paquetes para distintas
distribuciones. Comprueba que tu compilación incluya
el codificador libmp3lame:
ffmpeg -version &&
ffprobe -version &&
ffmpeg -hide_banner -encoders
Busca libmp3lame en la lista de codificadores. Instalar Pydub no instala FFmpeg.
Instala Pydub
Crea un directorio nuevo desde su directorio padre. Si audio-encoding ya existe,
elige otro nombre; la cadena && detiene la preparación antes de que
pueda instalar paquetes en un proyecto existente.
mkdir audio-encoding &&
cd audio-encoding &&
python3 -m venv .venv &&
.venv/bin/python -m pip install 'pydub==0.25.1' 'audioop-lts==0.2.2; python_version >= "3.13"'
Mantén esta sesión de shell en audio-encoding para los comandos restantes. Python
eliminó audioop en 3.13, pero Pydub 0.25.1
sigue importándolo. La dependencia condicional instala
audioop-lts en Python 3.13 y versiones
posteriores; Python 3.12 usa el módulo de su biblioteca estándar. No se necesita ningún paquete de
reproducción de audio.
Guarda el conversor
Guarda el script completo que aparece a continuación como encode_audio.py. WAV es
un contenedor: sus muestras pueden ser valores enteros o de punto flotante. El script primero pide
a FFmpeg que decodifique a PCM de 16 bits con signo y luego entrega esa representación conocida a
Pydub. Así se evita depender de la
ruta rápida para WAV de Pydub 0.25.1,
que no distingue todos los tipos de muestras WAV extensibles. Las profundidades de bits superiores
se reducen a 16 bits en este flujo de trabajo con MP3; conserva el WAV de origen como archivo maestro.
El MP3 se codifica en un directorio temporal junto a su destino. Solo se publica una codificación completada, mediante un enlace duro que falla si el destino ya existe. Usa un sistema de archivos local que admita enlaces duros. Los archivos de las conversiones correctas se conservan cuando falla otra entrada.
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import sys
from tempfile import TemporaryDirectory
from pydub import AudioSegment
from pydub.exceptions import CouldntDecodeError, CouldntEncodeError
def convert_one(source, output_dir, ffmpeg, fade_ms):
if source.suffix.lower() != '.wav' or not source.is_file():
raise ValueError('Expected an existing WAV file')
destination = output_dir / (source.stem + '.mp3')
if os.path.lexists(destination):
raise FileExistsError(f'Output already exists: {destination}')
with TemporaryDirectory(prefix='.encode-', dir=output_dir) as work:
decoded = Path(work) / 'decoded.wav'
encoded = Path(work) / 'encoded.mp3'
subprocess.run(
[ffmpeg, '-nostdin', '-hide_banner', '-v', 'error', '-xerror',
'-f', 'wav', '-i', str(source.resolve(strict=True)),
'-map', '0:a:0', '-c:a', 'pcm_s16le', '-ar', '44100', str(decoded)],
check=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
)
audio = AudioSegment.from_wav(decoded)
if audio.channels not in (1, 2) or len(audio) == 0:
raise ValueError('Expected nonempty mono or stereo audio')
if fade_ms > len(audio):
raise ValueError('The fade must not exceed the recording length')
if fade_ms:
audio = audio.fade_in(fade_ms).fade_out(fade_ms)
audio.export(encoded, format='mp3', codec='libmp3lame', bitrate='192k').close()
os.link(encoded, destination)
return destination
def main():
parser = argparse.ArgumentParser(description='Convert WAV recordings to MP3')
parser.add_argument('--out-dir', type=Path, required=True)
parser.add_argument('--fade-ms', type=int, default=0)
parser.add_argument('inputs', type=Path, nargs='+')
args = parser.parse_args()
if args.fade_ms < 0:
parser.error('--fade-ms must be zero or greater')
ffmpeg = shutil.which('ffmpeg')
if ffmpeg is None:
parser.error('ffmpeg is missing from PATH')
AudioSegment.converter = ffmpeg
try:
output_dir = args.out_dir.resolve()
output_dir.mkdir(parents=True, exist_ok=True)
except OSError as error:
parser.exit(2, f'Cannot prepare output directory: {error}\n')
failed = 0
for source in args.inputs:
try:
destination = convert_one(source, output_dir, ffmpeg, args.fade_ms)
except (OSError, ValueError, subprocess.CalledProcessError,
CouldntDecodeError, CouldntEncodeError) as error:
failed += 1
print(f'FAILED {source}: {error}', file=sys.stderr)
else:
print(f'OK {source} -> {destination}')
print(f'{len(args.inputs) - failed} converted, {failed} failed')
return 1 if failed else 0
if __name__ == '__main__':
sys.exit(main())
La opción -xerror de FFmpeg hace que los
errores de decodificación notificados provoquen el fallo de la conversión. Esto detecta entradas
dañadas que FFmpeg podría recuperar parcialmente de otro modo. No demuestra que cada grabación
esté completa: escucha los archivos de salida importantes y compara su duración con la duración
esperada de la grabación.
Convierte una grabación
Usa tu propio input.wav o crea un tono de prueba de tres segundos. La opción
-n impide sobrescribir un archivo existente:
ffmpeg -nostdin -hide_banner -v error -n -f lavfi \
-i 'sine=frequency=440:sample_rate=44100:duration=3' -c:a pcm_s16le input.wav
Ejecuta el conversor desde audio-encoding:
.venv/bin/python encode_audio.py --out-dir mp3 -- input.wav
Imprime una línea OK y 1 converted, 0 failed, y luego termina
con el estado 0. El archivo es mp3/input.mp3.
Si ejecutas el comando de nuevo, informa de un fallo por salida existente, termina con el estado
1 y conserva los bytes del primer MP3. Elige otro directorio de salida
para una nueva codificación.
Procesamiento de archivos de audio por lotes
Crea un directorio wav_files y coloca allí tus grabaciones WAV; luego pásalas
al mismo script:
.venv/bin/python encode_audio.py --out-dir mp3-batch -- wav_files/*.wav
Bash expande este patrón para los archivos que están directamente dentro del directorio, sin
recursividad. Coincide con los nombres .wav en minúsculas; pasa
explícitamente los archivos .WAV en mayúsculas. Si no hay coincidencias,
el patrón literal llega al script y produce un fallo por entrada inexistente. Pon entre comillas
las rutas individuales que contengan espacios. El separador -- también
permite nombres de entrada que empiecen con un guion.
Cada entrada recibe una línea OK o FAILED.
Un lote mixto conserva los MP3 válidos, continúa después de un archivo corrupto y termina con el
estado 1. Dos entradas como a/interview.wav y
b/interview.wav corresponden al mismo nombre de salida: la primera conversión
correcta lo ocupa y la segunda se notifica como un fallo. Por eso, volver a ejecutar un lote
completado también informa de fallos en lugar de omitir o reemplazar archivos sin avisar.
| Estado de salida | Significado |
|---|---|
0 | Se convirtieron todas las entradas solicitadas. |
1 | Falló al menos una entrada, incluido el caso de un destino existente. |
2 | Error de argumentos o de preparación, como un ejecutable de FFmpeg ausente o un directorio de salida inutilizable. |
Añade fundidos de forma deliberada
Para suavizar los primeros y los últimos 100 milisegundos, usa un directorio de salida nuevo:
.venv/bin/python encode_audio.py --out-dir mp3-faded --fade-ms 100 -- input.wav
Sin esta opción, el script no aplica fundidos, ganancia, normalización ni compresión de rango dinámico. Una tasa de bits de MP3 más alta no restaura la información que falta en el origen. Escucha antes de decidir si debes modificar los niveles; la normalización de picos seguida de un aumento de volumen puede causar recorte de audio. Consulta la API de Pydub para las operaciones de ganancia y fundido.
Comprueba la salida
Inspecciona el primer MP3 y decodifícalo de principio a fin:
ffprobe -v error -select_streams a:0 \
-show_entries stream=codec_name,sample_rate,channels,bit_rate:format=duration \
-of json mp3/input.mp3 &&
ffmpeg -nostdin -v error -xerror -i mp3/input.mp3 -f null -
Para el tono de prueba, el valor de codec_name debe ser
mp3, el de sample_rate debe ser
44100, debe haber un canal y la duración debe rondar los tres segundos.
El relleno de las tramas MP3 puede hacer que la duración indicada sea ligeramente mayor.
El comando de decodificación debería finalizar correctamente, sin mensajes de error. Los metadatos
y la capacidad de decodificar son comprobaciones útiles, pero ninguna determina la calidad de audio
percibida.
Solución de problemas comunes
No module named 'audioop'o'pyaudioop': vuelve a ejecutar la instalación de paquetes con versiones fijadas usando.venv/bin/python -m pipdentro del proyecto, incluida la dependencia condicionalaudioop-lts.- Falta FFmpeg o
Unknown encoder 'libmp3lame': comprueba el ejecutable y la lista de codificadores de la preparación. El paquete de Python y el codificador del sistema se instalan por separado. - Entrada corrupta o inexistente: lee la línea
FAILEDy cualquier diagnóstico previo de FFmpeg. Una decodificación o codificación fallida no publica un MP3 nuevo; un destino que ya existía permanece intacto. - Error de enlace duro o de permisos: usa un directorio de salida local con permisos de escritura y soporte para enlaces duros. El script elimina su directorio temporal ante fallos habituales y nunca reemplaza un destino para sortear un error del sistema de archivos.
Conserva las grabaciones originales y usa un directorio de salida nuevo cuando cambies los ajustes de codificación. Si necesitas trasladar la conversión fuera de tu proceso local de Python, explora nuestro servicio de encoding de audio.
