ebitengine/oto: a low-level audio playback library for Go, without Cgo
A low-level library to play sound on multiple platforms . Linux, FreeBSD, OpenBSD Oto uses PulseAudio on Linux and BSD systems via the pure-Go package github.com/jfreymuth/pulse, though BSD systems are not tested well.
At a glance
- What is it?
- Oto v3 plays PCM through a single Context and any number of Players, using PulseAudio on Linux and BSD, WASAPI or winmm on Windows, and platform APIs elsewhere. It is a building block for game and app audio, not a mixer or a decoder.
- Who is it for?
- Adopt Oto when you need raw PCM output in a Go program and want to keep decoders, mixing and resampling outside the audio layer. Do not adopt it if you expect the library to decode MP3 or Ogg for you, or if you need several independent audio contexts in one process.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What Oto actually does, and who it is aimed at
Oto is a low-level library to play sound, in the words of its README. It does not read files, decode compressed formats, mix tracks or apply effects. It takes an io.Reader that yields raw PCM bytes and pushes those bytes to the operating system's audio device. Everything above that line is your problem.
That places it in a specific niche. If you are writing a game engine, a music player or a tool that synthesises audio, you already have or intend to write a decoder and a mixer. Oto is the last mile: the part that talks to WASAPI, CoreAudio, PulseAudio or the browser's Web Audio API. Ebitengine itself is the most visible consumer of that arrangement, but the library is published as a standalone Go module under github.com/ebitengine/oto/v3 and can be used without the engine.
The audience is therefore Go developers who are comfortable owning the audio pipeline. If you want a one-call playFile function, Oto is the wrong layer and you will spend your first hour writing code that a higher-level library would have given you.
One Context, many Players: the shape of the API
The README names two main components. A Context handles interactions with the OS and audio drivers, and there can only be one context in your program. From that context you create any number of Players, each given an io.Reader from which it reads sound bytes and plays them.
That single-context rule is a hard constraint, not a style preference. It follows from how the underlying drivers are opened: the context owns the device handle, and the players are streams multiplexed onto it. Code that tries to create a second context, for example inside a test helper or a plugin system, will not have a supported path.
The other rule the README states plainly is that a single io.Reader must not be used by multiple players. Each player consumes its reader sequentially, so sharing one reader between two players would interleave reads and produce garbage rather than two copies of the same sound.
Internally the data flow has three stages. Bytes move from the io.Reader into the player's internal buffer, and from that buffer to the audio device. When the buffer hands data to the device is not guaranteed, so a small delay is normal. Player.BufferedSize() reports how much data is currently sitting in that buffer, which is the hook you use if you need to reason about latency or synchronise playback with something else on screen. The buffer's size is not fixed: a player can be type-asserted to oto.BufferSizeSetter and given a new size with SetBufferSize.
That type assertion works because players implement both a Player interface and a BufferSizeSetter interface. It is a deliberate Go idiom rather than an accident, but it also means the buffer size is not part of the constructor options, and you only discover it by reading the advanced usage section.
Installing Oto and playing your first file
Oto is a Go module, so installation is a go get against the v3 path. Because the module path carries the major version, the import in your source must match it exactly.
go get github.com/ebitengine/oto/v3The README's worked example pairs Oto with github.com/hajimehoshi/go-mp3 for decoding, which is a separate module you fetch the same way:
go get github.com/hajimehoshi/go-mp3On Linux and BSD, the README says Oto uses PulseAudio through the pure-Go package github.com/jfreymuth/pulse. If the PulseAudio server is not discoverable automatically, set PULSE_SERVER. When no PulseAudio server is reachable, Oto falls back to ALSA; that fallback loads libasound.so.2 dynamically at runtime, so no ALSA development headers are needed to build, but the shared library must be present when the program runs. This is the first thing to check if a build succeeds and the binary then fails to produce sound on a minimal container image.
Once the modules are in place, the shape of a program is: open or read the file, wrap it in a decoder, create the context with the format the decoder produces, create a player from that context and the decoder, call Play, and keep the process alive long enough for the sound to finish. The README's streaming variant uses os.Open rather than os.ReadFile and warns in a comment not to close the file before you finish playing. It also notes that you may need to keep a reference to the original file object alive, for instance by holding it in a struct, or you may hear static. That is a garbage collection hazard rather than an audio bug, and it is the single most common way a first attempt goes wrong.
For longer material, streaming is the recommended path. The README frames the trade-off directly: reading the whole file into memory is fine for small files but costs memory and load time for a long song.
Where Oto stops being the right tool
The most obvious limitation is that Oto is not a decoder. Its own examples reach for go-mp3 to turn an MP3 into PCM. If your application plays Ogg Vorbis, FLAC, WAV or AAC, you supply that decoder and you are responsible for matching its sample rate, channel count and bit depth to the options you hand to the context. Nothing in Oto will resample a 44.1 kHz stream to a device running at 48 kHz for you.
The single-context rule is the second boundary. Programs that want two independent output devices, or that want to isolate audio per document in a plugin architecture, have no supported way to do it through this API.
The third is platform confidence. The README lists FreeBSD and OpenBSD as supported, then adds that BSD systems are not tested well. That sentence should be read literally: the code paths exist, the testing does not. If your deployment target is a BSD variant, budget time for the PulseAudio or ALSA fallback behaviour on that specific system rather than assuming parity with Linux.
Finally, the buffer model means Oto is not a low-latency API in the sense a DAW requires. Data may sit in the player's internal buffer before it reaches the device, and the README does not publish a latency figure. For a game's sound effects that is usually irrelevant. For live monitoring or tightly synchronised playback, the buffering is a design property you would have to measure yourself.
How Oto differs from higher-level Go audio libraries
The natural alternative for a Go developer is a library that bundles decoding with playback, so that a single call takes a file path and produces sound. The difference is architectural rather than a matter of quality. A bundled library owns the decoder, which means it decides which formats exist, how they are buffered and how errors surface. Oto owns only the device: the decoder is an interface you satisfy, and swapping MP3 for a synthesiser means swapping the io.Reader, not the audio layer.
That split is what makes Oto usable in a game engine, where sound effects are often generated or mixed at runtime rather than read from a file. It is also what makes it more work for a simple player. The README's own examples are written against go-mp3, which is a fair signal about the intended division of labour: Oto plus a decoder, not Oto alone.
On the platform side, the notable property is that Windows, macOS, Linux, FreeBSD, OpenBSD and WebAssembly all build without Cgo. That is why the repository depends on github.com/ebitengine/purego, which is used to call into system libraries without a C toolchain. It is a real advantage for cross-compilation and for CI images that do not carry a C compiler.
Cross-compiling, maintenance and licence
Cross-compiling to macOS, Windows, Linux or BSD is, per the README, a matter of setting GOOS to darwin, windows, linux or the relevant BSD flavour. For other targets you install the libraries for the target architecture and set CGO_ENABLED=1, because Go disables Cgo on cross-compiles by default.
There is one sharp edge worth repeating from the README. On FreeBSD, building with CGO_ENABLED=0, for example when cross-compiling, additionally requires the flag -gcflags="github.com/ebitengine/purego/internal/fakecgo=-std". Native FreeBSD builds, where Cgo is enabled by default, need nothing extra. That flag points at an internal package path, so it is the kind of thing that can break silently if the dependency's internals are reorganised.
The module requires Go 1.25.0 and depends on purego v0.11.0, jfreymuth/pulse v0.1.3 and golang.org/x/sys v0.47.0. Those are few dependencies for a library that spans this many platforms, and it means the upgrade surface is small. The most recent release is v3.4.1, dated 2026-08-15, and the last push to the default branch is the same date. Earlier releases were v3.4.0 in October 2025 and v3.3.3 in March 2025, so the cadence is a few releases a year rather than continuous churn.
Oto is licensed under Apache-2.0. That is a permissive licence with an explicit patent grant, and it is compatible with the Go ecosystem's usual expectations, but the repository's LICENSE file is the authoritative text and anything beyond that is a question for your own counsel.
Editorial conclusion
Adopt Oto when you need raw PCM output in a Go program and want to keep decoders, mixing and resampling outside the audio layer. Do not adopt it if you expect the library to decode MP3 or Ogg for you, or if you need several independent audio contexts in one process. Before committing, verify that a Context can be created on your target machine (PulseAudio reachable, or libasound.so.2 present for the ALSA fallback) and that your decoder's output format matches the context options you pass to oto.NewContext.
Frequently asked questions
Does ebitengine/oto require Cgo to build?
On Windows, macOS, Linux, FreeBSD, OpenBSD and WebAssembly the README lists no Cgo requirement, which is why the module depends on purego. Some other targets, including console platforms, may need a working C/C++ toolchain.
Which audio backend does ebitengine/oto use on Linux?
It uses PulseAudio through the pure-Go package github.com/jfreymuth/pulse. When no PulseAudio server is reachable it falls back to ALSA, loading libasound.so.2 dynamically at runtime.
Can I create more than one Oto Context in a program?
No. The README states that the context handles interactions with the OS and audio drivers and that there can only be one context in your program. You create any number of Players from that single context instead.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ebitengine-oto)