mago is a Go wrapper around miniaudio.h that avoids Go-side CGO.
Instead of compiling C into your Go package, mago expects miniaudio to be built as a standalone shared library and then loads that library at runtime with purego.
The Go wrapper currently supports the same platforms this package is wired up to use with purego:
- Linux
- macOS
- Windows
- FreeBSD
- NetBSD
OpenBSD and other targets are intentionally skipped here unless purego support exists for them.
Like miniaudio itself, this project is intended to be available under a public domain / MIT dual-license model.
- You may treat it as public domain where that is recognized.
- Where public domain dedication is not recognized or not desired, you may use it under the MIT license instead.
miniaudio.h itself follows that same licensing model, and this project is designed to mirror it.
mago gives you a way to use miniaudio from Go without requiring CGO in your Go build.
The basic model is:
- Build the native bridge as a shared library.
- Ship that shared library with your application.
- Load it from Go with
mago.Open(). - Use the Go wrapper for version checks, context creation, device enumeration, and playback callbacks.
Important runtime rule:
- Building the shared library is not required at application build time.
- Providing the correct shared library artifact at runtime is required.
In other words, your Go binary does not need CGO, but it does need access to the right dynamic library:
- Linux:
libminiaudio.so - macOS:
libminiaudio.dylib - Windows:
miniaudio.dll - FreeBSD / NetBSD:
libminiaudio.so
miniaudio does not promise backward compatibility, so mago performs a strict runtime version check when opening the library.
The loaded shared library must report the exact miniaudio version vendored by this repository. Right now that is:
0.11.25
If the loaded library version does not match exactly, mago.Open() fails immediately.
The current implementation includes:
- runtime loading through
purego - strict
miniaudioversion validation - context creation
- backend device enumeration
- playback device creation
- callback-based audio output
- a higher-level
audiosubpackage for ergonomic stream playback - a
speakersubpackage compatible withgopxl/beep/speaker
Examples included in this repo:
examples/null-playback— plays silence through the null backend and proves callback flowexamples/list-devices— enumerates devices for common backendsexamples/tones— generates tones and plays them through a real deviceexamples/audio-wav— uses the higher-levelaudiopackage with an in-memory WAV clip
By default, mago looks for the platform-appropriate runtime library name in one of these places:
native/<platform-library-name>under the current working directory<platform-library-name>under the current working directorynative/<platform-library-name>next to the executable<platform-library-name>next to the executable
You can also override the path explicitly:
lib, err := mago.Open(mago.WithLibraryPath("/absolute/path/to/runtime-library"))Or via environment variable:
export MAGO_MINIAUDIO_LIB=/absolute/path/to/runtime-librarylib, err := mago.Open()
if err != nil {
panic(err)
}
defer lib.Close()
ctx, err := lib.NewContext(mago.BackendNull)
if err != nil {
panic(err)
}
defer ctx.Close()playback, capture, err := ctx.Devices()
if err != nil {
panic(err)
}
for i, device := range playback {
fmt.Printf("[%d] %s (default=%v)\n", i, device.Name, device.IsDefault)
}config := mago.DefaultPlaybackDeviceConfig()
config.DeviceIndex = 0
config.Channels = 2
config.SampleRate = 48000
config.PeriodSizeInFrames = 256
config.DataCallback = func(device *mago.Device, output unsafe.Pointer, input unsafe.Pointer, frameCount uint32) {
samples := unsafe.Slice((*float32)(output), int(frameCount*config.Channels))
for i := range samples {
samples[i] = 0
}
}
device, err := ctx.NewPlaybackDevice(config)
if err != nil {
panic(err)
}
defer device.Close()
if err := device.Start(); err != nil {
panic(err)
}If you want a more idiomatic playback API, use github.com/darkliquid/mago/audio.
The audio subpackage currently provides:
- loading WAV streams from
io.Readerandio.ReadSeeker - stream start / pause / resume / stop / close
- looping
- release of loaded clip buffers
- volume control
- playback speed adjustment
- reverse playback
- software resampling during playback
- crossfades between streams
This makes it the recommended entry point if you want application-facing playback code instead of low-level callback wiring.
Example:
engine, err := audio.Open(audio.DefaultConfig())
if err != nil {
panic(err)
}
defer engine.Close()
clip, err := engine.Load(reader)
if err != nil {
panic(err)
}
defer clip.Release()
stream, err := engine.Play(clip, audio.DefaultPlayOptions())
if err != nil {
panic(err)
}
defer stream.Close()
stream.SetLooping(true)
stream.SetVolume(0.5)
stream.SetSpeed(1.25)
stream.SetReverse(false)Notes:
audio.Open()starts the playback device automatically.- The initial loader supports WAV streams; higher-level format support can be added on top later.
- The included
examples/audio-wavdemo shows loading a WAV stream from memory and changing speed / direction / fades at runtime.
If you already have github.com/gopxl/beep streamers, use github.com/darkliquid/mago/speaker.
It mirrors the beep/speaker API closely:
speaker.Initspeaker.Playspeaker.PlayAndWaitspeaker.Lock/speaker.Unlockspeaker.Clearspeaker.Suspend/speaker.Resumespeaker.Close
Example:
if err := speaker.Init(beep.SampleRate(48_000), 512); err != nil {
panic(err)
}
defer speaker.Close()
speaker.Play(beep.StreamerFunc(func(samples [][2]float64) (int, bool) {
for i := range samples {
samples[i][0] = 0
samples[i][1] = 0
}
return len(samples), true
}))Notes:
- Playback is backed by
mago's miniaudio callback rather thanoto. SuspendandResumemap to stopping and starting the underlyingmago.Device.
List devices:
go run ./examples/list-devicesPlay tones on a real device:
go run ./examples/tonesUse the higher-level audio package demo:
go run ./examples/audio-wavYou can override the backend/device selection. The accepted backend values are platform-dependent:
go run ./examples/tones --backend pulse --device-index 0 --duration 3s
go run ./examples/tones --backend coreaudio
go run ./examples/tones --backend wasapiOr with environment variables:
MAGO_BACKEND=pulse MAGO_DEVICE_INDEX=0 MAGO_TONE_DURATION=2s go run ./examples/tones
MAGO_BACKEND=coreaudio go run ./examples/tones
MAGO_BACKEND=wasapi go run ./examples/tonesSupported demo overrides:
--backend/MAGO_BACKEND--device-index/MAGO_DEVICE_INDEX--device-name/MAGO_DEVICE_NAME--duration/MAGO_TONE_DURATION
The repository includes a Go-native builder that chooses the right output name and compiler flags for the current host platform and downloads miniaudio.h automatically:
go run ./internal/cmd/buildlibYou can also target a specific upstream miniaudio release:
go run ./internal/cmd/buildlib -version 0.11.25There is also a convenience wrapper:
bash native/build.shThat produces a platform-appropriate runtime library in native/, for example:
native/libminiaudio.so
Equivalent manual command:
cc -std=c11 -O2 -fPIC -shared \
-Wl,-soname,libminiaudio.so \
-o native/libminiaudio.so \
native/miniaudio_bridge.c \
-ldl -lm -lpthreadManual Linux command:
cc -std=c11 -O2 -fPIC -shared \
-Wl,-soname,libminiaudio.so \
-o native/libminiaudio.so \
native/miniaudio_bridge.c \
-ldl -lm -lpthreadYou can build a .dylib with clang:
clang -std=c11 -O2 -fPIC -dynamiclib \
-o native/libminiaudio.dylib \
native/miniaudio_bridge.c \
-framework CoreAudio \
-framework AudioToolbox \
-framework AudioUnit \
-framework Foundation \
-framework CoreFoundation \
-framework CoreServices \
-lm -lpthreadYou can build a .dll with MinGW-w64:
x86_64-w64-mingw32-gcc -std=c11 -O2 -shared \
-o native/miniaudio.dll \
native/miniaudio_bridge.c \
-lwinmm -lole32 -luuidOr from an MSYS2/MinGW shell:
gcc -std=c11 -O2 -shared \
-o native/miniaudio.dll \
native/miniaudio_bridge.c \
-lwinmm -lole32 -luuidManual BSD command:
cc -std=c11 -O2 -fPIC -shared \
-o native/libminiaudio.so \
native/miniaudio_bridge.c \
-lm -lpthreadmago itself avoids Go-side CGO, but the shared library is still native code, so cross-compiling the shared library requires an appropriate C toolchain for the target platform.
Typical examples:
- Linux -> Linux: system
ccorclang - Linux -> Windows:
x86_64-w64-mingw32-gcc - macOS -> macOS:
clang
For Go package cross-compilation, note that purego requires a special flag for freebsd and netbsd when building with CGO_ENABLED=0:
GOOS=freebsd GOARCH=amd64 go build -gcflags=github.com/ebitengine/purego/internal/fakecgo=-std ./...
GOOS=netbsd GOARCH=amd64 go build -gcflags=github.com/ebitengine/purego/internal/fakecgo=-std ./...The important deployment rule is:
- Your Go binary can be built without CGO
- Your deployment still needs the correct shared library for the target OS/architecture
For example:
- Linux deployment needs
libminiaudio.so - macOS deployment needs
libminiaudio.dylib - Windows deployment needs
miniaudio.dll - FreeBSD / NetBSD deployment needs
libminiaudio.so
You can place that artifact next to the executable, under a known app directory, or point mago at it explicitly with WithLibraryPath(...) or MAGO_MINIAUDIO_LIB.
- The bridge source lives in
native/miniaudio_bridge.c. - Generated Go symbol bindings are produced with:
go generate ./...- Tests:
go test ./...This project is currently an early cross-platform wrapper focused on:
- playback
- device enumeration
- runtime dynamic loading
- real-device and null-backend demos
There is room to expand the wrapper surface substantially, but the current priority is to keep the ABI boundary explicit and safe.