mordaunt.dev/code

code / nativeaudio

nativeaudio

audio playback for Go

  • Go
  • Library
  • Audio
  • Playback
  • Native
  • Bindings
  • Cross-Platform
  • Windows
  • macOS
  • Linux
clone
https://mordaunt.dev/code/nativeaudio
release
v1.2.02026-09-21105 commits1 commit since
license
Unlicense OR MIT
activity
47 commitsthis year, since 2021

nativeaudio is a convenience package that provides a Go interface over native audio APIs.

Output is PCM s16le, playable directly. Stacks that take an io.Reader of s16le, such as oto and Ebitengine, need no adapter. For the ones that want float64 sample pairs, such as beep, the pcm subpackage converts.

This package was inspired by the utter lack of audio decoders for AAC, which it turns out is a closed codec that Windows and macOS have bought licenses for.

Windows is implemented via the Media Foundation and macOS is implemented on AudioToolbox/AVFoundation.

Linux links FFmpeg’s libraries directly. Other platforms, and any build without cgo, run the FFmpeg binary as a sub-process instead.

The linking is dynamic, which is the case LGPL exists to permit. The original concern was Go’s static linking, which LGPL genuinely does not sit well with; dynamic linking does not raise it. Nothing is vendored or redistributed either way, so the patent question still sits with whoever installed FFmpeg.

One caveat worth knowing. Many distributions build FFmpeg with --enable-gpl, which makes the libraries GPL rather than LGPL; Arch’s is GPL-3.0. The FSF’s position is that linking, statically or dynamically, makes a combined work, so anyone shipping a binary linked against such a build inherits that obligation. Building with CGO_ENABLED=0 selects the sub-process path, which is arm’s length and raises no such question.

go get git.sr.ht/~jackmordaunt/nativeaudio

package main

import (
	"log"

	"git.sr.ht/~jackmordaunt/nativeaudio"
)

func main() {
	d, err := nativeaudio.New()
	if err != nil {
		log.Fatal(err)
	}
	defer d.Close()

	pcm, format, err := d.DecodeFile("audio.m4a")
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("%d bytes of PCM, %+v", len(pcm), format)
}

Or, to just hear it:

import "git.sr.ht/~jackmordaunt/nativeaudio/play"

play.File("audio.m4a")

API

  • New() returns a Decoder holding the platform state. Close it when you are done. Independent parts of a program can each hold their own, and closing one does not disturb another.
  • Decoder.DecodeFile(path) and Decoder.Decode(data) return s16le PCM and a Format. The core package has no audio-output dependency.
  • Decoder.StreamFile(path) and Decoder.Stream(data) return a Stream, an io.Reader over the same PCM, so a long track never has to sit in memory whole. Format is known before the first read. Close it when done. Every native backend decodes incrementally, and the sub-process backend pipes when given a file; only decoding from memory without a native backend buffers up front.
  • Format reports SampleRate, Channels and BytesPerSample, which is always 2.
  • A Decoder is safe for concurrent use, and Close waits for decodes already in flight.
  • New(WithLimits(...)) bounds how long a decode may run and how much PCM it may produce. Malformed audio can otherwise make a native decoder grind indefinitely, so the default budget is finite. Untrusted input should set its own.
  • FFmpegLoad, FFmpegDecode and FFmpegStream shell out to ffmpeg regardless of platform, and need no Decoder.
  • The pcm subpackage adapts the output to other audio stacks. pcm.NewStreamer presents s16le as pairs of float64 samples, which satisfies beep’s Streamer and StreamCloser without importing beep, since Go interfaces are structural. It takes an io.Reader, so it works over a Stream or over buffered PCM.
  • The play subpackage plays PCM through oto. The core package builds for every target Go supports; play is bounded by its audio backend, which needs cgo on Linux and has no FreeBSD support. Its context is fixed to the first file’s sample rate and channel count for the life of the process.

Versions

v1.1.0 is the first release to use. It replaced the package-level Start, End, Load, Decode and Play with a Decoder value and the play subpackage, renamed Format.BitDepth to BytesPerSample, made the Media Foundation bindings internal, and added streaming decodes, bounded decodes and the pcm adapters.

v1.0.0 is retracted. It went out before the API had settled, and it decoded 24-bit sources to 24-bit rather than the s16le documented here. Nothing depended on it.

Format support

Measured by TestFormatSupport, which generates a file per format with ffmpeg and decodes it. CI runs it on each platform and publishes the result, so this table comes from machines rather than from vendor documentation.

Format Windows macOS Linux and others
AAC-LC in MP4 yes yes yes
AAC-LC in ADTS yes yes yes
MP3 yes yes yes
WAV, 16-bit yes yes yes
WAV, 24-bit yes yes yes
FLAC yes yes yes
ALAC in MP4 yes yes yes
Opus in Ogg yes yes yes
Vorbis in Ogg yes not measured yes
WMA v2 yes no yes

Both native backends decode far more than the AAC this package was written for, so it is useful well beyond that. The only gap either of them has is WMA on macOS, which AudioToolbox rejects outright rather than mangling. Vorbis on macOS is unmeasured because the ffmpeg build there could not produce a file to try.

Output is s16le whatever goes in, including from a 24-bit source. Decoded lengths differ slightly between backends for codecs that carry priming samples, which is why the tests compare against a per-backend reference rather than one golden file.

AAC profiles

These deserve separate billing, because a decoder that handles only the base profile does not fail on the others. High efficiency AAC is an AAC-LC core plus an extension that older decoders are meant to ignore, and ignoring it yields audio at half the sample rate with the treble missing and no error raised. A backend that supports only AAC-LC is therefore worse than one that rejects the file.

This table is from vendor documentation rather than measured, because generating the high efficiency profiles needs an encoder ffmpeg does not ship by default.

Profile Windows macOS ffmpeg
AAC-LC yes, multichannel yes yes
HE-AAC v1 yes, multichannel yes yes
HE-AAC v2 yes, stereo yes yes
xHE-AAC yes, Windows 11 yes partial
AAC-LD and AAC-ELD no yes yes

Changelog

full log
v1.2.02026-09-21

incremental decoding on macOS and Linux

  • c7adf6fdocs: record that every native backend now streams
  • 561ed97macos: decode incrementally rather than buffering behind Stream
  • ddd6252ci: mirror the clone rather than the build's own checkout
  • 25ab199ci: point the mirror at the renamed github repository
  • 958747bci: cross-compile the 32-bit Windows target
  • 99be5fcci: run the suite under AddressSanitizer on Linux
  • and 5 more
v1.1.02026-09-18

nativeaudio v1.1.0

  • d53fc25docs: describe the new API, the versions and the measured format matrix
  • e0de7f9go.mod: retract v1.0.0
  • 21fbc82test: probe what each platform decoder actually accepts
  • d4b316ctest: fuzz the decode, stream and abandoned-stream paths
  • 99210bfpcm: adapt the output to other audio stacks
  • bb7c6e9play: bound the wait for playback to finish
  • and 10 more
v1.0.02026-09-17

nativeaudio v1.0.0

  • 6a7c98ddocs: describe the public API and the v1.0.0 breaking changes
  • e63bbc4audio: fix inverted package comment and document the Play format limit
  • 3bdbe9dtest: cover the ffmpeg fallbacks when ffmpeg is installed
  • 87ea90dtest: play a silent file twice and check each call runs full length
  • 9ffd772test: cover more formats, Start/End cycles and mid-stream corruption
  • a55f0actest: require equal length and allow under 1 LSB mean error
  • and 71 more

Mirrored to sourcehut and GitHub.