Generar formas de onda de audio con Go: guía paso a paso
Crea una forma de onda PNG transparente a partir de una grabación existente usando Go para ejecutar FFmpeg. Go comprueba la entrada, rechaza los errores reportados por FFmpeg y publica la imagen sin reemplazar un archivo existente. FFmpeg decodifica el audio y dibuja la forma de onda; no se necesitan paquetes de Go de terceros.
Requisitos previos
Usa una versión de Go que siga recibiendo mantenimiento y una instalación de FFmpeg con el codificador
PNG y el filtro showwavespic. El ejemplo se probó en Linux con Go 1.26.8
y FFmpeg 9.0.1. También necesitas ffprobe para el procedimiento de verificación
que aparece más adelante. MP3, PCM WAV, Vorbis en Ogg y FLAC funcionan cuando están presentes los
decodificadores correspondientes. Esta es una herramienta de línea de comandos para grabaciones
locales cortas y de confianza.
Configura tu entorno de Go
Instala Go siguiendo sus instrucciones oficiales y FFmpeg desde la página de descargas de FFmpeg. Los comandos siguientes usan Bash. Comprueba las herramientas antes de crear el proyecto:
go version && ffmpeg -version && ffprobe -version
Crea un módulo nuevo. El subshell te deja en el mismo directorio padre, y
GOWORK=off lo aísla de un espacio de trabajo de Go que lo contenga:
(
mkdir waveform-generator &&
cd waveform-generator &&
GOWORK=off go mod init waveform-generator &&
GOWORK=off go mod edit -go=1.22
)
Detente si falla la configuración. Si el directorio waveform-generator ya existe, se deja
intacto; elige otro directorio padre. Sigue usando ese directorio padre para los comandos siguientes.
Generación básica de formas de onda
Guarda este programa completo como waveform-generator/main.go. Genera la forma de onda de la primera
pista de audio, mezclando sus canales en mono. Un pequeño escritor reenvía los diagnósticos de FFmpeg
a tu terminal y registra si se emitió alguno, sin acumular el registro en memoria.
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")
}
Copia un archivo de audio de tu propiedad a waveform-generator/input.wav y luego ejecuta:
(cd waveform-generator && GOWORK=off go run . input.wav waveform.png)
Si se ejecuta correctamente, el comando imprime Waveform saved.
Abre waveform-generator/waveform.png en un visor de imágenes. Tiene 1024 × 256 píxeles y un fondo
transparente. El tiempo avanza de izquierda a derecha; cada columna resume las magnitudes de las
muestras, y las columnas más altas indican picos mayores. La escala de raíz cuadrada hace más visibles
las secciones de bajo volumen. Es una vista general de la amplitud, no un trazado de muestras con signo
ni una medida de la sonoridad percibida.
Funciones avanzadas
Procesamiento de archivos grandes en streaming
Aunque Go escribe el PNG directamente en disco, showwavespic
pone en cola los fotogramas de audio decodificados
antes de dibujar la imagen. El límite de 100 MiB se aplica al archivo de entrada. El contexto de dos
minutos se aplica a FFmpeg, con hasta cinco segundos para la limpieza de E/S del proceso; la
compilación de Go queda fuera de ese plazo. Ninguno de los límites restringe la memoria usada por el
audio decodificado. Mantén las grabaciones cortas y la concurrencia baja. Para grabaciones largas, usa
un algoritmo que agregue las muestras de forma incremental.
Colores y estilos personalizados
Cambia el valor fijo 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:
(cd waveform-generator && GOWORK=off go run . -width 800 -height 200 input.wav compact-waveform.png)
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, de la
cadena del filtro y añade split_channels=1 a las opciones de showwavespic.
Guarda el programa editado y usa un nombre de archivo de salida nuevo al volver a ejecutarlo. Así,
cada canal ocupa su propia franja horizontal.
Manejo de errores y validación
FFmpeg puede recuperarse de un error de decodificación y devolver cero mientras produce un PNG válido.
Su nivel de registro error incluye errores
recuperables, por lo que este programa rechaza tanto un proceso fallido como cualquier diagnóstico de
ese nivel antes de publicar la imagen. Por ejemplo, un PCM WAV truncado dentro de su última muestra
debe fallar en lugar de indicar que se completó correctamente con una forma de onda parcial.
Esta política no demuestra que la grabación esté intacta: las advertencias se suprimen, y un decodificador puede ocultar daños sin emitir un error. Comprueba la duración esperada de una grabación y escúchala cuando sea importante que esté completa. Una extensión de archivo, un PNG válido o un código de salida cero no bastan por sí solos para establecer la integridad de la entrada.
La ausencia de pistas de audio, la cancelación y una salida PNG no válida también impiden la publicación. Los archivos temporales se eliminan cuando la función retorna, incluso si falla.
Si FFmpeg reporta Too few samples, reduce el ancho: el filtro necesita al menos una
muestra decodificada por columna. El ancho debe mantenerse entre 16 y 4096 píxeles; el alto, entre 16
y 1024 píxeles.
El directorio de destino debe existir y admitir enlaces duros. Se conserva cualquier archivo de salida existente, incluso si se crea durante la codificación. Usa un nombre de archivo nuevo en cada ejecución.
Pruebas
Genera un tono sintético corto y luego crea su forma de onda:
(
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
)
El análisis debería identificar un PNG con un ancho de 1024 y un alto de 256. Abre también
waveform-generator/tone.png: debería contener una franja de picos de igual altura a lo largo de la
imagen. Un archivo vacío, un archivo que no sea de audio o una nueva ejecución con un destino ya
existente deben provocar un fallo sin reemplazar los archivos existentes. Usa nombres nuevos tanto
para tone.wav como para tone.png al repetir el procedimiento.
Reutiliza el comando
Conserva la grabación original junto a su vista previa para poder comparar las secciones silenciosas y la mezcla estéreo con lo que escuchas. El contexto de la función también permite que un ejecutor local de tareas cancele el trabajo; no es un servidor de subidas. Para el procesamiento gestionado, consulta los flujos de trabajo compatibles en los servicios de encoding de audio de Transloadit.
