Repite audio en Python con Pydub: copias y fundidos cruzados
Usa repeticiones directas cuando necesites copias exactas de un clip, o superpón su final y su inicio con un fundido cruzado para suavizar una unión abrupta. Esta guía crea un archivo WAV de duración finita con Pydub, informa de los fallos mediante su estado de salida y se niega a reemplazar una salida existente. Un fundido cruzado puede reducir un clic; no garantiza un bucle musicalmente continuo.
Requisitos previos
Los comandos usan Bash en Linux, Python con soporte para entornos virtuales, FFmpeg y ffprobe. La guía se ha probado con Python 3.12.13 y 3.14.7, Pydub 0.25.1, y FFmpeg 7.1.5 y 9.0.1. Instala Python y las herramientas nativas a través de tu distribución; la página de descargas de FFmpeg incluye enlaces a paquetes. Comprueba los ejecutables:
python3 --version && ffmpeg -version && ffprobe -version
Usa una entrada WAV PCM sin compresión, de 16 bits con signo, little-endian y con uno o dos canales. El script conserva su frecuencia de muestreo y sus canales, y rechaza otras codificaciones de muestras en lugar de interpretar sus bytes como enteros de 16 bits. Construye el resultado completo en memoria, así que empieza con clips cortos.
Configura las herramientas esenciales
Abre una terminal Bash en un nuevo directorio de trabajo vacío. Instala los paquetes con las
versiones fijadas en un entorno virtual nuevo; la primera comprobación impide reutilizar un
.venv existente:
test ! -e .venv && test ! -L .venv &&
python3 -m venv .venv &&
.venv/bin/python -m pip install 'pydub==0.25.1' 'audioop-lts==0.2.2; python_version >= "3.13"'
Python eliminó audioop en la versión 3.13. Pydub 0.25.1 lo usa,
por lo que la dependencia condicional proporciona
audioop-lts en las versiones más recientes de
Python. Estos comandos usan el entorno virtual directamente; no es necesario activarlo. Si la
instalación falla, la terminal permanece en tu directorio de trabajo; elige un directorio nuevo
antes de volver a intentar la configuración.
Repetición básica con Pydub
Guarda este programa completo como loop_audio.py. El
lector wave de la biblioteca estándar
valida el contenedor PCM, y el script comprueba que estén presentes todos los cuadros declarados
antes de construir un segmento de Pydub. Rechaza archivos vacíos, cantidades de repeticiones
no positivas, fundidos cruzados de duración negativa y fundidos cruzados tan largos como el clip.
La salida se prepara junto a su destino y se publica mediante un enlace duro. Usa un sistema de archivos local con permisos de escritura que admita enlaces duros, y un directorio de salida existente. Un fallo ordinario elimina el directorio de preparación; si el destino ya existe, se conserva.
import argparse
import os
from pathlib import Path
import sys
from tempfile import TemporaryDirectory
import wave
from pydub import AudioSegment
def read_pcm(source):
with wave.open(str(source), 'rb') as recording:
channels = recording.getnchannels()
rate = recording.getframerate()
frames = recording.getnframes()
if (recording.getcomptype() != 'NONE'
or recording.getsampwidth() != 2 or channels not in (1, 2)):
raise ValueError('Expected mono or stereo signed 16-bit PCM WAV')
data = recording.readframes(frames)
if frames == 0 or len(data) != frames * channels * 2:
raise ValueError('Empty or truncated PCM data')
return AudioSegment(data=data, sample_width=2, frame_rate=rate, channels=channels)
def main():
parser = argparse.ArgumentParser(description='Repeat a PCM WAV clip')
parser.add_argument('--repeats', type=int, default=3)
parser.add_argument('--crossfade-ms', type=int, default=0)
parser.add_argument('input', type=Path)
parser.add_argument('output', type=Path)
args = parser.parse_args()
if args.repeats < 1 or args.crossfade_ms < 0:
parser.error('--repeats must be positive; --crossfade-ms must be nonnegative')
try:
if args.output.suffix.lower() != '.wav':
raise ValueError('The output must have a .wav extension')
if os.path.lexists(args.output):
raise FileExistsError(f'Output already exists: {args.output}')
audio = read_pcm(args.input)
if args.crossfade_ms and args.crossfade_ms >= len(audio):
raise ValueError('The crossfade must be shorter than the clip')
if args.crossfade_ms == 0:
combined = audio * args.repeats
else:
combined = audio
for _ in range(args.repeats - 1):
combined = combined.append(audio, crossfade=args.crossfade_ms)
with TemporaryDirectory(prefix='.loop-', dir=args.output.parent) as work:
staged = Path(work) / 'loop.wav'
combined.export(staged, format='wav').close()
os.link(staged, args.output)
except (OSError, ValueError, wave.Error, EOFError) as error:
print(f'FAILED {args.input}: {error}', file=sys.stderr)
return 1
seconds = combined.frame_count() / combined.frame_rate
print(f'Wrote {args.output}: {seconds:.6f} seconds, {combined.channels} channel(s)')
return 0
if __name__ == '__main__':
sys.exit(main())
Usa tu propio input.wav compatible, o genera un tono de un segundo cuyos
extremos difieran intencionalmente. Las comprobaciones de existencia devuelven un estado de fallo
si el archivo de prueba ya existe; la opción -n de FFmpeg también
impide sobrescribirlo:
test ! -e input.wav && test ! -L input.wav &&
ffmpeg -nostdin -hide_banner -v error -n -f lavfi \
-i 'aevalsrc=0.25*sin(2*PI*440.5*t+PI/4):s=48000:d=1' -c:a pcm_s16le input.wav
Repítelo cinco veces:
.venv/bin/python loop_audio.py --repeats 5 -- input.wav repeated.wav
El resultado esperado es Wrote repeated.wav: 5.000000 seconds, 1 channel(s) y el estado 0.
La repetición directa conserva todos los cuadros de entrada, incluido el silencio, pero también
repite cualquier discontinuidad entre la última muestra y la primera. Si ejecutas el comando de
nuevo, devuelve el estado 1 y conserva repeated.wav.
Los argumentos no válidos devuelven el estado 2. Los errores de
entrada/salida, incluidos la ausencia de la entrada, el truncamiento detectado o las codificaciones
de muestras no admitidas, devuelven el estado 1. Comprobar los cuadros
declarados no permite determinar si una grabación estaba completa antes de guardarla o si se
alteraron valores de muestra válidos.
Aplica fundidos cruzados a las uniones internas
Elige un nombre de archivo de salida nuevo y una superposición de 25 ms:
.venv/bin/python loop_audio.py --repeats 5 --crossfade-ms 25 -- input.wav crossfaded.wav
El resultado esperado es Wrote crossfaded.wav: 4.900000 seconds, 1 channel(s). La
operación append de Pydub superpone el final
del audio acumulado con el inicio de la siguiente copia. Con N copias
de duración D y un fundido cruzado de duración
F, el resultado es aproximadamente N × D − (N − 1) × F.
Cinco copias de un segundo tienen cuatro uniones, por lo que pierden 100 ms en total. Pydub divide
el audio en milisegundos; los clips o las frecuencias de muestreo que no se alinean con esa
cuadrícula pueden introducir redondeos de muestras. La repetición directa usa los cuadros
originales sin esa división.
Comprueba los archivos WAV guardados
Inspecciona y decodifica por completo los dos archivos de salida con estos nombres exactos:
ffprobe -v error -select_streams a:0 \
-show_entries stream=codec_name,sample_rate,channels:format=duration -of json repeated.wav &&
ffmpeg -nostdin -v error -xerror -i repeated.wav -f null - &&
ffprobe -v error -select_streams a:0 \
-show_entries stream=codec_name,sample_rate,channels:format=duration -of json crossfaded.wav &&
ffmpeg -nostdin -v error -xerror -i crossfaded.wav -f null -
Para el tono generado, ambos archivos deberían indicar pcm_s16le,
48000 Hz y un canal. Sus duraciones son 5 segundos y 4,9 segundos,
respectivamente. Los comandos de decodificación deberían finalizar sin errores. Estas
comprobaciones permiten verificar el formato, la duración y la capacidad de decodificación;
escucha el audio en un reproductor para evaluar si las uniones son adecuadas para tu grabación.
Elige un límite adecuado para el bucle
Un cruce por cero puede reducir un salto de amplitud, pero dos cruces por cero pueden tener pendientes, fases o comportamientos de canal diferentes. La detección de pulsos sugiere puntos de edición rítmicos; no demuestra que las notas, el ambiente o la reverberación se unan de forma natural. Una entrada silenciosa sigue siendo silenciosa después de cualquiera de las dos operaciones, y un fundido cruzado puede difuminar un transitorio o alterar el ritmo al acortar cada repetición.
El script aplica fundidos solo a las uniones internas. No une el final del archivo guardado con su inicio, por lo que reproducir toda la salida indefinidamente aún puede producir clics. Escucha tanto las uniones internas como ese límite externo en el reproductor que vayas a usar. Para un bucle de un videojuego o de música de fondo, selecciona primero una región musicalmente compatible y comprueba ambos canales estéreo antes de elegir la superposición.
Repite archivos más largos con FFmpeg
Pydub mantiene en memoria la entrada y el resultado a medida que crece. Dividir en fragmentos un segmento completamente cargado no elimina esa asignación inicial de memoria. Si solo necesitas repetir archivos completos, FFmpeg puede procesarlos sin construir en Python todo el búfer PCM con las repeticiones:
test ! -e repeated-ffmpeg.wav && test ! -L repeated-ffmpeg.wav &&
ffmpeg -nostdin -hide_banner -v error -xerror -n -stream_loop 4 \
-i input.wav -map 0:a:0 -c:a pcm_s16le repeated-ffmpeg.wav
La opción de entrada -stream_loop cuenta las
reproducciones adicionales: 4 produce cinco copias, incluida la primera.
Este comando crea un WAV de cinco segundos a partir del tono, conserva su frecuencia de muestreo
y su número de canales, y no añade ningún fundido cruzado. Ejecuta estos comandos de forma
secuencial. Las comprobaciones de existencia hacen que una nueva ejecución falle antes de invocar
FFmpeg; -n por sí sola puede informar de una negativa a sobrescribir con
el estado 0 en FFmpeg 9.0.1. -xerror hace que los
errores de procesamiento notificados provoquen el fallo del comando. Un fallo después de iniciarse
la escritura puede dejar un archivo nuevo parcial; elimina esa salida fallida antes de volver a
intentarlo. Una decodificación correcta no demuestra que una grabación estuviera completa antes
de llegar a tu script.
Mantén WAV PCM como formato de salida de edición y evalúa las uniones antes de codificar para la entrega. La API de Pydub documenta otros fundidos y ajustes de ganancia por si la región que has elegido los necesita.
