#include "main.h" #include "mainwindow.h" #include "slxoutput.h" #include "setdefault.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // libkf5pulseaudioqt-dev // Public /** Ignore any signals from GUI elements when this is true. Set when we're updating the GUI */ bool g_IgnoreGui; // Private /** If not empty, on profile change, if profile belongs to this card, make its sink default */ static QString _pendingCardDefaultId; /** If not empty, on sink appearance, if port belongs to this sink, make it sink's default */ static QString _pendingCardDefaultPort; /** While running, we want to set each port's volume to 100% the first time we see it. Unfortunately * a port only starts to exist once its according profile is selected, so there is no way to have * a simple "set everything to 100%" method run on startup, at least not without switching through * all profiles on all cards, which sounds like it could be annoying and buggy. So do it this way * for now. */ static QSet _donePorts; /** Whe something changes, (re)start a short timeout first before actually updating the GUI, to * avoid multiple updates in rapid succession, for example when changing the active profile. */ static QTimer _updateDelay; static MainWindow *_mainWindow; /** * Queue a GUI update. By default, delay it by 50ms. If the timer is already active but hasn't * expired yet, it will be restarted with the given timeout. */ static void queueGuiUpdate(int ms = 50) { _updateDelay.start(ms); } /** * Make sure the port is enabled and volume up, the first time we see it at least. */ static void autoVolumeAdjustment(PulseAudioQt::Device *device, PulseAudioQt::Port *port, int portIndex = -1) { QString id = device->name() + port->name(); if (_donePorts.contains(id)) return; if (portIndex == -1) { int i = -1; for (auto *p : device->ports()) { ++i; if (port == p) { portIndex = i; } } if (portIndex == -1) { printf("Port not found\n"); return; } } bool portIsActive = portIndex == device->activePortIndex(); bool defaultSinkAndPort = (device->isDefault() && portIsActive); qint64 volume = device->volume(); // Only once, update port volume to be 100% if it's low if (defaultSinkAndPort) { _donePorts.insert(id); printf("Initial volume fix for %s\n", id.toLocal8Bit().constData()); if (volume < 60000) { volume = 65535; device->setVolume(volume); } device->setMuted(false); } } static void addDevicePortToWindow(PulseAudioQt::Card *card, PulseAudioQt::Device *device, PulseAudioQt::Port *port, int portIndex, bool isOutput) { //if (port->availability() == PulseAudioQt::Port::Unavailable) // continue; bool portIsActive = portIndex == device->activePortIndex(); bool defaultSinkAndPort = (device->isDefault() && portIsActive); qint64 volume = device->volume(); printf("[%c] Output: '%s %s', volume: %d, mute: %d\n", defaultSinkAndPort ? 'x' : ' ', device->description().toLocal8Bit().constData(), port->description().toLocal8Bit().constData(), (int)volume, (int)device->isMuted()); //knownCardPortCombos.insert(card->name() + ":" + port->name()); _mainWindow->getDevice(card, device, port, isOutput)->updateDeviceAndPort(defaultSinkAndPort, device->isMuted(), (portIsActive && device->isVolumeWritable()) ? device->volume() : -1); } /** * The actual GUI update logic. Iterate over all sinks and their ports, and add them to the GUI. * Then iterate over all non-selected profiles, and add them to the GUI too, so we have a simple, flat * list of "outputs" to select, which should be a bit more easy to grasp for a non-technical person * trying to make the computer go beep. (Hopefully) */ static void updateActiveOutput() { g_IgnoreGui = true; // Block GUI updates _mainWindow->mark(); // Mark all items in the lists as unused auto *i = PulseAudioQt::Context::instance(); auto cards = i->cards(); printf(".--------------------------------------------.\n"); //QSet knownCardPortCombos; for (auto *sink : i->sinks()) { PulseAudioQt::Card *card = nullptr; if (sink->cardIndex() < cards.size()) { card = cards.at(sink->cardIndex()); } int portIndex = -1; for (auto *port : sink->ports()) { portIndex++; addDevicePortToWindow(card, sink, port, portIndex, true); } } for (auto *source : i->sources()) { PulseAudioQt::Card *card = nullptr; if (source->cardIndex() < cards.size()) { card = cards.at(source->cardIndex()); } int portIndex = -1; for (auto *port : source->ports()) { portIndex++; addDevicePortToWindow(card, source, port, portIndex, false); } } // Now find all the profiles from all cards that are not active and add an entry to the list for each one for (auto *card : cards) { /* * Don't do this for now - even though we know now that there might be another port for a card that currently * doesn't exist for any of its sinks, we don't know what profile we need to switch to to get a sink with that port. * Rather refactor the UI a little bit to have a card switcher. int portIndex = -1; for (auto *port : card->ports()) { portIndex++; if (knownCardPortCombos.contains(card->name() + ":" + port->name())) continue; _mainWindow->getCardPort(card, port)->updateCardAndProfile(); } */ int profIndex = -1; for (auto *profile : card->profiles()) { profIndex++; // Ignore the active profile, and profiles that are unavailable if (profIndex == card->activeProfileIndex() || profile->availability() == PulseAudioQt::Profile::Unavailable) continue; // No point in selecting something that doesn't have any way to output sound if (profile->sinks() == 0 && profile->sources() == 0) continue; printf("[ ] Output: '%s', sinks: %d\n", profile->description().toLocal8Bit().constData(), profile->sinks()); _mainWindow->getCardProfile(card, profile)->updateCardAndProfile(); } } printf("`--------------------------------------------ยด\n"); _mainWindow->sweep(); // Removes all items from the list that are still marked unused, i.e. updateOutput() was not called on those g_IgnoreGui = false; // Allow GUI updates again } /** * Called some time after the user changed to a profile/port that was not enabled at the time of clicking, so * wait for PA state to update and then set the port as default port. */ static void checkShouldSetDefaultPort(PulseAudioQt::Device *sink) { if (_pendingCardDefaultPort.isEmpty()) return; int portIdx = -1; for (auto *port : sink->ports()) { portIdx++; if (port->name() == _pendingCardDefaultPort) { _pendingCardDefaultPort.clear(); sink->setActivePortIndex(portIdx); } } } /** * Called after the user set a GUI entry as default that represents a profile, after the active profile * or the list of available ports changed. Only after this happened can we actually set as default the sink/port * that was created from selecting that profile as the default sink. */ static void checkShouldSetDefaultSink() { if (_pendingCardDefaultId.isEmpty()) // No switch actually pending return; // Otherwise, it's the ID of the card that the profile belongs to that the user wants to set as default printf("Pending card default to %s\n", _pendingCardDefaultId.toLocal8Bit().constData()); auto *i = PulseAudioQt::Context::instance(); int cardIdx = -1; // Find the current index of that card for (auto *card : i->cards()) { cardIdx++; if (card->name() == _pendingCardDefaultId && !card->ports().isEmpty()) break; } // Then iterate over all sinks until we find one that belongs to the card for (auto *sink : i->sinks()) { if (sink->cardIndex() == cardIdx) { printf("MATCH SET!\n"); // Set as default, unmute, and clear the pending switch sink->setDefault(true); sink->setMuted(false); _pendingCardDefaultId.clear(); checkShouldSetDefaultPort(sink); } } } /** * Called when PA tells us about a new card; start listening to signals we're interesed in. */ static void newCardAppeared(PulseAudioQt::Card *card) { //if (_doneCards.contains(card->name())) // return; auto *i = PulseAudioQt::Context::instance(); QCoreApplication::connect(card, &PulseAudioQt::Card::profilesChanged, [=]() { printf("Card %p profiles changed\n", card); }); QCoreApplication::connect(card, &PulseAudioQt::Card::activeProfileIndexChanged, [=]() { printf("Card %p active profile index changed\n", card); checkShouldSetDefaultSink(); queueGuiUpdate(); }); QCoreApplication::connect(card, &PulseAudioQt::Card::portsChanged, [=]() { printf("Card %p ports changed\n", card); checkShouldSetDefaultSink(); queueGuiUpdate(); }); /* QCoreApplication::connect(card, &PulseAudioQt::Card::sinksChanged, [=]() { printf("Card %p sinks changed\n", card); }); */ } /** * Called when PA tells us about a new device; start listening to signals we're interested in. */ static void newDeviceAppeared(PulseAudioQt::Device *device) { QCoreApplication::connect(device, &PulseAudioQt::Sink::activePortIndexChanged, [device]() { printf("Sink %p changed active port\n", device); queueGuiUpdate(); }); QCoreApplication::connect(device, &PulseAudioQt::Sink::defaultChanged, [device]() { printf("Sink %p changed default\n", device); queueGuiUpdate(); }); QCoreApplication::connect(device, &PulseAudioQt::Sink::portsChanged, [device]() { printf("Sink %p ports changed\n", device); checkShouldSetDefaultPort(device); queueGuiUpdate(); }); QCoreApplication::connect(device, &PulseAudioQt::Sink::volumeChanged, [device]() { queueGuiUpdate(250); }); QCoreApplication::connect(device, &PulseAudioQt::Sink::isVolumeWritableChanged, [device]() { queueGuiUpdate(); }); checkShouldSetDefaultPort(device); queueGuiUpdate(); } /** * Mute/unmute device by its string identifier. */ void setMuted(const QString &deviceId, bool muted) { auto *i = PulseAudioQt::Context::instance(); for (auto *sp : i->sinks()) { if (sp->name() == deviceId) { sp->setMuted(muted); } } for (auto *sp : i->sources()) { if (sp->name() == deviceId) { sp->setMuted(muted); } } queueGuiUpdate(); } /** * Set a device's volume, by its string identifier. */ void setSinkVolume(const QString &deviceId, int volume) { auto *i = PulseAudioQt::Context::instance(); for (auto *sp : i->sinks()) { if (sp->name() == deviceId) { sp->setVolume(volume); queueGuiUpdate(250); } } for (auto *sp : i->sources()) { if (sp->name() == deviceId) { sp->setVolume(volume); queueGuiUpdate(250); } } } /** * Set the given card as default, preferably enabling the given profile * on it. If this card doesn't have a profile with the given ID, try * to use a profile that contains the string "Duplex" in its name, * otherwise, just use the first profile available. */ void enableCard(const QString &card, const QString &profile, const QString &port) { auto *i = PulseAudioQt::Context::instance(); PulseAudioQt::Card *matchingCard = nullptr; int profileIdx = -1; int cardIdx = -1; _pendingCardDefaultId.clear(); _pendingCardDefaultPort.clear(); for (auto *cp : i->cards()) { cardIdx++; if (cp->name() != card) continue; int i = -1; int exactProfileIdx = -1; for (auto *pp : cp->profiles()) { i++; if (pp->availability() == PulseAudioQt::Profile::Unavailable) continue; if (pp->description().contains(QLatin1String("Duplex"))) { // Prefer Duplex mode for analog outputs, it's usually listed after output only profileIdx = i; } if (profileIdx == -1) { // Otherwise default to first one in list profileIdx = i; } if (pp->name() == profile) { exactProfileIdx = i; } } if (exactProfileIdx != -1) { profileIdx = exactProfileIdx; } if (profileIdx != -1) { matchingCard = cp; break; } } if (matchingCard != nullptr && profileIdx < matchingCard->profiles().size()) { if (matchingCard->activeProfileIndex() == profileIdx) { // Profile already active, just unmute sink for (auto *sink : i->sinks()) { if (sink->cardIndex() == cardIdx) { sink->setMuted(false); sink->setDefault(true); _pendingCardDefaultPort = port; checkShouldSetDefaultPort(sink); } } for (auto *source : i->sources()) { if (source->cardIndex() == cardIdx) { source->setMuted(false); source->setDefault(true); } } } else { // Remember the ID of the card we switched to; we need this as only after PA is done // switching to the desired profile will we see any sink belonging to it, so we can only // set the according sink as fallback after that happens. See checkShouldSetDefault(). _pendingCardDefaultId = matchingCard->name(); _pendingCardDefaultPort = port; matchingCard->setActiveProfileIndex(profileIdx); } } queueGuiUpdate(); } /** * Set given sink as default sink, unmute it, and switch to its * port as passed to this function. If the sink doesn't have a * port with this ID, nothing happens. */ void enableSink(const QString &sink, const QString &port) { auto *i = PulseAudioQt::Context::instance(); _pendingCardDefaultId.clear(); _pendingCardDefaultPort.clear(); for (auto *sp : i->sinks()) { if (sp->name() != sink) continue; int i = -1; for (auto *pp : sp->ports()) { i++; if (pp->name() == port) { sp->setDefault(true); sp->setMuted(false); sp->setActivePortIndex(i); } } } queueGuiUpdate(); } /** * Set given source as default source, unmute it, and switch to its * port as passed to this function. If the source doesn't have a * port with this ID, nothing happens. */ void enableSource(const QString &source, const QString &port) { auto *i = PulseAudioQt::Context::instance(); _pendingCardDefaultId.clear(); _pendingCardDefaultPort.clear(); for (auto *sp : i->sources()) { if (sp->name() != source) continue; int i = -1; for (auto *pp : sp->ports()) { i++; if (pp->name() == port) { sp->setDefault(true); sp->setMuted(false); sp->setActivePortIndex(i); } } } queueGuiUpdate(); } static void setupAutoVolumeDevice(PulseAudioQt::Device *device) { auto fn = [device]() { int pi = device->activePortIndex(); if (pi < 0 || pi >= device->ports().size()) return; auto *port = device->ports().at(pi); autoVolumeAdjustment(device, port, pi); }; QObject::connect(device, &PulseAudioQt::Source::activePortIndexChanged, fn); fn(); } static void setupAutoVolume() { auto *i = PulseAudioQt::Context::instance(); for (auto *sink : i->sinks()) { setupAutoVolumeDevice(sink); } for (auto *source : i->sources()) { setupAutoVolumeDevice(source); } QObject::connect(i, &PulseAudioQt::Context::sinkAdded, [](PulseAudioQt::Sink *sink) { setupAutoVolumeDevice(sink); }); QObject::connect(i, &PulseAudioQt::Context::sourceAdded, [](PulseAudioQt::Source *source) { setupAutoVolumeDevice(source); }); } int main(int argc, char **argv) { QApplication a(argc, argv); auto *i = PulseAudioQt::Context::instance(); a.setOrganizationName(QStringLiteral("slxmix")); a.setApplicationVersion(QLatin1String("1.0.0.0.0")); QCommandLineParser parser; parser.setApplicationDescription(QObject::tr("SLXmix Volume Control")); parser.addHelpOption(); QCommandLineOption selectOption(QStringList() << QStringLiteral("output") << QStringLiteral("o"), QObject::tr("Select a specific output configuration and quit. Will select the Profile/Sink/Port combo" " that best matches the list of given keywords, e.g. 'HDMI 1 5.1'"), QStringLiteral("keywords")); QCommandLineOption workerOption(QStringList() << QStringLiteral("worker") << QStringLiteral("w"), QObject::tr("Run invisible worker that sets every port to 100% and unmuted the first time it's set default")); parser.addOption(selectOption); parser.addOption(workerOption); parser.process(a); // Select default output and exit if (parser.isSet(selectOption)) { if (parser.isSet(workerOption)) { printf("Cannot set both -w and -o at the same time.\n"); return 1; } QString what = parser.value(selectOption); QTimer::singleShot(100, [what]() { setDefaultOutput(what); }); return a.exec(); } if (parser.isSet(workerOption)) { if (parser.isSet(selectOption)) { printf("Cannot set both -w and -o at the same time.\n"); return 1; } QTimer::singleShot(100, &setupAutoVolume); return a.exec(); } // There's no signal, or no way to check if we're connected to PA, so // just wait a bit and hope for the best. QTimer::singleShot(100, [=]() { for (auto *card : i->cards()) { newCardAppeared(card); printf("Card: %s (index: %d)\n", card->name().toLocal8Bit().constData(), (int)card->index()); for (auto *profile : card->profiles()) { printf(" Profile: %s\n", profile->name().toLocal8Bit().constData()); } for (auto *port : card->ports()) { printf(" Port: %s -- %s (%i)\n", port->name().toLocal8Bit().constData(), port->description().toLocal8Bit().constData(), (int)port->availability()); } } for (auto *sink : i->sinks()) { printf("Sink: %s (for card index %d)\n", sink->name().toLocal8Bit().constData(), sink->cardIndex()); for (auto *port : sink->ports()) { printf(" Port: %s -- %s (%i)\n", port->name().toLocal8Bit().constData(), port->description().toLocal8Bit().constData(), (int)port->availability()); } newDeviceAppeared(sink); } for (auto *source : i->sources()) { printf("Source: %s (for card index %d)\n", source->name().toLocal8Bit().constData(), source->cardIndex()); for (auto *port : source->ports()) { printf(" Port: %s -- %s (%i)\n", port->name().toLocal8Bit().constData(), port->description().toLocal8Bit().constData(), (int)port->availability()); } newDeviceAppeared(source); } QCoreApplication::connect(i, &PulseAudioQt::Context::cardAdded, &newCardAppeared); QCoreApplication::connect(i, &PulseAudioQt::Context::sinkAdded, &newDeviceAppeared); QCoreApplication::connect(i, &PulseAudioQt::Context::sourceAdded, &newDeviceAppeared); printf("Initial output\n"); queueGuiUpdate(); }); QCoreApplication::connect(&_updateDelay, &QTimer::timeout, &updateActiveOutput); _updateDelay.setInterval(50); _updateDelay.setSingleShot(true); _mainWindow = new MainWindow; _mainWindow->show(); return a.exec(); }