Oto v3: A Low-Level Go Sound Library That Skips Cgo on Desktop
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 is a pure-Go audio playback library for Go developers who need cross-platform sound without Cgo on desktop systems. It handles context management, buffered playback, and platform-specific fallbacks, but its PulseAudio dependency on Linux and BSD warrants attention.
- Who is it for?
- Adopt Oto v3 if you write Go applications that need low-level audio playback across desktop platforms and want to avoid Cgo. Avoid it if your Linux or BSD target lacks PulseAudio and you cannot install libasound.so.2 at runtime, or if you need high-level features like mixing or effects.
- 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 received new commits within the last day.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- ebitengine/oto1,983 ★ · +41
| Repository | 2026-07-31 | 2026-11-14 |
|---|---|---|
| ebitengine/oto | 1.9k | 2k |
41 Stars in the last 106 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
What Oto Solves and Who Needs It
The core abstraction is a Context and Players. There can only be one Context per program, because the context manages the OS-level audio session. From that context you create any number of Players, each tied to a single io.Reader. The README warns that one io.Reader must not be used by multiple players. This design keeps the API small but puts the burden of managing concurrent playback on the caller.
The Mechanism: Context, Players, and Buffered Data Flow
Oto's architecture is visible in the README's advanced usage section. Data moves from the io.Reader into an internal buffer, then from that buffer to the audio device. The transfer from buffer to device is not guaranteed to happen immediately, so there can be a small delay between reading bytes and hearing them. The Player.BufferedSize() method returns how much data is currently in that internal buffer. This lets you monitor latency or decide when to pause reading from the source. The buffer size is adjustable via a type assertion: myPlayer.(oto.BufferSizeSetter).SetBufferSize(newBufferSize). This works because players implement both the Player interface and the BufferSizeSetter interface. The README does not specify what units the buffer size takes or what the default is, so you would need to inspect the source or documentation to set it correctly.
Platform Coverage and the PulseAudio Fallback
Oto lists support for Windows, macOS, Linux, FreeBSD, OpenBSD, Android, iOS, WebAssembly, Nintendo Switch, and Xbox. On Windows, macOS, Linux, and BSDs, it requires no Cgo. That is a significant advantage for cross-compilation and for environments where a C toolchain is undesirable. On Linux and BSD, Oto uses PulseAudio through the pure-Go package github.com/jfreymuth/pulse. The README admits that BSD systems are not tested well, so treat FreeBSD and OpenBSD support as experimental. If the PulseAudio server is not discoverable automatically, you can set the PULSE_SERVER environment variable. When no PulseAudio server is reachable, Oto falls back to ALSA. This fallback also avoids Cgo: libasound.so.2 is loaded dynamically at runtime, so you do not need ALSA development headers to build, but the shared library must be present on the target machine at runtime. That is a real constraint: a headless Linux server without PulseAudio and without libasound.so.2 will fail.
Getting It Running: Commands and Configuration
To use Oto, you add it as a dependency and write a small Go program. The README shows a minimal example for playing an MP3 from memory: read the file, wrap it in a bytes.Reader, decode with go-mp3, then create a context and a player. The exact code for creating the context and player is truncated in the README, but the pattern is standard for Oto: oto.NewContext(options) returns a context and an error, then context.NewPlayer(reader) returns a player. For file streaming, you use os.Open instead of reading the entire file into memory. The README stresses that the file object must be kept alive; closing it or letting it be garbage collected results in static. That means you need to hold a reference to the file for the duration of playback, for example by storing it in a struct. Cross-compiling to desktop platforms is as simple as setting GOOS, for instance GOOS=darwin or GOOS=linux, because no Cgo is required. For other platforms like Android or consoles, you must set CGO_ENABLED=1 and have the target libraries installed, since Go disables Cgo on cross-compiles by default. On FreeBSD, cross-compiling with CGO_ENABLED=0 requires an additional gcflags argument: -gcflags="github.com/ebitengine/purego/internal/fakecgo=-std". Native FreeBSD builds need nothing extra.
Limitations and Failure Modes
The most obvious limitation is the PulseAudio dependency on Linux and BSD. Many minimal Linux containers or embedded systems do not run PulseAudio. The ALSA fallback exists, but it requires libasound.so.2 at runtime, which is not always present. If neither is available, playback simply fails, though the README does not describe the exact error. Another limitation is the single-context rule: you cannot create multiple contexts, so if you need to switch audio devices or sample rates mid-program, you must tear down and recreate the context, which may cause a gap in playback. The README also warns about latency: the internal buffer introduces a delay that is not precisely controlled. For applications needing sample-accurate timing, this is a problem. Finally, the README does not mention any support for recording or audio input, so Oto is strictly for playback.
Comparing with Alternatives
A common alternative in the Go ecosystem is beep, which is a higher-level library that includes decoding, mixing, and effects. Beep sits on top of an audio backend, often using Oto itself as the backend. The difference in approach is clear: Oto gives you raw byte streams and leaves decoding and mixing to you, while Beep provides a Player that accepts audio sources and handles format conversion. If you want to play a simple sound effect without managing decoders and buffers, Beep is more convenient. If you need control over the exact buffer size, or you want to avoid the overhead of Beep's abstractions, Oto is the lower-level choice. Another alternative is the pure-Go package github.com/gordonklaus/portaudio, which wraps PortAudio and offers cross-platform audio I/O, but it requires Cgo. Oto's no-Cgo desktop support is its main differentiator.
Maintenance, Licensing, and Upgrade Cost
Oto is licensed under Apache-2.0, which is permissive and allows commercial use. The repository is active, with the latest release v3.4.1 pushed in August 2026, and a steady release cadence of about one minor version per year. The project is maintained by the Ebitengine team, which has a track record of long-term support. Upgrading between minor versions appears straightforward, as the API is stable; the README does not document any breaking changes between v3.3.3 and v3.4.1. However, the dependency on github.com/jfreymuth/pulse is a third-party pure-Go package, and its maintenance is outside Oto's control. If that package stops being updated, Oto could face issues with future PulseAudio versions. The FreeBSD-specific gcflags workaround is a sign that cross-compilation can be fragile. Overall, the maintenance cost is low for desktop targets, but you should pin your Oto version and test on your target platforms, especially BSD.
Editorial conclusion
Adopt Oto v3 if you write Go applications that need low-level audio playback across desktop platforms and want to avoid Cgo. Avoid it if your Linux or BSD target lacks PulseAudio and you cannot install libasound.so.2 at runtime, or if you need high-level features like mixing or effects. Before adopting, verify your target platform's audio server availability, test the ALSA fallback on your specific distribution, and check the buffer size behavior for latency-sensitive applications. For console targets, confirm your C/C++ toolchain and cross-compilation setup, as Oto's no-Cgo promise does not extend there.
Frequently asked questions
Which Oto platforms build without a C compiler?
Windows, macOS, Linux, FreeBSD, OpenBSD and WebAssembly are all listed as needing no Cgo. iOS may need a compiler in the path, with the clang or xcode-select route described, and the console drivers in the repository are C++ files.
Why does my Oto player output static instead of sound?
If you streamed from disk, the file object you passed as the io.Reader was collected and closed underneath the player. The documentation warns that the file must be kept alive, and that holding a reference to it yourself, for example in a struct, is often necessary, otherwise the symptom is static rather than an error.
How many Oto contexts and players can a program have?
Exactly one context per program, since it owns the interaction with the operating system and the audio drivers. Any number of players can be created from that context, but a single io.Reader must not be shared between players.
How do I play an MP3 file with Oto?
Oto does not decode anything itself, so you supply a decoder. The examples read the file, wrap it in a reader or pass the open file directly, and hand that reader to a decoder imported from a separate module, which streams while playing rather than loading everything into memory first.
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)Star history chart for your README
[](https://hysenlabs.com/projects/ebitengine-oto)Was this useful?