Genera y visualiza formas de onda de audio con Go
Crea un tono de 440 Hz y dos segundos en Go y luego convierte ese archivo WAV en una forma de onda SVG de 800 × 200. Guardarás el generador y el visualizador como comandos separados en un mismo módulo de Go, para poder ejecutar cada uno de forma independiente y probar variantes de onda cuadrada, triangular y con relleno.
Introducción a la visualización de formas de onda y sus aplicaciones
Una forma de onda representa la amplitud de las muestras en función del tiempo. Puede ayudarte a localizar pausas en una grabación o comparar señales sintéticas. Este ejemplo dibuja una vista estática del primer canal; no reproduce audio, combina canales estéreo ni ofrece un reproductor interactivo.
Configura tu entorno de Go para procesar audio
Dependencias
Los siguientes comandos usan Bash en Linux y se probaron con Go 1.26.8. Instala Go siguiendo las
instrucciones oficiales y luego comprueba que esté en tu PATH:
go version
Crea un directorio nuevo y resuelve estas versiones exactas de las bibliotecas. El
repositorio de go-audio/wav está archivado; este ejemplo usa
su versión fijada para un pequeño ejercicio local. La configuración requiere acceso a internet
para descargar los módulos. El subshell conserva tu directorio original, y
GOWORK=off aísla el ejemplo de un
espacio de trabajo de Go que lo contenga.
(
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
)
Detente si falla la configuración. Si ya existe un directorio waveform-generator, no se
modifica; elige otro directorio padre en lugar de pegar los pasos posteriores en un proyecto
existente. Guarda los dos programas completos de Go en waveform-generator/cmd/generate/main.go y
waveform-generator/cmd/visualize/main.go. Cada directorio tiene su propia función main.
Genera formas de onda básicas (senoidal, cuadrada, triangular) con Go
Guarda este programa como 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")
}
Ejecútalo desde el directorio padre donde pegaste los comandos de configuración:
(cd waveform-generator && GOWORK=off go run ./cmd/generate)
El programa escribe waveform-generator/sine.wav: audio mono, PCM de enteros de 16 bits a 44.100
muestras por segundo. Ábrelo en un reproductor de audio para escuchar el tono. Ambos programas
rechazan sobrescribir archivos de salida existentes; mueve o elimina de forma deliberada la salida
anterior antes de volver a ejecutar un programa editado.
Para generar una onda cuadrada, reemplaza la declaración de amplitude y el bucle
de generación senoidal en cmd/generate/main.go por el siguiente bloque. La salida sigue
llamándose sine.wav; mueve el WAV y el SVG anteriores a otra ubicación y luego
vuelve a ejecutar los comandos del generador y del visualizador para cada 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)
}
}
Para una onda triangular, reemplaza esa misma declaración y ese bucle por lo siguiente:
// 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))
}
}
Visualiza formas de onda con SVG
Guarda el siguiente programa como waveform-generator/cmd/visualize/main.go. Acepta archivos RIFF/WAVE clásicos
de PCM de enteros con etiqueta de formato 1 y muestras de 8, 16, 24 o 32 bits. Se rechazan las
entradas de punto flotante y WAVE_FORMAT_EXTENSIBLE. Las muestras son
sin signo a 8 bits y con signo en las demás profundidades admitidas.
Para entradas estéreo, selecciona el primer canal sin mezclarlos.
El gráfico toma muestras de 800 tramas espaciadas uniformemente desde la primera hasta la última.
Puede omitir picos entre esas tramas; usa la agregación de mínimos y máximos por píxel cuando
necesites una envolvente de amplitud.
FullPCMBuffer() carga toda la grabación en memoria, así que limita este ejercicio a
archivos locales cortos. Las comprobaciones de longitud detectan un contenido más corto que el
tamaño PCM declarado, pero no confirman que la grabación o todos sus metadatos estén intactos.
El límite explícito de bytes excluye el
relleno de RIFF
de las muestras decodificadas por la versión fijada de la biblioteca.
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")
}
Después de generar sine.wav, ejecuta el visualizador desde el mismo directorio
padre:
(cd waveform-generator && GOWORK=off go run ./cmd/visualize)
Abre waveform-generator/waveform.svg en un navegador o visor de imágenes. Contiene un fondo blanco,
una línea central en y = 100 y 800 líneas verticales de amplitud. Las muestras positivas se
extienden hacia arriba y las negativas, hacia abajo. Un tono de dos segundos contiene 880 ciclos,
por lo que las 800 columnas muestreadas forman una vista general con aliasing en lugar de un
trazado legible de cada ciclo.
Técnicas avanzadas de personalización de formas de onda
Puedes mejorar tus visualizaciones SVG con estas técnicas:
Ajuste de colores y grosor de línea
Edita las tres cadenas de estilo en cmd/visualize/main.go: cambia fill:white por fill:#f5f5f5,
stroke:#cccccc;stroke-width:1 por stroke:#e76f51;stroke-width:1 y
stroke:#0066cc;stroke-width:1 por stroke:#2a9d8f;stroke-width:2. Son estilos CSS de SVG que se pasan
a svgo, por lo que cambian el dibujo
sin mover el código de muestreo. Mueve el archivo waveform.svg anterior a otra
ubicación y luego vuelve a ejecutar el visualizador.
Creación de una forma de onda con relleno
En cmd/visualize/main.go, reemplaza solo el bucle for x := 0; x < width; x++ final por el
siguiente bloque. Conserva sampleAt, maxAmplitude y scale antes de él, y canvas.End() después.
Mueve el SVG anterior a otra ubicación y vuelve a ejecutar el mismo comando del visualizador.
// 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)
Casos de uso prácticos e ideas de proyectos
Prueba con una grabación corta de voz cambiando os.Open("sine.wav") por la ruta de tu
archivo. Mantén la entrada separada de waveform.svg y mueve cualquier SVG existente
antes de volver a ejecutar el programa. Para una línea de tiempo de un pódcast o un editor de
audio, el siguiente cambio útil es una envolvente: calcula la amplitud mínima y máxima en el
intervalo de tiempo de cada píxel. Así se conservan los picos que el muestreo de una sola trama
utilizado aquí puede omitir.
Conclusión y recursos adicionales
Conserva el WAV junto con su SVG mientras experimentas, para poder comparar el sonido con el gráfico muestreado. Para consultar las API de las bibliotecas utilizadas aquí, visita:
Para un flujo de trabajo alojado, consulta las entradas admitidas y las opciones de salida en la documentación de 🤖 /audio/waveform Robot de Transloadit y en el servicio de encoding de audio.
