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
1214MediaController::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
1522void MediaController::handleEarDetection (EarDetection *earDetection)
@@ -87,12 +94,9 @@ void MediaController::followMediaChanges() {
8794}
8895
8996bool 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
98102void 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
163171bool 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
225228void 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}
0 commit comments