Générer et visualiser des formes d’onde audio avec Go
Créez en Go une tonalité de 440 Hz durant deux secondes, puis transformez ce fichier WAV en forme d’onde SVG de 800 × 200. Vous enregistrerez le générateur et le visualiseur comme deux commandes distinctes dans un même module Go, afin de pouvoir exécuter chacune indépendamment et essayer des variantes de forme d’onde carrée, triangulaire et remplie.
Introduction à la visualisation des formes d’onde et à ses applications
Une forme d’onde représente l’amplitude des échantillons en fonction du temps. Elle peut vous aider à repérer les pauses dans un enregistrement ou à comparer des signaux synthétiques. Cet exemple dessine un aperçu statique du premier canal ; il ne lit pas l’audio, ne combine pas les canaux stéréo et ne fournit pas de lecteur en direct.
Configurer votre environnement Go pour le traitement audio
Dépendances
Les commandes ci-dessous utilisent Bash sous Linux et ont été testées avec Go 1.26.8. Installez Go
en suivant les instructions officielles, puis vérifiez qu’il se trouve dans votre PATH :
go version
Créez un nouveau répertoire et résolvez ces versions exactes des bibliothèques. Le
dépôt go-audio/wav est archivé ; cet exemple utilise sa
version épinglée pour un petit exercice local. La configuration nécessite un accès à Internet pour
télécharger les modules. Le sous-shell conserve votre répertoire d’origine, et GOWORK=off isole
l’exemple d’un espace de travail Go englobant.
(
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
)
Arrêtez-vous si la configuration échoue. Un répertoire waveform-generator existant n’est pas modifié ;
choisissez un nouveau répertoire parent au lieu de coller les étapes suivantes dans un projet
existant. Enregistrez les deux programmes Go complets dans waveform-generator/cmd/generate/main.go et
waveform-generator/cmd/visualize/main.go. Chaque répertoire possède sa propre fonction main.
Générer des formes d’onde de base (sinusoïdale, carrée, triangulaire) avec Go
Enregistrez ce programme sous 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")
}
Exécutez-le depuis le répertoire parent dans lequel vous avez collé la configuration :
(cd waveform-generator && GOWORK=off go run ./cmd/generate)
Le programme écrit waveform-generator/sine.wav : du PCM entier 16 bits mono à 44 100 échantillons
par seconde. Ouvrez-le dans un lecteur audio pour entendre la tonalité. Les deux programmes refusent
d’écraser les fichiers de sortie existants ; déplacez ou supprimez délibérément l’ancien fichier de
sortie avant de réexécuter un programme modifié.
Pour générer une onde carrée, remplacez la déclaration amplitude et la boucle de génération
sinusoïdale dans cmd/generate/main.go par le bloc suivant. Le fichier de sortie s’appelle toujours
sine.wav ; mettez de côté les fichiers WAV et SVG précédents, puis réexécutez les commandes
du générateur et du visualiseur pour chaque variante.
// 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)
}
}
Pour une onde triangulaire, remplacez cette même déclaration et cette même boucle par :
// 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))
}
}
Visualiser des formes d’onde avec SVG
Enregistrez le programme suivant sous waveform-generator/cmd/visualize/main.go. Il accepte les fichiers RIFF/WAVE
classiques en PCM entier avec la balise de format 1 et des échantillons de 8, 16, 24 ou 32 bits.
Les entrées à virgule flottante et WAVE_FORMAT_EXTENSIBLE sont rejetées. Les échantillons sont
non signés à 8 bits et signés aux autres profondeurs prises en charge.
Pour une entrée stéréo, il sélectionne le premier canal sans mixage.
Le tracé échantillonne 800 trames réparties uniformément de la première à la dernière trame. Il
peut manquer des pics situés entre ces trames ; utilisez une agrégation minimum/maximum par pixel
lorsque vous avez besoin d’une enveloppe d’amplitude. FullPCMBuffer() charge l’intégralité de
l’enregistrement en mémoire ; limitez donc cet exercice à de courts fichiers locaux. Les contrôles
de longueur détectent une charge utile plus courte que sa taille PCM déclarée, mais ne garantissent
pas que l’enregistrement ou l’ensemble de ses métadonnées soient intacts. La limite explicite en
octets exclut le remplissage RIFF
des échantillons décodés par la bibliothèque épinglée.
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")
}
Après avoir généré sine.wav, exécutez le visualiseur depuis le même répertoire parent :
(cd waveform-generator && GOWORK=off go run ./cmd/visualize)
Ouvrez waveform-generator/waveform.svg dans un navigateur ou une visionneuse d’images. Il contient un fond blanc,
une ligne centrale à y = 100 et 800 lignes d’amplitude verticales. Les échantillons positifs
s’étendent vers le haut et les échantillons négatifs vers le bas. Une tonalité de deux secondes
contient 880 cycles ; les 800 colonnes échantillonnées forment donc un aperçu soumis au repliement
(aliasing) plutôt qu’un tracé lisible de chaque cycle.
Techniques avancées de personnalisation des formes d’onde
Vous pouvez améliorer vos visualisations SVG grâce à ces techniques :
Ajuster les couleurs et l’épaisseur des lignes
Modifiez les trois chaînes de style dans cmd/visualize/main.go : remplacez fill:white par fill:#f5f5f5,
stroke:#cccccc;stroke-width:1 par stroke:#e76f51;stroke-width:1 et
stroke:#0066cc;stroke-width:1 par stroke:#2a9d8f;stroke-width:2. Il s’agit de styles CSS SVG transmis
à svgo ; ils modifient donc le dessin
sans déplacer le code d’échantillonnage. Mettez de côté l’ancien fichier waveform.svg, puis
réexécutez le visualiseur.
Créer une forme d’onde remplie
Dans cmd/visualize/main.go, remplacez uniquement la dernière boucle for x := 0; x < width; x++ par le
bloc suivant. Conservez sampleAt, maxAmplitude et scale au-dessus, et canvas.End() en dessous.
Mettez de côté le SVG précédent et réexécutez la même commande du visualiseur.
// 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)
Cas d’usage pratiques et idées de projets
Essayez un court enregistrement vocal en remplaçant os.Open("sine.wav") par le chemin de votre fichier.
Gardez l’entrée distincte de waveform.svg, et déplacez le SVG existant avant de réexécuter le
programme. Pour une frise chronologique de podcast ou un éditeur audio, la prochaine modification
utile est une enveloppe : calculez l’amplitude minimale et maximale dans l’intervalle de temps de
chaque pixel. Cela préserve les pics que l’échantillonnage à trame unique utilisé ici peut manquer.
Conclusion et ressources complémentaires
Pendant vos essais, conservez le fichier WAV à côté de son SVG afin de pouvoir comparer le son avec le tracé échantillonné. Pour les API des bibliothèques utilisées ici, consultez :
Pour un flux de travail hébergé, consultez les entrées prises en charge et les options de sortie dans la documentation du Robot 🤖 /audio/waveform (English) de Transloadit et du service d’encodage audio.
