mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-03-13 09:43:00 +00:00
linux: replace pactl calls with libpulse (#221)
This commit is contained in:
@@ -6,6 +6,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
|
|
||||||
find_package(Qt6 6.4 REQUIRED COMPONENTS Quick Widgets Bluetooth DBus)
|
find_package(Qt6 6.4 REQUIRED COMPONENTS Quick Widgets Bluetooth DBus)
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(PULSEAUDIO REQUIRED libpulse)
|
||||||
|
|
||||||
qt_standard_project_setup(REQUIRES 6.4)
|
qt_standard_project_setup(REQUIRES 6.4)
|
||||||
|
|
||||||
@@ -14,6 +16,8 @@ qt_add_executable(librepods
|
|||||||
logger.h
|
logger.h
|
||||||
media/mediacontroller.cpp
|
media/mediacontroller.cpp
|
||||||
media/mediacontroller.h
|
media/mediacontroller.h
|
||||||
|
media/pulseaudiocontroller.cpp
|
||||||
|
media/pulseaudiocontroller.h
|
||||||
airpods_packets.h
|
airpods_packets.h
|
||||||
trayiconmanager.cpp
|
trayiconmanager.cpp
|
||||||
trayiconmanager.h
|
trayiconmanager.h
|
||||||
@@ -66,9 +70,11 @@ qt_add_resources(librepods "resources"
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(librepods
|
target_link_libraries(librepods
|
||||||
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::DBus OpenSSL::SSL OpenSSL::Crypto
|
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::DBus OpenSSL::SSL OpenSSL::Crypto ${PULSEAUDIO_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_include_directories(librepods PRIVATE ${PULSEAUDIO_INCLUDE_DIRS})
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
install(TARGETS librepods
|
install(TARGETS librepods
|
||||||
BUNDLE DESTINATION .
|
BUNDLE DESTINATION .
|
||||||
|
|||||||
@@ -967,7 +967,7 @@ int main(int argc, char *argv[]) {
|
|||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
QSharedMemory sharedMemory;
|
QSharedMemory sharedMemory;
|
||||||
sharedMemory.setKey("TcpServer-Key");
|
sharedMemory.setKey("TcpServer-Key2");
|
||||||
|
|
||||||
// Check if app is already open
|
// Check if app is already open
|
||||||
if(sharedMemory.create(1) == false)
|
if(sharedMemory.create(1) == false)
|
||||||
|
|||||||
@@ -2,14 +2,21 @@
|
|||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "eardetection.hpp"
|
#include "eardetection.hpp"
|
||||||
#include "playerstatuswatcher.h"
|
#include "playerstatuswatcher.h"
|
||||||
|
#include "pulseaudiocontroller.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
#include <QThread>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
#include <QDBusConnectionInterface>
|
#include <QDBusConnectionInterface>
|
||||||
|
|
||||||
MediaController::MediaController(QObject *parent) : QObject(parent) {
|
MediaController::MediaController(QObject *parent) : QObject(parent) {
|
||||||
|
m_pulseAudio = new PulseAudioController(this);
|
||||||
|
if (!m_pulseAudio->initialize())
|
||||||
|
{
|
||||||
|
LOG_ERROR("Failed to initialize PulseAudio controller");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaController::handleEarDetection(EarDetection *earDetection)
|
void MediaController::handleEarDetection(EarDetection *earDetection)
|
||||||
@@ -87,12 +94,9 @@ void MediaController::followMediaChanges() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool MediaController::isActiveOutputDeviceAirPods() {
|
bool MediaController::isActiveOutputDeviceAirPods() {
|
||||||
QProcess process;
|
QString defaultSink = m_pulseAudio->getDefaultSink();
|
||||||
process.start("pactl", QStringList() << "get-default-sink");
|
LOG_DEBUG("Default sink: " << defaultSink);
|
||||||
process.waitForFinished();
|
return defaultSink.contains(connectedDeviceMacAddress);
|
||||||
QString output = process.readAllStandardOutput().trimmed();
|
|
||||||
LOG_DEBUG("Default sink: " << output);
|
|
||||||
return output.contains(connectedDeviceMacAddress);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaController::handleConversationalAwareness(const QByteArray &data) {
|
void MediaController::handleConversationalAwareness(const QByteArray &data) {
|
||||||
@@ -102,32 +106,29 @@ void MediaController::handleConversationalAwareness(const QByteArray &data) {
|
|||||||
|
|
||||||
if (lowered) {
|
if (lowered) {
|
||||||
if (initialVolume == -1 && isActiveOutputDeviceAirPods()) {
|
if (initialVolume == -1 && isActiveOutputDeviceAirPods()) {
|
||||||
QProcess process;
|
QString defaultSink = m_pulseAudio->getDefaultSink();
|
||||||
process.start("pactl", QStringList()
|
initialVolume = m_pulseAudio->getSinkVolume(defaultSink);
|
||||||
<< "get-sink-volume" << "@DEFAULT_SINK@");
|
if (initialVolume == -1) {
|
||||||
process.waitForFinished();
|
LOG_ERROR("Failed to get initial volume");
|
||||||
QString output = process.readAllStandardOutput();
|
|
||||||
QRegularExpression re("front-left: \\d+ /\\s*(\\d+)%");
|
|
||||||
QRegularExpressionMatch match = re.match(output);
|
|
||||||
if (match.hasMatch()) {
|
|
||||||
LOG_DEBUG("Matched: " << match.captured(1));
|
|
||||||
initialVolume = match.captured(1).toInt();
|
|
||||||
} else {
|
|
||||||
LOG_ERROR("Failed to parse initial volume from output: " << output);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
LOG_DEBUG("Initial volume: " << initialVolume << "%");
|
||||||
|
}
|
||||||
|
QString defaultSink = m_pulseAudio->getDefaultSink();
|
||||||
|
int targetVolume = initialVolume * 0.20;
|
||||||
|
if (m_pulseAudio->setSinkVolume(defaultSink, targetVolume)) {
|
||||||
|
LOG_INFO("Volume lowered to 0.20 of initial which is " << targetVolume << "%");
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Failed to lower volume");
|
||||||
}
|
}
|
||||||
QProcess::execute(
|
|
||||||
"pactl", QStringList() << "set-sink-volume" << "@DEFAULT_SINK@"
|
|
||||||
<< QString::number(initialVolume * 0.20) + "%");
|
|
||||||
LOG_INFO("Volume lowered to 0.20 of initial which is "
|
|
||||||
<< initialVolume * 0.20 << "%");
|
|
||||||
} else {
|
} else {
|
||||||
if (initialVolume != -1 && isActiveOutputDeviceAirPods()) {
|
if (initialVolume != -1 && isActiveOutputDeviceAirPods()) {
|
||||||
QProcess::execute("pactl", QStringList()
|
QString defaultSink = m_pulseAudio->getDefaultSink();
|
||||||
<< "set-sink-volume" << "@DEFAULT_SINK@"
|
if (m_pulseAudio->setSinkVolume(defaultSink, initialVolume)) {
|
||||||
<< QString::number(initialVolume) + "%");
|
LOG_INFO("Volume restored to " << initialVolume << "%");
|
||||||
LOG_INFO("Volume restored to " << initialVolume << "%");
|
} else {
|
||||||
|
LOG_ERROR("Failed to restore volume");
|
||||||
|
}
|
||||||
initialVolume = -1;
|
initialVolume = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,26 +139,33 @@ bool MediaController::isA2dpProfileAvailable() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QProcess process;
|
return m_pulseAudio->isProfileAvailable(m_deviceOutputName, "a2dp-sink-sbc_xq") ||
|
||||||
process.start("pactl", QStringList() << "list" << "cards");
|
m_pulseAudio->isProfileAvailable(m_deviceOutputName, "a2dp-sink-sbc") ||
|
||||||
if (!process.waitForFinished(3000)) {
|
m_pulseAudio->isProfileAvailable(m_deviceOutputName, "a2dp-sink");
|
||||||
LOG_ERROR("pactl command timed out while checking A2DP availability");
|
}
|
||||||
return false;
|
|
||||||
|
QString MediaController::getPreferredA2dpProfile() {
|
||||||
|
if (m_deviceOutputName.isEmpty()) {
|
||||||
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString output = process.readAllStandardOutput();
|
if (!m_cachedA2dpProfile.isEmpty() &&
|
||||||
|
m_pulseAudio->isProfileAvailable(m_deviceOutputName, m_cachedA2dpProfile)) {
|
||||||
// Check if the card section contains our device
|
return m_cachedA2dpProfile;
|
||||||
int cardStart = output.indexOf(m_deviceOutputName);
|
|
||||||
if (cardStart == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for a2dp-sink profile in the card's section
|
QStringList profiles = {"a2dp-sink-sbc_xq", "a2dp-sink-sbc", "a2dp-sink"};
|
||||||
int nextCard = output.indexOf("Name: ", cardStart + m_deviceOutputName.length());
|
|
||||||
QString cardSection = (nextCard == -1) ? output.mid(cardStart) : output.mid(cardStart, nextCard - cardStart);
|
|
||||||
|
|
||||||
return cardSection.contains("a2dp-sink");
|
for (const QString &profile : profiles) {
|
||||||
|
if (m_pulseAudio->isProfileAvailable(m_deviceOutputName, profile)) {
|
||||||
|
LOG_INFO("Selected best available A2DP profile: " << profile);
|
||||||
|
m_cachedA2dpProfile = profile;
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_cachedA2dpProfile.clear();
|
||||||
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MediaController::restartWirePlumber() {
|
bool MediaController::restartWirePlumber() {
|
||||||
@@ -165,11 +173,10 @@ bool MediaController::restartWirePlumber() {
|
|||||||
int result = QProcess::execute("systemctl", QStringList() << "--user" << "restart" << "wireplumber");
|
int result = QProcess::execute("systemctl", QStringList() << "--user" << "restart" << "wireplumber");
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
LOG_INFO("WirePlumber restarted successfully");
|
LOG_INFO("WirePlumber restarted successfully");
|
||||||
// Wait a bit for WirePlumber to rediscover profiles
|
QThread::sleep(2);
|
||||||
QProcess::execute("sleep", QStringList() << "2");
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("Failed to restart WirePlumber");
|
LOG_ERROR("Failed to restart WirePlumber. Do you use wireplumber?");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,11 +187,9 @@ void MediaController::activateA2dpProfile() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if A2DP profile is available
|
|
||||||
if (!isA2dpProfileAvailable()) {
|
if (!isA2dpProfileAvailable()) {
|
||||||
LOG_WARN("A2DP profile not available, attempting to restart WirePlumber");
|
LOG_WARN("A2DP profile not available, attempting to restart WirePlumber");
|
||||||
if (restartWirePlumber()) {
|
if (restartWirePlumber()) {
|
||||||
// Update device output name after restart
|
|
||||||
m_deviceOutputName = getAudioDeviceName();
|
m_deviceOutputName = getAudioDeviceName();
|
||||||
if (!isA2dpProfileAvailable()) {
|
if (!isA2dpProfileAvailable()) {
|
||||||
LOG_ERROR("A2DP profile still not available after WirePlumber restart");
|
LOG_ERROR("A2DP profile still not available after WirePlumber restart");
|
||||||
@@ -196,13 +201,15 @@ void MediaController::activateA2dpProfile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Activating A2DP profile for AirPods");
|
QString preferredProfile = getPreferredA2dpProfile();
|
||||||
int result = QProcess::execute(
|
if (preferredProfile.isEmpty()) {
|
||||||
"pactl", QStringList()
|
LOG_ERROR("No suitable A2DP profile found");
|
||||||
<< "set-card-profile"
|
return;
|
||||||
<< m_deviceOutputName << "a2dp-sink");
|
}
|
||||||
if (result != 0) {
|
|
||||||
LOG_ERROR("Failed to activate A2DP profile");
|
LOG_INFO("Activating A2DP profile for AirPods: " << preferredProfile);
|
||||||
|
if (!m_pulseAudio->setCardProfile(m_deviceOutputName, preferredProfile)) {
|
||||||
|
LOG_ERROR("Failed to activate A2DP profile: " << preferredProfile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,11 +220,7 @@ void MediaController::removeAudioOutputDevice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Removing AirPods as audio output device");
|
LOG_INFO("Removing AirPods as audio output device");
|
||||||
int result = QProcess::execute(
|
if (!m_pulseAudio->setCardProfile(m_deviceOutputName, "off")) {
|
||||||
"pactl", QStringList()
|
|
||||||
<< "set-card-profile"
|
|
||||||
<< m_deviceOutputName << "off");
|
|
||||||
if (result != 0) {
|
|
||||||
LOG_ERROR("Failed to remove AirPods as audio output device");
|
LOG_ERROR("Failed to remove AirPods as audio output device");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,6 +228,7 @@ void MediaController::removeAudioOutputDevice() {
|
|||||||
void MediaController::setConnectedDeviceMacAddress(const QString &macAddress) {
|
void MediaController::setConnectedDeviceMacAddress(const QString &macAddress) {
|
||||||
connectedDeviceMacAddress = macAddress;
|
connectedDeviceMacAddress = macAddress;
|
||||||
m_deviceOutputName = getAudioDeviceName();
|
m_deviceOutputName = getAudioDeviceName();
|
||||||
|
m_cachedA2dpProfile.clear();
|
||||||
LOG_INFO("Device output name set to: " << m_deviceOutputName);
|
LOG_INFO("Device output name set to: " << m_deviceOutputName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,40 +349,9 @@ QString MediaController::getAudioDeviceName()
|
|||||||
{
|
{
|
||||||
if (connectedDeviceMacAddress.isEmpty()) { return QString(); }
|
if (connectedDeviceMacAddress.isEmpty()) { return QString(); }
|
||||||
|
|
||||||
// Set up QProcess to run pactl directly
|
QString cardName = m_pulseAudio->getCardNameForDevice(connectedDeviceMacAddress);
|
||||||
QProcess process;
|
if (cardName.isEmpty()) {
|
||||||
process.start("pactl", QStringList() << "list" << "cards" << "short");
|
LOG_ERROR("No matching Bluetooth card found for MAC address: " << connectedDeviceMacAddress);
|
||||||
if (!process.waitForFinished(3000)) // Timeout after 3 seconds
|
|
||||||
{
|
|
||||||
LOG_ERROR("pactl command failed or timed out: " << process.errorString());
|
|
||||||
return QString();
|
|
||||||
}
|
}
|
||||||
|
return cardName;
|
||||||
// Check for execution errors
|
|
||||||
if (process.exitCode() != 0)
|
|
||||||
{
|
|
||||||
LOG_ERROR("pactl exited with error code: " << process.exitCode());
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and parse the command output
|
|
||||||
QString output = process.readAllStandardOutput();
|
|
||||||
QStringList lines = output.split("\n", Qt::SkipEmptyParts);
|
|
||||||
|
|
||||||
// Iterate through each line to find a matching Bluetooth sink
|
|
||||||
for (const QString &line : lines)
|
|
||||||
{
|
|
||||||
QStringList fields = line.split("\t", Qt::SkipEmptyParts);
|
|
||||||
if (fields.size() < 2) { continue; }
|
|
||||||
|
|
||||||
QString sinkName = fields[1].trimmed();
|
|
||||||
if (sinkName.startsWith("bluez") && sinkName.contains(connectedDeviceMacAddress))
|
|
||||||
{
|
|
||||||
return sinkName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No matching sink found
|
|
||||||
LOG_ERROR("No matching Bluetooth sink found for MAC address: " << connectedDeviceMacAddress);
|
|
||||||
return QString();
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#define MEDIACONTROLLER_H
|
#define MEDIACONTROLLER_H
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include "pulseaudiocontroller.h"
|
||||||
|
|
||||||
class QProcess;
|
class QProcess;
|
||||||
class EarDetection;
|
class EarDetection;
|
||||||
@@ -38,6 +39,7 @@ public:
|
|||||||
void removeAudioOutputDevice();
|
void removeAudioOutputDevice();
|
||||||
void setConnectedDeviceMacAddress(const QString &macAddress);
|
void setConnectedDeviceMacAddress(const QString &macAddress);
|
||||||
bool isA2dpProfileAvailable();
|
bool isA2dpProfileAvailable();
|
||||||
|
QString getBestA2dpProfile();
|
||||||
bool restartWirePlumber();
|
bool restartWirePlumber();
|
||||||
|
|
||||||
void setEarDetectionBehavior(EarDetectionBehavior behavior);
|
void setEarDetectionBehavior(EarDetectionBehavior behavior);
|
||||||
@@ -61,6 +63,8 @@ private:
|
|||||||
EarDetectionBehavior earDetectionBehavior = PauseWhenOneRemoved;
|
EarDetectionBehavior earDetectionBehavior = PauseWhenOneRemoved;
|
||||||
QString m_deviceOutputName;
|
QString m_deviceOutputName;
|
||||||
PlayerStatusWatcher *playerStatusWatcher = nullptr;
|
PlayerStatusWatcher *playerStatusWatcher = nullptr;
|
||||||
|
PulseAudioController *m_pulseAudio = nullptr;
|
||||||
|
QString m_cachedA2dpProfile;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // MEDIACONTROLLER_H
|
#endif // MEDIACONTROLLER_H
|
||||||
284
linux/media/pulseaudiocontroller.cpp
Normal file
284
linux/media/pulseaudiocontroller.cpp
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
#include "pulseaudiocontroller.h"
|
||||||
|
#include "logger.h"
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
PulseAudioController::PulseAudioController(QObject *parent)
|
||||||
|
: QObject(parent), m_mainloop(nullptr), m_context(nullptr), m_initialized(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
PulseAudioController::~PulseAudioController()
|
||||||
|
{
|
||||||
|
if (m_context)
|
||||||
|
{
|
||||||
|
pa_context_disconnect(m_context);
|
||||||
|
pa_context_unref(m_context);
|
||||||
|
}
|
||||||
|
if (m_mainloop)
|
||||||
|
{
|
||||||
|
pa_threaded_mainloop_stop(m_mainloop);
|
||||||
|
pa_threaded_mainloop_free(m_mainloop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PulseAudioController::initialize()
|
||||||
|
{
|
||||||
|
m_mainloop = pa_threaded_mainloop_new();
|
||||||
|
if (!m_mainloop)
|
||||||
|
{
|
||||||
|
LOG_ERROR("Failed to create PulseAudio mainloop");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pa_mainloop_api *api = pa_threaded_mainloop_get_api(m_mainloop);
|
||||||
|
m_context = pa_context_new(api, "LibrePods");
|
||||||
|
if (!m_context)
|
||||||
|
{
|
||||||
|
LOG_ERROR("Failed to create PulseAudio context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pa_context_set_state_callback(m_context, contextStateCallback, this);
|
||||||
|
|
||||||
|
if (pa_threaded_mainloop_start(m_mainloop) < 0)
|
||||||
|
{
|
||||||
|
LOG_ERROR("Failed to start PulseAudio mainloop");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pa_threaded_mainloop_lock(m_mainloop);
|
||||||
|
|
||||||
|
if (pa_context_connect(m_context, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0)
|
||||||
|
{
|
||||||
|
LOG_ERROR("Failed to connect to PulseAudio");
|
||||||
|
pa_threaded_mainloop_unlock(m_mainloop);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for context to be ready
|
||||||
|
while (pa_context_get_state(m_context) != PA_CONTEXT_READY)
|
||||||
|
{
|
||||||
|
if (!PA_CONTEXT_IS_GOOD(pa_context_get_state(m_context)))
|
||||||
|
{
|
||||||
|
LOG_ERROR("PulseAudio context failed");
|
||||||
|
pa_threaded_mainloop_unlock(m_mainloop);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pa_threaded_mainloop_wait(m_mainloop);
|
||||||
|
}
|
||||||
|
|
||||||
|
pa_threaded_mainloop_unlock(m_mainloop);
|
||||||
|
m_initialized = true;
|
||||||
|
LOG_INFO("PulseAudio controller initialized");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PulseAudioController::contextStateCallback(pa_context *c, void *userdata)
|
||||||
|
{
|
||||||
|
PulseAudioController *controller = static_cast<PulseAudioController*>(userdata);
|
||||||
|
pa_threaded_mainloop_signal(controller->m_mainloop, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PulseAudioController::getDefaultSink()
|
||||||
|
{
|
||||||
|
if (!m_initialized) return QString();
|
||||||
|
|
||||||
|
struct CallbackData {
|
||||||
|
QString sinkName;
|
||||||
|
pa_threaded_mainloop *mainloop;
|
||||||
|
} data;
|
||||||
|
data.mainloop = m_mainloop;
|
||||||
|
|
||||||
|
auto callback = [](pa_context *c, const pa_server_info *info, void *userdata) {
|
||||||
|
CallbackData *d = static_cast<CallbackData*>(userdata);
|
||||||
|
if (info && info->default_sink_name)
|
||||||
|
{
|
||||||
|
d->sinkName = QString::fromUtf8(info->default_sink_name);
|
||||||
|
}
|
||||||
|
pa_threaded_mainloop_signal(d->mainloop, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
pa_threaded_mainloop_lock(m_mainloop);
|
||||||
|
pa_operation *op = pa_context_get_server_info(m_context, callback, &data);
|
||||||
|
if (op)
|
||||||
|
{
|
||||||
|
waitForOperation(op);
|
||||||
|
pa_operation_unref(op);
|
||||||
|
}
|
||||||
|
pa_threaded_mainloop_unlock(m_mainloop);
|
||||||
|
|
||||||
|
return data.sinkName;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PulseAudioController::getSinkVolume(const QString &sinkName)
|
||||||
|
{
|
||||||
|
if (!m_initialized) return -1;
|
||||||
|
|
||||||
|
struct CallbackData {
|
||||||
|
int volume;
|
||||||
|
QString targetSink;
|
||||||
|
pa_threaded_mainloop *mainloop;
|
||||||
|
} data;
|
||||||
|
data.volume = -1;
|
||||||
|
data.targetSink = sinkName;
|
||||||
|
data.mainloop = m_mainloop;
|
||||||
|
|
||||||
|
auto callback = [](pa_context *c, const pa_sink_info *info, int eol, void *userdata) {
|
||||||
|
CallbackData *d = static_cast<CallbackData*>(userdata);
|
||||||
|
if (eol > 0)
|
||||||
|
{
|
||||||
|
pa_threaded_mainloop_signal(d->mainloop, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (info && QString::fromUtf8(info->name) == d->targetSink)
|
||||||
|
{
|
||||||
|
d->volume = (pa_cvolume_avg(&info->volume) * 100) / PA_VOLUME_NORM;
|
||||||
|
pa_threaded_mainloop_signal(d->mainloop, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pa_threaded_mainloop_lock(m_mainloop);
|
||||||
|
pa_operation *op = pa_context_get_sink_info_by_name(m_context, sinkName.toUtf8().constData(), callback, &data);
|
||||||
|
if (op)
|
||||||
|
{
|
||||||
|
waitForOperation(op);
|
||||||
|
pa_operation_unref(op);
|
||||||
|
}
|
||||||
|
pa_threaded_mainloop_unlock(m_mainloop);
|
||||||
|
|
||||||
|
return data.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PulseAudioController::setSinkVolume(const QString &sinkName, int volumePercent)
|
||||||
|
{
|
||||||
|
if (!m_initialized) return false;
|
||||||
|
|
||||||
|
pa_cvolume volume;
|
||||||
|
pa_cvolume_set(&volume, 2, (volumePercent * PA_VOLUME_NORM) / 100);
|
||||||
|
|
||||||
|
pa_threaded_mainloop_lock(m_mainloop);
|
||||||
|
pa_operation *op = pa_context_set_sink_volume_by_name(m_context, sinkName.toUtf8().constData(), &volume, nullptr, nullptr);
|
||||||
|
bool success = waitForOperation(op);
|
||||||
|
if (op) pa_operation_unref(op);
|
||||||
|
pa_threaded_mainloop_unlock(m_mainloop);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PulseAudioController::setCardProfile(const QString &cardName, const QString &profileName)
|
||||||
|
{
|
||||||
|
if (!m_initialized) return false;
|
||||||
|
|
||||||
|
pa_threaded_mainloop_lock(m_mainloop);
|
||||||
|
pa_operation *op = pa_context_set_card_profile_by_name(m_context,
|
||||||
|
cardName.toUtf8().constData(),
|
||||||
|
profileName.toUtf8().constData(),
|
||||||
|
nullptr, nullptr);
|
||||||
|
bool success = waitForOperation(op);
|
||||||
|
if (op) pa_operation_unref(op);
|
||||||
|
pa_threaded_mainloop_unlock(m_mainloop);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PulseAudioController::getCardNameForDevice(const QString &macAddress)
|
||||||
|
{
|
||||||
|
if (!m_initialized) return QString();
|
||||||
|
|
||||||
|
struct CallbackData {
|
||||||
|
QString cardName;
|
||||||
|
QString targetMac;
|
||||||
|
pa_threaded_mainloop *mainloop;
|
||||||
|
} data;
|
||||||
|
data.targetMac = macAddress;
|
||||||
|
data.mainloop = m_mainloop;
|
||||||
|
|
||||||
|
auto callback = [](pa_context *c, const pa_card_info *info, int eol, void *userdata) {
|
||||||
|
CallbackData *d = static_cast<CallbackData*>(userdata);
|
||||||
|
if (eol > 0)
|
||||||
|
{
|
||||||
|
pa_threaded_mainloop_signal(d->mainloop, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (info)
|
||||||
|
{
|
||||||
|
QString name = QString::fromUtf8(info->name);
|
||||||
|
if (name.startsWith("bluez") && name.contains(d->targetMac))
|
||||||
|
{
|
||||||
|
d->cardName = name;
|
||||||
|
pa_threaded_mainloop_signal(d->mainloop, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pa_threaded_mainloop_lock(m_mainloop);
|
||||||
|
pa_operation *op = pa_context_get_card_info_list(m_context, callback, &data);
|
||||||
|
if (op)
|
||||||
|
{
|
||||||
|
waitForOperation(op);
|
||||||
|
pa_operation_unref(op);
|
||||||
|
}
|
||||||
|
pa_threaded_mainloop_unlock(m_mainloop);
|
||||||
|
|
||||||
|
return data.cardName;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PulseAudioController::isProfileAvailable(const QString &cardName, const QString &profileName)
|
||||||
|
{
|
||||||
|
if (!m_initialized) return false;
|
||||||
|
|
||||||
|
struct CallbackData {
|
||||||
|
bool available;
|
||||||
|
QString targetCard;
|
||||||
|
QString targetProfile;
|
||||||
|
pa_threaded_mainloop *mainloop;
|
||||||
|
} data;
|
||||||
|
data.available = false;
|
||||||
|
data.targetCard = cardName;
|
||||||
|
data.targetProfile = profileName;
|
||||||
|
data.mainloop = m_mainloop;
|
||||||
|
|
||||||
|
auto callback = [](pa_context *c, const pa_card_info *info, int eol, void *userdata) {
|
||||||
|
CallbackData *d = static_cast<CallbackData*>(userdata);
|
||||||
|
if (eol > 0)
|
||||||
|
{
|
||||||
|
pa_threaded_mainloop_signal(d->mainloop, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (info && QString::fromUtf8(info->name) == d->targetCard)
|
||||||
|
{
|
||||||
|
for (uint32_t i = 0; i < info->n_profiles; i++)
|
||||||
|
{
|
||||||
|
if (QString::fromUtf8(info->profiles[i].name) == d->targetProfile)
|
||||||
|
{
|
||||||
|
d->available = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pa_threaded_mainloop_signal(d->mainloop, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pa_threaded_mainloop_lock(m_mainloop);
|
||||||
|
pa_operation *op = pa_context_get_card_info_by_name(m_context, cardName.toUtf8().constData(), callback, &data);
|
||||||
|
if (op)
|
||||||
|
{
|
||||||
|
waitForOperation(op);
|
||||||
|
pa_operation_unref(op);
|
||||||
|
}
|
||||||
|
pa_threaded_mainloop_unlock(m_mainloop);
|
||||||
|
|
||||||
|
return data.available;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PulseAudioController::waitForOperation(pa_operation *op)
|
||||||
|
{
|
||||||
|
if (!op) return false;
|
||||||
|
|
||||||
|
while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
|
||||||
|
{
|
||||||
|
pa_threaded_mainloop_wait(m_mainloop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pa_operation_get_state(op) == PA_OPERATION_DONE;
|
||||||
|
}
|
||||||
37
linux/media/pulseaudiocontroller.h
Normal file
37
linux/media/pulseaudiocontroller.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef PULSEAUDIOCONTROLLER_H
|
||||||
|
#define PULSEAUDIOCONTROLLER_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QObject>
|
||||||
|
#include <pulse/pulseaudio.h>
|
||||||
|
|
||||||
|
class PulseAudioController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit PulseAudioController(QObject *parent = nullptr);
|
||||||
|
~PulseAudioController();
|
||||||
|
|
||||||
|
bool initialize();
|
||||||
|
QString getDefaultSink();
|
||||||
|
int getSinkVolume(const QString &sinkName);
|
||||||
|
bool setSinkVolume(const QString &sinkName, int volumePercent);
|
||||||
|
bool setCardProfile(const QString &cardName, const QString &profileName);
|
||||||
|
QString getCardNameForDevice(const QString &macAddress);
|
||||||
|
bool isProfileAvailable(const QString &cardName, const QString &profileName);
|
||||||
|
|
||||||
|
private:
|
||||||
|
pa_threaded_mainloop *m_mainloop;
|
||||||
|
pa_context *m_context;
|
||||||
|
bool m_initialized;
|
||||||
|
|
||||||
|
static void contextStateCallback(pa_context *c, void *userdata);
|
||||||
|
static void sinkInfoCallback(pa_context *c, const pa_sink_info *info, int eol, void *userdata);
|
||||||
|
static void cardInfoCallback(pa_context *c, const pa_card_info *info, int eol, void *userdata);
|
||||||
|
static void serverInfoCallback(pa_context *c, const pa_server_info *info, void *userdata);
|
||||||
|
|
||||||
|
bool waitForOperation(pa_operation *op);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PULSEAUDIOCONTROLLER_H
|
||||||
Reference in New Issue
Block a user