Audio-Wellenformen mit Go: Schritt-für-Schritt-Anleitung
Erstellen Sie aus einer vorhandenen Aufnahme ein transparentes Wellenform-PNG, indem Sie FFmpeg mit Go ausführen. Go prüft die Eingabe, weist von FFmpeg gemeldete Fehler zurück und stellt das Bild bereit, ohne eine bestehende Datei zu ersetzen. FFmpeg decodiert das Audio und zeichnet die Wellenform; Go-Pakete von Drittanbietern sind nicht erforderlich.
Voraussetzungen
Verwenden Sie eine gepflegte Go-Version und eine FFmpeg-Installation mit PNG-Encoder und
showwavespic-Filter. Das Beispiel wurde unter Linux mit Go 1.26.8
und FFmpeg 9.0.1 getestet. Für die folgende Anleitung zur Verifikation benötigen Sie außerdem
ffprobe. MP3, PCM WAV, Vorbis in Ogg und FLAC funktionieren, wenn die
entsprechenden Decoder vorhanden sind. Dies ist ein Kommandozeilen-Tool für kurze,
vertrauenswürdige lokale Aufnahmen.
Go-Umgebung einrichten
Installieren Sie Go anhand der offiziellen Anleitung und FFmpeg von der FFmpeg-Downloadseite. Die folgenden Befehle verwenden Bash. Prüfen Sie die Tools, bevor Sie das Projekt erstellen:
go version && ffmpeg -version && ffprobe -version
Erstellen Sie ein neues Modul. Nach der Subshell befinden Sie sich wieder im selben übergeordneten
Verzeichnis, und GOWORK=off isoliert das Modul
von einem umgebenden Go-Workspace:
(
mkdir waveform-generator &&
cd waveform-generator &&
GOWORK=off go mod init waveform-generator &&
GOWORK=off go mod edit -go=1.22
)
Brechen Sie ab, wenn die Einrichtung fehlschlägt. Ein vorhandenes Verzeichnis
waveform-generator bleibt unverändert; wählen Sie stattdessen ein neues übergeordnetes
Verzeichnis. Verwenden Sie dieses übergeordnete Verzeichnis auch für die folgenden Befehle.
Einfache Wellenformgenerierung
Speichern Sie dieses vollständige Programm als waveform-generator/main.go. Es rendert die
erste Audiospur und mischt deren Kanäle zu Mono. Ein kleiner Writer leitet die Diagnosemeldungen
von FFmpeg an Ihr Terminal weiter und merkt sich, ob Meldungen ausgegeben wurden, ohne das
Protokoll im Arbeitsspeicher zu sammeln.
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")
}
Kopieren Sie eine eigene Audiodatei nach waveform-generator/input.wav und führen Sie dann
Folgendes aus:
(cd waveform-generator && GOWORK=off go run . input.wav waveform.png)
Bei Erfolg gibt der Befehl Waveform saved aus.
Öffnen Sie waveform-generator/waveform.png in einem Bildbetrachter. Das Bild ist 1024 × 256 Pixel
groß und hat einen transparenten Hintergrund. Die Zeit verläuft von links nach rechts;
jede Spalte fasst die Beträge der Samplewerte zusammen, und höhere Spalten zeigen größere
Spitzenwerte an. Die Quadratwurzelskala macht leise Abschnitte besser sichtbar. Dies ist eine
Amplitudenübersicht, kein vorzeichenbehafteter Sampleverlauf und kein Maß für die wahrgenommene
Lautheit.
Erweiterte Funktionen
Große Dateien streamen
Obwohl Go das PNG direkt auf die Festplatte schreibt, stellt showwavespic
decodierte Audioframes in eine Warteschlange,
bevor das Bild gezeichnet wird. Das Limit von 100 MiB gilt für die Eingabedatei. Der Kontext mit
einem Zeitlimit von zwei Minuten gilt für FFmpeg, mit bis zu fünf Sekunden für das Aufräumen der
Prozess-Ein-/Ausgabe; die Go-Kompilierung liegt außerhalb dieser Frist. Keines der Limits begrenzt
den Arbeitsspeicher für decodierte Daten. Halten Sie Aufnahmen kurz und die Anzahl gleichzeitiger
Ausführungen gering. Verwenden Sie für lange Aufnahmen einen Algorithmus, der Samples schrittweise
aggregiert.
Eigene Farben und Gestaltung
Ändern Sie den festen Wert colors=0x167d9a im Filter, um eine andere Wellenformfarbe
zu wählen. Verwenden Sie die validierten Flags für Breite und Höhe, um die Bildgröße zu ändern:
(cd waveform-generator && GOWORK=off go run . -width 800 -height 200 input.wav compact-waveform.png)
Das Mischen zu Mono ergibt eine kompakte Vorschau, doch gegenphasige Stereosignale können sich
auslöschen. Um Kanäle getrennt zu betrachten, entfernen Sie aformat=channel_layouts=mono, aus dem
Filterstring und fügen Sie split_channels=1 zu den Optionen von
showwavespic hinzu. Speichern Sie das bearbeitete Programm und verwenden Sie bei
der erneuten Ausführung einen neuen Ausgabedateinamen. Jeder Kanal erhält dann ein eigenes
horizontales Band.
Fehlerbehandlung und Validierung
FFmpeg kann sich von einem Decodierfehler erholen und null zurückgeben, während es dennoch ein
gültiges PNG erzeugt. Das Log-Level error
umfasst behebbare Fehler. Daher weist dieses Programm sowohl einen fehlgeschlagenen Prozess als
auch jede Diagnosemeldung auf dieser Stufe zurück, bevor es die Ausgabe bereitstellt. Eine
PCM-WAV-Datei, die innerhalb ihres letzten Samples abgeschnitten wurde, muss beispielsweise zu
einem Fehler führen, statt mit einer unvollständigen Wellenform Erfolg zu melden.
Diese Regel belegt nicht, dass die Aufnahme intakt ist: Warnungen werden unterdrückt, und ein Decoder kann Schäden verbergen, ohne einen Fehler zu melden. Prüfen Sie die erwartete Dauer einer Aufnahme und hören Sie sie an, wenn Vollständigkeit wichtig ist. Eine Dateiendung, ein gültiges PNG oder ein Exit-Status von null allein können die Integrität der Eingabe nicht belegen.
Fehlende Audiospuren, ein Abbruch und eine ungültige PNG-Ausgabe verhindern ebenfalls die Bereitstellung. Temporäre Dateien werden bei der Rückkehr aus der Funktion entfernt, auch im Fehlerfall.
Wenn FFmpeg Too few samples meldet, reduzieren Sie die Breite: Der Filter benötigt
mindestens ein decodiertes Sample pro Spalte. Die Breite muss zwischen 16 und 4096 Pixeln bleiben,
die Höhe zwischen 16 und 1024 Pixeln.
Das Zielverzeichnis muss vorhanden sein und Hardlinks unterstützen. Eine vorhandene Ausgabe bleibt erhalten, auch wenn sie während des Encodings erstellt wurde. Verwenden Sie für jede Ausführung einen neuen Dateinamen.
Testen
Erzeugen Sie einen kurzen synthetischen Ton und rendern Sie ihn anschließend:
(
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
)
Die Analyse sollte ein PNG mit der Breite 1024 und der Höhe 256 erkennen. Öffnen Sie auch
waveform-generator/tone.png: Das Bild sollte über die gesamte Breite ein Band gleich hoher
Spitzen enthalten. Eine leere Datei, eine Datei ohne Audio oder eine erneute Ausführung mit einem
bereits vorhandenen Ziel müssen fehlschlagen, ohne bestehende Dateien zu ersetzen. Verwenden Sie
bei einer Wiederholung der Anleitung sowohl für tone.wav als auch für
tone.png neue Namen.
Befehl wiederverwenden
Bewahren Sie die Originalaufnahme neben ihrer Vorschau auf, damit Sie stille Abschnitte und die Stereomischung mit dem Gehörten vergleichen können. Über den Kontext der Funktion kann auch ein lokaler Job-Runner die Verarbeitung abbrechen; das Tool ist kein Upload-Server. Informationen zur verwalteten Verarbeitung finden Sie in den unterstützten Workflows der Audio-Encoding-Dienste von Transloadit.
