Skip to content

feat: the no-audio-device probe without cgo - #23

Merged
LucasAntunesdeAlmeida merged 1 commit into
masterfrom
audio-probe-purego
Sep 30, 2026
Merged

LucasAntunesdeAlmeida merged 1 commit into
masterfrom
audio-probe-purego

Conversation

@LucasAntunesdeAlmeida

Copy link
Copy Markdown
Owner

What

The no-audio-device probe (contract item 9) without cgo, following oto 3.5's new order: PulseAudio first, then ALSA. With it the engine has no cgo left, and since Ebitengine v2.10 needs none on Linux, a Collider game builds for Linux with CGO_ENABLED=0, from any OS.

Before this, a cgo-free Linux build compiled the probe to a no-op and panicked at start on a machine without sound (the v0.6.1 bug back again). Checked in Valve's sniper SDK container under Xvfb with no /dev/snd, examples/horde built with CGO_ENABLED=0 GOOS=linux on Windows:

panic: audio: audio error: oto: initialization failed: PulseAudio: ... ALSA: oto: ALSA device initialization failed: "default": ...

Design

  • device_unix.go (oto's unix build tags): probeDevice tries PulseAudio, then probeALSA when the platform has it (set by device_alsa.go, as oto does), and names both failures in the one log line.
  • PulseAudio through github.com/jfreymuth/pulse (already in the module graph through oto, now a direct dependency): connect, create oto's stream (stereo float at 48 kHz) corked so nothing plays, close. The library's one-second request timeout keeps a server that does not answer from hanging the frame.
  • ALSA through purego (also already there, through Ebitengine and oto): libasound opened like oto (libasound.so.2, then libasound.so), oto 3.5's candidates (default, plug:default, then every output device except null, each followed by its plug: variant), opened non-blocking (busy counts as present: oto waits for it), first to take FLOAT_LE/2ch/48k wins. oto 3.5 moved past a device that opens but cannot be configured; the probe does too. A machine without libasound is an answer, not a crash.
  • ALSA and its plugins (JACK, OSS, pulse) print each failed attempt to stderr. The cgo probe replaced ALSA's error handler, which does not reach what a plugin's own library (libjack) prints. Now fd 2 points at /dev/null while the ALSA probe runs and is restored after, so the engine's line is the only output.
  • Other platforms: device_other.go no-op, as before.

No API change. README contract item 9 names PulseAudio/PipeWire and ALSA; PUBLISHING.md's Linux section now says CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build, no dev packages, and lists the runtime libraries. CI also runs CGO_ENABLED=0 GOOS=linux go vet . && CGO_ENABLED=0 GOOS=linux go build ./....

Proof

  • Tests (device_unix_test.go, device_alsa_test.go): oto's candidate order, stderr muted to the null device and restored, a missing server failing in well under a second, both errors named, and the probe answering on whatever machine runs it (CI has no server or card).
  • Local gate on Windows: gofmt, vet, staticcheck (also GOOS=linux), tests, js/wasm, windows, and the new cgo-free Linux vet/build. Linux tests in the sniper SDK container under Xvfb pass with CGO_ENABLED=0 and =1.
  • Real runs of the cgo-free examples/horde (ldd: libc, libdl, libpthread only):
    • sniper SDK, no device: one line, collider: no audio device, playing silently: PulseAudio: dial unix pulse/native: connect: no such file or directory; ALSA: no playback device among 16 ("default": No such file or directory), and the game keeps running. No ALSA or JACK noise.
    • sniper SDK, ALSA null as default: no line, plays.
    • Debian bookworm with a PulseAudio server on a null sink: no line; the server lists sink input application.name = "horde", Corked: no (oto is playing through PulseAudio).

Ebitengine v2.10 needs no cgo on Linux: X11, OpenGL and ALSA are
opened at run time through purego, so a Linux game cross-compiles
with CGO_ENABLED=0. The engine's own no-device probe was the last cgo
left (it linked libasound), and with cgo off it compiled to a no-op:
a cgo-free binary on a machine without sound panicked at start, as
before v0.6.1.

oto 3.5 also changed what it opens on Linux and the BSDs: a PulseAudio
server first (PipeWire speaks it too), through a pure Go client, and
ALSA only when there is none. The probe now follows it, in its order:

- PulseAudio: connect and create the stream oto will (stereo float,
  48 kHz), corked so nothing plays, then close both. The library's
  one-second request timeout keeps a silent server from hanging it.
- ALSA (Linux, FreeBSD, NetBSD, as oto): libasound loaded with purego,
  oto's device list (default, plug:default, every output device but
  null, each followed by its plug: variant), opened non-blocking
  (busy counts as there); the first that takes the float format wins.
  ALSA and its plugins (JACK, OSS) print each failed attempt, so fd 2
  points at the null device while it looks: the one log line stays
  the only output.

Behavior is unchanged otherwise: no device, one line ("collider: no
audio device, playing silently: PulseAudio: ...; ALSA: ...") and
silence. Tests: oto's candidate order, stderr muted and restored, a
missing server answering fast, both errors named, and the probe
answering on the machine it runs on. CI also vets and builds for
Linux with CGO_ENABLED=0. PUBLISHING.md: Linux builds need no cgo.
@LucasAntunesdeAlmeida
LucasAntunesdeAlmeida merged commit 4a5f913 into master Sep 30, 2026
1 check passed
@LucasAntunesdeAlmeida
LucasAntunesdeAlmeida deleted the audio-probe-purego branch September 30, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant