Play sound effects through SDL_mixer on Linux#531
Conversation
The non-Windows sound-effect API was a silent no-op pending a portable backend (issue sven-n#462). Implement it on SDL3_mixer, reusing the mixer device that AudioPlayer already creates for music: one decoded buffer per ESound with a small round-robin track pool, volumes mapped from DirectSound's hundredths-of-a-decibel scale, and looping requested at play time. Behavior mirrors the DirectSound backend in 2D; 3D spatialization is omitted since that backend never enables it either. Add S_FALSE to the Win32 compatibility shim for the already-loaded case.
DirectSound treats Play() on an already-playing buffer as a no-op, and the engine relies on that: the world ambient loops (SceneManager) and the blacksmith's hammer (ZzzCharacter) re-request their sound on every frame. MIX_PlayTrack restarts the track instead, so the sample was cut a few milliseconds in, sixty times a second: the ambient loops turned into a constant buzz and the anvil hit was preceded by a burst of crackling. The blacksmith was hit hardest because its buffer has a single channel, with no round-robin to spread the retriggers over. Skip the play request when the selected track is still playing, matching the DirectSound behaviour the call sites were written against.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a functional sound-effect backend for the Linux client by leveraging SDL3_mixer. It replaces previous no-op stubs with a robust implementation that maintains behavioral parity with the Windows DirectSound backend, including volume scaling and handling of redundant play requests. The changes ensure that sound effects are properly integrated into the existing audio infrastructure without requiring modifications to the game's high-level audio call sites. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request implements a non-Windows sound-effect backend using SDL3_mixer, replacing the previous silent no-op implementation for platforms without DirectSound. It also exposes the shared SDL mixer device from the AudioPlayer. The review feedback highlights critical improvements for thread safety and stability, specifically suggesting the use of atomic variables for enableSound_ and mixer_ to prevent data races and potential crashes when the mixer is null. Additionally, it recommends optimizing StopAll() to lock the mutex once rather than repeatedly inside a loop, and adding the header.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…lock - IsEnabled() also requires a mixer. SetEnableSound() cannot know that Initialize() failed, so without this a caller could enable sound with no device behind it. SDL_mixer tolerates a null mixer today, so this is defence in depth rather than a crash fix. - Extract StopEntryTracks() and take the mutex once in StopAll(), instead of locking per buffer and repeating the loop body. A single map change was taking a thousand lock/unlock pairs.
Implements the sound-effect backend for platforms without DirectSound, so the
native Linux client has working SFX instead of silence. Follow-up to #462 —
music already played through SDL_mixer; effects were stubbed out as no-ops.
Branch:
linux-sfx-sdl-mixer(2 commits, +441/−19)What changes
src/source/Audio/SdlSfxPlayer.cpp— implements theDSPlaySound.hAPI on top of SDL3_mixer, guarded by
#ifndef _WIN32. Windows keeps usingDirectSound; nothing on that path changes.
DSPlaySound.cpp— the non-Windows no-op stubs are removed, since thereal implementation now provides those symbols.
AudioPlayer— exposesGetMixer(), so effects reuse the mixer devicethat music already opened instead of opening a second one.
WinCompat.h— addsS_FALSE, needed by the "already loaded" returnpath that mirrors the DirectSound backend.
Design notes
One mixer device, shared with music.
AudioPlayer::Initialize()runs first(
Winmain.cpp), andFreeDirectSound()runs beforeAudioPlayer::Shutdown(),so the mixer outlives every track. If music failed to initialise there is no
audio device at all, and effects stay disabled rather than opening their own.
Behavioural parity with DirectSound, deliberately, including the quirks:
one decoded buffer per
ESoundwith a round-robin channel pool,S_FALSEona repeated load, and
SetMasterVolumeoverwriting per-buffer volumes. Volumesstay on DirectSound's scale (hundredths of a dB,
0…-10000) and areconverted to linear gain, so call sites need no changes.
Effects are predecoded (
MIX_LoadAudio(..., predecode=true)) — they areshort and replayed constantly, so the memory trade buys zero decode latency at
play time. Music stays streamed.
No 3D spatialisation. The DirectSound backend never enables it either (its
3D flag has no callers), so parity here is 2D and
Set3DSoundPosition()isempty.
The one non-obvious part
The engine assumes an undocumented DirectSound guarantee:
Play()on abuffer that is already playing is a no-op. Several call sites depend on it —
PlayWorldAmbientSounds()re-requests the ambient loop on every frame, andZzzCharacter.cppdoes the same for the blacksmith's hammer while itsanimation is in range.
SDL_mixer does the opposite:
MIX_PlayTrackrestarts a playing track. Thefirst build turned that assumption into audible corruption — ambient loops
never got past a few milliseconds and became a continuous buzz, and the anvil
hit was preceded by a burst of crackling. The blacksmith was worst affected
because its buffer is loaded with a single channel, so there was no round-robin
to spread the retriggers across.
The second commit restores the DirectSound semantics: skip the request when
the selected track is still playing.
This is worth flagging beyond this PR — the assumption is not written down
anywhere, and any future audio backend will hit it.
Testing
Built and run as the native Linux client (Ubuntu 24.04, x86-64):