Fix SFX and MIDI music not playing on Android

snd_start_digital() opened two separate SDL audio devices - a raw one
for cutscene/audiolog audio and a second via Mix_OpenAudio() for
SFX/MIDI - but Android's audio backends only ever allow one open
non-capture device, so the second open always failed silently.

- Consolidate onto a single Mix_OpenAudio() device; cutscene/audiolog
  PCM is now mixed into its output via Mix_SetPostMix() instead of a
  dedicated device (16-android-audio-single-device.patch)
- Add SDL_GetError() to the failure log so a future open failure is
  actually diagnosable (15-android-audio-error-detail.patch)
- Make NativeMidiInit() report failure on platforms with no real
  implementation (Android included), instead of silently claiming
  success without opening anything (17-android-native-midi-unsupported.patch)
- Strengthen the OpenSL ES driver hint to SDL_HINT_OVERRIDE and log the
  active audio driver as a standing diagnostic (05-android-audio-driver.patch)
This commit is contained in:
ml
2026-08-30 07:09:16 +02:00
parent d6e438a541
commit 504ed3e1cb
5 changed files with 307 additions and 11 deletions
+86 -8
View File
@@ -383,10 +383,14 @@ each one does:
emulation) for real GLES-native equivalents: a custom `pointSize`
uniform, a shader-side alpha discard, and native point-sprite
rasterization.
- **`05-android-audio-driver.patch`** - forces SDL2's older OpenSL ES
audio backend instead of AAudio, because AAudio only allows one open
playback device and the engine opens two (cutscene audio plus
`Mix_OpenAudio` for SFX/MIDI).
- **`05-android-audio-driver.patch`** - forces SDL2's OpenSL ES audio
backend instead of AAudio via
`SDL_SetHintWithPriority(..., SDL_HINT_OVERRIDE)` (so nothing else can
take precedence and silently leave AAudio selected), and logs the
actual `SDL_GetCurrentAudioDriver()` result right after `SDL_Init()`
as a standing on-device diagnostic. Does not by itself fix SFX/MIDI
playback - see `16-android-audio-single-device.patch` and README 5.5.3
for the actual cause and fix.
- **`06-android-resize-event.patch`** - makes the engine react to
`SDL_WINDOWEVENT_RESIZED`, not just `SIZE_CHANGED`, since Android's SDL
backend only ever sends the former for surface-driven resizes.
@@ -434,6 +438,32 @@ each one does:
inherited non-default unpack state left behind elsewhere in the frame
and misread every row. Fixed by resetting both to the packed defaults
immediately before this upload.
- **`15-android-audio-error-detail.patch`** - `snd_start_digital()`'s
`Mix_OpenAudio()` failure only logged a fixed string, not the actual
reason. Adds `SDL_GetError()` to the log line so the next on-device
failure reports the real underlying error.
- **`16-android-audio-single-device.patch`** - fixes SFX/MIDI music not
playing (see README 5.5.3). `snd_start_digital()` opened two SDL audio
devices: a raw `SDL_OpenAudioDevice()` for cutscene/audiolog audio and
a second one via `Mix_OpenAudio()` for SFX/MIDI - SDL's Android audio
backends only ever allow one open non-capture device, so the second
open always failed. Removes the raw device entirely and feeds cutscene/
audiolog PCM into the `Mix_OpenAudio()` device's output instead, via
`Mix_SetPostMix()` (mixing in rather than overwriting, so it doesn't
silence any SFX/MIDI music playing at the same time). `cutsloop.c`'s
and `audiolog.c`'s `SDL_PauseAudioDevice()` calls (no longer meaningful
without a dedicated device) are removed; the cutscene-audio-stream
pointer swaps they guarded are now protected by `SDL_LockAudio()`/
`SDL_UnlockAudio()` instead, since SDL_mixer's audio thread now reads
that pointer continuously via the post-mix hook.
- **`17-android-native-midi-unsupported.patch`** - `NativeMidiInit()` in
`MusicDevice.c` has real implementations only under `#ifdef WIN32` and
`#elif defined(USE_ALSA)`; on every other platform (Android included)
it fell through to `return 0` (success) without ever opening a device,
so a `midi-backend` preference of "Native" (see README 5.5.4) played
silently forever instead of falling back. Adds an `#else` branch that
returns failure and logs a `WARN`, so `InitDecXMI()`'s existing
fallback-to-dummy-driver logic actually triggers.
`android/gl4es-patches/`:
@@ -517,10 +547,58 @@ left behind elsewhere in the frame and misreads every row - a diagonal
shear/banding pattern. Menus never trigger it because nothing in the
menu-only code paths sets a custom unpack stride first.
**Fix** (`14-android-quad-unpack-state.patch`): explicitly reset both
`GL_UNPACK_ROW_LENGTH` and `GL_UNPACK_ALIGNMENT` to their tightly-packed
values immediately before this `glTexImage2D` call, removing the
dependency on whatever GL state preceded it.
#### 5.5.3. In-game SFX and MIDI music didn't play
Intro cutscene audio played correctly, but in-game SFX and MIDI/background
music never played, on-device.
**Root cause:** `snd_start_digital()` in `engine/src/MacSrc/SDLSound.c`
opened two separate SDL audio devices - a raw `SDL_OpenAudioDevice()`
call (used for cutscene/movie audio, always succeeded) and a second
device via `Mix_OpenAudio()` (used for SFX and, via `Mix_HookMusic`,
MIDI music - always failed, `SDL_GetError()` reporting "Audio device
already open"). SDL's Android audio backends (both AAudio and OpenSL ES)
only ever allow one open non-capture device - forcing OpenSL ES instead
of AAudio (`05-android-audio-driver.patch`) doesn't change this, since
the restriction isn't backend-specific.
**Fix (`16-android-audio-single-device.patch`):** removed the raw
`SDL_OpenAudioDevice()` call entirely. Cutscene/audiolog PCM (previously
delivered via that device's own callback) is now mixed into the single
`Mix_OpenAudio()` device's output via `Mix_SetPostMix()`, additively
(`SDL_MixAudioFormat`) rather than overwritten, so SFX/MIDI music
already mixed into the buffer isn't silenced whenever a cutscene or
audiolog plays. The stream-pointer swaps in `cutsloop.c`/`audiolog.c`
that used to be guarded by pausing the now-removed device are guarded by
`SDL_LockAudio()`/`SDL_UnlockAudio()` instead, since the post-mix hook
runs continuously on SDL_mixer's own audio thread.
#### 5.5.4. MIDI music silent even after audio otherwise worked
After fixing 5.5.3, SFX and cutscene audio played correctly, but MIDI
background music was still silent, with logcat showing `Creating native
MIDI device` instead of `Creating FluidSynth MIDI device`.
**Root cause:** the engine's `midi-backend` preference (`0` = ADLMIDI,
`1` = Native, `2` = FluidSynth) is persisted in a `prefs.txt` file in the
app's private storage, which Android retains across every APK
reinstall/upgrade (only a full uninstall clears it). A stale value of
`1` (Native) from earlier testing was carried forward through every
rebuild this whole session. `SetDefaultPrefs()` (`Prefs.c`) does default
to `2` (FluidSynth) whenever `USE_FLUIDSYNTH` is defined, which it
always has been for this build - the default was never wrong, only the
persisted override. `NativeMidiInit()` has no real implementation on
Android (see `17-android-native-midi-unsupported.patch`), so it silently
reported success without opening anything.
**Fix:** a full uninstall (not just installing a new build over the old
one) clears the stale preference, after which MIDI plays correctly.
`17-android-native-midi-unsupported.patch` additionally makes
`NativeMidiInit()` report failure on unsupported platforms, so a stale
"Native" preference falls back to `InitDecXMI()`'s existing dummy-driver
path (still silent, but with a clear `WARN` in logcat) instead of
silently reporting success while never opening anything - a much faster
diagnosis if this ever recurs.
## 6. License
@@ -1,6 +1,6 @@
--- a/src/MacSrc/Shock.c
+++ b/src/MacSrc/Shock.c
@@ -162,6 +162,17 @@
@@ -162,9 +162,26 @@
void InitSDL() {
SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl");
@@ -12,9 +12,18 @@
+ // src/audio/aaudio/SDL_aaudio.c). SDLSound.c opens two: one directly via
+ // SDL_OpenAudioDevice() for cutscene audio, one via Mix_OpenAudio() for
+ // SFX/MIDI. The older OpenSL ES backend has no such limitation, so force
+ // it instead of AAudio.
+ SDL_SetHint(SDL_HINT_AUDIODRIVER, "openslES");
+ // it instead of AAudio. Uses SDL_HINT_OVERRIDE priority so nothing set
+ // earlier (or by SDL/the platform) can take precedence over it.
+ SDL_SetHintWithPriority(SDL_HINT_AUDIODRIVER, "openslES", SDL_HINT_OVERRIDE);
+#endif
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO) < 0) {
DEBUG("%s: Init failed", __FUNCTION__);
}
+#ifdef __ANDROID__
+ // Standing diagnostic: reports which audio driver SDL actually selected,
+ // to confirm the openslES override above took effect on-device.
+ INFO("SDL audio driver: %s", SDL_GetCurrentAudioDriver());
+#endif
// TODO: figure out some universal set of settings that work...
#ifdef __ANDROID__
@@ -0,0 +1,11 @@
--- a/src/MacSrc/SDLSound.c
+++ b/src/MacSrc/SDLSound.c
@@ -42,7 +42,7 @@
}
if (Mix_OpenAudio(48000, AUDIO_S16SYS, 2, 2048) < 0) {
- ERROR("%s: Couldn't open audio device", __FUNCTION__);
+ ERROR("%s: Couldn't open audio device: %s", __FUNCTION__, SDL_GetError());
}
Mix_AllocateChannels(SND_MAX_SAMPLES);
@@ -0,0 +1,181 @@
--- a/src/MacSrc/SDLSound.c
+++ b/src/MacSrc/SDLSound.c
@@ -19,24 +19,6 @@
// Startup the sound system
- SDL_AudioSpec spec, obtained;
- spec.freq = 48000;
- spec.format = AUDIO_S16SYS;
- spec.channels = 2;
- spec.samples = 2048;
- spec.callback = AudioStreamCallback;
- spec.userdata = (void *)&cutscene_audiostream;
-
- extern SDL_AudioDeviceID device;
- device = SDL_OpenAudioDevice(NULL, 0, &spec, &obtained, 0);
-
- if (device == 0) {
- ERROR("Could not open SDL audio: %s", SDL_GetError());
- } else {
- INFO("Opened Music Stream, deviceID %d, freq %d, size %d, format %d, channels %d, samples %d", device,
- obtained.freq, obtained.size, obtained.format, obtained.channels, obtained.samples);
- }
-
if (Mix_Init(MIX_INIT_MP3) < 0) {
ERROR("%s: Init failed", __FUNCTION__);
}
@@ -50,10 +32,18 @@
Mix_HookMusic(MusicCallback, (void *)&MusicDev);
Mix_VolumeMusic(MIX_MAX_VOLUME); // use max volume for music stream
+ // Cutscene/audiolog playback (AudioStreamCallback, see cutsloop.c) used to
+ // run through its own SDL_OpenAudioDevice() device, separate from this
+ // one - SDL only ever allows one open non-capture device on Android's
+ // audio backends, so that second open always failed with "Audio device
+ // already open" and both SFX and MIDI music (both routed through this
+ // Mix_OpenAudio device) silently never played. Mixed into this device's
+ // post-mix stage instead, so there is only ever one device open.
+ Mix_SetPostMix(AudioStreamCallback, (void *)&cutscene_audiostream);
+
InitReadXMI();
atexit(Mix_CloseAudio);
- atexit(SDL_CloseAudio);
return OK;
}
--- a/src/MacSrc/Shock.c
+++ b/src/MacSrc/Shock.c
@@ -60,8 +60,6 @@
SDL_Palette *sdlPalette;
SDL_Renderer *renderer;
-SDL_AudioDeviceID device;
-
int num_args;
char **arg_values;
--- a/src/GameSrc/cutsloop.c
+++ b/src/GameSrc/cutsloop.c
@@ -83,15 +83,25 @@
extern char FrnSubtitle[256];
extern char GerSubtitle[256];
-extern SDL_AudioDeviceID device;
-
-
+// Runs as SDL_mixer's post-mix hook (see snd_start_digital() in
+// SDLSound.c) rather than a dedicated SDL audio device's callback, so
+// SFX/MIDI music sharing the same device are already mixed into `stream`
+// by the time this runs - mix in rather than overwrite, so they aren't
+// silenced whenever a cutscene or audiolog is playing.
void AudioStreamCallback(void *userdata, unsigned char *stream, int len)
{
+ static Uint8 mixbuf[MOVIE_DEFAULT_BLOCKLEN * 4];
SDL_AudioStream *as = *(SDL_AudioStream **)userdata;
- if (as != NULL && SDL_AudioStreamAvailable(as) > 0)
- SDL_AudioStreamGet(as, stream, len);
+ if (as == NULL || SDL_AudioStreamAvailable(as) <= 0)
+ return;
+
+ if (len > (int)sizeof(mixbuf))
+ len = (int)sizeof(mixbuf);
+
+ int got = SDL_AudioStreamGet(as, mixbuf, len);
+ if (got > 0)
+ SDL_MixAudioFormat(stream, mixbuf, AUDIO_S16SYS, got, SDL_MIX_MAXVOLUME);
}
@@ -153,11 +163,13 @@
if (cutscene_audiostream != NULL)
{
- SDL_PauseAudioDevice(device, 1);
- SDL_Delay(1);
-
+ // AudioStreamCallback (SDL_mixer's post-mix hook) runs continuously on
+ // SDL_mixer's own audio thread and reads this pointer - lock around
+ // freeing it so that thread can't be mid-read when it's freed.
+ SDL_LockAudio();
SDL_FreeAudioStream(cutscene_audiostream);
cutscene_audiostream = NULL;
+ SDL_UnlockAudio();
if (cutscene_audiobuffer)
{
@@ -184,8 +196,6 @@
if (cutscene_audiostream)
{
- SDL_PauseAudioDevice(device, 0);
-
if (cutscene_audiobuffer_size > 0)
{
// === adjust volume in buffer here ===
@@ -325,10 +335,9 @@
if (sfx_on)
{
- SDL_PauseAudioDevice(device, 1);
- SDL_Delay(1);
-
+ SDL_LockAudio();
cutscene_audiostream = SDL_NewAudioStream(AUDIO_U8, 1, fix_int(amovie->a.sampleRate), AUDIO_S16SYS, 2, 48000);
+ SDL_UnlockAudio();
cutscene_audiobuffer_pos = cutscene_audiobuffer;
}
--- a/src/GameSrc/audiolog.c
+++ b/src/GameSrc/audiolog.c
@@ -61,8 +61,6 @@
extern uchar curr_alog_vol;
extern char which_lang;
-extern SDL_AudioDeviceID device;
-
errtype audiolog_init(void) { return OK; }
errtype audiolog_play(int email_id) {
@@ -114,10 +112,9 @@
DEBUG("%s: Playing email", __FUNCTION__);
- SDL_PauseAudioDevice(device, 1);
- SDL_Delay(1);
-
+ SDL_LockAudio();
cutscene_audiostream = SDL_NewAudioStream(AUDIO_U8, 1, fix_int(palog->a.sampleRate), AUDIO_S16SYS, 2, 48000);
+ SDL_UnlockAudio();
audiolog_audiobuffer_pos = audiolog_audiobuffer;
@@ -150,11 +147,14 @@
}
if (cutscene_audiostream != NULL) {
- SDL_PauseAudioDevice(device, 1);
- SDL_Delay(1);
-
+ // AudioStreamCallback (SDL_mixer's post-mix hook, see cutsloop.c)
+ // runs continuously on SDL_mixer's own audio thread and reads this
+ // pointer - lock around freeing it so that thread can't be
+ // mid-read when it's freed.
+ SDL_LockAudio();
SDL_FreeAudioStream(cutscene_audiostream);
cutscene_audiostream = NULL;
+ SDL_UnlockAudio();
if (audiolog_audiobuffer) {
free(audiolog_audiobuffer);
@@ -177,8 +177,6 @@
errtype audiolog_loop_callback(void) {
if (cutscene_audiostream) {
- SDL_PauseAudioDevice(device, 0);
-
if (audiolog_audiobuffer_size > 0) {
int i, vol = curr_alog_vol * 127 / 100; // convert from 0-100 to 0-127
@@ -0,0 +1,17 @@
--- a/src/MusicSrc/MusicDevice.c
+++ b/src/MusicSrc/MusicDevice.c
@@ -569,6 +569,14 @@
// connected
ndev->dev.isOpen = 1;
ndev->dev.outputIndex = outputIndex;
+#else
+ // No native MIDI backend implemented for this platform (e.g. Android) -
+ // report failure so InitDecXMI()'s caller falls back to another device
+ // instead of treating an unopened device as successfully initialized.
+ (void)outputIndex;
+ (void)samplerate;
+ WARN("NativeMidiInit(): native MIDI is not implemented on this platform");
+ return -1;
#endif
// suppress compiler warnings
(void)outputIndex;