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 last received commits 3 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 14, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE 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.
Community notes