
SDL_mixer 3 bindings exposing mixer/device and decoder APIs: shared audio objects, tracks with per‑track controls and callbacks, bundled self‑contained decoders, and offline mixer rendering for headless use.
Kotlin Multiplatform bindings for SDL_mixer 3 (audio mixing with file decoding), built on top of sdl-kmp. The public API lives in the cn.enaium.sdl.mixer package and works directly with the sdl-kmp types (SDLAudioSpec, SDLIOStream, SDLProperties, ...).
Two implementations, mirroring sdl-kmp and sdl-ttf-kmp:
SDL_mixer submodule: WAV/AIFF/VOC/AU, FLAC via dr_flac, MP3 via dr_mp3, Ogg Vorbis via stb_vorbis and MIDI via timidity) are compiled by CMake (jni/) into a JNI shared library (libsdl_mixer_jni), shipped as per-OS/arch sdl-mixer-kmp-jni-jvm-* artifacts — the same self-contained approach as sdl-kmp's libsdl_jni. MixerNativeLoader extracts the matching binary at runtime. The process contains a second SDL3 copy; SDL_mixer errors are read through the mixer-side SDL_GetError (SDLMixer.error()).cn.enaium.sdl types.| Platform | Targets | Implementation |
|---|---|---|
| JVM |
jvm (Linux/macOS/Windows) |
JNI shared library (libsdl_mixer_jni), SDL3 + SDL_mixer compiled from source |
| macOS |
macosArm64, macosX64
|
cinterop + embedded static SDL_mixer |
| Linux |
linuxX64, linuxArm64
|
cinterop + embedded static SDL_mixer |
| Windows | mingwX64 |
cinterop + embedded static SDL_mixer |
| iOS |
iosArm64, iosX64, iosSimulatorArm64
|
cinterop + embedded static SDL_mixer |
| tvOS |
tvosArm64, tvosSimulatorArm64
|
cinterop + embedded static SDL_mixer |
| Android |
androidNativeArm64, androidNativeArm32, androidNativeX64, androidNativeX86
|
cinterop + embedded static SDL_mixer (built with the NDK) |
The bundled SDL_mixer is configured to build only the decoders implemented in its own source tree (WAV/AIFF/VOC/AU, dr_flac, dr_mp3, stb_vorbis, timidity). Formats that need SDL_mixer's external submodules (libogg/libvorbis/libopus, libmpg123, FluidSynth, game-music-emu, libxmp, WavPack) are disabled so the build is self-contained.
The published version requires sdl-kmp 1.0.7 (it is an api dependency, pulled in automatically).
build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("cn.enaium.sdl:sdl-mixer-kmp:1.0.0")
}
}
}import cn.enaium.sdl.*
import cn.enaium.sdl.mixer.*
fun main() {
SDL.setMainReady()
if (!SDL.init(SDLInitFlags.AUDIO)) {
error("SDL_Init failed: ${SDL.error()}")
}
if (!SDLMixer.init()) {
error("MIX_Init failed: ${SDLMixer.error()}")
}
// A mixer that plays directly to the default audio device.
SDLMixer.createMixerDevice(SDLAudioDeviceID.DEFAULT_PLAYBACK).use { mixer ->
val audio = mixer.loadAudio("/path/to/sound.wav")
// A track: one reusable slider on the mixer board.
val track = mixer.createTrack()
track.setAudio(audio)
track.tag("sfx")
// Loop forever with an 800ms fade-in (properties from SDLMixerPlayProp).
val options = SDLProperties.create()
SDLProperties.setProperty(options, SDLMixerPlayProp.LOOPS_NUMBER, -1L)
SDLProperties.setProperty(options, SDLMixerPlayProp.FADE_IN_MILLISECONDS_NUMBER, 800L)
track.play(options)
SDL.delay(5000)
track.stop() // or mixer.stopAllTracks()
track.close()
audio.close()
}
SDLMixer.quit()
SDL.quit()
}SDLMixer.init / quit, version, numAudioDecoders / audioDecoder.createMixerDevice plays to an audio device; createMixer renders to a memory buffer via SDLMixerDevice.generate (usable without any audio hardware — headless CI, servers, ...). Both expose master gain, frequencyRatio, format, locking, and tag/pause/resume/stop operations affecting all tracks.loadAudio / loadAudioIO (from a cn.enaium.sdl.SDLIOStream) / loadRawAudio / createSineWaveAudio return SDLAudios that can be played on any track of any mixer (they are shared, reference-counted objects); duration, format and the metadata properties (see SDLMixerMetadata) are exposed.setAudio / setAudioStream (raw SDL_AudioStream handle) / setIOStream, then play with loop/fade/seek options built from SDLMixerPlayProp properties. Per-track gain, frequency ratio, loops, playback position, tags, stereo/3D positioning, output channel maps and groups are all supported.setStoppedCallback), raw and cooked track mixing (setRawCallback / setCookedCallback), group post-mix (SDLMixerGroup.setPostMixCallback) and mixer post-mix (setPostMixCallback) — the PCM callbacks receive a copy of the float samples together with their SDLAudioSpec.createAudioDecoder / createAudioDecoderIO + SDLAudioDecoder.decode.SDLMixer.error().-XstartOnFirstThread (the example jvmRun task already sets it).sdl-mixer-kmp-jni-jvm-{os}-{arch} artifact is a transitive runtime dependency; MixerNativeLoader extracts libsdl_mixer_jni and System.load()s it. libsdl_mixer_jni bundles its own SDL3, so no java.library.path setup is needed.androidNative* target requires an installed Android NDK (found under $ANDROID_HOME/ndk); the SDL_mixer static library is cross-compiled with its CMake toolchain.SDL_VIDEO_DRIVER=dummy to run without a display. Without audio hardware, createMixerDevice may fail — createMixer (offline generation) always works, which is what the tests and the headless example use.examples/mixer_device — a mixer demo: plays to the default audio device with an automatic fallback to an offline memory mixer, drives a fixed timeline (loop playback with fade-in, track tags, pause/resume, fading stop, master gain/frequency-ratio changes) and demonstrates the stopped and post-mix callbacks. Takes an optional audio file path as the first argument (any format the bundled decoders support; defaults to a generated 440Hz sine wave) and an optional duration in seconds as the second:# headless (CI / servers; falls back to the offline memory mixer)
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:mixer_device:jvmRun
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:mixer_device:runDebugExecutableMacosArm64
# with a real audio device and a file
./gradlew :examples:mixer_device:jvmRun --args="song.mp3"Requirements: JDK 21, CMake, a C/C++ compiler; Xcode for Apple targets, the x86_64-w64-mingw32-gcc toolchain for MinGW cross-compiles (Linux host), the Android NDK for androidNative*.
git clone --recurse-submodules git@github.com:Enaium/sdl-mixer-kmp.git
cd sdl-mixer-kmp
# compile + test the JVM target
./gradlew :sdl-mixer-kmp:jvmTest
# run the example headless
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:mixer_device:jvmRun
# publish everything buildable on this host to Maven Local
./gradlew :sdl-mixer-kmp:publishToMavenLocal :mixer-jni-jvm-darwin-aarch64:publishToMavenLocalBoth workflows are manually triggered (Actions tab):
test.yml — local Maven publish + test: publishes every artifact the runner can build to Maven Local (no signing, no secrets), runs the JVM/native tests and the example headless. Use this to verify a change before publishing.publish.yml — formal Maven Central release: publishes the metadata + JVM module, all target klibs and the JNI artifacts to Maven Central, signed with PGP. The version is fixed at 1.0.0 (build.gradle.kts). Requires the repository secrets MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, SIGNING_KEY, SIGNING_KEY_ID and SIGNING_PASSWORD.MIT. The bundled SDL3 and SDL_mixer submodules are licensed under the zlib license.
Kotlin Multiplatform bindings for SDL_mixer 3 (audio mixing with file decoding), built on top of sdl-kmp. The public API lives in the cn.enaium.sdl.mixer package and works directly with the sdl-kmp types (SDLAudioSpec, SDLIOStream, SDLProperties, ...).
Two implementations, mirroring sdl-kmp and sdl-ttf-kmp:
SDL_mixer submodule: WAV/AIFF/VOC/AU, FLAC via dr_flac, MP3 via dr_mp3, Ogg Vorbis via stb_vorbis and MIDI via timidity) are compiled by CMake (jni/) into a JNI shared library (libsdl_mixer_jni), shipped as per-OS/arch sdl-mixer-kmp-jni-jvm-* artifacts — the same self-contained approach as sdl-kmp's libsdl_jni. MixerNativeLoader extracts the matching binary at runtime. The process contains a second SDL3 copy; SDL_mixer errors are read through the mixer-side SDL_GetError (SDLMixer.error()).cn.enaium.sdl types.| Platform | Targets | Implementation |
|---|---|---|
| JVM |
jvm (Linux/macOS/Windows) |
JNI shared library (libsdl_mixer_jni), SDL3 + SDL_mixer compiled from source |
| macOS |
macosArm64, macosX64
|
cinterop + embedded static SDL_mixer |
| Linux |
linuxX64, linuxArm64
|
cinterop + embedded static SDL_mixer |
| Windows | mingwX64 |
cinterop + embedded static SDL_mixer |
| iOS |
iosArm64, iosX64, iosSimulatorArm64
|
cinterop + embedded static SDL_mixer |
| tvOS |
tvosArm64, tvosSimulatorArm64
|
cinterop + embedded static SDL_mixer |
| Android |
androidNativeArm64, androidNativeArm32, androidNativeX64, androidNativeX86
|
cinterop + embedded static SDL_mixer (built with the NDK) |
The bundled SDL_mixer is configured to build only the decoders implemented in its own source tree (WAV/AIFF/VOC/AU, dr_flac, dr_mp3, stb_vorbis, timidity). Formats that need SDL_mixer's external submodules (libogg/libvorbis/libopus, libmpg123, FluidSynth, game-music-emu, libxmp, WavPack) are disabled so the build is self-contained.
The published version requires sdl-kmp 1.0.7 (it is an api dependency, pulled in automatically).
build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("cn.enaium.sdl:sdl-mixer-kmp:1.0.0")
}
}
}import cn.enaium.sdl.*
import cn.enaium.sdl.mixer.*
fun main() {
SDL.setMainReady()
if (!SDL.init(SDLInitFlags.AUDIO)) {
error("SDL_Init failed: ${SDL.error()}")
}
if (!SDLMixer.init()) {
error("MIX_Init failed: ${SDLMixer.error()}")
}
// A mixer that plays directly to the default audio device.
SDLMixer.createMixerDevice(SDLAudioDeviceID.DEFAULT_PLAYBACK).use { mixer ->
val audio = mixer.loadAudio("/path/to/sound.wav")
// A track: one reusable slider on the mixer board.
val track = mixer.createTrack()
track.setAudio(audio)
track.tag("sfx")
// Loop forever with an 800ms fade-in (properties from SDLMixerPlayProp).
val options = SDLProperties.create()
SDLProperties.setProperty(options, SDLMixerPlayProp.LOOPS_NUMBER, -1L)
SDLProperties.setProperty(options, SDLMixerPlayProp.FADE_IN_MILLISECONDS_NUMBER, 800L)
track.play(options)
SDL.delay(5000)
track.stop() // or mixer.stopAllTracks()
track.close()
audio.close()
}
SDLMixer.quit()
SDL.quit()
}SDLMixer.init / quit, version, numAudioDecoders / audioDecoder.createMixerDevice plays to an audio device; createMixer renders to a memory buffer via SDLMixerDevice.generate (usable without any audio hardware — headless CI, servers, ...). Both expose master gain, frequencyRatio, format, locking, and tag/pause/resume/stop operations affecting all tracks.loadAudio / loadAudioIO (from a cn.enaium.sdl.SDLIOStream) / loadRawAudio / createSineWaveAudio return SDLAudios that can be played on any track of any mixer (they are shared, reference-counted objects); duration, format and the metadata properties (see SDLMixerMetadata) are exposed.setAudio / setAudioStream (raw SDL_AudioStream handle) / setIOStream, then play with loop/fade/seek options built from SDLMixerPlayProp properties. Per-track gain, frequency ratio, loops, playback position, tags, stereo/3D positioning, output channel maps and groups are all supported.setStoppedCallback), raw and cooked track mixing (setRawCallback / setCookedCallback), group post-mix (SDLMixerGroup.setPostMixCallback) and mixer post-mix (setPostMixCallback) — the PCM callbacks receive a copy of the float samples together with their SDLAudioSpec.createAudioDecoder / createAudioDecoderIO + SDLAudioDecoder.decode.SDLMixer.error().-XstartOnFirstThread (the example jvmRun task already sets it).sdl-mixer-kmp-jni-jvm-{os}-{arch} artifact is a transitive runtime dependency; MixerNativeLoader extracts libsdl_mixer_jni and System.load()s it. libsdl_mixer_jni bundles its own SDL3, so no java.library.path setup is needed.androidNative* target requires an installed Android NDK (found under $ANDROID_HOME/ndk); the SDL_mixer static library is cross-compiled with its CMake toolchain.SDL_VIDEO_DRIVER=dummy to run without a display. Without audio hardware, createMixerDevice may fail — createMixer (offline generation) always works, which is what the tests and the headless example use.examples/mixer_device — a mixer demo: plays to the default audio device with an automatic fallback to an offline memory mixer, drives a fixed timeline (loop playback with fade-in, track tags, pause/resume, fading stop, master gain/frequency-ratio changes) and demonstrates the stopped and post-mix callbacks. Takes an optional audio file path as the first argument (any format the bundled decoders support; defaults to a generated 440Hz sine wave) and an optional duration in seconds as the second:# headless (CI / servers; falls back to the offline memory mixer)
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:mixer_device:jvmRun
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:mixer_device:runDebugExecutableMacosArm64
# with a real audio device and a file
./gradlew :examples:mixer_device:jvmRun --args="song.mp3"Requirements: JDK 21, CMake, a C/C++ compiler; Xcode for Apple targets, the x86_64-w64-mingw32-gcc toolchain for MinGW cross-compiles (Linux host), the Android NDK for androidNative*.
git clone --recurse-submodules git@github.com:Enaium/sdl-mixer-kmp.git
cd sdl-mixer-kmp
# compile + test the JVM target
./gradlew :sdl-mixer-kmp:jvmTest
# run the example headless
SDL_VIDEO_DRIVER=dummy ./gradlew :examples:mixer_device:jvmRun
# publish everything buildable on this host to Maven Local
./gradlew :sdl-mixer-kmp:publishToMavenLocal :mixer-jni-jvm-darwin-aarch64:publishToMavenLocalBoth workflows are manually triggered (Actions tab):
test.yml — local Maven publish + test: publishes every artifact the runner can build to Maven Local (no signing, no secrets), runs the JVM/native tests and the example headless. Use this to verify a change before publishing.publish.yml — formal Maven Central release: publishes the metadata + JVM module, all target klibs and the JNI artifacts to Maven Central, signed with PGP. The version is fixed at 1.0.0 (build.gradle.kts). Requires the repository secrets MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, SIGNING_KEY, SIGNING_KEY_ID and SIGNING_PASSWORD.MIT. The bundled SDL3 and SDL_mixer submodules are licensed under the zlib license.