Refactor PW and SB logic
This commit is contained in:
235
soundboard.cpp
Normal file
235
soundboard.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
#include "soundboard.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDirIterator>
|
||||
#include <QFileDialog>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const QString& string) {
|
||||
os << string.toStdString();
|
||||
return os;
|
||||
}
|
||||
|
||||
Soundboard::Soundboard(QWidget* parent/* = nullptr */) :
|
||||
QWidget(parent)
|
||||
{
|
||||
setWindowTitle("Soundboard");
|
||||
resize(500, 700);
|
||||
|
||||
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
||||
|
||||
// Top
|
||||
QHBoxLayout* top_layout = new QHBoxLayout();
|
||||
|
||||
m_combo_box = new QComboBox();
|
||||
m_combo_box->setToolTip("Select the destination application for the audio stream");
|
||||
top_layout->addWidget(m_combo_box, 1);
|
||||
connect(m_combo_box, &QComboBox::currentIndexChanged, this, &Soundboard::on_app_selected);
|
||||
|
||||
m_refresh_button = new QPushButton();
|
||||
m_refresh_button->setToolTip("Refresh the application list");
|
||||
m_refresh_button->setIcon(style()->standardIcon(QStyle::SP_BrowserReload));
|
||||
top_layout->addWidget(m_refresh_button);
|
||||
connect(m_refresh_button, &QPushButton::pressed, this, &Soundboard::refresh_apps);
|
||||
|
||||
m_folder_button = new QPushButton();
|
||||
m_folder_button->setToolTip("Select a folder containing audio files");
|
||||
m_folder_button->setIcon(style()->standardIcon(QStyle::SP_DirIcon));
|
||||
top_layout->addWidget(m_folder_button);
|
||||
connect(m_folder_button, &QPushButton::pressed, this, &Soundboard::load_directory);
|
||||
|
||||
m_label = new QLabel("Select a folder");
|
||||
top_layout->addWidget(m_label);
|
||||
|
||||
main_layout->addLayout(top_layout);
|
||||
|
||||
m_volume_slider = new QSlider(Qt::Horizontal);
|
||||
m_volume_slider->setToolTip("Adjust the output volume");
|
||||
m_volume_slider->setRange(0, 80);
|
||||
m_volume_slider->setValue(80);
|
||||
m_volume_slider->setSingleStep(5);
|
||||
main_layout->addWidget(m_volume_slider);
|
||||
connect(m_volume_slider, &QSlider::valueChanged, this, &Soundboard::set_volume);
|
||||
|
||||
// Middle
|
||||
m_sound_list = new QListWidget();
|
||||
m_sound_list->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
main_layout->addWidget(m_sound_list);
|
||||
connect(m_sound_list, &QListWidget::itemSelectionChanged, this, &Soundboard::play_sound);
|
||||
|
||||
// Bottom
|
||||
QHBoxLayout* bottom_layout = new QHBoxLayout();
|
||||
|
||||
m_stop_button = new QPushButton("Stop");
|
||||
m_stop_button->setToolTip("Stop all audio streams");
|
||||
bottom_layout->addWidget(m_stop_button);
|
||||
connect(m_stop_button, &QPushButton::pressed, this, &Soundboard::stop_sounds);
|
||||
|
||||
m_monitor_toggle = new QCheckBox("Monitor");
|
||||
m_monitor_toggle->setToolTip("Toggle audio monitoring on your system sink");
|
||||
bottom_layout->addWidget(m_monitor_toggle);
|
||||
connect(m_monitor_toggle, &QCheckBox::checkStateChanged, this, &Soundboard::toggle_monitor);
|
||||
|
||||
main_layout->addLayout(bottom_layout);
|
||||
|
||||
// Setup Pipewire Components
|
||||
m_graph.refresh();
|
||||
m_sink = Pipewire::Sink::create("soundboard", m_graph);
|
||||
if (!m_sink) {
|
||||
std::cerr << "[ERROR] Failed to initialize virtual sink for soundboard" << std::endl;
|
||||
}
|
||||
|
||||
refresh_apps();
|
||||
}
|
||||
|
||||
Soundboard::~Soundboard()
|
||||
{
|
||||
stop_sounds();
|
||||
}
|
||||
|
||||
void Soundboard::on_app_selected(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_apps.size() || !m_sink) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_graph.refresh();
|
||||
|
||||
int target_app_id = m_apps[index].id;
|
||||
int sink_id = m_sink->get_id();
|
||||
|
||||
// Destroy old output links from our sink
|
||||
auto existing_links = m_graph.get_links_from_node(sink_id);
|
||||
for (const auto& link : existing_links) {
|
||||
Pipewire::destroy_link(link.id);
|
||||
}
|
||||
|
||||
auto sink_out_ports = m_graph.get_ports_for_node(sink_id, "out");
|
||||
auto target_in_ports = m_graph.get_ports_for_node(target_app_id, "in");
|
||||
|
||||
// Link sink to target app
|
||||
// TODO(1): create a wrapper function to link all channels
|
||||
for (const auto& out_port : sink_out_ports) {
|
||||
for (const auto& in_port : target_in_ports) {
|
||||
if (out_port.channel == in_port.channel &&
|
||||
!Pipewire::link_ports(out_port.id, in_port.id)) {
|
||||
std::cerr << "[ERROR] Failed to link sink with target node" << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Link to monitor only if checked
|
||||
if (m_monitor_toggle->isChecked()) {
|
||||
auto sinks = m_graph.get_all_sinks();
|
||||
for (const auto& system_sink : sinks) {
|
||||
// Skip our own virtual sink
|
||||
if (system_sink.id == sink_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto output_ids = m_graph.get_ports_for_node(system_sink.id, "in");
|
||||
// TODO(1): create a wrapper function to link all channels
|
||||
for (const auto& out_port : sink_out_ports) {
|
||||
for (const auto& in_port : output_ids) {
|
||||
if (out_port.channel == in_port.channel) {
|
||||
Pipewire::link_ports(out_port.id, in_port.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[INFO] Updated routing" << std::endl;
|
||||
}
|
||||
|
||||
void Soundboard::refresh_apps()
|
||||
{
|
||||
m_graph.refresh();
|
||||
m_apps = m_graph.get_input_apps();
|
||||
|
||||
m_combo_box->clear();
|
||||
for (const auto& app : m_apps) {
|
||||
m_combo_box->addItem(app.get_display_name());
|
||||
}
|
||||
|
||||
std::cout << "[INFO] Updated app list" << std::endl;
|
||||
}
|
||||
|
||||
void Soundboard::load_directory()
|
||||
{
|
||||
m_audio_directory = QFileDialog::getExistingDirectory(
|
||||
nullptr,
|
||||
"Select audio directory",
|
||||
QDir::homePath(),
|
||||
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks
|
||||
);
|
||||
|
||||
if (m_audio_directory.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
m_label->setText(m_audio_directory);
|
||||
|
||||
m_sound_list->clear();
|
||||
|
||||
QDir dir(m_audio_directory);
|
||||
dir.setSorting(QDir::SortFlag::Name);
|
||||
dir.setNameFilters({ "*.mp3", "*.wav" });
|
||||
dir.setFilter(QDir::Files | QDir::NoDotAndDotDot);
|
||||
QDirIterator it(dir);
|
||||
while (it.hasNext()) {
|
||||
QString full_path = it.next();
|
||||
QString basename = QString::fromStdString(
|
||||
std::filesystem::path(full_path.toStdString()).filename().string()
|
||||
);
|
||||
m_sound_list->addItem(basename);
|
||||
}
|
||||
|
||||
std::cout << "[INFO] Selected audio directory: " << m_audio_directory << std::endl;
|
||||
}
|
||||
|
||||
void Soundboard::play_sound()
|
||||
{
|
||||
QListWidgetItem* item = m_sound_list->currentItem();
|
||||
if (item == nullptr || !m_sink) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString sound = item->text();
|
||||
QString full_path = QDir(m_audio_directory).filePath(sound);
|
||||
|
||||
m_player.play_file(full_path, m_sink->get_name());
|
||||
std::cout << "[INFO] Playing: " << sound << std::endl;
|
||||
}
|
||||
|
||||
void Soundboard::stop_sounds()
|
||||
{
|
||||
m_player.stop_all();
|
||||
std::cout << "[INFO] Stopped all playing sounds" << std::endl;
|
||||
}
|
||||
|
||||
void Soundboard::set_volume(int value)
|
||||
{
|
||||
if (!m_sink) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pipewire::set_volume(m_sink->get_name(), value);
|
||||
std::cout << "[INFO] Set volume to " << value << '%' << std::endl;
|
||||
}
|
||||
|
||||
void Soundboard::toggle_monitor(Qt::CheckState state)
|
||||
{
|
||||
(void) state;
|
||||
on_app_selected(m_combo_box->currentIndex());
|
||||
}
|
||||
Reference in New Issue
Block a user