From da330b0a0ab5426b1502f2f97254478940f1f094 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 31 Aug 2022 15:58:15 +0200 Subject: WIP: Support input too TODO: Layout, UI, etc. --- src/main.cpp | 225 ++++++++++++++++++++++++++++++++++++++++------------- src/main.h | 4 +- src/mainwindow.cpp | 41 +++++++--- src/mainwindow.h | 6 +- src/mainwindow.ui | 19 ++++- src/slxoutput.cpp | 6 +- src/slxoutput.h | 3 +- 7 files changed, 231 insertions(+), 73 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index adf515b..cc149a4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,8 +28,10 @@ bool g_IgnoreGui; // Private -/** If not empty, on profile change, if profile belings to this card, make its sink default */ -static QString _pendingCardDefault; +/** 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 startes to exist once its according profile is selected, so there is no way to have @@ -55,6 +57,31 @@ static void queueGuiUpdate(int ms = 50) _updateDelay.start(ms); } +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()); + QString id = device->name() + port->name(); + // Only once, update port volume to be 100% if it's low + if (defaultSinkAndPort && !_donePorts.contains(id)) { + _donePorts.insert(id); + if (volume < 60000) { + volume = 65535; + device->setVolume(volume); + } + } + //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 @@ -69,6 +96,7 @@ static void updateActiveOutput() auto cards = i->cards(); printf(".--------------------------------------------.\n"); + //QSet knownCardPortCombos; for (auto *sink : i->sinks()) { PulseAudioQt::Card *card = nullptr; if (sink->cardIndex() < cards.size()) { @@ -77,30 +105,34 @@ static void updateActiveOutput() int portIndex = -1; for (auto *port : sink->ports()) { portIndex++; - //if (port->availability() == PulseAudioQt::Port::Unavailable) - // continue; - bool portIsActive = portIndex == sink->activePortIndex(); - bool defaultSinkAndPort = (sink->isDefault() && portIsActive); - qint64 volume = sink->volume(); - printf("[%c] Output: '%s %s', volume: %d, mute: %d\n", - defaultSinkAndPort ? 'x' : ' ', - sink->description().toLocal8Bit().constData(), port->description().toLocal8Bit().constData(), - (int)volume, (int)sink->isMuted()); - QString id = sink->name() + port->name(); - // Only once, update port volume to be 100% if it's low - if (defaultSinkAndPort && !_donePorts.contains(id)) { - _donePorts.insert(id); - if (volume < 60000) { - volume = 65535; - sink->setVolume(volume); - } - } - _mainWindow->getDevice(card, sink, port)->updateDeviceAndPort(defaultSinkAndPort, sink->isMuted(), - (portIsActive && sink->isVolumeWritable()) ? sink->volume() : -1); + 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++; @@ -108,7 +140,7 @@ static void updateActiveOutput() 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) + if (profile->sinks() == 0 && profile->sources() == 0) continue; printf("[ ] Output: '%s', sinks: %d\n", profile->description().toLocal8Bit().constData(), profile->sinks()); @@ -120,23 +152,41 @@ static void updateActiveOutput() 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 checkShouldSetDefault() +static void checkShouldSetDefaultSink() { - if (_pendingCardDefault.isEmpty()) // No switch actually pending + 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", _pendingCardDefault.toLocal8Bit().constData()); + 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() == _pendingCardDefault && !card->ports().isEmpty()) + if (card->name() == _pendingCardDefaultId && !card->ports().isEmpty()) break; } // Then iterate over all sinks until we find one that belongs to the card @@ -146,7 +196,8 @@ static void checkShouldSetDefault() // Set as default, unmute, and clear the pending switch sink->setDefault(true); sink->setMuted(false); - _pendingCardDefault.clear(); + _pendingCardDefaultId.clear(); + checkShouldSetDefaultPort(sink); } } } @@ -164,12 +215,12 @@ static void newCardAppeared(PulseAudioQt::Card *card) }); QCoreApplication::connect(card, &PulseAudioQt::Card::activeProfileIndexChanged, [=]() { printf("Card %p active profile index changed\n", card); - checkShouldSetDefault(); + checkShouldSetDefaultSink(); queueGuiUpdate(); }); QCoreApplication::connect(card, &PulseAudioQt::Card::portsChanged, [=]() { printf("Card %p ports changed\n", card); - checkShouldSetDefault(); + checkShouldSetDefaultSink(); queueGuiUpdate(); }); /* @@ -180,39 +231,47 @@ static void newCardAppeared(PulseAudioQt::Card *card) } /** - * Called when PA tells us about a new sink; start listening to signals we're interested in. + * Called when PA tells us about a new device; start listening to signals we're interested in. */ -static void newSinkAppeared(PulseAudioQt::Sink *sink) +static void newDeviceAppeared(PulseAudioQt::Device *device) { - for (auto *port : sink->ports()) { - printf(" Port: %s\n", port->name().toLocal8Bit().constData()); - } - QCoreApplication::connect(sink, &PulseAudioQt::Sink::activePortIndexChanged, [=]() { - printf("Sink %p changed active port\n", sink); + QCoreApplication::connect(device, &PulseAudioQt::Sink::activePortIndexChanged, [device]() { + printf("Sink %p changed active port\n", device); queueGuiUpdate(); }); - QCoreApplication::connect(sink, &PulseAudioQt::Sink::defaultChanged, [=]() { - printf("Sink %p changed default\n", sink); + QCoreApplication::connect(device, &PulseAudioQt::Sink::defaultChanged, [device]() { + printf("Sink %p changed default\n", device); queueGuiUpdate(); }); - QCoreApplication::connect(sink, &PulseAudioQt::Sink::volumeChanged, [=]() { + QCoreApplication::connect(device, &PulseAudioQt::Sink::portsChanged, [device]() { + printf("Sink %p ports changed\n", device); + checkShouldSetDefaultPort(device); queueGuiUpdate(); }); - QCoreApplication::connect(sink, &PulseAudioQt::Sink::isVolumeWritableChanged, [=]() { + QCoreApplication::connect(device, &PulseAudioQt::Sink::volumeChanged, [device]() { + queueGuiUpdate(250); + }); + QCoreApplication::connect(device, &PulseAudioQt::Sink::isVolumeWritableChanged, [device]() { queueGuiUpdate(); }); + checkShouldSetDefaultPort(device); queueGuiUpdate(); } /** - * Mute/unmute sink by its string identifier. + * Mute/unmute device by its string identifier. */ -void setMuted(const QString &sink, bool muted) +void setMuted(const QString &deviceId, bool muted) { auto *i = PulseAudioQt::Context::instance(); for (auto *sp : i->sinks()) { - if (sp->name() == sink) { + if (sp->name() == deviceId) { + sp->setMuted(muted); + } + } + for (auto *sp : i->sources()) { + if (sp->name() == deviceId) { sp->setMuted(muted); } } @@ -220,14 +279,20 @@ void setMuted(const QString &sink, bool muted) } /** - * Set a sink's volume, by its string identifier. + * Set a device's volume, by its string identifier. */ -void setSinkVolume(const QString &sink, int volume) +void setSinkVolume(const QString &deviceId, int volume) { auto *i = PulseAudioQt::Context::instance(); for (auto *sp : i->sinks()) { - if (sp->name() == sink) { + if (sp->name() == deviceId) { + sp->setVolume(volume); + queueGuiUpdate(250); + } + } + for (auto *sp : i->sources()) { + if (sp->name() == deviceId) { sp->setVolume(volume); queueGuiUpdate(250); } @@ -240,14 +305,15 @@ void setSinkVolume(const QString &sink, int volume) * 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) +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; - _pendingCardDefault.clear(); + _pendingCardDefaultId.clear(); + _pendingCardDefaultPort.clear(); for (auto *cp : i->cards()) { cardIdx++; if (cp->name() != card) @@ -280,18 +346,28 @@ void enableCard(const QString &card, const QString &profile) } 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 { - matchingCard->setActiveProfileIndex(profileIdx); // 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(). - _pendingCardDefault = matchingCard->name(); + _pendingCardDefaultId = matchingCard->name(); + _pendingCardDefaultPort = port; + matchingCard->setActiveProfileIndex(profileIdx); } } queueGuiUpdate(); @@ -306,7 +382,8 @@ void enableSink(const QString &sink, const QString &port) { auto *i = PulseAudioQt::Context::instance(); - _pendingCardDefault.clear(); + _pendingCardDefaultId.clear(); + _pendingCardDefaultPort.clear(); for (auto *sp : i->sinks()) { if (sp->name() != sink) continue; @@ -323,6 +400,33 @@ void enableSink(const QString &sink, const QString &port) 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(); +} + int main(int argc, char **argv) { QApplication a(argc, argv); @@ -341,15 +445,26 @@ int main(int argc, char **argv) printf(" Profile: %s\n", profile->name().toLocal8Bit().constData()); } for (auto *port : card->ports()) { - printf(" Port: %s\n", port->name().toLocal8Bit().constData()); + 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()); - newSinkAppeared(sink); + 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, &newSinkAppeared); + QCoreApplication::connect(i, &PulseAudioQt::Context::sinkAdded, &newDeviceAppeared); + QCoreApplication::connect(i, &PulseAudioQt::Context::sourceAdded, &newDeviceAppeared); printf("Initial output\n"); queueGuiUpdate(); }); diff --git a/src/main.h b/src/main.h index 7402d6e..343f452 100644 --- a/src/main.h +++ b/src/main.h @@ -9,8 +9,10 @@ void setMuted(const QString &sink, bool muted); void setSinkVolume(const QString &sink, int volume); -void enableCard(const QString &card, const QString &profile); +void enableCard(const QString &card, const QString &profile, const QString &port); void enableSink(const QString &sink, const QString &port); +void enableSource(const QString &source, const QString &port); + #endif diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index b35a8f9..0fc6341 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -10,6 +10,8 @@ MainWindow::MainWindow() : QDialog() { setupUi(this); setWindowFlags(Qt::WindowStaysOnTopHint | windowFlags()); + outputTab->setLayout(new QVBoxLayout(outputTab)); + inputTab->setLayout(new QVBoxLayout(inputTab)); } MainWindow::~MainWindow() @@ -17,11 +19,11 @@ MainWindow::~MainWindow() } -SlxOutput* MainWindow::getDevice(const PulseAudioQt::Card *card, const PulseAudioQt::Sink *sink, const PulseAudioQt::Port *port) +SlxOutput* MainWindow::getDevice(const PulseAudioQt::Card *card, const PulseAudioQt::Device *device, const PulseAudioQt::Port *port, bool output) { PulseAudioQt::Profile *profile = nullptr; QString cardId; - QString id = sink->name() + port->name(); + QString id = device->name() + port->name(); SlxOutput* w = _widgets.value(id, nullptr); QString heading = port->description(); @@ -37,8 +39,8 @@ SlxOutput* MainWindow::getDevice(const PulseAudioQt::Card *card, const PulseAudi return w; } - w = new SlxOutput(SlxOutput::SinkPortItem, id, cardId, QLatin1String(), sink->name(), port->name(), - heading, sink->description()); + w = new SlxOutput(output ? SlxOutput::SinkPortItem : SlxOutput::SourcePortItem, id, cardId, QLatin1String(), device->name(), port->name(), + heading, device->description()); _widgets.insert(id, w); insertItemWidget(w, true); @@ -47,7 +49,6 @@ SlxOutput* MainWindow::getDevice(const PulseAudioQt::Card *card, const PulseAudi SlxOutput* MainWindow::getCardProfile(const PulseAudioQt::Card *card, const PulseAudioQt::Profile *profile) { - QString id = card->name() + profile->name(); SlxOutput* w = _widgets.value(id, nullptr); if (w != nullptr) @@ -61,22 +62,40 @@ SlxOutput* MainWindow::getCardProfile(const PulseAudioQt::Card *card, const Puls return w; } +SlxOutput* MainWindow::getCardPort(const PulseAudioQt::Card *card, const PulseAudioQt::Port *port) +{ + QString id = card->name() + port->name(); + SlxOutput* w = _widgets.value(id, nullptr); + if (w != nullptr) + return w; + + w = new SlxOutput(SlxOutput::CardProfileItem, id, card->name(), port->name(), QLatin1String(), QLatin1String(), + port->description(), card->description()); + _widgets.insert(id, w); + + insertItemWidget(w, false); + return w; +} + void MainWindow::insertItemWidget(SlxOutput* w, bool isDevice) { bool done = false; int idx; + QBoxLayout *container; + if (w->type() == SlxOutput::CardProfileItem) { + container = cards; + } else if (w->type() == SlxOutput::SinkPortItem) { + container = qobject_cast(outputTab->layout()); + } else { + container = qobject_cast(inputTab->layout()); + } for (idx = 0; idx < container->count(); ++idx) { auto *l = container->itemAt(idx); SlxOutput *other = qobject_cast(l->widget()); if (other == nullptr) continue; printf(" Comparing %s - %s\n", other->isDevice() ? "SINK" : "PROF", other->nameLabel->text().toLocal8Bit().constData()); - if (isDevice != other->isDevice()) { - if (isDevice) - break; - continue; - } if (w->compareTo(other) < 0) { done = true; printf("Inserting before that (%d)!\n", idx); @@ -98,6 +117,7 @@ void MainWindow::insertItemWidget(SlxOutput* w, bool isDevice) */ void MainWindow::mark() { + setUpdatesEnabled(false); for (auto *w : _widgets) { w->unused = true; } @@ -117,4 +137,5 @@ void MainWindow::sweep() w->deleteLater(); } } + setUpdatesEnabled(true); } diff --git a/src/mainwindow.h b/src/mainwindow.h index 6fbe9f2..5eede3f 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -8,9 +8,8 @@ class SlxOutput; namespace PulseAudioQt { -class Sink; +class Device; class Port; -class Source; class Card; class Profile; } @@ -23,8 +22,9 @@ public: MainWindow(); virtual ~MainWindow(); - SlxOutput* getDevice(const PulseAudioQt::Card *card, const PulseAudioQt::Sink *sink, const PulseAudioQt::Port *port); + SlxOutput* getDevice(const PulseAudioQt::Card *card, const PulseAudioQt::Device *device, const PulseAudioQt::Port *port, bool output); SlxOutput* getCardProfile(const PulseAudioQt::Card *card, const PulseAudioQt::Profile *profile); + SlxOutput* getCardPort(const PulseAudioQt::Card *card, const PulseAudioQt::Port *port); void insertItemWidget(SlxOutput* w, bool isDevice); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 80c81d9..af88c18 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -15,7 +15,24 @@ - + + + 0 + + + + Output + + + + + Input + + + + + + diff --git a/src/slxoutput.cpp b/src/slxoutput.cpp index 5bc319b..b1da26f 100644 --- a/src/slxoutput.cpp +++ b/src/slxoutput.cpp @@ -56,9 +56,11 @@ SlxOutput::SlxOutput(ItemType type, const QString &id, defaultToggleButton->setChecked(true); } if (_type == CardProfileItem) { - enableCard(_cardId, _profileId); - } else { + enableCard(_cardId, _profileId, _portId); + } else if (_type == SinkPortItem) { enableSink(_deviceId, _portId); + } else { + enableSource(_deviceId, _portId); } }); } diff --git a/src/slxoutput.h b/src/slxoutput.h index c175899..4df64fa 100644 --- a/src/slxoutput.h +++ b/src/slxoutput.h @@ -28,7 +28,8 @@ public: void updateCardAndProfile(); const QString &deviceId() const { return _deviceId; } - const bool isDevice() const { return !_deviceId.isEmpty(); } + bool isDevice() const { return !_deviceId.isEmpty(); } + ItemType type() const { return _type; } int compareTo(SlxOutput* other) const { -- cgit v1.2.3-55-g7522