Skip to content

Commit fad8862

Browse files
dakrkAlexOxorn
authored andcommitted
Implement music attentuation on Linux via MPRIS2
Please see hedge-dev/UnleashedRecomp#1117. Co-authored-by: AlexOxorn <[email protected]>
1 parent 5097a9e commit fad8862

File tree

3 files changed

+330
-4
lines changed

3 files changed

+330
-4
lines changed

MarathonRecomp/CMakeLists.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,9 +421,12 @@ target_include_directories(MarathonRecomp PRIVATE
421421
)
422422

423423
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
424+
find_package(PkgConfig REQUIRED)
424425
find_package(X11 REQUIRED)
425-
target_include_directories(MarathonRecomp PRIVATE ${X11_INCLUDE_DIR})
426-
target_link_libraries(MarathonRecomp PRIVATE ${X11_LIBRARIES})
426+
pkg_search_module(GLIB REQUIRED glib-2.0)
427+
pkg_search_module(GIO REQUIRED gio-2.0)
428+
target_include_directories(MarathonRecomp PRIVATE ${X11_INCLUDE_DIR} ${GLIB_INCLUDE_DIRS} ${GIO_INCLUDE_DIRS})
429+
target_link_libraries(MarathonRecomp PRIVATE ${X11_LIBRARIES} ${GLIB_LIBRARIES} ${GIO_LIBRARIES})
427430
endif()
428431

429432
target_precompile_headers(MarathonRecomp PUBLIC ${MARATHON_RECOMP_PRECOMPILED_HEADERS})
Lines changed: 323 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,328 @@
1+
#include <algorithm>
2+
#include <atomic>
3+
#include <optional>
4+
#include <string>
5+
#include <thread>
6+
#include <unordered_map>
7+
#include <ranges>
8+
#include <gio/gio.h>
19
#include <os/media.h>
10+
#include <os/logger.h>
11+
12+
enum class PlaybackStatus
13+
{
14+
Stopped,
15+
Playing,
16+
Paused
17+
};
18+
19+
static const char* DBusInterface = "org.freedesktop.DBus";
20+
static const char* DBusPropertiesInterface = "org.freedesktop.DBus.Properties";
21+
static const char* DBusPath = "/org/freedesktop/DBus";
22+
static const char* MPRIS2Interface = "org.mpris.MediaPlayer2";
23+
static const char* MPRIS2PlayerInterface = "org.mpris.MediaPlayer2.Player";
24+
static const char* MPRIS2Path = "/org/mpris/MediaPlayer2";
25+
26+
static std::optional<std::thread> g_dbusThread;
27+
static std::unordered_map<std::string, PlaybackStatus> g_playerStatus;
28+
static std::atomic<bool> g_isPlaying = false;
29+
30+
static PlaybackStatus PlaybackStatusFromString(const char* str)
31+
{
32+
if (g_str_equal(str, "Playing"))
33+
return PlaybackStatus::Playing;
34+
else if (g_str_equal(str, "Paused"))
35+
return PlaybackStatus::Paused;
36+
else
37+
return PlaybackStatus::Stopped;
38+
}
39+
40+
static void UpdateActiveStatus()
41+
{
42+
g_isPlaying = std::ranges::any_of(
43+
g_playerStatus | std::views::values,
44+
[](PlaybackStatus status) { return status == PlaybackStatus::Playing; }
45+
);
46+
}
47+
48+
static void UpdateActivePlayers(const char* name, PlaybackStatus status)
49+
{
50+
g_playerStatus.insert_or_assign(name, status);
51+
UpdateActiveStatus();
52+
}
53+
54+
static PlaybackStatus MPRISGetPlaybackStatus(GDBusConnection* connection, const gchar* name)
55+
{
56+
GError* error;
57+
GVariant* response;
58+
GVariant* tupleChild;
59+
GVariant* value;
60+
PlaybackStatus status;
61+
62+
error = NULL;
63+
64+
response = g_dbus_connection_call_sync(
65+
connection,
66+
name,
67+
MPRIS2Path,
68+
DBusPropertiesInterface,
69+
"Get",
70+
g_variant_new("(ss)", MPRIS2PlayerInterface, "PlaybackStatus"),
71+
G_VARIANT_TYPE("(v)"),
72+
G_DBUS_CALL_FLAGS_NONE,
73+
-1,
74+
NULL,
75+
&error
76+
);
77+
78+
if (!response)
79+
{
80+
LOGF_ERROR("Failed to process D-Bus Get: {}", error->message);
81+
g_clear_error(&error);
82+
return PlaybackStatus::Stopped;
83+
}
84+
85+
tupleChild = g_variant_get_child_value(response, 0);
86+
value = g_variant_get_variant(tupleChild);
87+
88+
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING))
89+
{
90+
LOG_ERROR("Failed to process D-Bus Get");
91+
g_variant_unref(tupleChild);
92+
return PlaybackStatus::Stopped;
93+
}
94+
95+
status = PlaybackStatusFromString(g_variant_get_string(value, NULL));
96+
97+
g_variant_unref(value);
98+
g_variant_unref(tupleChild);
99+
g_variant_unref(response);
100+
101+
return status;
102+
}
103+
104+
// Something is very wrong with the system if this happens
105+
static void DBusConnectionClosed(GDBusConnection* connection,
106+
gboolean remotePeerVanished,
107+
GError* error,
108+
gpointer userData)
109+
{
110+
LOG_ERROR("D-Bus connection closed");
111+
g_isPlaying = false;
112+
g_main_loop_quit((GMainLoop*)userData);
113+
}
114+
115+
static void DBusNameOwnerChanged(GDBusConnection* connection,
116+
const gchar* senderName,
117+
const gchar* objectPath,
118+
const gchar* interfaceName,
119+
const gchar* signalName,
120+
GVariant* parameters,
121+
gpointer userData)
122+
{
123+
const char* name;
124+
const char* oldOwner;
125+
const char* newOwner;
126+
127+
g_variant_get(parameters, "(&s&s&s)", &name, &oldOwner, &newOwner);
128+
129+
if (g_str_has_prefix(name, MPRIS2Interface))
130+
{
131+
if (oldOwner[0])
132+
{
133+
g_playerStatus.erase(oldOwner);
134+
}
135+
136+
UpdateActiveStatus();
137+
}
138+
}
139+
140+
static void MPRISPropertiesChanged(GDBusConnection* connection,
141+
const gchar* senderName,
142+
const gchar* objectPath,
143+
const gchar* interfaceName,
144+
const gchar* signalName,
145+
GVariant* parameters,
146+
gpointer userData)
147+
{
148+
const char* interface;
149+
GVariant* changed;
150+
GVariantIter iter;
151+
const char* key;
152+
GVariant* value;
153+
PlaybackStatus playbackStatus;
154+
155+
g_variant_get_child(parameters, 0, "&s", &interface);
156+
g_variant_get_child(parameters, 1, "@a{sv}", &changed);
157+
158+
g_variant_iter_init(&iter, changed);
159+
while (g_variant_iter_next(&iter, "{&sv}", &key, &value))
160+
{
161+
if (g_str_equal(key, "PlaybackStatus"))
162+
{
163+
playbackStatus = PlaybackStatusFromString(g_variant_get_string(value, NULL));
164+
UpdateActivePlayers(senderName, playbackStatus);
165+
g_variant_unref(value);
166+
break;
167+
}
168+
g_variant_unref(value);
169+
}
170+
171+
g_variant_unref(changed);
172+
}
173+
174+
/* Called upon CONNECT to discover already active MPRIS2 players by looking for
175+
well-known bus names that begin with the MPRIS2 path.
176+
g_playerStatus stores unique connection names,
177+
not their well-known ones, as the PropertiesChanged signal only provides the
178+
former. */
179+
static void DBusListNamesReceived(GObject* object, GAsyncResult* res, gpointer userData)
180+
{
181+
GDBusConnection* connection;
182+
GError* error;
183+
GVariant* response;
184+
GVariant* tupleChild;
185+
GVariantIter iter;
186+
const gchar* name;
187+
188+
connection = G_DBUS_CONNECTION(object);
189+
error = NULL;
190+
response = g_dbus_connection_call_finish(connection, res, &error);
191+
192+
if (!response)
193+
{
194+
LOGF_ERROR("Failed to process D-Bus ListNames: {}", error->message);
195+
g_clear_error(&error);
196+
return;
197+
}
198+
199+
tupleChild = g_variant_get_child_value(response, 0);
200+
201+
g_variant_iter_init(&iter, tupleChild);
202+
while (g_variant_iter_next(&iter, "&s", &name))
203+
{
204+
GVariant* ownerResponse;
205+
const gchar* ownerName;
206+
PlaybackStatus status;
207+
208+
if (!g_str_has_prefix(name, MPRIS2Interface))
209+
continue;
210+
211+
ownerResponse = g_dbus_connection_call_sync(
212+
connection,
213+
DBusInterface,
214+
DBusPath,
215+
DBusInterface,
216+
"GetNameOwner",
217+
g_variant_new("(s)", name),
218+
G_VARIANT_TYPE("(s)"),
219+
G_DBUS_CALL_FLAGS_NONE,
220+
-1,
221+
NULL,
222+
&error
223+
);
224+
225+
if (!ownerResponse)
226+
{
227+
LOGF_ERROR("Failed to process D-Bus GetNameOwner: {}", error->message);
228+
g_clear_error(&error);
229+
g_variant_unref(tupleChild);
230+
g_variant_unref(response);
231+
return;
232+
}
233+
234+
g_variant_get(ownerResponse, "(&s)", &ownerName);
235+
status = MPRISGetPlaybackStatus(connection, ownerName);
236+
237+
g_playerStatus.insert_or_assign(ownerName, status);
238+
g_variant_unref(ownerResponse);
239+
}
240+
241+
UpdateActiveStatus();
242+
243+
g_variant_unref(tupleChild);
244+
g_variant_unref(response);
245+
}
246+
247+
static void DBusThreadProc()
248+
{
249+
GMainContext* mainContext;
250+
GMainLoop* mainLoop;
251+
GError* error;
252+
GDBusConnection* connection;
253+
254+
mainContext = g_main_context_new();
255+
g_main_context_push_thread_default(mainContext);
256+
mainLoop = g_main_loop_new(mainContext, FALSE);
257+
error = NULL;
258+
259+
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
260+
if (!connection)
261+
{
262+
LOGF_ERROR("Failed to connect to D-Bus: {}", error->message);
263+
g_clear_error(&error);
264+
g_main_context_unref(mainContext);
265+
g_main_loop_unref(mainLoop);
266+
return;
267+
}
268+
269+
g_dbus_connection_set_exit_on_close(connection, FALSE);
270+
g_signal_connect(connection, "closed", G_CALLBACK(DBusConnectionClosed), mainLoop);
271+
272+
// Listen for player connection changes
273+
g_dbus_connection_signal_subscribe(
274+
connection,
275+
DBusInterface,
276+
DBusInterface,
277+
"NameOwnerChanged",
278+
DBusPath,
279+
NULL,
280+
G_DBUS_SIGNAL_FLAGS_NONE,
281+
DBusNameOwnerChanged,
282+
NULL,
283+
NULL
284+
);
285+
286+
// Listen for player status changes
287+
g_dbus_connection_signal_subscribe(
288+
connection,
289+
NULL,
290+
DBusPropertiesInterface,
291+
"PropertiesChanged",
292+
MPRIS2Path,
293+
NULL,
294+
G_DBUS_SIGNAL_FLAGS_NONE,
295+
MPRISPropertiesChanged,
296+
NULL,
297+
NULL
298+
);
299+
300+
// Request list of current players
301+
g_dbus_connection_call(
302+
connection,
303+
DBusInterface,
304+
DBusPath,
305+
DBusInterface,
306+
"ListNames",
307+
NULL,
308+
G_VARIANT_TYPE("(as)"),
309+
G_DBUS_CALL_FLAGS_NONE,
310+
-1,
311+
NULL,
312+
DBusListNamesReceived,
313+
NULL
314+
);
315+
316+
g_main_loop_run(mainLoop);
317+
}
2318

3319
bool os::media::IsExternalMediaPlaying()
4320
{
5-
// This functionality is not supported in Linux.
6-
return false;
321+
if (!g_dbusThread)
322+
{
323+
g_dbusThread.emplace(DBusThreadProc);
324+
g_dbusThread->detach();
325+
}
326+
327+
return g_isPlaying;
7328
}

MarathonRecomp/patches/audio_patches.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ bool AudioPatches::CanAttenuate()
1818
m_isAttenuationSupported = version.Major >= 10 && version.Build >= 17763;
1919

2020
return m_isAttenuationSupported;
21+
#elif __linux__
22+
return true;
2123
#else
2224
return false;
2325
#endif

0 commit comments

Comments
 (0)