Soundux/mainwindow.cpp

610 lines
18 KiB
C++
Raw Normal View History

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;
static string configFolder;
static string soundFilesConfig;
2019-09-15 01:44:12 +02:00
MainWindow::MainWindow(QWidget *parent)
2019-10-08 00:31:44 +02:00
: QMainWindow(parent), ui(new Ui::MainWindow)
2019-09-15 01:44:12 +02:00
{
ui->setupUi(this);
2019-10-08 00:31:44 +02:00
ui->tabWidget->setTabsClosable(true);
ui->tabWidget->setMovable(true);
ui->stopButton->setDisabled(true);
2019-09-15 01:44:12 +02:00
// Set the config variables
configFolder = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation)[0].toStdString() + "/" + windowTitle().toStdString();
2019-10-08 00:31:44 +02:00
if (!filesystem::exists(configFolder))
{
filesystem::create_directory(configFolder);
}
2019-10-08 00:31:44 +02:00
soundFilesConfig = configFolder + "/sounds.json";
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\"");
// 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-10-08 00:31:44 +02:00
if (defaultInput != "")
{
cout << "Found default input device " << defaultInput << endl;
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");
//TODO: Switch all recording streams back to default device
2019-09-15 01:44:12 +02:00
event->accept();
}
2019-10-08 00:31:44 +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);
2019-10-08 00:31:44 +02:00
if (!pipe)
{
2019-09-15 01:44:12 +02:00
throw runtime_error("popen() failed!");
}
2019-10-08 00:31:44 +02:00
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
{
2019-09-15 01:44:12 +02:00
result += buffer.data();
}
return result;
}
2019-10-08 00:31:44 +02:00
bool MainWindow::isValidDevice(PulseAudioRecordingStream *stream)
{
return !strstr(stream->source.c_str(), ".monitor") && !strstr(stream->flags.c_str(), "DONT_MOVE");
}
2019-10-08 00:31:44 +02:00
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;
// Tell me if there is a better way to parse the pulseaudio output
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;
2019-10-08 00:31:44 +02:00
PulseAudioRecordingStream *current = nullptr;
2019-09-15 01:44:12 +02:00
2019-10-08 00:31:44 +02:00
while ((pos = result.find(delimiter)) != string::npos)
{
2019-09-15 01:44:12 +02:00
currentLine = result.substr(0, pos);
2019-10-08 00:31:44 +02:00
if (regex_search(currentLine, sm, reg))
{
2019-09-15 01:44:12 +02:00
auto index = sm[3];
2019-10-08 00:31:44 +02:00
if (index.length() > 0)
{
2019-09-15 01:44:12 +02:00
2019-10-08 00:31:44 +02:00
if (current && isValidDevice(current))
{
2019-09-15 01:44:12 +02:00
streams.push_back(current);
}
current = new PulseAudioRecordingStream();
current->index = stoi(index);
2019-10-08 00:31:44 +02:00
}
else if (current)
{
2019-09-15 01:44:12 +02:00
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];
2019-10-08 00:31:44 +02:00
if (driver.length() > 0)
{
2019-09-15 01:44:12 +02:00
current->driver = driver.str();
}
2019-10-08 00:31:44 +02:00
if (state.length() > 0)
{
2019-09-15 01:44:12 +02:00
current->state = state.str();
}
2019-10-08 00:31:44 +02:00
if (flags.length() > 0)
{
2019-09-15 01:44:12 +02:00
current->flags = flags.str();
}
2019-10-08 00:31:44 +02:00
if (source.length() > 0)
{
2019-09-15 01:44:12 +02:00
current->source = source.str();
}
2019-10-08 00:31:44 +02:00
if (muted.length() > 0)
{
2019-09-15 01:44:12 +02:00
current->muted = muted == "yes" ? true : false;
}
2019-10-08 00:31:44 +02:00
if (propertyName.length() > 0)
{
if (propertyName == "application.name")
{
2019-09-15 01:44:12 +02:00
current->applicationName = propertyValue.str();
}
2019-10-08 00:31:44 +02:00
if (propertyName == "application.process.id")
{
2019-09-15 01:44:12 +02:00
current->processId = stoi(propertyValue);
}
2019-10-08 00:31:44 +02:00
if (propertyName == "application.process.binary")
{
2019-09-15 01:44:12 +02:00
current->processBinary = propertyValue.str();
}
}
}
}
result.erase(0, pos + delimiter.length());
}
2019-10-08 00:31:44 +02:00
if (isValidDevice(current))
{
2019-09-15 01:44:12 +02:00
streams.push_back(current);
}
2019-10-08 00:31:44 +02:00
for (auto stream : streams)
{
if (stream->driver == "<protocol-native.c>")
{
2019-09-15 01:44:12 +02:00
ui->outputApplication->addItem(QString(stream->processBinary.c_str()));
}
}
// 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;
}
2019-10-08 00:31:44 +02:00
void MainWindow::playSound(string path)
{
//TODO: Remove this and stop old playback or enable multiple sounds at once (maybe create a setting for it)
2019-10-09 22:14:11 +02:00
if (ui->stopButton->isEnabled())
{
2019-10-08 00:31:44 +02:00
return;
}
2019-09-15 01:44:12 +02:00
// Don't play the sound if the app changed (previous one no longer available)
2019-10-08 00:31:44 +02:00
if (!loadSources())
{
QMessageBox::warning(this, "", tr("Output stream no longer available...\nAborting\n"), QMessageBox::Ok);
return;
}
bool isMP3 = strstr(path.c_str(), ".mp3");
if (isMP3)
{
ostringstream mpg123Check;
mpg123Check << "which mpg123 >/dev/null 2>&1";
bool canPlayMP3 = (system(mpg123Check.str().c_str()) == 0);
if (!canPlayMP3)
{
QMessageBox::critical(this, "", tr("Can't play mp3 file!\nmpg123 is not installed\nPlease install it and restart the program\n"), QMessageBox::Ok);
return;
}
}
2019-09-15 01:44:12 +02:00
// Get selected application
string selectedApp = ui->outputApplication->currentText().toStdString();
2019-10-08 00:31:44 +02:00
PulseAudioRecordingStream *selected = nullptr;
for (auto stream : streams)
{
if (stream->processBinary == selectedApp)
{
2019-09-15 01:44:12 +02:00
selected = stream;
}
}
2019-10-08 00:31:44 +02:00
if (selected)
{
2019-09-15 01:44:12 +02:00
int index = selected->index;
string source = selected->source;
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;
2019-10-08 00:31:44 +02:00
// Switch recording stream device to game sink
2019-09-15 01:44:12 +02:00
system(moveToSink.c_str());
auto forMe = std::thread([=]() {
auto cmdForMe = "paplay \"" + path + "\"";
if (isMP3)
{
cmdForMe = "mpg123 -o pulse \"" + path + "\"";
}
system(cmdForMe.c_str());
2019-09-15 01:44:12 +02:00
});
forMe.detach();
2019-09-15 01:44:12 +02:00
auto forOthers = std::thread([=]() {
2019-10-08 00:31:44 +02:00
ui->stopButton->setDisabled(false);
auto cmdForOthers = "paplay -d soundboard_sink \"" + path + "\"";
if (isMP3)
{
cmdForOthers = "mpg123 -o pulse -a soundboard_sink \"" + path + "\"";
}
system(cmdForOthers.c_str());
// Switch recording stream device back
system(moveBack.c_str());
2019-10-08 00:31:44 +02:00
ui->stopButton->setDisabled(true);
// Repeat when the check box is checked
if (ui->repeatCheckBox->isChecked())
{
playSound(path);
}
});
forOthers.detach();
2019-09-15 01:44:12 +02:00
}
}
void MainWindow::checkAndChangeVolume(PulseAudioPlaybackStream *stream, int value)
{
// TODO: Only set it when this was created by Soundboard
// Set the volume if the application is paplay or mpg123
if (stream->applicationName == "paplay" || stream->applicationName == "mpg123")
{
system(("pacmd set-sink-input-volume " + to_string(stream->index) + " " + to_string(value)).c_str());
}
}
void MainWindow::syncVolume()
{
// Get volume from slider
int value = ui->volumeSlider->value();
char cmd[] = "pacmd list-sink-inputs";
string result = getCommandOutput(cmd);
string delimiter = "\n";
size_t pos = 0;
string currentLine;
// Tell me if there is a better way to parse the pulseaudio output
regex reg(R"rgx(((index: (\d+)))|(driver: )(.*)|(state: )(.*)|(flags: )(.*)|(source: .*)(<(.*)>)|(muted: )(.{0,3})|([a-zA-Z-.0-9_]*)\ =\ (\"(.*)\"))rgx");
smatch sm;
PulseAudioPlaybackStream *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)
{
checkAndChangeVolume(current, value);
}
current = new PulseAudioPlaybackStream();
current->index = stoi(index);
}
else
{
auto propertyName = sm[15];
auto propertyValue = sm[17];
if (propertyName.length() > 0)
{
if (propertyName == "application.name")
{
current->applicationName = propertyValue.str();
}
}
}
}
result.erase(0, pos + delimiter.length());
}
checkAndChangeVolume(current, value);
}
// Sync volume when the slider value has changed
void MainWindow::on_volumeSlider_valueChanged(int value)
{
syncVolume();
}
2019-09-15 01:44:12 +02:00
void MainWindow::on_refreshAppsButton_clicked()
{
loadSources();
}
void MainWindow::on_stopButton_clicked()
{
// Fix continuous playback
if (ui->repeatCheckBox->isChecked())
{
ui->repeatCheckBox->setChecked(false);
}
//TODO: Only kill players started from Soundboard
system("killall mpg123");
system("killall paplay");
2019-10-08 00:31:44 +02:00
ui->stopButton->setDisabled(true);
2019-09-15 01:44:12 +02:00
}
2019-10-09 22:14:11 +02:00
void MainWindow::on_addFolderButton_clicked()
{
auto selectedFolder = QFileDialog::getExistingDirectory(this, ("Select folder"), QDir::homePath());
if (selectedFolder != "")
{
QDir directory(selectedFolder);
QFileInfo fileInfo(selectedFolder);
auto created = createTab(fileInfo.fileName());
QStringList files = directory.entryList({"*.mp3", "*.wav", "*.ogg"}, QDir::Files);
2019-10-09 22:14:11 +02:00
for (auto fileName : files)
{
QFile file(directory.absoluteFilePath(fileName));
addSoundToView(file, created);
}
saveSoundFiles();
}
}
void MainWindow::addSoundToView(QFile &file, QListWidget *widget)
{
QFileInfo fileInfo(file);
auto path = fileInfo.absoluteFilePath().toStdString();
for (QListWidgetItem *item : widget->findItems("*", Qt::MatchWildcard))
{
// Check if Sound is already added
if (path == item->toolTip().toStdString())
{
auto already = "The sound " + item->text().toStdString() + " is already in the list";
QMessageBox::warning(this, "", tr(already.c_str()), QMessageBox::Ok);
2019-10-09 22:14:11 +02:00
return;
}
}
auto item = new QListWidgetItem();
item->setText(fileInfo.baseName());
item->setToolTip(fileInfo.absoluteFilePath());
widget->addItem(item);
}
2019-09-15 01:44:12 +02:00
void MainWindow::on_addSoundButton_clicked()
{
2019-10-08 00:31:44 +02:00
if (!getActiveView())
{
createTab("Main");
}
QStringList selectedFiles = QFileDialog::getOpenFileNames(this, tr("Select file"), QDir::homePath(), tr("Sound files (*.mp3 *.wav *.ogg)"));
2019-10-08 00:31:44 +02:00
for (auto selectedFile : selectedFiles)
{
if (selectedFile != "")
{
2019-10-06 23:25:17 +02:00
QFile file(selectedFile);
2019-10-09 22:14:11 +02:00
addSoundToView(file, getActiveView());
2019-10-06 23:25:17 +02:00
}
2019-09-15 01:44:12 +02:00
}
2019-10-09 22:14:11 +02:00
saveSoundFiles();
2019-09-15 01:44:12 +02:00
}
2019-10-08 00:31:44 +02:00
void MainWindow::on_soundsListWidget_itemDoubleClicked(QListWidgetItem *listWidgetItem)
{
if (listWidgetItem)
{
playSound(listWidgetItem->toolTip().toStdString());
}
}
2019-09-15 01:44:12 +02:00
void MainWindow::on_removeSoundButton_clicked()
{
2019-10-08 00:31:44 +02:00
if (getActiveView())
{
QListWidgetItem *it = getActiveView()->takeItem(getActiveView()->currentRow());
if (it)
{
delete it;
2019-10-09 22:14:11 +02:00
saveSoundFiles();
2019-10-08 00:31:44 +02:00
}
}
2019-09-15 01:44:12 +02:00
}
void MainWindow::on_clearSoundsButton_clicked()
{
2019-10-08 00:31:44 +02:00
if (getActiveView())
{
QMessageBox::StandardButton resBtn = QMessageBox::question(this, "Clear sounds", 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()
{
2019-10-08 00:31:44 +02:00
if (getActiveView())
{
QListWidgetItem *it = getActiveView()->item(getActiveView()->currentRow());
if (it)
{
playSound(it->toolTip().toStdString());
}
}
}
void MainWindow::on_addTabButton_clicked()
{
bool ok;
QString text = QInputDialog::getText(0, "Add a tab", "Tab Text:", QLineEdit::Normal, "", &ok);
if (ok && !text.isEmpty())
{
createTab(text);
saveSoundFiles();
2019-09-15 01:44:12 +02:00
}
}
2019-10-08 00:31:44 +02:00
void MainWindow::on_tabWidget_tabBarDoubleClicked(int index)
{
bool ok;
QString text = QInputDialog::getText(0, "Rename tab", "Tab Text:", QLineEdit::Normal, ui->tabWidget->tabText(index), &ok);
if (ok && !text.isEmpty())
2019-09-15 01:44:12 +02:00
{
2019-10-08 00:31:44 +02:00
ui->tabWidget->setTabText(index, text);
saveSoundFiles();
2019-09-15 01:44:12 +02:00
}
}
2019-10-08 00:31:44 +02:00
void MainWindow::on_tabWidget_tabCloseRequested(int index)
{
QMessageBox::StandardButton resBtn = QMessageBox::question(this, "Delete tab", tr("Are you sure?\n"), QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes);
if (resBtn == QMessageBox::Yes)
{
ui->tabWidget->removeTab(index);
saveSoundFiles();
}
}
QListWidget *MainWindow::createTab(QString title)
{
auto soundsListWidget = new QListWidget();
soundsListWidget->setObjectName(title);
connect(soundsListWidget, SIGNAL(itemDoubleClicked(QListWidgetItem *)), this, SLOT(on_soundsListWidget_itemDoubleClicked(QListWidgetItem *)));
ui->tabWidget->addTab(soundsListWidget, title);
return soundsListWidget;
}
void MainWindow::clearSoundFiles()
{
if (getActiveView())
{
while (getActiveView()->count() > 0)
{
getActiveView()->takeItem(0);
}
}
}
QListWidget *MainWindow::getActiveView()
{
return (QListWidget *)ui->tabWidget->widget(ui->tabWidget->currentIndex());
}
void MainWindow::saveSoundFiles()
{
json jsonTabs = json::array();
for (auto i = 0; i < ui->tabWidget->count(); i++)
{
auto title = ui->tabWidget->tabText(i).toStdString();
QListWidget *listWidget = (QListWidget *)ui->tabWidget->widget(i);
json tabJson;
json tabJsonSounds = json::array();
for (QListWidgetItem *item : listWidget->findItems("*", Qt::MatchWildcard))
{
json j;
j[item->text().toStdString()] = item->toolTip().toStdString();
tabJsonSounds.push_back(j);
}
tabJson[title] = tabJsonSounds;
jsonTabs.push_back(tabJson);
2019-09-15 01:44:12 +02:00
}
ofstream myfile;
myfile.open(soundFilesConfig);
2019-10-08 00:31:44 +02:00
myfile << jsonTabs.dump();
2019-09-15 01:44:12 +02:00
myfile.close();
}
2019-10-08 00:31:44 +02:00
void MainWindow::loadSoundFiles()
{
ifstream fileIn(soundFilesConfig);
if (fileIn.is_open())
{
2019-10-09 22:14:11 +02:00
clearSoundFiles();
2019-09-15 01:44:12 +02:00
2019-10-09 22:14:11 +02:00
string content((istreambuf_iterator<char>(fileIn)), istreambuf_iterator<char>());
2019-09-15 01:44:12 +02:00
json j = json::parse(content);
2019-10-08 00:31:44 +02:00
for (auto item : j.get<vector<json>>())
{
for (auto object : item.items())
{
auto tabName = object.key().c_str();
auto soundsListWidget = createTab(tabName);
auto childItems = object.value().get<vector<json>>();
for (auto _child : childItems)
{
for (auto child : _child.items())
{
auto soundName = child.key();
auto soundPath = child.value();
remove(soundPath.begin(), soundPath.end(), '"');
auto item = new QListWidgetItem();
item->setText(QString::fromStdString(soundName));
item->setToolTip(QString::fromStdString(soundPath));
soundsListWidget->addItem(item);
}
}
2019-09-15 01:44:12 +02:00
}
}
fileIn.close();
}
2019-10-08 00:31:44 +02:00
}