Boucler l’audio (Python, Pydub) : répétitions et fondus croisés
Utilisez la répétition sans fondu lorsque vous avez besoin de copies exactes d’un extrait, ou faites se chevaucher sa fin et son début avec un fondu croisé pour adoucir une jonction abrupte. Ce guide crée un fichier WAV de durée finie avec Pydub, signale les échecs par son code de sortie et refuse de remplacer un fichier de sortie existant. Un fondu croisé peut atténuer un clic ; il ne garantit pas une boucle sans rupture sur le plan musical.
Prérequis
Les commandes utilisent Bash sous Linux, Python avec prise en charge des environnements virtuels, FFmpeg et ffprobe. Ce guide a été testé avec Python 3.12.13 et 3.14.7, Pydub 0.25.1, ainsi que FFmpeg 7.1.5 et 9.0.1. Installez Python et les outils natifs via votre distribution ; la page de téléchargement de FFmpeg propose des liens vers des paquets. Vérifiez les exécutables :
python3 --version && ffmpeg -version && ffprobe -version
Utilisez en entrée un fichier WAV PCM non compressé, avec des échantillons signés sur 16 bits stockés en ordre petit-boutiste et un ou deux canaux. Le script conserve sa fréquence d’échantillonnage et ses canaux, et rejette les autres encodages d’échantillons plutôt que d’interpréter leurs octets comme des entiers sur 16 bits. Il construit l’intégralité du résultat en mémoire : commencez donc par des extraits courts.
Configurer les outils essentiels
Ouvrez un shell Bash dans un nouveau répertoire de travail vide. Installez les versions figées des
paquets dans un nouvel environnement virtuel ; le premier contrôle refuse de réutiliser
.venv s’il existe déjà :
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 a supprimé audioop dans sa version 3.13.
Pydub 0.25.1 l’utilise, donc la dépendance conditionnelle fournit
audioop-lts dans les versions plus récentes de Python.
Ces commandes utilisent directement l’environnement virtuel ; son activation n’est pas nécessaire.
Si l’installation échoue, le shell reste dans votre répertoire de travail ; choisissez un nouveau
répertoire avant de réessayer l’installation.
Mise en boucle simple avec Pydub
Enregistrez ce programme complet sous le nom loop_audio.py. Le
lecteur wave de la bibliothèque standard valide
le conteneur PCM, et le script vérifie la présence de chaque trame déclarée avant de construire un
segment Pydub. Il rejette les fichiers vides, les nombres de répétitions nuls ou négatifs, les durées
de fondu croisé négatives et les fondus croisés aussi longs que l’extrait.
La sortie est préparée temporairement à côté de sa destination, puis publiée à l’aide d’un lien physique. Utilisez un système de fichiers local accessible en écriture qui prend en charge les liens physiques, ainsi qu’un répertoire de sortie existant. Un échec ordinaire entraîne la suppression du répertoire temporaire ; une destination existante est préservée.
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())
Utilisez votre propre input.wav compatible, ou générez un son d’une seconde dont
les valeurs de début et de fin sont volontairement différentes. Les contrôles d’existence renvoient
un code d’échec si le fichier de test existe déjà ; l’option -n de FFmpeg
refuse elle aussi de l’écraser :
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
Répétez-le cinq fois :
.venv/bin/python loop_audio.py --repeats 5 -- input.wav repeated.wav
Le résultat attendu est Wrote repeated.wav: 5.000000 seconds, 1 channel(s), avec le code de sortie
0. La répétition sans fondu conserve toutes les trames d’entrée, y compris
le silence, mais reproduit aussi toute discontinuité entre le dernier et le premier échantillon.
Si vous relancez la commande, elle renvoie le code de sortie 1 tout en
préservant repeated.wav. En cas d’arguments non valides, le code de sortie est
2. Les erreurs d’entrée/sortie, notamment un fichier d’entrée absent,
une troncature détectée ou des encodages d’échantillons non pris en charge, renvoient le code de sortie
1. La vérification des trames déclarées ne permet pas de déterminer si un
enregistrement était complet avant sa sauvegarde ni si des valeurs d’échantillons valides ont été
altérées.
Appliquer un fondu croisé aux jonctions internes
Choisissez un nouveau nom de fichier de sortie et un chevauchement de 25 ms :
.venv/bin/python loop_audio.py --repeats 5 --crossfade-ms 25 -- input.wav crossfaded.wav
Le résultat attendu est Wrote crossfaded.wav: 4.900000 seconds, 1 channel(s). L’opération append
de Pydub fait se chevaucher la fin de l’audio accumulé et le début de la copie suivante. Pour
N copies de durée D et un fondu croisé de durée
F, le résultat dure environ N × D − (N − 1) × F. Cinq copies
d’une seconde comportent quatre jonctions, ce qui réduit leur durée totale de 100 ms. Pydub découpe
l’audio à la milliseconde ; les extraits ou les fréquences d’échantillonnage qui ne s’alignent pas sur
cette grille peuvent entraîner des arrondis au niveau des échantillons. La répétition sans fondu
utilise les trames d’origine sans ce découpage.
Vérifier les fichiers WAV enregistrés
Inspectez et décodez entièrement les deux fichiers de sortie aux noms indiqués :
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 -
Pour le son généré, les deux fichiers devraient indiquer pcm_s16le,
48000 Hz et un canal. Leur durée est de 5 secondes et de 4,9 secondes,
respectivement. Les commandes de décodage devraient se terminer sans erreur. Ces vérifications
confirment le format, la durée et la possibilité de décoder les fichiers ; écoutez-les dans un lecteur
audio pour juger si les jonctions conviennent à votre enregistrement.
Choisir un point de bouclage adapté
Un passage par zéro peut réduire un saut d’amplitude, mais deux passages par zéro peuvent différer par leur pente, leur phase ou le comportement des canaux. La détection des temps suggère des points de montage rythmiques ; elle ne permet pas d’établir que les notes, l’ambiance ou la réverbération se raccordent naturellement. Une entrée silencieuse reste silencieuse après l’une ou l’autre opération, et un fondu croisé peut estomper un transitoire ou modifier le rythme en raccourcissant chaque répétition.
Le script n’applique un fondu qu’aux jonctions internes. Il ne raccorde pas la fin du fichier enregistré à son début ; la lecture indéfinie en boucle de l’ensemble de la sortie peut donc encore produire un clic. Écoutez aussi bien les jonctions internes que ce raccord externe dans le lecteur que vous comptez utiliser. Pour une boucle destinée à un jeu ou à une musique de fond, sélectionnez d’abord une portion musicalement compatible et vérifiez les deux canaux stéréo avant de choisir le chevauchement.
Répéter des fichiers plus longs avec FFmpeg
Pydub conserve en mémoire l’entrée et le résultat qui s’allonge. Découper un segment entièrement chargé en morceaux ne supprime pas cette allocation initiale. Si vous avez seulement besoin de répéter le fichier entier, FFmpeg peut le traiter sans construire en Python l’intégralité du tampon PCM contenant les répétitions :
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
L’option d’entrée -stream_loop compte les lectures
supplémentaires : 4 produit cinq copies, la première comprise. Cette
commande crée un fichier WAV de cinq secondes à partir du son, conserve sa fréquence
d’échantillonnage et son nombre de canaux, et n’ajoute aucun fondu croisé. Exécutez ces commandes
l’une après l’autre. Les contrôles d’existence font échouer toute nouvelle exécution avant l’appel
à FFmpeg ; -n utilisé seul peut signaler un refus d’écrasement avec le code
de sortie 0 sous FFmpeg 9.0.1. -xerror fait échouer la
commande en cas d’erreurs de traitement signalées. Un échec survenant après le début de l’écriture
peut laisser un nouveau fichier partiel ; supprimez le fichier de sortie de cette tentative avant de
réessayer. Un décodage réussi ne prouve pas qu’un enregistrement était complet avant de parvenir à
votre script.
Conservez le format WAV PCM en sortie de montage et évaluez les jonctions avant l’encodage destiné à la diffusion. L’API de Pydub décrit d’autres fondus et réglages de gain si la portion que vous avez choisie en a besoin.
