Generating audio waveforms with Go: a step-by-step guide

Audio visualization helps developers create better user experiences. This tutorial shows you how to generate waveform images from audio files using Go and modern open-source tools.
Prerequisites
Before starting, ensure you have:
- Go 1.21 or later installed
- Required system dependencies:
- Linux: libasound2-dev
- macOS: No additional requirements
- Windows: No additional requirements
Setting up your Go environment
Create a new Go module and install the required dependency:
mkdir waveform-generator
cd waveform-generator
go mod init waveform-generator
go get github.com/mdlayher/waveform
Basic waveform generation
Below is a simple implementation using the mdlayher/waveform library:
package main
import (
"fmt"
"image/color"
"image/png"
"log"
"os"
"github.com/mdlayher/waveform"
)
func generateWaveform(inputPath, outputPath string) error {
f, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("failed to open audio file: %w", err)
}
defer f.Close()
opts := []waveform.Option{
waveform.Resolution(1024),
waveform.Scale(2.0),
waveform.Color(color.RGBA{255, 0, 0, 255}),
waveform.BackgroundColor(color.White),
}
img, err := waveform.New(f, opts...)
if err != nil {
return fmt.Errorf("failed to generate waveform: %w", err)
}
out, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer out.Close()
if err := png.Encode(out, img); err != nil {
return fmt.Errorf("failed to encode image: %w", err)
}
return nil
}
func main() {
if err := generateWaveform("input.mp3", "waveform.png"); err != nil {
log.Fatal(err)
}
fmt.Println("Waveform generated successfully")
}
Advanced features
Streaming large files
This example demonstrates how to handle large audio files by streaming data in chunks:
import "os"
func generateStreamingWaveform(inputPath string, chunkSize int64) error {
file, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return fmt.Errorf("failed to get file stats: %w", err)
}
if stat.Size() > 100*1024*1024 { // 100MB
opts := []waveform.Option{
waveform.Resolution(1024),
waveform.Scale(1.0),
waveform.Streaming(true),
waveform.ChunkSize(chunkSize),
}
img, err := waveform.New(file, opts...)
if err != nil {
return fmt.Errorf("failed to generate streaming waveform: %w", err)
}
out, err := os.Create("large_waveform.png")
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer out.Close()
return png.Encode(out, img)
}
return generateWaveform(inputPath, "waveform.png")
}
Custom colors and styling
Customize your waveform with specific colors and dimensions:
package main
import (
"fmt"
"image/color"
"image/png"
"os"
"github.com/mdlayher/waveform"
)
type WaveformConfig struct {
Resolution int
Scale float64
ForegroundColor color.Color
BackgroundColor color.Color
Width int
Height int
}
func generateCustomWaveform(inputPath string, config WaveformConfig) error {
f, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("failed to open audio file: %w", err)
}
defer f.Close()
opts := []waveform.Option{
waveform.Resolution(config.Resolution),
waveform.Scale(config.Scale),
waveform.Color(config.ForegroundColor),
waveform.BackgroundColor(config.BackgroundColor),
waveform.Width(config.Width),
waveform.Height(config.Height),
}
img, err := waveform.New(f, opts...)
if err != nil {
return fmt.Errorf("failed to generate waveform: %w", err)
}
out, err := os.Create("custom_waveform.png")
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer out.Close()
return png.Encode(out, img)
}
Error handling and validation
Ensure your audio files meet basic criteria before processing. Note: Import "strings" and "path/filepath" as needed.
import (
"fmt"
"os"
"strings"
"path/filepath"
)
func validateAudioFile(path string) error {
stat, err := os.Stat(path)
if err != nil {
return fmt.Errorf("failed to stat file: %w", err)
}
if stat.Size() == 0 {
return fmt.Errorf("file is empty")
}
if stat.Size() > 500*1024*1024 { // 500MB
return fmt.Errorf("file too large: %d bytes (max 500MB)", stat.Size())
}
ext := strings.ToLower(filepath.Ext(path))
supportedFormats := map[string]bool{
".mp3": true,
".wav": true,
".ogg": true,
".flac": true,
}
if !supportedFormats[ext] {
return fmt.Errorf("unsupported audio format: %s", ext)
}
return nil
}
Testing
Run tests with Go's testing package to ensure waveform generation works as expected:
import (
"image/color"
"testing"
)
func TestWaveformGeneration(t *testing.T) {
tests := []struct {
name string
input string
config WaveformConfig
wantErr bool
}{
{
name: "valid mp3",
input: "testdata/sample.mp3",
config: WaveformConfig{
Resolution: 1024,
Scale: 2.0,
ForegroundColor: color.RGBA{255, 0, 0, 255},
BackgroundColor: color.White,
Width: 800,
Height: 200,
},
wantErr: false,
},
{
name: "invalid file",
input: "nonexistent.mp3",
config: WaveformConfig{
Resolution: 1024,
Scale: 2.0,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := generateCustomWaveform(tt.input, tt.config)
if (err != nil) != tt.wantErr {
t.Errorf("generateCustomWaveform() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Performance optimization
Optimize waveform processing by leveraging parallelism. Ensure you import "runtime" as needed.
import "runtime"
func processInParallel(files []string, config WaveformConfig) error {
workers := runtime.NumCPU()
jobs := make(chan string, len(files))
results := make(chan error, len(files))
for w := 0; w < workers; w++ {
go func() {
for file := range jobs {
err := generateCustomWaveform(file, config)
results <- err
}
}()
}
for _, file := range files {
jobs <- file
}
close(jobs)
for range files {
if err := <-results; err != nil {
return fmt.Errorf("failed to process file: %w", err)
}
}
return nil
}
Web service integration
Use the following example to integrate waveform generation into a web service. The code now includes a helper to generate a waveform from an io.Reader.
import (
"image"
"image/color"
"image/png"
"io"
"net/http"
"github.com/mdlayher/waveform"
)
// generateWaveformFromReader creates a waveform image from an io.Reader using the provided config.
func generateWaveformFromReader(r io.Reader, config WaveformConfig) (image.Image, error) {
opts := []waveform.Option{
waveform.Resolution(config.Resolution),
waveform.Scale(config.Scale),
waveform.Color(config.ForegroundColor),
waveform.BackgroundColor(config.BackgroundColor),
waveform.Width(config.Width),
waveform.Height(config.Height),
}
return waveform.New(r, opts...)
}
func handleWaveform(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
file, header, err := r.FormFile("audio")
if err != nil {
http.Error(w, "Failed to read file", http.StatusBadRequest)
return
}
defer file.Close()
// File validation can be extended by checking header.Size or file content if necessary.
config := WaveformConfig{
Resolution: 1024,
Scale: 2.0,
ForegroundColor: color.RGBA{255, 0, 0, 255},
BackgroundColor: color.White,
Width: 800,
Height: 200,
}
img, err := generateWaveformFromReader(file, config)
if err != nil {
http.Error(w, "Failed to generate waveform", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "image/png")
if err := png.Encode(w, img); err != nil {
http.Error(w, "Failed to encode image", http.StatusInternalServerError)
}
}
Enhance your audio applications
Generating waveform images from audio files is straightforward with Go and modern open-source tools. These visuals improve user experience and add interactivity to your audio applications.
Looking for a robust solution to handle audio processing and waveform generation at scale? Transloadit offers powerful APIs and services that streamline your workflow.