Skip to content

fix(audio-miniaudio): restart sound groups in reset() to restore audio after loadGame#224

Merged
fbraz3 merged 1 commit into
fbraz3:mainfrom
coolswood:fix/audio-resume-groups-after-load
Jul 20, 2026
Merged

fix(audio-miniaudio): restart sound groups in reset() to restore audio after loadGame#224
fbraz3 merged 1 commit into
fbraz3:mainfrom
coolswood:fix/audio-resume-groups-after-load

Conversation

@coolswood

Copy link
Copy Markdown

Summary

Fixes silent audio immediately after loading a save game on the MiniAudio backend (default on macOS/Linux). After this fix, sound works the moment the save finishes loading — the user no longer needs to open and close the ESC menu to "wake up" audio.

Problem

Reproduction (verified on macOS, Beta 14, MiniAudio backend):

  1. Start a skirmish, confirm audio works (unit voices, music, UI clicks).
  2. Open ESC menu → Save/Load → Save Game.
  3. Open ESC menu → Save/Load → Load Game → pick the save from step 2.
  4. After the load completes, all audio is silent (unit clicks, music, speech).
  5. Workaround the user discovered: open the ESC menu, then close it — audio returns.

Root cause

When the user opens the ESC menu to pick "Load", GameLogic::setGamePaused(TRUE) runs and eventually reaches MiniAudioManager::pauseAudio(), which calls ma_sound_group_stop() on all four groups (m_soundGroup, m_sound3DGroup, m_speechGroup, m_musicGroup).

GameState::loadGame() then calls TheGameEngine->reset()SubsystemInterfaceList::resetAll()MiniAudioManager::reset(). Inside reset():

  • AudioManager::reset() only touches volume variables.
  • stopAllAudioImmediately() releases each individual PlayingAudio (via ma_sound_uninit()), clearing m_playingSounds / m_playing3DSounds / m_playingStreams.
  • But neither restarts the four ma_sound_group nodes. They stay in the stopped state set by the earlier pauseAudio().

After the load, every newly created sound is attached to one of those stopped groups via ma_sound_init_from_data_source(... groupToUse, ...). miniaudio does not output audio from children of a stopped group, and MiniAudioManager::processPlayingList() sees ma_sound_is_playing() return false for them and immediately reaps them as "finished". Net result: a fully silent game with an empty playing list — the user perceives this as "no audio at all".

Why the ESC menu "fixes" it:

  • setGamePaused(TRUE)pauseGameSound(TRUE) — since GameLogic::reset() cleared m_pauseSound = FALSE, the early-return at GameLogic.cpp:4545 does not fire, so m_pauseSound becomes TRUE and pauseAudio() is invoked again (idempotent — groups are already stopped).
  • setGamePaused(FALSE)pauseGameSound(FALSE)m_pauseSound == TRUE and paused == FALSE, so the early-return does not fire, and resumeAudio() finally runs → ma_sound_group_start() on all groups. Audio is restored.

Fix

Restart the four groups in MiniAudioManager::reset() so that any prior stopped state from pauseAudio() is cleared by the time new sounds start being created after a load.

AudioManager::reset();
stopAllAudioImmediately();
if (m_soundOn || m_sound3DOn || m_speechOn || m_musicOn) {
    resumeAudio(AudioAffect_All);
}

The guard via the per-type *On flags (set to FALSE by setOn(false, AudioAffect_All) when openDevice() fails to initialise the engine) ensures we never call ma_sound_group_start() on an uninitialised ma_sound_group — which would happen if openDevice() returned early before ma_sound_group_init() ran (e.g. TheGlobalData->m_audioOn == false, or any of the ma_*_init calls in openDevice failing).

ma_sound_group_start() is idempotent, so calling it on an already-started group during normal reset() paths (e.g. engine teardown, level transitions) is harmless.

Why this layer and not GameLogic / GameState

  • Placing it in the backend keeps GameLogic / GameState free of audio-backend-specific concerns.
  • It makes MiniAudioManager::reset() self-consistent: it now guarantees the device + groups are in a state ready to play, mirroring what the OpenAL backend achieves implicitly (per-source alSourcePlay in its own resumeAudio).
  • OpenALAudioManager::reset() does not have this bug because its pauseAudio()/resumeAudio() operate per-source (alSourceStop/alSourcePlay), and stopAllAudioImmediately() fully tears down sources — so the concept of a lingering "stopped group" does not exist there.

Verification

  • Built and tested locally on macOS (Apple Silicon, macOS 26.5), preset macos-vulkan.
  • Reproduced the bug on the unpatched build (silent audio after load; ESC toggling restored it).
  • Applied the fix: audio now works immediately after Load Game completes — no ESC toggle needed.
  • Regression checks: fresh skirmish start, exit-to-menu-and-back, and multiple consecutive loads all keep audio working.

AI usage disclosure

Per CONTRIBUTING.md: this change is the result of human + AI collaboration. The bug was diagnosed together (static analysis of the load/audio code paths plus live testing); the fix was written and verified by the human author of this PR.

Checklist

  • Code change follows the // GeneralsX @bugfix author DD/MM/YYYY ... inline comment convention
  • Commit follows Conventional Commits (fix(audio-miniaudio): ...)
  • No changes to build files, only MiniAudioManager.cpp
  • Built and tested on macOS (Apple Silicon)

…o after loadGame

MiniAudioManager::reset() released individual PlayingAudio entries via stopAllAudioImmediately() but never restarted the four ma_sound_group nodes. If a prior pauseAudio() had stopped the groups (e.g. via the ESC menu used to pick 'Load'), they stayed stopped across loadGame().

New sounds attach to the stopped groups via ma_sound_init_from_data_source() and are then silently reaped by processPlayingList() because ma_sound_is_playing() returns false. From the player's perspective this manifests as 'no sound after loading a save' — and toggling the ESC menu appears to fix it because the pause/unpause cycle finally calls resumeAudio() -> ma_sound_group_start().

Fix: restart the groups in MiniAudioManager::reset() so audio works immediately after a save load. Guarded by the per-type *On flags so the call is skipped when openDevice() never initialised the groups (e.g. audio disabled).
@coolswood
coolswood marked this pull request as draft July 18, 2026 15:05
@coolswood coolswood closed this Jul 18, 2026
@coolswood coolswood reopened this Jul 18, 2026
@coolswood
coolswood marked this pull request as ready for review July 18, 2026 15:25
@fbraz3
fbraz3 merged commit 29b891e into fbraz3:main Jul 20, 2026
20 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants