#include "soundboard.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include 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()); }