From bd13c3716248570f14982b8e03a4adf4926e0c24 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 22 Nov 2021 15:56:24 +0100 Subject: Add -o option to select output profile --- src/CMakeLists.txt | 2 + src/cardwidget.cc | 3 - src/cardwidget.h | 26 +------- src/helper.cc | 75 +++++++++++++++++++++++ src/helper.h | 61 +++++++++++++++++++ src/mainwindow.cc | 72 +--------------------- src/pavucontrol.cc | 172 ++++++++++++++++++++++++++++++++++++++++++++--------- 7 files changed, 285 insertions(+), 126 deletions(-) create mode 100644 src/helper.cc create mode 100644 src/helper.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4c95f63..7e4effe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,6 +17,7 @@ set(pavucontrol-qt_HDRS sourcewidget.h streamwidget.h elidinglabel.h + helper.h ) set(pavucontrol-qt_SRCS @@ -33,6 +34,7 @@ set(pavucontrol-qt_SRCS sourcewidget.cc streamwidget.cc elidinglabel.cc + helper.cc ) set(pavucontrol-qt_UI diff --git a/src/cardwidget.cc b/src/cardwidget.cc index 3b4c8b3..f401691 100644 --- a/src/cardwidget.cc +++ b/src/cardwidget.cc @@ -141,13 +141,11 @@ void ProfileGroup::addEntry(const char *id, const char *name) { while (*pos != '\0') { if (!paren && isspace(*pos)) { if (pos > tokenStart) { - qDebug() << tokenStart; pe->tokens.append(QString::fromUtf8(tokenStart, pos - tokenStart)); } tokenStart = pos + 1; } if (paren && *pos == ')') { - qDebug() << tokenStart; pe->tokens.append(QString::fromUtf8(tokenStart, pos - tokenStart + 1)); paren = false; tokenStart = pos + 1; @@ -158,7 +156,6 @@ void ProfileGroup::addEntry(const char *id, const char *name) { pos++; } if (pos > tokenStart) { - qDebug() << tokenStart; pe->tokens.append(QString::fromUtf8(tokenStart, pos - tokenStart)); } pe->id = id; diff --git a/src/cardwidget.h b/src/cardwidget.h index 7bd87d2..92d544d 100644 --- a/src/cardwidget.h +++ b/src/cardwidget.h @@ -22,37 +22,13 @@ #define cardwidget_h #include "pavucontrol.h" +#include "helper.h" #include "ui_cardwidget.h" #include #include #include #include -struct ProfileEntry { - QByteArray id; - QStringList tokens; - QString getName() const; -}; - -struct ProfileGroup { - QString name; - QList entries; - QString getProfileName(); - void addEntry(const char* id, const char* name); - bool containsProfile(const QByteArray &pro) const; -}; - -class PortInfo { -public: - QByteArray name; - QByteArray description; - uint32_t priority; - int available; - int direction; - int64_t latency_offset; - std::vector profiles; -}; - class CardWidget : public QWidget, public Ui::CardWidget { Q_OBJECT public: diff --git a/src/helper.cc b/src/helper.cc new file mode 100644 index 0000000..ffadbe9 --- /dev/null +++ b/src/helper.cc @@ -0,0 +1,75 @@ +#include "helper.h" + +#include "mainwindow.h" +#include + +void populatePorts(const pa_card_info &info, std::map &ports) +{ + ports.clear(); + for (uint32_t i = 0; i < info.n_ports; ++i) { + PortInfo p; + + p.name = info.ports[i]->name; + p.description = info.ports[i]->description; + p.priority = info.ports[i]->priority; + p.available = info.ports[i]->available; + p.direction = info.ports[i]->direction; + p.latency_offset = info.ports[i]->latency_offset; + for (pa_card_profile_info2 ** p_profile = info.ports[i]->profiles2; p_profile && *p_profile != nullptr; ++p_profile) + p.profiles.push_back((*p_profile)->name); + + ports[p.name] = p; + } +} + +void groupProfiles(const std::set &profile_priorities, + const std::map &ports, + QMap &profiles) +{ + profiles.clear(); + for (auto p_profile : profile_priorities) { + bool hasNo = false, hasOther = false; + std::map::const_iterator portIt; + QByteArray desc = p_profile->description; + + for (portIt = ports.begin(); portIt != ports.end(); portIt++) { + PortInfo port = portIt->second; + + if (std::find(port.profiles.begin(), port.profiles.end(), p_profile->name) == port.profiles.end()) + continue; + + if (port.available == PA_PORT_AVAILABLE_NO) + hasNo = true; + else { + hasOther = true; + break; + } + } + if (hasNo && !hasOther) + desc += MainWindow::tr(" (unplugged)").toUtf8().constData(); + + if (!p_profile->available) + desc += MainWindow::tr(" (unavailable)").toUtf8().constData(); + + QString parseId = QString::fromUtf8(p_profile->name); + int plus = parseId.indexOf(QLatin1Char('+')); + if (plus != -1) { + parseId = parseId.left(plus); + } + auto list = parseId.split(QRegularExpression(QLatin1String("[:\\-]"))); + parseId.clear(); + for (auto p : list) { + if (p == QLatin1String("input") || p == QLatin1String("output")) + continue; + if (parseId.isEmpty() || p.startsWith(QLatin1String("extra"))) { + parseId.append(p); + } + } + qDebug() << "ParseID:" << parseId; + ProfileGroup &group = profiles[parseId]; + if (p_profile->available) { + group.available = true; + } + group.addEntry(p_profile->name, desc.constData()); + } +} diff --git a/src/helper.h b/src/helper.h new file mode 100644 index 0000000..823f3e1 --- /dev/null +++ b/src/helper.h @@ -0,0 +1,61 @@ +#ifndef _HELPER_H_ +#define _HELPER_H_ + +#include "pavucontrol.h" +#include +#if HAVE_EXT_DEVICE_RESTORE_API +# include +#endif + +#include +#include +#include +#include +#include + +struct ProfileEntry { + QByteArray id; + QStringList tokens; + QString getName() const; +}; + +struct ProfileGroup { + ProfileGroup() : available(false) {} + bool available; + QString name; + QList entries; + QString getProfileName(); + void addEntry(const char* id, const char* name); + bool containsProfile(const QByteArray &pro) const; +}; + +struct PortInfo { + QByteArray name; + QByteArray description; + uint32_t priority; + int available; + int direction; + int64_t latency_offset; + std::vector profiles; +}; + + +/* Used for profile sorting */ +struct profile_prio_compare { + bool operator() (pa_card_profile_info2 const * const lhs, pa_card_profile_info2 const * const rhs) const { + + if (lhs->priority == rhs->priority) + return strcmp(lhs->name, rhs->name) > 0; + + return lhs->priority > rhs->priority; + } +}; + +void populatePorts(const pa_card_info &info, std::map &ports); + + +void groupProfiles(const std::set &profile_priorities, + const std::map &ports, + QMap &profiles); + +#endif diff --git a/src/mainwindow.cc b/src/mainwindow.cc index 66c0ac4..0b6b310 100644 --- a/src/mainwindow.cc +++ b/src/mainwindow.cc @@ -36,17 +36,6 @@ #include #include -/* Used for profile sorting */ -struct profile_prio_compare { - bool operator() (pa_card_profile_info2 const * const lhs, pa_card_profile_info2 const * const rhs) const { - - if (lhs->priority == rhs->priority) - return strcmp(lhs->name, rhs->name) > 0; - - return lhs->priority > rhs->priority; - } -}; - struct sink_port_prio_compare { bool operator() (const pa_sink_port_info& lhs, const pa_sink_port_info& rhs) const { @@ -211,6 +200,7 @@ void MainWindow::updateCard(const pa_card_info &info) { setIconByName(w->iconImage, icon, "audio-card"); w->hasSinks = w->hasSources = false; + profile_priorities.clear(); for (pa_card_profile_info2 ** p_profile = info.profiles2; p_profile && *p_profile != nullptr; ++p_profile) { w->hasSinks = w->hasSinks || ((*p_profile)->n_sinks > 0); @@ -218,65 +208,9 @@ void MainWindow::updateCard(const pa_card_info &info) { profile_priorities.insert(*p_profile); } - w->ports.clear(); - for (uint32_t i = 0; i < info.n_ports; ++i) { - PortInfo p; - - p.name = info.ports[i]->name; - p.description = info.ports[i]->description; - p.priority = info.ports[i]->priority; - p.available = info.ports[i]->available; - p.direction = info.ports[i]->direction; - p.latency_offset = info.ports[i]->latency_offset; - for (pa_card_profile_info2 ** p_profile = info.ports[i]->profiles2; p_profile && *p_profile != nullptr; ++p_profile) - p.profiles.push_back((*p_profile)->name); - - w->ports[p.name] = p; - } - - w->profiles.clear(); - for (auto p_profile : profile_priorities) { - bool hasNo = false, hasOther = false; - std::map::iterator portIt; - QByteArray desc = p_profile->description; - - for (portIt = w->ports.begin(); portIt != w->ports.end(); portIt++) { - PortInfo port = portIt->second; + populatePorts(info, w->ports); - if (std::find(port.profiles.begin(), port.profiles.end(), p_profile->name) == port.profiles.end()) - continue; - - if (port.available == PA_PORT_AVAILABLE_NO) - hasNo = true; - else { - hasOther = true; - break; - } - } - if (hasNo && !hasOther) - desc += tr(" (unplugged)").toUtf8().constData(); - - if (!p_profile->available) - desc += tr(" (unavailable)").toUtf8().constData(); - - QString parseId = QString::fromUtf8(p_profile->name); - int plus = parseId.indexOf(QLatin1Char('+')); - if (plus != -1) { - parseId = parseId.left(plus); - } - auto list = parseId.split(QRegularExpression(QLatin1String("[:\\-]"))); - parseId.clear(); - for (auto p : list) { - if (p == QLatin1String("input") || p == QLatin1String("output")) - continue; - if (parseId.isEmpty() || p.startsWith(QLatin1String("extra"))) { - parseId.append(p); - } - } - qDebug() << "ParseID:" << parseId; - ProfileGroup &group = w->profiles[parseId]; - group.addEntry(p_profile->name, desc.constData()); - } + groupProfiles(profile_priorities, w->ports, w->profiles); w->activeProfile = info.active_profile ? info.active_profile->name : ""; diff --git a/src/pavucontrol.cc b/src/pavucontrol.cc index f012d08..4b1d502 100644 --- a/src/pavucontrol.cc +++ b/src/pavucontrol.cc @@ -31,6 +31,7 @@ // #include +#include "helper.h" #include "pavucontrol.h" #include "minimalstreamwidget.h" #include "channel.h" @@ -50,6 +51,9 @@ #include #include #include +#include + +#include static pa_context* context = nullptr; static pa_mainloop_api* api = nullptr; @@ -57,6 +61,7 @@ static int n_outstanding = 0; static int default_tab = 0; static bool retry = false; static int reconnect_timeout = 1; +static QRegularExpression select_output; void show_error(const char *txt) { char buf[256]; @@ -77,7 +82,7 @@ static void dec_outstanding(MainWindow *w) { } } -void card_cb(pa_context *, const pa_card_info *i, int eol, void *userdata) { +static void card_cb(pa_context *, const pa_card_info *i, int eol, void *userdata) { MainWindow *w = static_cast(userdata); if (eol < 0) { @@ -96,6 +101,69 @@ void card_cb(pa_context *, const pa_card_info *i, int eol, void *userdata) { w->updateCard(*i); } +static void card_cb_setdef(pa_context * ctx, const pa_card_info *i, int eol, void *) { + if (eol < 0) { + if (pa_context_errno(context) == PA_ERR_NOENTITY) + return; + + show_error(QObject::tr("Card callback failure").toUtf8().constData()); + return; + } + + if (eol > 0) { + return; + } + + // TODO Check stuff + + std::set profile_priorities; + std::map ports; + QMap profiles; + + profile_priorities.clear(); + for (pa_card_profile_info2 ** p_profile = i->profiles2; p_profile && *p_profile != nullptr; ++p_profile) { + profile_priorities.insert(*p_profile); + } + + populatePorts(*i, ports); + groupProfiles(profile_priorities, ports, profiles); + + ProfileGroup *best = nullptr; + for (auto &p : profiles) { + if (p.getProfileName().contains(select_output)) { + // Maybe we should track per-profile availability too and scan the list... + if (best == nullptr || p.available) { + best = &p; + if (p.available) + break; + } + } + } + + if (best != nullptr) { + // Can we do this inside the callback? + pa_operation* o; + const auto *entry = best->entries.first().id.constData(); + printf("Selecting profile %s\n", entry); + if (!(o = pa_context_set_card_profile_by_index(ctx, i->index, entry, nullptr, nullptr))) { + printf("pa_context_set_card_profile_by_index() failed\n"); + return; + } + pa_operation_unref(o); + } +} + +static void context_state_callback_setdef(pa_context *c, void *) { + if (pa_context_get_state(c) == PA_CONTEXT_READY) { + pa_operation *o; + if (!(o = pa_context_get_card_info_list(c, card_cb_setdef, nullptr))) { + show_error(QObject::tr("pa_context_get_card_info_list() failed").toUtf8().constData()); + return; + } + pa_operation_unref(o); + } +} + #if HAVE_EXT_DEVICE_RESTORE_API static void ext_device_restore_subscribe_cb(pa_context *c, pa_device_type_t type, uint32_t idx, void *userdata); #endif @@ -597,12 +665,7 @@ pa_context* get_context(void) { return context; } -gboolean connect_to_pulse(gpointer userdata) { - MainWindow *w = static_cast(userdata); - - if (context) - return false; - +void connectToPulse(void) { pa_proplist *proplist = pa_proplist_new(); pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, QObject::tr("PulseAudio Volume Control").toUtf8().constData()); pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, "org.PulseAudio.pavucontrol"); @@ -613,6 +676,15 @@ gboolean connect_to_pulse(gpointer userdata) { g_assert(context); pa_proplist_free(proplist); +} + +gboolean connect_to_pulse(gpointer userdata) { + MainWindow *w = static_cast(userdata); + + if (context) + return false; + + connectToPulse(); pa_context_set_state_callback(context, context_state_callback, w); @@ -643,6 +715,7 @@ gboolean connect_to_pulse(gpointer userdata) { } int main(int argc, char *argv[]) { + int exit_code = 0, have_regex = 0; signal(SIGPIPE, SIG_IGN); @@ -678,35 +751,76 @@ int main(int argc, char *argv[]) { QCommandLineOption maximizeOption(QStringList() << QStringLiteral("maximize") << QStringLiteral("m"), QObject::tr("Maximize the window.")); parser.addOption(maximizeOption); + QCommandLineOption selectOption(QStringList() << QStringLiteral("output") << QStringLiteral("o"), QObject::tr("Select a specific output configuration and quit."), QStringLiteral("regex")); + parser.addOption(selectOption); + parser.process(app); default_tab = parser.value(tabOption).toInt(); retry = parser.isSet(retryOption); + if (parser.isSet(selectOption)) { + select_output = QRegularExpression(parser.value(selectOption)); + if (!select_output.isValid()) { + select_output = QRegularExpression(QRegularExpression::escape(parser.value(selectOption))); + } + have_regex = select_output.isValid(); + } - // ca_context_set_driver(ca_gtk_context_get(), "pulse"); - - MainWindow* mainWindow = new MainWindow(); - if(parser.isSet(maximizeOption)) - mainWindow->showMaximized(); - - pa_glib_mainloop *m = pa_glib_mainloop_new(g_main_context_default()); - g_assert(m); - api = pa_glib_mainloop_get_api(m); - g_assert(api); + if (have_regex) { + pa_mainloop *m = pa_mainloop_new(); + g_assert(m); + api = pa_mainloop_get_api(m); + g_assert(api); + + connectToPulse(); + pa_context_set_state_callback(context, context_state_callback_setdef, nullptr); + if (pa_context_connect(context, nullptr, PA_CONTEXT_NOFAIL, nullptr) >= 0) { + if (pa_context_errno(context) == PA_ERR_INVALID) { + printf("pa_context_errno = PA_ERR_INVALID\n"); + exit_code = 1; + } else { + int evs; + int tries = 10; + exit_code = 100; + // WTF + while ((evs = pa_mainloop_iterate(m, 0, &exit_code)) >= 0) { + if (evs == 0) { + if (--tries == 0) + break; + usleep(100000); + } + } + printf("Exit: %d\n", exit_code); + } + } - connect_to_pulse(mainWindow); - if (reconnect_timeout >= 0) { - mainWindow->show(); - app.exec(); - } + if (context) + pa_context_unref(context); + pa_mainloop_free(m); + } else { + MainWindow* mainWindow = new MainWindow(); + if(parser.isSet(maximizeOption)) + mainWindow->showMaximized(); + + pa_glib_mainloop *m = pa_glib_mainloop_new(g_main_context_default()); + g_assert(m); + api = pa_glib_mainloop_get_api(m); + g_assert(api); + + connect_to_pulse(mainWindow); + if (reconnect_timeout >= 0) { + mainWindow->show(); + app.exec(); + } - if (reconnect_timeout < 0) - show_error(QObject::tr("Fatal Error: Unable to connect to PulseAudio").toUtf8().constData()); + if (reconnect_timeout < 0) + show_error(QObject::tr("Fatal Error: Unable to connect to PulseAudio").toUtf8().constData()); - delete mainWindow; + delete mainWindow; - if (context) - pa_context_unref(context); - pa_glib_mainloop_free(m); + if (context) + pa_context_unref(context); + pa_glib_mainloop_free(m); + } - return 0; + return exit_code; } -- cgit v1.2.3-55-g7522