Skip to content

Latest commit

 

History

126 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mago

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.

License

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.

What this library is

mago gives you a way to use miniaudio from Go without requiring CGO in your Go build.

The basic model is:

  1. mago embeds the platform-specific native bridge as a shared library.
  2. At runtime, it automatically extracts this library to your user cache directory (e.g. ~/.cache/mago/ on Linux) if it's not already there.
  3. It loads the library from the cache using purego.
  4. Use the Go wrapper for version checks, context creation, device enumeration, and playback callbacks.

Important runtime rule:

  • Building the shared library is NOT required. Everything is bundled by default.
  • CGO is NOT required. mago uses purego for runtime loading.

If you need to use a custom version of the library or want to override the automatic loading, you can still do so.

Strict version matching

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.

Current features

The current implementation includes:

  • runtime loading through purego
  • strict miniaudio version validation
  • context creation
  • backend device enumeration
  • playback device creation
  • callback-based audio output
  • a node graph for building routing racks, with per-bus volume and scheduled node states
  • miniaudio's high-level engine for sounds, groups, fades and 3D spatialization
  • a higher-level audio subpackage for ergonomic stream playback
  • a speaker subpackage compatible with gopxl/beep/speaker

Examples included in this repo:

  • examples/null-playback — plays silence through the null backend and proves callback flow
  • examples/list-devices — enumerates devices for common backends
  • examples/tones — generates tones and plays them through a real device
  • examples/audio-wav — uses the higher-level audio package with an in-memory WAV clip

How to use it

1. Open the library and create a context

Just call mago.Open(). By default, it will handle the extraction and loading of the embedded library for you:

lib, err := mago.Open()
if err != nil {
	panic(err)
}
defer lib.Close()

Manual overrides

If you need to use a specific library file instead of the embedded one, you can override the behavior:

  1. Explicit path: Use mago.WithLibraryPath("/path/to/lib").
  2. Environment variable: Set MAGO_MINIAUDIO_LIB=/path/to/lib.

If an explicit path or environment variable is provided, mago will use that instead of the embedded one.

If both of those are empty, and for some reason the embedded library is missing (e.g. an unsupported architecture), mago will search for the platform-appropriate runtime library name in several default locations (like the current working directory or next to the executable).

2. Create a context

Once the library is open, you can create a context for audio operations:

ctx, err := lib.NewContext(mago.BackendNull)
if err != nil {
	panic(err)
}
defer ctx.Close()

3. Enumerate devices

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)
}

4. Play audio via callback

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)
}

Higher-level audio package

If you want a more idiomatic playback API, use github.com/darkliquid/mago/audio.

The audio subpackage currently provides:

  • loading WAV streams from io.Reader and io.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-wav demo shows loading a WAV stream from memory and changing speed / direction / fades at runtime.

NodeGraph and Engine

The root package also exposes miniaudio's own mixing layers, for when a single callback is not enough.

mago.NodeGraph is a routable pipeline: create nodes from the graph (NodeGraph.NewDataSourceNode, NewBiquadNode, NewSplitterNode, NewDelayNode and the rest), attach their output buses, and read the endpoint frame by frame. Per-bus volume and node start/stop times are scheduled on the graph's global clock.

mago.Engine is the high-level layer on top of it. It owns a node graph, a resource manager and, unless NoDevice is set, a playback device:

engine, err := lib.NewEngine(mago.DefaultEngineConfig(2, 48000))
music, err := engine.NewSoundGroup(mago.SoundGroupConfig{})
sound, err := engine.NewSoundFromFile("track.wav", mago.SoundConfig{Group: music})

sound.SetVolume(0.8)
sound.SetPosition(mago.Vec3{X: 4})
sound.SetAttenuationModel(mago.AttenuationInverse)
engine.Listener(0).SetPosition(mago.Vec3{})

sound.Start()

A new Sound starts stopped, so call Start to hear it; a SoundGroup starts playing immediately. Groups nest, so their volume and pan cascade down to the sounds attached to them. Sound.SetEndCallback reports when a sound finishes, and the callback runs on the mixing thread, so signal another goroutine rather than touching the sound from it.

Choosing between audio and Engine

The audio package is a pure-Go mixer built on a device callback: it decodes WAV itself, owns its own Clip/Stream model, and needs nothing from miniaudio beyond the device. Engine is miniaudio's own mixer and resource manager: it loads and decodes more formats, spatializes sounds in 3D, and mixes through a node graph you can route into.

Neither replaces the other, and both are CGO-free. Reach for audio (or speaker) when you want a small, predictable software mixer over in-memory data. Reach for Engine when you want file loading, groups, fades and 3D positioning without writing the mixing yourself.

Threading

For both layers, only Read is lock-free and belongs to a single audio thread. Everything else, including creating and closing nodes, sounds and groups, is a control-thread operation. A sound outlives nothing: close sounds and groups before the engine, and nodes before the graph, because miniaudio's own teardown reaches into its parent.

Engine can also run without a device at all. Set EngineConfig.NoDevice and drive it with Engine.Read, which is what examples/engine does and what makes the engine testable with no audio hardware.

beep-compatible speaker package

If you already have github.com/gopxl/beep streamers, use github.com/darkliquid/mago/speaker.

It mirrors the beep/speaker API closely:

  • speaker.Init
  • speaker.Play
  • speaker.PlayAndWait
  • speaker.Lock / speaker.Unlock
  • speaker.Clear
  • speaker.Suspend / speaker.Resume
  • speaker.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 than oto.
  • Suspend and Resume map to stopping and starting the underlying mago.Device.

Demos

List devices:

go run ./examples/list-devices

Play tones on a real device:

go run ./examples/tones

Use the higher-level audio package demo:

go run ./examples/audio-wav

Inspect a device's state, name, info and master volume:

go run ./examples/device-report

Capture from an input device and report peak/RMS (falls back to the null backend):

go run ./examples/capture --duration 2s

Echo captured input back to the output through a duplex device:

go run ./examples/duplex-echo --duration 3s

Route miniaudio's own diagnostics into Go logging:

go run ./examples/logging

Convert PCM between sample formats, and format/channel/rate in one call:

go run ./examples/convert-formats

Inspect standard channel maps and down-mix with the channel converter:

go run ./examples/channel-map

Resample between sample rates with the resampler and data converter:

go run ./examples/resample

Round-trip in-memory buffers and ring buffers:

go run ./examples/buffers

Decode an encoded stream from memory and from a file:

go run ./examples/decode

Encode a tone to WAV in memory and to a file:

go run ./examples/encode

Supply PCM from Go with a custom data source:

go run ./examples/datasource

Build a routing rack with the node graph:

go run ./examples/nodegraph

Play a sound through miniaudio's engine, with groups and a moving listener:

go run ./examples/engine

All of the examples accept --backend and fall back to the null backend when no real device backend is available, so they run on a headless machine. They load the embedded library and need no compiler; set MAGO_MINIAUDIO_LIB to run them against a locally built library instead. convert-formats, channel-map, resample, buffers, decode, encode, datasource, nodegraph and engine need no audio device at all.

You 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 wasapi

Or 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/tones

Supported demo overrides:

  • --backend / MAGO_BACKEND
  • --device-index / MAGO_DEVICE_INDEX
  • --device-name / MAGO_DEVICE_NAME
  • --duration / MAGO_TONE_DURATION

Building the shared library

Recommended builder

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/buildlib

You can also target a specific upstream miniaudio release:

go run ./internal/cmd/buildlib -version 0.11.25

There is also a convenience wrapper:

bash native/build.sh

That 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 -lpthread

Linux

Manual Linux command:

cc -std=c11 -O2 -fPIC -shared \
  -Wl,-soname,libminiaudio.so \
  -o native/libminiaudio.so \
  native/miniaudio_bridge.c \
  -ldl -lm -lpthread

macOS

You 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 -lpthread

Windows

You 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 -luuid

Or from an MSYS2/MinGW shell:

gcc -std=c11 -O2 -shared \
  -o native/miniaudio.dll \
  native/miniaudio_bridge.c \
  -lwinmm -lole32 -luuid

FreeBSD / NetBSD

Manual BSD command:

cc -std=c11 -O2 -fPIC -shared \
  -o native/libminiaudio.so \
  native/miniaudio_bridge.c \
  -lm -lpthread

Cross-compiling shared libraries

mago 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 cc or clang
  • 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 ./...

Runtime deployment

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.

Development notes

  • The bridge source lives in native/miniaudio_bridge.c.
  • Generated Go symbol bindings are produced with:
go generate ./...
  • Tests:
go test ./...

Status

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.

About

A pure-go wrapper around the miniaudio library

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages