Skip to content

feat(audio): mute on pause#6165

Draft
mwerle wants to merge 12 commits intopioneerspacesim:masterfrom
mwerle:trivial/mute-on-pause
Draft

feat(audio): mute on pause#6165
mwerle wants to merge 12 commits intopioneerspacesim:masterfrom
mwerle:trivial/mute-on-pause

Conversation

@mwerle
Copy link
Contributor

@mwerle mwerle commented Jun 15, 2025

DRAFT PR

TODO:

  • add option whether or not audio should be muted on pause
  • investigate if muting is possible when the game window loses focus
  • determine best place to send out the new LUA events "onFocusGained/Lost"
  • mute only SFX or at least add an option whether or not to mute the music

Mute all audio when the game is paused, either by opening the settings window, or by clicking on the Pause icon, and resume when the game is resumed.

NOTE: This PR conflicts directly with #5644 ; so as it appears this is very much personal preference, it may be necessary to add a settings toggle which would make this a slightly less trivial PR.

@DamagedEngine
Copy link

The toggle could have a third option added which only mutes the game when the game window is out-of-focus.

The setting could be called "Mute on pause:" with the following options:

  1. Always
  2. When out-of-focus
  3. Never

This way the music would not disturb players who pause the game to do something else, without interrupting the music for players who only paused the game to interact with menus and look around.

@impaktor
Copy link
Member

I would expect music to keep playing in paused state.

@DamagedEngine
Copy link

Perhaps it would be better to just add a mute-on-defocus option without tying it to the pause function? I do think the game needs a way to stop it from playing sound in the background when another window such as a web browser is in focus. That desire is presumably why some users might expect the game to become muted on pause.

@mwerle
Copy link
Contributor Author

mwerle commented Jun 15, 2025

I would expect music to keep playing in paused state.

I would not - hence this PR. Especially things like engine noise are incredibly annoying (to me) when I pause the game (I almost always have the music muted already anyway) to work on tweaking some code, but the audio engine does not allow just pausing and resuming the FX (intended or bug?)

However, point taken, I will create an option. Not sure if it's (easily) possible to detect the out-of-focus state, but will check if that can be supported.

Thank you for the feedback.

@mwerle mwerle marked this pull request as draft June 15, 2025 21:42
@mwerle
Copy link
Contributor Author

mwerle commented Jun 15, 2025

Perhaps it would be better to just add a mute-on-defocus option without tying it to the pause function? I do think the game needs a way to stop it from playing sound in the background when another window such as a web browser is in focus. That desire is presumably why some users might expect the game to become muted on pause.

Conversely I personally do NOT want the audio to become muted just because I switch to a note-taking app or browser in the middle of a gaming session when I am not pausing the game to do so (eg, during a long transfer burn). I am still playing, just using out-of-game tooling to support my game experience.

As I said, this is highly personal. On the one hand having a million game option toggles should be avoided (difficult to test, if nothing else), otoh, something as personal as this I think warrants it.

mwerle added 4 commits June 16, 2025 11:53
Mute all audio when the game is paused, either by opening the settings
window, or by clicking on the Pause icon, and resume when the game is
resumed.
Add a settings toggle in the Audio Settings tab controlling whether audio
is muted on pause or not (default: no).
TODO: This is probably not the best place to implement detecting the window
focus lost/gained status and sending out the corresponding LUA event, but
"It Works For Me(tm)".

Request suggestions on where to implement this instead. I have investigated
the "opengl/RendererGL.cpp" which would require also exposing this to the
generic "Renderer" class in order to be accessible from elsewhere.

Implementing it directly in the "RengererOGL" class is infeasible as it
does not know about LUA.
Add a settings toggle in the Audio Settings tab controlling whether audio
is muted on focus lost/gained or not (default: no).

This setting works in conjunction with the mute-on-pause.
@mwerle mwerle force-pushed the trivial/mute-on-pause branch from 81a4c53 to 0fae0df Compare June 16, 2025 02:54
@mwerle
Copy link
Contributor Author

mwerle commented Jun 16, 2025

I pushed an update which also (un)mutes on focus. This additional setting works in conjunction with the (un)mute-on-pause.

While my testing so far appears to show it working as intended, there are two (possibly more) issues:

  • doesn't work in the main menu (not sure if there's a place where the handlers can be implemented where it always works?)
  • the place where I implemented sending out the new LUA "onFocusGained/Lost" events (Pi.cpp:GameLoop::Update()) is probably not where this logic should be. I'd like suggestions on where to implement this instead (please see the commit message for more details)

@impaktor
Copy link
Member

We probably want the opinions of @bszlrd and @sturnclaw, and why not @fluffyfreak who works as a game dev: what is the sane / expected behaviour for music & FX when paused, and/or paused and/or unfocused window. FX be muted feels natural, but not sure about the other.

I assume FX and music can be handled separately since we can set their volume independently)

@sturnclaw
Copy link
Member

Quick feedback here before I go to bed:

IMO, the focus-related events should be pushed to the UI event queue rather than the main game event queue. IIRC, the main event queue is (intentionally) not emitted while the game is paused, and so all events which exist at a higher level than the game-world context should be sent to the UI queue instead. There is an existing example of how to do so in the codebase; you can find it by looking up callsites of the appropriate LuaEvent overloads which take a LuaRef as their first argument.

I will fully review the comment in question tomorrow regarding where to hang the event generation code off of.

On the whole I like the idea here; mute-on-minimize/focus loss and mute-on-pause are definitely orthogonal states; personally I'd like the ambient FX to stop playing but the music to continue while paused (as a baseline) regardless of the above setting states (ambient FX is game-layer audio, music is application-layer/interface-layer audio), but we can look at that in more depth with a deeper change to the sound system.

Side complication: how does this PR interact with interface sounds (i.e. clicks)? They should not be muted while the pause menu is open. We don't currently have a lot of them, but audible interface feedback is something I'd like to normalize across the UI long-term.

@mwerle
Copy link
Contributor Author

mwerle commented Jun 16, 2025

So possibly it's a bug, but muting SFX and then unmuting them (using the LUA functions) does NOT restore the SFX volume. This is different to how muting/unmuting the Master Volume works. This is primarily why I went for Master Volume in the current implementation..

Personal preference is for it to behave as I have implemented it - ie,both music and SFX are muted. I guess this will require another setting.. but then again it probably won't impact me much since, as I mentioned, I almost always have the music turned off anyway so assuming the above is agreed to be a bug, I can fix that bug and then change my implementation to only (un)mute the SFX..

Fully agree that interface clicks should not be muted, so will need to modify this PR anyway. Thanks for all the feedback so far.

@mwerle
Copy link
Contributor Author

mwerle commented Jun 17, 2025

IMO, the focus-related events should be pushed to the UI event queue rather than the main game event queue. ... There is an existing example of how to do so in the codebase; you can find it by looking up callsites of the appropriate LuaEvent overloads which take a LuaRef as their first argument.

Based on the only example I could find ("onGamePaused" and "onGameResumed"), I changed the event emitting to:

				LuaEvent::Queue(PiGui::GetEventQueue(), hasFocus? "onFocusGained" : "onFocusLost");

but haven't figured out how to register and receive these events on the LUA side, as "onGamePaused" is also emitted to the main game event queue and registered in the normal way on the LUA side.

Copy link
Member

@sturnclaw sturnclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully these answer your questions. I've left a few tangentially-related comments, as well as one nitpick on the wording of the UI string for the focus-lost option.

Input.EnableBindings()
end)

Event.Register("onGamePaused", function()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These four event handlers should become ui.Events:Register(..., function() ... end). onGamePaused et. al. is pushed to the UI event queue and there is no guarantee that events on the main queue will be processed until the pause ends.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out it needed to be ui.Events.Register(...) but thank you, finally managed to get the UI events working.

This is now also the only example in the entire codebase which registers a UI event in the LUA side (as far as my grep skills can tell).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you; implemented in b5e8818 (to be squashed)

src/Pi.cpp Outdated
bool hasFocus = (flags & SDL_WINDOW_INPUT_FOCUS) != 0;
if (hasFocus != lastFocus) {
lastFocus = hasFocus;
LuaEvent::Queue(hasFocus? "onFocusGained" : "onFocusLost");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proper place to handle this would be to listen for SDL_WINDOWEVENT_FOCUS_GAINED et. al. in core/GuiApplication.cpp, specifically the PollEvents function. That should dispatch to a virtual member method:

virtual void OnWindowFocusChanged(bool newFocus) {}

That method would then be implemented by Pi::App, and an event dispatched to the UI event queue like so:

void Pi::App::OnWindowFocusChanged(bool newFocus)
{
    if (!PiGui::GetEventQueue().IsValid())
        return;
    LuaEvent::Queue(PiGui::GetEventQueue(), newFocus ? "onFocusGained" : "onFocusLost");
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you; implemented in b5e8818 (to be squashed)

if (lua_isnone(l, 1)) {
return luaL_error(l, "SetMuteOnPause takes one boolean argument");
}
const bool mute= lua_toboolean(l, 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: the codebase's stylistic conventions require a space on both sides of the equals-sign here. We used to have a clang-format nag running on PRs to catch this stuff automatically, but it got far too opinionated without config-file levers to disable its opinions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks; typo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9240816 (to be squashed)

local effectsMuted = Engine.GetEffectsMuted()
local effectsLevel = Engine.GetEffectsVolume()*100
local muteOnPause = Engine.GetMuteOnPause()
local muteOnFocus = Engine.GetMuteOnFocus()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a tangent: I hate boilerplate like this and would much prefer we just expose an Engine.GetConfigOption(key, defaultValue), Engine.SetConfigOption(key, value) API with consistent (and searchable!) string keys used directly.

This is not actionable feedback within the scope of this PR, but is something you could potentially implement in a separate, later PR if you so desired.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a LUA programmer so will take your lead on this. Personally I'm not so fond of string parameterisation like that since compilers/analysers cannot detect errors, which can lead to some very subtle and difficult to trace bugs.

},
"MUTE_ON_FOCUS": {
"description": "Setting for muting audio when the game window loses focus.",
"message": "Mute on Focus"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"message": "Mute on Focus"
"message": "Mute on Window Focus Lost"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3bd058e (to be squashed)

mwerle and others added 3 commits June 17, 2025 17:44
As suggested by sturnclaw, refactor the code generating the "onFocusXX"
events to:
- process the SDL event in the GuiApplication class
- raise the events on the UI event queue rather than the main event
  queue
-- register "ui.Events" handlers instead.

Co-authored-by: Axtel Sturnclaw <sturnclaw@protonmail.com>
@mwerle
Copy link
Contributor Author

mwerle commented Jun 17, 2025

Thank you for the detailed information.

Addressed all issues, but still need to implement muting only the SFX. That will require a bugfix to the way the SFX are muted/unmuted (SFX volume is not restored when SFX are unmuted).

mwerle added 5 commits June 17, 2025 20:30
On the suggestion of sturnclaw, implement generic getters and setters for
game settings and use them in the new settings for this feature.

Future work will rework the existing settings to match this approach.
For some reason comparing the value to the return value to indicate changes
never returned false, which meant any code called due to a changed slider
got called every frame.
Presumably a copy-paste error introduced in commit
2a4a802.
When muting, the audio volume is set to 0 and the current audio volume
is saved to the settings.

When unmuting, the audio volume is set to the current audio volume (ie, 0)
instead of the audio volume saved in the settings, effectively resulting
in the audio still being muted.

Fixed by setting the volume to whatever was last saved into the settings.
Attempt to mute and unmute music and effects separately.

Kindof works for music (it is stopped and restarted instead of muted), and
doesn't work at all for effects (literally nothing happens).
@mwerle
Copy link
Contributor Author

mwerle commented Jun 17, 2025

Ok, basically, the way the music system is implemented does not really allow for muting/unmuting of music and effects.

Music: when muted, stops the music player, when unmuted, restarts playing the music from the beginning. (ie, it neither pauses the music, nor continues playing it muted. It's more a stop/start than a mute/unmute function)

Effects: literally does nothing. Once a sample has been queued for playing, it is played at its current volume. Muting it does nothing. Changing volume does nothing. Furthermore, UI effects and "background noise" appear to both come under "Effects" so it is not possible to mute the background noise but leave UI clicks enabled.

TEST: Start a new game and listen to the background noise effects. Open settings window and try to mute or change the volume of the effects. It will affect interface clicks (since each one is a new effect) but will not change the background noise effects.
Toggling through the external views (F1) will restart the effects; volume changes/muting take effect for the next time an effect is played.


Since this does not work as desired by the community, I will retract this PR and maintain this in my personal branch. Muting "Master Volume" does what I want it to do. Fixing the entire audio system so muting can work as desired by the community is beyond my interest and skill level.

That said, some of the commits may be useful to merge anyway to enable some other functionality, such as events for lost/gained focus. I also found a couple of minor bugs which should be merged. I will create a new "minor-fixes" branch for those.

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.

4 participants