Gerando formas de onda de áudio com Go: um guia passo a passo
Crie uma forma de onda em PNG a partir de um arquivo de áudio local usando Go para executar o FFmpeg. O Go cuida da validação, do cancelamento e da publicação da saída; o FFmpeg decodifica o áudio e desenha a forma de onda. Essa abordagem não precisa de nenhum pacote Go de terceiros.
Pré-requisitos
Use o Go 1.22 ou posterior e uma instalação do FFmpeg com o codificador PNG e o filtro showwavespic. O exemplo foi testado com o FFmpeg 9.0.1. MP3, PCM WAV, Vorbis em Ogg e FLAC funcionam quando os decodificadores correspondentes estão presentes. Esta é uma ferramenta de linha de comando para arquivos de áudio locais confiáveis.
Configurando seu ambiente Go
Instale o FFmpeg seguindo as instruções para a sua plataforma na página de download do FFmpeg e depois crie um módulo:
mkdir waveform-generator
cd waveform-generator
go mod init waveform-generator
go mod edit -go=1.22
ffmpeg -version
Geração básica de forma de onda
Salve este programa completo como main.go. Ele renderiza a primeira faixa de áudio em um
PNG transparente, mixando os canais em mono. Ele se recusa a sobrescrever um 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")
}
Execute-o com um arquivo de áudio que pertença a você:
go run . input.wav waveform.png
Abra waveform.png em um visualizador de imagens. O tempo corre da esquerda para a direita; picos
mais altos indicam amplitudes de amostra maiores. A escala de raiz quadrada deixa os trechos de
baixo volume mais visíveis.
Recursos avançados
Streaming de arquivos grandes
Embora o Go grave a saída do FFmpeg diretamente em disco, showwavespic mantém o áudio
decodificado em buffer para resumir a gravação inteira. Este não é um algoritmo de streaming com
memória constante. O limite de entrada de 100 MiB e o prazo de dois minutos limitam a carga de
trabalho deste tutorial, mas o tamanho do arquivo compactado não limita a memória decodificada.
Para gravações longas, renderize trechos mais curtos em tarefas separadas ou use um algoritmo de
forma de onda que agregue as amostras de forma incremental.
Cores e estilo personalizados
Altere o valor fixo colors=0x167d9a no filtro para escolher outra cor de forma de onda.
Use as flags validadas de largura e altura para mudar o tamanho da imagem:
go run . -width 800 -height 200 input.wav compact-waveform.png
A saída tem fundo transparente. A mixagem em mono gera uma prévia compacta, mas sinais estéreo
opostos podem se cancelar. Para inspecionar os canais separadamente, remova aformat=channel_layouts=mono,
e adicione split_channels=1 às opções de showwavespic.
Tratamento de erros e validação
O programa verifica o tamanho e o tipo do arquivo antes da decodificação. O FFmpeg inspeciona a mídia real; a extensão do nome do arquivo, por si só, não comprova que um arquivo contém áudio válido. Faixas de áudio ausentes, falhas de decodificação, cancelamento e saída PNG inválida impedem a publicação. Os arquivos temporários são removidos quando a função retorna, inclusive em caso de falha.
O diretório de destino precisa existir e oferecer suporte a hard links. Uma saída existente, incluindo uma criada durante a codificação, é preservada. Use um nome de arquivo novo a cada execução.
Testes
Gere um tom sintético curto e depois renderize-o:
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
A inspeção com o ffprobe deve identificar um PNG com largura 1024 e altura 256. Teste também um arquivo vazio, um arquivo que não seja de áudio e um destino existente; cada um deve falhar sem substituir arquivos existentes.
Otimização de desempenho
Cada tarefa inicia um processo do FFmpeg. Mantenha a concorrência baixa, porque cada processo guarda o próprio áudio decodificado. Armazene em cache as formas de onda concluídas com base no conteúdo do áudio e nas configurações de renderização quando, de outra forma, requisições repetidas processariam novamente a mesma gravação.
Integração com serviços web
A função generateWaveform aceita um contexto, então um servidor pode repassar ao FFmpeg o prazo de
uma requisição ou tarefa. Um serviço público de upload também precisa de um diretório privado
para cada tarefa, um limite de tamanho de upload, uma fila de workers limitada e limites de
memória por processo antes de decodificar mídia não confiável. Mantenha caminhos temporários e
diagnósticos do decodificador fora das respostas ao cliente.
Aprimore seus aplicativos de áudio
Agora você tem um comando Go que renderiza uma forma de onda real, valida a imagem resultante e a publica sem sobrescrever trabalhos anteriores. Para processamento de áudio gerenciado, explore os serviços de áudio da Transloadit.
