Compare commits

...

4 commits

Author SHA1 Message Date
DarkOK
ccd8f900e8
Merge 3a22976fec into 3c1badf183 2025-08-06 18:20:00 -05:00
squidbus
3c1badf183
Enable D3D12 Agility SDK in plume submodule. (#1646)
Some checks failed
validate-internal / build (push) Has been cancelled
2025-08-04 15:39:26 +03:00
Alex Oxorn
3a22976fec Check all active players for "Playing" Status 2025-04-25 23:04:07 +01:00
dakrk
8baa6d2a20
Implement music attenuation on Linux
This is implemented by listening for event changes from MPRIS2 clients
over D-Bus. Unfortunately there is no good API for this nor a good way
to identify current player as there is on Windows, and as such we have
to track and queue them manually just to get a seemingly good enough
result. This also gives us the issue of needing to guess and manually
prioritise which player to use when we first start, as we cannot
determine when something before us started.

playerctld is preferred to be read on systems that have it available,
however that has the behaviour of setting its status to the most recent
change. Those who use this and have media key scripts use it would
likely be expecting such behaviour though.

(All my editors were eager to fix inconsistent CRLF's in the CMakeLists,
sorry)
2025-04-25 21:06:21 +01:00
6 changed files with 344 additions and 27 deletions

View file

@ -23,10 +23,6 @@ if (APPLE)
enable_language(OBJC OBJCXX)
endif()
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
set(SDL_VULKAN_ENABLED ON CACHE BOOL "")
endif()
if (CMAKE_OSX_ARCHITECTURES)
set(UNLEASHED_RECOMP_ARCHITECTURE ${CMAKE_OSX_ARCHITECTURES})
elseif(CMAKE_SYSTEM_PROCESSOR)

View file

@ -352,23 +352,13 @@ if (UNLEASHED_RECOMP_FLATPAK)
)
endif()
if (UNLEASHED_RECOMP_D3D12)
find_package(directx-headers CONFIG REQUIRED)
find_package(directx12-agility CONFIG REQUIRED)
target_compile_definitions(UnleashedRecomp PRIVATE
UNLEASHED_RECOMP_D3D12
D3D12MA_USING_DIRECTX_HEADERS
D3D12MA_OPTIONS16_SUPPORTED
)
endif()
if (SDL_VULKAN_ENABLED)
target_compile_definitions(UnleashedRecomp PRIVATE SDL_VULKAN_ENABLED)
endif()
find_package(CURL REQUIRED)
if (UNLEASHED_RECOMP_D3D12)
find_package(directx-headers CONFIG REQUIRED)
find_package(directx12-agility CONFIG REQUIRED)
target_compile_definitions(UnleashedRecomp PRIVATE UNLEASHED_RECOMP_D3D12)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/D3D12)
add_custom_command(TARGET UnleashedRecomp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_PROPERTY:Microsoft::DirectX12-Core,IMPORTED_LOCATION_RELEASE> $<TARGET_FILE_DIR:UnleashedRecomp>/D3D12
@ -379,9 +369,6 @@ if (UNLEASHED_RECOMP_D3D12)
)
target_link_libraries(UnleashedRecomp PRIVATE
Microsoft::DirectX-Headers
Microsoft::DirectX-Guids
Microsoft::DirectX12-Agility
Microsoft::DirectXShaderCompiler
Microsoft::DXIL
dxgi
@ -395,7 +382,7 @@ if (WIN32)
ntdll
Shcore
Synchronization
winmm
winmm
windowsapp
)
endif()
@ -423,9 +410,12 @@ target_include_directories(UnleashedRecomp PRIVATE
)
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
find_package(PkgConfig REQUIRED)
find_package(X11 REQUIRED)
target_include_directories(UnleashedRecomp PRIVATE ${X11_INCLUDE_DIR})
target_link_libraries(UnleashedRecomp PRIVATE ${X11_LIBRARIES})
pkg_search_module(GLIB REQUIRED glib-2.0)
pkg_search_module(GIO REQUIRED gio-2.0)
target_include_directories(UnleashedRecomp PRIVATE ${X11_INCLUDE_DIR} ${GLIB_INCLUDE_DIRS} ${GIO_INCLUDE_DIRS})
target_link_libraries(UnleashedRecomp PRIVATE ${X11_LIBRARIES} ${GLIB_LIBRARIES} ${GIO_LIBRARIES})
endif()
target_precompile_headers(UnleashedRecomp PUBLIC ${UNLEASHED_RECOMP_PRECOMPILED_HEADERS})

View file

@ -1,7 +1,328 @@
#include <algorithm>
#include <atomic>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <ranges>
#include <gio/gio.h>
#include <os/media.h>
#include <os/logger.h>
enum class PlaybackStatus
{
Stopped,
Playing,
Paused
};
static const char* DBusInterface = "org.freedesktop.DBus";
static const char* DBusPropertiesInterface = "org.freedesktop.DBus.Properties";
static const char* DBusPath = "/org/freedesktop/DBus";
static const char* MPRIS2Interface = "org.mpris.MediaPlayer2";
static const char* MPRIS2PlayerInterface = "org.mpris.MediaPlayer2.Player";
static const char* MPRIS2Path = "/org/mpris/MediaPlayer2";
static std::optional<std::thread> g_dbusThread;
static std::unordered_map<std::string, PlaybackStatus> g_playerStatus;
static std::atomic<bool> g_isPlaying = false;
static PlaybackStatus PlaybackStatusFromString(const char* str)
{
if (g_str_equal(str, "Playing"))
return PlaybackStatus::Playing;
else if (g_str_equal(str, "Paused"))
return PlaybackStatus::Paused;
else
return PlaybackStatus::Stopped;
}
static void UpdateActiveStatus()
{
g_isPlaying = std::ranges::any_of(
g_playerStatus | std::views::values,
[](PlaybackStatus status) { return status == PlaybackStatus::Playing; }
);
}
static void UpdateActivePlayers(const char* name, PlaybackStatus status)
{
g_playerStatus.insert_or_assign(name, status);
UpdateActiveStatus();
}
static PlaybackStatus MPRISGetPlaybackStatus(GDBusConnection* connection, const gchar* name)
{
GError* error;
GVariant* response;
GVariant* tupleChild;
GVariant* value;
PlaybackStatus status;
error = NULL;
response = g_dbus_connection_call_sync(
connection,
name,
MPRIS2Path,
DBusPropertiesInterface,
"Get",
g_variant_new("(ss)", MPRIS2PlayerInterface, "PlaybackStatus"),
G_VARIANT_TYPE("(v)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&error
);
if (!response)
{
LOGF_ERROR("Failed to process D-Bus Get: {}", error->message);
g_clear_error(&error);
return PlaybackStatus::Stopped;
}
tupleChild = g_variant_get_child_value(response, 0);
value = g_variant_get_variant(tupleChild);
if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING))
{
LOG_ERROR("Failed to process D-Bus Get");
g_variant_unref(tupleChild);
return PlaybackStatus::Stopped;
}
status = PlaybackStatusFromString(g_variant_get_string(value, NULL));
g_variant_unref(value);
g_variant_unref(tupleChild);
g_variant_unref(response);
return status;
}
// Something is very wrong with the system if this happens
static void DBusConnectionClosed(GDBusConnection* connection,
gboolean remotePeerVanished,
GError* error,
gpointer userData)
{
LOG_ERROR("D-Bus connection closed");
g_isPlaying = false;
g_main_loop_quit((GMainLoop*)userData);
}
static void DBusNameOwnerChanged(GDBusConnection* connection,
const gchar* senderName,
const gchar* objectPath,
const gchar* interfaceName,
const gchar* signalName,
GVariant* parameters,
gpointer userData)
{
const char* name;
const char* oldOwner;
const char* newOwner;
g_variant_get(parameters, "(&s&s&s)", &name, &oldOwner, &newOwner);
if (g_str_has_prefix(name, MPRIS2Interface))
{
if (oldOwner[0])
{
g_playerStatus.erase(oldOwner);
}
UpdateActiveStatus();
}
}
static void MPRISPropertiesChanged(GDBusConnection* connection,
const gchar* senderName,
const gchar* objectPath,
const gchar* interfaceName,
const gchar* signalName,
GVariant* parameters,
gpointer userData)
{
const char* interface;
GVariant* changed;
GVariantIter iter;
const char* key;
GVariant* value;
PlaybackStatus playbackStatus;
g_variant_get_child(parameters, 0, "&s", &interface);
g_variant_get_child(parameters, 1, "@a{sv}", &changed);
g_variant_iter_init(&iter, changed);
while (g_variant_iter_next(&iter, "{&sv}", &key, &value))
{
if (g_str_equal(key, "PlaybackStatus"))
{
playbackStatus = PlaybackStatusFromString(g_variant_get_string(value, NULL));
UpdateActivePlayers(senderName, playbackStatus);
g_variant_unref(value);
break;
}
g_variant_unref(value);
}
g_variant_unref(changed);
}
/* Called upon CONNECT to discover already active MPRIS2 players by looking for
well-known bus names that begin with the MPRIS2 path.
g_playerStatus stores unique connection names,
not their well-known ones, as the PropertiesChanged signal only provides the
former. */
static void DBusListNamesReceived(GObject* object, GAsyncResult* res, gpointer userData)
{
GDBusConnection* connection;
GError* error;
GVariant* response;
GVariant* tupleChild;
GVariantIter iter;
const gchar* name;
connection = G_DBUS_CONNECTION(object);
error = NULL;
response = g_dbus_connection_call_finish(connection, res, &error);
if (!response)
{
LOGF_ERROR("Failed to process D-Bus ListNames: {}", error->message);
g_clear_error(&error);
return;
}
tupleChild = g_variant_get_child_value(response, 0);
g_variant_iter_init(&iter, tupleChild);
while (g_variant_iter_next(&iter, "&s", &name))
{
GVariant* ownerResponse;
const gchar* ownerName;
PlaybackStatus status;
if (!g_str_has_prefix(name, MPRIS2Interface))
continue;
ownerResponse = g_dbus_connection_call_sync(
connection,
DBusInterface,
DBusPath,
DBusInterface,
"GetNameOwner",
g_variant_new("(s)", name),
G_VARIANT_TYPE("(s)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&error
);
if (!ownerResponse)
{
LOGF_ERROR("Failed to process D-Bus GetNameOwner: {}", error->message);
g_clear_error(&error);
g_variant_unref(tupleChild);
g_variant_unref(response);
return;
}
g_variant_get(ownerResponse, "(&s)", &ownerName);
status = MPRISGetPlaybackStatus(connection, ownerName);
g_playerStatus.insert_or_assign(ownerName, status);
g_variant_unref(ownerResponse);
}
UpdateActiveStatus();
g_variant_unref(tupleChild);
g_variant_unref(response);
}
static void DBusThreadProc()
{
GMainContext* mainContext;
GMainLoop* mainLoop;
GError* error;
GDBusConnection* connection;
mainContext = g_main_context_new();
g_main_context_push_thread_default(mainContext);
mainLoop = g_main_loop_new(mainContext, FALSE);
error = NULL;
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (!connection)
{
LOGF_ERROR("Failed to connect to D-Bus: {}", error->message);
g_clear_error(&error);
g_main_context_unref(mainContext);
g_main_loop_unref(mainLoop);
return;
}
g_dbus_connection_set_exit_on_close(connection, FALSE);
g_signal_connect(connection, "closed", G_CALLBACK(DBusConnectionClosed), mainLoop);
// Listen for player connection changes
g_dbus_connection_signal_subscribe(
connection,
DBusInterface,
DBusInterface,
"NameOwnerChanged",
DBusPath,
NULL,
G_DBUS_SIGNAL_FLAGS_NONE,
DBusNameOwnerChanged,
NULL,
NULL
);
// Listen for player status changes
g_dbus_connection_signal_subscribe(
connection,
NULL,
DBusPropertiesInterface,
"PropertiesChanged",
MPRIS2Path,
NULL,
G_DBUS_SIGNAL_FLAGS_NONE,
MPRISPropertiesChanged,
NULL,
NULL
);
// Request list of current players
g_dbus_connection_call(
connection,
DBusInterface,
DBusPath,
DBusInterface,
"ListNames",
NULL,
G_VARIANT_TYPE("(as)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
DBusListNamesReceived,
NULL
);
g_main_loop_run(mainLoop);
}
bool os::media::IsExternalMediaPlaying()
{
// This functionality is not supported in Linux.
return false;
if (!g_dbusThread)
{
g_dbusThread.emplace(DBusThreadProc);
g_dbusThread->detach();
}
return g_isPlaying;
}

View file

@ -29,6 +29,8 @@ bool AudioPatches::CanAttenuate()
m_isAttenuationSupported = version.Major >= 10 && version.Build >= 17763;
return m_isAttenuationSupported;
#elif __linux__
return true;
#else
return false;
#endif

View file

@ -15,6 +15,14 @@ set(SDL2MIXER_OPUS OFF)
set(SDL2MIXER_VORBIS "VORBISFILE")
set(SDL2MIXER_WAVPACK OFF)
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
set(SDL_VULKAN_ENABLED ON CACHE BOOL "")
endif()
if (WIN32)
set(D3D12_AGILITY_SDK_ENABLED ON CACHE BOOL "")
endif()
add_subdirectory("${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/msdf-atlas-gen")
add_subdirectory("${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/nativefiledialog-extended")
add_subdirectory("${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/o1heap")

2
thirdparty/plume vendored

@ -1 +1 @@
Subproject commit fffeb35f836d8c945697ec82b735e77db401e2de
Subproject commit 11926860e878e68626ea99ec88562ce2b8badc4f