Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT(client): Add possibility to change notification volume #5725

Merged
merged 4 commits into from
Aug 1, 2022

Conversation

Hartmnt
Copy link
Member

@Hartmnt Hartmnt commented Jun 21, 2022

Fixes #3963

Here is the background information about my naive implementation:

  • AudioOutputSample objects - used for playing audio files, and AudioOutputSpeech objects - used for playing user speech, both implement AudioOutputUser.
  • AudioOutputUser objects are queued in qmOutputs in AudioOutput.cpp and mixed in its mix method.
  • There is a check in mix which tries to cast the queued AudioOutputUser objects to AudioOutputSpeech to determine, if the object is user speech or not. CODE
  • My implementation attaches to that check and handles the case in which the object is not user speech.
  • It then simply loads the newly created iNotificationVolume value from the settings and stores it in the volumeAdjustment variable already used for local volume adjustments and priority speaker status.

My naive implementation has the drawback that you can not independently set the volume of "Audio on/off/mute cues" and regular notification sounds. I want to improve the implementation, but that requires changing a lot more code.

Improved implementation considerations:

  • Add volume sliders to the settings
  • Add a volume member variable to AudioOutputSample.
  • AudioOutputSample objects are exclusively created in the playSample method of AudioOutput.cpp. Add an additional option to set the volume when calling playSample which then creates an AudioOutputSample with the corresponding value set.
  • Consider the playSample API change
  • Perfom the same cast check that is already done in mix, but for AudioOutputSample and if it succeeds, set volumeAdjustment to the value stored in the sample object.
  • Evaluate each call of the playSample function and determine whether or not to set the volume option to a specific value or a stored setting such as fNotificationVolume or fCueVolume

TODO:

  • Implement API change for playSample
  • Evaluate unifying code for volume adjustment (should this db -> factor be shared code somehow?)
  • How is the UI supposed to be changed
  • Update Notification&Cue sound volume in real time (as local volume adjustment is done)
  • Testing (including Plugins)
  • Translations
  • Squash commits
  • Rebase (if needed)
  • Developer Documentation
  1. I created a volume slider for notification sounds in Log.ui in the Misc box. If we implement the ability to change audio cue volume, we will have 3 different sliders for "notification volumes". One for TTS, one for audio cues, and one for actual notifications. Where do we put these sliders in the UI? (P.S.: Why is the Misc section in Log.ui not the last group item?)
No. Mockup Pros Cons
0 0 slider seperate, options seperate * Minimal effort
* Does not change existing UI much
* Volume right next to specific option
* All message volume sliders, which more or less do the same thing, thrown around in 3 different places
1 1 slider seperate, audio cue in notification tab * Volume right next to specific option
* All volume sliders in same tab
* Audio cue is moved to notifications, but it fits better to Audio Input where it currently is
2 2 slider together in notification tab * All volume sliders neatly packed together
* Group of functionally similar options
* Audio cue still in audio input
* Difference between audio cue volume and notification volume easily visible
* Audio cue volume separated from audio cue option
* TTS slider requires pre-processor macro and has to be turned off if compiled without TTS enabled
3 3 slider together, except TTS same as 2 same as 2, but TTS slider stays in TTS group

@Hartmnt
Copy link
Member Author

Hartmnt commented Jun 30, 2022

@Krzmbrzl Can you take a look at the feedback requests?

@Hartmnt
Copy link
Member Author

Hartmnt commented Jun 30, 2022

I think these are the options for the playSample API call

  • Leave it at 100% aka no change - independent of setting values
  • Apply notification volume setting value and don't expose volume of playSample via the API
  • Expose volume to API, but give it a default value of 1.f

@Krzmbrzl
Copy link
Member

Sorry for the late response, but I am currently quite busy 👀

Does the current coding guideline allow for default values in constructor parameters? Edit: I think I don't need default constructor parameters, since AudioOutputSample objects are only created by playSample.

Default values in constructor parameters should be fine

playSample is part of the Mumble/Plugin API. Do we want to change the signature of that method, or do we want to create an additional playSample with a volume parameter as to not break any existing usage of playSample (I suspect the latter)

We can add the additional parameter in a new version of the plugin API, ensuring that whenever the old version is used, a sensible default (probably 1) is used. So if a plugin is compiled against the more recent API version, it gets access to the volume parameter, otherwise it gets a handle to the legacy function (that applies the default when relaying the call).

The naive implementation uses the legacyLoad as well as SettingsMacros.h. Do we need to use both, or are new settings only supposed to be added to the SettingsMacros file?

legacyLoad is only required for settings that existed before the settings format change to ensure that the old settings file can still be read (and thus making sure the settings are not lost). New settings only need to implement the macro-stuff required for the JSON (de)serialization.

The naive implementation stores iNotificationVolume as an integer in the range [0, 200]. This is because QSlider are always used with integer values (for example the TTS volume slider). This has two problems: 1: The setting value needs post processing, since volumeAdjustment is a float multiplier. 2: Since humans do not process audio linearly, it would be better to apply some function to the setting value to get a logarithmic scale. I am not 100% sure how to do this. Use the code in VolumeAdjustment?

Can't we just use the same approach that we also use for volume adjustments, where we simply use a dB scale? See e.g. the UserVolumeAdjustment class for how this is done.

For feedback on the UI side, I'll need to find a bit more time than I currently have at hand.

@Krzmbrzl
Copy link
Member

Expose volume to API, but give it a default value of 1.f

That's probably what I would do. Auto-applying the notification volume to samples might not make sense as not all samples played by plugins can be (in general) thought of as notifications. However, by exposing the volume setting to the pluign, it can fetch this setting, if it is indeed playing a notification and wants to adhere to the setting.

@Hartmnt Hartmnt force-pushed the feat_notification_volume branch 3 times, most recently from 7b0fd5c to 638f256 Compare June 30, 2022 14:33
@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 1, 2022

@Krzmbrzl I implemented the advanced version of the feature and there is basically just the API change and the UI decision missing.

We can add the additional parameter in a new version of the plugin API, ensuring that whenever the old version is used, a sensible default (probably 1) is used. So if a plugin is compiled against the more recent API version, it gets access to the volume parameter, otherwise it gets a handle to the legacy function (that applies the default when relaying the call).

That's probably what I would do. Auto-applying the notification volume to samples might not make sense as not all samples played by plugins can be (in general) thought of as notifications. However, by exposing the volume setting to the pluign, it can fetch this setting, if it is indeed playing a notification and wants to adhere to the setting.

I understand what you are saying and agree, but I fail to immediately comprehend what actual code changes are necessary for that, because I'm not familiar with this kind of API design. Do I literally just add a default method parameter and call it a day? Or do I create a separate playSample method or even a new API_1.something file? If you could summarize what to do in a few sentences, I will gladly try to implement that.

@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 11, 2022

@Krzmbrzl I understand you are busy, but could you give me a few lines of text on how you want the API change implemented? I'd like to do the necessary changes myself, but going by trail and error would probably waste everyone's time.

@Krzmbrzl
Copy link
Member

The necessary changes for the API are

  • Copy the API header and in the new 1.2 version of the API, adapt the version specifier accordingly:
    #define MUMBLE_PLUGIN_FUNCTIONS_MINOR_MACRO 1
  • Add a new client-side function for fetching the API 1.2 (In case you're wondering: v1.1 only changed something on the plugin's side and thus there is no corresponding change in the client code)
    MumbleAPI_v_1_0_x getMumbleAPI_v_1_0_x() {
    return { freeMemory_v_1_0_x,
    getActiveServerConnection_v_1_0_x,
    isConnectionSynchronized_v_1_0_x,
    getLocalUserID_v_1_0_x,
    getUserName_v_1_0_x,
    getChannelName_v_1_0_x,
    getAllUsers_v_1_0_x,
    getAllChannels_v_1_0_x,
    getChannelOfUser_v_1_0_x,
    getUsersInChannel_v_1_0_x,
    getLocalUserTransmissionMode_v_1_0_x,
    isUserLocallyMuted_v_1_0_x,
    isLocalUserMuted_v_1_0_x,
    isLocalUserDeafened_v_1_0_x,
    getUserHash_v_1_0_x,
    getServerHash_v_1_0_x,
    getUserComment_v_1_0_x,
    getChannelDescription_v_1_0_x,
    requestLocalUserTransmissionMode_v_1_0_x,
    requestUserMove_v_1_0_x,
    requestMicrophoneActivationOverwrite_v_1_0_x,
    requestLocalMute_v_1_0_x,
    requestLocalUserMute_v_1_0_x,
    requestLocalUserDeaf_v_1_0_x,
    requestSetLocalUserComment_v_1_0_x,
    findUserByName_v_1_0_x,
    findChannelByName_v_1_0_x,
    getMumbleSetting_bool_v_1_0_x,
    getMumbleSetting_int_v_1_0_x,
    getMumbleSetting_double_v_1_0_x,
    getMumbleSetting_string_v_1_0_x,
    setMumbleSetting_bool_v_1_0_x,
    setMumbleSetting_int_v_1_0_x,
    setMumbleSetting_double_v_1_0_x,
    setMumbleSetting_string_v_1_0_x,
    sendData_v_1_0_x,
    log_v_1_0_x,
    playSample_v_1_0_x };
    }
  • It seems to make sense to rename API_v_1_0_x.cpp to API_v_1_x_x.cpp
  • Add a new API function playSample_v_1_2_x in analogy to

    mumble/src/mumble/API.h

    Lines 159 to 160 in 93c13bd

    void playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath,
    std::shared_ptr< api_promise_t > promise);

    The new function should then include the parameter for the volume. I would essentially copy the implementation of the old function and then replace the old function's implementation by an indirection that simply calls the new function with a default parameter.
  • Add a new else case to
    if (apiVersion >= mumble_version_t({ 1, 0, 0 }) && apiVersion < mumble_version_t({ 1, 2, 0 })) {
    MumbleAPI_v_1_0_x api = API::getMumbleAPI_v_1_0_x();
    registerAPIFunctions(&api);
    } else {

    where the new 1.2 API is created and handed to the plugin (if the plugin is recent enough)

@Krzmbrzl Krzmbrzl added client feature-request This issue or PR deals with a new feature labels Jul 22, 2022
@Hartmnt Hartmnt force-pushed the feat_notification_volume branch 5 times, most recently from c4115de to 9d0fb19 Compare July 25, 2022 17:00
@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 25, 2022

@Krzmbrzl Hi, and thanks for the reply.

I have implemented your API change directions (i think) in this commit, but there is still a structural problem.

The code now defines two structs each for one of the supported API versions (1.0 and 1.2) in the respective MumbleAPI_[...].h header files, but the headers contain an include guard which currently is the same for both files. That of course prevents both API structs of existing at the same time. But, both structs are used in Plugin.cpp

Do you want me to create distinctive include guards or are there side effects? What do we do about mumble_api_t? Or did I maybe misunderstand some of the directions?

@Krzmbrzl
Copy link
Member

Ah right.

Or did I maybe misunderstand some of the directions?

No, no, you did exactly as I instructed. That's just a bit of a flaw in my original design of how these files come together.

Do you want me to create distinctive include guards or are there side effects? What do we do about mumble_api_t?

Yes, I think this would be the best solution and I currently don't see any potential side-effects of this. The definition of mumble_api_t I would mask with an ifdef like this:

#ifndef EXTERNAL_MUMBLE_PLUGIN_API_NO_MUMBLE_API_T_DEFINE
typedef struct MumbleAPI_v_1_0_x mumble_api_t;
#endif

Do this in both of the API header files and then inside Plugin.cpp define this lengthy macro before including the API headers (and remember to undef again afterwards in order to not cause issues in unity builds).
This way, downstream users (who are supposed to only include a single API header) can continue using mumble_api_t as usual and wherever multiple API headers need to be included, we this macro will prevent name-clashes.

plugins/MumblePlugin_v_1_2_x.h Outdated Show resolved Hide resolved
src/mumble/API_v_1_x_x.cpp Outdated Show resolved Hide resolved
@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 26, 2022

@Krzmbrzl Now the build for the current commit fails, because the MUMBLE_PLUGIN_API_MAJOR_MACRO defines are done more than once.

I think this needs a new structure. I will think about the problem myself, but I also of course await further instructions.

@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 26, 2022

We need the structs for backwards compatibility, but would it be possible to define the rest of the header files only for the most recent API version (1.2 in this case) including mumble_api_t and the other vars/defines?

Plugins compiling against whatever the current source code version is would work the same. And legacy plugins don't need any of the other definitions as they are already compiled, right? They just need the backwards compatibility struct.

The only thing that would not work is compiling legacy plugins with a more recent code base. So you cant compile a plugin with API 1.0 using mumble source code with API 1.2.

Am I missing something?

@Krzmbrzl
Copy link
Member

Krzmbrzl commented Jul 26, 2022

because the MUMBLE_PLUGIN_API_MAJOR_MACRO defines are done more than once.

Hm. We could wrap those definitions inside some #ifdefs as well but this does not feel like the proper solution 🤔

We need the structs for backwards compatibility, but would it be possible to define the rest of the header files only for the most recent API version (1.2 in this case) including mumble_api_t and the other vars/defines?

Yes and no. In principle yes, but the point of explicitly keeping the older headers around is exactly that legacy plugins can still be compiled without requiring changes to their code.
If we followed the philosophy of "legacy plugins are compiled already, anways" then we could just make changes in-place to the header files.

So ideally, every of these headers would always work as a stand-alone header when it is the only header that is included but "do the right thing" if multiple headers are included (as in our case).

For the macro-side of things we could easily #undef old definitions and then redefine the new ones, but for the global variables that won't work.

What I could think of would be the following:
Every new iteration of the header defines the version macros and then includes the old header. And then in the v1.0.0 header file, those macros are guarded like this

#ifndef MUMBLE_PLUGIN_API_MAJOR_MACRO
#    define MUMBLE_PLUGIN_API_MAJOR_MACRO 1
#    define MUMBLE_PLUGIN_API_MINOR_MACRO 0
#    define MUMBLE_PLUGIN_API_PATCH_MACRO 0
#endif

Ensuring that in such a cascading event the version number of the top-most (most recent) header is used for all definitions.

This also reduces the amount of copied code but has the distinct disadvantage that any plugin developer now has to have all of those header files in their include path instead of only the latest.
Plus there would be now way to include only the most recent struct version without the older ones - not sure if this is really an issue though.

Maybe we could also just guard all of these "convenience definitions" (aka everything except the bare struct definition) via an #ifndef EXTERNAL_MUMBLE_NO_UTILITY_DEFINITIONS. After all in our code base we don't need any of these convenience definitions (in fact - as is evident here - they only cause trouble there).
That would make the code a bit less readable due to all of those preprocessor branches but at least from a functional point of view that seems to do the right thing 🤔

@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 26, 2022

@Krzmbrzl Could we create some sort of MumbleAPI_latest.h which we use in the mumble plugin backend to include all header files without the recursion and introduce some define there which prevents the double definition of all the vars/defines?

Then plugin authors only include the plugin header they need, and the defines would still work. And we include "latest" giving us all the structs but only the most recent defines

@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 26, 2022

@Krzmbrzl Check out this commit, where I introduced my idea. This now at least compiles correctly. Tell me what you think about this non-recursive design.

Edit: Changed some format, comments and naming

Copy link
Member

@Krzmbrzl Krzmbrzl left a comment

Choose a reason for hiding this comment

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

I like your latest approach to including the API structs.

I am wondering whether we need the auxiliary definitions at all in our code base though. So why not remove them from all headers by also including the most recent header only after the macro definition that prevents the defines from happening?

plugins/MumbleAPI_latest.h Outdated Show resolved Hide resolved
plugins/MumbleAPI_latest.h Outdated Show resolved Hide resolved
plugins/MumbleAPI_latest.h Outdated Show resolved Hide resolved
@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 26, 2022

I am wondering whether we need the auxiliary definitions at all in our code base though. So why not remove them from all headers by also including the most recent header only after the macro definition that prevents the defines from happening?

We are providing "the real" MUMBLE_PLUGIN_API_VERSION to plugins via setMumbleInfo in Plugin.cpp
Furthermore, it could be very much possible that we would use/display etc. the current API version in some form in the future. So I would populate these fields with the latest version.

I've pushed a new commit. What do you think?

@Krzmbrzl
Copy link
Member

Do you want me to make them update in real-time, too?

No - only when you hit Apply or Ok should the settings take effect (but that's the way it works anyway). The volume adjustment dialog is special in that it applies the changes in realtime so you can preview your changes as you go. But then again it is also not part of the regular settings page.

@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 29, 2022

@Krzmbrzl Noted. Upon investigating on how to theoretically do this, I have noticed it would require quite some change to ConfigWidget and ConfigDialog because a) accept and save are const in the widget and b) QWidget does not have a slot for reject by default, so you would have to connect the signal from ConfigDialog to each Widget and so on and so forth...

Anyway, I am going to squash the commits now. I have successfully tested the functionality and the USE_NO_TTS code path. I want to test compiling and installing a plugin with the new API version. What I can not test right now is TTS volume, but I suspect this still works as before since I only changed the slider position...

What do I do about the translations? Run the translation python script and add a new commit?

@Hartmnt Hartmnt marked this pull request as ready for review July 29, 2022 14:25
@Hartmnt Hartmnt requested a review from Krzmbrzl July 29, 2022 14:26
@Krzmbrzl
Copy link
Member

What do I do about the translations? Run the translation python script and add a new commit?

Exactly. Once the code changes are done, just run the script and it will create a separate commit with the translation update. Then just push both commits.

@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 29, 2022

A heads up: I messed up the argument order in the API and will push the commit again now

@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 29, 2022

@Krzmbrzl My tests have concluded. The playSample API call now works as intended. TTS volume was not tested.

I will await your final review now and then add the translation commit.

I would like to add, or suggest to add, documentation for developers in /docs/dev on how to increment the mumble API version. Thoughts?

PS: The plugin file caching seems kinda broken in Mumble right now. The file is not removed on "unload" and re-added to the list when you restart the application. I had to manually remove the plugin file between tests.

@Krzmbrzl
Copy link
Member

I would like to add, or suggest to add, documentation for developers on how to increment the mumble API version. Thoughts?

Sure, more docs is always welcome. Just add it in a separate DOCS commit.

PS: The plugin file caching seems kinda broken in Mumble right now. The file is not removed on "unload" and re-added to the list when you restart the application. I had to manually remove the plugin file between tests.

Well unloading is not supposed to remove any files. However, it should remove the plugin from the list of currently loaded plugins and on systems like windows, it should stop the access to that file so that the file can be modified. It's meant for hot-swapping plugins.

What you seem to have searched for is an unistall button. At this point that does not yet exist.

@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 29, 2022

Well unloading is not supposed to remove any files. However, it should remove the plugin from the list of currently loaded plugins and on systems like windows, it should stop the access to that file so that the file can be modified. It's meant for hot-swapping plugins.

What you seem to have searched for is an unistall button. At this point that does not yet exist.

I see. So the workflow on Linux is to just manually replace the file in XDG_SOMETHING and reload/restart Mumble, right? Because that's essentially what I did

@Krzmbrzl
Copy link
Member

I see. So the workflow on Linux is to just manually replace the file in XDG_SOMETHING and reload/restart Mumble, right?

What's XDG_SOMETHING meant to be? The workflow on Linux is to simply overwrite the plugin .so and then reload the plugin in Mumble or restart Mumble altogether.

@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 29, 2022

XDG_SOMETHING

The xdg default config folder on Linux where Mumble stores its settings. I do not remember the exact name off the top of my head.

src/VolumeAdjustment.h Outdated Show resolved Hide resolved
src/mumble/AudioOutput.cpp Outdated Show resolved Hide resolved
@Krzmbrzl
Copy link
Member

@Hartmnt I took the liberty and pushed two further commits to your branch.

  1. I added a spin-box for the TTS volume (in percent rather than in dB) because I found it looked weird to have spin boxes for cue and notification volume but not for TTS
  2. While code-reviewing I encountered a bug in a TTS implementation that I simply fixed

If you are okay with 1), then please just squash that into your feature commit, but please just leave 2) as a separate one.

@Hartmnt Hartmnt force-pushed the feat_notification_volume branch 2 times, most recently from 53441d2 to c3c2b40 Compare July 29, 2022 17:59
@Hartmnt
Copy link
Member Author

Hartmnt commented Jul 29, 2022

@Krzmbrzl I added the translation and the documentation

@Hartmnt Hartmnt changed the title WIP: FEAT(client): Add possibility to change notification volume FEAT(client): Add possibility to change notification volume Jul 29, 2022
docs/dev/IncrementingTheMumbleAPI.md Outdated Show resolved Hide resolved
docs/dev/IncrementingTheMumbleAPI.md Outdated Show resolved Hide resolved
Hartmnt and others added 4 commits August 1, 2022 12:27
Previously all notification sounds were played as is, without
taking anything into account. The only way to change the
volume was to manually edit the sound files, or change the
overall volume of the entire Mumble application.

This commit adds the ability for the user to adjust the
volume of notification sounds and audio cues. There are two
new settings added "notificationVolume" and "cueVolume"
to adjust the volume independently. Sliders in the "Messages"
have been added and together with the existing TTS volume
slider make up the new group "Message Volume".

A side effect is the centralization of the db <-> factor
conversion functions in the "VolumeAdjustment" class.

Furthermore, this commit also introduces a change to the
"playSample" API call, accepting a volume parameter,
and therefore bumps the Mumble API version to 1.2.x.

Implements mumble-voip#3963
When using the Qt-provided TTS implementation, the setVolume function
call would simply pass our setting for the TTS volume to Qt unchanged.
However, while our code uses an integer ranging from 0 to 100 to
indicate TTS volume, Qt's docs explicitly state
> This property holds the current volume, ranging from 0.0 to 1.0.
and thus we are essentially always passing in values that are way too
large (which presumably just get truncated to 1.0, effectively breaking
being able to adjust the TTS volume).

This commit fixes this by making sure to convert our TTS volume into the
proper range before passing it to Qt.
Describe necessary steps to implement a new API version for plugins.
@Krzmbrzl Krzmbrzl merged commit 2ca3edc into mumble-voip:master Aug 1, 2022
@Krzmbrzl
Copy link
Member

Krzmbrzl commented Aug 1, 2022

Thank you very much for implementing this 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
client feature-request This issue or PR deals with a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adjust sound notification volume
2 participants