Gerar e visualizar formas de onda de áudio com Go
Crie em Go um tom de 440 Hz com dois segundos de duração e transforme esse arquivo WAV em uma forma de onda SVG de 800 × 200. Você vai salvar o gerador e o visualizador como comandos separados em um único módulo Go, para executar cada um de forma independente e testar variantes de onda quadrada, triangular e preenchida.
Introdução à visualização de formas de onda e suas aplicações
Uma forma de onda representa a amplitude das amostras em função do tempo. Ela pode ajudar você a localizar pausas em uma gravação ou a comparar sinais sintéticos. Este exemplo desenha uma visão geral estática do primeiro canal; ele não reproduz áudio, não combina canais estéreo nem oferece um player ao vivo.
Configuração do ambiente Go para processamento de áudio
Dependências
Os comandos abaixo usam Bash no Linux e foram testados com Go 1.26.8. Instale o Go seguindo as
instruções oficiais e depois confirme que ele está no seu PATH:
go version
Crie um diretório novo e resolva exatamente estas versões das bibliotecas. O
repositório go-audio/wav está arquivado; este exemplo usa a versão fixada dele
para um pequeno exercício local. A configuração precisa de acesso à internet para baixar os módulos.
O subshell mantém seu diretório original, e GOWORK=off isola o exemplo de um
workspace do Go que o contenha.
(
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
)
Pare se a configuração falhar. Um diretório waveform-generator já existente não é alterado; escolha um novo
diretório pai em vez de colar as etapas seguintes em um projeto existente. Salve os dois programas
Go completos em waveform-generator/cmd/generate/main.go e
waveform-generator/cmd/visualize/main.go. Cada diretório tem sua própria função main.
Geração de formas de onda básicas (senoidal, quadrada, triangular) com Go
Salve 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")
}
Execute-o a partir do diretório pai onde você colou a configuração:
(cd waveform-generator && GOWORK=off go run ./cmd/generate)
O programa grava waveform-generator/sine.wav: mono, PCM inteiro de 16 bits a 44.100 amostras por
segundo. Abra o arquivo em um player de áudio para ouvir o tom. Os dois programas se recusam a
sobrescrever saídas existentes; mova ou exclua deliberadamente a saída antiga antes de executar
novamente um programa editado.
Para gerar uma onda quadrada, substitua a declaração amplitude e o loop de geração da senoide em
cmd/generate/main.go pelo bloco a seguir. A saída continua se chamando sine.wav; tire do caminho o
WAV e o SVG anteriores e execute novamente os comandos do gerador e do 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 uma onda triangular, substitua essa mesma declaração e o loop por:
// 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ção de formas de onda com SVG
Salve o próximo programa como waveform-generator/cmd/visualize/main.go. Ele aceita arquivos clássicos RIFF/WAVE
com PCM inteiro, tag de formato 1 e amostras de 8, 16, 24 ou 32 bits. Entradas em ponto flutuante e
WAVE_FORMAT_EXTENSIBLE são rejeitadas. As amostras são
sem sinal em 8 bits e com sinal nas demais profundidades suportadas.
Com entrada estéreo, ele seleciona o primeiro canal sem mixar.
O gráfico amostra 800 quadros distribuídos uniformemente do primeiro ao último quadro. Ele pode
perder picos entre esses quadros; use agregação de mínimo/máximo por pixel quando precisar de um
envelope de amplitude. FullPCMBuffer() carrega a gravação inteira na memória, então limite este
exercício a arquivos locais curtos. As verificações de tamanho detectam um payload menor que o
tamanho PCM declarado, mas não garantem que a gravação ou todos os seus metadados estejam intactos.
O limite explícito de bytes exclui o padding RIFF
das amostras decodificadas pela biblioteca fixada.
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")
}
Depois de gerar sine.wav, execute o visualizador a partir do mesmo diretório pai:
(cd waveform-generator && GOWORK=off go run ./cmd/visualize)
Abra waveform-generator/waveform.svg em um navegador ou visualizador de imagens. Ele contém um fundo branco,
uma linha central em y = 100 e 800 linhas verticais de amplitude. Amostras positivas se estendem
para cima e amostras negativas, para baixo. Um tom de dois segundos contém 880 ciclos, então as 800
colunas amostradas formam uma visão geral com aliasing, e não um traçado legível de cada ciclo.
Técnicas avançadas de personalização de formas de onda
Você pode aprimorar suas visualizações SVG com estas técnicas:
Ajuste de cores e espessura das linhas
Edite as três strings de estilo em cmd/visualize/main.go: troque fill:white por fill:#f5f5f5,
stroke:#cccccc;stroke-width:1 por stroke:#e76f51;stroke-width:1 e
stroke:#0066cc;stroke-width:1 por stroke:#2a9d8f;stroke-width:2. São estilos CSS do SVG passados
para svgo, então eles mudam o desenho
sem mexer no código de amostragem. Tire do caminho o waveform.svg anterior e execute o visualizador
novamente.
Criação de uma forma de onda preenchida
Em cmd/visualize/main.go, substitua apenas o loop for x := 0; x < width; x++ final pelo
bloco a seguir. Mantenha sampleAt, maxAmplitude e scale acima dele, e canvas.End() abaixo.
Tire do caminho o SVG anterior e execute novamente o mesmo comando do 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áticos e ideias de projetos
Experimente uma gravação curta de fala trocando os.Open("sine.wav") pelo caminho do seu arquivo. Mantenha a
entrada separada de waveform.svg e mova um SVG existente antes de executar de novo. Para a linha do
tempo de um podcast ou um editor de áudio, a próxima mudança útil é um envelope: calcule a
amplitude mínima e máxima no intervalo de tempo de cada pixel. Isso preserva os picos que a
amostragem de um único quadro usada aqui pode perder.
Conclusão e recursos adicionais
Mantenha o WAV junto do SVG correspondente enquanto experimenta, para comparar o som com o gráfico amostrado. Para as APIs das bibliotecas usadas aqui, consulte:
Para um fluxo de trabalho hospedado, consulte as entradas compatíveis e as opções de saída na documentação do Robot 🤖 /audio/waveform da Transloadit e no serviço de codificação de áudio.
