Generar formas de onda de audio con Go: guía paso a paso
Crea una forma de onda en PNG a partir de un archivo de audio local usando Go para ejecutar FFmpeg. Go se encarga de la validación, la cancelación y la publicación del resultado; FFmpeg decodifica el audio y dibuja la forma de onda. Este método no necesita paquetes de Go de terceros.
Requisitos previos
Usa Go 1.22 o posterior y una instalación de FFmpeg con el codificador PNG y el filtro showwavespic. El ejemplo se probó con FFmpeg 9.0.1. MP3, PCM WAV, Vorbis en Ogg y FLAC funcionan cuando están disponibles los decodificadores correspondientes. Esta es una herramienta de línea de comandos para archivos de audio locales confiables.
Configura tu entorno de Go
Instala FFmpeg siguiendo las instrucciones para tu plataforma en la página de descargas de FFmpeg y luego crea un módulo:
mkdir waveform-generator
cd waveform-generator
go mod init waveform-generator
go mod edit -go=1.22
ffmpeg -version
Generación básica de formas de onda
Guarda este programa completo como main.go. Representa la primera pista de
audio en un PNG transparente y mezcla sus canales en mono. No permite sobrescribir un destino
existente.
package main
import (
"context"
"flag"
"fmt"
"image/png"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"time"
)
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")
command.Stdout = temporary
command.Stderr = os.Stderr
command.WaitDelay = 5 * time.Second
if err := command.Run(); err != nil {
return fmt.Errorf("render waveform: %w", err)
}
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")
}
Ejecútalo con un archivo de audio de tu propiedad:
go run . input.wav waveform.png
Abre waveform.png en un visor de imágenes. El tiempo avanza de izquierda a derecha;
los picos más altos indican mayores amplitudes de las muestras. La escala de raíz cuadrada hace más
visibles las secciones de menor volumen.
Funciones avanzadas
Procesamiento de archivos grandes en streaming
Aunque Go escribe la salida de FFmpeg directamente en disco, showwavespic almacena
temporalmente el audio decodificado en memoria para resumir toda la grabación. No es un algoritmo
de streaming con uso constante de memoria. El límite de entrada de 100 MiB y el plazo máximo de dos
minutos acotan la carga de procesamiento de este tutorial, pero el tamaño del archivo comprimido
no limita la memoria necesaria para el audio decodificado. Para grabaciones largas, genera
secciones más cortas en tareas separadas o usa un algoritmo de formas de onda que agregue muestras
de manera incremental.
Colores y estilos personalizados
Cambia el valor fijo de colors=0x167d9a en el filtro para seleccionar otro color
para la forma de onda. Usa las opciones de ancho y alto, cuyos valores se validan, para cambiar
el tamaño de la imagen:
go run . -width 800 -height 200 input.wav compact-waveform.png
La imagen resultante tiene un fondo transparente. La mezcla en mono produce una vista previa
compacta, pero las señales estéreo opuestas pueden cancelarse. Para inspeccionar los canales
por separado, elimina aformat=channel_layouts=mono, y añade split_channels=1 a las
opciones de showwavespic.
Gestión de errores y validación
El programa comprueba el tamaño y el tipo de archivo antes de decodificarlo. FFmpeg inspecciona el contenido multimedia real; la extensión del nombre del archivo por sí sola no demuestra que contenga audio válido. La ausencia de pistas de audio, los errores de decodificación, la cancelación y una salida PNG no válida impiden la publicación. Los archivos temporales se eliminan cuando la función termina, incluso si se produce un error.
El directorio de destino debe existir y admitir enlaces duros. Si ya existe un archivo de salida, se conserva, incluso si se creó durante el encoding. Usa un nombre de archivo nuevo en cada ejecución.
Pruebas
Genera un tono sintético breve y luego crea su forma de onda:
ffmpeg -nostdin -n -f lavfi -i "sine=frequency=440:duration=2" -c:a pcm_s16le tone.wav
go run . tone.wav tone.png
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height -of json tone.png
El análisis debe identificar un PNG de 1024 de ancho y 256 de alto. Prueba también con un archivo vacío, un archivo que no sea de audio y un destino existente; cada caso debe fallar sin reemplazar archivos existentes.
Optimización del rendimiento
Cada tarea inicia un proceso de FFmpeg. Mantén una concurrencia baja porque cada proceso conserva su propio audio decodificado. Guarda en caché las formas de onda terminadas según el contenido del audio y los ajustes de renderizado cuando las solicitudes repetidas provocarían que se volviera a procesar la misma grabación.
Integración en servicios web
La función generateWaveform acepta un contexto, por lo que un servidor puede transmitir
a FFmpeg el plazo máximo de una solicitud o una tarea. Un servicio público de subida también necesita
un directorio privado para cada tarea, un límite de tamaño de subida, una cola acotada de procesos
de trabajo y límites de memoria por proceso antes de decodificar contenido multimedia no confiable.
Mantén las rutas temporales y los diagnósticos del decodificador fuera de las respuestas al cliente.
Mejora tus aplicaciones de audio
Ahora tienes un comando de Go que genera una forma de onda real, valida la imagen resultante y la publica sin sobrescribir el trabajo anterior. Para obtener procesamiento de audio administrado, explora los servicios de audio de Transloadit.
