2019-09-18 19:56:50 +02:00
|
|
|
|
2019-09-15 01:44:12 +02:00
|
|
|
#include "mainwindow.h"
|
|
|
|
#include "./ui_mainwindow.h"
|
|
|
|
|
|
|
|
/*
|
|
|
|
*
|
|
|
|
* TODO: Find another way how to play it for myself and others (maybe just loopback the default output to the sink monitor)
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
static vector<PulseAudioRecordingStream *> streams;
|
|
|
|
|
2019-10-06 01:28:46 +02:00
|
|
|
static string configFolder;
|
|
|
|
static string soundFilesConfig;
|
|
|
|
|
2019-09-15 01:44:12 +02:00
|
|
|
MainWindow::MainWindow(QWidget *parent)
|
|
|
|
: QMainWindow(parent)
|
|
|
|
, ui(new Ui::MainWindow)
|
|
|
|
{
|
|
|
|
ui->setupUi(this);
|
|
|
|
|
2019-10-06 01:28:46 +02:00
|
|
|
configFolder = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)[0].toStdString() + "/" + windowTitle().toStdString();
|
|
|
|
if(!filesystem::exists(configFolder)) {
|
|
|
|
filesystem::create_directory(configFolder);
|
|
|
|
}
|
|
|
|
soundFilesConfig = configFolder + "/soundFiles.json";
|
|
|
|
cout << soundFilesConfig << endl;
|
|
|
|
|
2019-09-15 01:44:12 +02:00
|
|
|
// Disable resizing
|
|
|
|
this->setFixedSize(this->width(), this->height());
|
|
|
|
|
|
|
|
//TODO: Only remove modules created by Soundboard
|
|
|
|
system("pacmd unload-module module-null-sink");
|
|
|
|
system("pacmd unload-module module-loopback");
|
|
|
|
|
|
|
|
// Create null sink
|
|
|
|
system("pacmd load-module module-null-sink sink_name=soundboard_sink sink_properties=device.description=Soundboard-Sink");
|
|
|
|
|
|
|
|
// Create loopback for output devices (so that you can hear it)
|
|
|
|
//system("pacmd load-module module-loopback source=\"soundboard_sink.monitor\"");
|
|
|
|
|
2019-09-15 02:50:45 +02:00
|
|
|
// get default input device
|
|
|
|
string defaultInput = "";
|
|
|
|
char cmd[] = "pacmd dump";
|
|
|
|
string result = getCommandOutput(cmd);
|
|
|
|
regex reg(R"rgx(set-default-source (.+))rgx");
|
|
|
|
smatch sm;
|
|
|
|
regex_search(result, sm, reg);
|
|
|
|
defaultInput = sm[1].str();
|
|
|
|
|
2019-09-15 01:44:12 +02:00
|
|
|
// Create loopback for input
|
2019-09-15 02:50:45 +02:00
|
|
|
if(defaultInput != "") {
|
2019-10-06 01:28:46 +02:00
|
|
|
cout << "Found default input device " << defaultInput << endl;
|
2019-09-15 02:50:45 +02:00
|
|
|
auto createLoopBack = "pacmd load-module module-loopback source=\"" + defaultInput + "\" sink=\"soundboard_sink\"";
|
|
|
|
system(createLoopBack.c_str());
|
|
|
|
}
|
2019-09-15 01:44:12 +02:00
|
|
|
|
|
|
|
loadSoundFiles();
|
|
|
|
loadSources();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::closeEvent(QCloseEvent *event)
|
|
|
|
{
|
|
|
|
//TODO: Only remove modules created by Soundboard
|
|
|
|
system("pacmd unload-module module-null-sink");
|
|
|
|
system("pacmd unload-module module-loopback");
|
2019-10-06 01:32:51 +02:00
|
|
|
//TODO: Switch all recording streams back to default device
|
2019-09-15 01:44:12 +02:00
|
|
|
event->accept();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-09-15 02:50:45 +02:00
|
|
|
string MainWindow::getCommandOutput(char cmd[]) {
|
2019-09-15 01:44:12 +02:00
|
|
|
array<char, 1028> buffer;
|
|
|
|
string result;
|
|
|
|
unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
|
|
|
|
if (!pipe) {
|
|
|
|
throw runtime_error("popen() failed!");
|
|
|
|
}
|
|
|
|
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
|
|
|
|
result += buffer.data();
|
|
|
|
}
|
2019-09-15 02:50:45 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool MainWindow::isValidDevice(PulseAudioRecordingStream* stream) {
|
|
|
|
return !strstr(stream->source.c_str(), ".monitor") && !strstr(stream->flags.c_str(), "DONT_MOVE");
|
|
|
|
}
|
|
|
|
|
|
|
|
bool MainWindow::loadSources() {
|
|
|
|
// Save previously selected applicaton
|
|
|
|
auto previouslySelected = ui->outputApplication->currentText();
|
|
|
|
|
|
|
|
streams.clear();
|
|
|
|
ui->outputApplication->clear();
|
|
|
|
|
|
|
|
char cmd[] = "pacmd list-source-outputs";
|
|
|
|
string result = getCommandOutput(cmd);
|
2019-09-15 01:44:12 +02:00
|
|
|
string delimiter = "\n";
|
|
|
|
size_t pos = 0;
|
|
|
|
string currentLine;
|
|
|
|
|
2019-09-18 19:51:12 +02:00
|
|
|
// Tell me if there is a better way to parse the pulseaudio source outputs
|
2019-09-15 01:44:12 +02:00
|
|
|
regex reg(R"rgx(((index: (\d+)))|(driver: )(.*)|(state: )(.*)|(flags: )(.*)|(source: .*)(<(.*)>)|(muted: )(.{0,3})|([a-zA-Z-.0-9_]*)\ =\ (\"(.*)\"))rgx");
|
|
|
|
smatch sm;
|
|
|
|
|
|
|
|
PulseAudioRecordingStream* current = nullptr;
|
|
|
|
|
|
|
|
while ((pos = result.find(delimiter)) != string::npos) {
|
|
|
|
currentLine = result.substr(0, pos);
|
|
|
|
if(regex_search(currentLine, sm, reg)) {
|
|
|
|
auto index = sm[3];
|
|
|
|
if(index.length() > 0) {
|
|
|
|
|
|
|
|
if(current && isValidDevice(current)) {
|
|
|
|
streams.push_back(current);
|
|
|
|
}
|
|
|
|
|
|
|
|
current = new PulseAudioRecordingStream();
|
|
|
|
current->index = stoi(index);
|
|
|
|
} else if(current) {
|
|
|
|
|
|
|
|
auto driver = sm[5];
|
|
|
|
auto state = sm[7];
|
|
|
|
auto flags = sm[9];
|
|
|
|
auto source = sm[12];
|
|
|
|
auto muted = sm[14];
|
|
|
|
auto propertyName = sm[15];
|
|
|
|
auto propertyValue = sm[17];
|
|
|
|
|
|
|
|
if(driver.length() > 0) {
|
|
|
|
current->driver = driver.str();
|
|
|
|
}
|
|
|
|
if(state.length() > 0) {
|
|
|
|
current->state = state.str();
|
|
|
|
}
|
|
|
|
if(flags.length() > 0) {
|
|
|
|
current->flags = flags.str();
|
|
|
|
}
|
|
|
|
if(source.length() > 0) {
|
|
|
|
current->source = source.str();
|
|
|
|
}
|
|
|
|
if(muted.length() > 0) {
|
|
|
|
current->muted = muted == "yes" ? true : false;
|
|
|
|
}
|
|
|
|
if(propertyName.length() > 0) {
|
|
|
|
if(propertyName == "application.name") {
|
|
|
|
current->applicationName = propertyValue.str();
|
|
|
|
}
|
|
|
|
if(propertyName == "application.process.id") {
|
|
|
|
current->processId = stoi(propertyValue);
|
|
|
|
}
|
|
|
|
if(propertyName == "application.process.binary") {
|
|
|
|
current->processBinary = propertyValue.str();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
result.erase(0, pos + delimiter.length());
|
|
|
|
}
|
|
|
|
if(isValidDevice(current)) {
|
|
|
|
streams.push_back(current);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for(auto stream : streams) {
|
|
|
|
if(stream->driver == "<protocol-native.c>") {
|
|
|
|
ui->outputApplication->addItem(QString(stream->processBinary.c_str()));
|
|
|
|
}
|
|
|
|
}
|
2019-09-15 02:50:45 +02:00
|
|
|
|
|
|
|
// This automatically sets the selected item to the previous one. if it does not exists it does nothing
|
|
|
|
ui->outputApplication->setCurrentText(previouslySelected);
|
|
|
|
|
|
|
|
// Return if the output was not changed
|
|
|
|
return ui->outputApplication->currentText() == previouslySelected;
|
2019-09-15 01:44:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
MainWindow::~MainWindow()
|
|
|
|
{
|
|
|
|
delete ui;
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::playSound(string path) {
|
|
|
|
//TODO: Stop old playback
|
|
|
|
|
2019-09-15 02:50:45 +02:00
|
|
|
// Don't play the sound if the app changed (previous one no longer available)
|
|
|
|
if(!loadSources()) {
|
|
|
|
QMessageBox::warning(this, "", tr("Output stream no longer available...\nAborting\n"), QMessageBox::Ok);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-09-15 01:44:12 +02:00
|
|
|
// Get selected application
|
|
|
|
string selectedApp = ui->outputApplication->currentText().toStdString();
|
|
|
|
PulseAudioRecordingStream* selected = nullptr;
|
|
|
|
for(auto stream : streams) {
|
|
|
|
if(stream->processBinary == selectedApp) {
|
|
|
|
selected = stream;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(selected) {
|
|
|
|
int index = selected->index;
|
|
|
|
string source = selected->source;
|
|
|
|
|
2019-10-06 01:28:46 +02:00
|
|
|
cout << "Source before was " << source << endl;
|
|
|
|
|
2019-09-15 01:44:12 +02:00
|
|
|
auto moveToSink = "pacmd move-source-output " + to_string(index) + " soundboard_sink.monitor";
|
|
|
|
auto moveBack = "pacmd move-source-output " + to_string(index) + " " + source;
|
|
|
|
|
|
|
|
// Switch recording stream device to game sink
|
|
|
|
system(moveToSink.c_str());
|
|
|
|
|
|
|
|
// Set volume for game sink monitor from slider
|
|
|
|
int value = ui->volumeSlider->value();
|
|
|
|
system(("pacmd set-source-volume soundboard_sink.monitor " + to_string(value)).c_str());
|
|
|
|
|
2019-10-06 15:52:55 +02:00
|
|
|
try {
|
|
|
|
forMe.join();
|
|
|
|
forOthers.join();
|
|
|
|
} catch(...) {}
|
|
|
|
|
2019-10-06 01:28:46 +02:00
|
|
|
forMe = std::thread([=]()
|
2019-09-15 01:44:12 +02:00
|
|
|
{
|
2019-10-06 01:28:46 +02:00
|
|
|
auto cmdForMe = "mpg123 -o pulse \"" + path + "\"";
|
|
|
|
system(cmdForMe.c_str());
|
2019-09-15 01:44:12 +02:00
|
|
|
});
|
|
|
|
|
2019-10-06 01:28:46 +02:00
|
|
|
forOthers = std::thread([=]()
|
|
|
|
{
|
|
|
|
auto cmdForOthers = "mpg123 -o pulse -a soundboard_sink \"" + path + "\"";
|
|
|
|
system(cmdForOthers.c_str());
|
|
|
|
// Switch recording stream device back
|
|
|
|
system(moveBack.c_str());
|
|
|
|
});
|
2019-09-15 01:44:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-18 19:51:12 +02:00
|
|
|
|
2019-09-15 01:44:12 +02:00
|
|
|
void MainWindow::on_refreshAppsButton_clicked()
|
|
|
|
{
|
|
|
|
loadSources();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::on_playCustomButton_clicked()
|
|
|
|
{
|
|
|
|
playSound(ui->customAudioText->toPlainText().toStdString());
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::on_customFileChoose_clicked()
|
|
|
|
{
|
|
|
|
QString selectedFile = QFileDialog::getOpenFileName(this, tr("Select file"), QDir::homePath(), tr("MP3 (*.mp3)"));
|
|
|
|
QFile file(selectedFile);
|
|
|
|
QFileInfo fileInfo((QFileInfo(file)));
|
|
|
|
ui->customAudioText->setText(fileInfo.absoluteFilePath());
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::on_stopButton_clicked()
|
|
|
|
{
|
2019-10-06 01:28:46 +02:00
|
|
|
//TODO: Only kill mpg123 started from Soundboard
|
|
|
|
system("killall mpg123");
|
|
|
|
//pthread_kill(forMe.native_handle(), 9);
|
|
|
|
//pthread_kill(forOthers.native_handle(), 9);
|
2019-09-15 01:44:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::on_addSoundButton_clicked()
|
|
|
|
{
|
2019-10-06 23:25:17 +02:00
|
|
|
QStringList selectedFiles = QFileDialog::getOpenFileNames(this, tr("Select file"), QDir::homePath(), tr("MP3 (*.mp3)"));
|
|
|
|
for(auto selectedFile : selectedFiles) {
|
|
|
|
if (selectedFile != "") {
|
|
|
|
QFile file(selectedFile);
|
|
|
|
QFileInfo fileInfo((QFileInfo(file)));
|
|
|
|
|
|
|
|
auto path = fileInfo.absoluteFilePath().toStdString();
|
|
|
|
|
|
|
|
for (QListWidgetItem* item : ui->soundsListWidget->findItems("*", Qt::MatchWildcard)) {
|
|
|
|
// Check if Sound is already added
|
|
|
|
if (path == item->toolTip().toStdString()) {
|
|
|
|
QMessageBox::warning(this, "", tr("This sound is already in the list"), QMessageBox::Ok);
|
|
|
|
return;
|
|
|
|
}
|
2019-09-15 01:44:12 +02:00
|
|
|
}
|
|
|
|
|
2019-10-06 23:25:17 +02:00
|
|
|
auto item = new QListWidgetItem();
|
|
|
|
item->setText(fileInfo.baseName());
|
|
|
|
item->setToolTip(fileInfo.absoluteFilePath());
|
|
|
|
ui->soundsListWidget->addItem(item);
|
2019-09-15 01:44:12 +02:00
|
|
|
|
2019-10-06 23:25:17 +02:00
|
|
|
saveSoundFiles();
|
|
|
|
}
|
2019-09-15 01:44:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::on_removeSoundButton_clicked()
|
|
|
|
{
|
|
|
|
QListWidgetItem *it = ui->soundsListWidget->takeItem(ui->soundsListWidget->currentRow());
|
|
|
|
if (it) {
|
|
|
|
delete it;
|
|
|
|
}
|
|
|
|
|
|
|
|
saveSoundFiles();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::on_clearSoundsButton_clicked()
|
|
|
|
{
|
2019-09-15 02:50:45 +02:00
|
|
|
QMessageBox::StandardButton resBtn = QMessageBox::question(this, "", tr("Are you sure?\n"), QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes);
|
|
|
|
if (resBtn == QMessageBox::Yes) {
|
|
|
|
clearSoundFiles();
|
|
|
|
saveSoundFiles();
|
|
|
|
}
|
2019-09-15 01:44:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::on_playSoundButton_clicked()
|
|
|
|
{
|
|
|
|
QListWidgetItem *it = ui->soundsListWidget->item(ui->soundsListWidget->currentRow());
|
|
|
|
if (it) {
|
|
|
|
playSound(it->toolTip().toStdString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::clearSoundFiles() {
|
|
|
|
while(ui->soundsListWidget->count()>0)
|
|
|
|
{
|
|
|
|
ui->soundsListWidget->takeItem(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::saveSoundFiles() {
|
|
|
|
json jsonArray = json::array();
|
|
|
|
for (QListWidgetItem* item : ui->soundsListWidget->findItems("*", Qt::MatchWildcard)) {
|
|
|
|
json j;
|
|
|
|
j[item->text().toStdString()] = item->toolTip().toStdString();
|
|
|
|
jsonArray.push_back(j);
|
|
|
|
}
|
|
|
|
|
|
|
|
ofstream myfile;
|
2019-10-06 01:28:46 +02:00
|
|
|
myfile.open(soundFilesConfig);
|
2019-09-15 01:44:12 +02:00
|
|
|
myfile << jsonArray.dump();
|
|
|
|
myfile.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
void MainWindow::loadSoundFiles() {
|
2019-10-06 01:28:46 +02:00
|
|
|
ifstream fileIn(soundFilesConfig);
|
2019-09-15 01:44:12 +02:00
|
|
|
if(fileIn.is_open()) {
|
|
|
|
string content((istreambuf_iterator<char>(fileIn)), istreambuf_iterator<char>());
|
|
|
|
|
|
|
|
json j = json::parse(content);
|
|
|
|
|
|
|
|
clearSoundFiles();
|
|
|
|
|
|
|
|
for(auto json : j) {
|
|
|
|
for (json::iterator it = json.begin(); it != json.end(); ++it) {
|
|
|
|
auto name = it.key();
|
|
|
|
auto path = it.value();
|
|
|
|
remove(path.begin(), path.end(), '"');
|
|
|
|
|
|
|
|
auto item = new QListWidgetItem();
|
|
|
|
item->setText(QString::fromStdString(name));
|
|
|
|
item->setToolTip(QString::fromStdString(path));
|
|
|
|
ui->soundsListWidget->addItem(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fileIn.close();
|
|
|
|
}
|
|
|
|
}
|