Java bindings (JNI) for Steam Audio 4.8.1 — Valve's physics-based spatial audio SDK. Targets Java 17+, ships the native libraries inside the jar, and covers ~98% of the C API (205 of 210 functions).
try (Context context = new Context(); // iplContextCreate
Scene scene = new Scene(context)) { // iplSceneCreate
scene.createStaticMesh(vertices, triangles,
materialIndices, new Material[]{Material.concrete()});
scene.commit(); // iplSceneCommit
try (Simulator simulator = LowSpecSimulator.create(context, 48000, 1024);
Source source = new Source(simulator, Simulator.FLAGS_DIRECT);
HRTF hrtf = new HRTF(context, 48000, 1024);
BinauralEffect hrtfRender = new BinauralEffect(context, 48000, 1024, hrtf)) {
simulator.setScene(scene);
simulator.commit();
source.add();
simulator.commit();
source.setDirectInputs(Source.DIRECT_SIM_DISTANCE_ATTENUATION
| Source.DIRECT_SIM_OCCLUSION,
new Vector3(3, 1, 0), new Vector3(0, 0, -1), new Vector3(0, 1, 0),
Source.OCCLUSION_RAYCAST, 0.5f, 8, 1);
// Each frame: simulate once, render per source.
simulator.runDirect();
source.getDirectOutputsInto(directOutputs); // float[9]
binaural.apply(in, out, direction, ...); // zero-alloc hot path
}
}Java (net.sixik.steamaudio.*) JNI (natives/src/*.cpp) Steam Audio
┌─────────────────────────┐ JNIEnv ┌──────────────────────┐ ┌────────────┐
│ Context, Scene, ... │ ─────────► │ SteamAudioJni.dll │──►│ phonon.dll │
│ long peer + AutoCloseable│ jlong / │ extern "C" functions │ │ (Valve) │
│ float[] / ByteBuffer │ float[] │ struct marshalling │ └────────────┘
└─────────────────────────┘ └──────────────────────┘
- Opaque handles stay native. Every C object (
IPLContext,IPLScene, ...) is held by a Java wrapper as along peer— never as aJObject. Wrappers areAutoCloseable;close()maps to the correspondingipl*Release, and double-close is always safe. - Structs are flattened at the JNI boundary.
IPLVector3becomes threefloatarguments or a mutableVector3; the giantIPLSimulationInputsis assembled inside the native call (with documented defaults) behind the focused methodsSource.setDirectInputs / setReflectionsInputs / setPathingInputs. NoGetFieldIDanywhere near the hot path. - Bulk data moves without copies. Geometry (vertices, triangles,
materials) is packed into direct
ByteBuffers and read on the native side viaGetDirectBufferAddress. Audio arrays useGetPrimitiveArrayCritical(JMH-verified:alloc.rate.norm ≈ 0 B/op). - Errors become exceptions. Every
IPLerrorfrom Steam Audio is thrown asSteamAudioException(message includes the status); misuse of closed wrappers throwsIllegalStateException; invalid indices throwIllegalArgumentException. In C the same mistakes are segfaults or silent error codes. - Natives are loaded automatically (
core.SteamAudio), in strict orderphonon→SteamAudioJni(the JNI library links against phonon):-Dsteamaudio.natives=<dir>— explicit override;- well-known build output directories (
natives/build/bin/Release, ...) for development; - extraction from the jar:
net/sixik/steamaudio/natives/<platform>/is unpacked into%TEMP%/steamaudio-java-natives/<platform>-<sha256-16>— the content hash prevents stale-version conflicts and lets several processes share the cache without fighting over locked DLLs.
gradlew jar triggers the whole chain automatically:
configureNatives (CMake configure) → buildNatives (Release build) →
copyNatives (DLLs into jar resources) → processResources. Running from
the IDE works too, because the loader falls back to the CMake output
directory. CMake requires a JDK (find_package(JNI)) and MSVC on Windows.
| Package | Contents |
|---|---|
core |
SteamAudio (loader), Context, Vector3, SerializedObject, SteamAudioException |
audio |
AudioBuffer (+ mix/downmix/ambisonics conversion), HRTF (built-in + SOFA), ImpulseResponse, Reconstructor, SpeakerLayout, AmbisonicsType |
geometry |
Scene (+ OBJ export, Embree/RadeonRays backends), StaticMesh, InstancedMesh, Material (+ presets) |
simulation |
Simulator, Source, ProbeArray, ProbeBatch, PathBaker, ReflectionsBaker, EnergyField, LowSpecSimulator |
effects |
BinauralEffect, DirectEffect, PanningEffect, VirtualSurroundEffect, ReflectionEffect, ReflectionMixer, PathEffect, Ambisonics{Encode,Rotation,Panning,Binaural,Decode}Effect |
gpu |
EmbreeDevice, OpenCLDeviceList (+ DeviceDesc), OpenCLDevice, RadeonRaysDevice, TrueAudioNextDevice |
This is "Steam Audio for Java", not a transliteration of phonon.h. The
function coverage is near-complete, but the surface is deliberately
idiomatic.
- All enum values (
SIMD_LEVEL_*,SpeakerLayout.*,DATA_TYPE_*, occlusion types, ...) use the same integers asphonon.h. - The simulation call sequence is identical:
setScene → setSharedInputs → commit → runDirect → getOutputs. - Nearly every
ipl*function has a Java counterpart with the same semantics (seeROADMAP.mdfor the exact coverage).
| C API | This library |
|---|---|
iplContextCreate(&settings, &ctx) / iplContextRelease(&ctx) |
new Context(...) / AutoCloseable.close() |
IPLerror return codes |
SteamAudioException (message carries the status) |
Structs by value / by pointer (IPLVector3, IPLCoordinateSpace3) |
Mutable Vector3 or flattened float arguments (allocation-free hot path overloads) |
One giant IPLSimulationInputs struct |
Three focused methods with sane native-side defaults: setDirectInputs, setReflectionsInputs, setPathingInputs |
Raw IPLfloat32** audio data |
float[] via critical sections; direct ByteBuffer for geometry |
| Invalid handles / indices = UB or silent codes | Java-side validation: isOpen(), IllegalStateException, IllegalArgumentException |
iplAudioBuffer*Interleave(ctx, buf, ptr) |
buffer.interleaveTo(float[]) / buffer.deinterleaveFrom(float[]) |
- No user callbacks are exposed: log function, allocator/deallocator,
distance-attenuation / air-absorption / directivity / deviation callbacks
(their default models are used;
Sourcedirectivity is omnidirectional), bake progress callbacks (a no-op is always passed — required, see below), pathing visualization, and custom ray tracer callbacks (soSCENE_TYPE_CUSTOMis not usable yet). Simulatortuning knobs are fixed in the native constructor:maxNumOcclusionSamples=256,maxNumRays=4096,numDiffuseSamples=32,maxDuration=2s,maxOrder=1,maxNumSources=8,numThreads=1,rayBatchSize=8,numVisSamples=8.PathEffectrenders unrotated Ambisonics only (spatialize=false); combine withAmbisonicsRotationEffect+AmbisonicsBinauralEffectfor head tracking.iplOpenCLDeviceCreateFromExisting(caller-ownedcl_command_queuepairs) is not wrapped.AudioBuffermemory is always allocated by Steam Audio; wrapping caller-owned sample data is not exposed.Retainvariants are not exposed (Java wrappers own their handles).
- Native loader with jar packaging and a content-hashed extraction cache.
-Dsteamaudio.simdLevelandContext.SIMD_LEVEL_AUTO(default AVX — the safe minimum for pre-Haswell CPUs such as the i5-3470; the officialphonon.dlladditionally caps the level at what the CPU supports via IPP).simulation.LowSpecSimulator— a preset for weak 4-core CPUs: parametric reverb (≈10x cheaper than convolution), 64 rays, 2 bounces, order-1 ambisonics.geometry.Materialpresets from the Steam Audio documentation (concrete(),brick(),metal(), ...).- Java-side validation everywhere; JNI signature mismatches cannot be caught
by the JVM, so the tests deliberately cross every boundary
(
ROADMAP.md→ "Conventions for new bindings").
Context context = new Context(); // SIMD auto → AVX
Simulator simulator = LowSpecSimulator.create(context, 48000, 1024);
// sources: Source.OCCLUSION_RAYCAST; reflections rendering:
// ReflectionEffect.TYPE_PARAMETRIC (≈10x cheaper than convolution)The per-frame hot path (direct + binaural, 1024-sample frames) measures
~3 µs/source on a modern desktop and comfortably fits a ~21 ms frame budget
even multiplied by 3–4x for older hardware. Avoid convolution reverb on many
sources; use ReflectionMixer on a separate thread if it is required.
| Backend | Class | Requires |
|---|---|---|
| Embree ray tracing | gpu.EmbreeDevice + Scene(context, SCENE_TYPE_EMBREE, device) |
embree.dll next to phonon.dll |
| OpenCL | gpu.OpenCLDeviceList → OpenCLDevice |
an OpenCL runtime |
| Radeon Rays (GPU ray tracing) | gpu.RadeonRaysDevice |
AMD GPU + OpenCL |
| TrueAudio Next (GPU convolution) | gpu.TrueAudioNextDevice |
AMD GPU + OpenCL |
Hardware caveats (also documented in gpu.GpuDevicesTest):
- Creation failures are clean (
SteamAudioException) when a runtime DLL is missing — except on non-AMD GPUs, where creating Radeon Rays / TrueAudio Next devices may crash inside phonon; the tests only exercise them on AMD hardware. - Creating an EMBREE-type scene with a NULL device crashes inside phonon — always pass a device.
gradlew test :: builds natives via CMake, then runs the JUnit suite
gradlew jar :: same + packages natives into the jar
gradlew jmh :: JMH benchmarks (src/jmh), results in build/reports/jmhRequirements: JDK 17+ (21 tested), CMake 3.20+, MSVC on Windows. Steam Audio
headers/libs live in natives/steamaudio/ (Apache-2.0, © Valve Corporation).
GitHub Actions (.github/workflows/build.yml) runs on every push/PR:
- Windows job — builds natives, runs the full test suite, uploads the
windows-x64DLLs as an artifact. - Linux job — same on ubuntu-22.04 (older glibc → wider distro support),
uploads the
linux-x64shared libraries. - Jar job — assembles a single fat jar bundling both platforms' natives.
Pushing a v* tag (e.g. v1.0.0) additionally publishes a GitHub
Release containing:
SteamAudio-Java-v1.0.0.jar— the fat jar (drop-in dependency);SteamAudio-Java-v1.0.0-natives-windows-x64.zip/...-linux-x64.zip— bare native libraries for custom deployments.
- steam-audio#523 —
iplPathBakerBake(and the reflections baker) invoke the progress callback unconditionally; a NULL callback segfaults. Our native layer always passes a no-op callback. - steam-audio#578 —
AVX2 alignment bug in
ArrayMath::multiplyAccumulate(4.8.1); not hit by our tests, but worth knowing. AmbisonicsEncodeEffectandPanningEffectcrossfade from the previous frame's direction: the firstapplyafter creation/reset is a warm-up pass (seeP1EffectsTest).
See ROADMAP.md for the full API coverage table, versioning of milestones, and conventions used for new bindings.