php-io-extensions / sdl3-audio
1:1 PHP bindings of SDL 3's audio API (SDL_audio.h), built on ext-sdl3
Package info
github.com/php-io-extensions/sdl3-audio
Language:C
Type:php-ext
Ext name:ext-sdl3_audio
pkg:composer/php-io-extensions/sdl3-audio
Requires
- php: >=8.4
- ext-sdl3: ^0.11.2
Requires (Dev)
- pestphp/pest: ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-12 04:05:38 UTC
README
ext-sdl3-audio
SDL 3's audio API bound 1:1 into PHP 8.4+, on ext-sdl3's object model
Sound out of PHP has meant handing a file to a player process: no device choice, no stream to feed, no way to hear what you just computed. Anything that generates audio, resamples it or mixes it needs a device it can push samples into and a callback that asks for more.
SDL 3's audio subsystem is that layer. It enumerates playback and recording devices, opens them by ID, converts between sample formats, channel layouts and rates through SDL_AudioStream, binds any number of streams to one device and mixes them, and hands you the final mix in a postmix callback. It loads WAV, mixes buffers and converts samples on its own. It runs on Core Audio, PipeWire, PulseAudio and ALSA.
The only problem? PHP can't reach it, and SDL calls its callbacks on an audio thread where PHP cannot run. This extension binds SDL_audio.h one to one: every function, constant and struct, under its C name. It builds on ext-sdl3: SDL_Init, SDL_GetError, properties and SDL_IOStream come from there, and an SDL_AudioStream is one of its handles. Callbacks are PHP callables, served on the PHP thread that set them. No defaults, no composites, no opinions: what SDL documents is what you call.
Basic usage
SDL_Init(SDL_INIT_AUDIO) || throw new RuntimeException(SDL_GetError()); $phase = 0.0; $stream = SDL_OpenAudioDeviceStream( SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, new SDL_AudioSpec(SDL_AUDIO_F32, 1, 48000), function (SDL_AudioStream $stream, int $additional, int $total) use (&$phase) { $samples = ''; for ($i = 0; $i < intdiv($additional, 4); $i++) { $samples .= pack('g', 0.2 * sin($phase)); $phase += 2 * M_PI * 440 / 48000; } SDL_PutAudioStreamData($stream, $samples, strlen($samples)); }, ); SDL_ResumeAudioStreamDevice($stream); $end = microtime(true) + 1.0; while (microtime(true) < $end) { // PHP code keeps running: SDL's requests are served at the VM's interrupt points. } SDL_DestroyAudioStream($stream); SDL_Quit();
Push instead of pull, from a WAV through ext-sdl3's SDL_IOStream:
SDL_Init(SDL_INIT_AUDIO) || throw new RuntimeException(SDL_GetError()); $spec = new SDL_AudioSpec(); // SDL fills it $bytes = null; $length = 0; SDL_LoadWAV_IO(SDL_IOFromFile('blip.wav', 'rb'), true, $spec, $bytes, $length) || throw new RuntimeException(SDL_GetError()); $stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, $spec, null); SDL_PutAudioStreamData($stream, $bytes, $length); SDL_FlushAudioStream($stream); SDL_ResumeAudioStreamDevice($stream); while (SDL_GetAudioStreamQueued($stream) > 0) { usleep(10000); } SDL_DestroyAudioStream($stream);
Requirements
| PHP | 8.4 or newer, NTS or ZTS |
| OS | Linux, macOS |
| Build | a C compiler, pkg-config, phpize and php-config for the PHP you install into |
| SDL | 3.2 or newer. Built against 3.4, the 3.4 functions are bound too |
| ext-sdl3 | 0.11.2 or newer, installed into the same PHP first: this extension compiles against its php_sdl3_api.h and loads after it |
# Debian trixie, Raspberry Pi OS sudo apt install build-essential php8.4-dev libsdl3-dev pkg-config # macOS: Xcode Command Line Tools, and Homebrew's PHP ships phpize and php-config xcode-select --install brew install sdl3 pkgconf
Installation
Install ext-sdl3 first, the same way.
With PHP PIE (recommended)
pie install php-io-extensions/sdl3-audio
PIE builds the module against the PHP it runs under and enables it.
Manually (install script)
The repository ships two installers. Each stops if ext-sdl3 is not loaded, builds for its PHP, installs sdl3_audio.so into that PHP's extension_dir, writes 30-sdl3_audio.ini (it sorts after 30-sdl3.ini), and checks the module loads. The macOS one builds for Homebrew php@8.4 and php@8.4-zts and ad-hoc signs the module; pass other PHP binaries as arguments to install those instead. The Debian one configures against the distro SDL in /usr/lib/$(gcc -dumpmachine) and puts /usr/include ahead of /usr/local/include, as ext-sdl3's does.
git clone https://github.com/php-io-extensions/sdl3-audio.git cd sdl3-audio ./install-macos.sh # macOS ./install-debian-trixie.sh # Debian 13, Raspberry Pi OS
Manually (phpize)
phpize ./configure --enable-sdl3-audio make -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu)" sudo make install echo "extension=sdl3_audio" | sudo tee "$(php -r 'echo PHP_CONFIG_FILE_SCAN_DIR;')/30-sdl3_audio.ini"
Verify
php --ri sdl3_audio
sdl3_audio support => enabled Version => 0.11.0 SDL version => 3.4.20
API
58 functions, all bound on SDL 3.4; 56 on SDL 3.2. SUPPORT.md lists each one with the SDL version that added it. SDL's audio documentation and SDL_audio.h document every call; the stubs in stubs/ give the PHP signature of each.
Translations
- Names. Functions, constants and classes are the C names.
- ext-sdl3's translations apply: a list and its C count are separate parameters, bytes are strings with their C length, out-parameters are by reference.
SDL_AudioSpecis a class withformat,channelsandfreq. Where SDL fills anSDL_AudioSpec *(SDL_LoadWAV_IO,SDL_GetAudioDeviceFormat), you pass anSDL_AudioSpecand SDL writes into it.SDL_AudioStreamis an ext-sdl3 handle:final, one PHP object per native pointer,pointer()andfromPointer().SDL_DestroyAudioStreammarks it released; a released stream reaches SDL as NULL, which SDL refuses itself.- Samples are strings of packed bytes in the stream's format (
pack('g*', …)forSDL_AUDIO_F32). - Failure is
false(or0,-1,nullas SDL returns it) andSDL_GetError(), as in C. - Callbacks take a PHP callable and no
userdata: a closure carries its own state.
Errors
The extension declines a call only where SDL would read memory it does not own. A length past the PHP string in SDL_PutAudioStreamData, SDL_PutAudioStreamDataNoCopy, SDL_PutAudioStreamPlanarData or SDL_MixAudio throws ValueError. The check has a probe test that crashes with it off (tests/Probes).
Threads and callbacks
SDL calls stream and postmix callbacks on its audio thread. The binding runs the PHP callable on the thread that set it, at that thread's next VM interrupt point (a loop iteration, a function call), and SDL's thread waits for it as long as the requested audio lasts. A thread that sleeps or sits in a long C call misses its turn, and SDL plays what it has. SDL calls the callable makes run on SDL's waiting thread, which holds the stream's lock. When PHP itself calls SDL_GetAudioStreamData or SDL_PutAudioStreamData, the callback runs inline. An exception thrown in a callback surfaces on that PHP thread.
The postmix callable gets the mixed float32 buffer as a by-reference string; a string of the same length is copied back, any other length leaves SDL's buffer as it was.
Do not destroy a stream inside its own callback (SDL's rule). SDL_QuitSubSystem(SDL_INIT_AUDIO) and SDL_Quit() release every SDL_AudioStream object and the callables the binding held. Request end clears every callback first; a forked child ends cleanly while the parent keeps its callbacks.
SDL 3.2
SDL_PutAudioStreamDataNoCopy, SDL_PutAudioStreamPlanarData and SDL_PROP_AUDIOSTREAM_AUTO_CLEANUP_BOOLEAN exist only when built against SDL 3.4. On SDL 3.2, SDL_GetAudioDeviceChannelMap and SDL_GetAudioStream{Input,Output}ChannelMap crash inside SDL when no channel map is set; fixed in SDL 3.4.0.
Extensions you might use with this one
- php-io-extensions/sdl3: required. Init, errors, properties,
SDL_IOStream, and windows, input and GPU. - php-io-extensions/miniaudio: miniaudio's device and node graph API, when you want decoding and effects as well.
Agentic Documentation
This repository carries an Open Knowledge Format bundle at .okf/: Markdown with YAML frontmatter, readable by you and loadable by a coding agent. It records what the README doesn't: the bindings by group, how SDL_AudioStream lives in ext-sdl3's identity table, the callback queue and call forwarding, and how to build and test.
Resources
- SDL 3 audio documentation: the manual for every call this extension binds; this README only adds what PHP changes.
- libsdl-org/SDL: SDL's source and
SDL_audio.h. - PIE: the PHP Foundation's extension installer.
gen_stub.php: the php-src stub format.stubs/holds the API declarations; each*_arginfo.his generated from its stub.- Open Knowledge Format: the spec the
.okf/bundle follows.
Running the test suite
Pest v4: formats and constants, device queries, streams, devices and binding, WAV loading, mixing and conversion, every callback, request end and fork, and the support list. phpunit.xml puts SDL on its dummy output, so nothing is audible and no device is needed. The tests load ext-sdl3 and this module from extension_dir, so install both first:
git clone https://github.com/php-io-extensions/sdl3-audio.git
cd sdl3-audio
composer install
php -d memory_limit=128M vendor/bin/pest
Tests: 3 skipped, 54 passed
The skipped tests are probe halves: they run only on a probe build (./configure --enable-sdl3-audio --enable-sdl3-audio-probes, never shipped), where the length check can be switched off to show the crash it prevents. On SDL 3.2 the 3.4-only tests skip too.
One test plays a one-second 440 Hz tone on the real default output. It is excluded by default:
php -d memory_limit=128M vendor/bin/pest --group=hardware
Contributing
Open an issue first for anything beyond a typo. Every binding is written by hand in src/, declared in its stub; regenerate arginfo with gen_stub, run php scripts/support.php (it rewrites SUPPORT.md; --check exits 1 when it is stale or a function is missing), and add a Pest test. CI must be green on every cell of the matrix, NTS and ZTS alike. Read AGENTS.md for the rules the code follows.
Security
ext-sdl3-audio runs SDL's audio thread inside the PHP process. See SECURITY.md for what it guards against and how to report a vulnerability.
License
MIT. See LICENSE. SDL is zlib, Copyright (c) Sam Lantinga.