Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/dev/build-instructions/cmake_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ All warnings are treated as errors.
Build support for WASAPI.
(Default: ON)

### win-universal-mute

Build support for Windows Universal Mute (Win11 22H2+) with simple fallback for earlier versions.
(Default: ON)

### xboxinput

Build support for global shortcuts from Xbox controllers via the XInput DLL.
Expand Down
15 changes: 15 additions & 0 deletions src/mumble/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ if(WIN32)
option(wasapi "Build support for WASAPI." ON)
option(xboxinput "Build support for global shortcuts from Xbox controllers via the XInput DLL." ON)
option(gkey "Build support for Logitech G-Keys. Note: This feature does not require any build-time dependencies, and requires Logitech Gaming Software to be installed to have any effect at runtime." ON)
if(MSVC)
option(win-universal-mute "Build support for Windows Universal Mute (Win11 22H2+) with simple fallback for earlier versions." ON)
endif()
elseif(UNIX)
if(APPLE)
option(coreaudio "Build support for CoreAudio." ON)
Expand Down Expand Up @@ -657,6 +660,18 @@ if(WIN32)
hid.lib
wintrust.lib
)

if(win-universal-mute)
target_sources(mumble_client_object_lib PRIVATE
"UniversalMute.cpp"
"UniversalMute.h"
)
target_compile_definitions(mumble_client_object_lib PUBLIC "USE_WIN_UNIVERSAL_MUTE")
# WinRT required; delay-load so the binary still starts on Windows 7 where runtimeobject.dll
# does not exist. IsWindows8OrGreater() must guard call sites at runtime.
target_link_libraries(mumble_client_object_lib PUBLIC runtimeobject.lib)
set_property(TARGET mumble_client_object_lib APPEND_STRING PROPERTY LINK_FLAGS " /DELAYLOAD:runtimeobject.dll")
Comment thread
citelao marked this conversation as resolved.
endif()
else()
target_sources(mumble_client_object_lib PRIVATE "SharedMemory_unix.cpp")

Expand Down
41 changes: 41 additions & 0 deletions src/mumble/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,23 @@ void MainWindow::setupGui() {
qaAudioMute->setChecked(Global::get().s.bMute);
qaAudioDeaf->setChecked(Global::get().s.bDeaf);

#ifdef USE_WIN_UNIVERSAL_MUTE
m_universalMuter.emplace(
[this]() {
// Fired on a WinRT thread pool thread — marshal to Qt main thread.
QMetaObject::invokeMethod(this, [this]() {
qaAudioMute->setChecked(true);
on_qaAudioMute_triggered();
}, Qt::QueuedConnection);
},
[this]() {
QMetaObject::invokeMethod(this, [this]() {
qaAudioMute->setChecked(false);
on_qaAudioMute_triggered();
}, Qt::QueuedConnection);
});
#endif

updateAudioToolTips();

#ifdef USE_NO_TTS
Expand Down Expand Up @@ -2754,6 +2771,14 @@ void MainWindow::on_qaAudioMute_triggered() {
Global::get().sh->setSelfMuteDeafState(Global::get().s.bMute, Global::get().s.bDeaf);
}

#ifdef USE_WIN_UNIVERSAL_MUTE
if (Global::get().s.bMute) {
m_universalMuter->setMuted();
} else {
m_universalMuter->setUnmuted();
}
#endif

updateAudioToolTips();
emit talkingStatusChanged();
}
Expand Down Expand Up @@ -2799,6 +2824,14 @@ void MainWindow::on_qaAudioDeaf_triggered() {
Global::get().sh->setSelfMuteDeafState(Global::get().s.bMute, Global::get().s.bDeaf);
}

#ifdef USE_WIN_UNIVERSAL_MUTE
if (Global::get().s.bMute) {
m_universalMuter->setMuted();
} else {
m_universalMuter->setUnmuted();
}
#endif

updateAudioToolTips();
emit talkingStatusChanged();
}
Expand Down Expand Up @@ -3530,6 +3563,10 @@ void MainWindow::serverConnected() {
updateFavoriteButton();
qaServerBanList->setEnabled(true);

#ifdef USE_WIN_UNIVERSAL_MUTE
m_universalMuter->startCall(tr("Connecting...").toStdWString(), tr("Mumble").toStdWString());
#endif

Channel *root = Channel::get(Mumble::ROOT_CHANNEL_ID);
pmModel->renameChannel(root, tr("Root"));
pmModel->setCommentHash(root, QByteArray());
Expand Down Expand Up @@ -3563,6 +3600,10 @@ void MainWindow::serverConnected() {
}

void MainWindow::serverDisconnected(QAbstractSocket::SocketError err, QString reason) {
#ifdef USE_WIN_UNIVERSAL_MUTE
m_universalMuter->tryEndCall();
#endif

// clear ChannelListener
Global::get().channelListenerManager->clear();

Expand Down
10 changes: 10 additions & 0 deletions src/mumble/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
#include <optional>
#include <stack>

#ifdef USE_WIN_UNIVERSAL_MUTE
# include "UniversalMute.h"
#endif

#include "ui_MainWindow.h"

#define MB_QEVENT (QEvent::User + 939)
Expand Down Expand Up @@ -206,6 +210,12 @@ class MainWindow : public QMainWindow, public Ui::MainWindow {
std::stack< unsigned int > m_previousChannels;
std::optional< unsigned int > m_movedBackFromChannel;

#ifdef USE_WIN_UNIVERSAL_MUTE
// A std::optional simply because we initialize this in setupGui(), not the
// constructor.
std::optional< UniversalMuter > m_universalMuter;
#endif

static constexpr int stateVersion();

void createActions();
Expand Down
5 changes: 5 additions & 0 deletions src/mumble/Messages.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ void MainWindow::msgServerSync(const MumbleProto::ServerSync &msg) {

Global::get().sh->setServerSynchronized(true);

#ifdef USE_WIN_UNIVERSAL_MUTE
if (user->cChannel)
m_universalMuter->trySetCallName(user->cChannel->qsName.toStdWString());
#endif

emit serverSynchronized();
}

Expand Down
148 changes: 148 additions & 0 deletions src/mumble/UniversalMute.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "UniversalMute.h"

#include "win.h"

// clang-format off
#include <versionhelpers.h>
#include <wrl.h>
#include <wrl/event.h>
#include <wrl/wrappers/corewrappers.h>
#include <roapi.h>
#include <windows.applicationmodel.calls.h>
// clang-format on

using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
using namespace ABI::Windows::ApplicationModel::Calls;
using namespace ABI::Windows::Foundation;

struct UniversalMuter::Impl {
std::function< void() > onMuted;
std::function< void() > onUnmuted;
ComPtr< IVoipCallCoordinator > coordinator;
ComPtr< IVoipPhoneCall > call;
EventRegistrationToken muteStateToken{};
};

namespace {
// C++/WinRT would be a bit simpler, but requires a newer couroutine ABI than
// we are currently using (requires _COROUTINE_ABI=2, while Qt is compiled with
// _COROUTINE_ABI=1). If the app fully upgrades to C++20, we can migrate to C++/WinRT.
//
// https://github.com/microsoft/cppwinrt/issues/1281
ComPtr< IVoipCallCoordinator > tryCreateCallCoordinator() {
// WinRT was added in Windows 8; check for that before using WinRT.
// The /DELAYLOAD:runtimeobject.dll linker flag ensures we don't attempt
// to load the DLL on older versions.
if (!IsWindows8OrGreater()) {
return nullptr;
}

ComPtr< IVoipCallCoordinatorStatics > statics;
HRESULT hr = RoGetActivationFactory(
HStringReference(RuntimeClass_Windows_ApplicationModel_Calls_VoipCallCoordinator).Get(),
IID_PPV_ARGS(&statics));
if (FAILED(hr)) {
return nullptr;
}

ComPtr< IVoipCallCoordinator > coordinator;
hr = statics->GetDefault(&coordinator);
if (FAILED(hr)) {
Comment thread
citelao marked this conversation as resolved.
return nullptr;
}

return coordinator;
}
}

UniversalMuter::UniversalMuter(std::function< void() > onMuted, std::function< void() > onUnmuted)
: m_impl(std::make_shared< Impl >()) {
m_impl->onMuted = std::move(onMuted);
m_impl->onUnmuted = std::move(onUnmuted);

m_impl->coordinator = tryCreateCallCoordinator();
if (!m_impl->coordinator)
return;

// Capture a weak_ptr so the callback safely no-ops if UniversalMuter is destroyed
// while a callback is in flight (e.g. racing with remove_MuteStateChanged).
std::weak_ptr< Impl > weakImpl = m_impl;
auto handler = Callback< ITypedEventHandler< VoipCallCoordinator *, MuteChangeEventArgs * > >(
[weakImpl](IVoipCallCoordinator *, IMuteChangeEventArgs *args) -> HRESULT {
auto impl = weakImpl.lock();
if (!impl)
return S_OK;
boolean muted = FALSE;
args->get_Muted(&muted);

// The callbacks are responsible for calling setMuted/setUnmuted when they have
// processed the event.
if (muted) {
if (impl->onMuted)
impl->onMuted();
} else {
if (impl->onUnmuted)
impl->onUnmuted();
}
Comment thread
citelao marked this conversation as resolved.
return S_OK;
});

// Ignore failures; best effort.
m_impl->coordinator->add_MuteStateChanged(handler.Get(), &m_impl->muteStateToken);
}

UniversalMuter::~UniversalMuter() {
if (m_impl->coordinator)
m_impl->coordinator->remove_MuteStateChanged(m_impl->muteStateToken);
tryEndCall();
}

void UniversalMuter::startCall(const std::wstring &contactName, const std::wstring &serviceName) {
if (!m_impl->coordinator)
return;

ComPtr< IVoipPhoneCall > call;
HString context, hContactName, hServiceName;
context.Set(L"");
hContactName.Set(contactName.c_str());
hServiceName.Set(serviceName.c_str());

// RequestNewOutgoingCall may fail with E_ACCESSDENIED if the app lacks package identity.
// The coordinator and MuteStateChanged events remain active regardless.
HRESULT hr = m_impl->coordinator->RequestNewOutgoingCall(context.Get(), hContactName.Get(),
hServiceName.Get(),
VoipPhoneCallMedia_Audio, &call);
if (SUCCEEDED(hr))
m_impl->call = call;
}

void UniversalMuter::tryEndCall() {
if (!m_impl->call)
return;
m_impl->call->NotifyCallEnded();
m_impl->call.Reset();
}

void UniversalMuter::trySetCallName(const std::wstring &callName) {
if (!m_impl->call)
return;
HString name;
name.Set(callName.c_str());
m_impl->call->put_ContactName(name.Get());
}

void UniversalMuter::setMuted() {
if (m_impl->coordinator)
m_impl->coordinator->NotifyMuted();
}

void UniversalMuter::setUnmuted() {
if (m_impl->coordinator)
m_impl->coordinator->NotifyUnmuted();
}
48 changes: 48 additions & 0 deletions src/mumble/UniversalMute.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#ifndef MUMBLE_MUMBLE_UNIVERSAL_MUTE_H_
#define MUMBLE_MUMBLE_UNIVERSAL_MUTE_H_

#include <functional>
#include <memory>
#include <string>

// A wrapper around the Windows 10 VoIP Call API, specifically enough to
// interact with the Windows 11 Universal Mute feature. This ensures physical mute buttons
// in recent laptops can mute Mumble.
//
// Universal Mute is only supported on Windows 11 22H2 and later, but this class
// gracefully no-ops on unsupported platforms.
//
// https://stackoverflow.com/questions/74683703/how-do-i-support-call-mute-universal-mute-in-my-app-for-windows-11-22h2
class UniversalMuter {
public:
// The onMuted/onUnmuted callbacks are called when the user mutes/unmutes themselves using
// the Universal Mute button. They must call setMuted()/setUnmuted() here when they have
// processed the event, or the button won't actually change state.
UniversalMuter(std::function< void() > onMuted, std::function< void() > onUnmuted);
~UniversalMuter();

UniversalMuter(const UniversalMuter &) = delete;
UniversalMuter &operator=(const UniversalMuter &) = delete;
UniversalMuter(UniversalMuter &&) = delete;
UniversalMuter &operator=(UniversalMuter &&) = delete;

void setMuted();
void setUnmuted();

void startCall(const std::wstring &contactName, const std::wstring &serviceName);
void tryEndCall();

void trySetCallName(const std::wstring &callName);

private:
// Use the PIMPL pattern to avoid including WRL/WinRT headers in the public header.
struct Impl;
std::shared_ptr< Impl > m_impl;
};

#endif // MUMBLE_MUMBLE_UNIVERSAL_MUTE_H_
4 changes: 4 additions & 0 deletions src/mumble/mumble_ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6883,6 +6883,10 @@ the channel&apos;s context menu.</source>
<source>This will check if mumble is up to date</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Connecting...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>That sound was the mute cue. It activates when you speak while muted. Would you like to keep it enabled?</source>
<translation type="unfinished"></translation>
Expand Down
4 changes: 4 additions & 0 deletions src/mumble/mumble_bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6880,6 +6880,10 @@ the channel&apos;s context menu.</source>
<source>This will check if mumble is up to date</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Connecting...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>That sound was the mute cue. It activates when you speak while muted. Would you like to keep it enabled?</source>
<translation type="unfinished"></translation>
Expand Down
4 changes: 4 additions & 0 deletions src/mumble/mumble_br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6879,6 +6879,10 @@ the channel&apos;s context menu.</source>
<source>This will check if mumble is up to date</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Connecting...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>That sound was the mute cue. It activates when you speak while muted. Would you like to keep it enabled?</source>
<translation type="unfinished"></translation>
Expand Down
4 changes: 4 additions & 0 deletions src/mumble/mumble_ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6946,6 +6946,10 @@ al menú contextual del canal.</translation>
<source>This will check if mumble is up to date</source>
<translation>Això comprova si el mumble està actualitzat</translation>
</message>
<message>
<source>Connecting...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>That sound was the mute cue. It activates when you speak while muted. Would you like to keep it enabled?</source>
<translation>Aquest so és el senyal de treure la paraula. S&apos;activa quan es parla mentre es no teniu la paraula. Voleu mantenir-ho activat?</translation>
Expand Down
Loading