Audio-Wellenformen mit Go erzeugen und visualisieren
Erzeugen Sie in Go einen zwei Sekunden langen Ton mit 440 Hz und wandeln Sie die WAV-Datei dann in eine SVG-Wellenform mit 800 × 200 um. Generator und Visualisierer speichern Sie als separate Befehle in einem Go-Modul. So können Sie beide unabhängig ausführen und Varianten mit Rechteck-, Dreieck- und gefüllten Wellenformen ausprobieren.
Einführung in die Wellenformvisualisierung und ihre Anwendungen
Eine Wellenform stellt die Sample-Amplitude über die Zeit dar. Sie kann helfen, Pausen in einer Aufnahme zu finden oder synthetische Signale zu vergleichen. Dieses Beispiel zeichnet eine statische Übersicht des ersten Kanals. Es spielt kein Audio ab, kombiniert keine Stereokanäle und bietet keinen Live-Player.
Go-Umgebung für die Audioverarbeitung einrichten
Abhängigkeiten
Die folgenden Befehle verwenden Bash unter Linux und wurden mit Go 1.26.8 getestet. Installieren Sie
Go nach der offiziellen Anleitung und prüfen Sie dann, ob es in
PATH verfügbar ist:
go version
Erstellen Sie ein neues Verzeichnis und lösen Sie genau diese Bibliotheksversionen auf. Das
Repository go-audio/wav ist archiviert; dieses Beispiel
verwendet dessen festgelegte Version für eine kleine lokale Übung. Die Einrichtung erfordert einen
Internetzugang für Modul-Downloads. Die Subshell behält Ihr ursprüngliches Verzeichnis bei, und
GOWORK=off isoliert das Beispiel von einem übergeordneten
Go-Workspace.
(
mkdir waveform-generator &&
cd waveform-generator &&
export GOWORK=off &&
go mod init waveform-generator &&
go get github.com/go-audio/audio@v1.0.0 &&
go get github.com/go-audio/wav@v1.1.0 &&
go get github.com/ajstarks/svgo@v0.0.0-20211024235047-1546f124cd8b &&
mkdir -p cmd/generate cmd/visualize
)
Brechen Sie ab, falls die Einrichtung fehlschlägt. Ein vorhandenes Verzeichnis
waveform-generator bleibt unverändert. Wählen Sie ein neues übergeordnetes Verzeichnis,
statt spätere Schritte in ein bestehendes Projekt einzufügen. Speichern Sie die beiden vollständigen
Go-Programme unter waveform-generator/cmd/generate/main.go und
waveform-generator/cmd/visualize/main.go. Jedes Verzeichnis hat eine eigene Funktion
main.
Grundlegende Wellenformen (Sinus, Rechteck, Dreieck) mit Go erzeugen
Speichern Sie dieses Programm als waveform-generator/cmd/generate/main.go:
package main
import (
"fmt"
"log"
"math"
"os"
"github.com/go-audio/audio"
"github.com/go-audio/wav"
)
func generateWaveform() error {
const sampleRate = 44100
const frequency = 440.0 // A4 note
const duration = 2 // seconds
// Validate input parameters
if sampleRate <= 0 || duration <= 0 || frequency <= 0 {
return fmt.Errorf("sampleRate, duration, and frequency must be positive")
}
numSamples := sampleRate * duration
buf := &audio.IntBuffer{
Format: &audio.Format{NumChannels: 1, SampleRate: sampleRate},
Data: make([]int, numSamples),
}
// Generate sine wave
amplitude := 32767.0 // Max amplitude for 16-bit audio
for i := 0; i < numSamples; i++ {
time := float64(i) / float64(sampleRate)
buf.Data[i] = int(amplitude * math.Sin(2*math.Pi*frequency*time))
}
// Create output file with proper error handling
f, err := os.OpenFile("sine.wav", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return fmt.Errorf("create sine.wav: %w", err)
}
completed := false
defer func() {
f.Close()
if !completed {
os.Remove(f.Name())
}
}()
// Encode audio data to WAV format
// Parameters: output io.WriteSeeker, sample rate, bit depth, num channels, audio format (1 = PCM)
encoder := wav.NewEncoder(f, sampleRate, 16, 1, 1)
if err := encoder.Write(buf); err != nil {
return fmt.Errorf("write WAV data: %w", err)
}
// Close the encoder to finalize the WAV file
if err := encoder.Close(); err != nil {
return fmt.Errorf("finalize WAV: %w", err)
}
if err := f.Close(); err != nil {
return err
}
completed = true
return nil
}
func main() {
if err := generateWaveform(); err != nil {
log.Fatal(err)
}
log.Println("Successfully generated sine.wav")
}
Führen Sie es aus dem übergeordneten Verzeichnis aus, in dem Sie die Einrichtung eingefügt haben:
(cd waveform-generator && GOWORK=off go run ./cmd/generate)
Das Programm schreibt waveform-generator/sine.wav: Mono, 16-Bit-Integer-PCM mit 44.100 Samples pro
Sekunde. Öffnen Sie die Datei in einem Audioplayer, um den Ton zu hören. Beide Programme verweigern
das Überschreiben vorhandener Ausgaben. Verschieben oder löschen Sie die alte Ausgabe bewusst,
bevor Sie ein bearbeitetes Programm erneut ausführen.
Um eine Rechteckwelle zu erzeugen, ersetzen Sie die Deklaration von amplitude
und die Schleife zur Sinuserzeugung in cmd/generate/main.go durch den folgenden Block.
Die Ausgabe heißt weiterhin sine.wav. Verschieben Sie die bisherigen WAV- und
SVG-Dateien und führen Sie dann für jede Variante die Befehle für Generator und Visualisierer erneut aus.
// Generate square wave
amplitude := 32767.0
for i := 0; i < numSamples; i++ {
time := float64(i) / float64(sampleRate)
if math.Sin(2*math.Pi*frequency*time) >= 0 {
buf.Data[i] = int(amplitude)
} else {
buf.Data[i] = int(-amplitude)
}
}
Für eine Dreieckwelle ersetzen Sie dieselbe Deklaration und Schleife durch:
// Generate triangle wave
amplitude := 32767.0
period := float64(sampleRate) / frequency
for i := 0; i < numSamples; i++ {
// Calculate phase within the period (0.0 to 1.0)
phase := math.Mod(float64(i), period) / period
if phase < 0.5 {
// Rising edge: scales from -1 to 1 over the first half
buf.Data[i] = int(amplitude * (phase*4.0 - 1.0))
} else {
// Falling edge: scales from 1 to -1 over the second half
buf.Data[i] = int(amplitude * (3.0 - phase*4.0))
}
}
Wellenformen mit SVG visualisieren
Speichern Sie das nächste Programm als waveform-generator/cmd/visualize/main.go. Es akzeptiert klassische
RIFF/WAVE-Integer-PCM-Dateien mit Format-Tag 1 und Samples mit 8, 16, 24 oder 32 Bit. Eingaben mit
Fließkommawerten und WAVE_FORMAT_EXTENSIBLE werden abgelehnt. Samples sind
bei 8 Bit vorzeichenlos und bei den anderen unterstützten Bittiefen vorzeichenbehaftet.
Bei Stereoeingaben wählt es den ersten Kanal aus, ohne die Kanäle zu mischen.
Die Darstellung verwendet 800 Frames, gleichmäßig verteilt vom ersten bis zum letzten Frame.
Spitzen zwischen diesen Frames können verloren gehen. Nutzen Sie eine Minimum-/Maximum-Aggregation
pro Pixel, wenn Sie eine Amplitudenhüllkurve benötigen. FullPCMBuffer() lädt die gesamte
Aufnahme in den Arbeitsspeicher. Beschränken Sie diese Übung daher auf kurze lokale Dateien.
Die Längenprüfungen erkennen Nutzdaten, die kürzer als die deklarierte PCM-Größe sind. Sie bestätigen
jedoch nicht, dass die Aufnahme oder alle ihre Metadaten intakt sind. Die explizite Byte-Grenze
schließt RIFF-Padding
von den Samples aus, die die Bibliothek in der festgelegten Version decodiert.
package main
import (
"bufio"
"encoding/binary"
"fmt"
"io"
"log"
"math"
"os"
"github.com/ajstarks/svgo"
"github.com/go-audio/wav"
)
func visualizeWaveform() error {
// Open the WAV file
file, err := os.Open("sine.wav") // Or any other .wav file
if err != nil {
return fmt.Errorf("open audio file: %w", err)
}
defer func() {
if err := file.Close(); err != nil {
log.Printf("Warning: Failed to close audio file: %v", err)
}
}()
// Create a new decoder
decoder := wav.NewDecoder(file)
if !decoder.IsValidFile() {
return fmt.Errorf("invalid WAV file")
}
if decoder.WavAudioFormat != 1 {
return fmt.Errorf("only integer PCM WAV files are supported")
}
if decoder.BitDepth != 8 && decoder.BitDepth != 16 && decoder.BitDepth != 24 && decoder.BitDepth != 32 {
return fmt.Errorf("unsupported PCM bit depth: %d", decoder.BitDepth)
}
if err := decoder.FwdToPCM(); err != nil {
return fmt.Errorf("find PCM data: %w", err)
}
dataStart, err := file.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
// v1.1.0 includes RIFF padding in PCMSize; read the unpadded data length.
var sizeBytes [4]byte
if _, err := file.ReadAt(sizeBytes[:], dataStart-4); err != nil {
return fmt.Errorf("read PCM length: %w", err)
}
pcmBytes := int64(binary.LittleEndian.Uint32(sizeBytes[:]))
bytesPerSample := int64(decoder.BitDepth / 8)
frameBytes := bytesPerSample * int64(decoder.NumChans)
if pcmBytes == 0 || pcmBytes%frameBytes != 0 {
return fmt.Errorf("empty or incomplete PCM frames")
}
info, err := file.Stat()
if err != nil {
return err
}
if dataStart+pcmBytes > info.Size() {
return fmt.Errorf("truncated PCM data")
}
// Decode samples only, excluding a possible RIFF padding byte.
decoder.PCMChunk.R = io.LimitReader(file, pcmBytes)
// Read the full PCM buffer
// Note: For large audio files, consider processing the audio in chunks
// rather than loading the entire file into memory with FullPCMBuffer().
buf, err := decoder.FullPCMBuffer()
if err != nil {
return fmt.Errorf("read PCM data: %w", err)
}
if int64(len(buf.Data))*bytesPerSample != pcmBytes {
return fmt.Errorf("truncated PCM data")
}
channels := buf.Format.NumChannels
if channels <= 0 || len(buf.Data) == 0 || len(buf.Data)%channels != 0 {
return fmt.Errorf("empty or incomplete PCM frames")
}
// Define SVG dimensions
width := 800
height := 200
centerY := height / 2
// Create SVG file
svgFile, err := os.OpenFile("waveform.svg", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return fmt.Errorf("create waveform.svg: %w", err)
}
completed := false
defer func() {
svgFile.Close()
if !completed {
os.Remove(svgFile.Name())
}
}()
// Initialize SVG canvas
writer := bufio.NewWriter(svgFile)
canvas := svg.New(writer)
canvas.Start(width, height)
canvas.Rect(0, 0, width, height, "fill:white") // Background
// Draw center line
canvas.Line(0, centerY, width, centerY, "stroke:#cccccc;stroke-width:1")
// WAV samples are interleaved by channel; choose the first channel in each frame.
numFrames := len(buf.Data) / channels
sampleAt := func(x int) float64 {
frame := x * (numFrames - 1) / (width - 1)
sample := float64(buf.Data[frame*channels])
// 8-bit WAV PCM is unsigned.
if buf.SourceBitDepth == 8 {
sample -= 128
}
return sample
}
// Draw waveform lines from center to amplitude
maxAmplitude := math.Ldexp(1, buf.SourceBitDepth-1)
scale := float64(centerY) / maxAmplitude // Scale factor for amplitude to fit height
for x := 0; x < width; x++ {
sampleValue := sampleAt(x)
// Calculate y coordinate, inverting for SVG (0 is top)
y := centerY - int(sampleValue*scale)
canvas.Line(x, centerY, x, y, "stroke:#0066cc;stroke-width:1")
}
canvas.End()
if err := writer.Flush(); err != nil {
return err
}
if err := svgFile.Close(); err != nil {
return err
}
completed = true
return nil
}
func main() {
if err := visualizeWaveform(); err != nil {
log.Fatal(err)
}
log.Println("Successfully generated waveform.svg")
}
Nachdem Sie sine.wav erzeugt haben, führen Sie den Visualisierer aus demselben
übergeordneten Verzeichnis aus:
(cd waveform-generator && GOWORK=off go run ./cmd/visualize)
Öffnen Sie waveform-generator/waveform.svg in einem Browser oder Bildbetrachter. Die Datei enthält einen
weißen Hintergrund, eine Mittellinie bei y = 100 und 800 vertikale Amplitudenlinien. Positive Samples
reichen nach oben, negative nach unten. Ein zwei Sekunden langer Ton enthält 880 Schwingungen.
Die 800 abgetasteten Spalten ergeben daher eine Übersicht mit Aliasing statt eines lesbaren Verlaufs
jeder einzelnen Schwingung.
Fortgeschrittene Techniken zur Anpassung von Wellenformen
Mit diesen Techniken können Sie Ihre SVG-Visualisierungen verbessern:
Farben und Linienstärke anpassen
Bearbeiten Sie die drei Stilzeichenfolgen in cmd/visualize/main.go: Ändern Sie
fill:white in fill:#f5f5f5,
stroke:#cccccc;stroke-width:1 in stroke:#e76f51;stroke-width:1 und
stroke:#0066cc;stroke-width:1 in stroke:#2a9d8f;stroke-width:2. Diese SVG-CSS-Stile werden an
svgo übergeben. Sie ändern die Zeichnung,
ohne den Abtastcode zu verschieben. Verschieben Sie die bisherige Datei
waveform.svg und führen Sie dann den Visualisierer erneut aus.
Eine gefüllte Wellenform erstellen
Ersetzen Sie in cmd/visualize/main.go nur die abschließende Schleife
for x := 0; x < width; x++ durch den folgenden Block. Lassen Sie
sampleAt, maxAmplitude und scale
darüber sowie canvas.End() darunter stehen.
Verschieben Sie die bisherige SVG-Datei und führen Sie denselben Visualisierungsbefehl erneut aus.
// Draw a filled waveform (area chart style)
fillColor := "#2a9d8f"
fillOpacity := 0.5
// Follow the sampled amplitudes, then close the polygon at the center line.
// Allocate slice capacity for efficiency
xs := make([]int, 0, width+2)
ys := make([]int, 0, width+2)
xs = append(xs, 0)
ys = append(ys, centerY)
lastX := 0
for x := 0; x < width; x++ {
sampleValue := sampleAt(x)
y := centerY - int(sampleValue*scale)
xs = append(xs, x)
ys = append(ys, y)
lastX = x
}
// Add the final point on the center line to close the shape
xs = append(xs, lastX)
ys = append(ys, centerY)
// Draw the polygon
style := fmt.Sprintf("fill:%s;fill-opacity:%.2f;stroke:none", fillColor, fillOpacity)
canvas.Polygon(xs, ys, style)
Praktische Anwendungsfälle und Projektideen
Probieren Sie eine kurze Sprachaufnahme aus, indem Sie os.Open("sine.wav") durch den Pfad
Ihrer Datei ersetzen. Halten Sie die Eingabe von waveform.svg getrennt und
verschieben Sie eine vorhandene SVG-Datei vor der erneuten Ausführung. Für eine Podcast-Zeitleiste
oder einen Audioeditor ist eine Hüllkurve die nächste sinnvolle Änderung: Berechnen Sie die minimale
und maximale Amplitude im Zeitintervall jedes Pixels. So bleiben Spitzen erhalten, die bei der hier
verwendeten Abtastung einzelner Frames verloren gehen können.
Fazit und weitere Ressourcen
Bewahren Sie beim Experimentieren die WAV-Datei zusammen mit ihrer SVG-Datei auf, damit Sie den Klang mit der abgetasteten Darstellung vergleichen können. Informationen zu den hier verwendeten Bibliotheks-APIs finden Sie unter:
Für einen gehosteten Workflow finden Sie die unterstützten Eingaben und Ausgabeoptionen in der Dokumentation zum Robot 🤖 /audio/waveform von Transloadit und beim Audio-Encoding-Service.
