summaryrefslogblamecommitdiffstats
path: root/src/main.cpp
blob: adf515be54f012bd4a9b3062ff76fd40af79a715 (plain) (tree)
































                                                                                              





                                                                                                    

                                


                                                                                               



                               



                                                                                              




                                       





                                                                                                      

                                

                                                                     
                                                    
                                
 

                                                                   



                                                           
                                   



                                                                                      

                                                                                      
                                                       
                                                                              
                                                                  
                                                                                                                                
                                                                      
                                                                 
                                                                               
                                                                             



                                                                

                                 

                                                                                                                                                                                                                  

                 
                                                                                                                 
                                  


                                                        
                                                                                       

                                                                                                                                     
                                                                                                    



                                                                                                       
                                                                                           


                                                                    

                                                                                                                                          

 




                                                                                                                

                                   
                                                                        
                       
                                                                                                               


                                                                                              
                                              




                                                                                    
                                                                                 


                                                   
                                                                               






                                                    


                                                                                           
























                                                                                               


                                                                                            





















                                                                                             


                                             











                                                    


                                                 











                                                    





                                                                       















































                                                                                                               


                                                                                                                





                                                                   




                                                               




























                                                          

                                                                             






                                                                                                                     


                                                                                               



















                                                                                                                            
#include "main.h"
#include "mainwindow.h"
#include "slxoutput.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>
// 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 belings to this card, make its sink default */
static QString _pendingCardDefault;

/** 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
 *  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);
}

/**
 * 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");
	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++;
			//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);
		}
	}
	// 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) {
		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)
				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 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()
{
	if (_pendingCardDefault.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());
	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())
			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);
			_pendingCardDefault.clear();
		}
	}
}

/**
 * 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);
		checkShouldSetDefault();
		queueGuiUpdate();
	});
	QCoreApplication::connect(card, &PulseAudioQt::Card::portsChanged, [=]() {
		printf("Card %p ports changed\n", card);
		checkShouldSetDefault();
		queueGuiUpdate();
	});
	/*
	QCoreApplication::connect(card, &PulseAudioQt::Card::sinksChanged, [=]() {
		printf("Card %p sinks changed\n", card);
	});
	*/
}

/**
 * Called when PA tells us about a new sink; start listening to signals we're interested in.
 */
static void newSinkAppeared(PulseAudioQt::Sink *sink)
{
	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);
		queueGuiUpdate();
	});
	QCoreApplication::connect(sink, &PulseAudioQt::Sink::defaultChanged, [=]() {
		printf("Sink %p changed default\n", sink);
		queueGuiUpdate();
	});
	QCoreApplication::connect(sink, &PulseAudioQt::Sink::volumeChanged, [=]() {
		queueGuiUpdate();
	});
	QCoreApplication::connect(sink, &PulseAudioQt::Sink::isVolumeWritableChanged, [=]() {
		queueGuiUpdate();
	});
	queueGuiUpdate();
}

/**
 * Mute/unmute sink by its string identifier.
 */
void setMuted(const QString &sink, bool muted)
{
	auto *i = PulseAudioQt::Context::instance();

	for (auto *sp : i->sinks()) {
		if (sp->name() == sink) {
			sp->setMuted(muted);
		}
	}
	queueGuiUpdate();
}

/**
 * Set a sink's volume, by its string identifier.
 */
void setSinkVolume(const QString &sink, int volume)
{
	auto *i = PulseAudioQt::Context::instance();

	for (auto *sp : i->sinks()) {
		if (sp->name() == sink) {
			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)
{
	auto *i = PulseAudioQt::Context::instance();
	PulseAudioQt::Card *matchingCard = nullptr;
	int profileIdx = -1;
	int cardIdx = -1;

	_pendingCardDefault.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) {
			for (auto *sink : i->sinks()) {
				if (sink->cardIndex() == cardIdx) {
					sink->setMuted(false);
					sink->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();
		}
	}
	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();

	_pendingCardDefault.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();
}

int main(int argc, char **argv)
{
	QApplication a(argc, argv);
	printf("Muh\n");
	auto *i = PulseAudioQt::Context::instance();
	printf("Pa is %p = %d\n", i, (int)i->isValid());
	printf("Cards: %d\n", (int)i->cards().size());

	// 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\n", port->name().toLocal8Bit().constData());
			}
		}
		for (auto *sink : i->sinks()) {
			printf("Sink: %s (for card index %d)\n", sink->name().toLocal8Bit().constData(), sink->cardIndex());
			newSinkAppeared(sink);
		}
		QCoreApplication::connect(i, &PulseAudioQt::Context::cardAdded, &newCardAppeared);
		QCoreApplication::connect(i, &PulseAudioQt::Context::sinkAdded, &newSinkAppeared);
		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();
}