#include "main.h"
#include "mainwindow.h"
#include "slxoutput.h"
#include "setdefault.h"
#include <cstdio>
#include <cstring>
#include <cstddef>
#include <QVector>
#include <QGuiApplication>
#include <QTimer>
#include <PulseAudioQt/Context>
#include <PulseAudioQt/Server>
#include <PulseAudioQt/Profile>
#include <PulseAudioQt/PulseObject>
#include <PulseAudioQt/SinkInput>
#include <PulseAudioQt/Sink>
#include <PulseAudioQt/Card>
#include <PulseAudioQt/Port>
#include <QList>
#include <QSet>
#include <QTimer>
#include <QCommandLineParser>
// 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<QString> _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<QString> 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();
}