Gerar formas de onda de áudio com Go: guia passo a passo
Crie um PNG transparente da forma de onda de uma gravação existente usando Go para executar o FFmpeg. O Go verifica a entrada, rejeita erros reportados pelo FFmpeg e publica a imagem sem substituir um arquivo existente. O FFmpeg decodifica o áudio e desenha a forma de onda; nenhum pacote Go de terceiros é necessário.
Pré-requisitos
Use uma versão do Go que ainda receba manutenção e uma instalação do FFmpeg com o codificador PNG e o
filtro showwavespic. O exemplo foi
testado no Linux com Go 1.26.8 e FFmpeg 9.0.1. Você também precisa de
ffprobe para o procedimento de verificação abaixo. MP3, PCM WAV, Vorbis em Ogg e
FLAC funcionam quando os decodificadores correspondentes estão presentes. Esta é uma ferramenta de
linha de comando para gravações locais curtas e confiáveis.
Configurar seu ambiente Go
Instale o Go seguindo as instruções oficiais e o FFmpeg pela página de download do FFmpeg. Os comandos abaixo usam Bash. Verifique as ferramentas antes de criar o projeto:
go version && ffmpeg -version && ffprobe -version
Crie um novo módulo. O subshell mantém você no mesmo diretório pai, e
GOWORK=off isola o módulo de um workspace Go que o contenha:
(
mkdir waveform-generator &&
cd waveform-generator &&
GOWORK=off go mod init waveform-generator &&
GOWORK=off go mod edit -go=1.22
)
Pare se a configuração falhar. Um diretório waveform-generator existente permanece
intacto; escolha outro diretório pai. Continue usando esse diretório pai nos comandos abaixo.
Geração básica de formas de onda
Salve este programa completo como waveform-generator/main.go. Ele renderiza a primeira faixa de
áudio, misturando seus canais em mono. Um pequeno componente de escrita encaminha os diagnósticos do
FFmpeg ao terminal e registra se algum foi emitido, sem acumular o log na memória.
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")
}
Copie um arquivo de áudio seu para waveform-generator/input.wav e execute:
(cd waveform-generator && GOWORK=off go run . input.wav waveform.png)
Em caso de sucesso, o comando imprime Waveform saved. Abra
waveform-generator/waveform.png em um visualizador de imagens. A imagem tem 1024 × 256 pixels e fundo
transparente. O tempo avança da esquerda para a direita;
cada coluna resume as magnitudes das amostras, e colunas mais altas indicam picos maiores. A escala
de raiz quadrada torna as seções de baixo volume mais visíveis. Esta é uma visão geral da amplitude,
não um traçado de amostras com sinal nem uma medida do volume percebido.
Recursos avançados
Processamento de arquivos grandes em fluxo
Embora o Go grave o PNG diretamente no disco, showwavespic
coloca quadros de áudio decodificados em uma fila
antes de desenhar a imagem. O limite de 100 MiB se aplica ao arquivo de entrada. O contexto com
prazo de dois minutos se aplica ao FFmpeg, com até cinco segundos para finalizar a E/S do processo;
a compilação do Go fica fora desse prazo. Nenhum dos limites restringe a memória usada pelo áudio
decodificado. Mantenha as gravações curtas e a concorrência baixa. Para gravações longas, use um
algoritmo que agregue amostras de forma incremental.
Cores e estilos personalizados
Altere o valor fixo colors=0x167d9a no filtro para selecionar outra cor para a forma de onda.
Use as opções validadas de largura e altura para alterar o tamanho da imagem:
(cd waveform-generator && GOWORK=off go run . -width 800 -height 200 input.wav compact-waveform.png)
A mistura 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, da string do filtro e adicione
split_channels=1 às opções de showwavespic. Salve o programa editado e use
um novo nome de arquivo de saída ao executar novamente. Cada canal passa a ocupar sua própria faixa
horizontal.
Tratamento de erros e validação
O FFmpeg pode se recuperar de um erro de decodificação e retornar zero, ainda produzindo um PNG
válido. Seu nível de log error inclui erros
recuperáveis, então este programa rejeita tanto um processo que falhou quanto qualquer diagnóstico
nesse nível antes de publicar. Por exemplo, um PCM WAV truncado dentro de sua última amostra deve
resultar em falha, em vez de reportar sucesso com uma forma de onda parcial.
Essa política não comprova que a gravação está intacta: os avisos são suprimidos, e um decodificador pode ocultar danos sem emitir um erro. Verifique a duração esperada de uma gravação e escute-a quando a integridade do conteúdo for importante. Uma extensão de arquivo, um PNG válido ou um status de saída zero, isoladamente, não comprovam a integridade da entrada.
A ausência de faixas de áudio, o cancelamento e uma saída PNG inválida também impedem a publicação. Os arquivos temporários são removidos quando a função retorna, inclusive em caso de falha.
Se o FFmpeg reportar Too few samples, reduza a largura: o filtro precisa de pelo menos uma
amostra decodificada por coluna. A largura deve ficar entre 16 e 4096 pixels; a altura deve ficar
entre 16 e 1024 pixels.
O diretório de destino deve existir e oferecer suporte a links físicos. Uma saída existente, inclusive uma criada durante a codificação, é preservada. Use um novo nome de arquivo a cada execução.
Testes
Gere um tom sintético curto e renderize-o:
(
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
)
A análise deve identificar um PNG com largura 1024 e altura 256. Abra também
waveform-generator/tone.png: a imagem deve conter uma faixa de picos de mesma altura ao longo de
toda a sua extensão. Um arquivo vazio, um arquivo que não seja de áudio ou uma nova execução com um
destino já existente devem resultar em falha, sem substituir arquivos existentes. Use novos nomes
tanto para tone.wav quanto para tone.png ao repetir o procedimento.
Reutilizar o comando
Mantenha a gravação original junto com sua prévia para comparar as seções silenciosas e a mistura estéreo com o que você ouve. O contexto da função também permite que um executor local de tarefas cancele o trabalho; ele não é um servidor de upload. Para processamento gerenciado, veja os fluxos de trabalho compatíveis nos serviços de codificação de áudio da Transloadit.
