diff --git a/README.md b/README.md index 73c1853..9f81f06 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/android/engine-patches/05-android-audio-driver.patch b/android/engine-patches/05-android-audio-driver.patch index 302d0a3..c498cb2 100644 --- a/android/engine-patches/05-android-audio-driver.patch +++ b/android/engine-patches/05-android-audio-driver.patch @@ -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__ diff --git a/android/engine-patches/15-android-audio-error-detail.patch b/android/engine-patches/15-android-audio-error-detail.patch new file mode 100644 index 0000000..8eb60af --- /dev/null +++ b/android/engine-patches/15-android-audio-error-detail.patch @@ -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); diff --git a/android/engine-patches/16-android-audio-single-device.patch b/android/engine-patches/16-android-audio-single-device.patch new file mode 100644 index 0000000..9327774 --- /dev/null +++ b/android/engine-patches/16-android-audio-single-device.patch @@ -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 + diff --git a/android/engine-patches/17-android-native-midi-unsupported.patch b/android/engine-patches/17-android-native-midi-unsupported.patch new file mode 100644 index 0000000..6f09708 --- /dev/null +++ b/android/engine-patches/17-android-native-midi-unsupported.patch @@ -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;