Générer des formes d’onde audio avec Go : guide pas à pas
Créez une forme d’onde au format PNG avec un fond transparent à partir d’un enregistrement existant, en utilisant Go pour exécuter FFmpeg. Go vérifie l’entrée, rejette les erreurs signalées par FFmpeg et publie l’image sans remplacer de fichier existant. FFmpeg décode l’audio et dessine la forme d’onde ; aucun paquet Go tiers n’est nécessaire.
Prérequis
Utilisez une version maintenue de Go et une installation de FFmpeg dotée de l’encodeur PNG et du
filtre showwavespic. L’exemple a été testé sous Linux avec
Go 1.26.8 et FFmpeg 9.0.1. Vous avez également besoin de ffprobe pour la
procédure de vérification ci-dessous. MP3, PCM WAV, Vorbis dans Ogg et FLAC fonctionnent lorsque
les décodeurs correspondants sont présents. Il s’agit d’un outil en ligne de commande destiné aux
courts enregistrements locaux de confiance.
Configurer votre environnement Go
Installez Go en suivant ses instructions officielles et FFmpeg depuis la page de téléchargement de FFmpeg. Les commandes ci-dessous utilisent Bash. Vérifiez les outils avant de créer le projet :
go version && ffmpeg -version && ffprobe -version
Créez un nouveau module. Le sous-shell vous laisse dans le même répertoire parent, et
GOWORK=off isole le module de tout espace de travail Go englobant :
(
mkdir waveform-generator &&
cd waveform-generator &&
GOWORK=off go mod init waveform-generator &&
GOWORK=off go mod edit -go=1.22
)
Arrêtez-vous si la configuration échoue. Si le répertoire waveform-generator existe déjà,
il reste intact ; choisissez plutôt un nouveau répertoire parent. Continuez à utiliser ce répertoire
parent pour les commandes ci-dessous.
Génération de base de formes d’onde
Enregistrez ce programme complet dans waveform-generator/main.go. Il produit le rendu de la
première piste audio en mixant ses canaux en mono. Un petit composant d’écriture transmet les
diagnostics de FFmpeg à votre terminal et mémorise si des messages ont été émis, sans accumuler
le journal en mémoire.
package main
import (
"context"
"flag"
"fmt"
"image/png"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"time"
)
type errorOutput struct {
reported bool
}
func (output *errorOutput) Write(data []byte) (int, error) {
if len(data) > 0 {
output.reported = true
}
return os.Stderr.Write(data)
}
func generateWaveform(ctx context.Context, input, output string, width, height int) error {
if width < 16 || width > 4096 || height < 16 || height > 1024 {
return fmt.Errorf("width must be 16–4096 and height 16–1024")
}
input, err := filepath.Abs(input)
if err != nil {
return err
}
info, err := os.Stat(input)
if err != nil {
return err
}
if !info.Mode().IsRegular() || info.Size() == 0 || info.Size() > 100*1024*1024 {
return fmt.Errorf("input must be a nonempty regular file of at most 100 MiB")
}
temporary, err := os.CreateTemp(filepath.Dir(output), ".waveform-*.png")
if err != nil {
return err
}
defer os.Remove(temporary.Name())
defer temporary.Close()
filter := fmt.Sprintf("[0:a:0]aformat=channel_layouts=mono,showwavespic=s=%dx%d:colors=0x167d9a:scale=sqrt:filter=peak[v]", width, height)
command := exec.CommandContext(ctx, "ffmpeg", "-hide_banner", "-loglevel", "error",
"-nostdin", "-protocol_whitelist", "file,pipe", "-i", input,
"-filter_complex", filter, "-map", "[v]", "-frames:v", "1",
"-c:v", "png", "-f", "image2pipe", "pipe:1")
diagnostics := &errorOutput{}
command.Stdout = temporary
command.Stderr = diagnostics
command.WaitDelay = 5 * time.Second
if err := command.Run(); err != nil {
return fmt.Errorf("render waveform: %w", err)
}
// FFmpeg can recover from a decoding error and still exit successfully.
if diagnostics.reported {
return fmt.Errorf("FFmpeg reported an error; waveform not published")
}
if err := ctx.Err(); err != nil {
return err
}
if _, err := temporary.Seek(0, io.SeekStart); err != nil {
return err
}
image, err := png.Decode(temporary)
if err != nil {
return fmt.Errorf("invalid waveform PNG: %w", err)
}
if image.Bounds().Dx() != width || image.Bounds().Dy() != height {
return fmt.Errorf("unexpected waveform dimensions")
}
if err := temporary.Close(); err != nil {
return err
}
// Linking publishes the complete file without replacing a concurrent writer's output.
if err := os.Link(temporary.Name(), output); err != nil {
return fmt.Errorf("publish waveform (destination must be new): %w", err)
}
return nil
}
func run() error {
width := flag.Int("width", 1024, "image width in pixels")
height := flag.Int("height", 256, "image height in pixels")
flag.Parse()
if flag.NArg() != 2 {
return fmt.Errorf("usage: waveform-generator [-width 1024] [-height 256] input.wav output.png")
}
interrupted, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
ctx, cancel := context.WithTimeout(interrupted, 2*time.Minute)
defer cancel()
return generateWaveform(ctx, flag.Arg(0), flag.Arg(1), *width, *height)
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("Waveform saved")
}
Copiez un fichier audio qui vous appartient vers waveform-generator/input.wav, puis exécutez :
(cd waveform-generator && GOWORK=off go run . input.wav waveform.png)
En cas de réussite, la commande affiche Waveform saved.
Ouvrez waveform-generator/waveform.png dans une visionneuse d’images. L’image mesure
1024 × 256 pixels et possède un fond transparent. Le temps s’écoule de gauche à droite ;
chaque colonne résume les valeurs absolues des échantillons, et les colonnes les plus hautes
indiquent des crêtes de plus grande amplitude. L’échelle en racine carrée rend les passages de
faible amplitude plus visibles. Il s’agit d’une vue d’ensemble des amplitudes, et non d’un tracé
des valeurs signées des échantillons ni d’une mesure de l’intensité sonore perçue.
Fonctionnalités avancées
Traitement en flux de fichiers volumineux
Bien que Go écrive le PNG directement sur disque, showwavespic
met en file d’attente les trames audio décodées
avant de dessiner l’image. La limite de 100 MiB s’applique au fichier d’entrée. Le délai de deux
minutes du contexte s’applique à FFmpeg, avec jusqu’à cinq secondes pour le nettoyage des
entrées/sorties du processus ; la compilation Go n’est pas comprise dans ce délai. Aucune de ces
limites ne plafonne la mémoire occupée par les données décodées. Utilisez des enregistrements courts
et limitez le nombre de traitements simultanés. Pour les enregistrements longs, utilisez un
algorithme qui agrège les échantillons de manière incrémentale.
Couleurs et style personnalisés
Modifiez la valeur fixe colors=0x167d9a dans le filtre pour sélectionner une autre
couleur de forme d’onde. Utilisez les options de largeur et de hauteur validées pour modifier les
dimensions de l’image :
(cd waveform-generator && GOWORK=off go run . -width 800 -height 200 input.wav compact-waveform.png)
Le mixage en mono produit un aperçu compact, mais des signaux stéréo opposés peuvent s’annuler.
Pour inspecter les canaux séparément, retirez aformat=channel_layouts=mono, de la chaîne de filtre
et ajoutez split_channels=1 aux options de showwavespic.
Enregistrez le programme modifié et utilisez un nouveau nom de fichier de sortie à la prochaine
exécution. Chaque canal occupe alors sa propre bande horizontale.
Gestion des erreurs et validation
FFmpeg peut se remettre d’une erreur de décodage et renvoyer zéro tout en produisant un PNG valide.
Son niveau de journalisation error
inclut les erreurs récupérables ; ce programme rejette donc aussi bien un échec du processus que
tout diagnostic de ce niveau avant de publier l’image. Par exemple, le traitement d’un fichier
PCM WAV tronqué au milieu de son dernier échantillon doit échouer au lieu de signaler une réussite
avec une forme d’onde partielle.
Cette politique ne prouve pas que l’enregistrement est intact : les avertissements sont masqués, et un décodeur peut dissimuler des dommages sans signaler d’erreur. Vérifiez la durée attendue d’un enregistrement et écoutez-le lorsque son intégralité est importante. Une extension de fichier, un PNG valide ou un code de sortie nul ne suffisent pas, à eux seuls, à établir l’intégrité de l’entrée.
L’absence de pistes audio, l’annulation et un PNG de sortie invalide empêchent également la publication. Les fichiers temporaires sont supprimés au retour de la fonction, y compris en cas d’échec.
Si FFmpeg signale Too few samples, réduisez la largeur : le filtre nécessite au moins
un échantillon décodé par colonne. La largeur doit rester comprise entre 16 et 4096 pixels ;
la hauteur doit rester comprise entre 16 et 1024 pixels.
Le répertoire de destination doit exister et prendre en charge les liens physiques. Tout fichier de sortie existant, y compris s’il a été créé pendant l’encodage, est préservé. Utilisez un nouveau nom de fichier à chaque exécution.
Tests
Générez une courte tonalité synthétique, puis produisez son rendu :
(
cd waveform-generator &&
ffmpeg -hide_banner -loglevel error -nostdin -n -f lavfi -i "sine=frequency=440:duration=2" -c:a pcm_s16le tone.wav &&
GOWORK=off go run . tone.wav tone.png &&
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height -of json tone.png
)
L’outil d’analyse devrait identifier un PNG d’une largeur de 1024 et d’une hauteur de 256. Ouvrez
également waveform-generator/tone.png : l’image devrait contenir une bande de crêtes de même
hauteur sur toute sa largeur. Un fichier vide, un fichier non audio ou une nouvelle exécution avec
une destination existante doivent provoquer un échec sans remplacer de fichiers existants.
Utilisez de nouveaux noms à la fois pour tone.wav et pour
tone.png lorsque vous répétez la procédure.
Réutiliser la commande
Conservez l’enregistrement d’origine avec son aperçu afin de pouvoir comparer les passages silencieux et le mixage stéréo à ce que vous entendez. Le contexte de la fonction permet aussi à un exécuteur local de tâches d’annuler le traitement ; il ne s’agit pas d’un serveur de téléversement. Pour un traitement géré, consultez les flux de travail pris en charge par les services d’encodage audio de Transloadit.
