Skip to content

Commit fe774d5

Browse files
authored
linux: replace pactl calls with libpulse (#221)
1 parent 1206815 commit fe774d5

File tree

6 files changed

+400
-96
lines changed

6 files changed

+400
-96
lines changed

linux/CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
66

77
find_package(Qt6 6.4 REQUIRED COMPONENTS Quick Widgets Bluetooth DBus)
88
find_package(OpenSSL REQUIRED)
9+
find_package(PkgConfig REQUIRED)
10+
pkg_check_modules(PULSEAUDIO REQUIRED libpulse)
911

1012
qt_standard_project_setup(REQUIRES 6.4)
1113

@@ -14,6 +16,8 @@ qt_add_executable(librepods
1416
logger.h
1517
media/mediacontroller.cpp
1618
media/mediacontroller.h
19+
media/pulseaudiocontroller.cpp
20+
media/pulseaudiocontroller.h
1721
airpods_packets.h
1822
trayiconmanager.cpp
1923
trayiconmanager.h
@@ -66,9 +70,11 @@ qt_add_resources(librepods "resources"
6670
)
6771

6872
target_link_libraries(librepods
69-
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::DBus OpenSSL::SSL OpenSSL::Crypto
73+
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::DBus OpenSSL::SSL OpenSSL::Crypto ${PULSEAUDIO_LIBRARIES}
7074
)
7175

76+
target_include_directories(librepods PRIVATE ${PULSEAUDIO_INCLUDE_DIRS})
77+
7278
include(GNUInstallDirs)
7379
install(TARGETS librepods
7480
BUNDLE DESTINATION .

linux/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,7 @@ int main(int argc, char *argv[]) {
967967
QApplication app(argc, argv);
968968

969969
QSharedMemory sharedMemory;
970-
sharedMemory.setKey("TcpServer-Key");
970+
sharedMemory.setKey("TcpServer-Key2");
971971

972972
// Check if app is already open
973973
if(sharedMemory.create(1) == false)

linux/media/mediacontroller.cpp

Lines changed: 67 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22
#include "logger.h"
33
#include "eardetection.hpp"
44
#include "playerstatuswatcher.h"
5+
#include "pulseaudiocontroller.h"
56

67
#include <QDebug>
78
#include <QProcess>
9+
#include <QThread>
810
#include <QRegularExpression>
911
#include <QDBusConnection>
1012
#include <QDBusConnectionInterface>
1113

1214
MediaController::MediaController(QObject *parent) : QObject(parent) {
15+
m_pulseAudio = new PulseAudioController(this);
16+
if (!m_pulseAudio->initialize())
17+
{
18+
LOG_ERROR("Failed to initialize PulseAudio controller");
19+
}
1320
}
1421

1522
void MediaController::handleEarDetection(EarDetection *earDetection)
@@ -87,12 +94,9 @@ void MediaController::followMediaChanges() {
8794
}
8895

8996
bool MediaController::isActiveOutputDeviceAirPods() {
90-
QProcess process;
91-
process.start("pactl", QStringList() << "get-default-sink");
92-
process.waitForFinished();
93-
QString output = process.readAllStandardOutput().trimmed();
94-
LOG_DEBUG("Default sink: " << output);
95-
return output.contains(connectedDeviceMacAddress);
97+
QString defaultSink = m_pulseAudio->getDefaultSink();
98+
LOG_DEBUG("Default sink: " << defaultSink);
99+
return defaultSink.contains(connectedDeviceMacAddress);
96100
}
97101

98102
void MediaController::handleConversationalAwareness(const QByteArray &data) {
@@ -102,32 +106,29 @@ void MediaController::handleConversationalAwareness(const QByteArray &data) {
102106

103107
if (lowered) {
104108
if (initialVolume == -1 && isActiveOutputDeviceAirPods()) {
105-
QProcess process;
106-
process.start("pactl", QStringList()
107-
<< "get-sink-volume" << "@DEFAULT_SINK@");
108-
process.waitForFinished();
109-
QString output = process.readAllStandardOutput();
110-
QRegularExpression re("front-left: \\d+ /\\s*(\\d+)%");
111-
QRegularExpressionMatch match = re.match(output);
112-
if (match.hasMatch()) {
113-
LOG_DEBUG("Matched: " << match.captured(1));
114-
initialVolume = match.captured(1).toInt();
115-
} else {
116-
LOG_ERROR("Failed to parse initial volume from output: " << output);
109+
QString defaultSink = m_pulseAudio->getDefaultSink();
110+
initialVolume = m_pulseAudio->getSinkVolume(defaultSink);
111+
if (initialVolume == -1) {
112+
LOG_ERROR("Failed to get initial volume");
117113
return;
118114
}
115+
LOG_DEBUG("Initial volume: " << initialVolume << "%");
116+
}
117+
QString defaultSink = m_pulseAudio->getDefaultSink();
118+
int targetVolume = initialVolume * 0.20;
119+
if (m_pulseAudio->setSinkVolume(defaultSink, targetVolume)) {
120+
LOG_INFO("Volume lowered to 0.20 of initial which is " << targetVolume << "%");
121+
} else {
122+
LOG_ERROR("Failed to lower volume");
119123
}
120-
QProcess::execute(
121-
"pactl", QStringList() << "set-sink-volume" << "@DEFAULT_SINK@"
122-
<< QString::number(initialVolume * 0.20) + "%");
123-
LOG_INFO("Volume lowered to 0.20 of initial which is "
124-
<< initialVolume * 0.20 << "%");
125124
} else {
126125
if (initialVolume != -1 && isActiveOutputDeviceAirPods()) {
127-
QProcess::execute("pactl", QStringList()
128-
<< "set-sink-volume" << "@DEFAULT_SINK@"
129-
<< QString::number(initialVolume) + "%");
130-
LOG_INFO("Volume restored to " << initialVolume << "%");
126+
QString defaultSink = m_pulseAudio->getDefaultSink();
127+
if (m_pulseAudio->setSinkVolume(defaultSink, initialVolume)) {
128+
LOG_INFO("Volume restored to " << initialVolume << "%");
129+
} else {
130+
LOG_ERROR("Failed to restore volume");
131+
}
131132
initialVolume = -1;
132133
}
133134
}
@@ -138,38 +139,44 @@ bool MediaController::isA2dpProfileAvailable() {
138139
return false;
139140
}
140141

141-
QProcess process;
142-
process.start("pactl", QStringList() << "list" << "cards");
143-
if (!process.waitForFinished(3000)) {
144-
LOG_ERROR("pactl command timed out while checking A2DP availability");
145-
return false;
146-
}
142+
return m_pulseAudio->isProfileAvailable(m_deviceOutputName, "a2dp-sink-sbc_xq") ||
143+
m_pulseAudio->isProfileAvailable(m_deviceOutputName, "a2dp-sink-sbc") ||
144+
m_pulseAudio->isProfileAvailable(m_deviceOutputName, "a2dp-sink");
145+
}
147146

148-
QString output = process.readAllStandardOutput();
147+
QString MediaController::getPreferredA2dpProfile() {
148+
if (m_deviceOutputName.isEmpty()) {
149+
return QString();
150+
}
149151

150-
// Check if the card section contains our device
151-
int cardStart = output.indexOf(m_deviceOutputName);
152-
if (cardStart == -1) {
153-
return false;
152+
if (!m_cachedA2dpProfile.isEmpty() &&
153+
m_pulseAudio->isProfileAvailable(m_deviceOutputName, m_cachedA2dpProfile)) {
154+
return m_cachedA2dpProfile;
154155
}
155156

156-
// Look for a2dp-sink profile in the card's section
157-
int nextCard = output.indexOf("Name: ", cardStart + m_deviceOutputName.length());
158-
QString cardSection = (nextCard == -1) ? output.mid(cardStart) : output.mid(cardStart, nextCard - cardStart);
157+
QStringList profiles = {"a2dp-sink-sbc_xq", "a2dp-sink-sbc", "a2dp-sink"};
159158

160-
return cardSection.contains("a2dp-sink");
159+
for (const QString &profile : profiles) {
160+
if (m_pulseAudio->isProfileAvailable(m_deviceOutputName, profile)) {
161+
LOG_INFO("Selected best available A2DP profile: " << profile);
162+
m_cachedA2dpProfile = profile;
163+
return profile;
164+
}
165+
}
166+
167+
m_cachedA2dpProfile.clear();
168+
return QString();
161169
}
162170

163171
bool MediaController::restartWirePlumber() {
164172
LOG_INFO("Restarting WirePlumber to rediscover A2DP profiles");
165173
int result = QProcess::execute("systemctl", QStringList() << "--user" << "restart" << "wireplumber");
166174
if (result == 0) {
167175
LOG_INFO("WirePlumber restarted successfully");
168-
// Wait a bit for WirePlumber to rediscover profiles
169-
QProcess::execute("sleep", QStringList() << "2");
176+
QThread::sleep(2);
170177
return true;
171178
} else {
172-
LOG_ERROR("Failed to restart WirePlumber");
179+
LOG_ERROR("Failed to restart WirePlumber. Do you use wireplumber?");
173180
return false;
174181
}
175182
}
@@ -180,11 +187,9 @@ void MediaController::activateA2dpProfile() {
180187
return;
181188
}
182189

183-
// Check if A2DP profile is available
184190
if (!isA2dpProfileAvailable()) {
185191
LOG_WARN("A2DP profile not available, attempting to restart WirePlumber");
186192
if (restartWirePlumber()) {
187-
// Update device output name after restart
188193
m_deviceOutputName = getAudioDeviceName();
189194
if (!isA2dpProfileAvailable()) {
190195
LOG_ERROR("A2DP profile still not available after WirePlumber restart");
@@ -196,13 +201,15 @@ void MediaController::activateA2dpProfile() {
196201
}
197202
}
198203

199-
LOG_INFO("Activating A2DP profile for AirPods");
200-
int result = QProcess::execute(
201-
"pactl", QStringList()
202-
<< "set-card-profile"
203-
<< m_deviceOutputName << "a2dp-sink");
204-
if (result != 0) {
205-
LOG_ERROR("Failed to activate A2DP profile");
204+
QString preferredProfile = getPreferredA2dpProfile();
205+
if (preferredProfile.isEmpty()) {
206+
LOG_ERROR("No suitable A2DP profile found");
207+
return;
208+
}
209+
210+
LOG_INFO("Activating A2DP profile for AirPods: " << preferredProfile);
211+
if (!m_pulseAudio->setCardProfile(m_deviceOutputName, preferredProfile)) {
212+
LOG_ERROR("Failed to activate A2DP profile: " << preferredProfile);
206213
}
207214
}
208215

@@ -213,18 +220,15 @@ void MediaController::removeAudioOutputDevice() {
213220
}
214221

215222
LOG_INFO("Removing AirPods as audio output device");
216-
int result = QProcess::execute(
217-
"pactl", QStringList()
218-
<< "set-card-profile"
219-
<< m_deviceOutputName << "off");
220-
if (result != 0) {
223+
if (!m_pulseAudio->setCardProfile(m_deviceOutputName, "off")) {
221224
LOG_ERROR("Failed to remove AirPods as audio output device");
222225
}
223226
}
224227

225228
void MediaController::setConnectedDeviceMacAddress(const QString &macAddress) {
226229
connectedDeviceMacAddress = macAddress;
227230
m_deviceOutputName = getAudioDeviceName();
231+
m_cachedA2dpProfile.clear();
228232
LOG_INFO("Device output name set to: " << m_deviceOutputName);
229233
}
230234

@@ -345,40 +349,9 @@ QString MediaController::getAudioDeviceName()
345349
{
346350
if (connectedDeviceMacAddress.isEmpty()) { return QString(); }
347351

348-
// Set up QProcess to run pactl directly
349-
QProcess process;
350-
process.start("pactl", QStringList() << "list" << "cards" << "short");
351-
if (!process.waitForFinished(3000)) // Timeout after 3 seconds
352-
{
353-
LOG_ERROR("pactl command failed or timed out: " << process.errorString());
354-
return QString();
355-
}
356-
357-
// Check for execution errors
358-
if (process.exitCode() != 0)
359-
{
360-
LOG_ERROR("pactl exited with error code: " << process.exitCode());
361-
return QString();
362-
}
363-
364-
// Read and parse the command output
365-
QString output = process.readAllStandardOutput();
366-
QStringList lines = output.split("\n", Qt::SkipEmptyParts);
367-
368-
// Iterate through each line to find a matching Bluetooth sink
369-
for (const QString &line : lines)
370-
{
371-
QStringList fields = line.split("\t", Qt::SkipEmptyParts);
372-
if (fields.size() < 2) { continue; }
373-
374-
QString sinkName = fields[1].trimmed();
375-
if (sinkName.startsWith("bluez") && sinkName.contains(connectedDeviceMacAddress))
376-
{
377-
return sinkName;
378-
}
352+
QString cardName = m_pulseAudio->getCardNameForDevice(connectedDeviceMacAddress);
353+
if (cardName.isEmpty()) {
354+
LOG_ERROR("No matching Bluetooth card found for MAC address: " << connectedDeviceMacAddress);
379355
}
380-
381-
// No matching sink found
382-
LOG_ERROR("No matching Bluetooth sink found for MAC address: " << connectedDeviceMacAddress);
383-
return QString();
356+
return cardName;
384357
}

linux/media/mediacontroller.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#define MEDIACONTROLLER_H
33

44
#include <QObject>
5+
#include "pulseaudiocontroller.h"
56

67
class QProcess;
78
class EarDetection;
@@ -38,6 +39,7 @@ class MediaController : public QObject
3839
void removeAudioOutputDevice();
3940
void setConnectedDeviceMacAddress(const QString &macAddress);
4041
bool isA2dpProfileAvailable();
42+
QString getBestA2dpProfile();
4143
bool restartWirePlumber();
4244

4345
void setEarDetectionBehavior(EarDetectionBehavior behavior);
@@ -61,6 +63,8 @@ class MediaController : public QObject
6163
EarDetectionBehavior earDetectionBehavior = PauseWhenOneRemoved;
6264
QString m_deviceOutputName;
6365
PlayerStatusWatcher *playerStatusWatcher = nullptr;
66+
PulseAudioController *m_pulseAudio = nullptr;
67+
QString m_cachedA2dpProfile;
6468
};
6569

6670
#endif // MEDIACONTROLLER_H

0 commit comments

Comments
 (0)