/*** This file is part of pavucontrol. Copyright 2006-2008 Lennart Poettering Copyright 2008 Sjoerd Simons pavucontrol is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. pavucontrol is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with pavucontrol. If not, see . ***/ #ifdef HAVE_CONFIG_H #include #endif #define PACKAGE_VERSION "0.1" #include #include #include #include // #include #include "helper.h" #include "pavucontrol.h" #include "minimalstreamwidget.h" #include "channel.h" #include "streamwidget.h" #include "cardwidget.h" #include "sinkwidget.h" #include "sourcewidget.h" #include "sinkinputwidget.h" #include "sourceoutputwidget.h" #include "rolewidget.h" #include "mainwindow.h" #include #include #include #include #include #include #include #include #include #include static pa_context* context = nullptr; static pa_mainloop_api* api = nullptr; 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]; snprintf(buf, sizeof(buf), "%s: %s", txt, pa_strerror(pa_context_errno(context))); QMessageBox::critical(nullptr, QObject::tr("Error"), QString::fromUtf8(buf)); qApp->quit(); } static void dec_outstanding(MainWindow *w) { if (n_outstanding <= 0) return; if (--n_outstanding <= 0) { // w->get_window()->set_cursor(); w->setConnectionState(true); } } static void card_cb(pa_context *, const pa_card_info *i, int eol, void *userdata) { MainWindow *w = static_cast(userdata); 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) { dec_outstanding(w); return; } 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 void sink_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata) { MainWindow *w = static_cast(userdata); if (eol < 0) { if (pa_context_errno(context) == PA_ERR_NOENTITY) return; show_error(QObject::tr("Sink callback failure").toUtf8().constData()); return; } if (eol > 0) { dec_outstanding(w); return; } #if HAVE_EXT_DEVICE_RESTORE_API if (w->updateSink(*i)) ext_device_restore_subscribe_cb(c, PA_DEVICE_TYPE_SINK, i->index, w); #else w->updateSink(*i); #endif } void source_cb(pa_context *, const pa_source_info *i, int eol, void *userdata) { MainWindow *w = static_cast(userdata); if (eol < 0) { if (pa_context_errno(context) == PA_ERR_NOENTITY) return; show_error(QObject::tr("Source callback failure").toUtf8().constData()); return; } if (eol > 0) { dec_outstanding(w); return; } w->updateSource(*i); } void sink_input_cb(pa_context *, const pa_sink_input_info *i, int eol, void *userdata) { MainWindow *w = static_cast(userdata); if (eol < 0) { if (pa_context_errno(context) == PA_ERR_NOENTITY) return; show_error(QObject::tr("Sink input callback failure").toUtf8().constData()); return; } if (eol > 0) { dec_outstanding(w); return; } w->updateSinkInput(*i); } void source_output_cb(pa_context *, const pa_source_output_info *i, int eol, void *userdata) { MainWindow *w = static_cast(userdata); if (eol < 0) { if (pa_context_errno(context) == PA_ERR_NOENTITY) return; show_error(QObject::tr("Source output callback failure").toUtf8().constData()); return; } if (eol > 0) { if (n_outstanding > 0) { /* At this point all notebook pages have been populated, so * let's open one that isn't empty */ if (default_tab != -1) { if (default_tab < 1 || default_tab > w->notebook->count()) { if (!w->sinkInputWidgets.empty()) w->notebook->setCurrentIndex(0); else if (!w->sourceOutputWidgets.empty()) w->notebook->setCurrentIndex(1); else if (!w->sourceWidgets.empty() && w->sinkWidgets.empty()) w->notebook->setCurrentIndex(3); else w->notebook->setCurrentIndex(2); } else { w->notebook->setCurrentIndex(default_tab - 1); } default_tab = -1; } } dec_outstanding(w); return; } w->updateSourceOutput(*i); } void client_cb(pa_context *, const pa_client_info *i, int eol, void *userdata) { MainWindow *w = static_cast(userdata); if (eol < 0) { if (pa_context_errno(context) == PA_ERR_NOENTITY) return; show_error(QObject::tr("Client callback failure").toUtf8().constData()); return; } if (eol > 0) { dec_outstanding(w); return; } w->updateClient(*i); } void server_info_cb(pa_context *, const pa_server_info *i, void *userdata) { MainWindow *w = static_cast(userdata); if (!i) { show_error(QObject::tr("Server info callback failure").toUtf8().constData()); return; } w->updateServer(*i); dec_outstanding(w); } void ext_stream_restore_read_cb( pa_context *, const pa_ext_stream_restore_info *i, int eol, void *userdata) { MainWindow *w = static_cast(userdata); if (eol < 0) { dec_outstanding(w); g_debug(QObject::tr("Failed to initialize stream_restore extension: %s").toUtf8().constData(), pa_strerror(pa_context_errno(context))); w->deleteEventRoleWidget(); return; } if (eol > 0) { dec_outstanding(w); return; } w->updateRole(*i); } static void ext_stream_restore_subscribe_cb(pa_context *c, void *userdata) { MainWindow *w = static_cast(userdata); pa_operation *o; if (!(o = pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, w))) { show_error(QObject::tr("pa_ext_stream_restore_read() failed").toUtf8().constData()); return; } pa_operation_unref(o); } #if HAVE_EXT_DEVICE_RESTORE_API void ext_device_restore_read_cb( pa_context *, const pa_ext_device_restore_info *i, int eol, void *userdata) { MainWindow *w = static_cast(userdata); if (eol < 0) { dec_outstanding(w); g_debug(QObject::tr("Failed to initialize device restore extension: %s").toUtf8().constData(), pa_strerror(pa_context_errno(context))); return; } if (eol > 0) { dec_outstanding(w); return; } /* Do something with a widget when this part is written */ w->updateDeviceInfo(*i); } static void ext_device_restore_subscribe_cb(pa_context *c, pa_device_type_t type, uint32_t idx, void *userdata) { MainWindow *w = static_cast(userdata); pa_operation *o; if (type != PA_DEVICE_TYPE_SINK) return; if (!(o = pa_ext_device_restore_read_formats(c, type, idx, ext_device_restore_read_cb, w))) { show_error(QObject::tr("pa_ext_device_restore_read_sink_formats() failed").toUtf8().constData()); return; } pa_operation_unref(o); } #endif void ext_device_manager_read_cb( pa_context *, const pa_ext_device_manager_info *, int eol, void *userdata) { MainWindow *w = static_cast(userdata); if (eol < 0) { dec_outstanding(w); g_debug(QObject::tr("Failed to initialize device manager extension: %s").toUtf8().constData(), pa_strerror(pa_context_errno(context))); return; } w->canRenameDevices = true; if (eol > 0) { dec_outstanding(w); return; } /* Do something with a widget when this part is written */ } static void ext_device_manager_subscribe_cb(pa_context *c, void *userdata) { MainWindow *w = static_cast(userdata); pa_operation *o; if (!(o = pa_ext_device_manager_read(c, ext_device_manager_read_cb, w))) { show_error(QObject::tr("pa_ext_device_manager_read() failed").toUtf8().constData()); return; } pa_operation_unref(o); } void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t index, void *userdata) { MainWindow *w = static_cast(userdata); switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { case PA_SUBSCRIPTION_EVENT_SINK: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) w->removeSink(index); else { pa_operation *o; if (!(o = pa_context_get_sink_info_by_index(c, index, sink_cb, w))) { show_error(QObject::tr("pa_context_get_sink_info_by_index() failed").toUtf8().constData()); return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_SOURCE: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) w->removeSource(index); else { pa_operation *o; if (!(o = pa_context_get_source_info_by_index(c, index, source_cb, w))) { show_error(QObject::tr("pa_context_get_source_info_by_index() failed").toUtf8().constData()); return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_SINK_INPUT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) w->removeSinkInput(index); else { pa_operation *o; if (!(o = pa_context_get_sink_input_info(c, index, sink_input_cb, w))) { show_error(QObject::tr("pa_context_get_sink_input_info() failed").toUtf8().constData()); return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) w->removeSourceOutput(index); else { pa_operation *o; if (!(o = pa_context_get_source_output_info(c, index, source_output_cb, w))) { show_error(QObject::tr("pa_context_get_sink_input_info() failed").toUtf8().constData()); return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_CLIENT: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) w->removeClient(index); else { pa_operation *o; if (!(o = pa_context_get_client_info(c, index, client_cb, w))) { show_error(QObject::tr("pa_context_get_client_info() failed").toUtf8().constData()); return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_SERVER: { pa_operation *o; if (!(o = pa_context_get_server_info(c, server_info_cb, w))) { show_error(QObject::tr("pa_context_get_server_info() failed").toUtf8().constData()); return; } pa_operation_unref(o); } break; case PA_SUBSCRIPTION_EVENT_CARD: if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) w->removeCard(index); else { pa_operation *o; if (!(o = pa_context_get_card_info_by_index(c, index, card_cb, w))) { show_error(QObject::tr("pa_context_get_card_info_by_index() failed").toUtf8().constData()); return; } pa_operation_unref(o); } break; } } /* Forward Declaration */ gboolean connect_to_pulse(gpointer userdata); void context_state_callback(pa_context *c, void *userdata) { MainWindow *w = static_cast(userdata); g_assert(c); switch (pa_context_get_state(c)) { case PA_CONTEXT_UNCONNECTED: case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: case PA_CONTEXT_SETTING_NAME: break; case PA_CONTEXT_READY: { pa_operation *o; reconnect_timeout = 1; /* Create event widget immediately so it's first in the list */ w->createEventRoleWidget(); pa_context_set_subscribe_callback(c, subscribe_cb, w); if (!(o = pa_context_subscribe(c, (pa_subscription_mask_t) (PA_SUBSCRIPTION_MASK_SINK| PA_SUBSCRIPTION_MASK_SOURCE| PA_SUBSCRIPTION_MASK_SINK_INPUT| PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT| PA_SUBSCRIPTION_MASK_CLIENT| PA_SUBSCRIPTION_MASK_SERVER| PA_SUBSCRIPTION_MASK_CARD), nullptr, nullptr))) { show_error(QObject::tr("pa_context_subscribe() failed").toUtf8().constData()); return; } pa_operation_unref(o); /* Keep track of the outstanding callbacks for UI tweaks */ n_outstanding = 0; if (!(o = pa_context_get_server_info(c, server_info_cb, w))) { show_error(QObject::tr("pa_context_get_server_info() failed").toUtf8().constData()); return; } pa_operation_unref(o); n_outstanding++; if (!(o = pa_context_get_client_info_list(c, client_cb, w))) { show_error(QObject::tr("pa_context_client_info_list() failed").toUtf8().constData()); return; } pa_operation_unref(o); n_outstanding++; if (!(o = pa_context_get_card_info_list(c, card_cb, w))) { show_error(QObject::tr("pa_context_get_card_info_list() failed").toUtf8().constData()); return; } pa_operation_unref(o); n_outstanding++; if (!(o = pa_context_get_sink_info_list(c, sink_cb, w))) { show_error(QObject::tr("pa_context_get_sink_info_list() failed").toUtf8().constData()); return; } pa_operation_unref(o); n_outstanding++; if (!(o = pa_context_get_source_info_list(c, source_cb, w))) { show_error(QObject::tr("pa_context_get_source_info_list() failed").toUtf8().constData()); return; } pa_operation_unref(o); n_outstanding++; if (!(o = pa_context_get_sink_input_info_list(c, sink_input_cb, w))) { show_error(QObject::tr("pa_context_get_sink_input_info_list() failed").toUtf8().constData()); return; } pa_operation_unref(o); n_outstanding++; if (!(o = pa_context_get_source_output_info_list(c, source_output_cb, w))) { show_error(QObject::tr("pa_context_get_source_output_info_list() failed").toUtf8().constData()); return; } pa_operation_unref(o); n_outstanding++; /* These calls are not always supported */ if ((o = pa_ext_stream_restore_read(c, ext_stream_restore_read_cb, w))) { pa_operation_unref(o); n_outstanding++; pa_ext_stream_restore_set_subscribe_cb(c, ext_stream_restore_subscribe_cb, w); if ((o = pa_ext_stream_restore_subscribe(c, 1, nullptr, nullptr))) pa_operation_unref(o); } else g_debug(QObject::tr("Failed to initialize stream_restore extension: %s").toUtf8().constData(), pa_strerror(pa_context_errno(context))); #if HAVE_EXT_DEVICE_RESTORE_API /* TODO Change this to just the test function */ if ((o = pa_ext_device_restore_read_formats_all(c, ext_device_restore_read_cb, w))) { pa_operation_unref(o); n_outstanding++; pa_ext_device_restore_set_subscribe_cb(c, ext_device_restore_subscribe_cb, w); if ((o = pa_ext_device_restore_subscribe(c, 1, nullptr, nullptr))) pa_operation_unref(o); } else g_debug(QObject::tr("Failed to initialize device restore extension: %s").toUtf8().constData(), pa_strerror(pa_context_errno(context))); #endif if ((o = pa_ext_device_manager_read(c, ext_device_manager_read_cb, w))) { pa_operation_unref(o); n_outstanding++; pa_ext_device_manager_set_subscribe_cb(c, ext_device_manager_subscribe_cb, w); if ((o = pa_ext_device_manager_subscribe(c, 1, nullptr, nullptr))) pa_operation_unref(o); } else g_debug(QObject::tr("Failed to initialize device manager extension: %s").toUtf8().constData(), pa_strerror(pa_context_errno(context))); break; } case PA_CONTEXT_FAILED: w->setConnectionState(false); w->removeAllWidgets(); w->updateDeviceVisibility(); pa_context_unref(context); context = nullptr; if (reconnect_timeout > 0) { g_debug("%s", QObject::tr("Connection failed, attempting reconnect").toUtf8().constData()); g_timeout_add_seconds(reconnect_timeout, connect_to_pulse, w); } return; case PA_CONTEXT_TERMINATED: default: qApp->quit(); return; } } pa_context* get_context(void) { return context; } void update_default_sinks(MainWindow *w) { pa_operation *o; if (!(o = pa_context_get_server_info(context, server_info_cb, w))) { show_error(QObject::tr("pa_context_get_server_info() failed").toUtf8().constData()); return; } pa_operation_unref(o); n_outstanding++; } 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"); pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, "audio-card"); pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, PACKAGE_VERSION); context = pa_context_new_with_proplist(api, nullptr, proplist); 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); w->setConnectingMessage(); if (pa_context_connect(context, nullptr, PA_CONTEXT_NOFAIL, nullptr) < 0) { if (pa_context_errno(context) == PA_ERR_INVALID) { w->setConnectingMessage(QObject::tr("Connection to PulseAudio failed. Automatic retry in 5s.

" "In this case this is likely because PULSE_SERVER in the Environment/X11 Root Window Properties" "or default-server in client.conf is misconfigured.
" "This situation can also arrise when PulseAudio crashed and left stale details in the X11 Root Window.
" "If this is the case, then PulseAudio should autospawn again, or if this is not configured you should" "run start-pulseaudio-x11 manually.").toUtf8().constData()); reconnect_timeout = 5; } else { if(!retry) { reconnect_timeout = -1; qApp->quit(); } else { g_debug("%s", QObject::tr("Connection failed, attempting reconnect").toUtf8().constData()); reconnect_timeout = 5; g_timeout_add_seconds(reconnect_timeout, connect_to_pulse, userdata); } } } return false; } int main(int argc, char *argv[]) { int exit_code = 0, have_regex = 0; signal(SIGPIPE, SIG_IGN); QApplication app(argc, argv); app.setOrganizationName(QStringLiteral("pavucontrol-qt")); app.setAttribute(Qt::AA_UseHighDpiPixmaps, true); QString locale = QLocale::system().name(); QTranslator qtTranslator; if(qtTranslator.load(QStringLiteral("qt_") + locale, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) qApp->installTranslator(&qtTranslator); QTranslator appTranslator; if(appTranslator.load(QStringLiteral("pavucontrol-qt_") + locale, QStringLiteral(PAVUCONTROL_QT_DATA_DIR) + QStringLiteral("/translations"))) qApp->installTranslator(&appTranslator); QCommandLineParser parser; parser.setApplicationDescription(QObject::tr("PulseAudio Volume Control")); parser.addHelpOption(); const QString VERINFO = QStringLiteral(PAVUCONTROLQT_VERSION "\nQt " QT_VERSION_STR); app.setApplicationVersion(VERINFO); parser.addVersionOption(); QCommandLineOption tabOption(QStringList() << QStringLiteral("tab") << QStringLiteral("t"), QObject::tr("Select a specific tab on load."), QStringLiteral("tab")); parser.addOption(tabOption); QCommandLineOption retryOption(QStringList() << QStringLiteral("retry") << QStringLiteral("r"), QObject::tr("Retry forever if pa quits (every 5 seconds).")); parser.addOption(retryOption); 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(); } 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); } } 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()); delete mainWindow; if (context) pa_context_unref(context); pa_glib_mainloop_free(m); } return exit_code; }