Repetir áudio em Python com Pydub: repetições e crossfades
Use a repetição direta quando precisar de cópias exatas de um clipe, ou sobreponha o fim e o início com um crossfade para suavizar uma junção abrupta. Este passo a passo cria um arquivo WAV de duração finita com Pydub, informa falhas pelo código de saída e se recusa a substituir uma saída existente. Um crossfade pode reduzir um estalo; ele não garante um loop musicalmente contínuo.
Pré-requisitos
Os comandos usam Bash no Linux, Python com suporte a ambientes virtuais, FFmpeg e ffprobe. Este passo a passo foi testado com Python 3.12.13 e 3.14.7, Pydub 0.25.1 e FFmpeg 7.1.5 e 9.0.1. Instale o Python e as ferramentas nativas pela sua distribuição; a página de download do FFmpeg tem links para os pacotes. Verifique os executáveis:
python3 --version && ffmpeg -version && ffprobe -version
Use como entrada um arquivo WAV PCM sem compressão, de 16 bits com sinal, little-endian e com um ou dois canais. O script preserva a taxa de amostragem e os canais, e rejeita outras codificações de amostras em vez de interpretar seus bytes como inteiros de 16 bits. Ele monta o resultado completo na memória, então comece com clipes curtos.
Configurar as ferramentas essenciais
Abra um shell Bash em um novo diretório de trabalho vazio. Instale os pacotes nas versões fixadas
em um novo ambiente virtual; a primeira verificação impede a reutilização de
.venv caso já exista:
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"'
O Python removeu audioop na versão 3.13. O Pydub 0.25.1
usa esse módulo, então a dependência condicional fornece
audioop-lts nas versões mais recentes do Python.
Esses comandos usam o ambiente virtual diretamente; não é necessário ativá-lo. Se a instalação
falhar, o shell permanece no diretório de trabalho; escolha um novo diretório antes de tentar
configurar o ambiente novamente.
Repetição básica com Pydub
Salve este programa completo como loop_audio.py. O leitor
wave da biblioteca padrão valida o contêiner PCM,
e o script verifica se todos os quadros declarados estão presentes antes de construir um segmento
do Pydub. Ele rejeita arquivos vazios, contagens de repetição não positivas, crossfades negativos
e crossfades com duração igual ou superior à do clipe.
A saída é preparada em um local temporário ao lado do destino e publicada por meio de um link físico. Use um sistema de arquivos local com permissão de escrita e suporte a links físicos, além de um diretório de saída já existente. Em caso de falha comum, o diretório temporário é removido; um destino existente é preservado.
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())
Use seu próprio input.wav compatível, ou gere um tom de um segundo com pontos
inicial e final intencionalmente incompatíveis. As verificações de existência retornam um código
de saída de erro se o arquivo de teste já existir; a opção -n do FFmpeg
também se recusa a sobrescrevê-lo:
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
Repita o áudio cinco vezes:
.venv/bin/python loop_audio.py --repeats 5 -- input.wav repeated.wav
O resultado esperado é Wrote repeated.wav: 5.000000 seconds, 1 channel(s), com código de saída 0.
A repetição direta mantém todos os quadros da entrada, incluindo o silêncio, mas também repete
qualquer descontinuidade entre a última e a primeira amostra. Execute o comando novamente e ele
retornará o código de saída 1, preservando repeated.wav.
Argumentos inválidos retornam o código de saída 2. Erros de entrada/saída,
incluindo ausência da entrada, truncamento detectado ou codificações de amostras não compatíveis,
retornam o código de saída 1. Verificar os quadros declarados não permite
determinar se uma gravação estava completa antes de ser salva ou se valores válidos de amostras
foram alterados.
Aplicar crossfade às junções internas
Escolha um novo nome para o arquivo de saída e uma sobreposição de 25 ms:
.venv/bin/python loop_audio.py --repeats 5 --crossfade-ms 25 -- input.wav crossfaded.wav
O resultado esperado é Wrote crossfaded.wav: 4.900000 seconds, 1 channel(s). A
operação append do Pydub sobrepõe o final do
áudio acumulado ao início da próxima cópia. Com N cópias de duração
D e um crossfade de duração F, o resultado
é aproximadamente N × D − (N − 1) × F. Cinco cópias de um segundo têm quatro junções,
então perdem 100 ms no total. O Pydub faz cortes em milissegundos; clipes ou taxas de amostragem que
não se alinham a essa grade podem introduzir arredondamento de amostras. A repetição direta usa
os quadros originais sem esses cortes.
Verificar os arquivos WAV salvos
Inspecione e decodifique por completo os dois arquivos de saída usando seus nomes exatos:
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 o tom gerado, ambos os arquivos devem informar pcm_s16le,
48000 Hz e um canal. Suas durações são de 5 segundos e 4,9 segundos,
respectivamente. Os comandos de decodificação devem terminar sem erros. Essas verificações
confirmam o formato, a duração e a possibilidade de decodificação; ouça em um reprodutor de áudio
para avaliar se as junções são adequadas para sua gravação.
Escolher um ponto de junção adequado para o loop
Um cruzamento por zero pode reduzir um salto de amplitude, mas dois cruzamentos por zero podem ter inclinações, fases ou comportamentos de canal diferentes. A detecção de batidas sugere pontos de edição rítmicos; ela não confirma que as notas, a ambiência ou a reverberação se unem naturalmente. Uma entrada silenciosa continua silenciosa após qualquer uma das operações, e um crossfade pode reduzir a nitidez de um transiente ou alterar o ritmo ao encurtar cada repetição.
O script aplica fades apenas às junções internas. Ele não une o fim do arquivo salvo ao seu início, então reproduzir toda a saída repetidamente por tempo indefinido ainda pode gerar estalos. Ouça tanto as junções internas quanto essa junção entre o fim e o início no reprodutor que pretende usar. Para um loop de jogo ou de música de fundo, selecione primeiro um trecho musicalmente compatível e verifique os dois canais estéreo antes de escolher a sobreposição.
Repetir arquivos mais longos com FFmpeg
O Pydub mantém na memória a entrada e o resultado que cresce a cada repetição. Dividir em blocos um segmento totalmente carregado não elimina essa alocação inicial. Se você só precisa repetir o arquivo inteiro, o FFmpeg pode processá-lo sem construir todo o buffer PCM repetido em Python:
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
A opção de entrada -stream_loop conta reproduções adicionais:
4 resulta em cinco cópias, incluindo a primeira. Esse comando cria um
WAV de cinco segundos a partir do tom, preserva a taxa de amostragem e o número de canais e não
adiciona crossfade. Execute estes comandos em sequência. As verificações de existência fazem com
que uma nova execução falhe antes de chamar o FFmpeg; -n, por si só, pode
informar uma recusa em sobrescrever com código de saída 0 no FFmpeg 9.0.1.
Com -xerror, erros de processamento informados fazem o comando falhar.
Uma falha após o início da escrita do arquivo de saída pode deixar um novo arquivo parcial; remova a saída da execução
que falhou antes de tentar novamente. Uma decodificação bem-sucedida não prova que uma gravação
estava completa antes de chegar ao seu script.
Mantenha WAV PCM como formato de saída para edição e avalie as junções antes de codificar para distribuição. A API do Pydub documenta outros fades e ajustes de ganho, caso o trecho escolhido precise deles.
