The only pure Go, high-quality audio resampler (that I know of). No CGO, no C toolchain, no build headaches.
Based on libsoxr (the SoX Resampler library) by Rob Sykes, this is a complete Go reimplementation delivering professional-grade quality without any C code or external dependencies.
The library implements polyphase FIR filtering with Kaiser window design for professional-grade sample rate conversion, following the multi-stage architecture that makes libsoxr one of the highest-quality resamplers available.
- Features
- Installation
- Quick Start
- Quality Presets
- Precision Modes (float32 vs float64)
- Architecture
- API Reference
- Command-Line Tools
- Performance
- Dependencies
- Contributing
- Sponsor
- License
- Acknowledgments
- 100% Pure Go - No CGO, no external C libraries, no build complexity. Just
go getand build anywhere - Cross-platform - Works on Linux, macOS, Windows, ARM, WebAssembly - anywhere Go runs
- Multiple quality presets - From quick/low-latency to very high quality mastering (8-bit to 32-bit precision)
- Selectable precision - Generic float32/float64 processing paths with type-safe SIMD operations
- Polyphase FIR filtering - Efficient multi-stage resampling architecture with cubic coefficient interpolation
- Kaiser window design - Optimal stopband attenuation with configurable parameters
- SIMD acceleration - AVX2/SSE optimizations via tphakala/simd for both float32 and float64
- Multi-channel support - Process stereo, surround, and multi-channel audio (up to 256 channels)
- Parallel channel processing - ~1.7x speedup for stereo, up to 8x for 7.1 surround audio
- Streaming API - Process audio in chunks with proper state management
- Zero-allocation streaming - Caller-owned output buffers (
ProcessInto/ProcessFloat32Into) for float32 and float64 hot paths - soxr-quality algorithms - Implements libsoxr's multi-stage architecture for professional-grade quality
go get github.com/tphakala/go-audio-resamplerRequires Go 1.26 or later.
package main
import (
"fmt"
"log"
resampling "github.com/tphakala/go-audio-resampler"
)
func main() {
// Resample mono audio from 44.1kHz to 48kHz
input := generateSineWave(44100, 1000, 1.0) // 1 second of 1kHz sine
output, err := resampling.ResampleMono(input, 44100, 48000, resampling.QualityHigh)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Resampled %d samples to %d samples\n", len(input), len(output))
}package main
import (
"log"
resampling "github.com/tphakala/go-audio-resampler"
)
func main() {
// Create a resampler for CD to DAT conversion (mono; for multi-channel
// audio use ProcessMulti, see "Multi-Channel Streaming" below)
config := &resampling.Config{
InputRate: 44100,
OutputRate: 48000,
Channels: 1,
Quality: resampling.QualitySpec{Preset: resampling.QualityHigh},
}
r, err := resampling.New(config)
if err != nil {
log.Fatal(err)
}
// Process audio in chunks
for chunk := range audioChunks {
output, err := r.Process(chunk)
if err != nil {
log.Fatal(err)
}
writeOutput(output)
}
// Flush remaining samples at end of stream
final, err := r.Flush()
if err != nil {
log.Fatal(err)
}
writeOutput(final)
}A streaming resampler has a startup deficit: the internal filter needs a few samples of history before it can emit correctly filtered output, so the first Process calls in a stream withhold roughly Latency() samples that later calls make up. Callers that need a fixed number of output samples per callback, such as a portaudio or miniaudio audio callback, should sit a small FIFO between the resampler and the callback, primed with Latency() samples of silence. Latency() matches the measured deficit to within about 2 samples, so priming with it keeps callbacks fed in practice; any 1-2 sample shortfall self-heals because the deficit-driven buffer sizing catches up within the first few callbacks.
Latency() is available on SimpleResampler and SimpleResamplerFloat32 (the NewEngine/NewEngineFloat32 path). Resamplers built from New(config) expose the same figure through GetLatency() (and GetInfo().Latency): the startup deficit in output samples, accurate to within a few samples across the pipeline's stages, so it primes a FIFO the same way.
r, err := resampling.NewEngineFloat32(44100, 48000, resampling.QualityHigh)
if err != nil {
log.Fatal(err)
}
fifo := make([]float32, r.Latency()) // prime with the startup deficit
for chunk := range audioChunks {
out, err := r.Process(chunk)
if err != nil {
log.Fatal(err)
}
fifo = append(fifo, out...)
// deliver fixed-size slices from fifo to the audio callback here
}
// End of stream: drain the filter tail exactly once.
tail, err := r.Flush()
if err != nil {
log.Fatal(err)
}
fifo = append(fifo, tail...)Flush is end-of-stream only. It pushes padding through the filter to drain its tail, so calling it once per chunk instead of once at the very end injects that padding into the middle of the stream and produces audible clicks at every chunk boundary. Call Flush exactly once, after the last Process call for the stream; a second Flush call is a no-op that returns an empty slice. Reset discards all filter state and is for starting an unrelated stream, never for use between chunks of the same stream. Each SimpleResampler or SimpleResamplerFloat32 processes one channel, so a multi-channel stream keeps one persistent instance per channel alive for the duration of the stream.
See examples/streaming for the complete, runnable FIFO pattern.
For allocation-sensitive pipelines, use caller-owned output buffers:
r, err := resampling.NewEngine(48000, 32000, resampling.QualityMedium)
if err != nil {
log.Fatal(err)
}
for chunk := range audioChunks {
out := make([]float64, r.EstimateOutput(len(chunk)))
n, err := r.ProcessInto(chunk, out)
if err != nil {
log.Fatal(err)
}
writeOutput(out[:n]) // Only output[:n] is valid
}Contract notes:
EstimateOutput(len(input))returns a conservative upper bound forProcessInto.ProcessIntoreturnsErrBufferTooSmallifoutputcannot hold results.- On
ErrBufferTooSmall, processing state is not advanced (safe to retry with a larger buffer). - Samples beyond
output[:n]are unspecified and should be ignored.
When using New(config), access ProcessInto via type assertion:
type processIntoResampler interface {
ProcessInto(input, output []float64) (int, error)
EstimateOutput(inputLen int) int
}
base, _ := resampling.New(config)
into, ok := base.(processIntoResampler)
if !ok {
log.Fatal("ProcessInto not available")
}For float32 audio, NewEngineFloat32 exposes a float32-native ProcessInto. The
underlying engine is float32-native, so there is no float64 round-trip:
r, err := resampling.NewEngineFloat32(48000, 32000, resampling.QualityMedium)
if err != nil {
log.Fatal(err)
}
for chunk := range audioChunks {
out := make([]float32, r.EstimateOutput(len(chunk)))
n, err := r.ProcessInto(chunk, out)
if err != nil {
log.Fatal(err)
}
writeOutput(out[:n]) // Only out[:n] is valid
}When using New(config), the float32 caller-owned-output method is
ProcessFloat32Into. It runs through the float64 pipeline using reused scratch
buffers, so it also reports 0 allocs/op once warm. Access it via type assertion:
type processFloat32IntoResampler interface {
ProcessFloat32Into(input, output []float32) (int, error)
EstimateOutput(inputLen int) int
}
base, _ := resampling.New(config)
into, ok := base.(processFloat32IntoResampler)
if !ok {
log.Fatal("ProcessFloat32Into not available")
}Both float32 methods follow the same contract as float64 ProcessInto: size the
output with EstimateOutput(len(input)), and on ErrBufferTooSmall no state is
advanced (safe to retry with a larger buffer).
// Common sample rate conversions
r, _ := resampling.NewCDtoDAT(resampling.QualityHigh) // 44.1kHz -> 48kHz
r, _ := resampling.NewDATtoCD(resampling.QualityHigh) // 48kHz -> 44.1kHz
r, _ := resampling.NewCDtoHiRes(resampling.QualityHigh) // 44.1kHz -> 88.2kHz
// Simple mono/stereo resamplers
r, _ := resampling.NewSimple(inputRate, outputRate)
r, _ := resampling.NewStereo(inputRate, outputRate, resampling.QualityMedium)
r, _ := resampling.NewMultiChannel(inputRate, outputRate, 6, resampling.QualityHigh)
// Direct engine access for maximum performance
r, _ := resampling.NewEngine(44100, 48000, resampling.QualityHigh)// Process stereo as separate channels
leftOut, rightOut, err := resampling.ResampleStereo(
leftChannel, rightChannel,
44100, 48000,
resampling.QualityHigh,
)
// Interleave/deinterleave helpers
interleaved := resampling.InterleaveToStereo(left, right)
left, right := resampling.DeinterleaveFromStereo(interleaved)When streaming multi-channel audio with ProcessMulti, drain with FlushMulti
rather than Flush. Flush only drains channel 0, so it would silently drop the
delay-line tails of the remaining channels at end-of-stream:
config := &resampling.Config{
InputRate: 44100,
OutputRate: 48000,
Channels: 6,
Quality: resampling.QualitySpec{Preset: resampling.QualityHigh},
}
r, _ := resampling.New(config)
for chunk := range multiChannelChunks { // chunk is [][]float64, one slice per channel
out, err := r.ProcessMulti(chunk)
if err != nil {
log.Fatal(err)
}
writeOutput(out)
}
// Drain every channel's tail (FlushMulti is an optional interface).
if mf, ok := r.(resampling.MultiFlusher); ok {
tail, _ := mf.FlushMulti()
writeOutput(tail)
}| Preset | Precision | Attenuation | Use Case |
|---|---|---|---|
QualityQuick |
8-bit | ~54 dB | Preview, real-time with low CPU |
QualityLow |
16-bit | ~102 dB | Speech, low-bandwidth audio |
QualityMedium |
16-bit | ~102 dB | General music playback |
QualityHigh |
20-bit | ~126 dB | Studio production, streaming |
QualityVeryHigh |
28-bit | ~175 dB | Mastering, archival |
The following THD (Total Harmonic Distortion) measurements compare this library against libsoxr reference implementation for 44.1kHz → 48kHz conversion at 1kHz test tone:
| Preset | libsoxr THD | Go THD | Difference |
|---|---|---|---|
| Low | -146.25 dB | -142.28 dB | +4.0 dB |
| Medium | -130.61 dB | -129.79 dB | +0.8 dB |
| High | -155.19 dB | -155.58 dB | -0.4 dB ✓ |
| VeryHigh | -162.22 dB | -162.19 dB | +0.03 dB |
Negative difference means Go implementation has better THD (lower distortion). Go
figures come from the bundled quality-regression suite and are reproducible with
go test ./internal/engine -run TestQualityRegression_THD -v.
Key findings:
- High and VeryHigh presets match or exceed libsoxr quality, within 0.5 dB (High slightly better, VeryHigh essentially identical)
- Low and Medium track libsoxr closely: Medium is within ~1 dB and Low within ~4 dB, trading a little stopband headroom for lower passband ripple
- All presets achieve SNR (Signal-to-Noise Ratio) matching libsoxr within measurement tolerance
- Downsampling (e.g., 48kHz → 32kHz) shows substantially better THD (below -190 dB across presets) thanks to the anti-aliasing filters
Why does Low show better THD than Medium? Both presets are 16-bit (same noise floor), so the difference is filter geometry, not precision. Low keeps its passband to 80% of Nyquist with a wide, gentle transition, which is easy to realize cleanly and yields excellent THD, but discards more high-frequency content. Medium extends the passband to 90% of Nyquist with a sharper transition: it preserves more bandwidth but leaks slightly more energy near Nyquist, so its single-tone THD is higher. Low and Medium are a bandwidth-versus-distortion trade, not a simple quality ranking, and libsoxr's own LQ/MQ recipes show the same ordering.
The table above uses 44.1kHz → 48kHz, an exact-rational ratio where the polyphase filter needs no sub-phase interpolation. Ratios that do engage the sub-phase Catmull-Rom interpolation banks are pinned separately (measured 2026-07-17, guarded by TestQualityRegression_THD_ActiveInterpolation):
| Conversion | High | Medium | Low |
|---|---|---|---|
| 32kHz → 44.1kHz | -162.05 dB | -134.95 dB | -144.22 dB |
| 44.1kHz → 64kHz | -151.63 dB | -135.03 dB | -139.47 dB |
config := &resampling.Config{
InputRate: 44100,
OutputRate: 48000,
Channels: 1,
Quality: resampling.QualitySpec{
Preset: resampling.QualityCustom,
Precision: 24, // Bits of precision
PhaseResponse: 50, // 0=minimum, 50=linear, 100=maximum
PassbandEnd: 0.95, // Preserve up to 95% of Nyquist
StopbandBegin: 0.99, // Attenuate above 99% of Nyquist
},
}The library supports both float32 and float64 processing paths:
// float64 for maximum precision (default)
r64, _ := resampling.NewEngine(44100, 48000, resampling.QualityHigh)
output64, _ := r64.Process(input64)
// float32 for ~2x SIMD throughput
r32, _ := resampling.NewEngineFloat32(44100, 48000, resampling.QualityHigh)
output32, _ := r32.Process(input32)Precision comparison (44.1kHz → 48kHz, High quality):
| Mode | THD @ 1kHz | Use Case |
|---|---|---|
| float64 | -145.25 dB | Maximum precision, critical audio |
| float32 | -145.50 dB | Higher throughput, general use |
Both modes achieve equivalent quality for most use cases. Use float32 when processing large amounts of audio data where throughput is more important than theoretical precision limits.
The library implements a multi-stage resampling architecture similar to libsoxr:
Input -> [DFT Pre-Stage] -> [Polyphase FIR] -> Output
(2x) (fine ratio)
- Integer ratios (2x, 3x, etc.): Single DFT stage for efficiency
- Non-integer ratios (44.1kHz→48kHz): DFT pre-upsampling + polyphase stage
- Polyphase decomposition: Reduces computation by processing only needed output phases
- Cubic coefficient interpolation: Sub-phase interpolation for excellent high-frequency THD
- Kaiser window: Optimal FIR filter design with configurable stopband attenuation
// Main resampler interface
type Resampler interface {
Process(input []float64) ([]float64, error)
ProcessFloat32(input []float32) ([]float32, error)
ProcessMulti(input [][]float64) ([][]float64, error)
Flush() ([]float64, error) // drains channel 0 only; see MultiFlusher
GetLatency() int
Reset()
GetRatio() float64
}
// Optional interface for draining every channel of a multi-channel stream.
// Flush() only drains channel 0, so after ProcessMulti use FlushMulti to
// avoid dropping the delay-line tails of channels 1..N-1.
type MultiFlusher interface {
FlushMulti() ([][]float64, error)
}
// Configuration
type Config struct {
InputRate float64 // Input sample rate in Hz
OutputRate float64 // Output sample rate in Hz
Channels int // Number of audio channels
Quality QualitySpec // Quality settings
MaxInputSize int // Optional buffer size hint
EnableSIMD bool // Enable SIMD optimizations
EnableParallel bool // Enable parallel channel processing
}r, _ := resampling.New(config)
info := resampling.GetInfo(r)
fmt.Printf("Algorithm: %s\n", info.Algorithm)
fmt.Printf("Filter length: %d taps\n", info.FilterLength)
fmt.Printf("Latency: %d samples\n", info.Latency)
fmt.Printf("Memory: %d bytes\n", info.MemoryUsage)
fmt.Printf("SIMD: %v (%s)\n", info.SIMDEnabled, info.SIMDType)Resample WAV audio files:
# Build
go build -o bin/resample-wav ./cmd/resample-wav
# Usage
resample-wav -rate 48 input.wav output.wav # Resample to 48kHz
resample-wav -rate 16 -quality high speech.wav out.wav # Downsample speech
resample-wav -rate 96 music.wav hires.wav # Upsample to hi-res
resample-wav -rate 48 -fast input.wav output.wav # Float32 precision (~40% faster)
resample-wav -rate 48 -parallel=false input.wav out.wav # Disable parallel processingParallel channel processing is enabled by default for stereo/multichannel files.
Interactive demo showing library capabilities:
go build -o bin/resample ./cmd/resample
./bin/resample -demoFilter complexity comparison for 44.1kHz → 48kHz conversion:
| Quality | DFT Stage | Polyphase Stage | Total Taps |
|---|---|---|---|
| Low | 132 taps × 2 phase | 32 taps × 80 phase | ~2820 |
| Medium | 132 taps × 2 phase | 32 taps × 80 phase | ~2820 |
| High | 166 taps × 2 phase | 64 taps × 80 phase | ~5452 |
| VeryHigh | 166 taps × 2 phase | 100 taps × 80 phase | ~8332 |
Actual performance varies by CPU. The simd library automatically uses AVX2/SSE when available. Setting GOAMD64=v3 can provide minor additional speedup for non-SIMD code paths.
For multi-channel audio, enabling parallel processing provides significant speedup:
config := &resampling.Config{
InputRate: 44100,
OutputRate: 48000,
Channels: 2,
Quality: resampling.QualitySpec{Preset: resampling.QualityHigh},
EnableParallel: true, // Process channels concurrently
}| Configuration | Processing Time | Speedup |
|---|---|---|
| Stereo (sequential) | 15.7 ms | 1.0x |
| Stereo (parallel) | 9.4 ms | 1.67x |
| 5.1 Surround (parallel) | ~17.1 ms | ~5.5x vs sequential |
| 7.1 Surround (parallel) | ~20.0 ms | ~7.0x vs sequential |
Benchmark: 1 second of audio, 44.1kHz → 48kHz, High quality, Intel i7-1260P.
Parallel processing is safe because each channel maintains independent filter state. Mono audio is unaffected (no channels to parallelize).
- github.com/tphakala/simd - SIMD operations for filter convolution (the core library's only runtime dependency)
- github.com/go-audio/wav - WAV file I/O (CLI tools only)
- github.com/go-audio/audio - PCM buffer types (CLI tools only)
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure
go test ./...passes - Submit a pull request
Pure Go audio DSP is a small niche, and keeping this library correct, fast, and matched to libsoxr quality takes ongoing work. If it saves you from wrestling with CGO and C toolchains, please consider sponsoring its development.
Every sponsorship, large or small, is appreciated and helps me dedicate time to maintenance, new features, and support.
This library is licensed under the GNU Lesser General Public License v2.1 (LGPL-2.1).
See the LICENSE file for the full license text.
This library is based on libsoxr by Rob Sykes (licensed under LGPL-2.1). The following were derived from libsoxr:
- Multi-stage resampling pipeline (DFT pre-stage + polyphase)
- Polyphase filter bank decomposition
- Quality preset parameters and filter specifications
- Integer phase arithmetic for polyphase interpolation
The Kaiser window filter design follows well-established DSP literature, particularly the work of James Kaiser on optimal window functions.
We gratefully acknowledge Rob Sykes' excellent work on libsoxr.